server_status_oled_display
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| server_status_oled_display [2025/03/21 10:34] – kenson | server_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/ | + | This is a device you can shove into a spare USB connector on say a server rack system to display info like IP/ |
| {{ : | {{ : | ||
| - | 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' |
| + | 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' | * The OLED's I2C address 0x3C, and I'm using adafruit' | ||
| * The Accelerometer' | * The Accelerometer' | ||
| + | |||
| + | |||
| + | ====== TLDR; ====== | ||
| + | |||
| + | |||
| + | Probe the port. | ||
| + | |||
| + | < | ||
| + | sudo tee ./ | ||
| + | # | ||
| + | set -u | ||
| + | |||
| + | BAUD=115200 | ||
| + | |||
| + | for PORT in / | ||
| + | [ -e " | ||
| + | |||
| + | echo " | ||
| + | echo " | ||
| + | |||
| + | if ! stty -F " | ||
| + | echo "Could not configure $PORT" | ||
| + | continue | ||
| + | fi | ||
| + | |||
| + | # Some Arduino/ | ||
| + | sleep 4 | ||
| + | |||
| + | # Wake / clear any stale output | ||
| + | printf ' | ||
| + | sleep 0.2 | ||
| + | timeout 1 cat " | ||
| + | |||
| + | # Ask version | ||
| + | printf ' | ||
| + | |||
| + | echo " | ||
| + | timeout 2 cat " | ||
| + | echo | ||
| + | done | ||
| + | EOF | ||
| + | chmod +x probe_displayinator.py | ||
| + | ./ | ||
| + | </ | ||
| + | |||
| + | Download the agent: {{ : | ||
| + | < | ||
| + | wget ' | ||
| + | unzip displayinatorhostip.zip | ||
| + | </ | ||
| + | |||
| + | Insert the service to autostart | ||
| + | < | ||
| + | sudo tee / | ||
| + | [Unit] | ||
| + | Description=Displayinator Host IP Sender | ||
| + | Wants=network-online.target | ||
| + | After=network-online.target multi-user.target | ||
| + | |||
| + | [Service] | ||
| + | Type=oneshot | ||
| + | WorkingDirectory=/ | ||
| + | ExecStartPre=/ | ||
| + | ExecStart=/ | ||
| + | StandardOutput=append:/ | ||
| + | StandardError=append:/ | ||
| + | |||
| + | [Install] | ||
| + | WantedBy=multi-user.target | ||
| + | EOF | ||
| + | </ | ||
| + | |||
| + | Edit / | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | Start the service. | ||
| + | |||
| + | < | ||
| + | systemctl daemon-reload | ||
| + | systemctl enable --now displayinator.service | ||
| + | systemctl status displayinator.service | ||
| + | </ | ||
| ==== 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" | + | Copy avrdude |
| Then from Arduino, select USBasp in Tools-> | Then from Arduino, select USBasp in Tools-> | ||
| 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 ' | ||
| + | * Key-Value pairs are prepended with a ' | ||
| + | * You can also indicate not to store values in EEPROM by putting a caret ' | ||
| + | |||
| + | ==== Commands ==== | ||
| !LIST which displays the stored key-value pairs | !LIST which displays the stored key-value pairs | ||
| Line 75: | Line 169: | ||
| </ | </ | ||
| - | Everything else is checked to see if it is a Key-Value pair. A pair is just two, space-delimited, alphanumeric words. E.g. | + | |
| + | ==== KEY VALUE STORAGE ==== | ||
| + | |||
| + | If the line starts with a ' | ||
| + | |||
| + | Note that this ' | ||
| < | < | ||
| - | HAPPY GILMORE | + | @HAPPY GILMORE |
| Space index found at: 5 | Space index found at: 5 | ||
| Key extracted: ' | Key extracted: ' | ||
| Added HAPPY = GILMORE | Added HAPPY = GILMORE | ||
| + | </ | ||
| + | |||
| + | Note only the first space is used as a delimiter, successive spaces are considered part of the Value. | ||
| + | |||
| + | < | ||
| + | @KEY Value cow | ||
| + | KEY Value cow, Space @: 3 | ||
| + | Key extracted: ' | ||
| + | Added KEY = Value cow | ||
| + | </ | ||
| + | |||
| + | 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 ' | ||
| + | |||
| + | < | ||
| + | @^CPU 48% | ||
| + | ^CPU 48%, Space @: 4 | ||
| + | Key extracted: ' | ||
| + | Updated ^CPU = 48% | ||
| </ | </ | ||
| 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 ' | ||
| ==== TrueNAS ===== | ==== TrueNAS ===== | ||
| Line 98: | Line 217: | ||
| It polls version, network and storage info and updates the display every 15 seconds. | It polls version, network and storage info and updates the display every 15 seconds. | ||
| - | + | ==== Code Dump: Arduino (32U4) | |
| - | ==== Code Dump ==== | + | |
| - | + | ||
| - | Arduino: | + | |
| <code cpp> | <code cpp> | ||
| #include < | #include < | ||
| Line 108: | Line 225: | ||
| #include < | #include < | ||
| #include < | #include < | ||
| - | #include < | + | #include < |
| - | //#include < | + | |
| - | //#include < | + | |
| #include < | #include < | ||
| Line 117: | 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, | : Adafruit_SSD1306(w, | ||
| - | | + | |
| 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 123: | Line 238: | ||
| // ----- EEPROM Layout Settings ----- | // ----- EEPROM Layout Settings ----- | ||
| - | #define MAX_KEY_LENGTH | + | #define MAX_KEY_LENGTH |
| - | #define MAX_VALUE_LENGTH | + | #define MAX_VALUE_LENGTH |
| #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 130: | Line 245: | ||
| // ----------------------------------- | // ----------------------------------- | ||
| - | // Use a renamed | + | // Renamed |
| - | enum Fuckme | + | enum umopapisdn |
| UNKNOWN, | UNKNOWN, | ||
| FACE_UP, | FACE_UP, | ||
| Line 142: | Line 257: | ||
| // CLI Code | // CLI Code | ||
| - | #define MAX_KEYS | + | #define MAX_KEYS |
| struct KeyValue { | struct KeyValue { | ||
| - | | + | |
| - | | + | |
| }; | }; | ||
| KeyValue keyValueStore[MAX_KEYS]; | KeyValue keyValueStore[MAX_KEYS]; | ||
| 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 | + | |
| - | bool stringComplete | + | #define INPUT_BUFFER_SIZE 67 |
| + | char inputBuffer[INPUT_BUFFER_SIZE]; | ||
| + | int inputIndex | ||
| + | bool commandReady | ||
| int orientChanged = 0; | int orientChanged = 0; | ||
| Line 162: | Line 280: | ||
| int landscape = -1; | int landscape = -1; | ||
| const float THRESHOLD = 7.0; // Minimum m/s² value to consider an axis " | const float THRESHOLD = 7.0; // Minimum m/s² value to consider an axis " | ||
| - | const unsigned long DEBOUNCE_DELAY = 250; // Time in milliseconds | + | const unsigned long DEBOUNCE_DELAY = 250; // Debounce delay in milliseconds |
| - | Fuckme | + | umopapisdn |
| 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 |
| #define OLED_RESET | #define OLED_RESET | ||
| MySSD1306 display(SCREEN_WIDTH, | MySSD1306 display(SCREEN_WIDTH, | ||
| Line 178: | Line 296: | ||
| // | // | ||
| void saveKeyValuesToEEPROM() { | void saveKeyValuesToEEPROM() { | ||
| - | | + | |
| - | | + | |
| for (int i = 0; i < keyCount; i++) { | for (int i = 0; i < keyCount; i++) { | ||
| - | int base = EEPROM_KEY_VALUE_OFFSET + i * PAIR_SIZE; | + | |
| - | // Save key as fixed-length | + | storedKeyCount++; |
| + | } | ||
| + | EEPROM.update(EEPROM_KEY_COUNT_OFFSET, | ||
| + | |||
| + | int storedIndex = 0; | ||
| + | // Store only non-filtered key-value pairs. | ||
| + | for (int i = 0; i < keyCount; i++) { | ||
| + | if (keyValueStore[i].key[0] == ' | ||
| + | | ||
| + | // Save key as fixed-length | ||
| for (int j = 0; j < MAX_KEY_LENGTH; | for (int j = 0; j < MAX_KEY_LENGTH; | ||
| - | char c = (j < keyValueStore[i].key.length()) ? keyValueStore[i].key.charAt(j) : ' | + | char c = (j < (int)strlen(keyValueStore[i].key)) ? keyValueStore[i].key[j] : ' |
| 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; | for (int j = 0; j < MAX_VALUE_LENGTH; | ||
| - | char c = (j < keyValueStore[i].value.length()) ? keyValueStore[i].value.charAt(j) : ' | + | char c = (j < (int)strlen(keyValueStore[i].value)) ? keyValueStore[i].value[j] : ' |
| EEPROM.update(base + MAX_KEY_LENGTH + j, c); | EEPROM.update(base + MAX_KEY_LENGTH + j, c); | ||
| } | } | ||
| + | storedIndex++; | ||
| } | } | ||
| } | } | ||
| Line 198: | Line 326: | ||
| 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; |
| - | | + | |
| - | } | + | |
| 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 213: | Line 339: | ||
| } | } | ||
| valueBuffer[MAX_VALUE_LENGTH] = ' | valueBuffer[MAX_VALUE_LENGTH] = ' | ||
| - | keyValueStore[i].key | + | |
| - | keyValueStore[i].value = String(valueBuffer); | + | keyValueStore[i].key[MAX_KEY_LENGTH] |
| + | strncpy(keyValueStore[i].value, | ||
| + | keyValueStore[i].value[MAX_VALUE_LENGTH] = ' | ||
| } | } | ||
| } | } | ||
| Line 220: | Line 348: | ||
| const void* activeFont = NULL; | const void* activeFont = NULL; | ||
| - | + | // customPrintln() prints text and repositions | |
| - | // customPrintln() prints text and only repositions | + | // If the string length exceeds ' |
| - | // If the string length exceeds ' | + | void customPrintln(const |
| - | void customPrintln(const | + | const void* desiredFont = (strlen(str) > threshold) ? (void*)& |
| - | // Determine which font we want based on the string length. | + | |
| - | const void* desiredFont = (str.length() > threshold) ? (void*)& | + | |
| - | + | ||
| - | // | + | |
| - | // | + | |
| int currY = display.getCursorY(); | int currY = display.getCursorY(); | ||
| - | if ((currY == 0) && (!landscape)) { // weird bug | + | if ((currY == 0) && (!landscape)) { |
| - | display.setCursor(0, | + | display.setCursor(0, |
| } | } | ||
| - | | ||
| - | // 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 |
| - | | + | |
| - | // For example, when switching to TomThumb, move up by 1 pixel; | + | |
| - | // when switching | + | |
| if (desiredFont == (void*)& | if (desiredFont == (void*)& | ||
| - | // | ||
| display.setCursor(0, | display.setCursor(0, | ||
| } else { | } else { | ||
| Line 250: | Line 366: | ||
| } | } | ||
| } | } | ||
| - | | ||
| - | // Print the string (this also moves the cursor downward). | ||
| display.println(str); | display.println(str); | ||
| } | } | ||
| - | |||
| // | // | ||
| void setup() { | void setup() { | ||
| Serial.begin(115200); | Serial.begin(115200); | ||
| - | //while (!Serial) delay(10); | ||
| - | |||
| - | // | ||
| - | | ||
| - | // Load key-value pairs from EEPROM | ||
| loadKeyValuesFromEEPROM(); | loadKeyValuesFromEEPROM(); | ||
| | | ||
| if (!mma.begin(ACCEL_ADDRESS)) { | if (!mma.begin(ACCEL_ADDRESS)) { | ||
| - | Serial.println(" | + | Serial.println(F(" |
| - | for(;; | + | for(;;); |
| - | } | + | } |
| - | Serial.println(" | + | Serial.println(F(" |
| + | |||
| if (!display.begin(SSD1306_SWITCHCAPVCC, | if (!display.begin(SSD1306_SWITCHCAPVCC, | ||
| Serial.println(F(" | Serial.println(F(" | ||
| - | for(;; | + | for(;;); |
| } | } | ||
| - | Serial.println(" | + | Serial.println(F(" |
| - | + | ||
| display.clearDisplay(); | display.clearDisplay(); | ||
| display.display(); | display.display(); | ||
| + | | ||
| mma.getEvent(& | mma.getEvent(& | ||
| - | float ax = event.acceleration.x; | + | 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, | updateOrientation(ax, | ||
| - | + | | |
| - | // | + | display.setTextSize(1); |
| - | display.setTextSize(1); | + | display.setTextColor(SSD1306_WHITE); |
| - | display.setTextColor(SSD1306_WHITE); | + | customPrintln(" |
| - | customPrintln(F(" | + | |
| - | + | ||
| - | /* customPrintln(F(" | + | |
| - | customPrintln(F(" | + | |
| - | customPrintln(F(" | + | |
| - | customPrintln(F(" | + | |
| - | */ | + | |
| display.display(); | display.display(); | ||
| - | | + | |
| - | + | | |
| + | | ||
| + | | ||
| | | ||
| mma.setRange(MMA8451_RANGE_2_G); | mma.setRange(MMA8451_RANGE_2_G); | ||
| - | | ||
| - | // | ||
| } | } | ||
| // | // | ||
| void loop() { | void loop() { | ||
| - | /* Get a new sensor event */ | ||
| - | |||
| mma.getEvent(& | mma.getEvent(& | ||
| - | float ax = event.acceleration.x; | + | 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, | updateOrientation(ax, | ||
| + | | ||
| 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(); | ||
| - | | + | if (inChar == ' |
| - | | + | |
| - | | + | |
| - | | + | break; |
| - | break; | + | } else if (inChar != ' |
| - | } else if (inChar != ' | + | |
| - | | + | inputBuffer[inputIndex++] = inChar; |
| - | } else { | + | |
| - | //Serial.print(" | + | |
| + | // If the input buffer is full, force processing. | ||
| + | if (inputIndex >= INPUT_BUFFER_SIZE - 1) { | ||
| + | commandReady = true; | ||
| + | break; | ||
| + | } | ||
| } | } | ||
| } | } | ||
| | | ||
| - | | + | if (commandReady) { |
| - | | + | |
| - | | + | int start = 0; |
| - | | + | while (inputBuffer[start] == ' ' || inputBuffer[start] == ' |
| - | | + | |
| - | processCommand(inputString); | + | while (end > start && (inputBuffer[end] == ' ' || inputBuffer[end] == ' |
| + | | ||
| + | end--; | ||
| + | } | ||
| + | if (strlen(inputBuffer + start) > 0) { | ||
| + | processCommand(inputBuffer + start); | ||
| } | } | ||
| - | | + | |
| - | | + | |
| - | | + | |
| } | } | ||
| delay(250); | delay(250); | ||
| Line 349: | Line 457: | ||
| // | // | ||
| - | 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 |
| - | 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, | display.setCursor(0, | ||
| | | ||
| - | // Display key-value pairs | ||
| if (keyCount == 0) { | if (keyCount == 0) { | ||
| - | customPrintln(F("No KV data"),sWidth); | + | customPrintln(" |
| } else { | } else { | ||
| for (int i = 0; i < keyCount; i++) { | for (int i = 0; i < keyCount; i++) { | ||
| - | if ((keyValueStore[i].key.length() + keyValueStore[i].value.length() + 1) <= sWidth) { | + | |
| - | | + | |
| - | | + | |
| + | | ||
| } else { | } else { | ||
| - | | + | |
| - | customPrintln(keyValueStore[i].value, | + | displayKey[MAX_KEY_LENGTH] = ' |
| + | } | ||
| + | char result[2 * MAX_KEY_LENGTH + MAX_VALUE_LENGTH + 3]; | ||
| + | if ((strlen(displayKey) + strlen(keyValueStore[i].value) + 1) <= (unsigned)sWidth) { | ||
| + | snprintf(result, | ||
| + | customPrintln(result, | ||
| + | } else { | ||
| + | customPrintln(displayKey, sWidth); | ||
| + | customPrintln(keyValueStore[i].value, | ||
| } | } | ||
| } | } | ||
| } | } | ||
| - | | ||
| display.display(); | display.display(); | ||
| } | } | ||
| Line 387: | Line 496: | ||
| // | // | ||
| - | Fuckme | + | umopapisdn |
| 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 398: | Line 507: | ||
| void updateOrientation(float ax, float ay, float az) { | void updateOrientation(float ax, float ay, float 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(" | ||
| switch (current) { | switch (current) { | ||
| - | case FACE_UP: | + | case FACE_UP: |
| - | | + | |
| + | landscape = 1; | ||
| break; | break; | ||
| - | case FACE_DOWN: | + | case FACE_DOWN: |
| - | | + | |
| + | landscape = 1; | ||
| break; | break; | ||
| - | case LANDSCAPE_RIGHT: | + | case LANDSCAPE_RIGHT: |
| - | Serial.println(" | + | |
| display.setRotation(0); | display.setRotation(0); | ||
| landscape = 1; | landscape = 1; | ||
| break; | break; | ||
| - | case LANDSCAPE_LEFT: | + | case LANDSCAPE_LEFT: |
| - | Serial.println(" | + | |
| display.setRotation(2); | display.setRotation(2); | ||
| landscape = 1; | landscape = 1; | ||
| break; | break; | ||
| - | case PORTRAIT_UP: | + | case PORTRAIT_UP: |
| - | Serial.println(" | + | |
| display.setRotation(3); | display.setRotation(3); | ||
| landscape = 0; | landscape = 0; | ||
| break; | break; | ||
| - | case PORTRAIT_DOWN: | + | case PORTRAIT_DOWN: |
| - | Serial.println(" | + | |
| display.setRotation(1); | display.setRotation(1); | ||
| landscape = 0; | landscape = 0; | ||
| break; | break; | ||
| default: | default: | ||
| - | | + | |
| + | landscape = 1; | ||
| break; | break; | ||
| } | } | ||
| Line 444: | Line 551: | ||
| // | // | ||
| - | void processCommand(String | + | void processCommand(const char *command) { |
| - | // Check if command is special (starts | + | // System commands starting |
| - | if (command.startsWith(" | + | if (command[0] == '!') { |
| if (startsWithProgmem(command, | if (startsWithProgmem(command, | ||
| listKeyValues(); | listKeyValues(); | ||
| Line 456: | Line 563: | ||
| processOrientCommand(command); | processOrientCommand(command); | ||
| } else { | } else { | ||
| - | Serial.println(F(" | + | Serial.println(F(" |
| } | } | ||
| - | } 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(" | + | // Skip ' |
| + | const char *ptr = command + 1; | ||
| + | while (*ptr == ' ' || *ptr == ' | ||
| + | ptr++; | ||
| + | } | ||
| + | int spaceIndex = -1; | ||
| + | for (int i = 0; ptr[i] != '\0'; i++) { | ||
| + | if (ptr[i] == ' ') { | ||
| + | spaceIndex = i; | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | Serial.print(ptr); | ||
| + | Serial.print(F(" | ||
| Serial.println(spaceIndex); | Serial.println(spaceIndex); | ||
| if (spaceIndex == -1) { | if (spaceIndex == -1) { | ||
| - | Serial.println(F(" | + | Serial.println(F(" |
| } else { | } else { | ||
| - | | + | |
| - | | + | char value[MAX_VALUE_LENGTH+1]; |
| - | | + | int i; |
| - | value.trim(); | + | // Copy at most MAX_KEY_LENGTH characters for the key. |
| + | for (i = 0; i < spaceIndex | ||
| + | key[i] = ptr[i]; | ||
| + | | ||
| + | key[i] | ||
| + | int j = 0; | ||
| + | // Copy at most MAX_VALUE_LENGTH characters for the value. | ||
| + | for (int k = spaceIndex + 1; ptr[k] != ' | ||
| + | value[j] = ptr[k]; | ||
| + | | ||
| + | value[j] = ' | ||
| Serial.print(F(" | Serial.print(F(" | ||
| Serial.print(key); | Serial.print(key); | ||
| Line 477: | Line 607: | ||
| setKeyValue(key, | setKeyValue(key, | ||
| } | } | ||
| + | } else { | ||
| + | Serial.println(F(" | ||
| } | } | ||
| } | } | ||
| - | void setKeyValue(String | + | 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 | + | |
| + | keyValueStore[i].value[MAX_VALUE_LENGTH] = ' | ||
| Serial.print(F(" | Serial.print(F(" | ||
| Serial.print(key); | Serial.print(key); | ||
| Line 493: | 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 | + | |
| - | keyValueStore[keyCount].value = value; | + | keyValueStore[keyCount].key[MAX_KEY_LENGTH] |
| + | strncpy(keyValueStore[keyCount].value, value, MAX_VALUE_LENGTH); | ||
| + | keyValueStore[keyCount].value[MAX_VALUE_LENGTH] = ' | ||
| keyCount++; | keyCount++; | ||
| Serial.print(F(" | Serial.print(F(" | ||
| Line 504: | Line 639: | ||
| saveKeyValuesToEEPROM(); | saveKeyValuesToEEPROM(); | ||
| } else { | } else { | ||
| - | Serial.println(F(" | + | Serial.println(F(" |
| } | } | ||
| } | } | ||
| Line 518: | Line 653: | ||
| void nukeKeyValues() { | void nukeKeyValues() { | ||
| - | keyCount = 0; // Clear the store by resetting the count | + | keyCount = 0; |
| Serial.println(F(" | Serial.println(F(" | ||
| saveKeyValuesToEEPROM(); | saveKeyValuesToEEPROM(); | ||
| Line 527: | Line 662: | ||
| } | } | ||
| - | void processOrientCommand(String | + | void processOrientCommand(const char *command) { |
| - | | + | |
| - | | + | |
| - | Serial.println(F(" | + | |
| - | return; | + | |
| - | } | + | |
| - | String arg = command.substring(spaceIndex + 1); | + | |
| - | arg.trim(); | + | |
| - | Serial.print(F(" | + | |
| - | Serial.print(arg); | + | |
| - | Serial.println(F("'" | + | |
| - | int newOrient = arg.toInt(); | + | |
| - | if (newOrient < 0 || newOrient > 3) { | + | |
| - | Serial.println(F(" | + | |
| - | } else { | + | |
| - | orient = newOrient; | + | |
| - | Serial.print(F(" | + | |
| - | Serial.println(orient); | + | |
| - | } | + | |
| } | } | ||
| - | // Helper | + | // Helper: compare a C string |
| - | bool startsWithProgmem(const | + | bool startsWithProgmem(const |
| - | char buffer[20]; | + | char buffer[20]; |
| strcpy_P(buffer, | strcpy_P(buffer, | ||
| - | return | + | |
| + | | ||
| } | } | ||
| Line 568: | Line 687: | ||
| </ | </ | ||
| - | Python: | + | ==== Code Dump: Python (Windows/ |
| <code python> | <code python> | ||
| import serial | import serial | ||
| Line 746: | Line 866: | ||
| # Query version | # Query version | ||
| response_ver = send_command(ser, | response_ver = send_command(ser, | ||
| - | print(f" | + | |
| + | |||
| + | response_ver = send_command(ser, | ||
| + | | ||
| + | | ||
| if " | if " | ||
| print(f" | print(f" | ||
| Line 772: | Line 896: | ||
| print(" | print(" | ||
| - | # Update hostname regardless; after nuke it should be empty | + | # Update hostname regardless; after nuke it should be empty. |
| - | cmd_hostname = f" | + | # Prepend ' |
| + | cmd_hostname = f"@Hostname {local_hostname}" | ||
| print(f" | print(f" | ||
| update_response = send_command(ser, | update_response = send_command(ser, | ||
| Line 781: | Line 906: | ||
| for index, ip in enumerate(local_ips, | for index, ip in enumerate(local_ips, | ||
| key = f" | key = f" | ||
| - | cmd_ip = f" | + | cmd_ip = f"@{key} {ip}" |
| print(f" | print(f" | ||
| update_response = send_command(ser, | update_response = send_command(ser, | ||
| Line 791: | Line 916: | ||
| if len(default_gateways) == 1: | if len(default_gateways) == 1: | ||
| # Only one gateway; use key " | # Only one gateway; use key " | ||
| - | cmd_gw = f"GW {default_gateways[0]}" | + | cmd_gw = f"@GW {default_gateways[0]}" |
| print(f" | print(f" | ||
| update_response = send_command(ser, | update_response = send_command(ser, | ||
| Line 799: | Line 924: | ||
| for index, gw in enumerate(default_gateways, | for index, gw in enumerate(default_gateways, | ||
| key = f" | key = f" | ||
| - | cmd_gw = f" | + | cmd_gw = f"@{key} {gw}" |
| print(f" | print(f" | ||
| update_response = send_command(ser, | update_response = send_command(ser, | ||
| Line 806: | Line 931: | ||
| # Retrieve and update WAN IP address using key " | # Retrieve and update WAN IP address using key " | ||
| wan_ip = get_wan_ip() | wan_ip = get_wan_ip() | ||
| - | cmd_wan = f"WAN {wan_ip}" | + | cmd_wan = f"@WAN {wan_ip}" |
| print(f" | print(f" | ||
| update_response = send_command(ser, | update_response = send_command(ser, | ||
| Line 818: | Line 943: | ||
| </ | </ | ||
| - | Bash: | + | ==== Code Dump: Bash (TrueNAS Scale) ==== |
| <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 ./ | ||
| SERIAL_PORT="/ | SERIAL_PORT="/ | ||
| Line 856: | Line 984: | ||
| echo " | echo " | ||
| sleep " | sleep " | ||
| + | |||
| + | send_cmd " | ||
| # 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 | + | echo "2nd Flush of Device Buffers: $DEVICE_RESPONSE" |
| echo " | echo " | ||
| - | # Send the " | + | # Send the " |
| send_cmd " | send_cmd " | ||
| Line 945: | Line 1075: | ||
| human_used=$(human_readable " | human_used=$(human_readable " | ||
| human_total=$(human_readable " | human_total=$(human_readable " | ||
| - | STORAGE_LINES+=(" | + | |
| + | | ||
| done <<< | done <<< | ||
| - | # Send initial key-value pairs to the device | + | # Send initial key-value pairs to the device |
| # OS version (TrueNAS version) | # OS version (TrueNAS version) | ||
| - | send_cmd " | + | 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 957: | Line 1088: | ||
| index=1 | index=1 | ||
| for ip in " | for ip in " | ||
| - | send_cmd " | + | send_cmd "@IP${index} ${ip}" |
| ((index++)) | ((index++)) | ||
| done | done | ||
| elif [ ${# | elif [ ${# | ||
| - | 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., " | + | # Storage info for each pool (e.g., "@^boot-pool 2.66G/ |
| for line in " | for line in " | ||
| - | send_cmd " | + | send_cmd "@$line" |
| done | done | ||
| Line 975: | Line 1106: | ||
| declare -A PREV_STORAGE | declare -A PREV_STORAGE | ||
| for line in " | for line in " | ||
| - | | + | |
| value=$(echo " | value=$(echo " | ||
| - | PREV_STORAGE[" | + | PREV_STORAGE[" |
| done | done | ||
| echo " | echo " | ||
| + | |||
| + | #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 994: | Line 1129: | ||
| sleep " | sleep " | ||
| for pool in " | for pool in " | ||
| - | # Get current info for this pool. | ||
| pool_info=$(zfs list -H -o name, | pool_info=$(zfs list -H -o name, | ||
| - | # pool_info example: " | ||
| cur_pool=$(echo " | cur_pool=$(echo " | ||
| used=$(echo " | used=$(echo " | ||
| Line 1006: | Line 1139: | ||
| human_total=$(human_readable " | human_total=$(human_readable " | ||
| new_value=" | new_value=" | ||
| - | | + | |
| + | # Caret-prefixed key for temporary display | ||
| + | key=" | ||
| # 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 [[ " | + | if [[ " |
| - | echo " | + | echo " |
| - | send_cmd "$pool $new_value" | + | send_cmd "@$key $new_value" |
| - | PREV_STORAGE[" | + | PREV_STORAGE[" |
| fi | fi | ||
| done | done | ||
server_status_oled_display.1742553296.txt.gz · Last modified: 2025/03/21 10:34 by kenson
