Files
3yearplan/schemas/docs/format-decisions.md
Joseph Doherty cd85159951 Add _base equipment-class template for universal cross-machine metadata that every machine in the OtOpcUa estate exposes regardless of vendor, protocol, or machine type. References OPC UA Companion Spec OPC 40010 (Machinery) for the Identification component (Manufacturer, Model, ProductInstanceUri, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction, ManufacturerUri, DeviceManual, AssetLocation) plus the MachineryOperationMode enum (Auto, Manual, Maintenance, Service, Setup, Other), OPC UA Part 9 for the alarm summary fields (HasActiveAlarms, ActiveAlarmCount, HighestActiveAlarmSeverity), ISO 22400 for the lifetime counter fields (TotalRunSeconds, TotalCycles) that feed Availability + Performance KPIs at Layer 3, and the 3-year-plan handoff §"Canonical Model Integration" for the canonical state vocabulary (Running / Idle / Faulted / Starved / Blocked) declared in _base.stateModel. Includes the OtOpcUa five-identifier set (EquipmentUuid, MachineCode, ZTag, SAPID, plus DeviceClass = EquipmentClassRef) so every machine surfaces the join keys downstream consumers need; ConnectionState + LastDataTimestamp + DriverType for driver-side observability that does not require any particular equipment-protocol feature; optional production context (CurrentWorkOrder, CurrentPartNumber, CurrentRecipe, CurrentOperator, CurrentShift) marked isRequired: false since not every machine type surfaces these. Plus two universal alarm definitions (communication-loss, data-stale) that apply to every equipment regardless of class.
Equipment-class.schema.json gains an `extends` field for class inheritance — child classes inherit signals, alarms, and stateModel from the parent and can add new ones or override individual entries by name. Convention: `_` prefix on classId marks an abstract base class (e.g. `_base`) intended only to be extended, not assigned directly to equipment via Equipment.EquipmentClassRef.

FANUC CNC class updated to extends: "_base"; redundant identity signals (Version, ActiveAlarmCount) removed since they're now in the base; remaining FANUC-specific signals updated with cross-references showing how they feed into the base signals at Layer 3 (RunState → canonical Running/Idle/Faulted derivation; AlarmActive → HasActiveAlarms / HighestActiveAlarmSeverity; PartsCount → TotalCycles; MainProgramNumber → CurrentRecipe).

Format-decisions.md adds D9 (rationale for `_base` + `extends` inheritance, with references to OPC 40010 / Part 9 / ISO 22400 / handoff) and D10 (signal `category` drives OPC UA folder placement, per OPC 40010 Identification + Status pattern, with a category-to-folder mapping table).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:54:17 -04:00

86 lines
7.5 KiB
Markdown

