rfid-silion-compat 0.1.0

Async Rust library for Silion RFID readers with serial and Web Serial support
Documentation
#![deny(missing_docs)]

//! Host-side implementation of the Silion reader communication protocol.
//!
//! The implementation follows the packet format defined in:
//! <https://en.silion.com.cn/En/doc_center/ModuleAPI_Docs/Communication_Protocol_Doc/html/Protocol_Introduction.html>
//! and command pages linked from that document.
//!
//! This crate is centered around the high-level [`SilionReader`] API for common
//! reader operations (version, region, inventory, and tag access).
//!
//! See the project README for installation and feature setup.
//!
//! ## API Entry Points
//!
//! - [`SilionReader`]: high-level async reader operations.
//! - [`ReaderAsyncInventoryStartData`]: typed async inventory start configuration.
//! - [`SelectOption`]: high-level select/singulation input for inventory and access.
//! - [`AsyncInventoryMessage`]: typed pushed messages from async inventory.
//! - [`EpcValue`]: high-level EPC wrapper with EPC Tag URI helpers.
//!
//! ## Typical Workflow
//!
//! 1. Create a transport (native serial or web serial).
//! 2. Construct [`SilionReader`] with that transport.
//! 3. Call typed async methods and work with parsed return values.
//!
//! ## Example: Single-Tag Inventory (native serial)
//!
//! ```rust,ignore
//! use rfid_silion_compat::MetadataFlags;
//! use rfid_silion_compat::SelectOption;
//! use rfid_silion_compat::SerialTransport;
//! use rfid_silion_compat::SilionReader;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let transport = SerialTransport::open("/dev/ttyUSB0", 115_200)?;
//! let mut reader = SilionReader::new(transport);
//!
//! let metadata = MetadataFlags::default().with_rssi(true).with_antenna_id(true);
//! let tag = reader
//!     .single_tag_inventory(1000, SelectOption::Disabled, Some(metadata))
//!     .await?;
//!
//! println!("EPC: {:02X?}", tag.epc_id);
//! println!("RSSI dBm: {:?}", tag.rssi_dbm);
//! Ok(())
//! }
//! ```

mod async_proto;
mod client;
/// Protocol constants and enums for commands, regions, antennas, and statuses.
pub mod codes;
/// Low-level command payload types and host packet builders.
pub mod command;
/// EPC value types and EPC URI/binary helpers.
pub mod epc;
mod error;
/// Wire-frame encoding/decoding primitives for reader/host packets.
pub mod frame;
/// Response payload decoders and parsed output data models.
pub mod parsers;
mod session;
mod silion_reader;
mod transport;

/// Shared mock helpers used by rustdoc examples and unit tests.
#[doc(hidden)]
pub mod test_support;

#[cfg(feature = "serial")]
/// Serial port transport adapter backed by the `tokio-serial` crate.
pub mod serial;

#[cfg(all(target_arch = "wasm32", feature = "web-serial"))]
/// Web Serial transport adapter for browser targets.
pub mod web_serial;

#[cfg(all(target_arch = "wasm32", feature = "web-serial"))]
/// wasm-bindgen JavaScript bindings for the reader API.
pub mod web_bindings;

pub use client::{ClientError, ReaderClient};
pub use codes::{AntennaPortsOption, RegionCode, StatusCode};
pub use command::{
    AntennaPortsConfiguration, EmbeddedReadTagData, InventoryEmbeddedCommandContent,
    InventorySearchFlags, MemBank, MetadataFlags,
};
pub use epc::{EpcSchema, EpcValue, Giai96, Sgtin96};
pub use error::ProtocolError;
pub use frame::ReaderFrame;
pub use parsers::{
    AntennaPair, AntennaPortsResponse, AntennaPower, AntennaPowerSettling,
    ProtocolConfigurationValue, ReaderConfigurationValue, RegulatoryHopTime, RunPhase,
    SerialNumberInfo, TagEpcAndMetaData, VersionInfo,
};
#[cfg(feature = "serial")]
pub use serial::SerialTransport;
pub use session::AsyncInventorySession;
pub use silion_reader::{
    AsyncInventoryMessage, ReaderAsyncInventoryStartData, SelectOption, SilionReader,
};
pub use transport::ReaderTransport;

#[cfg(test)]
mod tests {
    use std::collections::VecDeque;

    use crate::codes::CommandCode;
    use crate::command::HostCommand;
    use crate::frame::{parse_reader_frame, protocol_crc16};
    use crate::parsers::{
        parse_antenna_ports_response, parse_frequency_hopping_table, parse_run_phase,
        parse_version_info,
    };

    use super::*;

    #[test]
    fn crc16_matches_doc_example_for_get_version() {
        let msg = [0xFF, 0x00, 0x03];
        assert_eq!(protocol_crc16(&msg), 0x1D0C);
    }

    #[test]
    fn build_get_version_packet_matches_example() {
        let p = HostCommand::get_version().unwrap();
        assert_eq!(p, vec![0xFF, 0x00, 0x03, 0x1D, 0x0C]);
    }

