Auto: focas-f3a — cnc_rdalmhistry alarm-history extension
Adds FocasAlarmProjection with two modes (ActiveOnly default, ActivePlusHistory) that polls cnc_rdalmhistry on connect + on a configurable cadence (5 min default, HistoryDepth=100 capped at 250). Emits historic events via IAlarmSource with SourceTimestampUtc set from the CNC's reported timestamp; dedup keyed on (OccurrenceTime, AlarmNumber, AlarmType). Ships the ODBALMHIS packed-buffer decoder + encoder in Wire/FocasAlarmHistoryDecoder.cs and threads ReadAlarmHistoryAsync through IFocasClient (default no-op so existing transport variants stay back-compat). FocasDriver now implements IAlarmSource. 13 new unit tests cover: mode switch, dedup, distinct-timestamp emission, type-as-key behaviour, OccurrenceTime passthrough (not Now), HistoryDepth clamp/fallback, and decoder round-trip. All 341 FOCAS unit tests still pass. Docs: docs/drivers/FOCAS.md (new), docs/v2/focas-deployment.md (new), docs/v2/implementation/focas-wire-protocol.md (new), docs/v2/implementation/focas-simulator-plan.md (new), docs/drivers/FOCAS-Test-Fixture.md (alarm-history bullet appended). Closes #267
This commit is contained in:
45
docs/v2/focas-deployment.md
Normal file
45
docs/v2/focas-deployment.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# FOCAS deployment guide
|
||||
|
||||
Per-driver runbook for deploying the FANUC FOCAS driver. See
|
||||
[`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) for the per-feature
|
||||
reference and [`focas-version-matrix.md`](./focas-version-matrix.md) for
|
||||
the per-CNC-series capability surface.
|
||||
|
||||
## Operator config-knob cheat sheet
|
||||
|
||||
| Knob | Where | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `Devices[].HostAddress` | `FocasDriverOptions.Devices` | — | `focas://{ip}[:{port}]` |
|
||||
| `Devices[].Series` | `FocasDriverOptions.Devices` | `Unknown` | Drives per-series range validation in `FocasCapabilityMatrix`. |
|
||||
| `Devices[].OverrideParameters` | `FocasDriverOptions.Devices` | `null` | MTB-specific parameter numbers for Feed/Rapid/Spindle/Jog overrides. `null` suppresses the `Override/` subtree. |
|
||||
| `Probe.Enabled` | `FocasDriverOptions.Probe` | `true` | Background reachability probe. |
|
||||
| `Probe.Interval` | `FocasDriverOptions.Probe` | `00:00:05` | Probe cadence. |
|
||||
| `FixedTree.ApplyFigureScaling` | `FocasDriverOptions.FixedTree` | `true` | Divide position values by 10^decimal-places (issue #262). |
|
||||
| **`AlarmProjection.Mode`** | **`FocasDriverOptions.AlarmProjection`** | **`ActiveOnly`** | **`ActiveOnly` keeps today's behaviour. `ActivePlusHistory` polls `cnc_rdalmhistry` on connect + on `HistoryPollInterval` ticks (issue #267, plan PR F3-a).** |
|
||||
| **`AlarmProjection.HistoryPollInterval`** | **`FocasDriverOptions.AlarmProjection`** | **`00:05:00`** | **Cadence of the history poll. Operator dashboards run the default; high-frequency rigs can drop to 30 s.** |
|
||||
| **`AlarmProjection.HistoryDepth`** | **`FocasDriverOptions.AlarmProjection`** | **`100`** | **Most-recent-N ring-buffer entries pulled per poll. Hard-capped at `250` so misconfigured values can't blast the wire session.** |
|
||||
|
||||
## Sample `appsettings.json` snippet for `ActivePlusHistory`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"Drivers": {
|
||||
"FOCAS": {
|
||||
"Devices": [
|
||||
{ "HostAddress": "focas://10.0.0.5:8193", "Series": "Series30i" }
|
||||
],
|
||||
"AlarmProjection": {
|
||||
"Mode": "ActivePlusHistory",
|
||||
"HistoryPollInterval": "00:05:00",
|
||||
"HistoryDepth": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The history projection emits each unseen entry through
|
||||
`IAlarmSource.OnAlarmEvent` with `SourceTimestampUtc` set from the CNC's
|
||||
reported wall-clock — keep CNC clocks on UTC so the dedup key
|
||||
`(OccurrenceTime, AlarmNumber, AlarmType)` stays stable across DST
|
||||
transitions.
|
||||
102
docs/v2/implementation/focas-simulator-plan.md
Normal file
102
docs/v2/implementation/focas-simulator-plan.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# FOCAS simulator (focas-mock) plan
|
||||
|
||||
Notes on the focas-mock simulator that the FOCAS driver's integration
|
||||
tests will eventually talk to. Today there is no FOCAS integration-test
|
||||
project; this doc is the contract the future fixture will be built
|
||||
against. Keeping the contract tracked in repo means the wire-protocol
|
||||
command ids (and their request/response payloads) don't drift between the
|
||||
.NET wire client and a future Python implementation.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- Append-only command ids. Mirror
|
||||
[`focas-wire-protocol.md`](./focas-wire-protocol.md) verbatim.
|
||||
- Per-profile state. The simulator hosts N CNC profiles concurrently
|
||||
(`Series0i`, `Series30i`, `PowerMotion`, ...). Each profile has its own
|
||||
alarm-history ring buffer + its own override map.
|
||||
- Admin endpoints under `POST /admin/...` mutate state without going
|
||||
through the wire protocol; integration tests use these to seed canned
|
||||
inputs.
|
||||
|
||||
## Protocol surface (current scope)
|
||||
|
||||
| Cmd | API | State impact |
|
||||
| --- | --- | --- |
|
||||
| `0x0001` | `cnc_rdcncstat` | reads cached ODBST per profile |
|
||||
| `0x0002` | `cnc_rdparam` | reads parameter map per profile |
|
||||
| `0x0003` | `cnc_rdmacro` | reads macro variables per profile |
|
||||
| `0x0004` | `cnc_rddiag` | reads diagnostic map per profile |
|
||||
| `0x0010` | `pmc_rdpmcrng` | reads PMC byte ranges |
|
||||
| `0x0020` | `cnc_modal` | reads cached modal MSTB per profile |
|
||||
| ... | ... | ... |
|
||||
| **`0x0F1A`** | **`cnc_rdalmhistry`** | **dumps the per-profile alarm-history ring buffer (issue #267, plan PR F3-a)** |
|
||||
|
||||
## `cnc_rdalmhistry` mock behaviour
|
||||
|
||||
The simulator keeps a per-profile ring buffer of alarm-history entries.
|
||||
Default fixture seeds 5 profiles with 10 canned entries each (per the F3-a
|
||||
plan).
|
||||
|
||||
### Request decode
|
||||
|
||||
```
|
||||
[int16 LE depth]
|
||||
```
|
||||
|
||||
### Response encode
|
||||
|
||||
Use `FocasAlarmHistoryDecoder.Encode` semantics in reverse: emit the
|
||||
count followed by `ALMHIS_data` blocks padded to 4-byte boundaries. The
|
||||
.NET-side decoder consumes the same format verbatim, so a Python encoder
|
||||
written against the table in
|
||||
[`focas-wire-protocol.md`](./focas-wire-protocol.md) interoperates without
|
||||
extra glue.
|
||||
|
||||
### Admin endpoint — `POST /admin/mock_patch_alarmhistory`
|
||||
|
||||
Replaces the alarm-history ring buffer for a profile.
|
||||
|
||||
```
|
||||
POST /admin/mock_patch_alarmhistory
|
||||
{
|
||||
"profile": "Series30i",
|
||||
"entries": [
|
||||
{
|
||||
"occurrenceTime": "2025-04-01T09:30:00Z",
|
||||
"axisNo": 1,
|
||||
"alarmType": 2,
|
||||
"alarmNumber": 100,
|
||||
"message": "Spindle overload"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`entries` order is interpreted as ring-buffer order (most-recent first to
|
||||
match FANUC's natural surface).
|
||||
|
||||
### `FocasSimFixture.SeedAlarmHistoryAsync`
|
||||
|
||||
The future test-support helper wraps the admin endpoint:
|
||||
|
||||
```csharp
|
||||
await fixture.SeedAlarmHistoryAsync(
|
||||
profile: "Series30i",
|
||||
entries: new []
|
||||
{
|
||||
new FocasAlarmHistoryEntry(
|
||||
new DateTimeOffset(2025, 4, 1, 9, 30, 0, TimeSpan.Zero),
|
||||
AxisNo: 1, AlarmType: 2, AlarmNumber: 100, Message: "Spindle overload"),
|
||||
});
|
||||
```
|
||||
|
||||
Integration test `Series/AlarmHistoryProjectionTests.cs` will assert:
|
||||
|
||||
- historic events fire once with the seeded timestamps
|
||||
- second poll yields zero new events (dedup honoured end-to-end)
|
||||
- active-alarm raise/clear still works alongside the history poll
|
||||
|
||||
These tests are blocked on the focas-mock + integration-test project
|
||||
landing; the unit-test coverage in `FocasAlarmProjectionTests` already
|
||||
exercises every same-process invariant.
|
||||
76
docs/v2/implementation/focas-wire-protocol.md
Normal file
76
docs/v2/implementation/focas-wire-protocol.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# FOCAS wire protocol — packed-buffer surface
|
||||
|
||||
Notes on the language-neutral packed-buffer encoding the FOCAS driver +
|
||||
focas-mock simulator share. This format is **not** the FWLIB native struct
|
||||
layout — Tier-C Fwlib32 backends marshal directly from the FANUC C struct.
|
||||
The packed surface exists so the simulator (Python / FastAPI) and the .NET
|
||||
wire client can speak a common format over IPC without piping a Win32 DLL
|
||||
through both ends.
|
||||
|
||||
## Command id table
|
||||
|
||||
Each FOCAS-equivalent call gets a stable wire-protocol command id. Ids are
|
||||
**append-only** — never renumber, never reuse.
|
||||
|
||||
| Id | FOCAS API | Surface |
|
||||
| --- | --- | --- |
|
||||
| `0x0001` | `cnc_rdcncstat` | ODBST 9-field status struct |
|
||||
| `0x0002` | `cnc_rdparam` | parameter value (one number) |
|
||||
| `0x0003` | `cnc_rdmacro` | macro variable value |
|
||||
| `0x0004` | `cnc_rddiag` | diagnostic value |
|
||||
| ... | ... | ... |
|
||||
| `0x0F1A` | **`cnc_rdalmhistry`** | **ODBALMHIS alarm-history ring-buffer dump (issue #267, plan PR F3-a)** |
|
||||
|
||||
## ODBALMHIS — alarm history (`cnc_rdalmhistry`, command `0x0F1A`)
|
||||
|
||||
Issued by `FocasAlarmProjection` when
|
||||
`FocasDriverOptions.AlarmProjection.Mode == ActivePlusHistory`. Returns up
|
||||
to `depth` most-recent ring-buffer entries.
|
||||
|
||||
### Request
|
||||
|
||||
| Offset | Width | Field | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | `int16 LE` | `depth` | clamped client-side to `[1..250]` (`FocasAlarmProjectionOptions.MaxHistoryDepth`) |
|
||||
|
||||
### Response (packed buffer, little-endian)
|
||||
|
||||
| Offset | Width | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `int16 LE` | `num_alm` — number of entries that follow. `< 0` indicates CNC error. |
|
||||
| 2 | repeated | `ALMHIS_data alm[num_alm]` (see below) |
|
||||
|
||||
Each entry block:
|
||||
|
||||
| Offset (rel.) | Width | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `int16 LE` | `year` |
|
||||
| 2 | `int16 LE` | `month` |
|
||||
| 4 | `int16 LE` | `day` |
|
||||
| 6 | `int16 LE` | `hour` |
|
||||
| 8 | `int16 LE` | `minute` |
|
||||
| 10 | `int16 LE` | `second` |
|
||||
| 12 | `int16 LE` | `axis_no` (1-based; 0 = whole-CNC) |
|
||||
| 14 | `int16 LE` | `alm_type` (P/S/OT/SV/SR/MC/SP/PW/IO encoded numerically) |
|
||||
| 16 | `int16 LE` | `alm_no` |
|
||||
| 18 | `int16 LE` | `msg_len` (0..32 typical) |
|
||||
| 20 | `msg_len` | ASCII message (no null terminator) |
|
||||
| `20 + msg_len` | 0..3 | pad to 4-byte boundary so per-entry blocks stay self-delimiting |
|
||||
|
||||
The CNC stamps `year..second` in **its own local time**. The deployment
|
||||
guide instructs operators to keep CNC clocks on UTC so the projection's
|
||||
dedup key `(OccurrenceTime, AlarmNumber, AlarmType)` stays stable across
|
||||
DST transitions. The .NET decoder
|
||||
(`Wire/FocasAlarmHistoryDecoder.Decode`) constructs each
|
||||
`DateTimeOffset` with `TimeSpan.Zero` (UTC) on that assumption.
|
||||
|
||||
### Error handling
|
||||
|
||||
- A negative `num_alm` short-circuits decode to an empty list — the
|
||||
projection treats it as "no history this tick" and the next poll
|
||||
retries.
|
||||
- Malformed timestamps (e.g. month=0) are skipped per-entry instead of
|
||||
faulting the whole decode; the dedup key for malformed entries would be
|
||||
unstable anyway.
|
||||
- `msg_len` overrunning the payload truncates the entry list at the
|
||||
malformed entry rather than throwing.
|
||||
Reference in New Issue
Block a user