diff --git a/schemas/classes/_base.json b/schemas/classes/_base.json new file mode 100644 index 0000000..49f49a0 --- /dev/null +++ b/schemas/classes/_base.json @@ -0,0 +1,265 @@ +{ + "$schema": "../format/equipment-class.schema.json", + "classId": "_base", + "extends": null, + "version": "0.1.0", + "displayName": "Base Equipment", + "description": "Universal metadata that every equipment in the OtOpcUa estate exposes regardless of vendor, protocol, or machine type. References: OPC UA Companion Spec OPC 40010 (Machinery) for the Identification component, ISO 22400 for KPI inputs, OPC UA Part 9 for the alarm summary fields, the 3-year-plan handoff §Canonical Model Integration for the canonical state vocabulary. Every equipment-class template should `extends: \"_base\"`; consumers (OtOpcUa, Redpanda, dbt) can rely on every machine surfacing this set.", + "applicability": {}, + "signals": [ + { + "name": "ProductInstanceUri", + "displayName": "Product Instance URI", + "category": "Identity", + "dataType": "String", + "description": "Globally unique URI identifying this specific physical asset (OPC 40010 ProductInstanceUri). Default derivation: `urn:otopcua:equipment:{EquipmentUuid}`. Operators can override per equipment if a vendor-issued URI exists.", + "isRequired": true + }, + { + "name": "Manufacturer", + "displayName": "Manufacturer", + "category": "Identity", + "dataType": "String", + "description": "Equipment manufacturer name (OPC 40010). Static; operator-set at equipment registration.", + "isRequired": true + }, + { + "name": "Model", + "displayName": "Model", + "category": "Identity", + "dataType": "String", + "description": "Equipment model designator (OPC 40010). Static; operator-set.", + "isRequired": true + }, + { + "name": "SerialNumber", + "displayName": "Serial Number", + "category": "Identity", + "dataType": "String", + "description": "Manufacturer-assigned serial number (OPC 40010). Optional but strongly recommended.", + "isRequired": false + }, + { + "name": "MachineCode", + "displayName": "Machine Code", + "category": "Identity", + "dataType": "String", + "description": "Operator colloquial identifier (e.g. `machine_001`). Sourced from `Equipment.MachineCode` in the OtOpcUa central config DB.", + "isRequired": true + }, + { + "name": "ZTag", + "displayName": "ZTag (ERP ID)", + "category": "Identity", + "dataType": "String", + "description": "ERP equipment identifier. Sourced from `Equipment.ZTag` in the central config DB. Optional in OtOpcUa schema; required in equipment templates that depend on ERP integration.", + "isRequired": false + }, + { + "name": "SAPID", + "displayName": "SAP PM ID", + "category": "Identity", + "dataType": "String", + "description": "SAP Plant Maintenance equipment identifier. Sourced from `Equipment.SAPID`. Optional.", + "isRequired": false + }, + { + "name": "EquipmentUuid", + "displayName": "Equipment UUID", + "category": "Identity", + "dataType": "String", + "description": "Immutable UUIDv4 identifying this equipment across systems and time. The permanent join key for downstream events / dbt / Redpanda. Sourced from `Equipment.EquipmentUuid`.", + "isRequired": true + }, + { + "name": "HardwareRevision", + "displayName": "Hardware Revision", + "category": "Identity", + "dataType": "String", + "description": "Hardware revision (OPC 40010). Optional.", + "isRequired": false + }, + { + "name": "SoftwareRevision", + "displayName": "Software Revision", + "category": "Identity", + "dataType": "String", + "description": "Firmware / software revision (OPC 40010). Optional. Some drivers can read this dynamically (FANUC FOCAS via cnc_sysinfo, Beckhoff via TwinCAT.SystemInfo); others rely on operator-set static values.", + "isRequired": false + }, + { + "name": "DeviceClass", + "displayName": "Device Class", + "category": "Identity", + "dataType": "String", + "description": "Equipment class identifier — same value as `Equipment.EquipmentClassRef`. Lets clients classify equipment without a separate config lookup. Required so consumers can route by class.", + "isRequired": true + }, + { + "name": "AssetLocation", + "displayName": "Asset Location", + "category": "Identity", + "dataType": "String", + "description": "Free-text physical location supplementary to the UNS path (e.g. `Bay 3, Row 12`). Optional.", + "isRequired": false + }, + { + "name": "YearOfConstruction", + "displayName": "Year of Construction", + "category": "Identity", + "dataType": "Int16", + "description": "Year the equipment was manufactured (OPC 40010 YearOfConstruction). Optional.", + "isRequired": false + }, + { + "name": "ManufacturerUri", + "displayName": "Manufacturer URI", + "category": "Identity", + "dataType": "String", + "description": "Manufacturer's website / namespace URI (OPC 40010). Optional.", + "isRequired": false + }, + { + "name": "DeviceManual", + "displayName": "Device Manual", + "category": "Identity", + "dataType": "String", + "description": "URL or path to operator/maintenance manual for this equipment (OPC 40010). Optional.", + "isRequired": false + }, + { + "name": "DriverType", + "displayName": "Driver Type", + "category": "Diagnostic", + "dataType": "String", + "description": "OtOpcUa driver type populating this equipment (Galaxy / ModbusTcp / AbCip / AbLegacy / S7 / TwinCat / Focas / OpcUaClient). Sourced from the driver instance.", + "isRequired": true + }, + { + "name": "ConnectionState", + "displayName": "Connection State", + "category": "Status", + "dataType": "Int32", + "description": "Driver-side connection state to the underlying equipment. 0 = Disconnected, 1 = Connected, 2 = Reconnecting, 3 = Faulted. Always available from the driver — does not require the equipment to support any particular protocol feature.", + "isRequired": true + }, + { + "name": "LastDataTimestamp", + "displayName": "Last Data Timestamp", + "category": "Status", + "dataType": "DateTime", + "description": "UTC timestamp of the most recent successful data read from this equipment. Updated by the driver on every successful read regardless of value change. Lets consumers detect silent data loss even when ConnectionState reports Connected.", + "isRequired": true + }, + { + "name": "OperationMode", + "displayName": "Operation Mode", + "category": "Status", + "dataType": "String", + "description": "Operating mode per OPC 40010 MachineryOperationMode enum: Auto, Manual, Maintenance, Service, Setup, Other. Optional — many machine types (e.g. instruments) don't have an operation mode.", + "isRequired": false + }, + { + "name": "HasActiveAlarms", + "displayName": "Has Active Alarms", + "category": "Alarm", + "dataType": "Boolean", + "description": "True when one or more alarms are currently active on this equipment. Quick-check field — full alarm detail comes via OPC UA Part 9 alarm subscription.", + "isRequired": true + }, + { + "name": "ActiveAlarmCount", + "displayName": "Active Alarm Count", + "category": "Alarm", + "dataType": "Int32", + "description": "Number of currently active alarms on this equipment. 0 when none. Required even for machines that don't natively support alarms (always 0 in that case).", + "isRequired": true + }, + { + "name": "HighestActiveAlarmSeverity", + "displayName": "Highest Active Alarm Severity", + "category": "Alarm", + "dataType": "String", + "description": "Severity of the most severe currently active alarm: None / Low / Medium / High / Critical. Lets dashboards filter equipment with critical alarms without subscribing to full alarm streams.", + "isRequired": true + }, + + { + "name": "TotalRunSeconds", + "displayName": "Total Run Seconds (Lifetime)", + "category": "Counter", + "dataType": "Int64", + "unit": "s", + "description": "Lifetime total seconds the equipment has been in Running state. Optional — many equipment types don't track this; for those that do, value is monotonically increasing across power cycles.", + "isRequired": false, + "isHistorized": true + }, + { + "name": "TotalCycles", + "displayName": "Total Cycles (Lifetime)", + "category": "Counter", + "dataType": "Int64", + "description": "Lifetime total operation cycles (parts produced, machining cycles, packaging cycles, etc. — class-specific definition). Optional.", + "isRequired": false, + "isHistorized": true + }, + { + "name": "CurrentWorkOrder", + "displayName": "Current Work Order", + "category": "Process", + "dataType": "String", + "description": "Currently-running work order / job number / batch ID. Often sourced via Layer 3 (System Platform) push to the equipment, or via direct ERP integration. Optional.", + "isRequired": false + }, + { + "name": "CurrentPartNumber", + "displayName": "Current Part Number", + "category": "Process", + "dataType": "String", + "description": "Currently-running part / product number. Optional.", + "isRequired": false + }, + { + "name": "CurrentRecipe", + "displayName": "Current Recipe", + "category": "Process", + "dataType": "String", + "description": "Currently-loaded recipe / program identifier. Optional.", + "isRequired": false + }, + { + "name": "CurrentOperator", + "displayName": "Current Operator", + "category": "Process", + "dataType": "String", + "description": "Currently-logged-in operator on this equipment (where supported). Optional. Privacy: in some jurisdictions, operator names are personal data — surface as operator badge ID rather than name where applicable.", + "isRequired": false + }, + { + "name": "CurrentShift", + "displayName": "Current Shift", + "category": "Process", + "dataType": "String", + "description": "Current shift identifier (A/B/C, Day/Night/Swing, etc. — site-specific). Optional.", + "isRequired": false + } + ], + "alarms": [ + { + "alarmId": "communication-loss", + "displayName": "Communication Loss", + "severity": "High", + "description": "Driver lost communication with the equipment. Raised when ConnectionState transitions away from Connected; cleared when restored. Universal across all equipment classes." + }, + { + "alarmId": "data-stale", + "displayName": "Data Stale", + "severity": "Medium", + "description": "ConnectionState reports Connected but LastDataTimestamp has not advanced beyond the driver's expected polling interval. Indicates silent data loss. Universal." + } + ], + "stateModel": { + "states": ["Running", "Idle", "Faulted", "Starved", "Blocked"], + "derivationNotes": "Canonical machine state vocabulary per the 3-year-plan handoff §Canonical Model Integration. State derivation lives at Layer 3 (Aveva System Platform / Ignition) — OtOpcUa exposes the raw signals (RunState if available from the equipment, ConnectionState, HasActiveAlarms, OperationMode, plus class-specific signals like cycle-in-progress flags) and Layer 3 derives the canonical state. Reserved for future addition to the standard set: Changeover, Maintenance, Setup." + } +} diff --git a/schemas/classes/fanuc-cnc.json b/schemas/classes/fanuc-cnc.json index 57303d0..7dc42c8 100644 --- a/schemas/classes/fanuc-cnc.json +++ b/schemas/classes/fanuc-cnc.json @@ -1,25 +1,31 @@ { "$schema": "../format/equipment-class.schema.json", "classId": "fanuc-cnc", + "extends": "_base", "version": "0.1.0", "displayName": "FANUC CNC", - "description": "FANUC CNC machine tools (mills, lathes, machining centers) accessed via FOCAS2. Pilot equipment class per OtOpcUa corrections doc D1 — chosen because the FOCAS driver already exposes a fixed pre-defined node hierarchy (lmxopcua/docs/v2/driver-specs.md §7) which is essentially a class template already; this file formalizes it.", + "description": "FANUC CNC machine tools (mills, lathes, machining centers) accessed via FOCAS2. Extends `_base` for cross-machine identity / state / alarm metadata; adds CNC-specific axis, spindle, program, and PMC signals. Pilot equipment class per OtOpcUa corrections doc D1 — chosen because the FOCAS driver already exposes a fixed pre-defined node hierarchy (lmxopcua/docs/v2/driver-specs.md §7) which is essentially a class template already; this file formalizes it.", "vendor": "FANUC", "applicability": { "drivers": ["Focas"], "models": ["0i-F", "0i-D", "30i-B", "31i-B", "32i-B"] }, "signals": [ - { "name": "SeriesNumber", "category": "Identity", "dataType": "String", "description": "CNC series (e.g. '0i-F'). From cnc_sysinfo()." }, - { "name": "Version", "category": "Identity", "dataType": "String", "description": "CNC software version. From cnc_sysinfo()." }, + { + "name": "SeriesNumber", + "category": "Identity", + "dataType": "String", + "description": "CNC series read dynamically from the controller (e.g. '0i-F'). From cnc_sysinfo(). May differ from the operator-set Model if firmware reports a different series — useful for catching configuration drift.", + "isRequired": true + }, { "name": "MaxAxes", "category": "Identity", "dataType": "Int32", "description": "Maximum axes supported by this CNC. From cnc_sysinfo()." }, { "name": "AxisCount", "category": "Identity", "dataType": "Int32", "description": "Active axis count. From cnc_rdaxisname()." }, - { "name": "RunState", "category": "Status", "dataType": "Int32", "description": "0=STOP, 1=HOLD, 2=START, 3=MSTR. From cnc_statinfo()." }, - { "name": "Mode", "category": "Status", "dataType": "Int32", "description": "0=MDI, 1=AUTO, 3=EDIT, 4=HANDLE, 5=JOG, 7=REF. From cnc_statinfo()." }, + { "name": "RunState", "category": "Status", "dataType": "Int32", "description": "FANUC native run state — 0=STOP, 1=HOLD, 2=START, 3=MSTR. From cnc_statinfo(). Layer 3 derives the canonical Running/Idle/Faulted state (in `_base.stateModel`) primarily from this signal." }, + { "name": "Mode", "category": "Status", "dataType": "Int32", "description": "0=MDI, 1=AUTO, 3=EDIT, 4=HANDLE, 5=JOG, 7=REF. From cnc_statinfo(). Maps to `_base.OperationMode` enum at Layer 3." }, { "name": "MotionState", "category": "Status", "dataType": "Int32", "description": "0=idle, 1=MOTION, 2=DWELL, 3=WAIT. From cnc_statinfo()." }, { "name": "EmergencyStop", "category": "Status", "dataType": "Boolean", "description": "From cnc_statinfo()." }, - { "name": "AlarmActive", "category": "Status", "dataType": "Int32", "description": "Bitmask of active alarm categories. From cnc_statinfo()." }, + { "name": "AlarmActive", "category": "Status", "dataType": "Int32", "description": "Bitmask of active alarm categories. From cnc_statinfo(). Layer 3 derives `_base.HasActiveAlarms` and `_base.HighestActiveAlarmSeverity` from this and cnc_rdalmmsg() detail." }, { "name": "AxisAbsolutePosition", @@ -67,13 +73,11 @@ "description": "Per-spindle load percentage. From cnc_rdspmeter(). FOCAS2." }, - { "name": "MainProgramNumber", "category": "Process", "dataType": "Int32", "description": "From cnc_rdprgnum()." }, + { "name": "MainProgramNumber", "category": "Process", "dataType": "Int32", "description": "From cnc_rdprgnum(). Source for `_base.CurrentRecipe` at Layer 3 if the site uses program numbers as recipes." }, { "name": "RunningProgramNumber", "category": "Process", "dataType": "Int32", "description": "From cnc_rdprgnum()." }, { "name": "RunningProgramName", "category": "Process", "dataType": "String", "description": "Full program path/name. From cnc_exeprgname(). FOCAS2." }, { "name": "SequenceNumber", "category": "Process", "dataType": "Int32", "description": "Current N-number. From cnc_rdseqnum()." }, - { "name": "PartsCount", "category": "Counter", "dataType": "Int64", "description": "Parts produced. From cnc_rdparam(6711/6712)." }, - - { "name": "ActiveAlarmCount", "category": "Alarm", "dataType": "Int32", "description": "Number of active alarms. From cnc_rdalmmsg(). FOCAS2." } + { "name": "PartsCount", "category": "Counter", "dataType": "Int64", "description": "Parts produced. From cnc_rdparam(6711/6712). Source for `_base.TotalCycles` at Layer 3." } ], "alarms": [ { "alarmId": "ps-alarm", "displayName": "Program/Setup Alarm", "severity": "High", "description": "P/S alarm category." }, diff --git a/schemas/docs/format-decisions.md b/schemas/docs/format-decisions.md index c7b0329..3dd9596 100644 --- a/schemas/docs/format-decisions.md +++ b/schemas/docs/format-decisions.md @@ -54,3 +54,32 @@ Signal names (level 6) are vocabulary-level identifiers — they live inside an ## 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. diff --git a/schemas/format/equipment-class.schema.json b/schemas/format/equipment-class.schema.json index b8b8aa4..0225264 100644 --- a/schemas/format/equipment-class.schema.json +++ b/schemas/format/equipment-class.schema.json @@ -9,8 +9,13 @@ "properties": { "classId": { "type": "string", - "pattern": "^[a-z0-9-]{1,64}$", - "description": "Stable logical ID for the class. Lowercase, hyphens allowed. Once published, never changes — clients pin to it." + "pattern": "^[a-z0-9-_]{1,64}$", + "description": "Stable logical ID for the class. Lowercase, hyphens, underscores allowed. Once published, never changes — clients pin to it. Convention: `_` prefix indicates an abstract base class (e.g. `_base`) intended only to be extended, not used directly on equipment." + }, + "extends": { + "type": ["string", "null"], + "pattern": "^[a-z0-9-_]{1,64}$", + "description": "Optional parent class to inherit from. Inherited signals, alarms, and stateModel are merged into this class — the child can add new ones and override individual entries by `name`. The `_base` class (which every equipment template should extend) provides the universal cross-machine metadata (identity, state, alarms, optional production context)." }, "version": { "type": "string",