Files
Qris-Soundbox/soundbox-backend-mqtt-spec.md
2026-06-08 15:56:12 +07:00

16 KiB
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.
  • QF100 reports dev-model as EDOTF1.
  • DEFAULT_LANGUAGE is 2, used here as Indonesian/IDR mode.
  • Payment and QR amounts are treated as whole rupiah, not cents.
  • The QR amount label shown on screen is Nominal: Rp <amount>.
  • The status screen shows battery percentage and network signal with dBm.

2. Device Config API

The device sends a JSON request body to CONFIG_ADDR.

Current configured endpoint:

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

{
  "dev-model": "EDOTF1",
  "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

{
  "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

{
  "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.

{
  "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.
  • In the current Indonesian/IDR firmware, the value is treated as whole rupiah.
  • Example: pay-amount: 30000 is displayed as 30000 and spoken as tiga puluh ribu rupiah.
  • The device displays the amount and plays the payment audio.

Do not send pay-amount as a string:

{
  "data": {
    "pay-amount": "15000"
  }
}

Use a number instead:

{
  "data": {
    "pay-amount": 15000
  }
}

3.1 MQTT Transport Security

Current firmware setting:

#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:

#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:

{
  "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:

{
  "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.
  • In the current Indonesian/IDR firmware, the amount is shown as Nominal: Rp <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 Device Command Payload

The backend can ask the device to run a device-level command by publishing to mqtt.subscribe-topic.

Reboot command:

{
  "header": {
    "category": 5
  },
  "data": {
    "command": "reboot"
  }
}

Power off command:

{
  "header": {
    "category": 5
  },
  "data": {
    "command": "poweroff"
  }
}

Required Fields

Field Type Required Notes
header.category number yes Use 5 for device command.
data.command string yes Supported values: reboot, poweroff.

Behavior:

  • The device validates data.command before executing the command.
  • If data.command is missing or not one of the supported values, the payload is ignored.
  • For reboot, the device plays the reboot audio, shows Rebooting..., waits about 2 seconds, then restarts.
  • For poweroff, the device plays the power-off audio, shows Shutting down..., waits about 2 seconds, then powers off.
  • 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:

{mqtt.subscribe-topic}/heartbeat

Example:

soundbox/QF100123456/down/heartbeat

Payload:

{
  "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.
  • On the device status screen, GPRS signal quality 1..31 is converted to approximate dBm with -113 + 2 * rssi before display. The heartbeat payload still sends the SDK/modem rssi value.

8. Unsupported Categories

For unsupported categories, the firmware still requires:

{
  "header": {
    "category": 99
  },
  "data": {}
}

The current firmware handles categories 1, 2, 4, and 5 from backend-to-device messages. Category 5 supports data.command values reboot and poweroff. 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:

{
  "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:

{
  "dev-sn": "DEVICE_SN",
  "result": 0
}

The current firmware sends the request but does not parse the response body.

Use one downlink topic per device:

soundbox/{dev-sn}/down

Example:

soundbox/QF100123456/down

Return the topic in config response:

{
  "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:

soundbox/{dev-sn}/down/heartbeat

12. End-to-End Example

Config Response

{
  "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:

soundbox/QF100123456/down

Payload:

{
  "header": {
    "category": 1
  },
  "data": {
    "pay-amount": 25000
  }
}

OTA Trigger Publish

Publish to:

soundbox/QF100123456/down

Payload:

{
  "header": {
    "category": 2
  },
  "data": {
    "fw-version": "1.0.1",
    "fw-build": 2
  }
}

Dynamic QR Publish

Publish to:

soundbox/QF100123456/down

Payload:

{
  "header": {
    "category": 4
  },
  "data": {
    "qr-url": "https://pay.example/qr/abc123",
    "amount": 25000,
    "expire-seconds": 60
  }
}

Reboot Command Publish

Publish to:

soundbox/QF100123456/down

Payload:

{
  "header": {
    "category": 5
  },
  "data": {
    "command": "reboot"
  }
}

Power Off Command Publish

Publish to:

soundbox/QF100123456/down

Payload:

{
  "header": {
    "category": 5
  },
  "data": {
    "command": "poweroff"
  }
}

Heartbeat From Device

Subscribe to:

soundbox/QF100123456/down/heartbeat

Payload received:

{
  "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 device commands, send data.command exactly as one of the supported strings: reboot or poweroff.
  • 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.