606 lines
14 KiB
Markdown
Executable File
606 lines
14 KiB
Markdown
Executable File
# Soundbox Backend MQTT Specification
|
|
|
|
This document describes the backend contract used by the QF100 soundbox firmware in this project.
|
|
|
|
Source reference:
|
|
|
|
- MQTT payload parser: `app/source/MainApp/demo.c`
|
|
- Config endpoint constants: `app/source/MainApp/globalDefine.h`
|
|
|
|
## 1. Overview
|
|
|
|
The device communicates with the backend in two stages:
|
|
|
|
1. The device calls the config API to receive MQTT connection settings.
|
|
2. The device connects to MQTT and subscribes to the topic returned by the config API.
|
|
|
|
The backend then publishes JSON payloads to that topic.
|
|
|
|
Current firmware mode:
|
|
|
|
- `SAMPLE_MQTT_DEMO` is enabled by default.
|
|
- `MQTT_STRICT_TEST_DEMO` is disabled unless explicitly enabled in firmware.
|
|
|
|
## 2. Device Config API
|
|
|
|
The device sends a JSON request body to `CONFIG_ADDR`.
|
|
|
|
Current configured endpoint:
|
|
|
|
```text
|
|
http://sms.bizone.id/speaker/dev-config
|
|
```
|
|
|
|
Transport:
|
|
|
|
- HTTP is currently configured for the device config API.
|
|
- The firmware calls `sdk_http_get()` with the `http://` config URL.
|
|
- This avoids TLS/certificate validation issues during config fetch.
|
|
|
|
### Request Body
|
|
|
|
```json
|
|
{
|
|
"dev-model": "QF100",
|
|
"item-number": "00",
|
|
"dev-sn": "DEVICE_SN",
|
|
"hardware-config": "0x0F",
|
|
"fw-version": "1.0.0",
|
|
"fw-build": 1,
|
|
"app-config-version": 0,
|
|
"imei": "IMEI",
|
|
"imsi": "IMSI",
|
|
"iccid": "ICCID"
|
|
}
|
|
```
|
|
|
|
### Successful Response
|
|
|
|
```json
|
|
{
|
|
"error-code": 0,
|
|
"mqtt": {
|
|
"broker-ip": "broker.example.com",
|
|
"broker-port": 1883,
|
|
"client-id": "soundbox-DEVICE_SN",
|
|
"user-name": "mqtt_user",
|
|
"password": "mqtt_password",
|
|
"subscribe-topic": "soundbox/DEVICE_SN/down",
|
|
"keep-alive": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `error-code` | number | yes | Must be `0` for success. |
|
|
| `mqtt` | object | yes | Required when `error-code` is `0`. |
|
|
| `mqtt.broker-ip` | string | yes | Hostname or IP. |
|
|
| `mqtt.broker-port` | number | yes | MQTT port. |
|
|
| `mqtt.client-id` | string | yes | MQTT client ID. |
|
|
| `mqtt.user-name` | string | yes | MQTT username. |
|
|
| `mqtt.password` | string | yes | MQTT password. |
|
|
| `mqtt.subscribe-topic` | string | yes | Topic the device will subscribe to. |
|
|
| `mqtt.keep-alive` | number | yes | Keep-alive interval in seconds. |
|
|
|
|
Important:
|
|
|
|
- Numeric fields must be JSON numbers, not strings.
|
|
- If any required MQTT field is missing, the device treats the config as invalid.
|
|
- `app-config-version` parsing is present but commented out in firmware, so it is not currently applied.
|
|
|
|
### Error Response
|
|
|
|
```json
|
|
{
|
|
"error-code": 1001
|
|
}
|
|
```
|
|
|
|
Any non-zero `error-code` is treated as config failure by the firmware.
|
|
|
|
## 3. MQTT Payment Payload
|
|
|
|
The backend publishes this payload to `mqtt.subscribe-topic`.
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 1
|
|
},
|
|
"data": {
|
|
"pay-amount": 15000
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `header` | object | yes | Container for message metadata. |
|
|
| `header.category` | number | yes | Use `1` for payment amount playback. |
|
|
| `data` | object | yes | Container for message body. |
|
|
| `data.pay-amount` | number | yes | Must be greater than `0`. |
|
|
|
|
Behavior:
|
|
|
|
- The device formats `pay-amount` into a 12-digit string.
|
|
- Example: `15000` becomes `000000015000`.
|
|
- The device displays the amount and plays the payment audio.
|
|
|
|
Do not send `pay-amount` as a string:
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"pay-amount": "15000"
|
|
}
|
|
}
|
|
```
|
|
|
|
Use a number instead:
|
|
|
|
```json
|
|
{
|
|
"data": {
|
|
"pay-amount": 15000
|
|
}
|
|
}
|
|
```
|
|
|
|
## 3.1 MQTT Transport Security
|
|
|
|
Current firmware setting:
|
|
|
|
```c
|
|
#define MQTT_TLS_ENABLE (0)
|
|
```
|
|
|
|
This means MQTT currently connects without TLS.
|
|
|
|
The SDK and firmware code do support MQTT TLS:
|
|
|
|
- `sdk_MQTT_connect(..., tls, ...)` accepts a TLS flag.
|
|
- The firmware already passes `MQTT_TLS_ENABLE` into `sdk_MQTT_connect()`.
|
|
- Certificate setup hooks exist in firmware:
|
|
- `MQTT_SERVER_CA_CRT`
|
|
- `MQTT_CLIENT_CA_CRT`
|
|
- `MQTT_CLIENT_PRIKEY`
|
|
- `sdk_setx509cer(...)`
|
|
- `sdk_setx509_own_cer(...)`
|
|
- `sdk_setx509_prikey(...)`
|
|
|
|
To use MQTTS, firmware must be rebuilt with:
|
|
|
|
```c
|
|
#define MQTT_TLS_ENABLE (1)
|
|
```
|
|
|
|
Backend recommendation:
|
|
|
|
- For current firmware, expose plain MQTT, usually port `1883`.
|
|
- For TLS firmware, expose MQTTS, usually port `8883`, and provide the correct server CA chain expected by the firmware.
|
|
- Do not assume MQTTS is active unless the firmware was rebuilt with `MQTT_TLS_ENABLE (1)`.
|
|
|
|
## 4. MQTT OTA Trigger Payload
|
|
|
|
The backend can trigger an OTA check by publishing:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 2
|
|
},
|
|
"data": {
|
|
"fw-version": "1.0.1",
|
|
"fw-build": 2
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `header.category` | number | yes | Use `2` for OTA trigger. |
|
|
| `data.fw-version` | string | yes | Target firmware version. |
|
|
| `data.fw-build` | number | yes | Target firmware build, must be greater than `0`. |
|
|
|
|
Behavior:
|
|
|
|
- If `fw-version` is greater than the current firmware version, the device starts OTA.
|
|
- If `fw-version` is equal but `fw-build` is greater, the device starts OTA.
|
|
- Otherwise, the device logs that no update is needed.
|
|
|
|
## 5. MQTT Dynamic QR Display Payload
|
|
|
|
The backend can ask the device to display a dynamic QR code by publishing to `mqtt.subscribe-topic`:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 4
|
|
},
|
|
"data": {
|
|
"qr-url": "https://pay.example/qr/abc123",
|
|
"amount": 25000,
|
|
"expire-seconds": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `header.category` | number | yes | Use `4` for dynamic QR display. |
|
|
| `data.qr-url` | string | yes | Content encoded into the QR code. This can be a URL or any QR payload string. |
|
|
| `data.amount` | number | yes | Amount shown on the QR page. Must be greater than `0`. |
|
|
| `data.expire-seconds` | number | no | QR validity duration in seconds. Defaults to `60` if missing or invalid. |
|
|
|
|
Behavior:
|
|
|
|
- The device displays the QR code immediately after receiving a valid category `4` payload.
|
|
- The QR page shows `data.qr-url` as QR content and displays `data.amount`.
|
|
- When `expire-seconds` elapses, the device returns to the default `Bizone System` status screen.
|
|
- If a category `1` payment notification is received while the QR page is active, the device immediately hides the QR page and returns to the default status screen before playing the payment notification.
|
|
- This command only controls the display. It does not start the internal POS QR transaction flow.
|
|
|
|
## 6. MQTT Reboot Command Payload
|
|
|
|
The backend can ask the device to reboot by publishing to `mqtt.subscribe-topic`:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 5
|
|
},
|
|
"data": {
|
|
"command": "reboot"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `header.category` | number | yes | Use `5` for device reboot command. |
|
|
| `data.command` | string | yes | Must be exactly `reboot`. |
|
|
|
|
Behavior:
|
|
|
|
- The device validates `data.command` before rebooting.
|
|
- If `data.command` is missing or not exactly `reboot`, the payload is ignored.
|
|
- When valid, the device plays the reboot audio, shows `Rebooting...`, waits about 2 seconds, then restarts.
|
|
- This command is backend-to-device only.
|
|
|
|
## 7. MQTT Device Heartbeat
|
|
|
|
The firmware publishes an application-level heartbeat over MQTT after it has connected and subscribed successfully.
|
|
|
|
Publish topic used by the device:
|
|
|
|
```text
|
|
{mqtt.subscribe-topic}/heartbeat
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
soundbox/QF100123456/down/heartbeat
|
|
```
|
|
|
|
Payload:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 3
|
|
},
|
|
"data": {
|
|
"dev-sn": "QF100123456",
|
|
"client-id": "soundbox-QF100123456",
|
|
"fw-version": "1.0.0",
|
|
"fw-build": 1,
|
|
"time": "20260607123045",
|
|
"battery-level": 80,
|
|
"wifi-ap": {
|
|
"ssid": "testap1",
|
|
"mac": "00:0f:e2:4e:aa:3e",
|
|
"rssi": -13
|
|
},
|
|
"main-cell-info": {
|
|
"mcc": 460,
|
|
"mnc": 0,
|
|
"rssi": 20
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Timing:
|
|
|
|
- The device publishes one heartbeat immediately after MQTT subscribe succeeds.
|
|
- The device then publishes periodically using `mqtt.keep-alive` seconds from the config response.
|
|
- If `mqtt.keep-alive` is `0`, firmware falls back to `60` seconds.
|
|
- Current firmware synchronizes device time with NTP timezone `UTC+7`, so `data.time` represents WIB local time.
|
|
|
|
Backend handling:
|
|
|
|
- Subscribe to `{mqtt.subscribe-topic}/heartbeat`, or a wildcard such as `soundbox/+/down/heartbeat`.
|
|
- Treat each heartbeat message as the device's last-seen timestamp.
|
|
- This is separate from MQTT protocol `PINGREQ`/`PINGRESP`, which is handled by the broker and is not delivered as a subscribed message.
|
|
|
|
### Heartbeat Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `header.category` | number | yes | Always `3` for device heartbeat. |
|
|
| `data.dev-sn` | string | yes | Device serial number. |
|
|
| `data.client-id` | string | yes | MQTT client ID from config response. |
|
|
| `data.fw-version` | string | yes | Firmware version. |
|
|
| `data.fw-build` | number | yes | Firmware build number. |
|
|
| `data.time` | string | yes | Device local time in `YYYYMMDDHHMMSS` format, currently WIB/UTC+7. |
|
|
| `data.battery-level` | number | yes | Battery percentage calculated by firmware, `0` to `100`. |
|
|
| `data.wifi-ap` | object | optional | Present only when WiFi data is available. |
|
|
| `data.wifi-ap.ssid` | string | optional | Current configured WiFi SSID. |
|
|
| `data.wifi-ap.mac` | string | optional | Reserved for AP MAC/BSSID. Current stability-focused firmware does not scan AP list during heartbeat, so this is usually absent. |
|
|
| `data.wifi-ap.rssi` | number | optional | Connected WiFi RSSI from SDK. |
|
|
| `data.main-cell-info` | object | optional | Present only when GPRS/cellular data is available. |
|
|
| `data.main-cell-info.mcc` | number | optional | Parsed from IMSI when available. |
|
|
| `data.main-cell-info.mnc` | number | optional | Parsed from IMSI when available. |
|
|
| `data.main-cell-info.rssi` | number | optional | Cellular signal quality reported by the modem API. Current firmware does not expose LAC/cell-id through available SDK headers. |
|
|
|
|
Important:
|
|
|
|
- `wifi-ap` and `main-cell-info` are optional. Backend should not reject heartbeat if either object is absent.
|
|
- `wifi-ap.mac` is optional because the current SDK exposes AP MAC through scan results, not a direct "current BSSID" API. The firmware avoids WiFi scanning inside routine heartbeat to keep heartbeat stable.
|
|
- `main-cell-info.lac` and `main-cell-info.cell-id` are not sent by this firmware build because no SDK API for those values is available in this repo.
|
|
|
|
## 8. Unsupported Categories
|
|
|
|
For unsupported categories, the firmware still requires:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 99
|
|
},
|
|
"data": {}
|
|
}
|
|
```
|
|
|
|
The current firmware handles categories `1`, `2`, `4`, and `5` from backend-to-device messages. Category `3` is used by device-to-backend heartbeat. Other categories do not perform any action.
|
|
|
|
## 9. OTA Check API Response
|
|
|
|
After an OTA trigger, the device calls the update check API configured by `UPDATE_ADDR`.
|
|
|
|
The response parsed by firmware is:
|
|
|
|
```json
|
|
{
|
|
"error-code": 0,
|
|
"version": "1.0.1",
|
|
"build": 2,
|
|
"file-length": 123456,
|
|
"download-url": "https://example.com/app_fota.bin"
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Required | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `error-code` | number | yes | Must be `0` for update available. |
|
|
| `version` | string | yes | New firmware version. |
|
|
| `build` | number | yes | New firmware build. |
|
|
| `file-length` | number | yes | Must be greater than `0`. |
|
|
| `download-url` | string | yes | Firmware download URL. |
|
|
|
|
Special error codes:
|
|
|
|
- `1002`: no update needed
|
|
- `1005`: download version not found
|
|
|
|
The firmware treats both as no-update conditions.
|
|
|
|
## 10. OTA Result Upload
|
|
|
|
The device uploads OTA result to `RESULT_ADDR`.
|
|
|
|
Request body:
|
|
|
|
```json
|
|
{
|
|
"dev-sn": "DEVICE_SN",
|
|
"result": 0
|
|
}
|
|
```
|
|
|
|
The current firmware sends the request but does not parse the response body.
|
|
|
|
## 11. Recommended Topic Design
|
|
|
|
Use one downlink topic per device:
|
|
|
|
```text
|
|
soundbox/{dev-sn}/down
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
soundbox/QF100123456/down
|
|
```
|
|
|
|
Return the topic in config response:
|
|
|
|
```json
|
|
{
|
|
"error-code": 0,
|
|
"mqtt": {
|
|
"broker-ip": "broker.example.com",
|
|
"broker-port": 1883,
|
|
"client-id": "soundbox-QF100123456",
|
|
"user-name": "soundbox_user",
|
|
"password": "secret",
|
|
"subscribe-topic": "soundbox/QF100123456/down",
|
|
"keep-alive": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
Backend can listen for heartbeat on:
|
|
|
|
```text
|
|
soundbox/{dev-sn}/down/heartbeat
|
|
```
|
|
|
|
## 12. End-to-End Example
|
|
|
|
### Config Response
|
|
|
|
```json
|
|
{
|
|
"error-code": 0,
|
|
"mqtt": {
|
|
"broker-ip": "broker.bizone.id",
|
|
"broker-port": 1883,
|
|
"client-id": "soundbox-QF100123456",
|
|
"user-name": "soundbox_user",
|
|
"password": "secret",
|
|
"subscribe-topic": "soundbox/QF100123456/down",
|
|
"keep-alive": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
### Payment Publish
|
|
|
|
Publish to:
|
|
|
|
```text
|
|
soundbox/QF100123456/down
|
|
```
|
|
|
|
Payload:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 1
|
|
},
|
|
"data": {
|
|
"pay-amount": 25000
|
|
}
|
|
}
|
|
```
|
|
|
|
### OTA Trigger Publish
|
|
|
|
Publish to:
|
|
|
|
```text
|
|
soundbox/QF100123456/down
|
|
```
|
|
|
|
Payload:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 2
|
|
},
|
|
"data": {
|
|
"fw-version": "1.0.1",
|
|
"fw-build": 2
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dynamic QR Publish
|
|
|
|
Publish to:
|
|
|
|
```text
|
|
soundbox/QF100123456/down
|
|
```
|
|
|
|
Payload:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 4
|
|
},
|
|
"data": {
|
|
"qr-url": "https://pay.example/qr/abc123",
|
|
"amount": 25000,
|
|
"expire-seconds": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
### Reboot Command Publish
|
|
|
|
Publish to:
|
|
|
|
```text
|
|
soundbox/QF100123456/down
|
|
```
|
|
|
|
Payload:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 5
|
|
},
|
|
"data": {
|
|
"command": "reboot"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Heartbeat From Device
|
|
|
|
Subscribe to:
|
|
|
|
```text
|
|
soundbox/QF100123456/down/heartbeat
|
|
```
|
|
|
|
Payload received:
|
|
|
|
```json
|
|
{
|
|
"header": {
|
|
"category": 3
|
|
},
|
|
"data": {
|
|
"dev-sn": "QF100123456",
|
|
"client-id": "soundbox-QF100123456",
|
|
"fw-version": "1.0.0",
|
|
"fw-build": 1,
|
|
"time": "20260607123045",
|
|
"battery-level": 80
|
|
}
|
|
}
|
|
```
|
|
|
|
## 13. Notes For Backend Implementation
|
|
|
|
- Always publish valid JSON.
|
|
- Always use JSON numbers for numeric fields.
|
|
- Do not use strings for `category`, `pay-amount`, `amount`, `expire-seconds`, `fw-build`, `broker-port`, or `keep-alive`.
|
|
- For reboot, send `data.command` exactly as the string `reboot`.
|
|
- Keep `client-id` unique per device.
|
|
- Use the device serial number `dev-sn` as the main device identifier.
|
|
- The firmware logs MQTT payloads through CATStudio/DIAG, useful for debugging invalid payloads.
|
|
- Use MQTT broker ACLs carefully: the device must be allowed to publish to `{subscribe-topic}/heartbeat`.
|