# Format Decisions
Why the schemas repo looks the way it does. Each decision is open for the schemas-repo owner team to revisit.
## D1 — JSON Schema (Draft 2020-12) as the authoring format
**Alternatives considered**: Protobuf (`.proto`), YAML, custom DSL.
**Choice**: JSON Schema.
**Why**:
- Idiomatic for .NET 10 (System.Text.Json + JsonSchema.Net) — OtOpcUa reads templates with no extra dependencies
- Idiomatic for CI tooling — every CI runner can `jq` and validate JSON Schema without extra toolchain (`ajv`, `jsonschema`, etc.)
- Best authoring experience: text format, mergeable in git, structured diffing, IDE autocomplete via the `$schema` reference
- Validation at multiple layers: operator-visible Admin UI errors in OtOpcUa, schemas-repo CI gates, downstream consumer runtime validation
- Protobuf is better for *wire* serialization (size, speed, generated code) but worse for *authoring* (binary, requires `.proto` compiler, poor merge story in git)
- Where wire-format efficiency matters (Redpanda events), we code-generate Protobuf from the JSON Schema source. One-way derivation is simpler than bidirectional sync.
## D2 — Per-class versioning, semver
**Alternatives considered**: whole-repo versioning, no versioning.
**Choice**: each class file has its own `version` field (semver); the repo also tags overall releases.
**Why**:
- Different classes evolve at different rates (FANUC CNC may stabilize while Modbus PLC catalog grows)
- Consumers can pin per-class for fine-grained compatibility (e.g. `fanuc-cnc@0.1.0` + `modbus-plc@0.3.2`)
- Repo-level tags exist to bundle a known-good combination for consumers that want one anchor
## D3 — Strict additive policy on minor bumps
**Why**: removes ambiguity. If I see `class@1.3.0`, I know its signal set is a strict superset of `class@1.0.0` (and `class@1.x.y` for any earlier x.y). Breaking changes only happen at major-version boundaries.
## D4 — `_default` reserved as placeholder for unused UNS levels
Imported from `lmxopcua/docs/v2/plan.md` decision #108.
**Why**: some sites have no Area-level distinction (single-building sites). Rather than letting the UNS path have inconsistent depth across sites, we mandate 5 levels always with `_default` as the placeholder. Downstream consumers can rely on path depth.
## D5 — Tag names use PascalCase or snake_case (class's choice), NOT UNS-segment regex
**Why**: UNS path segments (Enterprise/Site/Area/Line/Equipment) are infrastructure-level identifiers — they go on the wire of every browse, every URI, every dashboard filter. The regex (`^[a-z0-9-]{1,32}$`) reflects that constraint.
Signal names (level 6) are vocabulary-level identifiers — they live inside an equipment node. Keeping them in PascalCase or snake_case (e.g. `RunState`, `actual_feedrate`) is more readable for operators looking at OPC UA browse output, and matches OPC UA SDK conventions which expect identifier-style names rather than URL-safe slugs.
## D6 — `stateModel` is informational, not authoritative
**Why**: state derivation lives at Layer 3 (System Platform / Ignition). Placing the derivation rules in the schemas repo would create dual sources of truth (and the schemas-repo version would inevitably drift). Instead, the class template lists which states the class supports + an informational note about what the rough mapping looks like; Layer 3 owns the actual derivation logic.
## D7 — No per-equipment overrides at this layer
**Why**: per-equipment config (which specific CNC has which program, etc.) is OtOpcUa's central config DB concern. Mixing per-instance config with per-class definitions in this repo would muddy the separation and cause the repo to grow with deployment-specific data instead of staying small + reusable.
## D8 — `applicability.drivers` lists OtOpcUa drivers explicitly
**Why**: the schemas repo is OT-side-focused. The OtOpcUa driver enumeration is the closest thing to a canonical "how do you get raw data from this equipment" vocabulary that exists across the org. If a future class is populated by a non-OtOpcUa source, the field becomes optional or extends. For now, listing OtOpcUa driver IDs makes the consumer-side validation (per `lmxopcua/docs/v2/plan.md` decision #111 — driver type ↔ namespace kind) trivial.
## D9 — `_base` class with `extends` inheritance for cross-machine metadata
**Why**: every machine in the estate, regardless of vendor or protocol, exposes a common metadata core — identity (Manufacturer, Model, SerialNumber, plus the OtOpcUa five-identifier set EquipmentUuid/EquipmentId/MachineCode/ZTag/SAPID), connection state, alarm summary, optional production context (work order, recipe, operator). Repeating these in every class template would invite drift: one class would forget HighestActiveAlarmSeverity, another would name SerialNumber differently, etc.
Solution: a single `_base` class (in `classes/_base.json`) declares the common set; every other class `extends: "_base"` and inherits the signals, alarms, and stateModel. The child class adds its specifics (axes for a CNC, registers for a Modbus device, etc.) and can override individual base entries by `name` if needed.
References for the `_base` content:
- **OPC UA Companion Spec OPC 40010 (Machinery)** — for the Identification component (Manufacturer, Model, ProductInstanceUri, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction, ManufacturerUri, DeviceManual, AssetLocation) and MachineryOperationMode enum (Auto, Manual, Maintenance, Service, Setup, Other)
- **OPC UA Part 9 (Alarms & Conditions)** — for the alarm summary fields (HasActiveAlarms, ActiveAlarmCount, HighestActiveAlarmSeverity)
- **3-year-plan handoff §"Canonical Model Integration"** — for the canonical state vocabulary (Running / Idle / Faulted / Starved / Blocked) declared in `_base.stateModel`
- **ISO 22400 (Manufacturing KPIs)** — for the lifetime counter fields (TotalRunSeconds, TotalCycles) that feed Availability + Performance KPIs at Layer 3
Convention: `_` prefix on the classId marks an abstract base class — clients should not assign `_base` directly to any equipment via `Equipment.EquipmentClassRef` (it has no specifics; it's only meant to be extended).
## D10 — Signal `category` drives OPC UA exposure pattern, not just dashboard filtering
**Why**: aligning with OPC 40010 Machinery's Identification + Status + (machine-specific) folder pattern. The `category` field on each signal (`Identity`, `Status`, `Process`, `Position`, `Velocity`, `Counter`, `Alarm`, `Diagnostic`, etc.) tells the OtOpcUa NodeManager which sub-folder to place the signal in under the equipment node:
| Category | OPC UA folder | Value source |
|----------|---------------|--------------|
| `Identity` | `Identification` | Static, from `Equipment` row in central config DB (operator-set) — except where the driver can read it dynamically (e.g. FANUC `SeriesNumber` from `cnc_sysinfo()`) |
| `Status` | `Status` | Dynamic, from driver |
| `Diagnostic` | `Diagnostic` | Dynamic, from driver or from OtOpcUa runtime |
| `Alarm` | `Alarms` (synthetic — full OPC UA Part 9 alarm subscription is separate) | Dynamic |
| `Process`, `Position`, `Velocity`, `Acceleration`, `Temperature`, `Pressure`, `Flow`, `Counter`, `Setpoint`, `Command` | (machine-specific folders by category) | Dynamic, from driver |
| `Other` | `Misc` | Dynamic |
This means the same template field describes both consumer behavior (filter / categorize) and OtOpcUa exposure (which folder, which value-source pattern). Avoids a separate "where does this go in OPC UA" annotation.