Device Control System (DCM)
The Device Control Module (DCM) connects IoT devices deployed in the field to the Raytio platform. Examples include vending machines, showers, and pool tables, but any device type can be modelled. It provides remote command dispatch, real-time event capture, and a configurable slot-and-product model that adapts to different device types without requiring code changes.
Tenant administrators use DCM to register and configure devices. API integrators use the REST API to trigger purchases and query device state. Device firmware developers use MQTT to send events and receive commands, and to fetch authoritative configuration at startup.
Architecture
Devices communicate with the cloud over MQTT. The Raytio database is the authoritative source of truth for device state; the cloud backend keeps the two sides in sync.
Devices subscribe to their command and configuration topics and publish on their event and status topics. The cloud backend translates between MQTT messages and database records. Administrators and integrators interact exclusively through the REST API — no direct MQTT access is needed.
Device firmware does not call the REST API directly. Firmware fetches its authoritative configuration over MQTT using the config/request → config/set flow. Management clients (admin UI, integration tools) read and write slots and device configuration through the REST API. See Config Discovery for the full contract.
Core Concepts
Devices
A device is a registered physical unit. Each device has:
- A unique device ID (UUID) assigned at registration
- A device type (e.g.
VENDING_MACHINE,SHOWER,POOL_TABLE) that determines its capabilities and attribute configuration - A status reflecting its current operational state
- A linked location indicating where it is physically installed
- A linked party (organisation or person) that owns or operates it
Devices are registered and managed through the REST API.
Device status
| Status | Meaning |
|---|---|
PROVISIONING | Device registered; certificate not yet issued |
ONLINE | Device is connected and communicating with the cloud |
OFFLINE | Device is not reachable |
MAINTENANCE | Device is undergoing maintenance; connectivity events are ignored |
DECOMMISSIONED | Device has been permanently removed from service |
ONLINE and OFFLINE are set automatically when the device connects or disconnects — firmware does not set its own status.
Device identity
Each device has three identifiers that serve different purposes:
| Identifier | Format | Used for |
|---|---|---|
| Device ID | UUID (e.g. a1b2c3d4-…) | MQTT topic segments, API paths |
| Thing name | dcm-{tenant short}-{device_key} (e.g. dcm-t1000000-SHOW-001) | MQTT client ID and device shadow |
| Device key | Human-readable short code (e.g. SHOWER-001) | Display in the web client and event payloads |
The thing name is created by Raytio when the device certificate is issued. It is included in the config.json download and in the config/set payload under device.iot_thing_name. Firmware must be configured with the exact thing name from config.json.
Slots
A slot is a logical position or channel on a device — the unit that holds a product or service offering.
| Field | Description |
|---|---|
slot_number | Primary position identifier on the device (e.g. 1, 2, 3) |
slot_id | UUID of the slot record; stable fallback identifier |
| Item catalog | The list of items (items[]) the slot is configured to sell |
item_id | The item for the currently active session (nullable; cleared on end) |
quantity | Current stock level |
capacity | Maximum stock level |
| Status | Availability — see slot status table below |
For example, a vending machine typically has one slot per product row, a shower can have multiple slots (one per shower head or bay), and a pool table might have a single slot representing the session itself.
Slot status
| Status | Meaning |
|---|---|
AVAILABLE | Slot is ready for use |
IN_USE | Slot is currently occupied by an active session |
RESERVED | Slot is reserved and not available for general use |
EMPTY | Slot has no product |
DISABLED | Slot has been taken out of service |
Slots are managed through the REST API. When slot data is included in MQTT payloads, slot_number is the primary identifier and slot_id (the UUID) is the fallback.
Firmware discovers its current slot assignments by sending a config/request with slots in the sections list. The backend responds on config/set with the authoritative slot list for that device. See Config Discovery.
Commands
A command is an instruction sent from the cloud to a device. Commands follow a request/response pattern over MQTT: the cloud publishes the command with a correlation ID and action, and the device publishes its acknowledgement referencing the same correlation ID.
Command status
| Status | Meaning |
|---|---|
PENDING | Created, not yet published to MQTT |
SENT | Published to the device's command topic |
ACKNOWLEDGED | Device confirmed receipt |
COMPLETED | Device confirmed successful execution |
TIMED_OUT | No device response received within the timeout |
FAILED | Device reported failure |
TIMED_OUT and FAILED are distinct: a timeout means the device did not reply at all; a failure means the device replied with an error.
Commands are accessible through the REST API.
Command types
| Command type | Description |
|---|---|
GET_STATUS | Request current device status |
SET_STATUS | Update device operational status |
HALT_STATUS | Temporarily suspend device operations (halt/release pattern) |
RELEASE_HALT | Resume device from halted state |
SELECT_TIMER | Start a timed session on a slot |
SELECT_PRODUCT | Initiate product dispensing from a slot |
CANCEL_TIMER | Cancel an active timer session |
SET_PRICES | Update pricing configuration on the device |
GET_PRICES | Retrieve current pricing from the device |
GET_LOGS | Retrieve operational logs from the device |
DISPENSE | Trigger product dispensing |
Events
An event is a message a device sends to report something that happened. Events are classified by type:
- Operational events — health, status changes, errors, and diagnostics. These appear on the device's Events tab in the web client.
- Billable events — completed interactions such as a successful dispense or session. Written to the Billing module (PBM) for metered charging.
Events are available through the REST API.
Data Model
The four core entities and their relationships:
Device
├── has many Slots
│ ├── items[] — item catalog: many items a slot can sell
│ └── item_id — the item for the current active session (nullable)
├── has many Commands (cloud → device instructions)
└── has many Events (device → cloud messages)
| Entity | Purpose |
|---|---|
| Device | Registered physical unit |
| Slot | Service position on a device |
| Command | Instruction sent to a device |
| Event | Message received from a device |
Commands and events are both linked to a device but represent opposite directions: commands flow cloud-to-device, events flow device-to-cloud.
MQTT Topics
Device firmware uses MQTT to communicate with the Raytio cloud. All topics for a device are scoped to its tenant and device ID.
Topic naming convention
dcm/{tenant_id}/{device_id}/{category}/{message_type}
| Segment | Description |
|---|---|
dcm | Fixed module prefix |
{tenant_id} | UUID of the tenant that owns the device |
{device_id} | UUID of this device |
{category} | command, events, status, config, or logs |
{message_type} | Specific message kind |
Topic reference
| Topic | Direction | Purpose |
|---|---|---|
dcm/{tenant_id}/{device_id}/command/{ACTION} | Cloud → Device | Deliver a command (action is the command type code) |
dcm/{tenant_id}/{device_id}/command/{ACTION}/response | Device → Cloud | Acknowledge or complete a command |
dcm/{tenant_id}/{device_id}/events | Device → Cloud | All device events (operational and billable) |
dcm/{tenant_id}/{device_id}/status/report | Device → Cloud | Device reports current shadow and slot state |
dcm/{tenant_id}/{device_id}/status/get | Cloud → Device | Request device to send a status report |
dcm/{tenant_id}/{device_id}/logs | Device → Cloud | Device publishes a log batch |
dcm/{tenant_id}/{device_id}/logs/get | Cloud → Device | Request device to send its log buffer |
dcm/{tenant_id}/{device_id}/config/request | Device → Cloud | Device requests authoritative configuration |
dcm/{tenant_id}/{device_id}/config/set | Cloud → Device | Cloud delivers authoritative configuration |
dcm/{tenant_id}/{device_id}/config/get | Cloud → Device | Request device to report its currently applied config |
dcm/{tenant_id}/{device_id}/config/report | Device → Cloud | Device reports currently applied config |
$aws/things/{thing_name}/shadow/update | Device → Cloud | Device updates its AWS IoT classic shadow |
$aws/things/{thing_name}/shadow/update/delta | Cloud → Device | Shadow service delivers desired-state delta |
For details on the config topics, request sections, and payload shapes, see Config Discovery.
Message format
All messages use JSON. Every command carries a correlation_id that links it to its response, and an action that names the command type.
Command (cloud → device):
{
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"action": "SELECT_TIMER",
"payload": {
"slot_number": 1,
"duration_seconds": 300
}
}
Command response (device → cloud):
{
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"success": true
}
Event (device → cloud):
{
"event_type": "HEARTBEAT",
"event_severity": "INFO",
"device_key": "SHOWER-001"
}
The correlation_id in a command response must exactly match the value from the originating command. Events do not reference command correlation IDs.
Command Flow
Commands follow a request/response cycle. The cloud creates the command record, publishes it to the device's MQTT command topic, and waits for a response keyed by correlation_id. The command record is updated at each stage.
If the device does not respond within the configured timeout, the command moves to TIMED_OUT.
Halt / set / release pattern
Remote purchases use a three-step sequence to prevent race conditions when multiple users might trigger the same slot simultaneously.
| Step | Action | Effect |
|---|---|---|
| 1. Halt | HALT_STATUS | Pauses local UI on the device; slot cannot be claimed locally |
| 2. Action | e.g. DISPENSE / SELECT_TIMER / SELECT_PRODUCT | Device executes the purchase |
| 3. Release | RELEASE_HALT | Releases the pause; device resumes normal operation |
The halt and release commands use derivative correlation IDs: the HALT_STATUS command uses {id}:halt and the RELEASE_HALT uses {id}:release, where {id} is the main command's correlation ID.
Failure handling:
- If
HALT_STATUStimes out or fails, the cloud abandons the purchase — no charge is applied. - If the action fails after a successful
HALT_STATUS, the cloud sendsRELEASE_HALTto free the device. - If
RELEASE_HALTfails, the device remains halted and an operator must investigate.
Event Processing
Events arrive from the device over MQTT on the …/events topic and are classified by the cloud before being persisted.
Operational events
Published on dcm/{tenant_id}/{device_id}/events with event_type set to one of:
| Event type | Trigger |
|---|---|
CONNECTED | Device connected to the cloud (set automatically) |
DISCONNECTED | Device disconnected from the cloud (set automatically) |
CONNECTIVITY | Device-reported connectivity change |
HEARTBEAT | Periodic connectivity confirmation from the device |
STATUS_CHANGE | Device operational status changed |
TIMER_START | A timed session has begun |
TIMER_END | A timed session has ended normally |
TIMER_CANCEL | A timed session was cancelled |
ERROR | An error condition occurred on the device |
STOCK_LOW | Product stock in a slot is running low |
Billable events
| Event type | Trigger |
|---|---|
PAYMENT | Payment received for a session or vend |
SESSION_COMPLETE | Time-limited session ended normally |
DISPENSE | Product successfully dispensed from a slot |
Billable events are written to the billing pipeline and are visible through the Billing module (PBM). The backend validates the item_id in a billable event against the slot's configured item catalog before accepting it.
Event payload
All events share a common payload structure:
{
"event_type": "HEARTBEAT",
"event_severity": "INFO",
"device_key": "SHOWER-001"
}
Additional fields depend on the event type. Billable events include slot_id, item_id, and quantity.
Status Reports and the Device Shadow
The device publishes its current state on dcm/{tenant_id}/{device_id}/status/report. The backend stores the JSON body as the device's shadow_state and updates last_seen_at.
{
"shadow_state": {
"device_key": "SHOWER-001",
"halted": false
},
"slots": [
{
"slot_number": 1,
"slot_status_code": "AVAILABLE",
"quantity": 0
}
]
}
The cloud also syncs device state to the AWS IoT classic shadow via $aws/things/{thing_name}/shadow/update. The shadow delta topic (…/shadow/update/delta) delivers desired-state changes back to the device.
Logs / GET_LOGS flow
The cloud sends a GET_LOGS command to request the device's in-memory log buffer. The device publishes its log entries on dcm/{tenant_id}/{device_id}/logs. Log batches appear on the device's Events tab in the web client, and the pending GET_LOGS command is marked as COMPLETED.
The device can also publish log batches proactively on dcm/{tenant_id}/{device_id}/logs without a preceding logs/get request.
Connectivity and Presence
ONLINE and OFFLINE device statuses are set automatically when the device connects or disconnects — the device does not publish these itself. A CONNECTED event is recorded when the device connects; a DISCONNECTED event is recorded when it disconnects.
Devices in MAINTENANCE, PROVISIONING, or DECOMMISSIONED status are not affected by connectivity changes.
Extensible Attributes
Different device types need different configuration fields. DCM handles this through the Extensible Fields framework, which stores device-type-specific attributes without requiring schema changes.
For a complete explanation of how device-type categories, attribute definitions, and attribute values are stored and delivered to firmware, see Config Discovery.
Related Systems
| Module | Relationship |
|---|---|
| Products (GPM) | Slot item catalogs reference GPM items. Each item defines a name and category. |
| Pricing (PCM) | Session and vend prices are resolved through the pricing module at purchase time, based on the active price list for the device's tenant. |
| Party Relationships (PRM) | Devices are linked to the party (organisation or person) that owns or operates them. |
| Billing (PBM) | Billable events from devices feed into the billing pipeline. |
| Foundation (FND) | Device types, command types, event types, and slot statuses are defined as lookup values. The Extensible Fields framework provides device-type categories and attributes. |
Further reading
- Config Discovery - how firmware fetches slots and category config over MQTT
- Item Attributes and Values - the GPM attribute pattern that DCM's extensible fields framework mirrors
- Usage Events and Aggregation - how DCM usage events are metered and charged
- Job Scheduling and Notifications - background processing used for async command dispatch