diff --git a/pico/README.md b/pico/README.md index 35ae423..101376c 100644 --- a/pico/README.md +++ b/pico/README.md @@ -5,4 +5,7 @@ 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_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/app/src/main.rs b/pico/app/src/main.rs index 32735e5..300e840 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( @@ -227,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 d85cd46..4a6d7cb 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)] @@ -114,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 new file mode 100644 index 0000000..7a9f721 --- /dev/null +++ b/pico/pico-lib/src/gps.rs @@ -0,0 +1,420 @@ +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 fasttime::Date; +use fasttime::DateTime; + +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(); + + // TODO defer { AtGnssPowerControlWrite::TurnOff }; would be better + + let mut location: Option = None; + for i in 0..max_retries { + match send_command_logged( + client, + &AtGnssNavigationInformationExecute, + format!("AtGnssNavigationInformationExecute {}", i), + ) + .await + { + Ok(resp) => { + log::info!(" 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: (datetime.unix_timestamp_nanos() / 1_000_000) as i64, + }); + break; + } + Err(_) => (), + } + pico.sleep(1000).await; + } + + 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, + }, + "AT+CGNSPWR=1\r", + ), + test_at_gnss_power_control_off: ( + AtGnssPowerControlWrite { + mode: PowerMode::TurnOff, + }, + "AT+CGNSPWR=0\r", + ), + test_at_gnss_navigation_information_execute: ( + AtGnssNavigationInformationExecute, + "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.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!( + GnssNavigationInformationResponse { + gnss_run_status: GNSSRunStatus::On, + fix_status: FixStatus::FixedPosition, + utc_date_time: serde_at::from_slice(b"20221212120221.123").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.123,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.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(); + 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: 5.75, + 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()); + } + + // 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: [,,,,,