Skip to main content

rust_ethernet_ip/
route.rs

1/// Ordered route hop for PLC communication.
2#[derive(Debug, Clone, PartialEq, Eq)]
3#[non_exhaustive]
4pub enum RouteHop {
5    /// Backplane/chassis hop. Rockwell ControlLogix backplanes normally use port 1.
6    Backplane {
7        /// CIP port number used to enter the backplane.
8        port: u8,
9        /// Destination chassis slot.
10        slot: u8,
11    },
12    /// Ethernet hop using an IPv4 link address. Rockwell Ethernet ports commonly use port 2.
13    Ethernet {
14        /// CIP network port number.
15        port: u8,
16        /// Link address, normally an IP address.
17        address: String,
18    },
19}
20
21/// Route path for PLC communication.
22#[derive(Debug, Clone)]
23pub struct RoutePath {
24    hops: Vec<RouteHop>,
25    /// Port staged by `add_port` to apply to the next `add_address` when no
26    /// Ethernet hop exists yet (supports the `add_port(p).add_address(a)` order).
27    pending_port: Option<u8>,
28}
29
30impl RoutePath {
31    const DEFAULT_BACKPLANE_PORT: u8 = 1;
32    const DEFAULT_ETHERNET_PORT: u8 = 2;
33
34    /// Creates a new route path
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            hops: Vec::new(),
39            pending_port: None,
40        }
41    }
42
43    /// Adds a backplane slot to the route
44    #[must_use]
45    pub fn add_slot(mut self, slot: u8) -> Self {
46        self.hops.push(RouteHop::Backplane {
47            port: Self::DEFAULT_BACKPLANE_PORT,
48            slot,
49        });
50        self
51    }
52
53    /// Sets the network port for the most recently added Ethernet hop.
54    ///
55    /// If no Ethernet hop exists yet, the port is staged and applied to the next
56    /// `add_address` call, so both `add_address(a).add_port(p)` and
57    /// `add_port(p).add_address(a)` produce the intended port. Previously the
58    /// latter ordering silently dropped the port.
59    #[must_use]
60    pub fn add_port(mut self, port: u8) -> Self {
61        if let Some(RouteHop::Ethernet { port: hop_port, .. }) = self
62            .hops
63            .iter_mut()
64            .rev()
65            .find(|hop| matches!(hop, RouteHop::Ethernet { .. }))
66        {
67            *hop_port = port;
68        } else {
69            self.pending_port = Some(port);
70        }
71        self
72    }
73
74    /// Adds a network address to the route
75    #[must_use]
76    pub fn add_address(mut self, address: String) -> Self {
77        let port = self
78            .pending_port
79            .take()
80            .or_else(|| self.pending_ethernet_port())
81            .unwrap_or(Self::DEFAULT_ETHERNET_PORT);
82        self.hops.push(RouteHop::Ethernet { port, address });
83        self
84    }
85
86    /// Adds a backplane hop with an explicit port number.
87    #[must_use]
88    pub fn add_backplane(mut self, port: u8, slot: u8) -> Self {
89        self.hops.push(RouteHop::Backplane { port, slot });
90        self
91    }
92
93    /// Adds an Ethernet hop using the common Rockwell Ethernet port number, 2.
94    #[must_use]
95    pub fn add_ethernet(self, address: impl Into<String>) -> Self {
96        self.add_ethernet_with_port(Self::DEFAULT_ETHERNET_PORT, address)
97    }
98
99    /// Adds an Ethernet hop with an explicit port number.
100    #[must_use]
101    pub fn add_ethernet_with_port(mut self, port: u8, address: impl Into<String>) -> Self {
102        let address = address.into();
103        self.hops.push(RouteHop::Ethernet { port, address });
104        self
105    }
106
107    /// Returns the ordered hops for this route.
108    #[must_use]
109    pub fn hops(&self) -> &[RouteHop] {
110        &self.hops
111    }
112
113    /// Returns legacy grouped backplane slots derived from the ordered hops.
114    #[must_use]
115    pub fn slots(&self) -> Vec<u8> {
116        self.hops
117            .iter()
118            .filter_map(|hop| match hop {
119                RouteHop::Backplane { slot, .. } => Some(*slot),
120                RouteHop::Ethernet { .. } => None,
121            })
122            .collect()
123    }
124
125    /// Returns legacy grouped Ethernet ports derived from the ordered hops.
126    #[must_use]
127    pub fn ports(&self) -> Vec<u8> {
128        self.hops
129            .iter()
130            .filter_map(|hop| match hop {
131                RouteHop::Backplane { .. } => None,
132                RouteHop::Ethernet { port, .. } => Some(*port),
133            })
134            .collect()
135    }
136
137    /// Returns legacy grouped Ethernet addresses derived from the ordered hops.
138    #[must_use]
139    pub fn addresses(&self) -> Vec<String> {
140        self.hops
141            .iter()
142            .filter_map(|hop| match hop {
143                RouteHop::Backplane { .. } => None,
144                RouteHop::Ethernet { address, .. } => Some(address.clone()),
145            })
146            .collect()
147    }
148
149    /// Builds CIP route path bytes
150    ///
151    /// Reference: EtherNetIP_Connection_Paths_and_Routing.md, Port Segment Encoding
152    /// According to the examples: Port 1 (backplane), Slot X = [0x01, X]
153    /// The 0x01 byte encodes both "Port Segment (8-bit link)" AND "Port 1 (backplane)"
154    /// Examples from documentation:
155    ///   - Slot 0: `01 00`
156    ///   - Slot 1: `01 01`
157    ///   - Slot 2: `01 02`
158    #[must_use]
159    pub fn to_cip_bytes(&self) -> Vec<u8> {
160        let mut path = Vec::new();
161
162        for hop in &self.hops {
163            Self::append_hop(&mut path, hop);
164        }
165
166        path
167    }
168
169    fn append_hop(path: &mut Vec<u8>, hop: &RouteHop) {
170        match hop {
171            RouteHop::Backplane { port, slot } => {
172                path.push(*port);
173                path.push(*slot);
174            }
175            RouteHop::Ethernet { port, address } => {
176                Self::append_extended_link_address_segment(path, *port, address);
177            }
178        }
179    }
180
181    fn append_extended_link_address_segment(path: &mut Vec<u8>, port: u8, address: &str) {
182        path.push(0x10 | (port & 0x0F));
183        path.push(address.len().saturating_add(1) as u8);
184        path.extend_from_slice(address.as_bytes());
185        path.push(0x00);
186        if !(address.len() + 1).is_multiple_of(2) {
187            path.push(0x00);
188        }
189    }
190
191    fn pending_ethernet_port(&self) -> Option<u8> {
192        self.hops
193            .iter()
194            .filter_map(|hop| match hop {
195                RouteHop::Ethernet { port, .. } => Some(*port),
196                RouteHop::Backplane { .. } => None,
197            })
198            .next_back()
199    }
200}
201
202impl Default for RoutePath {
203    fn default() -> Self {
204        Self::new()
205    }
206}