An ESP32C3-based CDBUS (RS-485) wireless bridge with BLE and Wi-Fi support.
- Default baud rate: 115200 bps
- Maximum speed: 50 Mbps
- Default address: 0xfe
The underlying protocol is CDBUS, with the following frame format:
src, dst, len, [payload], crc_l, crc_h
Each frame includes a 3-byte header, a variable-length payload, and a 2-byte CRC (identical to Modbus CRC).
For more information on the CDBUS protocol, please refer to:
The payload is encoded using the CDNET protocol. For detailed information, please refer to:
The full device name is: CD-ESP XXXXXX
XXXXXX represents the first 3 bytes of the device MAC address.
Example: CD-ESP dc1ed5
Manufacturer Specific Data: 6-byte full device address.
Service UUID:b3340001-56ba-40b1-8ecb-8fe18dfffddd
Characteristic RX:
- UUID:
b3340002-56ba-40b1-8ecb-8fe18dfffddd - Property:
write-no-response
Characteristic TX:
- UUID:
b3340003-56ba-40b1-8ecb-8fe18dfffddd - Property:
notify
For initial setup, the device must be provisioned via BLE or RS-485.
After successfully connecting to the specified Wi-Fi access point,
the device IP address can be queried via BLE or RS-485, discovered through the local mDNS service,
or accessed directly using the cd-esp.local hostname.
Subsequent communication uses the device UDP port 52685 (0xCDCD) for data transmission and reception.
- (1): Any RS-485 node accesses the CD-ESP itself, for example to configure the network.
- (2): The response packet corresponding to command (1), or a proactively reported data packet (e.g., CD-ESP sending debug print information to the CDBUS GUI Tool).
- (3)(5): Accessing the CD-ESP itself via BLE or UDP, for example to configure the network or query status.
- (4)(6): The response packets corresponding to commands (3) and (5).
- (7)(9): BLE or UDP accesses any RS-485 bus node through the CD-ESP as a proxy.
- (8)(10): The response packets corresponding to commands (7) and (9), or proactively reported data packets from any RS-485 node forwarded by the CD-ESP.
- (13)(14): The CD-ESP actively sends commands to any RS-485 node and receives the corresponding response packets.
All interfaces are based on the CDNET L0 protocol. The CDNET packet encapsulation over BLE and UDP is as follows:
- (1): The simplest mode — raw transmission of a single CDNET packet.
S-PORT is the source port and T-PORT is the destination port, each 1 byte.
Packet size ranges from 2 to 253 bytes. - (2): Concatenation of multiple CDNET packets.
Except for the last one, all CDNET packets must be exactly 253 bytes. - (3): Full format with a WHDR header.
(The same constraints as (2) apply when concatenating multiple CDNET packets.)- WHDR (Wireless Header): 1 byte, MSB is always 1.
- A-CNT: 2 bytes, AES256 counter, optional.
- T-MAC: 1 byte, target RS-485 node address, optional.
Recommendations:
- BLE: maximum 244 or 495 bytes per transmission.
- UDP: up to 5 CDNET packets per transmission.
- When bit5 of the CDNET temporary port is 0, the communication target is the CD-ESP itself.
- When bit5 is 1, the packet is forwarded to the other end via the CD-ESP proxy.
- Command/report packets from RS485 are proxied by default when the target port is greater than 8.
When proxying is enabled:
- If the target is the RS-485 node specified by the
p_macregister, the T-MAC field is not included. - Otherwise, the T-MAC field is included to specify the target RS-485 node address.
| FIELD | DESCRIPTION |
|---|---|
| [7] | Always 1 (indicates WHDR byte) |
| [6] | a_cnt_en |
| [5] | t_mac_en |
| [4:3] | frag_type (00: no fragment, 01: first, 10: continue, 11: last) |
| [2:0] | frag_cnt (frag_type ≠ 0) or err_code (frag_type = 0) |
For fragmented packets, the frag_cnt of the first fragment may be any value; it is incremented by 1 for each subsequent fragment.
This diagram illustrates encryption with fragmentation enabled. The actual transmitted and received packets are (3)(4)(5).
The number of fragments depends on the total payload size and the fragment size.
For example, when the T-MAC field is not enabled, the WHDR values of (3)(4)(5) are: 0b11001000, 0b11010001, 0b11011010.
If encryption only is enabled, the transmitted and received packet is (2).
For example, when the T-MAC field is not enabled, the WHDR of (2) is: 0b11000000.
If fragmentation only is enabled, the fragmented data is the unencrypted plaintext.
In this case, the WHDR values of plaintext-carrying (3)(4)(5) are: 0b10001000, 0b10010001, 0b10011010.
When encryption or fragmentation is enabled:
- On decryption failure, a single-byte WHDR packet is returned with err_code = 2 (bit[6:3] = 0).
- On fragment reassembly failure, an error is reported upon receiving the last fragment as a single-byte WHDR packet with err_code = 1 (bit[6:3] = 0).
Recommendations and limitations:
- Fragmentation is recommended only when encryption is enabled over BLE.
- UDP does not support fragmentation, as UDP packets are sufficiently large and do not require it.
When a packet requires encryption:
- Enable encryption by setting
a_cnt_en= 1. Then append 2 bytes of A-CNT after the WHDR (note: separate counters are maintained for send/receive and for BLE/Wi-Fi, totaling 4 counters). - Before communication, read the plaintext
k_random(changes on each power-up).
For example, if k_random = 0xabcd1234 and the default password string is "123456", the AES256 key is derived by computing the SHA256 of the string:cd_abcd1234_123456. The IV is fixed to all zeros. - Also read
k_cnt_rx_ble/udp(defaults to 0 at startup). Upon receiving an encrypted packet, CD-ESP checks this counter;
If it doesn’t match, an error is reported. Otherwise, the counter increments automatically. - For encrypted packets, only the 1-byte WHDR remains unencrypted; everything after WHDR is encrypted with AES256-CBC using PKCS#7 padding.
- When encryption is enabled (
k_enbit0 for BLE; bit1 for Wi-Fi), registers starting fromproxy_selcan still be read in plaintext. - For BLE, when encryption is enabled, the first encrypted transaction must complete within 8 seconds after connection, or the link is terminated.
Example 1:
- BLE single transmission: 495 bytes (excluding 1-byte WHDR → 494 bytes payload)
- Aggregate 8 transmissions for one large packet → encrypted data size: 494 × 8 = 3952 bytes
- AES256 block size: 16 bytes → 3952 ÷ 16 = 247 blocks, fits exactly, no wasted bandwidth.
- Due to PKCS#7 padding, the plaintext part is 1 byte smaller (if plaintext is a multiple of 16 bytes, padding adds 16 bytes).
- With 2 bytes of A-CNT at the start, the plaintext size available for cdnet_pkt is: 3952 - 1(pad) - 2(A-CNT) = 3949 bytes (If T-MAC is enabled, subtract 1 more byte.)
- Each cdnet_pkt is up to 253 bytes: 3949 ÷ 253 = 15 × 253 + 154
- Result: 15 full 253-byte packets + 1 final 154-byte packet → 16 cdnet_pkt in total.
Example 2:
- BLE single transmission: 244 bytes, aggregate 16 transmissions for one large packet.
- Encrypted size aligns with AES256 16-byte blocks.
- Resulting cdnet_pkt division: 15 full 253-byte packets + 1 final 90-byte packet → 16 cdnet_pkt in total.
Parameter table (read/write via CDNET port #05; F: retained after power-off, !: takes effect after reboot):
| Addr | Name | Attr | Type | Default | Description |
|---|---|---|---|---|---|
| 0x0000 | magic_code | R/W | u16 | 0xcdcd | Fixed value to check if flash contains a valid register table |
| 0x0002 | conf_ver | R/W | u16 | 0x0201 | Register table version: high byte: major, low byte: minor |
| 0x0004 | conf_from | R | u8 | 0 |
0: Default 1: Flash-stored table 2: Default from p_mac onwards (major version match only) |
| 0x0005 | do_reboot | R/W | u8 | 0 | Write 2: Normal reboot |
| 0x0007 | save_conf | R/W | u8 | 0 | Write 1: Save config to flash |
| 0x0008 | dbg_en | R/W/F | u8 | 0 |
0: No debug print 1: Report debug print |
| 0x000c | bus_mac | R/W/F! | u8 | 0xfe | Default serial address |
| 0x0010 | bus_baud_l | R/W/F! | u32 | 115200 | First byte default speed |
| 0x0014 | bus_baud_h | R/W/F! | u32 | 115200 | Following bytes default speed |
| 0x0018 | bus_filter_m | R/W/F! | u8[2] | 0xff 0xff | Multicast address filter |
| 0x001a | bus_mode | R/W/F! | u8 | 1 |
0: Traditional half-duplex mode 1: Arbitration mode 2: BS mode |
| 0x001c | bus_tx_permit_len | R/W/F! | u16 | 20 |
Waiting time to allows sending (10 bits) (Time unit: 1 bit duration) |
| 0x001e | bus_max_idle_len | R/W/F! | u16 | 200 | Max idle waiting time in BS mode (10 bits) |
| 0x0020 | bus_tx_pre_len | R/W/F! | u8 | 1 |
Enable TX_EN duration before TX output (2 bits) (Ignored in Arbitration mode) |
| 0x0080 | p_mac | R/W/F | u8 | 0x10 | Predefined target MAC |
| 0x008d | k_en | R/W/F | u8 | 0x02 |
- bit0: 1 = Enable BLE encryption - bit1: 1 = Enable UDP encryption |
| 0x008e | k_pwd | R/W/F! | c8[24] | "123456" | Password string |
| 0x00a6 | ble_itvl_min | R/W/F | u8 | 6 | BLE connection interval min |
| 0x00a7 | ble_itvl_max | R/W/F | u8 | 12 | BLE connection interval max |
| 0x00a8 | wifi_ssid | R/W/F | c8[32] | "" | Target Wi-Fi SSID |
| 0x00c8 | wifi_pwd | R/W/F | c8[64] | "" | Target Wi-Fi password |
| 0x0108 | wifi_conf | R/W/F | u8 | 0 |
0: Disconnect (improves BLE speed) 1: Station mode |
| 0x0109 | proxy_sel | R/W | u8 | 1 | 1: BLE, 2: UDP |
| 0x010a | ble_stop | R/W | u8 | 0 | 1: Stop BLE advertising (improves Wi-Fi speed) |
| 0x0118 | k_st_ble | R | u8 | 0 | 1: Password verified (always 1 if BLE encryption disabled) |
| 0x011c | k_random | R | u32 | -- | Used by AES-256 encryption |
| 0x0120 | k_cnt_rx_ble | R | u16 | 0 | Counter for BLE RX encryption |
| 0x0122 | k_cnt_tx_ble | R | u16 | 0 | Counter for BLE TX encryption |
| 0x0124 | k_cnt_rx_udp | R | u16 | 0 | Counter for UDP RX encryption |
| 0x0126 | k_cnt_tx_udp | R | u16 | 0 | Counter for UDP TX encryption |
| 0x0132 | ble_mtu_cur | R | u16 | -- | BLE current connection MTU |
| 0x0134 | ble_itvl_cur | R | u8 | -- | BLE current connection interval |
| 0x0148 | wifi_state | R | u8 | 0 |
- bit0 = 1: Scanning Wi-Fi - bit1 = 1: Wi-Fi connected - bit4 = 1: Attempting Wi-Fi connection |
| 0x0149 | remote_ip | R/W | u8[16] | ff...ff |
UDP client IP (starts with ffff = invalid) Raw IP data (not string) |
| 0x015a | remote_port | R/W | u16 | 0xffff | UDP client port (0xffff = invalid) |
| 0x015c | local_ip0 | R | u8[16] | ff...ff | IPv4 address |
| 0x016c | local_ip1 | R | u8[16] | ff...ff | IPv6 link-local |
| 0x017c | local_ip2 | R | u8[16] | ff...ff | IPv6 global / other |
| 0x018c | local_ip3 | R | u8[16] | ff...ff | IPv6 global / other |
| 0x019c | scan_start | R/W | u8 | 0 | 1: Start WiFi scanning |
| 0x019d | scan_auth | R | u8[20] | 00...00 |
Scan result auth mode: 1: Open 2: WEP 3: WPA_PSK 4: WPA2_PSK 5: WPA_WPA2_PSK ... |
| 0x01b1 | scan_rssi | R | i8[20] | 7f...7f | Scan result RSSI |
| 0x01c5 | scan_ssid0 | R | c8[32] | "" | Scan result SSID |
| 0x01e5 | scan_ssid1 | R | c8[32] | "" | Scan result SSID |
| ... | ... | ... | ... | ... | ... |
| 0x0425 | scan_ssid19 | R | c8[32] | "" | Scan result SSID |
BLE connection interval range:
- Default: 6–12
- Android: Larger intervals allow higher throughput (recommended 6–36)
- iOS: Smaller intervals allow higher throughput (recommended 6–12)
Rules: Transmission window ≤ connection interval
Explanation:
- On Android, longer intervals allow larger transmission windows, letting more data packets be sent per interval, improving throughput.
- On iOS, the transmission window is fixed and small; reducing the connection interval allows more windows per unit time, compensating for the small window size.
Effect: Changes take effect without reconnection.
- If remote_ip or remote_port is invalid:
- When encryption is enabled, they are updated to the client’s IP and port upon the next encrypted communication.
- When encryption is disabled, they are updated upon the next plaintext UDP communication.
- Proxy responses are sent to the updated remote_ip/port.
- Clients should check remote_ip/port before communication:
- If invalid, or equal to the client’s own ip and port, CD-ESP is idle and ready to communicate.
- After communication, the client can reset remote_ip/port to invalid to allow other clients to connect.
- If remote_ip/port remain occupied by another client and the plaintext k_cnt_rx_udp register does not change for an extended period, it is reasonable to assume the connection has been terminated; in this case, remote_ip/port can be forcibly updated.
Based on IDF v6.0-beta1, run source esp-idf/export.sh, then execute src/idf_patchs/patch_all.sh once.
After that, enter the src directory, run idf.py set-target esp32c3 (only required the first time), and then execute idf.py build.
Firmware can be upgraded by:
- Using the CDBUS GUI tool to perform RS-485 IAP with the HEX file in the build directory.
- Running
tests/ble_ota.pyfor OTA upgrade (or OTA via UDP). - Via the USB debug port.
After reboot, CD-ESP will automatically switch to the new firmware.
