docs(series4): the schedule record format, named by the firmware itself

A debug printf in the scheduler names the record's fields outright:

  SCHEDULER : _ReadRecord(%d) -> %s (Action=%u, 1/2h=%u, Day=%u, Setup="%s") [WDAY=%d]

They fit the captured record, and the name-length byte anchors the alignment --
it reads 40 for the 40-character name pulled off the unit and 9 for TEST1.mmb
written back, same position, both directions:

  [0]     Action = 3
  [1:4]   zero (padding, or Action is a uint32)
  [4]     1/2h = 15   -> 30-minute resolution, 48 slots/day
  [5]     Day = 3
  [6]     unidentified (WDAY?)
  [7]     name length -- 0x28=40 read, 0x09=9 written  <-- confirms the layout
  [8:]    setup name
  [264]   trailer 27 03 10

Slot 15 would be 07:30 counted from midnight; flagged unconfirmed because the
schedule's actual time was not recorded with the capture.

Six duty-cycle actions, not the five previously recorded -- there is also
DUTYCYCLE_START_MONITOR_WITH_SETUP_STOP_COMPLETE.  THOR exposes four.  Two start
variants it never offers, one needing no setup file.

Strengthened the setup-less-action finding and ruled out an alternative
explanation I had not considered: the Micromate has a separate Timer Mode
(MODE_TIMER, Monitor Once Only, under Special Setup), so START_MONITOR could have
belonged to that path.  It does not -- it is a case in _PSA(), the scheduler's own
dispatcher for _ReadRecord's Action field, and `_PSA() send ->> CMD_DUTYCYCLE_
START_MONITOR` shows it is live code sending a real message, not a dead case.

Also: SysPref.bMonitorScheduler places the scheduler enable in system
preferences, which confirms from the other side why the 0x71 block was
byte-identical when the scheduler was switched on -- the flag was never going to
be in the compliance config.  It also suggests 0x47 is a SysPref get/set rather
than anything scheduler-specific, which would explain its params[7] selector and
its response shape matching 0x48's page-0 descriptor.  Still a hypothesis.

Names the one capture that would settle the rest: a schedule with TWO entries at
different times with different actions.  That yields the record stride, the action
code values, and the time encoding at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
2026-09-24 20:29:51 -04:00
co-authored by Claude Opus 5
parent 3f58402085
commit 508448e2dd
+82
View File
@@ -1490,6 +1490,88 @@ other schedules — or the operator's own saved work — depend on. Nothing in
protocol or the ack reports that this happened. Validating the reference protocol or the ack reports that this happened. Validating the reference
instead of rewriting the referent avoids the whole class of problem. instead of rewriting the referent avoids the whole class of problem.
### The schedule record format, from the firmware's own debug printf
```
SCHEDULER : _ReadRecord(%d) -> %s (Action=%u, 1/2h=%u, Day=%u, Setup="%s") [WDAY=%d]
```
That names the fields outright, and they fit the captured record:
| offset | field | read capture | write capture |
|---|---|---|---|
| `[0]` | **Action** | `03` | `03` |
| `[1:4]` | zero — padding, or `Action` is a uint32 | `00 00 00` | `00 00 00` |
| `[4]` | **1/2h** — half-hour slot | `0f` = 15 | `0f` = 15 |
| `[5]` | **Day** | `03` | `03` |
| `[6]` | unidentified (`WDAY`?) | `02` | `02` |
| `[7]` | **name length** | `0x28` = **40** | `0x09` = **9** |
| `[8:]` | **Setup** name | 40-char name | `TEST1.mmb` |
| `[264]` | trailer | `27 03 10` | `27 03 10` |
**The length byte is what anchors this** — it reads 40 for the 40-character name
and 9 for `TEST1.mmb`, in the same position, in both directions. The layout is
not a guess.
`1/2h` implies **30-minute resolution, 48 slots per day**. Slot 15 would be
07:30 if counted from midnight — plausible but ⚠ **unconfirmed**, since the
schedule's actual time was not recorded alongside the capture. One question to
the operator settles it.
⚠ Still unknown, and all answerable with **one schedule containing two entries**:
- the **record stride** and how many records the file holds (one record was
captured; `27 03 10` at `[264]` may be a trailer or a second record's head)
- whether `Action` is one byte or a uint32
- the **action code values** — `03` is presumably
`DUTYCYCLE_START_MONITOR_WITH_SETUP` but the enum's base is unknown
- `[6]`, and how `Day` and `WDAY` divide the work
### Six duty-cycle actions, not five
The full `_PSA()` dispatch — the scheduler's own action processor, reading records
via `_ReadRecord`:
```
DUTYCYCLE_START_MONITOR ← no setup
DUTYCYCLE_START_MONITOR_WITH_SETUP
DUTYCYCLE_START_MONITOR_WITH_SETUP_STOP_COMPLETE ← a third start variant
DUTYCYCLE_STOP_MONITOR
DUTYCYCLE_CALLHOME
DUTYCYCLE_SELF_CHECK
```
**THOR exposes four.** Two start variants it never offers, one of which needs no
setup file at all.
`_PSA() send ->> CMD_DUTYCYCLE_START_MONITOR` shows the setup-less action is live
code that the scheduler genuinely dispatches, not a dead case — it sends a real
message. And since every one of these cases sits in the function that consumes
`_ReadRecord`'s `Action` field, **a schedule record can carry it.**
An alternative explanation was checked and ruled out: the Micromate does have a
separate *Timer Mode* (`MODE_TIMER`, `MODE_MONITOR_TIMER`, `Monitor Once Only`,
under Special Setup), so `START_MONITOR` could have belonged to that path
instead. It does not — it is in `_PSA()`, the scheduler's dispatcher.
### The scheduler enable lives in SysPref, not in the setup
```
SysPref.bMonitorScheduler = %s
_Task() CMD_SET_SCHEDULE - MonitorScheduler ENABLED
_Task() CMD_SET_SCHEDULE - ! MonitorScheduler NOT Enabled
```
This **confirms the failed prediction above from the other side**: the
`Scheduler On/Off` switch is a *system preference*, which is why the `0x71`
compliance block was byte-identical when the scheduler was turned on. It was
never going to be in there.
It also suggests `0x47` — the two bare frames at the end of the capture — is a
SysPref get/set rather than anything scheduler-specific, which would explain its
`params[7]` selector and its 15-byte response shape being shared with `0x48`'s
page-0 descriptor. ⚠ Still a hypothesis; the disable/enable capture settles it.
#### The schedule has a setup-less start action — Thor just never uses it #### The schedule has a setup-less start action — Thor just never uses it
The coupling above looked like it might be a workaround for the unit crashing on The coupling above looked like it might be a workaround for the unit crashing on