This document describes the serial protocol used between SimHub and ESP-SimHub devices (ESP32 / ESP8266). It covers the ARQ transport layer, the SimHub command layer, device-to-host output framing, ESP-NOW wireless framing, and the TCP/WiFi bridge.
- Overview
- Transport Layer — ARQ Serial Protocol
- Application Layer — SimHub Commands
- Device-to-Host Output Framing
- Custom Packets (Device-Initiated)
- Feature String
- Baud Rate Codes
- Session Lifecycle
- Communication Flow Examples
- ESP-NOW Wireless Protocol
- TCP/WiFi Bridge
- Constants Summary
SimHub communicates with hardware devices over a serial link (USB, WiFi virtual COM port, or ESP-NOW). The protocol has two layers:
-
Transport layer — An ARQ (Automatic Repeat Request) framing scheme that adds packet IDs and CRC-8 error detection so that corrupted or out-of-order frames can be detected and retransmitted. Implemented in
src/ArqSerial.h. -
Application layer — A simple command-response protocol carried inside ARQ packets. SimHub sends single-character commands preceded by a header byte (
0x03). The device responds with typed output frames. Implemented insrc/SHCommands.handsrc/main.cpp.
Three connection types are supported:
| Constant | Value | Description |
|---|---|---|
SERIAL |
0b00 |
USB / hardware UART |
WIFI |
0b01 |
WiFi TCP bridge (e.g. Perle TruePort) |
ESP_NOW |
0b10 |
Wireless ESP-NOW peer-to-peer |
Default baud rate: 19200.
Firmware revision identifier: 'j' (defined as VERSION in src/main.cpp).
Source: src/ArqSerial.h
Every packet sent from SimHub to the device has this layout:
+--------+--------+-----------+--------+------- - - ------+-------+
| 0x01 | 0x01 | Packet ID | Length | Data bytes | CRC8 |
| 1 byte | 1 byte | 1 byte | 1 byte | 1 – 32 bytes | 1 byte|
+--------+--------+-----------+--------+------- - - ------+-------+
| Field | Size | Description |
|---|---|---|
| Header | 2 bytes | Always 0x01 0x01. Marks the start of a packet. |
| Packet ID | 1 byte | Sequence number 0–127, then wraps to 0. 0xFF = broadcast. |
| Length | 1 byte | Number of data bytes that follow. Must be 1–32. |
| Data | 1–32 B | Payload — one or more SimHub command bytes. |
| CRC8 | 1 byte | Checksum over Packet ID + Length + all Data bytes. |
Example — Hello command:
01 01 FF 03 03 31 10 6A
└──┘ └──┘ ─── ─── └──────┘ ───
hdr hdr ID len data CRC
- Header:
01 01 - Packet ID:
FF(broadcast — Hello is always sent as broadcast) - Length:
03(3 data bytes) - Data:
03 31 10→MESSAGE_HEADER0x03, command'1'(0x31), extra byte0x10consumed and discarded byCommand_Hello - CRC8:
6A
Verified init-sequence packets (from captured traces):
| Command | Full packet bytes | ID | Data | CRC |
|---|---|---|---|---|
Hello ('1') |
01 01 FF 03 03 31 10 6A |
FF |
03 31 10 |
6A |
Features ('0') |
01 01 00 02 03 30 38 |
00 |
03 30 |
38 |
RGB LED Count ('4') |
01 01 01 02 03 34 83 |
01 |
03 34 |
83 |
TM1638 Count ('2') |
01 01 02 02 03 32 CD |
02 |
03 32 |
CD |
Simple Modules ('B') |
01 01 03 02 03 42 E3 |
03 |
03 42 |
E3 |
| X list | 01 01 04 09 03 03 03 58 6C 69 73 74 0A 51 |
04 |
03 03 03 58 6C 69 73 74 0A |
51 |
Device Name ('N') |
01 01 05 02 03 4E 7F |
05 |
03 4E |
7F |
Unique ID ('I') |
01 01 06 02 03 49 E4 |
06 |
03 49 |
E4 |
Buttons Count ('J') |
01 01 07 02 03 4A 0B |
07 |
03 4A |
0B |
| X mcutype | 01 01 08 0C 03 03 03 58 6D 63 75 74 79 70 65 0A 78 |
08 |
03 03 03 58 6D 63 75 74 79 70 65 0A |
78 |
Set Baud Rate ('8') |
01 01 09 03 03 38 0B 6C |
09 |
03 38 0B |
6C |
Gear ('G') |
01 01 0A 03 03 47 20 97 |
0A |
03 47 20 |
97 |
| X keepalive | 01 01 0B 0C 03 58 6B 65 65 70 61 6C 69 76 65 0A 11 |
0B |
03 58 6B 65 65 70 61 6C 69 76 65 0A |
11 |
Notes:
- Most simple commands carry 2 data bytes (
MESSAGE_HEADER+ command char). - Hello carries 3 bytes — the extra
0x10is consumed and discarded byCommand_Hello. - Baud rate, Gear, and similar commands carry 3 bytes — the third byte is the data argument.
- All X commands carry 12 bytes with the
03 03no-op preamble (see §3.3).
The checksum is calculated using a 256-entry CRC-8 lookup table stored in program memory
(PROGMEM). The calculation covers Packet ID, Length, and all Data bytes:
currentCrc = 0;
currentCrc = updateCrc(currentCrc, packetID);
currentCrc = updateCrc(currentCrc, length);
for (i = 0; i < length; i++) {
currentCrc = updateCrc(currentCrc, data[i]);
}where updateCrc(crc, byte) = crc_table_crc8[crc ^ byte].
The device tracks the last successfully received Packet ID in Arq_LastValidPacket
(initialised to 255 / broadcast).
The expected next Packet ID is:
nextPacketId = (Arq_LastValidPacket > 127) ? 0 : Arq_LastValidPacket + 1;
A received packet is accepted when:
- Its Packet ID equals
nextPacketId, or - Its Packet ID is
0xFF(broadcast — always accepted, sequence not advanced).
If neither condition is met the packet is silently discarded (but an ACK is still sent to keep SimHub's retransmit logic happy).
Observed init sequence from captured traces: FF → 00 → 01 → 02 → 03 → 04 → …
In the raw byte stream (particularly over ESP-NOW) null bytes (0x00) may appear between
ARQ packets because ESP-NOW transmits fixed-size buffers; unused trailing bytes are zeroed.
The ARQ receiver safely ignores any byte that is not 0x01 when looking for the header,
so these zeros are silently skipped.
After every received packet the device sends one of two responses directly on the serial stream (outside ARQ framing):
ACK — packet accepted:
03 <Packet ID>
NACK — packet rejected:
04 <Last valid Packet ID> <Reason>
Reason codes:
| Code | Meaning |
|---|---|
0x01 |
Bad / missing Packet ID |
0x02 |
Bad Length (≤ 0 or > 32) |
0x03 |
CRC byte missing / unreadable |
0x04 |
CRC mismatch |
0x05 |
Data byte(s) missing / unreadable |
| Timeout | Value | Scope |
|---|---|---|
| Per-byte read | 50 ms | Single Arq_TimedRead() call |
| Whole-read block | 400 ms | arqserial.read() — waiting for data |
Source: src/SHCommands.h, src/main.cpp
Inside the ARQ data payload SimHub uses a simple command frame:
MESSAGE_HEADER Command [Optional data bytes...]
0x03 1 byte
The device's main loop checks for MESSAGE_HEADER (0x03), reads the next byte as the
command character, then dispatches to the appropriate handler.
Multiple commands per ARQ packet. An ARQ payload can contain more than one command frame. The device loops over the DataBuffer consuming command frames one at a time until the buffer is empty.
Unrecognised command bytes are silently discarded. If the byte following MESSAGE_HEADER
does not match any case in the switch statement the device simply continues to the next
iteration. This is observable in the X command packet (see §3.3) where the payload starts
with a 03 03 pair (header + 0x03 command, which has no handler) followed by the real
command frame.
| Hex | Char | Handler | Device Response |
|---|---|---|---|
0x31 |
'1' |
Command_Hello |
08 6A — firmware version byte (e.g. 'j') |
0x30 |
'0' |
Command_Features |
One 06 01 XX 20 frame per feature code, then 06 01 0A 20 (see §6) |
0x34 |
'4' |
Command_RGBLEDSCount |
08 <count> |
0x32 |
'2' |
Command_TM1638Count |
08 <count> |
0x42 |
'B' |
Command_SimpleModulesCount |
08 <count> |
0x41 |
'A' |
Command_Acq |
08 03 — keep-alive ACK byte |
0x4E |
'N' |
Command_DeviceName |
06 <len> <name> 20 then 06 01 0A 20 |
0x49 |
'I' |
Command_UniqueId |
06 <len> <mac> 20 then 06 01 0A 20 |
0x4A |
'J' |
Command_ButtonsCount |
08 <count> |
0x58 |
'X' |
Extended command dispatch | See §3.3 |
0x33 |
'3' |
Command_TM1638Data |
(none — reads display data from stream) |
0x56 |
'V' |
Command_Motors |
See §3.4 |
0x53 |
'S' |
Command_7SegmentsData |
(none — reads display data from stream) |
0x36 |
'6' |
Command_RGBLEDSData |
08 15 after processing |
0x52 |
'R' |
Command_RGBMatrixData |
08 15 after processing |
0x4D |
'M' |
Command_MatrixData |
(none) |
0x47 |
'G' |
Command_GearData |
(none — gear char is 3rd byte of ARQ data) |
0x4C |
'L' |
Command_I2CLCDData |
(none) |
0x4B |
'K' |
Command_GLCDData |
(none — OLED / Nokia LCD) |
0x50 |
'P' |
Command_CustomProtocolData |
08 15 after processing |
0x38 |
'8' |
Command_SetBaudrate |
(none — baud rate code is 3rd byte of ARQ data, see §7) |
When command byte is 'X', the device reads a space- or newline-terminated ASCII string
from the stream and dispatches on it:
0x03 'X' <subcommand string> ' ' or '\n'
In practice SimHub packs the X command inside a 9-byte ARQ payload that begins with an
extra 03 03 no-op pair before the real command frame:
ARQ data: 03 03 03 58 6C 69 73 74 0A
└──┘ └──┘ └──────────┘ └─ '\n' terminator
no-op 'X' "list"
The 03 03 bytes are processed first (header + unrecognised command 0x03, discarded),
then 03 58 dispatches 'X' with subcommand "list\n".
| Sub-command string | Handler | Response | Always in X list? |
|---|---|---|---|
list |
Command_ExpandedCommandsList |
String frame per sub-command + 08 0A end |
— (meta command) |
mcutype |
Command_MCUType |
08 1E, 08 98, 08 01 (three single-byte frames) |
Yes |
keepalive |
(no handler) | ACK only — connection heartbeat | Yes |
tach |
Command_TachData |
Tachometer data (if enabled) | If tachometer enabled |
speedo |
Command_SpeedoData |
Speedometer data (if enabled) | If speedo enabled |
boost |
Command_BoostData |
Boost gauge data (if enabled) | If boost enabled |
temp |
Command_TempData |
Temperature gauge data (if enabled) | If temp enabled |
fuel |
Command_FuelData |
Fuel gauge data (if enabled) | If fuel enabled |
cons |
Command_ConsData |
Consumption gauge data (if enabled) | If cons enabled |
encoderscount |
Command_EncodersCount |
1-byte encoder count (if enabled) | If encoders enabled |
keepalive and mcutype are always emitted by Command_ExpandedCommandsList regardless
of device configuration. keepalive has no handler in the device firmware — SimHub sends
it continuously during the data streaming phase as a heartbeat; the device returns only
the ARQ ACK.
MCU signature bytes (emulating Arduino Mega ATmega2560):
| Byte | Value |
|---|---|
SIGNATURE_0 |
0x1E |
SIGNATURE_1 |
0x98 |
SIGNATURE_2 |
0x01 |
The 'V' command reads one additional byte to determine the motor sub-operation:
| Sub-byte | Meaning | Response |
|---|---|---|
'C' |
Count / capability | 0xFF, motor count byte, provider name strings + \n |
'S' |
Set motor values | (none — reads motor data bytes from stream) |
All data sent back to SimHub is prefixed with a type marker byte. These frames are sent outside the ARQ packet wrapper.
0x08 <byte>
Used for single-byte responses: counts, version character, ACK values. Examples from captured traces:
| Response | Bytes | Meaning |
|---|---|---|
| Hello version | 08 6A |
Firmware version 'j' (0x6A) |
| RGB LED count | 08 00 |
0 RGB LEDs configured |
| Command ACK | 08 03 |
Command_Acq response (0x03) |
0x06 <length> <string bytes> 0x20
lengthincludes the\nbyte whenPrintLnvariants are used.0x20(space) is the frame terminator.- Each call to
FlowSerialPrint()/FlowSerialPrintLn()produces exactly one frame. Multi-character strings are sent as one frame; single characters produce a 4-byte frame.
Example — one feature code 'G' (0x47):
06 01 47 20
0x07 <length> <string bytes> 0x20
Same structure as a string frame. Used for DebugPrintLn / DebugPrint calls.
0x09 <packet type> <length> <data bytes>
Sent asynchronously by the device for encoder movements, button events, etc. (see §5).
The device can push events to SimHub at any time using arqserial.CustomPacketStart().
Sent when a rotary encoder position changes (direction 0 or 1):
0x09 0x01 0x03 <encoderId> <direction> <position>
| Byte | Description |
|---|---|
0x09 |
Custom packet marker |
0x01 |
Packet type — encoder move |
0x03 |
Length = 3 |
| encoderId | 1-based encoder index |
| direction | 0 = clockwise, 1 = counter-CW |
| position | Absolute encoder position |
Sent when encoder button is pressed/released (direction ≥ 2):
0x09 0x02 0x02 <encoderId> <buttonState>
| Byte | Description |
|---|---|
0x09 |
Custom packet marker |
0x02 |
Packet type — encoder button |
0x02 |
Length = 2 |
| encoderId | 1-based encoder index |
| buttonState | direction − 2 (button press/release) |
Sent when a standalone button changes state:
0x09 0x03 0x02 <buttonId> <status>
| Byte | Description |
|---|---|
0x09 |
Custom packet marker |
0x03 |
Packet type — button event |
0x02 |
Length = 2 |
| buttonId | 1-based button index |
| status | 1 = pressed, 0 = released |
Sent when a TM1638 module button changes state:
0x09 0x04 0x03 <moduleIndex> <buttonIndex> <status>
| Byte | Description |
|---|---|
0x09 |
Custom packet marker |
0x04 |
Packet type — TM1638 button |
0x03 |
Length = 3 |
| moduleIndex | 1-based TM1638 module index |
| buttonIndex | 1-based button index (1–8) |
| status | 1 = pressed, 0 = released |
In response to command '0', the device sends each feature code as a separate string
frame (0x06 marker), one character per frame. The sequence is terminated by a final
frame containing only '\n'. There is no single combined frame for the whole string.
Each frame follows the standard string format:
0x06 0x01 <feature char> 0x20
Observed response for a minimal device (G, N, I, J, P, X features):
06 01 47 20 → 'G'
06 01 4E 20 → 'N'
06 01 49 20 → 'I'
06 01 4A 20 → 'J'
06 01 50 20 → 'P'
06 01 58 20 → 'X'
06 01 0A 20 → '\n' (terminator)
Feature codes and the conditions under which each is emitted:
| Code | Feature | Condition |
|---|---|---|
M |
LED matrix | MAX7221/HT16K33 matrix enabled |
L |
I²C LCD | I2CLCD_enabled == 1 |
K |
OLED or Nokia LCD | OLED or Nokia LCD count > 0 |
G |
Gear display | Always sent |
N |
Named device (Command_DeviceName supported) |
Always sent |
I |
Unique ID (Command_UniqueId supported) |
Always sent |
J |
Button count query supported | Always sent |
P |
Custom protocol supported | Always sent |
X |
Extended commands supported | Always sent |
R |
RGB matrix | WS2812B/DM163/Sunfounder matrix > 0 |
V |
Vibration / motor (ShakeIt) | Any motor driver enabled |
The codes are emitted in source order: M, L, K, G, N, I, J, P, X,
optionally R, optionally V, then \n.
Command '8' reads a 1-byte code from the stream and changes the serial baud rate:
| Code | Baud Rate |
|---|---|
| 1 | 300 |
| 2 | 1200 |
| 3 | 2400 |
| 4 | 4800 |
| 5 | 9600 |
| 6 | 14400 |
| 7 | 19200 (default) |
| 8 | 28800 |
| 9 | 38400 |
| 10 | 57600 |
| 11 | 115200 |
| 12 | 230400 |
| 13 | 250000 |
| 14 | 1000000 |
| 15 | 2000000 |
| 16 | 200000 |
| 17 | 500000 |
The device applies a 200 ms delay before switching baud rates to allow the host to follow.
Every time SimHub connects to a device it runs a fixed sequence of queries before it starts sending game data. The sequence has four phases, always in this order:
SimHub opens the connection by sending a Hello packet using the broadcast Packet ID
(0xFF) so it is always accepted regardless of device state.
SimHub → 01 01 FF 03 03 31 10 6A Hello ('1'), broadcast
Device → 03 FF ACK
Device → 08 6A Single byte: firmware version 'j'
SimHub retransmits Hello repeatedly (always with ID=FF) until it receives a clean ACK
and version byte. NACKs during this phase (04 FF 04, 04 FF 05, 04 FF 03) are normal
and occur when a packet is split across receive buffers.
Once Hello succeeds SimHub runs a fixed sequence of queries, each on its own sequential Packet ID, to discover what the device supports and how many of each peripheral exist. The exact order observed in captured traces:
| Packet ID | Command | ARQ data bytes | Query | Device response |
|---|---|---|---|---|
00 |
'0' |
03 30 |
Supported features | Feature chars, one 06 01 XX 20 frame each, then 06 01 0A 20 |
01 |
'4' |
03 34 |
RGB LED count | 08 <count> |
02 |
'2' |
03 32 |
TM1638 module count | 08 <count> |
03 |
'B' |
03 42 |
Simple 7-seg module count | 08 <count> |
04 |
'X' "list\n" |
03 03 03 58 6C 69 73 74 0A |
Extended command list | String frame per sub-command + 08 0A terminator |
05 |
'N' |
03 4E |
Device name | 06 <len> <name> 20 then 06 01 0A 20 |
06 |
'I' |
03 49 |
Unique ID (MAC address) | 06 <len> <mac> 20 then 06 01 0A 20 |
07 |
'J' |
03 4A |
Total button count | 08 <count> |
08 |
'X' "mcutype\n" |
03 03 03 58 6D 63 75 74 79 70 65 0A |
MCU signature | 08 1E, 08 98, 08 01 (three separate frames) |
If 'V' (motors) is in the feature string, SimHub also sends 'V' 'C' to query the motor
count and provider names. Additional 'X' sub-commands (tach, speedo, boost, temp,
fuel, cons, encoderscount) are queried only if the device reported them in the
X list response.
X list response format. Each supported sub-command is sent as a string-with-newline
frame, then terminated with a single-byte '\n':
06 08 6D 63 75 74 79 70 65 0A 20 → "mcutype\n" (always present)
06 0A 6B 65 65 70 61 6C 69 76 65 0A 20 → "keepalive\n" (always present)
08 0A → '\n' end-of-list marker
MCU type response. Three separate single-byte frames, one per signature byte:
08 1E → SIGNATURE_0 (0x1E)
08 98 → SIGNATURE_1 (0x98)
08 01 → SIGNATURE_2 (0x01)
Device name / Unique ID response. The value is sent as a single string frame, then a
separate '\n' frame:
06 <len> <value bytes> 20 → value string
06 01 0A 20 → '\n' terminator
After enumeration SimHub sends the '8' command with its preferred baud rate code (see §7).
The baud rate code is packed as a third byte directly in the ARQ payload:
ARQ data: 03 38 <code>
│ │ └─ baud rate code byte (e.g. 0x0B = 11 = 115200)
│ └─ '8' (0x38)
└─ MESSAGE_HEADER
Observed example (115200 baud, code 11 = 0x0B):
SimHub → 01 01 09 03 03 38 0B 6C Set baud rate to 115200
Device → 03 09 ACK
The device applies a 200 ms delay, then switches. SimHub follows on its side. All subsequent traffic runs at the new baud rate.
If SimHub is satisfied with 19200 (the default) this step is skipped entirely.
Streaming starts immediately after baud rate negotiation. The first data packet observed
is always 'G' (gear), with the gear character packed inline:
ARQ data: 03 47 20
│ │ └─ gear char (0x20 = space = no gear selected)
│ └─ 'G'
└─ MESSAGE_HEADER
SimHub then sends X keepalive on every subsequent Packet ID indefinitely as a
connection heartbeat. The device has no handler for keepalive — it simply returns,
and the ARQ layer sends back the ACK. This is how SimHub detects connection loss.
Interspersed with keepalives, SimHub sends data update commands whenever game state changes:
| Command | Peripheral updated | Device reply |
|---|---|---|
'6' |
RGB LEDs | 08 15 |
'R' |
RGB matrix | 08 15 |
'P' |
Custom protocol | 08 15 |
'3' |
TM1638 displays + LEDs | (none) |
'S' |
7-segment displays | (none) |
'G' |
Gear indicator | (none) |
'L' |
I²C LCD | (none) |
'K' |
OLED / Nokia LCD | (none) |
'M' |
LED matrix | (none) |
'V' 'S' |
Motors / ShakeIt | (none) |
X keepalive |
(heartbeat) | ACK only |
During streaming the device may push unsolicited custom packets to SimHub whenever a button is pressed or an encoder is turned (see §5).
SimHub Device
| |
|--[01 01 FF 03 03 31 10 6A]----------->| ARQ packet ID FF, data: 03 '1' 0x10
| |
|<--[03 FF]-------------------------------| ACK: Packet ID FF accepted
|<--[08 6A]-------------------------------| Single-byte: firmware version 'j' (0x6A)
| |
The 0x10 in the ARQ data is consumed and discarded by Command_Hello before the
version byte is sent.
SimHub Device
| |
|--[01 01 00 02 03 30 38]-------------->| ARQ packet ID 00, data: 03 '0', CRC 38
| |
|<--[03 00]-------------------------------| ACK: Packet ID 00 accepted
|<--[06 01 47 20]-------------------------| 'G'
|<--[06 01 4E 20]-------------------------| 'N'
|<--[06 01 49 20]-------------------------| 'I'
|<--[06 01 4A 20]-------------------------| 'J'
|<--[06 01 50 20]-------------------------| 'P'
|<--[06 01 58 20]-------------------------| 'X'
|<--[06 01 0A 20]-------------------------| '\n' terminator
| |
Each feature code is a separate 4-byte string frame. No single combined frame is sent.
SimHub Device
| |
|--[01 01 01 02 03 34 83]-------------->| ARQ packet ID 01, data: 03 '4', CRC 83
| |
|<--[03 01]-------------------------------| ACK: Packet ID 01 accepted
|<--[08 00]-------------------------------| Single-byte: 0 RGB LEDs
| |
SimHub Device
| |
|--[ARQ: 03 '6' <RGB data>]-------------->|
| |
|<--[03 <ID>]------------------------------| ACK
|<--[08 15]--------------------------------| Single-byte ACK 0x15 (data processed)
| |
SimHub Device
| |
| button pressed |
|<--[09 03 02 01 01]----------------------| Custom packet: button 1 pressed
| |
The following NACK reason codes have been observed in captured traces:
04 FF 04 CRC mismatch on broadcast packet
04 FF 05 Incomplete data on broadcast packet
04 FF 03 Missing CRC byte on broadcast packet
04 01 04 CRC mismatch on packet ID 01
04 01 02 Bad length on packet ID 01
04 01 05 Incomplete data on packet ID 01
Example retransmit sequence:
SimHub Device
| |
|--[01 01 01 02 03 34 FF]-------------->| Bad CRC (should be 83)
| |
|<--[04 00 04]----------------------------| NACK: last valid=0x00, reason=0x04 (CRC)
| |
|--[01 01 01 02 03 34 83]-------------->| Retransmit with correct CRC
| |
|<--[03 01]-------------------------------| ACK
| |
Source: lib/ESPNowSerialProtocol/EspNowProtocol.h, lib/ESPNowSerialBridge/ESPNowSerialBridge.h
ESP-NOW uses a two-device setup:
- Bridge device — connected to the computer via USB. Forwards serial data from SimHub to the feature device over ESP-NOW, and returns responses.
- Feature device — runs the full SimHub firmware. Receives data via ESP-NOW and processes it exactly as if it were on a direct serial port.
Each ESP-NOW transmission carries one EspNowMessage struct:
typedef struct __attribute__((packed)) EspNowMessage {
int version; // MESSAGE_VERSION = 1
int length; // Actual bytes used in simHubBytes
char simHubBytes[MAX_SIMHUB_BYTES]; // Up to 32 bytes of SimHub ARQ data
char bridgeBytes[MAX_BRIDGE_BYTES]; // Up to 8 bytes of bridge control data
} EspNowMessage;| Field | Size | Description |
|---|---|---|
version |
4 bytes | Protocol version, currently 1 |
length |
4 bytes | Number of valid bytes in simHubBytes |
simHubBytes |
32 bytes | SimHub ARQ packet data |
bridgeBytes |
8 bytes | Bridge control commands (see §9.2) |
Bridge commands are carried in bridgeBytes using this frame:
COMMAND_HEADER <length> <command> <data bytes…> COMMAND_END
0x03 1 byte 1 byte 0–4 bytes 0x0A
| Constant | Value |
|---|---|
COMMAND_HEADER |
0x03 |
COMMAND_END |
0x0A |
Supported bridge commands:
| Command byte | Code | Description |
|---|---|---|
'8' |
0x38 |
Change baud rate — same codes as §7. 3-byte value. |
'I' |
0x49 |
Ping — keep-alive from bridge to feature device |
'O' |
0x4F |
Pong — response to Ping |
Example — Baudrate command (19200):
03 03 38 00 4B 00 0A
│ │ │ └──────┘ │
│ │ │ 19200LE └─ end
│ │ └─ '8'
│ └─ length = 3
└─ header
Source: lib/TcpSerialBridge2/TcpSerialBridge2.h
The WiFi bridge creates a transparent TCP socket on port 10001 (default). Perle TruePort
or similar software on the SimHub host creates a virtual COM port that forwards to this
socket, making the wireless device appear as a regular serial port to SimHub.
Configuration in src/main.cpp:
#define CONNECTION_TYPE WIFI
#define BRIDGE_PORT 10001
#define USE_HARDCODED_CREDENTIALS false // or true + WIFI_SSID / WIFI_PASSWORDNo changes to the SimHub command or ARQ layers are needed — the TCP bridge is transparent.
| Constant / Symbol | Value | Source |
|---|---|---|
| ARQ header byte (×2) | 0x01 |
ArqSerial.h |
| ACK byte | 0x03 |
ArqSerial.h |
| NACK byte | 0x04 |
ArqSerial.h |
| Max packet data length | 32 bytes | ArqSerial.h |
| Broadcast Packet ID | 0xFF |
ArqSerial.h |
| Per-byte read timeout | 50 ms | ArqSerial.h |
| Block read timeout | 400 ms | ArqSerial.h |
| Constant | Value | Meaning |
|---|---|---|
| Single-byte write marker | 0x08 |
1-byte output |
| String write marker | 0x06 |
String output |
| Debug print marker | 0x07 |
Debug string output |
| Custom packet marker | 0x09 |
Device-initiated pkt |
| String frame terminator | 0x20 |
End of string frame |
| Constant | Value | Source |
|---|---|---|
MESSAGE_HEADER |
0x03 |
SHCommands.h |
VERSION |
'j' |
main.cpp |
SIGNATURE_0 |
0x1E |
EspSimHub.h |
SIGNATURE_1 |
0x98 |
EspSimHub.h |
SIGNATURE_2 |
0x01 |
EspSimHub.h |
| Initial baud rate | 19200 | main.cpp |
| Constant | Value | Source |
|---|---|---|
MESSAGE_VERSION |
1 |
EspNowProtocol.h |
MAX_SIMHUB_BYTES |
32 | EspNowProtocol.h |
MAX_BRIDGE_BYTES |
8 | EspNowProtocol.h |
COMMAND_HEADER |
0x03 |
EspNowProtocol.h |
COMMAND_END |
0x0A |
EspNowProtocol.h |
BAUDRATE_COMMAND |
'8' |
EspNowProtocol.h |
PING_COMMAND |
'I' |
EspNowProtocol.h |
PONG_COMMAND |
'O' |
EspNowProtocol.h |