feat(uno-q): resident-responder fast-path for the LED matrix (instant, no flash)
The agent now drives the matrix in <1s via the resident responder instead of compiling+flashing a sketch (~95s). Pieces: - matrix-relay/: a tiny container (python-apps-base) running relay.py — a TCP:9999 → RouterBridge relay. The ZeroClaw daemon runs on the host for hardware access, but the RouterBridge python binding (arduino.app_utils) only ships in the App Lab image, so JUST the relay runs containerized, mounting the router socket + publishing :9999 to loopback. - config.template.toml: add matrix_pattern + matrix_text to the default risk profile's allowed_tools/auto_approve (the capability filter hides tools not listed — this was why matrix_text wasn't exposed to the agent). - skills/led-matrix: lead with "use matrix_text / matrix_pattern first"; only flash for custom frames (flashing overwrites the responder). NOTE skills load from each agent's workspace copy, not shared/skills — push to all workspaces. - provision-host-daemon.sh: flash the responder once + start the relay. Proven on-hardware: "show a heart" and "scroll GO CLAWS" both instant via the agent, no flash. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1eb28c6e5e
commit
06f2342892
@@ -82,8 +82,8 @@ prompt_injection_mode = "compact"
|
|||||||
# without a human approver (the webhook path is non-interactive).
|
# without a human approver (the webhook path is non-interactive).
|
||||||
[risk_profiles.default]
|
[risk_profiles.default]
|
||||||
level = "supervised"
|
level = "supervised"
|
||||||
allowed_tools = ["uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
|
allowed_tools = ["matrix_pattern", "matrix_text", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
|
||||||
auto_approve = ["uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
|
auto_approve = ["matrix_pattern", "matrix_text", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
|
||||||
|
|
||||||
[runtime_profiles.unoq]
|
[runtime_profiles.unoq]
|
||||||
agentic = true
|
agentic = true
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# matrix-relay — a tiny TCP:9999 → RouterBridge relay for the resident MCU sketch.
|
||||||
|
#
|
||||||
|
# The ZeroClaw daemon runs on the HOST (for full hardware access), but its
|
||||||
|
# matrix_pattern / matrix_text tools speak a simple line protocol to :9999, and
|
||||||
|
# the actual MCU is reached via the Arduino RouterBridge (msgpack-rpc over
|
||||||
|
# /run/arduino-router.sock) whose python binding (arduino.app_utils) only ships
|
||||||
|
# in the App Lab container image. So we run JUST this relay in a minimal
|
||||||
|
# container that mounts the router socket and publishes :9999 to the host.
|
||||||
|
#
|
||||||
|
# Protocol (one line per connection):
|
||||||
|
# ping -> pong
|
||||||
|
# matrix <0-7> -> Bridge.call("matrix_set", id) preset animation
|
||||||
|
# text <words...> -> Bridge.call("matrix_text", str) scroll text
|
||||||
|
# gpio_write <p> <v>-> Bridge.call("digitalWrite", p, v)
|
||||||
|
# gpio_read <p> -> Bridge.call("digitalRead", p) -> value
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
from arduino.app_utils import Bridge # RouterBridge client (container-only binding)
|
||||||
|
|
||||||
|
PORT = 9999
|
||||||
|
|
||||||
|
|
||||||
|
def handle(conn):
|
||||||
|
try:
|
||||||
|
data = conn.recv(256).decode().strip()
|
||||||
|
parts = data.split()
|
||||||
|
cmd = parts[0].lower() if parts else ""
|
||||||
|
if cmd == "ping":
|
||||||
|
conn.sendall(b"pong\n")
|
||||||
|
elif cmd == "matrix" and len(parts) >= 2:
|
||||||
|
Bridge.call("matrix_set", int(parts[1]))
|
||||||
|
conn.sendall(b"ok\n")
|
||||||
|
elif cmd == "text" and len(parts) >= 2:
|
||||||
|
Bridge.call("matrix_text", " ".join(parts[1:]))
|
||||||
|
conn.sendall(b"ok\n")
|
||||||
|
elif cmd == "gpio_write" and len(parts) >= 3:
|
||||||
|
Bridge.call("digitalWrite", int(parts[1]), int(parts[2]))
|
||||||
|
conn.sendall(b"ok\n")
|
||||||
|
elif cmd == "gpio_read" and len(parts) >= 2:
|
||||||
|
conn.sendall(f"{Bridge.call('digitalRead', int(parts[1]))}\n".encode())
|
||||||
|
else:
|
||||||
|
conn.sendall(b"error: invalid command\n")
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
conn.sendall(f"error: {e}\n".encode())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(("0.0.0.0", PORT))
|
||||||
|
s.listen(5)
|
||||||
|
print(f"matrix-relay listening on :{PORT}", flush=True)
|
||||||
|
while True:
|
||||||
|
conn, _ = s.accept()
|
||||||
|
threading.Thread(target=handle, args=(conn,), daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# run-relay.sh — (re)start the matrix-relay container on the Uno Q host.
|
||||||
|
# Runs the TCP:9999 → RouterBridge relay in a minimal container (the RouterBridge
|
||||||
|
# python binding only ships in the App Lab image). Mounts the router socket,
|
||||||
|
# publishes :9999 to loopback for the host ZeroClaw daemon's matrix tools.
|
||||||
|
set -eu
|
||||||
|
IMAGE="ghcr.io/arduino/app-bricks/python-apps-base:0.11.0"
|
||||||
|
RELAY="${RELAY:-/home/arduino/matrix-relay/relay.py}"
|
||||||
|
docker rm -f matrix-relay >/dev/null 2>&1 || true
|
||||||
|
docker run -d --name matrix-relay --restart unless-stopped \
|
||||||
|
-v /run/arduino-router.sock:/var/run/arduino-router.sock \
|
||||||
|
-v "$RELAY":/relay.py:ro \
|
||||||
|
-p 127.0.0.1:9999:9999 \
|
||||||
|
--entrypoint python3 \
|
||||||
|
"$IMAGE" /relay.py
|
||||||
|
echo "matrix-relay started (:9999 → RouterBridge)"
|
||||||
@@ -53,6 +53,27 @@ echo "→ Arduino flashing prerequisite (the Zephyr core hard-requires this libr
|
|||||||
S "HOME=/home/arduino arduino-cli lib install Arduino_RouterBridge 2>&1 | tail -1"
|
S "HOME=/home/arduino arduino-cli lib install Arduino_RouterBridge 2>&1 | tail -1"
|
||||||
ok "Arduino_RouterBridge installed"
|
ok "Arduino_RouterBridge installed"
|
||||||
|
|
||||||
|
echo "→ flash the resident matrix responder (enables instant matrix_text / matrix_pattern)"
|
||||||
|
# The responder sketch provides matrix_set/matrix_text over RouterBridge, so the
|
||||||
|
# agent can drive the matrix in <1s instead of compiling+flashing (~95s) — and
|
||||||
|
# without overwriting the MCU each time. One-time flash here.
|
||||||
|
RESPONDER_SKETCH="${RESPONDER_SKETCH:-$HOME/projects/zeroclaw/firmware/zeroclaw-node/sketch/sketch.ino}"
|
||||||
|
if [ -f "$RESPONDER_SKETCH" ]; then
|
||||||
|
S "rm -rf /tmp/responder && mkdir -p /tmp/responder/responder"
|
||||||
|
adb -s "$SERIAL" push "$RESPONDER_SKETCH" /tmp/responder/responder/responder.ino >/dev/null
|
||||||
|
S "HOME=/home/arduino arduino-cli compile --upload -b arduino:zephyr:unoq /tmp/responder/responder >/dev/null 2>&1 && echo flashed || echo 'responder flash failed'" | grep -q flashed \
|
||||||
|
&& ok "matrix responder flashed to the MCU" || bad "responder flash failed (matrix tools will need it)"
|
||||||
|
else
|
||||||
|
bad "responder sketch not found ($RESPONDER_SKETCH) — skipping (set RESPONDER_SKETCH)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ start the matrix relay container (:9999 → RouterBridge; the RouterBridge"
|
||||||
|
echo " python binding only ships in the App Lab image, so run JUST the relay there)"
|
||||||
|
S "mkdir -p /home/arduino/matrix-relay"
|
||||||
|
adb -s "$SERIAL" push "$(dirname "$0")/matrix-relay/relay.py" /home/arduino/matrix-relay/relay.py >/dev/null
|
||||||
|
S "docker rm -f matrix-relay >/dev/null 2>&1; docker run -d --name matrix-relay --restart unless-stopped -v /run/arduino-router.sock:/var/run/arduino-router.sock -v /home/arduino/matrix-relay/relay.py:/relay.py:ro -p 127.0.0.1:9999:9999 --entrypoint python3 ghcr.io/arduino/app-bricks/python-apps-base:0.11.0 /relay.py >/dev/null 2>&1 && echo up"
|
||||||
|
ok "matrix relay running on :9999"
|
||||||
|
|
||||||
echo "→ hardware device perms — udev rule so the agent can read I2C/SPI sensors"
|
echo "→ hardware device perms — udev rule so the agent can read I2C/SPI sensors"
|
||||||
SU "printf 'KERNEL==\"i2c-[0-9]*\", MODE=\"0666\"\nKERNEL==\"spidev[0-9]*\", MODE=\"0666\"\n' > /etc/udev/rules.d/99-apess-hw.rules; udevadm control --reload; udevadm trigger --subsystem-match=i2c-dev >/dev/null 2>&1"
|
SU "printf 'KERNEL==\"i2c-[0-9]*\", MODE=\"0666\"\nKERNEL==\"spidev[0-9]*\", MODE=\"0666\"\n' > /etc/udev/rules.d/99-apess-hw.rules; udevadm control --reload; udevadm trigger --subsystem-match=i2c-dev >/dev/null 2>&1"
|
||||||
ok "i2c/spi readable by the daemon user"
|
ok "i2c/spi readable by the daemon user"
|
||||||
|
|||||||
@@ -8,6 +8,24 @@ description: Drive the Arduino Uno Q's built-in 13x8 blue LED matrix — draw fr
|
|||||||
The Uno Q has a built-in **13 columns × 8 rows** (104-pixel) **blue** LED matrix,
|
The Uno Q has a built-in **13 columns × 8 rows** (104-pixel) **blue** LED matrix,
|
||||||
driven by the STM32U585 MCU. (It is NOT an LCD, and NOT red.)
|
driven by the STM32U585 MCU. (It is NOT an LCD, and NOT red.)
|
||||||
|
|
||||||
|
## Instant runtime control — USE THIS FIRST (no flashing)
|
||||||
|
|
||||||
|
The board runs a **resident responder sketch** that already drives the matrix, so
|
||||||
|
you can change what it shows **instantly** with these tools — no sketch, no
|
||||||
|
compile, no flash:
|
||||||
|
|
||||||
|
- **`matrix_text`** — scroll a short message. Use for ANY "scroll / show / display
|
||||||
|
/ print `<text>`" request (e.g. `text="GO CLAWS"`).
|
||||||
|
- **`matrix_pattern`** — switch to a preset animation: `off, rain, heart, wave,
|
||||||
|
sparkle, checker, solid, blink`.
|
||||||
|
|
||||||
|
**Always reach for these tools first** for text or a preset animation. They take
|
||||||
|
effect in under a second. Do **NOT** write and flash a sketch for these — flashing
|
||||||
|
takes ~90s **and overwrites the resident responder, breaking `matrix_text` /
|
||||||
|
`matrix_pattern` until it's re-flashed.** Only write + flash a sketch (below) for a
|
||||||
|
**custom** frame pattern the tools can't produce, and know it replaces the
|
||||||
|
responder.
|
||||||
|
|
||||||
## Prefer the frame API (always available)
|
## Prefer the frame API (always available)
|
||||||
|
|
||||||
`Arduino_LED_Matrix` is bundled with the `arduino:zephyr` core — **no lib install**.
|
`Arduino_LED_Matrix` is bundled with the `arduino:zephyr` core — **no lib install**.
|
||||||
|
|||||||
Reference in New Issue
Block a user