Meh Belly Lint Collection

That awful moment when you realize,
THIS is YOUR circus and THOSE are YOUR monkeys.

User Tools

Site Tools


server_status_oled_display

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
server_status_oled_display [2025/03/21 22:19] kensonserver_status_oled_display [2026/05/06 04:56] (current) kenson
Line 2: Line 2:
 ===== Displayinator v1.0 ===== ===== Displayinator v1.0 =====
  
-This is a device you can shove into a spare USB connector on say a server rack system to display info like IP/Hostname/etc.+This is a device you can shove into a spare USB connector on say a server rack system to display info like IP/Hostname/etc. It has an accelerometer so it can determine orientation (for racks which have sideways USB) and a simple font compression selection so it can dynamically fit information on one line. 
 {{ :displayinator.jpg?direct&400 |}} {{ :displayinator.jpg?direct&400 |}}
  
-This is a old school project using a ATMEGA32U4.+This is a old school project using a ATMEGA32U4. Hindsight being 20/20 I should have chosen a more modern microcontroller as the 2.5K of RAM was a significant limitation requiring excessive use of PROGMEM in places you ordinarily wouldn't.
  
 +Notes:
   * The board is a clone of an Arduino Micro, and I2C is connnected to the OLED (SSD1315) and the Accelerometer (MMA8451).   * The board is a clone of an Arduino Micro, and I2C is connnected to the OLED (SSD1315) and the Accelerometer (MMA8451).
   * The OLED's I2C address 0x3C, and I'm using adafruit's 1306 driver, mainly for display rotation support.   * The OLED's I2C address 0x3C, and I'm using adafruit's 1306 driver, mainly for display rotation support.
   * The Accelerometer's address is 0x1C and the adafruit MMA8541 driver is basic but functional. Note that the more advanced features (e.g. tap to click) built into the chip are unavailable with this library.   * The Accelerometer's address is 0x1C and the adafruit MMA8541 driver is basic but functional. Note that the more advanced features (e.g. tap to click) built into the chip are unavailable with this library.
 +
 +
 +====== TLDR; ======
 +
 +
 +Probe the port.
 +
 +<code>
 +sudo tee ./probe_displayinator.py >/dev/null <<'EOF'
 +#!/usr/bin/env bash
 +set -u
 +
 +BAUD=115200
 +
 +for PORT in /dev/ttyUSB* /dev/ttyACM*; do
 +    [ -e "$PORT" ] || continue
 +
 +    echo "========================================"
 +    echo "Probing $PORT"
 +
 +    if ! stty -F "$PORT" "$BAUD" raw -echo -echoe -echok -echoctl -echoke 2>/dev/null; then
 +        echo "Could not configure $PORT"
 +        continue
 +    fi
 +
 +    # Some Arduino/32U4-style boards reset when serial opens
 +    sleep 4
 +
 +    # Wake / clear any stale output
 +    printf '\n' > "$PORT"
 +    sleep 0.2
 +    timeout 1 cat "$PORT" >/dev/null 2>&1
 +
 +    # Ask version
 +    printf '!VER\n' > "$PORT"
 +
 +    echo "Response from $PORT:"
 +    timeout 2 cat "$PORT" || true
 +    echo
 +done
 +EOF
 +chmod +x probe_displayinator.py
 +./probe_displayinator.py
 +</code>
 +
 +Download the agent: {{ :displayinatorhostip.zip |}}
 +<code>
 +wget 'https://mehngineering.com/wiki/lib/exe/fetch.php?media=displayinatorhostip.zip' -O displayinatorhostip.zip
 +unzip displayinatorhostip.zip
 +</code>
 +
 +Insert the service to autostart
 +<code>
 +sudo tee /etc/systemd/system/displayinator.service >/dev/null <<'EOF'
 +[Unit]
 +Description=Displayinator Host IP Sender
 +Wants=network-online.target
 +After=network-online.target multi-user.target
 +
 +[Service]
 +Type=oneshot
 +WorkingDirectory=/root
 +ExecStartPre=/bin/sleep 10
 +ExecStart=/usr/bin/python3 /root/DisplayinatorHostIP.py --port /dev/ttyACM0
 +StandardOutput=append:/var/log/displayinator-hostip.log
 +StandardError=append:/var/log/displayinator-hostip.log
 +
 +[Install]
 +WantedBy=multi-user.target
 +EOF
 +</code>
 +
 +Edit /etc/systemd/system/displayinator.service and change the port to whatever device the probe finds, usually /dev/ttyACM0 or /dev/ttyUSB0
 +<code>ExecStart=/usr/bin/python3 /root/DisplayinatorHostIP.py --port /dev/ttyACM0
 +</code>
 +
 +Start the service.
 +
 +<code>
 +systemctl daemon-reload
 +systemctl enable --now displayinator.service
 +systemctl status displayinator.service
 +</code>
  
 ==== Loading the Arduino Bootloader ==== ==== Loading the Arduino Bootloader ====
Line 18: Line 103:
  
 avrdude will complain about older firmware but my recommendation is to ignore it. avrdude will complain about older firmware but my recommendation is to ignore it.
-The TLDR; is the chinese firmware is modified and does some automagic stuff and in general works more reliably than the publicly available open source code version.+The [[USBasp flash|TLDR;]] is the chinese firmware is modified and does some automagic stuff and in general works more reliably than the publicly available open source code version.
  
 Use zadig to make it (USBasp) use libusb-win32 Use zadig to make it (USBasp) use libusb-win32
  
