MQTT Actors
Overview
port: 1883
address: AP-> 192.168.30.1
Eth1: 93.48.86.253
user: < not needed>
login <not needed>
1. Relay Outputs Actor
1.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Set/reset digital output pin/pins state
Both relays default state is open.
Req-Topic: relayOutputs/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"DO1": {
"state": "open"
},
"DO2": {
"state": "open"
}
}
}
}
Reply-Topic: relayOutputs/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DO1": {
"success": true,
"state": "open"
},
"DO2": {
"success": true,
"state": "open"
}
}
}
}
b) Get all relays state
Req-Topic: relayOutputs/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: relayOutputs/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DO1": {
"success": true,
"state": "open"
},
"DO2": {
"success": true,
"state": "close"
}
}
}
}
c) Set all relays state
Req-Topic: relayOutputs/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET",
"taskParams": {
"DO1": {
"state": "open"
},
"DO2": {
"state": "close"
}
}
}
}
Reply-Topic: relayOutputs/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DO1": {
"success": true,
"state": "open"
},
"DO2": {
"success": true,
"state": "close"
}
}
}
}
2 RTC Actor
2.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Set timer/alarm interruption
mode: Timer, alarm or none.
alarmSet: Date and time for alarm interruption (No effect if mode is timer or none).
timerSetSeconds: Period in seconds for timer interruption (No effect if mode is alarm or none).
Req-Topic: clock/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"CLOCK_SET_MODE": {
"mode": "timer",
"alarmSet": "[timestamp epoch ms]",
"timerSetSeconds": "<INT>"
}
}
}
}
Req-Topic: clock/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"CLOCK_SET_MODE": {
"mode": "timer",
"alarmSet": "[timestamp epoch ms]",
"timerSetSeconds": "<INT>"
}
}
}
}
b) Toggle RTC sync logic (described below)
tcState: State of the synchronization logic of the RTC, this value is non-persistent so every-time a reboot happens the value return to default (by default the value is ON)
Req-Topic: clock/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "TOGGLE"
}
}
Req-Topic: clock/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"rtcState": "OFF"
}
}
}
c) Read clock configs
mode: Timer, alarm or none.
alarmSet: Date and time for alarm interruption (No effect if mode is timer or none).
timerSetSeconds: Period in seconds for timer interruption (No effect if mode is alarm or none).
rtcState: Current state of the synchronization logic of the RTC.
Req-Topic: clock/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: clock/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"CLOCK_CONFIGS": {
"mode": "timer",
"alarmSet": "[timestamp epoch ms]",
"timerSetSeconds": "<INT>",
"rtcState": "ON"
}
}
}
}
d) Read clock
Req-Topic: clock/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"timestamp": "[timestamp epoch ms]"
}
}
}
2.2 Synchronization loop flowchart (every 30 seconds)
RTC Actor - Overview
3 System Health Actor
3.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker.
This interface allows any external App to receive system-level metrics such as CPU load, RAM usage, disk usage, temperature, core frequency, system throttle status and uptime.
No credentials are required to subscribe to this topic.
The System Health Actor does not receive commands. It is fully autonomous and publishes its metrics every 1 second.
3.2 Peripherals version
The Atlas peripherals version is on topic systemHealth/versions
{
"id": <TIMESTAMP>,
"origin": "system-health",
"version": "1.0.0"
}
3.3 Runtime Data
Topic: systemHealth/runtime/out
Publish rate: 1 Hz (one message per second, clock-jump safe)
a) Metrics Provided
| Key | Description | Unit |
|---|---|---|
| cpu_percent | Current CPU load | % |
| cpu_tempC | CPU temperature read from Linux thermal zones | ºC |
| cpu0_freq | CPU Core 0 current frequency | Hz |
| cpu1_freq | CPU Core 1 current frequency | Hz |
| cpu2_freq | CPU Core 2 current frequency | Hz |
| cpu3_freq | CPU Core 3 current frequency | Hz |
| cpu0_throttle | System throttle status register | Hex |
| ram_percent | RAM usage | % |
| disk_percent | Disk usage for filesystem root /
|
% |
| uptime_sec | Time since last reboot | seconds |
| emmc_rul | eMMC Remaining Useful Life object calculated from EmmcActor wear history | object |
Example Payload (systemHealth/runtime/out)
{
"id": <TIMESTAMP>,
"origin": "system-health",
"task": {
"taskResult": {
"cpu_percent": 7.1,
"cpu_tempC": 47.3,
"cpu0_freq": 1500000000,
"cpu1_freq": 1500000000,
"cpu2_freq": 1500000000,
"cpu3_freq": 1500000000,
"cpu0_throttle": "0x0",
"ram_percent": 32.9,
"disk_percent": 41.2,
"uptime_sec": 5321,
}
}
}
3.4 eMMC RUL - Remaining Useful Life
The SystemHealthActor also exposes the estimated eMMC Remaining Useful Life (RUL) inside the normal systemHealth/runtime/out payload.
The RUL data is calculated from the wear-level history maintained by the EmmcActor.
a) eMMC RUL Data Flow
b) Wear-Level Format
Each wear-level entry has the following format:
<WLxx:timestamp_ms>
Example:
<WL03:1782228844578>
| Part | Meaning |
|---|---|
| WL03 | eMMC wear-level tier |
| 1782228844578 | Unix timestamp in milliseconds when the wear-level was registered |
c) Wear-Level Interpretation
The wear-level tier represents a coarse estimation of consumed eMMC lifetime.
| Wear Level | Estimated Lifetime Consumed | Conservative Remaining |
|---|---|---|
| WL01 | 0% - 10% | 90% |
| WL02 | 10% - 20% | 80% |
| WL03 | 20% - 30% | 70% |
| WL04 | 30% - 40% | 60% |
| WL05 | 40% - 50% | 50% |
| WL06 | 50% - 60% | 40% |
| WL07 | 60% - 70% | 30% |
| WL08 | 70% - 80% | 20% |
| WL09 | 80% - 90% | 10% |
| WL10 | 90% - 100% | 0% |
| WL11 | Estimated lifetime exceeded | 0% |
When WL11 or higher is detected, the device should be considered beyond the estimated eMMC lifetime.
d) emmc_rul Payload
The calculated RUL is published inside the regular systemHealth/runtime/out payload.
Example:
{
"id": 1782230830898,
"origin": "system-health",
"task": {
"taskResult": {
"cpu_percent": 13.6,
"cpu_tempC": 63.3,
"disk_percent": 24.0,
"emmc_rul": {
"ok": true,
"state": "OK",
"current_wear_level": "WL03",
"current_tier": 3,
"consumed_percent_min": 20,
"consumed_percent_max": 30,
"consumed_percent_estimated": 25,
"remaining_percent_estimated": 75,
"remaining_percent_conservative": 70,
"history_entries": 3,
"estimated_rul_ms": null,
"estimated_rul_days": null,
"estimated_eol_timestamp_ms": null,
"confidence": "LOW",
"source": "emmc_actor",
"last_refresh_timestamp_ms": 1782230830898
},
"uptime_sec": 9339
}
}
}
e) emmc_rul Field Description
| Field | Type | Description |
|---|---|---|
| ok | boolean | Indicates whether the RUL calculation was successful. |
| state | string | General eMMC RUL state. Examples: OK, UNKNOWN, EXCEEDED_ESTIMATED_LIFETIME.
|
| current_wear_level | string | Current wear-level tier as text, for example WL01, WL03, or WL11.
|
| current_tier | integer | Numeric version of the wear level. For example, WL03 becomes 3.
|
| consumed_percent_min | number | Lower bound of estimated consumed lifetime for the current tier. |
| consumed_percent_max | number | Upper bound of estimated consumed lifetime for the current tier. |
| consumed_percent_estimated | number | Mid-point estimate between minimum and maximum consumed lifetime. |
| remaining_percent_estimated | number | Estimated remaining lifetime using the mid-point consumed value. |
| remaining_percent_conservative | number | Conservative remaining lifetime using the maximum consumed value. |
| history_entries | integer | Number of wear-history entries received from the EmmcActor.
|
| estimated_rul_ms | integer / null | Estimated remaining useful lifetime in milliseconds. null means there is not enough reliable time-history data.
|
| estimated_rul_days | number / null | Estimated remaining useful lifetime in days. null means time-based RUL is not available.
|
| estimated_eol_timestamp_ms | integer / null | Estimated end-of-life Unix timestamp in milliseconds. null means EOL cannot be estimated reliably.
|
| ms_per_wear_tier | integer / null | Average time observed for one wear-level tier transition. Only available when a valid transition history exists. |
| confidence | string | Confidence level of the RUL estimate. Suggested values: LOW, MEDIUM, HIGH.
|
| rul_reason | string / null | Optional explanation when time-based RUL is not calculated, for example OBSERVATION_WINDOW_TOO_SHORT.
|
| source | string | Source used for the calculation. Normally emmc_actor.
|
| last_refresh_timestamp_ms | integer | Unix timestamp in milliseconds when the SystemHealthActor last refreshed the eMMC RUL cache. |
f) Percentage-Based RUL Calculation
The percentage-based RUL can be calculated immediately from the current wear-level tier.
Formula:
consumed_percent_min = (current_tier - 1) * 10
consumed_percent_max = current_tier * 10
consumed_percent_estimated = (consumed_percent_min + consumed_percent_max) / 2
remaining_percent_estimated = 100 - consumed_percent_estimated
remaining_percent_conservative = 100 - consumed_percent_max
Example for WL03:
current_tier = 3
consumed_percent_min = (3 - 1) * 10 = 20
consumed_percent_max = 3 * 10 = 30
consumed_percent_estimated = (20 + 30) / 2 = 25
remaining_percent_estimated = 100 - 25 = 75
remaining_percent_conservative = 100 - 30 = 70
Result:
{
"current_wear_level": "WL03",
"consumed_percent_min": 20,
"consumed_percent_max": 30,
"consumed_percent_estimated": 25,
"remaining_percent_estimated": 75,
"remaining_percent_conservative": 70
}
g) Time-Based RUL Calculation
Time-based RUL estimates how many milliseconds, days, or calendar time remain until the eMMC reaches the end of the expected lifetime.
This calculation requires real historical wear transitions.
The minimum required data is:
WL01 at timestamp A
WL02 at timestamp B
Where:
timestamp B > timestamp A
Formula:
ms_per_wear_tier = (last_timestamp_ms - first_timestamp_ms) / (last_tier - first_tier)
remaining_tiers_to_wl10 = 10 - current_tier
estimated_rul_ms = remaining_tiers_to_wl10 * ms_per_wear_tier
estimated_eol_timestamp_ms = current_wear_timestamp_ms + estimated_rul_ms
Example:
WL01 at 1782225732187
WL03 at 1782235908273
Observed tier delta:
3 - 1 = 2 tiers
Observed time delta:
1782235908273 - 1782225732187 = 10,176,086 ms
Average time per tier:
10,176,086 / 2 = 5,088,043 ms per tier
Remaining tiers from WL03 to WL10:
10 - 3 = 7 tiers
Estimated RUL:
7 * 5,088,043 = 35,616,301 ms
35,616,301 ms = 0.41 days
This calculation is mathematically correct, but it may be operationally misleading if the observation window is too short.
h) Why Time-Based RUL Can Be null
The following fields can be null:
{
"estimated_rul_ms": null,
"estimated_rul_days": null,
"estimated_eol_timestamp_ms": null
}
This happens when the SystemHealthActor does not have enough reliable history to estimate the wear rate.
Common reasons:
| Reason | Description |
|---|---|
| NO_WEAR_HISTORY | No valid wear-history entries were received. |
| ONLY_ONE_HISTORY_ENTRY | Only one wear-level entry exists, so no rate can be calculated. |
| NON_INCREASING_TIMESTAMPS | Wear-level timestamps are not strictly increasing. |
| BACKFILLED_HISTORY | Several wear levels have the same timestamp, usually because they were synchronized or recreated at boot. |
| OBSERVATION_WINDOW_TOO_SHORT | The observed time span is too short to generate a trustworthy RUL estimate. |
Example with only one entry:
<WL01:1782225732187>The actor can calculate the percentage-based RUL:24.3.6 Peripherals health
WL01 = 0% - 10% consumed
remaining_percent_estimated = 95%
remaining_percent_conservative = 90%
But it cannot calculate:
estimated_rul_ms
estimated_rul_days
estimated_eol_timestamp_ms
because it does not know how fast the eMMC is wearing.
i) Important Rule: Do Not Trust Short Observation Windows
During development, stress tests, boot corrections, actor restarts, or history synchronization, several wear levels may be registered in a very short time.
Example:
<WL01:1782225732187>
<WL02:1782228844578>
<WL03:1782235908273>This may produce:
{
"ms_per_wear_tier": 5088043,
"estimated_rul_days": 0.41,
"confidence": "MEDIUM"
}
Although the math is correct, the result should not be trusted as a production RUL estimate.
The actor should avoid publishing a time-based RUL estimate when the observation period is too short.
Recommended rule:
Calculate time-based RUL only when:
- at least 2 real transitions exist;
- timestamps are strictly increasing;
- the total observation window is >= 7 days.For production deployments, a stricter rule may be used:
Minimum observation window >= 30 days.When the observation window is too short, publish:
{
"estimated_rul_ms": null,
"estimated_rul_days": null,
"estimated_eol_timestamp_ms": null,
"confidence": "LOW",
"rul_reason": "OBSERVATION_WINDOW_TOO_SHORT"
}
The percentage-based fields should still be published because they remain valid.
j) Recommended Confidence Rules
| Confidence | Meaning |
|---|---|
| LOW | Not enough real history exists, history was backfilled, or the observation window is too short. |
| MEDIUM | Real transitions exist and the observation window is acceptable, but the model is still an estimate. |
| HIGH | The eMMC has reached or exceeded WL11, or the state is known with high certainty.
|
Recommended behavior:
| Condition | Confidence | Time-Based RUL |
|---|---|---|
| No history | LOW | null |
| Only one entry | LOW | null |
| Same timestamp for multiple wear levels | LOW | null |
| Observation window below 7 days | LOW | null |
| Valid transitions over acceptable window | MEDIUM | calculated |
WL11 or higher
|
HIGH | 0 |
k) Critical State Handling
If the current tier is WL11 or higher, the eMMC has exceeded the estimated lifetime range.
Expected output:
{
"ok": true,
"state": "EXCEEDED_ESTIMATED_LIFETIME",
"current_wear_level": "WL11",
"current_tier": 11,
"consumed_percent_min": 100,
"consumed_percent_max": 100,
"consumed_percent_estimated": 100,
"remaining_percent_estimated": 0,
"remaining_percent_conservative": 0,
"estimated_rul_ms": 0,
"estimated_rul_days": 0,
"confidence": "HIGH"
}
Recommended service interpretation:
The device should be serviced or replaced.l) Example: Safe Output for Short Observation Window
If the device is already on WL03, but the transition history happened in a short test window, the safe output should be:
{
"emmc_rul": {
"ok": true,
"state": "OK",
"current_wear_level": "WL03",
"current_tier": 3,
"consumed_percent_min": 20,
"consumed_percent_max": 30,
"consumed_percent_estimated": 25,
"remaining_percent_estimated": 75,
"remaining_percent_conservative": 70,
"history_entries": 3,
"estimated_rul_ms": null,
"estimated_rul_days": null,
"estimated_eol_timestamp_ms": null,
"ms_per_wear_tier": 5088043,
"confidence": "LOW",
"rul_reason": "OBSERVATION_WINDOW_TOO_SHORT",
"source": "emmc_actor",
"last_refresh_timestamp_ms": 1782230830898
}
}
This output avoids reporting a misleading value such as:
{
"estimated_rul_days": 0.41
}
The correct interpretation is:
The eMMC is currently in WL03.
Estimated consumed lifetime is between 20% and 30%.
Estimated remaining lifetime is between 70% and 75%.
A reliable time-based RUL cannot yet be calculated.m) Summary
The emmc_rul object provides two different types of information:
| Type | Reliability | Description |
|---|---|---|
| Percentage-based RUL | Reliable immediately | Based directly on the current wear-level tier. |
| Time-based RUL | Requires history | Based on real wear-level transitions over time. |
The SystemHealthActor should always expose the percentage-based RUL when a valid wear level exists.
The time-based RUL should only be exposed when the history is reliable enough to avoid misleading estimates.
3.5 Peripherals health
In order get the peripherals health we had to implement a SystemHealth actor in each bus manager (busi2c-0, busi2c-1, etc....).
a) Example
For busi2c-0:
Reply topic: systemHealth/busi2c-0/runtime/out
Payload:
{
"id": <TIMESTAMP>,
"origin": "system-health-busi2c-0",
"task": {
"taskResult": {
"ADC": {
"status": "ERROR",
"description": "IRQ Retrying",
"last_report": 1770378753006,
"stats": {
"AI1": {
"samples": 563154,
"retries": 760548,
"discards": 253313
},
"AI2": {
"samples": 563154,
"retries": 760475,
"discards": 253314
},
"AI3": {
"samples": 563154,
"retries": 760528,
"discards": 253313
},
"AI4": {
"samples": 563154,
"retries": 760549,
"discards": 253312
}
}
},
"DAC": {
"status": "OK",
"description": "Running....",
"last_report": 1770378752576,
"stats": {
"AO1": {
"samples": 240755,
"retries": 0,
"discards": 0
},
"AO2": {
"samples": 240755,
"retries": 0,
"discards": 0
}
}
},
"RTC": {
"status": "OK",
"description": "",
"last_report": 1770378752094,
"stats": {
"samples": 248710,
"retries": 0,
"discards": 0
}
},
"TEMP": {
"status": "OK",
"description": "",
"last_report": 1770378752579,
"stats": {
"samples": 240754,
"retries": 0,
"discards": 0
}
}
}
}
}
For busi2c-1:
Reply topic: systemHealth/busi2c-1/runtime/out
Payload:
{
"id": <TIMESTAMP>,
"origin": "system-health-busi2c-1",
"task": {
"taskResult": {
"JOYSTICK": {
"status": "OK",
"description": "Running....",
"last_report": 1770382405050,
"stats": {
"read_all": {
"samples": 410154,
"retries": 0,
"discards": 0
},
"read_raw": {
"samples": 0,
"retries": 0,
"discards": 0
},
"buttons": {
"JDOWN": {
"reads": 410154,
"last_state": "pressed"
},
"JRIGHT": {
"reads": 410154,
"last_state": "pressed"
},
"JUP": {
"reads": 410154,
"last_state": "pressed"
},
"JLEFT": {
"reads": 410154,
"last_state": "pressed"
},
"JCENTER": {
"reads": 410154,
"last_state": "pressed"
}
}
}
},
"RELAYS": {
"status": "OK",
"description": "Relay Output MQTT connected",
"last_report": 1770298859099,
"stats": {
"read_all": {
"samples": 0,
"retries": 0,
"discards": 0
},
"set": {
"samples": 0,
"retries": 0,
"discards": 0
},
"set_many": {
"samples": 0,
"retries": 0,
"discards": 0
},
"raw_read_all": {
"samples": 0,
"retries": 0,
"discards": 0
},
"outputs": {
"DO1": {
"writes": 0,
"last_state": "unknown"
},
"DO2": {
"writes": 0,
"last_state": "unknown"
}
}
}
},
"DI": {
"status": "OK",
"description": "Running",
"last_report": 1770382404832,
"stats": {
"read_all": {
"samples": 181758,
"retries": 0,
"discards": 0
},
"read_raw": {
"samples": 0,
"retries": 0,
"discards": 0
},
"inputs": {
"DI1": {
"reads": 181758,
"last_state": "low"
},
"DI2": {
"reads": 181758,
"last_state": "low"
},
"DI3": {
"reads": 181758,
"last_state": "low"
},
"DI4": {
"reads": 181758,
"last_state": "low"
}
}
}
}
}
}
}
3.6 Commands
a) List usb devices
The list_usb_devices action returns a parsed list of USB devices detected on the system using the lsusb command.
Req-topic: systemHealth/runtime/in
{
"id": <TIMESTAMP>,
"origin": "Dev",
"task": {
"action": "LIST_USB_DEVICES"
}
}
Reply-topic: systemHealth/runtime/usb/devices/out
{
"id": <TIMESTAMP>,
"origin": "system-health",
"task": {
"action": "list_usb_devices",
"taskResult": {
"usb_devices": {
"success": true,
"command": "lsusb",
"returncode": 0,
"devices": [
{
"bus": "001",
"device": "001",
"vendor_id": "1d6b",
"product_id": "0002",
"description": "Linux Foundation 2.0 root hub",
"parsed": true
},
{
"bus": "001",
"device": "002",
"vendor_id": "0424",
"product_id": "9514",
"description": "Microchip Technology, Inc. (formerly SMSC) SMC9514 Hub",
"parsed": true
},
{
"bus": "001",
"device": "003",
"vendor_id": "0424",
"product_id": "ec00",
"description": "Microchip Technology, Inc. (formerly SMSC) SMSC9512/9514 Fast Ethernet Adapter",
"parsed": true
},
{
"bus": "001",
"device": "004",
"vendor_id": "1e0e",
"product_id": "9001",
"description": "Qualcomm / Option SimTech, Incorporated",
"parsed": true
}
],
"stderr": ""
}
}
}
}
| JSON Path | Type | Example | Description |
|---|---|---|---|
| id | string /number | "get-hw-info-001" | Request identifier echoed back in the response. |
| origin | string | "system-health" | Actor/device that generated the response. |
| task | object | {...} | Container for the action response. |
| task.action | string | "list_usb_devices" | Action executed by the actor. |
| task.taskResult | object | {...} | Main result object for the action. |
| task.taskResult.usb_devices | object | {...} | Contains the USB device scan result. |
| task.taskResult.usb_devices.success | boolean | true | Indicates whether the lsusb
commandexecuted successfully. |
| task.taskResult.usb_devices.command | string | "lsusb" | Command used to collect USB information. |
| task.taskResult.usb_devices.returncode | number /null | 0 | Process return code. 0 means success. null
means the command was not executed ortimed out. |
| task.taskResult.usb_devices.devices | array | [...] | List of USB devices detected by lsusb. |
| task.taskResult.usb_devices.devices[ ].bus | string | "001" | USB bus number where the device wasdetected. |
| task.taskResult.usb_devices.devices[ ].device | string | "004" | USB device number assigned on that bus. |
| task.taskResult.usb_devices.devices[ ].vendor_id | string | "1e0e" | USB vendor ID in hexadecimal format. |
| task.taskResult.usb_devices.devices[ ].product_id | string | "9001" | USB product ID in hexadecimal format. |
| task.taskResult.usb_devices.devices[ ].description | string | "Qualcomm / Option SimTech,Incorporated" | Human-readable device description returnedby lsusb. |
| task.taskResult.usb_devices.devices[ ].parsed | boolean | true | Indicates whether the lsusb line was parsed successfully. |
| task.taskResult.usb_devices.devices[ ].raw | string | Optional | Present only when a device line could not beparsed.
Contains the original raw lsusb line. |
| task.taskResult.usb_devices.stderr | string | " " | Error output from the command. Empty stringmeans no error. |
b) List connectivity specs
The list_connectivity_specs action allows the SystemHealth Actor to return connectivity information for the device, including the status of:
- Wi-Fi
- LTE modem
- eth0
- eth1
- Tunnel interfaces, for possible commissioning use
Req-topic: systemHealth/runtime/in
{
"id": "connectivity-001",
"origin": "APP",
"task": {
"action": "list_connectivity_specs"
}
}
Reply-topic: systemHealth/runtime/connectivity/out
{
"id": <TIMESTAMP>,
"origin": "system-health",
"task": {
"action": "list_connectivity_specs",
"taskResult": {
"connectivity": {
"sources": {
"sysfs": "/sys/class/net",
"ip_link": {
"available": true,
"command": "ip -j -d link show",
"error": null
},
"network_manager": {
"available": true,
"command": "nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status",
"error": null
}
},
"interfaces": {
"wifi": {
"available": true,
"interfaces": [
{
"interface": "wlan0",
"available": true,
"status": "down",
"operstate": "down",
"admin_up": true,
"carrier": false,
"mtu": 1500,
"type": 1,
"link_kind": null,
"mac_address": "2c:cf:67:fd:60:40",
"ipv4": [],
"ipv6": [],
"network_manager": {
"device": "wlan0",
"type": "wifi",
"state": "disconnected",
"connection": null
},
"wifi": {
"available": true,
"command": "iw dev wlan0 link",
"connected": false
}
}
]
},
"lte_modem": {
"available": true,
"tool_available": true,
"command": "mmcli -L",
"modems": [
{
"id": "0",
"success": true,
"command": "mmcli -m 0 --output-json",
"manufacturer": "QUALCOMM INCORPORATED",
"model": "SIMCOM_SIM7600G-H",
"revision": "LE20B04SIM7600G22",
"hardware_revision": "10000",
"device": "/sys/devices/platform/scb/fe9c0000.xhci/usb1/1-1/1-1.4",
"drivers": [
"qmi_wwan",
"option"
],
"plugin": "simtech",
"primary_port": "cdc-wdm0",
"ports": [
{
"name": "cdc-wdm0",
"type": "qmi"
},
{
"name": "ttyUSB0",
"type": "ignored"
},
{
"name": "ttyUSB1",
"type": "gps"
},
{
"name": "ttyUSB2",
"type": "at"
},
{
"name": "ttyUSB3",
"type": "at"
},
{
"name": "ttyUSB4",
"type": "audio"
},
{
"name": "wwan0",
"type": "net"
}
],
"state": "failed",
"power_state": "on",
"access_technologies": [],
"signal_quality": {
"recent": "yes",
"value": "0"
},
"current_capabilities": [
"gsm-umts, lte"
],
"supported_capabilities": [
"gsm-umts, lte"
],
"operator_code": "--",
"operator_name": "--",
"registration_state": "--",
"packet_service_state": "--",
"sim_path": null
}
],
"network_interfaces": [
{
"interface": "wwan0",
"available": true,
"status": "down",
"operstate": "down",
"admin_up": false,
"carrier": null,
"mtu": 1500,
"type": 65534,
"link_kind": null,
"mac_address": null,
"ipv4": [],
"ipv6": [],
"network_manager": null
}
]
},
"eth0": {
"interface": "eth0",
"available": true,
"status": "connected",
"operstate": "up",
"admin_up": true,
"carrier": true,
"mtu": 1500,
"type": 1,
"link_kind": null,
"mac_address": "2c:cf:67:fd:60:3f",
"ipv4": [
{
"address": "172.16.20.57",
"prefixlen": 24,
"scope": "global",
"dynamic": true
}
],
"ipv6": [
{
"address": "fe80::7fc1:f65f:5729:d234",
"prefixlen": 64,
"scope": "link",
"dynamic": false
}
],
"network_manager": {
"device": "eth0",
"type": "ethernet",
"state": "connected",
"connection": "wired-ip-dynamic"
}
},
"eth1": {
"interface": "eth1",
"available": true,
"status": "unavailable",
"operstate": "down",
"admin_up": true,
"carrier": false,
"mtu": 1500,
"type": 1,
"link_kind": null,
"mac_address": "2c:cf:67:fd:60:41",
"ipv4": [],
"ipv6": [],
"network_manager": {
"device": "eth1",
"type": "ethernet",
"state": "unavailable",
"connection": null
}
},
"tunnel": {
"available": false,
"interfaces": []
}
}
}
}
}
}
| Field | Type | Description |
|---|---|---|
| id | string/number | Request/response identifier used to correlate therequest with the response. |
| origin | string | Origin of the message. In the response, this is normallythe SystemHealth actor name. |
| task.action | string | Executed action. For this feature, the value is list_connectivity_specs. |
| task.taskResult.connectivity.sources.sysfs | string | Linux sysfs path used to discover network interfaces, usually/sys/class/net. |
| task.taskResult.connectivity.sources.ip_link.available | boolean | Indicates whether the ip command was available andexecuted successfully. |
| task.taskResult.connectivity.sources.ip_link.command | string | Command used to retrieve detailed link information,for example ip -j -d link show. |
| task.taskResult.connectivity.sources.ip_link.error | string/null | Error returned by the ip command, or null if no erroroccurred. |
| task.taskResult.connectivity.sources.network_manager.available | boolean | Indicates whether NetworkManager / nmcli was available and executed successfully. |
| task.taskResult.connectivity.sources.network_manager.command | string | Command used to retrieve NetworkManager device status. |
| task.taskResult.connectivity.sources.network_manager.error | string/null | Error returned by nmcli, or null if no error occurred. |
| task.taskResult.connectivity.interfaces.wifi.available | boolean | Indicates whether at least one Wi-Fi interface was found. |
| task.taskResult.connectivity.interfaces.wifi.interfaces[ ] | array | List of detected Wi-Fi interfaces, usually including wlan0 when Wi-Fi exists. |
| wifi.interfaces[ ].interface | string | Linux interface name, for example wlan0. |
| wifi.interfaces[ ].available | boolean | Indicates whether the Wi-Fi interface exists in the system. |
| wifi.interfaces[ ].status | string | Normalized interface status, for example connected, down, unavailable, no_carrier, or not_available. |
| wifi.interfaces[ ].operstate | string / null | Operational state reported by Linux, for example up, down, or unknown. |
| wifi.interfaces[ ].admin_up | boolean / null | Indicates whether the interface is administratively enabled. |
| wifi.interfaces[ ].carrier | boolean / null | Indicates whether carrier/link is detected. |
| wifi.interfaces[ ].mtu | number / null | Maximum Transmission Unit of the interface. |
| wifi.interfaces[ ].type | number / null | Linux interface type. |
| wifi.interfaces[ ].link_kind | string / null | Special link type, used mainly for virtual/tunnel interfaces. Usually null for normal Wi-Fi. |
| wifi.interfaces[ ].mac_address | string / null | MAC address of the Wi-Fi interface. |
| wifi.interfaces[ ].ipv4[ ] | array | List of IPv4 addresses configured on the Wi-Fi interface. |
| wifi.interfaces[ ].ipv6[ ] | array | List of IPv6 addresses configured on the Wi-Fi interface. |
| wifi.interfaces[ ].network_manager.device | string | NetworkManager device name. |
| wifi.interfaces[ ].network_manager.type | string | NetworkManager interface type, usually wifi. |
| wifi.interfaces[ ].network_manager.state | string | NetworkManager state, for example connected, disconnected, or unavailable. |
| wifi.interfaces[ ].network_manager.connection | string / null | Active NetworkManager profile name, or null if none is active. |
| wifi.interfaces[ ].wifi.available | boolean | Indicates whether Wi-Fi details could be queried using iw. |
| wifi.interfaces[ ].wifi.command | string | Command used to query Wi-Fi link state, for example iw dev wlan0 link. |
| wifi.interfaces[ ].wifi.connected | boolean / null | Indicates whether the Wi-Fi interface is associated with a Wi-Fi network. |
| wifi.interfaces[ ].wifi.ssid | string | SSID of the connected Wi-Fi network, when connected. |
| wifi.interfaces[ ].wifi.bssid | string | BSSID/MAC address of the connected access point, when connected. |
| wifi.interfaces[ ].wifi.frequency_mhz | number | Wi-Fi frequency in MHz, when connected. |
| wifi.interfaces[ ].wifi.signal_dbm | number | Wi-Fi signal strength in dBm, when available. |
| wifi.interfaces[ ].wifi.tx_bitrate | string | Wi-Fi transmit bitrate, when available. |
| wifi.interfaces[ ].wifi.rx_bitrate | string | Wi-Fi receive bitrate, when available. |
| task.taskResult.connectivity.interfaces.lte_modem.available | boolean | Indicates whether at least one LTE modem was detected. |
| task.taskResult.connectivity.interfaces.lte_modem.tool_available | boolean | Indicates whether mmcli / ModemManager is available. |
| task.taskResult.connectivity.interfaces.lte_modem.command | string | Command used to list LTE modems, usually mmcli -L. |
| task.taskResult.connectivity.interfaces.lte_modem.modems[ ] | array | List of LTE modems detected by ModemManager. |
| lte_modem.modems[ ].id | string | Modem ID in ModemManager, for example 0. |
| lte_modem.modems[ ].success | boolean | Indicates whether detailed modem information wasread successfully. |
| lte_modem.modems[ ].command | string | Command used to read modem details, for example mmcli -m 0 --output-json. |
| lte_modem.modems[ ].manufacturer | string / null | Modem manufacturer. |
| lte_modem.modems[ ].model | string / null | Modem model. |
| lte_modem.modems[ ].revision | string / null | Modem firmware/software revision. |
| lte_modem.modems[ ].hardware_revision | string / null | Modem hardware revision. |
| lte_modem.modems[ ].device | string / null | Sysfs/USB path of the modem device. |
| lte_modem.modems[ ].drivers[ ] | array | Linux drivers associated with the modem, for example qmi_wwan or option. |
| lte_modem.modems[ ].plugin | string / null | ModemManager plugin used to manage the modem. |
| lte_modem.modems[ ].primary_port | string / null | Primary modem control port, for example cdc-wdm0. |
| lte_modem.modems[ ].ports[ ] | array | List of ports associated with the modem. |
| lte_modem.modems[ ].ports[ ].name | string | Port/interface name, for example cdc-wdm0, ttyUSB2, or wwan0. |
| lte_modem.modems[ ].ports[ ].type | string / null | Port type, for example qmi, at, gps, audio, net, or ignored. |
| lte_modem.modems[ ].state | string / null | General modem state according to ModemManager,for example registered, connected, or failed. |
| lte_modem.modems[ ].power_state | string / null | Modem power state, for example on, off, or low. |
| lte_modem.modems[ ].access_technologies[ ] | array | Current access technologies in use, for example LTE,UMTS, or GSM. |
| lte_modem.modems[ ].signal_quality.recent | string / null | Indicates whether the signal quality value is recent. |
| lte_modem.modems[ ].signal_quality.value | string / number / null | Signal quality value reported by the modem. |
| lte_modem.modems[ ].current_capabilities[ ] | array | Current modem capabilities. |
| lte_modem.modems[ ].supported_capabilities[ ] | array | Capabilities supported by the modem. |
| lte_modem.modems[ ].operator_code | string / null | MCC/MNC operator code. |
| lte_modem.modems[ ].operator_name | string / null | Operator name. |
| lte_modem.modems[ ].registration_state | string / null | Mobile network registration state. |
| lte_modem.modems[ ].packet_service_state | string / null | Mobile packet data service state. |
| lte_modem.modems[ ].sim_path | string / null | SIM path in ModemManager, when available. |
| task.taskResult.connectivity.interfaces.lte_modem.network_interfaces[] | array | List of Linux network interfaces associated with the LTE modem, usually wwan0. |
| lte_modem.network_interfaces[ ].interface | string | LTE network interface name, for example wwan0. |
| lte_modem.network_interfaces[ ].available | boolean | Indicates whether the LTE network interface exists. |
| lte_modem.network_interfaces[ ].status | string | Normalized LTE network interface status. |
| lte_modem.network_interfaces[ ].operstate | string / null | Operational state reported by Linux. |
| lte_modem.network_interfaces[ ].admin_up | boolean / null | Indicates whether the LTE network interface is administratively enabled. |
| lte_modem.network_interfaces[ ].carrier | boolean / null | Indicates whether carrier/link information is availableand active. |
| lte_modem.network_interfaces[ ].mtu | number / null | Maximum Transmission Unit of the LTE network interface. |
| lte_modem.network_interfaces[ ].type | number / null | Linux interface type. |
| lte_modem.network_interfaces[ ].link_kind | string / null | Link kind, when available. |
| lte_modem.network_interfaces[ ].mac_address | string / null | MAC address, when available. |
| lte_modem.network_interfaces[ ].ipv4[ ] | array | IPv4 addresses configured on the LTE network interface. |
| lte_modem.network_interfaces[ ].ipv6[ ] | array | IPv6 addresses configured on the LTE network interface. |
| lte_modem.network_interfaces[ ].network_manager | object / null | NetworkManager information for the LTE interface, when available. |
| task.taskResult.connectivity.interfaces.eth0.interface | string | Ethernet interface name, eth0. |
| task.taskResult.connectivity.interfaces.eth0.available | boolean | Indicates whether eth0 exists. |
| task.taskResult.connectivity.interfaces.eth0.status | string | Normalized eth0 status. |
| task.taskResult.connectivity.interfaces.eth0.operstate | string / null | Operational state of eth0. |
| task.taskResult.connectivity.interfaces.eth0.admin_up | boolean / null | Indicates whether eth0 is administratively enabled. |
| task.taskResult.connectivity.interfaces.eth0.carrier | boolean / null | Indicates whether eth0 has physical link. |
| task.taskResult.connectivity.interfaces.eth0.mtu | number / null | MTU of eth0. |
| task.taskResult.connectivity.interfaces.eth0.type | number / null | Linux interface type of eth0. |
| task.taskResult.connectivity.interfaces.eth0.link_kind | string / null | Link kind of eth0, usually null. |
| task.taskResult.connectivity.interfaces.eth0.mac_address | string / null | MAC address of eth0. |
| task.taskResult.connectivity.interfaces.eth0.ipv4[ ] | array | IPv4 addresses configured on eth0. |
| task.taskResult.connectivity.interfaces.eth0.ipv6[ ] | array | IPv6 addresses configured on eth0. |
| task.taskResult.connectivity.interfaces.eth0.network_manager | object / null | NetworkManager information for eth0. |
| task.taskResult.connectivity.interfaces.eth1.interface | string | Ethernet interface name, eth1. |
| task.taskResult.connectivity.interfaces.eth1.available | boolean | Indicates whether eth1 exists. |
| task.taskResult.connectivity.interfaces.eth1.status | string | Normalized eth1 status. |
| task.taskResult.connectivity.interfaces.eth1.operstate | string / null | Operational state of eth1. |
| task.taskResult.connectivity.interfaces.eth1.admin_up | boolean / null | Indicates whether eth1 is administratively enabled. |
| task.taskResult.connectivity.interfaces.eth1.carrier | boolean / null | Indicates whether eth1 has physical link. |
| task.taskResult.connectivity.interfaces.eth1.mtu | number / null | MTU of eth1. |
| task.taskResult.connectivity.interfaces.eth1.type | number / null | Linux interface type of eth1. |
| task.taskResult.connectivity.interfaces.eth1.link_kind | string / null | Link kind of eth1, usually null. |
| task.taskResult.connectivity.interfaces.eth1.mac_address | string / null | MAC address of eth1. |
| task.taskResult.connectivity.interfaces.eth1.ipv4[ ] | array | IPv4 addresses configured on eth1. |
| task.taskResult.connectivity.interfaces.eth1.ipv6[ ] | array | IPv6 addresses configured on eth1. |
| task.taskResult.connectivity.interfaces.eth1.network_manager | object / null | NetworkManager information for eth1. |
| task.taskResult.connectivity.interfaces.tunnel.available | boolean | Indicates whether at least one tunnel interface was detected. |
| task.taskResult.connectivity.interfaces.tunnel.interfaces[ ] | array | List of detected tunnel interfaces, for example tun0, tap0, wg0, or tailscale0. |
| tunnel.interfaces[ ].interface | string | Tunnel interface name. |
| tunnel.interfaces[ ].available | boolean | Indicates whether the tunnel interface exists. |
| tunnel.interfaces[ ].status | string | Normalized tunnel interface status. |
| tunnel.interfaces[ ].operstate | string / null | Operational state reported by Linux. |
| tunnel.interfaces[ ].admin_up | boolean / null | Indicates whether the tunnel interface isadministratively enabled. |
| tunnel.interfaces[ ].carrier | boolean / null | Carrier/link information, when available. |
| tunnel.interfaces[ ].mtu | number / null | Tunnel interface MTU. |
| tunnel.interfaces[ ].type | number / null | Linux interface type. |
| tunnel.interfaces[ ].link_kind | string / null | Tunnel link kind, for example tun, tap, or wireguard. |
| tunnel.interfaces[ ].mac_address | string / null | MAC address, when available. |
| tunnel.interfaces[ ].ipv4[ ] | array | IPv4 addresses configured on the tunnel interface. |
| tunnel.interfaces[ ].ipv6[ ] | array | IPv6 addresses configured on the tunnel interface. |
| tunnel.interfaces[ ].network_manager | object / null | NetworkManager information for the tunnel interface, when available. |
| ipv4[ ].address | string | IPv4 address assigned to the interface. |
| ipv4[ ].prefixlen | number | IPv4 network prefix length. Example: 24. |
| ipv4[ ].scope | string | IPv4 scope, for example global. |
| ipv4[ ].dynamic | boolean | Indicates whether the IPv4 address was dynamically assigned. |
| ipv6[ ].address | string | IPv6 address assigned to the interface. |
| ipv6[ ].prefixlen | number | IPv6 network prefix length. |
| ipv6[ ].scope | string | IPv6 scope, for example link or global. |
| ipv6[].dynamic | boolean | Indicates whether the IPv6 address was dynamically assigned. |
c) Get hardware info
Req-topic: systemHealth/runtime/in
{
"id": <TIMESTAMP>,
"origin": "Dev",
"task": {
"action": "GET_HW_INFO"
}
}
Reply-topic: systemHealth/runtime/hw/info/out
{
"id": <TIMESTAMP>,
"origin": "system-health",
"task": {
"action": "get_hw_info",
"taskResult": {
"cpu": {......},
"ram": {......},
"emmc": {......},
"block_devices": {......},
"filesystems": {......},
"mmc_sysfs": {......},
"emmc_extcsd": {......}
}
}
}
| Section | Description |
| cpu | CPU and board model information. |
| ram | RAM total, used, and free memory. |
| emmc | Filtered eMMC device and partition information. |
| block_devices | Complete block device list from lsblk, including eMMC, boot partitions, loop devices, zram, NVMe, USB storage, etc. |
| filesystems | Mounted filesystem usage from df -h. |
| mmc_sysfs | Low-level MMC/eMMC identity fields read from /sys/block/mmcblk*/device. |
| emmc_extcsd | Parsed eMMC EXT_CSD information from mmc extcsd read, including health, revision, RPMB, boot, and partitioning data. |
- CPU
The cpu object describes the detected board and CPU.
{
"board_model": "Raspberry Pi Compute Module 4 Rev 1.1",
"cpu": {
"architecture": "aarch64",
"op_modes": "32-bit, 64-bit",
"byte_order": "Little Endian",
"vendor": "ARM",
"model_name": "Cortex-A72",
"model": "3",
"stepping": "r0p3",
"cpu_count": 4,
"online_cpus": "0-3",
"threads_per_core": 1,
"cores_per_cluster": 4,
"clusters": 1,
"frequency": {
"scaling_percent": 60.0,
"min_mhz": 600.0,
"max_mhz": 1500.0,
"bogomips": 108.0
}
}
}
| Field | Type | Description |
|---|---|---|
| board_model | string / null | Hardware board model read from device-tree. |
| cpu.architecture | string | CPU architecture, for example aarch64. |
| cpu.op_modes | string | Supported CPU operation modes. |
| cpu.byte_order | string | CPU byte order. |
| cpu.vendor | string | CPU vendor. |
| cpu.model_name | string | CPU model name. |
| cpu.model | string / number | CPU model identifier. |
| cpu.stepping | string | CPU stepping/revision. |
| cpu.cpu_count | number | Number of detected CPU cores. |
| cpu.online_cpus | string | Online CPU list. |
| cpu.threads_per_core | number | Number of threads per core. |
| cpu.cores_per_cluster | number | Number of cores per cluster. |
| cpu.clusters | number | Number of CPU clusters. |
| cpu.frequency.scaling_percent | number / null | Current CPU frequency scaling percentage. |
| cpu.frequency.min_mhz | number / null | Minimum CPU frequency in MHz. |
| cpu.frequency.max_mhz | number / null | Maximum CPU frequency in MHz. |
| cpu.frequency.bogomips | number / null | BogoMIPS value reported by the system. |
- RAM
The ram object reports system memory usage.
| Field | Type | Description |
|---|---|---|
| total | string | Total system RAM. |
| used | string | Currently used RAM. |
| free | string | Currently free RAM. |
- EMMC
The emmc object is a filtered eMMC view. It focuses on the main eMMC disk and its partitions.
{
"devices": [
{
"name": "mmcblk0",
"model": null,
"serial": "0x121b491e",
"size": "29.1G",
"type": "disk",
"transport": "mmc",
"filesystem": null,
"mountpoints": [],
"partitions": [
{
"name": "mmcblk0p1",
"size": "512M",
"type": "part",
"filesystem": "vfat",
"mountpoints": [
"/boot/firmware"
]
},
{
"name": "mmcblk0p2",
"size": "28.6G",
"type": "part",
"filesystem": "ext4",
"mountpoints": [
"/"
]
}
]
}
]
}
Device fields
| Field | Type | Description |
|---|---|---|
| devices[ ] | array | List of detected main eMMC devices. |
| devices[ ].serial | string / null | eMMC block device name, for example mmcblk0. |
| devices[ ].size | string | Device model, when available. |
| devices[ ].type | string | Device serial number. |
| devices[ ].transport | string / null | Device size. |
| devices[ ].filesystem | string / null | Device type, usually disk. |
| devices[ ].mountpoints | array | Transport type, usually mmc. |
| devices[ ].partitions | array | Filesystem directly on the disk. Usually null when partitions exist. |
| devices[ ].name | string | Mountpoints directly associated with the disk. |
| devices[ ].model | string / null | Partitions belonging to this eMMC disk. |
Partition fields
| Field | Type | Description |
|---|---|---|
| name | string | Partition name, for example mmcblk0p1. |
| size | string | Partition size. |
| type | string | Partition type, usually part. |
| filesystem | string / null | Filesystem type, for example vfat or ext4. |
| mountpoints | array | Mountpoints for the partition. |
block_devices
The block_devices object is the complete storage/device tree returned by lsblk.
{
"devices": [
{
"name": "mmcblk0",
"model": null,
"serial": "0x121b491e",
"size": "29.1G",
"type": "disk",
"transport": "mmc",
"filesystem": null,
"mountpoints": [],
"children": [
{
"name": "mmcblk0p1",
"model": null,
"serial": null,
"size": "512M",
"type": "part",
"transport": "mmc",
"filesystem": "vfat",
"mountpoints": [
"/boot/firmware"
],
"children": []
}
]
}
],
"stderr": ""
}
| Field | Type | Description |
|---|---|---|
| devices | array | List of block devices. |
| stderr | string | Command error output, if any. |
| devices[index].name | string | Device name. |
| devices[index].model | string / null | Device model. |
| devices[index].serial | string / null | Device serial number. |
| devices[index].size | string | Device size. |
| devices[index].type | string | Device type, for example disk , part , or loop. |
| devices[index].transport | string / null | Transport type, for example mmc , nvme , or usb. |
| devices[index].filesystem | string / null | Filesystem type, for example ext4 , vfat , or swap. |
| devices[index].mountpoints | array | List of mountpoints. Empty if not mounted. |
| devices[index].children | array | Child block devices, usually partitions. |
File_systems
The file_systems object reports mounted filesystem usage, equivalent to df -h.
{
"filesystems": [
{
"source": "/dev/mmcblk0p2",
"filesystem": "ext4",
"size": "29G",
"used": "5.8G",
"available": "23G",
"use_percent": "21%",
"mountpoint": "/",
"parsed": true
},
{
"source": "/dev/nvme0n1p1",
"filesystem": "ext4",
"size": "234G",
"used": "2.1M",
"available": "222G",
"use_percent": "1%",
"mountpoint": "/mnt/nvme",
"parsed": true
}
],
"stderr": ""
}
| Field | Type | Description |
|---|---|---|
| filesystems | array | List of mounted filesystems. |
| stderr | string | Command error output, if any. |
| filesystems[index].source | string | Filesystem source, for example /dev/mmcblk0p2, tmpfs, or udev.
|
| filesystems[index].filesystem | string | Filesystem type, for example ext4, vfat, tmpfs, or devtmpfs.
|
| filesystems[index].size | string | Total filesystem size. |
| filesystems[index].used | string | Used space. |
| filesystems[index].available | string | Available space. |
| filesystems[index].use_percent | string | Used percentage. |
| filesystems[index].mountpoint | string | Mount location. |
| filesystems[index].parsed | boolean | true if the parser successfully parsed this filesystem entry.
|
mmc_sysfs
The mmc_sysfs object exposes low-level MMC/eMMC identity fields read from:
/sys/block/mmcblk*/device/This is useful for production traceability, device identification and diagnostics.
{
"source": "/sys/block",
"fields": [
"name",
"cid",
"csd",
"date",
"manfid",
"oemid",
"serial"
],
"devices": [
{
"device": "mmcblk0",
"base_path": "/sys/block/mmcblk0/device",
"available": true,
"fields": {
"name": "BJTD4R",
"cid": "150100424a5444345203121b491e7b00",
"csd": "d02701320f5903fff6dbffef8e404000",
"date": "07/2024",
"manfid": "0x000015",
"oemid": "0x0100",
"serial": "0x121b491e"
}
}
]
}
| Field | Type | Description |
|---|---|---|
| source | string | Base sysfs source path. |
| fields | array | List of sysfs fields collected. |
| devices | array | List of detected MMC/eMMC devices. |
| devices[index].device | string | MMC block device name, for example mmcblk0. |
| devices[index].base_path | string | Sysfs path used for this device. |
| devices[index].available | boolean | Indicates whether the sysfs path exists. |
| devices[index].fields.name | string / null | eMMC product name. |
| devices[index].fields.cid | string / null | Card Identification register. |
| devices[index].fields.csd | string / null | Card Specific Data register. |
| devices[index].fields.date | string / null | Manufacturing date. |
| devices[index].fields.manfid | string / null | Manufacturer ID. |
| devices[index].fields.oemid | string / null | OEM/application ID. |
| devices[index].fields.serial | string / null | eMMC serial number. |
emmc_extcsd
The emmc_extcsd object contains parsed information from:
mmc extcsd read /dev/mmcblk0This section is useful for:
| Category | Description |
|---|---|
| revision | EXT_CSD and MMC version. |
| health | eMMC lifetime and pre-EOL state. |
| boot | Boot partition/protection configuration. |
| rpmb | Replay Protected Memory Block information. |
| sector | Sector count and capacity information, when available. |
| partitioning | Partitioning and enhanced-area information. |
{
"command": "mmc extcsd read <device>",
"devices": [
{
"success": true,
"device": "/dev/mmcblk0",
"command": "mmc extcsd read /dev/mmcblk0",
"returncode": 0,
"revision": {
"ext_csd_revision": "1.8",
"mmc_version": "MMC 5.1"
},
"health": {},
"boot": {},
"rpmb": {},
"sector": {},
"partitioning": {}
}
]
}
| Field | Type | Description |
|---|---|---|
| command | string | Generic command pattern. |
| devices | array | Parsed EXT_CSD results per eMMC device. |
| success | boolean | true if EXT_CSD was read successfully for this device.
|
| devices[index].device | string | Device path, for example /dev/mmcblk0.
|
| devices[index].command | string | Exact command executed for this device. |
| devices[index].returncode | number / null | Process return code. |
| devices[index].revision | object | EXT_CSD revision and MMC version. |
| devices[index].health | object | Lifetime and pre-EOL information. |
| devices[index].boot | object | Boot-related information. |
| devices[index].rpmb | object | RPMB information. |
| devices[index].sector | object | Sector count and capacity data. |
| devices[index].partitioning | object | Partitioning and enhanced-area configuration. |
emmc_extcsd.devices[].revision
{
"ext_csd_revision": "1.8",
"mmc_version": "MMC 5.1"
}
| Field | Type | Description |
|---|---|---|
| ext_csd_revision | string | EXT_CSD revision. |
| mmc_version | string | MMC/eMMC standard version. |
emmc_extcsd.devices[].health
{
"life_time_estimation_a": {
"field": {
"register": "DEVICE_LIFE_TIME_EST_TYP_A",
"index": null,
"description": "eMMC Life Time Estimation A",
"value": "0x01",
"value_int": 1,
"value_hex": "0x1"
},
"decoded": {
"state": "normal",
"used_percent_min": 0,
"used_percent_max": 10
}
},
"life_time_estimation_b": {
"field": {
"register": "DEVICE_LIFE_TIME_EST_TYP_B",
"index": null,
"description": "eMMC Life Time Estimation B",
"value": "0x01",
"value_int": 1,
"value_hex": "0x1"
},
"decoded": {
"state": "normal",
"used_percent_min": 0,
"used_percent_max": 10
}
},
"pre_eol": {
"field": {
"register": "PRE_EOL_INFO",
"index": null,
"description": "eMMC Pre EOL information",
"value": "0x01",
"value_int": 1,
"value_hex": "0x1"
},
"decoded": {
"state": "normal"
}
}
}
Fields
| Field | Description |
|---|---|
| life_time_estimation_a | Wear estimation for eMMC memory type A. |
| life_time_estimation_b | Wear estimation for eMMC memory type B. |
| pre_eol | Pre-end-of-life state. |
Lifetime Decoding
| Raw Value | Decoded State | Meaning |
|---|---|---|
| 0x00 | not_defined | Not defined by device. |
| 0x01 | normal | 0% to 10% estimated lifetime used. |
| 0x02 | normal | 10% to 20% estimated lifetime used. |
| 0x03 | normal | 20% to 30% estimated lifetime used. |
| 0x04 | normal | 30% to 40% estimated lifetime used. |
| 0x05 | normal | 40% to 50% estimated lifetime used. |
| 0x06 | normal | 50% to 60% estimated lifetime used. |
| 0x07 | normal | 60% to 70% estimated lifetime used. |
| 0x08 | normal | 70% to 80% estimated lifetime used. |
| 0x09 | normal | 80% to 90% estimated lifetime used. |
| 0x0A | normal | 90% to 100% estimated lifetime used. |
| 0x0B | exceeded | Estimated maximum lifetime exceeded. |
Pre-EOL Decoding
| Raw Value | Decoded State | Meaning |
|---|---|---|
| 0x00 | not_defined | Not defined. |
| 0x01 | normal | Normal condition. |
| 0x02 | warning | Warning state. Monitor or plan replacement. |
| 0x03 | urgent | Urgent state. Replacement recommended. |
emmc_extcsd.devices[].boot
{
"partition_config": {
"field": null,
"decoded": null
},
"boot_config_protection": null,
"boot_bus_conditions": null,
"boot_write_protection": {
"register": "BOOT_WP",
"index": null,
"description": "Boot Area Write protection",
"value": "0x00",
"value_int": 0,
"value_hex": "0x0"
},
"boot_size": {
"field": null,
"decoded": null
}
}
| Field | Type | Description |
|---|---|---|
| partition_config | object / null | Boot partition configuration, if available. |
| boot_config_protection | object / null | Boot configuration protection status. |
| boot_bus_conditions | object / null | Boot bus width/mode conditions. |
| boot_write_protection | object / null | Boot area write protection status. |
| boot_size | object / null | Boot partition size, if available. |
emmc_extcsd.devices[].rpmb
RPMB means Replay Protected Memory Block. It is a protected eMMC area often used for secure storage.
{
"size": {
"field": {
"register": "RPMB_SIZE_MULT",
"index": null,
"description": "RPMB Size",
"value": "0x20",
"value_int": 32,
"value_hex": "0x20"
},
"decoded": {
"bytes": 4194304,
"kb": 4194.3,
"mb": 4.19,
"gb": 0.0,
"kib": 4096.0,
"mib": 4.0,
"gib": 0.0
}
}
}
| Field | Type | Description |
|---|---|---|
| size.field | object / null | Raw RPMB size register information. |
| size.decoded.bytes | number | RPMB size in bytes. |
| size.decoded.kb | number | RPMB size in decimal KB. |
| size.decoded.mb | number | RPMB size in decimal MB. |
| size.decoded.gb | number | RPMB size in decimal GB. |
| size.decoded.kib | number | RPMB size in KiB. |
| size.decoded.mib | number | RPMB size in MiB. |
| size.decoded.gib | number | RPMB size in GiB. |
emmc_extcsd.devices[ ].sector
{
"sector_count": null,
"sector_size_bytes": 512,
"user_capacity": null
}
| Field | Type | Description |
|---|---|---|
| sector_count | object / null | Parsed sector count register, if available. |
| sector_size_bytes | number | Sector size used for capacity calculation. |
| user_capacity | object / null | Calculated user capacity, if sector count is available. |
emmc_extcsd.devices[].partitioning
{
"partitioning_support": {
"register": "PARTITIONING_SUPPORT",
"index": null,
"description": "Partitioning Support",
"value": "0x07",
"value_int": 7,
"value_hex": "0x7"
},
"partition_setting_completed": {
"register": "PARTITION_SETTING_COMPLETED",
"index": null,
"description": "Partitioning Setting",
"value": "0x00",
"value_int": 0,
"value_hex": "0x0"
},
"partition_attribute": {
"register": "PARTITIONS_ATTRIBUTE",
"index": null,
"description": "Partitions attribute",
"value": "0x00",
"value_int": 0,
"value_hex": "0x0"
},
"max_enhanced_area_size": {
"register": "MAX_ENH_SIZE_MULT",
"index": null,
"description": "Max Enhanced Area Size",
"value": "0x000747",
"value_int": 1863,
"value_hex": "0x747"
},
"enhanced_user_area_start": {
"register": "ENH_START_ADDR",
"index": null,
"description": "Enhanced User Data Start Address",
"value": "0x00000000",
"value_int": 0,
"value_hex": "0x0"
},
"enhanced_user_area_size": {
"register": "ENH_SIZE_MULT",
"index": null,
"description": "Enhanced User Data Area Size",
"value": "0x000000",
"value_int": 0,
"value_hex": "0x0"
}
}
| Field | Type | Description |
|---|---|---|
| partitioning_support | object / null | Supported partitioning features. |
| partition_setting_completed | object / null | Indicates whether partition settings were finalized. |
| user_capacity | object / null | Partition attribute register. |
| max_enhanced_area_size | object / null | Maximum enhanced area size multiplier. |
| enhanced_user_area_start | object / null | Enhanced user data area start address. |
| enhanced_user_area_size | object / null | Enhanced user data area size. |
4. TempSensor Actor
4.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Set temperature sensor configurations
Req-Topic: tempSensor/config/in
ALERT:
- activate: true(active)/false
- templow: alert temperature lower limit
- tempHigh: alert temperature upper limit
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"ALERT": {
"activate": true,
"tempLow": 0,
"tempHigh": 80
}
}
}
}
Reply-Topic: tempSensor/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"ALERT": {
"activate": true,
"tempLow": 0,
"tempHigh": 80
}
}
}
}
b) Read temperature sensor configurations
Req-Topic: tempSensor/config/in
ALERT:
- activate: true(active)/false
LIMITS:
- templow: alert temperature lower limit
- tempHigh: alert temperature upper limit
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: tempSensor/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"ALERT": {
"activate": true,
"tempLow": 0,
"tempHigh": 80
}
}
}
}
c) Read temperature and alert status
- unit: Temperature unit of measure
- alert: true (triggered) or false
Req-Topic: tempSensor/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: tempSensor/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true,
"data": {
"temperature": <_FLOAT_>,
"unit": "C",
"alert": true
}
}
}
}
5. TFT Actor
5.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to control and interact with the TFT display, or frontal panel LCD screen. Connecting to the broker for this Area requires no credentials.
5.2 TFT Display Actor
The TFT Actor is responsible for:
- Rendering static and informative screens on the TFT display
- Reacting to joystick input (LEFT/ RIGHT/ CENTER/ UP/ DOWN)
- Managing screen navigation and user confirmation flows
- Providing feedback to the application via MQTT
a) Screen Carousel Concept
The TFT actor maintains an ordered list of screens (carousel).
- Screens are navigated using the joystick:
- LEFT --> Previous screen
- RIGHT --> Next screen
- The carousel wraps around:
- LEFT on first screen --> last screen
- RIGHT on last screen --> first screen
b) Screen Types
The TFT actor supports the following screen types:
Image Screen
- Displays a static JPEG image
- Default screen shown at boot
- No user interaction required
Network Information Screen
- Display an interface with information about the networks of the device
- Updated on load
- No user interaction required
c) Joystick Navigation Logic
Carousel Navigation:
| Joystick Input | Action |
| LEFT | Previous screen |
| RIGHT | Next screen |
| DOWN | Refresh current screen |
d) Boot Behavior
At system boot:
- TFT actor initializes the display
- Last available carousel is loaded
- First screen is rendered
- Joystick subscriptions are activated
- Actor waits for runtime commands
e) Design Notes & Constraints
- TFT actor does not expose joystick state
- Joystick actor remains independent
- TFT actor internally consumes joystick events
- Carousel state is local to the TFT actor
- Screen rendering is synchronous; input handling is asynchronous
- Confirmation screens always require explicit CENTER press
5.3 User interaction through MQTT
Enabling interaction with the carousel using the topic tft/runtime/in . Actions available will be public to the user.
Runtime commands
Go to screen
Req-Topic: tft/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_CAROUSEL",
"params": {
"index": 2
}
}
}
Reply-Topic: tft/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "tft",
"task": {
"taskResult": {
"success": true,
"index": 2
}
}
}
Set TFT Screen Carousel Req-Topic: tft/runtime/in
{
"id": "TIMESTAMP",
"origin": "APP",
"task": {
"action": "SET_CAROUSEL",
"params": {
"startIndex": 0,
"screens": [
{
"type": "image",
"image": "default.jpg"
},
{
"type": "image",
"image": "status.jpg"
},
{
"type": "confirm",
"question": "Do you want to continue?"
}
]
}
}
}
Parameters:
| Field | Description |
| screens | Ordered list of screens |
| Start Index | Initial active screen (optional, default = 0) |
Reply
Reply-Topic: tft/runtime/out
{
"id": "TIMESTAMP",
"origin": "tft",
"task": {
"taskResult": {
"success": true,
"activeIndex": 0,
"totalScreens": 3
}
}
}
6. Analog Inputs Actor
6.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Configurations
Topic: analogInputs/config/in - Set ADC channels configurations
mode
- continuous
- single
type
- voltage
- current
{
"id": TIMESTAMP,
"origin": "Dev",
"task": {
"taskParams": {
"AI1": {
"mode": "single",
"type": "voltage"
},
"AI2": {
"mode": "single",
"type": "voltage"
},
"AI3": {
"mode": "single",
"type": "current"
},
"AI4": {
"mode": "single",
"type": "current"
}
}
}
}
Reply-Topic: analogInputs/config/out
{
"id": TIMESTAMP,
"origin": "ioExpander",
"task": {
"taskResult": {
"AI1": {
"success": true
},
"AI2": {
"success": true
},
"AI3": {
"success": true
},
"AI4": {
"success": true
}
}
}
}
b) Runtime Data
Topic: analogInputs/runtime/in.
action:
- READ
- PAUSE (only applicable in continuous mode)
- RESUME (only applicable in continuous mode)
{
"id": TIMESTAMP,
"origin": "Dev",
"task": {
"taskParams": {
"AI1": {
"action": "READ"
},
"AI2": {
"action": "READ"
},
"AI3": {
"action": "READ"
},
"AI4": {
"action": "READ"
}
}
}
}
Reply-Topic: analogInputs/runtime/out
{
"id": TIMESTAMP,
"origin": "Dev",
"task": {
"taskResult": {
"AI1": {
"result": _FLOAT_,
"unit": "V",
},
"AI2": {
"result": _FLOAT_,
"unit": "V",
},
"AI3": {
"result": _FLOAT_,
"unit": "mA",
},
"AI4": {
"result": _FLOAT_,
"unit": "mA",
}
}
}
}
7. Analog Outputs Actor
7.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Set analog outputs signal
Req-Topic: analogOutputs/config/in
Output: current within 0-20 mA
mode: on/off
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"mode": "on",
"AO1": {
"current": <_FLOAT_>
},
"AO2": {
"current": <_FLOAT_>
}
}
}
}
Reply-Topic: analogOutputs/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"mode": "on",
"AO1": {
"success": true,
"current": <_FLOAT_>
},
"AO2": {
"success": true,
"current": <_FLOAT_>
}
}
}
}
b) Set analog outputs
Req-Topic: analogOutputs/runtime/in
current: current within 0-20 mA
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"AO1": {
"action": "SET",
"current": <_FLOAT_>
},
"AO2": {
"action": "SET",
"current": <_FLOAT_>
}
}
}
}
Reply-Topic: analogOutputs/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"AO1": {
"success": true,
"current": <_FLOAT_>
},
"AO2": {
"success": true,
"current": <_FLOAT_>
}
}
}
}
c) Read analog outputs settings
Req-Topic: analogOutputs/runtime/in
current: current within 0-20 mA
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"AO1": {
"action": "READ"
},
"AO2": {
"action": "READ"
}
}
}
}
Reply-Topic: analogOutputs/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"AO1": {
"success": true,
"current": <_FLOAT_>
},
"AO2": {
"success": true,
"current": <_FLOAT_>
}
}
}
}
8. Digital Inputs Actor
8.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Set digital input pin/pins speed fast/slow
The fast speed setting supports edge detection speed up to 100 Hz and the slow speed settings supports up to 10 Hz.
Fast Mode generates a message on every I/O state change, which may result in a high message payload during operation. Fast Mode should therefore only be enabled when strictly required.
Req-Topic: digitalInputs/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"DI1": {
"speed": "fast"
},
"DI2": {
"speed": "slow"
},
"DI3": {
"speed": "fast"
},
"DI4": {
"speed": "fast"
}
}
}
}
Reply-Topic: digitalInputs/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DI1": {
"success": true,
"speed": "fast"
},
"DI2": {
"success": true,
"speed": "slow"
},
"DI3": {
"success": true,
"speed": "fast"
},
"DI4": {
"success": true,
"speed": "fast"
}
}
}
}
b) Get all digital input pins state
Req-Topic: digitalInputs/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: digitalInputs/runtime/out state: high/low
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DI1": {
"success": true,
"state": "high"
},
"DI2": {
"success": true,
"state": "low"
},
"DI3": {
"success": true,
"state": "high"
},
"DI4": {
"success": true,
"state": "low"
}
}
}
}
9. Digital Outputs Actor
9.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this area requires no credentials.
a) Get all digital output pins state
Req-Topic: digitalOutputs/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: digitalOutputs/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DO1": {
"success": true,
"state": "low"
},
"DO2": {
"success": true,
"state": "high"
},
"DO3": {
"success": true,
"state": "high"
},
"DO4": {
"success": true,
"state": "low"
},
"DO5": {
"success": true,
"state": "low"
},
"DO6": {
"success": true,
"state": "low"
},
"DO7": {
"success": true,
"state": "low"
},
"DO8": {
"success": true,
"state": "low"
}
}
}
}
b) Set all digital output pins state
Req-Topic: digitalOutputs/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskParams": {
"DO1": {
"action": "SET",
"state": "low"
},
"DO2": {
"action": "SET",
"state": "high"
},
"DO3": {
"action": "SET",
"state": "high"
},
"DO4": {
"action": "SET",
"state": "low"
},
"DO5": {
"action": "SET",
"state": "low"
},
"DO6": {
"action": "SET",
"state": "low"
},
"DO7": {
"action": "SET",
"state": "low"
},
"DO8": {
"action": "SET",
"state": "low"
}
}
}
}
Reply-Topic: digitalOutputs/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DO1": {
"success": true,
"state": "low"
},
"DO2": {
"success": true,
"state": "high"
},
"DO3": {
"success": true,
"state": "high"
},
"DO4": {
"success": true,
"state": "high"
},
"DO5": {
"success": true,
"state": "high"
},
"DO6": {
"success": true,
"state": "low"
},
"DO7": {
"success": true,
"state": "low"
},
"DO8": {
"success": true,
"state": "low"
}
}
}
}
9.2 Notes
If the busi2c-3 is not powered, we will get this:
Topic: digit a lOutputs/runtime/ out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"DO1": {
"success": false,
"state": "DO_not_powered"
},
"DO2": {
"success": false,
"state": "DO_not_powered"
},
"DO3": {
"success": false,
"state": "DO_not_powered"
},
"DO4": {
"success": false,
"state": "DO_not_powered"
},
"DO5": {
"success": false,
"state": "DO_not_powered"
},
"DO6": {
"success": false,
"state": "DO_not_powered"
},
"DO7": {
"success": false,
"state": "DO_not_powered"
},
"DO8": {
"success": false,
"state": "DO_not_powered"
}
}
}
}
10. Joystick Actor
10.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtain peripherals data. Connecting to the broker for this Area requires no credentials.
a) Get all joystick pins state
Req-Topic: joystick/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "READ"
}
}
Reply-Topic: joystick//runtime/out state: pressed/released
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"JLEFT": {
"success": true,
"state": "pressed"
},
"JRIGHT": {
"success": true,
"state": "released"
},
"JUP": {
"success": true,
"state": "released"
},
"JDOWN": {
"success": true,
"state": "released"
},
"JCENTER": {
"success": true,
"state": "released"
}
}
}
}
11. Buzzer Actor
11.1 Overview
The Buzzer Actor is responsible for controlling the onboard buzzer through MQTT commands. It receives runtime commands on an MQTT input topic, validates the requested buzzer operation, drives the buzzerthrough GPIO using libgpiod, and publishes status/final responses on an MQTT output topic.
The actor currently controls GPIO line 25 on /dev/gpiochip0, with ACTIVE_LOW = False. It creates a
GpiodBuzzerDriver instance and routes incoming MQTT actions to runtime handlers.
The supported runtime actions are:
| Action | Description |
| BEEP | Plays a finite beep sequence, an infinite beep sequence, or a continuous tone |
| STOP | Stops the currently active buzzer playback |
11.2 MQTT Topics
| Topic | Direction | Description |
| buzzer/runtime/in | Input | Receives buzzer commands |
| buzzer/runtime/out | Output | Publishes status messages and final command responses |
The actor subscribes to buzzer/runtime/in and publishes all status/final replies to buzzer/runtime/out.
11.3 Hardware and Datasheet Notes
The buzzer used is the MLT-8530 electro-magnetic SMD buzzer.
According to the datasheet:
| Parameter | Value |
| Rated voltage | 3.6 Vo-p |
| Operating voltage | 2.5 ~ 4.5 Vo-p |
| Rated current | Max. 95 mA |
| Rated test frequency | 2700 Hz |
| Duty cycle | 50% square wave |
| Sound output | Min. 80 dB at 10 cm |
| Coil resistance | 16 ± 3 Ω |
| Resonant frequency | 2700 Hz |
11.4 BEEP
The BEEP action supports two modes:
| Mode | Description |
| sequence | Plays one or more beeps with gaps between them |
| continuous | Plays one continuous tone until STOP is received |
If no mode is provided, the default mode is: sequence
All messages should be sent to the topic buzzer/runtime/in
a) Default Datasheet-Aligned Beep
{
"id": <TIMESTAMP>,
"origin": "APP",
"task": {
"action": "BEEP"
}
}
b) N Beeps
Play <BEEPS_QUANTITY> beeps at <FREQUENCY> Hz. Each beep lasts <BEEP_DURATION> seconds, with
<GAP_BETWEEN_BEEPS> seconds of silence between beeps.
{
"id": <TIMESTAMP>,
"origin": "APP",
"task": {
"action": "BEEP",
"params": {
"frequency_hz": <FREQUENCY>,
"duration_s": <BEEP_DURATION>,
"count": <BEEPS_QUANTITY>,
"gap_s": <GAP_BETWEEN_BEEPS>
}
}
}
If you want to play repeated beeps forever until a STOP command arrives then you only need to pass the count: 0
c) Continuous Tone
Play one continuous <FREQUENCY> Hz tone until STOP.
{
"id": <TIMESTAMP>,
"origin": "APP",
"task": {
"action": "BEEP",
"params": {
"mode": "continuous",
"frequency_hz": <FREQUENCY>
}
}
}
If frequency_hz is omitted, the handler uses the configured default frequency.
d) Stop Buzzer
If you want to stop the beep sequence, just send this payload to the buzzer/runtime/in
{
"id": <TIMESTAMP>,
"origin": "APP",
"task": {
"action": "STOP",
}
}
e) Buzzer payload parameters table
| Field | Required | Type | Default | Applies to | Description |
|---|---|---|---|---|---|
| task | Yes | object | - | All requests | Main task object. Contains the requested action and
optional parameters. |
| task.action | Yes | string | - | All requests | Action to execute. Supported values: "BEEP" and
"STOP". |
| task.params | No | object | {} | BEEP | Optional parameters for configuring the buzzer
behavior. Not needed for STOP. |
| task.params.mode | No | string | "sequence" | BEEP | Playback mode. Supported values: "sequence" and
"continuous". |
| task.params.frequency_hz | No | number | 2700.0 | BEEP | Buzzer frequency in Hz. Recommended/default value
is 2700 Hz , matching the buzzer resonant frequency. |
| task.params.duration_s | No | number | 0.2 | BEEP with
mode="sequence" |
Duration of each beep, in seconds. |
| task.params.count | No | integer | 1 | BEEP with
mode="sequence" |
Number of beeps to play. 0 means repeat forever
until STOP. |
| task.params.gap_s | No | number | 0.15 | BEEP with
mode="sequence" |
Silence gap between beeps, in seconds. |
| task.params.mode ="continuous" | No | string | - | BEEP | Plays one continuous tone until STOP . In this mode,
duration_s , count , and gap_s are ignored. |
11.5 Buzzer - diagram
11.6 Buzzer Command timing charts - Examples
The following charts represent the command-level buzzer state over time.
They do not show the internal 2700 Hz switching frequency.
Instead, they show when the buzzer is logically:
| State | Meaning |
| 1 | Buzzer active / sound ON |
| 0 | Buzzer inactive / silence |
a) Default BEEP command
{
"id": <TIMESTAMP>,
"origin": "APP",
"task": {
"action": "BEEP",
}
}
Default values used:
| Parameter | Value |
| duration_s | 0.2 s |
| count | 1 |
| gap_s | 0.15 s |
Buzzer State
duration_s = 0.2 s
count = 1
b) BEEP Sequence - 5 beeps
{
"id": 1779789836000,
"origin": "APP",
"task": {
"action": "BEEP",
"params": {
"duration_s": 1,
"count": 5,
"gap_s": <GAP_BETWEEN_BEEPS>
}
}
}
Buzzer State
gap_s = 0.5s
count = 5
c) Infinite BEEP Sequence
This happens when: "count": 0
{
"id": 1779789836000,
"origin": "APP",
"task": {
"action": "BEEP",
"params": {
"duration_s": 1,
"count": 0,
"gap_s": 0.5
}
}
}
Buzzer State
duration_s = 1.0 gap_s = 0.5
count = 0
d) Continuous BEEP
In continuous mode, the buzzer remains ON until a STOP command is received.
{
"id": 1779789836000,
"origin": "APP",
"task": {
"action": "BEEP",
"params": {
"mode": "continuous",
}
}
}
Buzzer State
mode = continuous
e) STOP
The STOP command immediately forces the buzzer to OFF.
Buzzer State
After STOP, the buzzer is forced to OFF.
12. Power Monitor actor
12.1 Overview
The Power Monitor actor is responsible for reading the ATLAS INA232 power monitor and publishing the measuredelectrical values through MQTT.
The actor publishes:
| Value | Unit | Description |
| voltage_v | V | Measured bus/input voltage |
| current_a | A | Measured current |
| power_w | W | Measured/calculated power |
The INA232 is a current, voltage, and power monitor with an I²C/SMBus interface. It reports current, busvoltage, and power values through internal registers. The calibration register must be configured to obtainvalid current and power results.
12.2 Runtime domain
- Automatic Runtime Publication
Publish topic:
ina/runtime/out
Period: 1000 ms === 1s
Response payload
{
"id": "<TIMESTAMP>",
"origin": "INA",
"task": {
"action": "PUBLISH_POWER",
"taskResult": {
"success": true,
"voltage_v": 24.12,
"current_a": 0.481,
"power_w": 11.6
}
}
}
| Field | Type | Required | Description |
| success | boolean | - | Indicates whether the read operation succeeded |
| voltage_v | number | V | Measured bus/input voltage |
| current_a | number | A | Measured current |
| power_w | number | W | Measured power |
13. Modem Actor
13.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker. Simplified interface to configure and obtainmodem (LTE) data. Connecting to the broker for this area requires no credentials.
13.2 Mobile (LTE Modem)
The Mobile actor manages the LTE modem using ModemManager (mmcli). It allows monitoring modem status, configuring connectivity (APN, operator,network mode), and performing diagnostics.
13.3 Scan Operators
Scans available network operators (requires SIM and modem enabled).
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SCAN_OPERATORS"
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"operators": [
{
"id": "26801",
"name": "vodafone P",
"status": "available"
},
{
"id": "26806",
"name": "MEO",
"status": "available"
}
]
}
}
}
13.4 Set Operator
The supported modes are:
- auto → modem selects de operator automatically (default).
- manual→ user forces a specific operator using operator_id.
- Operators are identified using MCC + MNC codes:
| Operator | Code | Country |
| Vodafone PT | 26801 | Portugal |
| MEO | 26806 | Portugal |
| NOS | 26803 | Portugal |
These values are obtained dynamically via SCAN_OPERATORS
- Recommendations:
Default should always be auto
Use manual only when:
- You need to force roaming behavior
- You want to lock to a specific network for stability/testing
Auto Selection
Req-Topic: modem/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_OPERATOR",
"taskParams": {
"mode": "auto"
}
}
}
Reply-Topic: modem/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true
}
}
}
Manual Selection
Req-Topic: modem/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_OPERATOR",
"taskParams": {
"mode": "manual",
"operator_id": "26801"
}
}
}
Reply-Topic: modem/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true
}
}
}
13.5 Set APN Configuration
Supports automatic (predefined APN) or custom configuration.
Actual Behavior
When SET_APN is executed, the system performs the following sequence:
- Disconnects the current data connection (if connected)
- Applies the new APN configuration
- Initiates a new connection using the provided APN
- Waits until the modem reaches connected state
This means a full reconnection is automatically performed.
No additional RESET or manual reconnection is required.
Impact & Considerations
- Active data connection is interrupted
- Ongoing communications (MQTT, HTTP, VPN, etc.) will be dropped
- IP address may change after reconnection
- Operation may take several seconds (typically 5–20s)
- If connection fails, the modem may remain disconnected
Known APNs
| Operator | APN CONF
NAME |
APN | PROXY | PORT | MCC | MNC | AUTH
TYPE |
APN
TYPE |
CARRIER | OBS. |
| MEO, UZO | -- | internet | -- | -- | -- | -- | -- | -- | -- | -- |
| 1nce | -- | iot.1nce.net | -- | -- | -- | -- | -- | -- | -- | -- |
| Vodafone | Vodafone
Internet |
net2.vodafone.pt | iproxy.vodafone.pt | 80 | 268 | 01 | PAP | default | unspecified | Docs |
Supported Modes
- auto
- custom
| Mode | Description |
| auto | Uses modem default / SIM-provided APN |
| custom | Uses user-defined APN credentials |
- Auto APN (e.g. 1NCE)
Uses the default APN embedded in the modem or provided by the SIM.
Req-Topic: modem/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_APN",
"taskParams": {
"mode": "auto"
}
}
}
Reply-Topic: modem/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"apn": "internet",
"success": true
}
}
}
- Custom APN
Allows full control over APN configuration.
Req-Topic: modem/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_APN",
"taskParams": {
"mode": "custom",
"apn": "internet",
"user": "user",
"password": "pass"
}
}
}
Reply-Topic: modem/config/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"apn": "internet",
"success": true
}
}
}
13.6 Get Modem Status
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "GET_STATUS"
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"modem": {
"present": true,
"enabled": true,
"state": "registered",
"signal": 34,
"access_tech": "lte",
"operator": "vodafone P",
"sim": "ready"
}
}
}
}
13.7 Enable / Disable Modem
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_MODEM_STATE",
"taskParams": {
"enabled": true
}
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"enabled": true,
"success": true
}
}
}
13.8 Reset Modem
Performs a full modem reset and recovery sequence.
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "RESET"
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"success": true
}
}
}
Actual Behavior
When RESET is executed, the system performs a complete modem restart flow:
- Executes system-level reset: ltectl reset-modem
- The modem disappears from the system (USB re-enumeration)
- The system waits for the modem to reappear
- Re-discovers the modem path
- Waits until the modem becomes ready
- Waits until the modem is connected again
This is a full recovery sequence, including automatic reconnection.
Impact on active connection
- Active data connection is immediately terminated
- All ongoing communications (MQTT, VPN, HTTP, etc.) will be interrupted
- Modem temporarily disappears from the system
- A new network registration is performed
- IP address will likely change
- Total recovery time may take 10–120 seconds
When to use
Use RESET only when:
- Modem is stuck in searching or not registering
- Connected but no data traffic is possible
- APN/operator changes are not taking effect
- Modem becomes unresponsive or unstable
When NOT to Use
Avoid using RESET :
- During OTA updates or file uploads
- During critical real-time operations
- If a simple reconnect ( SET_MODEM_STATE ) is sufficient
Application Considerations
Before RESET
- Ensure no critical operations are ongoing
- Persist any unsent or buffered data
- Expect loss of connectivity
After RESET
- Wait for modem to fully recover (can take up to ~2 minutes)
- Re-check modem status using: GET_STATUS
- Re-establish application-level connections: MQTT; VPN an Cloud sessions
13.9 Set Network Mode
Configures allowed radio technologies
Supported values:
- 2g
- 3g
- 4g
- auto
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SET_NETWORK_MODE",
"taskParams": {
"mode": "4g"
}
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"mode": "4g",
"success": true
}
}
}
13.10 Get Signal Strength
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "GET_SIGNAL"
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"signal": 34,
"unit": "%"
}
}
}
13.11 Get SIM card info
GET_SIM_INFO is a runtime action used to read SIM card information from the modem through ModemManager.
This action is read-only. It does not change the modem state, APN, operator, bearer, or NetworkManager configuration.
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "GET_SIM_INFO"
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "modem-actor",
"task": {
"taskResult": {
"success": true,
"sim": {
"id": "0",
"path": "/org/freedesktop/ModemManager1/SIM/0",
"active": true,
"iccid": "8935101234567890123",
"imsi": "268060123456789",
"operator_id": "26806",
"operator_name": "MEO",
"emergency_numbers": [
"112"
],
"preferred_networks": [
"operator-code: 42507, access-technologies: gsm, umts, lte",
"operator-code: 20810, access-technologies: gsm, umts, lte",
"operator-code: 64710, access-technologies: gsm, umts, lte",
"operator-code: 37001, access-technologies: gsm, umts, lte",
"operator-code: 34002, access-technologies: gsm, umts, lte"
]
}
}
}
}
| Field | Type | Description |
| id | number / string | Same request ID received in the input payload. |
| origin | string | Actor name, usually modem-actor. |
| task.taskResult.success | boolean | Indicates if the SIM information was read successfully. |
| task.taskResult.sim.id | string | SIM object ID used by ModemManager. Usually 0. |
| task.taskResult.sim.path | string | ModemManager DBus path for the SIM object. |
| task.taskResult.sim.active | boolean / null | Indicates if the SIM is active, when available. |
| task.taskResult.sim.iccid | string / null | SIM ICCID / SIM identifier. This is the physical SIM card identifier. |
| task.taskResult.sim.imsi | string / null | IMSI associated with the SIM subscription. |
| task.taskResult.sim.operator_id | string / null | Mobile operator identifier from the SIM, when available. |
| task.taskResult.sim.operator_name | string / null | Operator name from the SIM, when available. |
| task.taskResult.sim.emergency_numbers | array | Emergency numbers reported by the SIM/modem. |
| task.taskResult.sim.preferred_networks | array | Preferred networks stored on the SIM, when available. |
13.12 Test Modem
Performs a basic diagnostic test:
- Modem detection
- SIM presence
- Network registration
- Data connectivity
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "TEST"
}
}
Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"hw": true,
"sim": true,
"network": true,
"data": true
}
}
}
13.13 Speed Test
Performs a low-data network speed test over the modem interface.
This command does not run a full bandwidth benchmark.
Instead, it performs a short and capped download burst on interface wwan0 , measures the number of received bytes during a fixed time window, and
returns an estimated download speed together with latency and a confidence level.
This approach is designed for IoT / field devices using capped cellular SIM cards, where a classic speed test would consume too much data.
Req-Topic: modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"action": "SPEED_TEST"
}
}
Reply-Topic: modem/runtime/out Success reply:
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"download_mbps": 12.4,
"success": true
}
}
}
Error reply Example when the modem is not connected:
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"download_mbps": 12.4,
"success": true
}
}
}
Example when an unexpected internal error occurs:
{
"id": "<TIMESTAMP>",
"origin": "modem-actor",
"task": {
"taskResult": {
"success": false,
"reason": "unexpected_exception"
}
}
}
Reply parameters
| Parameter | Type | Description |
| success | boolean | Indicates whether the speed test completed successfully. true = test executed, false = failed (see reason ). |
| download_mbps | number | Estimated download speed in megabits per second (Mbps). Calculated from the number of bytes received on interface
wwan0 during the measurement window. |
| bytes_sampled | number | Total number of received bytes measured on the modem interface ( wwan0 ) during the test window. Used as the raw input
for speed calculation. |
| duration_s | number | Measurement window duration in seconds. Current implementation uses 5 seconds. |
| latency_ms | number | null |
| confidence | string | Qualitative indicator of measurement reliability based on sampled bytes:• low → < 50,000 bytes• medium → 50,000–
299,999 bytes• high → ≥ 300,000 bytes |
| reason | string | Present only when success=false . Provides failure reason (e.g., not_connected (<state>) , unexpected_exception ). |
Data consumption notice
This command performs a real network download and therefore consumes mobile data.
Although the test is designed to be low-data, it still generates traffic:
- Typical consumption per test: ~100 KB to ~500 KB
- Latency measurement adds a negligible amount (ICMP ping)
Estimated data usage
| Frequency | Estimated consumption |
| 1 test/day (~100 KB) | ~3 MB/month |
| 1 test/day (~250 KB) | ~7.5 MB/month |
| 1 test/day (~500 KB) | ~15 MB/month |
Recommendation
For devices using capped SIM cards, it is recommended to:
- Avoid running this test at high frequency
- Use signal and modem status ( mmcli ) for regular monitoring
- Reserve speed tests for:
- commissioning
- troubleshooting
- periodic health checks
Running frequent or extended tests may significantly impact the data allowance.
13.14 Get Raw Modem Info (Reserved)
Req-Topic: reserved/modem/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "modem-actor",
"task": {
"taskResult": {
"success": false,
"reason": "not_connected (registered)"
}
}
}
Reply-Topic: reserved/modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "DEV",
"task": {
"taskResult": {
"raw": "mmcli -m 0 output..."
}
}
}
13.15 Example: Raw Modem Output ( mmcli )
Command:
mmcli -m 0 --output-json
Example output:
{
"modem": {
"3gpp": {
"5gnr": {
"registration-settings": {
"drx-cycle": "--",
"mico-mode": "--"
}
},
"enabled-locks": [],
"eps": {
"initial-bearer": {
"dbus-path": "--",
"settings": {
"apn": "--",
"ip-type": "--",
"password": "--",
"user": "--"
}
},
"ue-mode-operation": "--"
},
"imei": "862636056280785",
"network-rejection-access-technology": "--",
"network-rejection-error": "--",
"network-rejection-operator-id": "--",
"network-rejection-operator-name": "--",
"operator-code": "--",
"operator-name": "--",
"packet-service-state": "--",
"pco": "--",
"registration-state": "--"
},
"cdma": {
"activation-state": "--",
"cdma1x-registration-state": "--",
"esn": "--",
"evdo-registration-state": "--",
"meid": "--",
"nid": "--",
"sid": "--"
},
"dbus-path": "/org/freedesktop/ModemManager1/Modem/0",
"generic": {
"access-technologies": [],
"bearers": [],
"carrier-configuration": "ROW_Gen_VoLTE",
"carrier-configuration-revision": "05010822",
"current-bands": [
"egsm",
"dcs",
"pcs",
"g850",
"utran-1",
"utran-4",
"utran-6",
"utran-5",
"utran-8",
"utran-2",
"eutran-1",
"eutran-2",
"eutran-3",
"eutran-4",
"eutran-5",
"eutran-7",
"eutran-8",
"eutran-12",
"eutran-13",
"eutran-18",
"eutran-19",
"eutran-20",
"eutran-25",
"eutran-26",
"eutran-28",
"eutran-34",
"eutran-38",
"eutran-39",
"eutran-40",
"eutran-41",
"eutran-66",
"utran-19"
],
"current-capabilities": [
"gsm-umts, lte"
],
"current-modes": "allowed: 4g; preferred: none",
"device": "/sys/devices/platform/scb/fe9c0000.xhci/usb1/1-1/1-1.4",
"device-identifier": "2c759b7021f62d2e26973150063ea040b3662a65",
"drivers": [
"option",
"qmi_wwan"
],
"equipment-identifier": "862636056280785",
"hardware-revision": "10000",
"manufacturer": "QUALCOMM INCORPORATED",
"model": "SIMCOM_SIM7600G-H",
"own-numbers": [],
"physdev": "/sys/devices/platform/scb/fe9c0000.xhci/usb1/1-1/1-1.4",
"plugin": "simtech",
"ports": [
"cdc-wdm0 (qmi)",
"ttyUSB0 (ignored)",
"ttyUSB1 (gps)",
"ttyUSB2 (at)",
"ttyUSB3 (at)",
"ttyUSB4 (audio)",
"wwan0 (net)"
],
"power-state": "on",
"primary-port": "cdc-wdm0",
"primary-sim-slot": "1",
"revision": "LE20B04SIM7600G22",
"signal-quality": {
"recent": "yes",
"value": "0"
},
"sim": "/org/freedesktop/ModemManager1/SIM/0",
"sim-slots": [
"/org/freedesktop/ModemManager1/SIM/0",
"/"
],
"state": "failed",
"state-failed-reason": "sim-missing",
"supported-bands": [
"egsm",
"dcs",
"pcs",
"g850",
"utran-1",
"utran-4",
"utran-6",
"utran-5",
"utran-8",
"utran-2",
"eutran-1",
"eutran-2",
"eutran-3",
"eutran-4",
"eutran-5",
"eutran-7",
"eutran-8",
"eutran-12",
"eutran-13",
"eutran-18",
"eutran-19",
"eutran-20",
"eutran-25",
"eutran-26",
"eutran-28",
"eutran-34",
"eutran-38",
"eutran-39",
"eutran-40",
"eutran-41",
"eutran-66",
"utran-19"
],
"supported-capabilities": [
"gsm-umts, lte"
],
"supported-ip-families": [
"ipv4",
"ipv6",
"ipv4v6"
],
"supported-modes": [
"allowed: 2g; preferred: none",
"allowed: 3g; preferred: none",
"allowed: 4g; preferred: none",
"allowed: 2g, 3g; preferred: 3g",
"allowed: 2g, 3g; preferred: 2g",
"allowed: 2g, 4g; preferred: 4g",
"allowed: 2g, 4g; preferred: 2g",
"allowed: 3g, 4g; preferred: 4g",
"allowed: 3g, 4g; preferred: 3g",
"allowed: 2g, 3g, 4g; preferred: 4g",
"allowed: 2g, 3g, 4g; preferred: 3g",
"allowed: 2g, 3g, 4g; preferred: 2g"
],
"unlock-required": "--",
"unlock-retries": []
}
}
}
Field Reference - mmcli -m 0 --output-json
| JSON Path | Meaning | Example Value | Notes / Interpretation |
| modem.dbus-path | Internal modem path | /org/.../Modem/0 | Used by mmcli ( -m 0 ) |
| modem.generic.manufacturer | Modem vendor | QUALCOMM INCORPORATED | Hardware info |
| modem.generic.model | Modem model | SIM7600G-H | |
| modem.generic.revision | Firmware version | LE20B04SIM7600G22 | Useful for debugging |
| modem.generic.device | Linux device path | /sys/devices/... | USB device |
| modem.generic.drivers | Kernel drivers | option, qmi_wwan | Must be present |
| modem.generic.primary-port | Control interface | cdc-wdm0 | Critical (QMI control) |
| modem.generic.ports | Available interfaces | ttyUSB*, wwan0 | Data + AT + GPS |
| modem.generic.power-state | Power status | on | |
| modem.generic.state | Modem state | failed | Main status field |
| modem.generic.state-failed-reason | Failure reason | sim-missing | Root cause of failure |
| modem.generic.signal-quality.value | Signal strength (%) | 0 | 0 = no signal |
| modem.generic.current-capabilities | Active radio tech | gsm-umts, lte | |
| modem.generic.current-modes | Active mode | allowed: 4g | Controlled via API |
| modem.generic.current-bands | Active frequency bands | [eutran-1, ...] | Coverage related |
| modem.generic.supported-bands | Supported bands | [eutran-*, utran-*] | Hardware capability |
| modem.generic.supported-ip-families | IP support | ipv4, ipv6, ipv4v6 | |
| modem.generic.sim | SIM path | /org/.../SIM/0 | |
| modem.generic.sim-slots | SIM slots | [slot1, slot2] | Slot 2 empty here |
| modem.generic.primary-sim-slot | Active SIM slot | 1 | |
| modem.generic.bearers | Active data sessions | [ ] | Empty = no connection |
| modem.3gpp.imei | Device IMEI | 862636056280785 | Unique identifier |
| modem.3gpp.operator-name | Network name | -- | Not available (no SIM) |
| modem.3gpp.operator-code | MCC/MNC | -- | Not available |
| modem.3gpp.registration-state | Network state | -- | Not registered |
| modem.3gpp.packet-service-state | Data service state | -- | No data session |
| modem.3gpp.eps.initial-bearer.settings.apn | APN | -- | Not set / no connection |
13.16 Notes:
- If no modem is detected: Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"modem": {
"present": false
}
}
}
}
- If SIM is locked or missing: Reply-Topic: modem/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"taskResult": {
"modem": {
"sim": "locked"
}
}
}
}
- Operator scan may take several seconds and should be handled asynchronously.
- Some actions require:
- SIM card inserted
- Modem enabled
- cdc-wdm0 interface available
- Signal strength is reported as percentage and may be cached by ModemActor.
14. CANopen Actor
14.1 App Interfacing
User App interface and functionalities exposed to the open part of the MQTT broker.
The CANopen Actor provides a generic protocol interface to CANopen devices.
The actor does NOT interpret device-specific meaning (Object Dictionary semantics).
It only handles communication (HOW to talk).
14.2 Transport and Message Envelope
a) MQTT Topics
| Topic | Direction | Description |
canopenRM8007/config/in
|
Request | Configuration commands, open broker area. |
canopenRM8007/config/out
|
Reply | Configuration replies. |
canopenRM8007/runtime/in
|
Request | Runtime commands, open broker area. |
canopenRM8007/runtime/out
|
Reply | Runtime replies and every continuous sample publication. |
reserved/canopenRM8007/config/in
|
Request | Same configuration commands, reserved (internal) broker area. |
reserved/canopenRM8007/config/out
|
Reply | Configuration replies for the reserved area. |
reserved/canopenRM8007/runtime/in
|
Request | Same runtime commands, reserved (internal) broker area. |
reserved/canopenRM8007/runtime/out
|
Reply | Runtime replies for the reserved area. |
Every reply is published on the request topic with /in replaced by /out.
Continuous sample publications (READ_CONTINUOUS, READ_PDO_CONTINUOUS) are always published on canopenRM8007/runtime/out, even when the start command arrived on reserved/canopenRM8007/runtime/in. Only the start/stop acknowledgement follows the request topic.
All publications use QoS 0 and are not retained.
b) Alias Defaults
task.alias should always be sent. When it is missing or empty, the actor assumes:
| Request topic | Assumed alias |
|---|---|
.../config/in
|
CONFIG_CANOPEN
|
.../runtime/in
|
PING
|
A payload that is not valid JSON is logged and discarded. No reply is published.
c) Node Reference (Yes*)
Parameters marked Yes* in the tables below mean "one of node_id, nodeId or node". The actor resolves them in this order:
node- logical name previously stored withCONFIG_CANOPEN. The stored node configuration is merged withtaskParams, andtaskParamswins on conflicts. An unknown name fails withunknown configured node <name>.node_id- numeric CANopen node ID (1..127).nodeId- alternative spelling fornode_id.- Single-node fallback - if none of the three is present and exactly one node is configured, that node is used automatically. With zero configured nodes, or with more than one, the command fails with
taskParams must include node_id, nodeId or node.
The single-node fallback does not apply to READ: with no node reference, READ reads every configured node (see Read All Configured Nodes).
Yes** marks a parameter that is mandatory for the operation but can also be resolved from the stored node configuration when node is used.
d) Error Envelope
There are two different error shapes, and they differ in origin.
Handler-level error - the command reached its handler and the CANopen operation failed in a controlled way. origin is canopen-actor:
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "SDO_READ",
"taskResult": {
"success": false,
"error": "SDO timeout 0x6004:00"
}
}
}
Actor-level error - unknown alias, malformed parameters, or an exception raised inside a handler. origin is Dev, not canopen-actor, and taskResult carries only success and error:
{
"id": "<TIMESTAMP>",
"origin": "Dev",
"task": {
"alias": "SDO_READ",
"taskResult": {
"success": false,
"error": "missing integer parameter"
}
}
}
Applications must therefore key on task.taskResult.success, never on origin.
| Error message | Envelope | Cause |
|---|---|---|
unknown alias <ALIAS>
|
Dev
|
Alias not implemented for that topic family. |
taskParams must include node_id, nodeId or node
|
Dev
|
No node reference and the single-node fallback did not apply. |
unknown configured node <name>
|
Dev
|
node name was never stored with CONFIG_CANOPEN.
|
missing integer parameter
|
Dev
|
A required index or value was not sent.
|
invalid literal for int() with base 0: ...
|
Dev
|
A hex string parameter is malformed. |
timeout
|
canopen-actor
|
The hardware owner did not answer the internal RPC in time. |
SDO timeout 0x<IDX>:<SUB>
|
canopen-actor
|
The node did not answer the SDO request. |
SDO abort 0x<IDX>:<SUB> -> 0x<CODE> (<NAME>)
|
canopen-actor
|
The node rejected the SDO request. |
14.3 Configuration
Configure CANopen Nodes
Req-Topic: canopenRM8007/config/in
Stores CANopen defaults and logical node configuration inside the actor.
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "CONFIG_CANOPEN",
"taskParams": {
"defaults": {
"sdo_timeout": "<SDO_TIMEOUT>"
},
"nodes": {
"<NODE_NAME>": {
"node_id": "<NODE_ID>",
"steps_per_rev": "<STEPS_PER_REV>"
}
}
}
}
}
Reply-Topic: canopenRM8007/config/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "CONFIG_CANOPEN",
"taskResult": {
"defaults": {
"success": true
},
"motor_1": {
"success": true
}
}
}
}
Request Parameters
id
|
string | Yes | "1777041089180"
|
Unique request identifier. The same value is returned in the response, allowing the application to match requests and responses. |
origin
|
string | No | "APP"
|
Identifies who sent the request. Common values can be APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the CANopen command alias and parameters. |
task.alias
|
string | Yes | "CONFIG_CANOPEN"
|
Defines the command to execute. For this section, the alias must be CONFIG_CANOPEN.
|
task.taskParams
|
object | Yes | {...}
|
Contains the configuration data to store inside the CANopen actor. |
task.taskParams.defaults
|
object | No | { "sdo_timeout": 2.0 }
|
Defines default CANopen settings used by the actor when no command-specific value is provided. |
task.taskParams.defaults.sdo_timeout
|
number | No | 2.0
|
Default timeout, in seconds, used for SDO operations. For example, 2.0 means the actor waits up to 2 seconds for an SDO response.
|
task.taskParams.nodes
|
object | No | { "motor_1": {...} }
|
List of logical CANopen nodes known by the actor. Each key is a user-defined node name. |
task.taskParams.nodes.<NODE_NAME>
|
object | Yes | "motor_1"
|
Logical name used to reference a node later without repeatedly sending its node_id.
|
task.taskParams.nodes.<NODE_NAME>.node_id
|
integer | Yes | 5
|
CANopen node ID on the CAN bus. Valid CANopen node IDs are normally in the range 1..127.
|
task.taskParams.nodes.<NODE_NAME>.steps_per_rev
|
integer | Recommended | 4096
|
Number of raw position counts corresponding to one full mechanical revolution, 360°. The actor uses this later to convert raw position into revolutions and angle_deg.
|
Response Parameters
| Parameter | Type | Example | Description |
id
|
string | "1777041089180"
|
Same request identifier received in the request. Used to correlate the response with the original command. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. For successful handler replies, this is canopen-actor.
|
task
|
object | {...}
|
Main response object containing the executed alias and the command result. |
task.alias
|
string | "CONFIG_CANOPEN"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object containing the configuration outcome. |
task.taskResult.defaults.success
|
boolean | true
|
Indicates that the defaults configuration was successfully stored. This field is present only if defaults was included in the request.
|
task.taskResult.<NODE_NAME>.success
|
boolean | true
|
Indicates that the logical node configuration was successfully stored. Example: taskResult.motor_1.success = true.
|
task.taskResult.success
|
boolean | true
|
Generic success field. This appears when no specific defaults or nodes result is generated.
|
task.taskResult.error
|
string | "unknown alias CONFIG_X"
|
Error description. This field is present only when the command fails. |
CONFIG_CANOPEN does not communicate with the CANopen device directly. It stores logical node configuration inside the actor so that later runtime commands can refer to nodes by name instead of always sending the numeric node_id.
Storage Behaviour
- Both
defaultsandnodesare optional. If neither is present, the reply is a plain{ "success": true }with no per-node entries. defaultsandnodesare merged into the stored configuration. SendingCONFIG_CANOPENagain adds or replaces entries and never clears previously configured nodes. There is no command to delete a node.- A node sent again is replaced as a whole, not merged field by field. To change only
steps_per_rev, resend the complete node object. - As an alternative to the
nodesobject, any other key oftaskParamswhose value is an object containingnode_idis also stored as a logical node. These two requests are equivalent:
{ "task": { "alias": "CONFIG_CANOPEN", "taskParams": { "nodes": { "motor_1": { "node_id": 5 } } } } }
{ "task": { "alias": "CONFIG_CANOPEN", "taskParams": { "motor_1": { "node_id": 5 } } } }
- Extra fields inside a node entry are stored as-is and reused later by other commands:
steps_per_rev, pluspdo_num,offset,sizeandsigned, which are also written automatically byCONFIG_POSITION_TPDO. - The configuration is held in memory only. It is not persisted across an actor restart, and neither are running continuous loops.
- Node configuration is optional. Every runtime command also accepts a raw
node_id;CONFIG_CANOPENonly provides logical names and per-node defaults.
Configure one CANopen Transmit PDO
Configures a TPDO to transmit position data.
The handler validates pdo_num as 1..4, defaults map_index to 0x6004, map_subindex to 0x00, map_bits to 32, transmission_type to 255, event_timer_ms to 20, and inhibit_time_100us to 0.
Req-Topic: canopenRM8007/config/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "CONFIG_POSITION_TPDO",
"taskParams": {
"node_id": 5,
"pdo_num": 1,
"map_index": "0x6004",
"map_subindex": "0x00",
"map_bits": 32,
"transmission_type": 255,
"event_timer_ms": 2,
"inhibit_time_100us": 20,
"offset": 0,
"size": 4,
"signed": false,
"steps_per_rev": 4096,
"timeout": 2.0
}
}
}
Reply-Topic: canopenRM8007/config/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "CONFIG_POSITION_TPDO",
"taskResult": {
"success": true,
"node_id": 5,
"pdo_num": 1,
"comm_index": 6144,
"map_index": 6656,
"mapped_object": "0x6004:00/32",
"transmission_type": 255,
"event_timer_ms": 2,
"inhibit_time_100us": 20,
"cob_id": 389
}
}
}
Behaviour
This command changes the NMT state of the node. The full SDO/NMT sequence executed is:
| Step | Operation | Object | Value |
|---|---|---|---|
| 1 | NMT PRE_OP (no wait)
|
- | - |
| 2 | Disable the TPDO | 0x1800 + (pdo_num-1) : 01
|
COB-ID with bit 31 set |
| 3 | Clear the mapping count | 0x1A00 + (pdo_num-1) : 00
|
0
|
| 4 | Write the mapping entry | 0x1A00 + (pdo_num-1) : 01
|
(map_index << 16) | (map_subindex << 8) | map_bits
|
| 5 | Set the mapping count | 0x1A00 + (pdo_num-1) : 00
|
1
|
| 6 | Transmission type | 0x1800 + (pdo_num-1) : 02
|
transmission_type
|
| 7 | Inhibit time | 0x1800 + (pdo_num-1) : 03
|
inhibit_time_100us
|
| 8 | Event timer | 0x1800 + (pdo_num-1) : 05
|
event_timer_ms
|
| 9 | Re-enable the TPDO | 0x1800 + (pdo_num-1) : 01
|
valid COB-ID |
| 10 | NMT START (no wait)
|
- | the node is left in Operational |
The COB-ID base is 0x180, 0x280, 0x380 or 0x480 for TPDO 1..4, plus the node ID.
Any failing step aborts the sequence immediately. The node may then be left in Pre-Operational with a partially configured TPDO, and the failure is reported through the actor-level error envelope (origin: "Dev"). Re-running the command is safe.
offset, size, signed, steps_per_rev and pdo_num are only stored for later reuse when the node is referenced by logical name (node) and that name was already configured with CONFIG_CANOPEN. When the node is referenced by node_id, as in the example above, those five parameters apply to this command only and are silently discarded, so READ_PDO_CONTINUOUS will have to receive them again explicitly.
A pdo_num outside 1..4 fails with pdo_num must be 1..4.
Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the request. |
origin
|
string | "canopen-actor"
|
Component that generated the response. |
task.alias
|
string | "CONFIG_POSITION_TPDO"
|
Alias of the command that was executed. |
task.taskResult.success
|
boolean | true
|
Always true on this reply. A failure is returned through the actor-level envelope.
|
task.taskResult.node_id
|
integer | 5
|
Node that was configured. |
task.taskResult.pdo_num
|
integer | 1
|
TPDO number that was configured. |
task.taskResult.comm_index
|
integer | 6144
|
TPDO communication parameter index, decimal. 6144 = 0x1800.
|
task.taskResult.map_index
|
integer | 6656
|
TPDO mapping parameter index, decimal. 6656 = 0x1A00.
|
task.taskResult.mapped_object
|
string | "0x6004:00/32"
|
Object that was mapped, formatted as index:subindex/bits.
|
task.taskResult.transmission_type
|
integer | 255
|
Transmission type written to the node. |
task.taskResult.event_timer_ms
|
integer | 2
|
Event timer written to the node, in milliseconds. |
task.taskResult.inhibit_time_100us
|
integer | 20
|
Inhibit time written to the node, in 100 µs units. |
task.taskResult.cob_id
|
integer | 389
|
Enabled COB-ID of the TPDO, decimal. 389 = 0x185 = 0x180 + 5.
|
Parameter description
| Parameter | Type | Required | Default | Description |
node_id
|
int | Yes* | — | CANopen node ID. |
node
|
string | Yes* | — | Logical node name configured previously with CONFIG_CANOPEN.
|
pdo_num
|
int | No | 1
|
TPDO number. Valid range: 1..4.
|
map_index
|
int/string | No | 0x6004
|
Object dictionary index to map. Hex strings such as "0x6004" are supported.
|
map_subindex
|
int/string | No | 0x00
|
Object dictionary subindex. |
map_bits
|
int | No | 32
|
Number of mapped bits. |
transmission_type
|
int | No | 255
|
CANopen TPDO transmission type. |
event_timer_ms
|
int | No | 20
|
TPDO event timer in milliseconds. |
inhibit_time_100us
|
int | No | 0
|
Inhibit time in 100 µs units. |
offset
|
int | No | 0
|
Byte offset used later when decoding PDO position. |
size
|
int | No | 4
|
Number of bytes used later when decoding PDO position. |
signed
|
bool | No | false
|
Whether decoded raw position should be interpreted as signed. |
steps_per_rev
|
int | Recommended | 4096 if saved into node config
|
Encoder steps per revolution. |
timeout
|
float | No | actor default | SDO/NMT operation timeout. |
14.4 Runtime Operations
Runtime aliases accepted on canopenRM8007/runtime/in:
| Alias | Purpose | Section |
|---|---|---|
PING
|
Actor liveness check. No CAN traffic. | Ping |
NMT_COMMAND
|
Network management: start, stop, pre-operational, reset. | NMT Control |
SDO_READ
|
Read one object dictionary entry. | SDO Read |
SDO_WRITE
|
Write one object dictionary entry. | SDO Write |
READ
|
Read position from one node, or from every configured node. | Read Position |
READ_CONTINUOUS
|
Start continuous position reading over SDO. | READ SDO Continuous |
STOP_READ_CONTINUOUS
|
Stop the running continuous loop. | READ SDO Continuous |
READ_PDO_CONTINUOUS
|
Start continuous position reading over TPDO. | Continuous PDO Reading |
STOP_READ_PDO_CONTINUOUS
|
Stop the running continuous loop. | Continuous PDO Reading |
READ_HEARTBEAT
|
Read the NMT state of a node from its heartbeat. | Read Heartbeat |
RECV_PDO
|
Receive one raw TPDO frame. | Receive PDO |
RECV_EMCY
|
Receive one emergency frame. | Receive EMCY |
QUERY_NODE_INFO
|
Read device type and steps per revolution. | Query Node Info |
Configuration aliases, accepted on canopenRM8007/config/in, are CONFIG_CANOPEN and CONFIG_POSITION_TPDO; see Configuration
An alias sent on the wrong topic family is rejected with unknown alias <ALIAS>: SDO_READ on /config/in does not work, and CONFIG_CANOPEN on /runtime/in does not either.
NMT Control
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "NMT_COMMAND",
"taskParams": {
"node_id": 5,
"command": "START",
"wait": false,
"timeout": 5.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "NMT_COMMAND",
"taskResult": {
"success": true,
"id": "nmt-start-node5",
"node_id": 5,
"command": "START",
"confirmed": true,
"state": null
}
}
}
- Supported NMT Commands
| Command | Meaning | Notes |
START
|
Puts the node into Operational state. | If wait = true, the actor waits for heartbeat state OPERATIONAL.
|
STOP
|
Puts the node into Stopped state. | If wait = true, the actor waits for heartbeat state STOPPED.
|
PRE_OP
|
Puts the node into Pre-Operational state. | Used before operations such as PDO remapping. |
PRE-OP
|
Same as PRE_OP.
|
Alternative accepted spelling. |
PREOP
|
Same as PRE_OP.
|
Alternative accepted spelling. |
PRE_OPERATIONAL
|
Same as PRE_OP.
|
Alternative accepted spelling. |
RESET_NODE
|
Resets the CANopen node application. | If wait = true, the actor waits for boot-up heartbeat.
|
RESET
|
Same as RESET_NODE.
|
Alternative accepted spelling. |
RESET_COMM
|
Resets CANopen communication. | Sends NMT reset communication command. Cannot be confirmed. |
Notes:
- The command string is case-insensitive.
RESET_COMMhas no confirmation implemented. Withwait: trueit always repliessuccess: false,confirmed: falseanderror: "RESET_COMM confirmation is not implemented", even though the frame was actually sent. Always usewait: falsewith this command.- Confirmation with
wait: truerelies on the heartbeat producer of the node (object0x1017) being enabled. With heartbeat disabled, the wait always times out and the reply issuccess: falsewitherror: "NMT <CMD> was sent but no node state was observed before timeout". - When
wait: false, the frame is sent without any verification andconfirmedis alwaystrue. - NMT commands are unconfirmed CANopen services:
success: truewithwait: falsemeans "frame written to the bus", not "node accepted it".
Request Parameters
| Parameter | Type | Required | Example | Description |
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. The same value is returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "NMT_COMMAND"
|
Defines the command to execute. For this operation, it must be NMT_COMMAND.
|
task.taskParams
|
object | Yes | {...}
|
Contains the NMT command parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID that will receive the NMT command. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.command
|
string | No | "START"
|
NMT command to send. If omitted, the actor defaults to START.
|
task.taskParams.wait
|
boolean | No | false
|
If true, the actor waits for heartbeat/state confirmation when supported. If false, the command is sent without waiting for confirmation.
|
task.taskParams.timeout
|
number | No | 5.0
|
Timeout, in seconds, used when waiting for NMT confirmation. Default is 5.0.
|
task.taskParams.sdo_timeout
|
number | No | 2.0
|
Internal timeout used when creating/updating the CANopen node wrapper. Usually not required for basic NMT usage. |
Response Parameters
| Parameter | Type | Example | Description |
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the request. Used to correlate request and response. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "NMT_COMMAND"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object containing the NMT command result. |
task.taskResult.success
|
boolean | true
|
Indicates whether the actor successfully executed the NMT command. |
task.taskResult.id
|
string | "nmt-start-node5"
|
Internal/request ID returned by the CANopen RPC call. Usually matches the original request id.
|
task.taskResult.node_id
|
integer | 5
|
CANopen node ID that received the NMT command. |
task.taskResult.command
|
string | "START"
|
NMT command that was executed. For aliases such as RESET, the response may normalize the command to RESET_NODE.
|
task.taskResult.confirmed
|
boolean | true
|
Indicates whether the command was considered successful. If wait = false, this usually means the command was sent successfully. If wait = true, this means the expected heartbeat/state confirmation was received.
|
task.taskResult.state
|
string/null | null
|
Last NMT state observed for this node through heartbeat, cached by the hardware owner. It is null until the first heartbeat is seen, and it can be non-null even with wait = false when an earlier command already observed one. Possible values: BOOT_UP, STOPPED, OPERATIONAL, PRE_OP, or the raw integer for a state the actor does not know.
|
task.taskResult.error
|
string | "unsupported NMT command X"
|
Error description. Present only when the command fails. |
SDO Read
Reads an object dictionary value using SDO upload.
Only expedited transfers (1 to 4 bytes) are supported; segmented and block transfers are not implemented.
The value is always returned as an unsigned integer. Object dictionary entries declared as signed must be converted by the application.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "SDO_READ",
"taskParams": {
"node_id": 5,
"index": "0x6004",
"subindex": "0x00",
"timeout": 2.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "SDO_READ",
"taskResult": {
"success": true,
"id": "sdo-read-position-node5",
"node_id": 5,
"index": 24580,
"subindex": 0,
"value": 123456
}
}
}
Request Parameters
| Parameter | Type | Required | Example | Description |
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. The same value is returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "SDO_READ"
|
Defines the command to execute. For this operation, it must be SDO_READ.
|
task.taskParams
|
object | Yes | {...}
|
Contains the SDO read parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID to read from. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.index
|
integer/string | Yes | "0x6004"
|
CANopen object dictionary index to read. Hex strings such as "0x6004" are supported.
|
task.taskParams.subindex
|
integer/string | No | "0x00"
|
CANopen object dictionary subindex to read. If omitted, defaults to 0.
|
task.taskParams.timeout
|
number | No | 2.0
|
Timeout, in seconds, used for the SDO read operation. If omitted, the actor default SDO timeout is used. |
Response Parameters
| Parameter | Type | Example | Description |
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the request. Used to correlate request and response. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "SDO_READ"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object containing the SDO read result. |
task.taskResult.success
|
boolean | true
|
Indicates whether the SDO read was successful. |
task.taskResult.id
|
string | "sdo-read-position-node5"
|
Internal/request ID returned by the CANopen RPC call. Usually matches the original request id.
|
task.taskResult.node_id
|
integer | 5
|
CANopen node ID that was read. |
task.taskResult.index
|
integer | 24580
|
Object dictionary index that was read, returned as decimal. Example: 24580 = 0x6004.
|
task.taskResult.subindex
|
integer | 0
|
Object dictionary subindex that was read. |
task.taskResult.value
|
integer | 123456
|
Raw value returned by the SDO read operation. |
task.taskResult.error
|
string | "SDO timeout 0x6004:00"
|
Error description. Present only when the command fails. |
- SDO Write
Writes an object dictionary value using SDO download.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "SDO_WRITE",
"taskParams": {
"node_id": 5,
"index": "0x6003",
"subindex": "0x00",
"value": 0,
"size": 4,
"timeout": 2.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "SDO_WRITE",
"taskResult": {
"success": true,
"id": "sdo-write-node5",
"node_id": 5,
"index": 24579,
"subindex": 0,
"value": 0,
"size": 4
}
}
}
- Parameter descirption
| Parameter | Type | Required | Default | Description |
node_id / nodeId / node
|
int/string | Yes | — | CANopen node reference. |
index
|
int/string | Yes | — | Object dictionary index. |
subindex
|
int/string | No | 0
|
Object dictionary subindex. |
value
|
int/string | Yes | — | Value to write. |
size
|
int | No | 4
|
Number of bytes. Valid: 1, 2, 3, 4.
|
timeout
|
float | No | actor default | SDO timeout in seconds. |
- Request Parameters
| Parameter | Type | Required | Example | Description |
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. The same value is returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "SDO_WRITE"
|
Defines the command to execute. For this operation, it must be SDO_WRITE.
|
task.taskParams
|
object | Yes | {...}
|
Contains the SDO write parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID to write to. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.index
|
integer/string | Yes | "0x6003"
|
CANopen object dictionary index to write. Hex strings such as "0x6003" are supported.
|
task.taskParams.subindex
|
integer/string | No | "0x00"
|
CANopen object dictionary subindex to write. If omitted, defaults to 0.
|
task.taskParams.value
|
integer/string | Yes | 0
|
Value to write into the selected object dictionary entry. Hex strings are also supported. |
task.taskParams.size
|
integer | No | 4
|
Number of bytes to write. Valid values are normally 1, 2, 3, or 4. If omitted, defaults to 4.
|
task.taskParams.timeout
|
number | No | 2.0
|
Timeout, in seconds, used for the SDO write operation. If omitted, the actor default SDO timeout is used. |
- Response Parameters
| Parameter | Type | Example | Description |
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the request. Used to correlate request and response. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "SDO_WRITE"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object containing the SDO write result. |
task.taskResult.success
|
boolean | true
|
Indicates whether the SDO write was successful. |
task.taskResult.id
|
string | "sdo-write-node5"
|
Internal/request ID returned by the CANopen RPC call. Usually matches the original request id.
|
task.taskResult.node_id
|
integer | 5
|
CANopen node ID that was written to. |
task.taskResult.index
|
integer | 24579
|
Object dictionary index that was written, returned as decimal. Example: 24579 = 0x6003.
|
task.taskResult.subindex
|
integer | 0
|
Object dictionary subindex that was written. |
task.taskResult.value
|
integer | 0
|
Value that was written. |
task.taskResult.size
|
integer | 4
|
Number of bytes written. |
task.taskResult.error
|
string | "SDO abort 0x6003:00 -> ..."
|
Error description. Present only when the command fails. |
Read Position
Reads position and returns raw value, revolutions, and angle.
This alias reads the position object, not the NMT state of the node. To read the NMT state, use READ_HEARTBEAT.
READ has two modes:
- single node - a node reference is present in
taskParams(described below); - all configured nodes - no node reference is present (see Read All Configured Nodes). Internally, this reads the configured position object, defaulting to
0x6004:00. Ifsteps_per_revis not provided, the owner reads0x6501:00to obtain it.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "READ",
"taskParams": {
"node_id": 5,
"index": "0x6004",
"subindex": "0x00",
"steps_per_rev": 4096,
"timeout": 2.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ",
"taskResult": {
"success": true,
"id": "read-node5",
"node_id": 5,
"raw": 1024,
"steps_per_rev": 4096,
"revolutions": 0.25,
"angle_deg": 90.0,
"index": 24580,
"subindex": 0
}
}
}
Request Parameters
| Parameter | Type | Required | Example | Description |
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. The same value is returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "READ"
|
Defines the command to execute. For this operation, it must be READ.
|
task.taskParams
|
object | Yes | {...}
|
Contains the position read parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID to read from. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.index
|
integer/string | No | "0x6004"
|
CANopen object dictionary index used as the raw position source. If omitted, defaults to 0x6004.
|
task.taskParams.subindex
|
integer/string | No | "0x00"
|
CANopen object dictionary subindex used as the raw position source. If omitted, defaults to 0x00.
|
task.taskParams.steps_per_rev
|
integer | Recommended | 4096
|
Number of raw counts per mechanical revolution. Used to calculate revolutions and angle_deg. If omitted, the owner attempts to read 0x6501:00.
|
task.taskParams.timeout
|
number | No | 2.0
|
Timeout, in seconds, used for the internal SDO reads. If omitted, the actor default SDO timeout is used. |
task.taskParams.sdo_timeout
|
number | No | 2.0
|
Internal SDO timeout used when creating/updating the node wrapper. Usually not required. |
Response Parameters
| Parameter | Type | Example | Description |
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the request. Used to correlate request and response. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "READ"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object containing the position read result. |
task.taskResult.success
|
boolean | true
|
Indicates whether the position read was successful. |
task.taskResult.id
|
string | "read-node5"
|
Internal/request ID returned by the CANopen RPC call. Usually matches the original request id.
|
task.taskResult.node_id
|
integer | 5
|
CANopen node ID that was read. |
task.taskResult.raw
|
integer | 1024
|
Raw position value read from the selected object dictionary entry. |
task.taskResult.steps_per_rev
|
integer | 4096
|
Number of raw counts per revolution used for the conversion. |
task.taskResult.revolutions
|
number | 0.25
|
Raw position converted into revolutions. Example: 1024 / 4096 = 0.25.
|
task.taskResult.angle_deg
|
number | 90.0
|
Position inside one revolution, in degrees. Example: 0.25 × 360 = 90°.
|
task.taskResult.index
|
integer | 24580
|
Object dictionary index read as the raw position source. Example: 24580 = 0x6004.
|
task.taskResult.subindex
|
integer | 0
|
Object dictionary subindex read as the raw position source. |
task.taskResult.error
|
string | "SDO timeout 0x6004:00"
|
Error description. Present only when the command fails. |
Read All Configured Nodes
If taskParams contains no node reference, READ iterates over every node stored with CONFIG_CANOPEN and returns one entry per logical node. The single-node fallback described in Node Reference does not apply to READ.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "READ",
"taskParams": {}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ",
"taskResult": {
"success": false,
"results": {
"motor_1": {
"success": true,
"node_id": 5,
"raw": 1024,
"steps_per_rev": 4096,
"revolutions": 0.25,
"angle_deg": 90.0,
"index": 24580,
"subindex": 0
},
"motor_2": {
"success": false,
"node_id": 6,
"error": "SDO timeout 0x6004:00"
}
}
}
}
}
The response shape is different from the single-node one: there is no raw, angle_deg or node_id at the root of taskResult; everything lives under results, keyed by logical node name.
Multi-node Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
task.taskResult.success
|
boolean | false
|
true only when every configured node was read successfully.
|
task.taskResult.results
|
object | {...}
|
One entry per configured logical node name. |
task.taskResult.results.<NODE_NAME>.success
|
boolean | true
|
Result of this individual node. |
task.taskResult.results.<NODE_NAME>.node_id
|
integer | 5
|
CANopen node ID that was read. |
task.taskResult.results.<NODE_NAME>.raw
|
integer | 1024
|
Raw position value. Present only when this node succeeded. |
task.taskResult.results.<NODE_NAME>.steps_per_rev
|
integer | 4096
|
Counts per revolution used for the conversion. Present only when this node succeeded. |
task.taskResult.results.<NODE_NAME>.revolutions
|
number | 0.25
|
Position in revolutions. Present only when this node succeeded. |
task.taskResult.results.<NODE_NAME>.angle_deg
|
number | 90.0
|
Position inside one revolution, in degrees. Present only when this node succeeded. |
task.taskResult.results.<NODE_NAME>.index
|
integer | 24580
|
Object dictionary index used as the raw position source. |
task.taskResult.results.<NODE_NAME>.subindex
|
integer | 0
|
Object dictionary subindex used as the raw position source. |
task.taskResult.results.<NODE_NAME>.error
|
string | "SDO timeout 0x6004:00"
|
Error for this node. Present only when this node failed. |
task.taskResult.error
|
string | "no configured nodes"
|
Present, together with success: false and no results, when no node is configured.
|
Notes:
- Any
taskParamsvalue (index,subindex,steps_per_rev,timeout,sdo_timeout) overrides the stored node configuration and is applied to every node. - Nodes are read sequentially. The reply is only published after the last node, so the total time grows with the number of nodes and with the number of failing (timing out) nodes.
- A node whose
node_idcannot be resolved does not abort the loop; it produces its ownsuccess: falseentry.
Continuous Reading: Task Exclusivity
The actor keeps a single continuous-task slot, shared by READ_CONTINUOUS and READ_PDO_CONTINUOUS. This has consequences that apply to both sections below:
- Starting
READ_CONTINUOUScancels a runningREAD_PDO_CONTINUOUS, and vice-versa. There is no notification for the cancelled loop; it simply stops publishing. - Starting the same alias again restarts the loop with the new parameters.
STOP_READ_CONTINUOUSandSTOP_READ_PDO_CONTINUOUSare interchangeable: each stops whichever loop is running. Only thealiasechoed back in the reply differs.stopped: falsein a stop reply means that no loop was running.- Only one node can be sampled continuously at a time. To follow several nodes, poll them with
READ. - Loops are not persisted. An actor restart stops them silently, with no final message.
READ SDO Continuous
Starts continuous SDO-based position reading.
The initial command returns an acknowledgement. After that, continuous samples are published repeatedly to canopenRM8007/runtime/out, including when the start command was sent on the reserved topic.
The continuous loop requires a single node reference and requires steps_per_rev.
Each cycle performs one SDO read, so the achievable rate is bounded by the SDO round-trip time. For high rates, use Continuous PDO Reading instead.
A start request without a node reference is still accepted when at least one node is configured: the acknowledgement returns success: true with mode: "all", but the loop cannot run in that mode. Every cycle then publishes a failed sample with error: "READ_CONTINUOUS fast mode requires a single node", at the configured rate. Always send node_id, nodeId or node.
steps_per_rev must be resolvable, either from taskParams or from the stored node configuration when node is used. Otherwise every cycle publishes error: "steps_per_rev is required for fast READ_CONTINUOUS".
Failed cycles caused by an exception, rather than by the CANopen operation itself, are published with origin: "Dev" and a newly generated id.
Start request
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "READ_CONTINUOUS",
"taskParams": {
"node_id": 5,
"steps_per_rev": 4096,
"index": "0x6004",
"subindex": "0x00",
"timeout": 0.2,
"interval_ms": 20
}
}
}
Reply-Topic: canopenRM8007/runtime/out
// Start acknowledgement
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_CONTINUOUS",
"taskResult": {
"success": true,
"running": true,
"mode": "single",
"interval_ms": 20
}
}
}
// Continuous sample response
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_CONTINUOUS",
"taskResult": {
"success": true,
"node_id": 5,
"angle_deg": 90.0,
"steps_per_rev": 4096,
"revolutions": 0.25,
"raw": 1024,
"index": 24580,
"subindex": 0,
"continuous": true,
"request_id": "read-cont-node5"
}
}
}
Start Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. Continuous sample responses include this value as request_id.
|
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "READ_CONTINUOUS"
|
Defines the command to execute. For this operation, it must be READ_CONTINUOUS.
|
task.taskParams
|
object | Yes | {...}
|
Contains the continuous read parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID to read continuously. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.steps_per_rev
|
integer | Yes** | 4096
|
Number of raw counts per mechanical revolution. Required by the fast continuous read loop to calculate revolutions and angle_deg.
|
task.taskParams.index
|
integer/string | No | "0x6004"
|
CANopen object dictionary index used as the raw position source. If omitted, defaults to 0x6004.
|
task.taskParams.subindex
|
integer/string | No | "0x00"
|
CANopen object dictionary subindex used as the raw position source. If omitted, defaults to 0x00.
|
task.taskParams.timeout
|
number | No | 0.2
|
Timeout, in seconds, used for each fast SDO read cycle. If omitted, defaults to 0.2.
|
task.taskParams.interval_ms
|
integer | No | 20
|
Delay between continuous read cycles, in milliseconds. If 0, the loop runs again immediately after each cycle yields control.
|
task.taskParams.action
|
string | No | "START"
|
Optional action. If set to STOP, OFF, CANCEL, or DISABLE, the actor stops the continuous read loop instead of starting it.
|
| Parameter | Type | Example | Description |
| id | string | "<TIMESTAMP>" | Same request identifier received in the start request. |
| origin | string | "canopen-actor" | Identifies the component that generated the response. |
| task | object | {...} | Main response object. |
| task.alias | string | "READ_CONTINUOUS" | Alias of the command that was executed. |
| task.taskResult | object | {...} | Result object confirming that continuous reading was started. |
| task.taskResult.success | boolean | true | Indicates whether the start command was accepted. |
| task.taskResult.running | boolean | true | Indicates that the continuous read loop is running. |
| task.taskResult.mode | string | "single" | Read mode. In this fast continuous path, a single node is used when node_id ,
nodeId , or node is provided. |
| task.taskResult.interval_ms | integer | 20 | Configured delay between continuous read cycles, in milliseconds. |
| task.taskResult.error | string | "no configured nodes" | Error description. Present only when the start command fails. |
- Continuous Sample Response Parameters
| Parameter | Type | Example | Description |
| id | integer/string | 1777041089180 | Sample response ID generated by the actor for each read cycle. |
| origin | string | "canopen-actor" | Identifies the component that generated the sample response. |
| task | object | {...} | Main response object. |
| task.alias | string | "READ_CONTINUOUS" | Alias of the running continuous command. |
| task.taskResult | object | {...} | Result object containing one continuous position sample. |
| task.taskResult.success | boolean | true | Indicates whether this specific read cycle was successful. |
| task.taskResult.node_id | integer | 5 | CANopen node ID that was read. |
| task.taskResult.angle_deg | number | 90.0 | Position inside one revolution, in degrees. |
| task.taskResult.steps_per_rev | integer | 4096 | Number of raw counts per revolution used for conversion. |
| task.taskResult.revolutions | number | 0.25 | Raw position converted into revolutions. |
| task.taskResult.raw | integer | 1024 | Raw position value read from the node. |
| task.taskResult.index | integer | 24580 | Object dictionary index read as the raw position source. Example: 24580 =
0x6004. |
| task.taskResult.subindex | integer | 0 | Object dictionary subindex read as the raw position source. |
| task.taskResult.continuous | boolean | true | Marks this response as a continuous sample, not just the initial
acknowledgement. |
| task.taskResult.request_id | string | "read-cont-node5" | Original request id that started the continuous loop. Useful to correlate
samples with the start command. |
| task.taskResult.error | string | "operation failed" | Error description for a failed sample. Present only when a read cycle fails. |
Stop request
You can stop using either STOP_READ_CONTINUOUS :
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "STOP_READ_CONTINUOUS",
"taskParams": {}
}
}
Or by sending READ_CONTINUOUS with action: "STOP" :
{
"id": "stop-read-cont",
"origin": "APP",
"task": {
"alias": "READ_CONTINUOUS",
"taskParams": {
"action": "STOP"
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "stop-read-cont",
"origin": "canopen-actor",
"task": {
"alias": "READ_CONTINUOUS",
"taskResult": {
"success": true,
"running": false,
"stopped": true
}
}
}
- Stop Request Parameters
| Parameter | Type | Required | Example | Description |
| id | string | Yes | "stop-read-cont" | Unique request identifier. |
| origin | string | No | "APP" | Identifies the sender of the request. |
| task.alias | string | Yes | "READ_CONTINUOUS" or
"STOP_READ_CONTINUOUS" |
Command used to stop the continuous read loop. |
| task.taskParams.action | string | No | "STOP" | When using READ_CONTINUOUS , set action to STOP , OFF , CANCEL ,
or DISABLE to stop the loop. Not required when using STOP_READ_CONTINUOUS. |
- Stop Response Parameters
| Parameter | Type | Example | Description |
| id | string | "stop-read-cont" | Same request identifier received in the stop request. |
| origin | string | "canopen-actor" | Identifies the component that generated the response. |
| task.alias | string | "READ_CONTINUOUS" | Alias returned by the stop handler. |
| task.taskResult.success | boolean | true | Indicates whether the stop request was processed successfully. |
| task.taskResult.running | boolean | false | Indicates that the continuous read loop is no longer running. |
| task.taskResult.stopped | boolean | true | Indicates whether there was an active task that was actually stopped. If no continuous
task was running, this may be false. |
Continuous PDO Reading
Starts continuous PDO-based position reading.
This command does not perform repeated SDO reads. Instead, it waits for TPDO frames already being transmitted by the CANopen node.
Normally, the TPDO should be configured first using CONFIG_POSITION_TPDO.
The initial command returns an acknowledgement. After that, continuous sample responses are published repeatedly to: canopenRM8007/runtime/out
- Start Request
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskParams": {
"node_id": 5,
"pdo_num": 1,
"steps_per_rev": 4096,
"offset": 0,
"size": 4,
"signed": false,
"timeout": 1.0,
"interval_ms": 20
}
}
}
- Start Acknowledgement
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskResult": {
"success": true,
"running": true,
"mode": "pdo",
"pdo_num": 1,
"interval_ms": 20
}
}
}
- Continuous Sample Response
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskResult": {
"success": true,
"node_id": 5,
"pdo_num": 1,
"angle_deg": 90.0,
"steps_per_rev": 4096,
"revolutions": 0.25,
"raw": 1024,
"data": [0, 4, 0, 0],
"offset": 0,
"size": 4,
"signed": false,
"continuous": true,
"request_id": "read-pdo-cont-node5"
}
}
}
Start Request Parameters
| Parameter | Type | Required | Example | Description |
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. Continuous sample responses include this value as request_id.
|
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "READ_CONTINUOUS"
|
Defines the command to execute. For this operation, it must be READ_CONTINUOUS.
|
task.taskParams
|
object | Yes | {...}
|
Contains the continuous read parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID to read continuously. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.steps_per_rev
|
integer | Yes** | 4096
|
Number of raw counts per mechanical revolution. Required by the fast continuous read loop to calculate revolutions and angle_deg.
|
task.taskParams.index
|
integer/string | No | "0x6004"
|
CANopen object dictionary index used as the raw position source. If omitted, defaults to 0x6004.
|
task.taskParams.subindex
|
integer/string | No | "0x00"
|
CANopen object dictionary subindex used as the raw position source. If omitted, defaults to 0x00.
|
task.taskParams.timeout
|
number | No | 0.2
|
Timeout, in seconds, used for each fast SDO read cycle. If omitted, defaults to 0.2.
|
task.taskParams.interval_ms
|
integer | No | 20
|
Delay between continuous read cycles, in milliseconds. If 0, the loop runs again immediately after each cycle yields control.
|
task.taskParams.action
|
string | No | "START"
|
Optional action. If set to STOP, OFF, CANCEL, or DISABLE, the actor stops the continuous read loop instead of starting it.
|
Start Acknowledgement Response Parameters
| Parameter | Type | Example | Description |
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the start request. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "READ_CONTINUOUS"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object confirming that continuous reading was started. |
task.taskResult.success
|
boolean | true
|
Indicates whether the start command was accepted. |
task.taskResult.running
|
boolean | true
|
Indicates that the continuous read loop is running. |
task.taskResult.mode
|
string | "single"
|
single when a node reference was sent, all when none was sent. Only single actually works; see the warning above.
|
task.taskResult.interval_ms
|
integer | 20
|
Configured delay between continuous read cycles, in milliseconds. |
task.taskResult.error
|
string | "no configured nodes"
|
Error description. Present only when the start command fails. |
Continuous Sample Response Parameters
| Parameter | Type | Example | Description |
id
|
integer/string | 1777041089180
|
Sample response ID generated by the actor for each read cycle. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the sample response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "READ_CONTINUOUS"
|
Alias of the running continuous command. |
task.taskResult
|
object | {...}
|
Result object containing one continuous position sample. |
task.taskResult.success
|
boolean | true
|
Indicates whether this specific read cycle was successful. |
task.taskResult.node_id
|
integer | 5
|
CANopen node ID that was read. |
task.taskResult.angle_deg
|
number | 90.0
|
Position inside one revolution, in degrees. |
task.taskResult.steps_per_rev
|
integer | 4096
|
Number of raw counts per revolution used for conversion. |
task.taskResult.revolutions
|
number | 0.25
|
Raw position converted into revolutions. |
task.taskResult.raw
|
integer | 1024
|
Raw position value read from the node. |
task.taskResult.index
|
integer | 24580
|
Object dictionary index read as the raw position source. Example: 24580 = 0x6004.
|
task.taskResult.subindex
|
integer | 0
|
Object dictionary subindex read as the raw position source. |
task.taskResult.continuous
|
boolean | true
|
Marks this response as a continuous sample, not just the initial acknowledgement. |
task.taskResult.request_id
|
string | "read-cont-node5"
|
Original request id that started the continuous loop. Useful to correlate samples with the start command.
|
task.taskResult.error
|
string | "operation failed"
|
Error description for a failed sample. Present only when a read cycle fails. |
Stop Request
You can stop using either STOP_READ_CONTINUOUS:
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "STOP_READ_CONTINUOUS",
"taskParams": {}
}
}
Or by sending READ_CONTINUOUS with action: "STOP":
{
"id": "stop-read-cont",
"origin": "canopen-actor",
"task": {
"alias": "READ_CONTINUOUS",
"taskResult": {
"success": true,
"running": false,
"stopped": true
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "stop-read-cont",
"origin": "canopen-actor",
"task": {
"alias": "READ_CONTINUOUS",
"taskResult": {
"success": true,
"running": false,
"stopped": true
}
}
}
Stop Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "stop-read-cont"
|
Unique request identifier. |
origin
|
string | No | "APP"
|
Identifies the sender of the request. |
task.alias
|
string | Yes | "READ_CONTINUOUS" or "STOP_READ_CONTINUOUS"
|
Command used to stop the continuous read loop. |
task.taskParams.action
|
string | No | "STOP"
|
When using READ_CONTINUOUS, set action to STOP, OFF, CANCEL, or DISABLE to stop the loop. Not required when using STOP_READ_CONTINUOUS.
|
Stop Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
id
|
string | "stop-read-cont"
|
Same request identifier received in the stop request. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task.alias
|
string | "READ_CONTINUOUS"
|
Alias returned by the stop handler. |
task.taskResult.success
|
boolean | true
|
Indicates whether the stop request was processed successfully. |
task.taskResult.running
|
boolean | false
|
Indicates that the continuous read loop is no longer running. |
task.taskResult.stopped
|
boolean | true
|
Indicates whether there was an active task that was actually stopped. If no continuous task was running, this may be false.
|
Continuous PDO Reading
Starts continuous PDO-based position reading.
This command does not perform repeated SDO reads. Instead, it waits for TPDO frames already being transmitted by the CANopen node.
Normally, the TPDO should be configured first using CONFIG_POSITION_TPDO.
Only one continuous loop can run at a time; see Continuous Reading: Task Exclusivity.
Only the "no node reference at all" case is validated before the acknowledgement. An unknown node name, or a steps_per_rev that cannot be resolved, is detected only after the acknowledgement was already published with success: true and running: true. The loop then terminates immediately and nothing else is ever published - no error message and no samples. If samples do not start arriving, check the node name and steps_per_rev.
Failed cycles always report the generic error: "pdo operation failed". The specific reason produced by the hardware layer (pdo timeout, pdo payload too short: len=... offset=... size=...) is currently lost.
timeout is how long each cycle waits for a TPDO frame, and must be comfortably larger than the event timer configured on the node, otherwise cycles fail spuriously.
task.taskResult.data carries the complete frame payload as delivered by the CAN driver, not only the mapped position bytes. offset and size select the position value inside it.
The initial command returns an acknowledgement. After that, continuous sample responses are published repeatedly to:
canopenRM8007/runtime/out
Start Request
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskParams": {
"node_id": 5,
"pdo_num": 1,
"steps_per_rev": 4096,
"offset": 0,
"size": 4,
"signed": false,
"timeout": 1.0,
"interval_ms": 20
}
}
}
Start Acknowledgement
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskResult": {
"success": true,
"running": true,
"mode": "pdo",
"pdo_num": 1,
"interval_ms": 20
}
}
}
Continuous Sample Response
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskResult": {
"success": true,
"node_id": 5,
"pdo_num": 1,
"angle_deg": 90.0,
"steps_per_rev": 4096,
"revolutions": 0.25,
"raw": 1024,
"data": [0, 4, 0, 0],
"offset": 0,
"size": 4,
"signed": false,
"continuous": true,
"request_id": "read-pdo-cont-node5"
}
}
}
Start Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier. Continuous sample responses include this value as request_id.
|
origin
|
string | No | "APP"
|
Identifies the sender of the request, for example APP, NodeRED, CLI, or TestClient.
|
task
|
object | Yes | {...}
|
Main command object containing the command alias and parameters. |
task.alias
|
string | Yes | "READ_PDO_CONTINUOUS"
|
Defines the command to execute. |
task.taskParams
|
object | Yes | {...}
|
Contains the PDO continuous read parameters. |
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID to read from. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN. If used, the actor can reuse saved steps_per_rev, pdo_num, offset, size, and signed values from the node configuration.
|
task.taskParams.pdo_num
|
integer | No | 1
|
TPDO number to listen to. If omitted, defaults to 1.
|
task.taskParams.steps_per_rev
|
integer | Yes** | 4096
|
Number of raw position counts per mechanical revolution. Used to convert raw into revolutions and angle_deg.
|
task.taskParams.offset
|
integer | No | 0
|
Byte offset inside the PDO payload where the position value starts. |
task.taskParams.size
|
integer | No | 4
|
Number of bytes to decode from the PDO payload. For a 32-bit position value, use 4.
|
task.taskParams.signed
|
boolean | No | false
|
Defines whether the raw value should be decoded as signed or unsigned. |
task.taskParams.timeout
|
number | No | 1.0
|
Timeout, in seconds, while waiting for each TPDO frame. If no TPDO is received within this time, the cycle fails. |
task.taskParams.interval_ms
|
integer | No | 20
|
Delay between read cycles, in milliseconds. If 0, the loop immediately starts the next cycle after yielding control.
|
task.taskParams.action
|
string | No | "START"
|
Optional action. If set to STOP, OFF, CANCEL, or DISABLE, the actor stops the continuous loop instead of starting it.
|
Start Acknowledgement Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the start request. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "READ_PDO_CONTINUOUS"
|
Alias of the command that was executed. |
task.taskResult
|
object | {...}
|
Result object confirming whether the continuous PDO read was started. |
task.taskResult.success
|
boolean | true
|
Indicates whether the start command was accepted. |
task.taskResult.running
|
boolean | true
|
Indicates that the continuous PDO read loop is running. |
task.taskResult.mode
|
string | "pdo"
|
Indicates that the loop is using PDO reception mode. |
task.taskResult.pdo_num
|
integer | 1
|
TPDO number being listened to. |
task.taskResult.interval_ms
|
integer | 20
|
Configured delay between read cycles, in milliseconds. |
task.taskResult.error
|
string | "READ_PDO_CONTINUOUS requires a single node"
|
Error description. Present only when the start command fails. |
Continuous Sample Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
id
|
integer/string | 1777041089180
|
Sample response ID generated by the actor for each PDO read cycle. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the sample. |
task
|
object | {...}
|
Main response object. |
task.alias
|
string | "READ_PDO_CONTINUOUS"
|
Alias of the running continuous command. |
task.taskResult
|
object | {...}
|
Result object containing one decoded PDO position sample. |
task.taskResult.success
|
boolean | true
|
Indicates whether this specific PDO read cycle was successful. |
task.taskResult.node_id
|
integer | 5
|
CANopen node ID that produced the TPDO. |
task.taskResult.pdo_num
|
integer | 1
|
TPDO number received and decoded. |
task.taskResult.angle_deg
|
number | 90.0
|
Position inside one revolution, in degrees. |
task.taskResult.steps_per_rev
|
integer | 4096
|
Number of raw counts per revolution used for conversion. |
task.taskResult.revolutions
|
number | 0.25
|
Raw position converted into revolutions. Example: 1024 / 4096 = 0.25.
|
task.taskResult.raw
|
integer | 1024
|
Decoded raw position value from the PDO payload. |
task.taskResult.data
|
array | [0, 4, 0, 0]
|
Raw PDO payload bytes received from the CANopen node. |
task.taskResult.offset
|
integer | 0
|
Byte offset used to extract the raw position value from data.
|
task.taskResult.size
|
integer | 4
|
Number of bytes decoded from data.
|
task.taskResult.signed
|
boolean | false
|
Indicates whether the raw value was decoded as signed or unsigned. |
task.taskResult.continuous
|
boolean | true
|
Marks this response as a continuous sample. |
task.taskResult.request_id
|
string | "read-pdo-cont-node5"
|
Original request id that started the continuous loop. Useful to correlate samples with the start command.
|
task.taskResult.error
|
string | "pdo operation failed"
|
Error description. Present only when a sample fails. |
Stop Request
Req-Topic: canopenRM8007/runtime/in
{
"id": "stop-pdo-cont",
"origin": "APP",
"task": {
"alias": "STOP_READ_PDO_CONTINUOUS",
"taskParams": {}
}
}
Alternative stop request using READ_PDO_CONTINUOUS:
{
"id": "stop-pdo-cont",
"origin": "APP",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskParams": {
"action": "STOP"
}
}
}
Stop Response
Reply-Topic: canopenRM8007/runtime/out
{
"id": "stop-pdo-cont",
"origin": "canopen-actor",
"task": {
"alias": "READ_PDO_CONTINUOUS",
"taskResult": {
"success": true,
"running": false,
"stopped": true
}
}
}
Stop Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
id
|
string | "stop-pdo-cont"
|
Same request identifier received in the stop request. |
origin
|
string | "canopen-actor"
|
Identifies the component that generated the response. |
task.alias
|
string | "READ_PDO_CONTINUOUS"
|
Alias returned by the stop handler. |
task.taskResult.success
|
boolean | true
|
Indicates whether the stop request was processed successfully. |
task.taskResult.running
|
boolean | false
|
Indicates that the continuous PDO loop is no longer running. |
task.taskResult.stopped
|
boolean | true
|
Indicates whether there was an active continuous task that was actually stopped. If no task was running, this may be false.
|
Decode Logic
The received PDO payload is decoded using:
offset: first byte to read from the PDO payload;size: number of bytes to read;signed: whether the value is signed or unsigned;steps_per_rev: scaling factor used to calculate revolutions and angle.
The raw value is decoded as little-endian:
raw = int.from_bytes(data[offset : offset + size], byteorder="little", signed=signed)Then:
revolutions = raw / steps_per_rev
angle_deg = ((raw % steps_per_rev) / steps_per_rev) * 360.0Ping
Liveness check of the actor and of the MQTT path. It generates no CAN traffic and requires no configured node. PING is also the alias assumed when a runtime request arrives without task.alias.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "PING",
"taskParams": {}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "PING",
"taskResult": {
"success": true,
"pong": true
}
}
}
Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
id
|
string | "<TIMESTAMP>"
|
Same request identifier received in the request. |
origin
|
string | "canopen-actor"
|
Component that generated the response. |
task.alias
|
string | "PING"
|
Alias that was executed. |
task.taskResult.success
|
boolean | true
|
Always true.
|
task.taskResult.pong
|
boolean | true
|
Always true. Confirms the actor loop is responsive.
|
taskParams is ignored and may be omitted.
Read Heartbeat
Waits for the next Heartbeat frame from the node (COB-ID 0x700 + node_id) and returns its NMT state. This is the command that reads the state of a node; READ reads position.
The heartbeat producer must be enabled on the node (object 0x1017, producer heartbeat time in milliseconds). With 0x1017 = 0 the node never transmits a heartbeat and this command always times out.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "READ_HEARTBEAT",
"taskParams": {
"node_id": 5,
"timeout": 2.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_HEARTBEAT",
"taskResult": {
"success": true,
"id": "<TIMESTAMP>",
"node_id": 5,
"state": "OPERATIONAL"
}
}
}
Reply when no heartbeat arrives within timeout:
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "READ_HEARTBEAT",
"taskResult": {
"success": false,
"id": "<TIMESTAMP>",
"node_id": 5,
"state": null,
"error": "no heartbeat received from node 5 before timeout"
}
}
}
Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier, returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request. |
task.alias
|
string | Yes | "READ_HEARTBEAT"
|
Must be READ_HEARTBEAT.
|
task.taskParams.node_id
|
integer | Yes* | 5
|
Numeric CANopen node ID. See Node Reference. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name previously configured with CONFIG_CANOPEN.
|
task.taskParams.timeout
|
number | No | 2.0
|
How long to wait for a heartbeat frame, in seconds. Defaults to 2.0, not to the configured sdo_timeout.
|
task.taskParams.sdo_timeout
|
number | No | 2.0
|
Only used to create or refresh the internal node wrapper. Not relevant for this command. |
Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
task.taskResult.success
|
boolean | true
|
false when no heartbeat was received before the timeout.
|
task.taskResult.id
|
string | "<TIMESTAMP>"
|
Request ID echoed by the CANopen RPC call. |
task.taskResult.node_id
|
integer | 5
|
Node that was monitored. |
task.taskResult.state
|
string/null | "OPERATIONAL"
|
BOOT_UP, STOPPED, OPERATIONAL, PRE_OP, the raw integer for an unknown state, or null on timeout.
|
task.taskResult.error
|
string | "no heartbeat received from node 5 before timeout"
|
Present only on failure. |
The reported state is byte 0 of the heartbeat frame masked with 0x7F. Reading a heartbeat also refreshes the cached state reported by NMT_COMMAND.
Receive PDO
Waits for a single TPDO frame from the node and returns its raw bytes, with no decoding. Useful to inspect a PDO mapping. For a decoded position, use Continuous PDO Reading.
The COB-ID listened to is 0x180, 0x280, 0x380 or 0x480 for pdo_num 1..4, plus the node ID.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "RECV_PDO",
"taskParams": {
"node_id": 5,
"pdo_num": 1,
"timeout": 1.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "RECV_PDO",
"taskResult": {
"success": true,
"id": "<TIMESTAMP>",
"node_id": 5,
"pdo_num": 1,
"data": [0, 4, 0, 0, 0, 0, 0, 0]
}
}
}
On timeout this command still replies success: true, with data: null. A null in data is the only way to tell "no frame received" from a real frame, so do not read success: true as "a PDO arrived".
Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier, returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request. |
task.alias
|
string | Yes | "RECV_PDO"
|
Must be RECV_PDO.
|
task.taskParams.node_id
|
integer | Yes* | 5
|
See Node Reference. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name. |
task.taskParams.pdo_num
|
integer | No | 1
|
TPDO number, 1..4. Defaults to 1. Any other value fails with invalid pdo_num: <n>.
|
task.taskParams.timeout
|
number | No | 1.0
|
How long to wait for a frame, in seconds. Defaults to 1.0.
|
task.taskParams.sdo_timeout
|
number | No | 2.0
|
Only used to create or refresh the internal node wrapper. |
Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
task.taskResult.success
|
boolean | true
|
true even on timeout. Check data.
|
task.taskResult.id
|
string | "<TIMESTAMP>"
|
Request ID echoed by the CANopen RPC call. |
task.taskResult.node_id
|
integer | 5
|
Node that was listened to. |
task.taskResult.pdo_num
|
integer | 1
|
TPDO number that was listened to. |
task.taskResult.data
|
array/null | [0, 4, 0, 0, 0, 0, 0, 0]
|
Complete frame payload as delivered by the CAN driver, or null on timeout.
|
Receive EMCY
Waits for an Emergency frame from the node (COB-ID 0x80 + node_id).
Known defect: when an EMCY frame is actually received, the reply cannot be serialised to JSON, because the hardware layer returns the payload as a bytearray. The actor then answers with the actor-level error envelope instead of the intended reply:
{
"id": "<TIMESTAMP>",
"origin": "Dev",
"task": {
"alias": "RECV_EMCY",
"taskResult": {
"success": false,
"error": "Object of type bytearray is not JSON serializable"
}
}
}
In practice, only the timeout case currently produces a usable reply. See Known Limitations.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "RECV_EMCY",
"taskParams": {
"node_id": 5,
"timeout": 1.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out - timeout, no EMCY on the bus:
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "RECV_EMCY",
"taskResult": {
"success": true,
"id": "<TIMESTAMP>",
"node_id": 5,
"emcy": null
}
}
}
Intended reply once the defect is fixed:
{
"task": {
"alias": "RECV_EMCY",
"taskResult": {
"success": true,
"node_id": 5,
"emcy": {
"error_code": 4096,
"error_reg": 1,
"data": [0, 0, 0, 0, 0]
}
}
}
}
Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier, returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request. |
task.alias
|
string | Yes | "RECV_EMCY"
|
Must be RECV_EMCY.
|
task.taskParams.node_id
|
integer | Yes* | 5
|
See Node Reference. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name. |
task.taskParams.timeout
|
number | No | 1.0
|
How long to wait for an EMCY frame, in seconds. Default 1.0.
|
task.taskParams.sdo_timeout
|
number | No | 2.0
|
Only used to create or refresh the internal node wrapper. |
Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
task.taskResult.success
|
boolean | true
|
true even on timeout. Check emcy.
|
task.taskResult.node_id
|
integer | 5
|
Node that was listened to. |
task.taskResult.emcy
|
object/null | null
|
null when no EMCY arrived within timeout.
|
task.taskResult.emcy.error_code
|
integer | 4096
|
CANopen emergency error code, bytes 0 to 1 of the frame, little-endian. |
task.taskResult.emcy.error_reg
|
integer | 1
|
Error register, byte 2 of the frame, mirror of object 0x1001.
|
task.taskResult.emcy.data
|
array | [0,0,0,0,0]
|
Manufacturer-specific field, bytes 3 to 7 of the frame. |
This command reads one frame per request; it does not subscribe to emergencies. There is no continuous EMCY mode.
Query Node Info
Identifies a node with two SDO reads:
| Object | Meaning | Reply field |
|---|---|---|
0x1000:00
|
Device type, mandatory in CiA 301 | device_type
|
0x6501:00
|
Measuring units per revolution, CiA 406 encoder profile | steps_per_rev
|
Useful to discover the steps_per_rev to store later with CONFIG_CANOPEN.
Req-Topic: canopenRM8007/runtime/in
{
"id": "<TIMESTAMP>",
"origin": "APP",
"task": {
"alias": "QUERY_NODE_INFO",
"taskParams": {
"node_id": 5,
"timeout": 2.0
}
}
}
Reply-Topic: canopenRM8007/runtime/out
{
"id": "<TIMESTAMP>",
"origin": "canopen-actor",
"task": {
"alias": "QUERY_NODE_INFO",
"taskResult": {
"success": true,
"id": "<TIMESTAMP>",
"node_id": 5,
"device_type": 196610,
"steps_per_rev": 4096
}
}
}
Request Parameters
| Parameter | Type | Required | Example | Description |
|---|---|---|---|---|
id
|
string | Yes | "<TIMESTAMP>"
|
Unique request identifier, returned in the response. |
origin
|
string | No | "APP"
|
Identifies the sender of the request. |
task.alias
|
string | Yes | "QUERY_NODE_INFO"
|
Must be QUERY_NODE_INFO.
|
task.taskParams.node_id
|
integer | Yes* | 5
|
See Node Reference. |
task.taskParams.nodeId
|
integer | Yes* | 5
|
Alternative spelling for node_id.
|
task.taskParams.node
|
string | Yes* | "motor_1"
|
Logical node name. |
task.taskParams.timeout
|
number | No | 2.0
|
Timeout applied to each of the two SDO reads. Defaults to the configured sdo_timeout.
|
Response Parameters
| Parameter | Type | Example | Description |
|---|---|---|---|
task.taskResult.success
|
boolean | true
|
true only when both SDO reads succeeded.
|
task.taskResult.id
|
string | "<TIMESTAMP>"
|
Request ID echoed by the CANopen RPC call. |
task.taskResult.node_id
|
integer | 5
|
Node that was queried. |
task.taskResult.device_type
|
integer | 196610
|
Raw value of 0x1000:00.
|
task.taskResult.steps_per_rev
|
integer | 4096
|
Raw value of 0x6501:00.
|
task.taskResult.error
|
string | "SDO abort 0x6501:00 -> 0x06020000 (OBJECT_NOT_FOUND)"
|
Present only on failure. |
Both reads must succeed. On a device that does not implement 0x6501, the whole command fails even though 0x1000 was read correctly. Use SDO_READ on 0x1000:00 alone to probe such a device.
14.5 Design Principles
- Protocol-Only Actor
The CANopen Actor is responsible for:
- CAN frame encoding/decoding;
- SDO communication;
- PDO reception (raw);
- NMT control;
- Heartbeat monitoring.
- No Device Semantics
The actor:
Does NOT know:
- What 0x6004 represents;
- Units (RPM, mm, °C, etc.);
- Device-specific logic.
Only returns:
- Raw values;
- Protocol-level responses.
Device Scope
The actor is split into a generic CANopen (CiA 301) layer and a thin position convenience layer. Only the second one assumes anything about the device, and the topic prefix canopenRM8007 reflects the first use case, not a restriction.
Works with any CANopen device
| Alias | Notes |
|---|---|
PING
|
No CAN traffic at all. |
NMT_COMMAND
|
Standard NMT services. |
SDO_READ
|
Any index and subindex. Expedited only, value unsigned. |
SDO_WRITE
|
Any index and subindex, 1 to 4 bytes. |
READ_HEARTBEAT
|
Standard heartbeat. Requires 0x1017 enabled on the node.
|
RECV_PDO
|
Raw TPDO bytes, with no interpretation. |
RECV_EMCY
|
Standard EMCY frame. See the known defect. |
CONFIG_CANOPEN
|
Local configuration only, no bus traffic. |
Assume a position or encoder device
| Alias | Assumption |
|---|---|
READ, READ_CONTINUOUS, READ_PDO_CONTINUOUS
|
The value is a single integer whose scale is steps_per_rev, reported as revolutions and angle_deg. The source defaults to 0x6004:00 (CiA 406 position value), but index and subindex are parameters, so any object can be used. The reply fields keep the angular names.
|
READ without steps_per_rev
|
Falls back to reading 0x6501:00 (CiA 406 measuring units per revolution). On a device without 0x6501, always send steps_per_rev explicitly.
|
QUERY_NODE_INFO
|
Hard-coded to 0x1000:00 and 0x6501:00, with no parameters for other objects. It fails as a whole on a device that does not implement 0x6501.
|
CONFIG_POSITION_TPDO
|
Defaults to mapping 0x6004:00/32, but map_index, map_subindex and map_bits are parameters, so any single object can be mapped. Exactly one object per TPDO.
|
For a valve, an I/O module or a drive, either use SDO_READ, SDO_WRITE, NMT_COMMAND and RECV_PDO and ignore the position aliases, or reuse the position aliases with an explicit index, subindex and steps_per_rev and read raw, ignoring angle_deg.
CANopen services not implemented
- PDO transmission (RPDO): the actor never writes process data. A device is driven only through
SDO_WRITE. - SYNC producer: no SYNC frame is generated, so synchronous TPDO transmission types (
1..240) will not produce data. Use255, event-driven. - Segmented and block SDO transfer: strings and values longer than 4 bytes cannot be read or written.
- Signed SDO values:
SDO_READalways returns an unsigned integer. Only the PDO position path has asignedoption. - Multi-object PDO mapping:
CONFIG_POSITION_TPDOmaps exactly one object, and RPDO mapping is not configurable at all. - LSS, TIME, node scanning, EDS or object dictionary upload.
- Continuous heartbeat and EMCY monitoring:
READ_HEARTBEATandRECV_EMCYare one-shot polls. The actor never publishes unsolicited alarms.
Node Management
- Nodes can be addressed directly by
node_id;CONFIG_CANOPENis optional and only adds logical names and per-node defaults - Node configuration lives in memory inside the actor and is lost on restart
- The hardware owner caches one CANopen node object per
node_id, including the last heartbeat state observed - Several nodes can be used simultaneously through one-shot commands, and
READpolls all configured nodes in a single request - Continuous loops are the exception: one node at a time, and one single loop for the whole actor
Integration Philosophy
Higher layers (NOT the actor) are responsible for:
- Mapping indexes to meaning
- Data interpretation
- Visualization (Node-RED, Grafana)
- Business logic
Known Limitations
Behaviour that the current implementation gets wrong or leaves incomplete. Documented so that integrations do not depend on it.
| # | Area | Limitation |
|---|---|---|
| 1 | RECV_EMCY
|
A real EMCY frame cannot be serialised to JSON, because the payload is a bytearray. The reply becomes an actor-level error and the emergency data is lost. Only the timeout case works.
|
| 2 | READ_CONTINUOUS
|
A start request with no node reference is acknowledged with mode: "all" and running: true, but the loop cannot run in that mode and only publishes failed samples.
|
| 3 | READ_PDO_CONTINUOUS
|
An unknown node name, or a steps_per_rev that cannot be resolved, is only detected after the acknowledgement. The loop then dies silently, with neither samples nor an error.
|
| 4 | READ_PDO_CONTINUOUS
|
Failed cycles always report pdo operation failed; the specific hardware error (pdo timeout, pdo payload too short: ...) is discarded.
|
| 5 | RECV_PDO, RECV_EMCY
|
A timeout is reported as success: true with data / emcy set to null, instead of success: false.
|
| 6 | NMT_COMMAND
|
RESET_COMM with wait: true always reports failure; confirmation is not implemented.
|
| 7 | Continuous loops | Only one loop exists for the whole actor: starting one kind silently cancels the other, and either stop alias stops either loop. |
| 8 | SDO | Expedited transfers only (1 to 4 bytes) and values always unsigned. No segmented or block transfer, and no signed decoding. |
| 9 | Error envelope | Actor-level errors are published with origin: "Dev" instead of canopen-actor, so origin cannot be used to identify the source of a reply.
|
| 10 | Persistence | Node configuration and running continuous loops are lost on actor restart, with no notification to the application. |
Summary
The CANopen Actor:
- Implements CANopen protocol communication
- Provides generic MQTT interface
- Supports multi-node networks
- Keeps strict separation between transport and semantics
15 SMS Actor
15.1 Overview
The ATLAS system can be remotely controlled over SMS. The SMS Actor listens to the cellular modem for incoming text messages and reacts to them in two ways: it reports every received SMS over MQTT, and it recognises a small set of predefined text commands that trigger actions directly on the device — without needing network or MQTT connectivity.
This is useful when a device is unreachable over its normal network path (e.g. no internet, VPN down, misconfigured mobile connection) but still has GSM/SIM coverage. An operator can send a plain SMS to the device's SIM number to remotely reboot it, reset its mobile connection, toggle its digital outputs/relays, or manage SSH/mobile data/VPN.
The SMS Actor does not expose a request/response MQTT command interface for external callers, like other actors (e.g. the eMMC actor). It is event-driven: it reacts to SMS messages arriving on the modem and publishes results to MQTT. There is no .../runtime/intopic for external callers to send it commands over MQTT. Internally, the actor does use an MQTT request/response bridge to the Digital Outputs and Relays actors to carry out the OUT##/RL# commands (see Command execution).
15.2 Supported SMS Commands
The actor recognises two kinds of commands:
- Fixed commands — the trimmed SMS body must exactly equal one of the values below. The auto-reply (if any) is fixed and is sent before the action executes, since the action itself may interrupt connectivity. What actually decides that timing is whether the command declares a static answer, not whether it takes a parameter. Only
resetMobileConnectiondeclares one.rebootis a fixed command with no static answer, so it follows the post-execution path like the parameterized commands — and because its handler returns no answer text, no SMS reply is ever sent for it. - Parameterized commands — the SMS body matches a pattern with a parameter (output index, on/off, open/close). The auto-reply reflects the actual result of the action and is sent after it executes.
A single SMS may contain multiple lines, but every recognised line must be the same action — e.g. several RL#:Open|Close lines to drive both relays together. Mixing different actions in one SMS (even two different parameterized commands, or a parameterized command together with a fixed one) is rejected outright: nothing executes. See Multiple commands per SMS.
a) Fixed commands
| SMS text | Action | Auto-reply SMS | Destructive |
| reboot | Reboots the device | none | Yes |
| resetMobileConnection | Resets the mobile connection (modem disable/enable cycle) | resetOnGoing | Yes |
b) Parameterized commands
| SMS text | Action | Auto-reply SMS (sent after execution) | Destructive |
| OUT[##]:High|Low | set_digital_output | OUT[##]St:High|Low | No |
| RL[1|2]:Open|Close | set_relay | RL[1|2]St:Open|Close | No |
| VPN:On|Off | set_vpn | VPNST:On<IpAddress>|VPNST:Off | No |
| SSH:On|Off | set_ssh | SSHST:On|Off | No |
| MobileData:On|Off | set_mobile_data | MobileDataStatus:On|Off | Yes (interrupts mobile connectivity) |
- The Destructive column above describes real-world impact, not the
destructiveflag published over MQTT. That flag is set only byrebootandresetMobileConnection.MobileData:Offcuts connectivity just as effectively but is not flagged, and the field is omitted entirely rather than sent asfalse(see SMS_COMMAND_STARTED).
##is a zero-padded digital output index01-08, mapped toDO1-DO8. Any other index (e.g.OUT09,OUT99) is treated as an unrecognised SMS.RL1/RL2map to the sameDO1/DO2hardware naming used internally by the relay actor.- Matching is case-sensitive and exact — the SMS body must equal/match one of the patterns above (after trimming whitespace).
- Any SMS that does not match a known fixed or parameterized command is still reported over MQTT (
SMS_RECEIVED) but no action is taken. - An SMS whose recognised lines are not all the same action (e.g. a relay command mixed with
reboot, or with a digital output command) is treated the same way: reported viaSMS_RECEIVED, but no command runs and no auto-reply is sent (see Multiple commands per SMS). - Unlike the fixed commands, a parameterized command's auto-reply is only sent when the action succeeds. If it fails, no SMS reply is sent, but the failure is still published over MQTT (see Command result (parameterized commands)).
Security note: there is currently no sender allow-list or authentication on inbound SMS commands. Any SMS with the exact matching text/pattern will trigger the corresponding action, regardless of sender. Restrict knowledge of the SIM number and treat it as a sensitive credential.
c) Multiple commands per SMS
An SMS body can carry several commands at once, one per line, so a single message can drive multiple outputs/relays together, e.g.:
RL1:Close
RL2:CloseThis is accepted because both lines are the same action (set_relay). But every recognised line in the SMS must be the same action — mixing, e.g.:
RL1:Close
rebootis rejected outright: none of the lines run, only a warning is logged, and no auto-reply is sent (same as an SMS matching no command at all). This applies to any combination of different actions, not just fixed-vs-parameterized — a set_relay line mixed with a set_digital_output line is rejected too.
- Each non-blank line is matched independently against the known command patterns (fixed and parameterized alike). Lines that don't match anything are silently skipped and don't count toward the action check — only the recognised lines' actions are compared.
- If every recognised line shares the same action, commands run sequentially, in the order their lines appear in the SMS. If the same output/relay appears more than once (e.g.
RL1:ClosethenRL1:Open), the last one wins. - Each line is tracked as its own
SMS_COMMAND_STARTED/ command-result MQTT event (see MQTT Interface), withsmsPayloadset to that line's text rather than the full SMS body. - The auto-replies for all lines that succeeded are combined into a single SMS reply, one line per successful command, in the same order the commands were received (e.g.
RL1St:Close\nRL2St:Close). Lines whose action failed contribute no reply line, mirroring the single-command behaviour.
15.3 How It Works
a) Startup cleanup
When the actor starts, it deletes all pre-existing SMS stored on the modem/SIM. This is a one-shot cleanup, not a periodic action.
It is controlled by the delete_existing_on_start constructor argument, which defaults to True and is not overridden where the actor is created. There is no configuration file, environment variable or MQTT command that changes it: disabling it requires a code change. The same applies to delete_sms_after_processing.
A failure during cleanup is logged as a warning and does not stop the actor. An SMS that could not be deleted stays in modem/SIM storage and is not added to the seen set, so a later D-Bus signal for that same message would be processed as if it were new.
b) Listening for SMS
The actor runs a background thread subscribed to ModemManager D-Bus signals (Added and Completed on the Modem.Messaging interface). There is no polling loop — processing is purely event-driven, triggered as soon as the modem reports a new SMS object.
Because multipart SMS may not be immediately readable, the actor retries reading a new message a few times before giving up (10 attempts, 0.75s apart). If the message still cannot be read, it is logged as a warning and deleted without being reported.
| Outcome | Behaviour |
Never readable — every mmcli -s <path> attempt failed
|
Logged as a warning, deleted and marked as seen. No SMS_RECEIVED is published and no command runs.
|
Readable but never ready — it was read, but stayed in state receiving/unknown with no text and no sender across all 10 attempts
|
The last snapshot read is used anyway and processing continues normally from there, including command matching. |
A message that becomes ready earlier returns immediately, without waiting out the remaining attempts.
c) Message deletion
Processed SMS are deleted from the modem/SIM after handling, by default. This keeps SIM storage from filling up and prevents the same message from being processed twice.
d) Command execution
Fixed commands: once matched, the fixed auto-reply (if any) is sent, then the actor waits briefly (2s) to let the MQTT publish and SMS reply/delete complete, then executes the underlying action:
reboot— runssystemctl reboot.resetMobileConnection— runsmmcli -m <modem> --disable, waits 3s, thenmmcli -m <modem> --enable.
Parameterized commands: the SMS is deleted immediately (same as fixed commands), the action executes, and only then — if it succeeded — is the computed auto-reply sent:
set_digital_output(OUT##:High|Low) — calls the Digital Outputs actor (busi2c-3process) over an MQTT request/response bridge: publishes{"action": "SET", "taskParams": {"DO<n>": {"state": "high"|"low"}}}todigitalOutputs/runtime/inand awaits the matching response ondigitalOutputs/runtime/out(correlated byid, 5s timeout).set_relay(RL1|2:Open|Close) — same bridge mechanism, targeting the Relays actor (busi2c-1process) viarelayOutputs/runtime/in/relayOutputs/runtime/out.set_ssh(SSH:On|Off) — runssystemctl start|stopon bothssh.socketandssh.service(start/stop only, does not change boot-time enablement). Both units are stopped/started —ssh.socketalone stays listening on :22 and socket-activatesssh.serviceon the next connection, so stopping only the service would not actually block new SSH sessions.set_mobile_data(MobileData:On|Off) — reuses the samenmcli/systemd modem enable/disable flow as the MQTTSET_MODEM_STATEcommand (not themmcli-based path used byresetMobileConnection), so there is a single mechanism for enabling/disabling the modem connection.set_vpn(VPN:On|Off) — runssystemctl start|stopon whichever VPN unit is installed. A device is provisioned with exactly one technology, never both, and the handler detects which one by checkingsystemctl show -p LoadStatefor each in turn:
| Order | Unit | Tunnel interface |
| --------- | -------------------------------- | --------------------- |
| 1 | wg-quick@wg0.service | wg0 |
| 2 | openvpn@pi | tun0 |
WireGuard is checked first, as the current default for new deployments. If neither unit is loadable, nothing is executed and the result is { "success": false, "error": "no VPN unit installed on this device" }.
The tunnel itself (keys, certificates, peer, AllowedIPs) is provisioned out-of-band on both the device and the gateway; this command only starts/stops the unit and never touches the config.
After a successful start/stop, the handler also runs systemctl enable|disable on the same unit so the requested state survives a reboot, and reports the outcome as result.persisted. A failure there is logged as a warning and does not fail the command: the tunnel is already in the requested state, only the next boot will not remember it.
On On, the reply includes the tunnel's IPv4 address read back via ip -4 addr show dev <interface> (e.g. VPNST:On10.100.0.28); if the address cannot be read, the reply is still VPNST:On with no address, and result.tunnelIp is null. On Off, the reply is VPNST:Off.
15.4 MQTT Interface
The SMS Actor publishes its events on a single topic and accepts no inbound commands over MQTT. It does subscribe to two topics, but only to collect the replies to its own bridge requests:
| Topic | Direction | Purpose |
sms/runtime/out
|
Publish | Every event described below. |
digitalOutputs/runtime/in
|
Publish | SET request for the OUT## command.
|
digitalOutputs/runtime/out
|
Subscribe | Reply to that request, correlated by id.
|
relayOutputs/runtime/in
|
Publish | SET request for the RL# command.
|
relayOutputs/runtime/out
|
Subscribe | Reply to that request, correlated by id.
|
A message on a bridge response topic whose id matches no pending request is silently ignored, as is one that is not valid JSON.
The MQTT client id is <name>-<epoch_ms>, freshly generated on each start so that a lingering previous process does not cause the broker to disconnect the new one. It is not the same as origin, which is the plain actor name.
Publish topic: sms/runtime/out (QoS 0, not retained)
All messages follow the standard ATLAS MQTT envelope, with the result carried in task.taskResult (not task.taskParams, unlike the request/response actors):
{
"id": "<TIMESTAMP>",
"origin": "SMSActor",
"task": {
"action": "<ACTION_OR_COMMAND_NAME>",
"taskResult": { }
}
}
id: millisecond epoch timestamp, generated fresh for each publish. The three messages describing the same SMS (see below) are not correlated byid.origin: always the actor's configured name (SMSActor).
There are three distinct events published to this topic.
a) SMS_RECEIVED
Published whenever a genuine incoming SMS is detected, whether or not it matches a known command.
Published message:
{
"id": "1782142697993",
"origin": "SMSActor",
"task": {
"action": "SMS_RECEIVED",
"taskResult": {
"success": true,
"from": "+351912345678",
"text": "some sms body",
"state": "received"
}
}
}
| Field | Type | Description |
|---|---|---|
success
|
boolean | true if the SMS was read successfully.
|
from
|
string | Sender's phone number, as reported by the modem. |
text
|
string | Full text body of the SMS. |
state
|
string | Normalised delivery state reported by ModemManager (e.g. "received", "sent", "failed").
|
b) SMS_COMMAND_STARTED
Published as soon as an incoming SMS matches a known command and its auto-reply (if any) has been sent — before the actual action executes.
Published message:
{
"id": "1782142697994",
"origin": "SMSActor",
"task": {
"action": "SMS_COMMAND_STARTED",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "resetMobileConnection",
"command": "reset_mobile_connection",
"answer": "resetOnGoing",
"destructive": true
}
}
}
| Field | Type | Description |
|---|---|---|
success
|
boolean | Always true. It marks that the command was recognised; the result of sending the auto-reply is not checked before publishing.
|
from
|
string | Sender's phone number. |
smsPayload
|
string | Original SMS text that triggered the command. |
command
|
string | Internal action identifier (e.g. "reboot", "reset_mobile_connection").
|
answer
|
string | Auto-reply text sent back to the sender. null/absent if none is defined.
|
destructive
|
boolean | true if the command is flagged destructive. Only reboot and resetMobileConnection are. The key is omitted when false, never sent as false.
|
c) Command result
Published once the command's action has finished executing. The task.action field is the internal command identifier (e.g. "reboot", "reset_mobile_connection"), not a fixed name.
Example — reset_mobile_connection:
{
"id": "1782142697995",
"origin": "SMSActor",
"task": {
"action": "reset_mobile_connection",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "resetMobileConnection",
"answer": "resetOnGoing",
"result": {
"success": true,
"step": "completed"
}
}
}
}
Example — reboot:
{
"id": "1782142697996",
"origin": "SMSActor",
"task": {
"action": "reboot",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "reboot",
"result": {
"success": true,
"returnCode": 0,
"stdout": "",
"stderr": ""
}
}
}
}
| Field | Type | Description |
|---|---|---|
success
|
boolean | Mirrors result.success, so it does reflect whether the action itself succeeded.
|
from
|
string | Sender's phone number. |
smsPayload
|
string | Original SMS text that triggered the command. |
answer
|
string | Auto-reply text submitted to the modem, if any. Delivery is not confirmed; see Known Limitations. |
result
|
object | Outcome of the underlying action. See below. |
result object (reset_mobile_connection):
| Field | Type | Description |
|---|---|---|
success
|
boolean | true if both the disable and enable steps completed without error.
|
step
|
string | "completed" on full success, or the failing step: "disable", "enable", "nmcli_connection_up" or "wait_connected".
|
returnCode
|
number | Exit code of the failing mmcli process. Present only on a disable or enable failure.
|
stdout
|
string | Standard output of the failing mmcli command. Present only on a disable/enable failure.
|
stderr
|
string | Standard error of the failing mmcli command. Present only on a disable/enable failure.
|
error
|
string | Present instead of the above if no modem could be found ("no_modem_found").
|
On success the result object carries only success and step; there is no returnCode, stdout or stderr, because the sequence spans several commands rather than a single one. The nmcli_connection_up and wait_connected failures likewise carry only success and step.
result object (reboot):
| Field | Type | Description |
|---|---|---|
success
|
boolean | true if systemctl reboot was issued successfully.
|
returnCode
|
number | Exit code of the systemctl reboot command.
|
stdout
|
string | Standard output of the command. |
stderr
|
string | Standard error of the command. |
If systemctl reboot itself raises (rather than returning a non-zero exit code), the result object is { "success": false, "error": "<exception text>" } instead, with no returnCode/stdout/stderr. The same shape is used by set_ssh, set_vpn and set_mobile_data when their subprocess call raises.
Defensive fallback (should not normally occur, since SMS text is pre-validated against the command table before dispatch):
{
"success": false,
"error": "handler_not_found",
"action": "<action>"
}
d) SMS_COMMAND_STARTED (parameterized commands)
Published as soon as an incoming SMS matches a parameterized command — before the action executes and before the auto-reply is known. answer is always null here; the actual reply is only computed after execution (see below).
If the SMS carries multiple commands (one per line, see Multiple commands per SMS), this event is published once per matched line, and smsPayload is that line's text, not the full SMS body.
{
"id": "1782142697994",
"origin": "SMSActor",
"task": {
"action": "SMS_COMMAND_STARTED",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "OUT01:High",
"command": "set_digital_output",
"answer": null
}
}
}
e) Command result (parameterized commands)
Published once the command's action has finished. task.action is the internal command name (set_digital_output, set_relay, set_ssh, set_mobile_data, set_vpn). taskResult.answer is the SMS reply that was actually sent — null/absent if the action failed or is not implemented.
For a multi-line SMS, this event is published once per matched line, each with its own smsPayload (that line's text) and result. However, taskResult.answer here is that line's own reply text (e.g. "RL1St:Close"); the SMS actually sent to the user is the combination of every successful line's answer (see Multiple commands per SMS).
Example — set_digital_output:
{
"id": "1782142697995",
"origin": "SMSActor",
"task": {
"action": "set_digital_output",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "OUT01:High",
"answer": "OUT01St:High",
"result": {
"success": true,
"output": "DO1",
"state": "high",
"answer": "OUT01St:High"
}
}
}
}
Example — set_relay:
{
"task": {
"action": "set_relay",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "RL1:Open",
"answer": "RL1St:Open",
"result": {
"success": true,
"relay": "RL1",
"state": "open",
"answer": "RL1St:Open"
}
}
}
}
Example — set_ssh:
{
"task": {
"action": "set_ssh",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "SSH:On",
"answer": "SSHST:On",
"result": {
"success": true,
"returnCode": 0,
"stdout": "",
"stderr": "",
"answer": "SSHST:On"
}
}
}
}
Example — set_mobile_data:
{
"task": {
"action": "set_mobile_data",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "MobileData:Off",
"answer": "MobileDataStatus:Off",
"result": { "success": true, "answer": "MobileDataStatus:Off" }
}
}
}
Example — set_vpn:
{
"task": {
"action": "set_vpn",
"taskResult": {
"success": true,
"from": "+351912345678",
"smsPayload": "VPN:On",
"answer": "VPNST:On10.100.0.28",
"result": {
"success": true,
"returnCode": 0,
"stdout": "",
"stderr": "",
"tunnelIp": "10.100.0.28",
"answer": "VPNST:On10.100.0.28"
}
}
}
}
| Field | Type | Description |
|---|---|---|
success
|
boolean | true if the action fully succeeded. When false, no SMS auto-reply was sent.
|
from
|
string | Sender's phone number. |
smsPayload
|
string | Original SMS text that triggered the command. |
answer
|
string | null | SMS reply text actually sent, or null if none was sent (failure).
|
result
|
object | Handler-specific outcome, always with success. See the table below.
|
result object per command:
| Command | Fields |
|---|---|
set_digital_output
|
output (DO1..DO8) and state, taken from the bridged actor's reply. When that reply carries no state for the output, the requested state is echoed back instead. A missing entry for the output means success: false.
|
set_relay
|
relay (RL1/RL2) and state, with the same fallback. Note the bridge request itself uses DO1/DO2 naming; only the result is renamed.
|
set_ssh
|
returnCode, stdout, stderr. Two units are driven (ssh.socket then ssh.service): returnCode is the first non-zero exit code across both, and stdout/stderr are the non-empty outputs of both, joined with a newline.
|
set_vpn
|
returnCode, stdout, stderr, persisted (whether systemctl enable/disable succeeded, false does not fail the command) and, on On, tunnelIp (null when the address could not be read back).
|
set_mobile_data
|
On success, only success and answer. On failure, step is "apply" or "wait_state"; there is no returnCode/stdout/stderr.
|
On an unexpected exception, every handler returns { "success": false, "error": "<exception text>" } instead of its usual fields.
15.6 Interpreting the Results
a) success: false on SMS_RECEIVED
success is always true on this event. It is a fixed field, not a status: an SMS whose content could not be read after the retries (typically a multipart message that never completed assembly) is deleted without any SMS_RECEIVED being published at all, so there is no such thing as SMS_RECEIVED with success: false. A message that goes missing between the modem receiving it and MQTT is only visible in the actor's warning log.
b) destructive: true
The command is one of the two flagged as able to interrupt connectivity or restart the device: reboot and reset_mobile_connection. For those, the SMS is deleted and any static auto-reply is sent before the action runs, precisely because the action may cut the path the reply would travel on.
The flag is absent on every other command. Note that this is a static property of the command table, not a judgement about impact: MobileData:Off disconnects the device just as thoroughly and carries no flag, so do not use the presence of destructive as a safety gate.
c) result.error: "no_modem_found"
The modem could not be located over D-Bus at the time the action ran. This can happen if the modem was disconnected or ModemManager restarted between the SMS being received and the action executing.
d) No response to unrecognised SMS
If an incoming SMS does not match any entry in the command table (fixed or parameterized), it is reported via SMS_RECEIVED only. No auto-reply is sent, and no action is executed.
e) Mixed commands in one SMS are rejected
If an incoming SMS's recognised lines are not all the same action (e.g. a relay command mixed with reboot, or with a digital output command), the whole message is rejected: a warning is logged server-side and the SMS is reported via SMS_RECEIVED, but no command runs and no auto-reply is sent — same as an SMS matching no command at all. Sending multiple lines of the same action (e.g. several RL# lines) is still fully supported.
f) Digital output / relay bridge timeout
set_digital_output and set_relay call the Digital Outputs / Relays actors over MQTT and wait up to 5s for a response. If that actor process is down or doesn't answer in time, the request raises a timeout, success is false in the command result, and no auto-reply is sent.
g) VPN systemd unit
set_vpn targets whichever VPN unit is actually installed, detected at command time with systemctl show -p LoadState <unit>:
| Order | Unit | Interface | Notes |
|---|---|---|---|
| 1 | wg-quick@wg0.service
|
wg0
|
WireGuard, the current default for new deployments. Checked first. |
| 2 | openvpn@pi
|
tun0
|
OpenVPN, used on devices provisioned before WireGuard. Checked as a fallback. |
A device is provisioned with exactly one of the two, never both. If neither unit is loadable, the command fails with error: "no VPN unit installed on this device" and nothing is started or stopped.
The tunnel config (keys, certificates, peer, AllowedIPs) is provisioned out-of-band on both the device and the gateway and is never touched by this command — it only starts/stops the already-configured unit, and enables/disables it so the state survives a reboot.
wg-quick@wg0.service is a oneshot unit with RemainAfterExit=yes, so systemctl start only returns once wg-quick has finished assigning the interface address. That is why no extra wait or poll is needed before reading the IP back for the On reply. Confirm the same holds for the installed openvpn@pi unit definition if it is ever changed.
A read-only systemctl is-active openvpn check exists elsewhere in the codebase for a status display. It is unrelated to this command and does not control anything.
h) SSH systemd units
set_ssh runs systemctl start|stop on both ssh.socket and ssh.service, confirmed on-device via systemctl list-units | grep -i ssh. On this image, ssh.service is TriggeredBy: ssh.socket and both are enabled independently — stopping only ssh.service leaves ssh.socket listening on :22, and it socket-activates a fresh sshd on the next connection attempt, so SSH would stay reachable despite SSH:Off reporting success. Some distros name the service unit sshd instead; reconfirm both unit names if targeting a different image.
15.7 Boot Behaviour
On every boot, the actor:
- Connects to MQTT and starts a background reconnector (exponential backoff, 1s → 30s cap, if the connection drops).
- Deletes all pre-existing SMS on the modem/SIM (unless disabled via configuration).
- Starts the D-Bus listener thread and begins reacting to incoming SMS.
There is no periodic check — all processing after boot is triggered by modem D-Bus signals.
15.8 Known Limitations
Behaviour that is easy to rely on by mistake, listed so integrations do not.
| # | Area | Limitation |
|---|---|---|
| 1 | Authentication | There is no sender allow-list and no authentication. Any handset that knows the SIM number can reboot the device, cut its mobile data, or drive its outputs. The SIM number is effectively a credential. |
| 2 | Auto-reply delivery | The result of mmcli --send is ignored by the caller. A reply that the modem refused to send is logged as a warning, but MQTT still reports it in answer as though it went out. answer means "submitted", never "delivered".
|
| 3 | Lost messages | An SMS that cannot be read after the retries produces no MQTT event at all. It is deleted and only a warning is logged, so an MQTT consumer cannot tell that a message arrived and was dropped. |
| 4 | Bridge errors | A set_digital_output/set_relay timeout surfaces as result.error set to an empty string, because the underlying timeout carries no message. A timeout also does not mean the output was left unchanged, only unconfirmed.
|
| 5 | destructive flag
|
Only reboot and reset_mobile_connection carry it. MobileData:Off is just as disruptive and is not flagged, so the field is not a safety gate.
|
| 6 | Configuration | delete_existing_on_start and delete_sms_after_processing are constructor arguments with no configuration surface. Changing them requires a code change.
|
| 7 | Delete failures | An SMS whose deletion fails is not added to the seen set, so a later D-Bus signal for the same message can execute the command a second time. |
| 8 | Seen-path tracking | Processed SMS paths are remembered in memory for the life of the process and never pruned. If ModemManager restarts and reuses a low object path (e.g. /SMS/1), a genuinely new message on that path is silently skipped.
|
| 9 | Destructive timing | Before reboot and resetMobileConnection the actor waits a fixed 2 s for the MQTT publish, the SMS reply and the deletion to complete. That is a delay, not a confirmation: on a slow modem the action can still cut them off.
|
| 10 | Mixed commands | One SMS carries one action type. A relay line mixed with a reboot line runs nothing at all, and the sender receives no reply telling them why.
|
| 11 | VPN persistence | systemctl enable/disable failing is only a warning. The tunnel is in the requested state now, but the next boot will not remember it. Check result.persisted.
|
15.9 Notes
- The SMS Actor keeps no persistent state across restarts (no EEPROM, file, or database). In-memory bookkeeping only tracks SMS already seen/being processed, to avoid double-handling from overlapping D-Bus signals.
- Message deletion after processing is enabled by default; this can be disabled via configuration if a full SMS audit trail is needed on the modem/SIM itself (subject to SIM storage limits).
- There is currently no sender allow-list or authentication on inbound SMS commands — any device holding the SIM number can trigger any of the commands above by sending the matching text.
set_digital_output/set_relayare the only commands that depend on another actor process (busi2c-3/busi2c-1) being up and reachable over MQTT; all other commands run entirely within the SMS actor's own process.