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