Skip to content

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:

  1. Send the start-inventory command, and make sure the stop command goes out on exit.
  2. 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.
  3. Extract each complete frame from the buffer following the 5A | PCW(4) | length(2) | data | crc(2) layout, detailed in the NRN reader protocol.
  4. 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.

go
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).

rust
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.

cpp
#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;
}

Easy Inventory operations and RFID integration documentation