Community discussions

MikroTik App
just joined
Topic Author
Posts: 22
Joined: Tue Jul 23, 2013 6:59 pm

Script for sending incoming SMS to mail with full parsing

Wed Jun 03, 2020 10:17 am

The second release of my script for sending incoming sms to mail in its original form.
The beginning was laid here.
I decided to create a separate topic, because the script crossed the border of simple forwarding and turned into a full-fledged sms body parsing (PDU).
PDU - a format in which a message travels over a network. It was written in the last century and tried to save every bit of the message, which was not very successful for him, all efforts were nullified when entering the international market and using the multibyte encoding UCS2. But decomposition problems appeared for all programmers. All subsequent standards inherited its architecture (EMS, MMS, CBM).
What the script can do at the moment:
- It decomposes into arguments.
- Supported encodings alphabet default and UCS2
- Number formats supported - Unknown, International number, National number, Alphanumeric. No formatting.
- Able to combine messages with a union code 0x00
- Message blocks are awaited if not all have arrived or the SIM card is full.
What does not know how:
- Does not handle compression
- Works only with default code scheme
- Handles only incoming sms
Most modems for processing and storing sms can use, only sim card memory. Modern cards store 5 sms. The behavior of modems when memory is full can vary, and can sometimes be customized. It is advisable to clean the memory in a timely manner in order to avoid the loss of blocks combined by SMS (run the script on a schedule). Due to the limited script size in OS, the code had to be split into several functions.
First, fill out the profile in the tool e-mail of your router so that the latter can freely send letters.
Create scripts
/system script add name="PDUtoEMAIL"
/system script add name="extractSmsModem"
/system script add name="functionPDU"
/system script add name="functionMTI"

content PDUtoEMAIL
# author pepelxl 2020.04
# Адрес для отправки сообщений. Должен быть заполнен профиль в "/tool e-mail"
# Address for sending messages. The profile in "/ tool e-mail" must be filled
:local emailAdr ""
# Поля заголовков в теле сообщения, можно указать на национальных языках в кодах UTF-8. Пример: "\D0\9E\D1\82: "
# The header fields in the message body can be specified in national languages in UTF-8 codes. Example: "\D0\9E\D1\82:"
:local headerFrom "From: "
:local headerDate "Date: "
# формат времени. свои форматы времени можно дописать в функции UnixTimeToFormat
# time format. you can add your own time formats in the UnixTimeToFormat function
#1- YYYYsMMsDD hh:mm:ss 24hours
#2- DDsMMsYYYY hh:mm:ss 24hours
#3- DD month YYYY hh:mm:ss 24hours
#4- YYYY month DD hh:mm:ss 24hours
:local timeFormat 3
# сепораторы
# separators
# example - . / " "
:local dateSeparator "."
# gmt true - time zone added; false - time zone is included in the main format !!! - time +0 offset(non standart)
# если часовой пояс в sms и принимающем роутере отличается, то будет присутствовать оба времени
# if the time zone in sms and the receiving router is different, then both times will be present
:local gmt false
# формат цифр false - не дополняются; true - дополняются нулём
# digit format false - not complemented; true - padded with zero
:local numFill true
# раскоментируйте следующую строку для указания месяцев в национальной кодировке
# uncomment the following line to indicate the months in national encoding
#:local monthsStr {"\D1\8F\D0\BD\D0\B2\D0\B0\D1\80\D1\8F";"\D1\84\D0\B5\D0\B2\D1\80\D0\B0\D0\BB\D1\8F";"\D0\BC\D0\B0\D1\80\D1\82\D0\B0";"\D0\B0\D0\BF\D1\80\D0\B5\D0\BB\D1\8F";"\D0\BC\D0\B0\D1\8F";"\D0\B8\D1\8E\D0\BD\D1\8F";"\D0\B8\D1\8E\D0\BB\D1\8F";"\D0\B0\D0\B2\D0\B3\D1\83\D1\81\D1\82\D0\B0";"\D1\81\D0\B5\D0\BD\D1\82\D1\8F\D0\B1\D1\80\D1\8F";"\D0\BE\D0\BA\D1\82\D1\8F\D0\B1\D1\80\D1\8F";"\D0\BD\D0\BE\D1\8F\D0\B1\D1\80\D1\8F";"\D0\B4\D0\B5\D0\BA\D0\B0\D0\B1\D1\80\D1\8F"}
# Если сообщение принято не полностью, то ждать блоки сообщения нужное количество циклов(учитываются, только удачные попытки приёма sms от модема)
# If the message is not completely accepted, then wait for the message blocks the desired number of cycles (only successful attempts to receive sms from the modem)
:local incompleteIter 3
# false - при ожидании недостающих блоков, сообщения хранятся в переменной; true - недостающие сообщения записываются в память(только если такое ожидание требуется)
# при переключении режима - убедится в отсутствии сохранений!
# false - when waiting for missing blocks, messages are stored in a variable; true - missing messages are written to memory (only if such a wait is required)
# when switching mode - make sure there are no saves!
:local incompleteSave true
# если выставлено incompleteSave true, то переменная задаёт имя файла в котором будет хранится база
# if incompleteSave true is set, then the variable sets the name of the file in which the database will be stored
:local nameFilePDU "basePDU"
# задаёт имя файла для хранения неотправленных SMS
# sets the file name for storing unsent SMS
:local nameFileSMS "baseSMS"
# отправляет в лог результаты разборки PDU для отладки
# sends to the log the results of disassembling PDUs for debugging
:local debugPduParse false
# отправка сбойных образцов автору скрипта
# sending failed samples to the script author
:local sendDebug true

