lora_e5/
lib.rs

1use serialport::{SerialPort, SerialPortType};
2use std::{
3    io::IoSlice,
4    time::{self, Duration},
5};
6
7mod error;
8pub use error::Error;
9
10mod types;
11pub use types::*;
12
13mod credentials;
14pub use credentials::*;
15
16mod parse;
17
18#[cfg(test)]
19mod tests;
20
21pub const SILICON_LABS_VID: u16 = 0x10C4;
22pub const CP210X_UART_BRIDGE_PID: u16 = 0xEA60;
23
24#[cfg(feature = "runtime")]
25pub mod process;
26
27pub struct LoraE5<const N: usize> {
28    port: Box<dyn SerialPort>,
29    buf: [u8; N],
30}
31
32pub type Result<T = ()> = std::result::Result<T, error::Error>;
33
34const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
35
36#[derive(Debug)]
37pub struct Downlink {
38    pub rssi: isize,
39    pub snr: f32,
40}
41
42#[derive(Debug, PartialEq, Eq)]
43pub enum JoinResponse {
44    JoinComplete,
45    JoinFailed,
46    AlreadyJoined,
47}
48
49impl<const N: usize> LoraE5<N> {
50    pub fn open_usb(vid: u16, pid: u16) -> Result<Self> {
51        let available_ports = serialport::available_ports()?;
52        for port in available_ports {
53            if let SerialPortType::UsbPort(usb_port) = port.port_type {
54                if usb_port.vid == vid && usb_port.pid == pid {
55                    let port = serialport::new(&port.port_name, 9600)
56                        .timeout(Duration::from_millis(10))
57                        .open()?;
58                    return Ok(Self { port, buf: [0; N] });
59                }
60            }
61        }
62        Err(Error::PortNotFound { vid, pid })
63    }
64
65    pub fn open_path<'a>(path: impl Into<std::borrow::Cow<'a, str>>) -> Result<Self> {
66        let port = serialport::new(path, 9600)
67            .timeout(Duration::from_millis(10))
68            .open()?;
69        Ok(Self { port, buf: [0; N] })
70    }
71
72    fn write_command(&mut self, cmd: &str) -> Result {
73        let n = self
74            .port
75            .write_vectored(&[IoSlice::new(cmd.as_bytes()), IoSlice::new(b"\n")])?;
76        let expected_n = cmd.len() + 1;
77        if n != expected_n {
78            Err(Error::IncorrectWrite(n, expected_n))
79        } else {
80            Ok(())
81        }
82    }
83
84    pub fn is_ok(&mut self) -> Result<bool> {
85        self.write_command("AT")?;
86        let n = self.read_until_break(Duration::from_millis(50))?;
87        Ok(self.check_framed_response(n, "+AT: ", "OK").is_ok())
88    }
89
90    pub fn get_version(&mut self) -> Result<String> {
91        const EXPECTED_PRELUDE: &str = "+VER: ";
92        self.write_command("AT+VER")?;
93        let n = self.read_until_break(DEFAULT_TIMEOUT)?;
94        let version = self.framed_response(n, EXPECTED_PRELUDE)?;
95        Ok(version.trim_end().to_string())
96    }
97
98    pub fn set_channel(&mut self, ch: u8, enable: bool) -> Result {
99        let cmd = format!("AT+CH={ch},{}", if enable { "on" } else { "off" });
100        self.write_command(&cmd)?;
101        let n = self.read_until_break(DEFAULT_TIMEOUT)?;
102        self.check_framed_response(n, "+CH: CH", &format!("{ch} off"))
103    }
104
105    pub fn subband2_only(&mut self) -> Result {
106        for n in (0..8).chain(16..72) {
107            self.set_channel(n, false)?;
108        }
109        Ok(())
110    }
111
112    pub fn set_region(&mut self, region: Region) -> Result {
113        const EXPECTED_PRELUDE: &str = "+DR: ";
114        let cmd = format!("AT+DR={}", region.as_str());
115        self.write_command(&cmd)?;
116        let n = self.read_until_break(DEFAULT_TIMEOUT)?;
117        self.check_framed_response(n, EXPECTED_PRELUDE, region.as_str())
118    }
119
120    pub fn set_mode(&mut self, mode: Mode) -> Result {
121        const EXPECTED_PRELUDE: &str = "+MODE: ";
122        let cmd = format!("AT+MODE={}", mode.as_str());
123        self.write_command(&cmd)?;
124        let n = self.read_until_break(DEFAULT_TIMEOUT)?;
125        self.check_framed_response(n, EXPECTED_PRELUDE, mode.as_str())
126    }
127
128    pub fn set_datarate(&mut self, dr: DR) -> Result {
129        let cmd = format!("AT+DR={}", dr.as_str());
130        self.write_command(&cmd)?;
131        let n = self.read_until_pattern(&DR::all_patterns(), DEFAULT_TIMEOUT)?;
132        let response = std::str::from_utf8(&self.buf[..n])?;
133        if response.contains(dr.termination_pattern()) {
134            Ok(())
135        } else {
136            Err(Error::UnexpectedResponse(response.to_string()))
137        }
138    }
139
140    pub fn join(&mut self) -> Result<JoinResponse> {
141        const JOIN_DONE: &str = "+JOIN: Done\r\n";
142        const ALREADY_JOINED: &str = "+JOIN: Joined already\r\n";
143
144        self.write_command("AT+JOIN")?;
145        let n = self.read_until_pattern(&[JOIN_DONE, ALREADY_JOINED], Duration::from_secs(20))?;
146        let response = std::str::from_utf8(&self.buf[..n])?;
147        Ok(if response.contains(ALREADY_JOINED) {
148            JoinResponse::AlreadyJoined
149        } else if response.contains("Network joined") {
150            JoinResponse::JoinComplete
151        } else {
152            JoinResponse::JoinFailed
153        })
154    }
155
156    pub fn force_join(&mut self) -> Result<JoinResponse> {
157        const JOIN_DONE: &str = "+JOIN: Done\r\n";
158        self.write_command("AT+JOIN=FORCE")?;
159        let n = self.read_until_pattern(&[JOIN_DONE], Duration::from_secs(20))?;
160        let response = std::str::from_utf8(&self.buf[..n])?;
161        Ok(if response.contains("Network joined") {
162            JoinResponse::JoinComplete
163        } else {
164            JoinResponse::JoinFailed
165        })
166    }
167
168    pub fn set_port(&mut self, port: u8) -> Result {
169        const EXPECTED_PRELUDE: &str = "+PORT: ";
170        let cmd = format!("AT+PORT={port}");
171        self.write_command(&cmd)?;
172        let n = self.read_until_break(DEFAULT_TIMEOUT)?;
173        self.check_framed_response(n, EXPECTED_PRELUDE, &port.to_string())
174    }
175
176    pub fn send(&mut self, data: &[u8], port: u8, confirmed: bool) -> Result<Option<Downlink>> {
177        self.set_port(port)?;
178        let start_line = if confirmed {
179            "+CMSGHEX: Start\r\n"
180        } else {
181            "+MSGHEX: Start\r\n"
182        };
183
184        let busy_line = if confirmed {
185            "+CMSGHEX: LoRaWAN modem is busy\r\n"
186        } else {
187            "+MSGHEX: LoRaWAN modem is busy\r\n"
188        };
189
190        let hex = hex::encode(data);
191        let cmd = format!(
192            "AT+{}=\"{hex}\"",
193            if confirmed { "CMSGHEX" } else { "MSGHEX" }
194        );
195        self.write_command(&cmd)?;
196        // wait for the Start
197        let n = self.read_until_pattern(&[start_line, busy_line], Duration::from_secs(3))?;
198        let response = std::str::from_utf8(&self.buf[..n])?;
199        let busy = response == busy_line;
200        let end_line = if confirmed {
201            "+CMSGHEX: Done\r\n"
202        } else {
203            "+MSGHEX: Done\r\n"
204        };
205        // wait for the Done
206        let n = self.read_until_pattern(&[end_line], Duration::from_secs(10))?;
207        let response = std::str::from_utf8(&self.buf[..n])?;
208
209        if busy {
210            return Err(Error::Busy);
211        }
212
213        // if we weren't busy, we may have gotten some attributes
214        if let Some(m) = response.find("RXWIN1") {
215            let (rssi, snr) = parse_rssi_snr(response, m)?;
216            Ok(Some(Downlink { rssi, snr }))
217        } else if let Some(m) = response.find("RXWIN2") {
218            let (rssi, snr) = parse_rssi_snr(response, m)?;
219            Ok(Some(Downlink { rssi, snr }))
220        } else if confirmed {
221            // we expect a downlink when sending confirmed uplinks
222            // todo: check for ACK in response
223            Err(Error::Nack)
224        } else {
225            Ok(None)
226        }
227    }
228
229    pub fn send_ascii(
230        &mut self,
231        data: &str,
232        port: u8,
233        confirmed: bool,
234    ) -> Result<Option<Downlink>> {
235        self.set_port(port)?;
236        let end_line = if confirmed {
237            "+CMSG: Done\r\n"
238        } else {
239            "+MSG: Done\r\n"
240        };
241        let hex = hex::encode(data);
242        let cmd = format!("AT+{}=\"{hex}\"", if confirmed { "CMSG" } else { "MSG" });
243        self.write_command(&cmd)?;
244        let n = self.read_until_pattern(&[end_line], Duration::from_secs(3))?;
245        let response = std::str::from_utf8(&self.buf[..n])?;
246
247        if let Some(m) = response.find("RXWIN1") {
248            let (rssi, snr) = parse_rssi_snr(response, m)?;
249            Ok(Some(Downlink { rssi, snr }))
250        } else if let Some(m) = response.find("RXWIN2") {
251            let (rssi, snr) = parse_rssi_snr(response, m)?;
252            Ok(Some(Downlink { rssi, snr }))
253        } else if confirmed {
254            // we expect a downlink when sending confirmed uplinks
255            // todo: check for ACK in response
256            Err(Error::Nack)
257        } else {
258            Ok(None)
259        }
260    }
261}
262
263pub(crate) fn parse_rssi_snr(response: &str, m: usize) -> Result<(isize, f32)> {
264    let (_, remaining_str) = response.split_at(m);
265    if let Some(n) = remaining_str.find("\r\n") {
266        let (line, _) = remaining_str.split_at(n);
267        let (_, signal) = line.split_at(", RSSI ".len());
268        if let Some(n) = signal.find(", ") {
269            let (rssi_remainder, snr_remainder) = signal.split_at(n);
270            let (_, rssi) = rssi_remainder.split_at(" RSSI ".len());
271            let (_, snr) = snr_remainder.split_at(", SNR ".len());
272            return Ok((
273                rssi.parse().map_err(Error::FailedToParseRssiInt)?,
274                snr.parse().map_err(Error::FailedToParseSnrF32)?,
275            ));
276        }
277    }
278    Err(Error::FailedToParseRssiSnr(response.to_string()))
279}