Skip to content

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)

ValueASCIIDescription
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

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

  1. Command header: [type, group, id, payload_len]
  2. Extended header (if applicable): [8-byte payload length]
  3. Payload data

Reference Implementations

Python: embedded_commands_python

C/C++: embedded_commands

Embedded implementation: See Lib/commands/commands.cpp:94-101

cpp
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 45

The 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)

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 C3

The 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):

cpp
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 26

Breakdown:

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

  1. LABEL_STRING (id=0): "CHOOSE PAYMENT"
  2. PRICE_SUBTITLE_STRING (id=1): "premium wash"
  3. 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 AD

Breakdown:

  • 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_EXTENDED

Timeout

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:

cpp
#define CMD_TIMEOUT_MS 3000

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

cpp
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 data

Multiple parameters can be concatenated in a single payload.