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}