Skip to main content

omron_fins/
header.rs

1//! FINS header structures and node addressing.
2//!
3//! This module defines the FINS protocol header structure and node addressing
4//! used for routing FINS frames between nodes on a FINS network.
5//!
6//! # FINS Header Structure
7//!
8//! The FINS header is a 10-byte structure that precedes every FINS command and response:
9//!
10//! | Byte | Field | Description |
11//! |------|-------|-------------|
12//! | 0 | ICF | Information Control Field |
13//! | 1 | RSV | Reserved (always 0x00) |
14//! | 2 | GCT | Gateway Count |
15//! | 3 | DNA | Destination Network Address |
16//! | 4 | DA1 | Destination Node Address |
17//! | 5 | DA2 | Destination Unit Address |
18//! | 6 | SNA | Source Network Address |
19//! | 7 | SA1 | Source Node Address |
20//! | 8 | SA2 | Source Unit Address |
21//! | 9 | SID | Service ID |
22//!
23//! # Node Addressing
24//!
25//! Each node in a FINS network is identified by three components:
26//!
27//! - **Network** (0-127): Network number (0 = local network)
28//! - **Node** (0-255): Node number within the network
29//! - **Unit** (0-255): Unit number within the node (0 = CPU unit)
30//!
31//! # Example
32//!
33//! ```
34//! use omron_fins::{FinsHeader, NodeAddress};
35//!
36//! // Create node addresses
37//! let source = NodeAddress::new(0, 1, 0);      // Local network, node 1, CPU
38//! let destination = NodeAddress::new(0, 10, 0); // Local network, node 10, CPU
39//!
40//! // Create a command header
41//! let header = FinsHeader::new_command(destination, source, 0x01);
42//! let bytes = header.to_bytes();
43//! assert_eq!(bytes.len(), 10);
44//! ```
45
46use crate::error::{FinsError, Result};
47
48/// FINS header size in bytes.
49pub const FINS_HEADER_SIZE: usize = 10;
50
51/// Node address for FINS communication.
52///
53/// Represents a network/node/unit address in the FINS protocol.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct NodeAddress {
56    /// Network address (0 = local network).
57    pub network: u8,
58    /// Node address (0 = local node for destination, or source node number).
59    pub node: u8,
60    /// Unit address (0 = CPU unit).
61    pub unit: u8,
62}
63
64impl NodeAddress {
65    /// Creates a new node address.
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// use omron_fins::NodeAddress;
71    ///
72    /// // Local CPU unit
73    /// let local = NodeAddress::new(0, 0, 0);
74    ///
75    /// // Remote PLC on network 1, node 10, CPU unit
76    /// let remote = NodeAddress::new(1, 10, 0);
77    /// ```
78    pub fn new(network: u8, node: u8, unit: u8) -> Self {
79        Self {
80            network,
81            node,
82            unit,
83        }
84    }
85
86    /// Creates a local node address (network 0, node 0, unit 0).
87    ///
88    /// # Example
89    ///
90    /// ```
91    /// use omron_fins::NodeAddress;
92    ///
93    /// let local = NodeAddress::local();
94    /// assert_eq!(local.network, 0);
95    /// assert_eq!(local.node, 0);
96    /// assert_eq!(local.unit, 0);
97    /// ```
98    pub fn local() -> Self {
99        Self::new(0, 0, 0)
100    }
101}
102
103impl Default for NodeAddress {
104    fn default() -> Self {
105        Self::local()
106    }
107}
108
109/// FINS command/response header (10 bytes).
110///
111/// The header contains addressing and control information for FINS frames.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct FinsHeader {
114    /// Information Control Field.
115    /// - Bit 7: 1 = response required (command), 0 = response not required
116    /// - Bit 6: 0 = command, 1 = response
117    /// - For commands: typically 0x80
118    /// - For responses: typically 0xC0
119    pub icf: u8,
120    /// Reserved byte (always 0x00).
121    pub rsv: u8,
122    /// Gateway Count (number of bridges to pass through, typically 0x02).
123    pub gct: u8,
124    /// Destination Network Address.
125    pub dna: u8,
126    /// Destination Node Address.
127    pub da1: u8,
128    /// Destination Unit Address.
129    pub da2: u8,
130    /// Source Network Address.
131    pub sna: u8,
132    /// Source Node Address.
133    pub sa1: u8,
134    /// Source Unit Address.
135    pub sa2: u8,
136    /// Service ID (used to match responses with requests).
137    pub sid: u8,
138}
139
140impl FinsHeader {
141    /// Creates a new command header.
142    ///
143    /// # Arguments
144    ///
145    /// * `destination` - Destination node address
146    /// * `source` - Source node address
147    /// * `sid` - Service ID for request/response matching
148    ///
149    /// # Example
150    ///
151    /// ```
152    /// use omron_fins::{FinsHeader, NodeAddress};
153    ///
154    /// let dest = NodeAddress::new(0, 10, 0);
155    /// let src = NodeAddress::new(0, 1, 0);
156    /// let header = FinsHeader::new_command(dest, src, 0x01);
157    /// ```
158    pub fn new_command(destination: NodeAddress, source: NodeAddress, sid: u8) -> Self {
159        Self {
160            icf: 0x80, // Command, response required
161            rsv: 0x00,
162            gct: 0x07, // Gateway count (max hops allowed)
163            dna: destination.network,
164            da1: destination.node,
165            da2: destination.unit,
166            sna: source.network,
167            sa1: source.node,
168            sa2: source.unit,
169            sid,
170        }
171    }
172
173    /// Serializes the header to bytes.
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// use omron_fins::{FinsHeader, NodeAddress};
179    ///
180    /// let header = FinsHeader::new_command(
181    ///     NodeAddress::new(0, 10, 0),
182    ///     NodeAddress::new(0, 1, 0),
183    ///     0x01
184    /// );
185    /// let bytes = header.to_bytes();
186    /// assert_eq!(bytes.len(), 10);
187    /// ```
188    pub fn to_bytes(self) -> [u8; FINS_HEADER_SIZE] {
189        [
190            self.icf, self.rsv, self.gct, self.dna, self.da1, self.da2, self.sna, self.sa1,
191            self.sa2, self.sid,
192        ]
193    }
194
195    /// Parses a header from bytes.
196    ///
197    /// # Errors
198    ///
199    /// Returns `FinsError::InvalidResponse` if the slice is too short.
200    ///
201    /// # Example
202    ///
203    /// ```
204    /// use omron_fins::FinsHeader;
205    ///
206    /// let bytes = [0xC0, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0A, 0x00, 0x01];
207    /// let header = FinsHeader::from_bytes(&bytes).unwrap();
208    /// assert_eq!(header.icf, 0xC0);
209    /// ```
210    pub fn from_bytes(data: &[u8]) -> Result<Self> {
211        if data.len() < FINS_HEADER_SIZE {
212            return Err(FinsError::invalid_response(format!(
213                "header too short: expected {} bytes, got {}",
214                FINS_HEADER_SIZE,
215                data.len()
216            )));
217        }
218
219        Ok(Self {
220            icf: data[0],
221            rsv: data[1],
222            gct: data[2],
223            dna: data[3],
224            da1: data[4],
225            da2: data[5],
226            sna: data[6],
227            sa1: data[7],
228            sa2: data[8],
229            sid: data[9],
230        })
231    }
232
233    /// Returns whether this is a response header.
234    pub fn is_response(self) -> bool {
235        (self.icf & 0x40) != 0
236    }
237
238    /// Returns the destination node address.
239    pub fn destination(self) -> NodeAddress {
240        NodeAddress::new(self.dna, self.da1, self.da2)
241    }
242
243    /// Returns the source node address.
244    pub fn source(self) -> NodeAddress {
245        NodeAddress::new(self.sna, self.sa1, self.sa2)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_node_address_new() {
255        let addr = NodeAddress::new(1, 10, 0);
256        assert_eq!(addr.network, 1);
257        assert_eq!(addr.node, 10);
258        assert_eq!(addr.unit, 0);
259    }
260
261    #[test]
262    fn test_node_address_local() {
263        let addr = NodeAddress::local();
264        assert_eq!(addr.network, 0);
265        assert_eq!(addr.node, 0);
266        assert_eq!(addr.unit, 0);
267    }
268
269    #[test]
270    fn test_header_new_command() {
271        let dest = NodeAddress::new(0, 10, 0);
272        let src = NodeAddress::new(0, 1, 0);
273        let header = FinsHeader::new_command(dest, src, 0x42);
274
275        assert_eq!(header.icf, 0x80);
276        assert_eq!(header.rsv, 0x00);
277        assert_eq!(header.gct, 0x07);
278        assert_eq!(header.dna, 0);
279        assert_eq!(header.da1, 10);
280        assert_eq!(header.da2, 0);
281        assert_eq!(header.sna, 0);
282        assert_eq!(header.sa1, 1);
283        assert_eq!(header.sa2, 0);
284        assert_eq!(header.sid, 0x42);
285    }
286
287    #[test]
288    fn test_header_to_bytes() {
289        let dest = NodeAddress::new(0, 10, 0);
290        let src = NodeAddress::new(0, 1, 0);
291        let header = FinsHeader::new_command(dest, src, 0x01);
292        let bytes = header.to_bytes();
293
294        assert_eq!(
295            bytes,
296            [0x80, 0x00, 0x07, 0x00, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x01]
297        );
298    }
299
300    #[test]
301    fn test_header_from_bytes() {
302        let bytes = [0xC0, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0A, 0x00, 0x01];
303        let header = FinsHeader::from_bytes(&bytes).unwrap();
304
305        assert_eq!(header.icf, 0xC0);
306        assert_eq!(header.rsv, 0x00);
307        assert_eq!(header.gct, 0x02);
308        assert_eq!(header.dna, 0);
309        assert_eq!(header.da1, 1);
310        assert_eq!(header.da2, 0);
311        assert_eq!(header.sna, 0);
312        assert_eq!(header.sa1, 10);
313        assert_eq!(header.sa2, 0);
314        assert_eq!(header.sid, 0x01);
315    }
316
317    #[test]
318    fn test_header_from_bytes_too_short() {
319        let bytes = [0xC0, 0x00, 0x02];
320        let result = FinsHeader::from_bytes(&bytes);
321        assert!(result.is_err());
322    }
323
324    #[test]
325    fn test_header_is_response() {
326        let command_header = FinsHeader {
327            icf: 0x80,
328            rsv: 0,
329            gct: 2,
330            dna: 0,
331            da1: 10,
332            da2: 0,
333            sna: 0,
334            sa1: 1,
335            sa2: 0,
336            sid: 1,
337        };
338        assert!(!command_header.is_response());
339
340        let response_header = FinsHeader {
341            icf: 0xC0,
342            ..command_header
343        };
344        assert!(response_header.is_response());
345    }
346
347    #[test]
348    fn test_header_roundtrip() {
349        let original =
350            FinsHeader::new_command(NodeAddress::new(1, 20, 0), NodeAddress::new(2, 30, 0), 0xFF);
351        let bytes = original.to_bytes();
352        let parsed = FinsHeader::from_bytes(&bytes).unwrap();
353        assert_eq!(original, parsed);
354    }
355}