Technical article
The robot command dispatcher: what happens between accepted and succeeded
Submitting a robot command never touches the broker directly. The management API writes the command ledger, audit, and an MQTT outbox record first — then the dispatcher, receipt ingest, state snapshots, and compensation markers carry the command to a terminal state.
accepted means queued, not executed
POST /service-management/v1/robots/{robotId}/commands passes schema, capability, denylist, and shared safety-policy checks, then writes the command ledger, audit, and MQTT outbox. The status=accepted and protocolReceipt.outboxStatus=pending in the response only prove the command entered a reliable dispatch queue — not that the robot received or ran anything.
Two fast failure modes sit in front of that queue. Policy rejections return 422 ROBOT_POLICY_REJECTED with nothing persisted. And if the same robotId + commandId is already reserved by an AI validation decision, the normal entry returns 422 ROBOT_AI_APPROVAL_REQUIRED — that command can only be submitted through the opaque decision ID, never by copying the candidate body.
Four pieces carry the command forward
Outbox dispatch
POST /robots/commands:dispatch-next claims one due pending outbox record and publishes it to MQTT. On success the outbox flips to dispatched, the command advances to dispatched, and a robot.command.dispatched audit entry is appended. It is a manual operations and compensation entry — not proof that an automatic scheduling loop is enabled.
Ack/result receipt ingest
POST /robots/mqtt-receipts:ingest takes ack and result receipts on robot/{siteId}/{robotId}/command/{commandId}/ack|result topics. An ack with accepted moves dispatched -> acknowledged; a result moves the command to succeeded, failed, or timeout. Results may arrive before acks; late acks after a terminal state are ignored, duplicates never double-write audit, and a failed audit write rolls the state transition back in the same transaction.
State snapshots
Every successful dispatch, ack, and result derives a fresh persisted snapshot. GET /robots/{robotId}/state prefers it; without one it returns a management sample marked sourceType=management_snapshot. GET /robots/state-snapshots pages read-only over persisted snapshots with fixed ordering updatedAt desc, robotId asc and pageSize 1..100.
Compensation marking
No new queue table: when writing ROBOT_STATE_SNAPSHOT fails, the command is marked STATUS=compensation_required and a robot.command.compensation_required audit event is written. GET /robots/compensations lists the backlog, and robot-command diagnostics surface compensationRequired plus a warning while one exists. It keeps ledger, audit, and state consistent — it does not retry the protocol or re-send MQTT.
Real commands, real responses
Submit a move_to command. The receipt names the mqtt5 protocol and the exact command topic built from the configured pattern:
curl -sS -X POST http://127.0.0.1:8080/service-management/v1/robots/quad-001/commands \
-H "Content-Type: application/json" \
-d '{"commandId":"cmd-001","robotId":"quad-001","siteId":"site-a","commandType":"move_to","mode":"submit","timeoutMs":30000,"target":{"frame":"map","x":1.2,"y":3.4}}'
{
"success": true,
"data": {
"commandId": "cmd-001",
"robotId": "quad-001",
"status": "accepted",
"protocolReceipt": {
"protocol": "mqtt5",
"dispatched": false,
"outboxStatus": "pending",
"topic": "robot/site-a/quad-001/command/cmd-001"
}
},
"error": null,
"timestamp": 1782817800000,
"requestId": "REQ-..."
}Queries stay read-only: GET /commands/{commandId} never creates a command or triggers dispatch, the audit query filters by commandId or eventType, and dispatch-next is the manual trigger for one dispatcher pass:
curl -sS http://127.0.0.1:8080/service-management/v1/robots/quad-001/commands/cmd-001
curl -sS http://127.0.0.1:8080/service-management/v1/robots/quad-001/audit?commandId=cmd-001
curl -sS -X POST http://127.0.0.1:8080/service-management/v1/robots/commands:dispatch-nextArchive and dispatcher configuration
ROBOT_AUDIT_LOG archiving is automatic — no CLI trigger. It runs daily at 01:00, keeps database audit rows for 1 month and SQL backups for 24 months by default, writes ROBOT_AUDIT_LOG-yyyyMMddHHmmss.sql files under ${lightesb.deployment.backup-dir}/audit via a .tmp-then-rename protocol, and deletes only the audit records it actually exported — never the command ledger or outbox. lightesb.robot.audit.archive.enabled defaults to true; a single field beyond lightesb.robot.audit.archive.max-field-chars=65536 fails the task without deleting anything.
The dispatcher itself is off by default: lightesb.robot.dispatcher.enabled=false, with broker-uri, qos=1, retained=false (keep it false in production), clean-start=true, and a single global command topic pattern robot/{siteId}/{robotId}/command/{commandId}. With a local EMQX or strong simulator, the bundled precheck script verifies the full dispatcher-to-receipt loop — the pass criterion is accepted -> dispatched -> acknowledged -> succeeded with submitted, dispatched, ack, and result audits queryable:
export LIGHTESB_BASE=http://127.0.0.1:8080
export ROBOT_MQTT_BROKER_URI=tcp://127.0.0.1:1883
tools/robot-mqtt-firmware-precheck/test_robot_mqtt_firmware_precheck.sh --dispatcher-ingestBoundaries worth repeating
The CLI only calls the management API and never connects to the broker. outboxStatus=pending does not mean the robot received or executed anything, and snapshot sourceType=command_status|ack|result|management_snapshot values are not real device-presence acceptance. Without MySQL, lightesb.poc.h2-fallback.enabled=true runs ledger, audit, outbox, and snapshots on H2 — a small-volume demo mode with no production archive, retention, backup, or migration promises.
The bundled RobotMqttCommandSrv sample proves the route layer offline: system.components=robotics, robot.mqtt.broker.enabled=false, server.running=false, and robotCommandValidateProcessor plus robotCommandEnvelopeProcessor emitting command, ack, result, and audit JSON to mock: sinks. It validates routing, policy, and envelope semantics — it does not prove a broker received the command or a robot executed it.