-Copy avrdude to the arduino avrdude location. (You can "Show verbose output during [X] compile [X] upload" in File->Preferences to try to upload to see where it is)+Copy avrdude v8 (The one that ships with arduino is v5 and doesn't work.) to the arduino avrdude location. (You can "Show verbose output during [X] compile [X] upload" in File->Preferences to try to upload to see where it is) 
  
 Then from Arduino, select USBasp in Tools->Programmer and burn using Tools->Burn Bootloader Then from Arduino, select USBasp in Tools->Programmer and burn using Tools->Burn Bootloader
Line 42: Line 127:
 I uses the accelerometer to detect orientation so it should be right side up. I uses the accelerometer to detect orientation so it should be right side up.
  
-The USB serial is nominally 115200, and the commands it takes are+The USB serial is nominally 115200
 + 
 +==== Command structure ==== 
 + 
 +The command protocol is simple:  
 +  * Commands are prepended with an '!' and terminated with a newline '\n'
 +  * Key-Value pairs are prepended with a '@'space ' ' delimited and terminated with a newline '\n' 
 +  * You can also indicate not to store values in EEPROM by putting a caret '^' after the @ symbol 
 + 
 +==== Commands ====
  
 !LIST which displays the stored key-value pairs !LIST which displays the stored key-value pairs
Line 75: Line 169:
 </code> </code>
  
-Everything else is checked to see if it is a Key-Value pair. pair is just two, space-delimited, alphanumeric wordsE.g.+ 
 +==== KEY VALUE STORAGE ==== 
 + 
 +If the line starts with a '@', it will indicate that we're passing in a Key Value pair. The Key-Value pair is space delimited.  
 + 
 +Note that this '@' symbol is not passed in 
 <code> <code>
-HAPPY GILMORE+@HAPPY GILMORE
 Space index found at: 5 Space index found at: 5
 Key extracted: 'HAPPY', Value extracted: 'GILMORE' Key extracted: 'HAPPY', Value extracted: 'GILMORE'
 Added HAPPY = GILMORE Added HAPPY = GILMORE
 +</code>
 +
 +Note only the first space is used as a delimiter, successive spaces are considered part of the Value.
 +
 +<code>
 +@KEY Value cow
 +KEY Value cow, Space @: 3
 +Key extracted: 'KEY', Value extracted: 'Value cow'
 +Added KEY = Value cow
 +</code>
 +
 +KV pairs are normally stored in flash and will be loaded on power on. Sometimes this behavior is undesired, for example rapidly changing information like CPU Load. In this case, a '^' is used to specify skipping permanent storage. Note that the caret is stored in the Key but not displayed.
 +
 +<code>
 +@^CPU 48%
 +^CPU 48%, Space @: 4
 +Key extracted: '^CPU', Value extracted: '48%'
 +Updated ^CPU = 48%
 </code> </code>
  
Line 90: Line 208:
   * It also likes to reset when you connect via serial so I wait 3 to 5 seconds for it to boot up before poking it.    * It also likes to reset when you connect via serial so I wait 3 to 5 seconds for it to boot up before poking it. 
   * Python raises DTR, and for whatever reason it breaks things, so I set ser.dtr = False.   * Python raises DTR, and for whatever reason it breaks things, so I set ser.dtr = False.
 +  * You never know whats in a serial buffer when you connect so send a '\n' and read the buffer to make sure everything is sane when you connect.
  
 ==== TrueNAS ===== ==== TrueNAS =====
Line 106: Line 225:
 #include <Adafruit_GFX.h> #include <Adafruit_GFX.h>
 #include <Adafruit_SSD1306.h> #include <Adafruit_SSD1306.h>
-#include <Fonts/TomThumb.h> // TomThumb +#include <Fonts/TomThumb.h>
-//#include <Fonts/Org_01.h> // Org_01 +
-//#include <Fonts/Picopixel.h> // Picopixel+
 #include <EEPROM.h> #include <EEPROM.h>
  
Line 115: Line 232:
   MySSD1306(int16_t w, int16_t h, TwoWire *twi, int8_t rst_pin)   MySSD1306(int16_t w, int16_t h, TwoWire *twi, int8_t rst_pin)
     : Adafruit_SSD1306(w, h, twi, rst_pin) {}     : Adafruit_SSD1306(w, h, twi, rst_pin) {}
-  +
   int16_t getCursorX() { return cursor_x; }   int16_t getCursorX() { return cursor_x; }
   int16_t getCursorY() { return cursor_y; }   int16_t getCursorY() { return cursor_y; }
Line 121: Line 238:
  
 // ----- EEPROM Layout Settings ----- // ----- EEPROM Layout Settings -----
-#define MAX_KEY_LENGTH 16       // Maximum characters for a key +#define MAX_KEY_LENGTH 32       // Maximum characters for a key 
-#define MAX_VALUE_LENGTH 16     // Maximum characters for a value+#define MAX_VALUE_LENGTH 32     // Maximum characters for a value
 #define PAIR_SIZE (MAX_KEY_LENGTH + MAX_VALUE_LENGTH) #define PAIR_SIZE (MAX_KEY_LENGTH + MAX_VALUE_LENGTH)
 #define EEPROM_KEY_COUNT_OFFSET 0 #define EEPROM_KEY_COUNT_OFFSET 0
Line 128: Line 245:
 // ----------------------------------- // -----------------------------------
  
-// Use a renamed enum to avoid potential conflicts +// Renamed enum to avoid potential conflicts 
-enum Fuckme {+enum umopapisdn {
   UNKNOWN,   UNKNOWN,
   FACE_UP,   FACE_UP,
Line 140: Line 257:
  
 // CLI Code // CLI Code
-#define MAX_KEYS 10  // Maximum number of key-value pairs+#define MAX_KEYS  // Maximum number of key-value pairs
 struct KeyValue { struct KeyValue {
-  String key; +  char key[MAX_KEY_LENGTH+1]
-  String value;+  char value[MAX_VALUE_LENGTH+1];
 }; };
 KeyValue keyValueStore[MAX_KEYS];  // Storage for key-value pairs KeyValue keyValueStore[MAX_KEYS];  // Storage for key-value pairs
 int keyCount = 0;                  // Current number of keys stored int keyCount = 0;                  // Current number of keys stored
 int orient = 0;                    // Stores Display Orientation (0-3) int orient = 0;                    // Stores Display Orientation (0-3)
-String inputString ""          // Buffer for incoming serial data + 
-bool stringComplete = false;       // Indicates if a full line has been received+#define INPUT_BUFFER_SIZE 67 
 +char inputBuffer[INPUT_BUFFER_SIZE]; 
 +int inputIndex 0
 +bool commandReady = false;
 int orientChanged = 0; int orientChanged = 0;
  
Line 160: Line 280:
 int landscape = -1; int landscape = -1;
 const float THRESHOLD = 7.0;         // Minimum m/s² value to consider an axis "dominant" const float THRESHOLD = 7.0;         // Minimum m/s² value to consider an axis "dominant"
-const unsigned long DEBOUNCE_DELAY = 250;  // Time in milliseconds to wait before confirming a change +const unsigned long DEBOUNCE_DELAY = 250;  // Debounce delay in milliseconds 
-Fuckme lastStableOrientation = UNKNOWN;+umopapisdn lastStableOrientation = UNKNOWN;
 unsigned long lastChangeTime = 0; unsigned long lastChangeTime = 0;
  
 #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_WIDTH 128 // OLED display width, in pixels
 #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels
-#define SCREEN_ADDRESS 0x3C // I2C Addr, Commonly 0x3D or 0x3C+#define SCREEN_ADDRESS 0x3C // I2C Addr (commonly 0x3C or 0x3D)
 #define OLED_RESET     -1  #define OLED_RESET     -1 
 MySSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); MySSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Line 177: Line 297:
 void saveKeyValuesToEEPROM() { void saveKeyValuesToEEPROM() {
   int storedKeyCount = 0;   int storedKeyCount = 0;
- +  // Count only keys that should be stored (skip keys starting with '^')
-  // First pass: count only the keys that should be stored+
   for (int i = 0; i < keyCount; i++) {   for (int i = 0; i < keyCount; i++) {
-    if (keyValueStore[i].key.startsWith("^")) { +    if (keyValueStore[i].key[0] == '^') continue;
-      continue; +
-    }+
     storedKeyCount++;     storedKeyCount++;
   }   }
Line 188: Line 305:
  
   int storedIndex = 0;   int storedIndex = 0;
-  // Second pass: store only non-filtered key-value pairs.+  // Store only non-filtered key-value pairs.
   for (int i = 0; i < keyCount; i++) {   for (int i = 0; i < keyCount; i++) {
-    if (keyValueStore[i].key.startsWith("^")) { +    if (keyValueStore[i].key[0] == '^') continue;
-      continue; +
-    } +
     int base = EEPROM_KEY_VALUE_OFFSET + storedIndex * PAIR_SIZE;     int base = EEPROM_KEY_VALUE_OFFSET + storedIndex * PAIR_SIZE;
- 
     // Save key as fixed-length     // Save key as fixed-length
     for (int j = 0; j < MAX_KEY_LENGTH; j++) {     for (int j = 0; j < MAX_KEY_LENGTH; j++) {
-      char c = (j < keyValueStore[i].key.length()) ? keyValueStore[i].key.charAt(j: '\0';+      char c = (j < (int)strlen(keyValueStore[i].key)) ? keyValueStore[i].key[j: '\0';
       EEPROM.update(base + j, c);       EEPROM.update(base + j, c);
     }     }
- 
     // Save value as fixed-length     // Save value as fixed-length
     for (int j = 0; j < MAX_VALUE_LENGTH; j++) {     for (int j = 0; j < MAX_VALUE_LENGTH; j++) {
-      char c = (j < keyValueStore[i].value.length()) ? keyValueStore[i].value.charAt(j: '\0';+      char c = (j < (int)strlen(keyValueStore[i].value)) ? keyValueStore[i].value[j: '\0';
       EEPROM.update(base + MAX_KEY_LENGTH + j, c);       EEPROM.update(base + MAX_KEY_LENGTH + j, c);
     }     }
- 
     storedIndex++;     storedIndex++;
   }   }
 } }
- 
  
 void loadKeyValuesFromEEPROM() { void loadKeyValuesFromEEPROM() {
   keyCount = EEPROM.read(EEPROM_KEY_COUNT_OFFSET);   keyCount = EEPROM.read(EEPROM_KEY_COUNT_OFFSET);
   // Validate keyCount (if invalid, reset to 0)   // Validate keyCount (if invalid, reset to 0)
-  if (keyCount < 0 || keyCount > MAX_KEYS) +  if (keyCount < 0 || keyCount > MAX_KEYS) keyCount = 0;
-    keyCount = 0; +
-  }+
   for (int i = 0; i < keyCount; i++) {   for (int i = 0; i < keyCount; i++) {
     int base = EEPROM_KEY_VALUE_OFFSET + i * PAIR_SIZE;     int base = EEPROM_KEY_VALUE_OFFSET + i * PAIR_SIZE;
Line 231: Line 339:
     }     }
     valueBuffer[MAX_VALUE_LENGTH] = '\0';     valueBuffer[MAX_VALUE_LENGTH] = '\0';
-    keyValueStore[i].key = String(keyBuffer); +    strncpy(keyValueStore[i].keykeyBuffer, MAX_KEY_LENGTH); 
-    keyValueStore[i].value String(valueBuffer);+    keyValueStore[i].key[MAX_KEY_LENGTH] '\0'; 
 +    strncpy(keyValueStore[i].value, valueBuffer, MAX_VALUE_LENGTH)
 +    keyValueStore[i].value[MAX_VALUE_LENGTH] = '\0';
   }   }
 } }
Line 238: Line 348:
  
 const void* activeFont = NULL; const void* activeFont = NULL;
- +// customPrintln() prints text and repositions the cursor when the font changes. 
-// customPrintln() prints text and only repositions if the font changes. +// If the string length exceeds 'threshold', the TomThumb font is used
-// If the string length exceeds 'threshold', we use the TomThumb font; otherwise, the default+void customPrintln(const char *str, size_t threshold) { 
-void customPrintln(const String &str, size_t threshold) { +  const void* desiredFont = (strlen(str) > threshold) ? (void*)&TomThumb : NULL;
-  // Determine which font we want based on the string length. +
-  const void* desiredFont = (str.length() > threshold) ? (void*)&TomThumb : NULL; +
- +
-  //Serial.print("Y:"); +
-  //Serial.println(display.getCursorY()); +
   int currY = display.getCursorY();   int currY = display.getCursorY();
-  if ((currY == 0) && (!landscape)) { // weird bug +  if ((currY == 0) && (!landscape)) { 
-    display.setCursor(0,7);+    display.setCursor(0, 7);
   }   }
-   
-  // Only change font (and reposition) if it differs from the active font. 
   if (activeFont != desiredFont) {   if (activeFont != desiredFont) {
     display.setFont(desiredFont);     display.setFont(desiredFont);
     activeFont = desiredFont;     activeFont = desiredFont;
-     +    // Reposition cursor when switching fonts.
-    // Reposition cursor when the font changes cause reasons. +
-    // For example, when switching to TomThumb, move up by 1 pixel; +
-    // when switching back to default, move down by 1 pixel.+
     if (desiredFont == (void*)&TomThumb) {     if (desiredFont == (void*)&TomThumb) {
-      //Serial.println(display.getCursorY()); 
       display.setCursor(0, display.getCursorY() - 1);       display.setCursor(0, display.getCursorY() - 1);
     } else {     } else {
Line 268: Line 366:
     }     }
   }   }
-   
-  // Print the string (this also moves the cursor downward). 
   display.println(str);   display.println(str);
 } }
- 
  
 //-------------------------------------------------- SETUP ------------------------------------------- //-------------------------------------------------- SETUP -------------------------------------------
 void setup() { void setup() {
   Serial.begin(115200);   Serial.begin(115200);
-  //while (!Serial) delay(10);     // will pause until serial console opens 
- 
-  //Serial.println("Displayinator Boot"); 
-   
-  // Load key-value pairs from EEPROM 
   loadKeyValuesFromEEPROM();   loadKeyValuesFromEEPROM();
      
   if (!mma.begin(ACCEL_ADDRESS)) {   if (!mma.begin(ACCEL_ADDRESS)) {
-    Serial.println("MMA8451 FAIL!"); +    Serial.println(F("MMA8451 FAIL!")); 
-    for(;;); // Don't proceed, loop forever +    for(;;); 
-  }   +  } 
-  Serial.println("MMA8451 OK."); +  Serial.println(F("MMA8451 OK.")); 
 +  
   if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {   if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
     Serial.println(F("SSD1306 FAIL"));     Serial.println(F("SSD1306 FAIL"));
-    for(;;); // Don't proceed, loop forever+    for(;;);
   }   }
-  Serial.println("SSD1306 OK."); +  Serial.println(F("SSD1306 OK.")); 
- +  
   display.clearDisplay();   display.clearDisplay();
   display.display();   display.display();
 +  
   mma.getEvent(&event);   mma.getEvent(&event);
-  float ax = event.acceleration.x; // acceleration is measured in m/s^2+  float ax = event.acceleration.x;
   float ay = event.acceleration.y;   float ay = event.acceleration.y;
   float az = event.acceleration.z;   float az = event.acceleration.z;
   updateOrientation(ax, ay, az);   updateOrientation(ax, ay, az);
- +   
-  //display.setFont(&Picopixel); +  display.setTextSize(1); 
-  display.setTextSize(1);      // Normal 1:1 pixel scale +  display.setTextColor(SSD1306_WHITE); 
-  display.setTextColor(SSD1306_WHITE); // Draw white text +  customPrintln("Displayinator", 10); 
-  customPrintln(F("Displayinator"),10); +  
- +
-/*  customPrintln(F("Displayinator"),10); +
-  customPrintln(F("123456789012345678901234567890"),10); +
-  customPrintln(F("123.456.789.012"),10); +
-  customPrintln(F("COW"),10); +
-*/+
   display.display();   display.display();
-  inputString.reserve(20);     // Reserve memory for the input string +   
- +  // Initialize input buffer 
 +  inputIndex = 0; 
 +  inputBuffer[0] = '\0';
      
   mma.setRange(MMA8451_RANGE_2_G);   mma.setRange(MMA8451_RANGE_2_G);
-   
-  //Serial.print("ACCRANGE = ");  Serial.print(2 << mma.getRange());  Serial.println("G"); 
 } }
  
 //-------------------------------------------------- LOOP -------------------------------------------- //-------------------------------------------------- LOOP --------------------------------------------
 void loop() { void loop() {
-  /* Get a new sensor event */  
- 
   mma.getEvent(&event);   mma.getEvent(&event);
-  float ax = event.acceleration.x; // acceleration is measured in m/s^2+  float ax = event.acceleration.x;
   float ay = event.acceleration.y;   float ay = event.acceleration.y;
   float az = event.acceleration.z;   float az = event.acceleration.z;
   updateOrientation(ax, ay, az);   updateOrientation(ax, ay, az);
 +  
   updateDisplay();   updateDisplay();
- +   
-  while (Serial.available()) { // Nonblocking serial input: read available characters one at a time+  while (Serial.available()) {
     char inChar = (char)Serial.read();     char inChar = (char)Serial.read();
-    //Serial.print(inChar); +    if (inChar == '\n') { 
-    if (inChar == '\n') {  // Newline signals end of command +      inputBuffer[inputIndex= '\0'
-      //Serial.print("[LF]")+      commandReady = true; 
-      stringComplete = true; +      break; 
-      break; // Process this command after breaking out +    } else if (inChar != '\r') { 
-    } else if (inChar != '\r') { // Ignore carriage return +      if (inputIndex < INPUT_BUFFER_SIZE - 1) { 
-      inputString += inChar; +        inputBuffer[inputIndex++] = inChar; 
-    else { +        inputBuffer[inputIndex] = '\0'; 
-      //Serial.print("[CR]");+      
 +      // If the input buffer is full, force processing. 
 +      if (inputIndex >= INPUT_BUFFER_SIZE - 1
 +        commandReady = true; 
 +        break; 
 +      }
     }     }
   }   }
      
-  // If a complete command was received, process it +  if (commandReady) { 
-  if (stringComplete) { +    // Trim leading and trailing whitespace in the full input line. 
-    inputString.trim(); +    int start = 0; 
-    if (inputString.length() > 0) { +    while (inputBuffer[start] == ' ' || inputBuffer[start] == '\t'start++
-      //Serial.println(inputString)+    int end = strlen(inputBuffer- 1; 
-      processCommand(inputString);+    while (end start && (inputBuffer[end] == ' ' || inputBuffer[end] == '\t')) { 
 +      inputBuffer[end] = '\0'
 +      end--;
     }     }
-    // Reset buffer for the next command +    if (strlen(inputBuffer + start) > 0) { 
-    inputString ""+      processCommand(inputBuffer + start); 
-    stringComplete = false;+    } 
 +    inputIndex = 0; 
 +    inputBuffer[0] '\0'
 +    commandReady = false;
   }   }
   delay(250);   delay(250);
