From d677c043c340e55eab3ab788e4c9133a5e693712 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Tue, 13 Jan 2026 17:20:53 +0100 Subject: [PATCH] project: sms.rs fixes. --- pico/app/src/main.rs | 15 +-- pico/pico-lib/src/call.rs | 17 +++- pico/pico-lib/src/gps.rs | 46 +-------- pico/pico-lib/src/sms.rs | 196 ++++++++++++++++++++++++++++++------- pico/pico-lib/src/utils.rs | 49 ++++++++++ 5 files changed, 233 insertions(+), 90 deletions(-) diff --git a/pico/app/src/main.rs b/pico/app/src/main.rs index 20f6a24..d4aced8 100644 --- a/pico/app/src/main.rs +++ b/pico/app/src/main.rs @@ -3,6 +3,7 @@ use alloc::string::ToString; use atat::asynch::Client; +use atat::heapless::String; use atat::{AtatIngress, DefaultDigester, Ingress, ResponseSlot, UrcChannel}; use core::ptr::addr_of_mut; use defmt::*; @@ -23,7 +24,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::send_command_logged; +use pico_lib::utils::{astring_to_string, send_command_logged}; use pico_lib::{at, battery, call, gps, gsm, network, sms}; extern crate alloc; @@ -131,7 +132,6 @@ async fn main(spawner: Spawner) { info!("After spawning Urc Task"); Timer::after(Duration::from_secs(2)).await; - pico.restart_module().await; info!("Network init"); Timer::after(Duration::from_secs(2)).await; @@ -167,21 +167,24 @@ async fn main(spawner: Spawner) { None => (), } - const PHONE_NUMBER: &'static str = "+36301234567"; + let phone_number: String<30> = String::try_from("+36301234567").unwrap(); call::call_number( &mut client, &mut pico, - PHONE_NUMBER, + &phone_number, Duration::from_secs(10).as_millis(), ) .await; + let mut tata_response: String<160> = String::try_from("$tATA/").unwrap(); + let _ = tata_response.push_str(dumped.as_str()); + sms::send_sms( &mut client, &mut pico, - PHONE_NUMBER, - "this is a text message", + &phone_number, + &astring_to_string(tata_response.as_str()), ) .await; diff --git a/pico/pico-lib/src/call.rs b/pico/pico-lib/src/call.rs index 4fcb577..abc0c33 100644 --- a/pico/pico-lib/src/call.rs +++ b/pico/pico-lib/src/call.rs @@ -32,7 +32,7 @@ pub enum AudioChannels { // ATD+36301234567,i; <- call this number and i: Deactivates CLIR (Enable presentation of own number to called party) #[derive(Clone, Debug)] pub struct AtDialNumber { - pub number: String<16>, + pub number: String<30>, } impl<'a> AtatCmd for AtDialNumber { @@ -104,6 +104,9 @@ pub async fn init( client: &mut T, _pico: &mut U, ) { + // Note, this is NO_SAVE, and by default is enabled on my device. + // It takes a bit of time, but having an extra Read command messes + // up the URC handling. send_command_logged( client, &AtCallingLineIdentificationPresentationWrite { @@ -118,7 +121,7 @@ pub async fn init( pub async fn call_number( client: &mut T, pico: &mut U, - number: &'static str, + number: &String<30>, duration_millis: u64, ) { send_command_logged( @@ -134,7 +137,7 @@ pub async fn call_number( send_command_logged( client, &AtDialNumber { - number: String::<16>::try_from(number).unwrap(), + number: number.clone(), }, "AtSwapAudioChannelsAtDialNumberWrite".to_string(), ) @@ -224,7 +227,13 @@ mod tests { client.results.push_back(Ok("".as_bytes())); let mut pico = crate::at::tests::PicoMock::default(); - call_number(&mut client, &mut pico, "+36301234567", 100).await; + call_number( + &mut client, + &mut pico, + &String::try_from("+36301234567").unwrap(), + 100, + ) + .await; assert_eq!(3, client.sent_commands.len()); assert_eq!("AT+CHFA=1\r", client.sent_commands.get(0).unwrap()); assert_eq!("ATD+36301234567,i;\r", client.sent_commands.get(1).unwrap()); diff --git a/pico/pico-lib/src/gps.rs b/pico/pico-lib/src/gps.rs index f215ad5..c26000f 100644 --- a/pico/pico-lib/src/gps.rs +++ b/pico/pico-lib/src/gps.rs @@ -2,10 +2,6 @@ use atat::serde_at; use defmt::Format; use defmt::debug; -use core::num::ParseFloatError; -use core::num::ParseIntError; -use core::str::Utf8Error; - use alloc::format; use alloc::string::ToString; use atat::atat_derive::AtatCmd; @@ -18,6 +14,7 @@ use fasttime::DateTime; use crate::at::NoResponse; use crate::location; use crate::utils; +use crate::utils::AtatError; use crate::utils::as_tokens; use crate::utils::bytes_to_string; use crate::utils::send_command_logged; @@ -92,47 +89,6 @@ pub struct GnssNavigationInformationResponse { // Length Format 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) - } -} - -impl From for AtatError { - fn from(_: atat::serde_at::de::Error) -> Self { - AtatError(atat::Error::Parse) - } -} - fn parse_gnss_navigation_information( response: &[u8], ) -> Result { diff --git a/pico/pico-lib/src/sms.rs b/pico/pico-lib/src/sms.rs index 6e5ac2a..d666b63 100644 --- a/pico/pico-lib/src/sms.rs +++ b/pico/pico-lib/src/sms.rs @@ -1,15 +1,16 @@ use defmt::Format; -use alloc::format; use alloc::string::ToString; use atat::AtatCmd; 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 defmt::info; use crate::at::NoResponse; +use crate::utils::AtatError; use crate::utils::send_command_logged; // 4.2.2 AT+CMGF Select SMS Message Format @@ -28,31 +29,61 @@ pub enum MessageMode { // 4.2.5 AT+CMGS Send SMS Message // AT+CMGS=[,[]]text is entered[ctrl-Z/ESC] +#[derive(Clone, Debug, Format, AtatCmd)] +#[at_cmd("+CMGS", SMSMessageResponse, timeout_ms = 5000)] +pub struct AtSMSSend { + pub number: String<30>, +} #[derive(Clone, Debug)] -pub struct AtSendSMSWrite { - pub number: String<16>, +pub struct AtSMSData { pub message: String<160>, } -impl<'a> AtatCmd for AtSendSMSWrite { - type Response = NoResponse; +impl<'a> AtatCmd for AtSMSData { + type Response = SMSDataResponse; - const MAX_LEN: usize = 16; + const MAX_LEN: usize = 160; + const MAX_TIMEOUT_MS: u32 = 60000; // TODO this is not working! fn write(&self, buf: &mut [u8]) -> usize { - let formatted = format!("AT+CMGS={}\r{}\x1a", self.number, self.message); - let cmd = formatted.as_bytes(); - let len = cmd.len(); - buf[..len].copy_from_slice(cmd); - len + let bytes = self.message.as_bytes(); + let len = bytes.len(); + let ctrl_z = b"\x1a"; + buf[..len].copy_from_slice(bytes); + buf[len..len + ctrl_z.len()].copy_from_slice(ctrl_z); + len + ctrl_z.len() } - fn parse(&self, _: Result<&[u8], atat::InternalError>) -> Result { - Ok(NoResponse) + fn parse( + &self, + resp: Result<&[u8], atat::InternalError>, + ) -> Result { + // TODO, deserialize with serde_at ? + match resp { + Ok(v) => { + let h = || -> Result { + let s = core::str::from_utf8(&v["+CMGS: ".len()..])?; + let mr: i32 = s.parse()?; + Ok(mr) + }; + + match h() { + Ok(v) => Ok(SMSDataResponse { mr: v }), + Err(_) => Err(atat::Error::Parse), + } + } + Err(_) => Err(atat::Error::Parse), + } } } +// +CMGS: +#[derive(Debug, Clone, AtatResp, PartialEq, Default)] +pub struct SMSDataResponse { + mr: i32, // GSM 03.40 TP-Message-Reference in integer format +} + // 4.2.3 AT+CMGL List SMS Messages from Preferred Store // AT+CMGL=[,] // Not Implemented, Read SMS one by one will be enough for this project @@ -81,7 +112,21 @@ pub struct SMSMessageResponse { sn: String<30>, mid: Option>, date_time: String<30>, - message: String<256>, + // Note the message is not in UTF-8. + // + // 3.2.12 AT+CSCS Select TE Character Set + // "GSM" 7-bit, "UCS2", "IRA", "HEX", "PCCP", "PCDN", "8859-1" + // + // The default is IRA on my module. A hungarian text was sent in seemingly + // ISO 8859-1 encoding, at least the following did work on it: + // fn latin1_to_string(s: &[u8]) -> String { + // s.iter().map(|&c| c as char).collect() + // } + // + // Some text arrives in UCS2 hex encoded, see the tests. + // + // For tATA, no proper decoding is needed. + message: Bytes<512>, } // 4.2.8 AT+CNMI New SMS Message Indications @@ -164,31 +209,39 @@ pub async fn init( .ok(); } -// TODO this is not working yet, need to debug pub async fn send_sms( client: &mut T, - _pico: &mut U, // TODO: - number: &'static str, // Bytes<16> ? (same for Call) - message: &'static str, // Bytes<160> ? + _pico: &mut U, + number: &String<30>, + message: &String<160>, ) { send_command_logged( client, - &AtSendSMSWrite { - number: String::<16>::try_from(number).unwrap(), - message: String::<160>::try_from(message).unwrap(), + &AtSMSSend { + number: number.clone(), }, - "AtSendSMSWrite".to_string(), + "AtSMSSend".to_string(), + ) + .await + .ok(); + send_command_logged( + client, + &AtSMSData { + message: message.clone(), + }, + "AtSMSData".to_string(), ) .await .ok(); } -// todo, temporary helper +// TODO, this is just a temporary helper function. pub async fn receive_sms( client: &mut T, - _pico: &mut U, + pico: &mut U, ) { - for i in 1..1000 { + for i in 1..100 { + pico.sleep(100).await; match send_command_logged( client, &AtReadSMSMessagesWrite { @@ -200,9 +253,13 @@ pub async fn receive_sms( .await { Ok(v) => { + use atat::nom::AsBytes; info!( "SMS RESP state={} date={} sender={} message={}", - v.stat, v.date_time, v.sn, v.message + v.stat, + v.date_time, + v.sn, + v.message.as_bytes() ); } Err(_) => break, @@ -215,7 +272,7 @@ mod tests { use crate::cmd_serialization_tests; use super::*; - use atat::AtatCmd; + use atat::{AtatCmd, serde_at}; cmd_serialization_tests! { test_at_select_sms_message_format_write: ( @@ -224,12 +281,17 @@ mod tests { }, "AT+CMGF=1\r", ), - test_at_send_sms_write: ( - AtSendSMSWrite { + test_at_send_sms_1: ( + AtSMSSend { number: String::try_from("+361234567").unwrap(), - message: String::try_from("this is the message content").unwrap(), }, - "AT+CMGS=+361234567\rthis is the message content\u{1a}", + "AT+CMGS=\"+361234567\"\r", + ), + test_at_send_sms_2: ( + AtSMSData { + message: String::try_from("this is the message").unwrap(), + }, + "this is the message\x1a", ), test_at_new_sms_message_indications_write: ( AtNewSMSMessageIndicationsWrite { @@ -266,6 +328,65 @@ mod tests { ); } + #[test] + fn test_send_sms_response() { + let cmd = AtSMSData { + message: String::try_from("data").unwrap(), + }; + + assert_eq!( + SMSDataResponse { mr: 13 }, + cmd.parse(Ok(b"+CMGS: 13")).unwrap(), + ); + } + + #[test] + fn test_sms_response() { + let cmd = AtReadSMSMessagesWrite { + index: 0, + mode: None, + }; + + assert_eq!( + SMSMessageResponse { + stat: String::try_from("REC READ").unwrap(), + sn: String::try_from("+36301234567").unwrap(), + mid: Some(String::new()), + date_time: String::try_from("25/04/25,10:37:39+08").unwrap(), + message: serde_at::from_slice(b"$tATA/location/12345").unwrap(), + }, + cmd.parse(Ok(b"+CMGR: \"REC READ\",\"+36301234567\",\"\",\"25/04/25,10:37:39+08\"\r\n$tATA/location/12345\r\n")) + .unwrap(), + ); + + assert_eq!( + SMSMessageResponse { + stat: String::try_from("REC READ").unwrap(), + sn: String::try_from("+36301234567").unwrap(), + mid: Some(String::new()), + date_time: String::try_from("25/04/25,10:37:39+08").unwrap(), + message: serde_at::from_slice(b"\xdcdv\xf6zlettel: Yettel").unwrap(), + }, + // seems like this is ISO 8859-1 encoded + cmd.parse(Ok(b"+CMGR: \"REC READ\",\"+36301234567\",\"\",\"25/04/25,10:37:39+08\"\r\n\xdcdv\xf6zlettel: Yettel\r\n")) + .unwrap(), + ); + + assert_eq!( + SMSMessageResponse { + stat: String::try_from("REC READ").unwrap(), + sn: String::try_from("+36301234567").unwrap(), + mid: Some(String::new()), + date_time: String::try_from("25/04/25,10:37:39+08").unwrap(), + message: serde_at::from_slice(b"n00540061006D00E100730020004400F6006D0151006B0020D83DDE0E").unwrap(), + }, + // seems like this is UCS2 character strings are converted to hexadecimal numbers from 0000 to FFFF + // the text was: Tamás Dömők :cool smiley + cmd.parse(Ok(b"+CMGR: \"REC READ\",\"+36301234567\",\"\",\"25/04/25,10:37:39+08\"\r\nn00540061006D00E100730020004400F6006D0151006B0020D83DDE0E\r\n")) + .unwrap(), + ); + } + #[tokio::test] async fn test_sms_init() { let mut client = crate::at::tests::ClientMock::default(); @@ -283,20 +404,25 @@ mod tests { async fn test_send_sms() { let mut client = crate::at::tests::ClientMock::default(); client.results.push_back(Ok(">".as_bytes())); + client.results.push_back(Ok("+CMGS: 1".as_bytes())); let mut pico = crate::at::tests::PicoMock::default(); send_sms( &mut client, &mut pico, - "+36301234567", - "this is the text message", + &String::try_from("+36301234567").unwrap(), + &String::try_from("this is the text message").unwrap(), ) .await; - assert_eq!(1, client.sent_commands.len()); + assert_eq!(2, client.sent_commands.len()); assert_eq!( - "AT+CMGS=+36301234567\rthis is the text message\u{1a}", + "AT+CMGS=\"+36301234567\"\r", client.sent_commands.get(0).unwrap() ); + assert_eq!( + "this is the text message\x1a", + client.sent_commands.get(1).unwrap() + ); } #[tokio::test] diff --git a/pico/pico-lib/src/utils.rs b/pico/pico-lib/src/utils.rs index 5477d74..c9a4dbf 100644 --- a/pico/pico-lib/src/utils.rs +++ b/pico/pico-lib/src/utils.rs @@ -1,5 +1,9 @@ use defmt::info; +use core::num::ParseFloatError; +use core::num::ParseIntError; +use core::str::Utf8Error; + use alloc::collections::vec_deque::VecDeque; use alloc::string::String; use libm::{asin, cos, pow, sin, sqrt}; @@ -41,6 +45,51 @@ pub fn bytes_to_string( return atat::heapless::String::::from_utf8(data).unwrap(); } +pub fn astring_to_string<'a, const N: usize>(string: &'a str) -> atat::heapless::String { + return atat::heapless::String::::try_from(string).unwrap(); +} + +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) + } +} + +impl From for AtatError { + fn from(_: atat::serde_at::de::Error) -> Self { + AtatError(atat::Error::Parse) + } +} + pub struct LogBE { context: String, }