From 0a87fa71f19d52814a19c18f4c5c92552303dda8 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Fri, 28 Aug 2026 16:48:05 +0200 Subject: [PATCH] Add environment-driven digital probe mode --- .env.example | 14 ++++++++++ docs/DIGITAL_PROBE.md | 19 +++++++++++++ firmware/app/main.py | 61 ++++++++++++++++++++++++++++++++++++++++++ firmware/manifest.json | 4 +-- 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 077790c..7992804 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,20 @@ UART_STOP_BITS=1 UART_TX_GPIO=20 UART_RX_GPIO=21 +# ---------- Read-only development probe ---------- +# Keep disabled until a safe, conditioned front-panel signal is connected. +PROBE_ENABLED=false +# Required when PROBE_ENABLED=true. GPIO3 is a candidate on the C3 Super Mini, +# but confirm the board routing and electrical interface before using it. +PROBE_GPIO= +# state | frequency | pulse | watch +PROBE_MODE=frequency +PROBE_INTERVAL_SECONDS=3 +PROBE_FREQUENCY_SECONDS=2 +PROBE_PULSE_TIMEOUT_US=1000000 +PROBE_WATCH_SECONDS=5 +PROBE_WATCH_MAX_EVENTS=200 + # ---------- Self-update ---------- UPDATE_ENABLED=true diff --git a/docs/DIGITAL_PROBE.md b/docs/DIGITAL_PROBE.md index a108512..f41f3ca 100644 --- a/docs/DIGITAL_PROBE.md +++ b/docs/DIGITAL_PROBE.md @@ -38,6 +38,25 @@ probe.pulse(timeout_us=1_000_000) probe.watch(seconds=5, max_events=200) ``` +## Continuous probe mode + +To collect data without interrupting the application, set the following in the +device-local `/.env` and reset the board: + +```env +PROBE_ENABLED=true +PROBE_GPIO=3 +PROBE_MODE=frequency +PROBE_FREQUENCY_SECONDS=2 +PROBE_INTERVAL_SECONDS=3 +``` + +`PROBE_MODE` may be `state`, `frequency`, `pulse`, or `watch`. The corresponding +`PROBE_*` settings in `.env.example` control the measurement window, timeout, +or event cap. The mode continuously writes its results to the serial console; +it does not expose any output pin or machine-control capability. Set +`PROBE_ENABLED=false` and reset to return to normal application operation. + `frequency()` counts rising edges in its measurement window. It is useful for signal discovery and approximate rates, but very fast or noisy signals can overflow or exceed MicroPython IRQ handling capacity. `pulse()` reports one diff --git a/firmware/app/main.py b/firmware/app/main.py index d2d3317..c5c0d83 100644 --- a/firmware/app/main.py +++ b/firmware/app/main.py @@ -10,6 +10,63 @@ except ImportError: import asyncio +def as_bool(value, default=False): + if value is None: + return default + return str(value).strip().lower() in ("1", "true", "yes", "on", "enabled") + + +def as_int(value, default=0, minimum=None): + try: + result = int(str(value).strip()) + except Exception: + result = default + if minimum is not None: + result = max(minimum, result) + return result + + +async def run_probe_mode(ctx): + """Continuously collect read-only probe data while the app keeps running.""" + env = ctx["env"] + pin_text = env.get("PROBE_GPIO", "").strip() + if not pin_text: + raise RuntimeError("PROBE_ENABLED=true requires PROBE_GPIO") + try: + pin_id = int(pin_text) + except Exception: + raise RuntimeError("PROBE_GPIO must be an integer GPIO number") + + mode = env.get("PROBE_MODE", "frequency").strip().lower() + if mode not in ("state", "frequency", "pulse", "watch"): + raise RuntimeError("Unknown PROBE_MODE=" + mode) + + from app.digital_probe import open_probe + probe = open_probe(pin_id) + frequency_seconds = as_int(env.get("PROBE_FREQUENCY_SECONDS"), 2, 1) + watch_seconds = as_int(env.get("PROBE_WATCH_SECONDS"), 5, 1) + watch_events = as_int(env.get("PROBE_WATCH_MAX_EVENTS"), 200, 1) + pulse_timeout_us = as_int(env.get("PROBE_PULSE_TIMEOUT_US"), 1_000_000, 1) + interval_seconds = as_int(env.get("PROBE_INTERVAL_SECONDS"), 3, 1) + activity = ctx["activity"] + + print("[probe] mode=%s interval=%ss" % (mode, interval_seconds)) + while True: + # Measurements are data collection, not a machine action, but marking + # activity prevents an OTA reboot from interrupting a capture. + activity.touch() + if mode == "state": + probe.state() + elif mode == "frequency": + probe.frequency(frequency_seconds) + elif mode == "pulse": + probe.pulse(pulse_timeout_us) + else: + probe.watch(watch_seconds, watch_events) + activity.touch() + await asyncio.sleep_ms(interval_seconds * 1000) + + async def setup(ctx): # Configure GPIOs, state machines, etc. here. # If this raises immediately after an OTA, boot.py can roll back. @@ -17,6 +74,10 @@ async def setup(ctx): async def main(ctx): + if as_bool(ctx["env"].get("PROBE_ENABLED"), False): + await run_probe_mode(ctx) + return + activity = ctx["activity"] uart = ctx["uart"] diff --git a/firmware/manifest.json b/firmware/manifest.json index 409e357..1b3d10c 100644 --- a/firmware/manifest.json +++ b/firmware/manifest.json @@ -1,6 +1,6 @@ { "schema": 1, - "release": "0.1.1-probe.1", + "release": "0.1.2-probe.2", "files": [ { "source": "firmware/launcher.py", @@ -20,7 +20,7 @@ { "source": "firmware/app/main.py", "target": "/app/main.py", - "sha256": "5b287d984df266007df4014ece8e323339c0da594f4c88bcf4a6da1ff067a16f" + "sha256": "7eaa8b4b8f213f44a080c82a1f76c8981b06458fa4b05e239a7d6c8cdc114c18" } ] }