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