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]>
This commit is contained in:
Omar Sobh
2026-07-16 09:14:00 -07:00
co-authored by Claude Opus 4.8
parent 7233a4b1c0
commit d2135a1938
12 changed files with 877 additions and 2 deletions
@@ -0,0 +1,61 @@
---
name: arduino-uno-q
description: Expert working knowledge of the Arduino UNO Q dual-brain single-board computer (Qualcomm Dragonwing QRB2210 Linux microprocessor + STM32U585 microcontroller). Use this skill whenever the user works with, teaches, prototypes on, or troubleshoots an Arduino UNO Q — any mention of 'UNO Q', App Lab, Bricks, the Bridge/RPC library, the arduino-router, connecting the Linux side to the Arduino/MCU side, running Python plus a sketch together, SSHing into the board, Network Mode, flashing or running apps, wiring or controlling sensors/actuators/LEDs/motors/shields, GPIO/ADC/DAC/PWM/SPI/I2C/Qwiic/UART pins, Wi-Fi or Bluetooth on the board, or preparing a workshop around it. Trigger even when the user doesn't say 'UNO Q' but is clearly on this board. Prefer this over generic Arduino advice — the UNO Q's two-processor architecture makes most single-MCU instructions wrong or incomplete.
---
# Arduino UNO Q
The UNO Q is not a normal Arduino. It is **two computers on one classic UNO-shaped board**, and almost every mistake people make with it comes from forgetting that. Internalize the architecture first; everything else follows from it.
## The one mental model that matters
The board carries two processors that run at the same time:
- **The MPU (the "Linux side")** — a Qualcomm Dragonwing **QRB2210**, quad-core Arm Cortex-A53 @ 2.0 GHz, running a full **Debian Linux** OS. This is where Python, AI/vision, networking, the desktop, SSH, and the **Arduino App Lab** live. Think of it as a Raspberry-Pi-class computer.
- **The MCU (the "Arduino side")** — an **STM32U585**, Arm Cortex-M33 @ 160 MHz, running your Arduino **sketch** on top of **Zephyr OS**. This is where real-time, deterministic pin control lives: GPIO, ADC, DAC, PWM, SPI, I2C, UART. Think of it as a classic Arduino.
They are wired together internally and talk to each other over a dedicated serial link managed by a Linux service called the **`arduino-router`**. The library layer on top of that link is the **Bridge (RPC)** library. **This bridge is the heart of the board** — "connect the Linux side to the Arduino side" always means "use the Bridge / arduino-router," never physical jumper wires.
Decision rule you will use constantly:
- Needs precise timing, or reads/writes a physical pin (a sensor, a motor driver, an LED, a shield)? → **MCU / sketch**.
- Needs Python, the network, a filesystem, heavy compute, a camera, or AI? → **MPU / Linux**.
- Needs both to cooperate (the usual case)? → run code on **each** side and connect them with the **Bridge**.
If you only remember one thing: **you don't control an attached component "from Linux" directly. You write a sketch on the MCU that owns the pin, expose a function over the Bridge, and call it from Python on the Linux side.** The [Bridge reference](references/04-bridge-rpc.md) shows exactly how.
## Two ways to develop, and where the code runs
- **Arduino App Lab** is the primary environment and comes **pre-installed** on the board. An "App" bundles a Python program (Linux/MPU) *and* an Arduino sketch (MCU) together, plus reusable building blocks called **Bricks**. Use App Lab whenever the project uses the Linux side at all (Python, AI, networking) or needs both processors. It can run **on the board itself** (Single-Board-Computer mode, with a monitor/keyboard) or **from your PC** over the network (**Network Mode**).
- **Arduino IDE (beta)** programs **only the MCU (STM32)**. It cannot touch the Qualcomm processor. Use it only for pure-microcontroller sketches when you don't need Linux at all.
So: if a workshop exercise involves Python or the network, it is an App Lab project. If it is "just blink an LED / read a sensor with a sketch," either tool works, but App Lab keeps everything in one place.
## How to help with this board
1. **Locate the work on the right processor(s)** using the decision rule above. State it explicitly to the user — it prevents 80% of confusion.
2. **If the two sides must cooperate, design the Bridge contract first**: which functions does the MCU `provide()`, and what does the Python side `call()`? Then write each side.
3. **For anything physical** (wiring a component, choosing a pin), go to the pin/peripheral reference for exact pin mappings, voltage levels, and library usage — the UNO Q has real gotchas (3.3 V logic, active-low LEDs, reserved `Serial1`/`/dev/ttyHS1`, fixed 500 Hz PWM).
4. **For remote/headless workshop setups**, use SSH + Network Mode + the CLI so students can work from their own laptops.
5. **Verify against the reference files** rather than reciting generic Arduino knowledge from memory — the details on this board are specific and easy to get wrong.
## Reference map — read the file that fits the task
Load these as needed; each is self-contained. Don't dump all of them into context at once — pick the one the current question is about.
- **[references/01-hardware-specs.md](references/01-hardware-specs.md)** — Full architecture, exact chip/model numbers, memory/storage variants, power options, USB-C capabilities, dimensions, onboard UI (8×13 LED matrix, 4 RGB LEDs, power button, debug UART). Read for "what is on the board / what can it do / how do I power it."
- **[references/02-setup-and-os.md](references/02-setup-and-os.md)** — First boot, Debian, installing/using App Lab, Bricks & Apps model, **Linux host udev setup (required for Linux PCs)**, running an App at startup, the `arduino-app-cli`, Arduino IDE core install. Read for "getting started / install / it won't connect / autostart."
- **[references/03-remote-access.md](references/03-remote-access.md)** — **SSH**, Network Mode (mDNS), finding the board's IP, `adb`, copying files with `scp`, running/stopping apps remotely. Read for "access the board remotely / headless / from my laptop / over the network."
- **[references/04-bridge-rpc.md](references/04-bridge-rpc.md)** — **The Linux↔MCU bridge**: the arduino-router service, the Bridge/RPC API, `provide()` vs `provide_safe()`, `call()`/`notify()`, complete Python-side and sketch-side examples, the raw Unix-socket/MessagePack path, and the reserved-resource warnings. Read for "make Python talk to the sketch / call an Arduino function from Linux / control a pin from Python."
- **[references/05-io-and-peripherals.md](references/05-io-and-peripherals.md)** — Every pin table (digital/analog/PWM), ADC/DAC, SPI, I2C, **Qwiic**, UART, and how to actually drive attached components (sensors, actuators, LEDs, shields). Read for "wire up / control / read a component / which pin."
- **[references/06-wireless.md](references/06-wireless.md)** — Wi-Fi (incl. WPA2-Enterprise / eduroam), Bluetooth, and reaching the network from the MCU via the Bridge. Read for "get it online / connect to Wi-Fi / Bluetooth."
- **[references/07-workshop-playbook.md](references/07-workshop-playbook.md)** — Opinionated setup checklist, common failure modes and fixes, and a canonical "Python button → Arduino LED" starter that ties the whole model together. Read when planning or running a hands-on session.
## Quick reference (the things people look up most)
- **Board name over the network:** the hostname you set during setup, reachable as `<name>.local` via mDNS; default Linux user is **`arduino`**.
- **SSH in:** `ssh arduino@<BOARD_IP>` (find IP with `ip addr show` → look under `wlan0`).
- **Router service:** `systemctl status arduino-router` · logs: `journalctl -u arduino-router -f`. **Never** open `/dev/ttyHS1` (Linux) or `Serial1` (MCU) yourself — the router owns them.
- **Bridge socket (advanced):** `/var/run/arduino-router.sock` (MessagePack-RPC).
- **Logic level on the pins:** the MCU I/O is **3.3 V**; the Qwiic connector is 3.3 V only. The onboard RGB LEDs are **active-low**.
- **Serial for debugging:** on the MCU, plain `Serial.print()` shows up in the App Lab console (platform ≥ 0.55.0); `Serial1` is the physical D0/D1 UART.
@@ -0,0 +1,108 @@
# UNO Q — Hardware & Specifications
## Architecture at a glance
The UNO Q combines two processors on one classic UNO form-factor board:
| Role | Chip | Details | Runs |
|---|---|---|---|
| **MPU (Linux side)** | Qualcomm **QRB2210** (Dragonwing) | Quad-core Arm Cortex-A53 @ **2.0 GHz**; Adreno 702 GPU @ 845 MHz; dual ISP up to 25 MP @ 30 fps | **Debian Linux** (full OS, upstream support) |
| **MCU (Arduino side)** | **STM32U585** | Arm Cortex-M33 @ up to **160 MHz**, **2 MB flash**, **786 KB SRAM** | Arduino sketches on **Zephyr OS** |
| **Radio** | **WCBN3536A** | Dual-band **Wi-Fi 5** (2.4/5 GHz) + **Bluetooth 5.1**, onboard antennas | — |
| **Multimedia codec** | **ANX7625** | Video/audio out over the USB-C connector (DisplayPort) | — |
| **PMIC** | Qualcomm **PM4145** | Power management | — |
The two processors are internally linked by a dedicated serial connection managed by the `arduino-router` service (see the Bridge reference). There is no need — and it is a mistake — to bridge them with external wires.
## Memory & storage variants
- **RAM:** 2 GB or 4 GB **LPDDR4**.
- **Storage:** 16 GB or 32 GB **eMMC**.
- Product SKU family: **ABX00162 … ABX00173**.
- Arduino recommends the **4 GB** variant for standalone / single-board-computer use (running the desktop + App Lab on-device). The 2 GB variant is fine when driven from a PC in Network Mode.
## Powering the board
Three supported ways to power it (choose one):
1. **USB-C** cable providing **5 VDC / 3 A** (cable not included). 15 W sink.
2. External **+5 VDC** to the **5V** pin.
3. External **+724 VDC** to the **VIN** pin.
For a standalone desk setup the usual path is a USB-C multiport (dongle) adapter with external power delivery feeding the board while also breaking out HDMI/USB. **Apple's USB-C dongle has been tested and found incompatible** — use a non-Apple multiport adapter.
The board **boots automatically when powered** — you do not press the power button to start it.
## USB-C connector — it does far more than power/programming
| Feature | Capability |
|---|---|
| USB power (sink) | 5 VDC / 3 A (15 W) |
| USB standard | USB 3.1 Gen 1 (5 Gb/s) |
| Display over USB-C | DisplayPort output |
With a USB-C **dongle** you additionally get: **HDMI** video out, **USB camera** input, **audio** (USB or 3.5 mm headset), **Ethernet**, **HID** (keyboard/mouse), and **storage** (microSD/USB drive). This is what makes SBC mode possible.
## Form factor & connectors
- **Classic Arduino UNO form factor** → compatible with the huge existing range of UNO **shields**.
- **Two connector tiers:**
- **UNO-style headers** on top — prototyping/debugging, shield-compatible.
- **High-speed header connectors** on the bottom (JMISC, JDIGITAL, JANALOG, JCTL, etc.) exposing extra digital pins and low-level interfaces.
- Pinout PDF, datasheet, schematics, and STEP files are published on the Arduino docs product page (model ABX00162). Point users there for mechanical/CAD detail.
## Onboard user interface
### 8×13 blue LED matrix (MCU-controlled)
- 104 LEDs (8 rows × 13 cols), driven by the STM32.
- Library: `Arduino_LED_Matrix.h`. Draw a frame from a `uint8_t[104]` array.
- Supports **grayscale/dimming**: `matrix.setGrayscaleBits(bits)` — e.g. `3` → 8 levels (07), `8` → 256 levels (0255).
```cpp
#include <Arduino_LED_Matrix.h>
Arduino_LED_Matrix matrix;
uint8_t frame[104] = { /* 8 rows of 13 values */ };
void setup() {
matrix.begin();
matrix.setGrayscaleBits(1); // 1 bit = on/off
matrix.draw(frame);
}
void loop() {}
```
### 4× RGB LEDs — split across both processors
- **LED 1 & LED 2 → controlled by the MPU (Linux)** via the sysfs LED class at `/sys/class/leds/...`.
- **LED 3 & LED 4 → controlled by the MCU (sketch)** via `digitalWrite()` on named pins.
- **All onboard RGB LEDs are ACTIVE-LOW** — logic `0` / `LOW` turns a segment **ON**.
MPU (Linux) sysfs names:
- LED 1: `red:user`, `green:user`, `blue:user`
- LED 2: `red:panic`, `green:wlan`, `blue:bt`
```bash
echo 1 | tee /sys/class/leds/red:user/brightness # ON
echo 0 | tee /sys/class/leds/red:user/brightness # OFF
```
Or from Python via App Lab's helper:
```python
from arduino.app_utils import App, Leds
Leds.set_led1_color(1, 0, 0) # LED1 red on
Leds.set_led1_color(0, 0, 0) # LED1 off
```
MCU (sketch) pin names: `LED3_R/LED3_G/LED3_B`, `LED4_R/LED4_G/LED4_B` — remember active-low:
```cpp
pinMode(LED3_R, OUTPUT);
digitalWrite(LED3_R, LOW); // red ON (active-low)
```
### Power button
- **Long press (5+ seconds) → reboots the Linux system.**
- Not needed to power on (auto-boots on power).
### Hardware debug UART (system console)
- A dedicated low-level UART on the **JCTL** connector exposes the SoC's main console (bootloader messages + Linux shell login).
- **Parameters: 115200 bps, 1.8 V logic.**
- ⚠️ **1.8 V logic only** — you must use a **1.8 V** USB-to-TTL converter (e.g. DSD Tech SH-U09C5). Using a 3.3 V/5 V adapter can damage the board.
- Use for deep debugging when the board won't boot or the network is unavailable; ordinary work should use SSH instead.
@@ -0,0 +1,96 @@
# UNO Q — Setup, Operating System & App Lab
## The operating systems, restated
- **MPU:** full **Debian Linux** with a desktop. It has a package manager (`apt`), systemd services, users/permissions, a filesystem, networking — treat it like any Debian machine. Arduino **App Lab** is pre-installed.
- **MCU:** **Zephyr OS** running your Arduino sketch. You rarely interact with Zephyr directly; you write ordinary Arduino code and App Lab / the IDE handles the rest.
## Two development environments
### Arduino App Lab (primary)
- **Pre-installed on the board.** Also installable on your PC (Windows/macOS/Linux) from the Arduino software page for Network-Mode development.
- An **App** = a Python program (runs on Linux/MPU) **+** an Arduino sketch (runs on MCU) **+ Bricks**.
- **Bricks** are reusable, pre-packaged building blocks (e.g. vision, audio, I/O helpers) you compose into an App — this is how App Lab gets you to AI/vision features quickly without writing everything from scratch.
- Runs two ways:
- **Single-Board-Computer (SBC) mode** — App Lab runs *on the board* with a monitor/keyboard/mouse attached via the dongle.
- **Network Mode** — App Lab runs on your PC and talks to the board over the LAN (see remote-access reference).
### Arduino IDE (beta) — MCU only
- Programs **only the STM32 microcontroller**. It **cannot** program the Qualcomm processor. For anything touching Linux/Python, use App Lab.
- Install the core: **Tools → Board → Boards Manager → search "UNO Q" → install "Arduino UNO Q Zephyr Core."**
- If it doesn't appear, add this to **File → Preferences → Additional Boards Manager URLs**:
```
https://downloads.arduino.cc/packages/package_zephyr_index.json
```
- Then select **Tools → Board → Arduino UNO Q Board**, pick the **Port**, and upload as usual. The classic `File → Examples → 01.Basics → Blink` works (`LED_BUILTIN` = the red channel of the built-in RGB LED).
## First boot as a Single-Board Computer
1. Plug the USB-C dongle into the board's USB-C port.
2. Keyboard + mouse into the dongle's USB-A ports; monitor into the dongle's HDMI; power supply (**+5 VDC / 3 A min**) into the dongle.
3. It boots automatically. At the Debian login the default user is **`arduino`**; you set a password.
4. App Lab's first-run wizard walks through keyboard layout, **board name**, **Wi-Fi**, and login credentials. The board name becomes the mDNS hostname (`<name>.local`) used for Network Mode and SSH.
The preloaded desktop includes Chromium, a file manager, a terminal, Vim, and media playback. Install more software the normal Debian way, e.g. `sudo apt install vlc`.
## Linux host setup (REQUIRED when driving the board from a Linux PC)
On a Linux PC, App Lab talks to the board over USB and needs udev permissions, or **it fails silently** and flashing throws permission errors. The board presents two USB identities:
- **Operating mode:** VID `2341`, PID `0078`
- **Emergency Download (EDL) mode:** VID `05c6`, PID `9008`
### Install the udev rules (canonical one-liner)
```bash
echo \
'# Operating mode
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="0078", MODE="0660", TAG+="uaccess"
# EDL mode
SUBSYSTEMS=="usb", ATTRS{idVendor}=="05c6", ATTRS{idProduct}=="9008", MODE="0660", TAG+="uaccess"' \
| sudo tee "/etc/udev/rules.d/60-Arduino-UNO-Q.rules" \
&& sudo udevadm control --reload-rules \
&& sudo udevadm trigger
```
`MODE="0660"` sets the device-file permissions; `TAG+="uaccess"` grants the logged-in user access via systemd-logind.
### Verify
```bash
cat /etc/udev/rules.d/60-Arduino-UNO-Q.rules
lsusb | grep -E "2341:0078|05c6:9008"
adb devices # board should be listed
adb shell # drops you into a shell on the board
```
### Apply
**Disconnect and reconnect the board** (wait a few seconds), then restart App Lab so it re-reads permissions.
### Alternative: official post-install script
From the ArduinoCore-zephyr repo — creates `/etc/udev/rules.d/60-arduino-zephyr.rules`:
```bash
cd ~/Downloads
wget https://raw.githubusercontent.com/arduino/ArduinoCore-zephyr/main/post_install.sh
chmod +x post_install.sh
sudo ./post_install.sh
```
(or `curl -O <same URL>`, or `git clone https://github.com/arduino/ArduinoCore-zephyr.git` then run `post_install.sh`). Reconnect the board afterward.
## Hello World (Blink) to confirm the toolchain
In App Lab the Examples section opens on launch → open **Blink LED** → **Run**. Success = the **red** channel of the built-in RGB LED blinks 1 s on / 1 s off. That LED is driven by the STM32 via the sketch, so a successful blink proves the MCU side is programmable.
## Running an App at startup (kiosk / standalone projects)
- You **cannot** set a built-in *Example* as the startup app directly — first **copy it to a new App** or create your own.
- In the App: click the **▼** arrow next to the **Run** button (top-right) → toggle **Run at startup** ON. A **DEFAULT** badge confirms it.
- CLI equivalent:
```bash
arduino-app-cli properties set default user:<NAME_OF_YOUR_APP>
```
## The App Lab CLI (`arduino-app-cli`)
Runs on the board (use it over SSH for headless control):
```bash
arduino-app-cli app start ~/ArduinoApps/<project-name> # start an app
arduino-app-cli app logs ~/ArduinoApps/<project-name> # stream its logs
arduino-app-cli app stop ~/ArduinoApps/<project-name> # stop it
arduino-app-cli properties set default user:<app-name> # set autostart app
```
Apps live under `~/ArduinoApps/` on the board.
@@ -0,0 +1,67 @@
# UNO Q — Remote & Headless Access
The Linux side is a real networked computer, so you can work with the board without a monitor. Three complementary paths: **Network Mode** (App Lab GUI over LAN), **SSH** (terminal), and **`adb`** (over USB). For a workshop where students use their own laptops, combine Network Mode for the GUI and SSH for the terminal.
## Prerequisite: get the board on the network
The board must be on Wi-Fi (or Ethernet via dongle) and you need either its **IP** or its **mDNS hostname**. SSH is **enabled automatically** during the App Lab first-run setup once you configure a username, password, and Wi-Fi.
## Find the board's IP address
Open a terminal on the board (App Lab bottom-left `>_` icon, or a desktop terminal) and run:
```bash
ip addr show
```
Look under the **`wlan0`** interface (or `eth0` if using Ethernet) for the `inet` address.
## Network Mode (App Lab over the LAN)
- Run App Lab on your PC; boards discovered on the local network appear tagged **"Network."** Select yours and enter credentials — you now develop from your PC as if local.
- Discovery uses **mDNS** (Bonjour/zeroconf). The board advertises `<board-name>.local`.
- **Firewall/OS notes:**
- Allow **UDP port 5353** (mDNS) through the firewall.
- On Windows, approve the `mdns-discovery.exe` prompt from Windows Defender.
- Reaching the board directly by browser, SSH, or raw IP does **not** guarantee it shows up in Network Mode — mDNS must be working for the App Lab discovery.
## SSH (terminal access)
Default Linux user is **`arduino`**. Connect with either the IP or the mDNS name:
```bash
ssh arduino@<BOARD_IP> # e.g. ssh [email protected]
ssh arduino@<board-name>.local # if mDNS resolves on your network
```
On first connect, accept the host-key fingerprint (`yes`) and enter the board password you set during setup.
For passwordless login, install your key the standard way:
```bash
ssh-copy-id arduino@<BOARD_IP>
```
(or append your public key to `~/.ssh/authorized_keys` on the board). After that, `ssh arduino@<BOARD_IP>` won't prompt for a password.
Once in over SSH you have the full board: `apt`, `systemctl` (e.g. manage `arduino-router`), the `arduino-app-cli`, the LED sysfs, etc.
## Copy files to the board
Use `scp` (or `rsync`) from your PC. Apps live in `~/ArduinoApps/` on the board:
```bash
mkdir -p ~/ArduinoApps/<project-name> # (run on the board)
scp -r * arduino@<BOARD_IP>:~/ArduinoApps/<project-name> # (run on your PC, from the project dir)
```
Then start/stop it remotely with the CLI (see setup reference):
```bash
arduino-app-cli app start ~/ArduinoApps/<project-name>
arduino-app-cli app logs ~/ArduinoApps/<project-name>
arduino-app-cli app stop ~/ArduinoApps/<project-name>
```
## `adb` (over USB, no network needed)
With the Linux host udev rules installed (see setup reference), the board also speaks `adb`:
```bash
adb devices # confirm the board is attached
adb shell # shell into the board over USB
```
Handy when Wi-Fi isn't configured yet, for first-time provisioning, or for recovery.
## Hardware serial console (last resort)
If the board won't boot or has no network, the **JCTL** debug UART gives the raw SoC console at **115200 bps, 1.8 V logic** (use a 1.8 V USB-TTL adapter — see hardware reference). It shows bootloader/kernel messages and offers a login prompt with your Linux credentials.
## Picking the right one
- **GUI development from your laptop** → Network Mode.
- **Scripting, service management, autostart, file transfer, headless control** → SSH (+ `scp`, `arduino-app-cli`).
- **No network yet / provisioning / recovery** → `adb` over USB.
- **Won't boot / diagnosing early startup** → 1.8 V hardware debug UART.
@@ -0,0 +1,145 @@
# 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):**
```python
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):**
```cpp
#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)
```bash
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:
```bash
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`):
```cpp
#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`:
```python
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}")
```
```bash
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.
@@ -0,0 +1,143 @@
# UNO Q — Pins, Buses & Controlling Attached Components
**All physical I/O belongs to the MCU (STM32).** You control attached components by writing an Arduino sketch that owns the pin/bus. If Linux/Python needs to be involved, expose the control as a Bridge function and call it from Python (see the Bridge reference). MCU I/O is **3.3 V logic**.
## Digital pins
47 digital pins total (22 on the UNO-style header, 25 on the JMISC connector). UNO-style header mapping:
| MCU pin | Arduino | Function |
|---|---|---|
| PB7 | D0 / RX | GPIO / UART RX |
| PB6 | D1 / TX | GPIO / UART TX |
| PB3 | D2 | GPIO |
| PB0 | D3 | GPIO / OPAMP OUT / **PWM** |
| PA12 | D4 / FDCAN1_TX | GPIO / CAN TX |
| PA11 | D5 / FDCAN1_RX | GPIO / CAN RX / **PWM** |
| PB1 | D6 | GPIO / **PWM** |
| PB2 | D7 | GPIO |
| PB4 | D8 | GPIO |
| PB8 | D9 | GPIO / **PWM** |
| PB9 | D10 / SS | GPIO / SPI SS / **PWM** |
| PB15 | D11 / MOSI | GPIO / SPI MOSI / **PWM** |
| PB14 | D12 / MISO | GPIO / SPI MISO |
| PB13 | D13 / SCK | GPIO / SPI SCK |
| PA4 | D14 / DAC0 | GPIO / ADC / DAC |
| PA5 | D15 / DAC1 | GPIO / ADC / DAC |
| PA6 | D16 | GPIO / ADC / OPAMP IN+ |
| PA7 | D17 | GPIO / ADC / OPAMP IN- |
| PC1 | D18 / SDA2 | GPIO / ADC / I2C SDA |
| PC0 | D19 / SCL2 | GPIO / ADC / I2C SCL |
| PB11 | D20 / SDA | GPIO / I2C SDA |
| PB10 | D21 / SCL | GPIO / I2C SCL |
Usage:
```cpp
pinMode(pin, INPUT | OUTPUT | INPUT_PULLUP);
state = digitalRead(pin);
digitalWrite(pin, HIGH | LOW);
```
Button-reads-input, LED-follows example: `pinMode(btn, INPUT_PULLUP); if (digitalRead(btn)==LOW) digitalWrite(led,HIGH);`
## Analog input (ADC) — JANALOG connector
6 channels, **14-bit** ADC.
| MCU pin | Arduino | Function |
|---|---|---|
| PA4 | A0 | ADC / DAC |
| PA5 | A1 | ADC / DAC |
| PA6 | A2 | ADC / OPAMP IN+ |
| PA7 | A3 | ADC / OPAMP IN- |
| PC1 | A4 | ADC / I2C SDA |
| PC0 | A5 | ADC / I2C SCL |
```cpp
analogReadResolution(14); // 016383
analogReference(AR_INTERNAL2V5); // set V_REF+ (see table)
int v = analogRead(A0);
```
Voltage-reference options: `AR_INTERNAL1V5` (1.5 V), `AR_INTERNAL1V8` (1.8 V), `AR_INTERNAL2V05` (2.048 V), `AR_INTERNAL2V5` (2.5 V), `AR_EXTERNAL` (2 V…VDD, external).
## Analog output (DAC)
Two true DAC outputs.
| MCU pin | Arduino |
|---|---|
| PA4 | DAC0 |
| PA5 | DAC1 |
```cpp
analogWriteResolution(12); // 04095
analogWrite(DAC0, value);
```
(Good for generating waveforms — the docs include a 60 Hz sine via a 256-point LUT clocked with `micros()`.)
## PWM
6 PWM-capable pins: **D3, D5, D6, D9, D10, D11**. Default resolution **8-bit (0255)**; change with `analogWriteResolution(bits)`. **PWM frequency is fixed at 500 Hz.**
```cpp
analogWriteResolution(10); // 01023
analogWrite(D3, dutyValue);
```
## SPI
| MCU pin | Arduino |
|---|---|
| PB9 | SS / D10 |
| PB15 | MOSI / D11 |
| PB14 | MISO / D12 |
| PB13 | SCK / D13 |
```cpp
#include <SPI.h>
#define SS D10
void setup(){ pinMode(SS,OUTPUT); digitalWrite(SS,HIGH); SPI.begin(); }
void loop(){
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(SS, LOW);
SPI.transfer(0x35); SPI.transfer(0xFA);
digitalWrite(SS, HIGH);
SPI.endTransaction();
}
```
## I2C — two buses
| Bus / object | SCL | SDA | Where |
|---|---|---|---|
| `Wire` | D21 (PB10) | D20 (PB11) | UNO-style headers |
| `Wire1` | I2C4_SCL (PD12) | I2C4_SDA (PD13) | **Qwiic** connector |
```cpp
#include <Wire.h>
void setup(){ Wire.begin(); } // or Wire1.begin() for Qwiic
// Wire.beginTransmission(addr); Wire.write(...); Wire.endTransmission();
```
### Qwiic connector
Plug-and-play I²C: polarized connector, daisy-chainable, built-in pull-ups, **3.3 V only** — no breadboard/soldering. Use `Wire1`. Works directly with Arduino's **Modulino** sensor/actuator family for solder-free prototyping.
## UART (hardware serial on the header)
| MCU pin | Arduino |
|---|---|
| PB6 | USART1_TX / D1 |
| PB7 | USART1_RX / D0 |
Use the **`Serial1`** object for the physical D0/D1 pins on the JDIGITAL connector:
```cpp
Serial1.begin(115200);
Serial1.println("Hello UNO Q");
while (Serial1.available()) { char c = Serial1.read(); /* ... */ }
```
> Note: `Serial1` here is the *header UART*. It is **not** the reserved router link — that reserved `Serial1` warning in the Bridge doc refers to the internal MPU↔MCU port; on the exposed headers `Serial1` drives D0/D1. Don't confuse the two: use `Serial1` for external UART devices, and never touch the internal router transport.
## `Serial` for debugging → App Lab console
Since platform **0.55.0**, plain `Serial` prints stream to the **Arduino App Lab Console** — use it like normal:
```cpp
void setup(){ Serial.begin(9600); }
void loop(){ Serial.println("Hello UNO Q"); delay(1000); }
```
The legacy `Monitor` object (`#include <Arduino_RouterBridge.h>``Monitor.begin()/println()`) still works for backward compatibility, but **use `Serial` for new projects.**
## How to actually control "our node's" attached hardware — recipe
1. Identify the interface the component uses (GPIO / ADC / PWM / SPI / I2C / UART) and the matching pins above. Mind **3.3 V** levels.
2. Write the **sketch** that drives it (the standard Arduino libraries work: `Servo`, `Wire`-based sensor libs, etc.).
3. If Python/AI/networking must control or read it, wrap each action in a `Bridge.provide_safe("name", fn)` on the MCU and `Bridge.call("name", ...)` from Python (see Bridge reference).
4. For quick prototyping of sensors/actuators, reach for **Qwiic + Modulino** modules to skip wiring.
@@ -0,0 +1,89 @@
# UNO Q — Wireless (Wi-Fi & Bluetooth)
The **WCBN3536A** module gives dual-band **Wi-Fi 5 (2.4/5 GHz)** and **Bluetooth 5.1**, with onboard antennas. The radio is owned by the **Linux/MPU side**, so networking is configured in Debian (via `nmcli` or the desktop). The MCU reaches the network **through the Bridge**, not directly.
## Wi-Fi from Linux (the normal way)
Use the network icon in the top-right of the desktop, or NetworkManager on the CLI (great over SSH):
```bash
sudo nmcli d wifi connect <SSID> password <YOUR_PASSWORD> # connect
sudo nmcli d disconnect wlan0 # disconnect
sudo nmcli connection delete <SSID> # forget a network
nmcli d wifi list # scan (handy)
```
## WPA2-Enterprise (university / corporate, e.g. eduroam)
NetworkManager handles 802.1X. **eduroam (PEAP/MSCHAPv2):**
```bash
nmcli con add \
type wifi \
connection.id Eduroam \
wifi.ssid eduroam \
wifi.mode infrastructure \
wifi-sec.key-mgmt wpa-eap \
802-1x.eap peap \
802-1x.phase2-auth mschapv2 \
802-1x.identity <your identity>
```
**TTLS + PAP variant:**
```bash
nmcli con add \
type wifi \
connection.id ExampleNetwork \
wifi.ssid <your Wi-Fi SSID> \
wifi.mode infrastructure \
wifi-sec.key-mgmt wpa-eap \
802-1x.eap ttls \
802-1x.phase2-auth pap \
802-1x.domain-suffix-match example.com \
802-1x.identity <your identity>
```
Bring a connection up and be prompted for the password interactively:
```bash
nmcli --ask con up <your network name>
```
This matters for workshops on campus/enterprise networks where a plain SSID+password won't work.
## Reaching the network from the MCU (via the Bridge)
The sketch doesn't have its own IP stack for Wi-Fi; it borrows the Linux side's connectivity through the Bridge. App Lab provides bridged network client classes, e.g. a TCP client:
```cpp
BridgeTCPClient<> client(Bridge);
void setup() {
if (!Bridge.begin()) { while (true) {} }
Serial.begin(9600);
}
void loop() {
if (client.connect("time.nist.gov", 13) < 0) { // Daytime protocol
Serial.println("Connection failed!");
delay(5000); return;
}
String line;
while (client.connected() || client.available()) {
if (client.available()) {
char c = client.read();
if (c == '\n') break;
if (c != '\r') line += c;
}
}
Serial.print("Server says: "); Serial.println(line);
client.stop();
delay(10000);
}
```
Takeaway: get the board online **on the Linux side first**, then the MCU can make network calls over the Bridge.
## Bluetooth (Linux side)
Manage from the top-right Bluetooth icon in SBC mode, or via CLI:
```bash
bluetoothctl power on
bluetoothctl power off
```
Inside the `bluetoothctl` prompt:
```
power on
scan on # discover devices
scan off
connect <MAC_ADDRESS> # pair/connect
```
Use it to link phones, computers, or BT sensors to the board.
@@ -0,0 +1,74 @@
# UNO Q — Workshop Playbook
Practical guidance for running a hands-on session with the board. Assumes the reader already understands the two-processor model (see SKILL.md).
## Pre-flight checklist (per station)
- [ ] Board + a **known-good USB-C cable** rated for **5 V / 3 A** (or a multiport dongle with external PD — **not Apple's**, it's incompatible).
- [ ] For SBC stations: HDMI monitor, USB keyboard/mouse via dongle, power to the dongle.
- [ ] Decide the mode up front: **SBC** (self-contained, needs 4 GB variant recommended) vs **Network Mode** (students drive it from their laptops).
- [ ] Each board: set a **board name** (becomes `<name>.local`), a password, and Wi-Fi during first-run. SSH turns on automatically after this.
- [ ] If any facilitator laptop runs **Linux**, install the **udev rules** first (see setup reference) — otherwise App Lab connects silently to nothing.
- [ ] Network allows **mDNS / UDP 5353** (needed for Network Mode discovery). On locked-down campus Wi-Fi this is the usual blocker.
- [ ] For enterprise/eduroam Wi-Fi, pre-stage the `nmcli` 802.1X command (see wireless reference).
## Common failure modes → fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| App Lab shows no board (Linux PC, USB) | Missing udev rules | Install rules, reconnect board, restart App Lab |
| Board absent in Network Mode | mDNS blocked | Open UDP 5353; approve `mdns-discovery.exe` on Windows; confirm same subnet |
| Flash/upload "permission denied" (Linux) | udev not applied | Re-run rules, `udevadm control --reload-rules && udevadm trigger`, replug |
| Python can't reach the sketch | router down / wrong function name | `systemctl status arduino-router`; check the `provide()` name matches the `call()` string; verbose logs |
| Handler crashes / hangs | Arduino API called from `provide()` (RPC thread) | Use `provide_safe()`; never `Serial.print`/`Bridge.call` inside a handler |
| Nothing on a pin | wrong logic level or wrong processor | MCU I/O is 3.3 V; onboard RGB LEDs are active-low; confirm the sketch (not Python) owns the pin |
| `Serial1` acting weird | confusing header UART with router link | `Serial1` on D0/D1 is fine for external UART; never open the internal reserved transport / `/dev/ttyHS1` |
| Can't SSH | no IP / SSH not set up | `ip addr show` (wlan0) on the board; SSH is enabled once first-run setup completes |
## The canonical teaching example: "Python button decides, Arduino acts"
This one App ties the entire board together and is the best first exercise because it forces students to touch **both** processors and the **Bridge**.
**Concept:** the sketch owns an LED (a physical pin); Python owns the *logic* and calls across the Bridge to actuate it. Swap the LED for a relay/motor driver and you have real device control; swap the Python timer for a camera/AI Brick and you have an intelligent actuator.
**MCU sketch (`sketch.ino`):**
```cpp
#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); // built-in LED active-low
}
```
**Linux Python (`main.py`):**
```python
from arduino.app_utils import *
import time
state = False
def loop():
global state
time.sleep(1)
state = not state
Bridge.call("set_led_state", state)
App.run(user_loop=loop)
```
**Teaching beats:**
1. Run it → LED blinks, but emphasize *the decision happened in Python on Linux, the action happened on the MCU.*
2. Break it on purpose: rename `"set_led_state"` on one side → show the router log error → fix. Cements the RPC contract idea.
3. Extend: have Python read something real (time of day, a web request, a camera Brick) and gate the LED on it.
4. Reverse it: MCU reads a real sensor and `Bridge.call()`s a Python function that logs/uploads it.
## Progression for a full session
1. **Blink** (App Lab example) — proves the MCU toolchain.
2. **Read a sensor in a sketch**, print with `Serial` → App Lab console — proves I/O + debugging.
3. **Bridge example above** — proves Linux↔MCU.
4. **Add a Brick** (vision/audio) on the Python side — shows the payoff of the Linux half.
5. **Deploy:** set the App to **Run at startup** and unplug the keyboard → it's now an appliance. Manage it over **SSH** + `arduino-app-cli`.
## Deploying student projects headlessly
- Copy the App to the board: `scp -r * arduino@<IP>:~/ArduinoApps/<name>`.
- Start/stop/log via `arduino-app-cli app start|stop|logs ~/ArduinoApps/<name>`.
- Autostart the finished one: `arduino-app-cli properties set default user:<name>`.