В этой статье цикла будут подробно описаны вопросы работы с USSD-запросами и получения ответов на них и их обработки, будет описана работа с кодировкой UCS2, в том числе и в USSD-ответах, а также подробно описан процесс отправки SMS-сообщений в PDU-формате на языках отличных от латиницы(кириллица — русский и пр.). Для полноценной работы с GSM/GPRS-модулем SIM800L понадобится официальный справочник по AT-командам — SIM800 Series_AT Command Manual_V1.10.pdf (4,01 MB).
|
|
03 |
|
|
04 |
Здесь и далее в статье, в примерах с модулем SIM800L используется одна схема: |
|
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). |
|
|
10 |
Исполнение команды, в случае корректного её исполнения, вернет ответ OK. Но непосредственно USSD-ответ будет получен в виде незапрашиваемого уведомления +CUSD. Именно его нужно отслеживать и обрабатывать, когда оно придет. Пример кода: |
|
11 | Arduino (C++) |
#include // Библиотека програмной реализации обмена по UART-протоколу String _response = ""; // Переменная для хранения ответа модуля sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными _response = sendATCommand("AT+CUSD=1,\"*100#\"", true); // Здесь необходимо указать свой USSD-запрос String sendATCommand(String cmd, bool waiting) { String waitResponse() { // Функция ожидания ответа и возврата полученного результата void loop() { |
|
13 |
Далее, не составит труда получить необходимую информацию и задать требуемую логику приложения. Функция по«извлечению» состояния баланса из сообщения: |
|
14 | Arduino (C++) |
void setup() { float getFloatFromString(String str) { // Функция извлечения цифр из сообщения - для парсинга баланса из USSD-запроса for (int i = 0; i < str.length(); i++) { return result.toFloat(); // Возвращаем полученное число. void loop() { |
|
16 |
Полный пример: |
|
17 | Arduino (C++) |
#include // Библиотека програмной реализации обмена по UART-протоколу String _response = ""; // Переменная для хранения ответа модуля sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными // Команды настройки модема при каждом запуске String sendATCommand(String cmd, bool waiting) { String waitResponse() { // Функция ожидания ответа и возврата полученного результата void loop() { void sendSMS(String phone, String message) float getFloatFromString(String str) { // Функция извлечения цифр из сообщения - для парсинга баланса из USSD-запроса |
|
19 |
Декодирование PDU Ну, а что же делать абонентам Мегафон, для которых провайдер исключил получение данных в некириллическом формате. Ничего страшного в этом нет. |
PDU(англ. Protocol Description Unit(не Protocol Data Unit)) — один из протоколов передачи SMS-сообщений в GSM-сетях
|
20 |
Рассмотрим на примере отправленного USSD-запроса баланса и полученного ответа, что нужно делать, чтобы получить вменяемый результат. Отправляем USSD-запрос: |
|
22 |
Полученный USSD-ответ 003700360031002E003200330440002E представляет из себя строку в кодировке UCS2(по сути это первый, устаревший вариант кодировки Unicode спецификации до версии 1.1, не поддерживающий суррогатные символы). В данной кодировке каждый символ имеет фиксированную ширину — 2 байта(16 бит), при этом каждый из байт представлен в HEX-формате. Таким образом каждые четыре знака UCS2-последовательности кодируют всего один символ: |
|
24 |
Таблица кодов UCS2 для кириллических символов. С её помощью нетрудно в ручном режиме осуществить преобразование любой строки в UCS2-формате: |
|
25 |
Кодировка UCS2
|
|
26 |
Для того, чтобы автоматически преобразовывать UCS2-строку в читаемый вид, автором написана функция UCS2ToString(): |
|
27 | Arduino (C++) |
String UCS2ToString(String s) { // Функция декодирования UCS2 строки unsigned char HexSymbolToChar(char c) { |
|
28 |
Использование функции: |
|
29 | Arduino (C++) |
void setup() { String UCS2ToDecode = "003700360031002E003200330440002E"; void loop() { |
|
30 |
Результат работы: |
|
32 |
Полный пример запроса баланса посредством USSD, с последующим декодированием ответа и парсингом суммы из полученной строки: |
|
33 | Arduino (C++) |
#include // Библиотека програмной реализации обмена по UART-протоколу String _response = ""; // Переменная для хранения ответа модуля sendATCommand("AT", true); // Отправили AT для настройки скорости обмена данными String sendATCommand(String cmd, bool waiting) { String waitResponse() { // Функция ожидания ответа и возврата полученного результата void loop() { String UCS2ToString(String s) { // Функция декодирования UCS2 строки unsigned char HexSymbolToChar(char c) { float getFloatFromString(String str) { // Функция извлечения цифр из сообщения - для парсинга баланса из USSD-запроса |
|
35 |
Отправка SMS на русском языке(кириллице, и не только) — PDU-формат Раз тема UCS2 уже затронута, нельзя обойти обратную операцию — конвертация обычного текста в UCS2-строку. Такую задачу нужно решать при отправке SMS на языках, отличных от латиницы, на кириллице в том числе. Делается это при помощи представления сообщения в, специально созданном для таких целей, PDU-формате. |
PDU(англ. Protocol Description Unit(не Protocol Data Unit)) — один из протоколов передачи SMS-сообщений в GSM-сетях.
Протокол PDU также очень подробно описан на Wikipedia. |
36 |
Но перед тем, как приступить к самой конвертации, нужно разобрать процедуру отправки SMS в PDU-формате, так как она совершенно отличается от отправки SMS в текстовом формате(Text Mode). |
|
|
38 | На заметку: |
В данном разделе будут использоваться следующие обозначения для представления чисел:
Поскольку PDU-пакет будет формироваться из байт в шестнадцатеричном представлении, каждый из них должен быть представлен двумя символами. Например, шестнадцатиричное представление байта 00001011b — Bh(число 11 в десятичном формате — 11d) должно быть дополнено нулем до двух знаков — 0Bh. 00001011b = 11d = 0Bh. |
|
39 | На заметку: |
Для простой конвертации значений в бинарный(двоичный) / десятичный / шестнадцатиричный формат, можно использовать штатный калькулятор Windows в варианте«Программист». Для этого, необходимо ввести значение в существующем представлении и переключить режим отображения на заданный:
|
|
40 |
Для формирования PDU-пакета, необходимо ознакомиться с его структурой — из каких полей он состоит, и какой длины эти поля могут быть. Структура PDU-пакета: |
|
42 |
Описание полей PDU-пакета: |
|
43 |
|
|
44 |
Теперь, для того, чтобы все стало понятно, вместе с подробным описанием каждого из полей PDU-пакета, будет поэтапно показано, как формируется PDU-пакет на примере сообщения с текстом "Тест формата PDU!", отправляемого на номер +7(890) 123-45-67. |
|
45 |
|
|
46 |
|
|
47 |
|
|
48 |
|
|
49 |
|
Подробнее о значениях поля PID можно прочитать в ETSI GSM 03.40, пункт 9.2.3.9
|
50 |
|
Подробнее о значениях, которые может принимать поле DCS можно почитать здесь DCS Values
|
51 |
|
|
52 |
|
|
53 | На заметку: |
Поскольку каждый символ кодируется двумя байтами, а максимальная длина сообщения 140 байт, то максимальная длина отправляемого сообщения в PDU-формате может составлять 70 символов(140/2=70).
|
|
54 |
|
Кодировка 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++) |
// =================================== Блок кодирования строки в представление UCS2 ================================= for (int k = 0; k < s.length(); k++) { // Начинаем перебирать все байты во входной строке // Максимальная длина символа в UTF-8 - 6 байт плюс завершающий ноль, итого 7 unsigned int charCode = symbolToUInt(symbolBytes); // Получаем DEC-представление символа из набора байтов unsigned int getCharSize(unsigned char b) { // Функция получения количества байт, которыми кодируется символ if (b < 128) return 1; // Если первый байт из системы ASCII, то он кодируется одним байтом // Дальше нужно посчитать сколько единиц в старших битах до первого нуля - таково будет количество байтов на символ. unsigned int symbolToUInt(const String& bytes) { // Функция для получения DEC-представления символа // Теперь у каждого следующего бита, убираем ненужные биты 10XXXXXX, а оставшиеся добавляем к result в соответствии с расположением String byteToHexString(byte i) { // Функция преобразования числового значения байта в шестнадцатиричное (HEX) |
|
63 | На заметку: |
Для лучшего понимания работы функций кодирования, автор рекомендует ознакомиться с принципами кодирования UTF-8.
|
|
64 |
Использование: |
|
65 | Arduino (C++) |
void setup() { String strTest = "Тест формата PDU!"; void loop() { |
|
66 |
|
Все примеры исполняются на этой схеме
|
67 |
Функция по отправке SMS в PDU-формате немного отличается от отправки SMS в текстовом формате. Дополнительно вводятся функции по формированию PDU-пакета, по схеме, описанной выше. Полный пример отправки SMS в PDU-формате: |
|
68 | Arduino (C++) |
#include // Библиотека програмной реализации обмена по UART-протоколу String _response = ""; // Переменная для хранения ответа модуля void setup() { Serial.println("Start!"); String strTest = "Тест формата PDU с прочими символами ₡❿𦈘!"; void loop() { void sendSMSinPDU(String phone, String message) // ============ Подготовка PDU-пакета ============================================================================================= String PDUPack; // Переменная для хранения PDU-пакета int PDUlen = 0; // Переменная для хранения длины PDU-пакета без SCA getPDUPack(ptrphone, ptrmessage, ptrPDUPack, ptrPDUlen); // Функция формирующая PDU-пакет, и вычисляющая длину пакета без SCA Serial.println("PDU-pack: " + PDUPack); // ============ Отправка PDU-сообщения ============================================================================================ void getPDUPack(String *phone, String *message, String *result, int *PDUlen) String msg = StringToUCS2(*message); // Конвертируем строку в UCS2-формат *result += byteToHexString(msg.length() / 2); // Поле UDL (User Data Length). Делим на 2, так как в UCS2-строке каждый закодированный символ представлен 2 байтами. *PDUlen = (*result).length() / 2; // Получаем длину PDU-пакета без поля SCA String getDAfield(String *phone, bool fullnum) { for (int i = 0; i < result.length(); i += 2) { // Попарно переставляем символы в номере result = fullnum ? "91" + result : "81" + result; // Добавляем формат номера получателя, поле PR return result;
String sendATCommand(String cmd, bool waiting) { String waitResponse() { // Функция ожидания ответа и возврата полученного результата
// =================================== Блок декодирования UCS2 в читаемую строку UTF-8 ================================= unsigned char HexSymbolToChar(char c) { // =================================== Блок кодирования строки в представление UCS2 ================================= for (int k = 0; k < s.length(); k++) { // Начинаем перебирать все байты во входной строке // Максимальная длина символа в UTF-8 - 6 байт плюс завершающий ноль, итого 7 unsigned int charCode = symbolToUInt(symbolBytes); // Получаем DEC-представление символа из набора байтов unsigned int getCharSize(unsigned char b) { // Функция получения количества байт, которыми кодируется символ if (b < 128) return 1; // Если первый байт из системы ASCII, то он кодируется одним байтом // Дальше нужно посчитать сколько единиц в старших битах до первого нуля - таково будет количество байтов на символ. unsigned int symbolToUInt(const String& bytes) { // Функция для получения DEC-представления символа // Теперь у каждого следующего бита, убираем ненужные биты 10XXXXXX, а оставшиеся добавляем к result в соответствии с расположением String byteToHexString(byte i) { // Функция преобразования числового значения байта в шестнадцатиричное (HEX) |
|
70 |
Все работает: |
|
72 |
Установка срока жизни SMS — биты VPF(Validity Period Format) и поле VP(Validity Period) В примере используемом в статье, в целях упрощения, битам VPF было присвоено значение 00. И здесь стоит напомнить, что эти биты задают формат поля VP, могут принимать и другие значения, которые будут влиять на содержимое PDU-пакета, и, как следствие, отношение SMS-центра к отправленному SMS:
|
|
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 |
|
|
79 |
Например, при необходимом времени жизни сообщения 5 часов, значение будет рассчитано по формуле 1 строки таблицы. Здесь нужно будет решить простое уравнение: |
|
80 |
http://www.w3.org/1998/Math/MathML" display="block">(VP+1)×5 минут=300 минут (5 часов×60 минут),(VP+1)=60,VP=59" role="presentation" style="display: table-cell !important; line-height: 0; text-indent: 0px; text-align: center; text-transform: none; font-style: normal; font-weight: normal; font-size: 18.08px; letter-spacing: normal; overflow-wrap: normal; word-spacing: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 24.711em; min-height: 0px; border: 0px; margin: 0px; padding: 1px 0px; width: 10000em; position: relative;">(��+1)×5 минут=300 минут (5 часов×60 минут),(��+1)=60,��=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 |
|
|
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: 10000010b→10001010b. Представление в HEX-формате: 10001010b→8Ah. |
|
94 |
Пример №2. GMT-3 — 29
В 3 часах 12 четвертей часа(3×4=12). Меняем цифры местами 12→21. 21h = 00100001b. Поскольку значение отрицательное, меняем третий бит на 1: 00100001b→00101001b. Представление в HEX-формате: 00101001b→29h. |
|
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>. |