Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Per-feature documentation with configuration, MQTT topic maps, and copy-paste te
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
| [heartbeat.md](heartbeat.md) | Self-heartbeat and service liveness tracking, interval configuration, test recipes |
| [telemetry.md](telemetry.md) | Periodic uptime telemetry, payload format, runtime configuration, test recipes |
| [control.md](control.md) | Command dispatch, runtime config get/set/reset, token authentication, exec subsystem, test recipes |
| [control.md](control.md) | Command dispatch registry, agent lifecycle (stop/start/reload/status), runtime config, token auth, exec, route, help |
| [bootstrap.md](bootstrap.md) | Profile-based provisioning flow, environment variables, cache management, test recipes |
| [ota.md](ota.md) | Over-the-air binary updates, trigger payload, download/verify/replace cycle, status reporting, test recipes |
| [terminal.md](terminal.md) | Interactive terminal sessions over MQTT, session lifecycle, PTY management, test recipes |
Expand Down
51 changes: 49 additions & 2 deletions docs/control.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@ All commands are sent as [SenML][senml] JSON arrays to the **commands channel re

### Command Subsystems

The dispatch registry is extensible — handlers can be registered at runtime — and each command carries metadata (description, usage) surfaced by the `help` command.

| `n` value | Handler | Description |
| --------- | ------------- | --------------------------------------------------------------- |
| `exec` | Execute | Run an allowlisted shell command |
| `config` | ServiceConfig | View services, get/set/reset runtime config, save export config |
| `service` | ServiceConfig | Alias for `config` — same handler |
| `control` | Control | Node-RED management commands |
| `control` | Control | Agent lifecycle (stop/start/reload/status) and Node-RED passthrough |
| `term` | Terminal | Open/close/write interactive terminal sessions |
| `nodered` | NodeRed | Node-RED flow operations |
| `ping` | Ping | Publish an immediate heartbeat |
| `reset` | Reset | Graceful shutdown and process restart |
| `ota` | OTA | Over-the-air binary update |
| `ota` | OTA | Over-the-air binary update (trigger/status/abort) |
| `devices` | DeviceManager | Downstream device CRUD |
| `route` | Route | Forward a payload to a downstream device interface |
| `help` | — | List available commands and their usage |

Authorization is enforced **per command**: when a command secret is configured, each command that requires auth must carry a matching `token` record in the SenML pack (see [Token Authentication](#token-authentication)).

## Message Format

Expand Down Expand Up @@ -257,6 +263,47 @@ mosquitto_pub \
-m "[{\"bn\":\"req-1:\", \"n\":\"config\", \"vs\":\"save,export,/path/to/config.toml,$CONTENT\"}]"
```

## Agent Lifecycle (control subsystem)

The `control` command manages the running agent without restarting the process. `reset` (below) is used for a full process restart.

| `vs` | Behavior | Response |
| ------------ | ------------------------------------------------------------------------------------- | ------------------------------------- |
| `stop` | Pause the heartbeat, telemetry, and device-scheduler loops; the process stays alive | `stopped` |
| `start` | Resume the paused loops and restart the device scheduler | `started` |
| `reload` | Re-apply persisted runtime config overrides (validated; invalid values are skipped) | `reloaded` or `reloaded:<keys>` |
| `status` | Report current runtime state | `{running, paused, uptime_seconds, version}` JSON |
| `nodered-*` | Node-RED passthrough (see [nodered.md](nodered.md)) | command-specific |

```bash
# Pause background publishing (agent stays alive), then resume
mosquitto_pub ... -m '[{"bn":"req-1:","n":"control","vs":"stop"}]'
mosquitto_pub ... -m '[{"bn":"req-1:","n":"control","vs":"start"}]'

# Re-apply persisted overrides and report status
mosquitto_pub ... -m '[{"bn":"req-1:","n":"control","vs":"reload"}]'
mosquitto_pub ... -m '[{"bn":"req-1:","n":"control","vs":"status"}]'
```

## Route to Downstream Device

The `route` command forwards a hex payload to a registered device's physical interface (opening it if needed), then optionally reads back `<read_bytes>` and returns them as a hex string. With no `read_bytes`, the number of bytes written is returned.

```bash
# Write bytes 01 a2 ff to <device-id> and read 16 bytes back
mosquitto_pub ... -m '[{"bn":"req-1:","n":"route","vs":"<device-id>,01a2ff,16"}]'
```

A missing device returns a clear "device not found" error. See [devices.md](devices.md) for device provisioning.

## Discover Commands (help)

The `help` command returns the registry as a JSON array of `{name, description, usage}`:

```bash
mosquitto_pub ... -m '[{"bn":"req-1:","n":"help","vs":""}]'
```

## Reset (process restart)

The `reset` command performs a graceful shutdown and then replaces the running process in-place via `syscall.Exec()`. The agent supports multiple reset modes:
Expand Down
2 changes: 2 additions & 0 deletions docs/devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ All device commands are sent via the `devices` dispatch name on the commands cha
| `read` | `devices,read,<device_id>,<n_bytes>` | Read n bytes from device, reply as hex string |
| `write` | `devices,write,<device_id>,<hex_data>` | Write hex-encoded bytes to the device |

> For a single write-then-read round trip to a device, the [`route`](control.md#route-to-downstream-device) command (`route,<device_id>,<hex_payload>[,<read_bytes>]`) opens the interface if needed, writes the payload, and returns the response in one command.

## Provisioning Flow

When `add` is called, the agent:
Expand Down
12 changes: 12 additions & 0 deletions docs/ota.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ mosquitto_pub \
-m '[{"bn":"req-1:","n":"ota","vs":"abort"}]'
```

### Query OTA status via commands channel

Returns the current OTA state (`busy` and `last_error`) as a JSON response on the control response topic:

```bash
mosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <client-id> -P <client-secret> --id "ota-$(date +%s)" \
-t "m/<domain-id>/c/<commands-channel-id>/req" \
-m '[{"bn":"req-1:","n":"ota","vs":"status"}]'
```

### Trigger OTA with token auth (when command_secret is set)

```bash
Expand Down
18 changes: 18 additions & 0 deletions middleware/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@ func (lm *loggingMiddleware) Control(uuid, cmd string) (err error) {
return lm.svc.Control(uuid, cmd)
}

func (lm *loggingMiddleware) Route(ctx context.Context, uuid, cmd string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("uuid", uuid),
slog.String("cmd", cmd),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Route command failed to complete successfully.", args...)
return
}
lm.logger.Info("Route command completed successfully.", args...)
}(time.Now())

return lm.svc.Route(ctx, uuid, cmd)
}

func (lm *loggingMiddleware) AddConfig(c agent.Config) (err error) {
defer func(begin time.Time) {
args := []any{
Expand Down
9 changes: 9 additions & 0 deletions middleware/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ func (ms *metricsMiddleware) Control(uuid, cmdStr string) error {
return ms.svc.Control(uuid, cmdStr)
}

func (ms *metricsMiddleware) Route(ctx context.Context, uuid, cmdStr string) error {
defer func(begin time.Time) {
ms.counter.With("method", "route").Add(1)
ms.latency.With("method", "route").Observe(time.Since(begin).Seconds())
}(time.Now())

return ms.svc.Route(ctx, uuid, cmdStr)
}

func (ms *metricsMiddleware) AddConfig(ec agent.Config) error {
defer func(begin time.Time) {
ms.counter.With("method", "add_config").Add(1)
Expand Down
Loading
Loading