Auto: focas-f5a — cycle time per part / last cycle delta

Closes #272
This commit is contained in:
Joseph Doherty
2026-04-26 09:11:21 -04:00
parent 45770e8d90
commit e3d7c65f61
7 changed files with 711 additions and 9 deletions

View File

@@ -8,6 +8,47 @@ Power Mate i families. Talks to the controller via the licensed
For range-validation and per-series capability surface see
[`docs/v2/focas-version-matrix.md`](../v2/focas-version-matrix.md).
## Fixed-tree `Production/` projection — issue #258 (F1-b) + issue #272 (F5-a)
Per-device read-only nodes refreshed from the same `cnc_rdparam` /
cycle-timer poll the probe loop already runs. No additional wire calls
are issued for any of these — they are all cache-or-derive reads.
| Node | DataType | Source | Notes |
| --- | --- | --- | --- |
| `Production/PartsProduced` | `Int32` | `cnc_rdparam(6711)` | Active parts-count counter. Wraps to 0 on operator reset. |
| `Production/PartsRequired` | `Int32` | `cnc_rdparam(6712)` | Operator-set target. |
| `Production/PartsTotal` | `Int32` | `cnc_rdparam(6713)` | Lifetime parts counter. |
| `Production/CycleTimeSeconds` | `Int32` | `cnc_rdtimer` (channel 0) | Live cycle-time accumulator. Resets to 0 on next cycle start (CNC-side behaviour). |
| **`Production/LastCycleSeconds`** | **`Float64`** | **derived** | **Plan PR F5-a — seconds for the most recently completed cycle, computed as `CycleTimeSeconds(now) - CycleTimeSeconds(at previous parts-count increment)`. `null` until the second observed parts-count increment establishes a delta. Pure derivation, no new wire calls. See edge-case rules below.** |
| **`Production/LastCycleStartUtc`** | **`DateTime`** *(UTC)* | **derived** | **Plan PR F5-a — UTC wall-clock of the most-recent cycle's start, computed as `nowUtc - LastCycleSeconds`. `null` alongside `LastCycleSeconds` until the second observed increment.** |
### F5-a derivation edge-case rules
- **First observation** establishes the baseline; `LastCycleSeconds` /
`LastCycleStartUtc` stay `null` until the second observed parts-count
increment produces the first delta.
- **Parts-count counter reset** (current value goes backwards, e.g.
shift-change zero) **preserves the last published values** so an
operator reading the tag mid-shift-change sees the last known cycle
duration rather than `null` / Bad. The next positive transition
produces a fresh delta from the new baseline.
- **Cycle-timer rollover** (delta would be negative — e.g. CNC zeroes
the cycle timer at part completion) **leaves the previously-published
values unchanged for one tick** and re-baselines so the next
increment produces a clean delta. The driver does NOT publish a
negative `LastCycleSeconds`.
- **Parts-count jumps `> 1`** (backfill — e.g. counter increments by
3 at once) publish the **timer delta over the window** as
`LastCycleSeconds`. The plan's "delta over the window between
successive parts-count increments" definition does not divide by the
count delta; the value reflects the actual elapsed timer between the
two observations.
- **Reconnect / reinit** clears the derivation state — the prior CNC
session's cycle-timer + parts-count snapshots may be invalidated by
the FWLIB session boundary, so the next post-reconnect probe tick
re-establishes the baseline before the next delta publishes.
## Alarm history (`cnc_rdalmhistry`) — issue #267, plan PR F3-a
`FocasAlarmProjection` exposes two modes via `FocasDriverOptions.AlarmProjection`:

View File

