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:
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:
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
nto 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
idstring property: it selects the target plugin. The allowed values are not listed in the schema; they are in thepluginsrecord. - 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\"]}"}]
typeis the plugin class name, repeated so that each record stands alone.idsare the ids of the attached plugins, sorted — the values a client puts inidto target a plugin. They are the same valuesListPluginIdsreturns.- 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) |
See also