Command Protocol
This page describes the binary command protocol used to communicate with the Nimbus CloudWash Payment Panel.
Overview
The protocol supports three types of commands:
- READ (
?/ 0x3F): Request data from the device - WRITE (
!/ 0x21): Send data to the device (standard payload up to 255 bytes) - WRITE_EXTENDED (
#/ 0x23): Send data to the device (extended payload, up to 3000 bytes)
Protocol Structure
Standard Commands (READ/WRITE)
| Type (1) | Group (1) | ID (1) | Payload Len (1) | Payload (0-255) | CRC (2) |Total size: 5 bytes (header) + payload length + 2 bytes (CRC)
Extended Commands (WRITE_EXTENDED)
| Type (1) | Group (1) | ID (1) | 0x00 (1) | Payload Len (8) | Payload (variable) | CRC (2) |Total size: 4 bytes (header) + 1 byte (0x00) + 8 bytes (length) + payload length + 2 bytes (CRC)
Field Details
Type (1 byte)
| Value | ASCII | Description |
|---|---|---|
| 0x3F | ? | READ command |
| 0x21 | ! | WRITE command (standard) |
| 0x23 | # | WRITE_EXTENDED command |
Group (1 byte)
Command group identifier. Groups related commands together.
Example: 0x01 for SWITCH_TO_SCREEN commands
ID (1 byte)
Specific command identifier within a group.
Example: Screen IDs 1-7 for different screen types
Payload Length
- Standard commands: 1 byte (0-255 bytes)
- Extended commands:
- Byte 4 must be
0x00(marker for extended mode) - Followed by 8 bytes (uint64, little-endian) indicating payload size
- Byte 4 must be
Payload
Variable-length data specific to the command. See command-specific documentation for payload structure.
CRC (2 bytes)
CRC16 checksum in little-endian format.
Calculated over: [type, group, id, payload_len] + payload
For extended commands: [type, group, id, 0x00, 8-byte-length] + payload
CRC Calculation
The protocol uses CRC16 for error detection.
Algorithm
- Standard: CRC16-CCITT
- Initial value: 0x0000
- Polynomial: 0x1021
- Input reflection: No
- Output reflection: No
- Final XOR: 0x0000
What to Include
- Command header:
[type, group, id, payload_len] - Extended header (if applicable):
[8-byte payload length] - Payload data
Reference Implementations
Python: embedded_commands_python
C/C++: embedded_commands
Embedded implementation: See Lib/commands/commands.cpp:94-101
uint16_t Commands::calcCrc(Command *command) {
uint16_t crc = calc_crc16((uint8_t *) command, CMD_POS_4_PAYLOAD_START);
if (command->type == CMD_TYPE_EXTENDED) {
return calc_crc16(command->payload,
((ExtendedCommandHeader *) command->payload)->payload_size,
crc);
} else {
return calc_crc16(command->payload, command->payload_len, crc);
}
}Examples
Example 1: Switch to Screen 1 (Default Parameters)
Switch to "Select Type of Carwash" screen with default label.
Command breakdown:
- Type:
!(0x21) - WRITE - Group:
0x01- SWITCH_TO_SCREEN - ID:
0x01- SELECT_TYPE_OF_CARWASH - Payload Length:
0x00- No parameters (use defaults) - Payload: (empty)
- CRC: (calculated over
[0x21, 0x01, 0x01, 0x00])
Binary representation (hex):
21 01 01 00 FB 45The CRC16 of [0x21, 0x01, 0x01, 0x00] is 0x45FB, transmitted little-endian as FB 45.
Example 2: Switch to Screen 3 with Custom Price
Switch to "Insert or Tap Your Card" screen with custom price string "€ 25".
Command breakdown:
- Type:
!(0x21) - WRITE - Group:
0x01- SWITCH_TO_SCREEN - ID:
0x03- INSERT_OR_TAP_YOUR_CARD - Payload Length:
0x0A(10 bytes) - Payload:
- ParamHeader for PRICE_STRING (param_id=1):
param_id(uint16_t):0x01 0x00(little-endian)len(uint16_t):0x05 0x00(5 bytes for "€ 25\0")
- Data:
E2 82 AC 20 32 35 00("€ 25\0" in UTF-8)
- ParamHeader for PRICE_STRING (param_id=1):
Payload structure (10 bytes total):
01 00 05 00 E2 82 AC 20 32 35
^ ^ ^
| | └─ "€ 25" (UTF-8: E2 82 AC = €, 20 = space, 32 35 = "25")
| └─ len = 5
└─ param_id = 1 (PRICE_STRING)Full command (hex):
21 01 03 0A 01 00 05 00 E2 82 AC 20 32 35 3C C3The CRC16 of the header + payload is 0xC33C, transmitted little-endian as 3C C3.
Example 3: Switch to Screen 6 with QR Code
Switch to "Scan QR Code" screen with custom QR data.
Command breakdown:
- Type:
!(0x21) - WRITE - Group:
0x01- SWITCH_TO_SCREEN - ID:
0x06- SCAN_QR_CODE_FOR_PAYMENT_INFORMATION - Payload: QR_DATA_BYTES parameter with URL
Screen 6 parameters (from Inc/communication/commands.h:72-81):
namespace SCAN_QR_CODE_FOR_PAYMENT_INFORMATION {
static const uint8_t id = 6;
enum e_params {
LABEL_STRING = 0,
PRICE_STRING = 1,
SUBTITLE_STRING = 2,
QR_DATA_BYTES = 3, // URL for QR code
};
}Payload for custom QR code (https://nimbus-cloudwash.si/pay/12345):
The URL is 37 bytes. Note: QR_DATA_BYTES does not require null terminator since lv_qrcode_update() takes an explicit length parameter.
03 00 25 00 [37 bytes of URL data]
^ ^ ^
| | └─ URL string (37 bytes, no null terminator)
| └─ len = 0x25 (37 bytes)
└─ param_id = 3 (QR_DATA_BYTES)Full command (header + 41-byte payload + CRC):
21 01 06 29 03 00 25 00 68 74 74 70 73 3A 2F 2F 6E 69 6D 62 75 73 2D 63 6C 6F 75 64 77 61 73 68 2E 73 69 2F 70 61 79 2F 31 32 33 34 35 17 26Breakdown:
- Header:
21 01 06 29(Type=WRITE, Group=1, ID=6, PayloadLen=0x29=41) - ParamHeader:
03 00 25 00(param_id=3, len=37) - URL:
68 74 74 70...("https://nimbus-cloudwash.si/pay/12345", 37 bytes) - CRC:
17 26(CRC16=0x2617 in little-endian)
Example 4: Multiple Parameters
Switch to Screen 2 with custom label, price, and subtitle.
Parameters:
- LABEL_STRING (id=0): "CHOOSE PAYMENT"
- PRICE_SUBTITLE_STRING (id=1): "premium wash"
- PRICE_STRING (id=2): "€ 20"
Payload construction:
Parameter 1 (LABEL_STRING):
00 00 0F 00 [15 bytes: "CHOOSE PAYMENT\0"]Parameter 2 (PRICE_SUBTITLE_STRING):
01 00 0D 00 [13 bytes: "premium wash\0"]Parameter 3 (PRICE_STRING):
02 00 07 00 [7 bytes: "€ 20\0" in UTF-8]Total payload: 4 + 15 + 4 + 13 + 4 + 7 = 47 bytes (0x2F)
Full command:
21 01 02 2F 00 00 0F 00 43 48 4F 4F 53 45 20 50 41 59 4D 45 4E 54 00 01 00 0D 00 70 72 65 6D 69 75 6D 20 77 61 73 68 00 02 00 07 00 E2 82 AC 20 32 30 00 D0 ADBreakdown:
- Header:
21 01 02 2F(Type=WRITE, Group=1, ID=2, PayloadLen=0x2F=47) - Param 1:
00 00 0F 00+ "CHOOSE PAYMENT\0" (15 bytes) - Param 2:
01 00 0D 00+ "premium wash\0" (13 bytes) - Param 3:
02 00 07 00+ "€ 20\0" (7 bytes, UTF-8: E2 82 AC = €) - CRC:
D0 AD(CRC16=0xADD0 in little-endian)
Extended Command Example
For payloads larger than 255 bytes, use WRITE_EXTENDED (# / 0x23).
Structure:
23 [GROUP] [ID] 00 [PAYLOAD_LEN_8_BYTES] [PAYLOAD] [CRC]Example: Large data transfer (512 bytes)
23 01 06 00 00 02 00 00 00 00 00 00 [512 bytes] [CRC_LOW] [CRC_HIGH]
^ ^ ^ ^ ^ ^ ^
| | | | └─ 512 as uint64 LE └─ Payload └─ CRC16
| | | └─ 0x00 marker
| | └─ Command ID
| └─ Group
└─ WRITE_EXTENDEDTimeout
The device implements a 3000ms timeout between bytes. If more than 3 seconds elapse between consecutive bytes, the command parser resets and discards the incomplete command.
See Lib/commands/commands.h:6:
#define CMD_TIMEOUT_MS 3000Implementation Notes
Maximum Payload Sizes
- Standard commands: 255 bytes (
CMD_MAX_PAYLOAD_LEN) - Extended commands: 3000 bytes (
CMD_MAX_EXTENDED_PAYLOAD_LEN)
Byte Order
- All multi-byte integers (uint16_t, uint64_t) use little-endian format
- CRC is transmitted little-endian (LSB first)
Parameter Structure
When sending screen parameters, each parameter follows this structure:
struct ParamHeader {
uint16_t param_id; // Little-endian
uint16_t len; // Little-endian, length of data following this header
};
// Followed by 'len' bytes of parameter dataMultiple parameters can be concatenated in a single payload.
Related Documentation
- Screens - Available screens and their parameters
- GitHub: embedded_commands_python - Python implementation
- GitHub: embedded_commands - C/C++ implementation