prns_config/plan/
rnode.rs1use super::error::PlanErrorKind;
2use crate::reference::keys::interface as interface_key;
3use crate::reference::keys::rnode as rnode_key;
4
5pub const RNODE_TCP_PORT: u16 = 7_633;
6
7const RNODE_BLE_ADDRESS_LEN: usize = 17;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct RNodeSerialDevice(String);
11
12impl RNodeSerialDevice {
13 #[must_use]
14 pub fn as_str(&self) -> &str {
15 &self.0
16 }
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct RNodeTcpHost(String);
21
22impl RNodeTcpHost {
23 #[must_use]
24 pub fn as_str(&self) -> &str {
25 &self.0
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RNodeBleAddress([u8; 6]);
31
32impl RNodeBleAddress {
33 fn parse(value: &str) -> Result<Self, RNodeBleTargetError> {
34 let mut octets = [0; 6];
35 let mut parts = value.split(':');
36 for octet in &mut octets {
37 let Some(part) = parts.next() else {
38 return Err(RNodeBleTargetError::InvalidAddress);
39 };
40 *octet =
41 u8::from_str_radix(part, 16).map_err(|_| RNodeBleTargetError::InvalidAddress)?;
42 }
43 if parts.next().is_some() {
44 return Err(RNodeBleTargetError::InvalidAddress);
45 }
46 Ok(Self(octets))
47 }
48
49 #[must_use]
50 pub const fn octets(&self) -> [u8; 6] {
51 self.0
52 }
53}
54
55impl core::fmt::Display for RNodeBleAddress {
56 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57 let [a, b, c, d, e, f] = self.0;
58 write!(formatter, "{a:02X}:{b:02X}:{c:02X}:{d:02X}:{e:02X}:{f:02X}")
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct RNodeBleName(String);
64
65impl RNodeBleName {
66 #[must_use]
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum RNodeBleTarget {
74 FirstBondedRnode,
75 Address(RNodeBleAddress),
76 Name(RNodeBleName),
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub(crate) enum RNodeBleTargetError {
81 InvalidAddress,
82}
83
84impl RNodeBleTarget {
85 pub(crate) fn from_uri_suffix(value: String) -> Result<Self, RNodeBleTargetError> {
86 if value.is_empty() {
87 return Ok(Self::FirstBondedRnode);
88 }
89 if looks_like_ble_address(&value) {
90 return RNodeBleAddress::parse(&value).map(Self::Address);
91 }
92 Ok(Self::Name(RNodeBleName(value)))
93 }
94}
95
96impl core::fmt::Display for RNodeBleTarget {
97 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98 match self {
99 Self::FirstBondedRnode => formatter.write_str("the first bonded `RNode ` device"),
100 Self::Address(address) => write!(formatter, "Bluetooth LE address {address}"),
101 Self::Name(name) => write!(formatter, "BLE device named {:?}", name.as_str()),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum RNodeTcpTarget {
108 Loopback,
109 Host(RNodeTcpHost),
110}
111
112impl RNodeTcpTarget {
113 #[must_use]
114 pub fn socket_target(&self) -> String {
115 let host = match self {
116 Self::Loopback => "localhost",
117 Self::Host(host) => host.as_str(),
118 };
119 if host.contains(':') && !host.starts_with('[') {
120 format!("[{host}]:{RNODE_TCP_PORT}")
121 } else {
122 format!("{host}:{RNODE_TCP_PORT}")
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum RNodeTransportPlan {
129 Serial(RNodeSerialDevice),
130 Tcp(RNodeTcpTarget),
131 Ble(RNodeBleTarget),
132}
133
134impl RNodeTransportPlan {
135 pub(super) fn from_configured_port(mut port: String) -> Result<Self, PlanErrorKind> {
136 if port.is_empty() {
137 return Err(PlanErrorKind::InvalidSetting {
138 key: interface_key::PORT,
139 });
140 }
141 if port
142 .as_bytes()
143 .get(..rnode_key::TCP_SCHEME.len())
144 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(rnode_key::TCP_SCHEME.as_bytes()))
145 {
146 let host = port.split_off(rnode_key::TCP_SCHEME.len());
147 return Ok(Self::Tcp(if host.is_empty() {
148 RNodeTcpTarget::Loopback
149 } else {
150 RNodeTcpTarget::Host(RNodeTcpHost(host))
151 }));
152 }
153 if port
154 .as_bytes()
155 .get(..rnode_key::BLE_SCHEME.len())
156 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(rnode_key::BLE_SCHEME.as_bytes()))
157 {
158 let target = port.split_off(rnode_key::BLE_SCHEME.len());
159 return RNodeBleTarget::from_uri_suffix(target)
160 .map(Self::Ble)
161 .map_err(|_| invalid_rnode_port());
162 }
163 Ok(Self::Serial(RNodeSerialDevice(port)))
164 }
165
166 #[must_use]
167 pub fn channel_tag(&self) -> Vec<u8> {
168 match self {
169 Self::Serial(device) => device.as_str().as_bytes().to_vec(),
170 Self::Tcp(RNodeTcpTarget::Loopback) => rnode_key::TCP_SCHEME.as_bytes().to_vec(),
171 Self::Tcp(RNodeTcpTarget::Host(host)) => {
172 let mut tag = rnode_key::TCP_SCHEME.as_bytes().to_vec();
173 tag.extend_from_slice(host.as_str().as_bytes());
174 tag
175 }
176 Self::Ble(RNodeBleTarget::FirstBondedRnode) => {
177 rnode_key::BLE_SCHEME.as_bytes().to_vec()
178 }
179 Self::Ble(RNodeBleTarget::Address(address)) => {
180 format!("{}{address}", rnode_key::BLE_SCHEME).into_bytes()
181 }
182 Self::Ble(RNodeBleTarget::Name(name)) => {
183 let mut tag = rnode_key::BLE_SCHEME.as_bytes().to_vec();
184 tag.extend_from_slice(name.as_str().as_bytes());
185 tag
186 }
187 }
188 }
189}
190
191fn looks_like_ble_address(value: &str) -> bool {
192 value.len() == RNODE_BLE_ADDRESS_LEN && value.split(':').count() == 6
193}
194
195const fn invalid_rnode_port() -> PlanErrorKind {
196 PlanErrorKind::InvalidSetting {
197 key: interface_key::PORT,
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn tcp_targets_have_a_fixed_stock_port_and_typed_loopback() {
207 let loopback = RNodeTransportPlan::from_configured_port("tcp://".to_string())
208 .expect("stock loopback URI");
209 assert_eq!(loopback, RNodeTransportPlan::Tcp(RNodeTcpTarget::Loopback));
210 assert_eq!(RNodeTcpTarget::Loopback.socket_target(), "localhost:7633");
211
212 let ipv6 = RNodeTransportPlan::from_configured_port("TCP://::1".to_string())
213 .expect("case-insensitive stock URI");
214 let RNodeTransportPlan::Tcp(target) = ipv6 else {
215 panic!("TCP transport expected")
216 };
217 assert_eq!(target.socket_target(), "[::1]:7633");
218 assert_eq!(RNodeTransportPlan::Tcp(target).channel_tag(), b"tcp://::1");
219 }
220
221 #[test]
222 fn a_serial_device_cannot_be_confused_with_a_tcp_target() {
223 let transport = RNodeTransportPlan::from_configured_port("/dev/ttyUSB0".to_string())
224 .expect("serial device");
225 assert_eq!(
226 transport,
227 RNodeTransportPlan::Serial(RNodeSerialDevice("/dev/ttyUSB0".to_string()))
228 );
229 }
230
231 #[test]
232 fn ble_targets_preserve_the_three_stock_selection_modes() {
233 let automatic = RNodeTransportPlan::from_configured_port("ble://".to_string())
234 .expect("stock automatic BLE URI");
235 assert_eq!(
236 automatic,
237 RNodeTransportPlan::Ble(RNodeBleTarget::FirstBondedRnode)
238 );
239
240 let named = RNodeTransportPlan::from_configured_port("BLE://RNode 1234".to_string())
241 .expect("case-insensitive named BLE URI");
242 assert_eq!(
243 named,
244 RNodeTransportPlan::Ble(RNodeBleTarget::Name(RNodeBleName("RNode 1234".to_string())))
245 );
246
247 let addressed =
248 RNodeTransportPlan::from_configured_port("ble://aa:bb:cc:dd:ee:ff".to_string())
249 .expect("addressed BLE URI");
250 assert_eq!(
251 addressed,
252 RNodeTransportPlan::Ble(RNodeBleTarget::Address(RNodeBleAddress([
253 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
254 ])))
255 );
256 assert_eq!(addressed.channel_tag(), b"ble://AA:BB:CC:DD:EE:FF");
257 }
258
259 #[test]
260 fn address_shaped_ble_targets_must_be_hexadecimal() {
261 let error = RNodeTransportPlan::from_configured_port("ble://GG:BB:CC:DD:EE:FF".to_string())
262 .expect_err("malformed Bluetooth LE address");
263 assert_eq!(
264 error,
265 PlanErrorKind::InvalidSetting {
266 key: interface_key::PORT
267 }
268 );
269 }
270}