Sub-device contract
How PLCs derive snapshot payloads from DeviceTemplate signal definitions — binary bitmask mapping and numeric field mapping with worked examples.
A DeviceTemplate is a server-side signal catalogue that tells Voke how to interpret raw bytes arriving in a snapshot envelope. The PLC does not send decoded signal keys; it sends raw bitmasks or values fields that match the template contract configured in Voke. The decoding happens entirely in Voke's SubDeviceTelemetryService.
Template structure
A DeviceTemplate at level SUB_DEVICE defines:
type— one ofCABINET,METER,INVERTER,BATTERY. Must match thetypefield in the snapshot device entry.signals[]— array of signal definitions. Up to 256 signals per template.actions[]— command vocabulary for Voke-initiated commands (not covered here).
Each signal is either binary (resolves from a bit in raw) or numeric (resolves from a field in values).
Binary signal
{
"key": "main_contactor",
"label": "Main Contactor",
"kind": "BINARY",
"bit": 0,
"featured": true
}| Field | Description |
|---|---|
key | Snake-case identifier, unique within the template |
label | Human-readable name shown in admin UI |
kind | "BINARY" |
bit | Bit position in the sub-device's raw uint32 word (0–63) |
featured | If true, shown in default admin dashboard cards (UI only — does not affect persistence) |
Numeric signal
{
"key": "active_power_kw",
"label": "Active Power",
"kind": "NUMERIC",
"field": "activePowerKw",
"unit": "kW",
"featured": true,
"chartGroup": "Power",
"chartAxis": "left"
}| Field | Description |
|---|---|
key | Snake-case identifier, unique within the template |
label | Human-readable name |
kind | "NUMERIC" |
field | Key name to look up in the snapshot device's values map |
unit | Optional unit label (documentation only — not part of the wire format) |
featured | UI display control — does not affect what the PLC sends |
chartGroup | Optional Live tab chart panel grouping (numeric only) |
chartAxis | Optional Live tab axis (left or right, numeric only) |
bit and field are mutually exclusive. A signal is either binary (reads from raw) or numeric (reads from values), never both. Current snapshot entries are also type-specific: CABINET sends raw; METER, INVERTER, and BATTERY send values.
Binary signal mapping
The PLC packs all binary signal states into a single unsigned 32-bit integer sent as raw.
Rule: If signal at bit: N is active (on), set bit N of raw to 1. If inactive (off), set bit N to 0.
bit position: 7 6 5 4 3 2 1 0
raw value: 0 0 0 0 0 1 0 1 = 5 (decimal)
bit 0 → main_contactor = true (1)
bit 1 → fault = false (0)
bit 2 → door_open = true (1)
bits 3–7 → (not defined) = ignored by VokeBits not referenced by any signal in the template are silently ignored on the server side. You may use those bit positions for internal diagnostics or future use.
State-change storage: Voke only writes a new telemetry row for a binary signal when its value flips (0→1 or 1→0). This keeps the time-series database lean. The PLC should send every snapshot regardless — Voke handles the deduplication internally using an in-process state cache.
Numeric signal mapping
The PLC writes numeric readings into the values map under the key name that matches the template's field property.
Rule: The key in values must exactly equal the field string in the template. Case-sensitive.
// Template signal definition (stored in Voke)
{ "kind": "NUMERIC", "field": "activePowerKw", ... }
// Snapshot device entry (what the PLC sends on the wire)
{ "values": { "activePowerKw": 12.4 } }If the values key does not match any template signal's field, the value is silently ignored. There is no error — the snapshot is still accepted, and all matching signals are decoded.
Tick storage: Numeric signals write a new telemetry row on every snapshot tick. There is no deduplication for numeric values.
The featured flag
featured: true controls whether the signal appears in default admin UI cards (the SignalCard component). It has no effect on the wire format, no effect on persistence, and no effect on alerting. The PLC always sends all values — the template controls what the UI surfaces by default.
Worked example: cabinet + meter
Template (stored in Voke admin)
{
"name": "Standard Cabinet R",
"level": "SUB_DEVICE",
"type": "CABINET",
"signals": [
{ "key": "main_contactor", "label": "Main Contactor", "kind": "BINARY", "bit": 0, "featured": true },
{ "key": "fault", "label": "Fault", "kind": "BINARY", "bit": 1, "featured": true },
{ "key": "door_open", "label": "Door Open", "kind": "BINARY", "bit": 2, "featured": false }
],
"actions": []
}{
"name": "Energy Meter M",
"level": "SUB_DEVICE",
"type": "METER",
"signals": [
{ "key": "active_power_kw", "label": "Active Power", "kind": "NUMERIC", "field": "activePowerKw", "unit": "kW", "featured": true, "chartGroup": "Power" },
{ "key": "voltage_v", "label": "Voltage", "kind": "NUMERIC", "field": "voltageV", "unit": "V", "featured": true }
],
"actions": []
}What the PLC sends on the wire
{
"ts": 1713540000000,
"n": "0a1b2c3d4e5f6789",
"sig": "3d8f2a1c...",
"timestamp": "2026-04-19T14:00:00Z",
"devices": [
{
"externalId": "R1",
"type": "CABINET",
"raw": 1
},
{
"externalId": "M1",
"type": "METER",
"values": {
"activePowerKw": 12.4,
"voltageV": 231.7
}
},
{
"externalId": "BAT1",
"type": "BATTERY",
"values": {
"stateOfChargePct": 61.2,
"batteryPowerW": -820
}
}
]
}How Voke decodes it
For R1 with raw: 1 (binary 0b001):
- bit 0 = 1 →
main_contactor = true - bit 1 = 0 →
fault = false - bit 2 = 0 →
door_open = false
For M1 with the given values:
activePowerKwmatches template field → stored asactive_power_kw = 12.4voltageVmatches template field → stored asvoltage_v = 231.7
For BAT1, the same numeric-field rule applies once a BATTERY template declares fields such as stateOfChargePct and batteryPowerW.
Type-specific payloads
The current shared schema is a discriminated union over type:
CABINETentries carryrawand decode binary signals.METER,INVERTER, andBATTERYentries carryvaluesand decode numeric signals.
Do not define numeric signals on a CABINET template or binary signals on a numeric-device template for production integrations. Those signals will not be decoded by the current ingest path.
Auto-discovery
If a snapshot device entry references an externalId that does not yet exist in Voke, a placeholder SubDevice row is created automatically. The placeholder is surfaced in the admin UI; operators assign a DeviceTemplate to start decoding. Until a template is assigned, raw payloads are received but no signal rows are written.
externalId values are uppercase alphanumeric (A–Z, 0–9) plus underscore and hyphen, up to 32 characters. Convention: cabinets use R1, R2, …; meters use E1, E2, …; inverters use M1, M2, …. Match whatever IDs are enrolled in your Voke plant configuration.
Cross-links
- Snapshot envelope — the full wire format, signing, and constraints
- Concepts / Device templates — template creation and management in Voke admin
- Concepts / Signals — signal kinds, stores, and aggregates
Snapshot envelope
Wire format for sub-device telemetry — the flat JSON envelope that carries per-device bitmasks and numeric values on the telemetry topic.
Commands (receipt)
How PLC devices receive signed commands from Voke, verify them, execute, and acknowledge — including failure modes and target routing.