Skip to main content

prns_config/plan/
rnode.rs

1use 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}