mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-29 01:21:16 -07:00
forgetest: the operator's part asked for by name, and fewer hands in a campaign
A campaign asked a person for about eighty things: lid, button and interlock actions, app jobs, and sixteen confirmations by eye, most of them as popups to read and answer while the head was already moving. This is the forgetest-only step of cutting that down. The operator channel. A test asks for its operator's part in four ways: ctx.ready() pre-announces a timed step and waits for the click that starts it; ctx.notice() is a standing instruction with no button, the test watching the machine for the result; ctx.act(channel, state) is a machine action by name (lid, interlock, button) - a notice for the operator today, proven done by the switch reading or an `until` condition, recorded in evidence.actions with who performed it, and the seam a bench actuator plugs into through runner.fixture; ctx.confirm() stays for the yes/no the evidence cannot answer. Tests declare `actions`; a `precheck` refuses a start the machine cannot honor (a reason, no result, a queue skips it and carries on). The page shows what you will do before it is asked: the running test's steps, a queue's attended tests still waiting, or the test whose title you clicked while idle; notices and prompts sit under it. The campaign card no longer carries baseline, queue and leftover notes: those go to the runner journal (daemon.log, syslog as `forgetest`, and the run in progress), with a Runner journal button in the footer. The catalog, 43 tests (27 auto, 8 operator, 8 live; was 45: 25/12/8): cloud.mode-switch absorbs cloud.hunt-lid-open and cloud.gfhome-homing (the connect made with the lid open, the hunt judged lid-open with its Z cycle, the re-hunt waited out after the close, the switch back, then $H judged by gfhome's own "homing complete" line with its motion windows; precheck homing_mode = gfcloud). kernel.fire-line is auto with the HV-not-good precheck, camera.snapshot is auto (a second frame with the lid lamp off differs and is smaller), motion.jog-roundtrip is auto (the head accelerometer per leg, the supervisor's own witness). The remaining attended tests use Ready gates and act(); the head's beam detector and the button LEDs replace the eye, leaving two confirms: the emission witness's mark and the app's display in cloud.pause-resume. Proof: tests/test_operator.py (the channel, the precheck, the journal), the cloud replays re-targeted to the merged round trip over a fake grbl and the bench excerpts, 196 host tests green, coverage lint 0 uncovered. Every re-ported attended test is owed one bench run on the next dev image (BRINGUP). Catalog consequence: the merged and reclassified tests' implementation hashes move; nothing else is invalidated.
This commit is contained in:
@@ -13,6 +13,7 @@ import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
@@ -152,6 +153,137 @@ def sysfs_write(attr, value):
|
||||
f.write(str(value))
|
||||
|
||||
|
||||
# ------------------------------------------------------ head accelerometer
|
||||
|
||||
# The head accelerometer (LIS2HH12 on i2c-3 at 0x1e): the supervisor's
|
||||
# motion-liveness witness, and the catalog's. Resolved by bus address,
|
||||
# never by iio index. The raw sysfs read is slow (~150 ms), which still
|
||||
# lands several samples in a one-second move; the verdict is peak-to-peak
|
||||
# on X or Y, against the thresholds forgectrl's probe established (a live
|
||||
# head reads 1800-2900 on its probe move, a wedged one <= 250).
|
||||
HEAD_ACCEL_I2C = "3-001e"
|
||||
ACCEL_P2P_MOVING = 800
|
||||
|
||||
|
||||
def head_accel_dir():
|
||||
"""The head accel's iio directory, or None. GF_IIO_ROOT overrides the
|
||||
/sys/bus/iio/devices root (host tests)."""
|
||||
root = os.environ.get("GF_IIO_ROOT") or "/sys/bus/iio/devices"
|
||||
try:
|
||||
names = sorted(os.listdir(root))
|
||||
except OSError:
|
||||
return None
|
||||
for n in names:
|
||||
d = os.path.join(root, n)
|
||||
try:
|
||||
target = os.readlink(d)
|
||||
except OSError:
|
||||
target = ""
|
||||
if HEAD_ACCEL_I2C in target or HEAD_ACCEL_I2C in n:
|
||||
return d
|
||||
# a plain directory (host fixture): its name carries the address
|
||||
try:
|
||||
with open(os.path.join(d, "name")) as f:
|
||||
if HEAD_ACCEL_I2C in f.read():
|
||||
return d
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def head_accel_read(d):
|
||||
"""(x, y) raw counts, or None."""
|
||||
out = []
|
||||
for axis in ("x", "y"):
|
||||
try:
|
||||
with open(os.path.join(d, "in_accel_%s_raw" % axis)) as f:
|
||||
out.append(int(f.read().strip()))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return tuple(out)
|
||||
|
||||
|
||||
class AccelSampler:
|
||||
"""Samples the head accelerometer in a thread for as long as it is
|
||||
running; `p2p(t0, t1)` is the peak-to-peak on X and Y over the samples
|
||||
in that window and how many there were. Used as a context manager
|
||||
around a motion the test wants the head to have made."""
|
||||
|
||||
def __init__(self, period=0.0):
|
||||
self.dir = head_accel_dir()
|
||||
self.period = period
|
||||
self.samples = [] # (t, x, y)
|
||||
self._stop = threading.Event()
|
||||
self._th = None
|
||||
self.errors = 0
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self.dir is not None
|
||||
|
||||
def __enter__(self):
|
||||
if self.dir is not None:
|
||||
self._th = threading.Thread(target=self._loop, daemon=True, name="forgetest-accel")
|
||||
self._th.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
self._stop.set()
|
||||
if self._th is not None:
|
||||
self._th.join(2.0)
|
||||
return False
|
||||
|
||||
def _loop(self):
|
||||
while not self._stop.is_set():
|
||||
v = head_accel_read(self.dir)
|
||||
if v is None:
|
||||
self.errors += 1
|
||||
else:
|
||||
self.samples.append((time.time(), v[0], v[1]))
|
||||
if self.period:
|
||||
self._stop.wait(self.period)
|
||||
|
||||
def p2p(self, t0, t1=None):
|
||||
"""(p2p_x, p2p_y, n) over [t0, t1]; (0, 0, 0) with no samples."""
|
||||
t1 = time.time() if t1 is None else t1
|
||||
xs = [x for t, x, _y in self.samples if t0 <= t <= t1]
|
||||
ys = [y for t, _x, y in self.samples if t0 <= t <= t1]
|
||||
if not xs:
|
||||
return 0, 0, 0
|
||||
return max(xs) - min(xs), max(ys) - min(ys), len(xs)
|
||||
|
||||
|
||||
# ----------------------------------------------------------- button LEDs
|
||||
|
||||
BUTTON_LEDS = ("button_led_1", "button_led_2", "button_led_3")
|
||||
|
||||
|
||||
def leds_root():
|
||||
r = os.environ.get("GF_LEDS_ROOT") or "/sys/class/leds/"
|
||||
return r if r.endswith("/") else r + "/"
|
||||
|
||||
|
||||
def button_leds():
|
||||
"""The three button LED brightnesses, or None where unreadable."""
|
||||
out = []
|
||||
for name in BUTTON_LEDS:
|
||||
try:
|
||||
with open(leds_root() + name + "/brightness") as f:
|
||||
out.append(int(f.read().strip()))
|
||||
except (OSError, ValueError):
|
||||
out.append(None)
|
||||
return out
|
||||
|
||||
|
||||
def button_lit():
|
||||
"""True when any button LED is on, False when all three read 0, None
|
||||
when none is readable."""
|
||||
vals = [v for v in button_leds() if v is not None]
|
||||
if not vals:
|
||||
return None
|
||||
return any(v > 0 for v in vals)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- init.d
|
||||
|
||||
def initd(service, action, timeout=60):
|
||||
|
||||
Reference in New Issue
Block a user