From 6d80fea9a6f825701085398f5b7d046fc8167f6c Mon Sep 17 00:00:00 2001 From: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:07:33 -0500 Subject: [PATCH] docs(bench): add the 2026-11-01 record Records #76, #77 and #83 on the gen2 Nano at a251158: all pass, and F4's over-temperature latch shows amber. Adds the #81 bisect: removing only main.cpp's masked windows, or slowing the loop, takes cut-short GET_STATE replies from 187 in 300 s to 0. Includes the bisect patch. --- .../2026-11-01-pid-tcsel-amber-bisect.md | 85 +++ docs/bench/README.md | 1 + .../i81-variants.patch | 186 +++++++ .../raw-session.txt | 505 ++++++++++++++++++ .../rlht_bench.py | 298 +++++++++++ 5 files changed, 1075 insertions(+) create mode 100644 docs/bench/2026-11-01-pid-tcsel-amber-bisect.md create mode 100644 docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/i81-variants.patch create mode 100644 docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/raw-session.txt create mode 100644 docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/rlht_bench.py diff --git a/docs/bench/2026-11-01-pid-tcsel-amber-bisect.md b/docs/bench/2026-11-01-pid-tcsel-amber-bisect.md new file mode 100644 index 0000000..f108c2d --- /dev/null +++ b/docs/bench/2026-11-01-pid-tcsel-amber-bisect.md @@ -0,0 +1,85 @@ +--- +title: "2026-11-01: PID timing, the tc-select lock, amber, and the #81 bisect" +parent: Bench Records +--- + +# 2026-11-01: PID timing, the tc-select lock, amber, and the #81 bisect + +**Status: complete.** R1a, R2, R3 and F4 passed. R4 found what cuts +GET_STATE replies short (#81): removing the loop's work, removing only +`main.cpp`'s masked windows, or slowing the loop each took the rate from 187 +bad replies in 300 s to 0. + +**Verifies:** on `main` at `a251158`, +[#76](https://github.com/feastorg/Slice_RLHT/pull/76) (compute the heater +PIDs once per thermocouple reading), +[#77](https://github.com/feastorg/Slice_RLHT/pull/77) (refuse a thermocouple +select change under a running heater) and +[#83](https://github.com/feastorg/Slice_RLHT/pull/83) (amber on a watchdog +trip or heater fault), with CRUMBS 0.17.0, bread-crumbs-contracts 0.11.1 and +bread-slice-core 0.2.0. +**By:** @CameronBrooks11 + +## Setup + +| | | +| --- | --- | +| Firmware | `a251158`, env `gen2_nano` (24038 B flash, 922 B RAM, `firmware.hex` sha256 `c926b0b35d60...`). CRUMBS 0.17.0, bread-crumbs-contracts 0.11.1, bread-slice-core 0.2.0. F4: the same commit built with `PLATFORMIO_BUILD_FLAGS="-DRLHT_OVERTEMP_C=30 -DRLHT_SETPOINT_MAX_C=25"`. R4: the same commit with [`i81-variants.patch`](assets/2026-11-01-pid-tcsel-amber-bisect/i81-variants.patch) applied and one flag per step (table below); then `main` reflashed | +| Board | gen2 Nano (ATmega328P) | +| Controller | Raspberry Pi 4 Model B, I2C at 50 kHz, address 19, CRUMBS Python binding 0.16.2 and contracts 0.11.0, via this session's [`rlht_bench.py`](assets/2026-11-01-pid-tcsel-amber-bisect/rlht_bench.py) (adds `tcsel` and `ontimes`) and the 2026-10-31 record's [`replylog.py`](assets/2026-10-31-refusal-latch-bus/replylog.py) and [`f4log.py`](assets/2026-10-31-refusal-latch-bus/f4log.py) | +| Power | backplane (I2C, 12 V and the e-stop line), with Slice_DCMT's gen2 Nano Every on the bus at address 21. The e-stop line is shared: the jumper counts on both boards | +| Outputs | an LED on each relay output; two MAX6675 thermocouples, not on a heater, warmed by hand in R2 and F4 | + +## Steps and Results + +| Step | Expected | Observed | Result | +| --- | --- | --- | --- | +| B0 baseline: `version`, `caps`, `state`, `wdog`, `faults` | CRUMBS 1700, module 1.0.0; caps `0101ff7f0000`; mode 0, relays off; watchdog disarmed; no faults | as expected; 21.5 C on both | pass | +| R1a mode 1, duty 50 50, `wdl 1000 1`, `watch 3`; jumper on, off; `wd 0`, `clear`, `duty 0 0` (#83) | relays blink, then off about 1 s in; LED amber; red with the jumper; amber after release; green after the clear | relays on at 11:09:20.04, off with `on=0,0` by 11:09:20.50; `wdog 01e8030101`; LED amber, red, amber; after `clear` `wdog 0000000001`, LED green, `duty 0 0` applied | pass | +| R2 `pid 10 0 25.5`, mode 0, `sp 40 0`, `ontimes 60`; TC1 warmed by hand from about t=20 s to t=40 s (#76) | on1 changes once per reading; derivative about 21 ms per 0.25 C step, not about 64 | steady 24.0 C: 160 ms; a step to 23.8: 183 ms (P alone 162, +21); a step to 24.3: 136 ms (P alone 157, -21). 600 polls, 0 failed, 177 on1 changes, median gap 0.30 s; 1 pair on consecutive polls, from an all-0xFF reply at 11:11:23.122 read as state (#81). TC1 26.0 C at 23.3 s, 37.0 C by 40 s | pass | +| R3 `tcsel` with heater 1 at a 40 C setpoint, then at setpoint 0, then in open loop (#77) | refused reason 6 while heating; applied otherwise | heating (`on=16`): `tcsel 2 2` status 2 reason 6, `tc=1,2`; `tcsel 1 2` applied; `sp 0 0`, `tcsel 2 2` applied, `tc=2,2`; mode 1, `tcsel 2 2` applied; back to `tc=1,2`, mode 0 | pass | +| F4 test build: `sp 26 0`, `sp 25 0`; mode 1, `duty 100 0`, `f4log.py 75` with TC1 warmed by hand; jumper on, off; cool below 30 C; `fclear` (#71, #83) | `sp 26` refused reason 4, `sp 25` applied; latch after three readings over 30.0 C: relay off, fault 1, LED amber; `duty 0 0` refused reason 8; red with the jumper, amber after; `fclear` clears, LED green | `sp 26` status 2 reason 4, `sp 25` applied. 30.3 C at 11:53:17.123, 30.8 C at 17.358; by 17.711 `relay1=0 fault=1 on1=0 flags=0x08`, held to the end at up to 33.8 C; `faults 0x01 H1_OVERTEMP`; `duty 0 0` reason 8; LED amber, red, amber (twice). Cleared at 27.3 C: `faults 0x00 none`, `fault=0` | pass | +| R4 #81 bisect: one flash and one 300 s `replylog.py 19 1 300` per build (below) | see below | see below | cause found | +| Final: `main` reflashed; `caps`, `faults`, `sp 26 0`; a closing 300 s capture | caps `0101ff7f0000`; `sp 26` applied (the 30 C test limits gone); the fault back at C0's rate | `firmware.hex` sha256 `c926b0b35d60...`; caps `0101ff7f0000`, `faults 0x00 none`, `sp 26` applied; 175 of 2552 bad | pass | + +### R4: GET_STATE replies per 300 s + +Every variant flag also forces both relays off. With no flag the patched tree +builds byte-identical to `main` (`firmware.hex` sha256 `c926b0b35d60...`). + +| Build | Flag | What it removes | Flash / RAM (B) | Polls | Failed | All-0xFF passed CRC | Bad | +| --- | --- | --- | --- | --- | --- | --- | --- | +| C0 | none | nothing | 24038 / 922 | 2552 | 174 | 13 | 187 | +| V-IDLE | `-DI81_IDLE_LOOP` | all of `loop()` but the watchdog reset, e-stop poll and status LED; fixed temperatures | 13022 / 798 | 2553 | 0 | 0 | 0 | +| V-NOMASK | `-DI81_NO_LOOP_MASK` | every `noInterrupts()` window in `main.cpp`, nothing else | 23778 / 922 | 2553 | 0 | 0 | 0 | +| V-PAD | `-DI81_LOOP_PAD_US=300` | nothing: 300 us with interrupts on is added to each loop pass | 23836 / 922 | 2553 | 0 | 0 | 0 | +| main, closing | none | nothing | 24038 / 922 | 2552 | 171 | 4 | 175 | + +## Raw Output + +[`raw-session.txt`](assets/2026-11-01-pid-tcsel-amber-bisect/raw-session.txt): +every RLHT command and its output. The R4 captures were cut to their last +lines with `tail`; their counts are complete. + +## Not Checked + +- R4's V-TC, V-NOSERIAL, V-PREBUILT, C-CLAMP and the July control: not + needed once V-NOMASK and V-PAD came back clean. +- Which of `main.cpp`'s masked windows dominates. +- The LED after the F4 clear. +- Heaters: the relays drive LEDs only. + +## Notes + +- **#81.** V-NOMASK keeps the whole loop and removes only the masked + windows; V-PAD keeps every window and makes them rarer. Both are clean, so + the cut-short replies come from how often the loop masks interrupts, not + from one long window or the reply builder. A late TWI interrupt fits the + one-bit lag seen in every bad frame. C0's 187 in 300 s (7.3 %) is above + 2026-10-31's 2 to 4 % on `d0e6ced`. +- **CRC passes.** Of the cut-short replies, the share whose 0xFF tail + passed the CRC as an all-0xFF payload was 13 of 187 in C0, 4 of 175 in the + closing capture, and 4 of 9 bad polls out of 186 in F4. That is far above + the 1 in 256 that feastorg/CRUMBS#130 assumes. +- **e-stop line.** `estop_count` rose with jumper presses made on Slice_DCMT: + the backplane carries one e-stop line to both boards. diff --git a/docs/bench/README.md b/docs/bench/README.md index 32a0f42..d018b16 100644 --- a/docs/bench/README.md +++ b/docs/bench/README.md @@ -34,3 +34,4 @@ in bread-infra. | 2026-10-24 | [bread-slice-core, serial burst and strict parsing](2026-10-24-core-and-serial.md) | `03089e1` | gen2 Nano, gen1 Nano Every | pass | | 2026-10-25 | [Setpoint limits, heater faults, the lease and wide gains](2026-10-25-faults-lease-pid.md) | `236a0c2` | gen2 Nano | pass; open-thermocouple fault not reached, F3 skipped, P2 not checked; two spurious over-temperature latches | | 2026-10-31 | [Opcode refusal, the fault latches and cut-short replies](2026-10-31-refusal-latch-bus.md) | `d0e6ced` | gen2 Nano | pass; 2 to 4 % of GET_STATE replies cut short (#81) | +| 2026-11-01 | [PID timing, the tc-select lock, amber, and the #81 bisect](2026-11-01-pid-tcsel-amber-bisect.md) | `a251158` | gen2 Nano | pass; #81 traced to the loop's masked windows | diff --git a/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/i81-variants.patch b/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/i81-variants.patch new file mode 100644 index 0000000..2478cbf --- /dev/null +++ b/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/i81-variants.patch @@ -0,0 +1,186 @@ +diff --git a/firmware/include/globals.h b/firmware/include/globals.h +index a6db139..b5b67fb 100644 +--- a/firmware/include/globals.h ++++ b/firmware/include/globals.h +@@ -116,5 +116,9 @@ void reply_get_state(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_ + void reply_get_caps(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_data); + void reply_get_watchdog(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_data); + void reply_get_faults(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_data); ++#ifdef I81_PREBUILT_REPLY ++// feastorg/Slice_RLHT#81 variant: loop() builds the GET_STATE payload. ++void rlht_stage_state(); ++#endif + + #endif // GLOBALS_H +diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp +index e6ac1ce..c8af604 100644 +--- a/firmware/src/main.cpp ++++ b/firmware/src/main.cpp +@@ -18,6 +18,22 @@ + #include + #endif + ++// feastorg/Slice_RLHT#81 bench variants, each gated by a build flag. With ++// none set this file builds as main. Any variant also holds both relays off ++// in actuateRelay (I81_RELAYS_OFF), whatever a command asks. ++#if defined(I81_IDLE_LOOP) || defined(I81_NO_LOOP_MASK) || defined(I81_LOOP_PAD_US) || \ ++ defined(I81_TC_STUB) || defined(I81_NO_SERIAL) || defined(I81_PREBUILT_REPLY) ++#define I81_RELAYS_OFF 1 ++#endif ++#ifdef I81_NO_LOOP_MASK ++// Drops every masked window in this file. Safe only while no command handler ++// writes slice: GET_STATE polling alone, whose SET_REPLY CRUMBS consumes. ++#undef noInterrupts ++#undef interrupts ++#define noInterrupts() do { } while (0) ++#define interrupts() do { } while (0) ++#endif ++ + static crumbs_context_t ctx; + #if RLHT_HAS_STATUS_LED + #if LED_PIN != 5 +@@ -261,12 +277,22 @@ void loop() + wdt_reset(); + processEStop(estopInput.poll(millis())); + updateStatusLed(); ++#ifndef I81_IDLE_LOOP + watchdogLogic(); + if (measureThermocouples()) + checkHeaterFaults(); + relayControlLogic(); ++#ifndef I81_NO_SERIAL + serialCommands(); ++#endif + printSerialOutput(); ++#endif ++#ifdef I81_PREBUILT_REPLY ++ rlht_stage_state(); ++#endif ++#ifdef I81_LOOP_PAD_US ++ delayMicroseconds(I81_LOOP_PAD_US); ++#endif + } + + // The commands that keep a commands-only lease alive: the ones that start or +@@ -340,7 +366,9 @@ void setupSlice() + { + int rc; + ++#ifndef I81_NO_SERIAL + Serial.begin(115200); ++#endif + + crumbs_arduino_init_peripheral(&ctx, I2C_ADR); + // CRUMBS rejects a frame typed for another family before any handler +@@ -465,6 +493,13 @@ void setupRLHT() + apply_tunings_if_needed(slice.relayHeater1.Kp, slice.relayHeater1.Ki, slice.relayHeater1.Kd, + slice.relayHeater2.Kp, slice.relayHeater2.Ki, slice.relayHeater2.Kd); + ++#ifdef I81_IDLE_LOOP ++ // No thermocouple reads: fixed room temperatures, so GET_STATE's reply ++ // builder does the same work as with real readings. ++ slice.temperature1 = 23.5; ++ slice.temperature2 = 22.75; ++#endif ++ + timing.lastThermoRead = millis(); + timing.lastSerialPrint = millis(); + timing.relay1Start = millis(); +@@ -530,8 +565,15 @@ bool measureThermocouples() + // reads these from the TWI ISR, and an unmasked 4-byte store landing + // mid-reply reports a mix of the old and new value + // (feastorg/Slice_RLHT#25). ++#ifdef I81_TC_STUB ++ // No MAX6675 bit-banging: fixed room temperatures. The masked store ++ // below stays. ++ const double t1 = 23.5; ++ const double t2 = 22.75; ++#else + const double t1 = thermocouple1.readCelsius(); + const double t2 = thermocouple2.readCelsius(); ++#endif + noInterrupts(); + slice.temperature1 = t1; + slice.temperature2 = t2; +@@ -817,6 +859,10 @@ void actuateRelay(uint8_t relayPin, unsigned long &relayStart, unsigned long rel + { + unsigned long currentTime = millis(); + ++#ifdef I81_RELAYS_OFF ++ relayOnTime = 0; ++#endif ++ + if (relayPeriod == 0) + { + digitalWrite(relayPin, LOW); +diff --git a/firmware/src/rlht_handlers.cpp b/firmware/src/rlht_handlers.cpp +index f11a2ce..a265a99 100644 +--- a/firmware/src/rlht_handlers.cpp ++++ b/firmware/src/rlht_handlers.cpp +@@ -414,6 +414,43 @@ void reply_version(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_da + crumbs_build_version_reply(reply, RLHT_TYPE_ID, RLHT_MODULE_VER_MAJOR, RLHT_MODULE_VER_MINOR, RLHT_MODULE_VER_PATCH); + } + ++#ifdef I81_PREBUILT_REPLY ++// feastorg/Slice_RLHT#81 variant: loop() fills the payload into the buffer ++// the ISR is not reading, then flips the index (one byte, so atomic). The ISR ++// only packs it, without the floating-point work below. ++static rlht_state_t stagedState[2]; ++static uint8_t stagedCount[2]; ++static volatile uint8_t stagedIdx = 0; ++static void fill_state(rlht_state_t &s); ++ ++void rlht_stage_state() ++{ ++ const uint8_t next = stagedIdx ^ 1; ++ fill_state(stagedState[next]); ++ stagedCount[next] = estopInput.count(); ++ stagedIdx = next; ++} ++ ++void reply_get_state(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_data) ++{ ++ (void)ctx; ++ (void)user_data; ++ livenessStamp(); ++ const uint8_t i = stagedIdx; ++ crumbs_msg_init(reply, RLHT_TYPE_ID, RLHT_OP_GET_STATE); ++ (void)rlht_state_pack(reply, &stagedState[i]); ++ (void)crumbs_msg_add_u8(reply, stagedCount[i]); ++} ++ ++static void fill_state(rlht_state_t &s) ++{ ++ uint8_t flags = 0; ++ uint8_t tc_pack = 0; ++ uint16_t on1 = 0; ++ uint16_t on2 = 0; ++ double on1d = 0.0; ++ double on2d = 0.0; ++#else + void reply_get_state(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_data) + { + rlht_state_t s; +@@ -428,6 +465,7 @@ void reply_get_state(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_ + double on2d = 0.0; + (void)ctx; + (void)user_data; ++#endif + + if (estopInput.asserted()) + flags |= RLHT_FLAG_ESTOP; +@@ -472,12 +510,14 @@ void reply_get_state(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_ + s.period1_ms = (uint16_t)slice.relayHeater1.relayPeriod; + s.period2_ms = (uint16_t)slice.relayHeater2.relayPeriod; + s.tc_select = tc_pack; ++#ifndef I81_PREBUILT_REPLY + + crumbs_msg_init(reply, RLHT_TYPE_ID, RLHT_OP_GET_STATE); + (void)rlht_state_pack(reply, &s); + // RLHT_CAP_ESTOP_COUNT: append estop_count at RLHT_STATE_OFF_ESTOP_COUNT, + // making the reply 20 bytes (rlht_state_pack writes the first 19). + (void)crumbs_msg_add_u8(reply, estopInput.count()); ++#endif + } + + void reply_get_caps(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_data) diff --git a/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/raw-session.txt b/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/raw-session.txt new file mode 100644 index 0000000..5c228b4 --- /dev/null +++ b/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/raw-session.txt @@ -0,0 +1,505 @@ +$ ssh 'cd rlht-bench && for c in version caps state wdog faults; do ../dcmt-bench/venv/bin/python rlht_bench.py $c 2>&1 | tail -1; done' +10:19:02.877 Version(crumbs_version=1700, major=1, minor=0, patch=0) +10:19:03.198 caps 0101ff7f0000 +10:19:03.501 state mode=0 estop=0 fault=0 relays=0,0 t=21.5,21.5 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=0 +10:19:03.805 wdog [armed][timeout_ms][tripped][trips] 0000000000 +10:19:04.107 faults 0x00 none + +$ ssh 'cd dcmt-bench && venv/bin/python dcmt_bench.py state 2>&1 | tail -1; cd ../rlht-bench && ../dcmt-bench/venv/bin/python rlht_bench.py state 2>&1 | tail -1; i2cdetect -y 1 2>&1 | tail -8' +TimeoutError: [Errno 110] Connection timed out +TimeoutError: [Errno 110] Connection timed out +bash: line 1: i2cdetect: command not found + +$ ssh 'pinctrl get 2,3; cd dcmt-bench && for c in state faults; do venv/bin/python dcmt_bench.py $c 2>&1 | tail -1; done; cd ../rlht-bench && ../dcmt-bench/venv/bin/python rlht_bench.py state 2>&1 | tail -1' + 2: a0 pu | hi // GPIO2 = SDA1 + 3: a0 pu | hi // GPIO3 = SCL1 +10:52:31.809 state mode=0 m1_pwm=0 m2_pwm=0 sp=-32768,-32768 pos=0,0 spd=-32768,-32768 brakes=0b00 estop=0 estop_count=0 +10:52:32.115 faults 0x00 none +10:52:32.418 state mode=0 estop=0 fault=0 relays=0,0 t=23.3,22.0 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=1 + +$ ssh 'cd rlht-bench && B="../dcmt-bench/venv/bin/python -u rlht_bench.py" && $B mode 1 && $B duty 50 50 && $B wdl 1000 1 && $B watch 3; $B wdog; $B state' +11:09:18.821 sent 0x01 01 -> ack Ack(counter=1, opcode=1, status=1, reason=0) +11:09:19.137 sent 0x06 3232 -> ack Ack(counter=2, opcode=6, status=1, reason=0) +11:09:19.469 sent 0x7E e80301 -> ack Ack(counter=3, opcode=126, status=1, reason=0) +11:09:19.791 state mode=1 estop=0 fault=0 relays=0,0 t=23.3,24.0 sp=0.0,0.0 on=500,500 period=1000,1000 tc=1,2 estop_count=2 +11:09:20.044 state mode=1 estop=0 fault=0 relays=1,1 t=24.0,24.0 sp=0.0,0.0 on=500,500 period=1000,1000 tc=1,2 estop_count=2 +11:09:20.496 state mode=1 estop=0 fault=0 relays=0,0 t=23.3,23.8 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=2 +11:09:22.974 wdog [armed][timeout_ms][tripped][trips] 01e8030101 +11:09:22.991 faults 0x00 none +11:09:23.296 wdog [armed][timeout_ms][tripped][trips] 01e8030101 +11:09:23.648 state mode=1 estop=0 fault=0 relays=0,0 t=24.0,24.3 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=2 + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python rlht_bench.py state; cd ../dcmt-bench && venv/bin/python dcmt_bench.py state' +11:09:47.043 state mode=1 estop=0 fault=0 relays=0,0 t=23.8,23.8 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 +11:09:47.350 state mode=0 m1_pwm=0 m2_pwm=0 sp=-32768,-32768 pos=1780,15718 spd=-32768,-32768 brakes=0b11 estop=0 estop_count=2 + +$ ssh 'cd rlht-bench && B="../dcmt-bench/venv/bin/python -u rlht_bench.py" && $B state && $B wd 0 && $B clear && $B wdog && $B duty 0 0 && $B state' +11:10:06.818 state mode=1 estop=0 fault=0 relays=0,0 t=24.0,24.3 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 +11:10:07.135 sent 0x7E 0000 -> ack Ack(counter=4, opcode=126, status=1, reason=0) +11:10:07.452 sent 0x7C -> ack Ack(counter=5, opcode=124, status=1, reason=0) +11:10:07.759 wdog [armed][timeout_ms][tripped][trips] 0000000001 +11:10:08.076 sent 0x06 0000 -> ack Ack(counter=6, opcode=6, status=1, reason=0) +11:10:08.381 state mode=1 estop=0 fault=0 relays=0,0 t=23.8,23.3 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 + +$ ssh 'cd rlht-bench && B="../dcmt-bench/venv/bin/python -u rlht_bench.py" && $B pid 10 0 25.5 && $B mode 0 && $B sp 40 0 && $B ontimes 60; $B sp 0 0; $B pid 1 0 0; $B state' +11:11:07.367 sent 0x03 102700009c63102700009c63 -> ack Ack(counter=7, opcode=3, status=1, reason=0) +11:11:07.700 sent 0x01 00 -> ack Ack(counter=8, opcode=1, status=1, reason=0) +11:11:08.016 sent 0x02 90010000 -> ack Ack(counter=9, opcode=2, status=1, reason=0) +11:11:08.304 ontimes 60 s +stamp t on1 on2 t1 sp1 sp2 relays fault +11:11:08.322 0.02 117 0 24.0 40.0 0.0 0,0 0 +11:11:08.439 0.13 160 0 24.0 40.0 0.0 0,0 0 +11:11:08.821 0.52 136 0 24.3 40.0 0.0 0,0 0 +11:11:09.121 0.82 181 0 24.0 40.0 0.0 0,0 0 +11:11:09.421 1.12 160 0 24.0 40.0 0.0 0,0 0 +11:11:09.721 1.42 136 0 24.3 40.0 0.0 0,0 0 +11:11:10.021 1.72 205 0 23.8 40.0 0.0 1,0 0 +11:11:10.321 2.02 162 0 23.8 40.0 0.0 0,0 0 +11:11:10.621 2.32 138 0 24.0 40.0 0.0 0,0 0 +11:11:10.939 2.63 183 0 23.8 40.0 0.0 1,0 0 +11:11:11.221 2.92 138 0 24.0 40.0 0.0 0,0 0 +11:11:11.521 3.22 160 0 24.0 40.0 0.0 0,0 0 +11:11:11.821 3.52 136 0 24.3 40.0 0.0 0,0 0 +11:11:12.121 3.82 157 0 24.3 40.0 0.0 0,0 0 +11:11:12.339 4.04 228 0 23.5 40.0 0.0 0,0 0 +11:11:12.722 4.42 93 0 24.3 40.0 0.0 0,0 0 +11:11:13.022 4.72 157 0 24.3 40.0 0.0 1,0 0 +11:11:13.322 5.02 205 0 23.8 40.0 0.0 0,0 0 +11:11:13.622 5.32 186 0 23.5 40.0 0.0 0,0 0 +11:11:13.922 5.62 141 0 23.8 40.0 0.0 0,0 0 +11:11:14.222 5.92 138 0 24.0 40.0 0.0 0,0 0 +11:11:14.522 6.22 183 0 23.8 40.0 0.0 0,0 0 +11:11:14.839 6.54 162 0 23.8 40.0 0.0 0,0 0 +11:11:15.339 7.04 138 0 24.0 40.0 0.0 0,0 0 +11:11:15.722 7.42 183 0 23.8 40.0 0.0 0,0 0 +11:11:16.022 7.72 138 0 24.0 40.0 0.0 1,0 0 +11:11:16.321 8.02 183 0 23.8 40.0 0.0 0,0 0 +11:11:16.621 8.32 162 0 23.8 40.0 0.0 0,0 0 +11:11:16.921 8.62 138 0 24.0 40.0 0.0 0,0 0 +11:11:17.221 8.92 183 0 23.8 40.0 0.0 0,0 0 +11:11:17.521 9.22 138 0 24.0 40.0 0.0 0,0 0 +11:11:17.821 9.52 160 0 24.0 40.0 0.0 0,0 0 +11:11:18.456 10.15 183 0 23.8 40.0 0.0 0,0 0 +11:11:18.721 10.42 162 0 23.8 40.0 0.0 0,0 0 +11:11:19.021 10.72 186 0 23.5 40.0 0.0 1,0 0 +11:11:19.321 11.02 117 0 24.0 40.0 0.0 0,0 0 +11:11:19.539 11.23 136 0 24.3 40.0 0.0 0,0 0 +11:11:19.921 11.62 205 0 23.8 40.0 0.0 0,0 0 +11:11:20.221 11.92 162 0 23.8 40.0 0.0 0,0 0 +11:11:20.821 12.52 138 0 24.0 40.0 0.0 0,0 0 +11:11:21.121 12.82 160 0 24.0 40.0 0.0 0,0 0 +11:11:21.339 13.04 207 0 23.5 40.0 0.0 0,0 0 +11:11:21.722 13.42 165 0 23.5 40.0 0.0 0,0 0 +11:11:22.022 13.72 141 0 23.8 40.0 0.0 1,0 0 +11:11:22.322 14.02 186 0 23.5 40.0 0.0 0,0 0 +11:11:22.622 14.32 165 0 23.5 40.0 0.0 0,0 0 +11:11:22.922 14.62 117 0 24.0 40.0 0.0 0,0 0 +11:11:23.122 14.82 65535 65535 -0.1 -0.1 -0.1 1,1 1 +11:11:23.222 14.92 160 0 24.0 40.0 0.0 0,0 0 +11:11:23.522 15.22 183 0 23.8 40.0 0.0 0,0 0 +11:11:23.822 15.52 186 0 23.5 40.0 0.0 0,0 0 +11:11:24.122 15.82 141 0 23.8 40.0 0.0 0,0 0 +11:11:24.339 16.04 138 0 24.0 40.0 0.0 0,0 0 +11:11:24.722 16.42 183 0 23.8 40.0 0.0 0,0 0 +11:11:25.022 16.72 138 0 24.0 40.0 0.0 1,0 0 +11:11:25.322 17.02 160 0 24.0 40.0 0.0 0,0 0 +11:11:26.222 17.92 183 0 23.8 40.0 0.0 0,0 0 +11:11:26.439 18.14 162 0 23.8 40.0 0.0 0,0 0 +11:11:26.822 18.52 115 0 24.3 40.0 0.0 0,0 0 +11:11:27.122 18.82 228 0 23.5 40.0 0.0 1,0 0 +11:11:27.422 19.12 117 0 24.0 40.0 0.0 0,0 0 +11:11:27.639 19.34 160 0 24.0 40.0 0.0 0,0 0 +11:11:28.022 19.72 136 0 24.3 40.0 0.0 1,0 0 +11:11:28.322 20.02 157 0 24.3 40.0 0.0 0,0 0 +11:11:29.222 20.92 181 0 24.0 40.0 0.0 0,0 0 +11:11:29.522 21.22 160 0 24.0 40.0 0.0 0,0 0 +11:11:29.839 21.54 207 0 23.5 40.0 0.0 0,0 0 +11:11:30.122 21.82 165 0 23.5 40.0 0.0 0,0 0 +11:11:30.422 22.12 117 0 24.0 40.0 0.0 0,0 0 +11:11:30.722 22.42 183 0 23.8 40.0 0.0 0,0 0 +11:11:31.022 22.72 138 0 24.0 40.0 0.0 1,0 0 +11:11:31.322 23.02 160 0 24.0 40.0 0.0 0,0 0 +11:11:31.622 23.32 0 0 26.0 40.0 0.0 0,0 0 +11:11:31.922 23.62 0 0 27.8 40.0 0.0 0,0 0 +11:11:32.139 23.84 0 0 30.5 40.0 0.0 0,0 0 +11:11:32.522 24.22 0 0 31.5 40.0 0.0 0,0 0 +11:11:32.822 24.52 13 0 32.3 40.0 0.0 0,0 0 +11:11:33.122 24.82 30 0 32.8 40.0 0.0 0,0 0 +11:11:33.439 25.14 72 0 32.8 40.0 0.0 0,0 0 +11:11:33.722 25.42 0 0 33.8 40.0 0.0 0,0 0 +11:11:34.022 25.72 62 0 33.8 40.0 0.0 0,0 0 +11:11:34.322 26.02 110 0 33.3 40.0 0.0 0,0 0 +11:11:34.622 26.32 20 0 33.8 40.0 0.0 0,0 0 +11:11:34.922 26.62 0 0 34.5 40.0 0.0 0,0 0 +11:11:35.221 26.92 78 0 34.3 40.0 0.0 0,0 0 +11:11:35.521 27.22 81 0 34.0 40.0 0.0 0,0 0 +11:11:35.821 27.52 60 0 34.0 40.0 0.0 0,0 0 +11:11:36.339 28.03 12 0 34.5 40.0 0.0 0,0 0 +11:11:36.721 28.42 31 0 34.8 40.0 0.0 0,0 0 +11:11:37.021 28.72 52 0 34.8 40.0 0.0 0,0 0 +11:11:37.322 29.02 28 0 35.0 40.0 0.0 0,0 0 +11:11:37.621 29.32 50 0 35.0 40.0 0.0 0,0 0 +11:11:37.921 29.62 73 0 34.8 40.0 0.0 0,0 0 +11:11:38.221 29.92 28 0 35.0 40.0 0.0 0,0 0 +11:11:38.521 30.22 73 0 34.8 40.0 0.0 0,0 0 +11:11:38.821 30.52 28 0 35.0 40.0 0.0 0,0 0 +11:11:39.139 30.83 2 0 35.5 40.0 0.0 0,0 0 +11:11:39.421 31.12 116 0 34.8 40.0 0.0 0,0 0 +11:11:39.721 31.42 76 0 34.5 40.0 0.0 0,0 0 +11:11:40.022 31.72 7 0 35.0 40.0 0.0 0,0 0 +11:11:40.339 32.04 50 0 35.0 40.0 0.0 0,0 0 +11:11:40.622 32.32 26 0 35.3 40.0 0.0 0,0 0 +11:11:40.922 32.62 47 0 35.3 40.0 0.0 0,0 0 +11:11:41.222 32.92 71 0 35.0 40.0 0.0 0,0 0 +11:11:41.522 33.22 26 0 35.3 40.0 0.0 0,0 0 +11:11:41.822 33.52 47 0 35.3 40.0 0.0 0,0 0 +11:11:42.722 34.42 71 0 35.0 40.0 0.0 0,0 0 +11:11:43.022 34.72 50 0 35.0 40.0 0.0 0,0 0 +11:11:43.322 35.02 0 0 35.8 40.0 0.0 0,0 0 +11:11:43.622 35.32 42 0 35.8 40.0 0.0 0,0 0 +11:11:43.922 35.62 90 0 35.3 40.0 0.0 0,0 0 +11:11:44.222 35.92 47 0 35.3 40.0 0.0 0,0 0 +11:11:44.522 36.22 23 0 35.5 40.0 0.0 0,0 0 +11:11:44.739 36.44 21 0 35.8 40.0 0.0 0,0 0 +11:11:45.122 36.82 42 0 35.8 40.0 0.0 0,0 0 +11:11:45.422 37.12 18 0 36.0 40.0 0.0 0,0 0 +11:11:45.739 37.44 16 0 36.3 40.0 0.0 0,0 0 +11:11:46.022 37.72 37 0 36.3 40.0 0.0 0,0 0 +11:11:46.622 38.32 61 0 36.0 40.0 0.0 0,0 0 +11:11:46.922 38.62 16 0 36.3 40.0 0.0 0,0 0 +11:11:47.222 38.92 37 0 36.3 40.0 0.0 0,0 0 +11:11:47.439 39.14 13 0 36.5 40.0 0.0 0,0 0 +11:11:47.822 39.52 11 0 36.8 40.0 0.0 0,0 0 +11:11:48.122 39.82 8 0 37.0 40.0 0.0 0,0 0 +11:11:48.439 40.14 77 0 36.5 40.0 0.0 0,0 0 +11:11:48.722 40.42 0 0 37.0 40.0 0.0 0,0 0 +11:11:49.021 40.72 30 0 37.0 40.0 0.0 0,0 0 +11:11:49.921 41.62 53 0 36.8 40.0 0.0 0,0 0 +11:11:50.139 41.83 8 0 37.0 40.0 0.0 0,0 0 +11:11:50.521 42.22 30 0 37.0 40.0 0.0 0,0 0 +11:11:51.121 42.82 53 0 36.8 40.0 0.0 0,0 0 +11:11:51.421 43.12 8 0 37.0 40.0 0.0 0,0 0 +11:11:51.721 43.42 30 0 37.0 40.0 0.0 0,0 0 +11:11:52.921 44.62 6 0 37.3 40.0 0.0 0,0 0 +11:11:53.221 44.92 51 0 37.0 40.0 0.0 0,0 0 +11:11:53.521 45.22 6 0 37.3 40.0 0.0 0,0 0 +11:11:53.821 45.52 27 0 37.3 40.0 0.0 0,0 0 +11:11:54.122 45.82 51 0 37.0 40.0 0.0 0,0 0 +11:11:54.422 46.12 30 0 37.0 40.0 0.0 0,0 0 +11:11:54.739 46.44 6 0 37.3 40.0 0.0 0,0 0 +11:11:55.022 46.72 51 0 37.0 40.0 0.0 0,0 0 +11:11:55.322 47.02 6 0 37.3 40.0 0.0 0,0 0 +11:11:55.622 47.32 51 0 37.0 40.0 0.0 0,0 0 +11:11:55.922 47.62 30 0 37.0 40.0 0.0 0,0 0 +11:11:56.822 48.52 53 0 36.8 40.0 0.0 0,0 0 +11:11:57.122 48.82 32 0 36.8 40.0 0.0 0,0 0 +11:11:57.422 49.12 0 0 37.3 40.0 0.0 0,0 0 +11:11:57.639 49.34 27 0 37.3 40.0 0.0 0,0 0 +11:11:58.022 49.72 3 0 37.5 40.0 0.0 0,0 0 +11:11:58.322 50.02 72 0 37.0 40.0 0.0 0,0 0 +11:11:58.622 50.32 30 0 37.0 40.0 0.0 0,0 0 +11:11:58.922 50.62 6 0 37.3 40.0 0.0 0,0 0 +11:11:59.222 50.92 51 0 37.0 40.0 0.0 0,0 0 +11:11:59.439 51.14 6 0 37.3 40.0 0.0 0,0 0 +11:11:59.822 51.52 51 0 37.0 40.0 0.0 0,0 0 +11:12:00.122 51.82 0 0 37.5 40.0 0.0 0,0 0 +11:12:00.422 52.12 72 0 37.0 40.0 0.0 0,0 0 +11:12:00.722 52.42 0 0 37.5 40.0 0.0 0,0 0 +11:12:01.022 52.72 72 0 37.0 40.0 0.0 0,0 0 +11:12:01.322 53.02 0 0 37.5 40.0 0.0 0,0 0 +11:12:01.622 53.32 25 0 37.5 40.0 0.0 0,0 0 +11:12:01.922 53.62 72 0 37.0 40.0 0.0 0,0 0 +11:12:02.222 53.92 6 0 37.3 40.0 0.0 0,0 0 +11:12:02.522 54.22 3 0 37.5 40.0 0.0 0,0 0 +11:12:02.822 54.52 25 0 37.5 40.0 0.0 0,0 0 +11:12:03.422 55.12 48 0 37.3 40.0 0.0 0,0 0 +11:12:03.722 55.42 51 0 37.0 40.0 0.0 0,0 0 +11:12:04.022 55.72 0 0 37.5 40.0 0.0 0,0 0 +11:12:04.322 56.02 25 0 37.5 40.0 0.0 0,0 0 +11:12:04.922 56.62 48 0 37.3 40.0 0.0 0,0 0 +11:12:05.222 56.92 27 0 37.3 40.0 0.0 0,0 0 +11:12:05.539 57.24 51 0 37.0 40.0 0.0 0,0 0 +11:12:05.822 57.52 0 0 37.5 40.0 0.0 0,0 0 +11:12:06.122 57.82 25 0 37.5 40.0 0.0 0,0 0 +11:12:06.422 58.12 48 0 37.3 40.0 0.0 0,0 0 +11:12:06.722 58.42 51 0 37.0 40.0 0.0 0,0 0 +11:12:07.022 58.72 0 0 37.5 40.0 0.0 0,0 0 +11:12:07.322 59.02 1 0 37.8 40.0 0.0 0,0 0 +11:12:07.622 59.32 46 0 37.5 40.0 0.0 0,0 0 +11:12:07.839 59.54 25 0 37.5 40.0 0.0 0,0 0 +11:12:08.222 59.92 1 0 37.8 40.0 0.0 0,0 0 +11:12:08.304 summary: 600 polls, 0 failed, 130 t1 changes + on1: 177 changes, gaps min 0.10 s, median 0.30 s, 1 under 0.15 s (consecutive polls) + on2: 2 changes, gaps min 0.10 s, median 0.10 s, 1 under 0.15 s (consecutive polls) +11:12:08.662 sent 0x02 00000000 -> ack Ack(counter=10, opcode=2, status=1, reason=0) +11:12:08.981 sent 0x03 e80300000000e80300000000 -> ack Ack(counter=11, opcode=3, status=1, reason=0) +11:12:09.285 state mode=0 estop=0 fault=0 relays=0,0 t=37.3,24.0 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 + +$ ssh 'cd rlht-bench && B="../dcmt-bench/venv/bin/python -u rlht_bench.py" && $B mode 0 && $B sp 40 0 && $B tcsel 2 2 && $B state && $B tcsel 1 2 && $B sp 0 0 && $B tcsel 2 2 && $B state && $B tcsel 1 2 && $B mode 1 && $B tcsel 2 2 && $B state && $B tcsel 1 2 && $B mode 0 && $B state' +11:17:13.946 sent 0x01 00 -> ack Ack(counter=12, opcode=1, status=1, reason=0) +11:17:14.262 sent 0x02 90010000 -> ack Ack(counter=13, opcode=2, status=1, reason=0) +11:17:14.580 sent 0x05 0202 -> ack Ack(counter=14, opcode=5, status=2, reason=6) +11:17:14.886 state mode=0 estop=0 fault=0 relays=0,0 t=23.8,22.5 sp=40.0,0.0 on=16,0 period=1000,1000 tc=1,2 estop_count=3 +11:17:15.202 sent 0x05 0102 -> ack Ack(counter=15, opcode=5, status=1, reason=0) +11:17:15.518 sent 0x02 00000000 -> ack Ack(counter=16, opcode=2, status=1, reason=0) +11:17:15.835 sent 0x05 0202 -> ack Ack(counter=17, opcode=5, status=1, reason=0) +11:17:16.140 state mode=0 estop=0 fault=0 relays=0,0 t=24.0,22.0 sp=0.0,0.0 on=0,0 period=1000,1000 tc=2,2 estop_count=3 +11:17:16.456 sent 0x05 0102 -> ack Ack(counter=18, opcode=5, status=1, reason=0) +11:17:16.780 sent 0x01 01 -> ack Ack(counter=19, opcode=1, status=1, reason=0) +11:17:17.096 sent 0x05 0202 -> ack Ack(counter=20, opcode=5, status=1, reason=0) +11:17:17.400 state mode=1 estop=0 fault=0 relays=0,0 t=24.0,22.8 sp=0.0,0.0 on=0,0 period=1000,1000 tc=2,2 estop_count=3 +11:17:17.717 sent 0x05 0102 -> ack Ack(counter=21, opcode=5, status=1, reason=0) +11:17:18.033 sent 0x01 00 -> ack Ack(counter=22, opcode=1, status=1, reason=0) +11:17:18.337 state mode=0 estop=0 fault=0 relays=0,0 t=24.0,22.5 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 + +$ ssh 'cd rlht-bench && B="../dcmt-bench/venv/bin/python -u rlht_bench.py" && $B version && $B faults && $B mode 0 && $B sp 26 0 && $B sp 25 0 && $B sp 0 0 && $B mode 1 && $B duty 100 0 && ../dcmt-bench/venv/bin/python -u f4log.py 75; $B duty 0 0; $B state; $B faults' +11:53:11.263 Version(crumbs_version=1700, major=1, minor=0, patch=0) +11:53:11.584 faults 0x00 none +11:53:11.918 sent 0x01 00 -> ack Ack(counter=1, opcode=1, status=1, reason=0) +11:53:12.234 sent 0x02 04010000 -> ack Ack(counter=2, opcode=2, status=2, reason=4) +11:53:12.550 sent 0x02 fa000000 -> ack Ack(counter=3, opcode=2, status=1, reason=0) +11:53:12.867 sent 0x02 00000000 -> ack Ack(counter=4, opcode=2, status=1, reason=0) +11:53:13.183 sent 0x01 01 -> ack Ack(counter=5, opcode=1, status=1, reason=0) +11:53:13.499 sent 0x06 6400 -> ack Ack(counter=6, opcode=6, status=1, reason=0) +11:53:13.798 t1=28.3 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:14.150 t1=28.0 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:14.402 t1=28.5 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:14.755 t1=28.8 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:15.594 t1=29.3 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:15.947 t1=29.5 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:16.182 t1=29.0 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:16.417 t1=-0.1 relay1=1 fault=1 on1=65535 flags=0xff +11:53:16.535 t1=29.5 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:17.123 t1=30.3 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:17.358 t1=30.8 relay1=1 fault=0 on1=1000 flags=0x02 +11:53:17.711 t1=30.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:18.570 t1=31.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:19.175 t1=30.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:19.563 t1=31.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:19.798 t1=31.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:20.150 t1=31.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:20.385 t1=-0.1 relay1=1 fault=1 on1=65535 flags=0xff +11:53:20.502 t1=31.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:20.737 t1=31.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:20.972 t1=31.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:21.359 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:21.881 t1=31.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:22.168 t1=31.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:22.755 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:23.108 t1=31.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:23.460 t1=31.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:23.695 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:24.300 t1=31.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:24.553 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:24.906 t1=31.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:25.159 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:25.512 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:25.747 t1=-0.1 relay1=1 fault=1 on1=65535 flags=0xff +11:53:25.865 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:26.100 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:26.453 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:26.689 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:27.042 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:27.295 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:27.900 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:28.876 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:29.129 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:29.364 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:29.735 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:29.988 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:30.358 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:30.964 t1=32.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:31.217 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:31.452 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:33.268 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:33.621 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:33.857 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:34.210 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:34.580 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:34.816 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:35.992 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:36.345 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:36.597 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:36.950 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:37.186 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:37.539 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:37.791 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:38.162 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:38.397 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:38.768 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:38.885 t1=-0.1 relay1=1 fault=1 on1=65535 flags=0xff +11:53:39.021 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:39.256 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:39.609 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:39.862 t1=32.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:40.215 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:41.361 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:41.713 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:42.066 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:42.302 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:42.655 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:42.890 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:43.243 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:43.478 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:43.831 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:44.437 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:45.025 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:45.278 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:45.649 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:45.884 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:46.154 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:46.577 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:46.813 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:47.065 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:47.706 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:48.446 poll failed FrameError('CRC 0xFF does not match 0x91') +11:53:48.917 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:49.810 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:50.163 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:50.416 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:51.610 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:52.215 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:53.056 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:53.409 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:53.762 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:54.015 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:54.368 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:54.621 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:54.856 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:55.479 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:56.068 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:56.673 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:57.026 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:57.279 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:57.632 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:53:57.868 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:58.221 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:58.456 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:53:58.809 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:53:59.432 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:53:59.685 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:00.038 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:00.291 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:00.949 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:01.184 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:01.537 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:02.125 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:02.361 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:03.101 poll failed FrameError('CRC 0xFF does not match 0xD4') +11:54:03.219 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:03.337 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:03.572 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:03.925 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:04.178 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:04.684 poll failed FrameError('CRC 0x00 does not match 0x01') +11:54:04.801 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:05.054 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:05.407 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:05.995 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:06.601 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:07.477 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:07.830 t1=33.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:08.083 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:08.471 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:08.959 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:09.312 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:11.164 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:11.417 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:11.770 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:12.040 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:12.275 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:12.898 poll failed FrameError('CRC 0xFF does not match 0x40') +11:54:13.016 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:13.252 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:13.487 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:13.757 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:14.481 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:14.716 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:15.069 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:15.304 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:15.657 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:15.910 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:16.263 t1=32.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:16.886 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:17.374 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:17.745 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:17.980 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:18.351 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:18.621 poll failed FrameError('CRC 0xFF does not match 0xC2') +11:54:19.209 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:19.832 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:21.008 t1=32.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:21.361 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:21.597 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:22.538 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:22.791 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:23.143 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:23.379 t1=33.0 relay1=0 fault=1 on1=0 flags=0x08 +11:54:23.667 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:24.037 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:24.660 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:25.483 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:25.871 t1=33.8 relay1=0 fault=1 on1=0 flags=0x08 +11:54:26.107 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:26.712 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:26.983 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:27.353 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:27.858 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:28.211 t1=33.3 relay1=0 fault=1 on1=0 flags=0x08 +11:54:28.564 t1=33.5 relay1=0 fault=1 on1=0 flags=0x08 +11:54:28.799 faults 0x01 H1_OVERTEMP +11:54:29.139 sent 0x06 0000 -> ack Ack(counter=7, opcode=6, status=2, reason=8) +11:54:29.461 state mode=1 estop=0 fault=1 relays=0,0 t=33.3,21.3 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=0 +11:54:29.765 faults 0x01 H1_OVERTEMP + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python rlht_bench.py state' +11:55:50.054 state mode=1 estop=0 fault=1 relays=0,0 t=28.0,21.8 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 + +$ ssh 'cd rlht-bench && B="../dcmt-bench/venv/bin/python -u rlht_bench.py" && $B state && $B fclear && $B faults && $B state' +11:55:53.760 state mode=1 estop=0 fault=1 relays=0,0 t=27.3,21.5 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 +11:55:54.075 sent 0x7B -> ack Ack(counter=8, opcode=123, status=1, reason=0) +11:55:54.379 faults 0x00 none +11:55:54.684 state mode=1 estop=0 fault=0 relays=0,0 t=27.8,21.3 sp=0.0,0.0 on=0,0 period=1000,1000 tc=1,2 estop_count=3 + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python replylog.py 19 1 300 2>&1 | tail -15' +12:03:34.962 FrameError('CRC 0xFF does not match 0x67') 0180140000e900e9000000000000000000e801ffffffffffffffffffffffff +12:03:35.785 FrameError('CRC 0xFF does not match 0x49') 0180140000eb00e600000000ffffffffffffffffffffffffffffffffffffff +12:03:37.431 FrameError('CRC 0xFF does not match 0x67') 0180140000e900e9000000000000000000e801ffffffffffffffffffffffff +12:03:37.901 all-FF payload passed CRC 01801400ffffffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:38.136 FrameError('CRC 0xFF does not match 0x09') 0180140000e600e60000ffffffffffffffffffffffffffffffffffffffffff +12:03:38.607 FrameError('CRC 0xFF does not match 0xFE') 0180140000f7ffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:42.133 FrameError('CRC 0xFF does not match 0xB0') 0180140000f000e6000000000000000000ffffffffffffffffffffffffffff +12:03:42.956 FrameError('the header declares 255 payload bytes, more than 27') 01c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:45.307 FrameError('CRC 0xFF does not match 0xFE') 0180140000f7ffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:46.130 FrameError('CRC 0xFF does not match 0xA9') 0180140000eb00e1000000000000ffffffffffffffffffffffffffffffffff +12:03:46.601 FrameError('CRC 0xFF does not match 0xED') 01800affffffffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:49.187 FrameError('CRC 0xFF does not match 0xA9') 0180140000ffffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:51.420 FrameError('CRC 0xFF does not match 0xFE') 0180140000f7ffffffffffffffffffffffffffffffffffffffffffffffffff +12:03:53.654 FrameError('CRC 0xD4 does not match 0xA9') 0180140000e900e9000000000000000000e803e8030900d4ffffffffffffff +polls=2552 failed=174 ff_passed=13 + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python replylog.py 19 1 300 2>&1 | tail -8' +12:06:58.058 start addr=19 type=1 +polls=2553 failed=0 ff_passed=0 + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python replylog.py 19 1 300 2>&1 | tail -8' +12:37:08.551 start addr=19 type=1 +polls=2553 failed=0 ff_passed=0 + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python replylog.py 19 1 300 2>&1 | tail -8' +12:44:32.053 start addr=19 type=1 +polls=2553 failed=0 ff_passed=0 + +$ ssh 'cd dcmt-bench && for c in version caps faults; do venv/bin/python dcmt_bench.py $c 2>&1 | tail -1; done; cd ../rlht-bench && ../dcmt-bench/venv/bin/python rlht_bench.py caps && ../dcmt-bench/venv/bin/python rlht_bench.py faults && ../dcmt-bench/venv/bin/python rlht_bench.py sp 26 0 && ../dcmt-bench/venv/bin/python rlht_bench.py sp 0 0' +12:58:18.082 Version(crumbs_version=1700, major=2, minor=0, patch=0) +12:58:18.405 caps 0103ff0f0000 +12:58:18.715 faults 0x00 none +12:58:19.018 caps 0101ff7f0000 +12:58:19.320 faults 0x00 none +12:58:19.635 sent 0x02 04010000 -> ack Ack(counter=1, opcode=2, status=1, reason=0) +12:58:19.949 sent 0x02 00000000 -> ack Ack(counter=2, opcode=2, status=1, reason=0) + +$ ssh 'cd rlht-bench && ../dcmt-bench/venv/bin/python replylog.py 19 1 300 2>&1 | tail -3' +13:03:24.405 FrameError('CRC 0xFF does not match 0x79') 0180140000e600e6000000000000000000e803e801ffffffffffffffffffff +13:03:24.641 FrameError('the header declares 255 payload bytes, more than 27') 01c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +polls=2552 failed=171 ff_passed=4 diff --git a/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/rlht_bench.py b/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/rlht_bench.py new file mode 100644 index 0000000..f542f02 --- /dev/null +++ b/docs/bench/assets/2026-11-01-pid-tcsel-amber-bisect/rlht_bench.py @@ -0,0 +1,298 @@ +"""RLHT bench helper for a Pi controller: CRUMBS over /dev/i2c-1 and the +board's USB serial port. Run it with a venv that pins crumbs-i2c 0.16.2 and +bread-crumbs-contracts 0.11.0: + + venv/bin/python rlht_bench.py state + + rlht_bench.py version | caps | state | wdog | ack + rlht_bench.py mode N | duty D1 D2 | sp T1 T2 | wd MS | clear + duty in percent; sp in degrees C + rlht_bench.py wdl MS FLAGS 3-byte SET_WATCHDOG (the lease; 1 = commands only) + rlht_bench.py pid KP KI KD 12-byte SET_PID, the same gains on both heaters + rlht_bench.py pid6 KP KI KD 6-byte SET_PID (u8 gains x10) + rlht_bench.py faults | fclear GET_FAULTS; CLEAR_FAULTS + rlht_bench.py raw OP HEX any SET opcode with a hex payload + rlht_bench.py watch SECONDS poll GET_STATE every 200 ms, print each change + of relays, fault flag, on-times or e-stop, then + GET_WATCHDOG and GET_FAULTS + rlht_bench.py ser LINE... send serial lines, print the replies + rlht_bench.py flood SECONDS invalid serial lines nonstop while polling + GET_STATE every 20 ms; prints each change + of the e-stop flag or count and any failed + poll + rlht_bench.py prep stop the port from resetting the board on + every open (stty -hupcl); run once + rlht_bench.py tcsel TC1 TC2 2-byte SET_TC_SELECT: the thermocouple (1 or + 2) each heater's PID reads + rlht_bench.py ontimes SECONDS poll GET_STATE every 100 ms, print each + change of on1, on2, t1 or the setpoints, + then a summary of the gaps between on-time + changes (one thermocouple reading is + 300 ms) + +Every command prints a timestamp. SETs print the GET_ACK that follows. +Address: RLHT_ADDR (default 19). Port: RLHT_PORT, or the only +ttyACM*/ttyUSB* present. +""" + +import os +import subprocess +import sys +import time + +from bread_crumbs_contracts import rlht_ops as r +from crumbs_i2c import Controller, LinuxBus, Message + +ADDR = int(os.environ.get("RLHT_ADDR", "19"), 0) + + +def default_port(): + # A Nano Every enumerates as ttyACM*, a classic Nano as ttyUSB*. Use the + # only one present; with none or several, RLHT_PORT has to say which. + import glob + + ports = sorted(glob.glob("/dev/ttyACM*") + glob.glob("/dev/ttyUSB*")) + return ports[0] if len(ports) == 1 else None + + +PORT = os.environ.get("RLHT_PORT") or default_port() +BREAD_OP_GET_WATCHDOG = 0x7D +BREAD_OP_SET_WATCHDOG = 0x7E +BREAD_OP_CLEAR_WATCHDOG_TRIP = 0x7C +BREAD_OP_GET_CAPS = 0x7F +BREAD_OP_GET_FAULTS = 0x7A +BREAD_OP_CLEAR_FAULTS = 0x7B +FAULT_NAMES = { + 0x01: "H1_OVERTEMP", 0x02: "H1_NO_RISE", 0x04: "H1_SENSOR", + 0x08: "H2_OVERTEMP", 0x10: "H2_NO_RISE", 0x20: "H2_SENSOR", +} + + +def stamp(): + # One clock read, so the seconds and the milliseconds agree. + t = time.time() + return time.strftime("%H:%M:%S", time.localtime(t)) + f".{int(t * 1000) % 1000:03d}" + + +def get_state(c): + return r.rlht_parse_state_payload( + c.query(ADDR, type_id=r.RLHT_TYPE_ID, opcode=r.RLHT_OP_GET_STATE).data + ) + + +def show_state(c): + s = get_state(c) + estop = 1 if s.flags & r.RLHT_FLAG_ESTOP else 0 + relay1 = 1 if s.flags & r.RLHT_FLAG_RELAY1_ON else 0 + relay2 = 1 if s.flags & r.RLHT_FLAG_RELAY2_ON else 0 + fault = 1 if s.flags & r.RLHT_FLAG_FAULT else 0 + print( + f"{stamp()} state mode={s.mode} estop={estop} fault={fault} relays={relay1},{relay2} " + f"t={s.t1_deci_c / 10},{s.t2_deci_c / 10} sp={s.sp1_deci_c / 10},{s.sp2_deci_c / 10} " + f"on={s.on1_ms},{s.on2_ms} period={s.period1_ms},{s.period2_ms} " + f"tc={s.tc1},{s.tc2} estop_count={s.estop_count}" + ) + return s + + +def send(c, opcode, payload): + c.send(ADDR, Message(type_id=r.RLHT_TYPE_ID, opcode=opcode, data=payload)) + time.sleep(0.01) + a = c.read_ack(ADDR) + print(f"{stamp()} sent 0x{opcode:02X} {payload.hex()} -> ack {a}") + + +def serial_port(): + import serial + + if not PORT: + sys.exit("no single serial port found; set RLHT_PORT") + return serial.Serial(PORT, 115200, timeout=0.2) + + +def main(): + cmd, *args = sys.argv[1:] + if cmd == "prep": + if not PORT: + sys.exit("no single serial port found; set RLHT_PORT") + subprocess.run(["stty", "-F", PORT, "-hupcl"], check=True) + print(f"{stamp()} {PORT}: hupcl off") + return + if cmd == "ser": + with serial_port() as p: + for line in args: + p.write(line.encode() + b"\n") + time.sleep(0.15) + print(f"{stamp()} > {line}") + for reply in p.read(4096).decode(errors="replace").splitlines(): + print(f"{stamp()} < {reply}") + return + with LinuxBus("/dev/i2c-1") as bus: + c = Controller(bus) + if cmd == "version": + print(stamp(), c.get_version(ADDR, type_id=r.RLHT_TYPE_ID)) + elif cmd == "caps": + q = c.query(ADDR, type_id=r.RLHT_TYPE_ID, opcode=BREAD_OP_GET_CAPS) + print(stamp(), "caps", q.data.hex()) + elif cmd == "wdog": + q = c.query(ADDR, type_id=r.RLHT_TYPE_ID, opcode=BREAD_OP_GET_WATCHDOG) + print(stamp(), "wdog [armed][timeout_ms][tripped][trips]", q.data.hex()) + elif cmd == "state": + show_state(c) + elif cmd == "ack": + print(stamp(), c.read_ack(ADDR)) + elif cmd == "mode": + send(c, r.RLHT_OP_SET_MODE, r.rlht_send_set_mode(int(args[0]))) + elif cmd == "duty": + send(c, r.RLHT_OP_SET_OPEN_DUTY, r.rlht_send_set_open_duty(int(args[0]), int(args[1]))) + elif cmd == "sp": + send( + c, + r.RLHT_OP_SET_SETPOINTS, + r.rlht_send_set_setpoints(round(float(args[0]) * 10), round(float(args[1]) * 10)), + ) + elif cmd == "wd": + send(c, BREAD_OP_SET_WATCHDOG, r.rlht_send_set_watchdog(int(args[0]))) + elif cmd == "clear": + send(c, BREAD_OP_CLEAR_WATCHDOG_TRIP, r.rlht_send_clear_watchdog_trip()) + elif cmd == "flood": + flood(c, float(args[0])) + elif cmd == "wdl": + send(c, BREAD_OP_SET_WATCHDOG, r.rlht_send_set_watchdog_lease(int(args[0]), int(args[1], 0))) + elif cmd == "pid": + g = [round(float(x) * 1000) for x in args[:3]] + send(c, r.RLHT_OP_SET_PID, r.rlht_send_set_pid_x1000(*g, *g)) + elif cmd == "pid6": + g = [round(float(x) * 10) for x in args[:3]] + send(c, r.RLHT_OP_SET_PID, r.rlht_send_set_pid_x10(*g, *g)) + elif cmd == "faults": + show_faults(c) + elif cmd == "fclear": + send(c, BREAD_OP_CLEAR_FAULTS, r.rlht_send_clear_faults()) + elif cmd == "raw": + send(c, int(args[0], 0), bytes.fromhex(args[1] if len(args) > 1 else "")) + elif cmd == "watch": + watch(c, float(args[0])) + elif cmd == "tcsel": + send(c, r.RLHT_OP_SET_TC_SELECT, r.rlht_send_set_tc_select(int(args[0]), int(args[1]))) + elif cmd == "ontimes": + ontimes(c, float(args[0])) + else: + sys.exit(f"unknown command {cmd}") + + +def show_faults(c): + q = c.query(ADDR, type_id=r.RLHT_TYPE_ID, opcode=BREAD_OP_GET_FAULTS) + bits = q.data[0] if q.data else 0 + names = [n for b, n in FAULT_NAMES.items() if bits & b] or ["none"] + print(f"{stamp()} faults 0x{bits:02x} {' '.join(names)}") + + +def watch(c, seconds): + # GET_STATE only: under a commands-only lease these polls do not refresh + # the watchdog, which is what the lease step checks. + end = time.monotonic() + seconds + last = None + while time.monotonic() < end: + try: + s = get_state(c) + now = (s.flags, s.on1_ms, s.on2_ms) + if now != last: + show_state(c) + last = now + except Exception as e: + print(f"{stamp()} poll failed: {e!r}") + time.sleep(0.2) + q = c.query(ADDR, type_id=r.RLHT_TYPE_ID, opcode=BREAD_OP_GET_WATCHDOG) + print(stamp(), "wdog [armed][timeout_ms][tripped][trips]", q.data.hex()) + show_faults(c) + + +def ontimes(c, seconds): + # GET_STATE every 100 ms on a fixed schedule. A line for each poll where + # on1, on2, t1 or a setpoint differs from the poll before. Since #76 the + # PIDs compute once per thermocouple reading (300 ms), so two on-time + # changes should never land on consecutive polls; before, PID_v1 computed + # every 100 ms and a derivative impulse showed as a change and its undo + # one poll apart. + print(f"{stamp()} ontimes {seconds:g} s") + print("stamp t on1 on2 t1 sp1 sp2 relays fault") + last = None + changes = {1: [], 2: []} + t1_changes = [] + polls = fails = 0 + t0 = time.monotonic() + nxt = t0 + while time.monotonic() - t0 <= seconds: + try: + s = get_state(c) + t = time.monotonic() - t0 + polls += 1 + now = (s.on1_ms, s.on2_ms, s.t1_deci_c, s.sp1_deci_c, s.sp2_deci_c) + if now != last: + if last is not None: + if now[0] != last[0]: + changes[1].append(t) + if now[1] != last[1]: + changes[2].append(t) + if now[2] != last[2]: + t1_changes.append(t) + relay1 = 1 if s.flags & r.RLHT_FLAG_RELAY1_ON else 0 + relay2 = 1 if s.flags & r.RLHT_FLAG_RELAY2_ON else 0 + fault = 1 if s.flags & r.RLHT_FLAG_FAULT else 0 + print( + f"{stamp()} {t:6.2f} {s.on1_ms} {s.on2_ms} {s.t1_deci_c / 10} " + f"{s.sp1_deci_c / 10} {s.sp2_deci_c / 10} {relay1},{relay2} {fault}" + ) + last = now + except Exception as e: + fails += 1 + print(f"{stamp()} poll failed: {e!r}") + nxt += 0.1 + time.sleep(max(0.0, nxt - time.monotonic())) + print(f"{stamp()} summary: {polls} polls, {fails} failed, {len(t1_changes)} t1 changes") + for ch in (1, 2): + ts = changes[ch] + gaps = [b - a for a, b in zip(ts, ts[1:])] + if not gaps: + print(f" on{ch}: {len(ts)} changes") + continue + close = sum(1 for g in gaps if g < 0.15) + print( + f" on{ch}: {len(ts)} changes, gaps min {min(gaps):.2f} s, " + f"median {sorted(gaps)[len(gaps) // 2]:.2f} s, {close} under 0.15 s (consecutive polls)" + ) + + +def flood(c, seconds): + # Each invalid line makes the firmware print its error reply, so the + # replies alone keep its transmit buffer full. + with serial_port() as p: + p.timeout = 0 + p.write_timeout = 0 + end = time.monotonic() + seconds + last = None + lines = polls = fails = 0 + while time.monotonic() < end: + try: + p.write(b"x\n" * 8) + lines += 8 + except Exception: + pass + p.reset_input_buffer() + try: + s = get_state(c) + polls += 1 + now = (1 if s.flags & r.RLHT_FLAG_ESTOP else 0, s.estop_count) + if now != last: + print(f"{stamp()} estop={now[0]} estop_count={now[1]}") + last = now + except Exception as e: + fails += 1 + print(f"{stamp()} poll failed: {e!r}") + time.sleep(0.02) + print(f"{stamp()} done: {lines} lines sent, {polls} polls, {fails} failed") + + +if __name__ == "__main__": + main()