@@ -44,6 +44,37 @@ reported wall-clock — keep CNC clocks on UTC so the dedup key
`(OccurrenceTime, AlarmNumber, AlarmType)` stays stable across DST
transitions.
## Derived telemetry — issue #272 (plan PR F5-a)
The `Production/` subtree gains two **derived** nodes alongside the four
F1-b wire-sourced fields:
- `Production/LastCycleSeconds` (`Float64`)
- `Production/LastCycleStartUtc` (`DateTime` UTC)
**No new wire calls.** Both nodes are computed client-visible from the
same `cnc_rdparam(6711)` + `cnc_rdtimer` poll the F1-b projection
already runs on every probe tick. There is no per-device knob — the
nodes are present for every CNC the driver connects to and surface
`null` until the second observed parts-count increment produces the
first delta.
This means:
- **No additional CNC load.** Probe-tick wire traffic is unchanged.
- **No new opt-in.** The nodes ship enabled by default and are
read-only (`SecurityClassification.ViewOnly`); no LDAP group needs
the new permission.
- **Reconnect re-baselines.** Per the FWLIB session boundary the
derivation state resets on reconnect / reinit, so the first cycle
observed after a reconnect re-establishes the baseline before
publishing the first post-reconnect delta.
See [`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) § "Fixed-tree
`Production/` projection" for the full edge-case behaviour matrix
(parts-count counter reset, cycle-timer rollover, parts-count jumps
> 1).
## Write safety — issue #269 (PARAM/MACRO, F4-b) + issue #270 (PMC, F4-c)
The FOCAS driver supports `cnc_wrparam`, `cnc_wrmacro`, and `pmc_wrpmcrng`

View File

@@ -304,6 +304,82 @@ Bit-level writes never appear here as a separate kind — they reach the
simulator as 1-byte writes after the driver's RMW wrapper, so the audit
shape is identical to a byte write at the same address.
## Cycle-time per part / last cycle delta — F5-a (issue #272)
Plan PR F5-a derives `Production/LastCycleSeconds` +
`Production/LastCycleStartUtc` from the existing `cnc_rdparam(6711)` +
`cnc_rdtimer` snapshot stream — **pure derivation, no new wire calls**.
The simulator does NOT need new wire commands; the existing
`cnc_rdparam` + `cnc_rdtimer` handlers already cover the read surface.
What focas-mock DOES need is an admin endpoint + test-fixture helper
that lets integration tests atomically increment the parts-count
counter alongside the cycle-time timer so the driver sees a clean
"cycle completed" transition on the next probe tick.
### Per-profile state
Already covered by the existing F1-b state map:
- `parameters: Dict[int, int]` (entry `6711` is the parts-count counter).
- `timers: Dict[int, int]` (entry `0` is the live cycle-time counter,
in seconds).
### Admin endpoint — `POST /admin/mock_simulate_cycle_completion`
Atomically advances both values to model "the CNC just finished a
cycle". Atomicity matters: the F5-a derivation samples both fields on
every probe tick, so if the simulator updated parts-count and the
timer in two separate writes the test could observe an intermediate
state where parts-count incremented but the timer hasn't updated yet
(producing a misleading `LastCycleSeconds`).
```
POST /admin/mock_simulate_cycle_completion
{
"profile": "Series30i",
"partsDelta": 1, // default 1; tests asserting backfill use 3+
"newCycleTimerSeconds": 18 // absolute value, NOT a delta
}
```
Handler steps:
1. `parameters[6711] += partsDelta` (under the per-profile lock).
2. `timers[0] = newCycleTimerSeconds`.
3. Return `200 OK` with the new values for verification.
The endpoint MUST hold the profile's update lock for the full
read-modify-write so a concurrent `cnc_rdparam` + `cnc_rdtimer` poll
sees both fields in their pre-update OR post-update state — never
half-applied.
### `FocasSimFixture.SimulateCycleCompletionAsync`
The future test-support helper wraps the admin endpoint:
```csharp
await fixture.SimulateCycleCompletionAsync(
profile: "Series30i",
partsDelta: 1,
newCycleTimerSeconds: 18);
```
Integration test `Series/CycleDeltaTests.cs` will assert:
- After a 5 -> 6 transition with `newCycleTimerSeconds=18`, the
driver's `Production/LastCycleSeconds` settles to `currentTimer -
prevTimer`.
- `Production/LastCycleStartUtc` is within driver-tolerance of
`nowUtc - LastCycleSeconds` (allow a small window for probe-tick
jitter).
- Counter reset (parts -> 0) preserves the last published values.
- Cycle-timer rollover does not publish a negative delta.
These tests are blocked on the focas-mock + integration-test project
landing; the unit-test coverage in `FocasCycleDeltaTests` already
exercises every same-process invariant of the derivation.
### Status
focas-mock simulator has not landed yet (tracked separately from F4-b /