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