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>
7.5 KiB
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
jqand validate JSON Schema without extra toolchain (ajv,jsonschema, etc.) - Best authoring experience: text format, mergeable in git, structured diffing, IDE autocomplete via the
$schemareference - 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
.protocompiler, 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.