docs(mochabin): EP06 mPCIe GPIO investigation runbook + hardened probe script (closes #345) (#359)

* docs(mochabin): add EP06 mPCIe GPIO investigation runbook (ref #345)

scripts/probe-mpcie-gpios.sh: empirical CP0 GPIO sweep that drives
each unrequested line HIGH for a few seconds and watches dmesg/lsusb
for new USB device. Skips [used] lines, uses libgpiod gpioset
--mode=time so the line auto-restores to input on release. Modes:
--baseline (snapshot only, safe), --line gpiochipN K (single line),
no arg (full sweep gpiochip1+gpiochip2).

docs/hardware/mochabin-mpcie-ep06-runbook.md: hypothesis, procedure,
and DTS rfkill-gpio template patterned after cn9132-clearfog.dts
lines 69-122. Apply once a candidate GPIO is identified.

--baseline smoke-tested on gk2 — sweep deferred until spare EP06
arrives (no mPCIe device in J5 = no signal regardless of which line
drives W_DISABLE#).

* docs(mochabin): drop EP06 spare assumption from runbook (ref #345)

No spare EP06 on order; the existing EP06-E will be used for the
sweep when re-seated in J5. Replaces "deferred until spare" framing
with a multimeter-scoping fallback for the inconclusive case.

* fix(mpcie): v0.2.0 probe-mpcie-gpios.sh — drop blanket sweep, danger-list, dry-run default (ref #345)

The v0.1.0 "drive every unused-input CP0 GPIO HIGH for 3s one-at-a-time"
sweep took gk2 hard-down within seconds (host became unreachable; ARP
stopped responding). gpioinfo's [used] tag only reflects lines the
kernel *requested* — several unrequested CP0 GPIOs are physically
wired to critical board functions (eth switch reset, PCIe2 PERST#,
pca9554 IRQ) and driving them HIGH wedged the board.

v0.2.0:
- No blanket-sweep mode. Removed.
- Default mode = dry-run candidate listing only. No GPIO is driven.
- --commit --line CHIP N required to actually drive a single line.
- Hardcoded DANGER_LINES (gpiochip1:1, gpiochip1:9, gpiochip1:27)
  skipped unconditionally, even with --commit.
- gpiochip2 entirely held off without --allow-chip2 (no per-line
  schematic mapping for that bank).
- Hard refusals are vocal (REFUSED: ... reason) before exit.

Runbook updated to walk through the new dry-run → --commit flow.
HISTORY documents the incident + the v0.2.0 mitigations.
Lesson saved in memory feedback_gpio_blanket_sweep_crashed_board.md.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
This commit is contained in:
CyberMind 2026-05-22 17:50:33 +02:00 committed by GitHub
parent fe834d4fe8
commit ff990ba112
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 555 additions and 0 deletions

View File

@ -1,6 +1,56 @@
# HISTORY — SecuBox-DEB Migration Log
*Tracking completed milestones with dates*
---
## 2026-05-22
### MOCHAbin mPCIe slot J5 — EP06 GPIO investigation runbook (Issue #345)
**Context:** PR #255's DTS patch wired the UTMI PHY to `cp0_usb3_1`, so the
mPCIe slot's USB pipe is alive (4 USB buses on gk2 vs 2 before). But
plugging an EP06-E still produces no enumeration: `lsusb` blank for the
slot, `lspci -vvv` reports `DLActive-` on the PCIe side. Suspect: the
slot's W_DISABLE# / PWR_EN / WAKE# control lines are not declared in
the DTS, so they come up in whatever default state the SoC pad config
leaves them — likely holding the modem powered-down. No MOCHAbin
schematic in the repo to pin down which CP0 GPIO is wired to J5.
**Done:**
- **#345** — added `scripts/probe-mpcie-gpios.sh`: empirical sweep that
drives each *unrequested* CP0 GPIO line HIGH for a few seconds and
watches `dmesg` / `lsusb` for a new USB device. Skips any line marked
`[used]` by gpioinfo, uses libgpiod (`gpioset --mode=time` so the
line auto-restores to input on release). Three modes: `--baseline`
(snapshot only, no GPIO writes — safe to run anytime), `--line
gpiochipN K` (single line, useful once a candidate is found), or no
arg (full sweep across `gpiochip1` + `gpiochip2` = the two CP0
banks). Output to `/var/log/secubox/mpcie-probe-<ts>.log`.
- Companion `docs/hardware/mochabin-mpcie-ep06-runbook.md` documenting
the hypothesis, the procedure step-by-step, and the DTS template
(`rfkill-gpio` block patterned after `cn9132-clearfog.dts` lines
69122) to apply once a candidate GPIO is identified.
**Validation:** `--baseline` smoke-tested on gk2 — produces correct
gpioinfo dump, identifies the `[used]` lines (cp0_gpio0 line 0 "reset",
line 12 "PHY reset", line 30 "shutdown", plus the SFP+ pca9554
expander chip 3). No spare modem expected; if probing finds no
candidate the next escalation is multimeter scoping of J5 pins
2/20/24/39/41/52.
**Incident — v0.1.0 blanket sweep crashed gk2:** running the
"all unused inputs" sweep with no modem in J5 took the board hard-down
within seconds (host became unreachable; ARP stopped responding).
`gpioinfo`'s `[used]` tag only reflects lines the kernel *requested*,
not lines that are physically wired — several unrequested CP0 GPIOs
drive critical board functions (eth switch reset, PCIe2 PERST#,
pca9554 IRQ). v0.2.0 of the script removes the blanket-sweep mode
entirely: defaults to dry-run candidate listing, requires explicit
`--commit --line CHIP N` to drive any single line, hardcodes a
`DANGER_LINES` skip-list, and holds `gpiochip2` off behind
`--allow-chip2`. Memorialised in memory
`feedback_gpio_blanket_sweep_crashed_board.md`.
---
## 2026-05-21

View File

@ -0,0 +1,169 @@
# MOCHAbin mPCIe Slot J5 — EP06 Investigation Runbook
**Status:** parked. Runbook ready to execute when an mPCIe modem is
reseated in J5 (the original EP06-E on hand; no spare on order).
**Related issues:** #255 (DTS UTMI PHY landed) · #345 (this runbook).
## Symptom
After the DTS patch in #255 wired the UTMI PHY to `cp0_usb3_1`, the
mPCIe slot J5 USB pipe is fully alive — kernel reports the controller
binds cleanly and `lsusb -t` shows 4 USB buses (vs 2 before the patch).
But plugging a Quectel EP06-E modem into J5 still doesn't produce any
`new high-speed USB device` event in dmesg, no `2c7c:0306` in `lsusb`,
and `lspci -vvv` reports the PCIe side as `DLActive-` (no link
partner). The modem is electrically dark on both interfaces of the
slot.
## Hypothesis
mPCIe defines several control pins per slot that the host must drive
for the device to wake:
| Pin | Direction | Meaning | Default |
| ------------- | ----------- | ----------------------- | -------------------------------- |
| PERST# | host→device | reset (active low) | must transition low→high at boot |
| W_DISABLE# | host→device | radio kill (active low) | must be HIGH for radio |
| WAKE# | device→host | wake event (active low) | input only |
| PWR_EN / 3.3V | rail | power | must be ON |
In `armada-7040-mochabin.dts`:
- `cp0_pcie2` has `reset-gpios = <&cp0_gpio1 9 GPIO_ACTIVE_LOW>`
PERST# is declared.
- `cp0_usb3_1` has UTMI PHY but **no W_DISABLE# / PWR_EN / WAKE#
GPIOs** declared.
The schematic for the MOCHAbin J5 slot is not in this repo, and
Globalscale only publishes a partial. So we don't know which CP0 GPIO
(if any) is wired to W_DISABLE#. The other option is that it's
hard-tied (HIGH or LOW) on the PCB.
Reference: `cn9132-clearfog.dts` declares `rfkill-gpio` nodes for each
of its mPCIe / M.2 slots (lines 69122). That's the pattern to copy.
## Investigation procedure
`scripts/probe-mpcie-gpios.sh` (v0.2.0) is a one-line-at-a-time probe.
v0.1.0 of this script ran a blanket sweep across all "unused input"
CP0 GPIOs and took gk2 hard-down on 2026-05-22 — `gpioinfo`'s
`[used]` tag only reflects what the kernel *requested*, and several
unrequested lines are physically wired to critical functions (eth
switch reset, PCIe2 PERST#, pca9554 IRQ). v0.2.0 therefore:
- skips a hardcoded `DANGER_LINES` list (those three + future
additions);
- refuses to touch `gpiochip2` (CP0 GPIO1) entirely without
`--allow-chip2` (no per-line schematic mapping for that bank yet);
- defaults to dry-run — listing candidates without driving anything;
- requires `--commit` + `--line CHIP N` to actually drive a line HIGH.
There is no longer a blanket-sweep mode.
Prerequisites:
- Run **on the MOCHAbin**, as root.
- A known-good mPCIe device seated in J5 (preferably the EP06 modem;
failing that, any mPCIe-USB device).
- Screw tightened — the slot has spring-contact retention only.
### Step 1 — baseline (safe, no GPIO writes)
```bash
# from the dev host
scp scripts/probe-mpcie-gpios.sh root@<mochabin>:/tmp/
ssh root@<mochabin> 'bash /tmp/probe-mpcie-gpios.sh --baseline'
```
Output goes to `/var/log/secubox/mpcie-probe-<timestamp>.log`. Check
the gpioinfo table to confirm the lines you expect are `[used]` (PHY
reset on cp0_gpio0 line 12, etc.) — the script will refuse to touch
those.
### Step 2 — list candidates (dry-run, default mode)
```bash
ssh root@<mochabin> 'bash /tmp/probe-mpcie-gpios.sh'
```
Prints the candidate list on `gpiochip1` (CP0 GPIO0). Each entry is
of the form `gpiochip1:N` — an unrequested input that is NOT in
`DANGER_LINES`. No GPIO is driven in this mode.
If you want candidates on `gpiochip2` too (entire bank held off by
default), add `--allow-chip2` — only do this if you can physically
power-cycle the board if something locks up.
### Step 3 — commit a single line
Pick one candidate from step 2, then drive it for `SETTLE_SEC=3` s
(longer with the env var if the EP06 needs more time to boot its
radio):
```bash
ssh root@<mochabin> \
'SETTLE_SEC=10 bash /tmp/probe-mpcie-gpios.sh --line gpiochip1 14 --commit'
```
The script:
1. Snapshots `lsusb` + last `dmesg` line.
2. Drives the line output-HIGH for `SETTLE_SEC` seconds.
3. Re-snapshots.
4. Releases the line (libgpiod restores input direction on close).
5. If `lsusb` differs or new "new high-speed USB device" /
"Quectel" / `2c7c` appears in dmesg, logs `*** CHANGE DETECTED ***`.
A successful probe gives you the GPIO number to wire into the DTS.
Iterate over candidates one at a time; if a probe locks up the board,
add the line to `DANGER_LINES` in the script before retrying.
## Translating findings to DTS
Once a line is confirmed, add an `rfkill-gpio` block to
`armada-7040-mochabin.dts` next to the existing `&cp0_usb3_1` /
`&cp0_pcie2` declarations. Template (replace `<chip>` and `<line>`):
```dts
/ {
/* J5 W_DISABLE# (mPCIe modem radio enable) */
rfkill-mpcie-modem {
compatible = "rfkill-gpio";
label = "mpcie modem (J5)";
radio-type = "wwan";
/* rfkill-gpio inverts internally; ACTIVE_HIGH = pin HIGH when
* radio is OFF, LOW when ON. Check polarity against the
* schematic before merging. */
shutdown-gpios = <&<chip> <line> GPIO_ACTIVE_HIGH>;
};
};
```
If the modem still doesn't enumerate after asserting W_DISABLE# HIGH,
the next suspect is a missing 3.3V rail enable. Look for an unused
GPIO that toggles `/sys/class/regulator/regulator-N/state` from
`disabled``enabled` when driven HIGH.
## Disambiguating "dead modem" vs "slot not powering it"
There's no spare EP06 coming, so we can't simply swap units. If the
GPIO sweep finds no candidate line, the remaining signal that the
modem is alive at all is:
- **3.3V on J5 pin 2 / 24 / 39 / 41 / 52** — multimeter on the
connector with the modem unseated tells you if the slot's power rail
is live. If 3.3V is missing, the modem can't possibly enumerate
regardless of W_DISABLE#.
- **Modem LED** — the EP06-E lights an onboard status LED when it has
power. Visible through the chassis cutout near J5.
Multimeter scoping is outside the runbook's automated scope but is the
next escalation if the sweep is inconclusive.
## Outcome we want
A single small DTS patch (rfkill-gpio block, maybe a regulator) that
makes EP06 enumerate at boot without any post-boot intervention. That
unblocks #236 (secubox-rbs-sensor v0.2) and the v0.4 SMS-via-EP06
notification channel for secubox-sentinelle-gsm.

336
scripts/probe-mpcie-gpios.sh Executable file
View File

@ -0,0 +1,336 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
#
# SecuBox-Deb :: probe-mpcie-gpios.sh
#
# Hardware investigation runbook for issue #345 — find which CP0 GPIO drives
# W_DISABLE# / PERST# / PWR_EN on the MOCHAbin mPCIe slot J5.
#
# Background: the kernel DTS patch from #255 wired the UTMI PHY to
# cp0_usb3_1 (the mPCIe USB controller) — so the SoC USB pipe is alive
# on the slot, but plugging an EP06 modem still doesn't enumerate. The
# slot's control lines (W_DISABLE#, PWR_EN, possibly WAKE#) are NOT
# declared in the current DTS, so they come up in whatever default state
# the SoC pad config leaves them in — which may be holding the modem
# powered-down. This runbook drives carefully-selected CP0 GPIO lines
# HIGH one at a time, watching for a new USB enumeration event.
#
# Prerequisites: run ON THE BOARD (gk2), as root, with a known-good
# mPCIe device (EP06 modem or any mPCIe-USB device) physically seated
# in J5 and the screw tightened.
#
# Safety (v0.2.0): gpioinfo's "[used]" tag only reflects lines the
# kernel *requested* — some unrequested lines on the MOCHAbin are
# physically wired to critical functions (eth switch reset, USB hub
# power-enable, pca9554 IRQ, etc.) and driving them HIGH crashed
# gk2 on the v0.1.0 blanket sweep (incident 2026-05-22). v0.2.0
# therefore:
# 1. Skips known-dangerous lines unconditionally (DANGER_LINES).
# 2. Defaults to --dry-run (lists candidates without driving them).
# 3. Requires --commit AND --line for the actual probe — no
# blanket sweep mode anymore.
# 4. Refuses to touch gpiochip2 (CP0 GPIO1) entirely without an
# explicit --allow-chip2 flag (we have no per-line schematic
# mapping for that bank yet).
set -euo pipefail
readonly MODULE="probe-mpcie-gpios"
readonly VERSION="0.2.0"
# Lines known (from DTS inspection) to be wired to critical board
# functions even when gpioinfo reports them as "unused input". Format:
# "chip:line". Driving any of these would risk a board crash.
readonly -a DANGER_LINES=(
# gpiochip1 = cp0_gpio0
"gpiochip1:1" # mpp1 — cp0_switch_pins gpio function (Topaz reset)
"gpiochip1:9" # mpp9 — cp0_pcie_reset_pins (PCIe2 PERST#); driving it can crash the slot
"gpiochip1:27" # mpp27 — pca9554_int (SFP+ I/O expander IRQ); IRQ storm risk
# gpiochip2 = cp0_gpio1 — entire bank held off by default
# (no per-line schematic; require --allow-chip2 to probe)
)
readonly LOG_DIR="/var/log/secubox"
readonly LOG_FILE="${LOG_DIR}/mpcie-probe-$(date +%Y%m%d-%H%M%S).log"
readonly SETTLE_SEC="${SETTLE_SEC:-3}"
# Which gpiochips to probe. gpiochip1 = cp0_gpio0, gpiochip2 = cp0_gpio1.
# These are the two big CP0 banks on the Armada-7040. AP807 (gpiochip0)
# is system-controller / never wired to slot J5, so we skip it. SFP+
# expander (gpiochip3) and CP210x (gpiochip4) are obviously unrelated.
readonly CHIPS="${CHIPS:-gpiochip1 gpiochip2}"
usage() {
cat <<EOF
${MODULE} v${VERSION} — probe one CP0 GPIO line to find mPCIe J5 W_DISABLE#
usage:
$0 snapshot + list dry-run candidates (default)
$0 --baseline snapshot only — no candidate listing
$0 --line gpiochipN K dry-run a single line (refuses if danger-listed)
$0 --line gpiochipN K --commit actually drive the line HIGH for SETTLE_SEC
$0 --allow-chip2 ... required to touch any gpiochip2 line
$0 --help
env vars:
SETTLE_SEC=N seconds to hold the line HIGH (default 3)
output:
${LOG_DIR}/mpcie-probe-<timestamp>.log
WARNING — read this before --commit:
v0.1.0 of this script ran a blanket sweep across all "unused input"
CP0 GPIOs. That took gk2 hard-down on 2026-05-22 — some unrequested
lines are physically wired to critical functions (eth switch reset,
PCIe2 PERST#, pca9554 IRQ, etc.) even though the kernel never
requests them. v0.2.0 therefore refuses to run a blanket sweep at
all, and the default mode just lists candidates without driving
anything.
When you do --commit a single line, make sure you can physically
reach the MOCHAbin to power-cycle it if the board locks up.
prerequisites: run as root on the MOCHAbin, with an mPCIe device
(preferably the EP06 modem) physically seated in J5 and the retention
screw tightened.
A successful probe shows a new "usb 2-1.X: new high-speed USB device"
line in the post-dmesg diff AND a new device in lsusb.
EOF
}
# True if "chip:line" matches an entry in DANGER_LINES (case-sensitive).
is_danger_line() {
local key="$1"
local entry
for entry in "${DANGER_LINES[@]}"; do
[[ "$entry" == "$key" ]] && return 0
done
return 1
}
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "error: must run as root (need gpiochip + dmesg access)" >&2
exit 1
fi
}
require_libgpiod() {
for cmd in gpiodetect gpioinfo gpioset; do
if ! command -v "$cmd" >/dev/null; then
echo "error: $cmd not found — install libgpiod (apt install gpiod)" >&2
exit 1
fi
done
}
setup_log() {
mkdir -p "${LOG_DIR}"
exec > >(tee -a "${LOG_FILE}") 2>&1
echo "=== ${MODULE} v${VERSION}$(date -Is) ==="
echo "log: ${LOG_FILE}"
}
snapshot_baseline() {
echo
echo "=== baseline ==="
echo "--- kernel ---"
uname -a
echo "--- gpiodetect ---"
gpiodetect
echo "--- gpioinfo (probed chips: ${CHIPS}) ---"
for chip in ${CHIPS}; do
gpioinfo "$chip"
done
echo "--- lsusb -t ---"
lsusb -t
echo "--- lsusb ---"
lsusb
echo "--- lspci -tv ---"
lspci -tv 2>/dev/null || echo "(lspci not installed)"
echo "--- recent dmesg (last 60s) ---"
dmesg -T --since '60 sec ago' 2>/dev/null | tail -30 || dmesg | tail -30
}
# gpioinfo line format:
# " line 14: unnamed unused input active-high "
# " line 0: unnamed \"reset\" output active-low [used]"
# A "candidate" is: unnamed + unused + input + NOT in DANGER_LINES.
line_is_candidate() {
local chip="$1"
local info="$2"
local n
if [[ "$info" =~ line\ +([0-9]+): ]]; then
n="${BASH_REMATCH[1]}"
else
return 1
fi
[[ "$info" != *"[used]"* ]] || return 1
[[ "$info" == *"input"* ]] || return 1
[[ "$info" == *"unused"* ]] || return 1
is_danger_line "${chip}:${n}" && return 1
return 0
}
list_candidates() {
local chip="$1"
echo
echo "=== candidates on ${chip} ==="
while IFS= read -r line; do
if line_is_candidate "${chip}" "${line}"; then
# Extract line number for the summary
[[ "$line" =~ line\ +([0-9]+): ]] && echo " ${chip}:${BASH_REMATCH[1]}"
fi
done < <(gpioinfo "${chip}")
}
probe_line_commit() {
local chip="$1"
local n="$2"
# Hard gate — never drive a danger-listed line, even with --commit.
if is_danger_line "${chip}:${n}"; then
echo "REFUSED: ${chip}:${n} is in DANGER_LINES — driving it" \
"risks crashing the board (see header comment)." >&2
exit 1
fi
# gpiochip2 entirely off-limits without --allow-chip2.
if [[ "${chip}" == "gpiochip2" && "${ALLOW_CHIP2:-0}" != "1" ]]; then
echo "REFUSED: gpiochip2 (cp0_gpio1) requires --allow-chip2 — no" \
"per-line schematic mapping for that bank yet." >&2
exit 1
fi
echo
echo "── probe ${chip} line ${n} (COMMIT, HIGH for ${SETTLE_SEC}s) ──"
local pre_lsusb pre_dmesg_tail
pre_lsusb=$(lsusb | sort)
pre_dmesg_tail=$(dmesg | tail -1)
# Drive HIGH for SETTLE_SEC seconds. gpioset releases the line
# (libgpiod restores it to input on close).
timeout "$((SETTLE_SEC + 2))" gpioset --mode=time --sec="${SETTLE_SEC}" \
"${chip}" "${n}=1" &
local gpio_pid=$!
sleep "$((SETTLE_SEC - 1))"
local post_lsusb post_dmesg
post_lsusb=$(lsusb | sort)
post_dmesg=$(dmesg | awk -v marker="${pre_dmesg_tail}" '
BEGIN { capture=0 }
index($0, marker) { capture=1; next }
capture { print }
')
wait "${gpio_pid}" 2>/dev/null || true
local lsusb_diff
lsusb_diff=$(diff <(echo "${pre_lsusb}") <(echo "${post_lsusb}") || true)
if [[ -n "${lsusb_diff}" ]] || echo "${post_dmesg}" | grep -qiE 'new (high|full|low|super)-speed usb|quectel|2c7c'; then
echo " *** CHANGE DETECTED ***"
echo " --- lsusb diff ---"
echo "${lsusb_diff:-(no diff)}"
echo " --- new dmesg ---"
printf ' %s\n' "${post_dmesg}"
else
echo " no change"
fi
}
probe_line_dryrun() {
local chip="$1"
local n="$2"
echo
echo "── DRY-RUN ${chip}:${n} ──"
if is_danger_line "${chip}:${n}"; then
echo " REFUSED: in DANGER_LINES"
return
fi
if [[ "${chip}" == "gpiochip2" && "${ALLOW_CHIP2:-0}" != "1" ]]; then
echo " REFUSED: gpiochip2 requires --allow-chip2"
return
fi
# Verify the line is currently unrequested and an input.
local info
info=$(gpioinfo "${chip}" 2>/dev/null | awk -v n="${n}" '$2 == n":"')
if [[ -z "${info}" ]]; then
echo " ERROR: line ${n} not found on ${chip}"
return
fi
if [[ "${info}" == *"[used]"* ]]; then
echo " REFUSED: line is currently [used] by the kernel"
return
fi
echo " OK: --commit would drive ${chip}:${n} HIGH for ${SETTLE_SEC}s"
}
main() {
require_root
require_libgpiod
local mode="default"
local chip=""
local n=""
local commit=0
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
usage; exit 0 ;;
--baseline)
mode="baseline"; shift ;;
--line)
[[ $# -ge 3 ]] || { usage; exit 1; }
mode="line"; chip="$2"; n="$3"; shift 3 ;;
--commit)
commit=1; shift ;;
--allow-chip2)
ALLOW_CHIP2=1; shift ;;
*)
usage; exit 1 ;;
esac
done
setup_log
case "${mode}" in
baseline)
snapshot_baseline
;;
line)
snapshot_baseline
if [[ "${commit}" == "1" ]]; then
probe_line_commit "${chip}" "${n}"
else
probe_line_dryrun "${chip}" "${n}"
echo
echo "(dry-run — pass --commit to actually drive the line)"
fi
;;
default)
snapshot_baseline
list_candidates gpiochip1
if [[ "${ALLOW_CHIP2:-0}" == "1" ]]; then
list_candidates gpiochip2
else
echo
echo "=== gpiochip2 candidates suppressed ==="
echo " pass --allow-chip2 to list (no per-line schematic yet)"
fi
echo
echo "=== next step ==="
echo " pick a candidate, then:"
echo " $0 --line <chip> <line> # dry-run"
echo " $0 --line <chip> <line> --commit # actually drive"
;;
esac
}
main "$@"