Skip to main content

omron_fins/
command.rs

1//! FINS command structures and serialization.
2//!
3//! This module contains all FINS command structures that can be sent to a PLC.
4//! Each command handles its own serialization to bytes for transmission.
5//!
6//! # Command Types
7//!
8//! The module provides the following command types:
9//!
10//! ## Memory Operations
11//! - [`ReadWordCommand`] - Read words from PLC memory
12//! - [`WriteWordCommand`] - Write words to PLC memory
13//! - [`ReadBitCommand`] - Read a single bit from PLC memory
14//! - [`WriteBitCommand`] - Write a single bit to PLC memory
15//! - [`FillCommand`] - Fill memory with a repeated value
16//! - [`TransferCommand`] - Transfer data between memory areas
17//! - [`MultipleReadCommand`] - Read from multiple addresses in one request
18//!
19//! ## PLC Control
20//! - [`RunCommand`] - Put PLC into run mode
21//! - [`StopCommand`] - Stop the PLC
22//!
23//! ## Forced I/O
24//! - [`ForcedSetResetCommand`] - Force bits ON/OFF
25//! - [`ForcedSetResetCancelCommand`] - Cancel all forced bits
26//!
27//! # Example
28//!
29//! Commands are typically created and used through the [`Client`](crate::Client) struct,
30//! but can also be used directly for lower-level control:
31//!
32//! ```
33//! use omron_fins::{ReadWordCommand, MemoryArea, NodeAddress};
34//!
35//! let dest = NodeAddress::new(0, 10, 0);
36//! let src = NodeAddress::new(0, 1, 0);
37//!
38//! let cmd = ReadWordCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 10).unwrap();
39//! let bytes = cmd.to_bytes();
40//! // bytes can now be sent over UDP
41//! ```
42//!
43//! # Constants
44//!
45//! - [`MAX_WORDS_PER_COMMAND`] - Maximum number of words (999) used historically for some Omron models.
46
47use crate::error::{FinsError, Result};
48use crate::header::{FinsHeader, NodeAddress, FINS_HEADER_SIZE};
49use crate::memory::MemoryArea;
50
51/// Memory Read command code (MRC).
52pub(crate) const MRC_MEMORY_READ: u8 = 0x01;
53/// Memory Read command sub-code (SRC).
54pub(crate) const SRC_MEMORY_READ: u8 = 0x01;
55/// Memory Write command code (MRC).
56pub(crate) const MRC_MEMORY_WRITE: u8 = 0x01;
57/// Memory Write command sub-code (SRC).
58pub(crate) const SRC_MEMORY_WRITE: u8 = 0x02;
59/// Memory Fill command sub-code (SRC).
60pub(crate) const SRC_MEMORY_FILL: u8 = 0x03;
61/// Multiple Memory Area Read command sub-code (SRC).
62pub(crate) const SRC_MULTIPLE_READ: u8 = 0x04;
63/// Memory Area Transfer command sub-code (SRC).
64pub(crate) const SRC_MEMORY_TRANSFER: u8 = 0x05;
65/// Run command code (MRC).
66pub(crate) const MRC_RUN: u8 = 0x04;
67/// Run command sub-code (SRC).
68pub(crate) const SRC_RUN: u8 = 0x01;
69/// Stop command sub-code (SRC).
70pub(crate) const SRC_STOP: u8 = 0x02;
71/// Forced Set/Reset command code (MRC).
72pub(crate) const MRC_FORCED: u8 = 0x23;
73/// Forced Set/Reset command sub-code (SRC).
74pub(crate) const SRC_FORCED_SET_RESET: u8 = 0x01;
75/// Forced Set/Reset Cancel command sub-code (SRC).
76pub(crate) const SRC_FORCED_CANCEL: u8 = 0x02;
77
78/// Maximum number of words that can be read/written in a single command on older models or standard UDP limits.
79///
80/// Note: The library chunks user requests automatically into blocks of this size or lower
81/// in order to respect standard Ethernet MTU (1500) and avoid UDP fragment dropping.
82/// 700 words = 1400 bytes, which fits safely inside the 1472 byte UDP payload max.
83pub const MAX_WORDS_PER_COMMAND: u16 = 700;
84
85/// Address specification for FINS commands.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct Address {
88    /// Word address in the memory area.
89    pub word: u16,
90    /// Bit position (0-15) for bit access, or 0 for word access.
91    pub bit: u8,
92}
93
94impl Address {
95    /// Creates a new word address (bit = 0).
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use omron_fins::Address;
101    ///
102    /// let addr = Address::word(100);
103    /// assert_eq!(addr.word, 100);
104    /// assert_eq!(addr.bit, 0);
105    /// ```
106    pub fn word(word: u16) -> Self {
107        Self { word, bit: 0 }
108    }
109
110    /// Creates a new bit address.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if bit > 15.
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// use omron_fins::Address;
120    ///
121    /// let addr = Address::bit(100, 5).unwrap();
122    /// assert_eq!(addr.word, 100);
123    /// assert_eq!(addr.bit, 5);
124    /// ```
125    pub fn bit(word: u16, bit: u8) -> Result<Self> {
126        if bit > 15 {
127            return Err(FinsError::invalid_parameter("bit", "must be 0-15"));
128        }
129        Ok(Self { word, bit })
130    }
131
132    /// Serializes address to 3 bytes (word high, word low, bit).
133    pub(crate) fn to_bytes(self) -> [u8; 3] {
134        [(self.word >> 8) as u8, (self.word & 0xFF) as u8, self.bit]
135    }
136}
137
138/// Command for reading words from PLC memory.
139#[derive(Debug, Clone)]
140pub struct ReadWordCommand {
141    header: FinsHeader,
142    area: MemoryArea,
143    address: Address,
144    count: u16,
145}
146
147impl ReadWordCommand {
148    /// Creates a new read word command.
149    ///
150    /// # Arguments
151    ///
152    /// * `destination` - Destination node address
153    /// * `source` - Source node address
154    /// * `sid` - Service ID for request/response matching
155    /// * `area` - Memory area to read from
156    /// * `address` - Starting word address
157    /// * `count` - Number of words to read (1 to maximum area capacity)
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if count is 0 or exceeds the available capacity for the target area.
162    ///
163    /// # Example
164    ///
165    /// ```
166    /// use omron_fins::{ReadWordCommand, MemoryArea, NodeAddress};
167    ///
168    /// let cmd = ReadWordCommand::new(
169    ///     NodeAddress::new(0, 10, 0),
170    ///     NodeAddress::new(0, 1, 0),
171    ///     0x01,
172    ///     MemoryArea::DM,
173    ///     100,
174    ///     10,
175    /// ).unwrap();
176    /// ```
177    pub fn new(
178        destination: NodeAddress,
179        source: NodeAddress,
180        sid: u8,
181        area: MemoryArea,
182        word_address: u16,
183        count: u16,
184    ) -> Result<Self> {
185        if count == 0 {
186            return Err(FinsError::invalid_parameter(
187                "count",
188                "must be greater than 0",
189            ));
190        }
191        if count > area.max_words() {
192            return Err(FinsError::invalid_parameter(
193                "count",
194                format!(
195                    "must not exceed area capacity of {} words",
196                    area.max_words()
197                ),
198            ));
199        }
200
201        Ok(Self {
202            header: FinsHeader::new_command(destination, source, sid),
203            area,
204            address: Address::word(word_address),
205            count,
206        })
207    }
208
209    /// Returns the service ID.
210    pub fn sid(&self) -> u8 {
211        self.header.sid
212    }
213
214    /// Serializes the command to bytes for transmission.
215    pub fn to_bytes(&self) -> Vec<u8> {
216        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 8);
217        bytes.extend_from_slice(&self.header.to_bytes());
218        bytes.push(MRC_MEMORY_READ);
219        bytes.push(SRC_MEMORY_READ);
220        bytes.push(self.area.word_code());
221        bytes.extend_from_slice(&self.address.to_bytes());
222        bytes.push((self.count >> 8) as u8);
223        bytes.push((self.count & 0xFF) as u8);
224        bytes
225    }
226}
227
228/// Command for writing words to PLC memory.
229#[derive(Debug, Clone)]
230pub struct WriteWordCommand {
231    header: FinsHeader,
232    area: MemoryArea,
233    address: Address,
234    data: Vec<u16>,
235}
236
237impl WriteWordCommand {
238    /// Creates a new write word command.
239    ///
240    /// # Arguments
241    ///
242    /// * `destination` - Destination node address
243    /// * `source` - Source node address
244    /// * `sid` - Service ID for request/response matching
245    /// * `area` - Memory area to write to
246    /// * `word_address` - Starting word address
247    /// * `data` - Words to write (1 to maximum area capacity)
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if data is empty or exceeds the available capacity for the target area.
252    ///
253    /// # Example
254    ///
255    /// ```
256    /// use omron_fins::{WriteWordCommand, MemoryArea, NodeAddress};
257    ///
258    /// let cmd = WriteWordCommand::new(
259    ///     NodeAddress::new(0, 10, 0),
260    ///     NodeAddress::new(0, 1, 0),
261    ///     0x01,
262    ///     MemoryArea::DM,
263    ///     100,
264    ///     &[0x1234, 0x5678],
265    /// ).unwrap();
266    /// ```
267    pub fn new(
268        destination: NodeAddress,
269        source: NodeAddress,
270        sid: u8,
271        area: MemoryArea,
272        word_address: u16,
273        data: &[u16],
274    ) -> Result<Self> {
275        if data.is_empty() {
276            return Err(FinsError::invalid_parameter("data", "must not be empty"));
277        }
278        if data.len() > area.max_words() as usize {
279            return Err(FinsError::invalid_parameter(
280                "data",
281                format!(
282                    "must not exceed area capacity of {} words",
283                    area.max_words()
284                ),
285            ));
286        }
287
288        Ok(Self {
289            header: FinsHeader::new_command(destination, source, sid),
290            area,
291            address: Address::word(word_address),
292            data: data.to_vec(),
293        })
294    }
295
296    /// Returns the service ID.
297    pub fn sid(&self) -> u8 {
298        self.header.sid
299    }
300
301    /// Serializes the command to bytes for transmission.
302    pub fn to_bytes(&self) -> Vec<u8> {
303        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 8 + self.data.len() * 2);
304        bytes.extend_from_slice(&self.header.to_bytes());
305        bytes.push(MRC_MEMORY_WRITE);
306        bytes.push(SRC_MEMORY_WRITE);
307        bytes.push(self.area.word_code());
308        bytes.extend_from_slice(&self.address.to_bytes());
309        bytes.push((self.data.len() >> 8) as u8);
310        bytes.push((self.data.len() & 0xFF) as u8);
311        for word in &self.data {
312            bytes.push((word >> 8) as u8);
313            bytes.push((word & 0xFF) as u8);
314        }
315        bytes
316    }
317}
318
319/// Command for reading a single bit from PLC memory.
320#[derive(Debug, Clone)]
321pub struct ReadBitCommand {
322    header: FinsHeader,
323    area: MemoryArea,
324    address: Address,
325}
326
327impl ReadBitCommand {
328    /// Creates a new read bit command.
329    ///
330    /// # Arguments
331    ///
332    /// * `destination` - Destination node address
333    /// * `source` - Source node address
334    /// * `sid` - Service ID for request/response matching
335    /// * `area` - Memory area to read from (must support bit access)
336    /// * `word_address` - Word address
337    /// * `bit` - Bit position (0-15)
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if:
342    /// - The memory area doesn't support bit access (DM)
343    /// - The bit position is > 15
344    ///
345    /// # Example
346    ///
347    /// ```
348    /// use omron_fins::{ReadBitCommand, MemoryArea, NodeAddress};
349    ///
350    /// let cmd = ReadBitCommand::new(
351    ///     NodeAddress::new(0, 10, 0),
352    ///     NodeAddress::new(0, 1, 0),
353    ///     0x01,
354    ///     MemoryArea::CIO,
355    ///     100,
356    ///     5,
357    /// ).unwrap();
358    /// ```
359    pub fn new(
360        destination: NodeAddress,
361        source: NodeAddress,
362        sid: u8,
363        area: MemoryArea,
364        word_address: u16,
365        bit: u8,
366    ) -> Result<Self> {
367        // Validate bit access is supported
368        area.bit_code()?;
369
370        Ok(Self {
371            header: FinsHeader::new_command(destination, source, sid),
372            area,
373            address: Address::bit(word_address, bit)?,
374        })
375    }
376
377    /// Returns the service ID.
378    pub fn sid(&self) -> u8 {
379        self.header.sid
380    }
381
382    /// Serializes the command to bytes for transmission.
383    pub fn to_bytes(&self) -> Result<Vec<u8>> {
384        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 8);
385        bytes.extend_from_slice(&self.header.to_bytes());
386        bytes.push(MRC_MEMORY_READ);
387        bytes.push(SRC_MEMORY_READ);
388        bytes.push(self.area.bit_code()?);
389        bytes.extend_from_slice(&self.address.to_bytes());
390        bytes.push(0x00); // Count high byte (always 1 bit)
391        bytes.push(0x01); // Count low byte
392        Ok(bytes)
393    }
394}
395
396/// Command for writing a single bit to PLC memory.
397#[derive(Debug, Clone)]
398pub struct WriteBitCommand {
399    header: FinsHeader,
400    area: MemoryArea,
401    address: Address,
402    value: bool,
403}
404
405impl WriteBitCommand {
406    /// Creates a new write bit command.
407    ///
408    /// # Arguments
409    ///
410    /// * `destination` - Destination node address
411    /// * `source` - Source node address
412    /// * `sid` - Service ID for request/response matching
413    /// * `area` - Memory area to write to (must support bit access)
414    /// * `word_address` - Word address
415    /// * `bit` - Bit position (0-15)
416    /// * `value` - Bit value to write
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if:
421    /// - The memory area doesn't support bit access (DM)
422    /// - The bit position is > 15
423    ///
424    /// # Example
425    ///
426    /// ```
427    /// use omron_fins::{WriteBitCommand, MemoryArea, NodeAddress};
428    ///
429    /// let cmd = WriteBitCommand::new(
430    ///     NodeAddress::new(0, 10, 0),
431    ///     NodeAddress::new(0, 1, 0),
432    ///     0x01,
433    ///     MemoryArea::CIO,
434    ///     100,
435    ///     5,
436    ///     true,
437    /// ).unwrap();
438    /// ```
439    pub fn new(
440        destination: NodeAddress,
441        source: NodeAddress,
442        sid: u8,
443        area: MemoryArea,
444        word_address: u16,
445        bit: u8,
446        value: bool,
447    ) -> Result<Self> {
448        // Validate bit access is supported
449        area.bit_code()?;
450
451        Ok(Self {
452            header: FinsHeader::new_command(destination, source, sid),
453            area,
454            address: Address::bit(word_address, bit)?,
455            value,
456        })
457    }
458
459    /// Returns the service ID.
460    pub fn sid(&self) -> u8 {
461        self.header.sid
462    }
463
464    /// Serializes the command to bytes for transmission.
465    pub fn to_bytes(&self) -> Result<Vec<u8>> {
466        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 9);
467        bytes.extend_from_slice(&self.header.to_bytes());
468        bytes.push(MRC_MEMORY_WRITE);
469        bytes.push(SRC_MEMORY_WRITE);
470        bytes.push(self.area.bit_code()?);
471        bytes.extend_from_slice(&self.address.to_bytes());
472        bytes.push(0x00); // Count high byte (always 1 bit)
473        bytes.push(0x01); // Count low byte
474        bytes.push(if self.value { 0x01 } else { 0x00 });
475        Ok(bytes)
476    }
477}
478
479/// Command for filling a memory area with a single value.
480#[derive(Debug, Clone)]
481pub struct FillCommand {
482    header: FinsHeader,
483    area: MemoryArea,
484    address: Address,
485    count: u16,
486    value: u16,
487}
488
489impl FillCommand {
490    /// Creates a new fill command.
491    ///
492    /// # Arguments
493    ///
494    /// * `destination` - Destination node address
495    /// * `source` - Source node address
496    /// * `sid` - Service ID for request/response matching
497    /// * `area` - Memory area to fill
498    /// * `word_address` - Starting word address
499    /// * `count` - Number of words to fill (1 to maximum area capacity)
500    /// * `value` - Value to fill with
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if count is 0 or exceeds the available capacity for the target area.
505    ///
506    /// # Example
507    ///
508    /// ```
509    /// use omron_fins::{FillCommand, MemoryArea, NodeAddress};
510    ///
511    /// let cmd = FillCommand::new(
512    ///     NodeAddress::new(0, 10, 0),
513    ///     NodeAddress::new(0, 1, 0),
514    ///     0x01,
515    ///     MemoryArea::DM,
516    ///     100,
517    ///     50,
518    ///     0x0000,
519    /// ).unwrap();
520    /// ```
521    pub fn new(
522        destination: NodeAddress,
523        source: NodeAddress,
524        sid: u8,
525        area: MemoryArea,
526        word_address: u16,
527        count: u16,
528        value: u16,
529    ) -> Result<Self> {
530        if count == 0 {
531            return Err(FinsError::invalid_parameter(
532                "count",
533                "must be greater than 0",
534            ));
535        }
536        if count > area.max_words() {
537            return Err(FinsError::invalid_parameter(
538                "count",
539                format!(
540                    "must not exceed area capacity of {} words",
541                    area.max_words()
542                ),
543            ));
544        }
545
546        Ok(Self {
547            header: FinsHeader::new_command(destination, source, sid),
548            area,
549            address: Address::word(word_address),
550            count,
551            value,
552        })
553    }
554
555    /// Returns the service ID.
556    pub fn sid(&self) -> u8 {
557        self.header.sid
558    }
559
560    /// Serializes the command to bytes for transmission.
561    pub fn to_bytes(&self) -> Vec<u8> {
562        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 10);
563        bytes.extend_from_slice(&self.header.to_bytes());
564        bytes.push(MRC_MEMORY_READ); // Memory commands use 0x01
565        bytes.push(SRC_MEMORY_FILL);
566        bytes.push(self.area.word_code());
567        bytes.extend_from_slice(&self.address.to_bytes());
568        bytes.push((self.count >> 8) as u8);
569        bytes.push((self.count & 0xFF) as u8);
570        bytes.push((self.value >> 8) as u8);
571        bytes.push((self.value & 0xFF) as u8);
572        bytes
573    }
574}
575
576/// PLC operating mode for Run command.
577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
578pub enum PlcMode {
579    /// Debug mode - step execution.
580    Debug,
581    /// Monitor mode - run with monitoring enabled.
582    Monitor,
583    /// Run mode - normal execution.
584    Run,
585}
586
587impl PlcMode {
588    /// Returns the FINS code for this mode.
589    pub(crate) fn code(self) -> u8 {
590        match self {
591            PlcMode::Debug => 0x01,
592            PlcMode::Monitor => 0x02,
593            PlcMode::Run => 0x04,
594        }
595    }
596}
597
598/// Command for putting the PLC into run mode.
599#[derive(Debug, Clone)]
600pub struct RunCommand {
601    header: FinsHeader,
602    mode: PlcMode,
603}
604
605impl RunCommand {
606    /// Creates a new run command.
607    ///
608    /// # Arguments
609    ///
610    /// * `destination` - Destination node address
611    /// * `source` - Source node address
612    /// * `sid` - Service ID for request/response matching
613    /// * `mode` - PLC operating mode
614    ///
615    /// # Example
616    ///
617    /// ```
618    /// use omron_fins::{RunCommand, PlcMode, NodeAddress};
619    ///
620    /// let cmd = RunCommand::new(
621    ///     NodeAddress::new(0, 10, 0),
622    ///     NodeAddress::new(0, 1, 0),
623    ///     0x01,
624    ///     PlcMode::Monitor,
625    /// );
626    /// ```
627    pub fn new(destination: NodeAddress, source: NodeAddress, sid: u8, mode: PlcMode) -> Self {
628        Self {
629            header: FinsHeader::new_command(destination, source, sid),
630            mode,
631        }
632    }
633
634    /// Returns the service ID.
635    pub fn sid(&self) -> u8 {
636        self.header.sid
637    }
638
639    /// Serializes the command to bytes for transmission.
640    pub fn to_bytes(&self) -> Vec<u8> {
641        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 5);
642        bytes.extend_from_slice(&self.header.to_bytes());
643        bytes.push(MRC_RUN);
644        bytes.push(SRC_RUN);
645        bytes.push(0xFF); // Program number high byte (current program)
646        bytes.push(0xFF); // Program number low byte
647        bytes.push(self.mode.code());
648        bytes
649    }
650}
651
652/// Command for stopping the PLC.
653#[derive(Debug, Clone)]
654pub struct StopCommand {
655    header: FinsHeader,
656}
657
658impl StopCommand {
659    /// Creates a new stop command.
660    ///
661    /// # Arguments
662    ///
663    /// * `destination` - Destination node address
664    /// * `source` - Source node address
665    /// * `sid` - Service ID for request/response matching
666    ///
667    /// # Example
668    ///
669    /// ```
670    /// use omron_fins::{StopCommand, NodeAddress};
671    ///
672    /// let cmd = StopCommand::new(
673    ///     NodeAddress::new(0, 10, 0),
674    ///     NodeAddress::new(0, 1, 0),
675    ///     0x01,
676    /// );
677    /// ```
678    pub fn new(destination: NodeAddress, source: NodeAddress, sid: u8) -> Self {
679        Self {
680            header: FinsHeader::new_command(destination, source, sid),
681        }
682    }
683
684    /// Returns the service ID.
685    pub fn sid(&self) -> u8 {
686        self.header.sid
687    }
688
689    /// Serializes the command to bytes for transmission.
690    pub fn to_bytes(&self) -> Vec<u8> {
691        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 2);
692        bytes.extend_from_slice(&self.header.to_bytes());
693        bytes.push(MRC_RUN);
694        bytes.push(SRC_STOP);
695        bytes
696    }
697}
698
699/// Command for transferring memory from one area to another.
700#[derive(Debug, Clone)]
701pub struct TransferCommand {
702    header: FinsHeader,
703    src_area: MemoryArea,
704    src_address: Address,
705    dst_area: MemoryArea,
706    dst_address: Address,
707    count: u16,
708}
709
710impl TransferCommand {
711    /// Creates a new transfer command.
712    ///
713    /// # Arguments
714    ///
715    /// * `destination` - Destination node address
716    /// * `source` - Source node address
717    /// * `sid` - Service ID for request/response matching
718    /// * `src_area` - Source memory area
719    /// * `src_address` - Source starting address
720    /// * `dst_area` - Destination memory area
721    /// * `dst_address` - Destination starting address
722    /// * `count` - Number of words to transfer (1 to maximum area capacity of the smallest area)
723    ///
724    /// # Errors
725    ///
726    /// Returns an error if count is 0 or exceeds the available capacity of either source or destination.
727    ///
728    /// # Example
729    ///
730    /// ```
731    /// use omron_fins::{TransferCommand, MemoryArea, NodeAddress};
732    ///
733    /// let cmd = TransferCommand::new(
734    ///     NodeAddress::new(0, 10, 0),
735    ///     NodeAddress::new(0, 1, 0),
736    ///     0x01,
737    ///     MemoryArea::DM,
738    ///     100,
739    ///     MemoryArea::DM,
740    ///     200,
741    ///     10,
742    /// ).unwrap();
743    /// ```
744    #[allow(clippy::too_many_arguments)]
745    pub fn new(
746        destination: NodeAddress,
747        source: NodeAddress,
748        sid: u8,
749        src_area: MemoryArea,
750        src_address: u16,
751        dst_area: MemoryArea,
752        dst_address: u16,
753        count: u16,
754    ) -> Result<Self> {
755        if count == 0 {
756            return Err(FinsError::invalid_parameter(
757                "count",
758                "must be greater than 0",
759            ));
760        }
761        let max_transfer = std::cmp::min(src_area.max_words(), dst_area.max_words());
762        if count > max_transfer {
763            return Err(FinsError::invalid_parameter(
764                "count",
765                format!("must not exceed area capacity of {} words", max_transfer),
766            ));
767        }
768
769        Ok(Self {
770            header: FinsHeader::new_command(destination, source, sid),
771            src_area,
772            src_address: Address::word(src_address),
773            dst_area,
774            dst_address: Address::word(dst_address),
775            count,
776        })
777    }
778
779    /// Returns the service ID.
780    pub fn sid(&self) -> u8 {
781        self.header.sid
782    }
783
784    /// Serializes the command to bytes for transmission.
785    pub fn to_bytes(&self) -> Vec<u8> {
786        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 12);
787        bytes.extend_from_slice(&self.header.to_bytes());
788        bytes.push(MRC_MEMORY_READ); // Memory commands use 0x01
789        bytes.push(SRC_MEMORY_TRANSFER);
790        bytes.push(self.src_area.word_code());
791        bytes.extend_from_slice(&self.src_address.to_bytes());
792        bytes.push(self.dst_area.word_code());
793        bytes.extend_from_slice(&self.dst_address.to_bytes());
794        bytes.push((self.count >> 8) as u8);
795        bytes.push((self.count & 0xFF) as u8);
796        bytes
797    }
798}
799
800/// Specification for forcing a bit.
801#[derive(Debug, Clone, Copy, PartialEq, Eq)]
802pub enum ForceSpec {
803    /// Force the bit OFF.
804    ForceOff,
805    /// Force the bit ON.
806    ForceOn,
807    /// Release the forced state.
808    Release,
809}
810
811impl ForceSpec {
812    /// Returns the FINS code for this spec.
813    pub(crate) fn code(self) -> u16 {
814        match self {
815            ForceSpec::ForceOff => 0x0000,
816            ForceSpec::ForceOn => 0x0001,
817            ForceSpec::Release => 0x8000,
818        }
819    }
820}
821
822/// A bit to be forced.
823#[derive(Debug, Clone)]
824pub struct ForcedBit {
825    /// Memory area of the bit.
826    pub area: MemoryArea,
827    /// Word address of the bit.
828    pub address: u16,
829    /// Bit position (0-15).
830    pub bit: u8,
831    /// Force specification.
832    pub spec: ForceSpec,
833}
834
835/// Command for forcing bits ON/OFF.
836#[derive(Debug, Clone)]
837pub struct ForcedSetResetCommand {
838    header: FinsHeader,
839    specs: Vec<ForcedBit>,
840}
841
842impl ForcedSetResetCommand {
843    /// Creates a new forced set/reset command.
844    ///
845    /// # Arguments
846    ///
847    /// * `destination` - Destination node address
848    /// * `source` - Source node address
849    /// * `sid` - Service ID for request/response matching
850    /// * `specs` - List of bits to force
851    ///
852    /// # Errors
853    ///
854    /// Returns an error if specs is empty, any area doesn't support bit access,
855    /// or any bit position is > 15.
856    ///
857    /// # Example
858    ///
859    /// ```
860    /// use omron_fins::{ForcedSetResetCommand, ForcedBit, ForceSpec, MemoryArea, NodeAddress};
861    ///
862    /// let cmd = ForcedSetResetCommand::new(
863    ///     NodeAddress::new(0, 10, 0),
864    ///     NodeAddress::new(0, 1, 0),
865    ///     0x01,
866    ///     vec![
867    ///         ForcedBit { area: MemoryArea::CIO, address: 0, bit: 0, spec: ForceSpec::ForceOn },
868    ///     ],
869    /// ).unwrap();
870    /// ```
871    pub fn new(
872        destination: NodeAddress,
873        source: NodeAddress,
874        sid: u8,
875        specs: Vec<ForcedBit>,
876    ) -> Result<Self> {
877        if specs.is_empty() {
878            return Err(FinsError::invalid_parameter("specs", "must not be empty"));
879        }
880
881        // Validate all specs
882        for spec in &specs {
883            spec.area.bit_code()?;
884            if spec.bit > 15 {
885                return Err(FinsError::invalid_parameter("bit", "must be 0-15"));
886            }
887        }
888
889        Ok(Self {
890            header: FinsHeader::new_command(destination, source, sid),
891            specs,
892        })
893    }
894
895    /// Returns the service ID.
896    pub fn sid(&self) -> u8 {
897        self.header.sid
898    }
899
900    /// Serializes the command to bytes for transmission.
901    pub fn to_bytes(&self) -> Result<Vec<u8>> {
902        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 4 + self.specs.len() * 6);
903        bytes.extend_from_slice(&self.header.to_bytes());
904        bytes.push(MRC_FORCED);
905        bytes.push(SRC_FORCED_SET_RESET);
906        bytes.push((self.specs.len() >> 8) as u8);
907        bytes.push((self.specs.len() & 0xFF) as u8);
908
909        for spec in &self.specs {
910            let code = spec.spec.code();
911            bytes.push((code >> 8) as u8);
912            bytes.push((code & 0xFF) as u8);
913            bytes.push(spec.area.bit_code()?);
914            bytes.push((spec.address >> 8) as u8);
915            bytes.push((spec.address & 0xFF) as u8);
916            bytes.push(spec.bit);
917        }
918
919        Ok(bytes)
920    }
921}
922
923/// Command for canceling all forced bits.
924#[derive(Debug, Clone)]
925pub struct ForcedSetResetCancelCommand {
926    header: FinsHeader,
927}
928
929impl ForcedSetResetCancelCommand {
930    /// Creates a new forced set/reset cancel command.
931    ///
932    /// # Arguments
933    ///
934    /// * `destination` - Destination node address
935    /// * `source` - Source node address
936    /// * `sid` - Service ID for request/response matching
937    ///
938    /// # Example
939    ///
940    /// ```
941    /// use omron_fins::{ForcedSetResetCancelCommand, NodeAddress};
942    ///
943    /// let cmd = ForcedSetResetCancelCommand::new(
944    ///     NodeAddress::new(0, 10, 0),
945    ///     NodeAddress::new(0, 1, 0),
946    ///     0x01,
947    /// );
948    /// ```
949    pub fn new(destination: NodeAddress, source: NodeAddress, sid: u8) -> Self {
950        Self {
951            header: FinsHeader::new_command(destination, source, sid),
952        }
953    }
954
955    /// Returns the service ID.
956    pub fn sid(&self) -> u8 {
957        self.header.sid
958    }
959
960    /// Serializes the command to bytes for transmission.
961    pub fn to_bytes(&self) -> Vec<u8> {
962        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 2);
963        bytes.extend_from_slice(&self.header.to_bytes());
964        bytes.push(MRC_FORCED);
965        bytes.push(SRC_FORCED_CANCEL);
966        bytes
967    }
968}
969
970/// Specification for reading from multiple memory areas.
971#[derive(Debug, Clone)]
972pub struct MultiReadSpec {
973    /// Memory area to read from.
974    pub area: MemoryArea,
975    /// Word address.
976    pub address: u16,
977    /// Optional bit position (None for word, Some(n) for bit n).
978    pub bit: Option<u8>,
979}
980
981/// Command for reading from multiple memory areas.
982#[derive(Debug, Clone)]
983pub struct MultipleReadCommand {
984    header: FinsHeader,
985    specs: Vec<MultiReadSpec>,
986}
987
988impl MultipleReadCommand {
989    /// Creates a new multiple memory area read command.
990    ///
991    /// # Arguments
992    ///
993    /// * `destination` - Destination node address
994    /// * `source` - Source node address
995    /// * `sid` - Service ID for request/response matching
996    /// * `specs` - List of read specifications
997    ///
998    /// # Errors
999    ///
1000    /// Returns an error if specs is empty, any bit area doesn't support bit access,
1001    /// or any bit position is > 15.
1002    ///
1003    /// # Example
1004    ///
1005    /// ```
1006    /// use omron_fins::{MultipleReadCommand, MultiReadSpec, MemoryArea, NodeAddress};
1007    ///
1008    /// let cmd = MultipleReadCommand::new(
1009    ///     NodeAddress::new(0, 10, 0),
1010    ///     NodeAddress::new(0, 1, 0),
1011    ///     0x01,
1012    ///     vec![
1013    ///         MultiReadSpec { area: MemoryArea::DM, address: 100, bit: None },
1014    ///         MultiReadSpec { area: MemoryArea::DM, address: 200, bit: None },
1015    ///     ],
1016    /// ).unwrap();
1017    /// ```
1018    pub fn new(
1019        destination: NodeAddress,
1020        source: NodeAddress,
1021        sid: u8,
1022        specs: Vec<MultiReadSpec>,
1023    ) -> Result<Self> {
1024        if specs.is_empty() {
1025            return Err(FinsError::invalid_parameter("specs", "must not be empty"));
1026        }
1027
1028        // Validate all specs
1029        for spec in &specs {
1030            if let Some(bit) = spec.bit {
1031                spec.area.bit_code()?;
1032                if bit > 15 {
1033                    return Err(FinsError::invalid_parameter("bit", "must be 0-15"));
1034                }
1035            }
1036        }
1037
1038        Ok(Self {
1039            header: FinsHeader::new_command(destination, source, sid),
1040            specs,
1041        })
1042    }
1043
1044    /// Returns the service ID.
1045    pub fn sid(&self) -> u8 {
1046        self.header.sid
1047    }
1048
1049    /// Serializes the command to bytes for transmission.
1050    pub fn to_bytes(&self) -> Result<Vec<u8>> {
1051        let mut bytes = Vec::with_capacity(FINS_HEADER_SIZE + 2 + self.specs.len() * 4);
1052        bytes.extend_from_slice(&self.header.to_bytes());
1053        bytes.push(MRC_MEMORY_READ);
1054        bytes.push(SRC_MULTIPLE_READ);
1055
1056        for spec in &self.specs {
1057            if let Some(bit) = spec.bit {
1058                bytes.push(spec.area.bit_code()?);
1059                bytes.push((spec.address >> 8) as u8);
1060                bytes.push((spec.address & 0xFF) as u8);
1061                bytes.push(bit);
1062            } else {
1063                bytes.push(spec.area.word_code());
1064                bytes.push((spec.address >> 8) as u8);
1065                bytes.push((spec.address & 0xFF) as u8);
1066                bytes.push(0x00);
1067            }
1068        }
1069
1070        Ok(bytes)
1071    }
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076    use super::*;
1077
1078    fn test_addresses() -> (NodeAddress, NodeAddress) {
1079        (NodeAddress::new(0, 10, 0), NodeAddress::new(0, 1, 0))
1080    }
1081
1082    #[test]
1083    fn test_address_word() {
1084        let addr = Address::word(0x1234);
1085        assert_eq!(addr.word, 0x1234);
1086        assert_eq!(addr.bit, 0);
1087        assert_eq!(addr.to_bytes(), [0x12, 0x34, 0x00]);
1088    }
1089
1090    #[test]
1091    fn test_address_bit() {
1092        let addr = Address::bit(0x1234, 5).unwrap();
1093        assert_eq!(addr.word, 0x1234);
1094        assert_eq!(addr.bit, 5);
1095        assert_eq!(addr.to_bytes(), [0x12, 0x34, 0x05]);
1096    }
1097
1098    #[test]
1099    fn test_address_bit_invalid() {
1100        let result = Address::bit(100, 16);
1101        assert!(result.is_err());
1102    }
1103
1104    #[test]
1105    fn test_read_word_command_serialization() {
1106        let (dest, src) = test_addresses();
1107        let cmd = ReadWordCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 10).unwrap();
1108        let bytes = cmd.to_bytes();
1109
1110        // Header (10 bytes) + MRC + SRC + Area + Address (3 bytes) + Count (2 bytes) = 18 bytes
1111        assert_eq!(bytes.len(), 18);
1112
1113        // Check header
1114        assert_eq!(bytes[0], 0x80); // ICF
1115        assert_eq!(bytes[9], 0x01); // SID
1116
1117        // Check command
1118        assert_eq!(bytes[10], MRC_MEMORY_READ);
1119        assert_eq!(bytes[11], SRC_MEMORY_READ);
1120        assert_eq!(bytes[12], 0x82); // DM word code
1121
1122        // Check address (100 = 0x0064)
1123        assert_eq!(bytes[13], 0x00);
1124        assert_eq!(bytes[14], 0x64);
1125        assert_eq!(bytes[15], 0x00); // bit
1126
1127        // Check count (10 = 0x000A)
1128        assert_eq!(bytes[16], 0x00);
1129        assert_eq!(bytes[17], 0x0A);
1130    }
1131
1132    #[test]
1133    fn test_read_word_command_invalid_count() {
1134        let (dest, src) = test_addresses();
1135
1136        let result = ReadWordCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 0);
1137        assert!(result.is_err());
1138
1139        let result = ReadWordCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 4097);
1140        assert!(result.is_err());
1141    }
1142
1143    #[test]
1144    fn test_write_word_command_serialization() {
1145        let (dest, src) = test_addresses();
1146        let cmd =
1147            WriteWordCommand::new(dest, src, 0x02, MemoryArea::DM, 100, &[0x1234, 0x5678]).unwrap();
1148        let bytes = cmd.to_bytes();
1149
1150        // Header (10) + MRC + SRC + Area + Address (3) + Count (2) + Data (4) = 22 bytes
1151        assert_eq!(bytes.len(), 22);
1152
1153        // Check command codes
1154        assert_eq!(bytes[10], MRC_MEMORY_WRITE);
1155        assert_eq!(bytes[11], SRC_MEMORY_WRITE);
1156
1157        // Check count (2)
1158        assert_eq!(bytes[16], 0x00);
1159        assert_eq!(bytes[17], 0x02);
1160
1161        // Check data
1162        assert_eq!(bytes[18], 0x12);
1163        assert_eq!(bytes[19], 0x34);
1164        assert_eq!(bytes[20], 0x56);
1165        assert_eq!(bytes[21], 0x78);
1166    }
1167
1168    #[test]
1169    fn test_write_word_command_invalid_data() {
1170        let (dest, src) = test_addresses();
1171
1172        let result = WriteWordCommand::new(dest, src, 0x01, MemoryArea::DM, 100, &[]);
1173        assert!(result.is_err());
1174    }
1175
1176    #[test]
1177    fn test_read_bit_command_serialization() {
1178        let (dest, src) = test_addresses();
1179        let cmd = ReadBitCommand::new(dest, src, 0x03, MemoryArea::CIO, 100, 5).unwrap();
1180        let bytes = cmd.to_bytes().unwrap();
1181
1182        // Header (10) + MRC + SRC + Area + Address (3) + Count (2) = 18 bytes
1183        assert_eq!(bytes.len(), 18);
1184
1185        // Check area code (CIO bit)
1186        assert_eq!(bytes[12], 0x30);
1187
1188        // Check address with bit
1189        assert_eq!(bytes[13], 0x00);
1190        assert_eq!(bytes[14], 0x64); // 100
1191        assert_eq!(bytes[15], 0x05); // bit 5
1192
1193        // Check count (always 1 for bit)
1194        assert_eq!(bytes[16], 0x00);
1195        assert_eq!(bytes[17], 0x01);
1196    }
1197
1198    #[test]
1199    fn test_read_bit_command_dm_fails() {
1200        let (dest, src) = test_addresses();
1201        let result = ReadBitCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 5);
1202        assert!(result.is_err());
1203    }
1204
1205    #[test]
1206    fn test_write_bit_command_serialization() {
1207        let (dest, src) = test_addresses();
1208        let cmd = WriteBitCommand::new(dest, src, 0x04, MemoryArea::WR, 50, 10, true).unwrap();
1209        let bytes = cmd.to_bytes().unwrap();
1210
1211        // Header (10) + MRC + SRC + Area + Address (3) + Count (2) + Data (1) = 19 bytes
1212        assert_eq!(bytes.len(), 19);
1213
1214        // Check area code (WR bit)
1215        assert_eq!(bytes[12], 0x31);
1216
1217        // Check address with bit
1218        assert_eq!(bytes[13], 0x00);
1219        assert_eq!(bytes[14], 0x32); // 50
1220        assert_eq!(bytes[15], 0x0A); // bit 10
1221
1222        // Check value
1223        assert_eq!(bytes[18], 0x01); // true
1224    }
1225
1226    #[test]
1227    fn test_write_bit_command_false_value() {
1228        let (dest, src) = test_addresses();
1229        let cmd = WriteBitCommand::new(dest, src, 0x05, MemoryArea::HR, 200, 0, false).unwrap();
1230        let bytes = cmd.to_bytes().unwrap();
1231
1232        assert_eq!(bytes[12], 0x32); // HR bit code
1233        assert_eq!(bytes[18], 0x00); // false
1234    }
1235
1236    #[test]
1237    fn test_fill_command_serialization() {
1238        let (dest, src) = test_addresses();
1239        let cmd = FillCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 50, 0xABCD).unwrap();
1240        let bytes = cmd.to_bytes();
1241
1242        // Header (10) + MRC + SRC + Area + Address (3) + Count (2) + Value (2) = 20 bytes
1243        assert_eq!(bytes.len(), 20);
1244
1245        // Check command codes
1246        assert_eq!(bytes[10], MRC_MEMORY_READ); // 0x01
1247        assert_eq!(bytes[11], SRC_MEMORY_FILL); // 0x03
1248        assert_eq!(bytes[12], 0x82); // DM word code
1249
1250        // Check address (100 = 0x0064)
1251        assert_eq!(bytes[13], 0x00);
1252        assert_eq!(bytes[14], 0x64);
1253        assert_eq!(bytes[15], 0x00); // bit
1254
1255        // Check count (50 = 0x0032)
1256        assert_eq!(bytes[16], 0x00);
1257        assert_eq!(bytes[17], 0x32);
1258
1259        // Check value (0xABCD)
1260        assert_eq!(bytes[18], 0xAB);
1261        assert_eq!(bytes[19], 0xCD);
1262    }
1263
1264    #[test]
1265    fn test_fill_command_invalid_count() {
1266        let (dest, src) = test_addresses();
1267
1268        let result = FillCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 0, 0x0000);
1269        assert!(result.is_err());
1270
1271        let result = FillCommand::new(dest, src, 0x01, MemoryArea::DM, 100, 4097, 0x0000);
1272        assert!(result.is_err());
1273    }
1274
1275    #[test]
1276    fn test_run_command_serialization() {
1277        let (dest, src) = test_addresses();
1278        let cmd = RunCommand::new(dest, src, 0x01, PlcMode::Monitor);
1279        let bytes = cmd.to_bytes();
1280
1281        // Header (10) + MRC + SRC + Program (2) + Mode (1) = 15 bytes
1282        assert_eq!(bytes.len(), 15);
1283
1284        // Check command codes
1285        assert_eq!(bytes[10], MRC_RUN); // 0x04
1286        assert_eq!(bytes[11], SRC_RUN); // 0x01
1287
1288        // Check program number (0xFFFF = current)
1289        assert_eq!(bytes[12], 0xFF);
1290        assert_eq!(bytes[13], 0xFF);
1291
1292        // Check mode (Monitor = 0x02)
1293        assert_eq!(bytes[14], 0x02);
1294    }
1295
1296    #[test]
1297    fn test_run_command_modes() {
1298        let (dest, src) = test_addresses();
1299
1300        let cmd = RunCommand::new(dest, src, 0x01, PlcMode::Debug);
1301        assert_eq!(cmd.to_bytes()[14], 0x01);
1302
1303        let cmd = RunCommand::new(dest, src, 0x01, PlcMode::Monitor);
1304        assert_eq!(cmd.to_bytes()[14], 0x02);
1305
1306        let cmd = RunCommand::new(dest, src, 0x01, PlcMode::Run);
1307        assert_eq!(cmd.to_bytes()[14], 0x04);
1308    }
1309
1310    #[test]
1311    fn test_stop_command_serialization() {
1312        let (dest, src) = test_addresses();
1313        let cmd = StopCommand::new(dest, src, 0x01);
1314        let bytes = cmd.to_bytes();
1315
1316        // Header (10) + MRC + SRC = 12 bytes
1317        assert_eq!(bytes.len(), 12);
1318
1319        // Check command codes
1320        assert_eq!(bytes[10], MRC_RUN); // 0x04
1321        assert_eq!(bytes[11], SRC_STOP); // 0x02
1322    }
1323
1324    #[test]
1325    fn test_transfer_command_serialization() {
1326        let (dest, src) = test_addresses();
1327        let cmd = TransferCommand::new(
1328            dest,
1329            src,
1330            0x01,
1331            MemoryArea::DM,
1332            100,
1333            MemoryArea::DM,
1334            200,
1335            10,
1336        )
1337        .unwrap();
1338        let bytes = cmd.to_bytes();
1339
1340        // Header (10) + MRC + SRC + SrcArea + SrcAddr (3) + DstArea + DstAddr (3) + Count (2) = 22 bytes
1341        assert_eq!(bytes.len(), 22);
1342
1343        // Check command codes
1344        assert_eq!(bytes[10], MRC_MEMORY_READ); // 0x01
1345        assert_eq!(bytes[11], SRC_MEMORY_TRANSFER); // 0x05
1346
1347        // Check source area and address
1348        assert_eq!(bytes[12], 0x82); // DM word code
1349        assert_eq!(bytes[13], 0x00);
1350        assert_eq!(bytes[14], 0x64); // 100
1351        assert_eq!(bytes[15], 0x00);
1352
1353        // Check destination area and address
1354        assert_eq!(bytes[16], 0x82); // DM word code
1355        assert_eq!(bytes[17], 0x00);
1356        assert_eq!(bytes[18], 0xC8); // 200
1357        assert_eq!(bytes[19], 0x00);
1358
1359        // Check count (10 = 0x000A)
1360        assert_eq!(bytes[20], 0x00);
1361        assert_eq!(bytes[21], 0x0A);
1362    }
1363
1364    #[test]
1365    fn test_transfer_command_invalid_count() {
1366        let (dest, src) = test_addresses();
1367
1368        let result =
1369            TransferCommand::new(dest, src, 0x01, MemoryArea::DM, 100, MemoryArea::DM, 200, 0);
1370        assert!(result.is_err());
1371
1372        let result = TransferCommand::new(
1373            dest,
1374            src,
1375            0x01,
1376            MemoryArea::DM,
1377            100,
1378            MemoryArea::DM,
1379            200,
1380            4097,
1381        );
1382        assert!(result.is_err());
1383    }
1384
1385    #[test]
1386    fn test_forced_set_reset_command_serialization() {
1387        let (dest, src) = test_addresses();
1388        let cmd = ForcedSetResetCommand::new(
1389            dest,
1390            src,
1391            0x01,
1392            vec![
1393                ForcedBit {
1394                    area: MemoryArea::CIO,
1395                    address: 0,
1396                    bit: 0,
1397                    spec: ForceSpec::ForceOn,
1398                },
1399                ForcedBit {
1400                    area: MemoryArea::CIO,
1401                    address: 0,
1402                    bit: 1,
1403                    spec: ForceSpec::ForceOff,
1404                },
1405            ],
1406        )
1407        .unwrap();
1408        let bytes = cmd.to_bytes().unwrap();
1409
1410        // Header (10) + MRC + SRC + Count (2) + 2 * Spec (6) = 26 bytes
1411        assert_eq!(bytes.len(), 26);
1412
1413        // Check command codes
1414        assert_eq!(bytes[10], MRC_FORCED); // 0x23
1415        assert_eq!(bytes[11], SRC_FORCED_SET_RESET); // 0x01
1416
1417        // Check count (2)
1418        assert_eq!(bytes[12], 0x00);
1419        assert_eq!(bytes[13], 0x02);
1420
1421        // Check first spec (ForceOn)
1422        assert_eq!(bytes[14], 0x00); // spec code high
1423        assert_eq!(bytes[15], 0x01); // spec code low (ForceOn = 0x0001)
1424        assert_eq!(bytes[16], 0x30); // CIO bit code
1425        assert_eq!(bytes[17], 0x00); // address high
1426        assert_eq!(bytes[18], 0x00); // address low
1427        assert_eq!(bytes[19], 0x00); // bit
1428
1429        // Check second spec (ForceOff)
1430        assert_eq!(bytes[20], 0x00); // spec code high
1431        assert_eq!(bytes[21], 0x00); // spec code low (ForceOff = 0x0000)
1432        assert_eq!(bytes[22], 0x30); // CIO bit code
1433        assert_eq!(bytes[23], 0x00); // address high
1434        assert_eq!(bytes[24], 0x00); // address low
1435        assert_eq!(bytes[25], 0x01); // bit
1436    }
1437
1438    #[test]
1439    fn test_forced_set_reset_command_empty_specs() {
1440        let (dest, src) = test_addresses();
1441        let result = ForcedSetResetCommand::new(dest, src, 0x01, vec![]);
1442        assert!(result.is_err());
1443    }
1444
1445    #[test]
1446    fn test_forced_set_reset_command_dm_fails() {
1447        let (dest, src) = test_addresses();
1448        let result = ForcedSetResetCommand::new(
1449            dest,
1450            src,
1451            0x01,
1452            vec![ForcedBit {
1453                area: MemoryArea::DM,
1454                address: 0,
1455                bit: 0,
1456                spec: ForceSpec::ForceOn,
1457            }],
1458        );
1459        assert!(result.is_err());
1460    }
1461
1462    #[test]
1463    fn test_forced_set_reset_cancel_command_serialization() {
1464        let (dest, src) = test_addresses();
1465        let cmd = ForcedSetResetCancelCommand::new(dest, src, 0x01);
1466        let bytes = cmd.to_bytes();
1467
1468        // Header (10) + MRC + SRC = 12 bytes
1469        assert_eq!(bytes.len(), 12);
1470
1471        // Check command codes
1472        assert_eq!(bytes[10], MRC_FORCED); // 0x23
1473        assert_eq!(bytes[11], SRC_FORCED_CANCEL); // 0x02
1474    }
1475
1476    #[test]
1477    fn test_multiple_read_command_serialization() {
1478        let (dest, src) = test_addresses();
1479        let cmd = MultipleReadCommand::new(
1480            dest,
1481            src,
1482            0x01,
1483            vec![
1484                MultiReadSpec {
1485                    area: MemoryArea::DM,
1486                    address: 100,
1487                    bit: None,
1488                },
1489                MultiReadSpec {
1490                    area: MemoryArea::DM,
1491                    address: 200,
1492                    bit: None,
1493                },
1494                MultiReadSpec {
1495                    area: MemoryArea::CIO,
1496                    address: 0,
1497                    bit: Some(5),
1498                },
1499            ],
1500        )
1501        .unwrap();
1502        let bytes = cmd.to_bytes().unwrap();
1503
1504        // Header (10) + MRC + SRC + 3 * Spec (4) = 24 bytes
1505        assert_eq!(bytes.len(), 24);
1506
1507        // Check command codes
1508        assert_eq!(bytes[10], MRC_MEMORY_READ); // 0x01
1509        assert_eq!(bytes[11], SRC_MULTIPLE_READ); // 0x04
1510
1511        // Check first spec (DM100 word)
1512        assert_eq!(bytes[12], 0x82); // DM word code
1513        assert_eq!(bytes[13], 0x00);
1514        assert_eq!(bytes[14], 0x64); // 100
1515        assert_eq!(bytes[15], 0x00);
1516
1517        // Check second spec (DM200 word)
1518        assert_eq!(bytes[16], 0x82); // DM word code
1519        assert_eq!(bytes[17], 0x00);
1520        assert_eq!(bytes[18], 0xC8); // 200
1521        assert_eq!(bytes[19], 0x00);
1522
1523        // Check third spec (CIO0.05 bit)
1524        assert_eq!(bytes[20], 0x30); // CIO bit code
1525        assert_eq!(bytes[21], 0x00);
1526        assert_eq!(bytes[22], 0x00); // 0
1527        assert_eq!(bytes[23], 0x05); // bit 5
1528    }
1529
1530    #[test]
1531    fn test_multiple_read_command_empty_specs() {
1532        let (dest, src) = test_addresses();
1533        let result = MultipleReadCommand::new(dest, src, 0x01, vec![]);
1534        assert!(result.is_err());
1535    }
1536
1537    #[test]
1538    fn test_multiple_read_command_dm_bit_fails() {
1539        let (dest, src) = test_addresses();
1540        let result = MultipleReadCommand::new(
1541            dest,
1542            src,
1543            0x01,
1544            vec![MultiReadSpec {
1545                area: MemoryArea::DM,
1546                address: 100,
1547                bit: Some(5),
1548            }],
1549        );
1550        assert!(result.is_err());
1551    }
1552
1553    #[test]
1554    fn test_force_spec_codes() {
1555        assert_eq!(ForceSpec::ForceOff.code(), 0x0000);
1556        assert_eq!(ForceSpec::ForceOn.code(), 0x0001);
1557        assert_eq!(ForceSpec::Release.code(), 0x8000);
1558    }
1559
1560    #[test]
1561    fn test_plc_mode_codes() {
1562        assert_eq!(PlcMode::Debug.code(), 0x01);
1563        assert_eq!(PlcMode::Monitor.code(), 0x02);
1564        assert_eq!(PlcMode::Run.code(), 0x04);
1565    }
1566}