This is an old revision of the document!
Table of Contents
Server Status OLED Display
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 old school project using a ATMEGA32U4.
- 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 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.
Loading the Arduino Bootloader
I'm using a cheap chinese USBasp (with avrdude v8.0)
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.
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)
Then from Arduino, select USBasp in Tools→Programmer and burn using Tools→Burn Bootloader
Flashing the code
Choose “Arduino Micro” in Tools → Board → Arduino AVR Micros Select the right COM port in Tools → Port I use Serial Port Notifier from Helm as a helper function. Select “Arduino as ISP” in Tools → Programmer
Then download the following code and upload it.
Arduino Code: oledusbdisplayaccx1.zip
Controlling the device
When the device boots, it checks the EEPROM for stored key-value pairs and displays them on screen. 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
!LIST which displays the stored key-value pairs
!LIST Key-Value Pairs: Hostname = DESKTOP-PRYPYAT IP1 = 10.10.22.102 IP2 = 10.10.22.61 GW = 10.10.22.1 WAN = 99.29.30.10
!VER reports the devicename and version of firmware
!VER Displayinator v1.0
!NUKE clears all stored key-value pairs from memory and eeprom
!NUKE All key-value pairs cleared.
!ORIENT [0..3] overrides the display orientation, but hasn't been implemented yet
!ORIENT 0 Orientation argument extracted: '0' Orientation set to 0
Everything else is checked to see if it is a Key-Value pair. A pair is just two space delimited alphanumeric words.
HAPPY GILMORE Space index found at: 5 Key extracted: 'HAPPY', Value extracted: 'GILMORE' Added HAPPY = GILMORE
Python Code: displayinatorhostip.zip
Code Tips
- The 32u4 is sloooow by modern standards, so after you send a command, I wait up to half a second before checking for a response.
- 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.
Code Dump
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()