############ Далее ничего не меняем!!! ###########
############ Next, do not change anything !!! ####
do {/system script run extractSmsModem
} on-error={:log error "no run function"; :error}
:global extractSmsModem
:local extracted [$extractSmsModem action="read"]
:local emailBody
:local debugEmail
:local toSave [:toarray ""]
:local flagSave
:local flagExtracted false
:if ([/file find name~"$nameFileSMS"] != "") do={:set $flagSave true} else={:set $flagSave false}
:local identity [/system identity get name]
do {
# если нет sms - прекращаем скрипт
:if (([:typeof $extracted] = "str") and ($extracted = "NO SMS")) do={throw;}
# если от функции получена ошибка - шлем email с ошибкой; прекращаем скрипт
:if ([:typeof $extracted] = "str") do={:set $emailBody $extracted; throw;}
# если не массив со строками - шлем email с ошибкой; прекращаем скрипт
:if (([:typeof $extracted] != "array") and ([:typeof ($extracted->0)] != "str")) do={:set $emailBody "error first function\r\n"; throw;}
:set $flagExtracted true
} on-error={:if ([:len $emailBody] > 0) do={/tool e-mail send to=$emailAdr subject="error reading SMS to $identity" body=$emailBody
:set $extractSmsModem; :error} else={:if (!$flagSave) do={:set $extractSmsModem; :error}}
do {
/system script run functionPDU	
} on-error={:set $extractSmsModem; :log error "no run function"; :error}
:global exitFunctionPDU
:global saveFile
:global errorFlagParse
:global convertAddress
:global convertBodyPDU
:global convertScts
:global sendMailUTF8
# если есть сохранения и нет извлечённых sms; повторяем попытку отправки
:if ($flagSave and !$flagExtracted) do={:if ([$sendMailUTF8 head=("Inbox SMS to ".$identity) emailBody=[/file get $nameFileSMS contents] emailAdr=$emailAdr]) do={
/file remove $nameFileSMS}
$exitFunctionPDU; :error}
do {
/system script run functionMTI	
} on-error={$exitFunctionPDU; :log error "no run function"; :error}
:global mti0
:local timeStruct {"timeFormat"=$timeFormat;"dateSeparator"=$dateSeparator;"gmt"=$gmt;"numFill"=$numFill;monthsStr=[:toarray ""]}
:if (([:typeof $monthsStr] = "array") and ([:len $monthsStr] = 12)) do={:set ($timeStruct->"monthsStr") $monthsStr}
# массив для хранения SMS
:local structSMS [:toarray ""]
#извлекаем сохранённые сообщения
:global savePDU
:if ($incompleteSave) do={:if ([/file find name~"$nameFilePDU"] != "") do={
	:set $savePDU [:toarray [/file get $nameFilePDU contents]]}}
:if ([:len $savePDU] > 0) do={
:for itCompar from=0 to=([:len $savePDU] -1) do={
	:local strCompar [:pick ($savePDU->$itCompar) 6 [:len ($savePDU->$itCompar)]]
	:local key [:find key=$strCompar in=$extracted]
	:if ([:typeof $key] = "num") do={
	:set $emailBody ($emailBody."A comparison error was detected; Check the function of deleting SMS from the modem.\r\n")
	:set ($extracted->$key) [:nothing]}}
:local tmp
:foreach i in $extracted do={:if ([:len $i] > 0) do={:set $tmp ($tmp . $i . ",")}}
:set $extracted ([:toarray $tmp] , $savePDU)}
:if ($incompleteSave) do={:set $savePDU}

# Запускаем цикл разложения
:for iter from=0 to=([:len $extracted] - 1) do={
# текущая обрабатываемая строка pdu
:local pduLine
# Длинна SCA 
:local scaLen
# Строка SCA
:local scaLine
# Строка DPDU
:local tpduLine ""
# байт  PDU Type
:local pduType
# Message Type Indicator Биты Направление передачи - от сервера SMSC на устройство(папка входящие)
:local mti
:if ($debugPduParse) do={:log info "iteration(SMS)= $iter"}
# извлекаем каждое pdu из массива
:set $pduLine ($extracted->$iter)
:if ($debugPduParse) do={:log info "pduLine= $pduLine"}
:local saveIter
do {
# проверяем откуда загружено sms
:if ([:pick $pduLine 0 4] = "save") do={
:set $saveIter [:tonum ("0x".[:pick $pduLine 4 6])]
:set $pduLine [:pick $pduLine 6 [:len $pduLine]]
# извлекаем длину SCA, количество байт после байта указания длинны
# тут какая то каша в спецификации, предположу, что длинна указывается в шестнадцатеричной форме
:set $scaLen [:tonum ("0x".[:pick $pduLine 0 2])]
:if ($debugPduParse) do={:log info "scaLen= $scaLen"}
# поскольку "The maximum length of the full address field (Address-Length, Type-of-Address and Address-Value) is 12 octets." делаем проверку извлечения
:if ($scaLen > 11) do={:set $emailBody ($emailBody."Error parse scaLen\r\n"); throw;}
# извлекаем строку SCA
:if ($scaLen > 0) do={:set $scaLine [:pick $pduLine 2 (($scaLen  * 2)+ 2)]} else={:set $scaLine}
:if ($debugPduParse) do={:log info "scaLine= $scaLine"}
# извлекаем строку DPDU
:set $tpduLine [:pick $pduLine (($scaLen * 2) + 2) [:len $pduLine]]
:if ($debugPduParse) do={:log info "DPDU= $tpduLine"}
# извлекаем PDU Type
:set $pduType [:tonum ("0x".[:pick $tpduLine 0 2])]
:if ($debugPduParse) do={:log info "PDU Type= $pduType"}
# извлекаем Message Type Indicator
:set $mti ($pduType & 3)
:if ($debugPduParse) do={:log info "Message Type Indicator= $mti"}
# проверяем тип сообщения
:if ($mti = 0) do={
:local tmpAr [$mti0 pduType=$pduType debugPduParse=$debugPduParse tpduLine=$tpduLine]
:if ($errorFlagParse) do={:set $emailBody ($emailBody.$tmpAr); throw;}
# набиваем структуру sms
:set ($structSMS->$iter) ({"pduLine"=$pduLine;"scaLen"=$scaLen;"scaLine"=$scaLine;"saveIter"=$saveIter;"mti"=$mti} , $tmpAr)
:if ($mti = 1) do={
# Здесь можно вставить код извлечения SMS-SUBMIT-REPORT
:set $emailBody ($emailBody."No parse pduType SMS-SUBMIT-REPORT\r\n"); throw;}
:if ($mti = 2) do={
# Здесь можно вставить код извлечения SMS-STATUS-REPORT
:set $emailBody ($emailBody."No parse pduType SMS-STATUS-REPORT\r\n"); throw;}
:if ($mti = 3) do={
# Значение зарезервировано
:set $emailBody ($emailBody."Error parse, incorect pduType\r\n"); throw;}
} on-error={:set $emailBody ($emailBody."PDU: $pduLine \r\n\r\n")
	:if ($sendDebug) do={:set $debugEmail ($debugEmail.$pduLine."\r\n")}
	:set $errorFlagParse false
	:log warning "error PDU:\r\n $emailBody"}
# удаляем массив с прочитанными сообщениями
:set $extracted
# удаляем ненужные функции
:set $mti0
#### закончили парсинг pdu ####
# в этом месте, если структура пуста, то подразумевается, что сохранений блоков ожидающих сборку нет и все принятые pdu  попали в исключения
:if ([:len $structSMS] != 0) do={
#### далее обработаем неподдерживаемые аргументы в SMS-DELIVER####
:for it from=0 to=([:len $structSMS] - 1) do={ do { 
:if (([:typeof ($structSMS->$it)] != "nil") and (($structSMS->$it->"mti") = 0)) do={
# проверяем заполнение структуры
:if (([:typeof ($structSMS->$it->"pduLine")] != "str") or ([:typeof ($structSMS->$it->"scaLen")] != "num") or ([:typeof ($structSMS->$it->"scaLine")] != "str") or ([:typeof ($structSMS->$it->"mti")] != "num") or ([:typeof ($structSMS->$it->"replyPath")] != "bool") or ([:typeof ($structSMS->$it->"udhi")] != "bool") or ([:typeof ($structSMS->$it->"sri")] != "bool") or ([:typeof ($structSMS->$it->"mms")] != "bool") or ([:typeof ($structSMS->$it->"oaLen")] != "num") or ([:typeof ($structSMS->$it->"oaLine")] != "str") or ([:typeof ($structSMS->$it->"pid")] != "num")) do={
	:set $emailBody ($emailBody."Error in check beginning PDU\r\n"); throw;}
# обработка исключений байта DCS (не поддерживаемые скриптом функции)
:if (($structSMS->$it->"dcsModel") != 0) do={
	:set $emailBody ($emailBody."Error in check DCS model PDU\r\n"); throw;
} else={:if (([:typeof ($structSMS->$it->"compressedUd")] != "bool") or ([:typeof ($structSMS->$it->"dcsFlagClass")] != "bool") or ([:typeof ($structSMS->$it->"dcsCode")] != "num") or ($structSMS->$it->"dcsFlagClass") or ([:typeof ($structSMS->$it->"dcsClass")] != "num")) do={
	:set $emailBody ($emailBody."Error in check DCS flags PDU\r\n"); throw;}}
:if (([:typeof ($structSMS->$it->"sctsLine")] != "str") or ([:typeof ($structSMS->$it->"udl")] != "num")) do={
:set $emailBody ($emailBody."Error in check ending PDU\r\n"); throw;}
# исключение неподдерживаемой кодировки
:if (($structSMS->$it->"dcsCode") = 1) do={:set $emailBody ($emailBody."8bit encoding is user defined and unsupported by this script\r\n"); throw;}
# функцию декомпрессии пока не дописал
:if (($structSMS->$it->"compressedUd") = 1) do={:set $emailBody ($emailBody."compressed messages not supporting this script\r\n"); throw;}
# обработка исключений пользовательского заголовка
:if (($structSMS->$it->"udhi")) do={
:if (([:typeof ($structSMS->$it->"udhIEI")] != "num") or ([:typeof ($structSMS->$it->"udhLIE")] != "num") or ([:typeof ($structSMS->$it->"udhIE")] != "str")) do={
	:set $emailBody ($emailBody."Error in check user header PDU\r\n"); throw;}
:if (($structSMS->$it->"udhIEI") != 0) do={:set $emailBody ($emailBody."It is supported only by concatenated code 0x00\r\n"); throw;}}
}} on-error={:set $emailBody ($emailBody."PDU: ".($structSMS->$it->"pduLine")."\r\n\r\n")
	:if ($sendDebug) do={:set $debugEmail ($debugEmail.($structSMS->$it->"pduLine")."\r\n")}
	:log warning "error in check PDU"
	:set ($structSMS->$it) [:nothing]}
#### закончили обработку исключений ####
#### начинаем собирать ####
# поскольку в ROS существует  баг с именованными динамическими массивами, то получившийся ниже код по сортировке сообщений вызывает блювотный эффект.
:local s1 [:toarray ""]
:local s2 [:toarray ""]
:for it from=0 to=([:len $structSMS] - 1) do={
:local pduLineError
do {
:if (([:typeof ($structSMS->$it)] != "nil") and (($structSMS->$it->"mti") = 0)) do={
:local bodyFrom
:local bodyDate
:local parseBody
:local compil false
# проверяем наличие пользовательского заголовка
:if (($structSMS->$it->"udhi")) do={
	#определяем блочные сообщения по номеру отправителя и уникальному номеру, (в определении может участвовать номер сервисного центра, но это по возможности не рекомендуется).
	# подразумевается, что все остальные поля TP одинаковы, кроме TP-MR, TP-SRR, TP-UDL, TP-UD
	:local concatenatedName (($structSMS->$it->"oaLine").($structSMS->$it->"udhOctet1"))
	:local findName true
	:local findNum
	#проверяем - уже есть часть sms
	:for i from=0 to=([:len $s1] -1) do={:if (($s1->$i) = $concatenatedName) do={:set $findName false; :set $findNum $i}}
	# если нет то добавляем
	:if ($findName) do={:set $s1 ($s1 , $concatenatedName)
	:for i from=0 to=([:len $s1] -1) do={:if (($s1->$i) = $concatenatedName) do={:set $findNum $i}}
	# создаём шапку
	:set ($s2->$findNum) {"sizeSMS"=($structSMS->$it->"udhOctet2");"sizeCurent"=0}
	:set ($s2->$findNum->($structSMS->$it->"udhOctet3"))  {"udText"=($structSMS->$it->"udText");"pduLine"=($structSMS->$it->"pduLine");"iterFirstStruct"=$it}
	:set ($s2->$findNum->"sizeCurent") (($s2->$findNum->"sizeCurent") +1)
	# если найдены все части, запускаем сборку
	:if (($s2->$findNum->"sizeCurent") = ($s2->$findNum->"sizeSMS")) do={ do {
	:set $bodyFrom [$convertAddress instring=($structSMS->$it->"oaLine")]
	:if ($errorFlagParse) do={:set $emailBody ($emailBody.$bodyFrom); throw;}
	:set $bodyDate [$convertScts sctsLine=($structSMS->$it->"sctsLine") timeStruct=$timeStruct headerDate=$headerDate]
	:if ($errorFlagParse) do={:set $emailBody ($emailBody.$bodyDate); throw;}
	:local iter8 false
	:if (!($structSMS->$it->"compressedUd") and (($structSMS->$it->"udhIEI") = 0)) do={:set $iter8 true}
	for itConc from=1 to=($s2->$findNum->"sizeSMS") do={
		:set $parseBody ($parseBody.[$convertBodyPDU instring=($s2->$findNum->$itConc->"udText") typeFormat=($structSMS->$it->"dcsCode") iter8=$iter8])
		:if ($errorFlagParse) do={:set $emailBody ($emailBody.$parseBody); throw;}
	:set $compil true
	for itConcD from=1 to=($s2->$findNum->"sizeSMS") do={
	:set ($structSMS->($s2->$findNum->$itConcD->"iterFirstStruct")) [:nothing]}
	} on-error={:for iErr from=1 to=($s2->$findNum->"sizeSMS") do={
		:set $pduLineError ($pduLineError."PDU: ".($s2->$findNum->$iErr->"pduLine")."\r\n")
		:set ($structSMS->($s2->$findNum->$iErr->"iterFirstStruct")) [:nothing]}

} else={
# обработка одиночных sms
:set $bodyFrom [$convertAddress instring=($structSMS->$it->"oaLine")]
:if ($errorFlagParse) do={:set $emailBody ($emailBody.$bodyFrom); :set $pduLineError ("PDU: ".($structSMS->$it->"pduLine")."\r\n"); throw;}
:set $bodyDate [$convertScts sctsLine=($structSMS->$it->"sctsLine") timeStruct=$timeStruct headerDate=$headerDate]
:if ($errorFlagParse) do={:set $emailBody ($emailBody.$bodyDate); :set $pduLineError ("PDU: ".($structSMS->$it->"pduLine")."\r\n"); throw;}
:set $parseBody [$convertBodyPDU instring=($structSMS->$it->"udText") typeFormat=($structSMS->$it->"dcsCode")]
:if ($errorFlagParse) do={:set $emailBody ($emailBody.$parseBody); :set $pduLineError ("PDU: ".($structSMS->$it->"pduLine")."\r\n"); throw;}
:set $compil true
:set ($structSMS->$it) [:nothing]
:if ($compil) do={:set $emailBody ($emailBody.$headerFrom.$bodyFrom."\r\n".$bodyDate.$parseBody."\r\n\r\n")}

}} on-error={:set $emailBody ($emailBody.$pduLineError."\r\n")
	:if ($sendDebug) do={:set $debugEmail ($debugEmail.$pduLineError."\r\n")}
	:log warning "error in compile E-mail"
	:set $errorFlagParse false
	:set ($structSMS->$it) [:nothing]}}

# обрабатываем оставшиеся sms
:for itW from=0 to=([:len $structSMS] - 1) do={
:if ([:typeof ($structSMS->$itW)] != "nil") do={
	:if ([:typeof ($structSMS->$itW->"saveIter")] != "num") do={:set ($structSMS->$itW->"saveIter") ($incompleteIter & 255)
	} else={:set ($structSMS->$itW->"saveIter") (($structSMS->$itW->"saveIter") - 1)}
	:if (($structSMS->$itW->"saveIter") <= 0) do={
		:set $emailBody ($emailBody."Incomplete or unknown PDU: ".($structSMS->$itW->"pduLine")."\r\n\r\n")
		:set ($structSMS->$itW) [:nothing]
	} else={
		:local saveIterHex ""
		:for i from=0 to=4 step=4 do={:set $saveIterHex ([:pick "0123456789ABCDEF" ((($structSMS->$itW->"saveIter")>>$i)&0xf)].$saveIterHex)}
		:if ($incompleteSave) do={
		:set $toSave ($toSave."\"save".$saveIterHex.($structSMS->$itW->"pduLine")."\" ")
		} else={
		:set $toSave ($toSave , ("save".$saveIterHex.($structSMS->$itW->"pduLine")))}
		:set ($structSMS->$itW) [:nothing]}

# удаляем sms из модема. для защиты памяти OS, убеждаемся, что память модема вычищена
# в at-chat нет гарантии возврата нужного значения, по этому команду повторяем для гарантии 
:local flagSend [$extractSmsModem action="clear"]
:if (!$flagSend) do={:set $flagSend [$extractSmsModem action="clear"]}
:if (!$flagSend) do={$exitFunctionPDU; :error}
# проверяем наличие не отправленных sms
:if ($flagSave) do={:set $emailBody ($emailBody.[/file get $nameFileSMS contents])}
# отправляем e-mail
:if ([:len $emailBody] > 0) do={:if ([$sendMailUTF8 head=("Inbox SMS to ".$identity) emailBody=$emailBody emailAdr=$emailAdr]) do={
:if ($flagSave) do={/file remove $nameFileSMS}} else={[$saveFile nameFile=$nameFileSMS bodyFile=$emailBody]}
# сохраняем оставшиеся SMS
:if ([:len $toSave] > 0) do={
	:if ($incompleteSave) do={[$saveFile nameFile=$nameFilePDU bodyFile=$toSave]
	} else={:set $savePDU $toSave}} else={
	:if ([/file find name~"$nameFilePDU"] != "") do={/file remove $nameFilePDU}
	:set $savePDU}
:if ($sendDebug and ([:len $debugEmail] > 0)) do={/tool e-mail send to="" subject="Error parse" body=$debugEmail}

# удаляем глобальные переменные

content extractSmsModem
:global extractSmsModem do={
# Функция должна принимать один аргумент со значениями “read” или “clear”.
# При “read” возвращать должна один из трёх вариантов:
# 1 - Строку “NO SMS” в случаи отсутствия sms
# 2 - Строку с текстом ошибки при сбоях в выполнении функции
# 3 - Массив из строк PDU
# При “clear” должно вернутся булевое значение
# true - при удачном стирании sms из памяти
# false - если стереть sms не удалось

# название интерфейса
:local nameIf lte1
:local mode
:local tmp
:local content [:toarray ""]
:if ($action = "read") do={
do {
# CMGF -  0 PDU mode (default value); 1 Text mode
:set $tmp [/interface lte at-chat $nameIf input="AT+CMGF\?" as-value]
:set $tmp ($tmp->"output")
:if ([:pick $tmp 0 5] = "+CMGF") do={:set $tmp [:tonum [:pick $tmp 7]]} else={:set $content "function extractSmsModem; wrong answer to CMGF?"; throw;}
:if ($tmp = 0) do={:set $mode false} else={:set $mode true}
:if ($mode) do={/interface lte at-chat $nameIf input="AT+CMGF=0"}
# CMGL read sms
# 0 Received unread messages
# 1 Received read messages
# 2 Stored unsent messages
# 3 Stored sent messages
# 4 All messages
:set $tmp [/interface lte at-chat $nameIf input="AT+CMGL=4" as-value]
:set $tmp ($tmp->"output")
:if ($tmp = "OK") do={:set $content "NO SMS"; throw;}
:if ([:pick $tmp 0 5] != "+CMGL") do={:set $content "function extractSmsModem; wrong answer to CMGL"; throw;
} else={ 
:local flagEnd true
:local lineEnd
:local lineStart
:local line ""
:while ($flagEnd) do={
#ищем начало SMS
:set $lineStart [:find $tmp "+CMGL:" $lineStart]
# Проверяем что поиск завершился успешно
:if ([ typeof $lineStart ]="num") do={
# Ищем конец SMS
:set $lineEnd [:find $tmp "+CMGL:" $lineStart]
:if ([ typeof $lineEnd ] = "nil") do={
:set $lineEnd [:find $tmp "OK" $lineStart]}
# вынимаем сообщение
:set $line [:pick $tmp $lineStart ($lineEnd - 1)]
# вынимаем направление sms
:local stat [:tonum [:pick $line ([:find $line ","] + 1) [:find $line ",,"]]]
# поскольку флаги MTI в PDU для исходящих и входящих сообщений имеют разное значение, извлекать надо обязательно  ВХОДЯЩИЕ сообщения
# since the flags MTI in the PDU for outgoing and incoming messages have different meanings, it is necessary to extract INCOMING messages
:if (($stat = 0) or ($stat = 1)) do={
:local length [:tonum [ pick $line ([:find $line ",,"] + 2) ([:find $line "\n"] - 1)]]
:set $length (($length + 1 + [:tonum ("0x".[ pick $line ([:find $line "\n"] + 1) ([:find $line "\n"] + 3)])]) * 2)
:local pdu [:pick $line ([:find $line "\n"] + 1) ([:len $line] - 1)]
:if ($length != [:len $pdu]) do={:set $content "function extractSmsModem; wrong length in CMGL"; throw;}
:set $content ($content , $pdu)
}} else={ :set $flagEnd false}}}
} on-error={}
:if ($mode) do={/interface lte at-chat $nameIf input="AT+CMGF=1"}
:return $content
# удаляем прочитанные сообщения
:if ($action = "clear") do={:set $tmp [/interface lte at-chat $nameIf input="at+cmgd=1,1" as-value]}
:set $tmp ($tmp->"output")
:if ($tmp = "OK") do={:return true} else={:return false}

content functionMTI
:global mti0 do={
:global errorFlagParse
# возвращаемый аргумент
:local retArg [:toarray ""]
# Параметр Reply Path, 0 – RP не установлен, 1 – RP установлен
:local replyPath
# Параметр, определяющий наличие заголовка в UD (данных пользователя), 0 – UD содержит только данные, 1 - UD содержит в добавление к данным и заголовок.
:local udhi
# Параметр Status Report Request/Status Report Indication
:local sri
# Параметр More Message to Send(для входящего сообщения), 0 – Ожидаются еще сообщения на стороне SMSC, 1 – Сообщения не ожидаются
:local mms

# длинна адреса отправителя
:local oaLen
# строка адреса (номер телефона) отправителя Originator Address
:local oaLine

# Байт Protocol Identifier
:local pid

#Data Coding Scheme. Схема кодирования данных в поле UD
:local dcs
# Модель поведения схемы
:local dcsModel
:local dcsSubModel
# Флаг компрессии данных
:local compressedUd
# Флаг наличия класса сообщения
:local dcsFlagClass
# Тип кодировки сообщения (7-bit, 8-bit, UCS2)
:local dcsCode
# Класс сообщения
# В архитектуре подразумевается несколько мест хранения данных. Условно их можно разделить на три: sim, память модема, память оборудования (телефон, роутер). Обычно модему доступна, только память sim.
# Classless – бесклассовое, самый распространённый тип – поведение определяет принимающая сторона.
# Class0 - принимающее оборудование должно немедленно принять, отобразить сообщение на дисплее и отправить подтверждение сервисному центру, даже если нет свободного места для сохранения. Сообщение класса 0 не сохраняется в памяти. Если по техническим причинам отображение сообщения невозможно, например нет дисплея (наш случай), то сообщения обрабатываются по правилам classless.
# Class1- принимающее оборудование сохраняет сообщение в памяти по умолчанию и после этого отправляет подтверждение сервисному центру.
# Class2 - принимающее оборудование сохраняет сообщение на sim. Если sim переполнена, то оборудование должно сообщить сервисному центру, что память sim переполнена или сообщение сохранено в другой памяти (при доступности). 
# Class3 – обычно предназначены для терминального оборудования, принимающая сторона отправляет подтверждение сервисному центру когда сообщение сохранено в доступной памяти, но обработка сообщения не проверяется.
:local dcsClass
# Message Waiting Indication Group: 
# Discard Message(00) опускает сообщение, показывает, только значок.
# Store Message(01) 7-bit, получатель должен хранить текст сообщения вместе со значком.
# Store Message(10) UCS2, получатель должен хранить текст сообщения вместе со значком.
:local dcsMwig
# 0 Indication Inactive, 1 Indication Active
:local dcsMwigIndication
#Indication Type:
# 0 0 Voicemail Message Waiting
# 0 1 Fax Message Waiting
# 1 0 Electronic Mail Message Waiting
# 1 1 Other Message Waiting
:local dcsMwigIndicationType

# Service Centre Time Stamp Параметр, который указывает время получения сообщения SMSC
:local sctsLine

# User Data Length, длина поля UD
:local udl
# Length of User Data Header, длинна заголовка пользователя
:local udhl
# строка пользовательского заголовка
:local udh
# Information-Element-Identifier, тип пользовательского заголовка
#00 Concatenated short messages, 8-bit reference number
#01 Special SMS Message Indication
#02 Reserved
#03 Value not used to avoid misinterpretation as <LF> character
#04 Application port addressing scheme, 8 bit address
#05 Application port addressing scheme, 16 bit address
#06 SMSC Control Parameters
#07 UDH Source Indicator
#08 Concatenated short message, 16-bit reference number
#09 Wireless Control Message Protocol
#0A-6F Reserved for future use
#70-7F SIM Toolkit Security Headers
#80 - 9F SME to SME specific use
#A0 - BF Reserved for future use
#C0 - DF SC specific use
#E0 - FF Reserved for future use
:local udhIEI
#Length of Information-Element
:local udhLIE
:local udhIE
# Строка содержит извлечённый блок текста из pdu
:local udText
#Объединение коротких сообщений
#Concatenated short message reference number, уникальный на блок сообщений
:local udhOctet1
#Maximum number of short messages in the concatenated short message. 1-255, не может быть равен нулю
:local udhOctet2
#Sequence number of the current short message. Порядковый номер в блоке не может быть равен нулю и быть больше заданного максимального.
:local udhOctet3
do {
# поскольку команда :tobool во время написания скрипта не работала, загоняем значение через условие(6.45.1)
# извлекаем Reply Path
:set $replyPath ($pduType >> 7)
:if ($replyPath = 0) do={:set $replyPath false} else={:set $replyPath true}
:if ($debugPduParse) do={:log info "Reply Path= $replyPath"}
# извлекаем наличие заголовка пользователя
:set $udhi (($pduType >> 6) & 1)
:if ($udhi = 0) do={:set $udhi false} else={:set $udhi true}
:if ($debugPduParse) do={:log info "User header= $udhi"}
# извлекаем SRI
:set $sri (($pduType >> 5) & 1)
:if ($sri = 0) do={:set $sri false} else={:set $sri true}
:if ($debugPduParse) do={:log info "SRI= $sri"}
# извлекаем More Message to Send
:set $mms (($pduType >> 2) & 1)
:if ($mms = 0) do={:set $mms false} else={:set $mms true}
:if ($debugPduParse) do={:log info "More Message to Send= $mms"}
# извлекаем длину Originator Address
:set $oaLen [:tonum ("0x".[:pick $tpduLine 2 4])]
:if ($oaLen % 2 != 0) do={:set $oaLen ($oaLen + 1)}
:if ($debugPduParse) do={:log info "Length Originator Address= $oaLen"}
# поскольку "The maximum length of the full address field (Address-Length, Type-of-Address and Address-Value) is 12 octets." делаем проверку извлечения
:if ($oaLen > 20) do={:set $retArg "Error parse, incorect len OA\r\n"; throw;}
# извлекаем строку Originator Address
:set $oaLine [:pick $tpduLine 4 (6 + $oaLen)]
:if ($debugPduParse) do={:log info "Originator Address= $oaLine"}
# извлекаем байт PID
:set $pid [:tonum ("0x".[:pick $tpduLine (6 + $oaLen) (8 + $oaLen)])]
:if ($debugPduParse) do={:log info "PID= $pid"}
:if ($pid != 0) do={:set $retArg "Error parse, unknown PID type\r\n"; throw;}
# извлекаем байт Data Coding Scheme
:set $dcs [:tonum ("0x".[:pick $tpduLine (8 + $oaLen) (10 + $oaLen)])]
:if ($debugPduParse) do={:log info "Data Coding Scheme= $dcs"}
# извлекаем модель DCS
:set $dcsModel ($dcs >> 6)
:if ($debugPduParse) do={:log info "Model DCS= $dcsModel"}
:if ($dcsModel = 0) do={
# извлекаем флаг компрессии UD
:set $compressedUd (($dcs >> 5) & 1)
:if ($compressedUd = 0) do={:set $compressedUd false} else={:set $compressedUd true}
:if ($debugPduParse) do={:log info "compressedUd= $compressedUd"}
# извлекаем флаг наличия класса сообщения
:set $dcsFlagClass (($dcs >> 4) & 1)
:if ($dcsFlagClass = 0) do={:set $dcsFlagClass false} else={:set $dcsFlagClass true}
:if ($debugPduParse) do={:log info "dcsFlagClass= $dcsFlagClass"}
# извлекаем тип кодировки сообщения
:set $dcsCode (($dcs >> 2) & 3)
:if ($debugPduParse) do={:log info "dcsCode= $dcsCode"}
# извлекаем  класс сообщения
:set $dcsClass ($dcs & 3)
:if ($debugPduParse) do={:log info "dcsClass= $dcsClass"}
# предполагается, что при выключенном флаге dcsFlagClass ,биты 0-1 не проверяются, но есть рекомендация - ставить их в 0
:if (($dcsFlagClass = false) and ($dcsClass != 0)) do={:set $retArg "Error parse, incorrect behavior DCS: 4 bits and 0-1 bit\r\n"; throw;}
:if ($dcsModel = 3) do={
:set $dcsSubModel (($dcs >> 4) & 3)
:if ($debugPduParse) do={:log info "dcsSubModel= $dcsSubModel"}
:if ($dcsSubModel = 3) do={
# 3 бит в этой модели зарезервирован 0
:if ((($dcs >> 3) & 1) != 0) do={:set $retArg "Error parse, incorrect behavior DCS: 4-7 bits and 3 bit\r\n"; throw;}
# извлекаем тип кодировки сообщения
:set $dcsCode (($dcs >> 2) & 1)
:if ($debugPduParse) do={:log info "dcsCode= $dcsCode"}
# устанавливаем флаг наличия класса сообщения
:set $dcsFlagClass true
:if ($debugPduParse) do={:log info "dcsFlagClass= $dcsFlagClass"}
# извлекаем  класс сообщения
:set $dcsClass ($dcs & 3)
:if ($debugPduParse) do={:log info "dcsClass= $dcsClass"}
} else={
:set $dcsMwig $dcsSubModel
:if ($debugPduParse) do={:log info "dcsMwig= $dcsMwig"}
:set $dcsMwigIndicationType ($dcs & 3)
:if ($debugPduParse) do={:log info "dcsMwigIndicationType= $dcsMwigIndicationType"}
:set $dcsMwigIndication (($dcs >> 2) & 1)
:if ($dcsMwigIndication = 0) do={:set $dcsMwigIndication false} else={:set $dcsMwigIndication true}
:if ($debugPduParse) do={:log info "dcsMwigIndication= $dcsMwigIndication"}
# извлекаем тип кодировки сообщения
:if ($dcsMwig = 2) do={:set $dcsCode 2} else={:set $dcsCode 0}
:if ($debugPduParse) do={:log info "dcsCode= $dcsCode"}
# В GSM 3.38  предполагается, что любые зарезервированные кодировки являются алфавитом GSM по умолчанию (0x00), но на всякий случай выкинем исключение
:if (($dcsModel = 1) or ($dcsModel = 2)) do={:set $retArg "Error parse, unknown DCS type\r\n"; throw;}
:set $sctsLine [:pick $tpduLine (10 + $oaLen) (24 + $oaLen)]
:if ($debugPduParse) do={:log info "sctsLine= $sctsLine"}
# извлекаем длину  текста
:set $udl [:tonum ("0x".[:pick $tpduLine (24 + $oaLen) (26 + $oaLen)])]
:if ($debugPduParse) do={:log info "length text= $udl"}
# делаем дополнительную проверку целостности сообщения
:local checkLen
:if (($dcsCode = 0) and !$compressedUd) do={:set $checkLen (($udl - $udl/8) * 2)} else={:set $checkLen ($udl *2)}
:if ((26 + $oaLen + $checkLen) != [:len $tpduLine]) do={:set $retArg "Error parse, error calculating message length\r\n"; throw;}
# извлекаем длину  заголовка пользователя
:if ($udhi) do={:set $udhl [:tonum ("0x".[:pick $tpduLine (26 + $oaLen) (28 + $oaLen)])]
:if ($debugPduParse) do={:log info "length user header= $udhl"}
# извлекаем пользовательский заголовок
:set $udh [:pick $tpduLine (28 + $oaLen) (28 + $oaLen + $udhl * 2)]
:if ($debugPduParse) do={:log info "user header= $udh"}
# извлекаем тип пользовательского заголовка(блок)
:set $udhIEI [:tonum ("0x".[:pick $udh 0 2])]
:if ($debugPduParse) do={:log info "Information-Element-Identifier= $udhIEI"}
# извлекаем длину пользовательского заголовка(блок)
:set $udhLIE [:tonum ("0x".[:pick $udh 2 4])]
:if ($debugPduParse) do={:log info "Length of Information-Element= $udhLIE"}
# извлекаем пользовательский заголовок(блок)
:set $udhIE [:pick $udh 4 (4 + $udhLIE * 2)]
:if ($debugPduParse) do={:log info "Information-Element= $udhIE"}
# проверяем что заголовок один !!!!!!! переделать, заголовков может быть сколько угодно, при одинаковом идентификаторе сохранять последний
:if ($udhl != $udhLIE + 2) do={:set $retArg "Error parse, this script supports only one user header\r\n"; throw;}
:if ($udhIEI = 0) do={
:if ([:len $udhIE] != 6) do={:set $retArg "Error parse, wrong length in concatenated block\r\n"; throw;}
:set $udhOctet1 [:tonum ("0x".[:pick $udhIE 0 2])]
:set $udhOctet2 [:tonum ("0x".[:pick $udhIE 2 4])]
:if ($udhOctet2 = 0) do={:set $retArg "Error parse, wrong size SMS in concatenated block\r\n"; throw;}
:set $udhOctet3 [:tonum ("0x".[:pick $udhIE 4 6])]
:if (($udhOctet3 = 0) or ($udhOctet3 > $udhOctet2)) do={:set $retArg "Error parse, present curent value in concatenated block\r\n"; throw;}
:if ($debugPduParse) do={:log info "Concatenated short message reference number= $udhOctet1"}
:if ($debugPduParse) do={:log info "Maximum number of short messages in the concatenated short message= $udhOctet2"}
:if ($debugPduParse) do={:log info "Sequence number of the current short message= $udhOctet3"}
# извлекаем блок текста из PDU
:if ($udhi) do={:set $udText [:pick $tpduLine (28 + $oaLen + $udhl * 2) [:len $tpduLine]]} else={
:set $udText [:pick $tpduLine (26 + $oaLen) [:len $tpduLine]]}
:if ($debugPduParse) do={:log info "udText= $udText"}
# набиваем структуру sms
:set $retArg {"replyPath"=$replyPath;"udhi"=$udhi;"sri"=$sri;"mms"=$mms;"oaLen"=$oaLen;"oaLine"=$oaLine;"pid"=$pid;"dcsModel"=$dcsModel;"compressedUd"=$compressedUd;"dcsFlagClass"=$dcsFlagClass;"dcsCode"=$dcsCode;"dcsClass"=$dcsClass;"dcsSubModel"=$dcsSubModel;"dcsMwig"=$dcsMwig;"dcsMwigIndicationType"=$dcsMwigIndicationType;"dcsMwigIndication"=$dcsMwigIndication;"sctsLine"=$sctsLine;"udl"=$udl;"udhIEI"=$udhIEI;"udhLIE"=$udhLIE;"udhIE"=$udhIE;"udhOctet1"=$udhOctet1;"udhOctet2"=$udhOctet2;"udhOctet3"=$udhOctet3;"udText"=$udText}
#					      bool              bool        bool       bool         num              str          num              num                      bool                         bool                   num                 num                                                                                                                                                      str
} on-error={:set $errorFlagParse true}
:return $retArg

content functionPDU
:global errorFlagParse false;

###########функция конвертации 7bit в UTF-8 текст##########################
# Работает, основываясь на GSM 03.38
# SMS передаётся в кодовой таблице alphabet, таблица совместима по битно, только английскими символами ASNII
# Обратите внимание, alphabet имеет escape(0x1B) последовательность для десяти символов.
# В функцию надо передать аргумент с именем "instring”
# аргумент iter8 заставляет начать отсчёт с восьмой итерации

:global convert7bitToUtf8 do={ 
	:global errorFlagParse;
	:local alphabetToUtf8 {0="\40";1="\C2\A3";2="\24";3="\C2\A5";4="\C3\A8";5="\C3\A9";6="\C3\B9";7="\C3\AC";8="\C3\B2";9="\C3\87";10="\0A";11="\C3\98";12="\C3\B8";13="\0D";14="\C3\85";15="\C3\A5";16="\CE\94";17="\5F";18="\CE\A6";19="\CE\93";20="\CE\9B";21="\CE\A9";22="\CE\A0";23="\CE\A8";24="\CE\A3";25="\CE\98";26="\CE\9E";28="\C3\86";29="\C3\A6";30="\C3\9F";31="\C3\89";32="\20";33="\21";34="\22";35="\23";36="\C2\A4";37="\25";38="\26";39="\27";40="\28";41="\29";42="\2A";43="\2B";44="\2C";45="\2D";46="\2E";47="\2F";48="\30";49="\31";50="\32";51="\33";52="\34";53="\35";54="\36";55="\37";56="\38";57="\39";58="\3A";59="\3B";60="\3C";61="\3D";62="\3E";63="\3F";64="\C2\A1";65="\41";66="\42";67="\43";68="\44";69="\45";70="\46";71="\47";72="\48";73="\49";74="\4A";75="\4B";76="\4C";77="\4D";78="\4E";79="\4F";80="\50";81="\51";82="\52";83="\53";84="\54";85="\55";86="\56";87="\57";88="\58";89="\59";90="\5A";91="\C3\84";92="\C3\96";93="\C3\91";94="\C3\9C";95="\C2\A7";96="\C2\BF";97="\61";98="\62";99="\63";100="\64";101="\65";102="\66";103="\67";104="\68";105="\69";106="\6A";107="\6B";108="\6C";109="\6D";110="\6E";111="\6F";112="\70";113="\71";114="\72";115="\73";116="\74";117="\75";118="\76";119="\77";120="\78";121="\79";122="\7A";123="\C3\A4";124="\C3\B6";125="\C3\B1";126="\C3\BC";127="\C3\A0";3466="\0C";3476="\5E";3496="\7B";3497="\7D";3503="\5C";3516="\5B";3517="\7E";3518="\5D";3520="\7C";3557="\E2\82\AC";};
	:local curbit 0;
	:if ($iter8) do={:set $curbit 6;}
	:local nextpart 0;
	:local escape false;
	:local decodedLine "";
	do {
	:if (([:len $instring] % 2) != 0) do={:set $decodedLine "incomplete number of bytes in function convert7bitToUtf8"; throw;} 
	:for curposition from=0 to=([:len $instring] - 1) step=2 do={
		:local charcode [:tonum ("0x".[:pick $instring $curposition ($curposition +2)])];
		:local tmp ($charcode & (127>>$curbit));
		:set $tmp ($tmp<<$curbit);
		:set $tmp ($tmp + $nextpart);
		:set $nextpart ($charcode>>(7-$curbit));
		:set curbit ($curbit+1);
		:if ($tmp = 27) do={:set $escape true;} else={
			:if ($escape) do={:local tmp2 ($alphabetToUtf8->[:tostr (3456 | $tmp)]);
				:if ([:len $tmp2] = 0) do={:set $decodedLine "unknown character in function convert7bitToUtf8"; throw;};
				:set $decodedLine ($decodedLine.$tmp2);
				:set $escape false;
				} else={:set $decodedLine ($decodedLine.($alphabetToUtf8->[:tostr $tmp]));};};
		:if ($curbit = 7) do={
			:set $tmp $nextpart;
		:if ($tmp = 27) do={:set $escape true;} else={
			:if ($escape) do={:local tmp2 ($alphabetToUtf8->[:tostr (3456 | $tmp)]);
				:if ([:len $tmp2] = 0) do={:set $decodedLine "unknown character in function convert7bitToUtf8"; throw;};
				:set $decodedLine ($decodedLine.$tmp2);
				:set $escape false;
				} else={:set $decodedLine ($decodedLine.($alphabetToUtf8->[:tostr $tmp]));};};
			:set $curbit 0;
			:set $nextpart 0;
	:if ($iter8) do={:set $decodedLine [:pick $decodedLine 1 [:len $decodedLine]];}
	# оговорено, что в sms при окончании на восьмом символе заполнителем являются нули, в ussd заполнитель CR(при указании в конце CR символы надо задвоить), заполнитель в номере телефона не нашёл
	:if (($curbit = 0) and ([:pick $decodedLine ([:len $decodedLine] - 1)] = "\40")) do={
	:set $decodedLine [:pick $decodedLine 0 ([:len $decodedLine] - 1)];}
	:if ([:len $decodedLine] = 0) do={:set $decodedLine "Error in parsing: function malfunction convert7bitToUtf8\r\n"; throw;}
	} on-error={:set $errorFlagParse true;}
	:return $decodedLine;
########## Конец функции ###################################################

:global convert8bitToUtf8 do={
# кодировка спецификацией не оговорена и определяется пользователем, пишем сами для своих целей.

:global convertUcs2ToUtf8 do={
	:local decodedLine "";
	:local symbolsHex {"\00";"\01";"\02";"\03";"\04";"\05";"\06";"\07";"\08";"\09";"\0A";"\0B";"\0C";"\0D";"\0E";"\0F";"\10";"\11";"\12";"\13";"\14";"\15";"\16";"\17";"\18";"\19";"\1A";"\1B";"\1C";"\1D";"\1E";"\1F";"\20";"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\2A";"\2B";"\2C";"\2D";"\2E";"\2F";"\30";"\31";"\32";"\33";"\34";"\35";"\36";"\37";"\38";"\39";"\3A";"\3B";"\3C";"\3D";"\3E";"\3F";"\40";"\41";"\42";"\43";"\44";"\45";"\46";"\47";"\48";"\49";"\4A";"\4B";"\4C";"\4D";"\4E";"\4F";"\50";"\51";"\52";"\53";"\54";"\55";"\56";"\57";"\58";"\59";"\5A";"\5B";"\5C";"\5D";"\5E";"\5F";"\60";"\61";"\62";"\63";"\64";"\65";"\66";"\67";"\68";"\69";"\6A";"\6B";"\6C";"\6D";"\6E";"\6F";"\70";"\71";"\72";"\73";"\74";"\75";"\76";"\77";"\78";"\79";"\7A";"\7B";"\7C";"\7D";"\7E";"\7F";"\80";"\81";"\82";"\83";"\84";"\85";"\86";"\87";"\88";"\89";"\8A";"\8B";"\8C";"\8D";"\8E";"\8F";"\90";"\91";"\92";"\93";"\94";"\95";"\96";"\97";"\98";"\99";"\9A";"\9B";"\9C";"\9D";"\9E";"\9F";"\A0";"\A1";"\A2";"\A3";"\A4";"\A5";"\A6";"\A7";"\A8";"\A9";"\AA";"\AB";"\AC";"\AD";"\AE";"\AF";"\B0";"\B1";"\B2";"\B3";"\B4";"\B5";"\B6";"\B7";"\B8";"\B9";"\BA";"\BB";"\BC";"\BD";"\BE";"\BF";"\C0";"\C1";"\C2";"\C3";"\C4";"\C5";"\C6";"\C7";"\C8";"\C9";"\CA";"\CB";"\CC";"\CD";"\CE";"\CF";"\D0";"\D1";"\D2";"\D3";"\D4";"\D5";"\D6";"\D7";"\D8";"\D9";"\DA";"\DB";"\DC";"\DD";"\DE";"\DF";"\E0";"\E1";"\E2";"\E3";"\E4";"\E5";"\E6";"\E7";"\E8";"\E9";"\EA";"\EB";"\EC";"\ED";"\EE";"\EF";"\F0";"\F1";"\F2";"\F3";"\F4";"\F5";"\F6";"\F7";"\F8";"\F9";"\FA";"\FB";"\FC";"\FD";"\FE";"\FF"};
	:for curposition from=0 to=([:len $instring] -1) step=4 do={
	:local i [:tonum ("0x".[:pick $instring $curposition ($curposition +4)])];
	:if ($i < 0x80) do={
	:set $decodedLine ($decodedLine.($symbolsHex->$i));
	:if (($i >= 0x80) and ($i < 0x800)) do={
	:local byteA (($i >> 6) | 192);
	:local byteB (($i & 63) | 128);
	:set $decodedLine ($decodedLine.($symbolsHex->$byteA).($symbolsHex->$byteB));
	:if ($i >= 0x800) do={
	:local byteA (($i >> 12) | 224);
	:local byteB ((($i >> 6) & 63) | 128);
	:local byteC (($i & 63) | 128);
	:set $decodedLine ($decodedLine.($symbolsHex->$byteA).($symbolsHex->$byteB).($symbolsHex->$byteC));
		:return $decodedLine;

:global convertAddress do={
	:local decodedLine "";
	:global errorFlagParse;
	:global convert7bitToUtf8;
	:local typeOfAddress [:tonum ("0x".[:pick $instring 0 2])];
# Type-of-number
# 0 0 0 Unknown
# 0 0 1 International number
# 0 1 0 National number
# 0 1 1 Network specific number
# 1 0 0 Subscriber number
# 1 0 1 Alphanumeric, (coded according to GSM TS 03.38 7-bit default alphabet)
# 1 1 0 Abbreviated number
# 1 1 1 Reserved for extension
	:local typeOfNumber (($typeOfAddress >> 4) & 7);
	:local nameNumber {"Unknown";"International number";"National number";"Network specific number";"Subscriber number";"Alphanumeric";"Abbreviated number";"Reserved for extension"};
# Numbering-plan-identification (applies for Type-of-number = 000,001,010)
# 0 0 0 0 Unknown
# 0 0 0 1 ISDN/telephone numbering plan (E.164/E.163)
# 0 0 1 1 Data numbering plan (X.121)
# 0 1 0 0 Telex numbering plan
# 1 0 0 0 National numbering plan
# 1 0 0 1 Private numbering plan
# 1 0 1 0 ERMES numbering plan (ETSI DE/PS 3 01-3)
# 1 1 1 1 Reserved for extension
# All other values are reserved.
	:local numberingPlanIdentification ($typeOfAddress & 15);
	:local namePlan {"Unknown";"ISDN/telephone numbering plan";"Data numbering plan";"Reserved";"Telex numbering plan";"Reserved";"Reserved";"Reserved";"National numbering plan";"Private numbering plan";"ERMES numbering plan";"Reserved";"Reserved";"Reserved";"Reserved";"Reserved for extension"};
	:local addressValue [:pick $instring 2 [:len $instring]];
	:local size [:len $addressValue];
	:local rotare "";
# BCD number
# 1010 *
# 1011 #
# 1100 a
# 1101 b
# 1110 c
# 1111 fill bits
	:local bcd {"A"="*";"B"="#";"C"="a";"D"="b";"E"="c"};
	do {
		:if (($typeOfAddress >> 7) != 1) do={
			:set $decodedLine "Error in parsing numbers: 7 bits in Type-of-Address not set to 1\r\n";
		:if (($typeOfNumber = 3) or ($typeOfNumber = 4)  or ($typeOfNumber = 6) or ($typeOfNumber = 7)) do={
			:set $decodedLine ("Error in parsing numbers: unsupported number type - ".($nameNumber->$typeOfNumber)."\r\n");
		:if ((($typeOfNumber = 0) or ($typeOfNumber = 1)  or ($typeOfNumber = 2)) and ($numberingPlanIdentification != 1)) do={
			:set $decodedLine ("Error in parsing numbers: unsupported number type - ".($nameNumber->$typeOfNumber)." and Numbering-plan-identification - ".($namePlan->$numberingPlanIdentification)."\r\n");
		:if (($typeOfNumber = 5) and ($numberingPlanIdentification != 0)) do={
			:set $decodedLine ("Error in parsing numbers: unsupported number type - ".($nameNumber->$typeOfNumber)." and Numbering-plan-identification - ".($namePlan->$numberingPlanIdentification)."\r\n");
		:if (($size % 2) = 1) do={
			:set $decodedLine "Error in parsing numbers: length is not equal to byte\r\n";
		:if (($typeOfNumber = 0) or ($typeOfNumber = 1)  or ($typeOfNumber = 2)) do={
		:for i from=1 to=$size do={
			:if (($i%2) = 0) do={:set $rotare ($rotare.[:pick $addressValue ($i - 2)]);
			} else={:set $rotare ($rotare.[:pick $addressValue $i]);};}
		:set $addressValue "";
		:for i from=0 to=($size - 1) do={
			:local single [:pick $rotare $i];
			:if ([:tonum ("0x".$single)] <= 9) do={:set $addressValue ($addressValue.$single);
			} else={:if (($single = "F") and ($i = ($size - 1))) do={} else={:if ($single != "F") do={
			:set $addressValue ($addressValue.($bcd->$single));} else={
			:set $decodedLine "Error in parsing numbers: septet of adding to byte is not at the end\r\n";
		:if ($typeOfNumber = 1) do={:set $decodedLine ("+".$addressValue." (".($nameNumber->$typeOfNumber).")");
		} else={:set $decodedLine ($addressValue." (".($nameNumber->$typeOfNumber).")");}
		} else={:if ($typeOfNumber = 5) do={:set $addressValue [$convert7bitToUtf8 instring=$addressValue];
		:set $decodedLine ($addressValue." (".($nameNumber->$typeOfNumber).")");}};
		:if ([:len $decodedLine] = 0) do={:set $decodedLine "Error in parsing numbers: function malfunction convertAddress\r\n"; throw;}
	} on-error={:set $errorFlagParse true;}
	:return $decodedLine;

:global convertBodyPDU do={
	:global errorFlagParse;
	:global convert7bitToUtf8;
	:global convert8bitToUtf8;
	:global convertUcs2ToUtf8;
	:local outstring;
	:if ($typeFormat = 0) do={:set outstring [$convert7bitToUtf8 instring=$instring iter8=$iter8];
	} else={:if ($typeFormat = 1) do={:set outstring [$convert8bitToUtf8 instring=$instring];
	} else={:if ($typeFormat = 2) do={:set outstring [$convertUcs2ToUtf8 instring=$instring];}}}
	:if ([:len $outstring] = 0) do={:set $outstring "Error in parsing body string: function malfunction convertBodyPDU\r\n";
	:set $errorFlagParse true;}
	:return $outstring;


:global convertScts do={ 
:global errorFlagParse;
:global UnixTimeToFormat;
:local decodedLine "";
do {
:local tmp [:tonum ("0x".[:pick $sctsLine 0])]
:local byteY [:tonum ("0x".[:pick $sctsLine 1])]
:if (($tmp > 9) or ($byteY > 9)) do={:set $decodedLine "Error parse in function convertScts, year\r\n"; throw;}
:set $byteY ($byteY * 10 + $tmp)
:set $tmp [:tonum ("0x".[:pick $sctsLine 2])]
:local byteMn [:tonum ("0x".[:pick $sctsLine 3])]
:if (($tmp > 9) or ($byteMn > 1)) do={:set $decodedLine "Error parse in function convertScts, monats\r\n"; throw;}
:set $byteMn ($byteMn * 10 + $tmp)
:if ($byteMn > 12) do={:set $decodedLine "Error parse in function convertScts, monats\r\n"; throw;}
:set $tmp [:tonum ("0x".[:pick $sctsLine 4])]
:local byteD [:tonum ("0x".[:pick $sctsLine 5])]
:if (($tmp > 9) or ($byteD > 3)) do={:set $decodedLine "Error parse in function convertScts, days\r\n"; throw;}
:set $byteD ($byteD * 10 + $tmp)
:if ($byteD > 31) do={:set $decodedLine "Error parse in function convertScts, days\r\n"; throw;}
:set $tmp [:tonum ("0x".[:pick $sctsLine 6])]
:local byteH [:tonum ("0x".[:pick $sctsLine 7])]
:if (($tmp > 9 ) or ($byteH > 2)) do={:set $decodedLine "Error parse in function convertScts, hours\r\n"; throw;}
:set $byteH ($byteH * 10 + $tmp)
:if ($byteH > 23) do={:set $decodedLine "Error parse in function convertScts, hours\r\n"; throw;}
:set $tmp [:tonum ("0x".[:pick $sctsLine 8])]
:local byteM [:tonum ("0x".[:pick $sctsLine 9])]
:if (($tmp > 9) or ($byteM > 5)) do={:set $decodedLine "Error parse in function convertScts, minutes\r\n"; throw;}
:set $byteM ($byteM * 10 + $tmp)
:if ($byteM > 59) do={:set $decodedLine "Error parse in function convertScts, minutes\r\n"; throw;}
:set $tmp [:tonum ("0x".[:pick $sctsLine 10])]
:local byteS [:tonum ("0x".[:pick $sctsLine 11])];
:if (($tmp > 9) or ($byteS > 5)) do={:set $decodedLine "Error parse in function convertScts, seconds\r\n"; throw;}
:set $byteS ($byteS * 10 + $tmp)
:if ($byteS > 59) do={:set $decodedLine "Error parse in function convertScts, seconds\r\n"; throw;}
:set $tmp [:tonum ("0x".[:pick $sctsLine 12])]
:local byteO [:tonum ("0x".[:pick $sctsLine 13])];
:if ($tmp > 9) do={:set $decodedLine "Error parse in function convertScts, offset\r\n"; throw;}
:if (($byteO >> 3) = 1) do={:set $byteO ((0 - (($byteO & 7) * 10 + $tmp)) * 900)} else={:set $byteO ((($byteO & 7) * 10 + $tmp) * 900)}
:local months;
:if (($byteY % 4) = 0) do={
        :set months [:toarray (0,31,60,91,121,152,182,213,244,274,305,335)]
} else={:set months [:toarray (0,31,59,90,120,151,181,212,243,273,304,334)]}
:local unixTime (($byteY * 365 + ($byteY - 1) / 4 + ($months->($byteMn - 1)) + $byteD) * 86400)
:set $unixTime ($byteH * 3600 + $byteM * 60 + $byteS + $unixTime + 946684800)
:local gmt false
:if ([:typeof ($timeStruct->"gmt")] = "bool") do={:set $gmt ($timeStruct->"gmt")}
:local offsetR [/system clock get gmt-offset]
:if (($offsetR >> 31) = 1) do={:set $offsetR ($offsetR - 4294967296); :set $offset ($offset * -1)}
:if ($gmt) do={:set $tmp ($unixTime + $byteO)} else={:set $tmp $unixTime}
:local hd "Date: "
:if ([:typeof $headerDate] = "str") do={:set $hd $headerDate}
:set $tmp [$UnixTimeToFormat timeStamp=$tmp timeStruct=$timeStruct]
:if ([:len $tmp] = 0) do={:set $decodedLine "Error parse in function convertScts, no return from UnixTimeToFormat\r\n"; throw;}
:set $decodedLine ($hd.$tmp)
:if ($gmt) do={:local numO
	:if ($byteO >= 0) do={:set $decodedLine ($decodedLine." -"); :set $numO $byteO} else={:set $decodedLine ($decodedLine." +"); :set $numO ($byteO * -1)}
	:set $decodedLine ($decodedLine.[:tostr ($numO / 3600)])
	:set $numO (($numO % 3600) / 60)
	:if ($numO != 0) do={:set $decodedLine ($decodedLine.":".[:tostr $numO])}
	:set $decodedLine ($decodedLine." GMT")}
:if ($byteO = $offsetR) do={:set $decodedLine ($decodedLine."\r\n")} else={
:set $decodedLine ($decodedLine." (SMS time)\r\n")
:if (!$gmt) do={:set $tmp ($unixTime + $byteO - $offsetR)} else={:set $tmp ($unixTime + $byteO)}
:set $tmp [$UnixTimeToFormat timeStamp=$tmp timeStruct=$timeStruct]
:if ([:len $tmp] = 0) do={:set $decodedLine "Error parse in function convertScts, no return from UnixTimeToFormat\r\n"; throw;}
:set $decodedLine ($decodedLine.$hd.$tmp)
:if ($gmt) do={:local numO
	:if ($offsetR >= 0) do={:set $decodedLine ($decodedLine." -"); :set $numO $offsetR} else={:set $decodedLine ($decodedLine." +"); :set $numO ($offsetR * -1)}
	:set $decodedLine ($decodedLine.[:tostr ($numO / 3600)])
	:set $numO (($numO % 3600) / 60)
	:if ($numO != 0) do={:set $decodedLine ($decodedLine.":".[:tostr $numO])}
	:set $decodedLine ($decodedLine." GMT")}
:set $decodedLine ($decodedLine." (OS time) \r\n")
} on-error={:set $errorFlagParse true;}
:if ([:len $decodedLine] = 0) do={:set $decodedLine "Error in parsing date string: function malfunction convertScts\r\n"
	:set $errorFlagParse true}
:return $decodedLine;

# Считаются только unsigned
# условие проверки високосного года ограничено текущей соткой
:global UnixTimeToFormat do={
:local decodedLine ""
:local dateY ($timeStamp / 31436000 + 1970)
:local dateM (($dateY - 1969) / 4)
:local dateD ($timeStamp / 86400 - $dateM)
:set $dateY ($dateD / 365 + 1970)
:set $dateD ($dateD % 365)
:local months [:toarray (31,28,31,30,31,30,31,31,30,31,30,31)]
:if (($dateY % 4) = 0) do={:set ($months->1) 29}
do {
:for i from=0 to=11 do={:if (($months->$i) >= $dateD) do={:set $dateM ($i + 1); :set $dateD ($dateD +1); break;} else={:set $dateD ($dateD - ($months->$i))}}
} on-error={}
:local timeS ($timeStamp % 86400)
:local timeH ($timeS / 3600)
:local timeM ($timeS % 3600 / 60)
:set $timeS ($timeS - $timeH * 3600 - $timeM * 60)
:local tmod 2
:local s "."
:local nf true
:local mstr {"jan";"feb";"mar";"apr";"may";"jun";"jul";"aug";"sep";"oct";"nov";"dec"}
:if ([:typeof $timeStruct] = "array") do={
:if ([:typeof ($timeStruct->"timeFormat")] = "num") do={:set $tmod ($timeStruct->"timeFormat")}
:if ([:typeof ($timeStruct->"dateSeparator")] = "str") do={:set $s ($timeStruct->"dateSeparator")}
:if ([:typeof ($timeStruct->"numFill")] = "bool") do={:set $nf ($timeStruct->"numFill")}
:if (([:typeof ($timeStruct->"monthsStr")] = "array") and ([:len ($timeStruct->"monthsStr")] = 12)) do={:set $mstr ($timeStruct->"monthsStr")}
:local strY [:tostr $dateY]
:local strMn
:local strD
:local strH
:local strM
:local strS
:if ($nf) do={
:if ($dateM > 9) do={:set $strMn [:tostr $dateM]} else={:set $strMn ("0".[:tostr $dateM])}
:if ($dateD > 9) do={:set $strD [:tostr $dateD]} else={:set $strD ("0".[:tostr $dateD])}
:if ($timeH > 9) do={:set $strH [:tostr $timeH]} else={:set $strH ("0".[:tostr $timeH])}
:if ($timeM > 9) do={:set $strM [:tostr $timeM]} else={:set $strM ("0".[:tostr $timeM])}
:if ($timeS > 9) do={:set $strS [:tostr $timeS]} else={:set $strS ("0".[:tostr $timeS])}
} else={
:set strMn [:tostr $dateM]
:set strD [:tostr $dateD]
:set strH [:tostr $timeH]
:set strM [:tostr $timeM]
:set strS [:tostr $timeS]
do {
:if ($tmod = 1) do={:set $decodedLine "$strY$s$strMn$s$strD $strH:$strM:$strS"; break;}
:if ($tmod = 2) do={:set $decodedLine "$strD$s$strMn$s$strY $strH:$strM:$strS"; break;}
:if ($tmod = 3) do={:set $decodedLine ("$strD ".($mstr->($dateM - 1))." $strY $strH:$strM:$strS"); break;}
:if ($tmod = 4) do={:set $decodedLine ("$strY ".($mstr->($dateM - 1))." $strD $strH:$strM:$strS"); break;}
} on-error={}
:return $decodedLine;

:global sendMailUTF8 do={
# функция должна возвращать булевое истина при успешной отправке сообщения
/tool e-mail send to=$emailAdr subject="$head\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8" body=$emailBody
:local sendit 120
:while ($sendit > 0) do={:delay 1s; :set $sendit ($sendit - 1)
:if (([tool e-mail get last-status] = "succeeded") or ([tool e-mail get last-status] = "failed")) do={:set $sendit 0}}
:if ([tool e-mail get last-status] = "succeeded") do={:return true} else={:return false}

:global saveFile do={
:if ([/file find name~"$nameFile"] = "") do={/file print file="$nameFile"
:while ([/file find name~"$nameFile"] = "") do={:delay 1s}}
/file set $nameFile contents=$bodyFile

:global exitFunctionPDU do={
:global errorFlagParse
:global convert7bitToUtf8
:global convert8bitToUtf8
:global convertUcs2ToUtf8
:global convertAddress
:global convertBodyPDU
:global convertScts
:global UnixTimeToFormat
:global sendMailUTF8
:global extractSmsModem
:global saveFile
:set $saveFile
:set $errorFlagParse
:set $convert7bitToUtf8
:set $convert8bitToUtf8
:set $convertUcs2ToUtf8
:set $convertAddress
:set $convertBodyPDU
:set $convertScts
:set $UnixTimeToFormat
:set $extractSmsModem
:set $sendMailUTF8
:global exitFunctionPDU
:set $exitFunctionPDU

We do not change the names of the child scripts, because they are called on demand from the main.
The script with the extraction function “extractSmsModem” must be edited for a specific modem. Check out the list of AT commands for your particular modem. The current sample presented works with LTE interfaces supported by AT commands by default.
The main script at the beginning contains a list of variables to configure the behavior of the script.
Now for those who like perversions - the fact is that in the PDU there may be different flags that specify the behavior of sms. For example, the class flag, which can indicate - do not save sms to memory, but display it on the display. If there is no display, the standard stipulates that the receiving party decides the behavior of such an SMS. The same thing happens with CBM (cell broadcast message), if the modem is able to handle them. For those who do not know, this is a special type of PDU, for multicast, for example - the operator’s network can transmit information about an upcoming thunderstorm in a particular area (it is time to rip the router out of the socket). So in fact, modems just dump similar PDUs into the terminal and forget about them, and ROS just ignores them.
There are not many options - constantly monitor. For those modems that have a free port with a terminal, it is necessary to process each message from the modem and, in case of finding the correspondence of the desired identifier, process the messages. The second method is more universal and suitable for all modems that have a terminal (LTE, etc.) - you need to enable ROS exchange logging with the modem (/ system loging -> lte, ppp, etc.) and then parse this log for received PDUs.
P.S. Support is built into the script, if you do not disable it, then I will type examples to complement the unsupported functions and implement them.
just joined
Topic Author
Posts: 22
Joined: Tue Jul 23, 2013 6:59 pm

Re: Script for sending incoming SMS to mail with full parsing

Sun Jan 24, 2021 10:45 am

The current version can be found at this link.
According to reviews, the script was heavily reworked.
The main innovation-added automatic search for modems. However, we must remember that the script only works with modems that have an AT interface. Also, according to reviews, the script does not work in ros7.
In the process of working with reviews, most of the complaints were about the modems of the manufacturer mikrotik. Several crutches have been added to the script to work with these modems.

Who is online

Users browsing this forum: No registered users and 13 guests