diff --git a/README.md b/README.md index d786c09..c3198d6 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ > [community forum](https://community.openglow.org). Open-source firmware for Glowforge brand CNC lasers. ForgeFIRM replaces the -cloud-dependent factory software on the **stock control board** — no hardware -modification — and gives the machine a local controller, a local web control +cloud-dependent factory software on the **stock control board** - no hardware +modification - and gives the machine a local controller, a local web control panel, and a standard Grbl interface. * [Latest Release](https://github.com/openglow-org/forgefirm/releases) @@ -50,7 +50,7 @@ machine is idle:** The control board is common to Glowforge Basic, Plus, and Pro. The 5 MP (OV5648) camera modules are fully supported and hardware-validated. The 8 MP -(OV8856) modules found in "HD" units have a complete capture path — the kernel +(OV8856) modules found in "HD" units have a complete capture path - the kernel patches, device tree and sensor-aware capture profile they need are all in the build, but it has never run on an 8 MP machine, so treat it as untested. diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 47570a7..b21fc37 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -1,11 +1,11 @@ # ForgeFIRM bring-up status & cold-start runbook -Last updated: **2026-08-26**. +Last updated: **2026-09-02**. This is the present state of the machine, the bench runbook, the measured -hardware facts, and the authoritative list of open work. **The dated record — +hardware facts, and the authoritative list of open work. **The dated record - bench campaigns, drills, scope gates, the audit remediation, the acceptance -campaigns — is [`CAMPAIGN-LOG.md`](CAMPAIGN-LOG.md)**; come here for what is +campaigns - is [`CAMPAIGN-LOG.md`](CAMPAIGN-LOG.md)**; come here for what is true now, go there for how it was proven. Read together with: @@ -24,12 +24,12 @@ hardware-validated.** 39.98 kHz; `CONFIG_PREEMPT=y`; both OV5648 cameras on the mainline imx-media pipeline with VPU JPEG encode; A/B slot install, signed `.fw` releases and factory restore. -- **GRBL mode cuts real jobs.** grblHAL (the stock core plus one local fix, and +- **GRBL mode cuts real jobs.** grblHAL (the stock core plus two local fixes, and the ForgeFIRM driver) speaks Grbl 1.1f over TCP:23; LightBurn drives motion, the laser and the camera stream. First light landed 2026-08-11. - **Cloud mode runs the factory experience end to end**, deliberately kept and maintained: sign-in, camera homing, prints, pause/resume, cancel. -- **forgectrl is the one machine-services daemon** behind both modes — +- **forgectrl is the one machine-services daemon** behind both modes - cooling engine, controller supervision, pulse-device broker, motion-liveness gate, cameras, telemetry, settings, diagnostics, logging, updates, web panel. - **Laser safety is hardware-first and bench-proven.** The chain @@ -42,15 +42,15 @@ hardware-validated.** modes (cancel-and-return on a lid or interlock open, button pause/resume), bench-validated 2026-08-17. - **Releases are gated by the acceptance tool** (`forgetest`, dev image only): - a 46-test catalog, domain-scoped inheritance, an always-required safety core, + a 54-test catalog, domain-scoped inheritance, an always-required safety core, a bench actuator that works the lid, the interlock and the button so most of the operator's part runs unattended, and a release gate that reads the - exported artifact. The latest full campaign, on dev image `20260824230512`, - satisfied 45 of 45 from nothing in 27 minutes (36 tests unattended) and - authorizes a release; **no release is cut yet.** + exported artifact. The latest full campaign satisfied every test from + nothing and authorizes a release (the record is in `CAMPAIGN-LOG.md`); + **no release is cut yet.** -Current bench state: dev image `20260824230512`, the board resting on the SD -dev image (eMMC slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in +Current bench state: dev image `20260901225926` on the SD card (eMMC slot 1 = +factory 2024, slot 2 = a ForgeFIRM pin-file image, archives in `/data/forgefirm/archive`). ## The bench @@ -64,7 +64,7 @@ dev image (eMMC slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in works: U-Boot (in eMMC boot0) reads the saved env at eMMC user-area 0x80000, which selects the boot device (bench board: `mmcdev=0 mmcroot=/dev/mmcblk1p1` = SD), then loads `/boot/uEnv.txt` and `/boot/zImage` - from that rootfs partition — so the kernel always comes from the burned SD. + from that rootfs partition - so the kernel always comes from the burned SD. Full map: "eMMC boot & recovery architecture" in the facts bank below. **Module-only changes hot-swap**: scp `glowforge.ko` over `/lib/modules//extras/`, then `rmmod glowforge && modprobe glowforge`. @@ -75,7 +75,7 @@ dev image (eMMC slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in - **Module hot-swap vs kernel re-stamps**: the hot-swap only loads if the module was built against the FLASHED kernel's patch state. Any edit under the kernel recipe's overlay (e.g. `glowforge.dts`) re-stamps - `CONFIG_LOCALVERSION_AUTO` — and the stamp does NOT reproduce by reverting + `CONFIG_LOCALVERSION_AUTO` - and the stamp does NOT reproduce by reverting the edit (the kernel patch tree is a fresh git commit each `do_patch`, not sstate-restored), so after any overlay edit the module can only ship with a full image flash. **Kernel/BSP changes therefore ride one image flash, @@ -91,11 +91,11 @@ dev image (eMMC slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in - **fwup lab (host)**: a host directory (``) holds host-built `fwup-0.14.2` (factory-era) and `fwup-v1.16.0` under `bin/` and the DEV signing keypair `devkeys/fwup-key.{priv,pub}` (`fwup-key-raw.pub` = raw - 32-byte form — what fwup 0.14.2 expects; 1.x reads both). Cross-version + 32-byte form - what fwup 0.14.2 expects; 1.x reads both). Cross-version compatibility is proven both ways (modern-packed signed archives apply with - 0.14.2; modern fwup verifies and applies the factory `.fw` — signer key + 0.14.2; modern fwup verifies and applies the factory `.fw` - signer key 2017-05-001.pub). **The production release key is held offline by the - operator** — the installer embeds its public key, so releases sign with that + operator** - the installer embeds its public key, so releases sign with that key only. Pack releases with `scripts/mkfw.sh`; the full pipeline is `scripts/release.sh`, invoked as: `FWUP=/bin/fwup-v1.16.0 FWUP_COMPAT=/bin/fwup-0.14.2 @@ -108,19 +108,16 @@ dev image (eMMC slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in the end of the session. Bench tools worth keeping live in `forgefirm/scripts/bench/` and ship on the dev image under `/usr/share/forgetest/bench/`. -- **Shell gotchas** (cost real time): PowerShell mangles embedded double quotes - in git-commit here-strings (avoid `"` in messages); `wsl -- bash -c '...'` - eats `$VAR` expansions (use script files run via PowerShell, not Git Bash, - which MSYS-mangles `/mnt/c` paths). ## Running the controller (grblHAL-glowforge on the board) -Source: the `grblHAL-glowforge` sibling repo — the **canonical grblHAL driver +Source: the `grblHAL-glowforge` sibling repo - the **canonical grblHAL driver repo** (github.com/openglow-org/grblHAL-glowforge, branch `main`): core as a submodule at `src/grbl` (→ openglow-org/grblHAL-core fork, branch `forgefirm` = upstream -master plus one local commit, the `step_us_min` buffer sizing that keeps a -fortified build from aborting in `settings_init`; the settings-write crash fix -merged upstream 2026-08-04 as grblHAL/core PR #999). `driver.c` implements the +master plus two local commits: the `step_us_min` buffer sizing that keeps a +fortified build from aborting in `settings_init`, and the laser-power +re-assert on the first segment after a hold, which is what keeps a resumed M3 +job lit; the settings-write crash fix merged upstream as grblHAL/core PR #999). `driver.c` implements the HAL; machine constants live in `src/boards/glowforge.h`. **The controller is spawned and supervised by forgectrl**: the supervisor @@ -129,29 +126,29 @@ child, respawns it on a crash (after safing the machine), and switches modes live via `POST /mode` / the Status-tab selector. The `grblhal` and `gfcloud` init scripts defer to it (they remain as manual emergency levers, routed through `POST /controller/stop|start`). The pulse device arrives as a -broker-inherited fd (`GF_PULSE_FD`) — the device never closes across mode +broker-inherited fd (`GF_PULSE_FD`) - the device never closes across mode switches, homing handovers or respawns, so the 40 V rail never cycles as a side -effect — and the supervisor verifies **physical motion** (head-accelerometer +effect - and the supervisor verifies **physical motion** (head-accelerometer liveness probe) before the first controller spawn of each session. Architecture: a wall-paced producer thread runs the core stepper ISR against a virtual step clock (1000× machine tick) and maps step events to pulse bytes; a SCHED_FIFO shipper feeds `/dev/glowforge` through a bounded queue; a recursive core mutex stands in for interrupt masking. `GFSINK` unset = null-sink mode -(full engine, no hardware I/O — host testing and CI). +(full engine, no hardware I/O - host testing and CI). 1. Build: `bash /forgefirm/scripts/bench/build-glowforge.sh` in the build - environment (from Windows, launch it through the WSL distro from PowerShell - — Git Bash mangles `/mnt/c` paths). Produces `build-arm/grblHAL_glowforge` + environment (on a Windows host, inside the WSL distro). Produces + `build-arm/grblHAL_glowforge` in the checkout (`-O1 -g`; machine constants force-included into the core: 53.333 µsteps/mm XY @ ×8, 2.832 half-steps/mm Z, 0.417" Z travel, - 12000 mm/min max, 700/590 mm/s² accel — factory-derived, see + 12000 mm/min max, 700/590 mm/s² accel - factory-derived, see `puls_profile.py`). 2. Deploy: move the new binary over `/usr/bin/grblHAL_glowforge` (mv replaces the inode, so the running instance is untouched), then kill the running - controller — the supervisor respawns it on the new binary within about a + controller - the supervisor respawns it on the new binary within about a second. -3. Standalone start (bench/debug only — requires forgectrl stopped, since the +3. Standalone start (bench/debug only - requires forgectrl stopped, since the broker's exclusive hold on `/dev/glowforge` makes any self-open fail EBUSY): `cd /data && GFSINK=/dev/glowforge grblHAL_glowforge -p 23 -e /data/EEPROM-glowforge.DAT`. Env knobs: `GFSINK_RATE` (machine tick, default @@ -162,7 +159,7 @@ core mutex stands in for interrupt masking. `GFSINK` unset = null-sink mode applies the full analog machine config at init either way (×8 modes, decay 1, motor_lock 8, laser latched, PIC hold currents) and swaps PIC run/hold currents around motion. Each motion run logs a producer-stats line - (callbacks, µs/call, max-behind, clamped) — `clamped` should stay 0. + (callbacks, µs/call, max-behind, clamped) - `clamped` should stay 0. 4. Connect LightBurn/UGS to `:23`, or jog raw: `$J=G91X40F1200`. `^X` mid-motion aborts via kernel `cnc/stop` (controlled decel) and raises an alarm; TCP disconnects never kill the process (the dead-man fd stays held). @@ -175,15 +172,15 @@ still the one loaded at start until the controller restarts (a mode switch, a `POST /controller/stop` + `start`, or a boot). Verified host-side: after a runtime `$35=0` the shipped duties stay floored. -**Stored `$`-settings beat freshly baked defaults** — after changing +**Stored `$`-settings beat freshly baked defaults** - after changing `GLOWFORGE_DEFAULTS` values, run `$RST=$` once on the board (settings persist in the eeprom file in `/data`). **Protocol-loop pacing is fd-blocking.** `serial_wait()` drains TX then `ppoll()`s the listen/client fds with a state-dependent timeout: idle and alarm at 10 ms (1 ms while a delay callback is pending), motion at 200 µs, and the -parked states — a completed feed hold, a parked door ajar or closed, and sleep -— at the coarse idle poll, while the motion sub-phases (`Hold_Pending` decel, +parked states - a completed feed hold, a parked door ajar or closed, and sleep + - at the coarse idle poll, while the motion sub-phases (`Hold_Pending` decel, `Parking_Retracting`/`Resuming`) keep the tight pace. Measured on the bench: idle 2.7 %, active move 34–35 %, parked 2.7–3.0 %. Client RX is armed only while the ring has a full read's worth of room, so a flow-control-violating @@ -274,21 +271,21 @@ planner left the following moves cutting at a stale duty, or dark. Contract rules enforced structurally: a power byte leads every kernel run before any fire bit (a run start resets duty to ~100 %), transitions are coalesced per tick so power bytes are never consecutive, and power bytes cost -no machine tick. Fire only ever rides motion segments of laser blocks — jogs, -G0 and homing are fire-free by construction — and the end-of-data backstop +no machine tick. Fire only ever rides motion segments of laser blocks - jogs, +G0 and homing are fire-free by construction - and the end-of-data backstop covers every stream end. **Duty persists after end-of-data** (PWMSAR retains its last value): the laser-off guarantee rests entirely on FIRE. -**Arming — the operator's button press is required.** The first laser-on of a +**Arming - the operator's button press is required.** The first laser-on of a job (M3/M4, planner-synced) refuses outright if a coolant fire gate stands or if no head is detected (`ALARM:3`, "laser fire blocked: no head detected"), else forces the run fan profile on, unlocks the kernel laser latch, lights the -button white and blocks the gcode stream — pumping real-time traffic — until +button white and blocks the gcode stream - pumping real-time traffic - until the operator presses the physical button (EV_SW bit 2), a soft reset aborts, a lid or interlock open cancels, or `laser_button_timeout_s` (default 300 s) expires into alarm 3. The coolant verdict is re-checked immediately after the wait, before the window opens. The armed window survives S changes and M5/M3 -toggles (no re-prompt mid-job) and closes — relocking the latch — at program +toggles (no re-prompt mid-job) and closes - relocking the latch - at program end (`M2`/`M30`/`%`), when the sender connection changes, after `laser_disarm_s` (default 60 s) of spindle-off idle, or immediately on alarm/homing/reset/stream fault. The disarm grace counts down in Hold, Door and @@ -339,7 +336,7 @@ factory 2.6.0-2228 session; measured numbers in the facts bank). the head returns to the position the job started from **with the lid still open**, the kernel latch relocks and the armed window closes. The return-home park ignores the lid and always runs to completion. The next job - re-arms with a button press — the same press the hardware button latch needs, + re-arms with a button press - the same press the hardware button latch needs, so software and hardware agree by construction. - **During the pre-run button wait:** a lid or interlock open cancels the job with the reason named (clean soft reset, no alarm); a press with the lid open @@ -372,9 +369,9 @@ doors = bit 3 (the series combination the safety chain itself uses), remote interlock = bit 5 (inverted sense), button = bit 2. The door signal is hidden from the core while it is IDLE, JOG or HOMING (`gfsw_visible`) so a lid cycle at idle cannot park the controller in `Door:0` and leave a sender waiting. -**hv_enable (bit 4) is never gated on** — it is the readback of the chain's +**hv_enable (bit 4) is never gated on** - it is the readback of the chain's HV_ENABLE output, telemetry only. **The interlock latch (bit 6) is not gated on -either** — the hardware chain enforces it. No switch device (host builds) = no +either** - the hardware chain enforces it. No switch device (host builds) = no capability advertised. `GF_SWITCH_FILE` is the file-backed EV_SW word that lets null-sink builds drive these edges in CI. @@ -384,15 +381,15 @@ Runtime-selectable through `homing_mode` in `/data/forgefirm.conf` (forgectrl `GET/POST /settings`, panel selector); the driver re-reads the file on every `$H`: -- `gfcloud` — factory camera homing via the Glowforge web service. **Live +- `gfcloud` - factory camera homing via the Glowforge web service. **Live verified**; a full cycle runs in 50–65 s. -- `switches` — the planned limit-switch cycle (falls through to the core, still +- `switches` - the planned limit-switch cycle (falls through to the core, still disabled `$22=0`). -- `none` — `$H` rejects with error 5. +- `none` - `$H` rejects with error 5. Architecture: `glowforge_homing.c` registers a driver `$H` that shadows the core's; for gfcloud it suspends the stream engine (only from a fully idle -kernel — closing the flock'd fd mid-program is an e-stop), spawns +kernel - closing the flock'd fd mid-program is an e-stop), spawns `/usr/sbin/gfhome.py` (config `/data/etc/gfhome.conf`, first-run copy from `/etc/gfhome.conf.sample`), pumps the protocol so senders keep getting status, then reacquires the device and re-applies the analog config and `step_freq`. @@ -400,7 +397,7 @@ then reacquires the device and re-applies the analog config and `step_freq`. `ALARM:18` like a failed core cycle (`gfcloud_home_timeout_s`, default 300). The runner drives the GFUIService dispatch itself (the stock `run()` loop can neither stop nor close the socket) and treats hunt + ≥1 -accelerometer-witnessed motion window + quiet (10 s) as complete — the modern +accelerometer-witnessed motion window + quiet (10 s) as complete - the modern v2.6.0 sequence, captured from a live service session, is settings → hunt → lid_image → single corner move → lid_image → silence. It then re-homes the lens against the hall for a deterministic Z. **A quiet service without an @@ -409,7 +406,7 @@ accel-witnessed motion window is a failure, not a homing.** Position semantics: factory home = machine origin (back-left corner, +Y = FRONT, workspace all-positive 0..495 × 0..279); Z top-of-travel = 10.6. `gfcloud_home_x/y/z` calibrate the post-home coordinates once measured -(defaults 0 / 0 / Z max). GRBL mode permits unhomed cutting — position shows +(defaults 0 / 0 / Z max). GRBL mode permits unhomed cutting - position shows counters-only and painted red until anchored. ## The machine-services daemon (forgectrl, port 8080) @@ -420,15 +417,15 @@ supervision**, the **pulse-device broker**, the **motion-liveness gate**, the **cooling engine** (single owner of fans/pump/TEC/heater for both modes), plus cameras, telemetry, settings, diagnostics, the web panel, updates and the **logging tree**. It runs under a respawn wrapper (its init script); a -restarted daemon retakes supervision once the machine is idle — an unmanaged +restarted daemon retakes supervision once the machine is idle - an unmanaged controller left running mid-move is replaced at idle, not adopted (the old inherited fd cannot be taken over). The meta-forgefirm recipe pins its SRCREV in `forgectrl-pin.inc` (bump deliberately after pushing) and installs the sysvinit script from the repo's `init/`; bench builds cross-compile with `forgefirm/scripts/bench/build-forgectrl.sh`. The **machine-services -contract** — EV_SW switch map, sensor conversions, hardware single-writer +contract** - EV_SW switch map, sensor conversions, hardware single-writer ownership, cooling channels, mode supervision, pulse-device ownership, logging -— is [forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/) on the documentation site. + - is [forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/) on the documentation site. Every state-changing endpoint requires the first-boot bearer token in `/data` (embedded in the panel), a Host address-literal check, and @@ -438,7 +435,7 @@ additionally require the physical button held. One ulfius daemon serves it all: -- `GET /` — the tabbed control panel (**Status / Machine / GF Cloud / GRBL / +- `GET /` - the tabbed control panel (**Status / Machine / GF Cloud / GRBL / Diagnostics / Logs / System**; sources in `forgectrl/src/ui/`, `index.html`, `panel.js`, `forms.js`, `help.js` and `theme.css`, bundled into the binary by `embed.cmake`; `tools/devserver.py` and the repo's `.devcontainer/` serve the @@ -449,13 +446,13 @@ One ulfius daemon serves it all: reboot. All settings controls disable (with a banner) while the machine is not idle **or a diagnostic is running**. `/?action=stream|snapshot` remain the mjpg-streamer-compatible aliases (lid camera; LightBurn uses the stream one). -- `GET /status` — motion state and true machine position (kernel step counters - anchored at homing via `/run/grblhal.homed` — the Grbl socket is never polled, +- `GET /status` - motion state and true machine position (kernel step counters + anchored at homing via `/run/grblhal.homed` - the Grbl socket is never polled, a connection there displaces the sender), coolant temps, pump/TEC, all four fan tachs, the sensed laser evidence (emission samples, HV current, lid IR), faults, the safety switches via `EVIOCGSW` (head = real presence, i.e. the head sysfs group exists), and a `diag` flag for the UI lock. -- `GET/POST /settings` — the shared machine settings store +- `GET/POST /settings` - the shared machine settings store (`/data/forgefirm.conf`, 0600, validated keys, empty-value-clears via query params; `gf_password` write-only). **Writes 409 unless `cnc/state` is idle** and 409 while a diagnostic owns the hardware; a multi-key POST lands as one @@ -465,51 +462,51 @@ One ulfius daemon serves it all: `laser_disarm_s`, `rail_settle_s`, `lid_lamp_idle`, `lid_policy`, `cloud_pause_backtrack_ticks`, `cloud_resume_lead_ticks`, `cloud_hold_max_s`, the twelve `log__disk|_remote` levels and `syslog_server|port|proto`. -- `GET /mode`, `POST /mode?controller=grbl|cloud` — the supervisor: current +- `GET /mode`, `POST /mode?controller=grbl|cloud` - the supervisor: current mode, controller state (`running | stopped | standby | motion-fault`), pid, and the motion-liveness verdict (`verified | unverified | fault`); the POST is the live idle-gated mode switch and the retry lever after a motion fault. -- `POST /controller/stop|start` — the routed emergency levers the init scripts +- `POST /controller/stop|start` - the routed emergency levers the init scripts use. Stop writes `cnc/stop` + `cnc/laser_latch=1` **before** the SIGTERM (kernel-level, instantaneous) and holds supervision suspended. - `POST /cool/state` (job-state reports from the active controller, level- triggered ~1 Hz) and `GET /cool/status` (engine phase, verdict, temps, report age). The verdict the controllers enforce is the `/run/forgefirm/cooling.state` file. -- `POST /diag/flow-verify|flow-calibrate|abort`, `GET /diag/status` — the +- `POST /diag/flow-verify|flow-calibrate|aa-offset-calibrate|abort`, `GET /diag/status` - the diagnostics runner (below). -- `GET /cam/stream?cam=lid|head` — multipart MJPEG at half the sensor's frame +- `GET /cam/stream?cam=lid|head` - multipart MJPEG at half the sensor's frame in each axis, 1296×972 on a 5 MP machine (2×2 Bayer-superpixel demosaic, JPEG q75; `FORGECTRL_STREAM_Q` overrides, `FORGECTRL_STREAM_FPS` caps the frame rate, unset/0 = sensor max). -- `GET /cam/snapshot?cam=lid|head&res=full|half&q=1..100` — single JPEG, +- `GET /cam/snapshot?cam=lid|head&res=full|half&q=1..100` - single JPEG, default the sensor's full frame, 2592×1944 on a 5 MP machine (own MIT bilinear demosaic). -- `GET /cam/status` — JSON (running/cam/clients/frames/fps/fps_cap/encoder/ +- `GET /cam/status` - JSON (running/cam/clients/frames/fps/fps_cap/encoder/ buffers/sensor, the stream + snapshot geometry the fitted sensor implies, and the privacy gate's `capture_allowed` / `stopped_by_lid`). Stream and snapshot answer 409 while the lid is open. - `GET /slots`, `POST /boot`, `POST /update/check|download|apply|upload`, - `GET /update/status`, `POST /restore/factory`, `POST /system/reboot` — the + `GET /update/status`, `POST /restore/factory`, `POST /system/reboot` - the A/B update manager ([install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/)). Upload is auth + idle + job gated; a booted-slot write is refused under any `root=` spelling. -- `GET /logs`, `GET /logs/tail`, `POST /logs/export` — the logging tree +- `GET /logs`, `GET /logs/tail`, `POST /logs/export` - the logging tree (below). -- `GET /fuse-identity` — serial, derived hostname and the SRK password, behind +- `GET /fuse-identity` - serial, derived hostname and the SRK password, behind the token AND the physical button; fetched on demand only. **Panel conventions:** the header identifies the machine by its **fuse identity** (the factory hostname derived from the OCOTP serial), regardless of any cloud identity override. **Units** are a display-only preference (`ui_units`): the backend stores metric, and saves post only fields whose -display string changed. **Position always shows** — counters-only and painted +display string changed. **Position always shows** - counters-only and painted red while unreferenced, normal once anchored. **Camera engine.** One worker owns the V4L2 node persistently (media-ctl / v4l2-ctl sequences identical to `gfhardware/cam.py`, factory exposure/gain/WB, software hflip in the demosaic); it starts on demand and tears down fully after 10 s idle so gfhardware one-shot grabs still work. **Privacy gate: neither -camera captures unless the lid is closed** — `machine_lid_closed()` (EV_SW +camera captures unless the lid is closed** - `machine_lid_closed()` (EV_SW bit 3, fail-closed) is checked at every entry point and once per frame, so an open lid refuses stream and snapshot with HTTP 409 and a lid opened mid-capture tears the pipeline down; `gfhardware.cam.capture()` enforces the same rule for @@ -518,7 +515,7 @@ disables it, and the factory's lid-open focus hunt now fails as a result ([the video pipeline](https://docs.forgefirm.org/technical/forgefirm/video-pipeline/)). Geometry, Bayer depth and the manual control set come from a **sensor profile** chosen by whichever driver bound on that camera's I2C bus, so one image serves both the 5 MP -OV5648 (2592×1944) and the 8 MP OV8856 (3264×2448) — both 8-bit BGGR, so the +OV5648 (2592×1944) and the 8 MP OV8856 (3264×2448) - both 8-bit BGGR, so the capture word and the demosaic are the same and only the geometry changes; `/cam/status` reports the model and the frame sizes that follow from it. A frame the capture queue flags errored is dropped rather than demosaiced, @@ -527,18 +524,18 @@ engine; `/cam/status` carries the running `health` counts (`src/camhealth.c`, host test `camhealth_test`). The cameras share the hardware video-mux and the NEWEST request wins it: **streams preempt** (the current stream's clients end cleanly), **snapshots borrow** (pause, switch, -grab one frame, switch back — a ~1–2 s freeze). The per-camera lamp +grab one frame, switch back - a ~1–2 s freeze). The per-camera lamp (`pic/lid_led` / `head/white_led`) is raised to `FORGECTRL_LAMP` (default 132) while capturing and restored to the resting level on idle. The resting lid lamp is the `lid_lamp_idle` setting (0–255, default 236), asserted at daemon start, on a live settings change, and at every controller spawn. Measured performance (bench): **15.0 fps sustained** at 1296×972, sensor- -limited — NEON superpixel→YUV420 convert (18–20 ms) plus CODA960 VPU JPEG +limited - NEON superpixel→YUV420 convert (18–20 ms) plus CODA960 VPU JPEG encode (7 ms) on cached (non-coherent) V4L2 capture buffers, so there is no bounce copy; daemon ~41 % CPU with one viewer. Full-res snapshot 2.4 s warm / 2.7 s cold. Fallbacks, each bench-verified: `FORGECTRL_NO_CACHED_BUFS` (bounce -copy, needed on a kernel without the `allow_cache_hints` patch — detected via +copy, needed on a kernel without the `allow_cache_hints` patch - detected via the `MMAP_CACHE_HINTS` capability bit), `FORGECTRL_NO_NEON` (scalar convert, bit-identical), `FORGECTRL_NO_VPU` (libjpeg; also the snapshot path). The default path is newer and bench-proven: the GC880 GPU demosaic feeding the @@ -555,7 +552,7 @@ stream directly** while jogging from the same session; motion coexistence is proven (`clamped 0`, max behind 4.5–7.2 ms of the 200 ms queue at 15 fps). Run by hand: `/usr/bin/forgectrl &` after `/etc/init.d/forgectrl stop` (kill -before scp when redeploying — text-file-busy). It logs through syslog +before scp when redeploying - text-file-busy). It logs through syslog (`/data/log/forgefirm/forgectrl/forgectrl.log`; a terminal, or `FFLOG_STDERR=1`, echoes the lines). @@ -608,7 +605,7 @@ override and wins for the process lifetime). **Flow verification, as it runs:** a one-shot check at flood start (M8) heats the loop at `cool_flow_heater_pct` for `cool_flow_check_s` and discriminates on -downstream temperature RISE, with periodic re-checks every `cool_recheck_s` — +downstream temperature RISE, with periodic re-checks every `cool_recheck_s` - a stopped pump is undetectable any other way. A check starts only once the sensors agree and the downstream reading is stationary (split-half mean difference, not peak-to-peak). An over-limit check is a **suspicion**, not a @@ -623,7 +620,7 @@ a JOG gets a jog-cancel. rsyslog is the system logger and the only log writer. forgectrl and the grblHAL driver emit through the shared non-blocking `fflog` emitter (drops, never waits -— a stalled log daemon can never park a controller thread), gfcloud/gfhome + - a stalled log daemon can never park a controller thread), gfcloud/gfhome through `SysLogHandler`, the kernel through `imklog`; a controller's stray stdout/stderr rides a per-controller `logger` relay under its own name. Tree: `/data/log/forgefirm/{forgectrl,grblhal,gfcloud,gfhome,kernel,system}/`, @@ -631,7 +628,7 @@ size-capped and rotated at boot and hourly (the `forgefirm-logging` recipe renders the rsyslog rules from the settings at S19 via `forgectrl --render-syslog`). Levels (`log__disk` / `_remote`) and the remote target (`syslog_server/port/proto`) are machine -settings **applied at reboot** — the panel's Logs tab shows configured vs. +settings **applied at reboot** - the panel's Logs tab shows configured vs. effective and offers the reboot, plus a live viewer and a sanitized `tar.gz` export for issue reports (`POST /logs/export`; `src/sanitize.c` replaces serial, hostname, cloud credentials, panel token, SSID/PSK, IPs, MACs and @@ -640,21 +637,21 @@ e-mails with stable placeholders). Design and contract: ## Release acceptance (forgetest, port 8090) -The release acceptance tool — catalog, campaigns, domain fingerprints, +The release acceptance tool - catalog, campaigns, domain fingerprints, inheritance, the always-required core, invalidate-all, the release gate and the -coverage currency rule — is specified on the site (Developers, "Acceptance"); the tool lives in +coverage currency rule - is specified on the site (Developers, "Acceptance"); the tool lives in `forgetest/` and ships only on the dev image (`/etc/init.d/forgetest`, HTTP -:8090). It is **bench-validated**: the full campaign on dev image -`20260824230512` (`c-20260824231028-b7ca`) satisfied 45 of 45 from nothing, -36 of them unattended with the bench actuator in the loop, in 27 minutes, and -the export reads "Release authorized: YES" for that image's manifest. That +:8090). It is **bench-validated**: a full campaign has satisfied the whole catalog +from nothing with the bench actuator in the loop (the record is in +`CAMPAIGN-LOG.md`), and its export reads "Release authorized: YES". That authorizes a release; it is not one until `releases/v/acceptance.json` is committed. -- **Catalog: 46 tests** in `forgetest/forgetest/suite/`, every one a port of a +- **Catalog: 54 tests** in `forgetest/forgetest/suite/`, every one a port of a proven bench drill or a bench-verified check: the always-required core (`image.health`, `kernel.latch-locked-idle`, `kernel.k1-k2`, - `kernel.fire-line`), `forgectrl.*`, `logs.*`, `update.*`, `motion.*` + `kernel.deadman-close`, `kernel.backtrack-bounds`, `kernel.fire-line`, + `laser.emission-witness`), `forgectrl.*`, `logs.*`, `update.*`, `motion.*` (pacing, jog round-trip, liveness probe, cancel/abort, dead-man, the lid, interlock and button parity tests), `cooling.*` (flow verification, fans quiet after motion, a gate setting tripping and off by value, a fan under @@ -667,7 +664,7 @@ is committed. the cloud client driven from a local socket with a synthesized laser-free job, no account, no network, nothing on the bed). Tests that share a setup are merged; the `auto` tests stay separate for failure - isolation. 28 are `auto`, 9 `operator`, 9 `live`; with the bench actuator up, + isolation. 34 are `auto`, 10 `operator`, 10 `live`; with the bench actuator up, eight of the operator tests run in the unattended queue. - **The operator's part is asked for by name, not by popup** (the site, Developers, "Acceptance", "The operator's part"): a Ready prompt before a @@ -687,7 +684,7 @@ is committed. `do_deploy`), `forgefirm-image-manifest.bbclass` assembles them plus the layer content hashes into `/etc/forgefirm-manifest.json` (also deployed beside the image as `*.forgefirm-manifest.json`), and `scripts/manifest-from-tree.py` - computes the byte-identical thing on a workstation — the identity is + computes the byte-identical thing on a workstation - the identity is content-defined, independent of the checkout's commit or dirty state. **Component pins live in `-pin.inc`** (SRCREV + the PV that moves with it, nothing else) and @@ -696,7 +693,7 @@ is committed. platform change and invalidates everything. - **Baseline rule:** every test and bench tool is bracketed by a baseline pass (`forgetest/baseline.py`), against a fresh-boot reference taken once per boot - **after a power cycle** (a soft reboot leaves the lid lamp dark — the PIC + **after a power cycle** (a soft reboot leaves the lid lamp dark - the PIC lights it at power-on). Takeover runs capture the controller-owned kernel attributes on entry and write them back before forgectrl restarts, so a leftover `motor_lock` mask can never read as a wedged driver. @@ -767,7 +764,7 @@ is committed. The per-job range in raw counts is the record, and a ceiling, if one is ever wanted, is set in raw counts from it (`temp_calibrate.py supply-*` stays for a machine where the heatsink is reachable). -- **The SoC guards itself.** The i.MX6DL (rev 1.3) on-die monitor is +- **The SoC guards itself.** The i.MX6 Solo (rev 1.3) on-die monitor is `thermal_zone0` (`imx_thermal_zone`, the same node as `hwmon0`), governor `step_wise`, trips at **85 C passive** and **90 C critical** (the consumer-grade points the driver derives from the fuses: hot point 95, @@ -799,7 +796,7 @@ is committed. - **DRV8825 stepper drivers wedge on 40 V rail glitches** (factory board; the TMC2130s belong to the upgraded OpenGlow board only). A glitch can leave the drivers unserviceable: SDMA playback and the position counters run normally - while the motors produce nothing. The supply itself is fine — this is a + while the motors produce nothing. The supply itself is fine - this is a driver failure mode, not a marginal rail. The kernel drives their nRESET and nSLEEP pins (`reset-gpio` gpio3 18, `sleep-gpio` gpio3 16), but only as a pair inside every enable/disable cycle, together with the rail: the reset @@ -813,24 +810,24 @@ is committed. motion**; keep the rail up (every power-up is a wedge lottery), which is why the pulse-device broker exists and why there is no idle-rail-off policy. - **Motion liveness = the head accelerometer** (`glowforge.dts` `head-accel`, - i2c-3 @0x1e — resolve iio devices by bus path, never by index; lid = i2c-0 + i2c-3 @0x1e - resolve iio devices by bus path, never by index; lid = i2c-0 @0x1e, board = i2c-3 @0x1d). Signatures on an identical commanded move: real motion **1800–2900 counts peak-to-peak** on X/Y (noise floor at 1 g ≈ 16384), a genuinely dead/wedged axis ≤ ~250, and the rail-on / current-step jolt up to - ~700 — which is why the forgectrl probe gates controller start at + ~700 - which is why the forgectrl probe gates controller start at **p2p ≥ 800** (`P2P_MOVING`), writes `cnc/motor_lock=0` for its own move (a leftover mask from any tool must not read as a wedge) and settles 300 ms after the run-current step before sampling. A masked axis reads 144–480. gfhome requires at least one accel-witnessed motion window before a quiet - service counts as homed. Raw sysfs accel reads are slow (~150 ms each) — + service counts as homed. Raw sysfs accel reads are slow (~150 ms each) - enough for a binary verdict over a multi-second window, not for waveforms. - **Any probe/liveness move goes RIGHT (+X) first, then back**: a cable lives at the end of LEFT travel and must never be crushed. - **Rail-contact signature** (from the retired accelerometer-homing spike, relevant to any future contact sensing; tools `accel_fast.py`, `bump_seek.py`): creep baseline ≈0.5–2 k counts, contact jumps to 29–42 k - within ~4 ms (20–40×). But **slow approaches are near-silent** — belt - compliance turns slow-speed skipping into sub-threshold grinding — so any + within ~4 ms (20–40×). But **slow approaches are near-silent** - belt + compliance turns slow-speed skipping into sub-threshold grinding - so any contact-sensing scheme must strike fast. Direct I²C (unbind st-accel, CTRL1=0x6F = 800 Hz ODR) reads ~530 Hz from Python; st_accel sysfs one-shots are ~6 Hz and the kernel has no IIO triggers. @@ -895,7 +892,7 @@ is committed. (authoritative). - **Z**: bit 6 SET = lens UP = +Z (hardware-verified). Home = hall trigger at TOP; usable travel ≈ 30 half-steps ≈ 10.6 mm ≈ 0.417"; 0.3534 mm/half-step. - Never blind-drive Z — hall-supervised only. + Never blind-drive Z - hall-supervised only. - **XY**: 0.15 mm per full step; DIR bit set = −X / +Y (Y1/Y2 complementary). **+Y physically moves the gantry toward the FRONT.** Home corner (convention, for the planned limit-switch homing) = back-left (X min, Y min), workspace @@ -917,14 +914,14 @@ is committed. (6.4 % measured vs 6.3 % commanded at PWMSAR=8). - **Laser duty thresholds** (ladder on scrap at F300, constant power): the tube has two thresholds, far apart. The discharge **strikes between 2 % and - 3 %** duty — 2 % (PWMSAR 2) draws no measurable `hv_current` and leaves - nothing at all, 3 % (PWMSAR 3) draws current — but it does **not lase + 3 %** duty - 2 % (PWMSAR 2) draws no measurable `hv_current` and leaves + nothing at all, 3 % (PWMSAR 3) draws current - but it does **not lase usefully until 16 %** (PWMSAR 20), the lowest duty leaving a continuous mark. Between them (3–14 %) is a **dead band**: current flows and climbs, and each line shows only a spot at its start (the strike transient) with a - dark line after it. So the usable analog range is ~16–100 %, and `$35` - (`DEFAULT_SPINDLE_PWM_MIN_VALUE`) ships at **16** to hold every nonzero S - above it. Raw `hv_current` counts are a presence/absence witness only: the + dark line after it. So the usable range on the retired analog model was ~16 to 100 %. On the + density model that ships, `$35` (`DEFAULT_SPINDLE_PWM_MIN_VALUE`) is **10**, + the derived density floor (`laser_floor_density`). Raw `hv_current` counts are a presence/absence witness only: the per-rung means are non-monotonic at the top of the ladder and the signal has no characterized transfer function. - **Feed hold and resume in GRBL mode** (stream-measured on the null-sink @@ -952,7 +949,7 @@ is committed. - **Factory power model** (three cloud cuts of one 1" square, same location, material and speed, only the UI power setting changed, pulse files captured from each): the **power byte is pinned at 127** - in all three runs — three occurrences each, one as the cut begins and a + in all three runs - three occurrences each, one as the cut begins and a refresh every ~27 000 ticks (~2.7 s). Analog duty is never a power control. **Dose is FIRE-bit density on a fixed 7-tick period** (700 µs at `STfr` = 10 000, ~1.43 kHz), the on-count dithered between adjacent integers @@ -961,7 +958,7 @@ is committed. = 7 of 7 (0.9965, continuous). The period was exactly 7 in all 570 measured cycles of both dithered runs, and the mix of adjacent on-counts matches the fractional part exactly (PP 1 wants 1.371; 2-runs are 212 of 571 = 0.371). - The three **headers are identical** — the power setting never reaches the + The three **headers are identical** - the power setting never reaches the machine, so the whole model is service-side. Motion is identical too: 5420 steps, 101.62 mm, 10.81 s at 9.44 mm/s. **Density tracks velocity through corners**, by the same relative factor at every power setting (corner/cruise @@ -995,10 +992,11 @@ is committed. at 40 %: 25/25 correct classifications, plus all three settle cases. Settled- loop noise is 0.52 °C peak-to-peak but only 0.11 °C split-half, which is why the stationarity gate uses split-half means. Coolant windows: run ceiling - 33 °C, resume 31 °C (factory job-header CMrx/…); the factory's low side - (floors ≈1.0/4.0 °C, ~16 °C warm-up gate) is not implemented yet. The + 33 °C, resume 31 °C (factory job-header CMrx/…); the low side ships as the + coolant floor (`cool_temp_min`, 5 °C, a fire gate) and the warm-up gate + (`cool_temp_start`, 16 °C), the `COLD` and `WARMUP` verdicts. The coolant thermistor conversion is the factory B-equation recovered from the - v2.6.0 binary — derivation on [sensors](https://docs.forgefirm.org/technical/machine/sensors/); the old + v2.6.0 binary - derivation on [sensors](https://docs.forgefirm.org/technical/machine/sensors/); the old UAPI "best guess" linear formula was 3–5 °C high and everything derived from it had to be re-derived. The flow check's bands hold from 19 to 27 C, the loop heater's ceiling in a 20 C room, with the margin widening warm; above @@ -1037,7 +1035,7 @@ is committed. against a 177 ceiling plus drift), so no lamp change can trip them; each job still logs baseline and peaks. - **Emission and HV witnesses**: `cnc/laser_on_sampled` goes to its full 255 - 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 firing) and is the only live HV telemetry on this PSU (`hv_voltage` is grounded). **`cnc/laser_pgood` (J1_14, GPIO4_21) is the supply's power-good, @@ -1123,8 +1121,8 @@ is committed. the v2.6.0 app carries a complete but config-gated subsystem. - **Switches**: truthy = closed/OK for lid/doors/button. **SW_INTERLOCK is INVERTED**: the remote interlock (the regulatory 2-pin lockout connector) - reads ACTIVE only when the loop is OPEN. Basic/Plus — including the bench - machine — ship the connector factory-jumpered, so the bit reads 0 = + reads ACTIVE only when the loop is OPEN. Basic/Plus - including the bench + machine - ship the connector factory-jumpered, so the bit reads 0 = satisfied; Pro brings it out for an external lockout chain. It must NOT gate motion (the beam is hardware-gated), but ForgeFIRM's kernel module does drive INTERLOCK_RESET high whenever the loop reads open, so the CD4043B latch @@ -1132,24 +1130,24 @@ is committed. (bench-verified: loop pulled → `interlock_latch`=1, `interlock_circuit` b4 set, all within one 50 ms sample; reinserted → all clear). - **`hv_enable` (EV_SW bit 4, GPIO4_06) is the readback of the safety chain's - HV_ENABLE output** through the U24 inverter — not an input. Active for the + HV_ENABLE output** through the U24 inverter - not an input. Active for the whole duration of any run, inactive at idle, and it drops 454 ± 3 ms after the last charge-pump pulse (one-shot t_w measured pulse-to-drop with `scripts/bench/cp_watchdog_timing.py`: 451.8 / 455.6 ms; feed period - 199.98 ms; matching the measured R·C ≈ 500 kΩ × ≈900 nF). It gates nothing — + 199.98 ms; matching the measured R·C ≈ 500 kΩ × ≈900 nF). It gates nothing - it is telemetry (`/status` `switches.hv_enable`, panel "HV enable"), read alongside `cnc/charge_pump_alive` (`interlock_circuit` b5). **Across a pause and a resume** (measured at the pads with `scripts/bench/resume_dark_lead.py`, ~2 kHz through /dev/mem): a pause stops motion 317 ms after the command and HV_ENABLE drops with the watchdog 550 ms after it, so a pause shorter than about half a second never drops HV at all; on the resume HV_ENABLE and the - watchdog are back within ~3 ms while motion only restarts at ~219 ms — the + watchdog are back within ~3 ms while motion only restarts at ~219 ms - the chain re-arms ~216 ms **before** the first step, so a resumed cut loses nothing and no dark dwell is warranted. Naming note: the factory design labels this net **E-STOP**; entries in `CAMPAIGN-LOG.md` written before the 2026-08-15 rename call it `estop`/`SW_ESTOP` with the pre-rename polarity (the DTS then declared the pin active-high, so the bit read HIGH at idle and - LOW through a run — the same physical behavior, inverted). The DTS now + LOW through a run - the same physical behavior, inverted). The DTS now declares it active-low, and the former `estop_halts_motion` / `MOTION.ESTOP_HALTS_MOTION` opt-in is gone: a real e-stop belongs in the lid-switch chain ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/)). Doors/door1/door2 stay stable during @@ -1160,7 +1158,7 @@ is committed. idle in 86–91 ms, the return-home park starting ~300–340 ms after the edge and running to completion **with the lid still open**, the job reported `:cancelled`. A cancel from the app takes the same path. The button pauses a - print — controlled stop, then a 2000-tick laser-off backtrack — and resumes it + print - controlled stop, then a 2000-tick laser-off backtrack - and resumes it with a 1950-tick laser-off lead; the button **flashes white while paused**. A lid open while paused cancels the job and parks from where it stands. The lens hunt is not lid-gated. @@ -1178,7 +1176,7 @@ is committed. `cnc/laser_latch` is write-only, so lock state is read from b3. - **Machine identity from OCOTP nvmem**: HW_OCOTP_MAC0 is the serial, base-23-encoded to the factory hostname (`BCDFGHJKMQRTVWXY2346789`, XXX-YYY) - — fuse-verified against the factory label, and the C implementation matches + - fuse-verified against the factory label, and the C implementation matches gfhardware `id.py` over 200 k random serials. The bench machine's actual values are deliberately not recorded here: this is a public document and a fuse identity cannot be rotated. @@ -1188,17 +1186,17 @@ is committed. - eMMC (`mmcblk2`): 3.6 GiB user area + two 16 MiB hardware boot partitions (`mmcblk2boot0/1`). Factory user-area MBR (per the factory `.fw` manifest): p1/p2 = 200 MiB rootfs A/B at blocks 8192/417792, p3 = `/data` from block - 827392 to end of disk. (The bench board ran the legacy ForgeFIRM layout — - p3 shrunk plus a p4 — until `slotmigrate` reclaimed it to the byte-exact + 827392 to end of disk. (The bench board ran the legacy ForgeFIRM layout - + p3 shrunk plus a p4 - until `slotmigrate` reclaimed it to the byte-exact factory geometry.) - **U-Boot lives in boot0** at 1 KiB (IMX IVT header), not in the user area. Any boot0 rewrite below 0xC0000 risks the bootloader. - **Saved env**: user area 0x80000 with a redundant copy at 0x82000 (what `ffboot`/`fw_setenv` target; boot0's own 0x80000 region is zeros). Slot - selection = `mmcdev`/`mmchwpart`/`mmcpart`/`mmcroot`. Gap: `ffboot` sets - three of the four but never `mmchwpart` — it relies on the saved 0. + selection = `mmcdev`/`mmchwpart`/`mmcpart`/`mmcroot`. `ffboot` writes all four in + one transaction (`mmchwpart` 0, the user area). - **Default (compiled-in) env boots recovery**: `mmcdev=1 mmchwpart=1 - boot_recovery=yes` — a blank or corrupt env lands in recovery mode, not a + boot_recovery=yes` - a blank or corrupt env lands in recovery mode, not a brick. `bootcmd`: select mmc dev+hwpart → load and import `/boot/uEnv.txt` from the selected partition → if `boot_recovery=yes`, boot kernel+DTB from raw boot0 sectors, else load `/boot/zImage` from the slot's rootfs. U-Boot @@ -1225,7 +1223,7 @@ is committed. headroom. - **Facts about the factory 2024 firmware** (learned during the slot install): no `/factory/imgN` mounts, the generic `fw_env.config` points at the WRONG - device (use the per-device `fw_env_mmcblk2.config` — ffboot's selection + device (use the per-device `fw_env_mmcblk2.config` - ffboot's selection logic), no SSH (serial console only), and the factory kernel cannot see the SD card (`ffboot -s` needs `-f` from factory). Factory `/etc/version` is a numeric datetime stamp, so newest-slot selection is integer comparison. @@ -1270,8 +1268,9 @@ feature requests, enhancements) will eventually be tracked as GitHub issues. from the authorized export, `scripts/release.sh`, the kas flip and the first GitHub release, per the site (Developers, "Release flow"), once ready to publish. Repoint the core submodule to - upstream if the `step_us_min` sizing fix merges. -5. **Update system — recovery refresh.** A refreshed recovery image in + upstream once both local commits (the `step_us_min` sizing and the + hold-resume power re-assert) merge. +5. **Update system - recovery refresh.** A refreshed recovery image in boot0; the design is on [install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/). 6. **Head IRQ (exploratory).** Owed for the head IRQ, only if a coarse hardware interrupt is wanted @@ -1329,7 +1328,7 @@ covers the warm-up hold), the supply temperature window (the service sends the whole ADC range and the factory binds it to nothing; the supply is watched per job instead), the head, lid, interconnect and fused temperature ceilings (no sensor at those locations; the chassis is watched per job), the -head accelerometer thresholds (item 6), the lid IR thresholds (the fire +head accelerometer thresholds (the crash watch, on local knobs), the lid IR thresholds (the fire watch runs on local knobs; the header values stay ignored), the HV current caps (the sampled emission witness covers the idle case, and HV current is ranged per job), the thermal report upload conditions and the @@ -1349,4 +1348,10 @@ pump flag. Beam detect stays with item 6. bound (`cloud_hold_max_s`); `FORGEFIRM_RELEASE` is 0.0.1, so the bench slot at v0.1.0 will meet the installer's downgrade prompt; the `faultpos` live-fire drill is gone; a coolant sensor unreadable for two ticks is the - SENSOR verdict. + SENSOR verdict. Bench measurements the audit asks for, pooled into the + same session: the reset-to-probe state of the 40 V enable, the heater + enable, the TEC enable and the laser-enable nets at the connector during + a cold boot (the pads are 100 k pull-ups until a driver probes; a net + that floats high gets a pull-down pad in the device tree); one forced + kernel hard hang to confirm that the bootloader's watchdog-timeout path + boots the factory recovery and that a power cycle returns from it. diff --git a/fixture/fixture.sh b/fixture/fixture.sh index 7f8b273..adb7764 100644 --- a/fixture/fixture.sh +++ b/fixture/fixture.sh @@ -45,6 +45,12 @@ cmd_env() { cmd_build() { [ -e fixture.env ] || { echo "no fixture.env: run ./fixture.sh env first" >&2; exit 1; } + # The file holds the WiFi PSK and the API key: nobody but the owner + # reads it (an MSYS shell cannot express the mode, so it is not judged). + if [ "$(uname -o 2>/dev/null)" != "Msys" ] && [ -n "$(find fixture.env -perm /077 2>/dev/null)" ]; then + echo "fixture.env is group- or world-readable: chmod 600 fixture.env" >&2 + exit 1 + fi if have idf.py; then idf.py set-target "$CHIP" >/dev/null idf.py build diff --git a/forgetest/forgetest/hw.py b/forgetest/forgetest/hw.py index d75a312..b022205 100644 --- a/forgetest/forgetest/hw.py +++ b/forgetest/forgetest/hw.py @@ -337,6 +337,25 @@ def pidof(comm): return out +def pgrep_f(needle): + """PIDs whose /proc//cmdline contains needle (a script run by an + interpreter has the interpreter's comm, so pidof cannot see it).""" + out = [] + try: + for pid in os.listdir("/proc"): + if not pid.isdigit(): + continue + try: + with open("/proc/%s/cmdline" % pid, "rb") as f: + if needle.encode() in f.read(): + out.append(int(pid)) + except OSError: + pass + except OSError: + pass + return out + + def run(cmd, timeout=60): """Run a command list; returns (rc, combined output).""" try: diff --git a/forgetest/forgetest/runner.py b/forgetest/forgetest/runner.py index 0fbb55d..7710731 100644 --- a/forgetest/forgetest/runner.py +++ b/forgetest/forgetest/runner.py @@ -500,9 +500,13 @@ class Takeover: rc, out = hw.initd("forgectrl", "stop") log("takeover: forgectrl stop -> rc %s" % rc) deadline = time.time() + 15 - while time.time() < deadline and (hw.pidof("forgectrl") or hw.pidof("grblHAL_glowfor")): + # Every holder of the pulse device: the daemon, the GRBL controller, + # and the cloud client (a script, so found by its command line). + def holders(): + return hw.pidof("forgectrl") + hw.pidof("grblHAL_glowfor") + hw.pgrep_f("gfcloud.py") + while time.time() < deadline and holders(): time.sleep(0.5) - left = hw.pidof("forgectrl") + hw.pidof("grblHAL_glowfor") + left = holders() if left: self.__exit__(None, None, None) raise Failed("takeover: processes still alive after stop: %s" % left) diff --git a/forgetest/forgetest/suite/cloud.py b/forgetest/forgetest/suite/cloud.py index 9ca0232..88ab489 100644 --- a/forgetest/forgetest/suite/cloud.py +++ b/forgetest/forgetest/suite/cloud.py @@ -1215,7 +1215,7 @@ def lid_interlock_abort_body(ctx, ev, off, job, offset): "The job is longer than the ring holds, so the ring is filled before the button " "lights; that takes a minute. When the button lights white, do NOT press it - open " "the lid, and close it when told. Nothing moves and nothing fires."], - description="A cloud print waiting for the button is cancelled by the lid: the wait ends " + description="A cloud print waiting for the button is canceled by the lid: the wait ends " "with the lid named as the reason, the laser latch relocks, the armed window " "closes, no run starts, and the job ends ':cancelled'. The job is longer than " "the ring, so its feeder is alive through the wait with the rest of the print " @@ -1285,7 +1285,7 @@ def lid_during_button_wait_body(ctx, ev, off, job, offset): ctx.act("lid", "close") settle_cloud(ctx, offset) ev["events"] = offline_events(off) - ctx.log("PASS: lid open at the button prompt cancelled the print; latch locked, armed=false, " + ctx.log("PASS: lid open at the button prompt canceled the print; latch locked, armed=false, " "button dark, ring empty with the feed stopped") @@ -1422,7 +1422,7 @@ def pause_resume(ctx): relocked = [ln for ln in log_lines_since(GFCLOUD_LOG, offset) if "relocking the laser" in ln or ("print [" in ln and CANCELLED in ln)] ev["relock_or_cancel_lines"] = len(relocked) - ctx.check(not relocked, "the pause relocked or cancelled the job (%s)", relocked[:2]) + ctx.check(not relocked, "the pause relocked or canceled the job (%s)", relocked[:2]) # The job's lifecycle, from the same print: a warm-up before the first # fire and a rest after the park are equipment protection the service @@ -1587,8 +1587,8 @@ def oversize_stream_body(ctx, ev, off, job, offset, before): # or interlock abort - stop, park back to the job start, relock, # disarm, ':cancelled' - judged in full. off.cancel(9004) - ctx.log("cancelled the print as the app would") - svc_stop = "action cancelled mid-run; stopping motion" + ctx.log("canceled the print as the app would") + svc_stop = "action canceled mid-run; stopping motion" got = wait_log(ctx, offset, [svc_stop, "start return home", "return home complete"], 300) fin = wait_action_finished(ctx, offset, "print", 60) ev["service_cancel"] = {k: message(v) for k, v in got.items()} @@ -1596,7 +1596,7 @@ def oversize_stream_body(ctx, ev, off, job, offset, before): for k, v in ev["service_cancel"].items(): ctx.log(" [cancel] %s: %s", k, "seen" if v else "MISSING") ctx.check(got[svc_stop], "the app's cancel did not stop the run") - ctx.check(got["return home complete"], "the cancelled print did not park to completion") + ctx.check(got["return home complete"], "the canceled print did not park to completion") judge_abort_tail(ctx, ev, offset, "app", fin) ctx.check(hw.sysfs_int("cnc/streaming", 0) == 0, "the device was left in live-feed mode after the job ended") @@ -1610,7 +1610,7 @@ def oversize_stream_body(ctx, ev, off, job, offset, before): "stopped it, parked, relocked and reported ':cancelled'") -@test("cloud.paused-lid-cancel", title="A paused cloud print is cancelled by the lid", +@test("cloud.paused-lid-cancel", title="A paused cloud print is canceled by the lid", subsystem="cloud", kind="live", est_min=6, covers=_MACHINE_RUN, requires=["cloud.pause-resume", "cloud.lid-interlock-abort"], actions=["button", "lid"], @@ -1618,7 +1618,7 @@ def oversize_stream_body(ctx, ev, off, job, offset, before): "The head needs 40 mm of free +X and +Y travel.", "Press the button to start; when asked, press it again (pause), then open the lid and " "leave it open until the head is back; close it when told."], - description="A job paused on the button is cancelled by the lid, from the state the factory " + description="A job paused on the button is canceled by the lid, from the state the factory " "ends it in - there is no resume past a lid open. The same tail as every abort: " "the motion stops, the head parks back at the job start, the latch relocks and " "the armed window closes, the button goes dark, and the print ends ':cancelled'. " @@ -1666,12 +1666,12 @@ def paused_lid_cancel_body(ctx, ev, off, job, offset): st, cs = ctx.forgectrl.get("/cool/status") ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None ev["latch_locked_after"] = latch_locked() - ctx.check(not ev["armed_after"], "armed window still open after the paused print was cancelled") + ctx.check(not ev["armed_after"], "armed window still open after the paused print was canceled") ctx.check(ev["latch_locked_after"], - "kernel latch not locked after the paused print was cancelled") + "kernel latch not locked after the paused print was canceled") ev["button_dark"] = hw.button_lit() ctx.check(ev["button_dark"] is False, "the button is still lit after the cancel (%s)", ev["button_dark"]) ctx.act("lid", "close") settle_cloud(ctx, offset) ev["events"] = offline_events(off) - ctx.log("PASS: a paused print cancelled by the lid stopped, parked, relocked and reported ':cancelled'") + ctx.log("PASS: a paused print canceled by the lid stopped, parked, relocked and reported ':cancelled'") diff --git a/forgetest/forgetest/suite/forgectrl.py b/forgetest/forgetest/suite/forgectrl.py index 371d666..a9281b1 100644 --- a/forgetest/forgetest/suite/forgectrl.py +++ b/forgetest/forgetest/suite/forgectrl.py @@ -45,7 +45,7 @@ def auth(ctx): for path, params in (("/controller/stop", None), ("/controller/start", None), ("/mode", {"controller": "grbl"}), ("/settings", {"ui_units": "mm"}), ("/diag/flow-verify", None), ("/diag/abort", None), - ("/update/apply", None), ("/boot", {"slot": "a"}), + ("/update/apply", None), ("/boot", {"target": "a"}), ("/system/reboot", None), ("/restore/factory", None)): st, body = fc.post(path, params=params, auth=False) ctx.log("POST %s (no token) -> %s %s", path, st, body if isinstance(body, dict) else "") diff --git a/forgetest/forgetest/suite/laser.py b/forgetest/forgetest/suite/laser.py index ff902dc..67afce6 100644 --- a/forgetest/forgetest/suite/laser.py +++ b/forgetest/forgetest/suite/laser.py @@ -715,7 +715,7 @@ def arm_wait_lid(ctx): ev["messages"] = [ln for ln in text.splitlines() if ln.startswith("[MSG:") or ln.startswith("ALARM")] ctx.log("controller: %s", ev["messages"]) ctx.check("lid opened during arm - job canceled" in text, - "the lid open was not reported as cancelling the arm") + "the lid open was not reported as canceling the arm") ctx.check("help]" in text, "no reset banner after the lid-open cancel (it must be a clean cancel)") ctx.check("ALARM" not in text, "an alarm was raised on the lid-open cancel") # Armed window closed and the kernel latch locked. @@ -869,7 +869,7 @@ def pause_resume_lid_cancel(ctx): ctx.check(zero_at is not None and zero_at < 3.0, "emission did not stop after the lid opened (first 0 at %s)", zero_at) ctx.check(tail_zero, "emission returned after the lid opened") - ctx.check("lid opened - job canceled" in text, "the lid open was not reported as cancelling the job") + ctx.check("lid opened - job canceled" in text, "the lid open was not reported as canceling the job") ctx.check("help]" in text, "no reset banner after the cancel") ctx.check("ALARM" not in text, "an alarm was raised on the cancel (position should be kept)") t3 = time.time() diff --git a/forgetest/forgetest/ui/app.js b/forgetest/forgetest/ui/app.js index 6774208..b73222c 100644 --- a/forgetest/forgetest/ui/app.js +++ b/forgetest/forgetest/ui/app.js @@ -12,7 +12,7 @@ * only updated in place. A poll that rebuilt them would swallow the click * it landed on: the button that took the mousedown would be gone before * the mouseup, so no click event would ever be raised. Every action also - * greys its control out on the press and pulls the next poll forward, so + * grays its control out on the press and pulls the next poll forward, so * the page answers the operator rather than the timer. The help popovers * (help.js) sit on static elements only, so no rebuild ever orphans one. * @@ -56,7 +56,7 @@ function schedule(ms){if(pollTimer)window.clearTimeout(pollTimer);pollTimer=wind function kick(){schedule(120)} function poll(){if(polling){schedule(150);return}polling=true; /* While an action is still in flight the state is asked for in full: - a 304 would skip the render that releases the greyed-out buttons. */ + a 304 would skip the render that releases the grayed-out buttons. */ var h=(stateEtag&&!pending)?{'If-None-Match':stateEtag}:null; api('GET','/state',null,function(s,d,x){polling=false; if(s===200){stateEtag=x.getResponseHeader('ETag');state=d; @@ -72,7 +72,7 @@ function setIgnoreReq(on){ignoreReq=!!on;try{window.localStorage.setItem('forget $('ignreq').checked=ignoreReq;setHtml($('ignreqon'),ignoreReq?"ON - prerequisites are not enforced":''); if(state&&catalog)renderGroups()} /* A start already sent but not yet seen in the state counts as busy, so - the buttons grey out on the click rather than on the next poll. The + the buttons gray out on the click rather than on the next poll. The window is capped in case the answer never arrives. A queue holds the machine between its tests as well as during them. */ function batchActive(){return !!(state&&state.batch&&!state.batch.finished)} diff --git a/forgetest/tests/test_cloud_suite.py b/forgetest/tests/test_cloud_suite.py index 7443178..f649609 100644 --- a/forgetest/tests/test_cloud_suite.py +++ b/forgetest/tests/test_cloud_suite.py @@ -757,7 +757,7 @@ class CloudSuiteTests(unittest.TestCase): self.hunted(1522) self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"]) lines = [l.replace("machine:_resume_retraced resuming (laser lead 1950 ticks)", - "machine:_resume_retraced resume refused ([Errno 22] Invalid argument); cancelling") + "machine:_resume_retraced resume refused ([Errno 22] Invalid argument); canceling") for l in fixture("pause")] pre, rest = cut(lines, "current state: MachineState.RUNNING") pre, rest = pre + [rest[0]], rest[1:] @@ -972,7 +972,7 @@ class CloudSuiteTests(unittest.TestCase): hooks = {"press it. The print then waits": lambda: self.append(wait + run[:1], delay=0.05)} self.assertFails(cloud.verdict_hold, "the run started under the warm-up hold", hooks=hooks) - # -- a paused print cancelled by the lid, a running one by the app -------- + # -- a paused print canceled by the lid, a running one by the app -------- def cancel_parts(self): """(print prologue, the pause lines, the lid stop + park + cancel, the same tail with the app's cancel as the trigger - what @@ -986,7 +986,7 @@ class CloudSuiteTests(unittest.TestCase): "2026-08-17T09:42:26.100000+00:00 gfcloud[1522] INFO machine:_run_loop " "paused at Position(x=41.2, y=17.0, z=0.0)"] app_cancel = [l.replace("lid opened mid-run; stopping motion", - "action cancelled mid-run; stopping motion") for l in rest] + "action canceled mid-run; stopping motion") for l in rest] return pre, paused, rest, app_cancel def test_paused_lid_cancel_on_the_bench_excerpt(self): @@ -1009,7 +1009,7 @@ class CloudSuiteTests(unittest.TestCase): self.assertFalse(ev["armed_after"]) self.assertFalse(ev["button_dark"]) self.assertEqual(self.fc.posts, []) - self.assertTrue(any("PASS: a paused print cancelled by the lid" in l for l in run.lines), run.lines) + self.assertTrue(any("PASS: a paused print canceled by the lid" in l for l in run.lines), run.lines) def test_paused_lid_cancel_fails_when_the_paused_print_resumes_instead(self): # a lid that resumed (or was ignored) leaves the print ':completed' diff --git a/kas/forgefirm-glowforge-debug.yml b/kas/forgefirm-glowforge-debug.yml index f150851..92a644a 100644 --- a/kas/forgefirm-glowforge-debug.yml +++ b/kas/forgefirm-glowforge-debug.yml @@ -1,5 +1,5 @@ # ============================================================================ -# ForgeFIRM — debug-kernel build variant +# ForgeFIRM - debug-kernel build variant # ============================================================================ # One target: a dev image whose kernel carries the lock-correctness options # (DEBUG_MUTEXES, PROVE_LOCKING, DEBUG_ATOMIC_SLEEP, DEBUG_SPINLOCK). It rides diff --git a/kas/forgefirm-glowforge.yml b/kas/forgefirm-glowforge.yml index bbd41ce..c003112 100644 --- a/kas/forgefirm-glowforge.yml +++ b/kas/forgefirm-glowforge.yml @@ -1,5 +1,5 @@ # ============================================================================ -# ForgeFIRM — kas build configuration (factory Glowforge control board) +# ForgeFIRM - kas build configuration (factory Glowforge control board) # ============================================================================ # The forgefirm repo is the BASE: it controls the build, the output firmware # images land here (build/tmp/deploy/images/glowforge/), and the status docs @@ -31,10 +31,10 @@ target: forgefirm-image # kas uses KAS_WORK_DIR (the dir you run kas from = the forgefirm repo root). # Upstream layers are cloned into ./layers/, the build runs in ./build/, and -# caches go in ./downloads + ./sstate-cache — all gitignored, all inside forgefirm. +# caches go in ./downloads + ./sstate-cache - all gitignored, all inside forgefirm. repos: - # --- This repo (forgefirm) — auto-detected, no url/path needed ------------- + # --- This repo (forgefirm) - auto-detected, no url/path needed ------------- forgefirm: layers: meta-forgefirm: @@ -73,7 +73,7 @@ repos: layers: .: - # --- meta-openglow — Glowforge BSP layers --------------------------------- + # --- meta-openglow - Glowforge BSP layers --------------------------------- # Referenced as a local sibling checkout (no url => kas performs no git ops, # so in-place BSP edits are what gets built). The kernel-module-glowforge # sources are NOT a layer; the kernel-module-glowforge.bb recipe fetches them @@ -98,14 +98,14 @@ repos: # local.conf additions # ---------------------------------------------------------------------------- local_conf_header: - # Accept the NXP/Freescale firmware EULA — required to unpack firmware-imx + # Accept the NXP/Freescale firmware EULA - required to unpack firmware-imx # (i.MX SDMA/VPU firmware blobs) pulled in by the i.MX6 BSP. ForgeFIRM targets # the factory i.MX6 Glowforge board, so this firmware is intrinsic to the image. eula: | ACCEPT_FSL_EULA = "1" # The kernel defaults to linux-fslc 6.12 in conf/machine/glowforge.conf - # (with the factory drivers forward-ported — EPIT/SDMA/OV5648/glowforge.ko — + # (with the factory drivers forward-ported - EPIT/SDMA/OV5648/glowforge.ko - # see the site, Developers, "Build"). This explicit pin is redundant but harmless. kernel: | PREFERRED_PROVIDER_virtual/kernel = "linux-fslc" diff --git a/meta-forgefirm/classes/forgefirm-image-manifest.bbclass b/meta-forgefirm/classes/forgefirm-image-manifest.bbclass index 54763cc..f846e69 100644 --- a/meta-forgefirm/classes/forgefirm-image-manifest.bbclass +++ b/meta-forgefirm/classes/forgefirm-image-manifest.bbclass @@ -114,7 +114,7 @@ def forgefirm_manifest_sha256_file(path): return h.hexdigest() python forgefirm_manifest_assemble() { - import glob, hashlib, json, os + import glob, hashlib, json, os, re rootfs = d.getVar('IMAGE_ROOTFS') components = {} @@ -145,7 +145,12 @@ python forgefirm_manifest_assemble() { layer_identity, layer_build = forgefirm_manifest_layers(d) platform = {'machine': d.getVar('MACHINE'), 'layers': layer_identity} - platform['kernel_modules'] = sorted(os.listdir(os.path.join(rootfs, 'lib', 'modules'))) \ + # The modules directory carries the kernel's LOCALVERSION_AUTO hash, + # which does not reproduce across a re-patch of the same source; the + # kernel's identity is its pinned SRCREV and config (the @srcrev and + # @config entries above), so the hash is stripped from the name here. + platform['kernel_modules'] = sorted(re.sub(r'[+-]g[0-9a-f]{7,}$', '', n) + for n in os.listdir(os.path.join(rootfs, 'lib', 'modules'))) \ if os.path.isdir(os.path.join(rootfs, 'lib', 'modules')) else [] dtbs = {} for p in sorted(glob.glob(os.path.join(rootfs, 'boot', '**', '*.dtb'), recursive=True)): diff --git a/meta-forgefirm/recipes-forgefirm/forgetest/forgetest.bb b/meta-forgefirm/recipes-forgefirm/forgetest/forgetest.bb index cc6d455..43e6c50 100644 --- a/meta-forgefirm/recipes-forgefirm/forgetest/forgetest.bb +++ b/meta-forgefirm/recipes-forgefirm/forgetest/forgetest.bb @@ -12,11 +12,21 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda # that builds the images, so the catalog and the gate agree by construction. FILESEXTRAPATHS:prepend := "${THISDIR}/../../../:" +# The package directory and the init script, not the tool's whole tree: the +# host unit tests and their caches would otherwise enter the fetch checksum +# and rebuild the dev image after a workstation test run. SRC_URI = " \ - file://forgetest/ \ + file://forgetest/forgetest/ \ + file://forgetest/forgetest.init \ file://scripts/bench/ \ " +do_unpack:append() { + import subprocess + subprocess.call(['find', d.getVar('WORKDIR') + '/forgetest', d.getVar('WORKDIR') + '/scripts', + '-name', '__pycache__', '-type', 'd', '-prune', '-exec', 'rm', '-rf', '{}', '+']) +} + S = "${WORKDIR}" inherit python3-dir update-rc.d forgefirm-manifest diff --git a/meta-forgefirm/recipes-forgefirm/grblhal-glowforge/grblhal-glowforge/grblhal.init b/meta-forgefirm/recipes-forgefirm/grblhal-glowforge/grblhal-glowforge/grblhal.init index c85bd5b..cd5bbca 100644 --- a/meta-forgefirm/recipes-forgefirm/grblhal-glowforge/grblhal-glowforge/grblhal.init +++ b/meta-forgefirm/recipes-forgefirm/grblhal-glowforge/grblhal-glowforge/grblhal.init @@ -56,6 +56,7 @@ case "$1" in ;; restart) $0 stop + $0 start ;; *) echo "Usage: $0 {start|stop|status|restart}" diff --git a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb index 47300b3..2a02811 100644 --- a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb +++ b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb @@ -9,7 +9,6 @@ DESCRIPTION = "OpenGlow/ForgeFIRM development image for Glowforge" # emulator fixtures are the canned frames its service-protocol test answers # the service with. IMAGE_INSTALL += " \ - forgectrl \ forgetest \ python3-gfutilities-emulator \ python3-mmap \ diff --git a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb index 8377b35..bc45ea3 100644 --- a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb +++ b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb @@ -20,7 +20,7 @@ DESCRIPTION = "OpenGlow/ForgeFIRM image for Glowforge" # nothing here imports. Each Python recipe declares the module packages it # imports (python3-core plus the few it uses), so the meta-package goes. FORGEFIRM_RELEASE_TRIM ?= "nano" -IMAGE_INSTALL:remove = "gfui-client python3 ${FORGEFIRM_RELEASE_TRIM}" +IMAGE_INSTALL:remove = "python3 ${FORGEFIRM_RELEASE_TRIM}" # grblhal-glowforge: the grblHAL motion controller (Grbl over TCP:23). # forgectrl: the ForgeFIRM machine-services daemon (HTTP :8080): controller diff --git a/scripts/bench/README.md b/scripts/bench/README.md index ccf0b96..aead3ea 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -46,15 +46,15 @@ page's takeover does that; from a host, stop them first. | `offset_probe.py` | Coolant-sensor offset probe (board; forgectrl idle; dark, no press): switches one actuator at a time (exhaust at 100/50/25 %, intakes, air assist, purge, heater, pump, TEC, lid lamp, then all run fans) with both thermistors sampled at 25 Hz and scores the common-mode step at every edge and the level toggling inside every dwell; `offset_probe.py ladder` runs the air-assist duty ladder alone. Every value is restored on exit. JSON record in the bench data directory. | | `flow_warm_validate.py` | Runs the real check from a heater-warmed baseline (board or host; forgectrl and controller stopped; `flow_warm_validate.py [cycles_per_case]`; results/log in the bench data directory; exit 1 if any run is misclassified). Note the ceiling: 100 % duty pushes the downstream sensor past 50 °C in 30 s while the bulk barely moves, so warm-loop validation above ~23 °C needs the laser, not the heater. | | `flow_recheck_char.py` | Characterizes short in-run re-checks and the differential metric (board or host; forgectrl and controller stopped; `flow_recheck_char.py [heater_pct] [window_s]`); shows why over-temp cannot see a stopped pump and why passive warming trends are ambiguous. | -| `flow_confirm_drill.py` | Coolant flow suspicion/confirmation drill (runs on the board): one continuous M8 session walks the verdict state machine through real pump-off transients — verified → SUSPECT (+ immediate re-check) → cleared → SUSPECT → FAULT (consecutive) → recovered — printing PASS/FAIL per transition. Leaves the machine idle (M9, pump on, heater off). | +| `flow_confirm_drill.py` | Coolant flow suspicion/confirmation drill (runs on the board): one continuous M8 session walks the verdict state machine through real pump-off transients - verified → SUSPECT (+ immediate re-check) → cleared → SUSPECT → FAULT (consecutive) → recovered - printing PASS/FAIL per transition. Leaves the machine idle (M9, pump on, heater off). | | `flow_escalate_drill.py` | Coolant starved-re-check escalation drill (runs on the board, controller running): sets the engine's confirmation budget `cool_confirm_max_s` to a short value through forgectrl's settings (`flow_escalate_drill.py [budget_s]`, default 60, the setting's minimum) and restores it after; with the pump off the job-start check reads SUSPECT, the stagnant loop cannot pass the settle gate inside the budget, and the engine must escalate to FAULT. PASS/FAIL (exit status), leaves the machine idle. | | `flow_sampler.py` | Board-side coolant sampler used by the flow tools (`flow_sampler.py `, prints `elapsed,raw_down,raw_up`); run on the board (dev image: `/usr/share/forgetest/bench/`) so cadence does not depend on ssh latency. | -| `temp_calibrate.py` | Coolant temperature spot-check helper (board or host): `watch [seconds]` / `point [note]` / `fit` — pairs a measured temperature with averaged raw ADC readings and fits a per-machine line to sanity-check the factory curve against a thermometer. Points accumulate in `temp_calibration.json` in the bench data directory. | +| `temp_calibrate.py` | Coolant temperature spot-check helper (board or host): `watch [seconds]` / `point [note]` / `fit` - pairs a measured temperature with averaged raw ADC readings and fits a per-machine line to sanity-check the factory curve against a thermometer. Points accumulate in `temp_calibration.json` in the bench data directory. | | `build-glowforge.sh` | Cross-compiles **grblHAL-glowforge** (the canonical driver repo) in the Yocto build environment, borrowing the recipe toolchain. Run: `bash /build-glowforge.sh` (from Windows, launch it through the WSL distro from PowerShell; Git Bash mangles /mnt/c paths). Env: `FF_SRC_TOP`, `FF_BUILD_TOP`. This is the production controller build. | | `build-forgectrl.sh` | Cross-compiles **forgectrl** (the canonical control-daemon repo) the same way, borrowing the toolchain from the forgectrl recipe workdir (regenerate with `bitbake forgectrl` after a clean). | | `accel_fast.py` | Direct-I2C sampler for the two head-bus LIS2HH12s (runs on the board; unbinds/rebinds st-accel around the capture, 800 Hz ODR, ~270 Hz per device polled): optional mid-capture jogs via local grblHAL TCP. CSV to /tmp/accel.csv. The head accel is i2c-3 0x1e. | | `bump_seek.py` | Accelerometer bump-seek homing prototype (runs on the board): creeps toward a rail in bounded jog segments via grblHAL TCP, learns the moving-noise baseline per segment, detects the contact jolt (~530 Hz sampling, 2-sample confirm), jog-cancels (0x85) and backs off. CSV to /tmp/bump.csv. | -| `accel_crash_probe.py` | Head-accel crash-detector de-risk drill (runs on the board; BRINGUP item 6): arms the head LIS2HH12's on-chip interrupt generator (IG_CFG1/IG_THS/IG_DUR1) and polls the latched IG_SRC1 for a strike, reporting the axes and raw magnitude. `coexist` (default) reaches the IG registers with I2C_SLAVE_FORCE while st_accel stays bound, so it proves whether the detector can be forgectrl-only with the liveness path untouched; `unbind` frees the device (stop the controller and forgectrl first). Touches only the IG registers, no full-scale change, no emission, no commanded motion by default (provoke a trip by hand or with one `--jog`). CSV to /tmp/accel_crash.csv. | +| `accel_crash_probe.py` | Head-accel crash-detector de-risk drill (runs on the board; the head crash detector): arms the head LIS2HH12's on-chip interrupt generator (IG_CFG1/IG_THS/IG_DUR1) and polls the latched IG_SRC1 for a strike, reporting the axes and raw magnitude. `coexist` (default) reaches the IG registers with I2C_SLAVE_FORCE while st_accel stays bound, so it proves whether the detector can be forgectrl-only with the liveness path untouched; `unbind` frees the device (stop the controller and forgectrl first). Touches only the IG registers, no full-scale change, no emission, no commanded motion by default (provoke a trip by hand or with one `--jog`). CSV to /tmp/accel_crash.csv. | | `build-feeder.sh` | Cross-compiles `feeder.c` the same way. | | `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. | @@ -80,7 +80,7 @@ forgectrl's cooling engine refuses to let the laser fire unless a heater-based flow check passes: with the pump commanded on, the loop heater runs at `COOL_FLOW_HEATER_PCT` (40 %) for `COOL_FLOW_CHECK_S` (50 s) and the downstream sensor's rise over its settled baseline must stay below -`COOL_FLOW_RISE_C` (14.4 °C) — a stopped pump lets the heater's output pool +`COOL_FLOW_RISE_C` (14.4 °C) - a stopped pump lets the heater's output pool at the downstream sensor instead of being carried away. The three constants live in `forgectrl/src/cool.h` and are the compiled defaults behind the `cool_flow_*` settings. diff --git a/scripts/bench/feeder.c b/scripts/bench/feeder.c index b4a9778..3107d4e 100644 --- a/scripts/bench/feeder.c +++ b/scripts/bench/feeder.c @@ -4,8 +4,9 @@ * * Streams NOP (0x00) pulse bytes to /dev/glowforge, pacing by wall clock to * hold a bounded queue depth (like a real grblHAL backend would), - * with the deadman flock held. Motors must be locked and the laser latch - * locked by the caller (bench_phase6.sh does this). + * with the deadman flock held. The caller locks the motors and the laser + * latch first: write 1 to /sys/glowforge/cnc/motor_lock and 1 to + * /sys/glowforge/cnc/laser_latch, and read both back before starting. * * Reports: feed statistics, worst scheduling stall, ENOMEM count, and the * final driver state (expect "running" throughout, "underrun" only after diff --git a/scripts/bench/flow_confirm_drill.py b/scripts/bench/flow_confirm_drill.py index af58825..96de412 100644 --- a/scripts/bench/flow_confirm_drill.py +++ b/scripts/bench/flow_confirm_drill.py @@ -62,9 +62,7 @@ def temps(): adc_f = 1024.0 * 1.3 rinf = 10000.0 * math.exp(-3380.0 / 298.15) - def degc(raw): - r = 10000.0 / (adc_f / float(raw) - 1.0) - return 3380.0 / math.log(r / rinf) - 273.15 + from gfbench import degc with open('/sys/glowforge/pic/water_temp_1') as f: d = degc(f.read()) with open('/sys/glowforge/pic/water_temp_2') as f: diff --git a/scripts/bench/flow_matrix_log.txt b/scripts/bench/flow_matrix_log.txt deleted file mode 100644 index 375dee1..0000000 --- a/scripts/bench/flow_matrix_log.txt +++ /dev/null @@ -1,205 +0,0 @@ -=== flow matrix started 2026-08-02 20:20:59 -duties=[10, 15, 20, 30, 40, 50] repeats=5 run=75s durations=[15, 20, 25, 30, 40, 50, 60, 75] -controller stopped for the experiment -initial settle (180 s with fans)... -baseline: down=22.62 up=22.46 (target for every run: <= 22.96) - [ 1/60] rep1 duty 10% FLOW start=22.77 down_rise@30s=3.93 - elapsed 0.1 h - [ 2/60] rep1 duty 10% NO-FLOW start=22.38 down_rise@30s=3.10 - elapsed 0.1 h - [ 3/60] rep1 duty 15% FLOW start=22.85 down_rise@30s=4.73 - elapsed 0.1 h - [ 4/60] rep1 duty 15% NO-FLOW start=22.38 down_rise@30s=3.45 - elapsed 0.2 h - [ 5/60] rep1 duty 20% FLOW start=22.77 down_rise@30s=3.64 - elapsed 0.2 h - [ 6/60] rep1 duty 20% NO-FLOW start=22.46 down_rise@30s=6.82 - elapsed 0.2 h - [ 7/60] rep1 duty 30% FLOW start=22.23 down_rise@30s=8.41 - elapsed 0.2 h - [ 8/60] rep1 duty 30% NO-FLOW start=22.30 down_rise@30s=11.03 - elapsed 0.3 h - [ 9/60] rep1 duty 40% FLOW start=22.93 down_rise@30s=11.46 - elapsed 0.3 h - [10/60] rep1 duty 40% NO-FLOW start=22.46 down_rise@30s=15.27 - elapsed 0.3 h - [11/60] rep1 duty 50% FLOW start=22.23 down_rise@30s=14.75 - elapsed 0.3 h - [12/60] rep1 duty 50% NO-FLOW start=22.54 down_rise@30s=18.74 - elapsed 0.4 h - [13/60] rep2 duty 10% FLOW start=21.84 down_rise@30s=3.14 - elapsed 0.4 h - [14/60] rep2 duty 10% NO-FLOW start=22.30 down_rise@30s=2.29 - elapsed 0.4 h - [15/60] rep2 duty 15% FLOW start=22.15 down_rise@30s=3.46 - elapsed 0.5 h - [16/60] rep2 duty 15% NO-FLOW start=22.77 down_rise@30s=4.08 - elapsed 0.5 h - [17/60] rep2 duty 20% FLOW start=22.15 down_rise@30s=4.69 - elapsed 0.5 h - [18/60] rep2 duty 20% NO-FLOW start=22.77 down_rise@30s=5.51 - elapsed 0.5 h - [19/60] rep2 duty 30% FLOW start=21.99 down_rise@30s=7.17 - elapsed 0.6 h - [20/60] rep2 duty 30% NO-FLOW start=22.93 down_rise@30s=7.10 - elapsed 0.6 h - [21/60] rep2 duty 40% FLOW start=22.62 down_rise@30s=8.82 - elapsed 0.6 h - [22/60] rep2 duty 40% NO-FLOW start=22.62 down_rise@30s=13.46 - elapsed 0.6 h - [23/60] rep2 duty 50% FLOW start=22.30 down_rise@30s=12.66 - elapsed 0.7 h - [24/60] rep2 duty 50% NO-FLOW start=22.54 down_rise@30s=18.18 - elapsed 0.7 h - [25/60] rep3 duty 10% FLOW start=22.07 down_rise@30s=2.43 - elapsed 0.7 h - [26/60] rep3 duty 10% NO-FLOW start=22.07 down_rise@30s=3.40 - elapsed 0.7 h - [27/60] rep3 duty 15% FLOW start=22.62 down_rise@30s=4.40 - elapsed 0.8 h - [28/60] rep3 duty 15% NO-FLOW start=21.76 down_rise@30s=4.62 - elapsed 0.8 h - [29/60] rep3 duty 20% FLOW start=22.85 down_rise@30s=4.35 - elapsed 0.8 h - [30/60] rep3 duty 20% NO-FLOW start=21.68 down_rise@30s=4.79 - elapsed 0.8 h - [31/60] rep3 duty 30% FLOW start=22.30 down_rise@30s=9.11 - elapsed 0.9 h - [32/60] rep3 duty 30% NO-FLOW start=22.15 down_rise@30s=7.28 - elapsed 0.9 h - [33/60] rep3 duty 40% FLOW start=21.91 down_rise@30s=10.00 - elapsed 0.9 h - [34/60] rep3 duty 40% NO-FLOW start=22.85 down_rise@30s=14.55 - elapsed 0.9 h - [35/60] rep3 duty 50% FLOW start=22.62 down_rise@30s=12.71 - elapsed 1.0 h - [36/60] rep3 duty 50% NO-FLOW start=22.62 down_rise@30s=18.19 - elapsed 1.0 h - [37/60] rep4 duty 10% FLOW start=22.54 down_rise@30s=2.75 - elapsed 1.0 h - [38/60] rep4 duty 10% NO-FLOW start=20.75 down_rise@30s=2.92 - elapsed 1.1 h - [39/60] rep4 duty 15% FLOW start=22.15 down_rise@30s=4.17 - elapsed 1.1 h - [40/60] rep4 duty 15% NO-FLOW start=22.62 down_rise@30s=4.71 - elapsed 1.1 h - [41/60] rep4 duty 20% FLOW start=21.37 down_rise@30s=5.48 - elapsed 1.1 h - [42/60] rep4 duty 20% NO-FLOW start=22.23 down_rise@30s=4.77 - elapsed 1.2 h - [43/60] rep4 duty 30% FLOW start=22.46 down_rise@30s=6.41 - elapsed 1.2 h - [44/60] rep4 duty 30% NO-FLOW start=22.62 down_rise@30s=6.84 - elapsed 1.2 h - [45/60] rep4 duty 40% FLOW start=22.54 down_rise@30s=10.50 - elapsed 1.2 h - [46/60] rep4 duty 40% NO-FLOW start=22.69 down_rise@30s=14.56 - elapsed 1.3 h - [47/60] rep4 duty 50% FLOW start=22.38 down_rise@30s=13.75 - elapsed 1.3 h - [48/60] rep4 duty 50% NO-FLOW start=22.23 down_rise@30s=18.61 - elapsed 1.3 h - [49/60] rep5 duty 10% FLOW start=22.38 down_rise@30s=2.67 - elapsed 1.3 h - [50/60] rep5 duty 10% NO-FLOW start=22.07 down_rise@30s=4.10 - elapsed 1.4 h - [51/60] rep5 duty 15% FLOW start=21.45 down_rise@30s=3.94 - elapsed 1.4 h - [52/60] rep5 duty 15% NO-FLOW start=22.15 down_rise@30s=3.19 - elapsed 1.4 h - [53/60] rep5 duty 20% FLOW start=21.91 down_rise@30s=4.26 - elapsed 1.4 h - [54/60] rep5 duty 20% NO-FLOW start=22.46 down_rise@30s=5.25 - elapsed 1.5 h - [55/60] rep5 duty 30% FLOW start=22.23 down_rise@30s=7.34 - elapsed 1.5 h - [56/60] rep5 duty 30% NO-FLOW start=22.77 down_rise@30s=11.06 - elapsed 1.5 h - [57/60] rep5 duty 40% FLOW start=22.93 down_rise@30s=9.65 - elapsed 1.5 h - [58/60] rep5 duty 40% NO-FLOW start=22.38 down_rise@30s=13.48 - elapsed 1.6 h - [59/60] rep5 duty 50% FLOW start=22.38 down_rise@30s=12.59 - elapsed 1.6 h - [60/60] rep5 duty 50% NO-FLOW start=21.68 down_rise@30s=20.05 - elapsed 1.6 h - -=== COST: bulk (upstream) rise during a check, flow case, degrees C - duty 15s 20s 25s 30s 40s 50s 60s 75s - 10% 0.34 0.62 0.67 0.33 0.30 0.58 0.09 0.34 - 15% 0.03 0.76 0.50 0.67 0.53 0.69 0.39 0.64 - 20% -0.08 -0.17 0.23 0.09 0.19 0.23 0.02 0.50 - 30% 0.79 0.73 1.39 0.83 1.11 1.17 1.42 1.22 - 40% 0.55 0.36 1.05 1.06 0.95 0.76 1.25 1.53 - 50% 1.26 1.83 1.72 1.59 2.03 2.03 2.19 2.69 - -=== PRECISION: downstream-rise discrimination (flow vs no-flow) - duty dur flow mean+-sd noflow mean+-sd sep worst d-prime - 10% 15s 2.68+-0.45 2.03+-0.71 -0.65 -2.20 -1.1 - 10% 20s 2.90+-0.69 2.81+-0.37 -0.09 -1.65 -0.2 - 10% 25s 2.76+-0.69 3.10+-0.60 0.33 -1.80 0.5 - 10% 30s 2.98+-0.59 3.16+-0.66 0.18 -1.64 0.3 - 10% 40s 2.90+-0.86 3.30+-0.42 0.40 -1.08 0.6 - 10% 50s 3.05+-0.85 3.88+-0.44 0.83 -1.09 1.2 - 10% 60s 2.37+-0.54 3.61+-0.54 1.24 -0.21 2.3 - 10% 75s 2.60+-0.83 4.12+-0.54 1.52 -0.36 2.2 - 15% 15s 2.93+-0.63 2.93+-0.36 -0.01 -1.21 -0.0 - 15% 20s 3.97+-0.33 3.54+-0.35 -0.42 -1.31 -1.2 - 15% 25s 3.89+-0.86 3.95+-0.45 0.06 -1.46 0.1 - 15% 30s 4.14+-0.48 4.01+-0.68 -0.13 -1.55 -0.2 - 15% 40s 4.11+-0.79 4.43+-0.65 0.32 -1.54 0.4 - 15% 50s 4.17+-0.78 5.01+-0.34 0.83 -0.24 1.4 - 15% 60s 4.17+-0.52 5.09+-0.48 0.92 -0.21 1.8 - 15% 75s 4.21+-0.41 5.75+-0.45 1.54 +0.37 3.6 - 20% 15s 3.84+-0.78 3.57+-0.83 -0.27 -2.08 -0.3 - 20% 20s 4.18+-0.53 4.14+-0.83 -0.03 -1.36 -0.0 - 20% 25s 4.71+-0.92 4.90+-1.03 0.19 -1.91 0.2 - 20% 30s 4.48+-0.67 5.43+-0.84 0.95 -0.71 1.2 - 20% 40s 4.84+-0.58 5.79+-1.16 0.96 -0.61 1.0 - 20% 50s 4.98+-0.39 6.41+-0.79 1.43 +0.22 2.3 - 20% 60s 4.53+-0.66 6.67+-1.39 2.14 -0.44 2.0 - 20% 75s 4.95+-0.65 7.34+-1.02 2.39 +0.64 2.8 - 30% 15s 6.32+-0.81 5.63+-1.72 -0.69 -3.43 -0.5 - 30% 20s 7.13+-1.31 6.86+-2.24 -0.27 -3.67 -0.1 - 30% 25s 7.65+-0.92 7.73+-1.93 0.08 -2.63 0.1 - 30% 30s 7.69+-1.07 8.66+-2.18 0.97 -2.27 0.6 - 30% 40s 7.69+-0.90 9.96+-2.65 2.27 -1.00 1.1 - 30% 50s 8.15+-0.75 10.24+-2.37 2.09 -0.93 1.2 - 30% 60s 8.20+-0.70 10.77+-2.34 2.57 -0.35 1.5 - 30% 75s 8.27+-1.28 10.86+-2.26 2.59 -0.82 1.4 - 40% 15s 8.06+-0.87 10.14+-0.65 2.08 +0.37 2.7 - 40% 20s 8.93+-1.07 12.07+-0.48 3.13 +1.06 3.8 - 40% 25s 9.61+-1.22 13.47+-0.98 3.86 +0.96 3.5 - 40% 30s 10.09+-0.98 14.26+-0.78 4.18 +2.00 4.7 - 40% 40s 10.29+-1.41 16.01+-0.88 5.72 +2.77 4.9 - 40% 50s 10.14+-0.87 16.95+-0.74 6.81 +4.58 8.4 - 40% 60s 10.71+-1.22 17.27+-0.96 6.56 +3.72 6.0 - 40% 75s 10.46+-1.22 17.88+-0.77 7.42 +5.38 7.2 - 50% 15s 10.73+-0.65 13.02+-0.71 2.29 +0.22 3.3 - 50% 20s 12.71+-0.89 15.58+-0.85 2.87 +0.55 3.3 - 50% 25s 12.71+-0.96 17.10+-0.78 4.39 +1.66 5.0 - 50% 30s 13.29+-0.95 18.75+-0.76 5.46 +3.43 6.4 - 50% 40s 14.04+-0.80 20.38+-1.16 6.34 +3.48 6.3 - 50% 50s 14.38+-0.57 21.61+-0.93 7.23 +5.08 9.4 - 50% 60s 14.24+-0.48 22.14+-1.02 7.90 +5.94 9.9 - 50% 75s 14.18+-0.78 23.17+-1.10 8.99 +6.51 9.4 - -=== RANKED by d-prime (separation in pooled standard deviations) - rank duty dur d-prime worst-margin bulk-cost threshold - 1 50% 60s 9.9 +5.94 2.19 18.19 - 2 50% 75s 9.4 +6.51 2.69 18.68 - 3 50% 50s 9.4 +5.08 2.03 18.00 - 4 40% 50s 8.4 +4.58 0.76 13.54 - 5 40% 75s 7.2 +5.38 1.53 14.17 - 6 50% 30s 6.4 +3.43 1.59 16.02 - 7 50% 40s 6.3 +3.48 2.03 17.21 - 8 40% 60s 6.0 +3.72 1.25 13.99 - 9 50% 25s 5.0 +1.66 1.72 14.91 - 10 40% 40s 4.9 +2.77 0.95 13.15 - 11 40% 30s 4.7 +2.00 1.06 12.18 - 12 40% 20s 3.8 +1.06 0.36 10.50 - - (worst-margin = min(no-flow) - max(flow); positive means every - observed no-flow run exceeded every observed flow run. - bulk-cost = degrees C added to the loop per check.) -controller restarted; total elapsed 1.63 h diff --git a/scripts/bench/flow_matrix_results.json b/scripts/bench/flow_matrix_results.json deleted file mode 100644 index 507b662..0000000 --- a/scripts/bench/flow_matrix_results.json +++ /dev/null @@ -1,4145 +0,0 @@ -{ - "base": 22.459370242678233, - "runs": [ - { - "duty": 10, - "flow": true, - "start_down": 22.070385781761104, - "start_up": 22.148125674399637, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 3.4521165180688627, - "up_rise": 0.9351786560037567, - "diff_rise": 2.516937862065106, - "down_abs": 25.522502299829966 - }, - "20": { - "t": 19.06, - "down_rise": 4.088061549762813, - "up_rise": 1.4828252166111042, - "diff_rise": 2.6052363331517085, - "down_abs": 26.158447331523917 - }, - "25": { - "t": 24.08, - "down_rise": 3.92879724358653, - "up_rise": 1.6396069480256301, - "diff_rise": 2.2891902955609, - "down_abs": 25.999183025347634 - }, - "30": { - "t": 29.09, - "down_rise": 3.92879724358653, - "up_rise": 1.1696813208146182, - "diff_rise": 2.7591159227719118, - "down_abs": 25.999183025347634 - }, - "40": { - "t": 39.12, - "down_rise": 4.088061549762813, - "up_rise": 1.4044873630488723, - "diff_rise": 2.6835741867139404, - "down_abs": 26.158447331523917 - }, - "50": { - "t": 49.15, - "down_rise": 4.487063750161724, - "up_rise": 1.6396069480256301, - "diff_rise": 2.8474568021360938, - "down_abs": 26.557449531922828 - }, - "60": { - "t": 59.19, - "down_rise": 2.9770373302504254, - "up_rise": 1.0914805579342328, - "diff_rise": 1.8855567723161926, - "down_abs": 25.04742311201153 - }, - "75": { - "t": 74.23, - "down_rise": 3.8492356020589114, - "up_rise": 1.6396069480256301, - "diff_rise": 2.2096286540332812, - "down_abs": 25.919621383820015 - } - }, - "rep": 0, - "cooldown_s": 0, - "base_at_start": 22.771088546071724 - }, - { - "duty": 10, - "flow": false, - "start_down": 24.1017307953295, - "start_up": 23.005202442288976, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.2959715888885057, - "up_rise": 0.5474105951595334, - "diff_rise": 1.7485609937289723, - "down_abs": 26.397702384218007 - }, - "20": { - "t": 19.06, - "down_rise": 3.0164209486147797, - "up_rise": 0.4691075792852075, - "diff_rise": 2.547313369329572, - "down_abs": 27.11815174394428 - }, - "25": { - "t": 24.07, - "down_rise": 3.096728295939613, - "up_rise": 0.7825301801362912, - "diff_rise": 2.314198115803322, - "down_abs": 27.198459091269115 - }, - "30": { - "t": 29.09, - "down_rise": 3.096728295939613, - "up_rise": 0.6257484487217653, - "diff_rise": 2.4709798472178477, - "down_abs": 27.198459091269115 - }, - "40": { - "t": 39.12, - "down_rise": 3.096728295939613, - "up_rise": 0.7041215176278683, - "diff_rise": 2.3926067783117446, - "down_abs": 27.198459091269115 - }, - "50": { - "t": 49.15, - "down_rise": 3.902785959321477, - "up_rise": 1.4898725793573249, - "diff_rise": 2.4129133799641522, - "down_abs": 28.00451675465098 - }, - "60": { - "t": 59.18, - "down_rise": 3.57970001889845, - "up_rise": 1.4111264481245485, - "diff_rise": 2.1685735707739013, - "down_abs": 27.68143081422795 - }, - "75": { - "t": 74.22, - "down_rise": 4.064670556224996, - "up_rise": 1.8052550805690544, - "diff_rise": 2.259415475655942, - "down_abs": 28.166401351554498 - } - }, - "rep": 0, - "cooldown_s": 0, - "base_at_start": 22.381515612920623 - }, - { - "duty": 15, - "flow": true, - "start_down": 22.225893698005905, - "start_up": 21.837331209719537, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 3.138075138642421, - "up_rise": 0.2330545720415671, - "diff_rise": 2.9050205666008537, - "down_abs": 25.363968836648326 - }, - "20": { - "t": 19.06, - "down_rise": 4.411503760409175, - "up_rise": 1.402275022614333, - "diff_rise": 3.0092287377948423, - "down_abs": 26.63739745841508 - }, - "25": { - "t": 24.08, - "down_rise": 4.97256539326321, - "up_rise": 1.0117637133643598, - "diff_rise": 3.96080167989885, - "down_abs": 27.198459091269115 - }, - "30": { - "t": 29.09, - "down_rise": 4.731800690129319, - "up_rise": 1.1678712325694391, - "diff_rise": 3.5639294575598797, - "down_abs": 26.957694388135224 - }, - "40": { - "t": 39.12, - "down_rise": 5.213807105982141, - "up_rise": 1.402275022614333, - "diff_rise": 3.8115320833678084, - "down_abs": 27.439700803988046 - }, - "50": { - "t": 49.15, - "down_rise": 4.97256539326321, - "up_rise": 2.107327034638729, - "diff_rise": 2.865238358624481, - "down_abs": 27.198459091269115 - }, - "60": { - "t": 59.18, - "down_rise": 4.49150179940591, - "up_rise": 1.4804757854947184, - "diff_rise": 3.0110260139111915, - "down_abs": 26.717395497411815 - }, - "75": { - "t": 74.23, - "down_rise": 4.49150179940591, - "up_rise": 1.1678712325694391, - "diff_rise": 3.3236305668364707, - "down_abs": 26.717395497411815 - } - }, - "rep": 0, - "cooldown_s": 32, - "base_at_start": 22.849094923083896 - }, - { - "duty": 15, - "flow": false, - "start_down": 25.284767845082115, - "start_up": 22.070385781761104, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.638892658718021, - "up_rise": 1.7173468406641632, - "diff_rise": 0.9215458180538576, - "down_abs": 27.923660503800136 - }, - "20": { - "t": 19.06, - "down_rise": 3.368695096479769, - "up_rise": 1.9527901816463782, - "diff_rise": 1.4159049148333906, - "down_abs": 28.653462941561884 - }, - "25": { - "t": 24.07, - "down_rise": 3.939701839541044, - "up_rise": 2.1099373397934187, - "diff_rise": 1.8297644997476255, - "down_abs": 29.22446968462316 - }, - "30": { - "t": 29.09, - "down_rise": 3.4500818926642864, - "up_rise": 1.4822272556874054, - "diff_rise": 1.967854636976881, - "down_abs": 28.7348497377464 - }, - "40": { - "t": 39.12, - "down_rise": 4.0215258384907315, - "up_rise": 1.7173468406641632, - "diff_rise": 2.3041789978265683, - "down_abs": 29.306293683572846 - }, - "50": { - "t": 49.15, - "down_rise": 4.760887173301285, - "up_rise": 2.5034747827869523, - "diff_rise": 2.2574123905143324, - "down_abs": 30.0456550183834 - }, - "60": { - "t": 59.18, - "down_rise": 4.513834765136494, - "up_rise": 2.1099373397934187, - "diff_rise": 2.403897425343075, - "down_abs": 29.79860261021861 - }, - "75": { - "t": 74.22, - "down_rise": 5.091249537822534, - "up_rise": 3.6902513396877907, - "diff_rise": 1.4009981981347437, - "down_abs": 30.37601738290465 - } - }, - "rep": 0, - "cooldown_s": 0, - "base_at_start": 22.381515612920623 - }, - { - "duty": 20, - "flow": true, - "start_down": 23.317806995214255, - "start_up": 22.615168696173612, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 3.9610124436977685, - "up_rise": 0.07794456483503609, - "diff_rise": 3.8830678788627324, - "down_abs": 27.278819438912024 - }, - "20": { - "t": 19.06, - "down_rise": 4.041426208584426, - "up_rise": -0.1557984534953789, - "diff_rise": 4.197224662079805, - "down_abs": 27.35923320379868 - }, - "25": { - "t": 24.08, - "down_rise": 5.417042742532146, - "up_rise": 0.7026382990406432, - "diff_rise": 4.714404443491503, - "down_abs": 28.7348497377464 - }, - "30": { - "t": 29.09, - "down_rise": 3.6398873929209685, - "up_rise": 0.1559198498981118, - "diff_rise": 3.4839675430228567, - "down_abs": 26.957694388135224 - }, - "40": { - "t": 39.12, - "down_rise": 4.202415663470674, - "up_rise": 0.7026382990406432, - "diff_rise": 3.499777364430031, - "down_abs": 27.52022265868493 - }, - "50": { - "t": 49.15, - "down_rise": 4.605853508585881, - "up_rise": 0.7026382990406432, - "diff_rise": 3.9032152095452375, - "down_abs": 27.923660503800136 - }, - "60": { - "t": 59.18, - "down_rise": 3.8003447487300264, - "up_rise": -0.1557984534953789, - "diff_rise": 3.9561432022254053, - "down_abs": 27.11815174394428 - }, - "75": { - "t": 74.23, - "down_rise": 5.8249024842558015, - "up_rise": 1.0157821948371293, - "diff_rise": 4.809120289418672, - "down_abs": 29.142709479470057 - } - }, - "rep": 0, - "cooldown_s": 11, - "base_at_start": 22.771088546071724 - }, - { - "duty": 20, - "flow": false, - "start_down": 23.55261303744851, - "start_up": 22.771088546071724, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 4.938257940846029, - "up_rise": 0.6249529202519852, - "diff_rise": 4.3133050205940435, - "down_abs": 28.490870978294538 - }, - "20": { - "t": 19.06, - "down_rise": 5.426765623323888, - "up_rise": 0.9382354138451205, - "diff_rise": 4.488530209478768, - "down_abs": 28.979378660772397 - }, - "25": { - "t": 24.07, - "down_rise": 6.4930419809348905, - "up_rise": 0.31221578433166997, - "diff_rise": 6.1808261966032205, - "down_abs": 30.0456550183834 - }, - "30": { - "t": 29.09, - "down_rise": 6.82340434545614, - "up_rise": 1.173569698286542, - "diff_rise": 5.649834647169598, - "down_abs": 30.37601738290465 - }, - "40": { - "t": 39.12, - "down_rise": 7.821354852110346, - "up_rise": 2.0393689767863066, - "diff_rise": 5.78198587532404, - "down_abs": 31.373967889558855 - }, - "50": { - "t": 49.15, - "down_rise": 7.570874105124801, - "up_rise": 1.6452403443418007, - "diff_rise": 5.9256337607830005, - "down_abs": 31.12348714257331 - }, - "60": { - "t": 59.18, - "down_rise": 8.91484766495347, - "up_rise": 2.751413753758243, - "diff_rise": 6.163433911195227, - "down_abs": 32.46746070240198 - }, - "75": { - "t": 74.22, - "down_rise": 9.084287531902817, - "up_rise": 3.4670622538930616, - "diff_rise": 5.617225278009755, - "down_abs": 32.636900569351326 - } - }, - "rep": 0, - "cooldown_s": 11, - "base_at_start": 22.459370242678233 - }, - { - "duty": 30, - "flow": true, - "start_down": 22.459370242678233, - "start_up": 22.459370242678233, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 6.031500735616305, - "up_rise": 0.5458321996107429, - "diff_rise": 5.485668536005562, - "down_abs": 28.490870978294538 - }, - "20": { - "t": 19.06, - "down_rise": 7.833952386032308, - "up_rise": 0.5458321996107429, - "diff_rise": 7.2881201864215654, - "down_abs": 30.29332262871054 - }, - "25": { - "t": 24.08, - "down_rise": 7.999412117826807, - "up_rise": 1.1715806483325082, - "diff_rise": 6.827831469494299, - "down_abs": 30.45878236050504 - }, - "30": { - "t": 29.09, - "down_rise": 8.414306912881102, - "up_rise": 1.4852880016800327, - "diff_rise": 6.929018911201069, - "down_abs": 30.873677155559335 - }, - "40": { - "t": 39.12, - "down_rise": 7.9166471402264165, - "up_rise": 1.406807014729111, - "diff_rise": 6.5098401254973055, - "down_abs": 30.37601738290465 - }, - "50": { - "t": 49.15, - "down_rise": 9.081963970045933, - "up_rise": 1.328362379747034, - "diff_rise": 7.753601590298899, - "down_abs": 31.541334212724166 - }, - "60": { - "t": 59.18, - "down_rise": 8.580773023064296, - "up_rise": 1.563805720729249, - "diff_rise": 7.016967302335047, - "down_abs": 31.04014326574253 - }, - "75": { - "t": 74.23, - "down_rise": 8.998242564740565, - "up_rise": 1.7995830815011686, - "diff_rise": 7.198659483239396, - "down_abs": 31.457612807418798 - } - }, - "rep": 0, - "cooldown_s": 21, - "base_at_start": 22.225893698005905 - }, - { - "duty": 30, - "flow": false, - "start_down": 23.317806995214255, - "start_up": 22.148125674399637, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 7.306717877700692, - "up_rise": 1.4044873630488723, - "diff_rise": 5.902230514651819, - "down_abs": 30.624524872914947 - }, - "20": { - "t": 19.06, - "down_rise": 9.403938637105114, - "up_rise": 1.9536051209298648, - "diff_rise": 7.450333516175249, - "down_abs": 32.72174563231937 - }, - "25": { - "t": 24.08, - "down_rise": 9.82943623777237, - "up_rise": 2.1108276497797647, - "diff_rise": 7.718608587992605, - "down_abs": 33.147243232986625 - }, - "30": { - "t": 29.09, - "down_rise": 11.032586771371257, - "up_rise": 2.583425754058794, - "diff_rise": 8.449161017312463, - "down_abs": 34.35039376658551 - }, - "40": { - "t": 39.12, - "down_rise": 12.695374979640576, - "up_rise": 4.01032165712428, - "diff_rise": 8.685053322516296, - "down_abs": 36.01318197485483 - }, - "50": { - "t": 49.15, - "down_rise": 12.254266210883031, - "up_rise": 4.489271784015443, - "diff_rise": 7.764994426867588, - "down_abs": 35.572073206097286 - }, - "60": { - "t": 59.18, - "down_rise": 12.695374979640576, - "up_rise": 4.649318383860248, - "diff_rise": 8.046056595780328, - "down_abs": 36.01318197485483 - }, - "75": { - "t": 74.23, - "down_rise": 12.872554797995406, - "up_rise": 5.211107529399044, - "diff_rise": 7.661447268596362, - "down_abs": 36.19036179320966 - } - }, - "rep": 0, - "cooldown_s": 11, - "base_at_start": 22.303690221175657 - }, - { - "duty": 40, - "flow": true, - "start_down": 22.459370242678233, - "start_up": 23.23960623233387, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 8.165154630236714, - "up_rise": -0.39051130924997324, - "diff_rise": 8.555665939486687, - "down_abs": 30.624524872914947 - }, - "20": { - "t": 19.06, - "down_rise": 10.43231839021297, - "up_rise": 0.15643523398983916, - "diff_rise": 10.27588315622313, - "down_abs": 32.8916886328912 - }, - "25": { - "t": 24.07, - "down_rise": 9.754548524215124, - "up_rise": 0.3913446586768714, - "diff_rise": 9.363203865538253, - "down_abs": 32.21391876689336 - }, - "30": { - "t": 29.09, - "down_rise": 11.459280572458908, - "up_rise": 1.4130796723769095, - "diff_rise": 10.046200900081999, - "down_abs": 33.91865081513714 - }, - "40": { - "t": 39.12, - "down_rise": 12.151198704366095, - "up_rise": 0.7050520120243959, - "diff_rise": 11.4461466923417, - "down_abs": 34.61056894704433 - }, - "50": { - "t": 49.15, - "down_rise": 11.459280572458908, - "up_rise": 0.07820076288038535, - "diff_rise": 11.381079809578523, - "down_abs": 33.91865081513714 - }, - "60": { - "t": 59.18, - "down_rise": 12.325128934434531, - "up_rise": 0.4697177275829745, - "diff_rise": 11.855411206851556, - "down_abs": 34.784499177112764 - }, - "75": { - "t": 74.22, - "down_rise": 11.545443566991537, - "up_rise": 1.1767226580796546, - "diff_rise": 10.368720908911882, - "down_abs": 34.00481380966977 - } - }, - "rep": 0, - "cooldown_s": 21, - "base_at_start": 22.927132764254054 - }, - { - "duty": 40, - "flow": false, - "start_down": 22.537254480322474, - "start_up": 22.148125674399637, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 10.609988752664151, - "up_rise": 0.46704302177397494, - "diff_rise": 10.142945730890176, - "down_abs": 33.147243232986625 - }, - "20": { - "t": 19.06, - "down_rise": 12.771391125897765, - "up_rise": 1.718051583007707, - "diff_rise": 11.053339542890058, - "down_abs": 35.30864560622024 - }, - "25": { - "t": 24.08, - "down_rise": 14.815442954927164, - "up_rise": 2.6623318484583933, - "diff_rise": 12.15311110646877, - "down_abs": 37.35269743524964 - }, - "30": { - "t": 29.09, - "down_rise": 15.267614086961771, - "up_rise": 3.9306658663753637, - "diff_rise": 11.336948220586407, - "down_abs": 37.804868567284245 - }, - "40": { - "t": 39.12, - "down_rise": 17.293684071072846, - "up_rise": 4.649318383860248, - "diff_rise": 12.644365687212598, - "down_abs": 39.83093855139532 - }, - "50": { - "t": 49.15, - "down_rise": 18.04622204488851, - "up_rise": 5.372096984285292, - "diff_rise": 12.674125060603217, - "down_abs": 40.58347652521098 - }, - "60": { - "t": 59.18, - "down_rise": 18.616543568755958, - "up_rise": 6.099304882156503, - "diff_rise": 12.517238686599455, - "down_abs": 41.15379804907843 - }, - "75": { - "t": 74.23, - "down_rise": 19.19213667870116, - "up_rise": 6.668171987900621, - "diff_rise": 12.52396469080054, - "down_abs": 41.729391159023635 - } - }, - "rep": 0, - "cooldown_s": 21, - "base_at_start": 22.459370242678233 - }, - { - "duty": 50, - "flow": true, - "start_down": 21.52696332098617, - "start_up": 22.225893698005905, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 11.705637597451982, - "up_rise": 1.2484163235682786, - "diff_rise": 10.457221273883704, - "down_abs": 33.23260091843815 - }, - "20": { - "t": 19.06, - "down_rise": 13.95719876409754, - "up_rise": 2.269181323640396, - "diff_rise": 11.688017440457145, - "down_abs": 35.48416208508371 - }, - "25": { - "t": 24.08, - "down_rise": 14.133123734852063, - "up_rise": 1.9544294235486177, - "diff_rise": 12.178694311303445, - "down_abs": 35.66008705583823 - }, - "30": { - "t": 29.09, - "down_rise": 14.752148654124312, - "up_rise": 1.7187645463523609, - "diff_rise": 13.033384107771951, - "down_abs": 36.27911197511048 - }, - "40": { - "t": 39.12, - "down_rise": 15.376450920094783, - "up_rise": 2.033059626173497, - "diff_rise": 13.343391293921286, - "down_abs": 36.90341424108095 - }, - "50": { - "t": 49.15, - "down_rise": 15.286933496980396, - "up_rise": 1.7187645463523609, - "diff_rise": 13.568168950628035, - "down_abs": 36.813896817966565 - }, - "60": { - "t": 59.18, - "down_rise": 14.929972716824238, - "up_rise": 1.4834302619109394, - "diff_rise": 13.446542454913299, - "down_abs": 36.45693603781041 - }, - "75": { - "t": 74.23, - "down_rise": 15.286933496980396, - "up_rise": 3.138075138642421, - "diff_rise": 12.148858358337975, - "down_abs": 36.813896817966565 - } - }, - "rep": 0, - "cooldown_s": 32, - "base_at_start": 22.225893698005905 - }, - { - "duty": 50, - "flow": false, - "start_down": 23.474310021574183, - "start_up": 23.161438802330906, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 11.922043064742411, - "up_rise": 0.3128712192432772, - "diff_rise": 11.609171845499134, - "down_abs": 35.396353086316594 - }, - "20": { - "t": 19.06, - "down_rise": 15.705262540411411, - "up_rise": 1.806954173329757, - "diff_rise": 13.898308367081654, - "down_abs": 39.179572561985594 - }, - "25": { - "t": 24.08, - "down_rise": 15.797919675415983, - "up_rise": 1.649018720527124, - "diff_rise": 14.148900954888859, - "down_abs": 39.272229696990166 - }, - "30": { - "t": 29.09, - "down_rise": 18.73890311015134, - "up_rise": 3.6360052559289784, - "diff_rise": 15.102897854222363, - "down_abs": 42.213213131725524 - }, - "40": { - "t": 39.12, - "down_rise": 20.513802440643076, - "up_rise": 5.085991754225233, - "diff_rise": 15.427810686417843, - "down_abs": 43.98811246221726 - }, - "50": { - "t": 49.15, - "down_rise": 21.727924864406646, - "up_rise": 5.817939858441491, - "diff_rise": 15.909985005965154, - "down_abs": 45.20223488598083 - }, - "60": { - "t": 59.18, - "down_rise": 22.035568189081857, - "up_rise": 6.4727978226123355, - "diff_rise": 15.562770366469522, - "down_abs": 45.50987821065604 - }, - "75": { - "t": 74.23, - "down_rise": 22.241608705780948, - "up_rise": 7.380179230505007, - "diff_rise": 14.86142947527594, - "down_abs": 45.71591872735513 - } - }, - "rep": 0, - "cooldown_s": 21, - "base_at_start": 22.537254480322474 - }, - { - "duty": 10, - "flow": true, - "start_down": 22.459370242678233, - "start_up": 21.914988917022924, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 2.667125132253659, - "up_rise": 0.934106006060972, - "diff_rise": 1.733019126192687, - "down_abs": 25.126495374931892 - }, - "20": { - "t": 19.06, - "down_rise": 2.9045985939700927, - "up_rise": 0.934106006060972, - "diff_rise": 1.9704925879091206, - "down_abs": 25.363968836648326 - }, - "25": { - "t": 24.08, - "down_rise": 2.114490321869823, - "up_rise": 1.1683154133804692, - "diff_rise": 0.946174908489354, - "down_abs": 24.573860564548056 - }, - "30": { - "t": 29.09, - "down_rise": 3.1424653181324516, - "up_rise": 1.0902135252660514, - "diff_rise": 2.0522517928664, - "down_abs": 25.601835560810684 - }, - "40": { - "t": 39.12, - "down_rise": 3.1424653181324516, - "up_rise": 0.8560996290487992, - "diff_rise": 2.2863656890836523, - "down_abs": 25.601835560810684 - }, - "50": { - "t": 49.15, - "down_rise": 2.825397602403882, - "up_rise": 0.7781243439857235, - "diff_rise": 2.0472732584181585, - "down_abs": 25.284767845082115 - }, - "60": { - "t": 59.18, - "down_rise": 1.9569586477352914, - "up_rise": 0.23313675737671247, - "diff_rise": 1.723821890358579, - "down_abs": 24.416328890413524 - }, - "75": { - "t": 74.23, - "down_rise": 2.667125132253659, - "up_rise": 1.1683154133804692, - "diff_rise": 1.49880971887319, - "down_abs": 25.126495374931892 - } - }, - "rep": 1, - "cooldown_s": 32, - "base_at_start": 21.837331209719537 - }, - { - "duty": 10, - "flow": false, - "start_down": 23.787732622425267, - "start_up": 22.459370242678233, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 1.338762752506625, - "up_rise": -0.6220390329586962, - "diff_rise": 1.9608017854653212, - "down_abs": 25.126495374931892 - }, - "20": { - "t": 19.06, - "down_rise": 2.450418177539518, - "up_rise": 0.38972468040566355, - "diff_rise": 2.0606934971338546, - "down_abs": 26.238150799964785 - }, - "25": { - "t": 24.07, - "down_rise": 2.131888761394748, - "up_rise": -0.15568002150257598, - "diff_rise": 2.287568782897324, - "down_abs": 25.919621383820015 - }, - "30": { - "t": 29.09, - "down_rise": 2.2910589183497336, - "up_rise": 0.6239340877251607, - "diff_rise": 1.667124830624573, - "down_abs": 26.078791540775 - }, - "40": { - "t": 39.12, - "down_rise": 3.0097114358346175, - "up_rise": 0.5458321996107429, - "diff_rise": 2.4638792362238746, - "down_abs": 26.797444058259885 - }, - "50": { - "t": 49.15, - "down_rise": 3.651968181562779, - "up_rise": 1.1715806483325082, - "diff_rise": 2.480387533230271, - "down_abs": 27.439700803988046 - }, - "60": { - "t": 59.18, - "down_rise": 2.7697169094975607, - "up_rise": 1.2499537172386113, - "diff_rise": 1.5197631922589494, - "down_abs": 26.557449531922828 - }, - "75": { - "t": 74.23, - "down_rise": 3.4910868164867566, - "up_rise": 0.8584367525360221, - "diff_rise": 2.6326500639507344, - "down_abs": 27.278819438912024 - } - }, - "rep": 1, - "cooldown_s": 0, - "base_at_start": 22.303690221175657 - }, - { - "duty": 15, - "flow": true, - "start_down": 22.615168696173612, - "start_up": 22.303690221175657, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 2.1952888266844184, - "up_rise": -0.23330443941455314, - "diff_rise": 2.4285932660989715, - "down_abs": 24.81045752285803 - }, - "20": { - "t": 19.06, - "down_rise": 3.7825336880443956, - "up_rise": 0.5454047019082395, - "diff_rise": 3.237128986136156, - "down_abs": 26.397702384218007 - }, - "25": { - "t": 24.08, - "down_rise": 3.7825336880443956, - "up_rise": 0.4673983248960667, - "diff_rise": 3.315135363148329, - "down_abs": 26.397702384218007 - }, - "30": { - "t": 29.09, - "down_rise": 3.463622844601389, - "up_rise": 0.23356425914681722, - "diff_rise": 3.2300585854545716, - "down_abs": 26.078791540775 - }, - "40": { - "t": 39.12, - "down_rise": 3.3044526876464033, - "up_rise": 0.23356425914681722, - "diff_rise": 3.070888428499586, - "down_abs": 25.919621383820015 - }, - "50": { - "t": 49.15, - "down_rise": 3.3044526876464033, - "up_rise": 0.15568002150257598, - "diff_rise": 3.1487726661438273, - "down_abs": 25.919621383820015 - }, - "60": { - "t": 59.18, - "down_rise": 4.262374855177484, - "up_rise": -0.3887013041527325, - "diff_rise": 4.651076159330216, - "down_abs": 26.877543551351096 - }, - "75": { - "t": 74.23, - "down_rise": 4.182275362086273, - "up_rise": 0.7796141092277367, - "diff_rise": 3.402661252858536, - "down_abs": 26.797444058259885 - } - }, - "rep": 1, - "cooldown_s": 11, - "base_at_start": 22.148125674399637 - }, - { - "duty": 15, - "flow": false, - "start_down": 24.73155142845843, - "start_up": 21.682095410064733, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.708149375529615, - "up_rise": 1.7139460562589761, - "diff_rise": 0.994203319270639, - "down_abs": 27.439700803988046 - }, - "20": { - "t": 19.06, - "down_rise": 3.434849923096067, - "up_rise": 1.8705176273837765, - "diff_rise": 1.5643322957122905, - "down_abs": 28.166401351554498 - }, - "25": { - "t": 24.07, - "down_rise": 3.759319549836107, - "up_rise": 2.0272285498521114, - "diff_rise": 1.7320909999839955, - "down_abs": 28.490870978294538 - }, - "30": { - "t": 29.09, - "down_rise": 4.084746233841827, - "up_rise": 1.6357115851495223, - "diff_rise": 2.4490346486923045, - "down_abs": 28.816297662300258 - }, - "40": { - "t": 39.12, - "down_rise": 4.329461191847997, - "up_rise": 2.419635385264769, - "diff_rise": 1.9098258065832283, - "down_abs": 29.06101262030643 - }, - "50": { - "t": 49.15, - "down_rise": 4.820601514825114, - "up_rise": 2.184081847342611, - "diff_rise": 2.636519667482503, - "down_abs": 29.552152943283545 - }, - "60": { - "t": 59.18, - "down_rise": 4.9848349331965665, - "up_rise": 2.3410805533427492, - "diff_rise": 2.6437543798538172, - "down_abs": 29.716386361654997 - }, - "75": { - "t": 74.22, - "down_rise": 5.892973444456516, - "up_rise": 3.0494560183936983, - "diff_rise": 2.8435174260628173, - "down_abs": 30.624524872914947 - } - }, - "rep": 1, - "cooldown_s": 0, - "base_at_start": 22.771088546071724 - }, - { - "duty": 20, - "flow": true, - "start_down": 23.39604146632371, - "start_up": 22.927132764254054, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 3.321354031088106, - "up_rise": -0.07803784117015766, - "diff_rise": 3.3993918722582634, - "down_abs": 26.717395497411815 - }, - "20": { - "t": 19.06, - "down_rise": 3.321354031088106, - "up_rise": -0.5456171513334311, - "diff_rise": 3.866971182421537, - "down_abs": 26.717395497411815 - }, - "25": { - "t": 24.08, - "down_rise": 3.321354031088106, - "up_rise": -0.4677625215758212, - "diff_rise": 3.789116552663927, - "down_abs": 26.717395497411815 - }, - "30": { - "t": 29.09, - "down_rise": 4.689388761298005, - "up_rise": 0.39067423096020093, - "diff_rise": 4.298714530337804, - "down_abs": 28.085430227621714 - }, - "40": { - "t": 39.12, - "down_rise": 4.366076493006403, - "up_rise": -0.4677625215758212, - "diff_rise": 4.8338390145822245, - "down_abs": 27.762117959330112 - }, - "50": { - "t": 49.15, - "down_rise": 5.257421475238175, - "up_rise": 0.07806967803492171, - "diff_rise": 5.179351797203253, - "down_abs": 28.653462941561884 - }, - "60": { - "t": 59.18, - "down_rise": 4.12418119236122, - "up_rise": -0.7790070898544172, - "diff_rise": 4.903188282215638, - "down_abs": 27.52022265868493 - }, - "75": { - "t": 74.23, - "down_rise": 4.12418119236122, - "up_rise": -0.4677625215758212, - "diff_rise": 4.591943713937042, - "down_abs": 27.52022265868493 - } - }, - "rep": 1, - "cooldown_s": 11, - "base_at_start": 22.148125674399637 - }, - { - "duty": 20, - "flow": false, - "start_down": 25.284767845082115, - "start_up": 22.693113261008648, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 3.6130393108580847, - "up_rise": 0.702928205315061, - "diff_rise": 2.9101111055430238, - "down_abs": 28.8978071559402 - }, - "20": { - "t": 19.06, - "down_rise": 4.349468779861127, - "up_rise": 0.46832554132225823, - "diff_rise": 3.8811432385388684, - "down_abs": 29.63423662494324 - }, - "25": { - "t": 24.08, - "down_rise": 5.174014515422925, - "up_rise": 1.094619361416619, - "diff_rise": 4.079395154006306, - "down_abs": 30.45878236050504 - }, - "30": { - "t": 29.09, - "down_rise": 5.505786111870009, - "up_rise": 1.2515449833496177, - "diff_rise": 4.254241128520391, - "down_abs": 30.790553956952124 - }, - "40": { - "t": 39.12, - "down_rise": 5.339757027832832, - "up_rise": 1.0162106989081963, - "diff_rise": 4.323546328924635, - "down_abs": 30.624524872914947 - }, - "50": { - "t": 49.15, - "down_rise": 6.844799646719025, - "up_rise": 1.9595726437021312, - "diff_rise": 4.885227003016894, - "down_abs": 32.12956749180114 - }, - "60": { - "t": 59.18, - "down_rise": 6.676338085722875, - "up_rise": 2.433382113923244, - "diff_rise": 4.242955971799631, - "down_abs": 31.96110593080499 - }, - "75": { - "t": 74.23, - "down_rise": 7.26737125461932, - "up_rise": 3.0675238604402466, - "diff_rise": 4.1998473941790735, - "down_abs": 32.552139099701435 - } - }, - "rep": 1, - "cooldown_s": 0, - "base_at_start": 22.771088546071724 - }, - { - "duty": 30, - "flow": true, - "start_down": 23.787732622425267, - "start_up": 22.070385781761104, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 5.5185610611475795, - "up_rise": 0.7787091413227927, - "diff_rise": 4.739851919824787, - "down_abs": 29.306293683572846 - }, - "20": { - "t": 19.06, - "down_rise": 6.753885410410646, - "up_rise": 1.4039242398130796, - "diff_rise": 5.349961170597567, - "down_abs": 30.541618032835913 - }, - "25": { - "t": 24.08, - "down_rise": 6.422965005850301, - "up_rise": 1.2474212134531513, - "diff_rise": 5.17554379239715, - "down_abs": 30.210697628275568 - }, - "30": { - "t": 29.09, - "down_rise": 7.169140808820487, - "up_rise": 1.4039242398130796, - "diff_rise": 5.765216569007407, - "down_abs": 30.956873431245754 - }, - "40": { - "t": 39.12, - "down_rise": 7.753601590298899, - "up_rise": 1.4039242398130796, - "diff_rise": 6.34967735048582, - "down_abs": 31.541334212724166 - }, - "50": { - "t": 49.15, - "down_rise": 7.669880184993531, - "up_rise": 2.031345013568398, - "diff_rise": 5.638535171425133, - "down_abs": 31.457612807418798 - }, - "60": { - "t": 59.18, - "down_rise": 7.335754520148043, - "up_rise": 1.0129185486422898, - "diff_rise": 6.322835971505754, - "down_abs": 31.12348714257331 - }, - "75": { - "t": 74.22, - "down_rise": 7.002821334526857, - "up_rise": 0.23330443941455314, - "diff_rise": 6.769516895112304, - "down_abs": 30.790553956952124 - } - }, - "rep": 1, - "cooldown_s": 11, - "base_at_start": 21.99267365190036 - }, - { - "duty": 30, - "flow": false, - "start_down": 27.600799188253063, - "start_up": 23.23960623233387, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 4.697551863404897, - "up_rise": 0.7835697310736123, - "diff_rise": 3.9139821323312844, - "down_abs": 32.29835105165796 - }, - "20": { - "t": 19.06, - "down_rise": 4.866661514148916, - "up_rise": 0.23470378924031365, - "diff_rise": 4.631957724908602, - "down_abs": 32.46746070240198 - }, - "25": { - "t": 24.07, - "down_rise": 6.404014621416707, - "up_rise": 1.8078168796776595, - "diff_rise": 4.596197741739047, - "down_abs": 34.00481380966977 - }, - "30": { - "t": 29.09, - "down_rise": 7.096686478023514, - "up_rise": 1.9660039230356574, - "diff_rise": 5.130682554987857, - "down_abs": 34.69748566627658 - }, - "40": { - "t": 39.12, - "down_rise": 7.532733613857431, - "up_rise": 2.521030889115025, - "diff_rise": 5.011702724742406, - "down_abs": 35.133532802110494 - }, - "50": { - "t": 49.15, - "down_rise": 8.147405055466038, - "up_rise": 2.839185308441131, - "diff_rise": 5.308219747024907, - "down_abs": 35.7482042437191 - }, - "60": { - "t": 59.18, - "down_rise": 8.767170657113013, - "up_rise": 4.119626971464811, - "diff_rise": 4.647543685648202, - "down_abs": 36.367969845366076 - }, - "75": { - "t": 74.22, - "down_rise": 9.932415429624825, - "up_rise": 4.441824581894082, - "diff_rise": 5.4905908477307435, - "down_abs": 37.53321461787789 - } - }, - "rep": 1, - "cooldown_s": 0, - "base_at_start": 22.927132764254054 - }, - { - "duty": 40, - "flow": true, - "start_down": 24.416328890413524, - "start_up": 22.693113261008648, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 7.125005322310642, - "up_rise": 1.2515449833496177, - "diff_rise": 5.873460338961024, - "down_abs": 31.541334212724166 - }, - "20": { - "t": 19.06, - "down_rise": 8.475359742477679, - "up_rise": 0.6246937342056071, - "diff_rise": 7.850666008272071, - "down_abs": 32.8916886328912 - }, - "25": { - "t": 24.07, - "down_rise": 9.502321924723617, - "up_rise": 1.330062702398834, - "diff_rise": 8.172259222324783, - "down_abs": 33.91865081513714 - }, - "30": { - "t": 29.09, - "down_rise": 8.816272028024628, - "up_rise": 0.46832554132225823, - "diff_rise": 8.34794648670237, - "down_abs": 33.23260091843815 - }, - "40": { - "t": 39.12, - "down_rise": 8.64564342226629, - "up_rise": 0.3901910693947457, - "diff_rise": 8.255452352871544, - "down_abs": 33.06197231267981 - }, - "50": { - "t": 49.15, - "down_rise": 9.330270414745598, - "up_rise": 0.7811967605655354, - "diff_rise": 8.549073654180063, - "down_abs": 33.74659930515912 - }, - "60": { - "t": 59.18, - "down_rise": 9.158581185830087, - "up_rise": 1.4086175343208538, - "diff_rise": 7.749963651509233, - "down_abs": 33.57491007624361 - }, - "75": { - "t": 74.22, - "down_rise": 9.416250608384246, - "up_rise": 1.7232156294048764, - "diff_rise": 7.6930349789793695, - "down_abs": 33.83257949879777 - } - }, - "rep": 1, - "cooldown_s": 11, - "base_at_start": 22.615168696173612 - }, - { - "duty": 40, - "flow": false, - "start_down": 24.2589533241794, - "start_up": 22.070385781761104, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 9.832115716181136, - "up_rise": 1.79579147564624, - "diff_rise": 8.036324240534896, - "down_abs": 34.09106904036054 - }, - "20": { - "t": 19.06, - "down_rise": 11.4892509195397, - "up_rise": 2.5823001229496754, - "diff_rise": 8.906950796590024, - "down_abs": 35.7482042437191 - }, - "25": { - "t": 24.07, - "down_rise": 12.913688973948922, - "up_rise": 3.293583054887222, - "diff_rise": 9.6201059190617, - "down_abs": 37.17264229812832 - }, - "30": { - "t": 29.09, - "down_rise": 13.455245927339377, - "up_rise": 3.6902513396877907, - "diff_rise": 9.764994587651586, - "down_abs": 37.71419925151878 - }, - "40": { - "t": 39.12, - "down_rise": 14.920619237806193, - "up_rise": 5.047765962183178, - "diff_rise": 9.872853275623015, - "down_abs": 39.179572561985594 - }, - "50": { - "t": 49.15, - "down_rise": 16.041282908900598, - "up_rise": 5.611045032466848, - "diff_rise": 10.43023787643375, - "down_abs": 40.30023623308 - }, - "60": { - "t": 59.18, - "down_rise": 16.041282908900598, - "up_rise": 6.096015569793394, - "diff_rise": 9.945267339107204, - "down_abs": 40.30023623308 - }, - "75": { - "t": 74.22, - "down_rise": 17.954259807546123, - "up_rise": 6.827421374179096, - "diff_rise": 11.126838433367027, - "down_abs": 42.213213131725524 - } - }, - "rep": 1, - "cooldown_s": 11, - "base_at_start": 22.615168696173612 - }, - { - "duty": 50, - "flow": true, - "start_down": 23.709323959916844, - "start_up": 22.381515612920623, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 10.641069806668668, - "up_rise": 0.8580906194132467, - "diff_rise": 9.782979187255421, - "down_abs": 34.35039376658551 - }, - "20": { - "t": 19.06, - "down_rise": 11.511715084182072, - "up_rise": 1.4846616444867209, - "diff_rise": 10.02705343969535, - "down_abs": 35.221039044098916 - }, - "25": { - "t": 24.08, - "down_rise": 12.215427126449981, - "up_rise": 1.406217009504644, - "diff_rise": 10.809210116945337, - "down_abs": 35.924751086366825 - }, - "30": { - "t": 29.09, - "down_rise": 12.658645885449232, - "up_rise": 1.406217009504644, - "diff_rise": 11.252428875944588, - "down_abs": 36.367969845366076 - }, - "40": { - "t": 39.12, - "down_rise": 13.283719897207902, - "up_rise": 2.271170291790156, - "diff_rise": 11.012549605417746, - "down_abs": 36.993043857124746 - }, - "50": { - "t": 49.15, - "down_rise": 13.733573974441583, - "up_rise": 1.7988075086338995, - "diff_rise": 11.934766465807684, - "down_abs": 37.44289793435843 - }, - "60": { - "t": 59.18, - "down_rise": 14.277240678118176, - "up_rise": 2.58687736274004, - "diff_rise": 11.690363315378136, - "down_abs": 37.98656463803502 - }, - "75": { - "t": 74.23, - "down_rise": 13.194090281164108, - "up_rise": 2.271170291790156, - "diff_rise": 10.922919989373952, - "down_abs": 36.90341424108095 - } - }, - "rep": 1, - "cooldown_s": 21, - "base_at_start": 22.303690221175657 - }, - { - "duty": 50, - "flow": false, - "start_down": 23.161438802330906, - "start_up": 22.615168696173612, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 13.117673172779575, - "up_rise": 1.0157821948371293, - "diff_rise": 12.101890977942446, - "down_abs": 36.27911197511048 - }, - "20": { - "t": 19.06, - "down_rise": 14.91615397824745, - "up_rise": 1.7224530898047306, - "diff_rise": 13.19370088844272, - "down_abs": 38.07759278057836 - }, - "25": { - "t": 24.07, - "down_rise": 17.422037722880077, - "up_rise": 3.224937519533512, - "diff_rise": 14.197100203346565, - "down_abs": 40.58347652521098 - }, - "30": { - "t": 29.09, - "down_rise": 18.18362882855064, - "up_rise": 3.7825336880443956, - "diff_rise": 14.401095140506243, - "down_abs": 41.345067630881545 - }, - "40": { - "t": 39.12, - "down_rise": 18.85778368178171, - "up_rise": 4.342525691961612, - "diff_rise": 14.5152579898201, - "down_abs": 42.01922248411262 - }, - "50": { - "t": 49.15, - "down_rise": 21.02724578143409, - "up_rise": 5.7944962404234275, - "diff_rise": 15.232749541010662, - "down_abs": 44.188684583764996 - }, - "60": { - "t": 59.18, - "down_rise": 21.531763198211365, - "up_rise": 6.609300988449547, - "diff_rise": 14.922462209761818, - "down_abs": 44.69320200054227 - }, - "75": { - "t": 74.22, - "down_rise": 23.59636634808146, - "up_rise": 7.6781539325369295, - "diff_rise": 15.91821241554453, - "down_abs": 46.757805150412366 - } - }, - "rep": 1, - "cooldown_s": 21, - "base_at_start": 22.537254480322474 - }, - { - "duty": 10, - "flow": true, - "start_down": 22.303690221175657, - "start_up": 22.927132764254054, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 2.3489956835351222, - "up_rise": -0.07803784117015766, - "diff_rise": 2.42703352470528, - "down_abs": 24.65268590471078 - }, - "20": { - "t": 19.07, - "down_rise": 2.5857143545545114, - "up_rise": -0.3119640680804423, - "diff_rise": 2.8976784226349537, - "down_abs": 24.88940457573017 - }, - "25": { - "t": 24.08, - "down_rise": 2.5857143545545114, - "up_rise": 0.0, - "diff_rise": 2.5857143545545114, - "down_abs": 24.88940457573017 - }, - "30": { - "t": 29.1, - "down_rise": 2.427861207282774, - "up_rise": -1.2450373541893214, - "diff_rise": 3.6728985614720955, - "down_abs": 24.73155142845843 - }, - "40": { - "t": 39.13, - "down_rise": 3.139523302303644, - "up_rise": -0.5456171513334311, - "diff_rise": 3.685140453637075, - "down_abs": 25.4432135234793 - }, - "50": { - "t": 49.16, - "down_rise": 2.981077623906458, - "up_rise": -0.3119640680804423, - "diff_rise": 3.2930416919869003, - "down_abs": 25.284767845082115 - }, - "60": { - "t": 59.19, - "down_rise": 2.3489956835351222, - "up_rise": -0.5456171513334311, - "diff_rise": 2.8946128348685534, - "down_abs": 24.65268590471078 - }, - "75": { - "t": 74.23, - "down_rise": 2.822805153756235, - "up_rise": 0.23430603807685202, - "diff_rise": 2.588499115679383, - "down_abs": 25.126495374931892 - } - }, - "rep": 2, - "cooldown_s": 32, - "base_at_start": 22.070385781761104 - }, - { - "duty": 10, - "flow": false, - "start_down": 23.23960623233387, - "start_up": 21.682095410064733, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 1.255468789312431, - "up_rise": 0.6215948111109242, - "diff_rise": 0.6338739782015068, - "down_abs": 24.4950750216463 - }, - "20": { - "t": 19.06, - "down_rise": 2.839185308441131, - "up_rise": 1.0889931360069909, - "diff_rise": 1.75019217243414, - "down_abs": 26.078791540775 - }, - "25": { - "t": 24.08, - "down_rise": 3.2379450772875202, - "up_rise": 1.2450373541893214, - "diff_rise": 1.9929077230981989, - "down_abs": 26.47755130962139 - }, - "30": { - "t": 29.09, - "down_rise": 3.3977912260812104, - "up_rise": 1.4012089203386608, - "diff_rise": 1.9965823057425496, - "down_abs": 26.63739745841508 - }, - "40": { - "t": 39.12, - "down_rise": 3.317843299588958, - "up_rise": 1.0110178509439152, - "diff_rise": 2.306825448645043, - "down_abs": 26.557449531922828 - }, - "50": { - "t": 49.15, - "down_rise": 3.8785455116104117, - "up_rise": 1.6357115851495223, - "diff_rise": 2.2428339264608894, - "down_abs": 27.11815174394428 - }, - "60": { - "t": 59.18, - "down_rise": 3.718088155801354, - "up_rise": 2.184081847342611, - "diff_rise": 1.5340063084587428, - "down_abs": 26.957694388135224 - }, - "75": { - "t": 74.23, - "down_rise": 4.039213206578154, - "up_rise": 2.3410805533427492, - "diff_rise": 1.6981326532354046, - "down_abs": 27.278819438912024 - } - }, - "rep": 2, - "cooldown_s": 0, - "base_at_start": 22.070385781761104 - }, - { - "duty": 15, - "flow": true, - "start_down": 21.914988917022924, - "start_up": 21.75970016295645, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 3.84564820442597, - "up_rise": 0.6996700797217841, - "diff_rise": 3.145978124704186, - "down_abs": 25.760637121448895 - }, - "20": { - "t": 19.06, - "down_rise": 4.084194108324709, - "up_rise": 0.855468533217163, - "diff_rise": 3.2287255751075463, - "down_abs": 25.999183025347634 - }, - "25": { - "t": 24.07, - "down_rise": 4.482713467195083, - "up_rise": 1.0893947601274476, - "diff_rise": 3.3933187070676354, - "down_abs": 26.397702384218007 - }, - "30": { - "t": 29.09, - "down_rise": 4.4029134324053985, - "up_rise": 0.855468533217163, - "diff_rise": 3.5474448991882355, - "down_abs": 26.317902349428323 - }, - "40": { - "t": 39.12, - "down_rise": 4.642460614899903, - "up_rise": 1.0893947601274476, - "diff_rise": 3.5530658547724556, - "down_abs": 26.557449531922828 - }, - "50": { - "t": 49.15, - "down_rise": 4.722408541392156, - "up_rise": 0.6218154499641741, - "diff_rise": 4.100593091427982, - "down_abs": 26.63739745841508 - }, - "60": { - "t": 59.18, - "down_rise": 4.722408541392156, - "up_rise": 0.855468533217163, - "diff_rise": 3.8669400081749927, - "down_abs": 26.63739745841508 - }, - "75": { - "t": 74.22, - "down_rise": 4.722408541392156, - "up_rise": 0.31068561880465495, - "diff_rise": 4.411722922587501, - "down_abs": 26.63739745841508 - } - }, - "rep": 2, - "cooldown_s": 11, - "base_at_start": 22.615168696173612 - }, - { - "duty": 15, - "flow": false, - "start_down": 23.866177257407344, - "start_up": 22.381515612920623, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 3.493055946391337, - "up_rise": 0.7799231894102832, - "diff_rise": 2.7131327569810537, - "down_abs": 27.35923320379868 - }, - "20": { - "t": 19.06, - "down_rise": 3.9766837900703536, - "up_rise": 0.3115976480880249, - "diff_rise": 3.6650861419823286, - "down_abs": 27.842861047477697 - }, - "25": { - "t": 24.07, - "down_rise": 4.705959576997486, - "up_rise": 0.7799231894102832, - "diff_rise": 3.926036387587203, - "down_abs": 28.57213683440483 - }, - "30": { - "t": 29.09, - "down_rise": 4.624693720887194, - "up_rise": 1.1710974245278862, - "diff_rise": 3.453596296359308, - "down_abs": 28.490870978294538 - }, - "40": { - "t": 39.12, - "down_rise": 5.276532222062713, - "up_rise": 1.406217009504644, - "diff_rise": 3.8703152125580687, - "down_abs": 29.142709479470057 - }, - "50": { - "t": 49.15, - "down_rise": 5.522004668180273, - "up_rise": 1.7202151824088787, - "diff_rise": 3.801789485771394, - "down_abs": 29.388181925587617 - }, - "60": { - "t": 59.18, - "down_rise": 5.031629898532856, - "up_rise": 1.8774377112587786, - "diff_rise": 3.1541921872740772, - "down_abs": 28.8978071559402 - }, - "75": { - "t": 74.22, - "down_rise": 5.685975685876201, - "up_rise": 1.8774377112587786, - "diff_rise": 3.8085379746174226, - "down_abs": 29.552152943283545 - } - }, - "rep": 2, - "cooldown_s": 0, - "base_at_start": 21.75970016295645 - }, - { - "duty": 20, - "flow": true, - "start_down": 22.849094923083896, - "start_up": 22.537254480322474, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 4.269056820860385, - "up_rise": 0.23383406574924948, - "diff_rise": 4.0352227551111355, - "down_abs": 27.11815174394428 - }, - "20": { - "t": 19.06, - "down_rise": 4.671127735601033, - "up_rise": 0.23383406574924948, - "diff_rise": 4.437293669851783, - "down_abs": 27.52022265868493 - }, - "25": { - "t": 24.07, - "down_rise": 4.832335891144055, - "up_rise": 0.7805525148917809, - "diff_rise": 4.051783376252274, - "down_abs": 27.68143081422795 - }, - "30": { - "t": 29.09, - "down_rise": 4.349364168185218, - "up_rise": -0.6222655632995497, - "diff_rise": 4.971629731484768, - "down_abs": 27.198459091269115 - }, - "40": { - "t": 39.12, - "down_rise": 4.751704265169167, - "up_rise": -0.15573886740185117, - "diff_rise": 4.907443132571018, - "down_abs": 27.600799188253063 - }, - "50": { - "t": 49.15, - "down_rise": 4.59060588090415, - "up_rise": 0.0, - "diff_rise": 4.59060588090415, - "down_abs": 27.439700803988046 - }, - "60": { - "t": 59.18, - "down_rise": 4.59060588090415, - "up_rise": 0.5460498500809194, - "diff_rise": 4.04455603082323, - "down_abs": 27.439700803988046 - }, - "75": { - "t": 74.23, - "down_rise": 5.3173064284706015, - "up_rise": 0.5460498500809194, - "diff_rise": 4.771256578389682, - "down_abs": 28.166401351554498 - } - }, - "rep": 2, - "cooldown_s": 11, - "base_at_start": 22.849094923083896 - }, - { - "duty": 20, - "flow": false, - "start_down": 25.999183025347634, - "start_up": 21.914988917022924, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.7356667123987677, - "up_rise": 1.559321104551259, - "diff_rise": 1.1763456078475087, - "down_abs": 28.7348497377464 - }, - "20": { - "t": 19.06, - "down_rise": 3.3071106582252128, - "up_rise": 0.7001797791506874, - "diff_rise": 2.6069308790745254, - "down_abs": 29.306293683572846 - }, - "25": { - "t": 24.07, - "down_rise": 3.7172033363073638, - "up_rise": 1.7943350428939198, - "diff_rise": 1.922868293413444, - "down_abs": 29.716386361654997 - }, - "30": { - "t": 29.09, - "down_rise": 4.79137093160449, - "up_rise": 1.4810525493007844, - "diff_rise": 3.3103183823037057, - "down_abs": 30.790553956952124 - }, - "40": { - "t": 39.12, - "down_rise": 5.374784864211222, - "up_rise": 1.7159619739878167, - "diff_rise": 3.658822890223405, - "down_abs": 31.373967889558855 - }, - "50": { - "t": 49.15, - "down_rise": 5.793779283980712, - "up_rise": 2.5013399733906, - "diff_rise": 3.2924393105901117, - "down_abs": 31.792962309328345 - }, - "60": { - "t": 59.18, - "down_rise": 6.214735741545724, - "up_rise": 3.0534040586377387, - "diff_rise": 3.161331682907985, - "down_abs": 32.21391876689336 - }, - "75": { - "t": 74.22, - "down_rise": 6.4682776770543455, - "up_rise": 3.84564820442597, - "diff_rise": 2.6226294726283754, - "down_abs": 32.46746070240198 - } - }, - "rep": 2, - "cooldown_s": 0, - "base_at_start": 21.682095410064733 - }, - { - "duty": 30, - "flow": true, - "start_down": 21.75970016295645, - "start_up": 21.371932016053336, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 7.46476952166671, - "up_rise": 1.867674216280534, - "diff_rise": 5.597095305386176, - "down_abs": 29.22446968462316 - }, - "20": { - "t": 19.06, - "down_rise": 8.533622465754092, - "up_rise": 1.7113723143500579, - "diff_rise": 6.822250151404035, - "down_abs": 30.29332262871054 - }, - "25": { - "t": 24.08, - "down_rise": 8.781917869879464, - "up_rise": 2.1023780055208476, - "diff_rise": 6.679539864358617, - "down_abs": 30.541618032835913 - }, - "30": { - "t": 29.09, - "down_rise": 9.113976992602886, - "up_rise": 2.1023780055208476, - "diff_rise": 7.0115989870820385, - "down_abs": 30.873677155559335 - }, - "40": { - "t": 39.12, - "down_rise": 8.533622465754092, - "up_rise": 1.3211812449553122, - "diff_rise": 7.21244122079878, - "down_abs": 30.29332262871054 - }, - "50": { - "t": 49.15, - "down_rise": 8.699082197548591, - "up_rise": 0.853961681952569, - "diff_rise": 7.845120515596022, - "down_abs": 30.45878236050504 - }, - "60": { - "t": 59.18, - "down_rise": 9.113976992602886, - "up_rise": 2.6512439473541463, - "diff_rise": 6.46273304524874, - "down_abs": 30.873677155559335 - }, - "75": { - "t": 74.23, - "down_rise": 9.697912644462349, - "up_rise": 2.494245241354008, - "diff_rise": 7.203667403108341, - "down_abs": 31.457612807418798 - } - }, - "rep": 2, - "cooldown_s": 21, - "base_at_start": 22.303690221175657 - }, - { - "duty": 30, - "flow": false, - "start_down": 27.762117959330112, - "start_up": 23.083304330403394, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 4.030844349998233, - "up_rise": 0.4693087070451156, - "diff_rise": 3.5615356429531175, - "down_abs": 31.792962309328345 - }, - "20": { - "t": 19.06, - "down_rise": 5.299854353349701, - "up_rise": 1.0970187911511289, - "diff_rise": 4.2028355621985725, - "down_abs": 33.06197231267981 - }, - "25": { - "t": 24.08, - "down_rise": 6.156532855807029, - "up_rise": 1.8850886452572695, - "diff_rise": 4.27144421054976, - "down_abs": 33.91865081513714 - }, - "30": { - "t": 29.09, - "down_rise": 7.284008325911145, - "up_rise": 2.280664506244932, - "diff_rise": 5.003343819666213, - "down_abs": 35.04612628524126 - }, - "40": { - "t": 39.12, - "down_rise": 8.428243833879549, - "up_rise": 2.995487210371607, - "diff_rise": 5.432756623507942, - "down_abs": 36.19036179320966 - }, - "50": { - "t": 49.15, - "down_rise": 8.694818078480296, - "up_rise": 3.314398053814614, - "diff_rise": 5.380420024665682, - "down_abs": 36.45693603781041 - }, - "60": { - "t": 59.18, - "down_rise": 9.14129628175084, - "up_rise": 4.034847413540888, - "diff_rise": 5.106448868209952, - "down_abs": 36.90341424108095 - }, - "75": { - "t": 74.23, - "down_rise": 8.873077981186555, - "up_rise": 4.19551510850863, - "diff_rise": 4.677562872677925, - "down_abs": 36.63519594051667 - } - }, - "rep": 2, - "cooldown_s": 0, - "base_at_start": 22.148125674399637 - }, - { - "duty": 40, - "flow": true, - "start_down": 23.317806995214255, - "start_up": 22.459370242678233, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 8.896111771679102, - "up_rise": 0.936671223645476, - "diff_rise": 7.959440548033626, - "down_abs": 32.21391876689336 - }, - "20": { - "t": 19.06, - "down_rise": 7.972591971184443, - "up_rise": 0.233743018330415, - "diff_rise": 7.738848952854028, - "down_abs": 31.290398966398698 - }, - "25": { - "t": 24.08, - "down_rise": 9.31909357413707, - "up_rise": 0.936671223645476, - "diff_rise": 8.382422350491595, - "down_abs": 32.636900569351326 - }, - "30": { - "t": 29.1, - "down_rise": 10.000238910316398, - "up_rise": 1.1715806483325082, - "diff_rise": 8.82865826198389, - "down_abs": 33.31804590553065 - }, - "40": { - "t": 39.13, - "down_rise": 10.171392946930666, - "up_rise": 1.406807014729111, - "diff_rise": 8.764585932201555, - "down_abs": 33.48919994214492 - }, - "50": { - "t": 49.16, - "down_rise": 10.085771737846244, - "up_rise": 0.8584367525360221, - "diff_rise": 9.227334985310222, - "down_abs": 33.4035787330605 - }, - "60": { - "t": 59.19, - "down_rise": 10.859610072454643, - "up_rise": 1.4852880016800327, - "diff_rise": 9.37432207077461, - "down_abs": 34.1774170676689 - }, - "75": { - "t": 74.24, - "down_rise": 10.085771737846244, - "up_rise": 1.7209528788762896, - "diff_rise": 8.364818858969954, - "down_abs": 33.4035787330605 - } - }, - "rep": 2, - "cooldown_s": 21, - "base_at_start": 21.914988917022924 - }, - { - "duty": 40, - "flow": false, - "start_down": 23.709323959916844, - "start_up": 22.225893698005905, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 10.037275345242278, - "up_rise": 1.875837097323597, - "diff_rise": 8.161438247918682, - "down_abs": 33.74659930515912 - }, - "20": { - "t": 19.06, - "down_rise": 11.862749246180442, - "up_rise": 2.1117280879724376, - "diff_rise": 9.751021158208005, - "down_abs": 35.572073206097286 - }, - "25": { - "t": 24.08, - "down_rise": 13.104572858049721, - "up_rise": 2.4267922067048744, - "diff_rise": 10.677780651344847, - "down_abs": 36.813896817966565 - }, - "30": { - "t": 29.1, - "down_rise": 14.550688816945637, - "up_rise": 2.9797164573636223, - "diff_rise": 11.570972359582015, - "down_abs": 38.26001277686248 - }, - "40": { - "t": 39.13, - "down_rise": 16.308923022666818, - "up_rise": 4.892258045938377, - "diff_rise": 11.416664976728441, - "down_abs": 40.01824698258366 - }, - "50": { - "t": 49.16, - "down_rise": 16.590912273163156, - "up_rise": 4.97256539326321, - "diff_rise": 11.618346879899946, - "down_abs": 40.30023623308 - }, - "60": { - "t": 59.19, - "down_rise": 17.731600216494428, - "up_rise": 5.859536529615809, - "diff_rise": 11.872063686878619, - "down_abs": 41.44092417641127 - }, - "75": { - "t": 74.24, - "down_rise": 17.34905934983209, - "up_rise": 6.264977280288633, - "diff_rise": 11.084082069543456, - "down_abs": 41.05838330974893 - } - }, - "rep": 2, - "cooldown_s": 11, - "base_at_start": 22.849094923083896 - }, - { - "duty": 50, - "flow": true, - "start_down": 22.771088546071724, - "start_up": 21.99267365190036, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 10.889621135107348, - "up_rise": 2.1090571434291405, - "diff_rise": 8.780563991678207, - "down_abs": 33.66070968117907 - }, - "20": { - "t": 19.06, - "down_rise": 12.977115697647378, - "up_rise": 1.7950589705249058, - "diff_rise": 11.182056727122472, - "down_abs": 35.7482042437191 - }, - "25": { - "t": 24.07, - "down_rise": 12.275037739169534, - "up_rise": 1.3251333433138939, - "diff_rise": 10.94990439585564, - "down_abs": 35.04612628524126 - }, - "30": { - "t": 29.09, - "down_rise": 12.713073539011987, - "up_rise": 1.0906306785030324, - "diff_rise": 11.622442860508954, - "down_abs": 35.48416208508371 - }, - "40": { - "t": 39.12, - "down_rise": 14.042808271894842, - "up_rise": 2.030502311507121, - "diff_rise": 12.012305960387721, - "down_abs": 36.813896817966565 - }, - "50": { - "t": 49.15, - "down_rise": 14.4015537520566, - "up_rise": 2.1090571434291405, - "diff_rise": 12.29249660862746, - "down_abs": 37.17264229812832 - }, - "60": { - "t": 59.18, - "down_rise": 13.596881299294353, - "up_rise": 1.716650308016483, - "diff_rise": 11.88023099127787, - "down_abs": 36.367969845366076 - }, - "75": { - "t": 74.22, - "down_rise": 14.491523900569177, - "up_rise": 2.975719323760302, - "diff_rise": 11.515804576808875, - "down_abs": 37.2626124466409 - } - }, - "rep": 2, - "cooldown_s": 21, - "base_at_start": 22.615168696173612 - }, - { - "duty": 50, - "flow": false, - "start_down": 24.023175963407482, - "start_up": 22.771088546071724, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 12.790720854559083, - "up_rise": 0.5467184491425314, - "diff_rise": 12.244002405416552, - "down_abs": 36.813896817966565 - }, - "20": { - "t": 19.06, - "down_rise": 14.511386690160293, - "up_rise": 2.3554068288601684, - "diff_rise": 12.155979861300125, - "down_abs": 38.534562653567775 - }, - "25": { - "t": 24.08, - "down_rise": 17.03520734634145, - "up_rise": 2.830747014738961, - "diff_rise": 14.20446033160249, - "down_abs": 41.05838330974893 - }, - "30": { - "t": 29.09, - "down_rise": 18.190037168318042, - "up_rise": 4.106455005279372, - "diff_rise": 14.08358216303867, - "down_abs": 42.213213131725524 - }, - "40": { - "t": 39.12, - "down_rise": 19.56586402253498, - "up_rise": 4.749134112613206, - "diff_rise": 14.816729909921776, - "down_abs": 43.58903998594246 - }, - "50": { - "t": 49.15, - "down_rise": 20.36678221195905, - "up_rise": 6.289924074234705, - "diff_rise": 14.076858137724344, - "down_abs": 44.38995817536653 - }, - "60": { - "t": 59.18, - "down_rise": 20.873089851136058, - "up_rise": 6.781064397211821, - "diff_rise": 14.092025453924236, - "down_abs": 44.89626581454354 - }, - "75": { - "t": 74.23, - "down_rise": 21.796049900538947, - "up_rise": 6.781064397211821, - "diff_rise": 15.014985503327125, - "down_abs": 45.81922586394643 - } - }, - "rep": 2, - "cooldown_s": 11, - "base_at_start": 22.615168696173612 - }, - { - "duty": 10, - "flow": true, - "start_down": 22.537254480322474, - "start_up": 22.537254480322474, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.510168631689055, - "up_rise": -0.46686869856137037, - "diff_rise": 2.9770373302504254, - "down_abs": 25.04742311201153 - }, - "20": { - "t": 19.06, - "down_rise": 2.431138495338189, - "up_rise": 0.3118404427614223, - "diff_rise": 2.1192980525767666, - "down_abs": 24.968392975660663 - }, - "25": { - "t": 24.07, - "down_rise": 2.747513364759641, - "up_rise": 0.3118404427614223, - "diff_rise": 2.4356729219982185, - "down_abs": 25.284767845082115 - }, - "30": { - "t": 29.09, - "down_rise": 2.747513364759641, - "up_rise": 0.0, - "diff_rise": 2.747513364759641, - "down_abs": 25.284767845082115 - }, - "40": { - "t": 39.12, - "down_rise": 2.273203042535556, - "up_rise": -0.15573886740185117, - "diff_rise": 2.428941909937407, - "down_abs": 24.81045752285803 - }, - "50": { - "t": 49.15, - "down_rise": 2.273203042535556, - "up_rise": -0.07788423764424124, - "diff_rise": 2.3510872801797973, - "down_abs": 24.81045752285803 - }, - "60": { - "t": 59.18, - "down_rise": 2.8267143563258514, - "up_rise": -0.23356425914681722, - "diff_rise": 3.0602786154726687, - "down_abs": 25.363968836648326 - }, - "75": { - "t": 74.22, - "down_rise": 1.8790744100910501, - "up_rise": -1.242801235760453, - "diff_rise": 3.121875645851503, - "down_abs": 24.416328890413524 - } - }, - "rep": 3, - "cooldown_s": 32, - "base_at_start": 22.537254480322474 - }, - { - "duty": 10, - "flow": false, - "start_down": 23.083304330403394, - "start_up": 22.303690221175657, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 2.3599091930759073, - "up_rise": 0.6234425430783972, - "diff_rise": 1.73646664999751, - "down_abs": 25.4432135234793 - }, - "20": { - "t": 19.06, - "down_rise": 2.439197969426573, - "up_rise": 0.07782539174496605, - "diff_rise": 2.361372577681607, - "down_abs": 25.522502299829966 - }, - "25": { - "t": 24.08, - "down_rise": 3.2345980190249293, - "up_rise": 0.15568002150257598, - "diff_rise": 3.0789179975223533, - "down_abs": 26.317902349428323 - }, - "30": { - "t": 29.09, - "down_rise": 2.91587869494424, - "up_rise": 0.6234425430783972, - "diff_rise": 2.292436151865843, - "down_abs": 25.999183025347634 - }, - "40": { - "t": 39.12, - "down_rise": 3.075143001120523, - "up_rise": 1.0141167740385981, - "diff_rise": 2.061026227081925, - "down_abs": 26.158447331523917 - }, - "50": { - "t": 49.15, - "down_rise": 3.3942469792179963, - "up_rise": 0.389423039832991, - "diff_rise": 3.0048239393850054, - "down_abs": 26.47755130962139 - }, - "60": { - "t": 59.18, - "down_rise": 3.714139727856491, - "up_rise": 0.8577485811552492, - "diff_rise": 2.8563911467012417, - "down_abs": 26.797444058259885 - }, - "75": { - "t": 74.23, - "down_rise": 4.034847413540888, - "up_rise": 1.1706198003985264, - "diff_rise": 2.8642276131423614, - "down_abs": 27.11815174394428 - } - }, - "rep": 3, - "cooldown_s": 0, - "base_at_start": 20.752756284325415 - }, - { - "duty": 15, - "flow": true, - "start_down": 22.303690221175657, - "start_up": 22.381515612920623, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.5067673016823733, - "up_rise": -0.2333899385209861, - "diff_rise": 2.7401572402033594, - "down_abs": 24.81045752285803 - }, - "20": { - "t": 19.06, - "down_rise": 4.014212128252666, - "up_rise": 0.4675793101632735, - "diff_rise": 3.5466328180893925, - "down_abs": 26.317902349428323 - }, - "25": { - "t": 24.07, - "down_rise": 2.822805153756235, - "up_rise": 0.07785462975760993, - "diff_rise": 2.744950523998625, - "down_abs": 25.126495374931892 - }, - "30": { - "t": 29.09, - "down_rise": 4.173861088445733, - "up_rise": 0.38957293315110064, - "diff_rise": 3.7842881552946324, - "down_abs": 26.47755130962139 - }, - "40": { - "t": 39.12, - "down_rise": 3.615931162644358, - "up_rise": -0.3111298311595192, - "diff_rise": 3.9270609938038774, - "down_abs": 25.919621383820015 - }, - "50": { - "t": 49.15, - "down_rise": 3.377523481222852, - "up_rise": -0.3111298311595192, - "diff_rise": 3.688653312382371, - "down_abs": 25.68121370239851 - }, - "60": { - "t": 59.18, - "down_rise": 3.377523481222852, - "up_rise": -0.9320803597239546, - "diff_rise": 4.3096038409468065, - "down_abs": 25.68121370239851 - }, - "75": { - "t": 74.23, - "down_rise": 3.695492804171977, - "up_rise": 0.5456171513334311, - "diff_rise": 3.1498756528385456, - "down_abs": 25.999183025347634 - } - }, - "rep": 3, - "cooldown_s": 11, - "base_at_start": 22.148125674399637 - }, - { - "duty": 15, - "flow": false, - "start_down": 24.023175963407482, - "start_up": 21.371932016053336, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 3.0949757805367994, - "up_rise": 0.7761936583463012, - "diff_rise": 2.3187821221904983, - "down_abs": 27.11815174394428 - }, - "20": { - "t": 19.06, - "down_rise": 3.8196850840702155, - "up_rise": 2.024109450270373, - "diff_rise": 1.7955756337998423, - "down_abs": 27.842861047477697 - }, - "25": { - "t": 24.08, - "down_rise": 3.8196850840702155, - "up_rise": 2.3373919438635085, - "diff_rise": 1.482293140206707, - "down_abs": 27.842861047477697 - }, - "30": { - "t": 29.09, - "down_rise": 4.711673774338919, - "up_rise": 1.9458749791609193, - "diff_rise": 2.765798795178, - "down_abs": 28.7348497377464 - }, - "40": { - "t": 39.12, - "down_rise": 4.874631192532718, - "up_rise": 2.4158006063719313, - "diff_rise": 2.4588305861607864, - "down_abs": 28.8978071559402 - }, - "50": { - "t": 49.15, - "down_rise": 5.201293721215677, - "up_rise": 2.57272622830493, - "diff_rise": 2.628567492910747, - "down_abs": 29.22446968462316 - }, - "60": { - "t": 59.18, - "down_rise": 5.857709865577874, - "up_rise": 2.8083911055011868, - "diff_rise": 3.0493187600766873, - "down_abs": 29.880885828985356 - }, - "75": { - "t": 74.23, - "down_rise": 6.352841419497167, - "up_rise": 3.2019285484947204, - "diff_rise": 3.150912871002447, - "down_abs": 30.37601738290465 - } - }, - "rep": 3, - "cooldown_s": 0, - "base_at_start": 22.615168696173612 - }, - { - "duty": 20, - "flow": true, - "start_down": 22.927132764254054, - "start_up": 22.693113261008648, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 2.8335043571948404, - "up_rise": -0.7781243439857235, - "diff_rise": 3.611628701180564, - "down_abs": 25.760637121448895 - }, - "20": { - "t": 19.06, - "down_rise": 4.512568039733992, - "up_rise": 0.0, - "diff_rise": 4.512568039733992, - "down_abs": 27.439700803988046 - }, - "25": { - "t": 24.08, - "down_rise": 4.351686674657969, - "up_rise": 0.2340195032454062, - "diff_rise": 4.117667171412563, - "down_abs": 27.278819438912024 - }, - "30": { - "t": 29.09, - "down_rise": 5.482532172342985, - "up_rise": 0.07797528506307572, - "diff_rise": 5.4045568872799095, - "down_abs": 28.40966493659704 - }, - "40": { - "t": 39.13, - "down_rise": 5.401385509757517, - "up_rise": 0.07797528506307572, - "diff_rise": 5.323410224694442, - "down_abs": 28.32851827401157 - }, - "50": { - "t": 49.16, - "down_rise": 4.996527739546082, - "up_rise": -0.15585878068617376, - "diff_rise": 5.152386520232255, - "down_abs": 27.923660503800136 - }, - "60": { - "t": 59.19, - "down_rise": 4.593089894430875, - "up_rise": 0.0, - "diff_rise": 4.593089894430875, - "down_abs": 27.52022265868493 - }, - "75": { - "t": 74.23, - "down_rise": 4.834985195076058, - "up_rise": 1.094619361416619, - "diff_rise": 3.740365833659439, - "down_abs": 27.762117959330112 - } - }, - "rep": 3, - "cooldown_s": 11, - "base_at_start": 21.371932016053336 - }, - { - "duty": 20, - "flow": false, - "start_down": 25.522502299829966, - "start_up": 22.381515612920623, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 3.212347437916435, - "up_rise": 1.1710974245278862, - "diff_rise": 2.0412500133885487, - "down_abs": 28.7348497377464 - }, - "20": { - "t": 19.06, - "down_rise": 4.111734325113275, - "up_rise": 0.7799231894102832, - "diff_rise": 3.331811135702992, - "down_abs": 29.63423662494324 - }, - "25": { - "t": 24.08, - "down_rise": 4.605639613783524, - "up_rise": 1.3278083469962212, - "diff_rise": 3.2778312667873024, - "down_abs": 30.12814191361349 - }, - "30": { - "t": 29.09, - "down_rise": 4.770820328880575, - "up_rise": 1.5631426314376426, - "diff_rise": 3.207677697442932, - "down_abs": 30.29332262871054 - }, - "40": { - "t": 39.12, - "down_rise": 4.853515083074683, - "up_rise": 1.5631426314376426, - "diff_rise": 3.2903724516370403, - "down_abs": 30.37601738290465 - }, - "50": { - "t": 49.15, - "down_rise": 5.684403247241903, - "up_rise": 2.271170291790156, - "diff_rise": 3.413232955451747, - "down_abs": 31.20690554707187 - }, - "60": { - "t": 59.18, - "down_rise": 5.10202257308498, - "up_rise": 2.428941909937407, - "diff_rise": 2.673080663147573, - "down_abs": 30.624524872914947 - }, - "75": { - "t": 74.23, - "down_rise": 6.860362558794179, - "up_rise": 3.1409866869093435, - "diff_rise": 3.7193758718848358, - "down_abs": 32.382864858624146 - } - }, - "rep": 3, - "cooldown_s": 0, - "base_at_start": 22.225893698005905 - }, - { - "duty": 30, - "flow": true, - "start_down": 23.474310021574183, - "start_up": 22.459370242678233, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 5.750159663048976, - "up_rise": 0.5458321996107429, - "diff_rise": 5.204327463438233, - "down_abs": 29.22446968462316 - }, - "20": { - "t": 19.06, - "down_rise": 5.097826812830647, - "up_rise": -0.3889844609171291, - "diff_rise": 5.486811273747776, - "down_abs": 28.57213683440483 - }, - "25": { - "t": 24.07, - "down_rise": 7.06730801126173, - "up_rise": 1.2499537172386113, - "diff_rise": 5.817354294023119, - "down_abs": 30.541618032835913 - }, - "30": { - "t": 29.09, - "down_rise": 6.406575807411173, - "up_rise": -0.6220390329586962, - "diff_rise": 7.028614840369869, - "down_abs": 29.880885828985356 - }, - "40": { - "t": 39.12, - "down_rise": 6.159926603369058, - "up_rise": 0.936671223645476, - "diff_rise": 5.223255379723582, - "down_abs": 29.63423662494324 - }, - "50": { - "t": 49.15, - "down_rise": 7.233193333895088, - "up_rise": 1.1715806483325082, - "diff_rise": 6.061612685562579, - "down_abs": 30.70750335546927 - }, - "60": { - "t": 59.18, - "down_rise": 7.732595525497686, - "up_rise": 0.936671223645476, - "diff_rise": 6.79592430185221, - "down_abs": 31.20690554707187 - }, - "75": { - "t": 74.23, - "down_rise": 6.819012607136358, - "up_rise": 0.3117183033934907, - "diff_rise": 6.507294303742867, - "down_abs": 30.29332262871054 - } - }, - "rep": 3, - "cooldown_s": 11, - "base_at_start": 22.459370242678233 - }, - { - "duty": 30, - "flow": false, - "start_down": 27.68143081422795, - "start_up": 21.914988917022924, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 4.4481366775731885, - "up_rise": 2.422632868955418, - "diff_rise": 2.0255038086177706, - "down_abs": 32.12956749180114 - }, - "20": { - "t": 19.06, - "down_rise": 5.5511701042102, - "up_rise": 2.5013399733906, - "diff_rise": 3.0498301308196005, - "down_abs": 33.23260091843815 - }, - "25": { - "t": 24.08, - "down_rise": 6.409638226132586, - "up_rise": 2.974415658707244, - "diff_rise": 3.435222567425342, - "down_abs": 34.09106904036054 - }, - "30": { - "t": 29.09, - "down_rise": 6.842317626623583, - "up_rise": 3.3697789280591905, - "diff_rise": 3.4725386985643922, - "down_abs": 34.523748440851534 - }, - "40": { - "t": 39.12, - "down_rise": 8.154994567882397, - "up_rise": 4.004632466797091, - "diff_rise": 4.150362101085307, - "down_abs": 35.83642538211035 - }, - "50": { - "t": 49.16, - "down_rise": 8.775505223582456, - "up_rise": 4.4029134324053985, - "diff_rise": 4.372591791177058, - "down_abs": 36.45693603781041 - }, - "60": { - "t": 59.19, - "down_rise": 9.401355508556094, - "up_rise": 4.80240658038889, - "diff_rise": 4.598948928167204, - "down_abs": 37.082786322784045 - }, - "75": { - "t": 74.23, - "down_rise": 8.953765126288715, - "up_rise": 4.482713467195083, - "diff_rise": 4.471051659093632, - "down_abs": 36.63519594051667 - } - }, - "rep": 3, - "cooldown_s": 0, - "base_at_start": 22.615168696173612 - }, - { - "duty": 40, - "flow": true, - "start_down": 22.303690221175657, - "start_up": 21.21699857403314, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 8.903215325896213, - "up_rise": 1.1645170388874817, - "diff_rise": 7.738698287008731, - "down_abs": 31.20690554707187 - }, - "20": { - "t": 19.06, - "down_rise": 9.657415709629333, - "up_rise": 0.5427015889233076, - "diff_rise": 9.114714120706026, - "down_abs": 31.96110593080499 - }, - "25": { - "t": 24.07, - "down_rise": 11.442909083983466, - "up_rise": 2.963324547521381, - "diff_rise": 8.479584536462085, - "down_abs": 33.74659930515912 - }, - "30": { - "t": 29.09, - "down_rise": 10.502984590555684, - "up_rise": 1.6320963490507552, - "diff_rise": 8.870888241504929, - "down_abs": 32.80667481173134 - }, - "40": { - "t": 39.12, - "down_rise": 11.185509720969264, - "up_rise": 2.1008084211811138, - "diff_rise": 9.08470129978815, - "down_abs": 33.48919994214492 - }, - "50": { - "t": 49.14, - "down_rise": 10.418055411143712, - "up_rise": 1.6320963490507552, - "diff_rise": 8.785959062092957, - "down_abs": 32.72174563231937 - }, - "60": { - "t": 59.18, - "down_rise": 11.271219855067955, - "up_rise": 2.335614463415368, - "diff_rise": 8.935605391652587, - "down_abs": 33.57491007624361 - }, - "75": { - "t": 74.22, - "down_rise": 11.960168233396018, - "up_rise": 2.5707340483921257, - "diff_rise": 9.389434185003893, - "down_abs": 34.263858454571675 - } - }, - "rep": 3, - "cooldown_s": 21, - "base_at_start": 22.537254480322474 - }, - { - "duty": 40, - "flow": false, - "start_down": 23.787732622425267, - "start_up": 21.837331209719537, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 9.274239690254547, - "up_rise": 1.3241075926113695, - "diff_rise": 7.950132097643177, - "down_abs": 33.06197231267981 - }, - "20": { - "t": 19.06, - "down_rise": 12.225449352429564, - "up_rise": 2.5002905762588057, - "diff_rise": 9.725158776170758, - "down_abs": 36.01318197485483 - }, - "25": { - "t": 24.08, - "down_rise": 12.402629170784394, - "up_rise": 2.264399585609965, - "diff_rise": 10.138229585174429, - "down_abs": 36.19036179320966 - }, - "30": { - "t": 29.09, - "down_rise": 14.563673418251312, - "up_rise": 4.0822901741004785, - "diff_rise": 10.481383244150834, - "down_abs": 38.35140604067658 - }, - "40": { - "t": 39.12, - "down_rise": 15.856437692415511, - "up_rise": 4.560371174498471, - "diff_rise": 11.29606651791704, - "down_abs": 39.64417031484078 - }, - "50": { - "t": 49.15, - "down_rise": 17.175381522206976, - "up_rise": 5.200565771412641, - "diff_rise": 11.974815750794335, - "down_abs": 40.96311414463224 - }, - "60": { - "t": 59.18, - "down_rise": 16.890439109165527, - "up_rise": 5.6828914489653926, - "diff_rise": 11.207547660200134, - "down_abs": 40.678171731590794 - }, - "75": { - "t": 74.23, - "down_rise": 17.557335008456278, - "up_rise": 6.978966452580721, - "diff_rise": 10.578368555875556, - "down_abs": 41.345067630881545 - } - }, - "rep": 3, - "cooldown_s": 11, - "base_at_start": 22.693113261008648 - }, - { - "duty": 50, - "flow": true, - "start_down": 22.615168696173612, - "start_up": 21.682095410064733, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 10.532074536813013, - "up_rise": 0.6215948111109242, - "diff_rise": 9.91047972570209, - "down_abs": 33.147243232986625 - }, - "20": { - "t": 19.06, - "down_rise": 12.430957589067646, - "up_rise": 1.9488554809460084, - "diff_rise": 10.482102108121637, - "down_abs": 35.04612628524126 - }, - "25": { - "t": 24.07, - "down_rise": 13.221256685936737, - "up_rise": 2.0272285498521114, - "diff_rise": 11.194028136084626, - "down_abs": 35.83642538211035 - }, - "30": { - "t": 29.09, - "down_rise": 13.752801149192464, - "up_rise": 2.1056372123605342, - "diff_rise": 11.64716393683193, - "down_abs": 36.367969845366076 - }, - "40": { - "t": 39.12, - "down_rise": 13.575193097036049, - "up_rise": 1.9488554809460084, - "diff_rise": 11.62633761609004, - "down_abs": 36.19036179320966 - }, - "50": { - "t": 49.15, - "down_rise": 14.377875160951135, - "up_rise": 2.3410805533427492, - "diff_rise": 12.036794607608385, - "down_abs": 36.993043857124746 - }, - "60": { - "t": 59.18, - "down_rise": 14.28824554490734, - "up_rise": 3.0494560183936983, - "diff_rise": 11.238789526513642, - "down_abs": 36.90341424108095 - }, - "75": { - "t": 74.22, - "down_rise": 14.109322238274785, - "up_rise": 2.8917651544833234, - "diff_rise": 11.217557083791462, - "down_abs": 36.7244909344484 - } - }, - "rep": 3, - "cooldown_s": 32, - "base_at_start": 22.381515612920623 - }, - { - "duty": 50, - "flow": false, - "start_down": 22.070385781761104, - "start_up": 21.604516584730902, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 13.677818461957997, - "up_rise": 2.104807375185942, - "diff_rise": 11.573011086772055, - "down_abs": 35.7482042437191 - }, - "20": { - "t": 19.06, - "down_rise": 16.281020258915476, - "up_rise": 1.7132904104833528, - "diff_rise": 14.567729848432123, - "down_abs": 38.35140604067658 - }, - "25": { - "t": 24.07, - "down_rise": 17.85413907062849, - "up_rise": 3.363876390929761, - "diff_rise": 14.49026267969873, - "down_abs": 39.924524852389595 - }, - "30": { - "t": 29.09, - "down_rise": 18.60778594982969, - "up_rise": 3.601093570638625, - "diff_rise": 15.006692379191065, - "down_abs": 40.678171731590794 - }, - "40": { - "t": 39.12, - "down_rise": 21.320137794862603, - "up_rise": 5.674302854181121, - "diff_rise": 15.645834940681482, - "down_abs": 43.39052357662371 - }, - "50": { - "t": 49.15, - "down_rise": 22.319572393605426, - "up_rise": 6.319143919069234, - "diff_rise": 16.000428474536193, - "down_abs": 44.38995817536653 - }, - "60": { - "t": 59.18, - "down_rise": 22.825880032782436, - "up_rise": 7.2932905712092975, - "diff_rise": 15.532589461573139, - "down_abs": 44.89626581454354 - }, - "75": { - "t": 74.22, - "down_rise": 24.372771856894474, - "up_rise": 8.771500798173747, - "diff_rise": 15.601271058720727, - "down_abs": 46.44315763865558 - } - }, - "rep": 3, - "cooldown_s": 21, - "base_at_start": 22.225893698005905 - }, - { - "duty": 10, - "flow": true, - "start_down": 22.537254480322474, - "start_up": 22.148125674399637, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.431138495338189, - "up_rise": 0.3891288059228373, - "diff_rise": 2.0420096894153517, - "down_abs": 24.968392975660663 - }, - "20": { - "t": 19.06, - "down_rise": 2.510168631689055, - "up_rise": 0.7009692486842596, - "diff_rise": 1.8091993830047954, - "down_abs": 25.04742311201153 - }, - "25": { - "t": 24.07, - "down_rise": 2.431138495338189, - "up_rise": 0.2333899385209861, - "diff_rise": 2.197748556817203, - "down_abs": 24.968392975660663 - }, - "30": { - "t": 29.09, - "down_rise": 2.668355675047053, - "up_rise": 0.6229628716720867, - "diff_rise": 2.045392803374966, - "down_abs": 25.205610155369527 - }, - "40": { - "t": 39.12, - "down_rise": 1.8790744100910501, - "up_rise": -0.07773989263853309, - "diff_rise": 1.9568143027295832, - "down_abs": 24.416328890413524 - }, - "50": { - "t": 49.15, - "down_rise": 2.668355675047053, - "up_rise": 0.857076767889339, - "diff_rise": 1.811278907157714, - "down_abs": 25.205610155369527 - }, - "60": { - "t": 59.18, - "down_rise": 1.7216988438569274, - "up_rise": -0.07773989263853309, - "diff_rise": 1.7994387364954605, - "down_abs": 24.2589533241794 - }, - "75": { - "t": 74.22, - "down_rise": 1.8003673056558682, - "up_rise": -0.07773989263853309, - "diff_rise": 1.8781071982944013, - "down_abs": 24.337621785978342 - } - }, - "rep": 4, - "cooldown_s": 33, - "base_at_start": 22.381515612920623 - }, - { - "duty": 10, - "flow": false, - "start_down": 22.381515612920623, - "start_up": 21.914988917022924, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.903252232161492, - "up_rise": 0.8560996290487992, - "diff_rise": 2.0471526031126928, - "down_abs": 25.284767845082115 - }, - "20": { - "t": 19.06, - "down_rise": 3.299698089477886, - "up_rise": 0.4665266958976986, - "diff_rise": 2.833171393580187, - "down_abs": 25.68121370239851 - }, - "25": { - "t": 24.07, - "down_rise": 3.7769317186032936, - "up_rise": 0.6222655632995497, - "diff_rise": 3.154666155303744, - "down_abs": 26.158447331523917 - }, - "30": { - "t": 29.09, - "down_rise": 4.096035696700767, - "up_rise": 0.6222655632995497, - "diff_rise": 3.473770133401217, - "down_abs": 26.47755130962139 - }, - "40": { - "t": 39.12, - "down_rise": 4.016186771297384, - "up_rise": 0.0, - "diff_rise": 4.016186771297384, - "down_abs": 26.397702384218007 - }, - "50": { - "t": 49.15, - "down_rise": 4.576178775214601, - "up_rise": 1.0121438472311297, - "diff_rise": 3.564034927983471, - "down_abs": 26.957694388135224 - }, - "60": { - "t": 59.18, - "down_rise": 4.255881845494457, - "up_rise": 0.934106006060972, - "diff_rise": 3.321775839433485, - "down_abs": 26.63739745841508 - }, - "75": { - "t": 74.22, - "down_rise": 4.977717590878058, - "up_rise": 1.8727437054023426, - "diff_rise": 3.104973885475715, - "down_abs": 27.35923320379868 - } - }, - "rep": 4, - "cooldown_s": 0, - "base_at_start": 22.070385781761104 - }, - { - "duty": 15, - "flow": true, - "start_down": 22.381515612920623, - "start_up": 22.148125674399637, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 2.9824532237277026, - "up_rise": -0.3107944646801002, - "diff_rise": 3.293247688407803, - "down_abs": 25.363968836648326 - }, - "20": { - "t": 19.06, - "down_rise": 3.538105770899392, - "up_rise": 0.544987586609011, - "diff_rise": 2.993118184290381, - "down_abs": 25.919621383820015 - }, - "25": { - "t": 24.07, - "down_rise": 3.3791215085282715, - "up_rise": -0.1554520224992757, - "diff_rise": 3.5345735310275472, - "down_abs": 25.760637121448895 - }, - "30": { - "t": 29.09, - "down_rise": 3.9363867365077, - "up_rise": 0.7009692486842596, - "diff_rise": 3.2354174878234403, - "down_abs": 26.317902349428323 - }, - "40": { - "t": 39.12, - "down_rise": 3.7769317186032936, - "up_rise": 0.2333899385209861, - "diff_rise": 3.5435417800823075, - "down_abs": 26.158447331523917 - }, - "50": { - "t": 49.15, - "down_rise": 4.496027938430473, - "up_rise": 0.857076767889339, - "diff_rise": 3.6389511705411337, - "down_abs": 26.877543551351096 - }, - "60": { - "t": 59.18, - "down_rise": 4.016186771297384, - "up_rise": 0.9351786560037567, - "diff_rise": 3.0810081152936277, - "down_abs": 26.397702384218007 - }, - "75": { - "t": 74.22, - "down_rise": 3.9363867365077, - "up_rise": 0.3891288059228373, - "diff_rise": 3.5472579305848626, - "down_abs": 26.317902349428323 - } - }, - "rep": 4, - "cooldown_s": 11, - "base_at_start": 21.44943525319667 - }, - { - "duty": 15, - "flow": false, - "start_down": 24.4950750216463, - "start_up": 22.070385781761104, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 2.703384069622814, - "up_rise": 0.6227274792475441, - "diff_rise": 2.08065659037527, - "down_abs": 27.198459091269115 - }, - "20": { - "t": 19.06, - "down_rise": 3.1057241666067625, - "up_rise": 0.544782914412508, - "diff_rise": 2.5609412521942545, - "down_abs": 27.600799188253063 - }, - "25": { - "t": 24.08, - "down_rise": 3.509441733004678, - "up_rise": 1.169220450572766, - "diff_rise": 2.340221282431912, - "down_abs": 28.00451675465098 - }, - "30": { - "t": 29.09, - "down_rise": 3.186355792581651, - "up_rise": 0.8567469824929503, - "diff_rise": 2.3296088100887005, - "down_abs": 27.68143081422795 - }, - "40": { - "t": 39.12, - "down_rise": 3.6713263299081973, - "up_rise": 1.5605651092496373, - "diff_rise": 2.11076122065856, - "down_abs": 28.166401351554498 - }, - "50": { - "t": 49.15, - "down_rise": 4.729394662976858, - "up_rise": 1.9527901816463782, - "diff_rise": 2.77660448133048, - "down_abs": 29.22446968462316 - }, - "60": { - "t": 59.18, - "down_rise": 5.057077921637244, - "up_rise": 2.031345013568398, - "diff_rise": 3.0257329080688464, - "down_abs": 29.552152943283545 - }, - "75": { - "t": 74.23, - "down_rise": 5.7156226066292675, - "up_rise": 2.9770373302504254, - "diff_rise": 2.738585276378842, - "down_abs": 30.210697628275568 - } - }, - "rep": 4, - "cooldown_s": 0, - "base_at_start": 22.148125674399637 - }, - { - "duty": 20, - "flow": true, - "start_down": 22.381515612920623, - "start_up": 22.225893698005905, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 4.816943478348492, - "up_rise": 0.15562191491471822, - "diff_rise": 4.6613215634337735, - "down_abs": 27.198459091269115 - }, - "20": { - "t": 19.06, - "down_rise": 4.335879884491192, - "up_rise": -0.3885624882863681, - "diff_rise": 4.72444237277756, - "down_abs": 26.717395497411815 - }, - "25": { - "t": 24.08, - "down_rise": 5.623001141730356, - "up_rise": -0.07776802360626789, - "diff_rise": 5.700769165336624, - "down_abs": 28.00451675465098 - }, - "30": { - "t": 29.09, - "down_rise": 4.255881845494457, - "up_rise": 0.46721956300274314, - "diff_rise": 3.788662282491714, - "down_abs": 26.63739745841508 - }, - "40": { - "t": 39.12, - "down_rise": 5.461345434557074, - "up_rise": 0.7793087442830711, - "diff_rise": 4.682036690274003, - "down_abs": 27.842861047477697 - }, - "50": { - "t": 49.15, - "down_rise": 5.461345434557074, - "up_rise": 0.5451948480658189, - "diff_rise": 4.916150586491256, - "down_abs": 27.842861047477697 - }, - "60": { - "t": 59.18, - "down_rise": 5.542144890879513, - "up_rise": 0.46721956300274314, - "diff_rise": 5.07492532787677, - "down_abs": 27.923660503800136 - }, - "75": { - "t": 74.23, - "down_rise": 4.656381368211555, - "up_rise": 0.3113607823165694, - "diff_rise": 4.345020585894986, - "down_abs": 27.037896981132178 - } - }, - "rep": 4, - "cooldown_s": 11, - "base_at_start": 21.914988917022924 - }, - { - "duty": 20, - "flow": false, - "start_down": 25.205610155369527, - "start_up": 22.615168696173612, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 3.366526679035303, - "up_rise": 0.780872770150097, - "diff_rise": 2.585653908885206, - "down_abs": 28.57213683440483 - }, - "20": { - "t": 19.06, - "down_rise": 3.5292395823768743, - "up_rise": 0.23392622691028464, - "diff_rise": 3.2953133554665897, - "down_abs": 28.7348497377464 - }, - "25": { - "t": 24.07, - "down_rise": 4.51077620628547, - "up_rise": 0.46813563422978177, - "diff_rise": 4.042640572055689, - "down_abs": 29.716386361654997 - }, - "30": { - "t": 29.09, - "down_rise": 5.253172205135513, - "up_rise": 1.0941552637432324, - "diff_rise": 4.159016941392281, - "down_abs": 30.45878236050504 - }, - "40": { - "t": 39.12, - "down_rise": 5.584943801582597, - "up_rise": 1.4865620991558899, - "diff_rise": 4.098381702426707, - "down_abs": 30.790553956952124 - }, - "50": { - "t": 49.15, - "down_rise": 6.168357734189328, - "up_rise": 1.8011601942399125, - "diff_rise": 4.367197539949416, - "down_abs": 31.373967889558855 - }, - "60": { - "t": 59.18, - "down_rise": 6.41952244474794, - "up_rise": 2.3532242794870513, - "diff_rise": 4.066298165260889, - "down_abs": 31.625132600117468 - }, - "75": { - "t": 74.22, - "down_rise": 7.00830861152383, - "up_rise": 2.828044827305689, - "diff_rise": 4.180263784218141, - "down_abs": 32.21391876689336 - } - }, - "rep": 4, - "cooldown_s": 0, - "base_at_start": 22.459370242678233 - }, - { - "duty": 30, - "flow": true, - "start_down": 22.459370242678233, - "start_up": 22.303690221175657, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 6.846923440894614, - "up_rise": 0.23356425914681722, - "diff_rise": 6.613359181747796, - "down_abs": 29.306293683572846 - }, - "20": { - "t": 19.06, - "down_rise": 7.421515586307123, - "up_rise": 0.389423039832991, - "diff_rise": 7.032092546474132, - "down_abs": 29.880885828985356 - }, - "25": { - "t": 24.08, - "down_rise": 7.999412117826807, - "up_rise": 1.1706198003985264, - "diff_rise": 6.828792317428281, - "down_abs": 30.45878236050504 - }, - "30": { - "t": 29.09, - "down_rise": 7.339232367540376, - "up_rise": -0.23330443941455314, - "diff_rise": 7.572536806954929, - "down_abs": 29.79860261021861 - }, - "40": { - "t": 39.12, - "down_rise": 8.08224779015768, - "up_rise": 0.4673983248960667, - "diff_rise": 7.614849465261614, - "down_abs": 30.541618032835913 - }, - "50": { - "t": 49.15, - "down_rise": 8.08224779015768, - "up_rise": 0.4673983248960667, - "diff_rise": 7.614849465261614, - "down_abs": 30.541618032835913 - }, - "60": { - "t": 59.18, - "down_rise": 8.248133112791038, - "up_rise": 0.9359160111582128, - "diff_rise": 7.312217101632825, - "down_abs": 30.70750335546927 - }, - "75": { - "t": 74.23, - "down_rise": 8.831028723720465, - "up_rise": 1.2489228162728523, - "diff_rise": 7.5821059074476125, - "down_abs": 31.290398966398698 - } - }, - "rep": 4, - "cooldown_s": 22, - "base_at_start": 22.225893698005905 - }, - { - "duty": 30, - "flow": false, - "start_down": 22.771088546071724, - "start_up": 21.914988917022924, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 7.687693814433317, - "up_rise": 0.7001797791506874, - "diff_rise": 6.987514035282629, - "down_abs": 30.45878236050504 - }, - "20": { - "t": 19.06, - "down_rise": 9.190017384733267, - "up_rise": 1.1683154133804692, - "diff_rise": 8.021701971352798, - "down_abs": 31.96110593080499 - }, - "25": { - "t": 24.08, - "down_rise": 9.865812023279602, - "up_rise": 1.1683154133804692, - "diff_rise": 8.697496609899133, - "down_abs": 32.636900569351326 - }, - "30": { - "t": 29.09, - "down_rise": 11.061490952726047, - "up_rise": 3.2115064579089676, - "diff_rise": 7.849984494817079, - "down_abs": 33.83257949879777 - }, - "40": { - "t": 39.12, - "down_rise": 12.977115697647378, - "up_rise": 4.004632466797091, - "diff_rise": 8.972483230850287, - "down_abs": 35.7482042437191 - }, - "50": { - "t": 49.15, - "down_rise": 13.33063012290728, - "up_rise": 4.004632466797091, - "diff_rise": 9.32599765611019, - "down_abs": 36.101718668979004 - }, - "60": { - "t": 59.18, - "down_rise": 13.864107394444943, - "up_rise": 4.88245514123696, - "diff_rise": 8.981652253207983, - "down_abs": 36.63519594051667 - }, - "75": { - "t": 74.23, - "down_rise": 13.685847491738684, - "up_rise": 4.243458414500992, - "diff_rise": 9.442389077237692, - "down_abs": 36.45693603781041 - } - }, - "rep": 4, - "cooldown_s": 11, - "base_at_start": 22.771088546071724 - }, - { - "duty": 40, - "flow": true, - "start_down": 24.180323121554522, - "start_up": 23.23960623233387, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 7.193644768004333, - "up_rise": -0.23440379004489387, - "diff_rise": 7.428048558049227, - "down_abs": 31.373967889558855 - }, - "20": { - "t": 19.06, - "down_rise": 8.118027930103437, - "up_rise": 0.23470378924031365, - "diff_rise": 7.883324140863124, - "down_abs": 32.29835105165796 - }, - "25": { - "t": 24.08, - "down_rise": 8.033595645338835, - "up_rise": -0.39051130924997324, - "diff_rise": 8.424106954588808, - "down_abs": 32.21391876689336 - }, - "30": { - "t": 29.09, - "down_rise": 9.652256377243248, - "up_rise": 0.6265710250734742, - "diff_rise": 9.025685352169774, - "down_abs": 33.83257949879777 - }, - "40": { - "t": 39.13, - "down_rise": 9.308876820590399, - "up_rise": 0.15643523398983916, - "diff_rise": 9.15244158660056, - "down_abs": 33.48919994214492 - }, - "50": { - "t": 49.16, - "down_rise": 9.394586954689089, - "up_rise": 0.4697177275829745, - "diff_rise": 8.924869227106115, - "down_abs": 33.57491007624361 - }, - "60": { - "t": 59.19, - "down_rise": 9.910745918806015, - "up_rise": 0.5481263900913973, - "diff_rise": 9.362619528714617, - "down_abs": 34.09106904036054 - }, - "75": { - "t": 74.23, - "down_rise": 9.308876820590399, - "up_rise": 0.4697177275829745, - "diff_rise": 8.839159093007424, - "down_abs": 33.48919994214492 - } - }, - "rep": 4, - "cooldown_s": 11, - "base_at_start": 22.927132764254054 - }, - { - "duty": 40, - "flow": false, - "start_down": 23.23960623233387, - "start_up": 22.303690221175657, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 10.937810835335029, - "up_rise": 1.4056337387411872, - "diff_rise": 9.532177096593841, - "down_abs": 34.1774170676689 - }, - "20": { - "t": 19.06, - "down_rise": 11.981432811765046, - "up_rise": 1.6409680231826087, - "diff_rise": 10.340464788582437, - "down_abs": 35.221039044098916 - }, - "25": { - "t": 24.08, - "down_rise": 14.113091202915768, - "up_rise": 3.139523302303644, - "diff_rise": 10.973567900612125, - "down_abs": 37.35269743524964 - }, - "30": { - "t": 29.09, - "down_rise": 13.484884702114527, - "up_rise": 2.90191993419387, - "diff_rise": 10.582964767920657, - "down_abs": 36.7244909344484 - }, - "40": { - "t": 39.12, - "down_rise": 15.662770135806852, - "up_rise": 4.734206759956521, - "diff_rise": 10.92856337585033, - "down_abs": 38.90237636814072 - }, - "50": { - "t": 49.15, - "down_rise": 16.872499495828663, - "up_rise": 5.377740593052295, - "diff_rise": 11.494758902776368, - "down_abs": 40.11210572816253 - }, - "60": { - "t": 59.18, - "down_rise": 17.06063000074613, - "up_rise": 5.5391708263020405, - "diff_rise": 11.52145917444409, - "down_abs": 40.30023623308 - }, - "75": { - "t": 74.23, - "down_rise": 17.343870292877114, - "up_rise": 5.5391708263020405, - "diff_rise": 11.804699466575073, - "down_abs": 40.58347652521098 - } - }, - "rep": 4, - "cooldown_s": 11, - "base_at_start": 22.381515612920623 - }, - { - "duty": 50, - "flow": true, - "start_down": 23.161438802330906, - "start_up": 22.070385781761104, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.04, - "down_rise": 9.900533510348907, - "up_rise": 1.4822272556874054, - "diff_rise": 8.418306254661502, - "down_abs": 33.06197231267981 - }, - "20": { - "t": 19.06, - "down_rise": 12.674986579779443, - "up_rise": 1.6389381781557404, - "diff_rise": 11.036048401623702, - "down_abs": 35.83642538211035 - }, - "25": { - "t": 24.07, - "down_rise": 11.710171258466744, - "up_rise": 1.8742724625971618, - "diff_rise": 9.835898795869582, - "down_abs": 34.87161006079765 - }, - "30": { - "t": 29.09, - "down_rise": 12.586765441388195, - "up_rise": 1.6389381781557404, - "diff_rise": 10.947827263232455, - "down_abs": 35.7482042437191 - }, - "40": { - "t": 39.12, - "down_rise": 13.92134752045314, - "up_rise": 1.8742724625971618, - "diff_rise": 12.047075057855977, - "down_abs": 37.082786322784045 - }, - "50": { - "t": 49.15, - "down_rise": 14.101173644309995, - "up_rise": 2.1885675424182978, - "diff_rise": 11.912606101891697, - "down_abs": 37.2626124466409 - }, - "60": { - "t": 59.18, - "down_rise": 14.101173644309995, - "up_rise": 2.1099373397934187, - "diff_rise": 11.991236304516576, - "down_abs": 37.2626124466409 - }, - "75": { - "t": 74.22, - "down_rise": 13.83160505479384, - "up_rise": 2.1885675424182978, - "diff_rise": 11.643037512375543, - "down_abs": 36.993043857124746 - } - }, - "rep": 4, - "cooldown_s": 21, - "base_at_start": 22.381515612920623 - }, - { - "duty": 50, - "flow": false, - "start_down": 22.070385781761104, - "start_up": 22.148125674399637, - "aborted_at": null, - "samples": 75, - "at": { - "15": { - "t": 14.05, - "down_rise": 13.589701274077129, - "up_rise": 1.3261843471745465, - "diff_rise": 12.263516926902582, - "down_abs": 35.66008705583823 - }, - "20": { - "t": 19.06, - "down_rise": 16.46417687180667, - "up_rise": 2.8992974376118923, - "diff_rise": 13.56487943419478, - "down_abs": 38.534562653567775 - }, - "25": { - "t": 24.08, - "down_rise": 17.38755029139719, - "up_rise": 2.8992974376118923, - "diff_rise": 14.488252853785298, - "down_abs": 39.457936073158294 - }, - "30": { - "t": 29.09, - "down_rise": 20.04575445352168, - "up_rise": 4.809568713735587, - "diff_rise": 15.236185739786094, - "down_abs": 42.116140235282785 - }, - "40": { - "t": 39.12, - "down_rise": 21.61816587327661, - "up_rise": 6.099304882156503, - "diff_rise": 15.518860991120107, - "down_abs": 43.68855165503771 - }, - "50": { - "t": 49.15, - "down_rise": 22.622816218781168, - "up_rise": 6.99458380507042, - "diff_rise": 15.628232413710748, - "down_abs": 44.69320200054227 - }, - "60": { - "t": 59.18, - "down_rise": 23.439492428894937, - "up_rise": 7.404027268883908, - "diff_rise": 16.03546516001103, - "down_abs": 45.50987821065604 - }, - "75": { - "t": 74.23, - "down_rise": 23.852339995222223, - "up_rise": 8.227891708505013, - "diff_rise": 15.624448286717211, - "down_abs": 45.92272577698333 - } - }, - "rep": 4, - "cooldown_s": 21, - "base_at_start": 21.682095410064733 - } - ] -} \ No newline at end of file diff --git a/scripts/bench/flow_warm_log.txt b/scripts/bench/flow_warm_log.txt deleted file mode 100644 index 5251724..0000000 --- a/scripts/bench/flow_warm_log.txt +++ /dev/null @@ -1,96 +0,0 @@ -=== warm-baseline validation 2026-08-02 22:43:41 duty=40% window=50s threshold=13.7 - cycle 1/3 FLOW - warmed to down=21.84 up=21.91 in 1.3 min - base_up=22.07 down_rise=12.18 -> ok CORRECT (peak down 34.4 C) - cycle 1/3 NO-FLOW - warmed to down=22.46 up=21.84 in 1.0 min - base_up=21.84 down_rise=18.16 -> FAULT CORRECT (peak down 40.4 C) - cycle 2/3 FLOW - warmed to down=21.84 up=22.07 in 1.0 min - base_up=21.29 down_rise=12.75 -> ok CORRECT (peak down 34.5 C) - cycle 2/3 NO-FLOW - warmed to down=22.07 up=21.91 in 1.0 min - base_up=21.45 down_rise=18.15 -> FAULT CORRECT (peak down 40.5 C) - cycle 3/3 FLOW - warmed to down=22.07 up=21.91 in 1.0 min - base_up=21.60 down_rise=10.95 -> ok CORRECT (peak down 34.3 C) - cycle 3/3 NO-FLOW - warmed to down=22.30 up=22.30 in 1.0 min - base_up=21.76 down_rise=17.85 -> FAULT CORRECT (peak down 40.4 C) - -=== warm-baseline results (baselines 21.3 - 22.1 C) - flow n=3 mean 11.96 sd 0.92 max 12.75 - no-flow n=3 mean 18.06 sd 0.17 min 17.85 - worst-case margin: +5.11 C threshold 13.7 sits +0.95 above flow max, +4.15 below no-flow min - misclassified: 0 of 6 -controller restarted -=== warm-baseline validation 2026-08-02 22:56:44 duty=40% window=50s threshold=13.7 - cycle 1/3 FLOW - warmed to down=22.30 up=22.07 in 1.3 min - base_up=22.23 down_rise=12.62 -> ok CORRECT (peak down 34.0 C) - cycle 1/3 NO-FLOW - warmed to down=22.23 up=21.84 in 1.0 min - base_up=21.76 down_rise=18.71 -> FAULT CORRECT (peak down 40.6 C) - cycle 2/3 FLOW - warmed to down=22.30 up=22.07 in 1.0 min - base_up=21.76 down_rise=11.03 -> ok CORRECT (peak down 34.3 C) - cycle 2/3 NO-FLOW - warmed to down=22.15 up=21.84 in 1.0 min - base_up=21.37 down_rise=18.26 -> FAULT CORRECT (peak down 40.1 C) - cycle 3/3 FLOW - warmed to down=22.54 up=22.15 in 1.0 min - base_up=21.99 down_rise=11.08 -> ok CORRECT (peak down 34.3 C) - cycle 3/3 NO-FLOW - warmed to down=22.07 up=21.84 in 1.0 min - base_up=21.91 down_rise=18.42 -> FAULT CORRECT (peak down 40.5 C) - -=== warm-baseline results (baselines 21.4 - 22.2 C) - flow n=3 mean 11.58 sd 0.91 max 12.62 - no-flow n=3 mean 18.46 sd 0.23 min 18.26 - worst-case margin: +5.63 C threshold 13.7 sits +1.08 above flow max, +4.56 below no-flow min - misclassified: 0 of 6 -controller restarted -=== warm-baseline validation 2026-08-02 23:10:04 duty=40% window=50s threshold=13.7 - cycle 1/2 FLOW - warm abort: downstream 51.95 at 0.5 min - warmed to down=22.15 up=21.84 in 1.3 min -=== warm-baseline validation 2026-08-02 23:11:51 duty=40% window=50s threshold=13.7 - cycle 1/2 FLOW - base_up=22.23 down_rise=15.53 -> FAULT *** WRONG *** (peak down 37.7 C) - cycle 1/2 NO-FLOW - warm abort: downstream 49.34 at 0.3 min - warm target reached: up=25.84 at 0.8 min - warmed to down=21.84 up=21.60 in 1.0 min - warmed to down=22.15 up=22.07 in 1.6 min - base_up=21.37 down_rise=10.69 -> ok *** WRONG *** (peak down 34.1 C) - cycle 2/2 FLOW - base_up=20.91 down_rise=11.48 -> ok CORRECT (peak down 34.4 C) - cycle 1/2 NO-FLOW - warming: 0.8 min down=37.53 up=23.71 - warming: 0.8 min down=38.17 up=24.65 - warming: 1.8 min down=37.80 up=23.94 - warming: 1.8 min down=37.99 up=24.50 - (warming plateaued at 24.18 C after 2.6 min) - warming: 2.9 min down=26.56 up=23.47 - (warming plateaued at 23.47 C after 2.9 min) - warmed to down=21.68 up=21.60 in 3.4 min - warmed to down=28.57 up=21.84 in 3.6 min - base_up=21.37 down_rise=12.18 -> ok *** WRONG *** (peak down 34.1 C) - cycle 2/2 FLOW - base_up=21.91 down_rise=3.76 -> ok CORRECT (peak down 35.8 C) - cycle 2/2 NO-FLOW - warm abort: downstream 48.90 at 0.3 min - warm target reached: up=25.68 at 0.5 min - warmed to down=22.23 up=21.84 in 1.0 min - warmed to down=21.29 up=21.68 in 1.3 min - base_up=21.45 down_rise=11.58 -> ok *** WRONG *** (peak down 34.4 C) - base_up=21.06 down_rise=11.30 -> ok CORRECT (peak down 34.2 C) - cycle 2/2 NO-FLOW - -=== warm-baseline results (baselines 21.4 - 22.2 C) - flow n=2 mean 9.65 sd 8.32 max 15.53 - no-flow n=2 mean 11.13 sd 0.63 min 10.69 - worst-case margin: -4.84 C threshold 13.7 sits -1.83 above flow max, -3.01 below no-flow min - misclassified: 3 of 4 -controller restarted - warming: 0.8 min down=37.71 up=24.50 diff --git a/scripts/bench/flow_warm_results.json b/scripts/bench/flow_warm_results.json deleted file mode 100644 index f68af1e..0000000 --- a/scripts/bench/flow_warm_results.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "flow": true, - "base_up": 20.90741361785507, - "base_down": 21.837331209719537, - "down_rise": 11.480714695811116, - "up_rise": 2.254025184475836, - "peak_down": 34.43702357179029, - "t": 49.15, - "cycle": 0 - }, - { - "flow": false, - "base_up": 21.371932016053336, - "base_down": 21.914988917022924, - "down_rise": 12.176080123337613, - "up_rise": 1.5552007482007184, - "peak_down": 34.09106904036054, - "t": 49.17, - "cycle": 0 - }, - { - "flow": true, - "base_up": 21.06216007856807, - "base_down": 21.75970016295645, - "down_rise": 11.302272149723365, - "up_rise": 2.6471638813487743, - "peak_down": 34.1774170676689, - "t": 49.16, - "cycle": 1 - } -] \ No newline at end of file diff --git a/scripts/bench/lid_ir_ambient_baseline.csv b/scripts/bench/lid_ir_ambient_baseline.csv deleted file mode 100644 index 7b8a0d7..0000000 --- a/scripts/bench/lid_ir_ambient_baseline.csv +++ /dev/null @@ -1,601 +0,0 @@ -t_s,lid_ir_1,lid_ir_2,lid_ir_3,lid_ir_4,hv_current,water_1,water_2,pwr_temp,laser_on_sampled -4665.89,37,37,40,40,0,672,669,586,0 -4666.45,37,36,40,40,0,674,674,586,0 -4667.01,37,36,40,40,0,668,673,586,0 -4667.57,37,36,39,40,0,667,669,593,0 -4668.14,36,36,39,40,0,667,674,592,0 -4668.70,38,36,39,40,0,668,674,593,0 -4669.27,37,35,38,41,0,665,669,591,0 -4669.83,39,37,39,39,0,667,666,592,0 -4670.40,38,36,40,40,0,674,667,589,0 -4670.96,37,36,38,41,0,668,667,593,0 -4671.52,37,37,41,40,0,671,674,590,0 -4672.09,38,36,41,40,0,671,666,589,0 -4672.65,37,36,40,40,0,669,666,590,0 -4673.22,37,36,38,41,0,669,676,593,0 -4673.78,38,37,40,41,0,675,674,590,0 -4674.35,36,38,40,40,0,669,667,590,0 -4674.91,37,36,39,40,0,663,667,590,0 -4675.48,38,38,39,40,0,667,666,592,0 -4676.05,38,36,40,40,0,666,670,587,0 -4676.62,38,36,38,39,0,672,667,586,0 -4677.18,38,36,38,40,0,670,667,589,0 -4677.75,37,36,39,40,0,666,664,592,0 -4678.31,36,37,40,39,0,665,672,593,0 -4678.88,37,36,40,40,0,667,669,593,0 -4679.44,37,36,40,40,0,674,678,591,0 -4680.01,37,36,40,39,0,667,665,587,0 -4680.57,37,36,39,38,0,676,677,587,0 -4681.14,37,35,40,40,0,675,670,587,0 -4681.70,38,37,39,41,0,672,671,593,0 -4682.27,36,36,39,40,0,675,671,592,0 -4682.84,38,37,38,41,0,675,667,592,0 -4683.40,37,36,38,40,0,667,666,593,0 -4683.97,36,36,40,40,0,668,666,591,0 -4684.53,37,36,39,40,0,669,669,594,0 -4685.10,38,36,40,40,0,674,667,592,0 -4685.66,37,36,39,39,0,673,669,586,0 -4686.23,37,37,40,40,0,668,667,586,0 -4686.79,36,36,39,39,0,668,666,593,0 -4687.36,37,37,39,40,0,673,666,586,0 -4687.92,37,36,40,41,0,666,669,593,0 -4688.49,37,37,39,40,0,674,666,587,0 -4689.06,36,36,39,41,0,666,674,586,0 -4689.63,37,37,40,40,0,673,675,588,0 -4690.19,37,36,40,40,0,662,674,593,0 -4690.76,38,35,39,40,0,673,673,586,0 -4691.32,37,37,40,41,0,671,671,593,0 -4691.89,38,36,40,40,0,673,665,592,0 -4692.45,37,36,40,40,0,674,672,591,0 -4693.02,37,36,40,40,0,675,677,587,0 -4693.58,38,37,40,40,0,677,668,592,0 -4694.15,37,36,40,39,0,667,675,587,0 -4694.71,37,36,40,39,0,676,675,593,0 -4695.27,37,37,40,40,0,666,671,588,0 -4695.84,38,36,40,40,0,674,673,587,0 -4696.40,37,36,40,40,0,670,672,590,0 -4696.97,37,36,38,40,0,669,674,588,0 -4697.53,37,37,39,40,0,676,674,589,0 -4698.10,38,36,40,39,0,677,669,591,0 -4698.67,37,38,39,38,0,673,676,593,0 -4699.23,38,36,39,40,0,670,675,587,0 -4699.80,38,36,40,40,0,675,671,588,0 -4700.37,38,36,40,40,0,673,668,583,0 -4700.93,37,37,39,39,0,671,670,591,0 -4701.50,37,35,41,39,0,675,674,587,0 -4702.06,37,37,39,40,0,668,675,588,0 -4702.63,37,36,39,40,0,669,666,591,0 -4703.19,37,37,39,41,0,667,666,588,0 -4703.75,36,36,39,40,0,672,667,588,0 -4704.32,37,36,40,38,0,671,673,587,0 -4704.88,37,36,39,40,0,676,673,586,0 -4705.44,38,36,38,40,0,676,667,586,0 -4706.01,37,36,40,40,0,670,669,587,0 -4706.58,36,36,40,39,0,677,673,586,0 -4707.15,37,37,40,41,0,674,670,589,0 -4707.72,37,36,40,41,0,672,668,593,0 -4708.28,38,37,39,40,0,669,665,591,0 -4708.85,37,35,40,41,0,667,672,586,0 -4709.41,37,36,40,41,0,671,674,586,0 -4709.98,37,37,40,40,0,675,667,587,0 -4710.54,37,37,39,41,0,666,667,590,0 -4711.11,38,37,40,40,0,667,666,591,0 -4711.67,37,36,38,40,0,670,668,586,0 -4712.23,37,36,38,41,0,667,667,593,0 -4712.80,38,36,39,41,0,666,673,592,0 -4713.36,37,37,39,40,0,675,669,592,0 -4713.93,37,36,40,39,0,671,672,589,0 -4714.49,38,37,41,40,0,672,666,587,0 -4715.05,37,36,38,39,0,666,672,586,0 -4715.62,37,36,40,39,0,667,671,592,0 -4716.18,39,36,40,40,0,665,665,590,0 -4716.75,38,37,40,41,0,673,665,589,0 -4717.31,36,36,39,40,0,678,673,593,0 -4717.87,37,36,39,39,0,674,665,589,0 -4718.44,38,37,40,42,0,668,669,593,0 -4719.00,38,36,40,40,0,671,674,586,0 -4719.57,37,36,39,40,0,668,667,592,0 -4720.13,37,36,40,40,0,673,668,590,0 -4720.69,38,37,40,40,0,674,667,592,0 -4721.26,37,36,41,40,0,664,672,591,0 -4721.83,37,37,39,40,0,671,669,588,0 -4722.40,36,37,40,39,0,665,666,587,0 -4722.96,38,37,39,40,0,676,675,586,0 -4723.53,37,37,40,41,0,672,667,591,0 -4724.10,38,36,39,40,0,669,668,594,0 -4724.66,37,37,39,41,0,666,671,586,0 -4725.23,37,37,40,40,0,673,674,586,0 -4725.79,38,37,39,41,0,676,674,591,0 -4726.36,37,37,40,41,0,675,670,586,0 -4726.92,37,36,38,40,0,675,669,592,0 -4727.49,37,37,39,40,0,673,676,590,0 -4728.05,38,36,40,39,0,669,669,588,0 -4728.62,37,36,40,40,0,676,674,586,0 -4729.19,37,36,40,40,0,666,674,586,0 -4729.75,37,36,38,41,0,667,674,587,0 -4730.32,37,36,39,39,0,669,670,590,0 -4730.89,38,36,39,41,0,668,666,594,0 -4731.45,38,37,40,40,0,673,672,587,0 -4732.02,38,36,39,40,0,674,665,587,0 -4732.58,37,35,39,41,0,676,672,588,0 -4733.14,38,35,39,40,0,677,669,589,0 -4733.71,38,36,40,40,0,674,667,592,0 -4734.27,36,36,39,39,0,673,666,593,0 -4734.84,37,36,38,39,0,667,669,587,0 -4735.40,38,37,39,40,0,669,666,593,0 -4735.96,38,36,40,40,0,668,674,587,0 -4736.53,37,36,38,40,0,667,675,587,0 -4737.09,38,37,40,40,0,674,668,586,0 -4737.66,38,36,39,41,0,667,673,588,0 -4738.22,37,37,40,40,0,674,672,593,0 -4738.78,37,36,39,40,0,670,667,587,0 -4739.35,36,36,39,40,0,668,669,593,0 -4739.92,37,37,40,40,0,667,671,591,0 -4740.48,37,37,39,39,0,668,674,590,0 -4741.05,37,36,39,39,0,668,666,586,0 -4741.62,37,36,40,40,0,674,675,592,0 -4742.18,37,36,39,39,0,670,676,586,0 -4742.75,38,36,39,40,0,669,666,591,0 -4743.32,37,36,40,40,0,675,674,587,0 -4743.88,36,37,40,40,0,672,665,587,0 -4744.45,37,36,39,40,0,674,671,592,0 -4745.01,37,36,39,40,0,674,669,588,0 -4745.58,37,36,39,40,0,675,665,587,0 -4746.15,38,36,40,40,0,673,667,587,0 -4746.72,37,35,41,40,0,667,665,590,0 -4747.28,37,37,40,41,0,668,674,594,0 -4747.85,38,36,39,41,0,668,674,588,0 -4748.42,38,36,39,41,0,667,668,587,0 -4748.99,36,37,39,39,0,674,674,586,0 -4749.56,37,36,39,41,0,674,670,593,0 -4750.12,37,36,40,40,0,675,672,592,0 -4750.69,37,37,39,39,0,670,668,589,0 -4751.25,38,37,39,40,0,672,670,590,0 -4751.82,37,36,41,39,0,667,666,591,0 -4752.38,38,37,39,40,0,669,666,587,0 -4752.95,37,36,39,40,0,669,666,593,0 -4753.51,38,36,39,40,0,676,669,592,0 -4754.08,37,37,39,40,0,668,667,587,0 -4754.65,38,35,38,40,0,667,670,594,0 -4755.22,37,36,39,39,0,677,673,587,0 -4755.79,38,36,39,40,0,672,666,587,0 -4756.35,37,36,39,41,0,670,671,587,0 -4756.92,38,36,39,41,0,675,669,593,0 -4757.48,37,36,39,39,0,675,674,588,0 -4758.05,37,36,39,40,0,672,674,593,0 -4758.61,37,36,39,40,0,674,665,586,0 -4759.18,37,37,39,39,0,673,669,588,0 -4759.75,37,35,39,40,0,674,669,585,0 -4760.32,37,37,40,39,0,671,673,587,0 -4760.88,38,37,39,40,0,674,673,592,0 -4761.45,38,36,39,40,0,673,673,591,0 -4762.01,37,36,39,41,0,674,673,592,0 -4762.58,37,36,39,41,0,666,672,589,0 -4763.14,39,37,39,40,0,665,669,593,0 -4763.70,37,36,40,40,0,673,675,587,0 -4764.27,36,36,39,40,0,668,670,585,0 -4764.83,37,36,39,40,0,668,675,587,0 -4765.40,37,37,40,40,0,667,664,592,0 -4765.97,38,37,40,40,0,670,676,593,0 -4766.54,37,36,39,41,0,665,670,593,0 -4767.10,37,36,39,40,0,675,665,587,0 -4767.67,37,37,40,40,0,668,666,591,0 -4768.23,37,36,40,40,0,668,670,592,0 -4768.80,37,36,40,39,0,665,671,589,0 -4769.36,37,36,40,40,0,673,671,586,0 -4769.93,38,36,39,39,0,676,667,585,0 -4770.49,38,36,39,40,0,672,668,586,0 -4771.06,38,37,39,41,0,667,674,593,0 -4771.62,37,37,38,40,0,668,668,587,0 -4772.19,37,36,40,40,0,669,667,586,0 -4772.76,37,36,41,40,0,674,666,588,0 -4773.33,38,37,39,40,0,666,673,586,0 -4773.89,37,36,39,40,0,674,671,591,0 -4774.46,37,36,39,40,0,676,671,587,0 -4775.03,37,37,40,39,0,669,673,592,0 -4775.59,37,37,39,39,0,674,669,587,0 -4776.16,38,36,40,40,0,673,672,591,0 -4776.72,37,37,40,40,0,674,667,587,0 -4777.29,37,36,40,40,0,670,676,592,0 -4777.85,38,37,40,41,0,667,670,592,0 -4778.42,38,37,39,40,0,669,665,586,0 -4778.99,38,37,39,40,0,669,670,594,0 -4779.56,38,37,39,40,0,670,673,593,0 -4780.12,38,37,38,40,0,667,675,587,0 -4780.68,37,36,41,40,0,666,668,592,0 -4781.25,37,37,40,40,0,666,666,591,0 -4781.82,37,36,40,41,0,665,667,592,0 -4782.39,38,37,39,41,0,667,673,587,0 -4782.96,37,36,40,40,0,666,667,591,0 -4783.52,38,36,40,40,0,674,675,593,0 -4784.09,38,36,39,41,0,662,673,586,0 -4784.65,37,36,40,41,0,676,671,587,0 -4785.22,37,38,40,40,0,669,666,591,0 -4785.78,37,37,39,40,0,668,668,592,0 -4786.35,37,36,40,40,0,676,673,590,0 -4786.91,37,36,40,40,0,669,675,592,0 -4787.48,37,37,40,40,0,668,667,587,0 -4788.04,37,37,41,39,0,676,674,586,0 -4788.60,37,36,39,40,0,669,672,588,0 -4789.17,38,37,40,40,0,667,674,592,0 -4789.74,38,37,40,40,0,674,676,592,0 -4790.30,37,36,39,41,0,662,669,592,0 -4790.87,38,36,39,40,0,666,666,588,0 -4791.44,37,37,40,40,0,675,675,590,0 -4792.01,37,36,40,40,0,668,668,587,0 -4792.57,37,36,40,40,0,672,667,587,0 -4793.14,37,36,39,40,0,673,669,593,0 -4793.70,37,36,41,41,0,666,669,588,0 -4794.27,37,37,40,40,0,676,673,588,0 -4794.83,37,36,38,40,0,671,674,588,0 -4795.40,36,36,40,40,0,671,667,588,0 -4795.97,37,36,39,39,0,672,670,586,0 -4796.54,37,36,39,40,0,667,667,587,0 -4797.10,37,35,40,40,0,675,673,586,0 -4797.67,37,37,40,40,0,670,669,592,0 -4798.24,37,36,40,40,0,669,671,588,0 -4798.81,37,37,40,39,0,675,674,592,0 -4799.37,37,37,38,40,0,670,669,591,0 -4799.94,37,36,39,40,0,674,673,593,0 -4800.51,36,36,40,40,0,674,672,587,0 -4801.07,37,36,40,40,0,673,673,589,0 -4801.64,37,36,40,40,0,671,667,587,0 -4802.20,37,35,39,41,0,670,668,586,0 -4802.77,37,36,40,39,0,674,667,588,0 -4803.33,37,37,40,39,0,670,678,589,0 -4803.91,37,35,40,40,0,671,675,592,0 -4804.47,38,36,40,41,0,666,675,592,0 -4805.04,37,36,39,40,0,668,672,593,0 -4805.60,38,36,40,40,0,676,670,588,0 -4806.17,38,36,38,39,0,670,677,590,0 -4806.74,37,37,39,40,0,675,671,587,0 -4807.30,37,36,40,40,0,672,668,587,0 -4807.87,38,37,39,40,0,667,668,591,0 -4808.43,37,36,40,39,0,666,666,586,0 -4809.00,37,36,41,39,0,673,667,591,0 -4809.56,36,37,38,40,0,675,668,586,0 -4810.13,36,36,40,40,0,672,669,592,0 -4810.70,38,36,40,39,0,673,667,592,0 -4811.27,38,36,40,41,0,672,673,587,0 -4811.83,37,36,40,40,0,668,667,586,0 -4812.40,37,37,39,39,0,666,669,586,0 -4812.96,37,36,40,41,0,671,667,589,0 -4813.52,37,37,39,40,0,674,673,587,0 -4814.09,38,36,39,40,0,668,671,592,0 -4814.65,37,36,39,38,0,669,669,594,0 -4815.22,37,36,39,40,0,667,669,587,0 -4815.78,37,36,39,40,0,671,674,591,0 -4816.35,37,37,40,39,0,666,663,589,0 -4816.92,37,37,40,40,0,672,668,588,0 -4817.49,37,37,39,39,0,675,671,589,0 -4818.05,38,37,40,41,0,675,666,592,0 -4818.62,38,37,40,40,0,666,666,591,0 -4819.18,37,37,39,40,0,671,667,591,0 -4819.75,37,36,39,40,0,673,669,591,0 -4820.32,37,37,39,40,0,674,674,587,0 -4820.88,37,37,40,40,0,675,666,585,0 -4821.45,36,36,40,41,0,667,675,593,0 -4822.02,38,37,39,39,0,668,675,591,0 -4822.58,36,37,41,40,0,667,672,590,0 -4823.15,37,36,38,40,0,676,671,589,0 -4823.72,37,37,39,41,0,673,671,587,0 -4824.28,38,36,40,41,0,666,670,592,0 -4824.85,37,36,40,40,0,666,672,590,0 -4825.41,37,36,40,41,0,674,674,586,0 -4825.99,38,36,40,41,0,675,671,593,0 -4826.55,37,36,38,40,0,673,670,588,0 -4827.12,37,37,39,39,0,668,665,587,0 -4827.68,38,37,40,41,0,666,666,587,0 -4828.25,37,36,39,39,0,673,668,585,0 -4828.81,38,36,40,40,0,668,666,588,0 -4829.38,37,37,39,40,0,668,675,592,0 -4829.95,37,37,40,39,0,665,667,589,0 -4830.51,36,37,39,40,0,669,675,593,0 -4831.08,38,36,40,40,0,667,669,586,0 -4831.65,37,36,40,40,0,677,668,592,0 -4832.21,37,38,39,40,0,668,667,589,0 -4832.78,37,37,40,41,0,665,675,589,0 -4833.34,37,36,39,40,0,676,673,586,0 -4833.91,37,37,38,40,0,666,667,587,0 -4834.49,37,37,40,39,0,673,668,587,0 -4835.05,36,37,40,39,0,667,667,586,0 -4835.62,37,37,40,40,0,671,667,587,0 -4836.18,38,36,39,40,0,673,674,588,0 -4836.75,37,37,39,39,0,668,669,589,0 -4837.31,38,37,39,40,0,672,667,589,0 -4837.88,37,37,39,40,0,670,674,592,0 -4838.45,37,36,40,41,0,667,674,593,0 -4839.02,38,35,39,40,0,671,666,586,0 -4839.59,38,37,40,40,0,670,669,591,0 -4840.15,38,36,40,40,0,673,674,594,0 -4840.72,37,35,39,41,0,666,667,588,0 -4841.28,37,36,39,40,0,667,666,593,0 -4841.85,37,36,39,39,0,674,671,588,0 -4842.41,37,37,38,40,0,672,671,587,0 -4842.98,38,36,41,41,0,674,666,587,0 -4843.54,37,37,40,40,0,667,668,587,0 -4844.10,38,36,39,41,0,673,675,588,0 -4844.67,37,36,38,40,0,671,669,592,0 -4845.23,38,37,39,40,0,676,665,587,0 -4845.80,38,37,40,40,0,674,675,590,0 -4846.36,37,37,40,40,0,671,672,586,0 -4846.94,38,36,40,40,0,674,676,587,0 -4847.50,38,38,40,40,0,679,673,589,0 -4848.07,38,36,40,41,0,671,666,592,0 -4848.64,36,37,40,40,0,668,665,590,0 -4849.20,37,36,40,40,0,670,668,586,0 -4849.77,37,35,40,40,0,669,670,588,0 -4850.33,37,37,39,39,0,666,666,594,0 -4850.90,37,36,40,39,0,672,672,586,0 -4851.47,37,36,39,39,0,675,675,593,0 -4852.04,37,36,40,40,0,669,671,592,0 -4852.61,38,37,39,40,0,669,676,593,0 -4853.18,38,36,40,40,0,671,671,586,0 -4853.75,37,36,40,40,0,675,670,589,0 -4854.32,38,37,39,40,0,675,668,589,0 -4854.88,38,36,39,40,0,668,667,592,0 -4855.45,37,36,40,40,0,674,668,591,0 -4856.01,37,36,39,40,0,675,667,587,0 -4856.58,36,37,40,39,0,665,667,588,0 -4857.14,38,37,40,40,0,669,665,586,0 -4857.71,37,36,39,40,0,675,667,587,0 -4858.27,37,36,40,40,0,673,672,590,0 -4858.84,38,36,39,40,0,677,674,587,0 -4859.41,37,36,41,39,0,668,675,587,0 -4859.98,37,36,39,39,0,676,670,586,0 -4860.54,37,36,40,40,0,676,666,588,0 -4861.11,37,36,39,40,0,676,673,591,0 -4861.67,38,36,39,40,0,669,671,589,0 -4862.24,37,36,40,39,0,676,675,589,0 -4862.80,37,36,39,40,0,669,668,593,0 -4863.37,38,36,40,41,0,673,674,588,0 -4863.93,38,36,40,40,0,668,669,589,0 -4864.50,38,36,40,40,0,669,674,594,0 -4865.06,37,36,40,40,0,673,665,594,0 -4865.63,37,37,39,40,0,674,675,594,0 -4866.20,38,36,40,40,0,673,667,594,0 -4866.76,37,36,39,40,0,669,668,586,0 -4867.33,37,37,38,39,0,665,666,586,0 -4867.90,37,36,39,39,0,667,669,592,0 -4868.46,37,37,40,39,0,668,677,589,0 -4869.03,37,37,39,40,0,674,675,587,0 -4869.60,37,36,39,39,0,674,669,593,0 -4870.17,38,37,39,40,0,676,673,587,0 -4870.73,37,37,40,40,0,676,667,587,0 -4871.30,38,37,39,40,0,669,674,588,0 -4871.86,38,36,40,40,0,663,666,592,0 -4872.43,38,37,39,40,0,670,670,594,0 -4873.00,38,36,40,40,0,668,666,590,0 -4873.56,37,35,39,40,0,667,674,591,0 -4874.12,37,36,40,39,0,675,666,587,0 -4874.70,37,37,40,39,0,674,673,592,0 -4875.26,36,36,39,40,0,668,677,588,0 -4875.83,38,36,39,41,0,673,667,593,0 -4876.40,36,36,39,40,0,671,669,593,0 -4876.96,37,36,40,40,0,669,672,588,0 -4877.53,36,37,39,40,0,666,667,594,0 -4878.10,38,35,39,40,0,667,667,586,0 -4878.67,38,36,38,40,0,676,674,587,0 -4879.24,37,36,39,40,0,667,676,588,0 -4879.80,37,36,40,39,0,675,673,594,0 -4880.36,38,35,40,40,0,676,668,592,0 -4880.93,38,37,39,41,0,665,671,588,0 -4881.49,36,35,38,41,0,667,673,594,0 -4882.06,38,36,39,40,0,664,667,592,0 -4882.62,37,36,40,41,0,671,674,593,0 -4883.19,37,37,38,41,0,672,667,587,0 -4883.75,37,37,40,40,0,670,673,592,0 -4884.31,37,37,40,40,0,672,669,586,0 -4884.88,38,36,39,40,0,674,674,593,0 -4885.44,38,37,40,40,0,667,674,588,0 -4886.01,38,36,39,41,0,669,669,593,0 -4886.57,37,36,40,41,0,667,674,586,0 -4887.14,38,36,38,41,0,674,674,588,0 -4887.71,38,37,39,41,0,672,668,592,0 -4888.28,39,38,39,40,0,667,668,591,0 -4888.84,37,37,39,38,0,669,672,592,0 -4889.41,36,36,40,39,0,669,667,591,0 -4889.97,38,36,39,40,0,674,677,593,0 -4890.54,37,37,40,39,0,674,670,586,0 -4891.10,37,36,39,41,0,667,675,591,0 -4891.67,37,36,39,41,0,668,666,593,0 -4892.23,37,37,39,40,0,674,670,588,0 -4892.80,38,37,39,41,0,675,671,591,0 -4893.36,38,37,39,41,0,667,670,592,0 -4893.92,37,36,39,39,0,670,673,592,0 -4894.49,37,36,40,40,0,669,667,593,0 -4895.05,37,36,38,40,0,674,674,588,0 -4895.62,37,36,41,39,0,674,675,587,0 -4896.19,38,36,40,40,0,674,667,588,0 -4896.76,37,37,40,41,0,666,667,591,0 -4897.33,37,35,39,40,0,676,678,592,0 -4897.89,37,36,40,39,0,669,672,588,0 -4898.46,37,37,40,40,0,669,666,587,0 -4899.02,37,36,41,40,0,673,666,586,0 -4899.58,36,35,39,39,0,665,674,592,0 -4900.15,37,37,40,40,0,670,667,586,0 -4900.71,37,36,40,41,0,666,664,589,0 -4901.28,37,36,40,40,0,674,674,594,0 -4901.85,37,36,40,40,0,669,671,592,0 -4902.42,37,36,40,40,0,668,667,592,0 -4902.98,38,36,39,39,0,673,667,592,0 -4903.55,38,37,38,40,0,674,674,588,0 -4904.12,39,37,40,41,0,669,665,587,0 -4904.68,37,36,40,41,0,666,673,592,0 -4905.25,38,37,39,39,0,664,675,591,0 -4905.81,37,36,39,40,0,669,667,587,0 -4906.38,37,35,40,39,0,675,671,593,0 -4906.94,37,37,39,40,0,674,674,591,0 -4907.51,38,37,39,39,0,669,665,592,0 -4908.07,37,37,40,40,0,669,666,586,0 -4908.64,38,37,40,39,0,676,669,589,0 -4909.21,38,36,39,40,0,666,668,593,0 -4909.78,38,36,40,40,0,670,671,592,0 -4910.34,38,36,39,39,0,665,670,588,0 -4910.91,38,36,39,40,0,672,669,593,0 -4911.47,37,36,40,39,0,669,665,587,0 -4912.03,38,36,40,40,0,669,673,589,0 -4912.60,37,37,39,40,0,674,673,587,0 -4913.17,36,37,39,40,0,673,675,587,0 -4913.73,37,38,39,40,0,674,674,594,0 -4914.30,36,37,38,40,0,668,674,592,0 -4914.87,38,37,39,40,0,670,666,591,0 -4915.44,37,36,40,40,0,672,675,589,0 -4916.00,37,37,39,41,0,666,666,588,0 -4916.56,37,36,38,39,0,667,666,588,0 -4917.13,37,37,39,40,0,672,672,586,0 -4917.70,37,37,40,40,0,668,665,587,0 -4918.26,37,37,39,40,0,674,674,586,0 -4918.82,38,35,40,40,0,675,675,588,0 -4919.39,37,36,39,41,0,670,674,590,0 -4919.95,37,37,40,41,0,676,673,594,0 -4920.52,38,36,39,41,0,672,669,593,0 -4921.08,37,35,40,40,0,669,667,586,0 -4921.65,39,37,39,40,0,671,673,590,0 -4922.21,37,36,38,40,0,669,676,593,0 -4922.78,37,36,40,40,0,670,666,586,0 -4923.35,37,36,40,40,0,669,669,586,0 -4923.92,37,37,40,41,0,674,674,588,0 -4924.49,37,36,40,40,0,666,673,593,0 -4925.05,38,37,40,39,0,674,675,586,0 -4925.62,37,38,38,40,0,673,674,593,0 -4926.19,38,36,39,40,0,675,674,588,0 -4926.76,37,36,40,40,0,672,674,587,0 -4927.32,37,36,39,40,0,669,676,593,0 -4927.89,38,36,40,40,0,670,671,588,0 -4928.45,37,36,40,40,0,675,668,588,0 -4929.02,38,38,40,40,0,669,673,588,0 -4929.58,37,36,39,39,0,670,674,592,0 -4930.15,37,37,39,38,0,668,668,586,0 -4930.71,37,37,39,40,0,672,667,587,0 -4931.28,37,37,40,40,0,673,667,589,0 -4931.84,37,36,39,41,0,673,671,587,0 -4932.41,38,37,38,41,0,675,669,594,0 -4932.98,38,37,39,40,0,668,667,594,0 -4933.55,38,37,40,41,0,673,667,588,0 -4934.11,37,36,40,41,0,666,670,591,0 -4934.68,38,36,40,40,0,674,674,585,0 -4935.24,37,36,40,40,0,674,668,591,0 -4935.81,37,36,40,40,0,676,674,587,0 -4936.37,37,37,40,40,0,668,673,587,0 -4936.94,38,35,39,40,0,673,675,588,0 -4937.50,37,36,40,40,0,668,668,593,0 -4938.07,37,35,40,40,0,668,671,588,0 -4938.64,38,36,40,40,0,675,667,594,0 -4939.21,37,35,40,40,0,673,669,593,0 -4939.77,37,36,39,40,0,671,672,592,0 -4940.34,37,35,40,41,0,673,673,588,0 -4940.90,37,36,40,39,0,667,673,589,0 -4941.47,38,36,39,39,0,667,675,588,0 -4942.03,38,37,39,40,0,674,670,587,0 -4942.60,37,36,39,41,0,675,674,590,0 -4943.16,37,37,40,41,0,671,664,587,0 -4943.73,37,36,38,40,0,666,669,588,0 -4944.30,37,36,40,40,0,667,667,585,0 -4944.87,37,36,40,40,0,669,667,588,0 -4945.43,37,37,40,40,0,673,675,590,0 -4946.00,36,36,39,40,0,676,668,594,0 -4946.56,36,36,39,40,0,668,670,592,0 -4947.13,39,37,39,40,0,674,669,588,0 -4947.69,37,35,39,39,0,676,669,593,0 -4948.26,37,36,40,39,0,676,673,587,0 -4948.82,38,36,40,40,0,668,671,592,0 -4949.39,37,36,39,40,0,669,668,593,0 -4949.96,38,37,38,40,0,666,674,587,0 -4950.53,37,36,40,40,0,670,666,586,0 -4951.09,37,36,40,40,0,673,674,587,0 -4951.66,37,36,40,40,0,666,665,591,0 -4952.22,36,36,39,40,0,676,676,589,0 -4952.79,37,37,40,40,0,670,669,593,0 -4953.35,37,36,40,40,0,678,672,592,0 -4953.92,37,37,39,40,0,669,674,587,0 -4954.48,38,36,39,40,0,667,666,592,0 -4955.06,37,37,39,40,0,674,676,587,0 -4955.62,38,36,40,40,0,668,669,593,0 -4956.19,38,36,39,40,0,667,666,588,0 -4956.76,37,36,40,40,0,667,672,592,0 -4957.32,37,35,40,40,0,666,672,594,0 -4957.88,37,36,40,40,0,674,674,590,0 -4958.45,37,37,40,40,0,669,664,586,0 -4959.01,38,36,39,40,0,674,667,587,0 -4959.58,38,37,40,40,0,673,673,593,0 -4960.15,37,36,40,41,0,669,674,587,0 -4960.72,37,36,41,40,0,674,675,592,0 -4961.29,38,36,39,40,0,671,673,588,0 -4961.85,36,36,40,40,0,670,665,594,0 -4962.42,37,36,39,41,0,669,667,589,0 -4962.98,37,36,39,38,0,669,674,593,0 -4963.55,37,37,39,40,0,669,667,591,0 -4964.11,38,35,41,41,0,671,669,594,0 -4964.68,37,36,40,39,0,674,668,588,0 -4965.24,37,36,40,40,0,675,673,593,0 -4965.81,38,36,39,40,0,671,676,594,0 -4966.38,38,37,39,40,0,674,669,586,0 -4966.94,36,36,40,40,0,675,674,594,0 -4967.51,37,36,40,39,0,666,666,589,0 -4968.07,37,35,40,41,0,669,674,592,0 -4968.64,37,36,40,40,0,669,668,587,0 -4969.21,39,36,38,40,0,667,674,595,0 -4969.77,37,36,40,40,0,665,675,586,0 -4970.33,38,37,40,40,0,669,669,588,0 -4970.90,38,36,40,40,0,668,666,588,0 -4971.46,37,36,39,40,0,668,667,593,0 -4972.03,37,37,39,40,0,672,671,594,0 -4972.59,37,36,40,40,0,674,669,594,0 -4973.16,38,37,39,40,0,668,669,594,0 -4973.72,37,37,39,40,0,677,671,590,0 -4974.28,38,37,40,40,0,673,673,588,0 -4974.85,37,36,39,42,0,675,675,592,0 -4975.41,37,37,40,40,0,671,670,591,0 -4975.98,37,36,39,40,0,669,667,589,0 -4976.54,37,36,38,40,0,667,672,587,0 -4977.10,37,37,41,40,0,671,675,587,0 -4977.67,37,35,38,41,0,671,668,593,0 -4978.23,36,36,40,40,0,676,673,592,0 -4978.80,38,36,40,39,0,667,677,588,0 -4979.36,38,37,40,40,0,672,673,587,0 -4979.92,38,36,40,41,0,669,670,588,0 -4980.49,37,35,40,40,0,670,669,593,0 -4981.05,37,37,39,40,0,671,672,593,0 -4981.62,37,37,40,40,0,669,672,589,0 -4982.19,38,36,39,40,0,669,673,591,0 -4982.76,38,36,40,39,0,666,674,593,0 -4983.33,39,36,39,39,0,671,671,593,0 -4983.89,37,37,40,40,0,669,668,593,0 -4984.46,37,36,39,40,0,669,668,592,0 -4985.03,37,37,40,40,0,670,664,592,0 -4985.60,37,36,39,39,0,676,673,589,0 -4986.17,38,37,40,40,0,668,668,586,0 -4986.73,37,36,38,39,0,666,671,591,0 -4987.30,37,36,39,40,0,673,666,587,0 -4987.86,38,36,40,40,0,667,667,587,0 -4988.43,37,36,38,40,0,668,676,592,0 -4989.00,37,37,39,40,0,675,673,589,0 -4989.57,36,37,39,40,0,674,667,593,0 -4990.13,37,36,40,40,0,661,674,594,0 -4990.70,38,37,39,39,0,670,675,590,0 -4991.26,38,36,39,40,0,669,668,594,0 -4991.83,37,36,40,39,0,668,668,587,0 -4992.39,37,37,39,41,0,668,672,587,0 -4992.96,38,37,40,40,0,676,665,594,0 -4993.52,36,36,39,41,0,669,671,594,0 -4994.08,38,37,39,40,0,676,666,588,0 -4994.65,37,36,39,40,0,668,666,591,0 -4995.21,38,35,39,40,0,674,665,590,0 -4995.78,38,36,40,40,0,670,667,589,0 -4996.35,37,36,40,41,0,666,672,588,0 -4996.92,38,36,40,40,0,676,673,589,0 -4997.48,38,35,38,39,0,675,666,589,0 -4998.05,38,36,40,41,0,672,670,588,0 -4998.61,37,37,40,40,0,676,677,593,0 -4999.18,37,38,40,40,0,669,671,593,0 -4999.75,38,36,38,40,0,673,667,586,0 -5000.31,36,36,39,41,0,668,666,590,0 -5000.88,37,35,40,40,0,669,673,584,0 -5001.45,38,36,40,40,0,666,666,594,0 -5002.02,37,36,40,40,0,666,672,589,0 -5002.59,37,36,38,40,0,667,666,587,0 -5003.15,37,36,40,39,0,675,672,586,0 -5003.72,37,36,38,41,0,677,670,590,0 -5004.29,38,36,39,40,0,674,669,594,0 -5004.86,37,36,40,41,0,675,674,592,0 diff --git a/scripts/bench/live_fire_drills.py b/scripts/bench/live_fire_drills.py index b6a200c..92f1601 100644 --- a/scripts/bench/live_fire_drills.py +++ b/scripts/bench/live_fire_drills.py @@ -28,8 +28,9 @@ Drills (pass a name): (default 300) on scrap, sampled like `witness`. Prints the per-channel peak delta over the ambient baseline and the engine's own "run telemetry" line is the record. Run it - >= 3 times on representative material; the highest peak - delta sizes cool_fire_ir_delta. + >= 3 times on representative material; the highest peaks + are what the engine's cool_fire_q1/q2 alert and critical + thresholds (absolute counts) must sit above. ircut [S] [F] e.g. ircut 1000 300 pthresh Laser power-threshold ladder: one line per power level on scrap, climbing from 2 % to 30 % of full, at constant power @@ -196,8 +197,29 @@ def panel_token(): except OSError: return '' -# Ambient lid-IR baseline (2026-08-14, lid closed, idle): per-channel means. -IR_BASELINE = [37.3, 36.3, 39.5, 40.0] + +def sample_ir_baseline(seconds=3.0): + """Per-channel lid-IR means at idle, lid closed, taken before the job: + the baseline this run is judged against (never a stored one).""" + acc = [0.0, 0.0, 0.0, 0.0] + n = 0 + t_end = time.time() + seconds + while time.time() < t_end: + s = sample_forgectrl() + if s and s['ir'] and len(s['ir']) == 4: + for i in range(4): + acc[i] += s['ir'][i] + n += 1 + time.sleep(0.25) + if not n: + return None + return [round(a / n, 1) for a in acc] + + +def ir_delta(peak, base): + if base is None: + return ['n/a'] * 4 + return [round(peak[i] - base[i], 1) for i in range(4)] def get_json(path): @@ -394,6 +416,7 @@ def drill_witness(g): 'G90', 'M2', # program end: X-3 job-based disarm trigger ] + ir_base = sample_ir_baseline() samples = run_and_sample(g, job) # Analysis. emis = [s['emission'] for s in samples if s['emission'] is not None] @@ -415,8 +438,7 @@ def drill_witness(g): print('pgood_samples during job: peak=%s (>=128 = power-good)' % (max(pgood_vals) if pgood_vals else '-')) print('lid_ir peak=%s vs baseline=%s delta=%s' - % (ir_peak, IR_BASELINE, - [round(ir_peak[i] - IR_BASELINE[i], 1) for i in range(4)])) + % (ir_peak, ir_base, ir_delta(ir_peak, ir_base))) print('hv_current range: %s..%s' % (min(hv_vals) if hv_vals else '-', max(hv_vals) if hv_vals else '-')) print('armed observed during job: %s' % armed_seen) @@ -436,10 +458,11 @@ def drill_witness(g): % (round(disarm_dt, 1) if disarm_dt is not None else '>75 (REVIEW)')) ok = peak_emis > 0 and end_emis == 0 print('WITNESS emission %s' % ('PASS' if ok else 'REVIEW - see values above')) - # Emit the recommended cool_fire_ir_delta floor. - worst = max(ir_peak[i] - IR_BASELINE[i] for i in range(4)) - print('suggest cool_fire_ir_delta >= max(15, %.0f) once several jobs ' - 'confirm the peak delta' % (2 * worst if worst > 0 else 15)) + # The engine's fire watch trips on absolute lid-IR counts, per quartile + # pair (cool_fire_q1_alert / _critical, cool_fire_q2_alert / _critical): + # the peaks above are what those thresholds must clear on a clean job. + print('fire-watch thresholds must sit above these peaks: %s ' + '(cool_fire_q1_* for channels 1-2, cool_fire_q2_* for 3-4)' % ir_peak) return samples @@ -502,6 +525,7 @@ def drill_ircut(g): 'G1 X-30 F%d' % feed, 'G1 Y-30 F%d' % feed, 'M5', 'G90', 'M2', ] + ir_base = sample_ir_baseline() samples = run_and_sample(g, job, overall_timeout=400) ir_peak = [0, 0, 0, 0] ir_min = [10 ** 6] * 4 @@ -522,14 +546,13 @@ def drill_ircut(g): print('\n--- results ---') print('samples: %d emission peak=%s fire_watch states=%s' % (len(samples), max(emis) if emis else '-', sorted(fw))) - delta = [round(ir_peak[i] - IR_BASELINE[i], 1) for i in range(4)] + delta = ir_delta(ir_peak, ir_base) print('lid_ir min=%s peak=%s baseline=%s peak delta=%s' - % (ir_min, ir_peak, IR_BASELINE, delta)) + % (ir_min, ir_peak, ir_base, delta)) print('hv_current range: %s..%s' % (min(hv_vals) if hv_vals else '-', max(hv_vals) if hv_vals else '-')) - worst = max(delta) - print('worst peak delta this job: %s counts -> cool_fire_ir_delta must sit ' - 'above the worst across ALL jobs (>= 2x it, never < 15)' % worst) + print('worst peaks this job: %s counts -> the cool_fire_q1/q2 alert ' + 'thresholds must sit above the worst across ALL jobs' % ir_peak) print('the engine logged its own "run telemetry: lid IR ..." line for this job') return samples @@ -961,20 +984,7 @@ def _linfit(xs, ys): return a, b, r2 -def _degc(raw): - """The factory coolant conversion when gfbench is importable (on the - board, or with GF_HOST set), else None and the raw count is quoted. - The helper lives beside this file in the repo and under the bench - directory on the dev image; a copy staged elsewhere still finds it.""" - for d in (os.path.dirname(os.path.abspath(__file__)), - '/usr/share/forgetest/bench'): - if d not in sys.path: - sys.path.append(d) - try: - from gfbench import degc - except (ImportError, SystemExit): - return None - return degc(raw) +from gfbench import degc as _degc def pcurve_levels(g, pcts): diff --git a/scripts/bench/offset_probe.py b/scripts/bench/offset_probe.py index 4cfb622..3e4ce36 100644 --- a/scripts/bench/offset_probe.py +++ b/scripts/bench/offset_probe.py @@ -43,11 +43,7 @@ ADC_F = 1024.0 * 1.3 RINF = 10000.0 * math.exp(-3380.0 / 298.15) -def degc(raw): - if raw is None or raw <= 0 or raw >= ADC_F: - return float('nan') - r = 10000.0 / (ADC_F / raw - 1.0) - return 3380.0 / math.log(r / RINF) - 273.15 +from gfbench import degc def rd(path): diff --git a/scripts/bench/temp_calibration.json b/scripts/bench/temp_calibration.json deleted file mode 100644 index c1c1bbf..0000000 --- a/scripts/bench/temp_calibration.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "points": [ - { - "measured_c": 25.67, - "raw1": 672.4, - "raw2": 672.6, - "note": "IR on reservoir+hoses 78.2F, ambient 78.0F, idle 1h, pump on, heater off", - "when": "2026-08-02 18:13:41" - } - ] -} \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh index 736500f..d29dd70 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -10,7 +10,7 @@ # sign, checksums, stage, publish cmd # (the acceptance gate reads # releases/v/acceptance.json) -# release.sh --dev build + pack a dev-signed .fw for +# release.sh --dev build + pack a dev-signed .fw of the dev image for # the GUI upload path; no staging # # Environment: @@ -89,6 +89,11 @@ if [ "$MODE" = "dev" ]; then KEY="${FORGEFIRM_DEV_KEY:?set FORGEFIRM_DEV_KEY to the dev signing key}" build_images resolve_ext4 + # The dev archive carries the dev image (forgetest, the bench tools), not + # the release rootfs: what the panel's upload path installs on the bench + # is what the bench runs. + EXT4="${EXT4/forgefirm-image-glowforge/forgefirm-image-dev-glowforge}" + [ -f "$EXT4" ] || die "dev rootfs not found: $EXT4" check_size REL=$(sed -n 's/^FORGEFIRM_RELEASE ?= "\(.*\)"/\1/p' "$IMAGE_BB") DEVVER="v${REL}-dev-$(date +%Y%m%d%H%M%S)"