Skip to main content

omron_fins/
client.rs

1//! High-level FINS client for communicating with Omron PLCs.
2//!
3//! This module provides the [`Client`] struct, which is the primary interface
4//! for communicating with Omron PLCs using the FINS protocol.
5//!
6//! # Overview
7//!
8//! The client provides a high-level API that handles:
9//! - Command construction and serialization
10//! - Request/response correlation via Service ID
11//! - Response parsing and error checking
12//! - Type conversion helpers (f32, f64, i32)
13//!
14//! # Example
15//!
16//! ```no_run
17//! use omron_fins::{Client, ClientConfig, MemoryArea};
18//! use std::net::Ipv4Addr;
19//!
20//! // Create and configure the client
21//! let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0);
22//! let client = Client::new(config)?;
23//!
24//! // Read data
25//! let data = client.read(MemoryArea::DM, 100, 10)?;
26//!
27//! // Write data
28//! client.write(MemoryArea::DM, 200, &[0x1234, 0x5678])?;
29//!
30//! // Read/write bits
31//! let bit = client.read_bit(MemoryArea::CIO, 0, 5)?;
32//! client.write_bit(MemoryArea::CIO, 0, 5, true)?;
33//!
34//! // Read/write typed values
35//! let temp: f32 = client.read_f32(MemoryArea::DM, 100)?;
36//! client.write_f32(MemoryArea::DM, 100, 25.5)?;
37//! # Ok::<(), omron_fins::FinsError>(())
38//! ```
39//!
40//! # Configuration
41//!
42//! The [`ClientConfig`] struct allows customization of:
43//! - PLC IP address and port
44//! - Communication timeout
45//! - Source and destination node addresses
46//! - Network addressing for multi-network setups
47//!
48//! # Thread Safety
49//!
50//! The `Client` uses an atomic counter for Service IDs, making it safe to share
51//! between threads. However, the underlying UDP socket operations are synchronous
52//! and will block.
53
54use std::net::SocketAddr;
55use std::sync::atomic::{AtomicU8, Ordering};
56use std::time::Duration;
57
58use crate::command::{
59    FillCommand, ForcedBit, ForcedSetResetCancelCommand, ForcedSetResetCommand, MultiReadSpec,
60    MultipleReadCommand, PlcMode, ReadBitCommand, ReadWordCommand, RunCommand, StopCommand,
61    TransferCommand, WriteBitCommand, WriteWordCommand, MAX_WORDS_PER_COMMAND,
62};
63use crate::error::Result;
64use crate::header::NodeAddress;
65use crate::memory::MemoryArea;
66use crate::response::FinsResponse;
67use crate::transport::{UdpTransport, DEFAULT_FINS_PORT, DEFAULT_TIMEOUT};
68use crate::types::{DataType, PlcValue};
69
70/// Configuration for creating a FINS client.
71#[derive(Debug, Clone)]
72pub struct ClientConfig {
73    /// PLC IP address or hostname.
74    pub plc_addr: SocketAddr,
75    /// Source node address (this client).
76    pub source: NodeAddress,
77    /// Destination node address (the PLC).
78    pub destination: NodeAddress,
79    /// Communication timeout.
80    pub timeout: Duration,
81}
82
83impl ClientConfig {
84    /// Creates a new client configuration with minimal required parameters.
85    ///
86    /// Uses default timeout and local node addresses.
87    ///
88    /// # Arguments
89    ///
90    /// * `plc_ip` - PLC IP address (port defaults to 9600)
91    /// * `source_node` - Source node number (this client)
92    /// * `dest_node` - Destination node number (the PLC)
93    ///
94    /// # Example
95    ///
96    /// ```
97    /// use omron_fins::ClientConfig;
98    /// use std::net::Ipv4Addr;
99    ///
100    /// let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0);
101    /// ```
102    pub fn new(plc_ip: std::net::Ipv4Addr, source_node: u8, dest_node: u8) -> Self {
103        Self {
104            plc_addr: SocketAddr::from((plc_ip, DEFAULT_FINS_PORT)),
105            source: NodeAddress::new(0, source_node, 0),
106            destination: NodeAddress::new(0, dest_node, 0),
107            timeout: DEFAULT_TIMEOUT,
108        }
109    }
110
111    /// Sets a custom PLC port (default is 9600).
112    ///
113    /// # Example
114    ///
115    /// ```
116    /// use omron_fins::ClientConfig;
117    /// use std::net::Ipv4Addr;
118    ///
119    /// let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0)
120    ///     .with_port(9601);
121    /// ```
122    pub fn with_port(mut self, port: u16) -> Self {
123        self.plc_addr.set_port(port);
124        self
125    }
126
127    /// Sets a custom timeout (default is 2 seconds).
128    ///
129    /// # Example
130    ///
131    /// ```
132    /// use omron_fins::ClientConfig;
133    /// use std::net::Ipv4Addr;
134    /// use std::time::Duration;
135    ///
136    /// let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0)
137    ///     .with_timeout(Duration::from_secs(5));
138    /// ```
139    pub fn with_timeout(mut self, timeout: Duration) -> Self {
140        self.timeout = timeout;
141        self
142    }
143
144    /// Sets custom source network/unit addresses.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use omron_fins::ClientConfig;
150    /// use std::net::Ipv4Addr;
151    ///
152    /// let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0)
153    ///     .with_source_network(1)
154    ///     .with_source_unit(0);
155    /// ```
156    pub fn with_source_network(mut self, network: u8) -> Self {
157        self.source.network = network;
158        self
159    }
160
161    /// Sets custom source unit address.
162    pub fn with_source_unit(mut self, unit: u8) -> Self {
163        self.source.unit = unit;
164        self
165    }
166
167    /// Sets custom destination network/unit addresses.
168    pub fn with_dest_network(mut self, network: u8) -> Self {
169        self.destination.network = network;
170        self
171    }
172
173    /// Sets custom destination unit address.
174    pub fn with_dest_unit(mut self, unit: u8) -> Self {
175        self.destination.unit = unit;
176        self
177    }
178}
179
180/// FINS client for communicating with Omron PLCs.
181///
182/// Provides a simple API for reading and writing PLC memory.
183/// Each operation produces exactly 1 request and 1 response.
184/// No automatic retries, caching, or reconnection.
185///
186/// # Example
187///
188/// ```no_run
189/// use omron_fins::{Client, ClientConfig, MemoryArea};
190/// use std::net::Ipv4Addr;
191///
192/// let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0);
193/// let client = Client::new(config).unwrap();
194///
195/// // Read 10 words from DM100
196/// let data = client.read(MemoryArea::DM, 100, 10).unwrap();
197///
198/// // Write values to DM200
199/// client.write(MemoryArea::DM, 200, &[0x1234, 0x5678]).unwrap();
200///
201/// // Read a single bit
202/// let bit = client.read_bit(MemoryArea::CIO, 0, 5).unwrap();
203///
204/// // Write a single bit
205/// client.write_bit(MemoryArea::CIO, 0, 5, true).unwrap();
206/// ```
207pub struct Client {
208    transport: UdpTransport,
209    source: NodeAddress,
210    destination: NodeAddress,
211    sid_counter: AtomicU8,
212}
213
214impl Client {
215    /// Creates a new FINS client with the given configuration.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the UDP transport cannot be created.
220    ///
221    /// # Example
222    ///
223    /// ```no_run
224    /// use omron_fins::{Client, ClientConfig};
225    /// use std::net::Ipv4Addr;
226    ///
227    /// let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0);
228    /// let client = Client::new(config).unwrap();
229    /// ```
230    pub fn new(config: ClientConfig) -> Result<Self> {
231        let transport = UdpTransport::new(config.plc_addr, config.timeout)?;
232
233        // Drain any stale packets from previous sessions
234        transport.drain_pending();
235
236        Ok(Self {
237            transport,
238            source: config.source,
239            destination: config.destination,
240            sid_counter: AtomicU8::new(0),
241        })
242    }
243
244    /// Generates the next Service ID.
245    fn next_sid(&self) -> u8 {
246        self.sid_counter.fetch_add(1, Ordering::Relaxed)
247    }
248
249    /// Sends a command and receives the response, with SID validation and retry.
250    ///
251    /// If the received response has a mismatched SID (stale packet), it will
252    /// drain pending packets and retry up to MAX_SID_RETRIES times.
253    fn send_receive_with_sid(&self, data: &[u8], expected_sid: u8) -> Result<FinsResponse> {
254        use crate::error::FinsError;
255        const MAX_SID_RETRIES: usize = 3;
256
257        for attempt in 0..=MAX_SID_RETRIES {
258            // On retry, drain any stale packets first
259            if attempt > 0 {
260                self.transport.drain_pending();
261            }
262
263            let response_bytes = self.transport.send_receive(data)?;
264            let response = FinsResponse::from_bytes(&response_bytes)?;
265
266            if response.header.sid == expected_sid {
267                return Ok(response);
268            }
269
270            // Log mismatch on first attempt only (for debugging)
271            if attempt == 0 {
272                // SID mismatch - stale packet detected, will retry
273            }
274        }
275
276        // All retries failed - return error with last received SID
277        // Drain and try one more time to get the actual received SID for error message
278        self.transport.drain_pending();
279        let response_bytes = self.transport.send_receive(data)?;
280        let response = FinsResponse::from_bytes(&response_bytes)?;
281        Err(FinsError::sid_mismatch(expected_sid, response.header.sid))
282    }
283
284    /// Reads words from PLC memory.
285    ///
286    /// # Arguments
287    ///
288    /// * `area` - Memory area to read from
289    /// * `address` - Starting word address
290    /// * `count` - Number of words to read (1-999)
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if:
295    /// - Count is 0 or > 999
296    /// - Communication fails
297    /// - PLC returns an error
298    ///
299    /// # Example
300    ///
301    /// ```no_run
302    /// use omron_fins::{Client, ClientConfig, MemoryArea};
303    /// use std::net::Ipv4Addr;
304    ///
305    /// let client = Client::new(ClientConfig::new(
306    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
307    /// )).unwrap();
308    ///
309    /// let data = client.read(MemoryArea::DM, 100, 10).unwrap();
310    /// println!("Read {} words: {:?}", data.len(), data);
311    /// ```
312    pub fn read(&self, area: MemoryArea, mut address: u16, mut count: u16) -> Result<Vec<u16>> {
313        area.check_bounds(address, count)?;
314
315        let mut result = Vec::with_capacity(count as usize);
316
317        while count > 0 {
318            let chunk_size = std::cmp::min(count, MAX_WORDS_PER_COMMAND);
319
320            let sid = self.next_sid();
321            let cmd = ReadWordCommand::new(
322                self.destination,
323                self.source,
324                sid,
325                area,
326                address,
327                chunk_size,
328            )?;
329            let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
330            response.check_error()?;
331
332            let words = response.to_words()?;
333            result.extend(words);
334
335            address += chunk_size;
336            count -= chunk_size;
337
338            if count > 0 {
339                std::thread::sleep(std::time::Duration::from_millis(1));
340            }
341        }
342
343        Ok(result)
344    }
345
346    /// Writes words to PLC memory.
347    ///
348    /// # Arguments
349    ///
350    /// * `area` - Memory area to write to
351    /// * `address` - Starting word address
352    /// * `data` - Words to write (1-999 words)
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if:
357    /// - Data is empty or > 999 words
358    /// - Communication fails
359    /// - PLC returns an error
360    ///
361    /// # Example
362    ///
363    /// ```no_run
364    /// use omron_fins::{Client, ClientConfig, MemoryArea};
365    /// use std::net::Ipv4Addr;
366    ///
367    /// let client = Client::new(ClientConfig::new(
368    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
369    /// )).unwrap();
370    ///
371    /// client.write(MemoryArea::DM, 100, &[0x1234, 0x5678]).unwrap();
372    /// ```
373    pub fn write(&self, area: MemoryArea, mut address: u16, data: &[u16]) -> Result<()> {
374        area.check_bounds(address, data.len() as u16)?;
375
376        let mut data_index = 0;
377        let mut count = data.len() as u16;
378
379        while count > 0 {
380            let chunk_size = std::cmp::min(count, MAX_WORDS_PER_COMMAND);
381            let chunk_data = &data[data_index..(data_index + chunk_size as usize)];
382
383            let sid = self.next_sid();
384            let cmd = WriteWordCommand::new(
385                self.destination,
386                self.source,
387                sid,
388                area,
389                address,
390                chunk_data,
391            )?;
392            let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
393            response.check_error()?;
394
395            address += chunk_size;
396            data_index += chunk_size as usize;
397            count -= chunk_size;
398
399            if count > 0 {
400                std::thread::sleep(std::time::Duration::from_millis(1));
401            }
402        }
403
404        Ok(())
405    }
406
407    /// Reads a single bit from PLC memory.
408    ///
409    /// # Arguments
410    ///
411    /// * `area` - Memory area to read from (must support bit access)
412    /// * `address` - Word address
413    /// * `bit` - Bit position (0-15)
414    ///
415    /// # Errors
416    ///
417    /// Returns an error if:
418    /// - Area doesn't support bit access (DM)
419    /// - Bit position > 15
420    /// - Communication fails
421    /// - PLC returns an error
422    ///
423    /// # Example
424    ///
425    /// ```no_run
426    /// use omron_fins::{Client, ClientConfig, MemoryArea};
427    /// use std::net::Ipv4Addr;
428    ///
429    /// let client = Client::new(ClientConfig::new(
430    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
431    /// )).unwrap();
432    ///
433    /// let bit = client.read_bit(MemoryArea::CIO, 0, 5).unwrap();
434    /// println!("CIO 0.05 = {}", bit);
435    /// ```
436    pub fn read_bit(&self, area: MemoryArea, address: u16, bit: u8) -> Result<bool> {
437        let sid = self.next_sid();
438        let cmd = ReadBitCommand::new(self.destination, self.source, sid, area, address, bit)?;
439
440        let response = self.send_receive_with_sid(&cmd.to_bytes()?, sid)?;
441        response.check_error()?;
442        response.to_bit()
443    }
444
445    /// Writes a single bit to PLC memory.
446    ///
447    /// # Arguments
448    ///
449    /// * `area` - Memory area to write to (must support bit access)
450    /// * `address` - Word address
451    /// * `bit` - Bit position (0-15)
452    /// * `value` - Bit value to write
453    ///
454    /// # Errors
455    ///
456    /// Returns an error if:
457    /// - Area doesn't support bit access (DM)
458    /// - Bit position > 15
459    /// - Communication fails
460    /// - PLC returns an error
461    ///
462    /// # Example
463    ///
464    /// ```no_run
465    /// use omron_fins::{Client, ClientConfig, MemoryArea};
466    /// use std::net::Ipv4Addr;
467    ///
468    /// let client = Client::new(ClientConfig::new(
469    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
470    /// )).unwrap();
471    ///
472    /// client.write_bit(MemoryArea::CIO, 0, 5, true).unwrap();
473    /// ```
474    pub fn write_bit(&self, area: MemoryArea, address: u16, bit: u8, value: bool) -> Result<()> {
475        let sid = self.next_sid();
476        let cmd = WriteBitCommand::new(
477            self.destination,
478            self.source,
479            sid,
480            area,
481            address,
482            bit,
483            value,
484        )?;
485
486        let response = self.send_receive_with_sid(&cmd.to_bytes()?, sid)?;
487        response.check_error()?;
488        Ok(())
489    }
490
491    /// Fills a memory area with a single value.
492    ///
493    /// # Arguments
494    ///
495    /// * `area` - Memory area to fill
496    /// * `address` - Starting word address
497    /// * `count` - Number of words to fill (1-999)
498    /// * `value` - Value to fill with
499    ///
500    /// # Errors
501    ///
502    /// Returns an error if:
503    /// - Count is 0 or > 999
504    /// - Communication fails
505    /// - PLC returns an error
506    ///
507    /// # Example
508    ///
509    /// ```no_run
510    /// use omron_fins::{Client, ClientConfig, MemoryArea};
511    /// use std::net::Ipv4Addr;
512    ///
513    /// let client = Client::new(ClientConfig::new(
514    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
515    /// )).unwrap();
516    ///
517    /// // Zero out DM100-DM149
518    /// client.fill(MemoryArea::DM, 100, 50, 0x0000).unwrap();
519    /// ```
520    pub fn fill(
521        &self,
522        area: MemoryArea,
523        mut address: u16,
524        mut count: u16,
525        value: u16,
526    ) -> Result<()> {
527        area.check_bounds(address, count)?;
528
529        while count > 0 {
530            let chunk_size = std::cmp::min(count, MAX_WORDS_PER_COMMAND);
531            let sid = self.next_sid();
532            let cmd = FillCommand::new(
533                self.destination,
534                self.source,
535                sid,
536                area,
537                address,
538                chunk_size,
539                value,
540            )?;
541
542            let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
543            response.check_error()?;
544
545            address += chunk_size;
546            count -= chunk_size;
547
548            if count > 0 {
549                std::thread::sleep(std::time::Duration::from_millis(1));
550            }
551        }
552
553        Ok(())
554    }
555
556    /// Puts the PLC into run mode.
557    ///
558    /// # Arguments
559    ///
560    /// * `mode` - PLC operating mode (Debug, Monitor, or Run)
561    ///
562    /// # Errors
563    ///
564    /// Returns an error if communication fails or PLC returns an error.
565    ///
566    /// # Example
567    ///
568    /// ```no_run
569    /// use omron_fins::{Client, ClientConfig, PlcMode};
570    /// use std::net::Ipv4Addr;
571    ///
572    /// let client = Client::new(ClientConfig::new(
573    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
574    /// )).unwrap();
575    ///
576    /// client.run(PlcMode::Monitor).unwrap();
577    /// ```
578    pub fn run(&self, mode: PlcMode) -> Result<()> {
579        let sid = self.next_sid();
580        let cmd = RunCommand::new(self.destination, self.source, sid, mode);
581
582        let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
583        response.check_error()?;
584        Ok(())
585    }
586
587    /// Stops the PLC.
588    ///
589    /// # Errors
590    ///
591    /// Returns an error if communication fails or PLC returns an error.
592    ///
593    /// # Example
594    ///
595    /// ```no_run
596    /// use omron_fins::{Client, ClientConfig};
597    /// use std::net::Ipv4Addr;
598    ///
599    /// let client = Client::new(ClientConfig::new(
600    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
601    /// )).unwrap();
602    ///
603    /// client.stop().unwrap();
604    /// ```
605    pub fn stop(&self) -> Result<()> {
606        let sid = self.next_sid();
607        let cmd = StopCommand::new(self.destination, self.source, sid);
608
609        let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
610        response.check_error()?;
611        Ok(())
612    }
613
614    /// Transfers data from one memory area to another within the PLC.
615    ///
616    /// # Arguments
617    ///
618    /// * `src_area` - Source memory area
619    /// * `src_address` - Source starting address
620    /// * `dst_area` - Destination memory area
621    /// * `dst_address` - Destination starting address
622    /// * `count` - Number of words to transfer (1-999)
623    ///
624    /// # Errors
625    ///
626    /// Returns an error if:
627    /// - Count is 0 or > 999
628    /// - Communication fails
629    /// - PLC returns an error
630    ///
631    /// # Example
632    ///
633    /// ```no_run
634    /// use omron_fins::{Client, ClientConfig, MemoryArea};
635    /// use std::net::Ipv4Addr;
636    ///
637    /// let client = Client::new(ClientConfig::new(
638    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
639    /// )).unwrap();
640    ///
641    /// // Copy DM100-DM109 to DM200-DM209
642    /// client.transfer(MemoryArea::DM, 100, MemoryArea::DM, 200, 10).unwrap();
643    /// ```
644    pub fn transfer(
645        &self,
646        src_area: MemoryArea,
647        mut src_address: u16,
648        dst_area: MemoryArea,
649        mut dst_address: u16,
650        mut count: u16,
651    ) -> Result<()> {
652        src_area.check_bounds(src_address, count)?;
653        dst_area.check_bounds(dst_address, count)?;
654
655        while count > 0 {
656            let chunk_size = std::cmp::min(count, MAX_WORDS_PER_COMMAND);
657            let sid = self.next_sid();
658            let cmd = TransferCommand::new(
659                self.destination,
660                self.source,
661                sid,
662                src_area,
663                src_address,
664                dst_area,
665                dst_address,
666                chunk_size,
667            )?;
668
669            let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
670            response.check_error()?;
671
672            src_address += chunk_size;
673            dst_address += chunk_size;
674            count -= chunk_size;
675
676            if count > 0 {
677                std::thread::sleep(std::time::Duration::from_millis(1));
678            }
679        }
680
681        Ok(())
682    }
683
684    /// Forces bits ON/OFF in the PLC, overriding normal program control.
685    ///
686    /// # Arguments
687    ///
688    /// * `specs` - List of bits to force with their specifications
689    ///
690    /// # Errors
691    ///
692    /// Returns an error if:
693    /// - Specs is empty
694    /// - Any area doesn't support bit access
695    /// - Any bit position > 15
696    /// - Communication fails
697    /// - PLC returns an error
698    ///
699    /// # Example
700    ///
701    /// ```no_run
702    /// use omron_fins::{Client, ClientConfig, ForcedBit, ForceSpec, MemoryArea};
703    /// use std::net::Ipv4Addr;
704    ///
705    /// let client = Client::new(ClientConfig::new(
706    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
707    /// )).unwrap();
708    ///
709    /// client.forced_set_reset(&[
710    ///     ForcedBit { area: MemoryArea::CIO, address: 0, bit: 0, spec: ForceSpec::ForceOn },
711    ///     ForcedBit { area: MemoryArea::CIO, address: 0, bit: 1, spec: ForceSpec::ForceOff },
712    /// ]).unwrap();
713    /// ```
714    pub fn forced_set_reset(&self, specs: &[ForcedBit]) -> Result<()> {
715        let sid = self.next_sid();
716        let cmd = ForcedSetResetCommand::new(self.destination, self.source, sid, specs.to_vec())?;
717
718        let response = self.send_receive_with_sid(&cmd.to_bytes()?, sid)?;
719        response.check_error()?;
720        Ok(())
721    }
722
723    /// Cancels all forced bits in the PLC.
724    ///
725    /// # Errors
726    ///
727    /// Returns an error if communication fails or PLC returns an error.
728    ///
729    /// # Example
730    ///
731    /// ```no_run
732    /// use omron_fins::{Client, ClientConfig};
733    /// use std::net::Ipv4Addr;
734    ///
735    /// let client = Client::new(ClientConfig::new(
736    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
737    /// )).unwrap();
738    ///
739    /// client.forced_set_reset_cancel().unwrap();
740    /// ```
741    pub fn forced_set_reset_cancel(&self) -> Result<()> {
742        let sid = self.next_sid();
743        let cmd = ForcedSetResetCancelCommand::new(self.destination, self.source, sid);
744
745        let response = self.send_receive_with_sid(&cmd.to_bytes(), sid)?;
746        response.check_error()?;
747        Ok(())
748    }
749
750    /// Reads from multiple memory areas in a single request.
751    ///
752    /// # Arguments
753    ///
754    /// * `specs` - List of read specifications
755    ///
756    /// # Returns
757    ///
758    /// A vector of u16 values in the same order as the specs.
759    /// For word reads, the full u16 value is returned.
760    /// For bit reads, 0x0000 (OFF) or 0x0001 (ON) is returned.
761    ///
762    /// # Errors
763    ///
764    /// Returns an error if:
765    /// - Specs is empty
766    /// - Any bit area doesn't support bit access
767    /// - Any bit position > 15
768    /// - Communication fails
769    /// - PLC returns an error
770    ///
771    /// # Example
772    ///
773    /// ```no_run
774    /// use omron_fins::{Client, ClientConfig, MultiReadSpec, MemoryArea};
775    /// use std::net::Ipv4Addr;
776    ///
777    /// let client = Client::new(ClientConfig::new(
778    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
779    /// )).unwrap();
780    ///
781    /// let values = client.read_multiple(&[
782    ///     MultiReadSpec { area: MemoryArea::DM, address: 100, bit: None },
783    ///     MultiReadSpec { area: MemoryArea::DM, address: 200, bit: None },
784    ///     MultiReadSpec { area: MemoryArea::CIO, address: 0, bit: Some(5) },
785    /// ]).unwrap();
786    /// // values[0] = DM100, values[1] = DM200, values[2] = CIO0.05 (0 or 1)
787    /// ```
788    pub fn read_multiple(&self, specs: &[MultiReadSpec]) -> Result<Vec<u16>> {
789        let sid = self.next_sid();
790        let cmd = MultipleReadCommand::new(self.destination, self.source, sid, specs.to_vec())?;
791
792        let response = self.send_receive_with_sid(&cmd.to_bytes()?, sid)?;
793        response.check_error()?;
794        response.to_words()
795    }
796
797    /// Reads an f32 (REAL) value from 2 consecutive words.
798    ///
799    /// # Arguments
800    ///
801    /// * `area` - Memory area to read from
802    /// * `address` - Starting word address
803    ///
804    /// # Errors
805    ///
806    /// Returns an error if communication fails or PLC returns an error.
807    ///
808    /// # Example
809    ///
810    /// ```no_run
811    /// use omron_fins::{Client, ClientConfig, MemoryArea};
812    /// use std::net::Ipv4Addr;
813    ///
814    /// let client = Client::new(ClientConfig::new(
815    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
816    /// )).unwrap();
817    ///
818    /// let temperature: f32 = client.read_f32(MemoryArea::DM, 100).unwrap();
819    /// ```
820    pub fn read_f32(&self, area: MemoryArea, address: u16) -> Result<f32> {
821        let words = self.read(area, address, 2)?;
822        // Omron uses word swap: low word first, high word second
823        let bytes = [
824            (words[1] >> 8) as u8,
825            (words[1] & 0xFF) as u8,
826            (words[0] >> 8) as u8,
827            (words[0] & 0xFF) as u8,
828        ];
829        Ok(f32::from_be_bytes(bytes))
830    }
831
832    /// Writes an f32 (REAL) value to 2 consecutive words.
833    ///
834    /// # Arguments
835    ///
836    /// * `area` - Memory area to write to
837    /// * `address` - Starting word address
838    /// * `value` - f32 value to write
839    ///
840    /// # Errors
841    ///
842    /// Returns an error if communication fails or PLC returns an error.
843    ///
844    /// # Example
845    ///
846    /// ```no_run
847    /// use omron_fins::{Client, ClientConfig, MemoryArea};
848    /// use std::net::Ipv4Addr;
849    ///
850    /// let client = Client::new(ClientConfig::new(
851    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
852    /// )).unwrap();
853    ///
854    /// client.write_f32(MemoryArea::DM, 100, 3.14159).unwrap();
855    /// ```
856    pub fn write_f32(&self, area: MemoryArea, address: u16, value: f32) -> Result<()> {
857        let bytes = value.to_be_bytes();
858        // Omron uses word swap: low word first, high word second
859        let words = [
860            u16::from_be_bytes([bytes[2], bytes[3]]),
861            u16::from_be_bytes([bytes[0], bytes[1]]),
862        ];
863        self.write(area, address, &words)
864    }
865
866    /// Reads an f64 (LREAL) value from 4 consecutive words.
867    ///
868    /// # Arguments
869    ///
870    /// * `area` - Memory area to read from
871    /// * `address` - Starting word address
872    ///
873    /// # Errors
874    ///
875    /// Returns an error if communication fails or PLC returns an error.
876    ///
877    /// # Example
878    ///
879    /// ```no_run
880    /// use omron_fins::{Client, ClientConfig, MemoryArea};
881    /// use std::net::Ipv4Addr;
882    ///
883    /// let client = Client::new(ClientConfig::new(
884    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
885    /// )).unwrap();
886    ///
887    /// let value: f64 = client.read_f64(MemoryArea::DM, 100).unwrap();
888    /// ```
889    pub fn read_f64(&self, area: MemoryArea, address: u16) -> Result<f64> {
890        let words = self.read(area, address, 4)?;
891        // Omron uses word swap: words in reverse order
892        let bytes = [
893            (words[3] >> 8) as u8,
894            (words[3] & 0xFF) as u8,
895            (words[2] >> 8) as u8,
896            (words[2] & 0xFF) as u8,
897            (words[1] >> 8) as u8,
898            (words[1] & 0xFF) as u8,
899            (words[0] >> 8) as u8,
900            (words[0] & 0xFF) as u8,
901        ];
902        Ok(f64::from_be_bytes(bytes))
903    }
904
905    /// Writes an f64 (LREAL) value to 4 consecutive words.
906    ///
907    /// # Arguments
908    ///
909    /// * `area` - Memory area to write to
910    /// * `address` - Starting word address
911    /// * `value` - f64 value to write
912    ///
913    /// # Errors
914    ///
915    /// Returns an error if communication fails or PLC returns an error.
916    ///
917    /// # Example
918    ///
919    /// ```no_run
920    /// use omron_fins::{Client, ClientConfig, MemoryArea};
921    /// use std::net::Ipv4Addr;
922    ///
923    /// let client = Client::new(ClientConfig::new(
924    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
925    /// )).unwrap();
926    ///
927    /// client.write_f64(MemoryArea::DM, 100, 3.141592653589793).unwrap();
928    /// ```
929    pub fn write_f64(&self, area: MemoryArea, address: u16, value: f64) -> Result<()> {
930        let bytes = value.to_be_bytes();
931        // Omron uses word swap: words in reverse order
932        let words = [
933            u16::from_be_bytes([bytes[6], bytes[7]]),
934            u16::from_be_bytes([bytes[4], bytes[5]]),
935            u16::from_be_bytes([bytes[2], bytes[3]]),
936            u16::from_be_bytes([bytes[0], bytes[1]]),
937        ];
938        self.write(area, address, &words)
939    }
940
941    /// Reads a custom structure from PLC memory based on a set of data types.
942    ///
943    /// # Arguments
944    ///
945    /// * `area` - Memory area to read from
946    /// * `address` - Starting word address
947    /// * `types` - List of data types to read in sequence
948    ///
949    /// # Example
950    ///
951    /// ```no_run
952    /// # use omron_fins::{Client, ClientConfig, MemoryArea, DataType, PlcValue};
953    /// # use std::net::Ipv4Addr;
954    /// # let client = Client::new(ClientConfig::new(Ipv4Addr::new(127, 0, 0, 1), 1, 10)).unwrap();
955    /// let my_struct = client.read_struct(MemoryArea::DM, 100, vec![
956    ///     DataType::LINT, // 8 bytes
957    ///     DataType::INT,  // 2 bytes
958    ///     DataType::REAL, // 4 bytes
959    /// ]).unwrap();
960    /// ```
961    pub fn read_struct(
962        &self,
963        area: MemoryArea,
964        address: u16,
965        types: Vec<DataType>,
966    ) -> Result<Vec<PlcValue>> {
967        let total_bytes: usize = types.iter().map(|t| (t.size() + 1) & !1).sum(); // Align to 2-byte words
968        let word_count = (total_bytes / 2) as u16;
969
970        let words = self.read(area, address, word_count)?;
971        let mut bytes = Vec::with_capacity(words.len() * 2);
972        for word in words {
973            bytes.extend_from_slice(&word.to_be_bytes());
974        }
975
976        let mut results = Vec::with_capacity(types.len());
977        let mut offset = 0;
978        for data_type in types {
979            let size = data_type.size();
980            let chunk = &bytes[offset..offset + size];
981            results.push(PlcValue::from_plc_bytes(data_type, chunk)?);
982            offset += (size + 1) & !1; // Advance by even bytes
983        }
984
985        Ok(results)
986    }
987
988    /// Writes a custom structure to PLC memory.
989    ///
990    /// # Arguments
991    ///
992    /// * `area` - Memory area to write to
993    /// * `address` - Starting word address
994    /// * `values` - List of values to write in sequence
995    ///
996    /// # Example
997    ///
998    /// ```no_run
999    /// # use omron_fins::{Client, ClientConfig, MemoryArea, PlcValue};
1000    /// # use std::net::Ipv4Addr;
1001    /// # let client = Client::new(ClientConfig::new(Ipv4Addr::new(127, 0, 0, 1), 1, 10)).unwrap();
1002    /// client.write_struct(MemoryArea::DM, 100, vec![
1003    ///     PlcValue::Lint(123456789),
1004    ///     PlcValue::Int(100),
1005    ///     PlcValue::Real(3.14159),
1006    /// ]).unwrap();
1007    /// ```
1008    pub fn write_struct(&self, area: MemoryArea, address: u16, values: Vec<PlcValue>) -> Result<()> {
1009        let mut bytes = Vec::new();
1010        for value in values {
1011            let val_bytes = value.to_plc_bytes();
1012            bytes.extend_from_slice(&val_bytes);
1013            // Ensure 16-bit alignment (even bytes)
1014            if val_bytes.len() % 2 != 0 {
1015                bytes.push(0);
1016            }
1017        }
1018
1019        let words: Vec<u16> = bytes
1020            .chunks_exact(2)
1021            .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
1022            .collect();
1023
1024        self.write(area, address, &words)
1025    }
1026
1027    /// Reads an i32 (DINT) value from 2 consecutive words.
1028    ///
1029    /// # Arguments
1030    ///
1031    /// * `area` - Memory area to read from
1032    /// * `address` - Starting word address
1033    ///
1034    /// # Errors
1035    ///
1036    /// Returns an error if communication fails or PLC returns an error.
1037    ///
1038    /// # Example
1039    ///
1040    /// ```no_run
1041    /// use omron_fins::{Client, ClientConfig, MemoryArea};
1042    /// use std::net::Ipv4Addr;
1043    ///
1044    /// let client = Client::new(ClientConfig::new(
1045    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
1046    /// )).unwrap();
1047    ///
1048    /// let counter: i32 = client.read_i32(MemoryArea::DM, 100).unwrap();
1049    /// ```
1050    pub fn read_i32(&self, area: MemoryArea, address: u16) -> Result<i32> {
1051        let words = self.read(area, address, 2)?;
1052        let bytes = [
1053            (words[0] >> 8) as u8,
1054            (words[0] & 0xFF) as u8,
1055            (words[1] >> 8) as u8,
1056            (words[1] & 0xFF) as u8,
1057        ];
1058        Ok(i32::from_be_bytes(bytes))
1059    }
1060
1061    /// Writes an i32 (DINT) value to 2 consecutive words.
1062    ///
1063    /// # Arguments
1064    ///
1065    /// * `area` - Memory area to write to
1066    /// * `address` - Starting word address
1067    /// * `value` - i32 value to write
1068    ///
1069    /// # Errors
1070    ///
1071    /// Returns an error if communication fails or PLC returns an error.
1072    ///
1073    /// # Example
1074    ///
1075    /// ```no_run
1076    /// use omron_fins::{Client, ClientConfig, MemoryArea};
1077    /// use std::net::Ipv4Addr;
1078    ///
1079    /// let client = Client::new(ClientConfig::new(
1080    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
1081    /// )).unwrap();
1082    ///
1083    /// client.write_i32(MemoryArea::DM, 100, -123456).unwrap();
1084    /// ```
1085    pub fn write_i32(&self, area: MemoryArea, address: u16, value: i32) -> Result<()> {
1086        let bytes = value.to_be_bytes();
1087        let words = [
1088            u16::from_be_bytes([bytes[0], bytes[1]]),
1089            u16::from_be_bytes([bytes[2], bytes[3]]),
1090        ];
1091        self.write(area, address, &words)
1092    }
1093
1094    /// Writes an ASCII string to consecutive words.
1095    ///
1096    /// Each word stores 2 ASCII characters (big-endian). If the string has an
1097    /// odd number of characters, the last byte is padded with 0x00.
1098    ///
1099    /// # Arguments
1100    ///
1101    /// * `area` - Memory area to write to
1102    /// * `address` - Starting word address
1103    /// * `value` - String to write (ASCII only)
1104    ///
1105    /// # Errors
1106    ///
1107    /// Returns an error if:
1108    /// - String is empty
1109    /// - String exceeds 1998 characters (999 words)
1110    /// - Communication fails
1111    /// - PLC returns an error
1112    ///
1113    /// # Example
1114    ///
1115    /// ```no_run
1116    /// use omron_fins::{Client, ClientConfig, MemoryArea};
1117    /// use std::net::Ipv4Addr;
1118    ///
1119    /// let client = Client::new(ClientConfig::new(
1120    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
1121    /// )).unwrap();
1122    ///
1123    /// // Write a product code to DM100
1124    /// client.write_string(MemoryArea::DM, 100, "PRODUCT-001").unwrap();
1125    /// ```
1126    pub fn write_string(&self, area: MemoryArea, address: u16, value: &str) -> Result<()> {
1127        use crate::command::MAX_WORDS_PER_COMMAND;
1128        use crate::error::FinsError;
1129
1130        if value.is_empty() {
1131            return Err(FinsError::InvalidParameter {
1132                parameter: "value".to_string(),
1133                reason: "string cannot be empty".to_string(),
1134            });
1135        }
1136
1137        let bytes = value.as_bytes();
1138        let word_count = (bytes.len() + 1) / 2;
1139
1140        if word_count > MAX_WORDS_PER_COMMAND as usize {
1141            return Err(FinsError::InvalidParameter {
1142                parameter: "value".to_string(),
1143                reason: format!(
1144                    "string too long: {} bytes requires {} words, max is {}",
1145                    bytes.len(),
1146                    word_count,
1147                    MAX_WORDS_PER_COMMAND
1148                ),
1149            });
1150        }
1151
1152        // Omron uses byte swap within words: first char in low byte, second char in high byte
1153        let words: Vec<u16> = bytes
1154            .chunks(2)
1155            .map(|chunk| {
1156                let low = chunk[0] as u16;
1157                let high = if chunk.len() > 1 { chunk[1] as u16 } else { 0 };
1158                (high << 8) | low
1159            })
1160            .collect();
1161
1162        self.write(area, address, &words)
1163    }
1164
1165    /// Reads an ASCII string from consecutive words.
1166    ///
1167    /// Each word contains 2 ASCII characters (big-endian). Null bytes (0x00)
1168    /// at the end of the string are trimmed.
1169    ///
1170    /// # Arguments
1171    ///
1172    /// * `area` - Memory area to read from
1173    /// * `address` - Starting word address
1174    /// * `word_count` - Number of words to read (1-999)
1175    ///
1176    /// # Errors
1177    ///
1178    /// Returns an error if:
1179    /// - Word count is 0 or > 999
1180    /// - Communication fails
1181    /// - PLC returns an error
1182    ///
1183    /// # Example
1184    ///
1185    /// ```no_run
1186    /// use omron_fins::{Client, ClientConfig, MemoryArea};
1187    /// use std::net::Ipv4Addr;
1188    ///
1189    /// let client = Client::new(ClientConfig::new(
1190    ///     Ipv4Addr::new(192, 168, 1, 250), 1, 0
1191    /// )).unwrap();
1192    ///
1193    /// // Read a product code from DM100 (up to 20 characters = 10 words)
1194    /// let code = client.read_string(MemoryArea::DM, 100, 10).unwrap();
1195    /// println!("Product code: {}", code);
1196    /// ```
1197    pub fn read_string(&self, area: MemoryArea, address: u16, word_count: u16) -> Result<String> {
1198        let words = self.read(area, address, word_count)?;
1199
1200        // Omron uses byte swap within words: first char in low byte, second char in high byte
1201        let mut bytes: Vec<u8> = Vec::with_capacity(words.len() * 2);
1202        for word in &words {
1203            bytes.push((word & 0xFF) as u8); // low byte first
1204            bytes.push((word >> 8) as u8); // high byte second
1205        }
1206
1207        // Trim null bytes from the end
1208        while bytes.last() == Some(&0) {
1209            bytes.pop();
1210        }
1211
1212        Ok(String::from_utf8_lossy(&bytes).to_string())
1213    }
1214
1215    /// Returns the source node address.
1216    pub fn source(&self) -> NodeAddress {
1217        self.source
1218    }
1219
1220    /// Returns the destination node address.
1221    pub fn destination(&self) -> NodeAddress {
1222        self.destination
1223    }
1224}
1225
1226impl std::fmt::Debug for Client {
1227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1228        f.debug_struct("Client")
1229            .field("transport", &self.transport)
1230            .field("source", &self.source)
1231            .field("destination", &self.destination)
1232            .finish()
1233    }
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238    use super::*;
1239    use std::net::Ipv4Addr;
1240
1241    #[test]
1242    fn test_client_config_new() {
1243        let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0);
1244
1245        assert_eq!(config.plc_addr.ip(), Ipv4Addr::new(192, 168, 1, 250));
1246        assert_eq!(config.plc_addr.port(), DEFAULT_FINS_PORT);
1247        assert_eq!(config.source.node, 1);
1248        assert_eq!(config.destination.node, 0);
1249        assert_eq!(config.timeout, DEFAULT_TIMEOUT);
1250    }
1251
1252    #[test]
1253    fn test_client_config_with_port() {
1254        let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0).with_port(9601);
1255
1256        assert_eq!(config.plc_addr.port(), 9601);
1257    }
1258
1259    #[test]
1260    fn test_client_config_with_timeout() {
1261        let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0)
1262            .with_timeout(Duration::from_secs(5));
1263
1264        assert_eq!(config.timeout, Duration::from_secs(5));
1265    }
1266
1267    #[test]
1268    fn test_client_config_with_network() {
1269        let config = ClientConfig::new(Ipv4Addr::new(192, 168, 1, 250), 1, 0)
1270            .with_source_network(1)
1271            .with_dest_network(2);
1272
1273        assert_eq!(config.source.network, 1);
1274        assert_eq!(config.destination.network, 2);
1275    }
1276
1277    #[test]
1278    fn test_client_creation() {
1279        // Note: This creates a socket but doesn't actually connect to a PLC
1280        let config = ClientConfig::new(Ipv4Addr::new(127, 0, 0, 1), 1, 10);
1281        let client = Client::new(config);
1282        assert!(client.is_ok());
1283    }
1284
1285    #[test]
1286    fn test_client_sid_increment() {
1287        let config = ClientConfig::new(Ipv4Addr::new(127, 0, 0, 1), 1, 10);
1288        let client = Client::new(config).unwrap();
1289
1290        assert_eq!(client.next_sid(), 0);
1291        assert_eq!(client.next_sid(), 1);
1292        assert_eq!(client.next_sid(), 2);
1293    }
1294
1295    #[test]
1296    fn test_client_debug() {
1297        let config = ClientConfig::new(Ipv4Addr::new(127, 0, 0, 1), 1, 10);
1298        let client = Client::new(config).unwrap();
1299        let debug_str = format!("{:?}", client);
1300        assert!(debug_str.contains("Client"));
1301    }
1302
1303    #[test]
1304    fn test_string_to_words_even_length() {
1305        // "Hi" = [0x48, 0x69] -> [0x6948] (byte swap: first char in low byte)
1306        let s = "Hi";
1307        let bytes = s.as_bytes();
1308        let words: Vec<u16> = bytes
1309            .chunks(2)
1310            .map(|chunk| {
1311                let low = chunk[0] as u16;
1312                let high = if chunk.len() > 1 { chunk[1] as u16 } else { 0 };
1313                (high << 8) | low
1314            })
1315            .collect();
1316        assert_eq!(words, vec![0x6948]);
1317    }
1318
1319    #[test]
1320    fn test_string_to_words_odd_length() {
1321        // "Hello" = [0x48, 0x65, 0x6C, 0x6C, 0x6F] -> [0x6548, 0x6C6C, 0x006F] (byte swap)
1322        let s = "Hello";
1323        let bytes = s.as_bytes();
1324        let words: Vec<u16> = bytes
1325            .chunks(2)
1326            .map(|chunk| {
1327                let low = chunk[0] as u16;
1328                let high = if chunk.len() > 1 { chunk[1] as u16 } else { 0 };
1329                (high << 8) | low
1330            })
1331            .collect();
1332        assert_eq!(words, vec![0x6548, 0x6C6C, 0x006F]);
1333    }
1334
1335    #[test]
1336    fn test_words_to_string() {
1337        // [0x6548, 0x6C6C, 0x006F] -> "Hello" (byte swap: low byte is first char)
1338        let words = vec![0x6548u16, 0x6C6C, 0x006F];
1339        let mut bytes: Vec<u8> = Vec::with_capacity(words.len() * 2);
1340        for word in &words {
1341            bytes.push((word & 0xFF) as u8); // low byte first
1342            bytes.push((word >> 8) as u8); // high byte second
1343        }
1344        while bytes.last() == Some(&0) {
1345            bytes.pop();
1346        }
1347        let result = String::from_utf8_lossy(&bytes).to_string();
1348        assert_eq!(result, "Hello");
1349    }
1350
1351    #[test]
1352    fn test_words_to_string_no_null() {
1353        // [0x6948] -> "Hi" (byte swap: low byte is first char)
1354        let words = vec![0x6948u16];
1355        let mut bytes: Vec<u8> = Vec::with_capacity(words.len() * 2);
1356        for word in &words {
1357            bytes.push((word & 0xFF) as u8); // low byte first
1358            bytes.push((word >> 8) as u8); // high byte second
1359        }
1360        while bytes.last() == Some(&0) {
1361            bytes.pop();
1362        }
1363        let result = String::from_utf8_lossy(&bytes).to_string();
1364        assert_eq!(result, "Hi");
1365    }
1366
1367    #[test]
1368    fn test_string_roundtrip() {
1369        // Test that string -> words -> string preserves the original (with byte swap)
1370        let original = "PRODUCT-001";
1371        let bytes = original.as_bytes();
1372        let words: Vec<u16> = bytes
1373            .chunks(2)
1374            .map(|chunk| {
1375                let low = chunk[0] as u16;
1376                let high = if chunk.len() > 1 { chunk[1] as u16 } else { 0 };
1377                (high << 8) | low
1378            })
1379            .collect();
1380
1381        let mut result_bytes: Vec<u8> = Vec::with_capacity(words.len() * 2);
1382        for word in &words {
1383            result_bytes.push((word & 0xFF) as u8); // low byte first
1384            result_bytes.push((word >> 8) as u8); // high byte second
1385        }
1386        while result_bytes.last() == Some(&0) {
1387            result_bytes.pop();
1388        }
1389        let result = String::from_utf8_lossy(&result_bytes).to_string();
1390        assert_eq!(result, original);
1391    }
1392
1393    #[test]
1394    fn test_float32_to_bytes() {
1395        let value: f32 = 3.14159;
1396        let bytes = value.to_be_bytes();
1397        assert_eq!(bytes, [0x40, 0x49, 0x0F, 0xD0]);
1398    }
1399}