# 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
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.
Your .py files run on the Qualcomm chip under Debian Linux. Full CPython 3 — all standard library modules available. pip install anything.
Arduino sketches (.ino) run on the STM32 chip. Controls all physical GPIO pins. Handles interrupt-driven hardware.
The arduinoio library lets Python call named handler functions registered in the C++ sketch — and read their return values.
Arduino App Lab manages both sides in one project. Single click deploys Python to MPU and C++ to MCU simultaneously.
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.
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.
# ┌─────────────────────────────────────────────────────────────────┐
# │ 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() │
# └─────────────────────────────────────────────────────────────────┘
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).
# ── 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()
// 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! }
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.
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.
| Type | Example | Notes for UNO Q |
|---|---|---|
| int | x = 42 | Arbitrary precision — no overflow like C++ |
| float | v = 3.14 | Full 64-bit IEEE 754 (unlike MicroPython's 32-bit) |
| bool | flag = True | Subclass of int. True==1, False==0 |
| str | s = "hello" | Immutable. Bridge.call() params must be strings |
| bytes | b = b"\x00\xFF" | Immutable byte buffer — used in I²C/SPI data |
| list | pins = [9, 10, 11] | Mutable ordered sequence |
| tuple | pos = (x, y) | Immutable — use for config, RGB colours, coords |
| dict | cfg = {"baud": 9600} | Hash map — ideal for sensor data, JSON payloads |
| set | modes = {"blink", "fade"} | Unordered unique values — fast membership test |
| None | val = None | Python's null. Check with is None |
| complex | z = 3+4j | Available (unlike MicroPython) — rarely needed in hardware |
# ── 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
| Category | Operators | Example | Notes |
|---|---|---|---|
| Arithmetic | + - * / // % ** | 10 // 3 → 3 | / always float. // floor div. ** power. No ++ or --. |
| Augmented assign | += -= *= /= //= **= %= | x += 1 | Use instead of x++ |
| Comparison | == != < > <= >= | x == 10 | Returns bool. Chainable: 0 <= x <= 100 |
| Logical | and or not | a and not b | English words, not && || ! |
| Identity | is is not | x is None | Object identity — use only for None/True/False |
| Membership | in not in | "A0" in VALID_PINS | Works on lists, dicts, sets, strings |
| Bitwise | & | ^ ~ << >> | reg &= ~(1 << 3) | Identical to C++ — useful for protocol flags |
| Ternary | x 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 |
# ── 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
# ── 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++)
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.
# ── 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")
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
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.
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)
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
# 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))
# ── 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
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.
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)
| Module | Purpose | UNO Q Use Case |
|---|---|---|
| time | time(), sleep(), monotonic() | Non-blocking timing, delays, timestamps |
| datetime | Date and time objects | Timestamps on sensor logs, CSV headers |
| json | JSON encode / decode | Config files, sensor log payloads, Bridge data |
| pathlib | Path object — file system | Config files on eMMC storage |
| collections | Counter, defaultdict, deque | Sensor event counting, rolling buffers |
| dataclasses | Auto-generated data classes | Sensor reading models, config structs |
| functools | wraps, lru_cache, partial, reduce | Decorators, memoised sensor conversions |
| threading | Thread, Lock, Event | Run Flask server + sensor loop concurrently |
| socket | TCP/UDP networking | Raw socket server for telemetry |
| subprocess | Run shell commands | Interface with Linux CLI tools on Debian |
| re | Regular expressions | Parse serial/bridge response strings |
| struct | Pack/unpack binary data | I²C / SPI protocol frames |
| math | sqrt, sin, cos, pi, log | Sensor calibration, signal processing |
| statistics | mean, median, stdev | Smooth noisy ADC readings |
| csv | Read/write CSV files | Sensor 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 |
| arduinoio | Bridge to MCU (built-in) | GPIO, LED matrix, Qwiic sensors |
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 Does | C++ handler (MCU side) |
|---|---|---|
| bridge = Bridge() | Create bridge object | — |
| bridge.begin() | Open RPC connection to MCU | Bridge.begin() in setup() |
| bridge.call("name") | Call named handler, returns str | void name(BridgeClient c) |
| bridge.call("name", "arg") | Call handler with argument string | c.readStringUntil('\n') |
| bridge.get("key") | Get a key/value stored on MCU | Bridge.put("key", "val") |
| bridge.put("key", "val") | Write a key/value to MCU store | Bridge.get("key") |
| matrix = LEDMatrix() | 8×13 LED matrix object | Handled 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 (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
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.
A UNO Q "Hello World" requires two files — the Python app and the companion C++ sketch. Both must be deployed together via App Lab.
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}")
#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!
}
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.
Arduino UNO Q · LEDs on pins 9, 10, 11 + resistors · Potentiometer on A0 · Pushbutton on D2
Dataclasses · Decorators · Classes · Properties · Dunder methods · Generators · Comprehensions · Collections · JSON · Closures · Exception handling · Context managers · Dispatch tables
Type in App Lab terminal: r (read), b (blink), c (chase), p (pulse), s (stats), l (log), ? (help), q (quit)
""" ╔══════════════════════════════════════════════════════════════╗ ║ 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()
/*
* 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");
}
}
| Code Element | Concept | Why It Matters |
|---|---|---|
| @dataclass class SensorReading | Dataclass | Auto-generates __init__, __repr__, __eq__ — no boilerplate |
| @functools.wraps / @retry(3) | Decorators | Wrap any function to add retry, logging without changing it |
| defaultdict(lambda: deque(maxlen=200)) | defaultdict + deque | Auto-creates rolling buffer per pin — no KeyError |
| @property / @mode.setter | Property with validation | Setter raises ValueError for invalid modes — type-safe attribute |
| def recent_readings: yield | Generator method | Lazy iteration — caller pulls values, nothing pre-allocated |
| make_threshold_alert(4.0) | Closure | threshold captured in inner function — reusable alert factory |
| C = {name: code for name, code in ...} | Dict comprehension | Build 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 guard | Python 3.10 pattern matching with condition guard |
| select.select([sys.stdin], [], [], 0) | Non-blocking stdin | Read input without blocking the main loop — Linux-only feature |
| with path.open("w") as f: json.dump | Context manager + JSON | File always closed cleanly; JSON structured log on real storage |
| Counter.most_common() | collections.Counter | Track and rank which LED modes are used most |
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.
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.
Always put server.process() in loop(). Use millis()-based non-blocking timing. Call analogReadResolution(12) in setup() for full 12-bit ADC.
Never use delay() in loop() — it blocks server.process() and breaks the Bridge. Never open the Arduino IDE Serial Monitor and App Lab simultaneously.
# ✅ 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
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.