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}