Arduino UNO Q · Full Python on Debian Linux

Python for
Arduino UNO Q

# Full CPython 3 on Debian Linux — not MicroPython
# The MPU runs your Python · the MCU runs C++ sketches
# They talk to each other through the Bridge API

Brain #1 — MPU (Your Python Lives Here)
Qualcomm QRB2210
  • OS Debian Linux
  • CPU Quad-Core ARM A53 @ 2 GHz
  • RAM 2 GB / 4 GB LPDDR4X
  • Language Full Python 3 + any Linux app
  • Libraries flask, numpy, opencv, requests…
Bridge RPC
arduinoio
Brain #2 — MCU (C++ Sketches)
STM32U585
  • OS Zephyr RTOS + Arduino Core
  • Pins D0–D13, A0–A5 (12-bit ADC)
  • PWM D3, D5, D6, D9, D10, D11
  • Language C++ (Arduino sketches)
  • Matrix 8×13 LED + 4 RGB LEDs
# Table of Contents
01

The Dual-Brain Architecture

The Arduino UNO Q is not a traditional microcontroller board. It contains two completely separate processors. Understanding which one runs what is the most important concept on this board.

Python = MPU Side

Your .py files run on the Qualcomm chip under Debian Linux. Full CPython 3 — all standard library modules available. pip install anything.

C++ = MCU Side

Arduino sketches (.ino) run on the STM32 chip. Controls all physical GPIO pins. Handles interrupt-driven hardware.

Bridge API

The arduinoio library lets Python call named handler functions registered in the C++ sketch — and read their return values.

IDE: App Lab

Arduino App Lab manages both sides in one project. Single click deploys Python to MPU and C++ to MCU simultaneously.

⚠ Critical: Python Cannot Touch GPIO Directly

Unlike MicroPython on the R4, Python on the UNO Q runs on Linux and has no direct access to the Arduino pins. You cannot call digitalWrite(13) from Python. All physical pin control must go through a C++ sketch on the MCU, which you reach via bridge.call("handler_name"). This is the fundamental pattern for all hardware interaction.

💡 What Python CAN Do Directly

On the MPU side, Python has full access to: the 8×13 LED matrix and 4 RGB LEDs (via arduinoio.LEDMatrix), Wi-Fi networking, file system, USB, HDMI, and the Qwiic I²C bus (via arduinoio). Hardware pins (A0–A5, D0–D13) still require the Bridge.

Architecture Overviewconceptual
# ┌─────────────────────────────────────────────────────────────────┐
# │                     Python (MPU / Debian Linux)                 │
# │   app/main.py            app/dashboard.py      app/ai.py        │
# │   from arduinoio import Bridge                                  │
# │   bridge.call("set_led", "1")  ←──── you call C++ by name       │
# │   val = bridge.call("read_adc")  ←── you read MCU return values  │
# └──────────────────────┬──────────────────────────────────────────┘
#                   Bridge RPC (USB-internal serial)
# ┌──────────────────────┴──────────────────────────────────────────┐
# │                   C++ Sketch (MCU / Zephyr)                     │
# │   #include                                            │
# │   void set_led(BridgeClient c) { digitalWrite(13,c.read()=='1');}│
# │   void read_adc(BridgeClient c){ c.print(analogRead(A0)); }     │
# │   void loop() { server.process(); }  ← MUST be in loop()        │
# └─────────────────────────────────────────────────────────────────┘

02

Program Structure

Python uses indentation (4 spaces) to define code blocks — no curly braces, no semicolons. A UNO Q App Lab project has two folders: app/ (Python, runs on MPU) and sketch/ (C++, runs on MCU).

Pythonapp/main.py ← MPU side
# ── 1. Imports ───────────────────────────────────────────────────
from arduinoio import Bridge, LEDMatrix   # UNO Q built-in library
import time                               # standard library (full CPython!)
import json
from pathlib import Path
from datetime import datetime

# ── 2. Constants (UPPER_CASE by convention) ───────────────────────
LED_PIN    = 13       # pin on MCU — referenced in C++ sketch
ADC_PIN    = "A0"    # analog pin string
BLINK_RATE = 0.5     # seconds

# ── 3. Setup Bridge (connects Python MPU → C++ MCU) ──────────────
bridge = Bridge()
bridge.begin()         # MUST call this first — opens the RPC channel

matrix = LEDMatrix()   # direct MPU access, no Bridge needed
matrix.begin()

# ── 4. Helper functions ───────────────────────────────────────────
def set_led(state: bool) -> None:
    """Turn pin 13 LED on or off via Bridge call to MCU."""
    bridge.call("set_led", "1" if state else "0")

def read_voltage() -> float:
    """Read A0 voltage from MCU via Bridge."""
    raw = bridge.call("read_adc").strip()
    return float(raw) * 5.0 / 4095       # 12-bit ADC on STM32

# ── 5. Main program ───────────────────────────────────────────────
def main() -> None:
    print("UNO Q Python ready!")

    while True:                # the main loop
        set_led(True)
        time.sleep(BLINK_RATE)
        set_led(False)
        time.sleep(BLINK_RATE)

# ── 6. Script guard ───────────────────────────────────────────────
if __name__ == "__main__":
    main()
C++sketch/sketch.ino ← MCU side (companion sketch)
// The MCU sketch exposes "handlers" that Python can call.
// This C++ side must ALWAYS be present — Python can't work alone.
#include <Bridge.h>
#include <BridgeServer.h>
#include <BridgeClient.h>

BridgeServer server;

// Handler: called by Python's bridge.call("set_led", "0"/"1")
void set_led(BridgeClient client) {
  String val = client.readStringUntil('\n');
  digitalWrite(13, val == "1" ? HIGH : LOW);
}

// Handler: called by Python's bridge.call("read_adc")
void read_adc(BridgeClient client) {
  analogReadResolution(12);          // 12-bit = 0–4095
  client.print(analogRead(A0));
}

void setup() {
  Bridge.begin();
  server.begin();
  server.addHandler("set_led",  set_led);
  server.addHandler("read_adc", read_adc);
  pinMode(13, OUTPUT);
}

void loop() {
  server.process();   // CRITICAL — processes Bridge RPC calls from Python
  // millis()-based non-blocking timing only — NO delay() here!
}
📌 Key Difference from MicroPython (R4)

On the Uno R4, Python (MicroPython) controls pins directly. On the UNO Q, Python runs on a Linux computer that is separate from the Arduino pins. Every hardware interaction requires a Bridge call. This extra step is the price for having a full Linux computer with Wi-Fi, 2 GB RAM, and the entire Python ecosystem.


03

Data Types & Variables

Python is dynamically typed — no type declarations needed. It is strongly typed — it won't silently coerce types ("1" + 1 raises TypeError). On the UNO Q, you have full Python 3 with all standard numeric precision.