Line 367: Line 457:
  
 //-------------------------------------------------- DISPLAY --------------------------------------- //-------------------------------------------------- DISPLAY ---------------------------------------
-unsigned long lastDisplayUpdate = 0;         // Tracks the last display update time +unsigned long lastDisplayUpdate = 0; 
-const unsigned long DISPLAY_UPDATE_INTERVAL = 1000; // Update interval in milliseconds+const unsigned long DISPLAY_UPDATE_INTERVAL = 1000;
  
 void updateDisplay() { void updateDisplay() {
   unsigned long now = millis();   unsigned long now = millis();
-  int sWidth = 10; +  int sWidth = landscape 21 10; 
-  if (landscape) { +  
-    sWidth = 21+
-  } else { +
-    sWidth = 10; +
-  +
   if (now - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL) {   if (now - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL) {
     lastDisplayUpdate = now;     lastDisplayUpdate = now;
-     
     display.clearDisplay();     display.clearDisplay();
     display.setCursor(0, 0);     display.setCursor(0, 0);
          
-    // Display key-value pairs 
     if (keyCount == 0) {     if (keyCount == 0) {
-      customPrintln(F("No KV data"), sWidth);+      customPrintln("No KV data", sWidth);
     } else {     } else {
       for (int i = 0; i < keyCount; i++) {       for (int i = 0; i < keyCount; i++) {
-        String displayKey = keyValueStore[i].key+        char displayKey[MAX_KEY_LENGTH+1]; 
-        if (displayKey.startsWith("^")) { +        if (keyValueStore[i].key[0] == '^') { 
-          displayKey = displayKey.substring(1); // Remove leading caret+          strncpy(displayKey, keyValueStore[i].key + 1, MAX_KEY_LENGTH); 
 +          displayKey[MAX_KEY_LENGTH] '\0'; 
 +        } else { 
 +          strncpy(displayKey, keyValueStore[i].key, MAX_KEY_LENGTH); 
 +          displayKey[MAX_KEY_LENGTH] = '\0';
         }         }
- +        char result[2 * MAX_KEY_LENGTH + MAX_VALUE_LENGTH + 3]; 
-        if ((displayKey.length() + keyValueStore[i].value.length() + 1) <= sWidth) { +        if ((strlen(displayKey) + strlen(keyValueStore[i].value) + 1) <= (unsigned)sWidth) { 
-          String result = displayKey + ":keyValueStore[i].value;+          snprintf(result, sizeof(result), "%s %s", displayKey, keyValueStore[i].value);
           customPrintln(result, sWidth);           customPrintln(result, sWidth);
         } else {         } else {
Line 404: Line 491:
       }       }
     }     }
-     
     display.display();     display.display();
   }   }
Line 410: Line 496:
  
 //-------------------------------------------------- ORIENTATION --------------------------------------- //-------------------------------------------------- ORIENTATION ---------------------------------------
-Fuckme detectOrientation(float ax, float ay, float az) {+umopapisdn detectOrientation(float ax, float ay, float az) {
   if (az > THRESHOLD) return FACE_UP;   if (az > THRESHOLD) return FACE_UP;
   if (az < -THRESHOLD) return FACE_DOWN;   if (az < -THRESHOLD) return FACE_DOWN;
Line 421: Line 507:
  
 void updateOrientation(float ax, float ay, float az) { void updateOrientation(float ax, float ay, float az) {
-  Fuckme current = detectOrientation(ax, ay, az);+  umopapisdn current = detectOrientation(ax, ay, az);
   unsigned long now = millis();   unsigned long now = millis();
 +  
   if (current != lastStableOrientation) {   if (current != lastStableOrientation) {
     if (now - lastChangeTime >= DEBOUNCE_DELAY) {     if (now - lastChangeTime >= DEBOUNCE_DELAY) {
       lastStableOrientation = current;       lastStableOrientation = current;
       orientChanged = 1;       orientChanged = 1;
-      //Serial.print("REORIENT: "); 
       switch (current) {       switch (current) {
-        case FACE_UP:          +        case FACE_UP:
-          //Serial.println("FACE_UP"); +
           display.setRotation(0);           display.setRotation(0);
           landscape = 1;           landscape = 1;
           break;           break;
-        case FACE_DOWN:        +        case FACE_DOWN:
-          //Serial.println("FACE_DOWN"); +
           display.setRotation(0);           display.setRotation(0);
           landscape = 1;           landscape = 1;
           break;           break;
-        case LANDSCAPE_RIGHT:  +        case LANDSCAPE_RIGHT:
-          //Serial.println("LANDSCAPE_RIGHT"); +
           display.setRotation(0);           display.setRotation(0);
           landscape = 1;           landscape = 1;
           break;           break;
-        case LANDSCAPE_LEFT:   +        case LANDSCAPE_LEFT:
-          //Serial.println("LANDSCAPE_LEFT"); +
           display.setRotation(2);           display.setRotation(2);
           landscape = 1;           landscape = 1;
           break;           break;
-        case PORTRAIT_UP:      +        case PORTRAIT_UP:
-          //Serial.println("PORTRAIT_UP"); +
           display.setRotation(3);           display.setRotation(3);
           landscape = 0;           landscape = 0;
           break;           break;
-        case PORTRAIT_DOWN:    +        case PORTRAIT_DOWN:
-          //Serial.println("PORTRAIT_DOWN"); +
           display.setRotation(1);           display.setRotation(1);
           landscape = 0;           landscape = 0;
           break;           break;
         default:         default:
-          //Serial.println("UNKNOWN");  
           display.setRotation(0);           display.setRotation(0);
           landscape = 1;           landscape = 1;
Line 473: Line 551:
  
 //-------------------------------------------------- COMMAND PROCESSING --------------------------------------- //-------------------------------------------------- COMMAND PROCESSING ---------------------------------------
-void processCommand(String command) { +void processCommand(const char *command) { 
-  // Check if command is special (starts with '!') +  // System commands starting with '!' 
-  if (command.startsWith("!")) {+  if (command[0] == '!') {
     if (startsWithProgmem(command, cmd_list)) {     if (startsWithProgmem(command, cmd_list)) {
       listKeyValues();       listKeyValues();
Line 485: Line 563:
       processOrientCommand(command);       processOrientCommand(command);
     } else {     } else {
-      Serial.println(F("Error: Unknown command"));+      Serial.println(F("Error: bad command"));
     }     }
-  } else { +  } 
-    // Assume key-value pair in the form "KEY VALUE" +  // Key-value pair commands must start with '@' 
-    int spaceIndex = command.indexOf(' '); +  else if (command[0] == '@') { 
-    Serial.print(F("Space index found at: "));+    // Skip '@' and following whitespace. 
 +    const char *ptr = command + 1; 
 +    while (*ptr == ' ' || *ptr == '\t') { 
 +      ptr++; 
 +    } 
 +    int spaceIndex = -1; 
 +    for (int i = 0; ptr[i] != '\0'; i++) { 
 +      if (ptr[i] == ' ') { 
 +        spaceIndex = i; 
 +        break; 
 +      } 
 +    } 
 +    Serial.print(ptr); 
 +    Serial.print(F("Space @: "));
     Serial.println(spaceIndex);     Serial.println(spaceIndex);
     if (spaceIndex == -1) {     if (spaceIndex == -1) {
-      Serial.println(F("Error: Command format incorrect. Expected 'KEY VALUE'"));+      Serial.println(F("Error: Expected '@KEY VALUE'"));
     } else {     } else {
-      String key = command.substring(0spaceIndex); +      char key[MAX_KEY_LENGTH+1]; 
-      String value command.substring(spaceIndex + 1); +      char value[MAX_VALUE_LENGTH+1]; 
-      key.trim(); +      int i; 
-      value.trim();+      // Copy at most MAX_KEY_LENGTH characters for the key. 
 +      for (i = 0; i < spaceIndex && i < MAX_KEY_LENGTH; i++
 +        key[i] = ptr[i]
 +      
 +      key[i] '\0'; 
 +      int j = 0; 
 +      // Copy at most MAX_VALUE_LENGTH characters for the value. 
 +      for (int k = spaceIndex + 1; ptr[k] != '\0' && j < MAX_VALUE_LENGTH; k++, j++
 +        value[j] = ptr[k]
 +      } 
 +      value[j] = '\0';
       Serial.print(F("Key extracted: '"));       Serial.print(F("Key extracted: '"));
       Serial.print(key);       Serial.print(key);
Line 506: Line 607:
       setKeyValue(key, value);       setKeyValue(key, value);
     }     }
 +  } else {
 +    Serial.println(F("Error: Invalid command. Key-value commands must start with '@'"));
   }   }
 } }
  
-void setKeyValue(String key, String value) { +void setKeyValue(const char *key, const char *value) { 
-  // Update if key exists+  // Update if key exists.
   for (int i = 0; i < keyCount; i++) {   for (int i = 0; i < keyCount; i++) {
-    if (keyValueStore[i].key == key) { +    if (strcmp(keyValueStore[i].key, key) == 0) { 
-      keyValueStore[i].value value;+      strncpy(keyValueStore[i].valuevalue, MAX_VALUE_LENGTH); 
 +      keyValueStore[i].value[MAX_VALUE_LENGTH] = '\0';
       Serial.print(F("Updated "));       Serial.print(F("Updated "));
       Serial.print(key);       Serial.print(key);
Line 522: Line 626:
     }     }
   }   }
-  // Add new key-value pair if space is available+  // Add new key-value pair if space is available.
   if (keyCount < MAX_KEYS) {   if (keyCount < MAX_KEYS) {
-    keyValueStore[keyCount].key key; +    strncpy(keyValueStore[keyCount].keykey, MAX_KEY_LENGTH)
-    keyValueStore[keyCount].value = value;+    keyValueStore[keyCount].key[MAX_KEY_LENGTH] '\0'; 
 +    strncpy(keyValueStore[keyCount].value, value, MAX_VALUE_LENGTH); 
 +    keyValueStore[keyCount].value[MAX_VALUE_LENGTH] = '\0';
     keyCount++;     keyCount++;
     Serial.print(F("Added "));     Serial.print(F("Added "));
Line 533: Line 639:
     saveKeyValuesToEEPROM();     saveKeyValuesToEEPROM();
   } else {   } else {
-    Serial.println(F("Error: Key store is full"));+    Serial.println(F("Error: full"));
   }   }
 } }
Line 547: Line 653:
  
 void nukeKeyValues() { void nukeKeyValues() {
-  keyCount = 0;  // Clear the store by resetting the count+  keyCount = 0;
   Serial.println(F("All key-value pairs cleared."));   Serial.println(F("All key-value pairs cleared."));
   saveKeyValuesToEEPROM();   saveKeyValuesToEEPROM();
Line 556: Line 662:
 } }
  
-void processOrientCommand(String command) { +void processOrientCommand(const char *command) { 
-  int spaceIndex = command.indexOf(' '); +  // Process orientation command if needed.
-  if (spaceIndex == -1) { +
-    Serial.println(F("Error: !ORIENT requires an argument (0-3)")); +
-    return; +
-  } +
-  String arg = command.substring(spaceIndex + 1); +
-  arg.trim(); +
-  Serial.print(F("Orientation argument extracted: '")); +
-  Serial.print(arg); +
-  Serial.println(F("'")); +
-  int newOrient = arg.toInt(); +
-  if (newOrient < 0 || newOrient > 3) { +
-    Serial.println(F("Error: Orientation must be between 0 and 3.")); +
-  } else { +
-    orient = newOrient; +
-    Serial.print(F("Orientation set to ")); +
-    Serial.println(orient); +
-  }+
 } }
  
-// Helper function: compare a String to a command stored in PROGMEM. +// Helper: compare a C string to a command stored in PROGMEM. 
-bool startsWithProgmem(const String &s, const char *progmemStr) { +bool startsWithProgmem(const char *s, const char *progmemStr) { 
-  char buffer[20]; // Adjust size if commands grow longer+  char buffer[20];
   strcpy_P(buffer, progmemStr);   strcpy_P(buffer, progmemStr);
-  return s.startsWith(buffer);+  int len = strlen(buffer); 
 +  return (strncmp(s, buffer, len) == 0);
 } }
  
Line 776: Line 866:
     # Query version     # Query version
     response_ver = send_command(ser, "!VER")     response_ver = send_command(ser, "!VER")
-    print(f"Response from !VER: {response_ver}")+    print(f"Flush !VER: {response_ver}"
 +     
 +    response_ver = send_command(ser, "!VER"
 +    print(f"Response from !VER: {response_ver}"    
 +    
     if "Displayinator v1.0" not in response_ver:     if "Displayinator v1.0" not in response_ver:
         print(f"Error: Version mismatch or no valid response from device. Expected 'Displayinator v1.0' but received: '{response_ver}'")         print(f"Error: Version mismatch or no valid response from device. Expected 'Displayinator v1.0' but received: '{response_ver}'")
Line 802: Line 896:
         print("Stored key-value pairs are up-to-date. No need to issue !NUKE.")         print("Stored key-value pairs are up-to-date. No need to issue !NUKE.")
  
-    # Update hostname regardless; after nuke it should be empty +    # Update hostname regardless; after nuke it should be empty
-    cmd_hostname = f"Hostname {local_hostname}"+    # Prepend '@' to indicate key-value command. 
 +    cmd_hostname = f"@Hostname {local_hostname}"
     print(f"Updating hostname: {cmd_hostname}")     print(f"Updating hostname: {cmd_hostname}")
     update_response = send_command(ser, cmd_hostname)     update_response = send_command(ser, cmd_hostname)
Line 811: Line 906:
     for index, ip in enumerate(local_ips, start=1):     for index, ip in enumerate(local_ips, start=1):
         key = f"IP{index}"         key = f"IP{index}"
-        cmd_ip = f"{key} {ip}"+        cmd_ip = f"@{key} {ip}"
         print(f"Updating IP with command: {cmd_ip}")         print(f"Updating IP with command: {cmd_ip}")
         update_response = send_command(ser, cmd_ip)         update_response = send_command(ser, cmd_ip)
Line 821: Line 916:
     if len(default_gateways) == 1:     if len(default_gateways) == 1:
         # Only one gateway; use key "GW"         # Only one gateway; use key "GW"
-        cmd_gw = f"GW {default_gateways[0]}"+        cmd_gw = f"@GW {default_gateways[0]}"
         print(f"Updating gateway with command: {cmd_gw}")         print(f"Updating gateway with command: {cmd_gw}")
         update_response = send_command(ser, cmd_gw)         update_response = send_command(ser, cmd_gw)
Line 829: Line 924:
         for index, gw in enumerate(default_gateways, start=1):         for index, gw in enumerate(default_gateways, start=1):
             key = f"GW{index}"             key = f"GW{index}"
-            cmd_gw = f"{key} {gw}"+            cmd_gw = f"@{key} {gw}"
             print(f"Updating gateway with command: {cmd_gw}")             print(f"Updating gateway with command: {cmd_gw}")
             update_response = send_command(ser, cmd_gw)             update_response = send_command(ser, cmd_gw)
Line 836: Line 931:
     # Retrieve and update WAN IP address using key "WAN"     # Retrieve and update WAN IP address using key "WAN"
     wan_ip = get_wan_ip()     wan_ip = get_wan_ip()
-    cmd_wan = f"WAN {wan_ip}"+    cmd_wan = f"@WAN {wan_ip}"
     print(f"Updating WAN IP with command: {cmd_wan}")     print(f"Updating WAN IP with command: {cmd_wan}")
     update_response = send_command(ser, cmd_wan)     update_response = send_command(ser, cmd_wan)
Line 852: Line 947:
 <code bash> <code bash>
 #!/bin/bash #!/bin/bash
-# Script for TrueNAS Scale 24.10.2 to interface with a USB serial display.+# Script for TrueNAS Scale 24 to interface with a USB serial display.
 # Advanced operations: validate device response, gather network and storage info, # Advanced operations: validate device response, gather network and storage info,
 # update key-value pairs on the display, and periodically refresh pool usage if changed. # update key-value pairs on the display, and periodically refresh pool usage if changed.
 +#
 +# Modify the serial port based on the device it shows up in dmesg
 +# launch with 'nohup ./truenasserialdisplay.sh &'
  
 SERIAL_PORT="/dev/ttyACM0" SERIAL_PORT="/dev/ttyACM0"
Line 886: Line 984:
 echo "Waiting ${SLEEP_BOOT} seconds for system boot up..." echo "Waiting ${SLEEP_BOOT} seconds for system boot up..."
 sleep "$SLEEP_BOOT" sleep "$SLEEP_BOOT"
 +
 +send_cmd "!VER"
  
 # Flush any initial device data # Flush any initial device data
 read -t 1 -r DEVICE_RESPONSE <&3 read -t 1 -r DEVICE_RESPONSE <&3
-echo "Flushing Device Buffers: $DEVICE_RESPONSE"+echo "2nd Flush of Device Buffers: $DEVICE_RESPONSE"
  
 echo "Querying Device" echo "Querying Device"
-# Send the "!VER" command to the device+# Send the "!VER" command to the device (system command remains unchanged)
 send_cmd "!VER" send_cmd "!VER"
  
Line 975: Line 1075:
   human_used=$(human_readable "$used_bytes")   human_used=$(human_readable "$used_bytes")
   human_total=$(human_readable "$total_bytes")   human_total=$(human_readable "$total_bytes")
-  STORAGE_LINES+=("$pool $human_used/$human_total")+  # Prepend a caret to mark this key as temporary 
 +  STORAGE_LINES+=("^$pool $human_used/$human_total")
 done <<< "$POOL_INFO" done <<< "$POOL_INFO"
  
-# Send initial key-value pairs to the device+# Send initial key-value pairs to the device (each key-value command now starts with '@')
  
 # OS version (TrueNAS version) # OS version (TrueNAS version)
-send_cmd "TrueNAS $OS_VERSION"+send_cmd "@TrueNAS $OS_VERSION"
  
 # IP addresses: if multiple, number them; if one, use unnumbered key. # IP addresses: if multiple, number them; if one, use unnumbered key.
Line 987: Line 1088:
   index=1   index=1
   for ip in "${IP_ADDRESSES[@]}"; do   for ip in "${IP_ADDRESSES[@]}"; do
-    send_cmd "IP${index} ${ip}"+    send_cmd "@IP${index} ${ip}"
     ((index++))     ((index++))
   done   done
 elif [ ${#IP_ADDRESSES[@]} -eq 1 ]; then elif [ ${#IP_ADDRESSES[@]} -eq 1 ]; then
-  send_cmd "IP ${IP_ADDRESSES[0]}"+  send_cmd "@IP ${IP_ADDRESSES[0]}"
 fi fi
  
 # Primary gateway. # Primary gateway.
-send_cmd "GW $GATEWAY"+send_cmd "@GW $GATEWAY"
  
-# Storage info for each pool (e.g., "boot-pool 2.66G/12.11G")+# Storage info for each pool (e.g., "@^boot-pool 2.66G/12.11G")
 for line in "${STORAGE_LINES[@]}"; do for line in "${STORAGE_LINES[@]}"; do
-  send_cmd "$line"+  send_cmd "@$line"
 done done
  
Line 1005: Line 1106:
 declare -A PREV_STORAGE declare -A PREV_STORAGE
 for line in "${STORAGE_LINES[@]}"; do for line in "${STORAGE_LINES[@]}"; do
-    pool=$(echo "$line" | awk '{print $1}')+    key=$(echo "$line" | awk '{print $1}')
     value=$(echo "$line" | cut -d' ' -f2-)     value=$(echo "$line" | cut -d' ' -f2-)
-    PREV_STORAGE["$pool"]="$value"+    PREV_STORAGE["$key"]="$value"
 done done
  
 echo "Initial storage values set. Entering update loop..." echo "Initial storage values set. Entering update loop..."
 +
 +#echo EXITING for DEBUG!
 +#exec 3>&-
 +#exit 0
  
 # Trap to close the serial port on exit. # Trap to close the serial port on exit.
Line 1024: Line 1129:
   sleep "$UPDATE_INTERVAL"   sleep "$UPDATE_INTERVAL"
   for pool in "${POOLS[@]}"; do   for pool in "${POOLS[@]}"; do
-    # Get current info for this pool. 
     pool_info=$(zfs list -H -o name,used,available "$pool")     pool_info=$(zfs list -H -o name,used,available "$pool")
-    # pool_info example: "boot-pool 2.66G 9.45G" 
     cur_pool=$(echo "$pool_info" | awk '{print $1}')     cur_pool=$(echo "$pool_info" | awk '{print $1}')
     used=$(echo "$pool_info" | awk '{print $2}')     used=$(echo "$pool_info" | awk '{print $2}')
Line 1036: Line 1139:
     human_total=$(human_readable "$total_bytes")     human_total=$(human_readable "$total_bytes")
     new_value="$human_used/$human_total"     new_value="$human_used/$human_total"
-    + 
 +    # Caret-prefixed key for temporary display 
 +    key="^$cur_pool" 
     # If the new value is different from the stored value, update the display.     # If the new value is different from the stored value, update the display.
-    if [[ "${PREV_STORAGE[$pool]}" != "$new_value" ]]; then +    if [[ "${PREV_STORAGE[$key]}" != "$new_value" ]]; then 
-      echo "Updating pool $pool: was ${PREV_STORAGE[$pool]}, now $new_value" +      echo "Updating pool $cur_pool: was ${PREV_STORAGE[$key]}, now $new_value" 
-      send_cmd "$pool $new_value" +      send_cmd "@$key $new_value" 
-      PREV_STORAGE["$pool"]="$new_value"+      PREV_STORAGE["$key"]="$new_value"
     fi     fi
   done   done
server_status_oled_display.1742595556.txt.gz · Last modified: 2025/03/21 22:19 by kenson

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki