Add environment-driven digital probe mode

This commit is contained in:
Franz Rolfsvaag 2026-08-28 16:48:05 +02:00
parent 935395797a
commit 0a87fa71f1
4 changed files with 96 additions and 2 deletions

View File

@ -39,6 +39,20 @@ UART_STOP_BITS=1
UART_TX_GPIO=20 UART_TX_GPIO=20
UART_RX_GPIO=21 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 ---------- # ---------- Self-update ----------
UPDATE_ENABLED=true UPDATE_ENABLED=true

View File

@ -38,6 +38,25 @@ probe.pulse(timeout_us=1_000_000)
probe.watch(seconds=5, max_events=200) 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 `frequency()` counts rising edges in its measurement window. It is useful for
signal discovery and approximate rates, but very fast or noisy signals can signal discovery and approximate rates, but very fast or noisy signals can
overflow or exceed MicroPython IRQ handling capacity. `pulse()` reports one overflow or exceed MicroPython IRQ handling capacity. `pulse()` reports one

View File

@ -10,6 +10,63 @@ except ImportError:
import asyncio 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): async def setup(ctx):
# Configure GPIOs, state machines, etc. here. # Configure GPIOs, state machines, etc. here.
# If this raises immediately after an OTA, boot.py can roll back. # If this raises immediately after an OTA, boot.py can roll back.
@ -17,6 +74,10 @@ async def setup(ctx):
async def main(ctx): async def main(ctx):
if as_bool(ctx["env"].get("PROBE_ENABLED"), False):
await run_probe_mode(ctx)
return
activity = ctx["activity"] activity = ctx["activity"]
uart = ctx["uart"] uart = ctx["uart"]

View File

@ -1,6 +1,6 @@
{ {
"schema": 1, "schema": 1,
"release": "0.1.1-probe.1", "release": "0.1.2-probe.2",
"files": [ "files": [
{ {
"source": "firmware/launcher.py", "source": "firmware/launcher.py",
@ -20,7 +20,7 @@
{ {
"source": "firmware/app/main.py", "source": "firmware/app/main.py",
"target": "/app/main.py", "target": "/app/main.py",
"sha256": "5b287d984df266007df4014ece8e323339c0da594f4c88bcf4a6da1ff067a16f" "sha256": "7eaa8b4b8f213f44a080c82a1f76c8981b06458fa4b05e239a7d6c8cdc114c18"
} }
] ]
} }