Appearance
BLE / USB CDC Command Protocol
This page describes the binary command protocol used to communicate with the iCartridge 2.0 over BLE and USB CDC interfaces. Both interfaces share the same protocol and command set.
Overview
The protocol supports three types of commands:
- READ (
?/0x3F): Request data from the device - WRITE (
!/0x21): Send data to the device (payload up to 255 bytes) - WRITE_EXTENDED (
#/0x23): Send data to the device (extended payload, up to 3000 bytes)
Responses from the device use the same packet format.
Packet Structure
Standard Commands (READ / WRITE)
| Type (1) | Group (1) | ID (1) | Payload Len (1) | Payload (0-255) | CRC (2) |Total size: 4 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) + 8 bytes (length) + payload length + 2 bytes (CRC)
Field Details
Type (1 byte)
| Value | ASCII | Description |
|---|---|---|
0x3F | ? | READ - request data from the device |
0x21 | ! | WRITE - send data to the device |
0x23 | # | WRITE_EXTENDED - send large data (> 255 bytes) |
Group (1 byte)
| Value | Group | Description |
|---|---|---|
0x01 | APP | Application commands (ping, reboot) |
0x02 | COILS | Modbus coils (read/write booleans) |
0x03 | INPUT | Modbus input registers (read-only 16-bit) |
0x04 | HOLDING | Modbus holding registers (read/write 16-bit) |
0x05 | DISCRETE | Modbus discrete inputs (read-only booleans) |
0x06 | LOGGING | Data logging control |
ID (1 byte)
Command identifier within a group. See Command Reference below.
Payload Length
- Standard commands: 1 byte (0-255)
- Extended commands: Byte 3 must be
0x00(marker), followed by 8 bytes (uint64_t, little-endian) specifying the payload size (including the 8-byte length field itself)
CRC (2 bytes)
CRC16 checksum in little-endian format, calculated over the header and payload.
CRC Calculation
Algorithm
- Standard: CRC16-CCITT (table-based)
- Initial value:
0x0000 - Polynomial:
0x1021
What to Include
- Header bytes:
[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
CRC16 C Implementation
c
const uint16_t crc16_tab[] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
/* ... full 256-entry table ... */
};
uint16_t calc_crc16(const uint8_t *buf, uint32_t len, uint16_t cksum) {
for (uint32_t i = 0; i < len; i++) {
cksum = crc16_tab[(((cksum >> 8) ^ *buf++) & 0xFF)] ^ (cksum << 8);
}
return cksum;
}Byte Order
All multi-byte integers (uint16_t, uint64_t) use little-endian byte order, including the CRC.
Timeout
The device implements a 2000ms timeout between bytes. If more than 2 seconds elapse between consecutive bytes, the command parser resets and discards the incomplete command.
Command Reference
APP Commands (Group 0x01)
| ID | Name | Type | Payload | Description |
|---|---|---|---|---|
0x01 | Ping | WRITE | None | BLE keep-alive. Must be sent at least every 5 seconds to maintain the BLE session. |
0x02 | Reboot | WRITE | None | Triggers an immediate MCU reset. |
BLE Connection Management
The device tracks BLE activity via the Ping command. If no Ping is received within 5 seconds, the BLE session is marked inactive and automatic logging is disabled. Sending a Ping re-activates the session and re-enables logging.
Modbus Register Commands (Groups 0x02-0x05)
All modbus commands use ID = 0x00.
These commands provide direct access to the same register data available via the Modbus RTU interface, but over BLE or USB CDC.
Reading Registers
Send a READ (0x3F) command with a 3-byte payload:
| offset (uint16_t LE) | len (uint8_t) |- For 16-bit groups (Input Registers
0x03, Holding Registers0x04):offsetandlenare in register units (each register = 2 bytes). Offset 0, len 2 reads registers 0 and 1 (4 bytes). - For boolean groups (Coils
0x02, Discrete Inputs0x05):offsetandlenare in byte units. Each byte represents one boolean.
The device responds with a packet containing the requested data as the payload (same type/group/id header).
Writing Registers
Send a WRITE (0x21) command with a variable-length payload:
| offset (uint16_t LE) | data (variable) |- For 16-bit groups:
offsetis in register units. - For boolean groups:
offsetis in byte units.
The data bytes are written directly to the register memory starting at the given offset. After a successful write, register values are automatically saved to flash.
WARNING
Writes are silently ignored if offset + data length exceeds the register group size. No error response is sent.
Register Group Memory Layout
Coils (Group 0x02) - 4 bytes
| Offset | Field | Default |
|---|---|---|
| 0 | temperature_auto | true |
| 1 | process_barrier_pressure_auto | true |
| 2 | solenoid_valve_1 | false (forced on boot) |
| 3 | solenoid_valve_2 | false (forced on boot) |
Discrete Inputs (Group 0x05) - 12 bytes, read-only
| Offset | Field |
|---|---|
| 0 | status_icartridge_error |
| 1 | status_icartridge_state |
| 2 | status_process_pressure_ready |
| 3 | status_barrier_fluid_pressure_ready |
| 4 | status_pump_feedback_ready |
| 5 | status_temperature_ready |
| 6 | status_pump_standby |
| 7 | status_pump_fault_blocked |
| 8 | status_pump_fault_electrical |
| 9 | status_pump_fault_warning |
| 10 | status_accelerometer_external_ready |
| 11 | status_accelerometer_onboard_ready |
Input Registers (Group 0x03) - 17 registers (34 bytes), read-only
| Reg Offset | Field | Unit |
|---|---|---|
| 0 | temperature_deci_c | deci °C |
| 1 | process_pressure_centi_bar | centibar |
| 2 | barrier_fluid_cbar | centibar |
| 3 | d_pBarrier_pProcess_centi_bar | centibar |
| 4 | pump_power_centi_watts | centiwatts |
| 5 | time_year | e.g. 2025 |
| 6 | time_month | 1-12 |
| 7 | time_day | 1-31 |
| 8 | time_hour | 0-23 |
| 9 | time_minute | 0-59 |
| 10 | time_second | 0-59 |
| 11 | accelerometer_external_x_mg | mg |
| 12 | accelerometer_external_y_mg | mg |
| 13 | accelerometer_external_z_mg | mg |
| 14 | accelerometer_onboard_x_mg | mg |
| 15 | accelerometer_onboard_y_mg | mg |
| 16 | accelerometer_onboard_z_mg | mg |
Holding Registers (Group 0x04) - 15 registers (30 bytes)
| Reg Offset | Field | Unit |
|---|---|---|
| 0 | set_point_temperature_deci_c | deci °C |
| 1 | set_point_d_process_barrier_centi_bar | centibar |
| 2 | pid_temperature_p | ×100 |
| 3 | pid_temperature_i | ×1000/s |
| 4 | pid_temperature_d | ×1000 s |
| 5 | pid_temperature_output_percent | deci % |
| 6 | deadband_process_barrier_centi_bar | centibar |
| 7 | minimum_pulse_time_centi_seconds | centiseconds |
| 8 | set_time_year | -1 = skip |
| 9 | set_time_month | -1 = skip |
| 10 | set_time_day | -1 = skip |
| 11 | set_time_hour | -1 = skip |
| 12 | set_time_minute | -1 = skip |
| 13 | set_time_second | -1 = skip |
| 14 | pressure_hysteresis_centi_bar | centibar |
Logging Commands (Group 0x06)
| ID | Name | Type | Description |
|---|---|---|---|
0x01 | Enable | WRITE | Enable automatic logging (200ms interval) |
0x02 | Disable | WRITE | Disable automatic logging |
0x03 | Log Data | READ | Device-initiated log data snapshot |
When logging is enabled and the BLE session is active, the device automatically sends Log Data packets every 200ms. Each packet contains a snapshot of all register data.
Log Data Payload Structure
| version (uint16_t LE) | coils (4 bytes) | discrete (12 bytes) | input (34 bytes) | holding (30 bytes) |- Version: Currently
1 - Total payload size: 2 + 4 + 12 + 34 + 30 = 82 bytes
The data is a raw memory copy of each register group, in the order listed above. Parse it using the memory layouts defined in Register Group Memory Layout.
Examples
Example 1: Ping (BLE Keep-Alive)
21 01 01 00 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x21 | WRITE |
| 1 | 0x01 | APP group |
| 2 | 0x01 | Ping |
| 3 | 0x00 | No payload |
| 4-5 | CRC | CRC16 of [0x21, 0x01, 0x01, 0x00] |
Example 2: Read All Input Registers
Read all 17 input registers (temperature, pressure, time, accelerometers):
3F 03 00 03 00 00 11 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x3F | READ |
| 1 | 0x03 | Input Registers group |
| 2 | 0x00 | cmd_all |
| 3 | 0x03 | Payload length = 3 |
| 4-5 | 0x00 0x00 | Offset = 0 (register units, LE) |
| 6 | 0x11 | Length = 17 (register units = 34 bytes) |
| 7-8 | CRC | CRC16 of bytes 0-6 |
Response: Device sends back a packet with type=0x3F, group=0x03, id=0x00, and 34 bytes of payload containing the raw int16_t register values in little-endian.
Example 3: Read Temperature Only
Read just the first input register (temperature):
3F 03 00 03 00 00 01 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 4-5 | 0x00 0x00 | Offset = 0 |
| 6 | 0x01 | Length = 1 register (2 bytes) |
Response payload: 2 bytes, int16_t little-endian. Example: 0xFA 0x00 = 250 = 25.0 °C.
Example 4: Write Temperature Setpoint
Set temperature setpoint to 30.0 °C (= 300 in deci °C = 0x012C):
21 04 00 04 00 00 2C 01 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x21 | WRITE |
| 1 | 0x04 | Holding Registers group |
| 2 | 0x00 | cmd_all |
| 3 | 0x04 | Payload length = 4 |
| 4-5 | 0x00 0x00 | Offset = 0 (register 40001) |
| 6-7 | 0x2C 0x01 | Value = 300 (LE) = 30.0 °C |
Example 5: Write Coil (Enable Auto Temperature)
Set coil 0 (temperature_auto) to true:
21 02 00 03 00 00 01 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x21 | WRITE |
| 1 | 0x02 | Coils group |
| 2 | 0x00 | cmd_all |
| 3 | 0x03 | Payload length = 3 (2 offset + 1 data) |
| 4-5 | 0x00 0x00 | Offset = 0 (byte units) |
| 6 | 0x01 | Value = true |
Example 6: Read Discrete Inputs (All Status Flags)
3F 05 00 03 00 00 0C [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x3F | READ |
| 1 | 0x05 | Discrete Inputs group |
| 2 | 0x00 | cmd_all |
| 3 | 0x03 | Payload length = 3 |
| 4-5 | 0x00 0x00 | Offset = 0 |
| 6 | 0x0C | Length = 12 bytes |
Response payload: 12 bytes, each byte is a boolean (0 or 1).
Example 7: Enable Logging
21 06 01 00 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x21 | WRITE |
| 1 | 0x06 | Logging group |
| 2 | 0x01 | Enable |
| 3 | 0x00 | No payload |
After this command (and while the BLE session is active via Ping), the device will begin sending Log Data packets every 200ms.
Example 8: Reboot Device
21 01 02 00 [CRC_LO] [CRC_HI]| Byte | Value | Description |
|---|---|---|
| 0 | 0x21 | WRITE |
| 1 | 0x01 | APP group |
| 2 | 0x02 | Reboot |
| 3 | 0x00 | No payload |
Triggers an immediate MCU reset. The device will disconnect and restart.
Communication Flow
Typical BLE Session
- Connect to iCartridge 2.0 BLE device
- Send Ping every 2-3 seconds to maintain the session
- Send Enable Logging to start receiving automatic data
- Receive Log Data packets every 200ms
- Send Read / Write commands as needed for configuration
- Send Disable Logging or simply stop sending Pings to end the session
USB CDC Session
- Connect via USB (device enumerates as CDC virtual COM port)
- Send Read / Write commands directly (no Ping required for USB)
- Responses are sent back over the same USB connection
TIP
Both BLE and USB CDC share the same command parser. Any command received on either interface produces a response sent to both interfaces simultaneously.
Data Persistence
- Holding Registers and Coils are saved to internal flash after every write operation
- Values persist across power cycles and reboots
- Exception: Solenoid valve coils (00003, 00004) are forced to
falseon every boot for safety
Maximum Payload Sizes
| Command Type | Max Payload |
|---|---|
| Standard (READ/WRITE) | 255 bytes |
| Extended (WRITE_EXTENDED) | 3000 bytes |