Created
February 22, 2026 17:18
-
-
Save parkerlreed/c4285cf5dd74c9b59d565e0d3722dd99 to your computer and use it in GitHub Desktop.
UNO Q IODD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| . /home/arduino/.venv/bin/activate | |
| python3 /home/arduino/uno.py "Unmounted -----" | |
| MEDIA_DIR="/home/arduino/msd_media" | |
| STATE_FILE="/home/arduino/.msd_cycle_last" # stores last served full path | |
| EXTS=("iso" "img") # cycle these extensions | |
| G=/sys/kernel/config/usb_gadget/msd | |
| UDC_NAME="4e00000.usb" | |
| shopt -s nullglob | |
| files=() | |
| for ext in "${EXTS[@]}"; do | |
| while IFS= read -r -d '' f; do files+=("$f"); done < <( | |
| find "$MEDIA_DIR" -maxdepth 1 -type f -iname "*.${ext}" -print0 | sort -z | |
| ) | |
| done | |
| shopt -u nullglob | |
| if (( ${#files[@]} == 0 )); then | |
| echo "No media files found in $MEDIA_DIR for: ${EXTS[*]}" >&2 | |
| exit 1 | |
| fi | |
| last="" | |
| if [[ -f "$STATE_FILE" ]]; then | |
| IFS= read -r last < "$STATE_FILE" || last="" | |
| fi | |
| pick="${files[0]}" | |
| if [[ -n "$last" ]]; then | |
| for i in "${!files[@]}"; do | |
| if [[ "${files[$i]}" == "$last" ]]; then | |
| pick="${files[$(( (i + 1) % ${#files[@]} ))]}" | |
| break | |
| fi | |
| done | |
| fi | |
| printf '%s\n' "$pick" > "$STATE_FILE" | |
| pick_base="$(basename "$pick")" | |
| unbind_udc_owner() { | |
| for udc in /sys/kernel/config/usb_gadget/*/UDC; do | |
| [[ -e "$udc" ]] || continue | |
| current="$(cat "$udc" 2>/dev/null || true)" | |
| if [[ "$current" == "$UDC_NAME" ]]; then | |
| echo "" | sudo tee "$udc" >/dev/null 2>/dev/null || true | |
| fi | |
| done | |
| } | |
| teardown_gadget_dir() { | |
| local gadget_dir="$1" | |
| if [[ -e "$gadget_dir/UDC" ]]; then | |
| echo "" | sudo tee "$gadget_dir/UDC" >/dev/null 2>/dev/null || true | |
| fi | |
| sleep 0.2 | |
| sudo rm -f "$gadget_dir/configs/c.1/mass_storage.usb0" 2>/dev/null || true | |
| sudo rmdir "$gadget_dir/functions/mass_storage.usb0" 2>/dev/null || true | |
| sudo rmdir "$gadget_dir/configs/c.1/strings/0x409" 2>/dev/null || true | |
| sudo rmdir "$gadget_dir/configs/c.1" 2>/dev/null || true | |
| sudo rmdir "$gadget_dir/strings/0x409" 2>/dev/null || true | |
| sudo rmdir "$gadget_dir" 2>/dev/null || true | |
| } | |
| unbind_udc_owner | |
| if [[ -d "$G" ]]; then | |
| teardown_gadget_dir "$G" | |
| fi | |
| sudo modprobe libcomposite | |
| sudo mkdir -p "$G" | |
| cd "$G" | |
| echo 0x1d6b | sudo tee idVendor >/dev/null | |
| echo 0x0104 | sudo tee idProduct >/dev/null | |
| sudo mkdir -p strings/0x409 | |
| echo "msd0001" | sudo tee strings/0x409/serialnumber >/dev/null | |
| echo "Arduino" | sudo tee strings/0x409/manufacturer >/dev/null | |
| echo "File MSD" | sudo tee strings/0x409/product >/dev/null | |
| sudo mkdir -p configs/c.1/strings/0x409 | |
| echo "MSD Config" | sudo tee configs/c.1/strings/0x409/configuration >/dev/null | |
| echo 120 | sudo tee configs/c.1/MaxPower >/dev/null | |
| sudo mkdir -p functions/mass_storage.usb0 | |
| echo 1 | sudo tee functions/mass_storage.usb0/lun.0/removable >/dev/null | |
| echo 0 | sudo tee functions/mass_storage.usb0/lun.0/ro >/dev/null | |
| echo "$pick" | sudo tee functions/mass_storage.usb0/lun.0/file >/dev/null | |
| sudo ln -s functions/mass_storage.usb0 configs/c.1/ | |
| if ! echo "$UDC_NAME" | sudo tee UDC >/dev/null 2>/dev/null; then | |
| unbind_udc_owner | |
| sleep 0.2 | |
| echo "$UDC_NAME" | sudo tee UDC >/dev/null | |
| fi | |
| python3 /home/arduino/uno.py "$pick_base" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <Arduino_LED_Matrix.h> | |
| #include <Arduino_RouterBridge.h> | |
| ArduinoLEDMatrix matrix; | |
| static const int W = 13; | |
| static const int H = 8; | |
| static const uint8_t ON = 7; | |
| static const uint8_t OFF = 0; | |
| static const size_t MAX_LINE = 160; | |
| char lineBuf[MAX_LINE + 1]; | |
| size_t lineLen = 0; | |
| String msg = "UNOQ READY"; | |
| const char* GAP = " "; | |
| const uint16_t STEP_MS = 30; // scroll speed (lower = faster) | |
| unsigned long lastStep = 0; | |
| int scrollPx = 0; | |
| uint8_t pixels[H][W]; | |
| static const uint8_t font5x7[][5] = { | |
| {0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00}, | |
| {0x14,0x7F,0x14,0x7F,0x14},{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62}, | |
| {0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},{0x00,0x1C,0x22,0x41,0x00}, | |
| {0x00,0x41,0x22,0x1C,0x00},{0x14,0x08,0x3E,0x08,0x14},{0x08,0x08,0x3E,0x08,0x08}, | |
| {0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00}, | |
| {0x20,0x10,0x08,0x04,0x02},{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00}, | |
| {0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},{0x18,0x14,0x12,0x7F,0x10}, | |
| {0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03}, | |
| {0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00}, | |
| {0x00,0x56,0x36,0x00,0x00},{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14}, | |
| {0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},{0x32,0x49,0x79,0x41,0x3E}, | |
| {0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22}, | |
| {0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01}, | |
| {0x3E,0x41,0x49,0x49,0x7A},{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00}, | |
| {0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},{0x7F,0x40,0x40,0x40,0x40}, | |
| {0x7F,0x02,0x0C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E}, | |
| {0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46}, | |
| {0x46,0x49,0x49,0x49,0x31},{0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F}, | |
| {0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},{0x63,0x14,0x08,0x14,0x63}, | |
| {0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43},{0x00,0x7F,0x41,0x41,0x00}, | |
| {0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04}, | |
| {0x40,0x40,0x40,0x40,0x40},{0x00,0x03,0x05,0x00,0x00},{0x20,0x54,0x54,0x54,0x78}, | |
| {0x7F,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},{0x38,0x44,0x44,0x48,0x7F}, | |
| {0x38,0x54,0x54,0x54,0x18},{0x08,0x7E,0x09,0x01,0x02},{0x0C,0x52,0x52,0x52,0x3E}, | |
| {0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x44,0x3D,0x00}, | |
| {0x7F,0x10,0x28,0x44,0x00},{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x18,0x04,0x78}, | |
| {0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},{0x7C,0x14,0x14,0x14,0x08}, | |
| {0x08,0x14,0x14,0x18,0x7C},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20}, | |
| {0x04,0x3F,0x44,0x40,0x20},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C}, | |
| {0x3C,0x40,0x30,0x40,0x3C},{0x44,0x28,0x10,0x28,0x44},{0x0C,0x50,0x50,0x50,0x3C}, | |
| {0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},{0x00,0x00,0x7F,0x00,0x00}, | |
| {0x00,0x41,0x36,0x08,0x00},{0x08,0x04,0x08,0x10,0x08} | |
| }; | |
| static inline const uint8_t* glyphFor(char c) { | |
| if (c < 32 || c > 126) c = '?'; | |
| return font5x7[c - 32]; | |
| } | |
| static inline void clearPixels() { | |
| for (int y = 0; y < H; y++) | |
| for (int x = 0; x < W; x++) | |
| pixels[y][x] = OFF; | |
| } | |
| static inline void setPixel(int x, int y, uint8_t v) { | |
| if (x < 0 || x >= W || y < 0 || y >= H) return; | |
| pixels[y][x] = v; | |
| } | |
| int messagePixelWidth() { | |
| return msg.length() * 6; | |
| } | |
| void drawTextAt(int x0, int y0, const String& s) { | |
| int x = x0; | |
| for (int i = 0; i < (int)s.length(); i++) { | |
| const uint8_t* g = glyphFor(s[i]); | |
| for (int col = 0; col < 5; col++) { | |
| uint8_t colBits = g[col]; // bit0 = top | |
| for (int row = 0; row < 7; row++) { | |
| if ((colBits >> row) & 0x01) setPixel(x + col, y0 + row, ON); | |
| } | |
| } | |
| x += 6; | |
| } | |
| } | |
| void rx(String s) { | |
| Monitor.println("MCU got: " + s); | |
| setMessageFromLine(s.c_str()); | |
| } | |
| void setMessageFromLine(const char* line) { | |
| String s(line); | |
| s.trim(); | |
| if (s.length() == 0) return; | |
| msg = s + GAP; | |
| scrollPx = 0; | |
| } | |
| static inline void commitLine() { | |
| lineBuf[lineLen] = '\0'; | |
| setMessageFromLine(lineBuf); | |
| lineLen = 0; | |
| } | |
| void setup() { | |
| matrix.begin(); | |
| matrix.setGrayscaleBits(3); | |
| bool okBridge = Bridge.begin(); | |
| bool okMon = Monitor.begin(); | |
| Bridge.provide_safe("rx", rx); | |
| Monitor.print("Bridge.begin(): "); | |
| Monitor.println(okBridge ? "OK" : "FAIL"); | |
| Monitor.print("Monitor.begin(): "); | |
| Monitor.println(okMon ? "OK" : "FAIL"); | |
| Monitor.println("Type a line and press Enter."); | |
| setMessageFromLine("UNOQ READY"); | |
| } | |
| void loop() { | |
| while (Monitor.available() > 0) { | |
| char c = (char)Monitor.read(); | |
| pixels[0][0] = ON; | |
| matrix.draw(&pixels[0][0]); | |
| if (c == '\n' || c == '\r') { | |
| if (lineLen > 0) commitLine(); | |
| } else { | |
| if (lineLen < MAX_LINE) lineBuf[lineLen++] = c; | |
| } | |
| } | |
| unsigned long now = millis(); | |
| if (now - lastStep >= STEP_MS) { | |
| lastStep = now; | |
| clearPixels(); | |
| int xStart = W - scrollPx; // start just off the right edge | |
| drawTextAt(xStart, 0, msg); | |
| matrix.draw(&pixels[0][0]); | |
| scrollPx++; | |
| int wrapAt = messagePixelWidth() + W; | |
| if (scrollPx >= wrapAt) scrollPx = 0; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import socket | |
| import msgpack | |
| import itertools | |
| import sys | |
| SOCK = "/var/run/arduino-router.sock" | |
| def rpc_call(method, params): | |
| msgid = next(rpc_call._ids) | |
| request = [0, msgid, method, params] | |
| s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
| s.connect(SOCK) | |
| s.sendall(msgpack.packb(request, use_bin_type=True)) | |
| unpacker = msgpack.Unpacker(raw=False) | |
| while True: | |
| chunk = s.recv(4096) | |
| if not chunk: | |
| raise RuntimeError("Socket closed before response") | |
| unpacker.feed(chunk) | |
| for resp in unpacker: | |
| if resp[0] == 1 and resp[1] == msgid: | |
| s.close() | |
| return resp[2], resp[3] | |
| rpc_call._ids = itertools.count(1) | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print("Usage: uno_rpc_send.py \"text to send\"") | |
| sys.exit(1) | |
| text = sys.argv[1] | |
| err, result = rpc_call("rx", [text]) | |
| if err is not None: | |
| print("RPC error:", err, file=sys.stderr) | |
| sys.exit(2) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment