1#[allow(unused_imports)]
4use defmt::{debug, error, info, trace, warn};
5use embedded_hal::blocking::delay::DelayMs;
6use embedded_hal::blocking::i2c::{Read, SevenBitAddress, Write};
7use serde::{Deserialize, Serialize};
8
9use super::{str_string, FutureResponse, NoteError, Notecard};
10
11pub struct Card<'a, IOM: Write<SevenBitAddress> + Read<SevenBitAddress>, const BS: usize> {
12 note: &'a mut Notecard<IOM, BS>,
13}
14
15pub enum Transport {
17 Reset,
18 WifiCell,
19 Wifi,
20 Cell,
21 NTN,
22 WifiNTN,
23 CellNTN,
24 WifiCellNTN,
25}
26
27impl Transport {
28 pub fn str(&self) -> &'static str {
29 use Transport::*;
30
31 match self {
32 Reset => "-",
33 WifiCell => "wifi-cell",
34 Wifi => "wifi",
35 Cell => "cell",
36 NTN => "ntn",
37 WifiNTN => "wifi-ntn",
38 CellNTN => "cell-ntn",
39 WifiCellNTN => "wifi-cell-ntn",
40 }
41 }
42}
43
44impl<'a, IOM: Write<SevenBitAddress> + Read<SevenBitAddress>, const BS: usize> Card<'a, IOM, BS> {
45 pub fn from(note: &mut Notecard<IOM, BS>) -> Card<'_, IOM, BS> {
46 Card { note }
47 }
48
49 pub fn time(
53 self,
54 delay: &mut impl DelayMs<u16>,
55 ) -> Result<FutureResponse<'a, res::Time, IOM, BS>, NoteError> {
56 self.note.request_raw(delay, b"{\"req\":\"card.time\"}\n")?;
57 Ok(FutureResponse::from(self.note))
58 }
59
60 pub fn status(
62 self,
63 delay: &mut impl DelayMs<u16>,
64 ) -> Result<FutureResponse<'a, res::Status, IOM, BS>, NoteError> {
65 self.note
66 .request_raw(delay, b"{\"req\":\"card.status\"}\n")?;
67 Ok(FutureResponse::from(self.note))
68 }
69
70 pub fn restart(
72 self,
73 delay: &mut impl DelayMs<u16>,
74 ) -> Result<FutureResponse<'a, res::Empty, IOM, BS>, NoteError> {
75 self.note
76 .request_raw(delay, b"{\"req\":\"card.restart\"}\n")?;
77 Ok(FutureResponse::from(self.note))
78 }
79
80 pub fn location(
82 self,
83 delay: &mut impl DelayMs<u16>,
84 ) -> Result<FutureResponse<'a, res::Location, IOM, BS>, NoteError> {
85 self.note
86 .request_raw(delay, b"{\"req\":\"card.location\"}\n")?;
87 Ok(FutureResponse::from(self.note))
88 }
89
90 pub fn location_mode(
92 self,
93 delay: &mut impl DelayMs<u16>,
94 mode: Option<&str>,
95 seconds: Option<u32>,
96 vseconds: Option<&str>,
97 delete: Option<bool>,
98 max: Option<u32>,
99 lat: Option<f32>,
100 lon: Option<f32>,
101 minutes: Option<u32>,
102 ) -> Result<FutureResponse<'a, res::LocationMode, IOM, BS>, NoteError> {
103 self.note.request(
104 delay,
105 req::LocationMode {
106 req: "card.location.mode",
107 mode: str_string(mode)?,
108 seconds,
109 vseconds: str_string(vseconds)?,
110 delete,
111 max,
112 lat,
113 lon,
114 minutes,
115 },
116 )?;
117 Ok(FutureResponse::from(self.note))
118 }
119
120 pub fn location_track(
123 self,
124 delay: &mut impl DelayMs<u16>,
125 start: bool,
126 heartbeat: bool,
127 sync: bool,
128 hours: Option<i32>,
129 file: Option<&str>,
130 ) -> Result<FutureResponse<'a, res::LocationTrack, IOM, BS>, NoteError> {
131 self.note.request(
132 delay,
133 req::LocationTrack {
134 req: "card.location.track",
135 start: start.then(|| true),
136 stop: (!start).then(|| true),
137 heartbeat: heartbeat.then(|| true),
138 sync: sync.then(|| true),
139 hours,
140 file: str_string(file)?,
141 },
142 )?;
143
144 Ok(FutureResponse::from(self.note))
145 }
146
147 pub fn wireless(
148 self,
149 delay: &mut impl DelayMs<u16>,
150 mode: Option<&str>,
151 apn: Option<&str>,
152 method: Option<&str>,
153 hours: Option<u32>,
154 ) -> Result<FutureResponse<'a, res::Wireless, IOM, BS>, NoteError> {
155 self.note.request(
156 delay,
157 req::Wireless {
158 req: "card.wireless",
159 mode: str_string(mode)?,
160 method: str_string(method)?,
161 apn: str_string(apn)?,
162 hours,
163 },
164 )?;
165
166 Ok(FutureResponse::from(self.note))
167 }
168
169 pub fn version(
171 self,
172 delay: &mut impl DelayMs<u16>,
173 ) -> Result<FutureResponse<'a, res::Version, IOM, BS>, NoteError> {
174 self.note
175 .request_raw(delay, b"{\"req\":\"card.version\"}\n")?;
176 Ok(FutureResponse::from(self.note))
177 }
178
179 pub fn dfu(
182 self,
183 delay: &mut impl DelayMs<u16>,
184 name: Option<req::DFUName>,
185 on: Option<bool>,
186 stop: Option<bool>,
187 ) -> Result<FutureResponse<'a, res::DFU, IOM, BS>, NoteError> {
188 self.note.request(delay, req::DFU::new(name, on, stop))?;
189 Ok(FutureResponse::from(self.note))
190 }
191
192 pub fn transport(
193 self,
194 delay: &mut impl DelayMs<u16>,
195 method: Transport,
196 allow: Option<bool>,
197 umin: Option<bool>,
198 ) -> Result<FutureResponse<'a, res::Transport, IOM, BS>, NoteError> {
199 self.note.request(
200 delay,
201 req::Transport {
202 req: "card.transport",
203 method: method.str(),
204 allow,
205 umin,
206 },
207 )?;
208 Ok(FutureResponse::from(self.note))
209 }
210}
211
212pub mod req {
213 use super::*;
214
215 #[derive(Deserialize, Serialize, defmt::Format, Default)]
216 pub struct Transport {
217 pub req: &'static str,
218
219 pub method: &'static str,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub allow: Option<bool>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub umin: Option<bool>,
226 }
227
228 #[derive(Deserialize, Serialize, defmt::Format, Default)]
229 pub struct Wireless {
230 pub req: &'static str,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub mode: Option<heapless::String<20>>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub apn: Option<heapless::String<120>>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub method: Option<heapless::String<120>>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub hours: Option<u32>,
243 }
244
245 #[derive(Deserialize, Serialize, defmt::Format, Default)]
246 pub struct LocationTrack {
247 pub req: &'static str,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub start: Option<bool>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub heartbeat: Option<bool>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub sync: Option<bool>,
257
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub stop: Option<bool>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub hours: Option<i32>,
263
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub file: Option<heapless::String<20>>,
266 }
267
268 #[derive(Deserialize, Serialize, defmt::Format, Default)]
269 pub struct LocationMode {
270 pub req: &'static str,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub mode: Option<heapless::String<20>>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub seconds: Option<u32>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub vseconds: Option<heapless::String<20>>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub delete: Option<bool>,
283
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub max: Option<u32>,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub lat: Option<f32>,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub lon: Option<f32>,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub minutes: Option<u32>,
295 }
296
297 #[derive(Deserialize, Serialize, defmt::Format, PartialEq, Debug)]
298 #[serde(rename_all = "lowercase")]
299 pub enum DFUName {
300 Esp32,
301 Stm32,
302 #[serde(rename = "stm32-bi")]
303 Stm32Bi,
304 McuBoot,
305 #[serde(rename = "-")]
306 Reset,
307 }
308
309 #[derive(Deserialize, Serialize, defmt::Format)]
310 pub struct DFU {
311 pub req: &'static str,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub name: Option<req::DFUName>,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub on: Option<bool>,
318
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub off: Option<bool>,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub stop: Option<bool>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub start: Option<bool>,
327 }
328
329 impl DFU {
330 pub fn new(name: Option<req::DFUName>, on: Option<bool>, stop: Option<bool>) -> Self {
331 Self {
337 req: "card.dfu",
338 name,
339 on: on.and_then(|v| if v { Some(true) } else { None }),
340 off: on.and_then(|v| if v { None } else { Some(true) }),
341 stop: stop.and_then(|v| if v { Some(true) } else { None }),
342 start: stop.and_then(|v| if v { None } else { Some(true) }),
343 }
344 }
345 }
346}
347
348pub mod res {
349 use super::*;
350
351 #[derive(Deserialize, defmt::Format)]
352 pub struct Empty {}
353
354 #[derive(Deserialize, defmt::Format)]
355 pub struct LocationTrack {
356 pub start: Option<bool>,
357 pub stop: Option<bool>,
358 pub heartbeat: Option<bool>,
359 pub seconds: Option<u32>,
360 pub hours: Option<i32>,
361 pub file: Option<heapless::String<20>>,
362 }
363
364 #[derive(Deserialize, defmt::Format)]
365 pub struct LocationMode {
366 pub mode: heapless::String<60>,
367 pub seconds: Option<u32>,
368 pub vseconds: Option<heapless::String<40>>,
369 pub max: Option<u32>,
370 pub lat: Option<f64>,
371 pub lon: Option<f64>,
372 pub minutes: Option<u32>,
373 }
374
375 #[derive(Deserialize, defmt::Format)]
376 pub struct Location {
377 pub status: heapless::String<120>,
378 pub mode: heapless::String<120>,
379 pub lat: Option<f64>,
380 pub lon: Option<f64>,
381 pub time: Option<u32>,
382 pub max: Option<u32>,
383 }
384
385 #[derive(Deserialize, defmt::Format)]
386 pub struct Time {
387 pub time: Option<u32>,
388 pub area: Option<heapless::String<120>>,
389 pub zone: Option<heapless::String<120>>,
390 pub minutes: Option<i32>,
391 pub lat: Option<f64>,
392 pub lon: Option<f64>,
393 pub country: Option<heapless::String<120>>,
394 }
395
396 #[derive(Deserialize, defmt::Format)]
397 pub struct Status {
398 pub status: heapless::String<40>,
399 #[serde(default)]
400 pub usb: bool,
401 pub storage: usize,
402 pub time: Option<u64>,
403 #[serde(default)]
404 pub connected: bool,
405 }
406
407 #[derive(Deserialize, defmt::Format)]
408 pub struct WirelessNet {
409 iccid: Option<heapless::String<24>>,
410 imsi: Option<heapless::String<24>>,
411 imei: Option<heapless::String<24>>,
412 modem: Option<heapless::String<35>>,
413 band: Option<heapless::String<24>>,
414 rat: Option<heapless::String<24>>,
415 ratr: Option<heapless::String<24>>,
416 internal: Option<bool>,
417 rssir: Option<i32>,
418 rssi: Option<i32>,
419 rsrp: Option<i32>,
420 sinr: Option<i32>,
421 rsrq: Option<i32>,
422 bars: Option<i32>,
423 mcc: Option<i32>,
424 mnc: Option<i32>,
425 lac: Option<i32>,
426 cid: Option<i32>,
427 modem_temp: Option<i32>,
428 updated: Option<u32>,
429 }
430
431 #[derive(Deserialize, defmt::Format)]
432 pub struct Wireless {
433 pub status: Option<heapless::String<24>>,
434 pub mode: Option<heapless::String<24>>,
435 pub count: Option<u8>,
436 pub net: Option<WirelessNet>,
437 }
438
439 #[derive(Deserialize, defmt::Format)]
440 pub struct VersionInner {
441 pub org: heapless::String<24>,
442 pub product: heapless::String<24>,
443 pub version: heapless::String<24>,
444 pub ver_major: u8,
445 pub ver_minor: u8,
446 pub ver_patch: u8,
447 pub ver_build: u32,
448 pub built: heapless::String<24>,
449 pub target: Option<heapless::String<5>>,
450 }
451
452 #[derive(Deserialize, defmt::Format)]
453 pub struct Version {
454 pub body: VersionInner,
455 pub version: heapless::String<24>,
456 pub device: heapless::String<24>,
457 pub name: heapless::String<30>,
458 pub board: heapless::String<24>,
459 pub sku: heapless::String<24>,
460 pub api: Option<u16>,
461 pub cell: Option<bool>,
462 pub gps: Option<bool>,
463 pub ordering_code: Option<heapless::String<50>>,
464 }
465
466 #[derive(Deserialize, defmt::Format)]
467 pub struct DFU {
468 pub name: req::DFUName,
469 }
470
471 #[derive(Deserialize, defmt::Format)]
472 pub struct Transport {
473 pub method: heapless::String<120>,
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use crate::NotecardError;
481
482 #[test]
483 fn test_version() {
484 let r = br##"{
485 "body": {
486 "org": "Blues Wireless",
487 "product": "Notecard",
488 "version": "notecard-1.5.0",
489 "ver_major": 1,
490 "ver_minor": 5,
491 "ver_patch": 0,
492 "ver_build": 11236,
493 "built": "Sep 2 2020 08:45:10"
494 },
495 "version": "notecard-1.5.0.11236",
496 "device": "dev:000000000000000",
497 "name": "Blues Wireless Notecard",
498 "board": "1.11",
499 "sku": "NOTE-WBNA500",
500 "api": 1
501}"##;
502 serde_json_core::from_slice::<res::Version>(r).unwrap();
503 }
504
505 #[test]
506 fn test_version_411() {
507 let r = br##"{"version":"notecard-4.1.1.4015681","device":"dev:000000000000000","name":"Blues Wireless Notecard","sku":"NOTE-WBEX-500","board":"1.11","api":4,"body":{"org":"Blues Wireless","product":"Notecard","version":"notecard-4.1.1","ver_major":4,"ver_minor":1,"ver_patch":1,"ver_build":4015681,"built":"Dec 5 2022 12:54:58"}}"##;
508 serde_json_core::from_slice::<res::Version>(r).unwrap();
509 }
510
511 #[test]
512 fn test_version_752() {
513 let r = br##"{"version":"notecard-7.5.2.17004","device":"dev:861059067974133","name":"Blues Wireless Notecard","sku":"NOTE-NBGLN","ordering_code":"EB0WT1N0AXBA","board":"5.13","cell":true,"gps":true,"body":{"org":"Blues Wireless","product":"Notecard","target":"u5","version":"notecard-u5-7.5.2","ver_major":7,"ver_minor":5,"ver_patch":2,"ver_build":17004,"built":"Nov 26 2024 14:01:26"}}"##;
514 serde_json_core::from_slice::<res::Version>(r).unwrap();
515 }
516
517 #[test]
518 fn test_card_wireless() {
519 let r = br##"{"status":"{modem-on}","count":3,"net":{"iccid":"89011703278520607527","imsi":"310170852060752","imei":"864475044204278","modem":"BG95M3LAR02A03_01.006.01.006","band":"GSM 900","rat":"gsm","rssir":-77,"rssi":-77,"bars":3,"mcc":242,"mnc":1,"lac":11001,"cid":12313,"updated":1643923524}}"##;
520 serde_json_core::from_slice::<res::Wireless>(r).unwrap();
521
522 let r = br##"{"status":"{cell-registration-wait}","net":{"iccid":"89011703278520606586","imsi":"310170852060658","imei":"864475044197092","modem":"BG95M3LAR02A03_01.006.01.006"}}"##;
523 serde_json_core::from_slice::<res::Wireless>(r).unwrap();
524
525 let r = br##"{"status":"{modem-off}","net":{}}"##;
526 serde_json_core::from_slice::<res::Wireless>(r).unwrap();
527
528 let r = br##"{"status":"{network-up}","mode":"auto","count":3,"net":{"iccid":"89011703278520578660","imsi":"310170852057866","imei":"867730051260788","modem":"BG95M3LAR02A03_01.006.01.006","band":"GSM 900","rat":"gsm","rssir":-77,"rssi":-78,"bars":3,"mcc":242,"mnc":1,"lac":11,"cid":12286,"updated":1646227929}}"##;
529 serde_json_core::from_slice::<res::Wireless>(r).unwrap();
530
531 let r = br##"{"mode":"auto","count":2,"net":{"iccid":"89011704278930030582","imsi":"310170893003058","imei":"860264054655247","modem":"EG91EXGAR08A05M1G_01.001.01.001","band":"LTE BAND 20","rat":"lte","ratr":"\"LTE\"","internal":true,"rssir":-59,"rssi":-60,"rsrp":-92,"sinr":15,"rsrq":-9,"bars":2,"mcc":242,"mnc":2,"lac":2501,"cid":35398693,"modem_temp":34,"updated":1746004605}}"##;
533 serde_json_core::from_slice::<res::Wireless>(r).unwrap();
534 }
535
536 #[test]
537 fn test_card_time_ok() {
538 let r = br##"
539 {
540 "time": 1599769214,
541 "area": "Beverly, MA",
542 "zone": "CDT,America/New York",
543 "minutes": -300,
544 "lat": 42.5776,
545 "lon": -70.87134,
546 "country": "US"
547 }
548 "##;
549
550 serde_json_core::from_slice::<res::Time>(r).unwrap();
551 }
552
553 #[test]
554 fn test_card_time_sa() {
555 let r = br##"
556 {
557 "time": 1599769214,
558 "area": "Kommetjie Western Cape",
559 "zone": "Africa/Johannesburg",
560 "minutes": -300,
561 "lat": 42.5776,
562 "lon": -70.87134,
563 "country": "ZA"
564 }
565 "##;
566
567 serde_json_core::from_slice::<res::Time>(r).unwrap();
568 }
569
570 #[test]
571 fn test_card_time_err() {
572 let r = br##"{"err":"time is not yet set","zone":"UTC,Unknown"}"##;
573 serde_json_core::from_slice::<NotecardError>(r).unwrap();
574 }
575
576 #[test]
577 pub fn test_status_ok() {
578 serde_json_core::from_str::<res::Status>(
579 r#"
580 {
581 "status": "{normal}",
582 "usb": true,
583 "storage": 8,
584 "time": 1599684765,
585 "connected": true
586 }"#,
587 )
588 .unwrap();
589 }
590
591 #[test]
592 pub fn test_status_mising() {
593 serde_json_core::from_str::<res::Status>(
594 r#"
595 {
596 "status": "{normal}",
597 "usb": true,
598 "storage": 8
599 }"#,
600 )
601 .unwrap();
602 }
603
604 #[test]
605 fn test_partial_location_mode() {
606 serde_json_core::from_str::<res::LocationMode>(r#"{"seconds":60,"mode":"periodic"}"#)
607 .unwrap();
608 }
609
610 #[test]
611 fn test_parse_exceed_string_size() {
612 serde_json_core::from_str::<res::LocationMode>(
613 r#"{"seconds":60,"mode":"periodicperiodicperiodicperiodicperiodicperiodicperiodic"}"#,
614 )
615 .ok();
616 }
617
618 #[test]
619 fn test_location_searching() {
620 serde_json_core::from_str::<res::Location>(
621 r#"{"status":"GPS search (111 sec, 32/33 dB SNR, 0/1 sats) {gps-active} {gps-signal} {gps-sats}","mode":"continuous"}"#).unwrap();
622 }
623
624 #[test]
625 fn test_location_mode_err() {
626 let r = br##"{"err":"seconds: field seconds: unmarshal: expected a int32 {io}"}"##;
627 serde_json_core::from_slice::<NotecardError>(r).unwrap();
628 }
629
630 #[test]
631 fn test_dfu_name() {
632 let (res, _) = serde_json_core::from_str::<req::DFUName>(r#""esp32""#).unwrap();
633 assert_eq!(res, req::DFUName::Esp32);
634 let (res, _) = serde_json_core::from_str::<req::DFUName>(r#""stm32""#).unwrap();
635 assert_eq!(res, req::DFUName::Stm32);
636 let (res, _) = serde_json_core::from_str::<req::DFUName>(r#""stm32-bi""#).unwrap();
637 assert_eq!(res, req::DFUName::Stm32Bi);
638 let (res, _) = serde_json_core::from_str::<req::DFUName>(r#""mcuboot""#).unwrap();
639 assert_eq!(res, req::DFUName::McuBoot);
640 let (res, _) = serde_json_core::from_str::<req::DFUName>(r#""-""#).unwrap();
641 assert_eq!(res, req::DFUName::Reset);
642 }
643
644 #[test]
645 fn test_dfu_req() {
646 let req = req::DFU::new(None, None, None);
648 let res: heapless::String<1024> = serde_json_core::to_string(&req).unwrap();
649 assert_eq!(res, r#"{"req":"card.dfu"}"#);
650
651 let req = req::DFU::new(Some(req::DFUName::Esp32), Some(true), None);
653 let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap();
654 assert_eq!(res, r#"{"req":"card.dfu","name":"esp32","on":true}"#);
655
656 let req = req::DFU::new(None, Some(false), None);
658 let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap();
659 assert_eq!(res, r#"{"req":"card.dfu","off":true}"#);
660
661 let req = req::DFU::new(None, None, Some(true));
663 let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap();
664 assert_eq!(res, r#"{"req":"card.dfu","stop":true}"#);
665
666 let req = req::DFU::new(None, None, Some(false));
668 let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap();
669 assert_eq!(res, r#"{"req":"card.dfu","start":true}"#);
670 }
671
672 #[test]
673 fn test_dfu_res() {
674 serde_json_core::from_str::<res::DFU>(r#"{"name": "stm32"}"#).unwrap();
675 }
676}