Files
apress/deploy/uno-q/skills/arduino-uno-q/references/04-bridge-rpc.md
T
Omar SobhandClaude Opus 4.8 d2135a1938 feat(uno-q): ship the arduino-uno-q expert skill on every node by default
Vendors the comprehensive UNO Q skill (SKILL.md + 7 references/*.md) and installs
it into EVERY agent's workspace on each board, so agents know this board's
specifics (dual-brain arch, Bridge/RPC, pin tables, LED matrix + the
ArduinoGraphics-not-installed gotcha) instead of guessing generic Arduino.

Why per-agent workspace: ZeroClaw's read_skill returns only SKILL.md; the agent
reads references/*.md via the workspace-sandboxed file_read tool, so references
are only reachable under ~/.zeroclaw/agents/<alias>/workspace/skills/. A
shared/skills bundle surfaces the skill but its references get sandbox-blocked.

- push-skill.sh installs a SKILL.md+references skill into every agent workspace
  (discovers aliases from the board); provision-fleet runs it per board.
- config.template risk profile now allows + auto-approves read_skill + file_read
  so agents load skills without a human approver (webhook path is non-interactive).
- Flattened the folded 'description: >-' to single-line (ZeroClaw's frontmatter
  parser is a flat scanner, not full YAML).

Verified on board 65301572 with cloud/Sonnet-5: discovered arduino-uno-q →
read_skill(SKILL.md) → file_read references/04-bridge-rpc.md → correct
board-specific answer citing the file.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-16 09:14:00 -07:00

6.7 KiB

UNO Q — The Bridge (Linux ↔ MCU communication)

This is the single most important capability of the board and the thing people get wrong most often. "Connecting the Linux side to the Arduino side" means using the Bridge/RPC over the arduino-router. It never means wiring pins together.

The model: transparent remote procedure calls (RPC)

The two processors exchange data over a dedicated internal serial link. On top of it, a Remote Procedure Call system lets a function on one processor be invoked from the other as if it were a local call. So Python on Linux can call a function that actually runs on the STM32 (and vice-versa), passing arguments and getting results back.

Building blocks:

  • arduino-router — a background Linux systemd service implementing a star-topology network using MessagePack-RPC. Multiple Linux processes can talk to the MCU and to each other through it simultaneously (multipoint). Clients register the functions they offer; the router keeps a directory and routes calls (service discovery).
  • The Bridge library — on the MCU side, Arduino_RouterBridge.h (built on Arduino_RPClite) gives you the Bridge object. On the Linux side, App Lab's arduino.app_utils exposes the matching Bridge in Python.

Source repos (for reference): github.com/arduino/arduino-router and github.com/arduino-libraries/Arduino_RouterBridge.

Reserved resources — DO NOT TOUCH

The router owns the physical link. Never open these in your own code:

  • Linux: /dev/ttyHS1
  • MCU: the Serial1 hardware port

(Opening them yourself breaks the bridge. Note this is distinct from ordinary Serial debug output, which is fine — see the I/O reference.)

The MCU-side API (Bridge)

  • Bridge.begin() — initialize the bridge + serial transport (call in setup(); it can return false if it fails).
  • Bridge.provide(name, fn) — expose an MCU function so Linux can call it. Runs in a high-priority RPC thread.
  • Bridge.provide_safe(name, fn) — same, but the function runs inside the main loop() context, which is safe for using the normal Arduino APIs. Prefer provide_safe when your handler calls Arduino functions (digitalWrite, analogRead, most libraries).
  • Bridge.call(method, args...) — call a Linux-side function and wait for the result.
  • Bridge.notify(method, args...) — fire-and-forget call (no result awaited).
  • Monitor — a predefined object to stream text from Linux to the MCU (via the mon/write RPC method); legacy, Serial is now preferred for console output.
  • Concurrency is handled with Zephyr mutexes (k_mutex); a background thread services incoming updates.

⚠️ Do not call Bridge.call(), Monitor.print(), or Serial.print() inside a provide() handler — the handler runs in the RPC thread and these will misbehave/deadlock. Do the work in the handler, and do any printing/bridge-calling from loop() instead. (This is exactly why provide_safe() exists.)

The Python-side API (App Lab)

from arduino.app_utils import * gives you App and Bridge.

  • Bridge.call("method_name", args...) — invoke a function the MCU registered with provide().
  • App.run(user_loop=loop) — App Lab's main loop runner for the Python program.

Canonical example — Linux drives an MCU-owned pin

The MCU owns LED_BUILTIN; Python decides when to toggle it and calls across the bridge.

Linux side — main.py (runs on the MPU):

from arduino.app_utils import *
import time

led_state = False

def loop():
    global led_state
    time.sleep(1)
    led_state = not led_state
    Bridge.call("set_led_state", led_state)   # invoke the MCU function

App.run(user_loop=loop)

MCU side — sketch.ino (runs on the STM32):

#include "Arduino_RouterBridge.h"

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    Bridge.begin();
    Bridge.provide("set_led_state", set_led_state);  // expose to Linux
}

void loop() {}

void set_led_state(bool state) {
    digitalWrite(LED_BUILTIN, state ? LOW : HIGH);   // built-in LED is active-low
}

That is the whole pattern for controlling anything attached to the board from the Linux/Python side: the sketch owns the pin and provide()s a function; Python call()s it. To go the other direction (MCU reads a sensor and pushes to Linux), have Python provide a function and the MCU Bridge.call() it — or have the MCU notify() events.

Managing the router service (troubleshooting the bridge)

systemctl status arduino-router            # is it running?
sudo systemctl restart arduino-router      # restart it
journalctl -u arduino-router -f            # live logs

Enable verbose logging by appending --verbose to the ExecStart= line in /etc/systemd/system/arduino-router.service:

ExecStart=/usr/bin/arduino-router --unix-port /var/run/arduino-router.sock --serial-port /dev/ttyHS1 --serial-baudrate 115200 --verbose

then:

sudo systemctl daemon-reload
sudo systemctl restart arduino-router
journalctl -u arduino-router -f

Advanced: talk to the router from any language via the Unix socket

The router listens on a Unix domain socket and speaks MessagePack-RPC, so any language (Python, C++, Rust, Go…) can drive the MCU without App Lab:

/var/run/arduino-router.sock

MCU sketch (note provide_safe, since the handler uses digitalWrite):

#include "Arduino_RouterBridge.h"
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Bridge.begin();
  Bridge.provide_safe("set_led_state", set_led_state);
}
void loop() {}
void set_led_state(bool state) { digitalWrite(LED_BUILTIN, state ? LOW : HIGH); }

Raw Python client (no App Lab) — install sudo apt install python3-msgpack:

import socket, msgpack, sys

SOCKET_PATH = "/var/run/arduino-router.sock"
led_state = True
if len(sys.argv) > 1:
    led_state = (sys.argv[1] == "1")

# MessagePack-RPC request: [type=0 (request), msgid, method, params]
request = [0, 1, "set_led_state", [led_state]]
packed_req = msgpack.packb(request)

try:
    with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
        client.connect(SOCKET_PATH)
        client.sendall(packed_req)
        response = msgpack.unpackb(client.recv(1024))
        print(f"Router Response: {response}")
except Exception as e:
    print(f"Connection failed: {e}")
python3 msgpack_test.py 1   # LED ON
python3 msgpack_test.py 0   # LED OFF

Mental checklist when wiring the two sides together

  1. Which side owns the physical resource? → that side provide()s the function.
  2. Does the handler touch Arduino APIs? → use provide_safe().
  3. Never print or call() from inside a provide() handler.
  4. Leave /dev/ttyHS1 and Serial1 alone.
  5. If nothing gets through, check systemctl status arduino-router and the verbose logs first.