Appendix: writing your own receive loop for Go, Rust, and C++
Read this page only if you write Go, Rust, or C++ and need continuous inventory; the Python and Web Serial packages ship their own read loop, see Reading tags and deduplication. In these three languages you hold the serial port, receive the reader's data stream yourself, then use the SDK's parser to extract tag data. Each sample below is a complete program you can copy into your project and run.
All three samples share one shape, which is also the shape of every correct receive loop:
- Send the start-inventory command, and make sure the stop command goes out on exit.
- Read the serial port in chunks into a buffer: data can arrive glued together or cut mid-message, and no read may ever be assumed to hold one tidy notification.
- Extract each complete frame from the buffer following the
5A | PCW(4) | length(2) | data | crc(2)layout, detailed in the NRN reader protocol. - For frames that are tag notifications, hand the data portion to the SDK's parser, then deduplicate exactly like every other package.
Go
NewNRNReaderWithTransport is public, so you can keep your own port handle and still use ParseEPC.
package main
import (
"bytes"
"encoding/binary"
"log"
"sync"
"time"
nrn "github.com/Nextwaves-Industries/nextwaves-sdk/sdk/nation/go"
"go.bug.st/serial"
)
// crc16 for the NRN protocol: polynomial 0x1021, init 0x0000.
func crc16(data []byte) uint16 {
var crc uint16
for _, b := range data {
crc ^= uint16(b) << 8
for i := 0; i < 8; i++ {
if crc&0x8000 != 0 {
crc = crc<<1 ^ 0x1021
} else {
crc <<= 1
}
}
}
return crc
}
// buildFrame builds an NRN command: 5A | PCW(4) | length(2) | data | crc(2)
func buildFrame(mid uint16, payload []byte) []byte {
body := []byte{0x00, 0x01, byte(mid >> 8), byte(mid)}
body = binary.BigEndian.AppendUint16(body, uint16(len(payload)))
body = append(body, payload...)
frame := append([]byte{0x5A}, body...)
return binary.BigEndian.AppendUint16(frame, crc16(body))
}
// nextFrame returns the first complete frame in b and how many bytes to drop.
func nextFrame(b []byte) (frame []byte, consumed int, ok bool) {
start := bytes.IndexByte(b, 0x5A)
if start < 0 {
return nil, len(b), false // no header in sight, discard the noise
}
rest := b[start:]
if len(rest) < 9 {
return nil, start, false // header found, frame still incomplete
}
total := 9 + int(binary.BigEndian.Uint16(rest[5:7]))
if len(rest) < total {
return nil, start, false
}
return rest[:total], start + total, true
}
type deduper struct {
mu sync.Mutex
window time.Duration
seen map[string]time.Time
}
func newDeduper(window time.Duration) *deduper {
return &deduper{window: window, seen: make(map[string]time.Time)}
}
func (d *deduper) accept(epc string) bool {
now := time.Now()
d.mu.Lock()
defer d.mu.Unlock()
last, found := d.seen[epc]
d.seen[epc] = now
if found && now.Sub(last) < d.window {
return false
}
for key, seenAt := range d.seen {
if now.Sub(seenAt) > d.window*10 {
delete(d.seen, key)
}
}
return true
}
func main() {
const portName = "/dev/ttyUSB0"
port, err := serial.Open(portName, &serial.Mode{
BaudRate: 115200,
DataBits: 8,
Parity: serial.NoParity,
StopBits: serial.OneStopBit,
})
if err != nil {
log.Fatal(err)
}
defer port.Close()
if err := port.SetReadTimeout(500 * time.Millisecond); err != nil {
log.Fatal(err)
}
// The reader is used only as a tag-payload decoder; we own the port.
decoder := nrn.NewNRNReaderWithTransport(port, portName, 115200)
mask := nrn.BuildAntennaMask([]int{1})
payload := binary.BigEndian.AppendUint32(nil, mask)
payload = append(payload, 0x01) // continuous
if _, err := port.Write(buildFrame(0x0210, payload)); err != nil {
log.Fatal(err)
}
defer port.Write(buildFrame(0x02FF, nil)) //nolint:errcheck // best-effort stop
dedupe := newDeduper(2 * time.Second)
buf := make([]byte, 0, 8192)
chunk := make([]byte, 1024)
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
n, err := port.Read(chunk)
if err != nil {
log.Fatal(err)
}
if n == 0 {
continue // read timeout, nothing in the field
}
buf = append(buf, chunk[:n]...)
for {
frame, consumed, ok := nextFrame(buf)
if !ok {
buf = buf[consumed:]
break
}
notify := frame[3]&0x10 != 0
category := frame[3] & 0x0F
mid := frame[4]
if notify && category == 0x02 && mid == 0x10 {
dataLen := int(binary.BigEndian.Uint16(frame[5:7]))
tag := decoder.ParseEPC(frame[7 : 7+dataLen])
if tag.EPC != "" && dedupe.accept(tag.EPC) {
log.Printf("EPC=%s antenna=%d", tag.EPC, tag.AntennaID)
}
}
buf = buf[consumed:]
}
}
}Rust
The Rust sample holds the serial port itself and decodes the tag data directly, using the utility functions the crate exports (build_antenna_mask, calculate_rssi).
use nrn_sdk::{build_antenna_mask, calculate_frequency, calculate_rssi};
use std::collections::HashMap;
use std::io::Read;
use std::time::{Duration, Instant};
fn crc16(data: &[u8]) -> u16 {
let mut crc: u16 = 0;
for &byte in data {
crc ^= u16::from(byte) << 8;
for _ in 0..8 {
crc = if crc & 0x8000 != 0 { (crc << 1) ^ 0x1021 } else { crc << 1 };
}
}
crc
}
fn build_frame(mid: u16, payload: &[u8]) -> Vec<u8> {
let mut body = vec![0x00, 0x01, (mid >> 8) as u8, mid as u8];
body.extend_from_slice(&(payload.len() as u16).to_be_bytes());
body.extend_from_slice(payload);
let mut frame = vec![0x5A];
frame.extend_from_slice(&body);
frame.extend_from_slice(&crc16(&body).to_be_bytes());
frame
}
struct Tag {
epc: String,
antenna_id: u8,
rssi: Option<i32>,
frequency: Option<f64>,
}
fn parse_tag(data: &[u8]) -> Option<Tag> {
if data.len() < 5 {
return None;
}
let epc_len = u16::from_be_bytes([data[0], data[1]]) as usize;
if data.len() < 2 + epc_len + 3 {
return None;
}
let epc = data[2..2 + epc_len]
.iter()
.map(|b| format!("{b:02X}"))
.collect::<String>();
let mut tag = Tag { epc, antenna_id: data[2 + epc_len + 2], rssi: None, frequency: None };
let mut cursor = 2 + epc_len + 3;
while cursor < data.len() {
let pid = data[cursor];
cursor += 1;
match pid {
0x01 => {
tag.rssi = data.get(cursor).map(|&raw| calculate_rssi(raw));
cursor += 1;
}
0x08 if cursor + 4 <= data.len() => {
let khz = u32::from_be_bytes([
data[cursor], data[cursor + 1], data[cursor + 2], data[cursor + 3],
]);
tag.frequency = Some(f64::from(khz) / 1000.0);
cursor += 4;
}
0x09 => cursor += 1,
_ => break, // unknown parameter, stop rather than misread the rest
}
}
Some(tag)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut port = serialport::new("/dev/ttyUSB0", 115_200)
.timeout(Duration::from_millis(500))
.open()?;
let mask = build_antenna_mask(&[1]);
let mut payload = mask.to_be_bytes().to_vec();
payload.push(0x01); // continuous
port.write_all(&build_frame(0x0210, &payload))?;
let window = Duration::from_secs(2);
let mut seen: HashMap<String, Instant> = HashMap::new();
let mut buf: Vec<u8> = Vec::with_capacity(8192);
let mut chunk = [0u8; 1024];
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
match port.read(&mut chunk) {
Ok(0) => continue,
Ok(n) => buf.extend_from_slice(&chunk[..n]),
Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => continue,
Err(e) => return Err(e.into()),
}
loop {
let Some(start) = buf.iter().position(|&b| b == 0x5A) else {
buf.clear();
break;
};
buf.drain(..start);
if buf.len() < 9 {
break;
}
let data_len = u16::from_be_bytes([buf[5], buf[6]]) as usize;
let total = 9 + data_len;
if buf.len() < total {
break;
}
let notify = buf[3] & 0x10 != 0;
let category = buf[3] & 0x0F;
let mid = buf[4];
if notify && category == 0x02 && mid == 0x10 {
if let Some(tag) = parse_tag(&buf[7..7 + data_len]) {
let now = Instant::now();
let fresh = seen
.get(&tag.epc)
.is_none_or(|last| now.duration_since(*last) >= window);
seen.insert(tag.epc.clone(), now);
if fresh {
println!("EPC={} antenna={} rssi={:?}", tag.epc, tag.antenna_id, tag.rssi);
}
seen.retain(|_, last| now.duration_since(*last) < window * 10);
}
}
buf.drain(..total);
}
}
port.write_all(&build_frame(0x02FF, &[]))?;
Ok(())
}calculate_frequency is imported for channel-index conversion; tag notifications carry frequency in kHz directly, as handled above.
C++
The C++ sample creates an NRNReader purely as a tag-data parser (parse_epc works independently of the connection) while you keep the serial I/O.
#include "nrn.hpp"
#include <chrono>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
using Clock = std::chrono::steady_clock;
uint16_t crc16(const std::vector<uint8_t>& data) {
uint16_t crc = 0;
for (uint8_t byte : data) {
crc ^= static_cast<uint16_t>(byte) << 8;
for (int i = 0; i < 8; ++i) {
crc = (crc & 0x8000) ? static_cast<uint16_t>((crc << 1) ^ 0x1021)
: static_cast<uint16_t>(crc << 1);
}
}
return crc;
}
std::vector<uint8_t> build_frame(uint16_t mid, const std::vector<uint8_t>& payload) {
std::vector<uint8_t> body{0x00, 0x01, static_cast<uint8_t>(mid >> 8),
static_cast<uint8_t>(mid & 0xFF)};
body.push_back(static_cast<uint8_t>(payload.size() >> 8));
body.push_back(static_cast<uint8_t>(payload.size() & 0xFF));
body.insert(body.end(), payload.begin(), payload.end());
std::vector<uint8_t> frame{0x5A};
frame.insert(frame.end(), body.begin(), body.end());
uint16_t crc = crc16(body);
frame.push_back(static_cast<uint8_t>(crc >> 8));
frame.push_back(static_cast<uint8_t>(crc & 0xFF));
return frame;
}
int main() {
// Never opened: used only for parse_epc.
nrn::NRNReader codec("", 115200);
// Your own serial handle. open_port/read_port/write_port are platform code.
auto port = open_port("/dev/ttyUSB0", 115200);
uint32_t mask = nrn::build_antenna_mask({1});
std::vector<uint8_t> payload{
static_cast<uint8_t>(mask >> 24), static_cast<uint8_t>(mask >> 16),
static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(mask),
0x01, // continuous
};
write_port(port, build_frame(0x0210, payload));
const auto window = std::chrono::seconds(2);
std::unordered_map<std::string, Clock::time_point> seen;
std::vector<uint8_t> buf;
while (running) {
auto chunk = read_port(port); // may return 0 bytes on timeout
buf.insert(buf.end(), chunk.begin(), chunk.end());
while (true) {
auto it = std::find(buf.begin(), buf.end(), 0x5A);
if (it == buf.end()) { buf.clear(); break; }
buf.erase(buf.begin(), it);
if (buf.size() < 9) break;
size_t data_len = (static_cast<size_t>(buf[5]) << 8) | buf[6];
size_t total = 9 + data_len;
if (buf.size() < total) break;
bool notify = (buf[3] & 0x10) != 0;
uint8_t category = buf[3] & 0x0F;
uint8_t mid = buf[4];
if (notify && category == 0x02 && mid == 0x10) {
std::vector<uint8_t> data(buf.begin() + 7, buf.begin() + total);
nrn::TagData tag = codec.parse_epc(data);
auto now = Clock::now();
auto found = seen.find(tag.epc);
bool fresh = found == seen.end() || (now - found->second) >= window;
seen[tag.epc] = now;
if (!tag.epc.empty() && fresh) {
std::cout << tag.epc << " antenna " << int(tag.antenna_id) << std::endl;
}
}
buf.erase(buf.begin(), buf.begin() + total);
}
}
write_port(port, build_frame(0x02FF, {}));
return 0;
}Read next
- Reading tags and deduplication: the read mechanics and the three deduplication layers, which apply to these samples unchanged.
- Appendix: NRN reader protocol: the frame layout, CRC, and message ids these samples build by hand.

