Đọc thẻ và chống trùng
Trang này đưa bạn từ "đầu đọc đã nối được" đến "ứng dụng nhận được dòng sự kiện thẻ sạch": chạy vòng đọc, hiểu vì sao dữ liệu về ồ ạt, và lọc nó thành sự kiện nghiệp vụ dùng được. Code mẫu Python và Web Serial nằm ngay trong trang; đây là hai bản SDK có sẵn vòng đọc liên tục, nên với đa số dự án, trang này là tất cả những gì cần cho phần đọc thẻ.
Cơ chế đọc
Kiểm kê RFID không theo mô hình request và response quen thuộc. Bạn gọi bắt đầu kiểm kê một lần, rồi đầu đọc liên tục đẩy dữ liệu thẻ về cho tới khi bạn gọi dừng. Ba hệ quả theo suốt trang này:
- Cùng một thẻ báo về rất nhiều lần. Một thẻ nằm yên trong vùng đọc sinh dữ liệu ở mỗi vòng quét, có thể hàng trăm lần mỗi giây. Việc gom các lần đọc đó thành một sự kiện thuộc về ứng dụng của bạn, theo ba lớp ở cuối trang.
- Dữ liệu về theo luồng liên tục. Bản Python và Web Serial nhận và ghép luồng này sẵn rồi mới gọi callback của bạn, nên bạn không phải bận tâm. Viết bằng Go, Rust hoặc C++ và cần vòng đọc liên tục thì dùng mẫu ở phụ lục receive loop.
- Không có sự kiện báo thẻ rời đi. Đầu đọc chỉ kể cái nó đang thấy, không kể cái đã biến mất. "Thẻ đã rời vùng đọc" là một timeout bạn tự cài, không phải thông báo nhận được.
Trình tự bắt đầu đọc
Cấu hình khi đầu đọc đang idle, xong hết rồi mới bắt đầu kiểm kê:
- Mở serial port và khởi tạo kết nối.
- Gọi
query_rfid_ability()để biết dải công suất và số ăng-ten thật của thiết bị. - Đặt antenna mask và công suất từng cổng.
- Đặt RF band và danh sách kênh được phép tại nơi lắp đặt.
- Đặt cửa sổ filter phía đầu đọc.
- Bắt đầu kiểm kê và xử lý dữ liệu thẻ trong callback.
- Khi xong: dừng kiểm kê, để SDK xử lý nốt dữ liệu còn chờ, đóng port.
Đổi cấu hình khi đang đọc: phải dừng trước
Đầu đọc chỉ nhận lệnh cấu hình khi đang idle. Gọi lệnh cấu hình giữa lúc kiểm kê đang chạy thì lệnh đến một thiết bị đang bận, và phản hồi của nó lẫn vào dòng dữ liệu thẻ đang đổ về trên cùng serial port.
Nhiều method của bản Python tự dừng giúp bạn trước khi làm việc: query_reader_power, set_rf_band, configure_baseband, query_baseband_profile và các đường ghi EPC. Riêng configure_reader_power để bạn toàn quyền, nên muốn đổi công suất giữa phiên thì tự dừng trước:
reader.stop_inventory()
reader.is_idle() # gửi STOP và chờ phần cứng ổn định
reader.configure_reader_power({1: 28}, persistence=False)
reader.start_inventory_with_mode(antenna_mask=[1], callback=on_tag)is_idle() yêu cầu dừng, chờ đầu đọc xác nhận, rồi chờ thêm settle_delay (mặc định 0,5 giây) cho phần cứng ổn định. Bỏ bước chờ này thì lệnh đổi công suất dễ không ăn.
Quy tắc chung: coi công suất, ăng-ten, band, kênh và profile là cấu hình của cả phiên. Chốt giá trị trước khi bắt đầu đọc, và chỉ đổi giữa chừng khi nghiệp vụ thật sự cần.
Đọc bằng Python
SDK tự chạy vòng đọc và gọi callback trên thread riêng của nó.
import logging
import queue
import threading
import time
from nrn import create_reader
class Deduper:
"""Bỏ qua các lần đọc lặp của cùng một EPC trong một sliding window."""
def __init__(self, window_s: float = 2.0):
self._window = window_s
self._seen: dict[str, float] = {}
self._lock = threading.Lock()
def accept(self, epc: str) -> bool:
now = time.monotonic()
with self._lock:
last = self._seen.get(epc)
self._seen[epc] = now
if last is not None and now - last < self._window:
return False
cutoff = now - self._window * 10
for stale in [k for k, seen_at in self._seen.items() if seen_at < cutoff]:
del self._seen[stale]
return True
events: queue.Queue = queue.Queue()
dedupe = Deduper(window_s=2.0)
reader = create_reader("/dev/ttyUSB0", log_level=logging.INFO)
reader.open()
try:
reader.Connect_Reader_And_Initialize()
reader.configure_reader_power({1: 25}, persistence=False)
reader.set_filter_settings(repeated_time_ms=200, rssi_threshold=0)
def on_tag(tag):
# Chạy trên inventory thread của SDK. Giữ thật ngắn: lọc rồi chuyển đi.
epc = tag.get("epc")
if not epc or not dedupe.accept(epc):
return
events.put(tag)
reader.start_inventory_with_mode(antenna_mask=[1], callback=on_tag)
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
try:
tag = events.get(timeout=0.5)
except queue.Empty:
continue
print(tag["epc"], tag["rssi"], tag["antenna_id"])
reader.stop_inventory()
finally:
reader.close()on_tag chạy trên inventory thread của SDK, không phải thread chính của bạn. Làm việc nặng ở đó, như ghi database hay gọi HTTP, sẽ chặn việc nhận dữ liệu và làm rơi thẻ. Cứ lọc rồi đẩy sang một queue, việc nặng để thread khác lo.
Đọc bằng TypeScript và Web Serial
SDK chạy một async receive loop và gọi callback trên event loop của trình duyệt, nên callback chạy đơn luồng và không cần lock.
import { createNRNReader, type TagData } from "@nextwaves/nrn-sdk";
class Deduper {
private seen = new Map<string, number>();
constructor(private readonly windowMs = 2000) {}
accept(epc: string): boolean {
const now = performance.now();
const last = this.seen.get(epc);
this.seen.set(epc, now);
if (last !== undefined && now - last < this.windowMs) {
return false;
}
const cutoff = now - this.windowMs * 10;
for (const [key, seenAt] of this.seen) {
if (seenAt < cutoff) this.seen.delete(key);
}
return true;
}
}
const dedupe = new Deduper(2000);
const reader = createNRNReader({ baudrate: 115200 });
// Phải chạy trong một cú click hoặc user gesture khác.
await reader.connect();
await reader.startInventory([1], (tag: TagData) => {
if (!dedupe.accept(tag.epc)) return;
console.log(tag.epc, tag.rssi, tag.antenna_id);
});
// Sau đó, từ nút dừng của bạn:
await reader.stopInventory();
await reader.disconnect();Callback dùng chung event loop với giao diện. Đẩy thẻ vào một store hoặc mảng gom nhóm rồi render theo chu kỳ, đừng gọi setState cho từng thẻ một.
Go, Rust và C++: dùng phụ lục
Ba bản này có đầy đủ lệnh truy vấn, cấu hình và bộ parse dữ liệu thẻ. Để chạy kiểm kê liên tục, bạn tự viết receive loop theo mẫu hoàn chỉnh cho từng ngôn ngữ ở Phụ lục: tự viết receive loop. Cấu trúc vòng đọc, cách chống trùng và mọi quy tắc trên trang này áp dụng y hệt; chỉ khác ở chỗ vòng nhận dữ liệu do bạn cầm.
Chống trùng
Làm ở ba lớp. Mỗi lớp loại một kiểu trùng khác nhau, và không lớp nào thay được lớp kia:
Lớp 1: filter phía đầu đọc
Đầu đọc chặn bớt bản đọc lặp trước khi chúng tới máy chủ, tiết kiệm băng thông serial và CPU:
reader.set_filter_settings(repeated_time_ms=200, rssi_threshold=0)repeated_time_msđược làm tròn xuống bội số của 10 ms: giá trị dưới 10 tắt filter,250giữ nguyên, còn255thành250.rssi_thresholdnhận giá trị thô 0 tới 255, không phải dBm; mức thô 128 tương đương khoảng -65 dBm. Đặt 0 để tắt ngưỡng.- Đây là thiết lập của thiết bị, không phải quy tắc nghiệp vụ: nó không diễn đạt được ý "mỗi pallet một sự kiện mỗi ca".
Giữ cửa sổ này ngắn, cỡ vài trăm mili giây. Đặt dài quá, đầu đọc sẽ giấu luôn những thẻ thật sự quay lại vùng đọc, và phía máy chủ không còn cách nào biết được.
Lớp 2: cửa sổ phía máy chủ
Đây chính là Deduper trong các sample ở trên. Chọn độ dài cửa sổ theo sự kiện nghiệp vụ, không theo tốc độ đọc:
| Điểm đọc | Cửa sổ | Lý do |
|---|---|---|
| Cổng xuất nhập | 2 tới 5 giây | Một lượt pallet đi qua là một sự kiện |
| Băng chuyền | 0,5 tới 1 giây | Các món hàng nối nhau rất nhanh |
| Kiểm kê cầm tay | cả phiên làm việc | Mỗi EPC chỉ đếm một lần trên một phiếu kiểm |
| Quầy thu ngân | 1 tới 2 giây | Giỏ hàng thay đổi trong lúc nhân viên thao tác |
Có hai kiểu cửa sổ với hành vi khác nhau, và chọn nhầm là lỗi phổ biến nhất ở phần này:
- Sliding window, như các sample ở trên, làm mới mốc thời gian ở mỗi lần đọc: thẻ nằm yên trong vùng đọc chỉ phát sự kiện một lần, và chỉ phát lại sau khi vắng mặt đủ một cửa sổ. Dùng cho ý "hàng vừa tới".
- Fixed window không làm mới mốc khi bản đọc bị chặn: thẻ nằm yên phát sự kiện mỗi cửa sổ một lần. Dùng làm heartbeat "hàng vẫn còn ở đây".
Nhớ giới hạn kích thước map. Các sample đều dọn entry cũ hơn mười cửa sổ, vì một map EPC lớn mãi không dọn là chỗ rò rỉ bộ nhớ âm ỉ trong service của bạn khi nó chạy liền nhiều tuần.
Lớp 3: idempotency nghiệp vụ
Hai lớp trên chỉ giảm số bản trùng, không bao giờ loại hết: đầu đọc khởi động lại, mất kết nối rồi nối lại, hay chính ứng dụng của bạn restart đều xóa sạch cửa sổ. Vì vậy sự kiện đi tới sổ kho phải an toàn khi bị áp dụng hai lần.
Cách làm quen thuộc với dân web: đặt cho mỗi sự kiện một khóa tự nhiên, ví dụ (epc, điểm đọc, cửa sổ nghiệp vụ), rồi cho thao tác ghi phụ thuộc khóa đó, kiểu INSERT ... ON CONFLICT DO NOTHING. Bản trùng khi ấy chỉ là thao tác rỗng, không phải lần biến động tồn kho thứ hai. Đây là lớp duy nhất sống sót qua restart, nên hãy coi hai lớp kia là tối ưu hóa, còn lớp này là chỗ dựa.
Hướng dẫn vận hành
Theo dõi song song hai con số: EPC duy nhất mỗi phút và tổng bản đọc mỗi phút. Tổng bản đọc tăng mà EPC duy nhất đứng yên: công suất đang quá cao hoặc cửa sổ chống trùng quá ngắn. EPC duy nhất giảm mà tổng bản đọc vẫn đều: có thể một thẻ mạnh ở gần đang lấn át các thẻ yếu hơn, xem hướng dẫn triển khai RFID.
Đọc tiếp
- Phụ lục: tự viết receive loop khi dùng Go, Rust hoặc C++.
- Tham chiếu SDK cho danh sách method đầy đủ.
- Danh sách kiểm tra production trước khi cho chạy thật.

