Обновлено 23 ноября 2017
Кравченко Виктор

GSM-модуль SIM800L: часть 3 — USSD, PDU-формат, отправка SMS в PDU-формате (на кириллице)

Цифровые устройства Arduino Arduino Lang Умный дом Датчики, модули Протоколы Микроконтроллеры и мини ПК
02

В этой статье цикла будут подробно описаны вопросы работы с USSD-запросами и получения ответов на них и их обработки, будет описана работа с кодировкой UCS2, в том числе и в USSD-ответах, а также подробно описан процесс отправки SMS-сообщений в PDU-формате на языках отличных от латиницы (кириллица — русский и пр.).

Для полноценной работы с GSM/GPRS-модулем SIM800L понадобится официальный справочник по AT-командам — SIM800 Series_AT Command Manual_V1.10.pdf (4,01 MB).

03
У данной статьи есть видеоверсия!
Подписывайтесь на канал, чтобы быть в курсе обновлений!

04

Здесь и далее в статье, в примерах с модулем SIM800L используется одна схема:

05
06

USSD-запросы

Часто бывает полезно, чтобы GSM-модуль сам отслеживал состояние баланса SIM-карты и вовремя информировал владельца о приближении окончания денег. Как известно самый простой способ узнать об остатке — отправить USSD-запрос.

USSD (англ. Unstructured Supplementary Service Data) — стандартный сервис в сетях GSM, позволяющий организовать интерактивное взаимодействие между абонентом сети и сервисным приложением в режиме передачи коротких сообщений.
07 На заметку:
Если в коде не предусмотрена обработка кодировки UCS2 и для работы используется текстовый режим (Text Mode), то необходимо использовать USSD-команды, возвращающие сообщения не на кириллице.
Например, стандартный USSD-запрос проверки баланса для оператора Билайн — *102# вернет ответ в кодировке UCS2 вида:

1
+CUSD: 0, "003100390038002E............02A0033003100390023", 72

Для возврата ответа в текстовом режиме должен использоваться другой USSD-запрос — #102#. Он вернет уже понятный ответ:

1
2
+CUSD: 0, " Vash balans 198.02 r. Dlya Vas - nedelya besplatnogo SMS-obsh'eniya s druz'yami! Podkl.: *319#", 15
08

Для отправки USSD-запроса существует команда AT+CUSD=<n>[,<str>[,<dcs>]] (по-умолчанию установлена кодировка IRA).

09
Описание Команда Параметр(ы) Ответ Пример(ы)
Отправить USSD-запрос AT+CUSD=<n>[,<str>[,<dcs>]]

Незапрашиваемое уведомление:
+CUSD: <n>[,<str_urc>[,<dcs>]]
<n> задает статус ответа:
0 — не получать ответ
1 — получать ответ
2 — отменить сеанс
<str> — строка запроса в кавычках
<dcs> — схема кодирования данных (целое число, по умолчанию — 0)
<str_urc> — текст ответ на USSD-запрос
OK AT+CUSD=1,"*100#"
10

Исполнение команды, в случае корректного её исполнения, вернет ответ OK. Но непосредственно USSD-ответ будет получен в виде незапрашиваемого уведомления +CUSD. Именно его нужно отслеживать и обрабатывать, когда оно придет. Пример кода:

11 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <SoftwareSerial.h> // Библиотека програмной реализации обмена по UART-протоколу SoftwareSerial SIM800(8, 9); // RX, TX String _response = ""; // Переменная для хранения ответа модуля void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером SIM800.begin(9600); // Скорость обмена данными с модемом Serial.println("Start!"); sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными _response = sendATCommand("AT+CUSD=1,\"*100#\"", true); // Здесь необходимо указать свой USSD-запрос } String sendATCommand(String cmd, bool waiting) { String _resp = ""; // Переменная для хранения результата Serial.println(cmd); // Дублируем команду в монитор порта SIM800.println(cmd); // Отправляем команду модулю if (waiting) { // Если необходимо дождаться ответа... _resp = waitResponse(); // ... ждем, когда будет передан ответ // Если Echo Mode выключен (ATE0), то эти 3 строки можно закомментировать if (_resp.startsWith(cmd)) { // Убираем из ответа дублирующуюся команду _resp = _resp.substring(_resp.indexOf("\r", cmd.length()) + 2); } Serial.println(_resp); // Дублируем ответ в монитор порта } return _resp; // Возвращаем результат. Пусто, если проблема } String waitResponse() { // Функция ожидания ответа и возврата полученного результата String _resp = ""; // Переменная для хранения результата long _timeout = millis() + 10000; // Переменная для отслеживания таймаута (10 секунд) while (!SIM800.available() && millis() < _timeout) {}; // Ждем ответа 10 секунд, если пришел ответ или наступил таймаут, то... if (SIM800.available()) { // Если есть, что считывать... _resp = SIM800.readString(); // ... считываем и запоминаем } else { // Если пришел таймаут, то... Serial.println("Timeout..."); // ... оповещаем об этом и... } return _resp; // ... возвращаем результат. Пусто, если проблема } void loop() { if (SIM800.available()) { // Если модем, что-то отправил... _response = waitResponse(); // Получаем ответ от модема для анализа _response.trim(); // Убираем лишние пробелы в начале и конце Serial.println(_response); // Если нужно выводим в монитор порта //.... if (_response.startsWith("+CUSD:")) { // Пришло уведомление о USSD-ответе if (_response.indexOf("\"") > -1) { // Если ответ содержит кавычки, значит есть сообщение (предохранитель от "пустых" USSD-ответов) String msgBalance = _response.substring(_response.indexOf("\"") + 2); // Получаем непосредственно текст msgBalance = msgBalance.substring(0, msgBalance.indexOf("\"")); Serial.println("USSD: " + msgBalance); // Выводим полученный ответ } } } if (Serial.available()) { // Ожидаем команды по Serial... SIM800.write(Serial.read()); // ...и отправляем полученную команду модему }; }
12
13

Далее, не составит труда получить необходимую информацию и задать требуемую логику приложения. Функция по «извлечению» состояния баланса из сообщения:

