Creating a Device Type
This page walks through authoring a device type end to end, using the real
consystence.pump.centrifugal package that ships in the Consystence equipment
library. A type is a reusable definition — its tags, states, calculations,
faceplates, alarms and commands are written once and then bound to many
per-site instances (each with its own AssetNumber, e.g. PP-01). For the
type-versus-instance distinction, see the type system.
Anatomy of a type package
Section titled “Anatomy of a type package”A type is a folder of YAML, keyed by its typeId. The centrifugal pump looks
like this:
consystence.pump.centrifugal/├── manifest.yaml # typeId, tags, states, calculations, sceneBindings, aiContext├── simulation.yaml # optional deterministic sim profile (never writes Output tags)├── components/│ ├── faceplate.yaml # full operator control + monitoring screen│ └── status-card.yaml # compact overview card├── alarms/│ └── alarms.yaml└── commands/ └── commands.yamlmanifest.yaml is the spine — every other file references the tags, states and
calculations it declares. We will build the manifest first, then layer the
components, commands and alarms on top.
Step 1 — Manifest header
Section titled “Step 1 — Manifest header”Open manifest.yaml and declare the identity and metadata. The typeId is the
dotted, reverse-domain identifier; it is the stable key the platform and the
marketplace use, so it never changes once published.
typeId: consystence.pump.centrifugalcontrolModel: peer # control-routing model for this typeversion: "1.0.0"name: Centrifugal Pumpdescription: > A general-purpose centrifugal pump with VFD speed control, including monitoring of discharge pressure, flow rate, and motor parameters. Supports start/stop control with interlock checking and fault handling.publisher: Consystencelicense: MITcategory: Pumpsplatform: ">=1.0.0"Quote version and platform so YAML reads them as strings, not numbers.
controlModel: peer declares how the device participates in control resolution —
a property of the type, not a per-instance choice. The field is live on the
manifest: peer routes instances to the peer control model, while omitting it
(or declaring legacy) keeps the combined routing — a library update alone can
change it, with no platform release.
Step 2 — Declaring tags
Section titled “Step 2 — Declaring tags”Tags are the data points the type exposes. Casing matters: dataType and
access use the platform’s canonical capitalisation. Declare a physicalRole
on every tag — it defaults to Input when omitted, but declaring it explicitly
keeps the ownership split below legible. Here are real tags from the manifest,
covering each shape you will need:
tags: - name: Running dataType: Bool access: Read physicalRole: Output description: Pump is running (motor energised). Latched by the vPLC's start logic from StartCommand + interlocks.
- name: Speed dataType: Real access: ReadWrite physicalRole: Output unit: rpm rangeMin: 0 rangeMax: 3600 description: Current motor speed. Ramped by the vPLC's control logic toward SpeedSetpoint when Running.
- name: SpeedSetpoint dataType: Real access: ReadWrite physicalRole: Input unit: rpm rangeMin: 0 rangeMax: 3600 description: Speed setpoint for VFD. Operator/AI writes; vPLC reads to drive ramp logic.
- name: BearingTemp dataType: Real access: Read physicalRole: Input unit: degC description: Bearing temperature. Simulator computes from thermal model; vPLC sees as a sensor reading.
- name: FaultCode dataType: Int access: Read physicalRole: Output description: Fault code (0=none, 1=overload, 2=bearing, 3=seal, 4=cavitation). Written by vPLC when fault active.Field-by-field:
| Field | Allowed values (this manifest) | Notes |
|---|---|---|
dataType | Bool, Int, Real (also DInt, String) | Capitalised; mirrors the controller types |
access | Read, ReadWrite (also Write) | The UI-facing read/write capability |
physicalRole | Input, Output (also Internal, System) | Recommended on every tag (defaults to Input if omitted) — direction from the controller’s point of view |
unit | rpm, bar, m3/h, A, degC | Engineering unit for display and trending |
rangeMin / rangeMax | e.g. 0 / 3600 | Drives gauge scaling and validation |
The key idea is that physicalRole is independent of access. Speed is
ReadWrite in the UI but physicalRole: Output because the control logic owns
it; SpeedSetpoint is also ReadWrite in the UI but physicalRole: Input
because the controller reads it. Input tags are sensor readings and external
signals (DischargePressure, FlowRate, MotorCurrent, BearingTemp,
Interlock, SpeedSetpoint); output tags are owned by the control logic
(Running, Speed, Fault, FaultCode, LocalMode, AutoMode,
SetpointRangeFault). This ownership split is what lets a simulation profile or
a real controller drive the same type without conflict:
flowchart LR
Field["Field sensors / simulator<br/>physicalRole: Input"] -->|readings + signals| DI["Device instance<br/>tag cache"]
vPLC["Control logic / vPLC<br/>physicalRole: Output"] -->|Running, Speed, Fault| DI
DI -->|"reads via {tags.X}"| UI["Faceplate + scene<br/>UI session"]
UI -->|"commands -> tagWrites"| DI
For the full data-type and access reference, see Tags. How those tags actually reach a physical device — the edge runtime and the PLCnext gateway — is covered in PLC communication.
Step 3 — States
Section titled “Step 3 — States”States are derived labels evaluated from tag conditions. Each state lists one or
more boolean conditions (all must hold) and a color. Order matters — the
first matching state wins, so put the most specific or most severe first:
states: - name: Stopped conditions: - "!{tags.Running}" - "!{tags.Fault}" color: slate
- name: Running conditions: - "{tags.Running}" - "!{tags.Fault}" color: emerald
- name: Faulted conditions: - "{tags.Fault}" color: red
- name: Interlocked conditions: - "{tags.Interlock}" - "!{tags.Running}" color: amber
- name: Local conditions: - "{tags.LocalMode}" color: blueThe resolved state surfaces in the UI as {state.name} and {state.color},
which the faceplate uses for its status banner.
Step 4 — Calculations
Section titled “Step 4 — Calculations”Calculations are read-only derived values defined by an expression over tag
bindings. They are addressable from components as {calculations.X}:
calculations: - name: HydraulicPower expression: "({tags.FlowRate} * {tags.DischargePressure}) / 36" unit: kW description: Calculated hydraulic power output
- name: Efficiency expression: "(({tags.FlowRate} * {tags.DischargePressure}) / 36) / ({tags.MotorCurrent} * 0.4) * 100" unit: "%" description: Estimated pump efficiencyQuote "%" so YAML does not treat it specially.
Step 5 — Scene bindings
Section titled “Step 5 — Scene bindings”sceneBindings are per-type aliases consumed by the process-scene resolver. Each
binding gives the scene layout a stable name that the resolver joins with a
placement prefix to form the full {tags.<prefix><name>} expression — so a P&ID
scene can reference “the pump’s running indicator” without hard-coding tag names:
sceneBindings: - name: Running tag: Running description: Pump running indicator for the P&ID body colour - name: Speed tag: Speed description: Pump speed readout - name: Fault tag: Fault description: Fault banner visibility - name: Current tag: MotorCurrent description: Motor current readout - name: AutoMode tag: AutoMode description: Auto/manual mode indicatorStep 6 — Component faceplate
Section titled “Step 6 — Component faceplate”Components live under components/ and describe a semantic UI tree — you
author the meaning, not the markup. The tree is designed to project
to multiple renderers from one definition: the browser and the CLI are peer
renderers of the same JSON wire format (the legacy HTML shell is being
removed), and WinUI3 is a planned target.
components/faceplate.yaml uses four binding namespaces — {tags.X},
{calculations.X}, {state.X} and {instance.X}:
componentType: faceplatename: Pump Faceplatedescription: Full control and monitoring faceplate for centrifugal pump
layout: type: card props: title: "{instance.name}" subtitle: "Centrifugal Pump" statusColor: "{state.color}" statusText: "{state.name}" children: - type: gauge props: value: "{tags.Speed}" min: 0 max: 3600 unit: rpm label: Speed
- type: metric props: value: "{calculations.HydraulicPower}" unit: kW label: Power precision: 1
- type: slider props: value: "{tags.SpeedSetpoint}" min: 0 max: 3600 step: 100 unit: rpm label: Speed Setpoint disabled: "{!tags.AutoMode}"
- type: button props: label: Start command: start variant: primary disabled: "{tags.Running || tags.Fault || tags.Interlock}"Bindings can be inverted ({!tags.AutoMode}) and combined with boolean
operators ({tags.Running || tags.Fault || tags.Interlock}) directly inside
prop expressions — that is how the setpoint slider is disabled outside auto mode
and the Start button is disabled while running, faulted or interlocked. The
compact status-card.yaml follows the same model with clickAction: open-faceplate to drill into the full faceplate.
Step 7 — Commands
Section titled “Step 7 — Commands”Commands live under commands/ and are the only sanctioned way the UI
writes tags. Each command declares preconditions (guards with operator-facing
messages), tagWrites, and an audit flag. Parameterised commands reference
their arguments with {params.X}:
- id: set-speed name: Set Speed description: Sets the speed setpoint confirm: false audit: true parameters: - name: speed dataType: Real min: 0 max: 3600 unit: rpm description: Target speed in RPM preconditions: - condition: "!{tags.LocalMode}" message: Pump is in local mode tagWrites: - tagName: SpeedSetpoint value: "{params.speed}"
- id: start name: Start Pump description: Starts the pump at current speed setpoint confirm: true confirmMessage: "Start {instance.name}?" audit: true preconditions: - condition: "!{tags.Running}" message: Pump is already running - condition: "!{tags.Fault}" message: Clear fault before starting - condition: "!{tags.Interlock}" message: Safety interlock is active - condition: "!{tags.LocalMode}" message: Pump is in local mode tagWrites: - tagName: Running value: "true"Note that start only writes the Running tag and relies on preconditions
plus the controller’s start logic for interlocks. Preconditions are
server-enforced operability gates, evaluated fail-closed against good-quality
tag data (missing or degraded data rejects the command) — but still not a
safety function; interlocks remain in the controller.
Alarms follow the same pattern under alarms/ (condition + priority + message);
they are covered in detail in Alarms and commands.
Step 8 — Simulation profile (optional)
Section titled “Step 8 — Simulation profile (optional)”simulation.yaml is an optional deterministic profile that lets the type run
without hardware. By design it never writes output tags — those stay owned
by the control logic; the profile drives input tags (sensor readings and
external signals) and, where declared, simulator-only Internal tags:
schemaVersion: 1deviceType: consystence.pump.centrifugaldeviceTypeVersion: ">=1.0.0"
initialValues: SpeedSetpoint: 75 MotorCurrent: 0 BearingTemp: 32 FlowRate: 0 DischargePressure: 0 Interlock: false
stateScopedDynamics: - state: Running tag: FlowRate model: firstOrderLag parameters: steadyState: "Speed / 3600 * 120" timeConstant: 2.0Every tag a profile writes must not be declared physicalRole: Output in
the manifest — the validator rejects simulator writes to controller-owned
outputs. Profiles write Input tags (and Internal tags, for model-internal
state); that alignment with Step 2 is what makes this profile valid.
For the full profile schema — dynamics models, disturbances, failure modes and validation rules — see Simulation profiles.
Validate and next steps
Section titled “Validate and next steps”Once the package is assembled, validate it before publishing:
- Confirm
dataType,accessandphysicalRolecasing on every tag. - Check that all
{tags.X},{calculations.X}andsceneBindingsnames resolve to declared entries. - Verify command
tagWritesonly target tags that exist, and that simulation profiles never writeOutputtags.
To go deeper:
- Tags — data types, access levels and ranges in full.
- Alarms and commands — alarm rules and the command model.
- Type system — types versus instances, and the engineering model.