laser power-good: the line characterized, the kernel-drill guard, the probe, the dev image's mmap and ctypes

The supply's power-good line is active high, static across HV enable and emission, and driven; the facts bank and CAMPAIGN-LOG carry the measurement and the item closes. The kernel-drill latch-unlock guard read the old inverted value as HV not good, a check that was vacuous and would refuse every run once the module reads the line correctly; it now uses the chain's own witnesses, the charge-pump watchdog and the engine state. pgood_probe.py watches the line beside the chain through the kernel readbacks and is registered on the bench page. The dev image lists python3-mmap and python3-ctypes again for the pad-level bench tools the python trim had left without them.
This commit is contained in:
ScottW514
2026-09-01 18:48:47 -04:00
parent fb804e32bd
commit 64f552fc28
7 changed files with 250 additions and 43 deletions
+21 -17
View File
@@ -324,8 +324,10 @@ from the kernel counters. Contract: [forgectrl](https://docs.forgefirm.org/techn
**Emission evidence.** `cnc/laser_on_sampled` (surfaced as `/status` **Emission evidence.** `cnc/laser_on_sampled` (surfaced as `/status`
`laser.emission_samples`) is the reliable live-emission witness; emission `laser.emission_samples`) is the reliable live-emission witness; emission
sensed with no armed window relocks the latch and stops motion. `pic/hv_current` sensed with no armed window relocks the latch and stops motion. `pic/hv_current`
is the only live HV telemetry on this PSU. `cnc/laser_pgood_sampled` is **not** is the only live HV telemetry on this PSU. `cnc/laser_pgood` is the supply's
a usable witness here — it reads 0 through real cutting. power-good, driven high the whole time the supply is healthy (it follows
neither HV_ENABLE nor emission): a supply-fault witness, never an emission
witness (facts bank "Emission and HV witnesses").
## Lid, interlock and button policy ## Lid, interlock and button policy
@@ -1031,8 +1033,22 @@ is committed.
count on a commanded fire window and returns to 0 at Idle — the reliable count on a commanded fire window and returns to 0 at Idle — the reliable
witness. `pic/hv_current` tracks the cut (0 idle → hundreds/1023 raw while witness. `pic/hv_current` tracks the cut (0 idle → hundreds/1023 raw while
firing) and is the only live HV telemetry on this PSU (`hv_voltage` is firing) and is the only live HV telemetry on this PSU (`hv_voltage` is
grounded). `cnc/laser_pgood_sampled` stays 0 through real cutting: not usable grounded). **`cnc/laser_pgood` (J1_14, GPIO4_21) is the supply's power-good,
here. active high**, measured 2026-09-01 through the kernel readbacks at ~780 Hz:
the pin is high at idle, through four HV_ENABLE cycles of a dry run, and
through an armed S400 cut (714 laser pulses, `hv_current` to 1023), with
zero transitions in 225 s, and it stays high against a 100 kΩ pull-down
switched in at the pad (IOMUXC `0x020E03C4`, restored to `0x100b0`), so the
supply drives it. The supply's supervisor is a Weltrend WT7525 (PC-supply
supervisor; open-drain PGO reports every DC output within spec, drops on an
over/under-voltage or over-current fault, 300 ms delay after good), and the
reverse-engineering pinout sheets label J1_14 `HV_PFC_STOP` (TP_A2C). The
factory app reports the line as the `HVpg`/`HVps` tags and read it the same
way (0 at idle under the old inverted convention). The kernel now reads it
active high: `laser_pgood` 1 and `laser_pgood_sampled` 255 on a healthy
supply; a drop during an armed window is the cooling engine's supply
power-good warning, and it means a supply fault. The line has never been
seen low on this machine.
- **The head MCU flag register and HEAD_IRQ.** The head MCU (a KL17 at - **The head MCU flag register and HEAD_IRQ.** The head MCU (a KL17 at
i2c-3 @0x47, I²C-slave-only to the SoC) samples four head-local GPIO input i2c-3 @0x47, I²C-slave-only to the SoC) samples four head-local GPIO input
levels once per main loop into the read-only flag register 0x05: b0 levels once per main loop into the read-only flag register 0x05: b0
@@ -1270,19 +1286,7 @@ feature requests, enhancements) will eventually be tracked as GitHub issues.
far): re-measure the two heat coefficients and the machine's far): re-measure the two heat coefficients and the machine's
air-assist offset; and if a lit check still trips, the air-assist offset; and if a lit check still trips, the
void-on-emission design with the tube as its own flow tracer. void-on-emission design with the tube as its own flow tracer.
8. **Laser power-good: what the line means.** `cnc/laser_pgood` and its 8. **Initial commissioning: measure and set the machine's own numbers
sampled count are defined in the UAPI (active low, one sample every
~3.9 ms), the facts bank records that the sampled count reads 0 through
real cutting, and the cooling engine warns
`laser power-good degraded during the armed window` whenever fewer
than half the samples read low, so the warning fires at every session
open and carries no information. Nobody knows what the line reports
on this PSU: whether it is the supply's own power-good, an HV-present
flag, a polarity we have inverted, or unconnected. Owed: the line on a
scope against `hv_current` through an armed cut, its meaning written
into the facts bank and the UAPI, and then either a warning that means
something or no warning.
9. **Initial commissioning: measure and set the machine's own numbers
methodically.** Every tunable that was measured on the bench machine methodically.** Every tunable that was measured on the bench machine
and shipped as a default varies from machine to machine: the flow and shipped as a default varies from machine to machine: the flow
check's bands and `cool_flow_rise`, the tube's heat coefficients check's bands and `cool_flow_rise`, the tube's heat coefficients
+42
View File
@@ -6977,6 +6977,48 @@ from the LAN by `live_fire_drills.py`, the operator on the button:
Items 7 and 8 are bench-proven. The board runs the hot-deployed binary Items 7 and 8 are bench-proven. The board runs the hot-deployed binary
until the next flash. until the next flash.
## 2026-09-01: the laser supply's power-good line, characterized without a scope
The "laser power-good" item asked what J1_14 reports. No scope on the
bench, so the line was read through the kernel's own readbacks with a
new probe (`scripts/bench/pgood_probe.py`, fed to the board over ssh
stdin) at 770 to 790 Hz, against LASER_ON, FIRE, the charge-pump
watchdog, HV_ENABLE and the doors from the switch device, and
`hv_current` at 20 Hz. Image 20260901220626 (dev), fresh boot.
- **Dry run, 75 s.** Four jogs; `charge_pump_alive` and HV_ENABLE rose
and fell together within 5 ms at every run start and end. The pin
stayed high for every one of 59,518 samples.
- **Armed run, 150 s**: the `witness` drill, a 20 mm square at S400 F600.
714 LASER_ON pulses over 8.0 s, `hv_current` 0 to 1023, HV_ENABLE up
for the run. The pin stayed high for every one of 115,872 samples.
- **Driven, not floating.** With a cross-built register tool the pad's
internal pull was switched to 100 kΩ pull-down (IOMUXC `0x020E03C4`,
`0x100b0` to `0x130b0`), then pull-up, then restored; the pin read
high under all three and the pinctrl view confirmed the restore.
- **What the supply has.** The reverse-engineering archive holds the
supply's datasheets and board photos: the supervisor board carries a
Weltrend WT7525 (PC-supply supervisor: open-drain PGO high once every
DC output is within spec, low on an over/under-voltage or over-current
fault, 300 ms delay), LM2901 comparators and four PC817 optocouplers.
The pinout and test-point sheets label J1_14 `HV_PFC_STOP` (TP_A2C).
The factory app reports the line as the `HVpg`/`HVps` header tags and
its logs show 0 at idle under the same inverted convention the module
inherited.
Disposition: J1_14 is the supply's power-good, active high, static across
HV enable and emission, and driven. The module now reads it active high
(`laser_pgood` 1, `laser_pgood_sampled` counts good samples, 255 on a
healthy supply); the cooling engine's once-per-session warning keeps its
threshold and now means a supply fault; the catalog's kernel-drill
precheck, which read the old value as "HV not good", moves to the chain's
own witnesses. The line has never been seen low; a supply fault is the
only thing that would take it there. Owed: the change rides the next image
(kernel module), and the dev image regains `python3-mmap` and
`python3-ctypes`, which the python trim removed and which
`resume_dark_lead.py`, `cp_watchdog_timing.py` and the accelerometer
probes need.
## Reference notes ## Reference notes
### Head-IRQ source validation — the beam-emission hypothesis ### Head-IRQ source validation — the beam-emission hypothesis
+7
View File
@@ -228,6 +228,13 @@ TOOLS = [
"a pause, how fast the chain re-arms, and - on a live run - the dark lead between FIRE and " "a pause, how fast the chain re-arms, and - on a live run - the dark lead between FIRE and "
"LASER_ON that a resumed cut loses. Dry by default; --run live needs the arm press, eye " "LASER_ON that a resumed cut loses. Dry by default; --run live needs the arm press, eye "
"protection, fire watch, extinguisher, exhaust."}, "protection, fire watch, extinguisher, exhaust."},
{"id": "pgood-probe", "title": "Supply power-good line against the chain", "script": "pgood_probe.py",
"safety": "dry", "where": "board", "ported": True,
"args": [_arg("secs", "float", 90.0, "sampling window", flag="--secs")],
"desc": "Watches the supply's power-good line (cnc/laser_pgood, reported as the raw pin level) beside "
"LASER_ON, FIRE, the charge-pump watchdog, HV_ENABLE and the doors through the kernel readbacks at a "
"few hundred hertz, with hv_current at 20 Hz, and prints every transition. It only watches: drive "
"the machine meanwhile (a jog, an armed cut, a pause, a lid open). Needs no pad mapping."},
# -- host-side harnesses (CI) ------------------------------------------------------ # -- host-side harnesses (CI) ------------------------------------------------------
{"id": "laser-stream-test", "title": "Laser pulse-stream emission harness", "script": "laser_stream_test.py", {"id": "laser-stream-test", "title": "Laser pulse-stream emission harness", "script": "laser_stream_test.py",
"safety": "dry", "where": "host", "ported": False, "args": [], "safety": "dry", "where": "host", "ported": False, "args": [],
+40 -26
View File
@@ -9,9 +9,10 @@ laser_on_sampled (the gated LASER_ON output), interlock_circuit bit 3
(the commanded latch), faults and underruns. Every drill forces duty to (the commanded latch), faults and underruns. Every drill forces duty to
zero before any FIRE bit, keeps motor_lock=15 (no axis moves), and zero before any FIRE bit, keeps motor_lock=15 (no axis moves), and
re-locks the latch on every exit path. K3 and fire B/U unlock the latch re-locks the latch on every exit path. K3 and fire B/U unlock the latch
for their run and therefore refuse to proceed while laser_pgood reports for their run and therefore refuse to proceed unless the safety chain
the HV supply good (the operator opens the lid: the safety chain holds holds HV off: the charge-pump watchdog dead and the pulse engine idle
HV off). (HV_ENABLE follows the pump; laser_pgood is the supply's power-good and
says nothing about HV).
""" """
import errno import errno
import os import os
@@ -130,33 +131,46 @@ class PulseDevice:
return False return False
def hv_not_good(): def hv_off_reason():
"""Precheck for the drills that unlock the latch with a zero-duty """Why the safety chain is NOT holding HV off right now, or None. The
stream: they run only while the HV supply does NOT report good. At chain asserts HV_ENABLE only while a run feeds the charge-pump
idle the chain holds HV_ENABLE low, so this refuses a start only on a watchdog, so a dead watchdog and an idle pulse engine mean HV off.
machine that is not idle the way it should be; the cure is the lid laser_pgood is the supply's power-good, high whenever the supply is
(the safety chain holds HV off with it open).""" healthy, and says nothing about HV, so it plays no part here."""
pgood = rd("cnc/laser_pgood") alive = rd("cnc/charge_pump_alive")
if pgood is None: state = rd("cnc/state")
return "cnc/laser_pgood unreadable" if alive is None or state is None:
if pgood != "0": return "cnc/charge_pump_alive or cnc/state unreadable"
return ("laser_pgood=%s: the HV supply reports good; this drill unlocks the latch with a " if alive != "0" or state != "idle":
"zero-duty stream and needs HV not good (open the lid, then start again)" % pgood) return ("charge_pump_alive=%s state=%s: the chain may be holding HV_ENABLE up" % (alive, state))
return None return None
def require_hv_not_good(ctx): def hv_off():
"""Precheck for the drills that unlock the latch with a zero-duty
stream: they run only while the chain holds HV off. At idle nothing
feeds the watchdog, so this refuses a start only on a machine that is
not idle the way it should be."""
why = hv_off_reason()
if why:
return ("%s; this drill unlocks the latch with a zero-duty stream and needs HV off "
"(let the machine go idle, then start again)" % why)
return None
def require_hv_off(ctx):
"""The same rule at the start of the run (the precheck ran a moment """The same rule at the start of the run (the precheck ran a moment
earlier; the machine must still agree).""" earlier; the machine must still agree)."""
pgood = rd("cnc/laser_pgood") why = hv_off_reason()
ctx.evidence["laser_pgood"] = pgood ctx.evidence["charge_pump_alive"] = rd("cnc/charge_pump_alive")
ctx.check(pgood == "0", "laser_pgood=%s (HV supply reports good) - refusing the latch unlock", pgood) ctx.evidence["kernel_state"] = rd("cnc/state")
ctx.check(why is None, "%s - refusing the latch unlock", why)
def check_hv_not_good(ctx): def check_hv_off(ctx):
"""The hard check right before an unlock (no prompt: forgectrl is down).""" """The hard check right before an unlock (no prompt: forgectrl is down)."""
pgood = rd("cnc/laser_pgood") why = hv_off_reason()
ctx.check(pgood == "0", "laser_pgood=%s (HV supply reports good) - refusing the latch unlock", pgood) ctx.check(why is None, "%s - refusing the latch unlock", why)
# ---------------------------------------------------------------- readbacks # ---------------------------------------------------------------- readbacks
@@ -389,7 +403,7 @@ def _k3_phase(ctx):
"""K3: the latch unlocked during the accel ramp must not restore the FIRE """K3: the latch unlocked during the accel ramp must not restore the FIRE
drive while the run is in flight. Runs inside the caller's takeover.""" drive while the run is in flight. Runs inside the caller's takeover."""
ev = ctx.evidence ev = ctx.evidence
check_hv_not_good(ctx) check_hv_off(ctx)
stream = POWER0 + FIRE * (3 * TICK_HZ) + PAD * (TICK_HZ // 2) stream = POWER0 + FIRE * (3 * TICK_HZ) + PAD * (TICK_HZ // 2)
ctx.log("K3: %d bytes = %.1f s of FIRE bits; ramp_rate 10000 Hz/s (~0.9 s accel " ctx.log("K3: %d bytes = %.1f s of FIRE bits; ramp_rate 10000 Hz/s (~0.9 s accel "
"window); unlock at t=+0.15 s", len(stream), len(stream) / TICK_HZ) "window); unlock at t=+0.15 s", len(stream), len(stream) / TICK_HZ)
@@ -442,7 +456,7 @@ def _fire_phase(ctx, mode):
stream = _fire_stream() stream = _fire_stream()
ctx.log("fire %s: stream %d bytes = %.3f s", mode, len(stream), len(stream) / TICK_HZ) ctx.log("fire %s: stream %d bytes = %.3f s", mode, len(stream), len(stream) / TICK_HZ)
if unlock: if unlock:
check_hv_not_good(ctx) check_hv_off(ctx)
snap(ctx, "fire %s pre" % mode) snap(ctx, "fire %s pre" % mode)
wr("cnc/motor_lock", 15) wr("cnc/motor_lock", 15)
wr("cnc/step_freq", TICK_HZ) wr("cnc/step_freq", TICK_HZ)
@@ -518,7 +532,7 @@ def _fire_phase(ctx, mode):
"mid-run unlock", "mid-run unlock",
subsystem="kernel", kind="auto", hardware="takeover", always=True, est_min=4, subsystem="kernel", kind="auto", hardware="takeover", always=True, est_min=4,
covers=_KERNEL_COVERS, covers=_KERNEL_COVERS,
requires=["kernel.k1-k2"], precheck=hv_not_good, requires=["kernel.k1-k2"], precheck=hv_off,
steps=["Phases B, U and K3 unlock the latch with a zero-duty stream, so the drill starts only " steps=["Phases B, U and K3 unlock the latch with a zero-duty stream, so the drill starts only "
"while the HV supply does not report good (true at idle; if it is refused, open the " "while the HV supply does not report good (true at idle; if it is refused, open the "
"lid - the safety chain holds HV off - and start it again)."], "lid - the safety chain holds HV off - and start it again)."],
@@ -532,7 +546,7 @@ def _fire_phase(ctx, mode):
"flight - laser_enable stays 0 for the whole run.") "flight - laser_enable stays 0 for the whole run.")
def fire_line(ctx): def fire_line(ctx):
ev = ctx.evidence ev = ctx.evidence
require_hv_not_good(ctx) require_hv_off(ctx)
with ctx.takeover(): with ctx.takeover():
try: try:
a = _fire_phase(ctx, "A") a = _fire_phase(ctx, "A")
@@ -12,9 +12,16 @@ IMAGE_INSTALL += " \
forgectrl \ forgectrl \
forgetest \ forgetest \
python3-gfutilities-emulator \ python3-gfutilities-emulator \
python3-mmap \
python3-ctypes \
htop \ htop \
" "
# python3-mmap and python3-ctypes serve the bench scripts that read the SoC
# pads through /dev/mem (resume_dark_lead.py, cp_watchdog_timing.py, the
# accelerometer probes). They are bench tooling: the release image carries
# neither, and forgetest itself imports neither.
# The release recipe trims nano from the shared base list (bench # The release recipe trims nano from the shared base list (bench
# convenience, 8.7 MB with libmagic); the dev image keeps it. Removal specs # convenience, 8.7 MB with libmagic); the dev image keeps it. Removal specs
# accumulate and apply after every append, so the variable inside the # accumulate and apply after every append, so the variable inside the
+1
View File
@@ -59,6 +59,7 @@ page's takeover does that; from a host, stop them first.
| `puls_profile.py` | Decodes factory `.puls` streams (raw or GF1-headered) into velocity/accel profiles: peak speeds, ramp-slope fits, per-move segments, Z cadence. Runs anywhere (stdlib only). Source of the factory-true grblHAL defaults: 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. | | `puls_profile.py` | Decodes factory `.puls` streams (raw or GF1-headered) into velocity/accel profiles: peak speeds, ramp-slope fits, per-move segments, Z cadence. Runs anywhere (stdlib only). Source of the factory-true grblHAL defaults: 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. |
| `cp_watchdog_timing.py` | HV charge-pump watchdog one-shot timing (runs on the board): latches every CHG_PUMP feed pulse in GPIO3's edge detector (pin 24 only, IMR untouched, ICR2 restored on exit) and polls the `!Q` (`charge_pump_alive`) and `!HV_ENABLE` (`hv_enable`) pads through /dev/mem in a tight loop while it commands short local jogs; prints per-run t_w (last pulse → Q fall), Q → HV_ENABLE delay, priming latency and the feed period, with the loop's worst gap as the resolution. Motion only, laser locked, no other Grbl client attached. | | `cp_watchdog_timing.py` | HV charge-pump watchdog one-shot timing (runs on the board): latches every CHG_PUMP feed pulse in GPIO3's edge detector (pin 24 only, IMR untouched, ICR2 restored on exit) and polls the `!Q` (`charge_pump_alive`) and `!HV_ENABLE` (`hv_enable`) pads through /dev/mem in a tight loop while it commands short local jogs; prints per-run t_w (last pulse → Q fall), Q → HV_ENABLE delay, priming latency and the feed period, with the loop's worst gap as the resolution. Motion only, laser locked, no other Grbl client attached. |
| `resume_dark_lead.py` | Pause/resume safety-chain timing (runs on the board, as root): samples LASER_ON, FIRE, HV_ENABLE, the charge-pump watchdog, the button and the doors straight off the SoC pads through /dev/mem at ~2 kHz, with motion dated from the kernel step counters, across a pause and a resume driven by the operator's button presses. Reports how long HV_ENABLE survives the stream stopping, how fast the chain re-arms on the resume, and - on `--run live` - the dark lead between FIRE going back on and LASER_ON following it, in milliseconds and in millimeters at the job's feed. `--run dry` (default) commands no laser at all and `--auto P,R` drives the pause and resume with `!`/`~` for an unattended rehearsal; `--run live` needs the arm press, eye protection, fire watch, extinguisher and exhaust. GRBL mode, no other Grbl client attached. | | `resume_dark_lead.py` | Pause/resume safety-chain timing (runs on the board, as root): samples LASER_ON, FIRE, HV_ENABLE, the charge-pump watchdog, the button and the doors straight off the SoC pads through /dev/mem at ~2 kHz, with motion dated from the kernel step counters, across a pause and a resume driven by the operator's button presses. Reports how long HV_ENABLE survives the stream stopping, how fast the chain re-arms on the resume, and - on `--run live` - the dark lead between FIRE going back on and LASER_ON following it, in milliseconds and in millimeters at the job's feed. `--run dry` (default) commands no laser at all and `--auto P,R` drives the pause and resume with `!`/`~` for an unattended rehearsal; `--run live` needs the arm press, eye protection, fire watch, extinguisher and exhaust. GRBL mode, no other Grbl client attached. |
| `pgood_probe.py` | The supply's power-good line (J1_14, `cnc/laser_pgood`) against the laser chain, without a scope (runs on the board, as root): polls the kernel readbacks (`laser_pgood` reported as the raw pin level, `laser_on`, `laser_enable`, `charge_pump_alive`) and the switch device's HV_ENABLE and doors bits at a few hundred hertz, `hv_current` at 20 Hz, and prints every transition with a timestamp plus a per-line summary. Drive the machine meanwhile (a dry jog, an armed cut, a pause, a lid open); the probe only watches. Feed it over ssh stdin: `ssh root@<board> 'python3 - --secs 90' < pgood_probe.py`. |
| `bench_m2.py` | Motion-quality bench, runs against the board over TCP:23: bounded round-trip jogs (sanity, max-rate, diagonal) + feed-hold/resume mid-move, reporting peak feed, state transitions, and position drift. | | `bench_m2.py` | Motion-quality bench, runs against the board over TCP:23: bounded round-trip jogs (sanity, max-rate, diagonal) + feed-hold/resume mid-move, reporting peak feed, state transitions, and position drift. |
Data files kept beside the tools: `flow_matrix_results.json` / Data files kept beside the tools: `flow_matrix_results.json` /
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""LASER_PGOOD (J1_14) against the rest of the laser chain, through the
kernel's readbacks.
Runs ON the board as root. Polls the kernel's GPIO readbacks (sysfs, one
open descriptor each, re-read with pread) and the switch device's EV_SW
word (EVIOCGSW) as fast as the loop allows, a few hundred hertz, and
reports every transition of the watched lines with a timestamp, plus a
per-line summary, so the meaning of the supply's line can be read off
against what the chain and the supply were doing: idle, a dry run
(HV_ENABLE follows the charge pump), an armed cut (LASER_ON, FIRE,
hv_current), a pause and a resume, a lid open. hv_current comes from the
PIC at a lower rate and rides along as a range.
PGOOD is reported as the RAW PIN LEVEL (the kernel's laser_pgood attribute
is the logical, inverted value: 1 = pin low). Everything else is the
kernel's logical value.
The loop must not hog the CPU: single core, the protocol thread is
SCHED_OTHER, so the sampler sleeps between passes and reports its worst gap.
Usage: pgood_probe.py [--secs N] [--json FILE]
Drive the machine from a sender or the button meanwhile; the probe only
watches. GRBL mode, any state.
"""
import argparse
import fcntl
import json
import os
import struct
import sys
import time
CNC = '/sys/glowforge/cnc/'
ATTRS = [ # name, attribute, invert-to-raw
('PGOOD', CNC + 'laser_pgood', True),
('LASER_ON', CNC + 'laser_on', False),
('FIRE', CNC + 'laser_enable', False),
('CP_ALIVE', CNC + 'charge_pump_alive', False),
]
SWITCH_DEV = '/dev/input/event0'
SW_BITS = [('HV_ENABLE', 4), ('DOORS', 3)] # EV_SW codes on the switch device
EVIOCGSW = (2 << 30) | (8 << 16) | (0x45 << 8) | 0x1b
HV_CURRENT = '/sys/glowforge/pic/hv_current'
STATE = CNC + 'state'
HV_HZ = 20.0
SLEEP_S = 0.001
def rd(fd):
return os.pread(fd, 32, 0).strip()
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--secs', type=float, default=60.0)
ap.add_argument('--json', default='')
args = ap.parse_args()
try:
os.nice(5)
except OSError:
pass
fds = [(n, os.open(p, os.O_RDONLY), inv) for n, p, inv in ATTRS]
sw = os.open(SWITCH_DEV, os.O_RDONLY)
hv_fd = os.open(HV_CURRENT, os.O_RDONLY)
st_fd = os.open(STATE, os.O_RDONLY)
def sample():
s = {}
for n, fd, inv in fds:
v = int(rd(fd) or b'0')
s[n] = (1 - v) if inv else v
buf = fcntl.ioctl(sw, EVIOCGSW, b'\0' * 8)
for n, bit in SW_BITS:
s[n] = (buf[bit >> 3] >> (bit & 7)) & 1
return s
t0 = time.monotonic()
state = sample()
hv = int(rd(hv_fd) or b'0')
print('t=%8.3f start %s hv=%d kstate=%s'
% (0.0, ' '.join('%s=%d' % kv for kv in state.items()), hv, rd(st_fd).decode()))
sys.stdout.flush()
trans = []
hv_log = []
counts = {k: [0, 0] for k in state}
worst_gap = 0.0
n = 0
next_hv = t0
t_prev = t0
while True:
now = time.monotonic()
if now - t0 > args.secs:
break
gap = now - t_prev
if gap > worst_gap:
worst_gap = gap
t_prev = now
new = sample()
n += 1
for k in new:
if new[k] != state[k]:
trans.append((now - t0, k, new[k]))
print('t=%8.3f %-9s -> %d hv=%s kstate=%s'
% (now - t0, k, new[k], rd(hv_fd).decode(), rd(st_fd).decode()))
sys.stdout.flush()
counts[k][new[k]] += 1
state = new
if now >= next_hv:
hv_log.append((now - t0, int(rd(hv_fd) or b'0')))
next_hv = now + 1.0 / HV_HZ
time.sleep(SLEEP_S)
secs = time.monotonic() - t0
print('--- %.1f s, %d passes (%.0f Hz), worst gap %.1f ms' % (secs, n, n / secs, worst_gap * 1e3))
for k, (c0, c1) in counts.items():
print(' %-9s 0: %6.2f %% 1: %6.2f %%' % (k, 100.0 * c0 / n, 100.0 * c1 / n))
if hv_log:
vals = [v for _t, v in hv_log]
lit = [t for t, v in hv_log if v > 20]
print(' hv_current min %d max %d; > 20 raw for %.1f s%s'
% (min(vals), max(vals), len(lit) / HV_HZ,
(' (%.1f .. %.1f s)' % (lit[0], lit[-1])) if lit else ''))
if args.json:
with open(args.json, 'w') as f:
json.dump({'secs': secs, 'passes': n, 'worst_gap_ms': worst_gap * 1e3,
'counts': counts, 'transitions': trans, 'hv': hv_log}, f)
print('record: %s' % args.json)
return 0
if __name__ == '__main__':
sys.exit(main())