Phụ lục: tự viết receive loop cho Go, Rust và C++
Chỉ đọc trang này nếu bạn viết Go, Rust hoặc C++ và cần vòng kiểm kê liên tục; bản Python và Web Serial đã có sẵn vòng đọc, xem Đọc thẻ và chống trùng. Ở ba ngôn ngữ này, bạn cầm serial port, tự nhận dòng dữ liệu từ đầu đọc, rồi dùng bộ parse của SDK để lấy dữ liệu thẻ. Mỗi mẫu dưới đây là một chương trình hoàn chỉnh, copy vào dự án là chạy được.
Cả ba mẫu có cùng một hình dạng, và cũng là hình dạng của mọi receive loop đúng:
- Gửi lệnh bắt đầu kiểm kê, và bảo đảm lệnh dừng được gửi khi thoát.
- Đọc serial port theo từng khúc, dồn vào một buffer: dữ liệu có thể về dồn cục hoặc bị cắt giữa chừng, không bao giờ được giả định mỗi lần đọc là một thông báo tròn trịa.
- Tách từng frame hoàn chỉnh khỏi buffer theo cấu trúc
5A | PCW(4) | length(2) | data | crc(2), chi tiết ở giao thức đầu đọc NRN. - Với frame là tag notification, đưa phần data cho bộ parse của SDK rồi chống trùng như mọi bản khác.
Go
NewNRNReaderWithTransport là hàm công khai, nên bạn giữ được handle của serial port mà vẫn dùng được 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 của giao thức NRN: polynomial 0x1021, khởi tạo 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 dựng một lệnh NRN: 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 trả về frame hoàn chỉnh đầu tiên trong b và số byte cần bỏ đi.
func nextFrame(b []byte) (frame []byte, consumed int, ok bool) {
start := bytes.IndexByte(b, 0x5A)
if start < 0 {
return nil, len(b), false // không thấy header, bỏ phần nhiễu
}
rest := b[start:]
if len(rest) < 9 {
return nil, start, false // có header nhưng frame chưa đủ
}
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)
}
// Reader chỉ dùng để decode payload của thẻ; serial port do ta giữ.
decoder := nrn.NewNRNReaderWithTransport(port, portName, 115200)
mask := nrn.BuildAntennaMask([]int{1})
payload := binary.BigEndian.AppendUint32(nil, mask)
payload = append(payload, 0x01) // liên tục
if _, err := port.Write(buildFrame(0x0210, payload)); err != nil {
log.Fatal(err)
}
defer port.Write(buildFrame(0x02FF, nil)) //nolint:errcheck // cố gắng dừng
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 // hết timeout đọc, không có gì trong vùng đọc
}
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 ăng-ten=%d", tag.EPC, tag.AntennaID)
}
}
buf = buf[consumed:]
}
}
}Rust
Mẫu Rust tự giữ serial port và tự decode dữ liệu thẻ, dùng các hàm tiện ích mà crate export sẵn (build_antenna_mask, calculate_rssi).
use nrn_sdk::{build_antenna_mask, 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, // parameter lạ, dừng lại còn hơn đọc sai phần còn lại
}
}
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); // liên tục
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={} ăng-ten={} 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(())
}C++
Mẫu C++ tạo một NRNReader chỉ để parse dữ liệu thẻ (parse_epc hoạt động độc lập với kết nối), còn serial I/O do bạn giữ.
#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() {
// Không bao giờ gọi open(): chỉ dùng cho parse_epc.
nrn::NRNReader codec("", 115200);
// Serial handle của riêng bạn. open_port/read_port/write_port là mã theo nền tảng.
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, // liên tục
};
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); // có thể trả về 0 byte khi 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 << " ăng-ten " << int(tag.antenna_id) << std::endl;
}
}
buf.erase(buf.begin(), buf.begin() + total);
}
}
write_port(port, build_frame(0x02FF, {}));
return 0;
}Đọc tiếp
- Đọc thẻ và chống trùng: cơ chế đọc và ba lớp chống trùng, áp dụng nguyên vẹn cho các mẫu trên.
- Phụ lục: giao thức đầu đọc NRN: cấu trúc frame, CRC và message id mà các mẫu này dựng tay.

