This is a development version of the documentation. Content may change without notice.
Voke Documentation
PLC / Devices

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 of CABINET, METER, INVERTER, BATTERY. Must match the type field 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
}
FieldDescription
keySnake-case identifier, unique within the template
labelHuman-readable name shown in admin UI
kind"BINARY"
bitBit position in the sub-device's raw uint32 word (0–63)
featuredIf 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"
}
FieldDescription
keySnake-case identifier, unique within the template
labelHuman-readable name
kind"NUMERIC"
fieldKey name to look up in the snapshot device's values map
unitOptional unit label (documentation only — not part of the wire format)
featuredUI display control — does not affect what the PLC sends
chartGroupOptional Live tab chart panel grouping (numeric only)
chartAxisOptional 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 Voke

Bits 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.


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:

  • activePowerKw matches template field → stored as active_power_kw = 12.4
  • voltageV matches template field → stored as voltage_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:

  • CABINET entries carry raw and decode binary signals.
  • METER, INVERTER, and BATTERY entries carry values and 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.


On this page