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

В этой статье цикла будут подробно описаны вопросы работы с 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).

 
Описание Команда Параметр(ы) Ответ Пример(ы)
Отправить 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++)

#include // Библиотека програмной реализации обмена по 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++)

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++)

#include // Библиотека програмной реализации обмена по 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++)

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++)

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++)

#include // Библиотека програмной реализации обмена по 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).

 
Описание Команда Параметр(ы) Ответ Пример(ы)
Выбор формата 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-пакет будет формироваться из байт в шестнадцатеричном представлении, каждый из них должен быть представлен двумя символами. Например, шестнадцатиричное представление байта 00001011b — Bh(число 11 в десятичном формате — 11d) должно быть дополнено нулем до двух знаков — 0Bh.

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

 
40  

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

 
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. Подробно об этом будет рассказано далее, а поскольку, в примере заданы нулевые биты VPF — 00000001, то это поле не будет входить в состав 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++)

// =================================== Блок кодирования строки в представление 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++)

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++)

#include // Библиотека програмной реализации обмена по 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 = ☎ // Указатель на переменную с телефонным номером
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  

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

 
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…8F 0…143 (VP + 1) × 5 минут 12 часов
90…A7 144…167 12 часов +(VP — 143) × 30 минут 24 часа
A8…C4 168…196 (VP — 166) × 1 день 30 дней
C5…FF 197…255 (VP — 192) × 1 неделя 63 недели
 
79  

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

 
80  
http://www.w3.org/1998/Math/MathML" display="block">(VP+1)&#x00D7;5&#xA0;&#x43C;&#x438;&#x43D;&#x443;&#x442;=300&#xA0;&#x43C;&#x438;&#x43D;&#x443;&#x442;&#xA0;(5&#xA0;&#x447;&#x430;&#x441;&#x43E;&#x432;&#x00D7;60&#xA0;&#x43C;&#x438;&#x43D;&#x443;&#x442;),(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  
Байт 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>.

Додати коментар


Захисний код
Оновити

EcoMonitoring

ЛІЧІЛЬНИК ВІДВІДУВАННЬ

Сьогодні 492
Вчора386
Цього тижня 878
Минулого тижня 2262
Цей місяць 8436
Минулий місяць 9310
За весь час 172470
Ваш IP: 18.188.171.53
Сегодня: 2025-04-28
Пользователей на сайте: 0
Гостей на сайте: 40