TypeExampleNotes for UNO Q
intx = 42Arbitrary precision — no overflow like C++
floatv = 3.14Full 64-bit IEEE 754 (unlike MicroPython's 32-bit)
boolflag = TrueSubclass of int. True==1, False==0
strs = "hello"Immutable. Bridge.call() params must be strings
bytesb = b"\x00\xFF"Immutable byte buffer — used in I²C/SPI data
listpins = [9, 10, 11]Mutable ordered sequence
tuplepos = (x, y)Immutable — use for config, RGB colours, coords
dictcfg = {"baud": 9600}Hash map — ideal for sensor data, JSON payloads
setmodes = {"blink", "fade"}Unordered unique values — fast membership test
Noneval = NonePython's null. Check with is None
complexz = 3+4jAvailable (unlike MicroPython) — rarely needed in hardware
Pythontypes_vars.py
# ── Declaration (no type keyword needed) ─────────────────────────
name       = "UNO Q"
pin_count  = 13
adc_ref    = 5.0           # full 64-bit float on Linux
is_running = True
nothing    = None

# Multiple assignment
x, y, z = 1, 2, 3         # tuple unpacking
a = b = c = 0              # chained assignment

# ── f-strings — the preferred way to format ──────────────────────
print(f"Board: {name}, Pins: {pin_count}")
print(f"Voltage: {adc_ref:.3f} V")     # format spec: 3 decimal places
print(f"ADC raw: {0x0FFF}")             # hex literal = 4095

# ── Type introspection ────────────────────────────────────────────
print(type(42))               # <class 'int'>
print(isinstance(42, int))    # True — preferred over type() ==

# ── Type conversion ───────────────────────────────────────────────
raw_str = bridge.call("read_adc").strip()  # Bridge always returns str
raw_int = int(raw_str)                       # convert to int
voltage = raw_int * 5.0 / 4095              # convert to float volts

# ── Truthiness — falsy values ─────────────────────────────────────
# None, False, 0, 0.0, "", [], {}, () → ALL falsy
# Everything else → truthy
if raw_str:                   # True only if non-empty string
    print("got response")
if val is not None:          # explicit None check — preferred
    print("has value")

# ── Type hints (Python 3.5+) — docs, not enforcement ─────────────
def read_temp(pin: str = "A0") -> float:
    raw: int = int(bridge.call("read_adc", pin).strip())
    return raw * 5.0 / 4095    # 12-bit ADC on STM32

# ── String methods ────────────────────────────────────────────────
response = "  3.14\n  "
response.strip()              # "3.14" — remove whitespace
"hello".upper()               # "HELLO"
"a,b,c".split(",")           # ['a', 'b', 'c']
",".join(["a", "b"])          # "a,b"
int("42")                      # 42
float("3.14")                  # 3.14

04

Operators

CategoryOperatorsExampleNotes
Arithmetic+ - * / // % **10 // 3 → 3/ always float. // floor div. ** power. No ++ or --.
Augmented assign+= -= *= /= //= **= %=x += 1Use instead of x++
Comparison== != < > <= >=x == 10Returns bool. Chainable: 0 <= x <= 100
Logicaland or nota and not bEnglish words, not && || !
Identityis is notx is NoneObject identity — use only for None/True/False
Membershipin not in"A0" in VALID_PINSWorks on lists, dicts, sets, strings
Bitwise& | ^ ~ << >>reg &= ~(1 << 3)Identical to C++ — useful for protocol flags
Ternaryx if c else y"on" if flag else "off"Inline if/else expression
Walrus:=if (n := len(data)) > 0:Python 3.8+ — assign and test in one expression
Pythonoperators.py
# ── No ++ operator! Use += 1 ─────────────────────────────────────
count = 0
count += 1       # NOT count++

# ── Division always returns float ────────────────────────────────
print(10 / 3)    # 3.3333…  — always float
print(10 // 3)   # 3        — floor division
print(2 ** 8)    # 256      — exponentiation
print(10 % 3)    # 1        — modulo (same as C++)

# ── Logic uses WORDS, not symbols ────────────────────────────────
x, y = 5, 10
if x > 0 and y < 20:    # not &&
    print("valid range")
if not is_running:       # not !
    print("stopped")

# ── Chained comparison (very Pythonic) ───────────────────────────
raw = 2048
if 0 <= raw <= 4095:     # same as raw >= 0 and raw <= 4095
    print("valid 12-bit ADC reading")

# ── Ternary expression ────────────────────────────────────────────
led_cmd = "1" if is_on else "0"
bridge.call("set_led", led_cmd)

# ── Bitwise (for protocol / flag manipulation) ───────────────────
status_byte = 0b10110000
status_byte |=  (1 << 2)   # set bit 2
status_byte &= ~(1 << 7)   # clear bit 7

# ── Walrus operator — read and check in one step ─────────────────
if (response := bridge.call("read_adc").strip()):
    voltage = float(response) * 5.0 / 4095

05

Control Flow

Pythoncontrol_flow.py
# ── IF / ELIF / ELSE ─────────────────────────────────────────────
voltage = 2.5
if voltage > 4.0:
    print("High")
elif voltage > 2.0:         # elif — NOT else if
    print("Medium")
else:
    print("Low")

# ── MATCH / CASE (Python 3.10+) — like switch/case ───────────────
mode = "blink"
match mode:
    case "blink":  bridge.call("set_led", "1")
    case "fade":   bridge.call("set_pwm", "128")
    case "off":    bridge.call("set_led", "0")
    case _:        print(f"Unknown mode: {mode}")  # default

# ── FOR LOOP — iterates directly over any iterable ───────────────
sensors = ["A0", "A1", "A2"]
for pin in sensors:          # no index needed
    val = bridge.call("read_adc", pin).strip()
    print(f"{pin}: {val}")

for i in range(10):           # 0–9 (like C for(i=0;i<10;i++))
    print(i)
for i in range(0, 256, 16): # 0, 16, 32, ..., 240
    bridge.call("set_pwm", str(i))

# enumerate() — get index AND value (Pythonic, no range(len()))
for i, pin in enumerate(sensors):
    print(f"Sensor {i} on pin {pin}")

# for…else — else runs only if loop DIDN'T break
for pin in sensors:
    if bridge.call("read_adc", pin).strip() == "0":
        print(f"{pin} is zero")
        break
else:
    print("All sensors have readings")

# ── WHILE LOOP ───────────────────────────────────────────────────
count = 0
while count < 5:
    count += 1

# ── NON-BLOCKING TIMING — time.time() equivalent of millis() ─────
import time
last_read = time.time()     # float seconds since epoch
INTERVAL  = 1.0            # seconds

while True:
    now = time.time()
    if now - last_read >= INTERVAL:
        last_read = now
        v = read_voltage()
        print(f"V = {v:.3f}")  # periodic read — non-blocking
    # other code runs here every iteration

# ── BREAK / CONTINUE / PASS ──────────────────────────────────────
for i in range(10):
    if i == 5:  break       # exit loop
    if i % 2: continue      # skip odd
    print(i)

def not_yet():
    pass                    # no-op placeholder (like {} in C++)

06

Functions

Functions in Python are first-class objects — they can be stored in variables, passed as arguments, and returned from other functions. This is used extensively for things like callbacks, dispatch tables, and the Bridge command pattern.

Pythonfunctions.py
# ── Basic function ────────────────────────────────────────────────
def add(a: int, b: int) -> int:
    """Docstring: describes the function. Type hints are documentation."""
    return a + b

# ── Default parameters ────────────────────────────────────────────
def read_sensor(pin: str = "A0", retries: int = 3) -> float:
    for _ in range(retries):
        raw = bridge.call("read_adc", pin).strip()
        if raw:
            return float(raw) * 5.0 / 4095
    return 0.0

read_sensor()                  # uses defaults: pin="A0", retries=3
read_sensor("A1", retries=5) # keyword argument — order irrelevant

# ── Multiple return values (as a tuple) ──────────────────────────
def read_all_sensors():
    temp  = read_sensor("A0")
    light = read_sensor("A1")
    return temp, light              # returns a tuple

t, l = read_all_sensors()        # unpack immediately

# ── *args — variadic positional args (received as tuple) ─────────
def send_commands(*commands: str):
    for cmd in commands:
        bridge.call(cmd)

send_commands("blink", "read_adc", "reset")

# ── **kwargs — variadic keyword args (received as dict) ──────────
def configure_bridge(**opts):
    for key, val in opts.items():
        bridge.call(key, str(val))

configure_bridge(set_led=1, set_pwm=128, set_freq=1000)

# ── Lambda — anonymous single-expression function ─────────────────
to_volts    = lambda raw: raw * 5.0 / 4095
to_pct      = lambda raw: int(raw / 4095 * 100)

readings    = [0, 1024, 2048, 4095]
voltages    = list(map(to_volts, readings))   # apply function to each
valid_reads = list(filter(lambda v: v > 0.1, voltages))  # filter

# ── Closure — inner function captures outer variable ──────────────
def make_adc_reader(ref_voltage: float):
    def read(raw: int) -> float:
        return raw * ref_voltage / 4095  # ref_voltage captured
    return read

read_5v = make_adc_reader(5.0)   # closure baked with 5V
read_3v = make_adc_reader(3.3)   # closure baked with 3.3V

# ── Generator — lazy sequence (memory-efficient) ─────────────────
def scan_sensors(pins: list):
    """Lazily yield (pin, voltage) tuples — only reads when iterated."""
    for pin in pins:
        raw = bridge.call("read_adc", pin).strip()
        yield pin, float(raw) * 5.0 / 4095  # pauses here each iteration

for pin, v in scan_sensors(["A0", "A1", "A2"]):
    print(f"{pin}: {v:.3f} V")

07

Classes & OOP

Pythonbridge_controller.py
from arduinoio import Bridge

# ── Class definition ─────────────────────────────────────────────
class BridgeController:
    """Wraps the arduinoio Bridge with a clean, typed API."""

    # Class variable — shared by all instances
    _instance_count: int = 0

    def __init__(self, ref_voltage: float = 5.0):
        """Constructor — sets up Bridge connection."""
        self._bridge    = Bridge()      # _prefix = private by convention
        self._ref_v     = ref_voltage
        self._connected = False
        BridgeController._instance_count += 1

    # ── Regular methods ──────────────────────────────────────────
    def begin(self) -> None:
        """Open the Bridge connection. Must be called first."""
        self._bridge.begin()
        self._connected = True
        print(f"Bridge connected (ref={self._ref_v}V)")

    def set_led(self, state: bool) -> None:
        self._require_connected()
        self._bridge.call("set_led", "1" if state else "0")

    def read_adc(self, pin: str = "A0") -> float:
        self._require_connected()
        raw = self._bridge.call("read_adc", pin).strip()
        return float(raw) * self._ref_v / 4095

    def set_pwm(self, pin: int, duty: int) -> None:
        duty = max(0, min(255, duty))      # clamp 0–255
        self._bridge.call("set_pwm", f"{pin}:{duty}")

    def _require_connected(self) -> None:    # private helper
        if not self._connected:
            raise RuntimeError("Call begin() first")

    # ── Property — computed / validated attribute ─────────────────
    @property
    def ref_voltage(self) -> float:
        return self._ref_v

    @ref_voltage.setter
    def ref_voltage(self, value: float) -> None:
        if not (0 < value <= 5.5):
            raise ValueError(f"Invalid ref voltage: {value}")
        self._ref_v = value

    # ── Dunder (magic) methods ────────────────────────────────────
    def __repr__(self) -> str:     # repr(obj)
        return f"BridgeController(ref={self._ref_v}V, connected={self._connected})"

    def __str__(self) -> str:      # str(obj) / print(obj)
        status = "✓ online" if self._connected else "✗ offline"
        return f"Bridge [{status}] @ {self._ref_v}V"

    def __bool__(self) -> bool:    # if ctrl:
        return self._connected

    # ── Class and static methods ──────────────────────────────────
    @classmethod
    def count(cls) -> int:           # cls = the class itself
        return cls._instance_count

    @staticmethod
    def raw_to_volts(raw: int, ref: float = 5.0) -> float:
        return raw * ref / 4095      # no self or cls needed


# ── Inheritance ───────────────────────────────────────────────────
class LoggingController(BridgeController):
    """Extends BridgeController with automatic CSV logging."""

    def __init__(self, log_path: str = "sensor_log.csv"):
        super().__init__(5.0)          # call parent __init__
        self._log_path = log_path
        self._readings: list[dict] = []

    def read_and_log(self, pin: str = "A0") -> float:
        from datetime import datetime
        v = super().read_adc(pin)      # call parent method
        self._readings.append({
            "time": datetime.now().isoformat(),
            "pin":  pin,
            "v":    round(v, 4)
        })
        return v


# ── Usage ─────────────────────────────────────────────────────────
ctrl = BridgeController(5.0)
ctrl.begin()
ctrl.set_led(True)
ctrl.ref_voltage = 3.3           # uses setter
print(ctrl)                       # uses __str__
print(BridgeController.count())   # class method

08

Dataclasses & Decorators

Two powerful Python features that reduce boilerplate: dataclasses auto-generate __init__, __repr__, and __eq__ from field annotations. Decorators wrap functions to extend their behaviour without modifying them.

Pythonsensor_models.py
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import functools

# ── DATACLASS — auto-generates __init__, __repr__, __eq__ ─────────
@dataclass
class SensorReading:
    """Represents one sensor measurement from the MCU."""
    pin:       str
    voltage:   float
    raw:       int
    unit:      str  = "V"              # optional with default
    timestamp: str  = field(
        default_factory=lambda: datetime.now().isoformat()
    )
    # field(default_factory=...) prevents the classic mutable-default gotcha
    tags:      list = field(default_factory=list)

    def to_dict(self) -> dict:
        return vars(self)         # vars() returns __dict__ of instance

    @classmethod
    def from_dict(cls, d: dict):
        return cls(**d)            # ** unpacks dict as keyword args

# Automatically gets __init__, __repr__, __eq__, __hash__:
r = SensorReading(pin="A0", voltage=2.5, raw=2048)
print(r)              # SensorReading(pin='A0', voltage=2.5, raw=2048, ...)
print(r.voltage)      # 2.5


# ── DECORATORS ───────────────────────────────────────────────────
# A decorator is a function that takes a function and returns a new one.
# @syntax is shorthand: @log_errors above def f  ==  f = log_errors(f)

def log_errors(func):
    """Decorator: catch and print exceptions cleanly."""
    @functools.wraps(func)    # preserves __name__ and __doc__
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except (ValueError, RuntimeError) as e:
            print(f"[ERROR] {func.__name__}: {e}")
            return None
    return wrapper

def retry(times: int = 3, delay: float = 0.1):
    """Parameterised decorator: retry a function on exception."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            import time
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == times - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

# ── Applying decorators to functions ─────────────────────────────
@log_errors
def parse_reading(raw_str: str) -> float:
    return float(raw_str.strip()) * 5.0 / 4095

@retry(times=3, delay=0.05)
def reliable_read(pin: str) -> str:
    return bridge.call("read_adc", pin)

09

Collections

Pythoncollections.py
from collections import defaultdict, Counter, deque

# ══ LIST — mutable ordered sequence ═════════════════════════════
pins = ["A0", "A1", "A2"]
pins.append("A3")            # add to end
pins.insert(0, "A4")         # insert at index
pins.pop()                    # remove and return last
pins.remove("A4")            # remove by value
pins.sort()                   # in-place sort
pins[0]                       # first element
pins[-1]                      # last element
pins[1:3]                     # slice
len(pins)                     # length

# ══ TUPLE — immutable — use for fixed config ════════════════════
RGB_RED   = (255, 0,   0)      # constant colour — never changes
ADC_RANGE = (0, 4095)         # min, max
r, g, b   = RGB_RED            # tuple unpacking
first, *rest = (1, 2, 3, 4)  # star unpacking: first=1, rest=[2,3,4]

# ══ DICT — key-value map ════════════════════════════════════════
config = {
    "baud":      9600,
    "adc_bits":  12,
    "ref_v":     5.0,
    "pins":      ["A0", "A1"],
}
config["baud"]              # 9600 — KeyError if missing
config.get("baud")          # 9600 — None if missing (safe!)
config.get("x", 0)          # 0    — default if missing
config["mode"] = "blink"    # add / update key
del config["baud"]          # delete key
"baud" in config            # True — key membership test
for k, v in config.items():
    print(f"  {k}: {v}")

# Dict merge (Python 3.9+)
defaults   = {"ref_v": 5.0, "bits": 12}
overrides  = {"ref_v": 3.3}
final      = defaults | overrides  # {'ref_v': 3.3, 'bits': 12}

# ══ SET — unordered, unique values ══════════════════════════════
VALID_MODES = {"blink", "chase", "pulse", "off"}
active_pins = {"A0", "A1"}
active_pins.add("A2")
"blink" in VALID_MODES     # True — O(1) lookup
all_pins  = {"A0", "A1", "A2", "A3"}
idle_pins = all_pins - active_pins   # {'A3'}

# ══ COLLECTIONS MODULE ══════════════════════════════════════════
# defaultdict — auto-creates missing keys with a default factory
readings = defaultdict(list)
readings["A0"].append(2.5)   # no KeyError on first access
readings["A1"].append(1.2)

# Counter — count occurrences
modes_used = Counter(["blink", "blink", "chase", "blink", "off"])
modes_used.most_common(2)     # [('blink', 3), ('chase', 1)]

# deque — double-ended queue, O(1) append/pop from both ends
log = deque(maxlen=100)       # rolling window of last 100 readings
log.append(2.5)               # oldest auto-discarded when full

10

Comprehensions & Generators

Pythoncomprehensions.py
# Syntax: [expression  for item in iterable  if condition]

# ── LIST comprehension ────────────────────────────────────────────
squares    = [x**2 for x in range(10)]
pwm_levels = [int(i * 255 / 9) for i in range(10)]   # 0-255 in 10 steps
valid_v    = [v for v in voltages if v > 0.1]         # filter

# Build SensorReading objects from raw pin list
PINS = ["A0", "A1", "A2"]
raws = [int(bridge.call("read_adc", p).strip()) for p in PINS]

# ── DICT comprehension ────────────────────────────────────────────
pin_voltages = {p: float(bridge.call("read_adc", p).strip()) * 5.0/4095
                for p in PINS}
# {'A0': 2.5, 'A1': 1.2, 'A2': 0.9}

inverted = {v: k for k, v in config.items()}   # flip keys/values

# ── SET comprehension ─────────────────────────────────────────────
unique_modes = {entry["mode"] for entry in event_log}

# ── GENERATOR EXPRESSION — lazy, no list built in RAM ────────────
# Use () instead of []. Values computed only when consumed.
total = sum(float(bridge.call("read_adc", p).strip()) for p in PINS)
avg   = total / len(PINS)

# ── zip() — iterate multiple lists together ───────────────────────
labels   = ["Temp", "Light", "Pot"]
readings = [2.5, 0.9, 3.3]
for label, v in zip(labels, readings):
    print(f"{label}: {v:.3f}V")

# ── Sorting with key= ─────────────────────────────────────────────
sensors_list = [
    {"pin": "A2", "v": 3.1},
    {"pin": "A0", "v": 1.2},
    {"pin": "A1", "v": 2.5},
]
sorted(sensors_list, key=lambda s: s["v"])  # sort by voltage

# ── map() / filter() ──────────────────────────────────────────────
doubled = list(map(lambda v: v * 2, readings))
high_v  = list(filter(lambda v: v > 2.0, readings))

11

Exception Handling

Pythonexceptions.py
# ── try / except / else / finally ────────────────────────────────
try:
    raw = bridge.call("read_adc").strip()
    voltage = float(raw) * 5.0 / 4095
except ValueError as e:             # float("") raises ValueError
    print(f"Bad ADC value: {e}")
    voltage = 0.0
except (ConnectionError, OSError) as e:   # catch multiple
    print(f"Bridge error: {e}")
    raise                           # re-raise the original exception
except Exception as e:             # catch-all (use sparingly)
    print(f"Unexpected: {e}")
else:
    print("Read OK")               # runs ONLY if no exception raised
finally:
    print("Always runs")           # cleanup: close files, etc.

# ── Custom exception class ────────────────────────────────────────
class BridgeError(Exception):
    def __init__(self, msg: str, handler: str = ""):
        super().__init__(msg)
        self.handler = handler

raise BridgeError("No response", handler="read_adc")

# ── Context manager (with statement) — auto-cleanup ───────────────
# Used for files, locks, connections — even custom objects
with open("sensor_log.csv", "a") as f:
    f.write(f"{datetime.now().isoformat()},{voltage:.4f}\n")
    # file auto-closed when block exits, even on exception

# ── Common exceptions on UNO Q ────────────────────────────────────
# ValueError    — bad string conversion: float("") or int("abc")
# OSError       — file not found, Bridge disconnect, I/O error
# ConnectionError — Wi-Fi/network failure
# KeyError      — missing dict key
# IndexError    — list index out of range
# RuntimeError  — custom logical error (e.g. bridge not started)
# KeyboardInterrupt — Ctrl+C in terminal — always catch in main loop

# ── Safe bridge call pattern ──────────────────────────────────────
def safe_call(handler: str, *args, default=None):
    try:
        return bridge.call(handler, *args).strip()
    except Exception as e:
        print(f"[WARN] {handler} failed: {e}")
        return default

12

File I/O & JSON

Because the UNO Q runs Debian Linux with 16–32 GB of eMMC storage, Python can read and write files freely. This is a major advantage over bare-metal microcontrollers like the R3/R4.

Pythonfile_io.py
import json
from pathlib import Path
from datetime import datetime

# ── Text files — always use 'with' context manager ───────────────
with open("data.txt", "w") as f:   # "w" = overwrite, "a" = append
    f.write("Hello UNO Q\n")

with open("data.txt", "r") as f:
    content = f.read()              # whole file as string

with open("log.csv", "a", encoding="utf-8") as f:
    f.write(f"{datetime.now().isoformat()},{2.5:.4f}\n")

# ── JSON — perfect for config and sensor logs ─────────────────────
config = {"ref_v": 5.0, "sample_rate": 10, "pins": ["A0", "A1"]}

# Serialize to JSON file
with open("config.json", "w") as f:
    json.dump(config, f, indent=2)

# Parse from JSON file
with open("config.json") as f:
    loaded = json.load(f)

# In-memory JSON strings
text   = json.dumps(config)          # dict → string
parsed = json.loads(text)            # string → dict

# ── pathlib — modern object-oriented file paths ───────────────────
p = Path("logs")                     # relative path
p.mkdir(parents=True, exist_ok=True) # create directory if needed

log_file = p / "sensor.csv"          # / operator joins paths
log_file.exists()                    # bool
log_file.suffix                      # '.csv'
log_file.stem                        # 'sensor'
log_file.parent                      # Path('logs')

log_file.write_text("time,voltage\n")   # shorthand
text = log_file.read_text()              # shorthand

list(p.glob("*.csv"))               # all .csv files in directory

# ── Config file pattern — load or create with defaults ───────────
CONFIG_FILE = Path("config.json")
DEFAULTS    = {"ref_v": 5.0, "rate": 1.0}

if CONFIG_FILE.exists():
    with CONFIG_FILE.open() as f:
        cfg = json.load(f)
else:
    cfg = DEFAULTS.copy()
    with CONFIG_FILE.open("w") as f:
        json.dump(cfg, f, indent=2)

13

Modules & Standard Library

ModulePurposeUNO Q Use Case
timetime(), sleep(), monotonic()Non-blocking timing, delays, timestamps
datetimeDate and time objectsTimestamps on sensor logs, CSV headers
jsonJSON encode / decodeConfig files, sensor log payloads, Bridge data
pathlibPath object — file systemConfig files on eMMC storage
collectionsCounter, defaultdict, dequeSensor event counting, rolling buffers
dataclassesAuto-generated data classesSensor reading models, config structs
functoolswraps, lru_cache, partial, reduceDecorators, memoised sensor conversions
threadingThread, Lock, EventRun Flask server + sensor loop concurrently
socketTCP/UDP networkingRaw socket server for telemetry
subprocessRun shell commandsInterface with Linux CLI tools on Debian
reRegular expressionsParse serial/bridge response strings
structPack/unpack binary dataI²C / SPI protocol frames
mathsqrt, sin, cos, pi, logSensor calibration, signal processing
statisticsmean, median, stdevSmooth noisy ADC readings
csvRead/write CSV filesSensor data export
requests*HTTP client (pip install)Send sensor data to web API, webhooks
flask*Web framework (pip install)Host a dashboard at arduino-uno-q.local
arduinoioBridge to MCU (built-in)GPIO, LED matrix, Qwiic sensors

14

The Bridge API (arduinoio)

The arduinoio library is the heart of UNO Q Python development. It is the only way Python can interact with physical Arduino pins.

Python (MPU side)What It DoesC++ handler (MCU side)
bridge = Bridge()Create bridge object
bridge.begin()Open RPC connection to MCUBridge.begin() in setup()
bridge.call("name")Call named handler, returns strvoid name(BridgeClient c)
bridge.call("name", "arg")Call handler with argument stringc.readStringUntil('\n')
bridge.get("key")Get a key/value stored on MCUBridge.put("key", "val")
bridge.put("key", "val")Write a key/value to MCU storeBridge.get("key")
matrix = LEDMatrix()8×13 LED matrix objectHandled internally by MPU
matrix.begin()Initialize the LED matrix
matrix.clear()Turn off all LEDs
matrix.set(row, col, on)Set one LED on/off
matrix.print_text("Hi")Scroll text on matrix
Python + C++full bridge example
# ── Python (app/main.py) ──────────────────────────────────────────
from arduinoio import Bridge, LEDMatrix
import time

bridge = Bridge()
bridge.begin()                     # must come before any bridge.call()

matrix = LEDMatrix()
matrix.begin()

# Read a pin via Bridge
raw  = bridge.call("read_adc", "A0").strip()   # always returns str
volts = float(raw) * 5.0 / 4095

# Toggle LED via Bridge
bridge.call("set_led", "1")    # all Bridge args are strings
time.sleep(0.5)
bridge.call("set_led", "0")

# Set PWM brightness (0–255) via Bridge
bridge.call("set_pwm", "9:128")  # custom format: "pin:duty"

# Use key-value store for shared state
bridge.put("mode", "blink")          # Python → MCU store
mode = bridge.get("mode")            # Python ← MCU store

# LED Matrix (direct, no Bridge needed)
matrix.clear()
matrix.print_text("Hi!")             # scrolls text on 8×13 matrix
matrix.set(0, 0, True)              # set top-left LED on
⚠ All Bridge Arguments Are Strings

bridge.call() always receives and returns strings. You must convert: bridge.call("set_pwm", str(duty)) and then float(bridge.call("read_adc").strip()) on the way back. Never forget .strip() — Bridge responses often have trailing newlines.


15

Hello World

A UNO Q "Hello World" requires two files — the Python app and the companion C++ sketch. Both must be deployed together via App Lab.

Pythonapp/main.py
from arduinoio import Bridge, LEDMatrix
import time

# Setup
bridge = Bridge()
bridge.begin()
matrix = LEDMatrix()
matrix.begin()

count = 0
print("Hello, UNO Q World!")

while True:
    # Blink LED via MCU
    bridge.call("set_led", "1")
    matrix.print_text("Hi")
    time.sleep(0.5)
    bridge.call("set_led", "0")
    time.sleep(0.5)
    count += 1
    print(f"Blink #{count}")
C++sketch/sketch.ino
#include <Bridge.h>
#include <BridgeServer.h>
#include <BridgeClient.h>

BridgeServer server;

void set_led(BridgeClient c) {
  String v = c.readStringUntil('\n');
  digitalWrite(13, v=="1" ? HIGH:LOW);
}

void setup() {
  Bridge.begin();
  server.begin();
  server.addHandler("set_led", set_led);
  pinMode(13, OUTPUT);
}

void loop() {
  server.process();
  // never use delay() here!
}

16

Comprehensive Beginner Project: Sensor Station

A two-file project (Python + C++ sketch) that demonstrates virtually every core Python concept through a real-world sensor monitoring station with LED matrix display, serial command interface, JSON data logging, and live statistics.

Hardware

Arduino UNO Q · LEDs on pins 9, 10, 11 + resistors · Potentiometer on A0 · Pushbutton on D2

Concepts Covered

Dataclasses · Decorators · Classes · Properties · Dunder methods · Generators · Comprehensions · Collections · JSON · Closures · Exception handling · Context managers · Dispatch tables

Commands

Type in App Lab terminal: r (read), b (blink), c (chase), p (pulse), s (stats), l (log), ? (help), q (quit)

Python — Full Projectapp/sensor_station.py (MPU side)
"""
╔══════════════════════════════════════════════════════════════╗
║       SENSOR STATION — Python Arduino UNO Q Project          ║
║  Demonstrates core Python concepts in one cohesive program   ║
╚══════════════════════════════════════════════════════════════╝

CIRCUIT:
  D9, D10, D11  → LED → 220Ω → GND
  A0            → potentiometer middle pin (outer pins to 5V/GND)
  D2            → pushbutton → GND  (MCU uses INPUT_PULLUP)

CONCEPTS DEMONSTRATED:
  Imports / modules            Constants, variables, f-strings
  Type hints                   Dataclass + field()
  Decorator (@retry, @log)     Classes, properties, dunders
  Inheritance                  *args / **kwargs
  Generators (yield)           List/dict/set comprehensions
  Exception handling           Context manager (with)
  File I/O + JSON              collections (defaultdict, deque, Counter)
  functools (wraps, partial)   Closures
  Dict dispatch tables         match/case
  Non-blocking timing          __name__ == "__main__" guard
"""

# ══ IMPORTS ══════════════════════════════════════════════════════
from arduinoio       import Bridge, LEDMatrix
from dataclasses     import dataclass, field
from pathlib         import Path
from datetime        import datetime
from collections     import defaultdict, deque, Counter
from typing          import Optional, Callable
import time, json, functools, math, statistics


# ══ CONSTANTS ════════════════════════════════════════════════════
VALID_MODES = {"off", "blink", "chase", "pulse"}  # set literal
LED_PINS    = (9, 10, 11)                            # tuple = immutable
ADC_BITS    = 12
ADC_MAX     = (2 ** ADC_BITS) - 1                    # 4095
REF_VOLTS   = 5.0
LOG_FILE    = Path("logs") / "sensor_log.json"      # pathlib path join
TICK_SEC    = 0.05                                    # 50ms main loop tick

HELP_TEXT = """
=== Sensor Station Commands ===
  r  → Read all sensors now
  b  → Blink mode
  c  → Chase mode
  p  → Pulse / breathing mode
  0  → All LEDs off
  s  → Show statistics
  l  → Save log to JSON
  ?  → Show this help
  q  → Quit
================================"""

# Colour codes for terminal output — built via dict comprehension
_RAW_COLOURS = [
    ("green",  "\033[92m"),  ("yellow", "\033[93m"),
    ("red",    "\033[91m"),  ("cyan",   "\033[96m"),
    ("bold",   "\033[1m"),   ("reset",  "\033[0m"),
    ("dim",    "\033[2m"),
]
C = {name: code for name, code in _RAW_COLOURS}       # dict comprehension


# ══ DECORATORS ════════════════════════════════════════════════════
def log_errors(func: Callable) -> Callable:
    """Decorator: catch and print exceptions; return None on failure."""
    @functools.wraps(func)              # preserve __name__ and __doc__
    def wrapper(*args, **kwargs):        # *args=tuple, **kwargs=dict
        try:
            return func(*args, **kwargs)
        except ValueError as e:
            print(f"{C['red']}[ValueError] {func.__name__}: {e}"{C['reset']}}")
        except Exception as e:
            print(f"{C['red']}[ERROR] {func.__name__}: {e}"{C['reset']}}")
        return None
    return wrapper

def retry(times: int = 3, delay: float = 0.05):
    """Parameterised decorator: retry a function N times on any exception."""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == times - 1: raise
                    time.sleep(delay)
        return wrapper
    return decorator


# ══ DATACLASS ════════════════════════════════════════════════════
@dataclass
class SensorReading:
    """A single voltage reading from the MCU ADC."""
    pin:       str
    raw:       int
    voltage:   float
    timestamp: str = field(
        default_factory=lambda: datetime.now().isoformat(timespec="seconds")
    )                               # lambda: anonymous function
    tags:      list = field(default_factory=list)  # safe mutable default

    def to_dict(self) -> dict:
        return vars(self)          # returns instance __dict__

    @classmethod
    def from_dict(cls, d: dict) -> "SensorReading":
        return cls(**d)             # ** unpacks dict as kwargs

    def __str__(self) -> str:
        return f"[{self.pin}] {self.voltage:.3f}V (raw={self.raw}) @ {self.timestamp}"


# ══ CLASS — SensorStation ═════════════════════════════════════════
class SensorStation:
    """
    Manages Bridge communication, LED modes, and sensor data history.

    Demonstrates: __init__, properties, dunder methods, class variables,
    generators, defaultdict, deque, Counter, closures.
    """

    _count: int = 0               # class variable — shared by all instances

    def __init__(self, ref_v: float = REF_VOLTS):
        self._bridge   = Bridge()
        self._matrix   = LEDMatrix()
        self._ref_v    = ref_v
        self._mode     = "off"
        self._tick     = 0
        self._last     = time.time()
        # defaultdict(list) auto-creates [] for new pin keys
        self._history: defaultdict = defaultdict(lambda: deque(maxlen=200))
        self._mode_log: Counter = Counter()    # count mode uses
        SensorStation._count += 1

    def begin(self) -> None:
        self._bridge.begin()
        self._matrix.begin()
        self._matrix.clear()
        print(f"{C['green']}Station online{C['reset']} (ref={self._ref_v}V)")

    # ── Property ─────────────────────────────────────────────────
    @property
    def mode(self) -> str:
        return self._mode

    @mode.setter
    def mode(self, value: str) -> None:
        if value not in VALID_MODES:
            raise ValueError(f"Invalid mode: {value}. Choose: {VALID_MODES}")
        self._mode = value
        self._tick = 0                # reset tick counter on mode change
        self._mode_log[value] += 1  # Counter tracks how often each mode used
        self._matrix.print_text(value[:4].upper())

    # ── Dunder methods ────────────────────────────────────────────
    def __repr__(self) -> str:
        return f"SensorStation(mode={self._mode!r}, tick={self._tick})"

    def __str__(self) -> str:
        total = sum(len(v) for v in self._history.values())  # generator expr
        return f"Station[{self._mode.upper()} | {total} readings]"

    def __len__(self) -> int:       # len(station)
        return sum(len(v) for v in self._history.values())

    # ── Hardware methods (all Bridge calls here) ──────────────────
    @retry(times=3)               # decorator applied to method
    def read_adc(self, pin: str = "A0") -> Optional[SensorReading]:
        raw_str = self._bridge.call("read_adc", pin).strip()
        if not raw_str:
            return None
        raw  = int(raw_str)
        v    = raw * self._ref_v / ADC_MAX
        reading = SensorReading(pin=pin, raw=raw, voltage=round(v, 4))
        self._history[pin].append(reading)    # defaultdict creates deque
        return reading

    def set_led(self, pin: int, state: bool) -> None:
        self._bridge.call("set_led", f"{pin}:{1 if state else 0}")

    def set_pwm(self, pin: int, duty: int) -> None:
        duty = max(0, min(255, duty))
        self._bridge.call("set_pwm", f"{pin}:{duty}")

    def all_off(self) -> None:
        for pin in LED_PINS:
            self.set_led(pin, False)

    # ── Generator method ──────────────────────────────────────────
    def recent_readings(self, pin: str, n: int = 10):
        """Lazily yield the last N readings for a pin."""
        hist = list(self._history[pin])
        for r in hist[-n:]:
            yield r        # generator: pauses here each iteration

    # ── Statistics — uses standard library ───────────────────────
    def stats(self, pin: str) -> dict:
        """Return mean/min/max/stdev for a pin's history."""
        voltages = [r.voltage for r in self._history[pin]]   # list comprehension
        if not voltages:
            return {}
        return {
            "pin":   pin,
            "count": len(voltages),
            "mean":  round(statistics.mean(voltages), 4),
            "min":   round(min(voltages), 4),
            "max":   round(max(voltages), 4),
            "stdev": round(statistics.stdev(voltages), 4) if len(voltages) > 1 else 0,
            "modes": dict(self._mode_log.most_common()),  # Counter → dict
        }

    # ── LED mode update functions ─────────────────────────────────
    def _update_blink(self) -> None:
        on = (self._tick // 10) % 2 == 0    # toggle every 10 ticks
        for pin in LED_PINS:
            self.set_led(pin, on)

    def _update_chase(self) -> None:
        active = (self._tick // 8) % len(LED_PINS)  # cycles 0→1→2→0
        for i, pin in enumerate(LED_PINS):
            self.set_led(pin, i == active)

    def _update_pulse(self) -> None:
        # Triangle wave: 0→255→0, each LED offset by 85 (= 255/3)
        phase = (self._tick * 4) % 256
        duties = [
            int(((phase + i * 85) % 256) * 2
                if ((phase + i * 85) % 256) < 128
                else (255 - (phase + i * 85) % 256) * 2)
            for i in range(len(LED_PINS))          # list comprehension
        ]
        for pin, duty in zip(LED_PINS, duties):    # zip() — parallel iterate
            self.set_pwm(pin, duty)

    def tick(self) -> None:
        """Advance one timer tick — dispatched by mode dict."""
        self._tick += 1
        # Dict dispatch table — replaces if/elif chain
        MODE_FNS: dict[str, Callable] = {
            "off":   self.all_off,
            "blink": self._update_blink,
            "chase": self._update_chase,
            "pulse": self._update_pulse,
        }
        MODE_FNS[self._mode]()           # call the matching update function

    # ── Closure ───────────────────────────────────────────────────
    def make_threshold_alert(self, threshold: float):
        """Return a closure that fires if voltage exceeds threshold."""
        label = f"Alert(>{threshold:.2f}V)"
        def check(v: float) -> bool:
            if v > threshold:
                print(f"{C['yellow']}{label}: {v:.3f}V{C['reset']}")
                return True
            return False
        return check                    # threshold captured in closure

    # ── File I/O ──────────────────────────────────────────────────
    def save_log(self, path: Path = LOG_FILE) -> None:
        """Save all readings to a JSON file using context manager."""
        path.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            pin: [r.to_dict() for r in readings]   # dict + list comprehension
            for pin, readings in self._history.items()
        }
        with path.open("w", encoding="utf-8") as f:  # context manager
            json.dump(payload, f, indent=2)
        print(f"{C['green']}Saved {len(self)} readings → {path}"{C['reset']}}")

    # ── Class method ──────────────────────────────────────────────
    @classmethod
    def instance_count(cls) -> int:
        return cls._count


# ══ DISPLAY HELPERS ══════════════════════════════════════════════
def display_reading(r: Optional[SensorReading]) -> None:
    if r is None:
        print(f"  {C['red']}No reading{C['reset']}")
        return
    bar_len = int(r.voltage / REF_VOLTS * 30)           # voltage → bar length
    bar     = "█" * bar_len + "░" * (30 - bar_len)
    colour  = C["green"] if r.voltage < 3.0 else C["yellow"]
    print(f"  {colour}{r.pin:4} {bar} {r.voltage:.3f}V{C['reset']}")

def display_stats(s: dict) -> None:
    if not s:
        print(f"  {C['dim']}No data yet.{C['reset']}"); return
    print(f"\n  {C['bold']}Stats for {s['pin']}:{C['reset']}")
    for key in ("count", "mean", "min", "max", "stdev"):
        print(f"    {key:8} {s[key]}")
    print(f"    modes    {s['modes']}\n")


# ══ COMMAND HANDLERS ═════════════════════════════════════════════
# Functions stored as values in a dict — "dispatch table" pattern

def cmd_read(station: SensorStation) -> None:
    print(f"\n  {C['cyan']}Reading sensors:{C['reset']}")
    for pin in ("A0",):             # extend with "A1","A2" for more sensors
        r = station.read_adc(pin)
        display_reading(r)

def cmd_stats(station: SensorStation) -> None:
    display_stats(station.stats("A0"))
    # Recent readings using generator method
    recent = list(station.recent_readings("A0", n=5))
    if recent:
        print(f"  Last 5 readings:")
        for r in recent:
            print(f"    {r}")             # calls SensorReading.__str__

def cmd_log(station: SensorStation) -> None:
    station.save_log()

def cmd_mode(station: SensorStation, mode: str) -> None:
    try:
        station.mode = mode             # uses property setter (validates)
        print(f"  Mode → {C['cyan']}{mode.upper()}{C['reset']}")
    except ValueError as e:
        print(f"  {C['red']}{e}{C['reset']}")


# ══ MAIN ══════════════════════════════════════════════════════════
def main() -> None:
    """
    Entry point. Sets up station, attaches threshold alert closure,
    and runs the combined main loop + command dispatcher.
    """
    print(f"\n  {C['bold']}{C['cyan']}Arduino UNO Q Sensor Station{C['reset']}  v1.0\n")

    station = SensorStation(5.0)
    station.begin()

    # Create a closure-based threshold alert
    alert = station.make_threshold_alert(4.0)  # fires when > 4.0V

    # Dict-based command dispatch table
    # Functions are first-class objects — they can live in a dict!
    dispatch: dict[str, Callable] = {
        "r": cmd_read,
        "s": cmd_stats,
        "l": cmd_log,
        "b": lambda st: cmd_mode(st, "blink"),
        "c": lambda st: cmd_mode(st, "chase"),
        "p": lambda st: cmd_mode(st, "pulse"),
        "0": lambda st: cmd_mode(st, "off"),
        "?": lambda _: print(HELP_TEXT),
    }

    last_auto_read = time.time()
    AUTO_INTERVAL  = 5.0             # auto-read every 5 seconds
    running        = True

    print(HELP_TEXT)
    print(f"  {C['dim']}{station}{C['reset']}\n")   # calls __str__

    while running:
        try:

            # ── Non-blocking timer tick ─────────────────────────
            now = time.time()
            if now - station._last >= TICK_SEC:
                station._last = now
                station.tick()                 # advance LED animation

            # ── Auto-read A0 every AUTO_INTERVAL seconds ────────
            if now - last_auto_read >= AUTO_INTERVAL:
                last_auto_read = now
                r = station.read_adc("A0")
                if r:
                    display_reading(r)
                    alert(r.voltage)            # check closure threshold

            # ── Non-blocking serial input ────────────────────────
            # select.select allows non-blocking stdin poll on Linux
            import select
            ready, _, _ = select.select([__import__("sys").stdin], [], [], 0)
            if not ready:
                continue

            cmd = __import__("sys").stdin.readline().strip().lower()

            # match/case — Python 3.10+ structural pattern matching
            match cmd:
                case "q":
                    running = False
                case "":
                    pass                       # ignore empty input
                case c if c in dispatch:
                    dispatch[c](station)        # call handler from dict
                case _:
                    print(f"  Unknown: '{cmd}' — type ? for help")

        except KeyboardInterrupt:
            running = False
        except Exception as e:
            print(f"  {C['red']}Loop error: {e}"{C['reset']}}")

    # ── Cleanup on exit ──────────────────────────────────────────
    print(f"\n  {C['green']}Shutting down…{C['reset']}")
    station.mode = "off"
    station.save_log()               # final log save on exit
    print(f"  {C['dim']}{station}{C['reset']}")


# ══ SCRIPT GUARD ══════════════════════════════════════════════════
# When run directly: __name__ == "__main__"
# When imported: __name__ == "sensor_station"
# This prevents main() from running when another script imports us.
if __name__ == "__main__":
    main()
C++ Companion Sketchsketch/sketch.ino (MCU side)
/*
 * Companion sketch for sensor_station.py
 * Exposes four Bridge handlers callable from Python:
 *   "read_adc"  — read pin A0..A5, return int string
 *   "set_led"   — set digital pin HIGH/LOW  (arg: "pin:0" or "pin:1")
 *   "set_pwm"   — set PWM duty 0-255        (arg: "pin:duty")
 *   "btn_state" — read button on D2, return "0" or "1"
 *
 * CRITICAL: server.process() must run every loop() iteration.
 * Never use delay() — use millis()-based timing instead.
 */
#include <Bridge.h>
#include <BridgeServer.h>
#include <BridgeClient.h>

BridgeServer server;

void read_adc(BridgeClient client) {
  String pin_str = client.readStringUntil('\n');
  pin_str.trim();
  int pin = pin_str.length() > 0 ? pin_str.substring(1).toInt() : 0; // "A0"→0
  analogReadResolution(12);           // 12-bit = 0–4095
  client.print(analogRead(A0 + pin)); // A0,A1,A2… are sequential
}

void set_led(BridgeClient client) {
  String arg = client.readStringUntil('\n');  // "13:1" or "9:0"
  arg.trim();
  int colon = arg.indexOf(':');
  if (colon < 0) return;
  int pin   = arg.substring(0, colon).toInt();
  int state = arg.substring(colon + 1).toInt();
  pinMode(pin, OUTPUT);
  digitalWrite(pin, state ? HIGH : LOW);
}

void set_pwm(BridgeClient client) {
  String arg = client.readStringUntil('\n');  // "9:128"
  arg.trim();
  int colon = arg.indexOf(':');
  if (colon < 0) return;
  int pin  = arg.substring(0, colon).toInt();
  int duty = constrain(arg.substring(colon + 1).toInt(), 0, 255);
  analogWrite(pin, duty);
}

void btn_state(BridgeClient client) {
  client.print(digitalRead(2) == LOW ? "1" : "0");  // LOW = pressed (pullup)
}

void setup() {
  analogReadResolution(12);
  pinMode(2, INPUT_PULLUP);    // button with internal pull-up
  pinMode(9,  OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  Bridge.begin();
  server.begin();
  server.addHandler("read_adc",  read_adc);
  server.addHandler("set_led",   set_led);
  server.addHandler("set_pwm",   set_pwm);
  server.addHandler("btn_state", btn_state);
}

unsigned long lastBtn  = 0;
bool          prevBtn  = false;

void loop() {
  server.process();       // MUST be here — handles all Python Bridge calls

  // Example: read button locally and store in Bridge key-value store
  bool btnNow = (digitalRead(2) == LOW);
  if (btnNow != prevBtn && millis() - lastBtn > 200) {  // debounce
    lastBtn = millis();
    prevBtn = btnNow;
    Bridge.put("btn", btnNow ? "1" : "0");
  }
}

Concept Map

Code ElementConceptWhy It Matters
@dataclass class SensorReadingDataclassAuto-generates __init__, __repr__, __eq__ — no boilerplate
@functools.wraps / @retry(3)DecoratorsWrap any function to add retry, logging without changing it
defaultdict(lambda: deque(maxlen=200))defaultdict + dequeAuto-creates rolling buffer per pin — no KeyError
@property / @mode.setterProperty with validationSetter raises ValueError for invalid modes — type-safe attribute
def recent_readings: yieldGenerator methodLazy iteration — caller pulls values, nothing pre-allocated
make_threshold_alert(4.0)Closurethreshold captured in inner function — reusable alert factory
C = {name: code for name, code in ...}Dict comprehensionBuild colour lookup from tuple data in one line
for pin, duty in zip(LED_PINS, duties)zip()Iterate two sequences together — clean Pythonic pattern
match cmd: case c if c in dispatch:match/case with guardPython 3.10 pattern matching with condition guard
select.select([sys.stdin], [], [], 0)Non-blocking stdinRead input without blocking the main loop — Linux-only feature
with path.open("w") as f: json.dumpContext manager + JSONFile always closed cleanly; JSON structured log on real storage
Counter.most_common()collections.CounterTrack and rank which LED modes are used most

17

Best Practices for the UNO Q

✅ Python Do

Always call bridge.begin() first. Always .strip() bridge responses. Use time.time() for non-blocking timing. Use try/except around every bridge.call(). Use with open() for file I/O.

❌ Python Don't

Don't call analogRead() or digitalWrite() from Python — these don't exist on the MPU side. Don't use time.sleep() in the main loop — use non-blocking timing.

✅ C++ Do

Always put server.process() in loop(). Use millis()-based non-blocking timing. Call analogReadResolution(12) in setup() for full 12-bit ADC.

❌ C++ Don't

Never use delay() in loop() — it blocks server.process() and breaks the Bridge. Never open the Arduino IDE Serial Monitor and App Lab simultaneously.

Pythonpatterns.py
# ✅ Always strip Bridge responses
raw = bridge.call("read_adc").strip()   # response has trailing \n

# ✅ Convert ALL Bridge args to string
bridge.call("set_pwm", f"9:{duty}")      # NOT bridge.call("set_pwm", 9, duty)

# ✅ Non-blocking timing with time.time()
last = time.time()
while True:
    if time.time() - last >= 1.0:
        last = time.time()
        do_periodic_work()         # runs every ~1 second without blocking

# ✅ Wrap bridge calls in try/except
try:
    v = float(bridge.call("read_adc").strip())
except (ValueError, OSError) as e:
    print(f"Bridge error: {e}")
    v = 0.0

# ✅ Use __name__ guard — prevents running on import
if __name__ == "__main__":
    main()

# ✅ Config file pattern with pathlib
cfg_path = Path("config.json")
defaults = {"ref_v": 5.0, "interval": 1.0}
if cfg_path.exists():
    cfg = json.loads(cfg_path.read_text())
    cfg = defaults | cfg              # merge: user settings override defaults
else:
    cfg = defaults.copy()

# ✅ Use dataclass for structured sensor data
@dataclass
class Reading:
    pin: str; voltage: float; ts: str = field(default_factory=datetime.now().isoformat)

# ✅ LED matrix for visual feedback
matrix.clear()
matrix.print_text(f"{voltage:.1f}V")   # show reading on 8×13 LED matrix
💡 Next Steps

After mastering this guide: use flask to host a real-time sensor dashboard at arduino-uno-q.local:5000, explore the Qwiic connector with Modulino nodes (no soldering — snap in sensors for temp, distance, RGB), try threading.Thread to run Flask + the sensor loop concurrently, and experiment with tflite for on-device AI inference using the Adreno GPU on the Qualcomm chip.