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_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

View File

@ -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

View File

@ -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"]

View File

@ -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"
}
]
}