From c331bd353c070df6dbcf612841610275ef617c84 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Wed, 14 Jan 2026 12:57:45 +0100 Subject: [PATCH 1/3] pico-lib: UCS2 SMS handling. --- pico/app/src/main.rs | 12 +- pico/pico-lib/src/hexstr.rs | 297 ++++++++++++++++++++++++++++++++++++ pico/pico-lib/src/lib.rs | 1 + pico/pico-lib/src/sms.rs | 101 ++++++------ 4 files changed, 354 insertions(+), 57 deletions(-) create mode 100644 pico/pico-lib/src/hexstr.rs diff --git a/pico/app/src/main.rs b/pico/app/src/main.rs index d4aced8..c847b9e 100644 --- a/pico/app/src/main.rs +++ b/pico/app/src/main.rs @@ -90,15 +90,15 @@ async fn main(spawner: Spawner) { let (tx_pin, rx_pin, uart) = (p.PIN_0, p.PIN_1, p.UART0); static INGRESS_BUF: StaticCell<[u8; INGRESS_BUF_SIZE]> = StaticCell::new(); - static TX_BUF: StaticCell<[u8; 256]> = StaticCell::new(); - static RX_BUF: StaticCell<[u8; 256]> = StaticCell::new(); + static TX_BUF: StaticCell<[u8; 512]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 512]> = StaticCell::new(); let uart = BufferedUart::new( uart, tx_pin, rx_pin, Irqs, - TX_BUF.init([0; 256]), - RX_BUF.init([0; 256]), + TX_BUF.init([0; 512]), + RX_BUF.init([0; 512]), uart::Config::default(), ); let (writer, reader) = uart.split(); @@ -111,11 +111,11 @@ async fn main(spawner: Spawner) { &RES_SLOT, &URC_CHANNEL, ); - static BUF: StaticCell<[u8; 1024]> = StaticCell::new(); + static BUF: StaticCell<[u8; 2048]> = StaticCell::new(); let mut client = Client::new( writer, &RES_SLOT, - BUF.init([0; 1024]), + BUF.init([0; 2048]), atat::Config::default(), ); diff --git a/pico/pico-lib/src/hexstr.rs b/pico/pico-lib/src/hexstr.rs new file mode 100644 index 0000000..57efa35 --- /dev/null +++ b/pico/pico-lib/src/hexstr.rs @@ -0,0 +1,297 @@ +use alloc::{fmt, format, vec::Vec}; +use atat::{ + heapless::String, + serde_at::serde::{self, Deserialize, de::Visitor}, +}; +use core::{num::ParseIntError, str::FromStr}; +use defmt::Format; + +pub fn decode_hex_u8(s: &str) -> Result, ParseIntError> { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) + .collect() +} + +pub fn decode_utf8_hex_string(v: &[u8]) -> Result, &'static str> { + let hex_str = core::str::from_utf8(&v).map_err(|_o| -> &'static str { "utf-8 error" })?; + let bytes = decode_hex_u8(&hex_str).map_err(|_o| -> &'static str { "decode_hex_u8 error" })?; + let utf8_str = + core::str::from_utf8(&bytes).map_err(|_o| -> &'static str { "from_utf8 error" })?; + String::from_str(utf8_str).map_err(|_o| -> &'static str { "from_str error" }) +} + +pub fn encode_utf8_hex_string(v: &[u8]) -> Result, &'static str> { + let mut hex_str = String::::new(); + for c in v { + let s = format!("{:02X}", c); + hex_str + .push_str(s.as_str()) + .map_err(|_o| -> &'static str { "push_str error" })?; + } + Ok(hex_str) +} + +pub fn decode_hex_u16(s: &str) -> Result, ParseIntError> { + (0..s.len()) + .step_by(4) + .map(|i| u16::from_str_radix(&s[i..i + 4], 16)) + .collect() +} + +pub fn decode_utf16_hex_string(v: &[u8]) -> Result, &'static str> { + let hex_str = core::str::from_utf8(&v).map_err(|_o| -> &'static str { "utf-8 error" })?; + let bytes = + decode_hex_u16(&hex_str).map_err(|_o| -> &'static str { "decode_hex_u16 error" })?; + let utf16_str = alloc::string::String::from_utf16(&bytes) + .map_err(|_o| -> &'static str { "from_utf16 error" })?; + String::from_str(utf16_str.as_str()).map_err(|_o| -> &'static str { "from_str error" }) +} + +pub fn encode_utf16_hex_string(v: &[u8]) -> Result, &'static str> { + let utf8_str = core::str::from_utf8(v).map_err(|_o| -> &'static str { "from_utf8 error" })?; + let s = String::::from_str(utf8_str).map_err(|_o| -> &'static str { "from_str error" })?; + let v: Vec = s.encode_utf16().collect(); + let mut hex_str = String::::new(); + for c in v { + let s = format!("{:04X}", c); + hex_str + .push_str(s.as_str()) + .map_err(|_o| -> &'static str { "push_str error" })?; + } + Ok(hex_str) +} + +struct HexStringVisitor; + +impl<'de, const N: usize> Visitor<'de> for HexStringVisitor { + type Value = (String, bool); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a hex string in utf-8 / utf-16 format") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut s = core::str::from_utf8(v).map_err(serde::de::Error::custom)?; + let mut quoted = false; + if s.starts_with('"') && s.ends_with('"') { + quoted = true; + let mut chars = s.chars(); + chars.next(); + chars.next_back(); + s = chars.as_str(); + } + + let decoded = decode_utf16_hex_string(s.as_bytes()); + match decoded { + Ok(d) => { + if d.len() > N { + return Err(serde::de::Error::custom("source string too long")); + } + return Ok((d, quoted)); + } + Err(e) => { + return Err(serde::de::Error::custom(e)); + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Default, Format)] +pub struct UCS2HexString { + pub text: String, + pub quoted: bool, +} + +impl<'de, const N: usize> Deserialize<'de> for UCS2HexString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let out = deserializer.deserialize_bytes(HexStringVisitor)?; + + Ok(Self { + text: out.0, + quoted: out.1, + }) + } +} + +impl From<&str> for UCS2HexString { + fn from(s: &str) -> Self { + Self { + text: String::::from_str(s).unwrap(), + quoted: false, + } + } +} + +impl fmt::Display for UCS2HexString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.text) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_encode_utf8_hex_string() { + assert_eq!( + String::<30>::try_from("").unwrap(), + encode_utf8_hex_string::<30>(b"").unwrap() + ); + assert_eq!( + String::<30>::try_from("3030303036").unwrap(), + encode_utf8_hex_string::<30>(b"00006").unwrap() + ); + assert_eq!( + String::<30>::try_from("2B3336333031323334353637").unwrap(), + encode_utf8_hex_string::<30>(b"+36301234567").unwrap() + ); + assert_eq!( + String::<50>::try_from("24744154412F6C6F636174696F6E2F3132333435").unwrap(), + encode_utf8_hex_string::<50>(b"$tATA/location/12345").unwrap() + ); + assert_eq!( + String::<100>::try_from( + "4BC3B6737AC3B66E6AC3BC6B2E0AC39C6476C3B67A6C657474656C2059657474656C2E" + ) + .unwrap(), + encode_utf8_hex_string::<100>( + String::<100>::try_from("Köszönjük.\nÜdvözlettel Yettel.") + .unwrap() + .as_bytes() + ) + .unwrap() + ); + assert_eq!( + String::<100>::try_from("54616DC3A1732044C3B66DC5916B20F09F988E").unwrap(), + encode_utf8_hex_string::<100>( + String::<100>::try_from("Tamás Dömők 😎") + .unwrap() + .as_bytes() + ) + .unwrap() + ); + } + + #[test] + fn test_decode_utf8_hex_string() { + assert_eq!( + String::<30>::try_from("").unwrap(), + decode_utf8_hex_string::<30>(b"").unwrap() + ); + assert_eq!( + String::<30>::try_from("00006").unwrap(), + decode_utf8_hex_string::<30>(b"3030303036").unwrap() + ); + assert_eq!( + String::<30>::try_from("+36301234567").unwrap(), + decode_utf8_hex_string::<30>(b"2B3336333031323334353637").unwrap() + ); + assert_eq!( + String::<30>::try_from("$tATA/location/12345").unwrap(), + decode_utf8_hex_string::<30>(b"24744154412F6C6F636174696F6E2F3132333435").unwrap() + ); + assert_eq!( + String::<100>::try_from("Köszönjük.\nÜdvözlettel Yettel.").unwrap(), + decode_utf8_hex_string::<100>( + b"4BC3B6737AC3B66E6AC3BC6B2E0AC39C6476C3B67A6C657474656C2059657474656C2E" + ) + .unwrap() + ); + assert_eq!( + String::<100>::try_from("Tamás Dömők 😎").unwrap(), + decode_utf8_hex_string::<100>(b"54616DC3A1732044C3B66DC5916B20F09F988E").unwrap() + ); + } + + #[test] + fn test_encode_utf16_hex_string() { + assert_eq!( + String::<100>::try_from("").unwrap(), + encode_utf16_hex_string::<100>(b"").unwrap() + ); + assert_eq!( + String::<100>::try_from("00300030003000300036").unwrap(), + encode_utf16_hex_string::<100>(b"00006").unwrap() + ); + assert_eq!( + String::<100>::try_from("002B00330036003300300031003200330034003500360037").unwrap(), + encode_utf16_hex_string::<100>(b"+36301234567").unwrap() + ); + assert_eq!( + String::<100>::try_from( + "00240074004100540041002F006C006F0063006100740069006F006E002F00310032003300340035" + ) + .unwrap(), + encode_utf16_hex_string::<100>(b"$tATA/location/12345").unwrap() + ); + assert_eq!( + String::<500>::try_from( + "004B00F60073007A00F6006E006A00FC006B002E000A00DC0064007600F6007A006C0065007400740065006C002000590065007400740065006C002E" + ) + .unwrap(), + encode_utf16_hex_string::<500>( + String::<500>::try_from("Köszönjük.\nÜdvözlettel Yettel.") + .unwrap() + .as_bytes() + ) + .unwrap() + ); + assert_eq!( + String::<500>::try_from("00540061006D00E100730020004400F6006D0151006B0020D83DDE0E") + .unwrap(), + encode_utf16_hex_string::<500>( + String::<500>::try_from("Tamás Dömők 😎") + .unwrap() + .as_bytes() + ) + .unwrap() + ); + } + + #[test] + fn test_decode_utf16_hex_string() { + assert_eq!( + String::<500>::try_from("").unwrap(), + decode_utf16_hex_string::<30>(b"").unwrap() + ); + assert_eq!( + String::<500>::try_from("00006").unwrap(), + decode_utf16_hex_string::<500>(b"00300030003000300036").unwrap() + ); + assert_eq!( + String::<500>::try_from("+36301234567").unwrap(), + decode_utf16_hex_string::<500>(b"002B00330036003300300031003200330034003500360037") + .unwrap() + ); + assert_eq!( + String::<500>::try_from("$tATA/location/12345").unwrap(), + decode_utf16_hex_string::<500>( + b"00240074004100540041002F006C006F0063006100740069006F006E002F00310032003300340035" + ) + .unwrap() + ); + assert_eq!( + String::<500>::try_from("Köszönjük.\nÜdvözlettel Yettel.").unwrap(), + decode_utf16_hex_string::<500>( + b"004B00F60073007A00F6006E006A00FC006B002E000A00DC0064007600F6007A006C0065007400740065006C002000590065007400740065006C002E" + ) + .unwrap() + ); + assert_eq!( + String::<500>::try_from("Tamás Dömők 😎").unwrap(), + decode_utf16_hex_string::<500>( + b"00540061006D00E100730020004400F6006D0151006B0020D83DDE0E" + ) + .unwrap() + ); + } +} diff --git a/pico/pico-lib/src/lib.rs b/pico/pico-lib/src/lib.rs index baa9e22..5d1a54c 100644 --- a/pico/pico-lib/src/lib.rs +++ b/pico/pico-lib/src/lib.rs @@ -7,6 +7,7 @@ pub mod battery; pub mod call; pub mod gps; pub mod gsm; +pub mod hexstr; pub mod location; pub mod network; pub mod poro; diff --git a/pico/pico-lib/src/sms.rs b/pico/pico-lib/src/sms.rs index d666b63..6cda105 100644 --- a/pico/pico-lib/src/sms.rs +++ b/pico/pico-lib/src/sms.rs @@ -6,11 +6,10 @@ 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::hexstr::UCS2HexString; use crate::utils::send_command_logged; // 4.2.2 AT+CMGF Select SMS Message Format @@ -45,7 +44,6 @@ impl<'a> AtatCmd for AtSMSData { const MAX_LEN: usize = 160; const MAX_TIMEOUT_MS: u32 = 60000; - // TODO this is not working! fn write(&self, buf: &mut [u8]) -> usize { let bytes = self.message.as_bytes(); let len = bytes.len(); @@ -59,19 +57,14 @@ impl<'a> AtatCmd for AtSMSData { &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), - } + let s = core::str::from_utf8(&v["+CMGS: ".len()..]) + .map_err(|_o| -> atat::Error { atat::Error::Parse })?; + let mr: i32 = s + .parse() + .map_err(|_o| -> atat::Error { atat::Error::Parse })?; + return Ok(SMSDataResponse { mr: mr }); } Err(_) => Err(atat::Error::Parse), } @@ -109,24 +102,11 @@ pub enum ReadSMSMode { #[derive(Debug, Clone, AtatResp, PartialEq, Default)] pub struct SMSMessageResponse { stat: String<30>, - sn: String<30>, + sn: UCS2HexString<64>, mid: Option>, date_time: String<30>, - // 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>, + // Text mode + UCS2 charset is assumed + message: UCS2HexString<1024>, } // 4.2.8 AT+CNMI New SMS Message Indications @@ -180,10 +160,23 @@ pub struct NewMessageIndicationUrc { pub index: i32, } +// 3.2.12 AT+CSCS Select TE Character Set +// AT+CSCS= +// The character set affects transmission and reception of SMS and SMS Cell Broadcast messages, +// the entry and display of phone book entries text field and SIM Application Toolkit alpha strings. +#[derive(Clone, Debug, Format, AtatCmd)] +#[at_cmd("+CSCS", NoResponse)] +pub struct AtSelectTECharsetWrite { + pub chset: String<30>, // "GSM" 7-bit, "UCS2", "IRA", "HEX", "PCCP", "PCDN", "8859-1" +} + pub async fn init( client: &mut T, _pico: &mut U, ) { + // PDU mode might make more sense, currently HEX + Text mod is assumed. + // http://rfc.nop.hu/sms/default.htm + // https://en.wikipedia.org/wiki/GSM_03.40 send_command_logged( client, &AtSelectSMSMessageFormatWrite { @@ -194,6 +187,17 @@ pub async fn init( .await .ok(); + // TODO: SMSSending is not adjusted yet. Either use plain text for send and UCS2 only for receive or try it with UCS2HexString<> parameters. + send_command_logged( + client, + &AtSelectTECharsetWrite { + chset: String::try_from("UCS2").unwrap(), + }, + "AtSelectTECharsetWrite".to_string(), + ) + .await + .ok(); + send_command_logged( client, &AtNewSMSMessageIndicationsWrite { @@ -253,13 +257,9 @@ 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.as_bytes() + v.stat, v.date_time, v.sn, v.message ); } Err(_) => break, @@ -272,7 +272,7 @@ mod tests { use crate::cmd_serialization_tests; use super::*; - use atat::{AtatCmd, serde_at}; + use atat::AtatCmd; cmd_serialization_tests! { test_at_select_sms_message_format_write: ( @@ -350,39 +350,36 @@ mod tests { assert_eq!( SMSMessageResponse { stat: String::try_from("REC READ").unwrap(), - sn: String::try_from("+36301234567").unwrap(), + sn: UCS2HexString { text: String::try_from("+36301234567").unwrap(), quoted: true }, 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(), + message: UCS2HexString { text: String::try_from("$tATA/location/12345").unwrap(), quoted: false }, }, - cmd.parse(Ok(b"+CMGR: \"REC READ\",\"+36301234567\",\"\",\"25/04/25,10:37:39+08\"\r\n$tATA/location/12345\r\n")) + cmd.parse(Ok(b"+CMGR: \"REC READ\",\"002B00330036003300300031003200330034003500360037\",\"\",\"25/04/25,10:37:39+08\"\r\n00240074004100540041002F006C006F0063006100740069006F006E002F00310032003300340035\r\n")) .unwrap(), ); assert_eq!( SMSMessageResponse { stat: String::try_from("REC READ").unwrap(), - sn: String::try_from("+36301234567").unwrap(), + sn: UCS2HexString { text: String::try_from("+36301234567").unwrap(), quoted: true }, 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(), + message: UCS2HexString { text: String::try_from("Köszönjük.\nÜdvözlettel Yettel.").unwrap(), quoted: false }, }, - // 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")) + cmd.parse(Ok(b"+CMGR: \"REC READ\",\"002B00330036003300300031003200330034003500360037\",\"\",\"25/04/25,10:37:39+08\"\r\n004B00F60073007A00F6006E006A00FC006B002E000A00DC0064007600F6007A006C0065007400740065006C002000590065007400740065006C002E\r\n")) .unwrap(), ); assert_eq!( SMSMessageResponse { stat: String::try_from("REC READ").unwrap(), - sn: String::try_from("+36301234567").unwrap(), + sn: UCS2HexString { text: String::try_from("+36301234567").unwrap(), quoted: true }, 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(), + message: UCS2HexString { text: String::try_from("Tamás Dömők 😎").unwrap(), quoted: false }, }, - // 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")) + cmd.parse(Ok(b"+CMGR: \"REC READ\",\"002B00330036003300300031003200330034003500360037\",\"\",\"25/04/25,10:37:39+08\"\r\n00540061006D00E100730020004400F6006D0151006B0020D83DDE0E\r\n")) .unwrap(), ); } @@ -392,12 +389,14 @@ mod tests { let mut client = crate::at::tests::ClientMock::default(); client.results.push_back(Ok("".as_bytes())); client.results.push_back(Ok("".as_bytes())); + client.results.push_back(Ok("".as_bytes())); let mut pico = crate::at::tests::PicoMock::default(); init(&mut client, &mut pico).await; - assert_eq!(2, client.sent_commands.len()); + assert_eq!(3, client.sent_commands.len()); assert_eq!("AT+CMGF=1\r", client.sent_commands.get(0).unwrap()); - assert_eq!("AT+CNMI=2,1,0,0,0\r", client.sent_commands.get(1).unwrap()); + assert_eq!("AT+CSCS=\"UCS2\"\r", client.sent_commands.get(1).unwrap()); + assert_eq!("AT+CNMI=2,1,0,0,0\r", client.sent_commands.get(2).unwrap()); } #[tokio::test] @@ -428,7 +427,7 @@ mod tests { #[tokio::test] async fn test_receive_sms() { let mut client = crate::at::tests::ClientMock::default(); - client.results.push_back(Ok("+CMGR: \"REC READ\",\"+36301234567\",\"\",\"26/01/10,17:25:32+04\"\r\n$tATA/location/12345".as_bytes())); + client.results.push_back(Ok("+CMGR: \"REC READ\",\"002B00330036003300300031003200330034003500360037\",\"\",\"26/01/10,17:25:32+04\"\r\n00240074004100540041002F006C006F0063006100740069006F006E002F00310032003300340035".as_bytes())); client.results.push_back(Err(atat::InternalError::Timeout)); let mut pico = crate::at::tests::PicoMock::default(); From adc0d9a663f9fe91513ed2bcfdd0a40f67328f18 Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Mon, 19 Jan 2026 17:13:49 +0100 Subject: [PATCH 2/3] sms sending works --- README.md | 2 +- pico/pico-lib/src/hexstr.rs | 22 +++++++++++++++++++- pico/pico-lib/src/sms.rs | 40 ++++++++++++++++++++++++------------- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 61bc2f6..5cc3b39 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ cargo check # format cargo fmt -# fun a specific test case +# run a specific test case RUST_BACKTRACE=1 cargo test test_call_number -- --nocapture ``` diff --git a/pico/pico-lib/src/hexstr.rs b/pico/pico-lib/src/hexstr.rs index 57efa35..92756e6 100644 --- a/pico/pico-lib/src/hexstr.rs +++ b/pico/pico-lib/src/hexstr.rs @@ -1,7 +1,8 @@ use alloc::{fmt, format, vec::Vec}; use atat::{ + AtatLen, heapless::String, - serde_at::serde::{self, Deserialize, de::Visitor}, + serde_at::serde::{self, Deserialize, Serialize, de::Visitor}, }; use core::{num::ParseIntError, str::FromStr}; use defmt::Format; @@ -135,6 +136,25 @@ impl fmt::Display for UCS2HexString { } } +impl Serialize for UCS2HexString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if !self.quoted { + todo!() + } + + let v: String<512> = encode_utf16_hex_string(self.text.as_bytes()) + .map_err(|_o| -> S::Error { serde::ser::Error::custom("encode utf-16 error") })?; + return serializer.serialize_str(v.as_str()); + } +} + +impl AtatLen for UCS2HexString { + const LEN: usize = N * 2; +} + #[cfg(test)] mod tests { diff --git a/pico/pico-lib/src/sms.rs b/pico/pico-lib/src/sms.rs index 6cda105..787c427 100644 --- a/pico/pico-lib/src/sms.rs +++ b/pico/pico-lib/src/sms.rs @@ -10,6 +10,7 @@ use defmt::info; use crate::at::NoResponse; use crate::hexstr::UCS2HexString; +use crate::hexstr::encode_utf16_hex_string; use crate::utils::send_command_logged; // 4.2.2 AT+CMGF Select SMS Message Format @@ -29,13 +30,13 @@ 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)] +#[at_cmd("+CMGS", NoResponse, timeout_ms = 5000)] // NoResponse == waiting for prompt ">" pub struct AtSMSSend { - pub number: String<30>, + pub number: UCS2HexString<30>, } #[derive(Clone, Debug)] pub struct AtSMSData { - pub message: String<160>, + pub message: UCS2HexString<160>, } impl<'a> AtatCmd for AtSMSData { @@ -45,7 +46,9 @@ impl<'a> AtatCmd for AtSMSData { const MAX_TIMEOUT_MS: u32 = 60000; fn write(&self, buf: &mut [u8]) -> usize { - let bytes = self.message.as_bytes(); + // TODO: UCS2HexString could be enchanted with append ctrl+z then AtSMSData would not need custom write/parse. + let v: String<512> = encode_utf16_hex_string(self.message.text.as_bytes()).unwrap(); + let bytes = v.as_bytes(); let len = bytes.len(); let ctrl_z = b"\x1a"; buf[..len].copy_from_slice(bytes); @@ -174,7 +177,7 @@ pub async fn init( client: &mut T, _pico: &mut U, ) { - // PDU mode might make more sense, currently HEX + Text mod is assumed. + // PDU mode might make more sense, currently UCS2 + Text mod is assumed. // http://rfc.nop.hu/sms/default.htm // https://en.wikipedia.org/wiki/GSM_03.40 send_command_logged( @@ -222,7 +225,10 @@ pub async fn send_sms( send_command_logged( client, &AtSMSSend { - number: number.clone(), + number: UCS2HexString { + text: number.clone(), + quoted: true, + }, }, "AtSMSSend".to_string(), ) @@ -231,7 +237,10 @@ pub async fn send_sms( send_command_logged( client, &AtSMSData { - message: message.clone(), + message: UCS2HexString { + text: message.clone(), + quoted: false, + }, }, "AtSMSData".to_string(), ) @@ -283,15 +292,15 @@ mod tests { ), test_at_send_sms_1: ( AtSMSSend { - number: String::try_from("+361234567").unwrap(), + number: UCS2HexString { text: String::try_from("+36301234567").unwrap(), quoted: true }, }, - "AT+CMGS=\"+361234567\"\r", + "AT+CMGS=\"002B00330036003300300031003200330034003500360037\"\r", ), test_at_send_sms_2: ( AtSMSData { - message: String::try_from("this is the message").unwrap(), + message: UCS2HexString { text: String::try_from("this is the text message").unwrap(), quoted: false }, }, - "this is the message\x1a", + "00740068006900730020006900730020007400680065002000740065007800740020006D006500730073006100670065\x1a", ), test_at_new_sms_message_indications_write: ( AtNewSMSMessageIndicationsWrite { @@ -331,7 +340,10 @@ mod tests { #[test] fn test_send_sms_response() { let cmd = AtSMSData { - message: String::try_from("data").unwrap(), + message: UCS2HexString { + text: String::try_from("data").unwrap(), + quoted: false, + }, }; assert_eq!( @@ -415,11 +427,11 @@ mod tests { .await; assert_eq!(2, client.sent_commands.len()); assert_eq!( - "AT+CMGS=\"+36301234567\"\r", + "AT+CMGS=\"002B00330036003300300031003200330034003500360037\"\r", client.sent_commands.get(0).unwrap() ); assert_eq!( - "this is the text message\x1a", + "00740068006900730020006900730020007400680065002000740065007800740020006D006500730073006100670065\x1a", client.sent_commands.get(1).unwrap() ); } From 43bab71f7035828f1516f4be68cb17451473b0af Mon Sep 17 00:00:00 2001 From: Tamas Domok Date: Mon, 19 Jan 2026 17:37:47 +0100 Subject: [PATCH 3/3] hex_str handle quoted serialization --- pico/pico-lib/src/hexstr.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pico/pico-lib/src/hexstr.rs b/pico/pico-lib/src/hexstr.rs index 92756e6..f85a14e 100644 --- a/pico/pico-lib/src/hexstr.rs +++ b/pico/pico-lib/src/hexstr.rs @@ -141,12 +141,13 @@ impl Serialize for UCS2HexString { where S: serde::Serializer, { + let v: String<512> = encode_utf16_hex_string(self.text.as_bytes()) + .map_err(|_o| -> S::Error { serde::ser::Error::custom("encode utf-16 error") })?; + if !self.quoted { - todo!() + return serializer.serialize_bytes(v.as_bytes()); } - let v: String<512> = encode_utf16_hex_string(self.text.as_bytes()) - .map_err(|_o| -> S::Error { serde::ser::Error::custom("encode utf-16 error") })?; return serializer.serialize_str(v.as_str()); } }