    #[test]
    fn parse_reader_frame_success() {
        let packet = [0xFF, 0x01, 0x0C, 0x00, 0x00, 0x12, 0x63, 0x43];
        let f = parse_reader_frame(&packet).unwrap();
        assert_eq!(f.command, 0x0C);
        assert_eq!(f.status_raw, 0x0000);
        assert_eq!(f.status, Some(StatusCode::Success));
        assert_eq!(f.data, vec![0x12]);
    }

    #[test]
    fn build_set_current_region_packet() {
        let p = HostCommand::set_current_region(RegionCode::NorthAmerica).unwrap();
        assert_eq!(p, vec![0xFF, 0x01, 0x97, 0x01, 0x4B, 0xBC]);
    }

    #[test]
    fn build_set_antenna_access_pair_packet() {
        let p =
            HostCommand::set_antenna_ports(&AntennaPortsConfiguration::AccessPair(AntennaPair {
                tx: 0x01,
                rx: 0x01,
            }))
            .unwrap();
        assert_eq!(p, vec![0xFF, 0x03, 0x91, 0x00, 0x01, 0x01, 0x62, 0x87]);
    }

    #[test]
    fn async_stop_matches_document_example() {
        let p = HostCommand::async_stop().unwrap();
        assert_eq!(
            p,
            vec![
                0xFF, 0x0E, 0xAA, 0x4D, 0x6F, 0x64, 0x75, 0x6C, 0x65, 0x74, 0x65, 0x63, 0x68, 0xAA,
                0x49, 0xF3, 0xBB, 0x03, 0x91,
            ]
        );
    }

    #[test]
    fn parse_version_info_ok() {
        let data = [
            0x13, 0x04, 0x15, 0x00, 0xA8, 0x00, 0x00, 0x01, 0x20, 0x13, 0x05, 0x22, 0x13, 0x05,
            0x23, 0x00, 0x00, 0x00, 0x00, 0x10,
        ];
        let v = parse_version_info(&data).unwrap();
        assert_eq!(v.supported_protocol, [0x00, 0x00, 0x00, 0x10]);
    }

    #[test]
    fn parse_get_run_phase() {
        assert_eq!(parse_run_phase(&[0x11]).unwrap(), RunPhase::Bootloader);
        assert_eq!(parse_run_phase(&[0x12]).unwrap(), RunPhase::AppFirmware);
    }

    #[test]
    fn parse_get_frequency_hopping_table() {
        let data = [0x00, 0x0D, 0xF7, 0x32, 0x00, 0x0D, 0xC8, 0x52];
        let freqs = parse_frequency_hopping_table(&data).unwrap();
        assert_eq!(freqs, vec![915_250, 903_250]);
    }

    #[test]
    fn parse_get_antenna_ports_power() {
        let data = [0x03, 0x01, 0x0B, 0xB8, 0x0B, 0xB8];
        let parsed = parse_antenna_ports_response(AntennaPortsOption::Power, &data).unwrap();
        assert_eq!(
            parsed,
            AntennaPortsResponse::Power(vec![AntennaPower {
                tx: 0x01,
                read_power: 0x0BB8,
                write_power: 0x0BB8,
            }])
        );
    }

    #[derive(Debug)]
    struct TestTransport {
        rx: VecDeque<u8>,
        tx: Vec<u8>,
    }

    impl TestTransport {
        fn from_frames(frames: Vec<Vec<u8>>) -> Self {
            let mut rx = VecDeque::new();
            for frame in frames {
                rx.extend(frame);
            }
            Self { rx, tx: Vec::new() }
        }
    }

    impl ReaderTransport for TestTransport {
        type Error = &'static str;

        async fn write_all(&mut self, data: &[u8]) -> Result<(), Self::Error> {
            self.tx.extend_from_slice(data);
            Ok(())
        }

        async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
            if self.rx.len() < buf.len() {
                return Err("eof");
            }
            for b in buf.iter_mut() {
                *b = self.rx.pop_front().ok_or("eof")?;
            }
            Ok(())
        }
    }

    fn frame(command: u8, status: u16, data: &[u8]) -> Vec<u8> {
        let mut out = Vec::new();
        out.push(0xFF);
        out.push(data.len() as u8);
        out.push(command);
        out.extend_from_slice(&status.to_be_bytes());
        out.extend_from_slice(data);
        let crc = protocol_crc16(&out);
        out.extend_from_slice(&crc.to_be_bytes());
        out
    }

    #[test]
    fn silion_reader_get_version_and_region() {
        let version = frame(
            CommandCode::GetVersion as u8,
            0x0000,
            &[
                0x13, 0x04, 0x15, 0x00, 0xA8, 0x00, 0x00, 0x01, 0x20, 0x13, 0x05, 0x22, 0x13, 0x05,
                0x23, 0x00, 0x00, 0x00, 0x00, 0x10,
            ],
        );
        let region = frame(CommandCode::GetCurrentRegion as u8, 0x0000, &[0x01]);

        let transport = TestTransport::from_frames(vec![version, region]);
        let mut reader = SilionReader::new(transport);

        let v = futures::executor::block_on(reader.get_version()).unwrap();
        assert_eq!(v.supported_protocol, [0, 0, 0, 0x10]);
        let r = futures::executor::block_on(reader.get_current_region()).unwrap();
        assert_eq!(r, RegionCode::NorthAmerica);
    }
}