nrf_modem/
sms.rs

1use crate::{error::Error, send_at, LteLink};
2use arrayvec::{ArrayString, ArrayVec};
3use core::{fmt::Write, write};
4
5// ASCII table for coverting ASCII to GSM 7 bit
6// Copied from https://github.com/nrfconnect/sdk-nrf/blob/main/lib/sms/string_conversion.c#L36
7
8const ASCII_TO_7BIT_TABLE: [u8; 256] = [
9    /* Standard ASCII, character codes 0-127 */
10    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, /* 0-7:   Control characters */
11    0x20, 0x20, 0x0A, 0x20, 0x20, 0x0D, 0x20, 0x20, /* 8-15:  ...LF,..CR...      */
12    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, /* 16-31: Control characters */
13    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x21, 0x22, 0x23, 0x02, 0x25, 0x26,
14    0x27, /* 32-39: SP ! " # $ % & ' */
15    0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, /* 40-47: ( ) * + , - . /  */
16    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, /* 48-55: 0 1 2 3 4 5 6 7  */
17    0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, /* 56-63: 8 9 : ; < = > ?  */
18    0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, /* 64-71: @ A B C D E F G  */
19    0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, /* 72-79: H I J K L M N O  */
20    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, /* 80-87: P Q R S T U V W  */
21    0x58, 0x59, 0x5A, 0xBC, 0xAF, 0xBE, 0x94, 0x11, /* 88-95: X Y Z [ \ ] ^ _  */
22    0x27, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, /* 96-103: (` -> ') a b c d e f g */
23    0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, /* 104-111:h i j k l m n o  */
24    0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, /* 112-119: p q r s t u v w  */
25    0x78, 0x79, 0x7A, 0xA8, 0xC0, 0xA9, 0xBD, 0x20, /* 120-127: x y z { | } ~ DEL */
26    /* Character codes 128-255 (beyond standard ASCII) have different possible
27     * interpretations. This table has been done according to ISO-8859-15.
28     */
29    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, /* 128-159: Undefined   */
30    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
31    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x40, 0x63, 0x01, 0xE5, 0x03, 0x53,
32    0x5F, /* 160-167: ..£, €... */
33    0x73, 0x63, 0x20, 0x20, 0x20, 0x2D, 0x20, 0x20, /* 168-175 */
34    0x20, 0x20, 0x20, 0x20, 0x5A, 0x75, 0x0A, 0x20, /* 176-183 */
35    0x7A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x59, 0x60, /* 184-191 */
36    0x41, 0x41, 0x41, 0x41, 0x5B, 0x0E, 0x1C, 0x09, /* 192-199: ..Ä, Å... */
37    0x45, 0x1F, 0x45, 0x45, 0x49, 0x49, 0x49, 0x49, /* 200-207 */
38    0x44, 0x5D, 0x4F, 0x4F, 0x4F, 0x4F, 0x5C, 0x2A, /* 208-215: ..Ö... */
39    0x0B, 0x55, 0x55, 0x55, 0x5E, 0x59, 0x20, 0x1E, /* 216-223 */
40    0x7F, 0x61, 0x61, 0x61, 0x7B, 0x0F, 0x1D, 0x63, /* 224-231: ..ä, å... */
41    0x04, 0x05, 0x65, 0x65, 0x07, 0x69, 0x69, 0x69, /* 232-239 */
42    0x20, 0x7D, 0x08, 0x6F, 0x6F, 0x6F, 0x7C, 0x2F, /* 240-247: ..ö... */
43    0x0C, 0x06, 0x75, 0x75, 0x7E, 0x79, 0x20, 0x79, /* 248-255 */
44];
45
46// Masks need when encoding ASCII to GSM 7 bit
47const STR_7BIT_ESCAPE_IND: u8 = 0x80;
48const STR_7BIT_CODE_MASK: u8 = 0x7F;
49const STR_7BIT_ESCAPE_CODE: u8 = 0x1B;
50
51/// A struct holding both number and message with can be send as an SMS
52pub struct Sms<'a> {
53    number: &'a str,
54    message: &'a str,
55}
56
57impl<'a> Sms<'a> {
58    /// Creates a new Sms message
59    /// `number` should be in national format, including the country code at start. The + character is not need at start and will be ignored.
60    /// Max `message` lenght 160 chars
61    pub fn new(number: &'a str, message: &'a str) -> Self {
62        Self { number, message }
63    }
64    // Encode number in the way modem expect it
65    // Reimplement from https://github.com/nrfconnect/sdk-nrf/blob/main/lib/sms/sms_submit.c#L46
66    fn encode_number(number: &str) -> Result<ArrayString<15>, Error> {
67        let mut number: ArrayString<15> = ArrayString::from(number.trim_start_matches('+'))
68            .map_err(|_| Error::BufferTooSmall(None))?;
69
70        if number.len() % 2 != 0 {
71            number
72                .try_push('F')
73                .map_err(|_| Error::BufferTooSmall(None))?;
74        }
75
76        if number.is_ascii() {
77            let mut swapped_number = ArrayString::from_byte_string(
78                &number
79                    .as_bytes()
80                    .chunks(2)
81                    .flat_map(|c| [c[1], c[0]])
82                    .chain((0..15 - number.len()).map(|_| 0))
83                    .collect::<ArrayVec<u8, 15>>()
84                    .into_inner()
85                    .unwrap(),
86            )
87            .unwrap();
88            swapped_number.truncate(number.len());
89
90            Ok(swapped_number)
91        } else {
92            Err(Error::SmsNumberNotAscii)
93        }
94    }
95    // Convert a ASCII string to GSM 7bit
96    // Reimplement from https://github.com/nrfconnect/sdk-nrf/blob/main/lib/sms/string_conversion.c#L162
97    fn ascii_to_gsm7bit<const N: usize>(text: &str) -> Result<ArrayString<N>, Error> {
98        let mut encoded_message = ArrayString::new();
99
100        for c in text.chars() {
101            if c.is_ascii() {
102                let char_7bit = ASCII_TO_7BIT_TABLE[c as usize];
103                if char_7bit & STR_7BIT_ESCAPE_IND == 0 {
104                    encoded_message
105                        .try_push(char_7bit as char)
106                        .map_err(|_| Error::BufferTooSmall(None))?;
107                } else {
108                    encoded_message
109                        .try_push(STR_7BIT_ESCAPE_CODE as char)
110                        .map_err(|_| Error::BufferTooSmall(None))?;
111                    encoded_message
112                        .try_push((char_7bit & STR_7BIT_CODE_MASK) as char)
113                        .map_err(|_| Error::BufferTooSmall(None))?;
114                }
115            }
116        }
117
118        Ok(encoded_message)
119    }
120    // Pack a GSM 7 bit strings into 7 bites without 1 bit padding
121    // Reimplement from https://github.com/nrfconnect/sdk-nrf/blob/main/lib/sms/string_conversion.c#L294
122    fn pack_gsm7bit<const N: usize>(text: ArrayString<N>) -> ArrayVec<u8, N> {
123        let mut src: usize = 0;
124        let mut dst: usize = 0;
125        let mut shift: usize = 0;
126        let len = text.len();
127        let mut bytes: ArrayVec<u8, N> = ArrayVec::new();
128        bytes.try_extend_from_slice(text.as_bytes()).unwrap();
129
130        while src < len {
131            bytes[dst] = bytes[src] >> shift;
132            src += 1;
133            if src < len {
134                bytes[dst] |= bytes[src] << (7 - shift);
135                shift += 1;
136                if shift == 7 {
137                    shift = 0;
138                    src += 1;
139                }
140            }
141            dst += 1;
142        }
143        bytes.truncate(dst);
144        bytes
145    }
146    /// Sends the craftes message
147    /// `N` is need to provide internal buffer size for message and number encoding. Needs to be at least 2 * message.len() + 34
148    /// Max ever need value for the buffer should be not more then 354 bytes
149    pub async fn send<const N: usize>(self) -> Result<(), Error> {
150        let encoded_number = Self::encode_number(self.number).unwrap();
151
152        #[cfg(feature = "defmt")]
153        defmt::trace!("encoded_number: {}", encoded_number.as_str());
154
155        let encoded_message = Self::pack_gsm7bit(Self::ascii_to_gsm7bit::<N>(self.message)?);
156
157        let size = 2 + /* First header byte and TP-MR fields */
158		1 + /* Length of phone number */
159		1 + /* Phone number Type-of-Address byte */
160		encoded_number.len()/2 +
161		2 + /* TP-PID and TP-DCS fields */
162		1 + /* TP-UDL field */
163		encoded_message.len();
164
165        let mut at_cmgs: ArrayString<N> = ArrayString::new();
166        let mut encoded_number_len = encoded_number.len();
167        if self.number.trim_start_matches('+').len() % 2 != 0 {
168            encoded_number_len -= 1;
169        }
170        // Write the at command and begin with encoded number and it's lenght
171        write!(
172            &mut at_cmgs,
173            "AT+CMGS={}\r{:04X}{:04X}91{}",
174            size, 0x01, encoded_number_len, encoded_number
175        )
176        .map_err(|_| Error::BufferTooSmall(None))?;
177        // Write the message lenght
178        write!(&mut at_cmgs, "00{:04X}", self.message.len())
179            .map_err(|_| Error::BufferTooSmall(None))?;
180        // Write the GSM 7 bit packaged message as hex string
181        for c in &encoded_message {
182            write!(&mut at_cmgs, "{c:02X}").map_err(|_| Error::BufferTooSmall(None))?;
183        }
184        // End character
185        write!(&mut at_cmgs, "\x1A").map_err(|_| Error::BufferTooSmall(None))?;
186
187        #[cfg(feature = "defmt")]
188        defmt::trace!("at_cmgs: {:?}", at_cmgs.as_str());
189
190        // Wait for LteLink to send the message
191        let lte_link = LteLink::new().await?;
192        lte_link.wait_for_link().await?;
193
194        #[cfg(feature = "defmt")]
195        defmt::trace!("link found");
196
197        // Configure the SMS parameters in modem
198        // This might need some rework when reciving SMS is add and reporting
199        if send_at::<6>("AT+CNMI=3,2,0,1").await?.as_str() != "OK\r\n" {
200            return Err(Error::UnexpectedAtResponse);
201        }
202
203        // Send the SMS
204        let result = send_at::<18>(&at_cmgs).await?;
205
206        #[cfg(feature = "defmt")]
207        defmt::trace!("result: {}", result.as_str());
208
209        lte_link.deactivate().await?;
210        if result.ends_with("OK\r\n") {
211            Ok(())
212        } else {
213            Err(Error::UnexpectedAtResponse)
214        }
215    }
216}