Skip to main content

rfid_silion_compat/
command.rs

1//! Command-layer protocol types and host packet builders.
2//!
3//! This module contains low-level request payload models and helpers for
4//! constructing wire packets for Silion reader commands.
5
6use crate::async_proto::{ASYNC_MARKER, ASYNC_TERMINATOR, subcommand_crc};
7use crate::codes::{AntennaPortsOption, CommandCode, RegionCode};
8use crate::error::ProtocolError;
9use crate::frame::{build_host_frame, push_u16_be, push_u32_be};
10use crate::parsers::{AntennaPair, AntennaPower, AntennaPowerSettling};
11
12/// Select/singulation payload used by inventory and tag access commands.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SelectContent {
15    /// Address (bits).
16    pub address_bits: u32,
17    /// Number of selected bits.
18    pub bit_len: u16,
19    /// Select data bytes.
20    pub data: Vec<u8>,
21}
22
23impl SelectContent {
24    /// Encode the SelectContent according to the Silion protocol, using select_option_bits to determine encoding.
25    ///
26    /// - If select_option_bits.extended_data_length() is true, bit_len is encoded as u16 (big-endian), otherwise as u8.
27    /// - Data is encoded as-is.
28    /// Encode the SelectContent according to the Silion protocol, using InventoryOption to determine encoding.
29    ///
30    /// - If option.select_option_bits_struct().extended_data_length() is true, bit_len is encoded as u16 (big-endian), otherwise as u8.
31    /// - Data is encoded as-is.
32    pub(crate) fn encode_with_option(&self, out: &mut Vec<u8>, option: InventoryOption) {
33        let select_option_bits = option.select_option_bits();
34        if select_option_bits.mode() != Some(SelectMode::Epc) {
35            push_u32_be(out, self.address_bits);
36        }
37        if select_option_bits.extended_data_length() {
38            // Encode bit_len as u16 (big-endian)
39            out.push((self.bit_len >> 8) as u8);
40            out.push((self.bit_len & 0xFF) as u8);
41        } else {
42            // Encode bit_len as u8
43            out.push(self.bit_len as u8);
44        }
45        out.extend_from_slice(&self.data);
46    }
47}
48
49/// Tag singulation/select operation mode for inventory commands.
50///
51/// These values represent the target memory bank or select operation mode as defined
52/// in the Silion protocol for Tag Inventory commands. They occupy bits 0-3 of the
53/// select-option field.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[repr(u8)]
56pub enum SelectMode {
57    /// Select functionality is disabled. First tag found will be the tag operated on.
58    /// No other Tag Singulation Fields should be specified.
59    /// Note: When Select is disabled, commands do not support an access password.
60    /// Use `SelectMode::PasswordOnly` to send a password without Select Content.
61    Disabled = 0x00,
62    /// Select on the value of the EPC.
63    Epc = 0x01,
64    /// Select on contents of TID memory bank (Gen2 bank 0x02).
65    Tid = 0x02,
66    /// Select on contents of User Memory bank (Gen2 bank 0x03).
67    UserMemory = 0x03,
68    /// Select on contents of the EPC memory bank (Gen2 bank 0x01).
69    EpcBank = 0x04,
70    /// Use this option to specify an access password without performing a Select.
71    /// When this option is used, do not pass a Select Content field.
72    PasswordOnly = 0x05,
73}
74
75impl SelectMode {
76    /// Return the raw protocol value.
77    pub const fn as_u8(self) -> u8 {
78        self as u8
79    }
80
81    /// Create from the raw protocol value.
82    pub const fn from_u8(value: u8) -> Option<Self> {
83        match value {
84            0x00 => Some(SelectMode::Disabled),
85            0x01 => Some(SelectMode::Epc),
86            0x02 => Some(SelectMode::Tid),
87            0x03 => Some(SelectMode::UserMemory),
88            0x04 => Some(SelectMode::EpcBank),
89            0x05 => Some(SelectMode::PasswordOnly),
90            _ => None,
91        }
92    }
93}
94
95/// Select-option bits for inventory commands.
96///
97/// The select-option field (bits 0, 1, 2, 3, 5 of the option byte) controls tag
98/// singulation/select behavior. Includes the select mode plus optional flags.
99///
100/// | Mode | Value | Meaning |
101/// |------|-------|---------|
102/// | Select Mode (bits 0-2) | 0x00-0x05 | Target memory bank or operation (see [`SelectMode`]) |
103/// | Invert Flag (bit 3) | 0x08 | Invert matching: return tags that do NOT match |
104/// | Extended Data Length (bit 5) | 0x20 | Select Data Length is 2 bytes instead of 1 |
105///
106/// # Examples
107///
108/// Create a select option for EPC matching:
109/// ```
110/// use rfid_silion_compat::command::{SelectMode, SelectOptionBits};
111///
112/// let opts = SelectOptionBits::new(SelectMode::Epc);
113/// assert_eq!(opts.raw(), 0x01);
114/// ```
115///
116/// Create a select option with invert flag:
117/// ```
118/// use rfid_silion_compat::command::{SelectMode, SelectOptionBits};
119///
120/// let opts = SelectOptionBits::new(SelectMode::UserMemory)
121///     .with_invert_flag(true);
122/// assert_eq!(opts.raw(), 0x0B); // 0x03 | 0x08
123/// ```
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct SelectOptionBits(u8);
126
127impl SelectOptionBits {
128    /// Create with the given select mode and no additional flags.
129    pub const fn new(mode: SelectMode) -> Self {
130        Self(mode.as_u8())
131    }
132
133    /// Create from the raw protocol byte (lower bits only, 0x2F mask).
134    pub const fn from_raw(raw: u8) -> Self {
135        Self(raw & 0x2F)
136    }
137
138    /// Return the raw protocol byte (includes all select-option bits).
139    pub const fn raw(self) -> u8 {
140        self.0
141    }
142
143    /// Return the select mode (bits 0-2, values 0x00-0x05).
144    pub const fn mode(self) -> Option<SelectMode> {
145        SelectMode::from_u8(self.0 & 0x0F)
146    }
147
148    /// Return whether the invert flag is set (bit 3).
149    ///
150    /// When set, tags NOT matching the specified Tag Singulation Fields will be returned.
151    pub const fn invert_flag(self) -> bool {
152        (self.0 & 0x08) != 0
153    }
154
155    /// Return whether the extended data length flag is set (bit 5).
156    ///
157    /// When set, Select Data Length is 2 bytes instead of 1, allowing Select Data
158    /// to be greater than 255 bits.
159    pub const fn extended_data_length(self) -> bool {
160        (self.0 & 0x20) != 0
161    }
162
163    /// Set or clear the invert flag (bit 3).
164    pub const fn with_invert_flag(self, en: bool) -> Self {
165        Self(if en { self.0 | 0x08 } else { self.0 & !0x08 })
166    }
167
168    /// Set or clear the extended data length flag (bit 5).
169    pub const fn with_extended_data_length(self, en: bool) -> Self {
170        Self(if en { self.0 | 0x20 } else { self.0 & !0x20 })
171    }
172}
173
174impl From<SelectMode> for SelectOptionBits {
175    fn from(mode: SelectMode) -> Self {
176        Self::new(mode)
177    }
178}
179
180impl From<u8> for SelectOptionBits {
181    fn from(value: u8) -> Self {
182        Self::from_raw(value)
183    }
184}
185
186impl From<SelectOptionBits> for u8 {
187    fn from(value: SelectOptionBits) -> Self {
188        value.raw()
189    }
190}
191
192/// Option byte used by inventory commands (for example `0x22` and async start `0xAA48`).
193///
194/// Lower bits include select-option flags documented under Tag Inventory
195/// commands. Higher bits are command-specific non-select option flags.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct InventoryOption(u8);
198
199impl InventoryOption {
200    /// Create a default option byte with all bits cleared.
201    pub const fn default() -> Self {
202        Self(0)
203    }
204
205    /// Create from the raw protocol byte.
206    pub const fn from_raw(raw: u8) -> Self {
207        Self(raw)
208    }
209
210    /// Return the raw protocol byte.
211    pub const fn raw(self) -> u8 {
212        self.0
213    }
214
215    /// Return select-option bits as a SelectOptionBits struct.
216    pub const fn select_option_bits(self) -> SelectOptionBits {
217        SelectOptionBits::from_raw(self.0 & 0x2F)
218    }
219
220    /// Return whether Single Tag Inventory metadata mode is enabled (bit 4 / `0x10`).
221    ///
222    /// For command `0x21`: when enabled, host command includes 2-byte Metadata Flags
223    /// and reader returns EPC + metadata. When disabled, command omits Metadata Flags
224    /// and reader returns EPC-only payload.
225    pub const fn single_tag_metadata_enabled(self) -> bool {
226        (self.0 & 0x10) != 0
227    }
228
229    /// Set or clear Single Tag Inventory metadata mode bit (bit 4 / `0x10`).
230    pub const fn with_single_tag_metadata(self, enabled: bool) -> Self {
231        if enabled {
232            Self(self.0 | 0x10)
233        } else {
234            Self(self.0 & !0x10)
235        }
236    }
237}
238
239impl From<u8> for InventoryOption {
240    fn from(value: u8) -> Self {
241        Self::from_raw(value)
242    }
243}
244
245impl From<SelectOptionBits> for InventoryOption {
246    fn from(bits: SelectOptionBits) -> Self {
247        Self(bits.raw())
248    }
249}
250
251impl From<InventoryOption> for u8 {
252    fn from(value: InventoryOption) -> Self {
253        value.raw()
254    }
255}
256
257/// Search flags used by inventory commands (for example `0x22` and `0xAA48`).
258///
259/// For asynchronous inventory, protocol docs define extra semantics:
260/// - bits 8..=11: rest ratio steps (0..=15)
261/// - bit 15: heartbeat enable
262/// - bit 14: auto-stop enable
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264#[cfg_attr(feature = "web-serial", derive(serde::Serialize))]
265pub struct InventorySearchFlags(u16);
266
267impl InventorySearchFlags {
268    /// Create a default search flags value with all bits cleared.
269    pub const fn new() -> Self {
270        Self(0)
271    }
272
273    /// Create from the raw protocol value.
274    pub const fn from_raw(raw: u16) -> Self {
275        Self(raw)
276    }
277
278    /// Return the raw protocol value.
279    pub const fn raw(self) -> u16 {
280        self.0
281    }
282
283    /// Extract asynchronous rest-ratio steps from bits 8..=11.
284    pub const fn async_rest_ratio_steps(self) -> u8 {
285        ((self.0 >> 8) & 0x0F) as u8
286    }
287
288    /// Return whether asynchronous heartbeat is enabled (bit 15).
289    pub const fn async_heartbeat_enabled(self) -> bool {
290        (self.0 & 0x8000) != 0
291    }
292
293    /// Return whether asynchronous auto-stop is enabled (bit 14).
294    pub const fn async_auto_stop_enabled(self) -> bool {
295        (self.0 & 0x4000) != 0
296    }
297
298    /// Return whether inventory embedded command mode is enabled (bit 2).
299    ///
300    /// This bit is documented for command `0x22` and reused by
301    /// asynchronous inventory start (`0xAA48`).
302    pub const fn embedded_command_enabled(self) -> bool {
303        (self.0 & 0x0004) != 0
304    }
305
306    /// Set asynchronous rest-ratio steps (0..=15) in bits 8..=11.
307    pub fn with_async_rest_ratio_steps(self, steps: u8) -> Result<Self, ProtocolError> {
308        if steps > 15 {
309            return Err(ProtocolError::InvalidArgument(
310                "async rest ratio steps must be in 0..=15",
311            ));
312        }
313        let raw = (self.0 & !(0x0F << 8)) | ((steps as u16) << 8);
314        Ok(Self(raw))
315    }
316
317    /// Set or clear asynchronous heartbeat enable (bit 15).
318    pub const fn with_async_heartbeat(self, enabled: bool) -> Self {
319        if enabled {
320            Self(self.0 | 0x8000)
321        } else {
322            Self(self.0 & !0x8000)
323        }
324    }
325
326    /// Set or clear asynchronous auto-stop enable (bit 14).
327    pub const fn with_async_auto_stop(self, enabled: bool) -> Self {
328        if enabled {
329            Self(self.0 | 0x4000)
330        } else {
331            Self(self.0 & !0x4000)
332        }
333    }
334
335    /// Set or clear inventory embedded command mode (bit 2).
336    pub const fn with_embedded_command(self, enabled: bool) -> Self {
337        if enabled {
338            Self(self.0 | 0x0004)
339        } else {
340            Self(self.0 & !0x0004)
341        }
342    }
343}
344
345impl From<u16> for InventorySearchFlags {
346    fn from(value: u16) -> Self {
347        Self::from_raw(value)
348    }
349}
350
351impl From<InventorySearchFlags> for u16 {
352    fn from(value: InventorySearchFlags) -> Self {
353        value.raw()
354    }
355}
356
357impl Default for InventorySearchFlags {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363/// Metadata flags that control which per-tag metadata fields the reader
364/// includes in inventory responses.
365///
366/// Defined in the Single Tag Inventory (`0x21`), Get Tag Buffer (`0x29`), and
367/// Asynchronous Inventory (`0xAA`) command specifications.
368///
369/// Each enabled bit requests one additional metadata field in the response.
370/// When all bits are zero the reader returns only the EPC and tag CRC.
371///
372/// | Bit | Value  | Field         | Size    | Description |
373/// |-----|--------|---------------|---------|-------------|
374/// |  0  | 0x0001 | Read Count    | 1 byte  | Number of times the tag was archived |
375/// |  1  | 0x0002 | RSSI          | 1 byte  | Signal strength, signed (dBm) |
376/// |  2  | 0x0004 | Antenna ID    | 1 byte  | Logic antenna number |
377/// |  3  | 0x0008 | Frequency     | 3 bytes | Frequency at archival (kHz) |
378/// |  4  | 0x0010 | Timestamp     | 4 bytes | Elapsed time from inventory start (ms) |
379/// |  5  | 0x0020 | RFU           | 2 bytes | Reserved for future use |
380/// |  6  | 0x0040 | Protocol ID   | 1 byte  | Tag protocol (0x05 = Gen2) |
381/// |  7  | 0x0080 | Data Length   | 2 bytes | Tag data length (0x0000 for 0x21) |
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
383#[cfg_attr(feature = "web-serial", derive(serde::Serialize))]
384pub struct MetadataFlags(u16);
385
386impl MetadataFlags {
387    /// No metadata: only EPC and tag CRC are returned.
388    pub const NONE: Self = Self(0x0000);
389    /// All defined metadata bits set (`0x00FF`).
390    pub const ALL: Self = Self(0x00FF);
391
392    /// Create from the raw protocol value.
393    pub const fn from_raw(raw: u16) -> Self {
394        Self(raw)
395    }
396
397    /// Return the raw protocol value.
398    pub const fn raw(self) -> u16 {
399        self.0
400    }
401
402    /// Whether the Read Count field is requested (bit 0).
403    pub const fn read_count(self) -> bool {
404        (self.0 & 0x0001) != 0
405    }
406    /// Whether the RSSI field is requested (bit 1).
407    pub const fn rssi(self) -> bool {
408        (self.0 & 0x0002) != 0
409    }
410    /// Whether the Antenna ID field is requested (bit 2).
411    pub const fn antenna_id(self) -> bool {
412        (self.0 & 0x0004) != 0
413    }
414    /// Whether the Frequency field is requested (bit 3).
415    pub const fn frequency(self) -> bool {
416        (self.0 & 0x0008) != 0
417    }
418    /// Whether the Timestamp field is requested (bit 4).
419    pub const fn timestamp(self) -> bool {
420        (self.0 & 0x0010) != 0
421    }
422    /// Whether the RFU reserved field is requested (bit 5).
423    pub const fn rfu(self) -> bool {
424        (self.0 & 0x0020) != 0
425    }
426    /// Whether the Protocol ID field is requested (bit 6).
427    pub const fn protocol_id(self) -> bool {
428        (self.0 & 0x0040) != 0
429    }
430    /// Whether the Data Length field is requested (bit 7).
431    pub const fn data_length(self) -> bool {
432        (self.0 & 0x0080) != 0
433    }
434
435    /// Set or clear the Read Count bit (bit 0).
436    pub const fn with_read_count(self, en: bool) -> Self {
437        Self(if en {
438            self.0 | 0x0001
439        } else {
440            self.0 & !0x0001
441        })
442    }
443    /// Set or clear the RSSI bit (bit 1).
444    pub const fn with_rssi(self, en: bool) -> Self {
445        Self(if en {
446            self.0 | 0x0002
447        } else {
448            self.0 & !0x0002
449        })
450    }
451    /// Set or clear the Antenna ID bit (bit 2).
452    pub const fn with_antenna_id(self, en: bool) -> Self {
453        Self(if en {
454            self.0 | 0x0004
455        } else {
456            self.0 & !0x0004
457        })
458    }
459    /// Set or clear the Frequency bit (bit 3).
460    pub const fn with_frequency(self, en: bool) -> Self {
461        Self(if en {
462            self.0 | 0x0008
463        } else {
464            self.0 & !0x0008
465        })
466    }
467    /// Set or clear the Timestamp bit (bit 4).
468    pub const fn with_timestamp(self, en: bool) -> Self {
469        Self(if en {
470            self.0 | 0x0010
471        } else {
472            self.0 & !0x0010
473        })
474    }
475    /// Set or clear the RFU reserved bit (bit 5).
476    pub const fn with_rfu(self, en: bool) -> Self {
477        Self(if en {
478            self.0 | 0x0020
479        } else {
480            self.0 & !0x0020
481        })
482    }
483    /// Set or clear the Protocol ID bit (bit 6).
484    pub const fn with_protocol_id(self, en: bool) -> Self {
485        Self(if en {
486            self.0 | 0x0040
487        } else {
488            self.0 & !0x0040
489        })
490    }
491    /// Set or clear the Data Length bit (bit 7).
492    pub const fn with_data_length(self, en: bool) -> Self {
493        Self(if en {
494            self.0 | 0x0080
495        } else {
496            self.0 & !0x0080
497        })
498    }
499}
500
501impl From<u16> for MetadataFlags {
502    fn from(value: u16) -> Self {
503        Self::from_raw(value)
504    }
505}
506
507impl From<MetadataFlags> for u16 {
508    fn from(value: MetadataFlags) -> Self {
509        value.raw()
510    }
511}
512
513/// Tag memory bank selector used by read/write access commands.
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515#[repr(u8)]
516pub enum MemBank {
517    /// Gen2 Reserved bank (`0x00`).
518    Reserved = 0x00,
519    /// Gen2 EPC bank (`0x01`).
520    Epc = 0x01,
521    /// Gen2 TID bank (`0x02`).
522    Tid = 0x02,
523    /// Gen2 User bank (`0x03`).
524    User = 0x03,
525}
526
527impl MemBank {
528    /// Return the raw protocol value.
529    pub const fn as_u8(self) -> u8 {
530        self as u8
531    }
532
533    /// Parse a raw protocol value into a typed memory bank.
534    pub fn from_u8(raw: u8) -> Result<Self, ProtocolError> {
535        match raw {
536            0x00 => Ok(Self::Reserved),
537            0x01 => Ok(Self::Epc),
538            0x02 => Ok(Self::Tid),
539            0x03 => Ok(Self::User),
540            _ => Err(ProtocolError::InvalidArgument(
541                "membank must be one of 0x00..=0x03",
542            )),
543        }
544    }
545}
546
547impl From<MemBank> for u8 {
548    fn from(value: MemBank) -> Self {
549        value.as_u8()
550    }
551}
552
553/// Typed inventory embedded command content.
554///
555/// The protocol currently documents embedded command opcode `0x28`
556/// (Read Tag Data) for inventory commands.
557#[derive(Debug, Clone, PartialEq, Eq)]
558pub enum InventoryEmbeddedCommandContent {
559    /// Embedded command `0x28` (Read Tag Data).
560    ReadTagData(EmbeddedReadTagData),
561}
562
563impl InventoryEmbeddedCommandContent {
564    fn encoded_len(&self) -> usize {
565        match self {
566            Self::ReadTagData(cmd) => 3 + cmd.data_field_len(),
567        }
568    }
569
570    fn encode(&self, out: &mut Vec<u8>) {
571        match self {
572            Self::ReadTagData(cmd) => cmd.encode(out),
573        }
574    }
575}
576
577/// Typed fields for embedded command `0x28` (Read Tag Data).
578#[derive(Debug, Clone, PartialEq, Eq)]
579pub struct EmbeddedReadTagData {
580    /// Target memory bank.
581    pub read_membank: MemBank,
582    /// Start address in words.
583    pub read_address_words: u32,
584    /// Number of words to read.
585    pub word_count: u8,
586}
587
588impl EmbeddedReadTagData {
589    fn data_field_len(&self) -> usize {
590        // Timeout(2) + Option(1) + MemBank(1) + Address(4) + WordCount(1)
591        9
592    }
593
594    fn encode(&self, out: &mut Vec<u8>) {
595        // Embedded command frame format:
596        // Count(1)=1 | Length(1) | Opcode(1=0x28) | DataField(9)
597        out.push(0x01);
598        out.push(self.data_field_len() as u8);
599        out.push(CommandCode::ReadTagData.as_u8());
600
601        // Vendor docs state timeout/option are ignored for embedded reads.
602        push_u16_be(out, 0x0000);
603        out.push(0x00);
604        out.push(self.read_membank.as_u8());
605        push_u32_be(out, self.read_address_words);
606        out.push(self.word_count);
607    }
608}
609
610/// Asynchronous inventory subcommand IDs.
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612#[repr(u16)]
613pub enum AsyncSubcommandCode {
614    /// Start async inventory.
615    Start = 0xAA48,
616    /// Stop async inventory.
617    Stop = 0xAA49,
618}
619
620/// Typed subcommand data for Start AsyncInventory (`0xAA48`).
621#[derive(Debug, Clone, PartialEq, Eq)]
622pub struct AsyncInventoryStartData {
623    /// Metadata flags controlling which per-tag fields the reader returns.
624    pub metadata_flags: MetadataFlags,
625    /// Option byte, same meaning as command `0x22` select option bits.
626    pub option: InventoryOption,
627    /// Search flags (2 bytes), same meaning as command `0x22`.
628    pub search_flags: InventorySearchFlags,
629    /// Optional access password (4 bytes) when required by option.
630    pub access_password: Option<u32>,
631    /// Optional select content when select operation is enabled.
632    pub select_content: Option<SelectContent>,
633    /// Optional typed embedded command content.
634    pub embedded_command_content: Option<InventoryEmbeddedCommandContent>,
635}
636
637impl AsyncInventoryStartData {
638    fn encode(&self) -> Vec<u8> {
639        let mut data = Vec::with_capacity(
640            2 + 1
641                + 2
642                + if self.access_password.is_some() { 4 } else { 0 }
643                + self
644                    .select_content
645                    .as_ref()
646                    .map(|s| 4 + 2 + s.data.len()) // worst case: 2 bytes for bit_len
647                    .unwrap_or(0)
648                + self
649                    .embedded_command_content
650                    .as_ref()
651                    .map(InventoryEmbeddedCommandContent::encoded_len)
652                    .unwrap_or(0),
653        );
654
655        push_u16_be(&mut data, self.metadata_flags.raw());
656        data.push(self.option.raw());
657        push_u16_be(&mut data, self.search_flags.raw());
658
659        if let Some(password) = self.access_password {
660            push_u32_be(&mut data, password);
661        }
662
663        if let Some(select) = &self.select_content {
664            select.encode_with_option(&mut data, self.option);
665        }
666
667        if let Some(embedded) = &self.embedded_command_content {
668            embedded.encode(&mut data);
669        }
670        data
671    }
672}
673
674/// Typed payload variants for command `0x91` (Set Antenna Ports).
675#[derive(Debug, Clone, PartialEq, Eq)]
676pub enum AntennaPortsConfiguration {
677    /// Set the single TX/RX pair used for tag access operations.
678    AccessPair(AntennaPair),
679    /// Set the ordered TX/RX pairs used during inventory operations.
680    InventoryPairs(Vec<AntennaPair>),
681    /// Set read/write power per logical TX antenna.
682    ///
683    /// Power fields use `0.01 dBm` units in the protocol docs, though current
684    /// firmware is documented as effectively applying about `1 dBm` precision.
685    Power(Vec<AntennaPower>),
686    /// Set read/write power and settling time per logical TX antenna.
687    ///
688    /// Power fields use `0.01 dBm` units in the protocol docs, though current
689    /// firmware is documented as effectively applying about `1 dBm` precision.
690    PowerAndSettling(Vec<AntennaPowerSettling>),
691}
692
693impl AntennaPortsConfiguration {
694    fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
695        match self {
696            Self::AccessPair(pair) => Ok(vec![
697                AntennaPortsOption::AccessPair.as_u8(),
698                pair.tx,
699                pair.rx,
700            ]),
701            Self::InventoryPairs(pairs) => {
702                if pairs.is_empty() {
703                    return Err(ProtocolError::InvalidArgument(
704                        "inventory antenna pairs cannot be empty",
705                    ));
706                }
707                let mut data = Vec::with_capacity(1 + pairs.len() * 2);
708                data.push(AntennaPortsOption::InventoryPairs.as_u8());
709                for pair in pairs {
710                    data.push(pair.tx);
711                    data.push(pair.rx);
712                }
713                Ok(data)
714            }
715            Self::Power(entries) => {
716                if entries.is_empty() {
717                    return Err(ProtocolError::InvalidArgument(
718                        "antenna power entries cannot be empty",
719                    ));
720                }
721                let mut data = Vec::with_capacity(1 + entries.len() * 5);
722                data.push(AntennaPortsOption::Power.as_u8());
723                for entry in entries {
724                    data.push(entry.tx);
725                    push_u16_be(&mut data, entry.read_power);
726                    push_u16_be(&mut data, entry.write_power);
727                }
728                Ok(data)
729            }
730            Self::PowerAndSettling(entries) => {
731                if entries.is_empty() {
732                    return Err(ProtocolError::InvalidArgument(
733                        "antenna power and settling entries cannot be empty",
734                    ));
735                }
736                let mut data = Vec::with_capacity(1 + entries.len() * 7);
737                data.push(AntennaPortsOption::PowerAndSettling.as_u8());
738                for entry in entries {
739                    data.push(entry.tx);
740                    push_u16_be(&mut data, entry.read_power);
741                    push_u16_be(&mut data, entry.write_power);
742                    push_u16_be(&mut data, entry.settling_time_us);
743                }
744                Ok(data)
745            }
746        }
747    }
748}
749
750/// Command builders for all protocol command groups.
751pub struct HostCommand;
752
753impl HostCommand {
754    /// Build a raw command packet from command code and data field bytes.
755    ///
756    /// Use this when the crate does not yet provide a typed builder for a
757    /// command variant you need.
758    ///
759    /// # Examples
760    /// ```rust
761    /// use rfid_silion_compat::command::HostCommand;
762    ///
763    /// let packet = HostCommand::raw(0x03, &[]).unwrap();
764    /// assert_eq!(packet, vec![0xFF, 0x00, 0x03, 0x1D, 0x0C]);
765    /// ```
766    pub fn raw(command: u8, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
767        build_host_frame(command, data)
768    }
769
770    /// Build command `0x01` (Write Flash).
771    ///
772    /// This bootloader command writes firmware words into flash memory.
773    /// `finflag` indicates whether this is the final chunk (`0xFF` means last).
774    pub fn write_flash(
775        finflag: u8,
776        write_addr: u32,
777        write_data: &[u8],
778    ) -> Result<Vec<u8>, ProtocolError> {
779        if write_data.is_empty() || (write_data.len() % 4 != 0) {
780            return Err(ProtocolError::InvalidArgument(
781                "write_data must be non-empty and a multiple of 4 bytes",
782            ));
783        }
784        if write_data.len() > 128 {
785            return Err(ProtocolError::InvalidArgument(
786                "write_data cannot exceed 128 bytes",
787            ));
788        }
789        let words = (write_data.len() / 4) as u8;
790        let mut data = Vec::with_capacity(1 + 4 + 1 + write_data.len());
791        data.push(finflag);
792        push_u32_be(&mut data, write_addr);
793        data.push(words);
794        data.extend_from_slice(write_data);
795        build_host_frame(CommandCode::WriteFlash.as_u8(), &data)
796    }
797
798    /// Build command `0x02` (Read Flash).
799    ///
800    /// This bootloader command reads flash contents from `read_addr` for
801    /// `read_len_words * 4` bytes.
802    pub fn read_flash(read_addr: u32, read_len_words: u8) -> Result<Vec<u8>, ProtocolError> {
803        if read_len_words > 32 {
804            return Err(ProtocolError::InvalidArgument(
805                "read_len_words must be <= 32",
806            ));
807        }
808        let mut data = Vec::with_capacity(5);
809        push_u32_be(&mut data, read_addr);
810        data.push(read_len_words);
811        build_host_frame(CommandCode::ReadFlash.as_u8(), &data)
812    }
813
814    /// Build command `0x03` (Get Version).
815    ///
816    /// Reader replies with bootloader/hardware/firmware version fields and
817    /// supported protocol flags.
818    ///
819    /// # Examples
820    /// ```rust
821    /// use rfid_silion_compat::command::HostCommand;
822    ///
823    /// let packet = HostCommand::get_version().unwrap();
824    /// assert_eq!(packet, vec![0xFF, 0x00, 0x03, 0x1D, 0x0C]);
825    /// ```
826    pub fn get_version() -> Result<Vec<u8>, ProtocolError> {
827        build_host_frame(CommandCode::GetVersion.as_u8(), &[])
828    }
829
830    /// Build command `0x04` (Boot Firmware).
831    ///
832    /// Switches execution to app firmware when currently in bootloader.
833    pub fn boot_firmware() -> Result<Vec<u8>, ProtocolError> {
834        build_host_frame(CommandCode::BootFirmware.as_u8(), &[])
835    }
836
837    /// Build command `0x06` (Set Baud Rate).
838    ///
839    /// `baud_rate` is encoded as a 32-bit big-endian integer as required by
840    /// the protocol documentation.
841    ///
842    /// # Examples
843    /// ```rust
844    /// use rfid_silion_compat::command::HostCommand;
845    ///
846    /// // 115200 decimal = 0x0001C200
847    /// let packet = HostCommand::set_baud_rate(115_200).unwrap();
848    /// assert_eq!(packet, vec![0xFF, 0x04, 0x06, 0x00, 0x01, 0xC2, 0x00, 0xA4, 0x60]);
849    /// ```
850    pub fn set_baud_rate(baud_rate: u32) -> Result<Vec<u8>, ProtocolError> {
851        let mut data = Vec::with_capacity(4);
852        push_u32_be(&mut data, baud_rate);
853        build_host_frame(CommandCode::SetBaudRate.as_u8(), &data)
854    }
855
856    /// Build command `0x08` (Verify Firmware).
857    ///
858    /// This is the bootloader verification command used after firmware burn.
859    pub fn verify_firmware(
860        check_addr: u32,
861        check_data_len_words: u32,
862        check_crc: u32,
863    ) -> Result<Vec<u8>, ProtocolError> {
864        let mut data = Vec::with_capacity(12);
865        push_u32_be(&mut data, check_addr);
866        push_u32_be(&mut data, check_data_len_words);
867        push_u32_be(&mut data, check_crc);
868        build_host_frame(CommandCode::VerifyFirmware.as_u8(), &data)
869    }
870
871    /// Build command `0x09` (Boot Bootloader).
872    ///
873    /// Requests transition from app firmware back to bootloader.
874    pub fn boot_bootloader() -> Result<Vec<u8>, ProtocolError> {
875        build_host_frame(CommandCode::BootBootloader.as_u8(), &[])
876    }
877
878    /// Build command `0x0C` (Get Run Phase).
879    ///
880    /// Reader returns whether it is currently in bootloader or app firmware.
881    ///
882    /// # Examples
883    /// ```rust
884    /// use rfid_silion_compat::command::HostCommand;
885    ///
886    /// let packet = HostCommand::get_run_phase().unwrap();
887    /// assert_eq!(packet, vec![0xFF, 0x00, 0x0C, 0x1D, 0x03]);
888    /// ```
889    pub fn get_run_phase() -> Result<Vec<u8>, ProtocolError> {
890        build_host_frame(CommandCode::GetRunPhase.as_u8(), &[])
891    }
892
893    /// Build command `0x10` (Get Serial Number).
894    ///
895    /// `option` and `data_flags` are reserved by the vendor docs.
896    pub fn get_serial_number(option: u8, data_flags: u8) -> Result<Vec<u8>, ProtocolError> {
897        build_host_frame(CommandCode::GetSerialNumber.as_u8(), &[option, data_flags])
898    }
899
900    /// Build command `0x21` (Single Tag Inventory).
901    ///
902    /// Inventories one tag within `timeout_ms`. Optional metadata and select
903    /// filter fields follow the protocol option bits.
904    pub fn single_tag_inventory(
905        timeout_ms: u16,
906        option: InventoryOption,
907        metadata_flags: Option<MetadataFlags>,
908        select: Option<SelectContent>,
909    ) -> Result<Vec<u8>, ProtocolError> {
910        if option.single_tag_metadata_enabled() && metadata_flags.is_none() {
911            return Err(ProtocolError::InvalidArgument(
912                "single_tag_inventory requires metadata_flags when option bit 4 (0x10) is set",
913            ));
914        }
915        if !option.single_tag_metadata_enabled() && metadata_flags.is_some() {
916            return Err(ProtocolError::InvalidArgument(
917                "single_tag_inventory must omit metadata_flags when option bit 4 (0x10) is clear",
918            ));
919        }
920
921        let mut data = Vec::new();
922        push_u16_be(&mut data, timeout_ms);
923        data.push(option.raw());
924        if let Some(flags) = metadata_flags {
925            push_u16_be(&mut data, flags.raw());
926        }
927        if let Some(sel) = select {
928            sel.encode_with_option(&mut data, option);
929        }
930        build_host_frame(CommandCode::SingleTagInventory.as_u8(), &data)
931    }
932
933    /// Build command `0x22` (Synchronous Inventory).
934    ///
935    /// Performs timed multi-tag inventory and archives tag results into reader
936    /// tag buffer for later retrieval by command `0x29`.
937    pub fn synchronous_inventory(
938        option: InventoryOption,
939        search_flags: u16,
940        timeout_ms: u16,
941        access_password: Option<u32>,
942        select: Option<SelectContent>,
943        embedded_command: Option<&[u8]>,
944    ) -> Result<Vec<u8>, ProtocolError> {
945        Self::synchronous_inventory_raw_embedded(
946            option,
947            InventorySearchFlags::from_raw(search_flags),
948            timeout_ms,
949            access_password,
950            select,
951            embedded_command,
952        )
953    }
954
955    /// Build command `0x22` (Synchronous Inventory) using typed option/flags
956    /// and typed embedded command content.
957    pub fn synchronous_inventory_typed(
958        option: InventoryOption,
959        search_flags: InventorySearchFlags,
960        timeout_ms: u16,
961        access_password: Option<u32>,
962        select: Option<SelectContent>,
963        embedded_command: Option<InventoryEmbeddedCommandContent>,
964    ) -> Result<Vec<u8>, ProtocolError> {
965        let mut data = Vec::new();
966        data.push(option.raw());
967        push_u16_be(&mut data, search_flags.raw());
968        push_u16_be(&mut data, timeout_ms);
969        if let Some(pw) = access_password {
970            push_u32_be(&mut data, pw);
971        }
972        if let Some(sel) = select {
973            sel.encode_with_option(&mut data, option);
974        }
975        if let Some(embedded) = embedded_command {
976            embedded.encode(&mut data);
977        }
978        build_host_frame(CommandCode::SynchronousInventory.as_u8(), &data)
979    }
980
981    /// Build command `0x22` (Synchronous Inventory) with raw embedded bytes.
982    ///
983    /// Prefer [`HostCommand::synchronous_inventory_typed`] where possible.
984    pub fn synchronous_inventory_raw_embedded(
985        option: InventoryOption,
986        search_flags: InventorySearchFlags,
987        timeout_ms: u16,
988        access_password: Option<u32>,
989        select: Option<SelectContent>,
990        embedded_command: Option<&[u8]>,
991    ) -> Result<Vec<u8>, ProtocolError> {
992        let mut data = Vec::new();
993        data.push(option.raw());
994        push_u16_be(&mut data, search_flags.raw());
995        push_u16_be(&mut data, timeout_ms);
996        if let Some(pw) = access_password {
997            push_u32_be(&mut data, pw);
998        }
999        if let Some(sel) = select {
1000            sel.encode_with_option(&mut data, option);
1001        }
1002        if let Some(embedded) = embedded_command {
1003            data.extend_from_slice(embedded);
1004        }
1005        build_host_frame(CommandCode::SynchronousInventory.as_u8(), &data)
1006    }
1007
1008    /// Build command `0x29` (Get Tag Buffer).
1009    ///
1010    /// Retrieves archived tag EPC/metadata records from synchronous inventory.
1011    pub fn get_tag_buffer(
1012        metadata_flags: MetadataFlags,
1013        option: InventoryOption,
1014    ) -> Result<Vec<u8>, ProtocolError> {
1015        let mut data = Vec::with_capacity(3);
1016        push_u16_be(&mut data, metadata_flags.raw());
1017        data.push(option.raw());
1018        build_host_frame(CommandCode::GetTagBuffer.as_u8(), &data)
1019    }
1020
1021    /// Build command `0x23` (Write Tag EPC).
1022    ///
1023    /// Writes EPC data and lets reader update EPC length bits in PC word.
1024    pub fn write_tag_epc(
1025        timeout_ms: u16,
1026        option: InventoryOption,
1027        access_password: Option<u32>,
1028        select: Option<SelectContent>,
1029        epc: &[u8],
1030    ) -> Result<Vec<u8>, ProtocolError> {
1031        if epc.is_empty() {
1032            return Err(ProtocolError::InvalidArgument("epc cannot be empty"));
1033        }
1034        let mut data = Vec::new();
1035        push_u16_be(&mut data, timeout_ms);
1036        data.push(option.raw());
1037        if option.raw() == 0x00 {
1038            data.push(0x00); // RFU byte required when option is 0x00
1039        }
1040        if option.raw() != 0x00 {
1041            if let Some(pw) = access_password {
1042                push_u32_be(&mut data, pw);
1043            } else {
1044                push_u32_be(&mut data, 0x00000000);
1045            }
1046        }
1047        if let Some(sel) = select {
1048            sel.encode_with_option(&mut data, option);
1049        }
1050        data.extend_from_slice(epc);
1051        build_host_frame(CommandCode::WriteTagEpc.as_u8(), &data)
1052    }
1053
1054    /// Build command `0x24` (Write Tag Data).
1055    ///
1056    /// Writes user-supplied bytes to a target bank/address on selected tag.
1057    pub fn write_tag_data(
1058        timeout_ms: u16,
1059        option: InventoryOption,
1060        write_address_words: u32,
1061        write_membank: MemBank,
1062        access_password: Option<u32>,
1063        select: Option<SelectContent>,
1064        write_data: &[u8],
1065    ) -> Result<Vec<u8>, ProtocolError> {
1066        if write_data.is_empty() || (write_data.len() % 2 != 0) {
1067            return Err(ProtocolError::InvalidArgument(
1068                "write_data must be non-empty and multiple of 2 bytes",
1069            ));
1070        }
1071        if write_data.len() > 64 {
1072            return Err(ProtocolError::InvalidArgument(
1073                "write_data must be <= 64 bytes",
1074            ));
1075        }
1076        let mut data = Vec::new();
1077        push_u16_be(&mut data, timeout_ms);
1078        data.push(option.raw());
1079        push_u32_be(&mut data, write_address_words);
1080        data.push(write_membank.as_u8());
1081        if let Some(pw) = access_password {
1082            push_u32_be(&mut data, pw);
1083        } else {
1084            push_u32_be(&mut data, 0x00000000);
1085        }
1086        if let Some(sel) = select {
1087            sel.encode_with_option(&mut data, option);
1088        }
1089        data.extend_from_slice(write_data);
1090        build_host_frame(CommandCode::WriteTagData.as_u8(), &data)
1091    }
1092
1093    /// Build command `0x25` (Lock Tag).
1094    ///
1095    /// Applies Gen2 lock actions defined by `mask_bits` and `action_bits`.
1096    pub fn lock_tag(
1097        timeout_ms: u16,
1098        option: InventoryOption,
1099        access_password: u32,
1100        mask_bits: u16,
1101        action_bits: u16,
1102        select: Option<SelectContent>,
1103    ) -> Result<Vec<u8>, ProtocolError> {
1104        let mut data = Vec::new();
1105        push_u16_be(&mut data, timeout_ms);
1106        data.push(option.raw());
1107        push_u32_be(&mut data, access_password);
1108        push_u16_be(&mut data, mask_bits);
1109        push_u16_be(&mut data, action_bits);
1110        if let Some(sel) = select {
1111            sel.encode_with_option(&mut data, option);
1112        }
1113        build_host_frame(CommandCode::LockTag.as_u8(), &data)
1114    }
1115
1116    /// Build command `0x26` (Kill Tag).
1117    ///
1118    /// Permanently kills a matching tag using `kill_password`.
1119    pub fn kill_tag(
1120        timeout_ms: u16,
1121        option: InventoryOption,
1122        kill_password: u32,
1123        select: Option<SelectContent>,
1124    ) -> Result<Vec<u8>, ProtocolError> {
1125        let mut data = Vec::new();
1126        push_u16_be(&mut data, timeout_ms);
1127        data.push(option.raw());
1128        push_u32_be(&mut data, kill_password);
1129        data.push(0x00); // RFU byte required by protocol docs
1130        if let Some(sel) = select {
1131            sel.encode_with_option(&mut data, option);
1132        }
1133        build_host_frame(CommandCode::KillTag.as_u8(), &data)
1134    }
1135
1136    /// Build command `0x28` (Read Tag Data).
1137    ///
1138    /// Reads memory words from a tag bank and optionally requests metadata.
1139    pub fn read_tag_data(
1140        timeout_ms: u16,
1141        option: InventoryOption,
1142        metadata_flags: Option<MetadataFlags>,
1143        read_membank: MemBank,
1144        read_address_words: u32,
1145        word_count: u8,
1146        access_password: Option<u32>,
1147        select: Option<SelectContent>,
1148    ) -> Result<Vec<u8>, ProtocolError> {
1149        if word_count == 0 || word_count > 96 {
1150            return Err(ProtocolError::InvalidArgument(
1151                "word_count must be in 1..=96",
1152            ));
1153        }
1154        let mut data = Vec::new();
1155        push_u16_be(&mut data, timeout_ms);
1156        data.push(option.raw());
1157        if let Some(flags) = metadata_flags {
1158            push_u16_be(&mut data, flags.raw());
1159        }
1160        data.push(read_membank.as_u8());
1161        push_u32_be(&mut data, read_address_words);
1162        data.push(word_count);
1163        if let Some(pw) = access_password {
1164            push_u32_be(&mut data, pw);
1165        } else {
1166            push_u32_be(&mut data, 0x00000000);
1167        }
1168        if let Some(sel) = select {
1169            sel.encode_with_option(&mut data, option);
1170        }
1171        println!("Read Tag Data command data: {:02X?}", data);
1172        build_host_frame(CommandCode::ReadTagData.as_u8(), &data)
1173    }
1174
1175    /// Build command `0x91` (Set Antenna Ports).
1176    ///
1177    /// The payload shape depends on the configuration variant and is encoded
1178    /// according to the option-specific layout from the protocol docs.
1179    ///
1180    /// # Examples
1181    /// ```rust
1182    /// use rfid_silion_compat::{AntennaPair, AntennaPortsConfiguration};
1183    /// use rfid_silion_compat::command::HostCommand;
1184    ///
1185    /// let packet = HostCommand::set_antenna_ports(&AntennaPortsConfiguration::AccessPair(
1186    ///     AntennaPair { tx: 0x01, rx: 0x01 },
1187    /// ))
1188    /// .unwrap();
1189    /// assert_eq!(packet, vec![0xFF, 0x03, 0x91, 0x00, 0x01, 0x01, 0x62, 0x87]);
1190    /// ```
1191    pub fn set_antenna_ports(config: &AntennaPortsConfiguration) -> Result<Vec<u8>, ProtocolError> {
1192        let data = config.encode()?;
1193        build_host_frame(CommandCode::SetAntennaPorts.as_u8(), &data)
1194    }
1195
1196    /// Build command `0x93` (Set Current Tag Protocol).
1197    ///
1198    /// Current firmware expects `protocol` equal to `0x0005` (GEN2).
1199    pub fn set_current_tag_protocol(protocol: u16) -> Result<Vec<u8>, ProtocolError> {
1200        let mut data = Vec::with_capacity(2);
1201        push_u16_be(&mut data, protocol);
1202        build_host_frame(CommandCode::SetCurrentTagProtocol.as_u8(), &data)
1203    }
1204
1205    /// Build command `0x95` (Set Frequency Hopping).
1206    ///
1207    /// Sets hop table or reserved regulatory hopping time format.
1208    pub fn set_frequency_hopping(data_field: &[u8]) -> Result<Vec<u8>, ProtocolError> {
1209        build_host_frame(CommandCode::SetFrequencyHopping.as_u8(), data_field)
1210    }
1211
1212    /// Build command `0x96` (Set GPO / Get GPO status).
1213    ///
1214    /// Non-empty `data_field` sets GPO pin states. Empty data requests current
1215    /// GPO status in the response payload.
1216    pub fn set_gpo(data_field: &[u8]) -> Result<Vec<u8>, ProtocolError> {
1217        build_host_frame(CommandCode::SetGpo.as_u8(), data_field)
1218    }
1219
1220    /// Build command `0x97` (Set Current Region).
1221    ///
1222    /// Selects working region code used by frequency/hopping constraints.
1223    ///
1224    /// # Examples
1225    /// ```rust
1226    /// use rfid_silion_compat::RegionCode;
1227    /// use rfid_silion_compat::command::HostCommand;
1228    ///
1229    /// let packet = HostCommand::set_current_region(RegionCode::NorthAmerica).unwrap();
1230    /// assert_eq!(packet, vec![0xFF, 0x01, 0x97, 0x01, 0x4B, 0xBC]);
1231    /// ```
1232    pub fn set_current_region(region_code: RegionCode) -> Result<Vec<u8>, ProtocolError> {
1233        build_host_frame(
1234            CommandCode::SetCurrentRegion.as_u8(),
1235            &[region_code.as_u8()],
1236        )
1237    }
1238
1239    /// Build command `0x9A` (Set Reader Configuration).
1240    ///
1241    /// Sets one reader key/value under `option` 0x01 format.
1242    pub fn set_reader_configuration(
1243        option: u8,
1244        key: u8,
1245        value: u8,
1246    ) -> Result<Vec<u8>, ProtocolError> {
1247        build_host_frame(
1248            CommandCode::SetReaderConfiguration.as_u8(),
1249            &[option, key, value],
1250        )
1251    }
1252
1253    /// Build command `0x9B` (Set Protocol Configuration).
1254    ///
1255    /// Sets protocol parameter values (session, target, Q, etc.) according to
1256    /// option/value presence required by the selected parameter.
1257    pub fn set_protocol_configuration(
1258        protocol_value: u8,
1259        parameter: u8,
1260        option: Option<u8>,
1261        value: Option<u8>,
1262    ) -> Result<Vec<u8>, ProtocolError> {
1263        let mut data = vec![protocol_value, parameter];
1264        if let Some(opt) = option {
1265            data.push(opt);
1266        }
1267        if let Some(v) = value {
1268            data.push(v);
1269        }
1270        build_host_frame(CommandCode::SetProtocolConfiguration.as_u8(), &data)
1271    }
1272
1273    /// Build command `0x61` (Get Antenna Ports).
1274    ///
1275    /// `option` selects which antenna view is requested (access pair,
1276    /// inventory pairs, powers, powers+settling, or connection states).
1277    pub fn get_antenna_ports(option: AntennaPortsOption) -> Result<Vec<u8>, ProtocolError> {
1278        build_host_frame(CommandCode::GetAntennaPorts.as_u8(), &[option.as_u8()])
1279    }
1280
1281    /// Build command `0x63` (Get Current Tag Protocol).
1282    ///
1283    /// Returns active tag protocol (currently GEN2 `0x0005`).
1284    pub fn get_current_tag_protocol() -> Result<Vec<u8>, ProtocolError> {
1285        build_host_frame(CommandCode::GetCurrentTagProtocol.as_u8(), &[])
1286    }
1287
1288    /// Build command `0x65` (Get Frequency Hopping).
1289    ///
1290    /// `None` requests the full hop table. `Some(0x01)` requests regulatory
1291    /// hopping time payload format.
1292    ///
1293    /// # Examples
1294    /// ```rust
1295    /// use rfid_silion_compat::command::HostCommand;
1296    ///
1297    /// let table_req = HostCommand::get_frequency_hopping(None).unwrap();
1298    /// assert_eq!(table_req, vec![0xFF, 0x00, 0x65, 0x1D, 0x6A]);
1299    ///
1300    /// let hop_time_req = HostCommand::get_frequency_hopping(Some(0x01)).unwrap();
1301    /// assert_eq!(hop_time_req, vec![0xFF, 0x01, 0x65, 0x01, 0xB9, 0xBC]);
1302    /// ```
1303    pub fn get_frequency_hopping(option: Option<u8>) -> Result<Vec<u8>, ProtocolError> {
1304        match option {
1305            Some(v) => build_host_frame(CommandCode::GetFrequencyHopping.as_u8(), &[v]),
1306            None => build_host_frame(CommandCode::GetFrequencyHopping.as_u8(), &[]),
1307        }
1308    }
1309
1310    /// Build command `0x66` (Get GPI).
1311    ///
1312    /// Returns input pin states ordered by pin number.
1313    pub fn get_gpi() -> Result<Vec<u8>, ProtocolError> {
1314        build_host_frame(CommandCode::GetGpi.as_u8(), &[])
1315    }
1316
1317    /// Build command `0x67` (Get Current Region).
1318    ///
1319    /// Returns active region code.
1320    pub fn get_current_region() -> Result<Vec<u8>, ProtocolError> {
1321        build_host_frame(CommandCode::GetCurrentRegion.as_u8(), &[])
1322    }
1323
1324    /// Build command `0x71` (Get Available Regions).
1325    ///
1326    /// Returns region codes supported by the connected reader firmware.
1327    pub fn get_available_regions() -> Result<Vec<u8>, ProtocolError> {
1328        build_host_frame(CommandCode::GetAvailableRegions.as_u8(), &[])
1329    }
1330
1331    /// Build command `0x6A` (Get Reader Configuration).
1332    ///
1333    /// Requests one key under a given option namespace.
1334    pub fn get_reader_configuration(option: u8, key: u8) -> Result<Vec<u8>, ProtocolError> {
1335        build_host_frame(CommandCode::GetReaderConfiguration.as_u8(), &[option, key])
1336    }
1337
1338    /// Build command `0x6B` (Get Protocol Configuration).
1339    ///
1340    /// Requests one protocol parameter for a selected protocol id.
1341    ///
1342    /// # Examples
1343    /// ```rust
1344    /// use rfid_silion_compat::command::HostCommand;
1345    ///
1346    /// // Protocol 0x05 (GEN2), parameter 0x00 (session)
1347    /// let packet = HostCommand::get_protocol_configuration(0x05, 0x00).unwrap();
1348    /// assert_eq!(packet, vec![0xFF, 0x02, 0x6B, 0x05, 0x00, 0x3A, 0x6F]);
1349    /// ```
1350    pub fn get_protocol_configuration(
1351        protocol_value: u8,
1352        parameter: u8,
1353    ) -> Result<Vec<u8>, ProtocolError> {
1354        build_host_frame(
1355            CommandCode::GetProtocolConfiguration.as_u8(),
1356            &[protocol_value, parameter],
1357        )
1358    }
1359
1360    /// Build command `0x72` (Get Current Temperature).
1361    ///
1362    /// Returns reader board temperature as one byte.
1363    pub fn get_current_temperature() -> Result<Vec<u8>, ProtocolError> {
1364        build_host_frame(CommandCode::GetCurrentTemperature.as_u8(), &[])
1365    }
1366
1367    /// Build command `0xAA` Start Async Inventory subcommand (`0xAA48`).
1368    ///
1369    /// `start` follows the documented subcommand data format:
1370    /// `MetadataFlags(2) | Option(1) | SearchFlags(2) | [AccessPassword(4)] |
1371    /// [SelectContent] | [EmbeddedCommandContent]`.
1372    ///
1373    /// # Examples
1374    /// ```rust
1375    /// use rfid_silion_compat::{
1376    ///     EmbeddedReadTagData, InventoryEmbeddedCommandContent, InventorySearchFlags, MemBank,
1377    ///     MetadataFlags,
1378    /// };
1379    /// use rfid_silion_compat::command::{AsyncInventoryStartData, HostCommand, InventoryOption};
1380    ///
1381    /// let search_flags = InventorySearchFlags::new()
1382    ///     .with_async_heartbeat(true)
1383    ///     .with_async_auto_stop(false)
1384    ///     .with_embedded_command(true)
1385    ///     .with_async_rest_ratio_steps(3)
1386    ///     .unwrap();
1387    ///
1388    /// let start = AsyncInventoryStartData {
1389    ///     metadata_flags: MetadataFlags::ALL,
1390    ///     option: InventoryOption::default(),
1391    ///     search_flags,
1392    ///     access_password: None,
1393    ///     select_content: None,
1394    ///     embedded_command_content: Some(InventoryEmbeddedCommandContent::ReadTagData(
1395    ///         EmbeddedReadTagData {
1396    ///             read_membank: MemBank::Tid,
1397    ///             read_address_words: 0,
1398    ///             word_count: 2,
1399    ///         },
1400    ///     )),
1401    /// };
1402    ///
1403    /// let packet = HostCommand::async_start(&start).unwrap();
1404    /// assert_eq!(packet[0], 0xFF);
1405    /// assert_eq!(packet[2], 0xAA);
1406    /// ```
1407    pub fn async_start(start: &AsyncInventoryStartData) -> Result<Vec<u8>, ProtocolError> {
1408        let subcommand_data = start.encode();
1409        Self::async_inventory(AsyncSubcommandCode::Start, &subcommand_data)
1410    }
1411
1412    /// Build command `0xAA` Stop Async Inventory subcommand (`0xAA49`).
1413    ///
1414    /// # Examples
1415    /// ```rust
1416    /// use rfid_silion_compat::command::HostCommand;
1417    ///
1418    /// let packet = HostCommand::async_stop().unwrap();
1419    /// assert_eq!(
1420    ///     packet,
1421    ///     vec![
1422    ///         0xFF, 0x0E, 0xAA, 0x4D, 0x6F, 0x64, 0x75, 0x6C, 0x65, 0x74,
1423    ///         0x65, 0x63, 0x68, 0xAA, 0x49, 0xF3, 0xBB, 0x03, 0x91,
1424    ///     ]
1425    /// );
1426    /// ```
1427    pub fn async_stop() -> Result<Vec<u8>, ProtocolError> {
1428        Self::async_inventory(AsyncSubcommandCode::Stop, &[])
1429    }
1430
1431    /// Build a generic `0xAA` asynchronous inventory command packet.
1432    ///
1433    /// This inserts the fixed marker (`Moduletech`), subcommand, subcommand
1434    /// payload, sub-CRC (8-bit sum), and terminator (`0xBB`) before wrapping the
1435    /// bytes in a normal host frame.
1436    pub fn async_inventory(
1437        subcommand: AsyncSubcommandCode,
1438        subcommand_data: &[u8],
1439    ) -> Result<Vec<u8>, ProtocolError> {
1440        let mut data = Vec::with_capacity(10 + 2 + subcommand_data.len() + 2);
1441        data.extend_from_slice(ASYNC_MARKER);
1442        push_u16_be(&mut data, subcommand as u16);
1443        data.extend_from_slice(subcommand_data);
1444        let sub_crc = subcommand_crc(subcommand as u16, subcommand_data);
1445        data.push(sub_crc);
1446        data.push(ASYNC_TERMINATOR);
1447        build_host_frame(CommandCode::AsynchronousInventory.as_u8(), &data)
1448    }
1449}