NRSuite Wire Protocol Specification¶
Wire major: 1.0 Latest additive revision: proto_minor 2 (pull-based scan results) Next draft major/minor: 1.1 (Mesh Foundation) Status: Stable (beta)
This document is the authoritative reference for the NRSuite Android app and ESP32 firmware transport.
Physical Transport¶
| Parameter | Value |
|---|---|
| Interface | USB serial (CDC-ACM or CH34x/CP210x/FTDI via USB-OTG) |
| Baud rate | 115200 |
| Data bits | 8 |
| Parity | None |
| Stop bits | 1 |
| Flow control | None |
The transport is a raw byte stream. Framing and resynchronization are handled by this protocol.
Frame Format¶
Every message is an 8-byte header followed by a payload.
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | Magic byte 0: 0xAD |
| 1 | 1 | Magic byte 1: 0xDE |
| 2 | 1 | Frame type |
| 3 | 1 | Frame ID |
| 4 | 4 | Payload length, little-endian uint32 |
| 8 | N | Payload (0..1024 bytes) |
Constants¶
| Name | Value |
|---|---|
MAGIC_0 |
0xAD |
MAGIC_1 |
0xDE |
HEADER_SIZE |
8 bytes |
MAX_PAYLOAD_SIZE |
1024 bytes |
Frame IDs¶
- Command IDs are allocated per sender, starting at
0x01, wrapping back to0x01after0xFF. - A
RESPONSEcarries the same frame ID as theCOMMANDit answers. - PCAP chunk IDs use the same 1-byte field and are acknowledged by
ACKframes. - ID
0x00is reserved for uncorrelated frames; currently used for app-to-firmware PCAP ACKs.
Resynchronization¶
The decoder scans for 0xAD 0xDE. If a frame is invalid (unknown type, payload longer than 1024 bytes, truncated), it discards one byte and scans forward for the next magic pair.
Frame Types¶
| Name | Code | Direction | Description |
|---|---|---|---|
COMMAND |
0x01 |
App -> Firmware | JSON command with optional arguments |
RESPONSE |
0x02 |
Firmware -> App | Response to a command, same frame ID |
EVENT |
0x03 |
Firmware -> App | Async notification |
PCAP |
0x04 |
Firmware -> App | Raw PCAP chunk |
ACK |
0x05 |
App -> Firmware | Acknowledge a PCAP chunk |
HTML |
0x06 |
App -> Firmware | Reserved/legacy HTML upload frame. Current app uses the RESET_HTML and SET_HTML_CHUNK commands instead. |
Session Lifecycle¶
App Firmware
| |
|-- COMMAND (id=1, cmd="PING") -----------> |
|<- RESPONSE (id=1, {"ok":true}) ---------- |
| |
|-- COMMAND (id=2, cmd="STATUS") ---------> |
|<- RESPONSE (id=2, {chip, fw, features}) - |
| |
| session active |
| |
|-- COMMAND (id=N, cmd="...") -------------> |
|<- RESPONSE (id=N, {...}) ----------------- |
| |
|<- EVENT (type="...") --------------------- | async, any time
| |
|<- PCAP (id=chunk, raw bytes) ------------- | streaming capture
|-- ACK (id=0, {"chunk":chunk}) ----------> |
Connection establishment¶
- App opens the USB serial port.
- App sends
PINGwith a short timeout. - App sends
STATUSand reads chip, firmware version, device ID, and feature flags. - Session is considered active after a valid
STATUSresponse.
Timeouts¶
| Operation | Timeout |
|---|---|
PING |
1500 ms (retried by app during reconnect) |
STATUS |
5000 ms |
| Default command | 8000 ms |
| Long-running commands | specified per command |
Disconnection¶
A USB read/write error is treated as a normal disconnect. Either side can close the port at any time.
JSON Payloads¶
All COMMAND, RESPONSE, EVENT, and ACK payloads are UTF-8 JSON. PCAP and legacy HTML payloads are raw bytes.
COMMAND¶
RESPONSE¶
Failures may include a message:
EVENT¶
Events use a type field:
Important: the implementation uses
type, notevent.
PCAP¶
Raw PCAP bytes. The firmware uses a sliding window and the app sends an ACK after each accepted chunk.
ACK¶
chunk is the frame ID of the PCAP chunk being acknowledged. ACK frames use frame ID 0x00.
STATUS Response¶
STATUS is the main capability handshake.
{
"ok": true,
"uptime": 123456,
"heap": 123456,
"heap_total": 327680,
"heap_min_free": 98765,
"heap_max_alloc": 110592,
"psram_total": 8388608,
"psram_free": 8000000,
"chip": "ESP32-S3",
"chip_cores": 2,
"cpu_mhz": 240,
"temp_c": 48.6,
"flash_size": 4194304,
"sdk": "v5.5.0",
"proto": 1,
"proto_minor": 2,
"fw": "1.0.0-beta.2",
"device_id": "NR1A2B3C4D",
"country": "US",
"allowed_2ghz_channels": [1,2,3,4,5,6,7,8,9,10,11],
"allowed_5ghz_channels": [36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144,149,153,157,161,165],
"features": ["wifi", "sniff", "deauth", "beacon", "portal"],
"sniffing": false,
"client_detecting": false,
"portal": false,
"beacon": false,
"deauth_detector": false,
"hidden_ap": false,
"ble_scanning": false,
"ble_profiling": false
}
| Field | Type | Description |
|---|---|---|
ok |
bool | Always present |
uptime |
uint32 | Firmware uptime in milliseconds |
heap |
uint32 | Free heap in bytes |
heap_total |
uint32? | Total allocator heap size in bytes. Additive optional field |
heap_min_free |
uint32? | Minimum free heap observed since boot. Additive optional field |
heap_max_alloc |
uint32? | Largest currently allocatable heap block in bytes. Additive optional field |
psram_total |
uint32? | PSRAM size in bytes; 0 when PSRAM is absent. Additive optional field |
psram_free |
uint32? | Free PSRAM in bytes; 0 when PSRAM is absent. Additive optional field |
chip |
string | ESP32, ESP32-C2, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, or ESP32-C6 |
chip_cores |
uint8? | Number of CPU cores. Additive optional field |
cpu_mhz |
uint16? | Current CPU frequency in MHz. Additive optional field |
temp_c |
float? | Internal chip/die temperature in Celsius. Additive optional field; omitted when the sensor is unavailable |
flash_size |
uint32? | Flash size in bytes. Additive optional field |
sdk |
string? | ESP-IDF/Arduino SDK version string. Additive optional field |
proto |
int | Protocol major version. Currently 1 |
proto_minor |
int | Protocol minor version. Omitted means 0; mesh builds report 1; pull-based scan result builds report 2 |
fw |
string | Firmware version |
device_id |
string | Persistent NRxxxxxxxx ID from NVS (10 characters total) |
country |
string | Active Wi-Fi regulatory country, e.g. 01, US, GB |
allowed_2ghz_channels |
int[] | 2.4 GHz channels currently permitted by the active regulatory country |
allowed_5ghz_channels |
int[] | 5 GHz channels currently permitted; empty when 5 GHz is unavailable |
Device telemetry fields are additive and optional. Clients must tolerate their
absence when connected to older firmware. Values reported in STATUS are a
snapshot; clients that display live uptime/heap should re-request STATUS
periodically rather than extrapolating from the initial handshake.
| features | string[] | Compiled feature flags |
| sniffing | bool | Sniffer active |
| client_detecting | bool | Client detector active |
| oversized_frames | uint32 | Count of oversized frames observed by firmware |
| channel | int | Current sniffer channel, when sniffing |
| portal | bool | Captive portal active |
| beacon | bool | Beacon spam active |
| deauth_detector | bool | Deauth detector active |
| hidden_ap | bool | Hidden AP detector active |
| ble_scanning | bool | BLE scanner active |
| ble_profiling | bool | BLE GATT profiler active |
| beacon_ssids | int | Beacon SSID count, when active |
| beacon_sent | uint32 | Beacon frames sent, when active |
| beacon_channel | int | Beacon channel, when active |
| deauth_detector_channel | int | Deauth detector channel, when active |
| deauth_detector_hopping | bool | Deauth detector hopping mode |
| deauth_detected | uint32 | Deauth frames detected |
| deauth_detector_sent | uint32 | Events sent to app |
| deauth_detector_dropped | uint32 | Events dropped by firmware |
| hidden_ap_channel | int | Hidden AP detector channel |
| hidden_ap_hopping | bool | Hidden AP detector hopping mode |
| hidden_ap_seen | uint32 | Hidden APs seen |
| hidden_ap_candidates | uint32 | Candidate SSIDs seen |
| hidden_ap_resolved | uint32 | SSIDs resolved |
| hidden_ap_sent | uint32 | Events sent |
| hidden_ap_dropped | uint32 | Events dropped |
HEAP is an alias for STATUS.
Feature flags¶
| Flag | Meaning |
|---|---|
wifi |
Wi-Fi scan |
sniff |
Packet sniffer |
client_detect |
Client/presence detector |
deauth |
Deauth injection |
deauth_multi |
Firmware-native multi-target deauth with per-target channel scheduling |
deauth_detect |
Deauth detector |
beacon |
Beacon spam |
portal |
Captive portal / Evil Twin AP |
portal_html_offset |
HTML chunks support offset-based upload; current portals are file-backed (FAT on S2/S3, LittleFS on C3/C5/classic) |
html_diag |
HTML upload diagnostic events |
stop_all |
STOP_ALL command |
wps |
WPS flag included in scan_ap |
scan_results_pull |
Firmware retains a bounded SCAN_WIFI/WPS_LOCK_SCAN snapshot and supports SCAN_RESULTS chunk retrieval with CRC32 verification |
wifi_2ghz |
2.4 GHz Wi-Fi band is supported |
wifi_5ghz |
5 GHz Wi-Fi band is enabled in this firmware build (advertised by C5 builds) |
psram |
Internal heap is accompanied by initialized PSRAM; psram_total and psram_free are meaningful |
temp_sensor |
Firmware can report internal chip/die temperature as optional temp_c |
hidden_ap |
Hidden AP revealer |
storage |
Mass storage/file operations |
ld2450 |
HLK-LD2450 mmWave radar parser and events |
ble_hid |
BLE HID |
ble_scan |
BLE scanner |
ble_profile |
Read-only BLE GATT profiler |
ble_pin_bruteforce |
BLE HID PIN range/wordlist tester with live CDC control |
msc |
USB MSC mode |
msc_read_chunk |
Chunked MSC_READ |
badusb |
Native USB HID BadUSB |
pin_bruteforce |
Native USB HID PIN range/wordlist tester |
mesh_provision |
Mesh provisioning support is compiled in; advertised even before keys are stored |
mesh |
Mesh Foundation available; only advertised when valid derived keys exist |
Pull-based scan results¶
Firmware that advertises scan_results_pull accepts
SCAN_WIFI/WPS_LOCK_SCAN with pull_results:true. The command response then
contains scan_id, count, kind, and (if applicable) truncated. The app
retrieves bounded chunks with:
{
"ok": true,
"scan_id": 42,
"kind": "wifi",
"offset": 0,
"count": 4,
"total": 7,
"next_offset": 4,
"crc32": 123456789,
"results": []
}
See commands/wifi.md for the
canonical CRC input and retry rules. Legacy firmware continues to emit
scan_ap/wps_lock_ap events when pull mode is not requested.
Reference Index¶
Detailed command and event references are split by radio category:
Error Handling¶
| Scenario | Behaviour |
|---|---|
| Unknown command | RESPONSE {ok:false,msg:"unknown command"} |
| Missing command | RESPONSE {ok:false,msg:"missing cmd"} |
| Bad JSON | Frame dropped or RESPONSE {ok:false,msg:"parse error"} |
| Payload > 1024 bytes | App rejects before sending; firmware drops/resyncs |
| Unknown frame type | Decoder resynchronizes |
| Invalid length | Decoder skips byte(s) and resyncs |
| No matching command ID | RESPONSE discarded by app |
| Command timeout | App removes pending entry and returns null |
Planned Extensions (future minor versions)¶
| Addition | Notes |
|---|---|
| FastPair model ID | BLE service-data mapping, app-side |
| Mesh foundation | Draft protocol 1.1. See commands/mesh.md and events/mesh.md |
| Peripheral frame type | Reserved 0x07 for sub-GHz/nRF24/RFID data |
| Fragmentation envelope | frag / frag_total fields for logical messages over 1024 bytes |