Skip to main content

rfid_silion_compat/
lib.rs

1#![deny(missing_docs)]
2
3//! Host-side implementation of the Silion reader communication protocol.
4//!
5//! The implementation follows the packet format defined in:
6//! <https://en.silion.com.cn/En/doc_center/ModuleAPI_Docs/Communication_Protocol_Doc/html/Protocol_Introduction.html>
7//! and command pages linked from that document.
8//!
9//! This crate is centered around the high-level [`SilionReader`] API for common
10//! reader operations (version, region, inventory, and tag access).
11//!
12//! See the project README for installation and feature setup.
13//!
14//! ## API Entry Points
15//!
16//! - [`SilionReader`]: high-level async reader operations.
17//! - [`ReaderAsyncInventoryStartData`]: typed async inventory start configuration.
18//! - [`SelectOption`]: high-level select/singulation input for inventory and access.
19//! - [`AsyncInventoryMessage`]: typed pushed messages from async inventory.
20//! - [`EpcValue`]: high-level EPC wrapper with EPC Tag URI helpers.
21//!
22//! ## Typical Workflow
23//!
24//! 1. Create a transport (native serial or web serial).
25//! 2. Construct [`SilionReader`] with that transport.
26//! 3. Call typed async methods and work with parsed return values.
27//!
28//! ## Example: Single-Tag Inventory (native serial)
29//!
30//! ```rust,ignore
31//! use rfid_silion_compat::MetadataFlags;
32//! use rfid_silion_compat::SelectOption;
33//! use rfid_silion_compat::SerialTransport;
34//! use rfid_silion_compat::SilionReader;
35//!
36//! #[tokio::main]
37//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! let transport = SerialTransport::open("/dev/ttyUSB0", 115_200)?;
39//! let mut reader = SilionReader::new(transport);
40//!
41//! let metadata = MetadataFlags::default().with_rssi(true).with_antenna_id(true);
42//! let tag = reader
43//!     .single_tag_inventory(1000, SelectOption::Disabled, Some(metadata))
44//!     .await?;
45//!
46//! println!("EPC: {:02X?}", tag.epc_id);
47//! println!("RSSI dBm: {:?}", tag.rssi_dbm);
48//! Ok(())
49//! }
50//! ```
51
52mod async_proto;
53mod client;
54/// Protocol constants and enums for commands, regions, antennas, and statuses.
55pub mod codes;
56/// Low-level command payload types and host packet builders.
57pub mod command;
58/// EPC value types and EPC URI/binary helpers.
59pub mod epc;
60mod error;
61/// Wire-frame encoding/decoding primitives for reader/host packets.
62pub mod frame;
63/// Response payload decoders and parsed output data models.
64pub mod parsers;
65mod session;
66mod silion_reader;
67mod transport;
68
69/// Shared mock helpers used by rustdoc examples and unit tests.
70#[doc(hidden)]
71pub mod test_support;
72
73#[cfg(feature = "serial")]
74/// Serial port transport adapter backed by the `tokio-serial` crate.
75pub mod serial;
76
77#[cfg(all(target_arch = "wasm32", feature = "web-serial"))]
78/// Web Serial transport adapter for browser targets.
79pub mod web_serial;
80
81#[cfg(all(target_arch = "wasm32", feature = "web-serial"))]
82/// wasm-bindgen JavaScript bindings for the reader API.
83pub mod web_bindings;
84
85pub use client::{ClientError, ReaderClient};
86pub use codes::{AntennaPortsOption, RegionCode, StatusCode};
87pub use command::{
88    AntennaPortsConfiguration, EmbeddedReadTagData, InventoryEmbeddedCommandContent,
89    InventorySearchFlags, MemBank, MetadataFlags,
90};
91pub use epc::{EpcSchema, EpcValue, Giai96, Sgtin96};
92pub use error::ProtocolError;
93pub use frame::ReaderFrame;
94pub use parsers::{
95    AntennaPair, AntennaPortsResponse, AntennaPower, AntennaPowerSettling,
96    ProtocolConfigurationValue, ReaderConfigurationValue, RegulatoryHopTime, RunPhase,
97    SerialNumberInfo, TagEpcAndMetaData, VersionInfo,
98};
99#[cfg(feature = "serial")]
100pub use serial::SerialTransport;
101pub use session::AsyncInventorySession;
102pub use silion_reader::{
103    AsyncInventoryMessage, ReaderAsyncInventoryStartData, SelectOption, SilionReader,
104};
105pub use transport::ReaderTransport;
106
107#[cfg(test)]
108mod tests {
109    use std::collections::VecDeque;
110
111    use crate::codes::CommandCode;
112    use crate::command::HostCommand;
113    use crate::frame::{parse_reader_frame, protocol_crc16};
114    use crate::parsers::{
115        parse_antenna_ports_response, parse_frequency_hopping_table, parse_run_phase,
116        parse_version_info,
117    };
118
119    use super::*;
120
121    #[test]
122    fn crc16_matches_doc_example_for_get_version() {
123        let msg = [0xFF, 0x00, 0x03];
124        assert_eq!(protocol_crc16(&msg), 0x1D0C);
125    }
126
127    #[test]
128    fn build_get_version_packet_matches_example() {
129        let p = HostCommand::get_version().unwrap();
130        assert_eq!(p, vec![0xFF, 0x00, 0x03, 0x1D, 0x0C]);
131    }
132
133    #[test]
134    fn parse_reader_frame_success() {
135        let packet = [0xFF, 0x01, 0x0C, 0x00, 0x00, 0x12, 0x63, 0x43];
136        let f = parse_reader_frame(&packet).unwrap();
137        assert_eq!(f.command, 0x0C);
138        assert_eq!(f.status_raw, 0x0000);
139        assert_eq!(f.status, Some(StatusCode::Success));
140        assert_eq!(f.data, vec![0x12]);
141    }
142
143    #[test]
144    fn build_set_current_region_packet() {
145        let p = HostCommand::set_current_region(RegionCode::NorthAmerica).unwrap();
146        assert_eq!(p, vec![0xFF, 0x01, 0x97, 0x01, 0x4B, 0xBC]);
147    }
148
149    #[test]
150    fn build_set_antenna_access_pair_packet() {
151        let p =
152            HostCommand::set_antenna_ports(&AntennaPortsConfiguration::AccessPair(AntennaPair {
153                tx: 0x01,
154                rx: 0x01,
155            }))
156            .unwrap();
157        assert_eq!(p, vec![0xFF, 0x03, 0x91, 0x00, 0x01, 0x01, 0x62, 0x87]);
158    }
159
160    #[test]
161    fn async_stop_matches_document_example() {
162        let p = HostCommand::async_stop().unwrap();
163        assert_eq!(
164            p,
165            vec![
166                0xFF, 0x0E, 0xAA, 0x4D, 0x6F, 0x64, 0x75, 0x6C, 0x65, 0x74, 0x65, 0x63, 0x68, 0xAA,
167                0x49, 0xF3, 0xBB, 0x03, 0x91,
168            ]
169        );
170    }
171
172    #[test]
173    fn parse_version_info_ok() {
174        let data = [
175            0x13, 0x04, 0x15, 0x00, 0xA8, 0x00, 0x00, 0x01, 0x20, 0x13, 0x05, 0x22, 0x13, 0x05,
176            0x23, 0x00, 0x00, 0x00, 0x00, 0x10,
177        ];
178        let v = parse_version_info(&data).unwrap();
179        assert_eq!(v.supported_protocol, [0x00, 0x00, 0x00, 0x10]);
180    }
181
182    #[test]
183    fn parse_get_run_phase() {
184        assert_eq!(parse_run_phase(&[0x11]).unwrap(), RunPhase::Bootloader);
185        assert_eq!(parse_run_phase(&[0x12]).unwrap(), RunPhase::AppFirmware);
186    }
187
188    #[test]
189    fn parse_get_frequency_hopping_table() {
190        let data = [0x00, 0x0D, 0xF7, 0x32, 0x00, 0x0D, 0xC8, 0x52];
191        let freqs = parse_frequency_hopping_table(&data).unwrap();
192        assert_eq!(freqs, vec![915_250, 903_250]);
193    }
194
195    #[test]
196    fn parse_get_antenna_ports_power() {
197        let data = [0x03, 0x01, 0x0B, 0xB8, 0x0B, 0xB8];
198        let parsed = parse_antenna_ports_response(AntennaPortsOption::Power, &data).unwrap();
199        assert_eq!(
200            parsed,
201            AntennaPortsResponse::Power(vec![AntennaPower {
202                tx: 0x01,
203                read_power: 0x0BB8,
204                write_power: 0x0BB8,
205            }])
206        );
207    }
208
209    #[derive(Debug)]
210    struct TestTransport {
211        rx: VecDeque<u8>,
212        tx: Vec<u8>,
213    }
214
215    impl TestTransport {
216        fn from_frames(frames: Vec<Vec<u8>>) -> Self {
217            let mut rx = VecDeque::new();
218            for frame in frames {
219                rx.extend(frame);
220            }
221            Self { rx, tx: Vec::new() }
222        }
223    }
224
225    impl ReaderTransport for TestTransport {
226        type Error = &'static str;
227
228        async fn write_all(&mut self, data: &[u8]) -> Result<(), Self::Error> {
229            self.tx.extend_from_slice(data);
230            Ok(())
231        }
232
233        async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
234            if self.rx.len() < buf.len() {
235                return Err("eof");
236            }
237            for b in buf.iter_mut() {
238                *b = self.rx.pop_front().ok_or("eof")?;
239            }
240            Ok(())
241        }
242    }
243
244    fn frame(command: u8, status: u16, data: &[u8]) -> Vec<u8> {
245        let mut out = Vec::new();
246        out.push(0xFF);
247        out.push(data.len() as u8);
248        out.push(command);
249        out.extend_from_slice(&status.to_be_bytes());
250        out.extend_from_slice(data);
251        let crc = protocol_crc16(&out);
252        out.extend_from_slice(&crc.to_be_bytes());
253        out
254    }
255
256    #[test]
257    fn silion_reader_get_version_and_region() {
258        let version = frame(
259            CommandCode::GetVersion as u8,
260            0x0000,
261            &[
262                0x13, 0x04, 0x15, 0x00, 0xA8, 0x00, 0x00, 0x01, 0x20, 0x13, 0x05, 0x22, 0x13, 0x05,
263                0x23, 0x00, 0x00, 0x00, 0x00, 0x10,
264            ],
265        );
266        let region = frame(CommandCode::GetCurrentRegion as u8, 0x0000, &[0x01]);
267
268        let transport = TestTransport::from_frames(vec![version, region]);
269        let mut reader = SilionReader::new(transport);
270
271        let v = futures::executor::block_on(reader.get_version()).unwrap();
272        assert_eq!(v.supported_protocol, [0, 0, 0, 0x10]);
273        let r = futures::executor::block_on(reader.get_current_region()).unwrap();
274        assert_eq!(r, RegionCode::NorthAmerica);
275    }
276}