Skip to main content

rfid_silion_compat/
silion_reader.rs

1use crate::async_proto::parse_async_payload_owned;
2use crate::client::{ClientError, ReaderClient};
3use crate::codes::{AntennaPortsOption, CommandCode, RegionCode};
4use crate::command::{
5    AntennaPortsConfiguration, AsyncInventoryStartData as CommandAsyncInventoryStartData,
6    AsyncSubcommandCode, InventoryOption, InventorySearchFlags, MemBank, MetadataFlags,
7    SelectContent, SelectMode, SelectOptionBits,
8};
9use crate::error::ProtocolError;
10use crate::parsers::{
11    AntennaPortsResponse, ProtocolConfigurationValue, ReaderConfigurationValue, RegulatoryHopTime,
12    RunPhase, SerialNumberInfo, TagEpcAndMetaData, VersionInfo, parse_antenna_ports_response,
13    parse_available_regions, parse_current_region, parse_current_tag_protocol,
14    parse_current_temperature, parse_frequency_hopping_table, parse_pin_states,
15    parse_protocol_configuration_value, parse_reader_configuration_value,
16    parse_regulatory_hop_time, parse_run_phase, parse_serial_number_info,
17    parse_single_tag_inventory_response, parse_tag_epc_and_meta_data, parse_version_info,
18};
19use crate::session::AsyncInventorySession;
20use crate::transport::ReaderTransport;
21
22/// High-level reader API that returns typed values for common protocol operations.
23pub struct SilionReader<T: ReaderTransport> {
24    client: ReaderClient<T>,
25}
26
27/// One asynchronous inventory message pushed by the reader.
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "web-serial", derive(serde::Serialize))]
30#[cfg_attr(feature = "web-serial", serde(tag = "kind", rename_all = "camelCase"))]
31pub enum AsyncInventoryMessage {
32    /// Reader reply for Start AsyncInventory (`0xAA48`), no subcommand data.
33    StartAck,
34    /// Reader reply for Stop AsyncInventory (`0xAA49`), no subcommand data.
35    StopAck,
36    /// Unrequested tag information packet.
37    TagInformation {
38        /// Metadata flags echoed back from the start command.
39        metadata_flags: MetadataFlags,
40        /// Parsed tag EPC and metadata block.
41        tag: TagEpcAndMetaData,
42    },
43    /// Unrequested heartbeat packet.
44    Heartbeat {
45        /// Search flags from heartbeat payload.
46        search_flags: InventorySearchFlags,
47        /// State data bytes after search flags.
48        state_data: Vec<u8>,
49    },
50    /// Unrecognized asynchronous subcommand payload wrapped by `Moduletech` marker.
51    Subcommand {
52        /// Parsed asynchronous inventory subcommand code.
53        subcommand: u16,
54        /// Subcommand-specific payload bytes.
55        subcommand_data: Vec<u8>,
56    },
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60#[cfg_attr(feature = "web-serial", derive(serde::Serialize))]
61#[cfg_attr(feature = "web-serial", derive(serde::Deserialize))]
62#[cfg_attr(feature = "web-serial", serde(tag = "type", rename_all = "camelCase"))]
63/// Tag singulation/select options used by inventory and access helpers.
64pub enum SelectOption {
65    /// Disable select/singulation and operate on the first matching tag.
66    Disabled,
67    /// Select against EPC data.
68    Epc {
69        /// Number of select bits.
70        select_length_bits: u16,
71        /// Raw select bytes.
72        select_data: Vec<u8>,
73        /// Invert select result (match non-equal tags).
74        invert: bool,
75    },
76    /// Select against TID memory bank.
77    Tid {
78        /// Bit address within the bank.
79        select_address: u32,
80        /// Number of select bits.
81        select_length_bits: u16,
82        /// Raw select bytes.
83        select_data: Vec<u8>,
84        /// Invert select result (match non-equal tags).
85        invert: bool,
86    },
87    /// Select against User memory bank.
88    UserMemory {
89        /// Bit address within the bank.
90        select_address: u32,
91        /// Number of select bits.
92        select_length_bits: u16,
93        /// Raw select bytes.
94        select_data: Vec<u8>,
95        /// Invert select result (match non-equal tags).
96        invert: bool,
97    },
98    /// Select against EPC memory bank (Gen2 bank 0x01).
99    EpcBank {
100        /// Bit address within the bank.
101        select_address: u32,
102        /// Number of select bits.
103        select_length_bits: u16,
104        /// Raw select bytes.
105        select_data: Vec<u8>,
106        /// Invert select result (match non-equal tags).
107        invert: bool,
108    },
109    /// Send only access password without select content.
110    PasswordOnly,
111}
112
113impl SelectOption {
114    fn into_option_content(self) -> (InventoryOption, Option<SelectContent>) {
115        match self {
116            SelectOption::Disabled => (InventoryOption::default(), None),
117            SelectOption::Epc {
118                select_length_bits,
119                select_data,
120                invert,
121            } => (
122                SelectOptionBits::new(SelectMode::Epc)
123                    .with_invert_flag(invert)
124                    .with_extended_data_length(select_length_bits > 255)
125                    .into(),
126                Some(SelectContent {
127                    address_bits: 0,
128                    bit_len: select_length_bits,
129                    data: select_data,
130                }),
131            ),
132            SelectOption::Tid {
133                select_address,
134                select_length_bits,
135                select_data,
136                invert,
137            } => (
138                SelectOptionBits::new(SelectMode::Tid)
139                    .with_invert_flag(invert)
140                    .with_extended_data_length(select_length_bits > 255)
141                    .into(),
142                Some(SelectContent {
143                    address_bits: select_address,
144                    bit_len: select_length_bits,
145                    data: select_data,
146                }),
147            ),
148            SelectOption::UserMemory {
149                select_address,
150                select_length_bits,
151                select_data,
152                invert,
153            } => (
154                SelectOptionBits::new(SelectMode::UserMemory)
155                    .with_invert_flag(invert)
156                    .with_extended_data_length(select_length_bits > 255)
157                    .into(),
158                Some(SelectContent {
159                    address_bits: select_address,
160                    bit_len: select_length_bits,
161                    data: select_data,
162                }),
163            ),
164            SelectOption::EpcBank {
165                select_address,
166                select_length_bits,
167                select_data,
168                invert,
169            } => (
170                SelectOptionBits::new(SelectMode::EpcBank)
171                    .with_invert_flag(invert)
172                    .with_extended_data_length(select_length_bits > 255)
173                    .into(),
174                Some(SelectContent {
175                    address_bits: select_address,
176                    bit_len: select_length_bits,
177                    data: select_data,
178                }),
179            ),
180            SelectOption::PasswordOnly => {
181                (SelectOptionBits::new(SelectMode::PasswordOnly).into(), None)
182            }
183        }
184    }
185}
186
187/// Start configuration for asynchronous inventory.
188///
189/// This wrapper uses [`SelectOption`] so callers do not need to manually map
190/// to low-level `InventoryOption + SelectContent` fields.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct ReaderAsyncInventoryStartData {
193    /// Metadata flags controlling which per-tag fields the reader returns.
194    pub metadata_flags: MetadataFlags,
195    /// Tag singulation/select configuration.
196    pub select_option: SelectOption,
197    /// Search flags (2 bytes), same meaning as command `0x22`.
198    pub search_flags: InventorySearchFlags,
199    /// Optional access password (4 bytes) when required by option.
200    pub access_password: Option<u32>,
201    /// Optional typed embedded command content.
202    pub embedded_command_content: Option<crate::InventoryEmbeddedCommandContent>,
203}
204
205impl ReaderAsyncInventoryStartData {
206    fn to_command_data(&self) -> CommandAsyncInventoryStartData {
207        let (option, select_content) = self.select_option.clone().into_option_content();
208        CommandAsyncInventoryStartData {
209            metadata_flags: self.metadata_flags,
210            option,
211            search_flags: self.search_flags,
212            access_password: self.access_password,
213            select_content,
214            embedded_command_content: self.embedded_command_content.clone(),
215        }
216    }
217}
218
219impl<T: ReaderTransport> SilionReader<T> {
220    /// Create a high-level reader API from a transport.
221    pub fn new(transport: T) -> Self {
222        Self {
223            client: ReaderClient::new(transport),
224        }
225    }
226
227    /// Create a high-level reader API from an existing low-level client.
228    pub fn from_client(client: ReaderClient<T>) -> Self {
229        Self { client }
230    }
231
232    /// Consume the reader API and return the wrapped transport.
233    pub fn into_inner(self) -> T {
234        self.client.into_inner()
235    }
236
237    /// Return a mutable reference to the wrapped transport.
238    pub fn transport_mut(&mut self) -> &mut T {
239        self.client.transport_mut()
240    }
241
242    /// Consume the reader API and return the wrapped low-level client.
243    pub fn into_client(self) -> ReaderClient<T> {
244        self.client
245    }
246
247    /// Run a raw command transaction and return the validated response frame.
248    ///
249    /// This is useful for callers that need command coverage beyond the typed
250    /// helper methods on `SilionReader`.
251    pub async fn transact_raw(
252        &mut self,
253        command: u8,
254        data: &[u8],
255    ) -> Result<crate::ReaderFrame, ClientError<T::Error>> {
256        self.client.transact(command, data).await
257    }
258
259    /// Convert this host into an awaited-read [`AsyncInventorySession`].
260    pub fn into_async_session(self) -> AsyncInventorySession<T> {
261        AsyncInventorySession::new(self.client)
262    }
263
264    /// Run command `0x03` (Get Version) and parse version fields.
265    pub async fn get_version(&mut self) -> Result<VersionInfo, ClientError<T::Error>> {
266        let frame = self
267            .client
268            .transact(CommandCode::GetVersion as u8, &[])
269            .await?;
270        parse_version_info(&frame.data).map_err(ClientError::Protocol)
271    }
272
273    /// Run command `0x04` (Boot Firmware).
274    pub async fn boot_firmware(&mut self) -> Result<(), ClientError<T::Error>> {
275        let _ = self
276            .client
277            .transact(CommandCode::BootFirmware as u8, &[])
278            .await?;
279        Ok(())
280    }
281
282    /// Run command `0x09` (Boot Bootloader).
283    pub async fn boot_bootloader(&mut self) -> Result<(), ClientError<T::Error>> {
284        let _ = self
285            .client
286            .transact(CommandCode::BootBootloader as u8, &[])
287            .await?;
288        Ok(())
289    }
290
291    /// Run command `0x0C` (Get Run Phase) and parse phase enum.
292    pub async fn get_run_phase(&mut self) -> Result<RunPhase, ClientError<T::Error>> {
293        let frame = self
294            .client
295            .transact(CommandCode::GetRunPhase as u8, &[])
296            .await?;
297        parse_run_phase(&frame.data).map_err(ClientError::Protocol)
298    }
299
300    /// Run command `0x10` (Get Serial Number).
301    pub async fn get_serial_number(
302        &mut self,
303        option: u8,
304        data_flags: u8,
305    ) -> Result<SerialNumberInfo, ClientError<T::Error>> {
306        let frame = self
307            .client
308            .transact(CommandCode::GetSerialNumber as u8, &[option, data_flags])
309            .await?;
310        parse_serial_number_info(&frame.data).map_err(ClientError::Protocol)
311    }
312
313    /// Run command `0x63` (Get Current Tag Protocol).
314    pub async fn get_current_tag_protocol(&mut self) -> Result<u16, ClientError<T::Error>> {
315        let frame = self
316            .client
317            .transact(CommandCode::GetCurrentTagProtocol as u8, &[])
318            .await?;
319        parse_current_tag_protocol(&frame.data).map_err(ClientError::Protocol)
320    }
321
322    /// Run command `0x97` (Set Current Region).
323    pub async fn set_current_region(
324        &mut self,
325        region_code: RegionCode,
326    ) -> Result<(), ClientError<T::Error>> {
327        let _ = self
328            .client
329            .transact(CommandCode::SetCurrentRegion as u8, &[region_code.as_u8()])
330            .await?;
331        Ok(())
332    }
333
334    /// Run command `0x67` (Get Current Region).
335    pub async fn get_current_region(&mut self) -> Result<RegionCode, ClientError<T::Error>> {
336        let frame = self
337            .client
338            .transact(CommandCode::GetCurrentRegion as u8, &[])
339            .await?;
340        parse_current_region(&frame.data).map_err(ClientError::Protocol)
341    }
342
343    /// Run command `0x71` (Get Available Regions).
344    pub async fn get_available_regions(
345        &mut self,
346    ) -> Result<Vec<RegionCode>, ClientError<T::Error>> {
347        let frame = self
348            .client
349            .transact(CommandCode::GetAvailableRegions as u8, &[])
350            .await?;
351        parse_available_regions(&frame.data).map_err(ClientError::Protocol)
352    }
353
354    /// Run command `0x72` (Get Current Temperature).
355    pub async fn get_current_temperature(&mut self) -> Result<u8, ClientError<T::Error>> {
356        let frame = self
357            .client
358            .transact(CommandCode::GetCurrentTemperature as u8, &[])
359            .await?;
360        parse_current_temperature(&frame.data).map_err(ClientError::Protocol)
361    }
362
363    /// Run command `0x66` (Get GPI) and return pin states.
364    pub async fn get_gpi(&mut self) -> Result<Vec<u8>, ClientError<T::Error>> {
365        let frame = self.client.transact(CommandCode::GetGpi as u8, &[]).await?;
366        parse_pin_states(&frame.data).map_err(ClientError::Protocol)
367    }
368
369    /// Run command `0x96` with empty payload to read GPO states.
370    pub async fn get_gpo_states(&mut self) -> Result<Vec<u8>, ClientError<T::Error>> {
371        let frame = self.client.transact(CommandCode::SetGpo as u8, &[]).await?;
372        parse_pin_states(&frame.data).map_err(ClientError::Protocol)
373    }
374
375    /// Run command `0x91` (Set Antenna Ports).
376    pub async fn set_antenna_ports(
377        &mut self,
378        config: &AntennaPortsConfiguration,
379    ) -> Result<(), ClientError<T::Error>> {
380        let packet = crate::command::HostCommand::set_antenna_ports(config)
381            .map_err(ClientError::Protocol)?;
382        let _ = self.client.transact_frame(&packet).await?;
383        Ok(())
384    }
385
386    /// Send command `0xAA48` to enable asynchronous inventory.
387    pub async fn enable_async_inventory(
388        &mut self,
389        start: &ReaderAsyncInventoryStartData,
390    ) -> Result<(), ClientError<T::Error>> {
391        let command_data = start.to_command_data();
392        let packet = crate::command::HostCommand::async_start(&command_data)
393            .map_err(ClientError::Protocol)?;
394        let response = self.client.transact_frame(&packet).await?;
395        if response.command != CommandCode::AsynchronousInventory as u8 {
396            return Err(ClientError::UnexpectedResponseCommand {
397                expected: CommandCode::AsynchronousInventory as u8,
398                actual: response.command,
399            });
400        }
401        if response.status_raw != 0x0000 {
402            return Err(ClientError::ReaderStatus {
403                status_raw: response.status_raw,
404                status: response.status,
405            });
406        }
407        Ok(())
408    }
409
410    /// Send command `0xAA49` to disable asynchronous inventory.
411    pub async fn disable_async_inventory(&mut self) -> Result<(), ClientError<T::Error>> {
412        let packet = crate::command::HostCommand::async_stop().map_err(ClientError::Protocol)?;
413        let response = self.client.transact_frame(&packet).await?;
414        if response.command != CommandCode::AsynchronousInventory as u8 {
415            return Err(ClientError::UnexpectedResponseCommand {
416                expected: CommandCode::AsynchronousInventory as u8,
417                actual: response.command,
418            });
419        }
420        if response.status_raw != 0x0000 {
421            return Err(ClientError::ReaderStatus {
422                status_raw: response.status_raw,
423                status: response.status,
424            });
425        }
426        Ok(())
427    }
428
429    /// Receive one pushed asynchronous inventory message from the reader.
430    pub async fn recv_async_inventory_message(
431        &mut self,
432    ) -> Result<AsyncInventoryMessage, ClientError<T::Error>> {
433        let frame = self.client.read_frame().await?;
434        if frame.command != CommandCode::AsynchronousInventory as u8 {
435            return Err(ClientError::UnexpectedResponseCommand {
436                expected: CommandCode::AsynchronousInventory as u8,
437                actual: frame.command,
438            });
439        }
440        if frame.status_raw != 0x0000 {
441            return Err(ClientError::ReaderStatus {
442                status_raw: frame.status_raw,
443                status: frame.status,
444            });
445        }
446        parse_async_frame_data(&frame.data).map_err(ClientError::Protocol)
447    }
448
449    /// Perform a single tag inventory read using command `0x21` (Single Tag Inventory).
450    ///
451    /// This command inventories one tag within the specified timeout and returns the
452    /// parsed tag data with requested metadata.
453    ///
454    /// # Arguments
455    ///
456    /// * `timeout_ms` - Maximum time to wait for a tag response in milliseconds
457    /// * `option` - Select option flags (see [`SelectOptionBits`])
458    /// * `metadata_flags` - Which metadata fields to include in the response
459    /// * `select_content` - Optional tag singulation/select rule
460    ///
461    /// Returns the tag EPC and metadata, or an error if no tag is found or timeout occurs.
462    pub async fn single_tag_inventory(
463        &mut self,
464        timeout_ms: u16,
465        select_option: SelectOption,
466        metadata_flags: Option<MetadataFlags>,
467    ) -> Result<TagEpcAndMetaData, ClientError<T::Error>> {
468        // Derive option bit 4 (`0x10`) from metadata presence.
469        let (option, select_content) = select_option.into_option_content();
470        let option = option.with_single_tag_metadata(metadata_flags.is_some());
471
472        let packet = crate::command::HostCommand::single_tag_inventory(
473            timeout_ms,
474            option,
475            metadata_flags,
476            select_content,
477        )
478        .map_err(ClientError::Protocol)?;
479
480        let frame = self.client.transact_frame(&packet).await?;
481        if frame.command != CommandCode::SingleTagInventory as u8 {
482            return Err(ClientError::UnexpectedResponseCommand {
483                expected: CommandCode::SingleTagInventory as u8,
484                actual: frame.command,
485            });
486        }
487        if frame.status_raw != 0x0000 {
488            return Err(ClientError::ReaderStatus {
489                status_raw: frame.status_raw,
490                status: frame.status,
491            });
492        }
493
494        let (_response_metadata_flags, tag) =
495            parse_single_tag_inventory_response(option.raw(), &frame.data)
496                .map_err(ClientError::Protocol)?;
497        Ok(tag)
498    }
499
500    /// Run command `0x28` (Read Tag Data) and parse the returned tag payload.
501    ///
502    /// This helper performs a single-tag read against the requested memory bank,
503    /// optionally including metadata fields in the response.
504    ///
505    /// # Arguments
506    ///
507    /// * `timeout_ms` - Maximum time to wait for a tag response in milliseconds
508    /// * `select_option` - Tag singulation/select rule applied before the read
509    /// * `metadata_flags` - Optional metadata fields to request alongside tag data
510    /// * `read_membank` - Memory bank to read from (Gen2 0x01=EPC, 0x02=TID, 0x03=User)
511    /// * `read_address_words` - Start address in 16-bit words within `read_membank`
512    /// * `word_count` - Number of 16-bit words to read (protocol range `1..=96`)
513    ///
514    /// Returns parsed tag data and metadata when enabled, or an error if the command
515    /// fails, times out, or the reader returns a non-success status.
516    pub async fn read_tag_data(
517        &mut self,
518        timeout_ms: u16,
519        select_option: SelectOption,
520        metadata_flags: Option<MetadataFlags>,
521        read_membank: MemBank,
522        read_address_words: u32,
523        word_count: u8,
524    ) -> Result<TagEpcAndMetaData, ClientError<T::Error>> {
525        let (option, select_content) = select_option.into_option_content();
526        let option = option.with_single_tag_metadata(metadata_flags.is_some());
527
528        let packet = crate::command::HostCommand::read_tag_data(
529            timeout_ms,
530            option,
531            metadata_flags,
532            read_membank,
533            read_address_words,
534            word_count,
535            None,
536            select_content,
537        )
538        .map_err(ClientError::Protocol)?;
539
540        let frame = self.client.transact_frame(&packet).await?;
541        if frame.command != CommandCode::ReadTagData as u8 {
542            return Err(ClientError::UnexpectedResponseCommand {
543                expected: CommandCode::ReadTagData as u8,
544                actual: frame.command,
545            });
546        }
547        if frame.status_raw != 0x0000 {
548            return Err(ClientError::ReaderStatus {
549                status_raw: frame.status_raw,
550                status: frame.status,
551            });
552        }
553
554        let returned_options = InventoryOption::from_raw(frame.data[0]);
555        if returned_options.single_tag_metadata_enabled() {
556            return parse_tag_epc_and_meta_data(
557                metadata_flags.unwrap_or(MetadataFlags::NONE),
558                &frame.data,
559            )
560            .map_err(ClientError::Protocol);
561        } else {
562            let frame_data = frame.data[1..].to_vec();
563            return Ok(TagEpcAndMetaData {
564                read_count: None,
565                rssi_dbm: None,
566                antenna_id: None,
567                frequency_khz: None,
568                timestamp_ms: None,
569                rfu: None,
570                protocol_id: None,
571                tag_data_bit_length: Some(frame_data.len() as u16 * 8),
572                tag_data: Some(frame_data),
573                epc_bit_length: None,
574                pc_word: None,
575                epc_id: Vec::new(),
576                tag_crc: 0,
577            });
578        }
579    }
580
581    /// Run command `0x23` (Write Tag EPC).
582    ///
583    /// This helper writes a new EPC to a selected tag. The reader updates EPC
584    /// length bits in the PC word according to the provided EPC payload.
585    ///
586    /// # Arguments
587    ///
588    /// * `timeout_ms` - Maximum time to wait for a tag response in milliseconds
589    /// * `select_option` - Tag singulation/select rule applied before the write
590    /// * `access_password` - Optional access password when required by the tag
591    /// * `epc` - New EPC bytes to write
592    pub async fn write_tag_epc(
593        &mut self,
594        timeout_ms: u16,
595        select_option: SelectOption,
596        access_password: Option<u32>,
597        epc: &[u8],
598    ) -> Result<(), ClientError<T::Error>> {
599        let (option, select_content) = select_option.into_option_content();
600        let packet = crate::command::HostCommand::write_tag_epc(
601            timeout_ms,
602            option,
603            access_password,
604            select_content,
605            epc,
606        )
607        .map_err(ClientError::Protocol)?;
608
609        let frame = self.client.transact_frame(&packet).await?;
610        if frame.command != CommandCode::WriteTagEpc as u8 {
611            return Err(ClientError::UnexpectedResponseCommand {
612                expected: CommandCode::WriteTagEpc as u8,
613                actual: frame.command,
614            });
615        }
616        if frame.status_raw != 0x0000 {
617            return Err(ClientError::ReaderStatus {
618                status_raw: frame.status_raw,
619                status: frame.status,
620            });
621        }
622        Ok(())
623    }
624
625    /// Run command `0x24` (Write Tag Data).
626    ///
627    /// This helper writes bytes to a selected memory bank and word address on
628    /// a selected tag.
629    ///
630    /// # Arguments
631    ///
632    /// * `timeout_ms` - Maximum time to wait for a tag response in milliseconds
633    /// * `select_option` - Tag singulation/select rule applied before the write
634    /// * `write_address_words` - Start address in 16-bit words within `write_membank`
635    /// * `write_membank` - Memory bank to write (Gen2 0x01=EPC, 0x02=TID, 0x03=User)
636    /// * `access_password` - Optional access password when required by the tag
637    /// * `write_data` - Bytes to write (must be non-empty and even-length)
638    pub async fn write_tag_data(
639        &mut self,
640        timeout_ms: u16,
641        select_option: SelectOption,
642        write_address_words: u32,
643        write_membank: MemBank,
644        access_password: Option<u32>,
645        write_data: &[u8],
646    ) -> Result<(), ClientError<T::Error>> {
647        let (option, select_content) = select_option.into_option_content();
648        let packet = crate::command::HostCommand::write_tag_data(
649            timeout_ms,
650            option,
651            write_address_words,
652            write_membank,
653            access_password,
654            select_content,
655            write_data,
656        )
657        .map_err(ClientError::Protocol)?;
658
659        let frame = self.client.transact_frame(&packet).await?;
660        if frame.command != CommandCode::WriteTagData as u8 {
661            return Err(ClientError::UnexpectedResponseCommand {
662                expected: CommandCode::WriteTagData as u8,
663                actual: frame.command,
664            });
665        }
666        if frame.status_raw != 0x0000 {
667            return Err(ClientError::ReaderStatus {
668                status_raw: frame.status_raw,
669                status: frame.status,
670            });
671        }
672        Ok(())
673    }
674
675    /// Run command `0x65` (Get Frequency Hopping table form).
676    pub async fn get_frequency_hopping_table(&mut self) -> Result<Vec<u32>, ClientError<T::Error>> {
677        let frame = self
678            .client
679            .transact(CommandCode::GetFrequencyHopping as u8, &[])
680            .await?;
681        parse_frequency_hopping_table(&frame.data).map_err(ClientError::Protocol)
682    }
683
684    /// Run command `0x65` with option `0x01` (Regulatory Hopping Time).
685    pub async fn get_regulatory_hop_time(
686        &mut self,
687    ) -> Result<RegulatoryHopTime, ClientError<T::Error>> {
688        let frame = self
689            .client
690            .transact(CommandCode::GetFrequencyHopping as u8, &[0x01])
691            .await?;
692        parse_regulatory_hop_time(&frame.data).map_err(ClientError::Protocol)
693    }
694
695    /// Run command `0x61` (Get Antenna Ports) and decode by option.
696    pub async fn get_antenna_ports(
697        &mut self,
698        option: AntennaPortsOption,
699    ) -> Result<AntennaPortsResponse, ClientError<T::Error>> {
700        let frame = self
701            .client
702            .transact(CommandCode::GetAntennaPorts as u8, &[option.as_u8()])
703            .await?;
704        parse_antenna_ports_response(option, &frame.data).map_err(ClientError::Protocol)
705    }
706
707    /// Run command `0x6A` (Get Reader Configuration).
708    pub async fn get_reader_configuration(
709        &mut self,
710        option: u8,
711        key: u8,
712    ) -> Result<ReaderConfigurationValue, ClientError<T::Error>> {
713        let frame = self
714            .client
715            .transact(CommandCode::GetReaderConfiguration as u8, &[option, key])
716            .await?;
717        parse_reader_configuration_value(&frame.data).map_err(ClientError::Protocol)
718    }
719
720    /// Run command `0x6B` (Get Protocol Configuration).
721    pub async fn get_protocol_configuration(
722        &mut self,
723        protocol_value: u8,
724        parameter: u8,
725    ) -> Result<ProtocolConfigurationValue, ClientError<T::Error>> {
726        let frame = self
727            .client
728            .transact(
729                CommandCode::GetProtocolConfiguration as u8,
730                &[protocol_value, parameter],
731            )
732            .await?;
733        parse_protocol_configuration_value(&frame.data).map_err(ClientError::Protocol)
734    }
735}
736
737/// Parse the data bytes of a validated `0xAA` asynchronous inventory frame
738/// into a typed [`AsyncInventoryMessage`].
739pub(crate) fn parse_async_frame_data(data: &[u8]) -> Result<AsyncInventoryMessage, ProtocolError> {
740    if data.starts_with(b"Moduletech") {
741        let payload = parse_async_payload_owned(data)?;
742        return Ok(match payload.subcommand {
743            x if x == AsyncSubcommandCode::Start as u16 => AsyncInventoryMessage::StartAck,
744            x if x == AsyncSubcommandCode::Stop as u16 => AsyncInventoryMessage::StopAck,
745            _ => AsyncInventoryMessage::Subcommand {
746                subcommand: payload.subcommand,
747                subcommand_data: payload.subcommand_data,
748            },
749        });
750    }
751
752    if data.starts_with(b"XTSJ") {
753        if data.len() < 6 {
754            return Err(ProtocolError::InvalidResponse(
755                "heartbeat payload too short",
756            ));
757        }
758        let search_flags = InventorySearchFlags::from_raw(u16::from_be_bytes([data[4], data[5]]));
759        return Ok(AsyncInventoryMessage::Heartbeat {
760            search_flags,
761            state_data: data[6..].to_vec(),
762        });
763    }
764
765    if data.len() < 2 {
766        return Err(ProtocolError::InvalidResponse(
767            "tag information payload too short",
768        ));
769    }
770
771    let metadata_flags = MetadataFlags::from_raw(u16::from_be_bytes([data[0], data[1]]));
772
773    Ok(AsyncInventoryMessage::TagInformation {
774        metadata_flags,
775        tag: parse_tag_epc_and_meta_data(metadata_flags, &data[2..])?,
776    })
777}
778
779#[cfg(test)]
780mod tests {
781    use super::SilionReader;
782    use crate::codes::CommandCode;
783    use crate::test_support::{MockInteraction, MockTransport, reply_frame};
784    use crate::{AsyncInventoryMessage, InventorySearchFlags, RegionCode};
785
786    #[test]
787    fn get_version_and_region() {
788        let version_data = vec![
789            0x13, 0x04, 0x15, 0x00, 0xA8, 0x00, 0x00, 0x01, 0x20, 0x13, 0x05, 0x22, 0x13, 0x05,
790            0x23, 0x00, 0x00, 0x00, 0x00, 0x10,
791        ];
792        let transport = MockTransport::scripted(vec![
793            MockInteraction {
794                request_command: CommandCode::GetVersion as u8,
795                response_status: 0x0000,
796                response_data: version_data,
797            },
798            MockInteraction {
799                request_command: CommandCode::GetCurrentRegion as u8,
800                response_status: 0x0000,
801                response_data: vec![0x01],
802            },
803        ]);
804
805        let mut reader = SilionReader::new(transport);
806        let version =
807            futures::executor::block_on(reader.get_version()).expect("version should parse");
808        assert_eq!(version.supported_protocol, [0x00, 0x00, 0x00, 0x10]);
809
810        let region =
811            futures::executor::block_on(reader.get_current_region()).expect("region should parse");
812        assert_eq!(region, RegionCode::NorthAmerica);
813    }
814
815    #[test]
816    fn recv_async_inventory_heartbeat() {
817        let mut data = b"XTSJ".to_vec();
818        data.extend_from_slice(&0x8000u16.to_be_bytes());
819        data.push(0x01);
820        let packet = reply_frame(0xAA, 0x0000, &data);
821        let transport = MockTransport::from_replies(vec![packet]);
822
823        let mut reader = SilionReader::new(transport);
824        let message = futures::executor::block_on(reader.recv_async_inventory_message())
825            .expect("async message should parse");
826
827        match message {
828            AsyncInventoryMessage::Heartbeat {
829                search_flags,
830                state_data,
831            } => {
832                assert_eq!(search_flags, InventorySearchFlags::from_raw(0x8000));
833                assert_eq!(state_data, vec![0x01]);
834            }
835            other => panic!("unexpected async message: {other:?}"),
836        }
837    }
838
839    #[test]
840    #[cfg(feature = "web-serial")]
841    fn tag_information_serialization() {
842        use crate::command::MetadataFlags;
843        use crate::parsers::TagEpcAndMetaData;
844
845        let tag = TagEpcAndMetaData {
846            read_count: Some(1),
847            rssi_dbm: Some(-50),
848            antenna_id: Some(1),
849            frequency_khz: Some(902250),
850            timestamp_ms: Some(1000),
851            rfu: None,
852            protocol_id: Some(5),
853            tag_data_bit_length: None,
854            tag_data: None,
855            epc_bit_length: 96,
856            pc_word: 0x3000,
857            epc_id: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06],
858            tag_crc: 0x1234,
859        };
860
861        let msg = AsyncInventoryMessage::TagInformation {
862            metadata_flags: MetadataFlags::from_raw(0x00FF),
863            tag,
864        };
865
866        let json_str = serde_json::to_string(&msg).expect("should serialize to JSON");
867        let json_value: serde_json::Value =
868            serde_json::from_str(&json_str).expect("should parse JSON");
869
870        // Check that the message has the expected kind
871        assert_eq!(
872            json_value.get("kind").and_then(|v| v.as_str()),
873            Some("tagInformation")
874        );
875
876        // With no `flatten`, tag data must be nested under `tag`.
877        let tag_obj = json_value
878            .get("tag")
879            .and_then(|v| v.as_object())
880            .expect("tag should be an object");
881        assert!(
882            tag_obj.get("epcId").is_some(),
883            "tag.epcId should be present"
884        );
885        assert!(
886            tag_obj.get("readCount").is_some(),
887            "tag.readCount should be present"
888        );
889        assert!(
890            tag_obj.get("rssiDbm").is_some(),
891            "tag.rssiDbm should be present"
892        );
893        assert!(
894            tag_obj.get("antennaId").is_some(),
895            "tag.antennaId should be present"
896        );
897        assert!(
898            tag_obj.get("frequencyKhz").is_some(),
899            "tag.frequencyKhz should be present"
900        );
901        assert!(
902            tag_obj.get("timestampMs").is_some(),
903            "tag.timestampMs should be present"
904        );
905        assert!(
906            tag_obj.get("protocolId").is_some(),
907            "tag.protocolId should be present"
908        );
909        assert!(
910            tag_obj.get("epcBitLength").is_some(),
911            "tag.epcBitLength should be present"
912        );
913        assert!(
914            tag_obj.get("pcWord").is_some(),
915            "tag.pcWord should be present"
916        );
917        assert!(
918            tag_obj.get("tagCrc").is_some(),
919            "tag.tagCrc should be present"
920        );
921    }
922
923    #[test]
924    fn single_tag_inventory_success() {
925        use crate::command::MetadataFlags;
926        use crate::silion_reader::SelectOption;
927
928        // Construct `0x21` response data in metadata mode:
929        // Option (1) + MetadataFlags (2) + EPC ID + Tag CRC
930        let mut response_data = Vec::new();
931
932        // Option (bit 4 set: metadata mode)
933        response_data.push(0x10);
934        // MetadataFlags (none)
935        response_data.extend_from_slice(&0x0000u16.to_be_bytes());
936
937        // EPC ID: 6 bytes
938        response_data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
939
940        // Tag CRC: 0x1234
941        response_data.extend_from_slice(&0x1234u16.to_be_bytes());
942
943        let transport = MockTransport::from_replies(vec![reply_frame(
944            CommandCode::SingleTagInventory as u8,
945            0x0000,
946            &response_data,
947        )]);
948
949        let mut reader = SilionReader::new(transport);
950        let tag = futures::executor::block_on(reader.single_tag_inventory(
951            5000,                                  // 5 second timeout
952            SelectOption::Disabled,                // option
953            Some(MetadataFlags::from_raw(0x0000)), // no metadata fields
954        ))
955        .expect("single tag inventory should succeed");
956
957        assert_eq!(tag.pc_word, Some(0x0000));
958        assert_eq!(tag.epc_bit_length, Some(48));
959        assert_eq!(tag.epc_id, vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
960        assert_eq!(tag.tag_crc, 0x1234);
961        assert_eq!(tag.read_count, None); // Not requested
962        assert_eq!(tag.rssi_dbm, None); // Not requested
963    }
964}