DiamondLightSource / DiamondLightSource/fastcs-catio
Support multiple firmware revisions of the same terminal in one YAML
- Dominant language
- Python
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Problem
`terminal_types.yaml` is keyed by terminal name and stores one identity tuple per entry:
```yaml
EP2338-0002:
identity:
vendor_id: 2
product_code: 153239634
revision_number: 1048578 # 0x00100002
```
`get_terminal_type_by_identity(vendor, product, revision)` prefers exact `(vendor, product, revision)` match but falls back to `(vendor, product)`, picking the first hit. There is no way to ship two YAML entries for different firmware revisions of the same product, even when the bus PDO naming differs across revisions.
## How this surfaced
While fixing the EP4374-0002 AO-write bug (combo terminals using non-1-based PDO channel numbering — see uncommitted branch `issue-54-rebase`), an audit of the ESI XML cache turned up a second case where the same root cause hits *only on certain firmware revisions*:
**EP2338-0002** (8 Ch. Dig. Input/Output 24V, M12)
| Revision | RxPDO names | TxPDO names |
|---|---|---|
| `0x00100002` (pinned in YAML) | `Channel 1` .. `Channel 8` | `Channel 1` .. `Channel 8` |
| `0x00110002` | `Channel 1` .. `Channel 8` | `Channel 1` .. `Channel 8` |
| `0x00120002` and later | **`Channel 9` .. `Channel 16`** | `Channel 1` .. `Channel 8` |
A rig with firmware ≥ `0x00120002` falls through the loose-revision match, lands on the pinned `0x00100002` YAML, fabricates `Channel 1..8` symbol names for its outputs, and every AO write logs `No match for controller N and ADS Symbol 'Channel K'`. Inputs are unaffected.
The EP4374-0002 case is fixed in the same branch by adding `channel_indices: [3, 4]` to the YAML row — but that works because every revision of EP4374-0002 uses Channel 3/4 for AO. EP2338-0002 cannot be fixed that way: the pinned revision is correctly described, and editing it would break older firmware.
## Two possible directions
### Option A — Multiple YAML entries per product code (smaller)
Allow `terminal_types.yaml` to carry multiple entries that share a product code but differ on revision. Dict keys would gain a revision suffix where needed:
```yaml
EP2338-0002: # rev 0x00100002 .. 0x00110002
identity: { ..., revision_number: 1048578 }
symbol_nodes: [...]
EP2338-0002@0x00120002: # rev 0x00120002 onward
identity: { ..., revision_number: 1179650 }
symbol_nodes:
- name_template: Channel {channel}
channels: 8
channel_indices: [9, 10, 11, 12, 13, 14, 15, 16]
...
```
Change `get_terminal_type_by_identity` from "first (vendor, product) hit wins" to "pick the entry with the highest revision ≤ slave's revision" so newer firmware degrades to the closest documented version.
The IOC's per-slave identity lookup (`expand_symbols_for_slave(slave)`) already runs once per slave, so a chain with mixed-firmware EP2338-0002 modules would route each slave to the right YAML automatically.
### Option B — Embed revisions inside one entry
Add a `revisions: [{revision_number, row_overrides}]` block inside the existing terminal entry. The base entry stays as-is; overrides patch specific rows for newer firmwares. More compact for cases where only a couple of rows change, but introduces a new "inherit + patch" schema and the diffing/editing in the GUI editor (`catio-terminals edit`) becomes more complex.
## Recommendation
**Option A** — the schema stays a flat dict of `TerminalType`, the identity lookup change is local, and the GUI editor needs no understanding of revision inheritance. The cost is some duplicated CoE blocks across revisions of the same product; in practice PDO renames are the much rarer change, so most products will only ever have one entry.
Until this is implemented, the workaround is to manually keep the YAML's pinned revision matched to the actual rig firmware — but that breaks for chains with mixed firmware versions of the same product.
## Files in scope
- `src/catio_terminals/models.py` — `TerminalConfig.terminal_types` stays `dict[str, TerminalType]`; consider whether to enforce or just document the `Name@0xRRRRRRRR` key convention
- `src/fastcs_catio/terminal_config.py::get_terminal_type_by_identity` — change loose-match policy to "highest revision ≤ slave's revision"
- `tests/test_symbol_expansion.py::TestGetTerminalTypeByIdentity` — add cases for multi-revision selection
- `src/catio_terminals/terminals/terminal_types.yaml` — add the EP2338-0002 ≥ `0x00120002` entry once the lookup supports it
## Audit data
Script: replay `catio_terminals.xml.pdo.extract_channel_pattern` over every `/` `` in `/root/.cache/catio_terminals/beckhoff_xml/*.xml`, dedup per `(terminal, kind, pattern)` with `sorted(set(channels))`, flag groups where the unique indices aren't `[1..N]`.
In the current ESI cache, EP2338-0002 is the only shipped terminal whose PDO channel numbers drift across firmware revisions. Worth re-running the audit next time the XML cache is refreshed (`uv run catio-terminals update-cache`) to see if more cases appear.
## Related
- #54 — drove the YAML-based symbol expansion this issue extends
- #58 — also extends per-slave granularity (PDO group per slave instance)
Contributor guide
Assessment
This issue has not been assessed yet.