Skip to content

NRN reader protocol

This page describes the wire protocol the Nextwaves SDK implements. You need it in three situations: building your own tooling, decoding a capture, or debugging a reader that answers with an error notification. For normal integration, the SDK already handles everything on this page.

This is not the tag-to-reader air-interface specification. For that behavior, consult the current GS1 Gen2 UHF RFID standard. For host-to-reader commands, the firmware manual shipped with the target reader remains authoritative.

Serial connection

SettingValue
InterfaceRS-232 or CP210x USB-to-serial
Baud rate115200
Data bits8
Stop bits1
ParityNone
Flow controlNone

Frame structure

text
5A | PCW(4) | [Address(1) when RS485] | Length(2, big-endian) | Data(N) | CRC16(2, big-endian)
FieldSizeMeaning
Header1 byteAlways 0x5A
PCW4 bytesProtocol Control Word
Address1 bytePresent only when the RS485 flag is set
Length2 bytesPayload length, big-endian
DataN bytesCommand-specific payload
CRC162 bytesChecksum over every byte after the header, big-endian

The CRC covers the PCW, the optional address, the length, and the data. It does not cover the 0x5A header.

Protocol Control Word

text
byte 0: protocol type      0x00
byte 1: protocol version   0x01
byte 2: flags and category  bit 5 = RS485, bit 4 = notify, low nibble = category
byte 3: message id within the category

Category and message id together form the SDK MID constant: category 0x02 with message id 0x10 is READ_EPC_TAG = 0x0210. The notify bit distinguishes a reader-initiated notification from a response to your command, which is why a tag notification for category 0x02 arrives with byte 2 equal to 0x12.

CRC16

The checksum is CRC16 with polynomial 0x1021 and initial value 0x0000, computed over every byte after the header and transmitted big-endian.

python
def crc16(data: bytes) -> int:
    crc = 0
    for byte in data:
        crc ^= byte << 8
        for _ in range(8):
            crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
    return crc

Recompute the checksum whenever you change any byte of a frame. Never copy a checksum from a sample frame onto a modified one.

Worked example

READ_EPC_TAG on antenna 1, continuous mode, as built by the Python SDK:

text
5A 0001 0210 0005 00000001 01 F487
│  │    │    │    │        │  └─ CRC16 over 0001021000050000000101
│  │    │    │    │        └──── continuous flag: 0x01
│  │    │    │    └───────────── antenna mask: antenna 1
│  │    │    └────────────────── payload length: 5
│  │    └─────────────────────── category 0x02, message id 0x10
│  └──────────────────────────── protocol type 0x00, version 0x01
└─────────────────────────────── header

A tag notification for the same session:

text
5A 0001 1210 0016 0008 3000112233445566 3000 01 01 80 08 000E0BD4 09 40 B44F
        │         │    │                │    │  └ RSSI  └ freq    └ phase
        │         │    │                │    └─ antenna port 1
        │         │    │                └────── PC word 0x3000
        │         │    └─────────────────────── EPC, 8 bytes
        │         └──────────────────────────── EPC length: 8
        └────────────────────────────────────── notify bit set, category 0x02, id 0x10

The optional parameters decode to RSSI 0x80 = 128 raw = -65 dBm, frequency 0x000E0BD4 = 920532 kHz = 920.532 MHz, and phase 0x40 = 64 raw, which the SDK reports as 64 / 128 × 2π = 3.1416 radians.

Inventory response payload

text
EPC length:  2 bytes, big-endian
EPC data:    N bytes
PC:          2 bytes
Antenna ID:  1 byte
Optional parameters, each introduced by a PID byte:
  0x01 RSSI            1 byte, raw 0-255
  0x02 Reading result  1 byte
  0x03 TID             2-byte length followed by data
  0x04 Tag data area   2-byte length followed by data
  0x05 Reserved area   2-byte length followed by data
  0x06 Sub-antenna     1 byte
  0x07 UTC read time   8 bytes
  0x08 Frequency       4 bytes, kHz, big-endian
  0x09 Phase           1 byte, raw 0-128

The SDK converts RSSI to dBm, frequency to MHz, and phase to radians before it reaches your callback. The 1.0.0 tag structure carries the parameters listed above; parameters 0x02 and 0x04 through 0x07 will be added in a later release, and self-written tooling that needs them can parse the payload directly from this table.

Message ids