14 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером String balance[5] = { // Несколько строк для примера "Vash balans 198.02 r.\r\nDlya Vas - nedelya besplatnogo SMS-obsh'eniya s druz'yami! Podkl.: *319#", "Баланс 200,15 р.", "Your balance is 1 500.24", "Баланс вашего счета равен -2 523,94 рубля", "Задолженность на вашем счете составляет: -542,78" }; for (int i = 0; i < sizeof(balance) / sizeof(String); i++) { Serial.println("Строка для извлечения баланса:\r\n" + balance[i] + "\r\n"); Serial.println("Извлеченный баланс: " + (String)getFloatFromString(balance[i])); Serial.println("--------------\r\n"); } } float getFloatFromString(String str) { // Функция извлечения цифр из сообщения - для парсинга баланса из USSD-запроса bool flag = false; String result = ""; str.replace(",", "."); // Если в качестве разделителя десятичных используется запятая - меняем её на точку. for (int i = 0; i < str.length(); i++) { if (isDigit(str[i]) || (str[i] == (char)46 && flag)) { // Если начинается группа цифр (при этом, на точку без цифр не обращаем внимания), if (result == "" && i > 0 && (String)str[i - 1] == "-") { // Нельзя забывать, что баланс может быть отрицательным result += "-"; // Добавляем знак в начале } result += str[i]; // начинаем собирать их вместе if (!flag) flag = true; // Выставляем флаг, который указывает на то, что сборка числа началась. } else { // Если цифры закончились и флаг говорит о том, что сборка уже была, if (str[i] != (char)32) { // Если порядок числа отделен пробелом - игнорируем его, иначе... if (flag) break; // ...считаем, что все. } } } return result.toFloat(); // Возвращаем полученное число. } void loop() { }
15
16

Полный пример:

17 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <SoftwareSerial.h> // Библиотека програмной реализации обмена по UART-протоколу SoftwareSerial SIM800(8, 9); // RX, TX String _response = ""; // Переменная для хранения ответа модуля void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером SIM800.begin(9600); // Скорость обмена данными с модемом Serial.println("Start!"); sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными // Команды настройки модема при каждом запуске //_response = sendATCommand("AT+CLIP=1", true); // Включаем АОН //_response = sendATCommand("AT+DDET=1", true); // Включаем DTMF //_response = sendATCommand("AT+CMGF=1", true); // Включаем текстовый режим SMS (Text mode) _response = sendATCommand("AT+CUSD=1,\"*100#\"", true); // Здесь необходимо указать свой USSD-запрос } String sendATCommand(String cmd, bool waiting) { String _resp = ""; // Переменная для хранения результата Serial.println(cmd); // Дублируем команду в монитор порта SIM800.println(cmd); // Отправляем команду модулю if (waiting) { // Если необходимо дождаться ответа... _resp = waitResponse(); // ... ждем, когда будет передан ответ // Если Echo Mode выключен (ATE0), то эти 3 строки можно закомментировать if (_resp.startsWith(cmd)) { // Убираем из ответа дублирующуюся команду _resp = _resp.substring(_resp.indexOf("\r", cmd.length()) + 2); } Serial.println(_resp); // Дублируем ответ в монитор порта } return _resp; // Возвращаем результат. Пусто, если проблема } String waitResponse() { // Функция ожидания ответа и возврата полученного результата String _resp = ""; // Переменная для хранения результата long _timeout = millis() + 10000; // Переменная для отслеживания таймаута (10 секунд) while (!SIM800.available() && millis() < _timeout) {}; // Ждем ответа 10 секунд, если пришел ответ или наступил таймаут, то... if (SIM800.available()) { // Если есть, что считывать... _resp = SIM800.readString(); // ... считываем и запоминаем } else { // Если пришел таймаут, то... Serial.println("Timeout..."); // ... оповещаем об этом и... } return _resp; // ... возвращаем результат. Пусто, если проблема } void loop() { if (SIM800.available()) { // Если модем, что-то отправил... _response = waitResponse(); // Получаем ответ от модема для анализа _response.trim(); // Убираем лишние пробелы в начале и конце Serial.println(_response); // Если нужно выводим в монитор порта //.... if (_response.startsWith("+CUSD:")) { // Пришло уведомление о USSD-ответе if (_response.indexOf("\"") > -1) { // Если ответ содержит кавычки, значит есть сообщение (предохранитель от "пустых" USSD-ответов) String msgBalance = _response.substring(_response.indexOf("\"") + 2); // Получаем непосредственно текст msgBalance = msgBalance.substring(0, msgBalance.indexOf("\"")); Serial.println("USSD: " + msgBalance); // Выводим полученный USSD-ответ float balance = getFloatFromString(msgBalance); // Извлекаем информацию о балансе Serial.println("\r\nBalance: " + (String)balance ); // Выводим информацию о балансе } } } if (Serial.available()) { // Ожидаем команды по Serial... SIM800.write(Serial.read()); // ...и отправляем полученную команду модему }; } void sendSMS(String phone, String message) { String _result = ""; sendATCommand("AT+CMGF=1", true); // Включаем текстовый режима SMS (Text mode) sendATCommand("AT+CMGS=\"" + phone + "\"", true); // Переходим в режим ввода текстового сообщения _result = sendATCommand(message + (String)((char)26), true); // После текста отправляем перенос строки и Ctrl+Z } float getFloatFromString(String str) { // Функция извлечения цифр из сообщения - для парсинга баланса из USSD-запроса bool flag = false; String result = ""; str.replace(",", "."); // Если в качестве разделителя десятичных используется запятая - меняем её на точку. for (int i = 0; i < str.length(); i++) { if (isDigit(str[i]) || (str[i] == (char)46 && flag)) { // Если начинается группа цифр (при этом, на точку без цифр не обращаем внимания), if (result == "" && i > 0 && (String)str[i - 1] == "-") { // Нельзя забывать, что баланс может быть отрицательным result += "-"; // Добавляем знак в начале } result += str[i]; // начинаем собирать их вместе if (!flag) flag = true; // Выставляем флаг, который указывает на то, что сборка числа началась. } else { // Если цифры закончились и флаг говорит о том, что сборка уже была, if (str[i] != (char)32) { // Если порядок числа отделен пробелом - игнорируем его, иначе... if (flag) break; // ...считаем, что все. } } } return result.toFloat(); // Возвращаем полученное число. }
18
19

Декодирование PDU

Ну, а что же делать абонентам Мегафон, для которых провайдер исключил получение данных в некириллическом формате. Ничего страшного в этом нет.

PDU (англ. Protocol Description Unit (не Protocol Data Unit)) — один из протоколов передачи SMS-сообщений в GSM-сетях
20

Рассмотрим на примере отправленного USSD-запроса баланса и полученного ответа, что нужно делать, чтобы получить вменяемый результат. Отправляем USSD-запрос:

21
22

Полученный USSD-ответ 003700360031002E003200330440002E представляет из себя строку в кодировке UCS2 (по сути это первый, устаревший вариант кодировки Unicode спецификации до версии 1.1, не поддерживающий суррогатные символы). В данной кодировке каждый символ имеет фиксированную ширину — 2 байта (16 бит), при этом каждый из байт представлен в HEX-формате. Таким образом каждые четыре знака UCS2-последовательности кодируют всего один символ:

23
24

Таблица кодов UCS2 для кириллических символов. С её помощью нетрудно в ручном режиме осуществить преобразование любой строки в UCS2-формате:

25
Кодировка UCS2
Кодировка UCS2
26

Для того, чтобы автоматически преобразовывать UCS2-строку в читаемый вид, автором написана функция UCS2ToString():

27 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
String UCS2ToString(String s) { // Функция декодирования UCS2 строки String result = ""; unsigned char c[5] = ""; // Массив для хранения результата for (int i = 0; i < s.length() - 3; i += 4) { // Перебираем по 4 символа кодировки unsigned long code = (((unsigned int)HexSymbolToChar(s[i])) << 12) + // Получаем UNICODE-код символа из HEX представления (((unsigned int)HexSymbolToChar(s[i + 1])) << 8) + (((unsigned int)HexSymbolToChar(s[i + 2])) << 4) + ((unsigned int)HexSymbolToChar(s[i + 3])); if (code <= 0x7F) { // Теперь в соответствии с количеством байт формируем символ c[0] = (char)code; c[1] = 0; // Не забываем про завершающий ноль } else if (code <= 0x7FF) { c[0] = (char)(0xC0 | (code >> 6)); c[1] = (char)(0x80 | (code & 0x3F)); c[2] = 0; } else if (code <= 0xFFFF) { c[0] = (char)(0xE0 | (code >> 12)); c[1] = (char)(0x80 | ((code >> 6) & 0x3F)); c[2] = (char)(0x80 | (code & 0x3F)); c[3] = 0; } else if (code <= 0x1FFFFF) { c[0] = (char)(0xE0 | (code >> 18)); c[1] = (char)(0xE0 | ((code >> 12) & 0x3F)); c[2] = (char)(0x80 | ((code >> 6) & 0x3F)); c[3] = (char)(0x80 | (code & 0x3F)); c[4] = 0; } result += String((char*)c); // Добавляем полученный символ к результату } return (result); } unsigned char HexSymbolToChar(char c) { if ((c >= 0x30) && (c <= 0x39)) return (c - 0x30); else if ((c >= 'A') && (c <= 'F')) return (c - 'A' + 10); else return (0); }
28

Использование функции:

29 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером String UCS2ToDecode = "003700360031002E003200330440002E"; Serial.println("Входная строка:"); Serial.println(UCS2ToDecode); Serial.println("Результат декодирования:"); Serial.println(UCS2ToString(UCS2ToDecode)); } void loop() { }
30

Результат работы:

31
32

Полный пример запроса баланса посредством USSD, с последующим декодированием ответа и парсингом суммы из полученной строки:

33 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <SoftwareSerial.h> // Библиотека програмной реализации обмена по UART-протоколу SoftwareSerial SIM800(8, 9); // RX, TX String _response = ""; // Переменная для хранения ответа модуля void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером SIM800.begin(9600); // Скорость обмена данными с модемом Serial.println("Start!"); sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными _response = sendATCommand("AT+CUSD=1,\"*100#\"", true); // Здесь необходимо указать свой USSD-запрос } String sendATCommand(String cmd, bool waiting) { String _resp = ""; // Переменная для хранения результата Serial.println(cmd); // Дублируем команду в монитор порта SIM800.println(cmd); // Отправляем команду модулю if (waiting) { // Если необходимо дождаться ответа... _resp = waitResponse(); // ... ждем, когда будет передан ответ // Если Echo Mode выключен (ATE0), то эти 3 строки можно закомментировать if (_resp.startsWith(cmd)) { // Убираем из ответа дублирующуюся команду _resp = _resp.substring(_resp.indexOf("\r", cmd.length()) + 2); } Serial.println(_resp); // Дублируем ответ в монитор порта } return _resp; // Возвращаем результат. Пусто, если проблема } String waitResponse() { // Функция ожидания ответа и возврата полученного результата String _resp = ""; // Переменная для хранения результата long _timeout = millis() + 10000; // Переменная для отслеживания таймаута (10 секунд) while (!SIM800.available() && millis() < _timeout) {}; // Ждем ответа 10 секунд, если пришел ответ или наступил таймаут, то... if (SIM800.available()) { // Если есть, что считывать... _resp = SIM800.readString(); // ... считываем и запоминаем } else { // Если пришел таймаут, то... Serial.println("Timeout..."); // ... оповещаем об этом и... } return _resp; // ... возвращаем результат. Пусто, если проблема } void loop() { if (SIM800.available()) { // Если модем, что-то отправил... _response = waitResponse(); // Получаем ответ от модема для анализа _response.trim(); // Убираем лишние пробелы в начале и конце Serial.println(_response); // Если нужно выводим в монитор порта //.... if (_response.startsWith("+CUSD:")) { // Пришло уведомление о USSD-ответе String msgBalance = _response.substring(_response.indexOf("\"") + 1); // Получаем непосредственно содержимое ответа msgBalance = msgBalance.substring(0, msgBalance.indexOf("\"")); Serial.println("USSD ответ: " + msgBalance); // Выводим полученный ответ // Ответ в UCS2-формате - декодируем и извлекаем число msgBalance = UCS2ToString(msgBalance); // Декодируем ответ Serial.println("Декодируем: " + msgBalance); // Выводим полученный ответ float balance = getFloatFromString(msgBalance); // Парсим ответ на содержание числа Serial.println("Результат парсинга суммы: " + (String(balance))); // Выводим полученный ответ } } if (Serial.available()) { // Ожидаем команды по Serial... SIM800.write(Serial.read()); // ...и отправляем полученную команду модему }; } String UCS2ToString(String s) { // Функция декодирования UCS2 строки String result = ""; unsigned char c[5] = ""; // Массив для хранения результата for (int i = 0; i < s.length() - 3; i += 4) { // Перебираем по 4 символа кодировки unsigned long code = (((unsigned int)HexSymbolToChar(s[i])) << 12) + // Получаем UNICODE-код символа из HEX представления (((unsigned int)HexSymbolToChar(s[i + 1])) << 8) + (((unsigned int)HexSymbolToChar(s[i + 2])) << 4) + ((unsigned int)HexSymbolToChar(s[i + 3])); if (code <= 0x7F) { // Теперь в соответствии с количеством байт формируем символ c[0] = (char)code; c[1] = 0; // Не забываем про завершающий ноль } else if (code <= 0x7FF) { c[0] = (char)(0xC0 | (code >> 6)); c[1] = (char)(0x80 | (code & 0x3F)); c[2] = 0; } else if (code <= 0xFFFF) { c[0] = (char)(0xE0 | (code >> 12)); c[1] = (char)(0x80 | ((code >> 6) & 0x3F)); c[2] = (char)(0x80 | (code & 0x3F)); c[3] = 0; } else if (code <= 0x1FFFFF) { c[0] = (char)(0xE0 | (code >> 18)); c[1] = (char)(0xE0 | ((code >> 12) & 0x3F)); c[2] = (char)(0x80 | ((code >> 6) & 0x3F)); c[3] = (char)(0x80 | (code & 0x3F)); c[4] = 0; } result += String((char*)c); // Добавляем полученный символ к результату } return (result); } unsigned char HexSymbolToChar(char c) { if ((c >= 0x30) && (c <= 0x39)) return (c - 0x30); else if ((c >= 'A') && (c <= 'F')) return (c - 'A' + 10); else return (0); } float getFloatFromString(String str) { // Функция извлечения цифр из сообщения - для парсинга баланса из USSD-запроса bool flag = false; String result = ""; str.replace(",", "."); // Если в качестве разделителя десятичных используется запятая - меняем её на точку. for (int i = 0; i < str.length(); i++) { if (isDigit(str[i]) || (str[i] == (char)46 && flag)) { // Если начинается группа цифр (при этом, на точку без цифр не обращаем внимания), result += str[i]; // начинаем собирать их вместе if (!flag) flag = true; // Выставляем флаг, который указывает на то, что сборка числа началась. } else { // Если цифры закончились и флаг говорит о том, что сборка уже была, if (flag) break; // считаем, что все. } } return result.toFloat(); // Возвращаем полученное число. }
34
35

Отправка SMS на русском языке (кириллице, и не только) — PDU-формат

Раз тема UCS2 уже затронута, нельзя обойти обратную операцию — конвертация обычного текста в UCS2-строку. Такую задачу нужно решать при отправке SMS на языках, отличных от латиницы, на кириллице в том числе. Делается это при помощи представления сообщения в, специально созданном для таких целей, PDU-формате.

PDU (англ. Protocol Description Unit (не Protocol Data Unit)) — один из протоколов передачи SMS-сообщений в GSM-сетях.

Протокол PDU также очень подробно описан на Wikipedia.
36

Но перед тем, как приступить к самой конвертации, нужно разобрать процедуру отправки SMS в PDU-формате, так как она совершенно отличается от отправки SMS в текстовом формате (Text Mode).

37
Описание Команда Параметр(ы) Ответ Пример(ы)
Выбор формата SMS AT+CMGF=<mode> <mode> — формат сообщений, значения:
0 — PDU-формат (по умолчанию)
1 — текстовый формат
OK AT+CMGF=0 формат SMS в PDU-формат (PDU Mode)
Отправить SMS в формате PDU AT+CMGS=<length><CR><PDU-pack><ctrl-Z/ESC> <length> — размер сообщения в PDU-пакете
<CR> — начало строки, после отправки, модуль переходит в режим приема PDU-пакета
<PDU-pack> — PDU-пакет сообщения
После того как текст сообщения передан, необходимо отправить либо <ctrl-Z> для отправки сообщения, либо <ESC> для отмены.
+CMGS: <n>

OK
AT+CMGS=23
>
0001000B919782198144F400080A04220435044104420021
>
+CMGS: 122

OK
38 На заметку:
В данном разделе будут использоваться следующие обозначения для представления чисел:
  • b – двоичное представление,
  • d – десятичное представление,
  • h – шестнадцатеричное представление.

Поскольку PDU-пакет будет формироваться из байт в шестнадцатеричном представлении, каждый из них должен быть представлен двумя символами. Например, шестнадцатиричное представление байта 00001011bBh (число 11 в десятичном формате — 11d) должно быть дополнено нулем до двух знаков — 0Bh.

00001011b = 11d = 0Bh.
39 На заметку:
Для простой конвертации значений в бинарный (двоичный) / десятичный / шестнадцатиричный формат, можно использовать штатный калькулятор Windows в варианте «Программист». Для этого, необходимо ввести значение в существующем представлении и переключить режим отображения на заданный:

40

Для формирования PDU-пакета, необходимо ознакомиться с его структурой — из каких полей он состоит, и какой длины эти поля могут быть. Структура PDU-пакета:

41
42

Описание полей PDU-пакета:

43
Название поля Длина, байт Описание
SCA (Service Center Address) 1...12 Номер телефона Центра SMS
PDU Type 1 Тип PDU
MR (Message Reference) 1 Порядковый номер сообщения
DA (Destination Address) 2...12 Номер телефона получателя сообщения
PID (Protocol Identifier) 1 Идентификатор протокола
DCS (Data Coding Scheme) 1 Кодировка сообщения
VP (Validity Period) 0,1 или 7 Время жизни SMS
UDL (User Data Length) 1 Длина поля User Data в байтах
UD (User Data) 0...140 Сообщение
44

Теперь, для того, чтобы все стало понятно, вместе с подробным описанием каждого из полей PDU-пакета, будет поэтапно показано, как формируется PDU-пакет на примере сообщения с текстом "Тест формата PDU!", отправляемого на номер +7 (890) 123-45-67.

45
  • SCA (Service Center Address)
    Данное поле является необязательным, так как номер SMS-центра (SMSC) по умолчанию зашит в SIM-карте. При этом значение поля SCA должно быть равно 00h. Применение дефолтного номера SMSC удобно ещё и тем, что сформированный PDU-пакет не привязан к конкретному мобильному оператору и является универсальным. PDU-пакет:
    SCA PDU-type 00 ...
46
  • PDU-type
    Каждый бит этого однобайтового поля PDU-type имеет различное назначение:
    • RP (Reply Path) - путь для ответа:
      0 — не определен,
      1 — определен, используется тот же SMSC (центр отправки SMS).
    • UDHI (User Data Header Included) - содержит ли поле User Data, помимо сообщения, дополнительный заголовок:
      0 — не содержит,
      1 — содержит.
    • SRR (Status Report Request) - запрос статуса сообщения:
      0 — не запрашивается,
      1 — запрашивается.
    • VPF (Validity Period Format) - формат поля VP (Validity Period, время жизни SMS):
      00 — поле VP отсутствует,
      01 — зарезервировано,
      10 — поле VP содержит временные данные в относительном формате,
      11 — поле VP содержит временные данные в абсолютном формате.
    • RD (Reject Duplicates) - правила обращения с дубликатами сообщений (при одинаковых полях MR и DA). Используется в случаях, когда в SMS-центр поступает сообщение с такими же значениями полей MR и DA, что и у предыдущего сообщения:
      0 — переслать сообщение,
      1 — отклонить сообщение.
    • MTI (Message Type Indicator) - тип сообщения:
      00 — принимаемое сообщение или подтверждение приема отправленного SMS,
      01 — отправляемое сообщение или подтверждение отправки SMS,
      10 — отчет о доставке SMS или SMS-команда (от модуля к SMSC),
      11 — зарезервировано.
    При использовании значений по умолчанию - все биты нулевые, 2 бита MTI - 01 (тип сообщения - отправляемое SMS), байт PDU-type примет значение 1:
    Таким образом, значение байта PDU-type принимается равным 00000001b = 01h:
    SCA PDU-type MR 00 01 ...
47
  • MR (Message Reference)
    Автоматически присваиваемый модулем идентификатор (порядковый номер) сообщения. Значение поля MR принимается равным 00h:
    SCA PDU-type MR DA 00 01 00 ...
48
  • DA (Destination Address)
    Закодированный специальным образом номер телефона получателя, состоит из 3 полей:
    • PL (Phone Length) — количество цифр в номере телефона получателя, без учета прочих знаков типа "+". Например для телефона +7 (897) 012-34-56 количество цифр - 11. Значение поля PL в шестнадцатиричном формате должно быть 11d = 0Bh.
    • PT (Phone Type) — тип номера получателя. В основном используются 2 значения:
      91h — международный формат (как в примере +7 (897) 01...),
      81h — местный формат, либо короткие номера.
    • RP (Recipient's Phone) — специально подготовленный номер получателя. Правила подготовки очень простые. Во-первых, нужно оставить только цифры: было +7 (897) 012-34-56, стало 78970123456. Во-вторых, если количество цифр нечетное, то в конце нужно добавить букву F: было 78970123456, стало 78970123456F. И в-третьих, каждую пару цифр нужно поменять местами: было 78970123456F, стало 8779103254F6.
    Таким образом поле DA примет вид:
    PL PT RP 0B 91 8779103254F6
    PDU-пакет:
    SCA PDU-type MR DA PID 00 01 00 0B918779103254F6 ...
49
  • PID (Protocol Identifier)
    Идентификатор протокола. Для задач, определенных данной статьей, поле принимается равным 00h:
    SCA PDU-type MR DA PID DCS 00 01 00 0B918779103254F6 00 ...
Подробнее о значениях поля PID можно прочитать в ETSI GSM 03.40, пункт 9.2.3.9
50
  • DCS (Data Coding Scheme)
    Кодировка сообщения. Поскольку, речь о PDU-формате, идет, исходя из необходимости отправки SMS на русском языке, то данное поле должно устанавливать кодировку UCS2. Для этого можно использовать 2 значения:
    • 08h — для отправки обычных сообщений,
    • 18h — для отправки flash-сообщений.
    SCA PDU-type MR DA PID DCS VP 00 01 00 0B918779103254F6 00 08 ...
Подробнее о значениях, которые может принимать поле DCS можно почитать здесь DCS Values
51
  • VP (Validity Period)
    Время жизни сообщения определяет срок хранения сообщения в SMSC, в случае если его не удается доставить адресату. Это поле связано со значениями битов VPF поля PDU-type. Подробно об этом будет рассказано далее, а поскольку, в примере заданы нулевые биты VPF00000001, то это поле не будет входить в состав PDU-пакета.
    SCA PDU-type MR DA PID DCS VP UDL 00 01 00 0B918779103254F6 00 08 ...
52
  • UDL (User Data Length)
    Длина поля UD в байтах. Поскольку каждый символ в кодировке UCS2 кодируется 2 байтами, то для получения значения поля UDL, нужно количество символов в сообщении умножить на 2. Текст примера "Тест формата PDU!" состоит из 17 символов (включая пробелы), таким образом значение поля UDL будет представлять 17*2 = 34d = 22h:
    SCA PDU-type MR DA PID DCS VP UDL UD 00 01 00 0B918779103254F6 00 08 22 ...
53 На заметку:
Поскольку каждый символ кодируется двумя байтами, а максимальная длина сообщения 140 байт, то максимальная длина отправляемого сообщения в PDU-формате может составлять 70 символов (140/2=70).
54
  • UD (User Data)
    Непосредственно сообщение в кодировке UCS. Пример "Тест формата PDU!" в кодировке UCS2 будет выглядеть так:
    Т - 0422 о - 043E - 0020 е - 0435 р - 0440 P - 0050 с - 0441 м - 043C D - 0044 т - 0442 а - 0430 U - 0055 - 0020 т - 0442 ! - 0021 ф - 0444 а - 0430
    Поле UD — 042204350441044200200444043E0440043C04300442043000200050004400550021.
    SCA PDU-type MR DA PID DCS VP UDL UD 00 01 00 0B918779103254F6 00 08 22 042204350441044200200444043E0440043C04300442043000200050004400550021
Кодировка UCS2
Кодировка UCS2
55

Сформированный PDU-пакет будет выглядеть так: 0001000B918779103254F6000822 042204350441044200200444043E0440043C04300442043000200050004400550021.

56

Для того, чтобы отправить SMS в PDU-формате, в первую очередь необходимо установить режим отправки PDU:

57
1
AT+CMGF=0
58

Далее, командой AT+CMGS=<length> в параметре <length> необходимо передать длину PDU-пакета в байтах без учета поля SCA. Поскольку каждый байт кодируется двумя символами, нужно исключить поле SCA и разделить оставшееся количество символов пополам. Таким образом, значение параметра <length> будет равно 47:

59
1
AT+CMGS=47
60

Далее, как и при отправке SMS в текстовом формате — передается PDU-пакет, и в завершение Ctrl+Z.

61

Отправка SMS в PDU-формате: Arduino

Программная реализация отправки SMS в PDU-формате очень проста, за исключением части, отвечающей за кодирование строки в UCS2-формат. Блок кодирования состоит из нескольких функций и выглядит так:

62 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// =================================== Блок кодирования строки в представление UCS2 ================================= String StringToUCS2(String s) { String output = ""; // Переменная для хранения результата for (int k = 0; k < s.length(); k++) { // Начинаем перебирать все байты во входной строке byte actualChar = (byte)s[k]; // Получаем первый байт unsigned int charSize = getCharSize(actualChar); // Получаем длину символа - кличество байт. // Максимальная длина символа в UTF-8 - 6 байт плюс завершающий ноль, итого 7 char symbolBytes[charSize + 1]; // Объявляем массив в соответствии с полученным размером for (int i = 0; i < charSize; i++) symbolBytes[i] = s[k + i]; // Записываем в массив все байты, которыми кодируется символ symbolBytes[charSize] = '\0'; // Добавляем завершающий 0 unsigned int charCode = symbolToUInt(symbolBytes); // Получаем DEC-представление символа из набора байтов if (charCode > 0) { // Если все корректно преобразовываем его в HEX-строку // Остается каждый из 2 байт перевести в HEX формат, преобразовать в строку и собрать в кучу output += byteToHexString((charCode & 0xFF00) >> 8) + byteToHexString(charCode & 0xFF); } k += charSize - 1; // Передвигаем указатель на начало нового символа } return output; // Возвращаем результат } unsigned int getCharSize(unsigned char b) { // Функция получения количества байт, которыми кодируется символ // По правилам кодирования UTF-8, по старшим битам первого октета вычисляется общий размер символа // 1 0xxxxxxx - старший бит ноль (ASCII код совпадает с UTF-8) - символ из системы ASCII, кодируется одним байтом // 2 110xxxxx - два старших бита единицы - символ кодируется двумя байтами // 3 1110xxxx - 3 байта и т.д. // 4 11110xxx // 5 111110xx // 6 1111110x if (b < 128) return 1; // Если первый байт из системы ASCII, то он кодируется одним байтом // Дальше нужно посчитать сколько единиц в старших битах до первого нуля - таково будет количество байтов на символ. // При помощи маски, поочереди исключаем старшие биты, до тех пор пока не дойдет до нуля. for (int i = 1; i <= 7; i++) { if (((b << i) & 0xFF) >> 7 == 0) { return i; } } return 1; } unsigned int symbolToUInt(const String& bytes) { // Функция для получения DEC-представления символа unsigned int charSize = bytes.length(); // Количество байт, которыми закодирован символ unsigned int result = 0; if (charSize == 1) { return bytes[0]; // Если символ кодируется одним байтом, сразу отправляем его } else { unsigned char actualByte = bytes[0]; // У первого байта оставляем только значимую часть 1110XXXX - убираем в начале 1110, оставляем XXXX // Количество единиц в начале совпадает с количеством байт, которыми кодируется символ - убираем их // Например (для размера 2 байта), берем маску 0xFF (11111111) - сдвигаем её (>>) на количество ненужных бит (3 - 110) - 00011111 result = actualByte & (0xFF >> (charSize + 1)); // Было 11010001, далее 11010001&(11111111>>(2+1))=10001 // Каждый следующий байт начинается с 10XXXXXX - нам нужны только по 6 бит с каждого последующего байта // А поскольку остался только 1 байт, резервируем под него место: result = result << (6 * (charSize - 1)); // Было 10001, далее 10001<<(6*(2-1))=10001000000 // Теперь у каждого следующего бита, убираем ненужные биты 10XXXXXX, а оставшиеся добавляем к result в соответствии с расположением for (int i = 1; i < charSize; i++) { actualByte = bytes[i]; if ((actualByte >> 6) != 2) return 0; // Если байт не начинается с 10, значит ошибка - выходим // В продолжение примера, берется существенная часть следующего байта // Например, у 10011111 убираем маской 10 (биты в начале), остается - 11111 // Теперь сдвигаем их на 2-1-1=0 сдвигать не нужно, просто добавляем на свое место result |= ((actualByte & 0x3F) << (6 * (charSize - 1 - i))); // Было result=10001000000, actualByte=10011111. Маской actualByte & 0x3F (10011111&111111=11111), сдвигать не нужно // Теперь "пристыковываем" к result: result|11111 (10001000000|11111=10001011111) } return result; } } String byteToHexString(byte i) { // Функция преобразования числового значения байта в шестнадцатиричное (HEX) String hex = String(i, HEX); if (hex.length() == 1) hex = "0" + hex; hex.toUpperCase(); return hex; }
63 На заметку:
Для лучшего понимания работы функций кодирования, автор рекомендует ознакомиться с принципами кодирования UTF-8.
64

Использование:

65 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером String strTest = "Тест формата PDU!"; Serial.println("Входная строка:"); Serial.println(">> " + strTest); Serial.println("Результат кодирования:"); Serial.println(">> " + StringToUCS2(strTest)); } void loop() { }
66
Все примеры исполняются на этой схеме
Все примеры исполняются на этой схеме
67

Функция по отправке SMS в PDU-формате немного отличается от отправки SMS в текстовом формате. Дополнительно вводятся функции по формированию PDU-пакета, по схеме, описанной выше. Полный пример отправки SMS в PDU-формате:

68 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#include <SoftwareSerial.h> // Библиотека програмной реализации обмена по UART-протоколу SoftwareSerial SIM800(8, 9); // RX, TX String _response = ""; // Переменная для хранения ответа модуля void setup() { Serial.begin(9600); // Скорость обмена данными с компьютером SIM800.begin(9600); // Скорость обмена данными с модемом Serial.println("Start!"); sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными String strTest = "Тест формата PDU с прочими символами ₡❿𦈘!"; sendSMSinPDU("+7928xxxxxxx", strTest); } void loop() { if (SIM800.available()) { // Если модем, что-то отправил... _response = waitResponse(); // Получаем ответ от модема для анализа _response.trim(); // Убираем лишние пробелы в начале и конце Serial.println(_response); // Если нужно выводим в монитор порта } if (Serial.available()) { // Ожидаем команды по Serial... SIM800.write(Serial.read()); // ...и отправляем полученную команду модему }; } void sendSMSinPDU(String phone, String message) { Serial.println("Отправляем сообщение: " + message); // ============ Подготовка PDU-пакета ============================================================================================= // В целях экономии памяти будем использовать указатели и ссылки String *ptrphone = &phone; // Указатель на переменную с телефонным номером String *ptrmessage = &message; // Указатель на переменную с сообщением String PDUPack; // Переменная для хранения PDU-пакета String *ptrPDUPack = &PDUPack; // Создаем указатель на переменную с PDU-пакетом int PDUlen = 0; // Переменная для хранения длины PDU-пакета без SCA int *ptrPDUlen = &PDUlen; // Указатель на переменную для хранения длины PDU-пакета без SCA getPDUPack(ptrphone, ptrmessage, ptrPDUPack, ptrPDUlen); // Функция формирующая PDU-пакет, и вычисляющая длину пакета без SCA Serial.println("PDU-pack: " + PDUPack); Serial.println("PDU length without SCA:" + (String)PDUlen); // ============ Отправка PDU-сообщения ============================================================================================ sendATCommand("AT+CMGF=0", true); // Включаем PDU-режим sendATCommand("AT+CMGS=" + (String)PDUlen, true); // Отправляем длину PDU-пакета sendATCommand(PDUPack + (String)((char)26), true); // После PDU-пакета отправляем Ctrl+Z } void getPDUPack(String *phone, String *message, String *result, int *PDUlen) { // Поле SCA добавим в самом конце, после расчета длины PDU-пакета *result += "01"; // Поле PDU-type - байт 00000001b *result += "00"; // Поле MR (Message Reference) *result += getDAfield(phone, true); // Поле DA *result += "00"; // Поле PID (Protocol Identifier) *result += "08"; // Поле DCS (Data Coding Scheme) //*result += ""; // Поле VP (Validity Period) - не используется String msg = StringToUCS2(*message); // Конвертируем строку в UCS2-формат *result += byteToHexString(msg.length() / 2); // Поле UDL (User Data Length). Делим на 2, так как в UCS2-строке каждый закодированный символ представлен 2 байтами. *result += msg; *PDUlen = (*result).length() / 2; // Получаем длину PDU-пакета без поля SCA *result = "00" + *result; // Добавляем поле SCA } String getDAfield(String *phone, bool fullnum) { String result = ""; for (int i = 0; i <= (*phone).length(); i++) { // Оставляем только цифры if (isDigit((*phone)[i])) { result += (*phone)[i]; } } int phonelen = result.length(); // Количество цифр в телефоне if (phonelen % 2 != 0) result += "F"; // Если количество цифр нечетное, добавляем F for (int i = 0; i < result.length(); i += 2) { // Попарно переставляем символы в номере char symbol = result[i + 1]; result = result.substring(0, i + 1) + result.substring(i + 2); result = result.substring(0, i) + (String)symbol + result.substring(i); } result = fullnum ? "91" + result : "81" + result; // Добавляем формат номера получателя, поле PR result = byteToHexString(phonelen) + result; // Добавляем длиу номера, поле PL return result; } String sendATCommand(String cmd, bool waiting) { String _resp = ""; // Переменная для хранения результата Serial.println(cmd); // Дублируем команду в монитор порта SIM800.println(cmd); // Отправляем команду модулю if (waiting) { // Если необходимо дождаться ответа... _resp = waitResponse(); // ... ждем, когда будет передан ответ // Если Echo Mode выключен (ATE0), то эти 3 строки можно закомментировать if (_resp.startsWith(cmd)) { // Убираем из ответа дублирующуюся команду _resp = _resp.substring(_resp.indexOf("\r", cmd.length()) + 2); } Serial.println(_resp); // Дублируем ответ в монитор порта } return _resp; // Возвращаем результат. Пусто, если проблема } String waitResponse() { // Функция ожидания ответа и возврата полученного результата String _resp = ""; // Переменная для хранения результата long _timeout = millis() + 10000; // Переменная для отслеживания таймаута (10 секунд) while (!SIM800.available() && millis() < _timeout) {}; // Ждем ответа 10 секунд, если пришел ответ или наступил таймаут, то... if (SIM800.available()) { // Если есть, что считывать... _resp = SIM800.readString(); // ... считываем и запоминаем } else { // Если пришел таймаут, то... Serial.println("Timeout..."); // ... оповещаем об этом и... } return _resp; // ... возвращаем результат. Пусто, если проблема } // =================================== Блок декодирования UCS2 в читаемую строку UTF-8 ================================= String UCS2ToString(String s) { // Функция декодирования UCS2 строки String result = ""; unsigned char c[5] = ""; // Массив для хранения результата for (int i = 0; i < s.length() - 3; i += 4) { // Перебираем по 4 символа кодировки unsigned long code = (((unsigned int)HexSymbolToChar(s[i])) << 12) + // Получаем UNICODE-код символа из HEX представления (((unsigned int)HexSymbolToChar(s[i + 1])) << 8) + (((unsigned int)HexSymbolToChar(s[i + 2])) << 4) + ((unsigned int)HexSymbolToChar(s[i + 3])); if (code <= 0x7F) { // Теперь в соответствии с количеством байт формируем символ c[0] = (char)code; c[1] = 0; // Не забываем про завершающий ноль } else if (code <= 0x7FF) { c[0] = (char)(0xC0 | (code >> 6)); c[1] = (char)(0x80 | (code & 0x3F)); c[2] = 0; } else if (code <= 0xFFFF) { c[0] = (char)(0xE0 | (code >> 12)); c[1] = (char)(0x80 | ((code >> 6) & 0x3F)); c[2] = (char)(0x80 | (code & 0x3F)); c[3] = 0; } else if (code <= 0x1FFFFF) { c[0] = (char)(0xE0 | (code >> 18)); c[1] = (char)(0xE0 | ((code >> 12) & 0x3F)); c[2] = (char)(0x80 | ((code >> 6) & 0x3F)); c[3] = (char)(0x80 | (code & 0x3F)); c[4] = 0; } result += String((char*)c); // Добавляем полученный символ к результату } return (result); } unsigned char HexSymbolToChar(char c) { if ((c >= 0x30) && (c <= 0x39)) return (c - 0x30); else if ((c >= 'A') && (c <= 'F')) return (c - 'A' + 10); else return (0); } // =================================== Блок кодирования строки в представление UCS2 ================================= String StringToUCS2(String s) { String output = ""; // Переменная для хранения результата for (int k = 0; k < s.length(); k++) { // Начинаем перебирать все байты во входной строке byte actualChar = (byte)s[k]; // Получаем первый байт unsigned int charSize = getCharSize(actualChar); // Получаем длину символа - кличество байт. // Максимальная длина символа в UTF-8 - 6 байт плюс завершающий ноль, итого 7 char symbolBytes[charSize + 1]; // Объявляем массив в соответствии с полученным размером for (int i = 0; i < charSize; i++) symbolBytes[i] = s[k + i]; // Записываем в массив все байты, которыми кодируется символ symbolBytes[charSize] = '\0'; // Добавляем завершающий 0 unsigned int charCode = symbolToUInt(symbolBytes); // Получаем DEC-представление символа из набора байтов if (charCode > 0) { // Если все корректно преобразовываем его в HEX-строку // Остается каждый из 2 байт перевести в HEX формат, преобразовать в строку и собрать в кучу output += byteToHexString((charCode & 0xFF00) >> 8) + byteToHexString(charCode & 0xFF); } k += charSize - 1; // Передвигаем указатель на начало нового символа if (output.length() >= 280) break; // Строка превышает 70 (4 знака на символ * 70 = 280) символов, выходим } return output; // Возвращаем результат } unsigned int getCharSize(unsigned char b) { // Функция получения количества байт, которыми кодируется символ // По правилам кодирования UTF-8, по старшим битам первого октета вычисляется общий размер символа // 1 0xxxxxxx - старший бит ноль (ASCII код совпадает с UTF-8) - символ из системы ASCII, кодируется одним байтом // 2 110xxxxx - два старших бита единицы - символ кодируется двумя байтами // 3 1110xxxx - 3 байта и т.д. // 4 11110xxx // 5 111110xx // 6 1111110x if (b < 128) return 1; // Если первый байт из системы ASCII, то он кодируется одним байтом // Дальше нужно посчитать сколько единиц в старших битах до первого нуля - таково будет количество байтов на символ. // При помощи маски, поочереди исключаем старшие биты, до тех пор пока не дойдет до нуля. for (int i = 1; i <= 7; i++) { if (((b << i) & 0xFF) >> 7 == 0) { return i; } } return 1; } unsigned int symbolToUInt(const String& bytes) { // Функция для получения DEC-представления символа unsigned int charSize = bytes.length(); // Количество байт, которыми закодирован символ unsigned int result = 0; if (charSize == 1) { return bytes[0]; // Если символ кодируется одним байтом, сразу отправляем его } else { unsigned char actualByte = bytes[0]; // У первого байта оставляем только значимую часть 1110XXXX - убираем в начале 1110, оставляем XXXX // Количество единиц в начале совпадает с количеством байт, которыми кодируется символ - убираем их // Например (для размера 2 байта), берем маску 0xFF (11111111) - сдвигаем её (>>) на количество ненужных бит (3 - 110) - 00011111 result = actualByte & (0xFF >> (charSize + 1)); // Было 11010001, далее 11010001&(11111111>>(2+1))=10001 // Каждый следующий байт начинается с 10XXXXXX - нам нужны только по 6 бит с каждого последующего байта // А поскольку остался только 1 байт, резервируем под него место: result = result << (6 * (charSize - 1)); // Было 10001, далее 10001<<(6*(2-1))=10001000000 // Теперь у каждого следующего бита, убираем ненужные биты 10XXXXXX, а оставшиеся добавляем к result в соответствии с расположением for (int i = 1; i < charSize; i++) { actualByte = bytes[i]; if ((actualByte >> 6) != 2) return 0; // Если байт не начинается с 10, значит ошибка - выходим // В продолжение примера, берется существенная часть следующего байта // Например, у 10011111 убираем маской 10 (биты в начале), остается - 11111 // Теперь сдвигаем их на 2-1-1=0 сдвигать не нужно, просто добавляем на свое место result |= ((actualByte & 0x3F) << (6 * (charSize - 1 - i))); // Было result=10001000000, actualByte=10011111. Маской actualByte & 0x3F (10011111&111111=11111), сдвигать не нужно // Теперь "пристыковываем" к result: result|11111 (10001000000|11111=10001011111) } return result; } } String byteToHexString(byte i) { // Функция преобразования числового значения байта в шестнадцатиричное (HEX) String hex = String(i, HEX); if (hex.length() == 1) hex = "0" + hex; hex.toUpperCase(); return hex; }
69
70

Все работает:

71
72

Установка срока жизни SMS — биты VPF (Validity Period Format) и поле VP (Validity Period)

В примере используемом в статье, в целях упрощения, битам VPF было присвоено значение 00. И здесь стоит напомнить, что эти биты задают формат поля VP, могут принимать и другие значения, которые будут влиять на содержимое PDU-пакета, и, как следствие, отношение SMS-центра к отправленному SMS:

  • 00 — поле VP отсутствует,
  • 01 — зарезервировано,
  • 10 — поле VP содержит временные данные в относительном формате,
  • 11 — поле VP содержит временные данные в абсолютном формате.

73

Здесь имеет смысл повторить, что данная информация становится актуальной в случаях, когда нет возможности доставить SMS адресату, когда он, например, находится вне зоны действия сети. И этим параметром можно указать срок, в течение которого SMS-центр не будет его удалять, а будет пытаться его доставить. По истечению этого срока, если сообщение не было доставлено, оно будет удалено.

74

Далее будут рассмотрены 2 возможных варианта формата поля VP, задаваемые битами VPF.

75

Вариант 1 — поле VP содержит данные в относительном формате.

Данный вариант устанавливается значениями битов VPF — 10. И говорит о том, что поле VP хранит данные о сроке жизни SMS в относительном формате, то есть по отношению ко времени его создания. Например, 30 минут, сутки или 30 дней, с момента получения сообщения SMS-центром.

76

Теперь поле PDU-type примет вид 00010001b=11h.

77

В данном случае, длина поля VP составляет 1 байт. Значение этого байта устанавливается в соответствии с таблицей:

78
Шестнадцатеричное значение поля VP Десятичное значение поля VP Время, соответствующее значению поля VP Максимальное время жизни
1…8F0…143(VP + 1) × 5 минут12 часов
90…A7144…16712 часов + (VP — 143) × 30 минут24 часа
A8…C4168…196(VP — 166) × 1 день30 дней
C5…FF197…255(VP — 192) × 1 неделя63 недели
79

Например, при необходимом времени жизни сообщения 5 часов, значение будет рассчитано по формуле 1 строки таблицы. Здесь нужно будет решить простое уравнение:

80
$$(VP + 1) \times 5~минут = 300~минут~(5~часов \times 60~минут), \\ (VP + 1) = 60, \\ VP=59 $$
81

Таким образом поле VP должно будет принять значение 59d = 3Bh.

82

Исходя из вышеизложенного весь PDU-пакет необходимо пересобрать — изменить поле PDU-type и добавить поле VP:

83
SCA PDU-type MR DA PID DCS VP UDL UD 00 11 00 0B918779103254F6 00 08 3B 22 0422043504410442002004...
84

Также, необходимо пересчитать, с учетом изменившегося PDU-пакета, его длину, отправляемую параметром команды AT+CMGS=<length>.

85

Вариант 2 — поле VP содержит данные в абсолютном формате.

Данный вариант устанавливается значениями битов VPF — 11. И говорит о том, что поле VP хранит данные о конкретном времени, до наступления которого сообщение не будет удалено. Например, срок жизни сообщения до 09:20:50 30 декабря 2017 года. При наступлении этого времени, если сообщение не было доставлено, оно удалится.

86

Поле PDU-type теперь будет иметь значение 00011001b=19h.

87

В данном варианте, длина поля VP составляет 7 байт и его структура выглядит так:

88
Байт 1 Байт 2 Байт 3 Байт 4 Байт 5 Байт 6 Байт 7
Год Месяц День Час Минуты Секунды Часовой пояс
89

Каждый байт представлен двухзначным десятичным числом с переставленными цифрами. При обозначении года используются последние две цифры. Часовой пояс показывает смещение времени относительно Гринвича (GMT), выраженную в четвертях часа. В случае отрицательного смещения (GMT-2), третий бит байта устанавливается в 1. Например, необходимо установить следующее значение 25 марта 2018 года 15:23:54 (GMT-7).

90
Г М Д Ч м с ЧП было: 18 03 25 15 23 54 -7 стало: 81 30 52 51 32 54 8А
91

Поле VP принимает значение: 8130525132548А.

92

Но здесь имеет смысл подробнее остановиться на получении корректного значения часового пояса. Если с положительным смещением, все более-менее просто, то с отрицательным, алгоритм требует пояснений. Ниже представлены несколько примеров получения 7 байта поля VP:

93
Пример №1. GMT-7 — 8A
В 7 часах 28 четвертей часа (7×4=28). Меняем цифры местами 28→82. 82h = 10000010b. Поскольку значение отрицательное, меняем третий бит на 1: 10000010b10001010b. Представление в HEX-формате: 10001010b8Ah.
94
Пример №2. GMT-3 — 29
В 3 часах 12 четвертей часа (3×4=12). Меняем цифры местами 12→21. 21h = 00100001b. Поскольку значение отрицательное, меняем третий бит на 1: 00100001b00101001b. Представление в HEX-формате: 00101001b29h.
95
Пример №3. GMT+4 — 61
В 4 часах 16 четвертей часа (4×4=16). Меняем цифры местами 16→61. 61h. Поскольку значение положительное, менять ничего не нужно — 61h.
96

Теперь, также как и в предыдущем варианте весь PDU-пакет нужно пересобрать — изменить поле PDU-type и добавить поле VP:

97
SCA PDU-type MR DA PID DCS VP UDL UD 00 19 00 0B918779103254F6 00 08 8130525132548А 22 0422043504410442002004...
98

И снова нужно будет пересчитать, с учетом изменившегося PDU-пакета, его длину, отправляемую параметром команды AT+CMGS=<length>.

101

Похожие запросы:

  • Посылаем/принимает SMS на русском
  • Отправка SMS-сообщений в формате PDU, теория с примерами на Arduino
  • GSM модуль SIM800L — самое полное руководство на русском языке
  • Как отправить SMS на русском
  • Декодирование входящих русских SMS
  • Декодирование кириллицы из PDU-формата
  • SMS PDU Mode
  • Декодирование SMS-сообщений в формате PDU
  • Как расшифровать PDU-формат
  • Использование PDU формата для оповещения через SMS сообщения
  • Формат PDU или как сформировать и распаковать SMS
  • Что такое PDU и как послать по GSM
comments powered by HyperComments