Skip to content

Instantly share code, notes, and snippets.

@parkerlreed
Created February 22, 2026 17:18
Show Gist options
  • Select an option

  • Save parkerlreed/c4285cf5dd74c9b59d565e0d3722dd99 to your computer and use it in GitHub Desktop.

Select an option

Save parkerlreed/c4285cf5dd74c9b59d565e0d3722dd99 to your computer and use it in GitHub Desktop.
UNO Q IODD
#!/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"
#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;
}
}
#!/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