From e76a04f47b92e7d188990c06c4bf8b36ebfc9b70 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Tue, 6 Jan 2026 14:12:35 +0100 Subject: [PATCH 1/5] project: added gps location --- pico/README.md | 2 + pico/app/src/main.rs | 18 +- pico/pico-lib/src/call.rs | 56 ++--- pico/pico-lib/src/gps.rs | 386 ++++++++++++++++++++++++++++++++++ pico/pico-lib/src/lib.rs | 2 + pico/pico-lib/src/location.rs | 7 + pico/pico-lib/src/network.rs | 99 +++++---- pico/pico-lib/src/poro.rs | 13 +- pico/pico-lib/src/sms.rs | 48 ++--- pico/pico-lib/src/utils.rs | 30 +++ 10 files changed, 530 insertions(+), 131 deletions(-) create mode 100644 pico/pico-lib/src/gps.rs create mode 100644 pico/pico-lib/src/location.rs diff --git a/pico/README.md b/pico/README.md index 35ae423..29260f8 100644 --- a/pico/README.md +++ b/pico/README.md @@ -5,4 +5,6 @@ This project uses [atat](https://github.com/FactbirdHQ/atat/) a no_std crate for ## Sim868 documentation - [SIM800 Series AT Command Manual V1.11.pdf](https://www.waveshare.com/wiki/File:SIM800_Series_AT_Command_Manual_V1.11.pdf) +- [SIM868_Series_GNSS_Application_Note_V1.02.pdf](https://www.waveshare.com/wiki/File:SIM868_Series_GNSS_Application_Note_V1.02.pdf) +- [SIM800_Series_GSM_Location_Application_Note_V1.03.pdf](https://www.waveshare.com/wiki/File:SIM800_Series_GSM_Location_Application_Note_V1.03.pdf) - [Sim868](https://www.simcom.com/product/SIM868.html) diff --git a/pico/app/src/main.rs b/pico/app/src/main.rs index 32735e5..881fb9f 100644 --- a/pico/app/src/main.rs +++ b/pico/app/src/main.rs @@ -2,7 +2,7 @@ #![no_main] use alloc::string::ToString; -use atat::asynch::{AtatClient, Client}; +use atat::asynch::Client; use atat::{AtatIngress, DefaultDigester, Ingress, ResponseSlot, UrcChannel}; use core::ptr::addr_of_mut; use embassy_executor::Spawner; @@ -23,7 +23,7 @@ use {defmt_rtt as _, panic_probe as _}; use pico_lib::at::PicoHW; use pico_lib::poro; use pico_lib::urc; -use pico_lib::utils::LogBE; +use pico_lib::utils::send_command_logged; use pico_lib::{at, battery, call, network, sms}; extern crate alloc; @@ -143,13 +143,15 @@ async fn main(spawner: Spawner) { Timer::after(Duration::from_millis(100)).await; } + match send_command_logged( + &mut client, + &battery::AtBatteryChargeExecute, + "AtBatteryChargeExecute".to_string(), + ) + .await { - let _l = LogBE::new("AtBatteryChargeExecute".to_string()); - let r = client.send(&battery::AtBatteryChargeExecute).await; - match r { - Ok(b) => log::info!(" OK {:?}", b), - Err(e) => log::info!(" ERROR: {:?}", e), - } + Ok(v) => log::info!(" {:?}", v), + Err(_) => (), } call::call_number( diff --git a/pico/pico-lib/src/call.rs b/pico/pico-lib/src/call.rs index d85cd46..6d22ae1 100644 --- a/pico/pico-lib/src/call.rs +++ b/pico/pico-lib/src/call.rs @@ -7,6 +7,7 @@ use atat::heapless::String; use crate::at::NoResponse; use crate::utils::LogBE; +use crate::utils::send_command_logged; // 6.2.19 AT+CHFA Swap the Audio Channels #[derive(Clone, Debug, AtatCmd)] @@ -61,45 +62,34 @@ pub async fn call_number( number: &'static str, duration_millis: u64, ) { - { - let _l = LogBE::new("AtSwapAudioChannelsWrite".to_string()); - let r = client - .send(&AtSwapAudioChannelsWrite { - n: AudioChannels::Main, - }) - .await; - match r { - Ok(_) => log::info!(" OK"), - Err(e) => log::info!(" ERROR: {:?}", e), - } - } - - { - let _l = LogBE::new("AtDialNumber".to_string()); - let r = client - .send(&AtDialNumber { - number: String::<16>::try_from(number).unwrap(), - }) - .await; - match r { - Ok(_) => log::info!(" OK"), - Err(e) => log::info!(" ERROR: {:?}", e), - } - } + send_command_logged( + client, + &AtSwapAudioChannelsWrite { + n: AudioChannels::Main, + }, + "AtSwapAudioChannelsWrite".to_string(), + ) + .await + .ok(); + + send_command_logged( + client, + &AtDialNumber { + number: String::<16>::try_from(number).unwrap(), + }, + "AtSwapAudioChannelsAtDialNumberWrite".to_string(), + ) + .await + .ok(); { let _l = LogBE::new("Sleeping".to_string()); pico.sleep(duration_millis).await; } - { - let _l = LogBE::new("AtHangup".to_string()); - let r = client.send(&AtHangup).await; - match r { - Ok(_) => log::info!(" OK"), - Err(e) => log::info!(" ERROR: {:?}", e), - } - } + send_command_logged(client, &AtHangup, "AtHangup".to_string()) + .await + .ok(); } #[cfg(test)] diff --git a/pico/pico-lib/src/gps.rs b/pico/pico-lib/src/gps.rs new file mode 100644 index 0000000..813c255 --- /dev/null +++ b/pico/pico-lib/src/gps.rs @@ -0,0 +1,386 @@ +use core::num::ParseFloatError; +use core::num::ParseIntError; +use core::str::Utf8Error; + +use alloc::format; +use alloc::string::ToString; +use atat::atat_derive::AtatCmd; +use atat::atat_derive::AtatEnum; +use atat::atat_derive::AtatResp; +use atat::heapless::String; + +use crate::at::NoResponse; +use crate::location; +use crate::utils; +use crate::utils::as_tokens; +use crate::utils::send_command_logged; + +// SIM868_Series_GNSS_Application_Note_V1.02.pdf + +// 2.1 AT+CGNSPWR GNSS Power Control +// AT+CMGF=[] +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CGNSPWR", NoResponse)] +pub struct AtGnssPowerControlWrite { + pub mode: PowerMode, +} + +#[derive(Debug, Clone, PartialEq, AtatEnum)] +pub enum PowerMode { + TurnOff = 0, + TurnOn = 1, +} + +// 2.3 AT+CGNSINF GNSS navigation information parsed from NMEA sentences +// AT+CGNSINF=[] +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CGNSINF", GnssNavigationInformationResponse, parse = parse_gnss_navigation_information)] +pub struct AtGnssNavigationInformationExecute; + +// +CGNSINF: ,,,,,,,,,,,,,,,,,,,, +#[derive(Debug, Clone, AtatResp, PartialEq, Default)] +#[rustfmt::skip] +pub struct GnssNavigationInformationResponse { // Length Format + #[at_arg(position = 0)] + pub gnss_run_status: GNSSRunStatus, // 1 + #[at_arg(position = 1)] + pub fix_status: FixStatus, // 1 + #[at_arg(position = 2)] + pub utc_date_time: String<18>, // 18 yyyyMMddhhmmss.sss [1980-2039][1-12][1-31][0-23][0-59][0.000-60.999] + #[at_arg(position = 3)] + pub latitude: f64, // 10 [-90.000000,90.000000] + #[at_arg(position = 4)] + pub longitude: f64, // 11 [-180.000000,180.000000] + #[at_arg(position = 5)] + pub msl_altitude: f64, // 8 [-180.000000,180.000000] meters + #[at_arg(position = 6)] + pub speed_over_ground: f64, // 6 [0,999.999] km/h + #[at_arg(position = 7)] + pub course_over_ground: f64, // 6 [0,360.00] degrees + #[at_arg(position = 8)] + pub fix_mode: u8, // 1 [0,1,2(reserved)] + #[at_arg(position = 9)] + pub reserved1: Option, // 0 + #[at_arg(position = 10)] + pub hdop: f64, // 4 [0,99.9] (Horizontal Dilution of Precision) + #[at_arg(position = 11)] + pub pdop: f64, // 4 [0,99.9] (Position Dilution of Precision) + #[at_arg(position = 12)] + pub vdop: f64, // 4 [0,99.9] (Vertical Dilution of Precision) + #[at_arg(position = 13)] + pub reserved2: Option, // 0 + #[at_arg(position = 14)] + pub gps_satellites_in_view: Option, // 2 [0,99] + #[at_arg(position = 15)] + pub gnss_satellites_used: Option, // 2 [0,99] + #[at_arg(position = 16)] + pub glonass_satellites_in_view: Option, // 2 [0,99] + #[at_arg(position = 17)] + pub reserved3: Option, // 0 + #[at_arg(position = 18)] + pub c_n0_max: u8, // 2 [0,55] dBHz + #[at_arg(position = 19)] + pub hpa: Option, // 6 [0,9999.9] meters (Horizontal Positional Accuracy) reversed + #[at_arg(position = 20)] + pub vpa: Option, // 6 [0,9999.9] meters (Vertical Positional Accuracy) reversed +} // 94 + +extern crate atat; + +#[allow(dead_code)] // field `0` is never read, TODO: research +pub struct AtatError(atat::Error); + +impl From for AtatError { + fn from(_: ParseFloatError) -> Self { + AtatError(atat::Error::Parse) + } +} + +impl From for AtatError { + fn from(_: ParseIntError) -> Self { + AtatError(atat::Error::Parse) + } +} + +impl From<()> for AtatError { + fn from(_: ()) -> Self { + AtatError(atat::Error::Parse) + } +} + +impl From for AtatError { + fn from(_: Utf8Error) -> Self { + AtatError(atat::Error::Parse) + } +} + +impl From for AtatError { + fn from(value: atat::Error) -> Self { + AtatError(value) + } +} + +fn parse_gnss_navigation_information( + response: &[u8], +) -> Result { + log::debug!(" parse_gnss_navigation_information input: {:?}", response); + let text = core::str::from_utf8(&response[10..])?; // removes "AT+CGNSINF+", ends with \r\n + let mut tokens = as_tokens(text.trim_end().to_string(), ","); + log::debug!(" input: {:?}, len={}", tokens, tokens.len()); + if tokens.len() != 21 { + return Err(atat::Error::Parse.into()); + } + + let mut resp = GnssNavigationInformationResponse::default(); + + match tokens.pop_front().unwrap().as_str() { + "0" => resp.gnss_run_status = GNSSRunStatus::Off, + "1" => resp.gnss_run_status = GNSSRunStatus::On, + _ => { + return Err(atat::Error::Parse.into()); + } + }; + + match tokens.pop_front().unwrap().as_str() { + "0" => resp.fix_status = FixStatus::NotFixedPosition, + "1" => resp.fix_status = FixStatus::FixedPosition, + _ => { + return Err(atat::Error::Parse.into()); + } + }; + + resp.utc_date_time = String::try_from(tokens.pop_front().unwrap().as_str())?; + resp.latitude = tokens.pop_front().unwrap().parse()?; + resp.longitude = tokens.pop_front().unwrap().parse()?; + resp.msl_altitude = tokens.pop_front().unwrap().parse()?; + resp.speed_over_ground = tokens.pop_front().unwrap().parse()?; + resp.course_over_ground = tokens.pop_front().unwrap().parse()?; + resp.fix_mode = tokens.pop_front().unwrap().parse()?; + tokens.pop_front(); // reserved1 + resp.hdop = tokens.pop_front().unwrap().parse()?; + resp.pdop = tokens.pop_front().unwrap().parse()?; + resp.vdop = tokens.pop_front().unwrap().parse()?; + tokens.pop_front(); // reserved2 + + match tokens.pop_front().unwrap().as_str() { + "" => Ok::<(), AtatError>(()), + text => { + let u: u8 = text.parse()?; + resp.gps_satellites_in_view = Some(u); + Ok(()) + } + }?; + + match tokens.pop_front().unwrap().as_str() { + "" => Ok::<(), AtatError>(()), + text => { + let u: u8 = text.parse()?; + resp.gnss_satellites_used = Some(u); + Ok(()) + } + }?; + + match tokens.pop_front().unwrap().as_str() { + "" => Ok::<(), AtatError>(()), + text => { + let u: u8 = text.parse()?; + resp.glonass_satellites_in_view = Some(u); + Ok(()) + } + }?; + + tokens.pop_front(); // reserved3 + + resp.c_n0_max = tokens.pop_front().unwrap().parse()?; + + match tokens.pop_front().unwrap().as_str() { + "" => Ok::<(), AtatError>(()), + text => { + let u: f64 = text.parse()?; + resp.hpa = Some(u); + Ok(()) + } + }?; + + match tokens.pop_front().unwrap().as_str() { + "" => Ok::<(), AtatError>(()), + text => { + let u: f64 = text.parse()?; + resp.vpa = Some(u); + Ok(()) + } + }?; + + return Ok(resp); +} + +#[derive(Debug, Clone, PartialEq, AtatEnum, Default)] +pub enum GNSSRunStatus { + #[default] + Off = 0, + On = 1, +} + +#[derive(Debug, Clone, PartialEq, AtatEnum, Default)] +pub enum FixStatus { + #[default] + NotFixedPosition = 0, + FixedPosition = 1, +} + +pub async fn get_gps_location( + client: &mut T, + _pico: &mut U, + max_retries: u8, +) -> Option { + send_command_logged( + client, + &AtGnssPowerControlWrite { + mode: PowerMode::TurnOn, + }, + "AtGnssPowerControlWrite ON".to_string(), + ) + .await + .ok(); + + let mut location: Option = None; + for i in 0..max_retries { + match send_command_logged( + client, + &AtGnssNavigationInformationExecute, + format!("AtGnssNavigationInformationExecute {}", i), + ) + .await + { + Ok(resp) => { + location = Some(location::Location { + latitude: resp.latitude, + longitude: resp.longitude, + accuracy: utils::estimate_gps_accuracy(resp.hdop, resp.vdop, resp.pdop), + timestamp: 0, + }); + break; + } + Err(_) => (), + } + } + + send_command_logged( + client, + &AtGnssPowerControlWrite { + mode: PowerMode::TurnOff, + }, + "AtGnssPowerControlWrite OFF".to_string(), + ) + .await + .ok(); + + return location; +} + +#[cfg(test)] +extern crate std; + +#[cfg(test)] +mod tests { + use crate::{at, cmd_serialization_tests}; + + use super::*; + use atat::AtatCmd; + use atat::serde_at; + + cmd_serialization_tests! { + test_at_gnss_power_control_on: ( + AtGnssPowerControlWrite { + mode: PowerMode::TurnOn, + }, + 13, + "AT+CGNSPWR=1\r", + ), + test_at_gnss_power_control_off: ( + AtGnssPowerControlWrite { + mode: PowerMode::TurnOff, + }, + 13, + "AT+CGNSPWR=0\r", + ), + test_at_gnss_navigation_information_execute: ( + AtGnssNavigationInformationExecute, + 11, + "AT+CGNSINF\r", + ), + } + + #[test] + fn test_at_gnss_navigation_information_responses() { + at::tests::init_env_logger(); + let cmd = AtGnssNavigationInformationExecute; + + assert_eq!( + atat::Error::Parse, + cmd.parse(Ok(b"+CGNSINF: ,,,,\r\n")).err().unwrap(), + ); + + assert_eq!( + atat::Error::Parse, + cmd.parse(Ok(b"+CGNSINF: 1,1,20221212120221.000,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,f,,,51,,\r\n")).err().unwrap(), + ); + + assert_eq!( + GnssNavigationInformationResponse { + gnss_run_status: GNSSRunStatus::On, + fix_status: FixStatus::FixedPosition, + utc_date_time: serde_at::from_slice(b"20221212120221.000").unwrap(), + latitude: 46.7624859, + longitude: 18.6304591, + msl_altitude: 329.218, + speed_over_ground: 2.20, + course_over_ground: 285.8, + fix_mode: 1, + reserved1: None, + hdop: 2.1, + pdop: 2.3, + vdop: 0.9, + reserved2: None, + gps_satellites_in_view: Some(7), + gnss_satellites_used: Some(6), + glonass_satellites_in_view: None, + reserved3: None, + c_n0_max: 51, + hpa: None, + vpa: None, + }, + cmd.parse(Ok(b"+CGNSINF: 1,1,20221212120221.000,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,6,,,51,,\r\n")).unwrap() + ); + } + + #[tokio::test] + async fn test_get_gps_location() { + at::tests::init_env_logger(); + + let mut client = crate::at::tests::ClientMock::default(); + client.results.push_back(Ok("".as_bytes())); // Turn On + client.results.push_back(Ok("+CGNSINF: ,,,,".as_bytes())); // error + client.results.push_back(Ok("+CGNSINF: ,,,,".as_bytes())); // error + client.results.push_back(Ok("+CGNSINF: 1,1,20221212120221.000,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,6,,,51,,".as_bytes())); // location + client.results.push_back(Ok("".as_bytes())); // Turn off + + let mut pico = crate::at::tests::PicoMock::default(); + let loc1 = get_gps_location(&mut client, &mut pico, 5).await; + assert_eq!(5, client.sent_commands.len()); + assert_eq!("AT+CGNSPWR=1\r", client.sent_commands.get(0).unwrap()); + assert_eq!("AT+CGNSINF\r", client.sent_commands.get(1).unwrap()); + assert_eq!("AT+CGNSINF\r", client.sent_commands.get(2).unwrap()); + assert_eq!("AT+CGNSINF\r", client.sent_commands.get(3).unwrap()); + assert_eq!("AT+CGNSPWR=0\r", client.sent_commands.get(4).unwrap()); + assert_eq!( + location::Location { + latitude: 46.7624859, + longitude: 18.6304591, + accuracy: 500.0, + timestamp: 0, + }, + loc1.unwrap() + ); + } +} diff --git a/pico/pico-lib/src/lib.rs b/pico/pico-lib/src/lib.rs index 78ffc53..ce26fee 100644 --- a/pico/pico-lib/src/lib.rs +++ b/pico/pico-lib/src/lib.rs @@ -5,6 +5,8 @@ extern crate alloc; pub mod at; pub mod battery; pub mod call; +pub mod gps; +pub mod location; pub mod network; pub mod poro; pub mod sms; diff --git a/pico/pico-lib/src/location.rs b/pico/pico-lib/src/location.rs new file mode 100644 index 0000000..865408b --- /dev/null +++ b/pico/pico-lib/src/location.rs @@ -0,0 +1,7 @@ +#[derive(Clone, Debug, PartialEq)] +pub struct Location { + pub latitude: f64, + pub longitude: f64, + pub accuracy: f64, + pub timestamp: i64, +} diff --git a/pico/pico-lib/src/network.rs b/pico/pico-lib/src/network.rs index 551770d..59983ed 100644 --- a/pico/pico-lib/src/network.rs +++ b/pico/pico-lib/src/network.rs @@ -5,7 +5,7 @@ use atat::atat_derive::AtatResp; use atat::heapless::String; use crate::at::NoResponse; -use crate::utils::LogBE; +use crate::utils::send_command_logged; #[derive(Clone, Debug, AtatCmd)] #[at_cmd("AT", NoResponse, cmd_prefix = "", timeout_ms = 5000)] @@ -94,79 +94,74 @@ pub async fn init_network( client: &mut T, pico: &mut U, ) { - { - let _l = LogBE::new("AtSetCommandEchoOff".to_string()); - let r = client.send(&AtSetCommandEchoOff).await; - match r { - Ok(_) => log::info!(" OK"), - Err(e) => log::info!(" ERROR: {:?}", e), - } - } + send_command_logged( + client, + &AtSetCommandEchoOff, + "AtSetCommandEchoOff".to_string(), + ) + .await + .ok(); loop { - let _l = LogBE::new("AtInit".to_string()); - let r = client.send(&AtInit).await; - match r { - Ok(_) => { - log::info!(" OK"); - break; - } - Err(e) => { - log::info!(" ERROR: {:?}", e); - pico.power_on_off().await; - } + match send_command_logged(client, &AtInit, "AtInit".to_string()).await { + Ok(_) => break, + Err(_) => pico.power_on_off().await, } } loop { - let _l = LogBE::new("AtNetworkRegistrationRead".to_string()); - let r = client.send(&AtNetworkRegistrationRead).await; - match r { - Ok(n) => { - log::info!(" OK stat={:?}", n.stat); - if n.stat == NetworkRegistrationStatus::Registered - || n.stat == NetworkRegistrationStatus::RegisteredRoaming + match send_command_logged( + client, + &AtNetworkRegistrationRead, + "AtNetworkRegistrationRead".to_string(), + ) + .await + { + Ok(v) => { + log::info!(" {:?}", v); + if v.stat == NetworkRegistrationStatus::Registered + || v.stat == NetworkRegistrationStatus::RegisteredRoaming { break; } } - Err(e) => log::info!(" ERROR: {:?}", e), + Err(_) => (), } pico.sleep(1000).await; } - { - let _l = LogBE::new("AtEnterPinRead".to_string()); - let r = client.send(&AtEnterPinRead).await; - match r { - Ok(n) => { - log::info!(" OK code={:?}", n.code); - if n.code != "READY" { - pico.set_led_high(); - log::info!(" !!!DISABLE PIN ON SIM CARD!!!"); - pico.sleep(60 * 1000).await; - } + match send_command_logged(client, &AtEnterPinRead, "AtEnterPinRead".to_string()).await { + Ok(v) => { + log::info!(" {:?}", v); + if v.code != "READY" { + pico.set_led_high(); + log::info!(" !!!DISABLE PIN ON SIM CARD!!!"); + pico.sleep(60 * 1000).await; } - Err(e) => log::info!(" ERROR: {:?}", e), } + Err(_) => (), } + match send_command_logged( + client, + &AtSignalQualityReportExecute, + "AtSignalQualityReportExecute".to_string(), + ) + .await { - let _l = LogBE::new("AtSignalQualityReportExecute".to_string()); - let r = client.send(&AtSignalQualityReportExecute).await; - match r { - Ok(n) => log::info!(" OK rssi={:?}", n.rssi), - Err(e) => log::info!(" ERR: {:?}", e), - } + Ok(v) => log::info!(" {:?}", v), + Err(_) => (), } + match send_command_logged( + client, + &AtOperatorSelectionRead, + "AtOperatorSelectionRead".to_string(), + ) + .await { - let _l = LogBE::new("AtOperatorSelectionRead".to_string()); - let r = client.send(&AtOperatorSelectionRead).await; - match r { - Ok(n) => log::info!(" OK operator={:?}", n.oper), - Err(e) => log::info!(" ERROR: {:?}", e), - } + Ok(v) => log::info!(" {:?}", v), + Err(_) => (), } } diff --git a/pico/pico-lib/src/poro.rs b/pico/pico-lib/src/poro.rs index 7a995bc..57de363 100644 --- a/pico/pico-lib/src/poro.rs +++ b/pico/pico-lib/src/poro.rs @@ -101,15 +101,6 @@ pub struct Watcher { // Utils -fn as_tokens(input: String) -> VecDeque { - let parts = input.split(DELIMITER); - let mut tokens = VecDeque::new(); - for part in parts { - tokens.push_back(String::from(part)); - } - return tokens; -} - // https://stackoverflow.com/questions/50277050/format-convert-a-number-to-a-string-in-any-base-including-bases-other-than-deci fn to_str_radix(value: i64, radix: u64) -> String { let mut buf = Vec::new(); @@ -386,7 +377,7 @@ impl ProtectorMachine { } pub fn parse(&self, d: String) -> Result { - let mut tokens = as_tokens(d); + let mut tokens = utils::as_tokens(d, DELIMITER); let mut p = Protector::default(); p.x_parse(&mut tokens)?; Ok(p) @@ -522,7 +513,7 @@ impl WatcherMachine { } pub fn parse(&self, d: String) -> Result { - let mut tokens = as_tokens(d); + let mut tokens = utils::as_tokens(d, DELIMITER); let mut w = Watcher::default(); w.x_parse(&mut tokens)?; Ok(w) diff --git a/pico/pico-lib/src/sms.rs b/pico/pico-lib/src/sms.rs index 973cb09..04aa3eb 100644 --- a/pico/pico-lib/src/sms.rs +++ b/pico/pico-lib/src/sms.rs @@ -6,7 +6,7 @@ use atat::atat_derive::AtatEnum; use atat::heapless::String; use crate::at::NoResponse; -use crate::utils::LogBE; +use crate::utils::send_command_logged; // 4.2.2 AT+CMGF Select SMS Message Format // AT+CMGF=[] @@ -54,32 +54,26 @@ pub async fn send_sms( number: &'static str, // Bytes<16> ? (same for Call) message: &'static str, // Bytes<160> ? ) { - { - let _l = LogBE::new("AtSelectSMSMessageFormatWrite".to_string()); - let r = client - .send(&AtSelectSMSMessageFormatWrite { - mode: MessageMode::Text, - }) - .await; - match r { - Ok(_) => log::info!(" OK"), - Err(e) => log::info!(" ERROR: {:?}", e), - } - } - - { - let _l = LogBE::new("AtSendSMSWrite".to_string()); - let r = client - .send(&AtSendSMSWrite { - number: String::<16>::try_from(number).unwrap(), - message: String::<160>::try_from(message).unwrap(), - }) - .await; - match r { - Ok(_) => log::info!(" OK"), - Err(e) => log::info!(" ERROR: {:?}", e), - } - } + send_command_logged( + client, + &AtSelectSMSMessageFormatWrite { + mode: MessageMode::Text, + }, + "AtSelectSMSMessageFormatWrite".to_string(), + ) + .await + .ok(); + + send_command_logged( + client, + &AtSendSMSWrite { + number: String::<16>::try_from(number).unwrap(), + message: String::<160>::try_from(message).unwrap(), + }, + "AtSendSMSWrite".to_string(), + ) + .await + .ok(); } #[cfg(test)] diff --git a/pico/pico-lib/src/utils.rs b/pico/pico-lib/src/utils.rs index e28db69..4ab7416 100644 --- a/pico/pico-lib/src/utils.rs +++ b/pico/pico-lib/src/utils.rs @@ -1,3 +1,4 @@ +use alloc::collections::vec_deque::VecDeque; use alloc::string::String; use libm::{asin, cos, pow, sin, sqrt}; @@ -13,6 +14,21 @@ pub fn get_distance_in_meters(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 return earth_radius_in_meters * c; } +// todo research, how to do this +pub fn estimate_gps_accuracy(_pdop: f64, _vdop: f64, _hdop: f64) -> f64 { + return 500.0; + //return sqrt(pow(pdop / 2.0, 2.0) + pow(vdop / 2.0, 2.0) + pow(hdop / 2.0, 2.0)); +} + +pub fn as_tokens(input: String, delimiter: &'static str) -> VecDeque { + let parts = input.split(delimiter); + let mut tokens = VecDeque::new(); + for part in parts { + tokens.push_back(String::from(part)); + } + return tokens; +} + pub struct LogBE { context: String, } @@ -30,6 +46,20 @@ impl Drop for LogBE { } } +pub async fn send_command_logged( + client: &mut T, + command: &U, + context: String, +) -> Result<::Response, atat::Error> { + let _l = LogBE::new(context); + let r = client.send(command).await; + match r.as_ref() { + Ok(_) => log::info!(" OK"), // TODO: {:?}, v ? + Err(e) => log::info!(" ERROR: {:?}", e), + } + return r; +} + #[cfg(test)] mod tests { use super::*; From 05be2079bf626c4df3b8de78241b3265a8427def Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Tue, 6 Jan 2026 22:04:04 +0100 Subject: [PATCH 2/5] accuracy --- pico/README.md | 1 + pico/pico-lib/src/gps.rs | 4 ++-- pico/pico-lib/src/utils.rs | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pico/README.md b/pico/README.md index 29260f8..101376c 100644 --- a/pico/README.md +++ b/pico/README.md @@ -7,4 +7,5 @@ This project uses [atat](https://github.com/FactbirdHQ/atat/) a no_std crate for - [SIM800 Series AT Command Manual V1.11.pdf](https://www.waveshare.com/wiki/File:SIM800_Series_AT_Command_Manual_V1.11.pdf) - [SIM868_Series_GNSS_Application_Note_V1.02.pdf](https://www.waveshare.com/wiki/File:SIM868_Series_GNSS_Application_Note_V1.02.pdf) - [SIM800_Series_GSM_Location_Application_Note_V1.03.pdf](https://www.waveshare.com/wiki/File:SIM800_Series_GSM_Location_Application_Note_V1.03.pdf) +- [SIM868_Series_Hardware_Design_V1.07.pdf](https://www.waveshare.com/wiki/File:SIM868_Series_Hardware_Design_V1.07.pdf) - [Sim868](https://www.simcom.com/product/SIM868.html) diff --git a/pico/pico-lib/src/gps.rs b/pico/pico-lib/src/gps.rs index 813c255..f217d0b 100644 --- a/pico/pico-lib/src/gps.rs +++ b/pico/pico-lib/src/gps.rs @@ -256,7 +256,7 @@ pub async fn get_gps_location location = Some(location::Location { latitude: resp.latitude, longitude: resp.longitude, - accuracy: utils::estimate_gps_accuracy(resp.hdop, resp.vdop, resp.pdop), + accuracy: utils::estimate_gps_accuracy(resp.pdop), timestamp: 0, }); break; @@ -377,7 +377,7 @@ mod tests { location::Location { latitude: 46.7624859, longitude: 18.6304591, - accuracy: 500.0, + accuracy: 5.75, timestamp: 0, }, loc1.unwrap() diff --git a/pico/pico-lib/src/utils.rs b/pico/pico-lib/src/utils.rs index 4ab7416..f3ee70a 100644 --- a/pico/pico-lib/src/utils.rs +++ b/pico/pico-lib/src/utils.rs @@ -14,10 +14,10 @@ pub fn get_distance_in_meters(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 return earth_radius_in_meters * c; } -// todo research, how to do this -pub fn estimate_gps_accuracy(_pdop: f64, _vdop: f64, _hdop: f64) -> f64 { - return 500.0; - //return sqrt(pow(pdop / 2.0, 2.0) + pow(vdop / 2.0, 2.0) + pow(hdop / 2.0, 2.0)); +// https://gis.stackexchange.com/questions/111004/translating-hdop-pdop-and-vdop-to-metric-accuracy-from-given-nmea-strings +pub fn estimate_gps_accuracy(pdop: f64) -> f64 { + // Accuracy 2.5m CEP (circular error probable) + return 2.5 * pdop; } pub fn as_tokens(input: String, delimiter: &'static str) -> VecDeque { From 989e13f369950a464c8c536930622f07cc8336d8 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Wed, 7 Jan 2026 20:16:17 +0100 Subject: [PATCH 3/5] parse date time and 1 sec delay between polls --- pico/pico-lib/src/gps.rs | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/pico/pico-lib/src/gps.rs b/pico/pico-lib/src/gps.rs index f217d0b..f723d11 100644 --- a/pico/pico-lib/src/gps.rs +++ b/pico/pico-lib/src/gps.rs @@ -8,6 +8,8 @@ use atat::atat_derive::AtatCmd; use atat::atat_derive::AtatEnum; use atat::atat_derive::AtatResp; use atat::heapless::String; +use fasttime::Date; +use fasttime::DateTime; use crate::at::NoResponse; use crate::location; @@ -230,7 +232,7 @@ pub enum FixStatus { pub async fn get_gps_location( client: &mut T, - _pico: &mut U, + pico: &mut U, max_retries: u8, ) -> Option { send_command_logged( @@ -243,6 +245,8 @@ pub async fn get_gps_location .await .ok(); + // TODO defer { AtGnssPowerControlWrite::TurnOff }; would be better + let mut location: Option = None; for i in 0..max_retries { match send_command_logged( @@ -253,16 +257,43 @@ pub async fn get_gps_location .await { Ok(resp) => { + if resp.utc_date_time.len() != 18 { + continue; + } + + let (year, rest) = resp.utc_date_time.as_str().split_at(4); + let (month, rest) = rest.split_at(2); + let (day, rest) = rest.split_at(2); + let (hour, rest) = rest.split_at(2); + let (minute, rest) = rest.split_at(2); + let (second, rest) = rest.split_at(2); + let (_, millis) = rest.split_at(1); + + let datetime = DateTime { + date: Date { + year: year.parse().unwrap_or_default(), + month: month.parse().unwrap_or_default(), + day: day.parse().unwrap_or_default(), + }, + time: fasttime::Time { + hour: hour.parse().unwrap_or_default(), + minute: minute.parse().unwrap_or_default(), + second: second.parse().unwrap_or_default(), + nanosecond: millis.parse::().unwrap_or_default() * 1_000_000u32, + }, + }; + location = Some(location::Location { latitude: resp.latitude, longitude: resp.longitude, accuracy: utils::estimate_gps_accuracy(resp.pdop), - timestamp: 0, + timestamp: (datetime.unix_timestamp_nanos() / 1_000_000) as i64, }); break; } Err(_) => (), } + pico.sleep(1000).await; } send_command_logged( @@ -323,7 +354,7 @@ mod tests { assert_eq!( atat::Error::Parse, - cmd.parse(Ok(b"+CGNSINF: 1,1,20221212120221.000,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,f,,,51,,\r\n")).err().unwrap(), + cmd.parse(Ok(b"+CGNSINF: 1,1,20221212120221.123,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,f,,,51,,\r\n")).err().unwrap(), ); assert_eq!( @@ -350,7 +381,7 @@ mod tests { hpa: None, vpa: None, }, - cmd.parse(Ok(b"+CGNSINF: 1,1,20221212120221.000,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,6,,,51,,\r\n")).unwrap() + cmd.parse(Ok(b"+CGNSINF: 1,1,20221212120221.123,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,6,,,51,,\r\n")).unwrap() ); } @@ -362,7 +393,7 @@ mod tests { client.results.push_back(Ok("".as_bytes())); // Turn On client.results.push_back(Ok("+CGNSINF: ,,,,".as_bytes())); // error client.results.push_back(Ok("+CGNSINF: ,,,,".as_bytes())); // error - client.results.push_back(Ok("+CGNSINF: 1,1,20221212120221.000,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,6,,,51,,".as_bytes())); // location + client.results.push_back(Ok("+CGNSINF: 1,1,20221212120221.123,46.7624859,18.6304591,329.218,2.20,285.8,1,,2.1,2.3,0.9,,7,6,,,51,,".as_bytes())); // location client.results.push_back(Ok("".as_bytes())); // Turn off let mut pico = crate::at::tests::PicoMock::default(); @@ -378,9 +409,12 @@ mod tests { latitude: 46.7624859, longitude: 18.6304591, accuracy: 5.75, - timestamp: 0, + timestamp: 1670846541123, }, loc1.unwrap() ); + assert_eq!(2, pico.sleep_calls.len()); + assert_eq!(1000, *pico.sleep_calls.get(0).unwrap()); + assert_eq!(1000, *pico.sleep_calls.get(1).unwrap()); } } From b64a64c262848d17a73a409ca5b4951f42719e47 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Wed, 7 Jan 2026 20:58:06 +0100 Subject: [PATCH 4/5] fix test run --- pico/pico-lib/src/gps.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico/pico-lib/src/gps.rs b/pico/pico-lib/src/gps.rs index f723d11..7c86b94 100644 --- a/pico/pico-lib/src/gps.rs +++ b/pico/pico-lib/src/gps.rs @@ -361,7 +361,7 @@ mod tests { GnssNavigationInformationResponse { gnss_run_status: GNSSRunStatus::On, fix_status: FixStatus::FixedPosition, - utc_date_time: serde_at::from_slice(b"20221212120221.000").unwrap(), + utc_date_time: serde_at::from_slice(b"20221212120221.123").unwrap(), latitude: 46.7624859, longitude: 18.6304591, msl_altitude: 329.218, From 0e1d0a6dc6a3e03e4c9ca20d0ee37f0a50827123 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Thu, 8 Jan 2026 17:43:00 +0100 Subject: [PATCH 5/5] gsm location --- pico/app/src/main.rs | 3 + pico/pico-lib/src/at.rs | 4 +- pico/pico-lib/src/battery.rs | 1 - pico/pico-lib/src/call.rs | 3 - pico/pico-lib/src/gps.rs | 6 +- pico/pico-lib/src/gsm.rs | 606 +++++++++++++++++++++++++++++++++++ pico/pico-lib/src/lib.rs | 1 + pico/pico-lib/src/network.rs | 6 - pico/pico-lib/src/sms.rs | 2 - pico/pico-lib/src/urc.rs | 10 + 10 files changed, 625 insertions(+), 17 deletions(-) create mode 100644 pico/pico-lib/src/gsm.rs diff --git a/pico/app/src/main.rs b/pico/app/src/main.rs index 881fb9f..300e840 100644 --- a/pico/app/src/main.rs +++ b/pico/app/src/main.rs @@ -229,6 +229,9 @@ async fn urc_handler_task( urc::Urc::SMSReady => { log::info!("URC SMSReady"); } + urc::Urc::SetBearer(v) => { + log::info!("SetBearer {:?}", v); + } }, pubsub::WaitResult::Lagged(b) => { log::info!("Urc Lagged messages: {}", b); diff --git a/pico/pico-lib/src/at.rs b/pico/pico-lib/src/at.rs index 1dba20c..4baa8f1 100644 --- a/pico/pico-lib/src/at.rs +++ b/pico/pico-lib/src/at.rs @@ -36,9 +36,9 @@ pub mod tests { $( #[test] fn $name() { - let (cmd, len, text) = $value; + let (cmd, text) = $value; let mut buffer = crate::at::tests::zeros(); - assert_eq!(len, cmd.write(&mut buffer)); + assert_eq!(text.len(), cmd.write(&mut buffer)); assert_eq!( String::from_utf8(buffer) .unwrap() diff --git a/pico/pico-lib/src/battery.rs b/pico/pico-lib/src/battery.rs index b998cc7..abb01cf 100644 --- a/pico/pico-lib/src/battery.rs +++ b/pico/pico-lib/src/battery.rs @@ -37,7 +37,6 @@ mod tests { cmd_serialization_tests! { test_at_battery_charge_execute: ( AtBatteryChargeExecute, - 7, "AT+CBC\r", ), } diff --git a/pico/pico-lib/src/call.rs b/pico/pico-lib/src/call.rs index 6d22ae1..4a6d7cb 100644 --- a/pico/pico-lib/src/call.rs +++ b/pico/pico-lib/src/call.rs @@ -104,19 +104,16 @@ mod tests { AtSwapAudioChannelsWrite { n: AudioChannels::Main, }, - 10, "AT+CHFA=1\r", ), test_at_dial_number: ( AtDialNumber { number: String::try_from("+361234567").unwrap(), }, - 17, "ATD+361234567,i;\r", ), test_at_hangup: ( AtHangup, - 9, "AT+CHUP;\r", ), } diff --git a/pico/pico-lib/src/gps.rs b/pico/pico-lib/src/gps.rs index 7c86b94..7a9f721 100644 --- a/pico/pico-lib/src/gps.rs +++ b/pico/pico-lib/src/gps.rs @@ -257,6 +257,7 @@ pub async fn get_gps_location .await { Ok(resp) => { + log::info!(" OK {:?}", resp); if resp.utc_date_time.len() != 18 { continue; } @@ -325,19 +326,16 @@ mod tests { AtGnssPowerControlWrite { mode: PowerMode::TurnOn, }, - 13, "AT+CGNSPWR=1\r", ), test_at_gnss_power_control_off: ( AtGnssPowerControlWrite { mode: PowerMode::TurnOff, }, - 13, "AT+CGNSPWR=0\r", ), test_at_gnss_navigation_information_execute: ( AtGnssNavigationInformationExecute, - 11, "AT+CGNSINF\r", ), } @@ -417,4 +415,6 @@ mod tests { assert_eq!(1000, *pico.sleep_calls.get(0).unwrap()); assert_eq!(1000, *pico.sleep_calls.get(1).unwrap()); } + + // TODO test error handling } diff --git a/pico/pico-lib/src/gsm.rs b/pico/pico-lib/src/gsm.rs new file mode 100644 index 0000000..d16473d --- /dev/null +++ b/pico/pico-lib/src/gsm.rs @@ -0,0 +1,606 @@ +use alloc::format; +use alloc::string::ToString; +use atat::atat_derive::AtatCmd; +use atat::atat_derive::AtatEnum; +use atat::atat_derive::AtatResp; +use atat::heapless::String; +use atat::heapless_bytes::Bytes; +use fasttime::Date; +use fasttime::DateTime; + +use crate::at::NoResponse; +use crate::location; +use crate::utils::send_command_logged; + +// 7.2.1 AT+CGATT Attach or Detach from GPRS Service +// AT+CGATT=[] +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CGATT", NoResponse, timeout_ms = 7500)] +pub struct AtAttachGPRS { + pub state: AttachState, +} + +#[derive(Debug, Clone, PartialEq, AtatEnum)] +pub enum AttachState { + Detach = 0, + Attach = 1, +} + +// 8.2.9 AT+CSTT Start Task and Set APN, USER NAME, PASSWORD +// AT+CSTT=[,[,]] +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CSTT", NoResponse)] +pub struct AtSetApnWrite { + pub apn: String<50>, + pub user_name: Option>, + pub password: Option>, +} + +// 8.2.10 AT+CIICR Bring up Wireless Connection with GPRS or CSD +// AT+CIICR +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CIICR", NoResponse, timeout_ms = 85000)] +pub struct AtBringUpWirelessConnectionExecute; + +// 8.2.11 AT+CIFSR Get Local IP Address +// AT+CIFSR +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CIFSR", GetLocalIPAddressResponse)] +pub struct AtGetLocalIPAddressExecute; + +#[derive(Debug, Clone, AtatResp, PartialEq, Default)] +pub struct GetLocalIPAddressResponse { + pub address: String<50>, +} + +// 9.2.1 AT+SAPBR Bearer Settings for Applications Based on IP +// AT+SAPBR=,[,,] +// if == 2 (QueryBearer) +// +SAPBR: ,, +// if == 4 (GetBearerParameters) +// +SAPBR: , +// Only type 1 (OpenBearer), type 0 (CloseBearer) and type 3 (SetBearerParameter) is used, so ok with NoResponse +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+SAPBR", NoResponse, timeout_ms = 85000)] // 85 sec for 1 (OpenBearer), 65 sec for 0 (CloseBearer) +#[rustfmt::skip] +pub struct AtSetBearerWrite { + pub cmd_type: CmdType, + pub cid: u8, // connection id + pub con_param_tag: Option>, // CONTYPE, APN, USER, PWD, PHONENUM, RATE + pub con_param_value: Option>, // CSD/GPRS len(64) len(32) len(32) for CSD for CSD (2400, 4800, 9600, 14400) +} + +#[derive(Debug, Clone, PartialEq, AtatEnum)] +pub enum CmdType { + CloseBearer = 0, + OpenBearer = 1, + QueryBearer = 2, + SetBearerParameters = 3, + GetBearerParameters = 4, +} + +// SIM800_Series_GSM_Location_Application_Note_V1.03.pdf + +// 2.2 AT+CLBSCFG Base station Location Configuration +// AT+CLBSCFG=,[,] +// NoResponse for Operate 1 (Set) +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CLBSCFG", NoResponse)] +pub struct AtBaseStationLocationConfWrite { + pub operate: Operate, + pub para: Para, + pub value: Option>, +} + +#[derive(Debug, Clone, PartialEq, AtatEnum)] +pub enum Operate { + Read = 0, + Set = 1, +} + +#[derive(Debug, Clone, PartialEq, AtatEnum, Default)] +pub enum Para { + #[default] + CustomerID = 0, + TimesHaveUsedPositioningCommand = 1, + ServerAddress = 3, // lbs-simcom.com:3001 lbs-simcom.com:3000 lbs-simcom.com:3002 (default, free) +} + +// 2.1 AT+CLBS Base station Location +// AT+CLBS=,,[[,],[]] +#[derive(Clone, Debug, AtatCmd)] +#[at_cmd("+CLBS", BaseStationLocationResponseType4)] // NOTE: only type 4 response is implemented +pub struct AtBaseStationLocationWrite { + pub type_: LocationType, + pub cid: u8, + pub longitude: Option, + pub latitude: Option, + pub lon_type: Option, +} + +#[derive(Debug, Clone, PartialEq, AtatEnum, Default)] +pub enum LocationType { + #[default] + Use3Cell = 1, + GetAccessTimes = 3, + GetLongLatDateTime = 4, + ReportPositionError = 9, +} + +#[derive(Debug, Clone, AtatResp, PartialEq, Default)] +// +CLBS: [,,,,,