Skip to content

SenML Message Format

Py ToloMEO uses a SenML-inspired JSON format (RFC 8428) for all messages exchanged over NATS and HTTP.

Record Structure

A message is a JSON array containing one SenML record:

[
  {
    "bn": "urn:cpt:device:sn:abc123:",
    "n": "CommandName",
    "vs": "{\"id\": \"swupdate\", \"fw_version\": \"1.2.3\"}",
    "t": 1700000010.5
  }
]

Fields

Field Type Required Description
bn string NATS only Base name — device URN prefix (urn:cpt:device:sn:<serial>:)
n string Yes Name — command name or metric identifier
vs string For commands JSON-encoded string payload. Contains the command parameters dict.
v number For readings Numeric sensor value
u string No Unit of measure (e.g. "°C")
t number No Unix timestamp in seconds (float)

Command Messages

Commands use n for the command class name and vs for the JSON-encoded payload:

[{"bn":"", "t":1700000010.5, "n":"StartOTADownload", "vs":"{\"id\":\"swupdate\"}"}]

Note

vs is a JSON-encoded string, not a direct JSON object. The inner dict must be serialised to a string before being placed in vs.

Sensor Data Messages

Sensor readings use n for the metric name, v for the value, and u for the unit:

[{"bn":"urn:cpt:device:sn:abc123:", "n":"temperature_sensor:temperature", "u":"°C", "v":23.5, "t":1700000010.0}]

Command Response Messages

Responses are published to events.params with vs containing the JSON-encoded result dict:

[{"bn":"urn:cpt:device:sn:abc123:", "n":"GetOTAStatus",
  "vs":"{\"id\": \"swupdate\", \"result\": \"success\", \"status\": \"update_ready\"}"}]

Command Definition Records

Every service announces the commands it accepts as a commands record on events.params, sent once after the service is set up. vs holds a JSON-encoded object with the service commands and the commands of its plugins:

[{"bn": "urn:cpt:device:sn:abc123:", "n": "commands", "vs": "<json string of the object below>"}]

The example is abridged: only some commands are shown, and the title and description keys Pydantic adds are omitted; clients can ignore them.

{
  "service": {
    "GetServiceSettings": {"type": "object", "properties": {}},
    "ConnectDevice": {
      "type": "object",
      "properties": {
        "address": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": "Unknown"}
      }
    }
  },
  "plugin": {
    "type": "BLESensorPlugin",
    "commands": {
      "BLEConnect": {
        "type": "object",
        "properties": {"id": {"type": "string"}},
        "required": ["id"]
      },
      "BLEDiscover": {
        "type": "object",
        "properties": {"id": {"type": "string"}, "address": {"type": "string", "minLength": 1}},
        "required": ["id", "address"]
      }
    }
  }
}
Key Description
service Service commands, keyed by command class name
plugin.type Class name of the plugin the service manages
plugin.commands Plugin commands, keyed by command class name. All plugins of a service share them
  • Each key is the string a client puts in n to invoke the command.
  • Each value is the JSON schema of the command's payload, as produced by the command's Pydantic model. A command that declares no model is not validated and is described as {"type": "object", "properties": {}}.
  • Plugin schemas always include a required id string property: it selects the target plugin. The allowed values are not listed in the schema; they are in the plugins record.
  • Because the schemas depend only on code, a service with no plugin attached still announces its plugin commands.

Plugin Topology Records

A plugins record lists the plugins a service currently has attached:

[{"bn": "urn:cpt:device:sn:abc123:", "n": "plugins",
  "vs": "{\"type\": \"BLESensorPlugin\", \"ids\": [\"AA:BB:CC:DD:EE:FF\"]}"}]
  • type is the plugin class name, repeated so that each record stands alone.
  • ids are the ids of the attached plugins, sorted — the values a client puts in id to target a plugin. They are the same values ListPluginIds returns.
  • The record is sent at startup, possibly with "ids": [], and after every plugin attach and detach. Changes close together may be merged into one record; the last record always reflects the final state, including "ids": [] once the last plugin detaches.

On-demand counterparts

The info commands return the same schemas on request. The existing commands key is unchanged; schemas is added next to it:

Command Reply
ListServiceCommands {"commands": [names], "schemas": {name: schema}}
ListPluginCommands {"id": ..., "commands": [names], "schemas": {name: schema}}
ListPluginIds {"ids": [...]}

The schemas objects match the corresponding parts of the commands record; plugin schemas include the id property.

Command Error Messages

A command whose payload does not match its model is rejected before it runs, and the reply is an error record. vs names the command and each offending field:

[{"bn": "urn:cpt:device:sn:abc123:", "n": "error",
  "vs": "Invalid payload for 'BLEDiscover': address: Field required"}]

Commands that report errors in-band, such as the OTA commands, keep their own format and answer under the command name with "result": "error"; see the OTA Command Reference.

NATS Subject Conventions

Subject Direction Purpose
commands.{service}.req Inbound Command messages sent to the service
events.data Outbound Sensor metric readings
events.params Outbound Command responses, command and plugin announcements
events.infos Outbound OTA status events and service info
heartbeat.{service}.service Outbound Liveness heartbeat (every 10 s)