ConstantValuePurpose
QUERY_INFO0x0100Serial number, firmware, uptime
QUERY_RFID_ABILITY0x1000Power range, antenna count, frequency and protocol lists
READ_EPC_TAG0x0210Start EPC inventory
WRITE_EPC_TAG0x0211Write the EPC bank
PHASE_INVENTORY0x0214Inventory with phase reporting
STOP_INVENTORY0x02FFStop inventory
CONFIGURE_READER_POWER0x0201Set transmit power per antenna
QUERY_READER_POWER0x0202Read transmit power per antenna
SET_RF_BAND0x0203Set regional band
QUERY_RF_BAND0x0204Read regional band
SET_WORKING_FREQUENCY0x0205Set the working channel list
QUERY_WORKING_FREQUENCY0x0206Read the working channel list
SET_FILTER_SETTINGS0x0209Reader-side duplicate window and RSSI cutoff
QUERY_FILTER_SETTINGS0x020ARead filter settings
CONFIG_BASEBAND0x020BSession, Q, inventory flag, profile
QUERY_BASEBAND0x020CRead baseband configuration
READER_POWER_CALIBRATION0x0103Power calibration
CONFIGURE_GPO0x0109Set a general-purpose output
QUERY_GPI0x010ARead a general-purpose input
CONFIGURE_GPI_TRIGGER0x010BConfigure an input trigger
BUZZER_SWITCH0x011EControl the buzzer
READ_END0x1231Inventory ended
ERROR_NOTIFICATION0x00Reader-initiated error

A technical note for hand-built commands: CONFIGURE_ANTENNA_ENABLE (0x0203) and QUERY_ANTENNA_ENABLE (0x0202) share their numeric values with the RF band and power constants, so disambiguate by the payload you send with them, never by the constant name.

Antennas

Up to 32 ports are addressed by a 32-bit bitmask, antenna n at bit n - 1, transmitted big-endian:

text
00 00 00 01  -> antenna 1
00 00 00 03  -> antennas 1 and 2
00 00 00 0F  -> antennas 1 to 4
80 00 00 49  -> antennas 1, 4, 7, and 32

The reader scans enabled ports sequentially and reports the port on every tag notification. Preserve antenna_id end to end: it is how you know which physical area each read came from.

Power and RSSI

query_rfid_ability returns the reader's own minimum and maximum power in dBm together with the antenna count. Configure within that range; the SDK does not clamp.

Raw RSSI converts to dBm as:

text
dBm = -100 + round((raw × 70) / 255)
RawdBm
0-100
64-82
128-65
200-45
255-30

This conversion formula is specific to the reader model; before applying it to another model, verify it against that model's firmware.

Frequency

text
frequency MHz = 920.0 + channelIndex × 0.5

Tag notifications report frequency in kHz as a 32-bit value, which the SDK converts to MHz. Configure only the channels and power permitted by the equipment certification and the current rules at the deployment location.

  1. Open the port and send the connect-and-initialize handshake.
  2. QUERY_INFO for identity, QUERY_RFID_ABILITY for capability.
  3. Set the antenna mask and per-port power.
  4. Set the verified regional band and channel list.
  5. Set the filter window.
  6. Send READ_EPC_TAG.
  7. Buffer incoming bytes, split frames on the 0x5A header, validate CRC, and parse notifications.
  8. Send STOP_INVENTORY, drain pending frames, then close the port.

Bytes from a serial port do not arrive aligned to frames. Always buffer and re-scan: extract_valid_frames in the Python package does this, and a custom receive loop must do the same.

EPC Gen2 memory

BankIDTypical content
Reserved0032-bit kill password and 32-bit access password
EPC0196-bit or longer identifier
TID10Read-only chip and manufacturer identity
User11Chip-dependent application memory

Writing at this layer demands four things, and skipping any one is a risk: singulation so exactly one tag sits in the zone, access-password handling when the tag is locked, a read-back after every write, and safeguards around the kill command because it cannot be undone. These concepts are explained in the glossary.

Frame utilities in the SDK

Every package ships frame-layer helpers so you can build tooling, load capture files, or run your own receive loop without rewriting the framing code:

UtilityPythonTypeScriptGoRustC++
Build a framebuild_frameNRNUtils.buildFrameBuildFramebuild_framebuild_frame
Parse a frameparse_frameNRNUtils.parseFrameParseFrameparse_frameparse_frame
Split a byte stream into framesextract_valid_framesNRNUtils.extractValidFramesper the receive loop appendixper the receive loop appendixper the receive loop appendix
Parse a tag notificationparse_epcinternalParseEPCparse_epcparse_epc
CRC16crc16_ccittNRNUtils.crc16CCITTCRC16CCITTcrc16_ccittcrc16_ccitt
Raw RSSI to dBmcalculate_rssiNRNUtils.calculateRSSICalculateRSSIcalculate_rssicalculate_rssi
Channel index to MHzcalculate_frequencyNRNUtils.calculateFrequencyCalculateFrequencycalculate_frequencycalculate_frequency

parse_frame verifies the CRC and errors or returns invalid on a bad checksum, so use it instead of comparing CRCs yourself.

Test vectors

The repository ships shared fixtures under sdk/nation/testdata/:

  • frames/ holds hex-encoded request, response, and notification frames in both CRC families.
  • crc_vectors.json holds CRC inputs and expected values for both polynomials plus the CCITT-FALSE reference.
  • rssi_vectors.json holds raw-to-dBm pairs.
  • antenna_masks.json holds antenna-list-to-mask pairs.

Run your own parser against these before you point it at a reader.

Easy Inventory operations and RFID integration documentation