server_status_oled_display
This is an old revision of the document!
Server Status OLED Display
I'm using a cheap chinese USBasp (with avrdude v8.0), the software complains about older firmware but ignore it.
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)
Arduino Code: oledusbdisplayaccx1.zip Python Code: displayinatorhostip.zip
Arduino:
#include <Wire.h> #include <Adafruit_MMA8451.h> #include <Adafruit_Sensor.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <Fonts/TomThumb.h> // TomThumb //#include <Fonts/Org_01.h> // Org_01 //#include <Fonts/Picopixel.h> // Picopixel #include <EEPROM.h> class MySSD1306 : public Adafruit_SSD1306 { public: MySSD1306(int16_t w, int16_t h, TwoWire *twi, int8_t rst_pin) : Adafruit_SSD1306(w, h, twi, rst_pin) {} int16_t getCursorX() { return cursor_x; } int16_t getCursorY() { return cursor_y; } }; // ----- EEPROM Layout Settings ----- #define MAX_KEY_LENGTH 16 // Maximum characters for a key #define MAX_VALUE_LENGTH 16 // Maximum characters for a value #define PAIR_SIZE (MAX_KEY_LENGTH + MAX_VALUE_LENGTH) #define EEPROM_KEY_COUNT_OFFSET 0 #define EEPROM_KEY_VALUE_OFFSET 1 // Data begins here // ----------------------------------- // Use a renamed enum to avoid potential conflicts enum Fuckme { UNKNOWN, FACE_UP, FACE_DOWN, LANDSCAPE_RIGHT, LANDSCAPE_LEFT, PORTRAIT_UP, PORTRAIT_DOWN }; // CLI Code #define MAX_KEYS 10 // Maximum number of key-value pairs struct KeyValue { String key; String value; }; KeyValue keyValueStore[MAX_KEYS]; // Storage for key-value pairs int keyCount = 0; // Current number of keys stored 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 int orientChanged = 0; // PROGMEM constant command strings const char cmd_list[] PROGMEM = "!LIST"; const char cmd_nuke[] PROGMEM = "!NUKE"; const char cmd_ver[] PROGMEM = "!VER"; const char cmd_orient[] PROGMEM = "!ORIENT"; int landscape = -1; 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 Fuckme lastStableOrientation = UNKNOWN; unsigned long lastChangeTime = 0; #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define SCREEN_ADDRESS 0x3C // I2C Addr, Commonly 0x3D or 0x3C #define OLED_RESET -1 MySSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); #define ACCEL_ADDRESS 0x1C Adafruit_MMA8451 mma = Adafruit_MMA8451(); sensors_event_t event; //-------------------------------------------------- EEPROM Functions --------------------------------------- void saveKeyValuesToEEPROM() { // Save the number of key-value pairs at address 0. EEPROM.update(EEPROM_KEY_COUNT_OFFSET, keyCount); for (int i = 0; i < keyCount; i++) { int base = EEPROM_KEY_VALUE_OFFSET + i * PAIR_SIZE; // Save key as fixed-length char array for (int j = 0; j < MAX_KEY_LENGTH; j++) { char c = (j < keyValueStore[i].key.length()) ? keyValueStore[i].key.charAt(j) : '\0'; EEPROM.update(base + j, c); } // Save value as fixed-length char array for (int j = 0; j < MAX_VALUE_LENGTH; j++) { char c = (j < keyValueStore[i].value.length()) ? keyValueStore[i].value.charAt(j) : '\0'; EEPROM.update(base + MAX_KEY_LENGTH + j, c); } } } void loadKeyValuesFromEEPROM() { keyCount = EEPROM.read(EEPROM_KEY_COUNT_OFFSET); // Validate keyCount (if invalid, reset to 0) if (keyCount < 0 || keyCount > MAX_KEYS) { keyCount = 0; } for (int i = 0; i < keyCount; i++) { int base = EEPROM_KEY_VALUE_OFFSET + i * PAIR_SIZE; char keyBuffer[MAX_KEY_LENGTH + 1]; char valueBuffer[MAX_VALUE_LENGTH + 1]; for (int j = 0; j < MAX_KEY_LENGTH; j++) { keyBuffer[j] = EEPROM.read(base + j); } keyBuffer[MAX_KEY_LENGTH] = '\0'; for (int j = 0; j < MAX_VALUE_LENGTH; j++) { valueBuffer[j] = EEPROM.read(base + MAX_KEY_LENGTH + j); } valueBuffer[MAX_VALUE_LENGTH] = '\0'; keyValueStore[i].key = String(keyBuffer); keyValueStore[i].value = String(valueBuffer); } } //----------------------------------------------------------------------------------------------------- const void* activeFont = NULL; // customPrintln() prints text and only repositions if the font changes. // If the string length exceeds 'threshold', we use the TomThumb font; otherwise, the default. void customPrintln(const String &str, size_t threshold) { // 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(); if ((currY == 0) && (!landscape)) { // weird bug display.setCursor(0,7); } // Only change font (and reposition) if it differs from the active font. if (activeFont != desiredFont) { display.setFont(desiredFont); activeFont = desiredFont; // 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) { //Serial.println(display.getCursorY()); display.setCursor(0, display.getCursorY() - 1); } else { display.setCursor(0, display.getCursorY() + 1); } } // Print the string (this also moves the cursor downward). display.println(str); } //-------------------------------------------------- SETUP ------------------------------------------- void setup() { Serial.begin(115200); //while (!Serial) delay(10); // will pause until serial console opens //Serial.println("Displayinator Boot"); // Load key-value pairs from EEPROM loadKeyValuesFromEEPROM(); if (!mma.begin(ACCEL_ADDRESS)) { Serial.println("MMA8451 FAIL!"); for(;;); // Don't proceed, loop forever } Serial.println("MMA8451 OK."); if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 FAIL")); for(;;); // Don't proceed, loop forever } Serial.println("SSD1306 OK."); display.clearDisplay(); display.display(); mma.getEvent(&event); float ax = event.acceleration.x; // acceleration is measured in m/s^2 float ay = event.acceleration.y; float az = event.acceleration.z; updateOrientation(ax, ay, az); //display.setFont(&Picopixel); display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(SSD1306_WHITE); // Draw white text 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(); inputString.reserve(20); // Reserve memory for the input string mma.setRange(MMA8451_RANGE_2_G); //Serial.print("ACCRANGE = "); Serial.print(2 << mma.getRange()); Serial.println("G"); } //-------------------------------------------------- LOOP -------------------------------------------- void loop() { /* Get a new sensor event */ mma.getEvent(&event); float ax = event.acceleration.x; // acceleration is measured in m/s^2 float ay = event.acceleration.y; float az = event.acceleration.z; updateOrientation(ax, ay, az); updateDisplay(); while (Serial.available()) { // Nonblocking serial input: read available characters one at a time char inChar = (char)Serial.read(); //Serial.print(inChar); if (inChar == '\n') { // Newline signals end of command //Serial.print("[LF]"); stringComplete = true; break; // Process this command after breaking out } else if (inChar != '\r') { // Ignore carriage return inputString += inChar; } else { //Serial.print("[CR]"); } } // If a complete command was received, process it if (stringComplete) { inputString.trim(); if (inputString.length() > 0) { //Serial.println(inputString); processCommand(inputString); } // Reset buffer for the next command inputString = ""; stringComplete = false; } delay(250); } //-------------------------------------------------- DISPLAY --------------------------------------- unsigned long lastDisplayUpdate = 0; // Tracks the last display update time const unsigned long DISPLAY_UPDATE_INTERVAL = 1000; // Update interval in milliseconds void updateDisplay() { unsigned long now = millis(); int sWidth = 10; if (landscape) { sWidth = 21; } else { sWidth = 10; } if (now - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL) { lastDisplayUpdate = now; display.clearDisplay(); display.setCursor(0, 0); // Display key-value pairs if (keyCount == 0) { customPrintln(F("No KV data"),sWidth); } else { for (int i = 0; i < keyCount; i++) { if ((keyValueStore[i].key.length() + keyValueStore[i].value.length() + 1) <= sWidth) { String result = keyValueStore[i].key + ":" + keyValueStore[i].value; customPrintln(result,sWidth); } else { customPrintln(keyValueStore[i].key,sWidth); customPrintln(keyValueStore[i].value,sWidth); } } } display.display(); } } //-------------------------------------------------- ORIENTATION --------------------------------------- Fuckme detectOrientation(float ax, float ay, float az) { if (az > THRESHOLD) return FACE_UP; if (az < -THRESHOLD) return FACE_DOWN; if (ax > THRESHOLD) return LANDSCAPE_RIGHT; if (ax < -THRESHOLD) return LANDSCAPE_LEFT; if (ay > THRESHOLD) return PORTRAIT_UP; if (ay < -THRESHOLD) return PORTRAIT_DOWN; return UNKNOWN; } void updateOrientation(float ax, float ay, float az) { Fuckme current = detectOrientation(ax, ay, az); unsigned long now = millis(); if (current != lastStableOrientation) { if (now - lastChangeTime >= DEBOUNCE_DELAY) { lastStableOrientation = current; orientChanged = 1; Serial.print("REORIENT: "); switch (current) { case FACE_UP: Serial.println("FACE_UP"); break; case FACE_DOWN: Serial.println("FACE_DOWN"); break; case LANDSCAPE_RIGHT: Serial.println("LANDSCAPE_RIGHT"); display.setRotation(0); landscape = 1; break; case LANDSCAPE_LEFT: Serial.println("LANDSCAPE_LEFT"); display.setRotation(2); landscape = 1; break; case PORTRAIT_UP: Serial.println("PORTRAIT_UP"); display.setRotation(3); landscape = 0; break; case PORTRAIT_DOWN: Serial.println("PORTRAIT_DOWN"); display.setRotation(1); landscape = 0; break; default: Serial.println("UNKNOWN"); break; } } } else { lastChangeTime = now; } } //-------------------------------------------------- COMMAND PROCESSING --------------------------------------- void processCommand(String command) { // Check if command is special (starts with '!') if (command.startsWith("!")) { if (startsWithProgmem(command, cmd_list)) { listKeyValues(); } else if (startsWithProgmem(command, cmd_nuke)) { nukeKeyValues(); } else if (startsWithProgmem(command, cmd_ver)) { printVersion(); } else if (startsWithProgmem(command, cmd_orient)) { processOrientCommand(command); } else { Serial.println(F("Error: Unknown command")); } } else { // Assume key-value pair in the form "KEY VALUE" int spaceIndex = command.indexOf(' '); Serial.print(F("Space index found at: ")); Serial.println(spaceIndex); if (spaceIndex == -1) { Serial.println(F("Error: Command format incorrect. Expected 'KEY VALUE'")); } else { String key = command.substring(0, spaceIndex); String value = command.substring(spaceIndex + 1); key.trim(); value.trim(); Serial.print(F("Key extracted: '")); Serial.print(key); Serial.print(F("', Value extracted: '")); Serial.print(value); Serial.println(F("'")); setKeyValue(key, value); } } } void setKeyValue(String key, String value) { // Update if key exists for (int i = 0; i < keyCount; i++) { if (keyValueStore[i].key == key) { keyValueStore[i].value = value; Serial.print(F("Updated ")); Serial.print(key); Serial.print(F(" = ")); Serial.println(value); saveKeyValuesToEEPROM(); return; } } // Add new key-value pair if space is available if (keyCount < MAX_KEYS) { keyValueStore[keyCount].key = key; keyValueStore[keyCount].value = value; keyCount++; Serial.print(F("Added ")); Serial.print(key); Serial.print(F(" = ")); Serial.println(value); saveKeyValuesToEEPROM(); } else { Serial.println(F("Error: Key store is full")); } } void listKeyValues() { Serial.println(F("Key-Value Pairs:")); for (int i = 0; i < keyCount; i++) { Serial.print(keyValueStore[i].key); Serial.print(F(" = ")); Serial.println(keyValueStore[i].value); } } void nukeKeyValues() { keyCount = 0; // Clear the store by resetting the count Serial.println(F("All key-value pairs cleared.")); saveKeyValuesToEEPROM(); } void printVersion() { Serial.println(F("Displayinator v1.0")); } void processOrientCommand(String command) { int spaceIndex = command.indexOf(' '); 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. bool startsWithProgmem(const String &s, const char *progmemStr) { char buffer[20]; // Adjust size if commands grow longer strcpy_P(buffer, progmemStr); return s.startsWith(buffer); } void displayKeyValuePairs() { if (keyCount == 0) { display.println(F("No KV data")); } else { for (int i = 0; i < keyCount; i++) { display.print(keyValueStore[i].key); display.print(F(" = ")); display.println(keyValueStore[i].value); } } }
Python:
import serial import time import socket import re import sys import netifaces import argparse import platform import urllib.request import json """ pip install pyserial netifaces """ def send_command(ser, command): """ Sends a command to the serial device and returns its response. Clears the input buffer, sends the command with a newline, waits briefly, then reads available lines. """ try: ser.reset_input_buffer() # Write command (append newline) ser.write((command + "\n").encode('utf-8')) time.sleep(0.5) except Exception as e: print(f"Error writing command '{command}' to port: {e}") return "" response = "" try: while ser.in_waiting > 0: line = ser.readline().decode('utf-8', errors='replace') response += line except Exception as e: print(f"Warning: error reading response for command '{command}': {e}") return response.strip() def get_ips_with_gateway(): """ Returns a list of IPv4 addresses from interfaces that have an associated gateway. Requires the netifaces module. """ gateways = netifaces.gateways() interfaces_with_gateway = set() if netifaces.AF_INET in gateways: for gw_info in gateways[netifaces.AF_INET]: if isinstance(gw_info, tuple) and len(gw_info) >= 2: interface = gw_info[1] interfaces_with_gateway.add(interface) ip_list = [] for interface in interfaces_with_gateway: addrs = netifaces.ifaddresses(interface) if netifaces.AF_INET in addrs: for link in addrs[netifaces.AF_INET]: ip = link.get('addr') if ip and ip != "127.0.0.1": ip_list.append(ip) return ip_list def get_default_gateways(): """ Returns a deduplicated list of default IPv4 gateway addresses. """ gateways = netifaces.gateways() gw_set = set() if netifaces.AF_INET in gateways: for gw_info in gateways[netifaces.AF_INET]: if isinstance(gw_info, tuple): gw_set.add(gw_info[0]) return list(gw_set) def get_wan_ip(): """ Retrieves the WAN IP address using the ipify API. """ url = "https://api.ipify.org?format=json" try: with urllib.request.urlopen(url) as response: data = response.read().decode('utf-8') ip_info = json.loads(data) return ip_info.get("ip", "Unknown") except Exception as e: return f"Error retrieving IP: {e}" def parse_key_value_response(response): """ Parses a !LIST response into a dictionary. Expected response format: Key-Value Pairs: Hostname = SOMETHING IP1 = X.X.X.X ... """ kv_pairs = {} lines = response.splitlines() start_parsing = False for line in lines: if "Key-Value Pairs:" in line: start_parsing = True continue if start_parsing and line.strip(): match = re.match(r'^\s*(\S+)\s*=\s*(\S+)\s*$', line) if match: key = match.group(1) value = match.group(2) kv_pairs[key] = value return kv_pairs def needs_nuke(stored_kv, local_hostname, local_ips): """ Compares the stored key-value pairs against the local hostname and IP addresses. Returns True if a difference is detected. """ # Check hostname difference if stored_kv.get("Hostname", "") != local_hostname: print(f"Hostname difference detected: stored='{stored_kv.get('Hostname', None)}' vs local='{local_hostname}'") return True # Gather stored IPs in order: IP1, IP2, ... stored_ips = [] i = 1 while True: key = f"IP{i}" if key in stored_kv: stored_ips.append(stored_kv[key]) i += 1 else: break # Compare lists: if number or order of IPs is different, we consider it a difference. if stored_ips != local_ips: print(f"IP address difference detected: stored={stored_ips} vs local={local_ips}") return True return False def main(): # Determine default port based on the OS if platform.system() == "Linux": default_port = "/dev/ttyUSB0" else: default_port = "COM70" # Parse command-line arguments to get the serial port parser = argparse.ArgumentParser(description="Serial updater for Displayinator") parser.add_argument("--port", type=str, default=default_port, help="Serial port to connect to (e.g., /dev/ttyUSB0 on Linux or COM70 on Windows)") args = parser.parse_args() com_port = args.port baud_rate = 115200 # Open and configure the serial port try: ser = serial.Serial( port=com_port, baudrate=baud_rate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=2, write_timeout=2 ) ser.dtr = False # Allow time for Arduino reset upon opening the port time.sleep(5) except Exception as e: print(f"Failed to open port {com_port}. Error details: {e}") sys.exit(1) # Query version response_ver = send_command(ser, "!VER") print(f"Response from !VER: {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}'") ser.close() sys.exit(1) # Query key-value pairs with !LIST response_list = send_command(ser, "!LIST") print("Response from !LIST:") print(response_list) stored_kv_pairs = parse_key_value_response(response_list) # Get the machine's hostname and IP addresses (only those with a gateway) local_hostname = socket.gethostname() local_ips = get_ips_with_gateway() print(f"Machine Hostname: {local_hostname}") print(f"Machine IP Addresses (with gateway): {', '.join(local_ips)}") # Determine if stored values differ from local values if needs_nuke(stored_kv_pairs, local_hostname, local_ips): print("Differences detected. Issuing !NUKE command to clear stored key-value pairs...") nuke_response = send_command(ser, "!NUKE") print(f"Response from !NUKE: {nuke_response}") else: print("Stored key-value pairs are up-to-date. No need to issue !NUKE.") # Update hostname regardless; after nuke it should be empty cmd_hostname = f"Hostname {local_hostname}" print(f"Updating hostname: {cmd_hostname}") update_response = send_command(ser, cmd_hostname) print(f"Response: {update_response}") # Update IP addresses, assigning them as IP1, IP2, etc. for index, ip in enumerate(local_ips, start=1): key = f"IP{index}" cmd_ip = f"{key} {ip}" print(f"Updating IP with command: {cmd_ip}") update_response = send_command(ser, cmd_ip) print(f"Response: {update_response}") # Update default gateways: default_gateways = get_default_gateways() print(f"Default Gateways: {', '.join(default_gateways)}") if len(default_gateways) == 1: # Only one gateway; use key "GW" cmd_gw = f"GW {default_gateways[0]}" print(f"Updating gateway with command: {cmd_gw}") update_response = send_command(ser, cmd_gw) print(f"Response: {update_response}") else: # Multiple gateways; use keys GW1, GW2, etc. for index, gw in enumerate(default_gateways, start=1): key = f"GW{index}" cmd_gw = f"{key} {gw}" print(f"Updating gateway with command: {cmd_gw}") update_response = send_command(ser, cmd_gw) print(f"Response: {update_response}") # Retrieve and update WAN IP address using key "WAN" wan_ip = get_wan_ip() cmd_wan = f"WAN {wan_ip}" print(f"Updating WAN IP with command: {cmd_wan}") update_response = send_command(ser, cmd_wan) print(f"Response: {update_response}") # Close the serial port ser.close() if __name__ == '__main__': main()
server_status_oled_display.1742540909.txt.gz · Last modified: 2025/03/21 07:08 by kenson
