Страница 3 из 4

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 26 июл 2019, 12:45
pepelxl
Поскольку в кодовых таблицах sms творится каша, работать предпочтительней, всё-таки, в PDU режиме.
Небольшая функция для преобразования текста из 7-bit.
Доделал скрипт от Andrey.Radomanov с первой страницы.
Соответственно массив alphabetToUtf8 можно заменить на UTF-16 или любую другую, хотя я и не представляю однобайтовую кодировку, в которую влезет каша из alphabet.

Код: Выделить всё

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


:local convert7bitToUtf8 do={
	: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;
	:local nextpart 0;
	:local escape false;
	:local decodedLine "";
	: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={:set $decodedLine ($decodedLine.($alphabetToUtf8->[:tostr (3456 | $tmp)]));
				: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={:set $decodedLine ($decodedLine.($alphabetToUtf8->[:tostr (3456 | $tmp)]));
				:set $escape false;
				} else={:set $decodedLine ($decodedLine.($alphabetToUtf8->[:tostr $tmp]));};};
			:set $curbit 0;
			:set $nextpart 0;
		};
	};
	:return $decodedLine;
};
########## Конец функции ###################################################

# Пример:
# Получаем строку из SMS, USSD, номер телефона и так далее
# Для примера использована строка "Hello world!"
:local smsText "C8329BFD06DDDF72363904";
# Указываем переменную куда будет помещена декомпилированная строка
# и вызываем функцию
:local parsedTEXT [$convert7bitToUtf8 instring=$smsText];
:log info $parsedTEXT;

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 06 сен 2019, 11:53
andrey.volosatov
Добрый день!

Подскажите пожалуйста, с каким модемом будет СМС принимать?
У меня Huawei E3372h
Mikritik говорит модем не поддерживается.

Код: Выделить всё

 /interface lte at-chat lte1 input="AT+CMGF=0";
failure: Modem unsupported

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 06 сен 2019, 17:49
pepelxl
У вас модем определился в каком режиме? Заставить можно и этот, было бы желание

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 09 сен 2019, 17:54
andrey.volosatov
Была прошивка HiLink, поставил стоковую Stick работает.

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 14 фев 2020, 13:17
drummah
Хороший скрипт! есть небольшой нюанс, возможно это только на модели Mikrotik LHG LTE kit / Router OS version: 6.43.3

Так как скрипт переключает LTE модем в режим чтения SMS TEXT mode - то 1й раз он отрабатывает отлично, а затем все SMSки пустые приходят, и даже номер отправителя очень странный, типа "323231 1312312 000"

в конце скрипта, перед строкой если добавить обратное переключение модема в SMS PDU mode, все начинает отлично работать. То есть код в конце будет выглядеть так:
--------------------
/interface lte at-chat lte1 input="AT+CMGF=0";

#:log info "END";
---------------------

В таком виде отлично работает, добавил его в scheduler на каждые 60 сек, и все SMSки теперь валятся на почту) на всех языках)

Спасибо автору скрипта!
:men:


Andrey.Radomanov писал(а): 01 мар 2019, 15:01 Вот скрипт, который я в итоге создал, чтобы обойти проблему с кириллицей в SMS.
Скрипт пишет содержимое сообщений в журнал роутера в транслите.

Я транслитерирую русские символы в латинские, чтобы их можно было напечтать в журнал, иначе никак. Скрипт работает корректно с модемами Huawei 121. С R11e-LTE тоже работает. Все символы, которые не распознаются скриптом печатаются в строке в квадратных скобках, чтобы вы смогли их добавить в словарь самостоятельно.
Лично у себя я назвал этот скрипт "sms_patrol" и поставил его на запуск раз в час. Для журнала warning и выше я рекомендую настроить запись на диск, поэтому вы не пропустите сообщения, даже если роутер перезагружался.
В нижней части скрипта приводится пример отправки полученных смс по E-mail, а также пример поиска в тексте сообщения о балансе с последующим сравнением с заданным минимальным порогом.
Также напоминаю всем, что существует прекрасный сервис по моментальной пересылке e-mail-сообщений в ваш телеграм - называется etlgr. Т.е. вы можете без особых хлопот наладить пересылку sms в телеграм.

В этом же скрипте в двух местах вы найдете реализацию декодера UCS-2 7-bit кодировки. Надеюсь это будет кому-то полезно.

Пишите, если у вас будут проблемы, посмотрим что можно будет с этим сделать.

Код: Выделить всё

#Скрипт для проверки SMS на роутерах Mikrotik. Автор: Андрей Радоманов. Версия 2.
:local autodelete 1;

#---- system variables ---
:local smsArray ({});
:local inboxCursor 0;
:local inboxCount 0;

#:log info "Задаем режим работы в PDU-режиме.";
/interface lte at-chat lte1 input="AT+CMGF=0";

#Выбираем хранилище SMS на SIM карте
:local res [/interface lte at-chat lte1 input="AT+CPMS=\"SM\"" as-value wait=yes];
:set res ($res->"output");
#:put $res;

:if ([:typeof [:find $res "+CPMS: "]] = "num") do={
  :set inboxCount [:tonum [:pick $res ([:find $res "+CPMS: "]+7) ([:find $res ","])]];
  :put "SMS Inbox count: $inboxCount";
};

:if ($inboxCount>0) do={

  :local symbols {"-";" ";" ";"!";"\"";"#";"\$";"%";"&";"'";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"?";"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_";"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~","\"","\""};

  :local symbolsUTF [:toarray "2013,000A,0020,0021,0022,0023,0024,0025,0026,0027,0028,0029,002A,002B,002C,002D,002E,002F,0030,0031,0032,0033,0034,0035,0036,0037,0038,0039,003A,003B,003C,003D,003E,003F,0040,0041,0042,0043,0044,0045,0046,0047,0048,0049,004A,004B,004C,004D,004E,004F,0050,0051,0052,0053,0054,0055,0056,0057,0058,0059,005A,005B,005C,005D,005E,005F,0060,0061,0062,0063,0064,0065,0066,0067,0068,0069,006A,006B,006C,006D,006E,006F,0070,0071,0072,0073,0074,0075,0076,0077,0078,0079,007A,007B,007C,007D,007E,00AB,00BB"];

  :local symbolsRus [:toarray "A,B,V,G,D,E,Yo,Zh,Z,I,Y,K,L,M,N,O,P,R,S,T,U,F,Kh,C,Ch,Sh,Sch,',I,',E,Yu,Ya,a,b,v,g,d,e,yo,zh,z,i,y,k,l,m,n,o,p,r,s,t,u,f,kh,c,ch,sh,sch,',i,',e,yu,ya,_,!,_,_,_,%,&,',(,),*,+,_,-,.,/,0,1,2,3,4,5,6,7,8,9"];

  :local symbolsRusUTF [:toarray "0410,0411,0412,0413,0414,0415,0401,0416,0417,0418,0419,041A,041B,041C,041D,041E,041F,0420,0421,0422,0423,0424,0425,0426,0427,0428,0429,042A,042B,042C,042D,042E,042F,0430,0431,0432,0433,0434,0435,0451,0436,0437,0438,0439,043A,043B,043C,043D,043E,043F,0440,0441,0442,0443,0444,0445,0446,0447,0448,0449,044A,044B,044C,044D,044E,044F"];


  :local hexstr "0123456789ABCDEF";
  :local ascii " !\"#\$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";

  :while ($inboxCursor < $inboxCount) do={
    :set inboxCursor ($inboxCursor+1);
    
    :local sms [/interface lte at-chat lte1 input="AT+CMGR=$inboxCursor" as-value wait=yes];
    :set sms ($sms->"output");
    #:put ("inbox $inboxCursor:'".$sms."'\n");
    
    :if (( [:typeof [:find $sms "+CMGR"]] != "nil" ) and ([:typeof [:find $sms "\n"]] != "nil")) do={
       :set sms [:pick $sms ([:find $sms "+CMGR"]+1) ([:len $sms]+1)];
       :local lineStart ([:find $sms "\n"]+1);
       :local lineEnd [:find $sms "\nOK" $lineStart] ;
       :if ([:typeof $lineEnd] = "nil") do={
         :set lineEnd ([:len $sms]+1);
        }
       :set sms [:pick $sms $lineStart $lineEnd];

       #Removes unesessary \r\n at the end of lines
       :if ([:pick $sms ([:len $sms]-1)] = "\n") do={
         :set sms [:pick $sms 0 ([:len $sms]-1)];
        }
       :if ([:pick $sms ([:len $sms]-1)] = "\r") do={
         :set sms [:pick $sms 0 ([:len $sms]-1)];
        }
       :if ([:pick $sms ([:len $sms]-1)] = "\n") do={
         :set sms [:pick $sms 0 ([:len $sms]-1)];
        }
       :if ([:pick $sms ([:len $sms]-1)] = "\r") do={
         :set sms [:pick $sms 0 ([:len $sms]-1)];
        }
       #:put ("sms-raw $inboxCursor:'".$sms."'\n");

       :local i;
       :if ([:len $sms]>8) do={
         :local pduEnd 0;
         :local smsLen [:len $sms];
         :local pduPartNum 1;
         :local curPos 0;
         :local smsTS;
         :local smsFrom;
         :local smsNum "0000";
         :local dcs "00";
         :while (($pduPartNum < 8) and ($curPos < $smsLen)) do={
           #См. http://embeddedpro.ucoz.ru/app_notes/send_short_SMS/PDU1.pdf
           #Хуавей не отдает первый байт с длиной SCA, поэтому исходим из стандартных 12 байт + 2 байта на тип.=14
           :local pduPartLen;
           :if ($pduPartNum = 1) do={ #SCA - Service Center Address
             :set pduPartLen 14;
             :if (([:pick $sms $curPos ($curPos+2)])!="91") do={
                :put "first two bytes is not 91, skipping...";
                :set pduPartLen ($pduPartLen+2);
              }
           }
           :if ($pduPartNum = 2) do={#PDU Type. Пока что он нас не интересует. Пропускаем заодно
             :set pduPartLen 2;
           }
           :if ($pduPartNum = 3) do={ #OA - Originator Address
             #Тут уже все по настоящему - номер отправителя передается в формате SCA/OA. В первом байте шифруется кол-во цифр в номере  
             :local tmpHex1 [:pick $sms $curPos ($curPos+1)]
             :local tmpHex2 [:pick $sms ($curPos+1) ($curPos+2)]
             :local fromLen;
             :set pduPartLen 0;
             :if ([:typeof [:find $hexstr $tmpHex1]] = "num") do={
               :set fromLen ([:find $hexstr $tmpHex1] * 16);
             }
             :if ([:typeof [:find $hexstr $tmpHex2]] = "num") do={
               :set fromLen ([:find $hexstr $tmpHex2]+$fromLen);
             }
              #Если число цифр в номере было не четное - округляем до ближайшего четного
              if ( [:pick [:tostr (($fromLen*10)/2)] ([:len [:tostr (($fromLen*10)/2)]]-1) [:len [:tostr (($fromLen*10)/2)]]] = "5") do={
                :set pduPartLen ($fromLen+1);
              } else={
                :set pduPartLen $fromLen;
              }

             #Добавляем 2 байта - один указывал кол-во цифр, второй - это нотация номера ("91")
             :set pduPartLen ($pduPartLen+4);
             if (([:pick $sms ($curPos+2) ($curPos+4)]="91") or ([:pick $sms ($curPos+2) ($curPos+4)]="81")) do={
               :set smsFrom "+";
               :for i from=0 to=($fromLen-1) do={
                  if ( [:pick [:tostr (($i*10)/2)] ([:len [:tostr (($i*10)/2)]]-1) [:len [:tostr (($i*10)/2)]]] = "5") do={
                   :set smsFrom ($smsFrom.[:pick $sms ($curPos+4+$i-1)]);
                  } else {
                   :set smsFrom ($smsFrom.[:pick $sms ($curPos+4+$i+1)]);
                  }
               }
             } else={
              :set smsFrom [:pick $sms ($curPos+2) ($curPos+$pduPartLen)];
              #Если это 7-byte сообщение - делаем распаковку
              :if ([:pick $smsFrom 0 2] = "D0") do={
                :set smsFrom [:pick $smsFrom 2 [:len $smsFrom]];
                :local curbit 0;
                :local nextpart 0;
                :local smsDecoded "";
                :for i from=0 to=([:len $smsFrom]-1) step=2 do={
                  :local tmp [:pick $smsFrom $i];
                  :local charcode ([:find "0123456789ABCDEF" $tmp]*16);
                  :set tmp [:pick $smsFrom ($i+1)];
                  :set charcode ($charcode+[:find "0123456789ABCDEF" $tmp]);
                  :if ($curbit<7) do={
                    :set tmp ($charcode & (127>>$curbit)); #Отсекаем левую часть битов (столько битов, сколько нужно отсечь)
                    :set tmp ($tmp<<$curbit);
                    :set tmp ($tmp + $nextpart); #Прибавляем то, что осталось от предыдущей части
                    :set nextpart ($charcode>>(7-$curbit));
                    :set curbit ($curbit+1);
                   }
                  :set smsDecoded ($smsDecoded.[:pick $ascii ($tmp-32)]);
                  :if ($curbit=7) do={
                    :set tmp $nextpart;
                    :set curbit 0;
                    :set nextpart 0;
                    :set smsDecoded ($smsDecoded.[:pick $ascii ($tmp-32)]);
                  }
                 }
               :set smsFrom $smsDecoded;
               }
             }
             #:put ("FROM: ".$smsFrom);
           }
           :if ($pduPartNum = 4) do={ #PID - Protocol Identifier

              :set pduPartLen 2;
           }
           :if ($pduPartNum = 5) do={ #DCS -  Data Coding Scheme
             :set pduPartLen 2;
             #08 - без сжатия, 16-бит Unicode
             #00 - USC2 7-bit (сжатая)
             :set dcs [:pick $sms $curPos ($curPos+2)];
           }
           :if ($pduPartNum = 6) do={ #SCTS - Service Centre Time Stamp
             :set pduPartLen 14;
             :set smsTS "20";
             :for i from=0 to=11 do={
                if ( [:pick [:tostr (($i*10)/2)] ([:len [:tostr (($i*10)/2)]]-1) [:len [:tostr (($i*10)/2)]]] = "5") do={
                 :set smsTS ($smsTS.[:pick $sms ($curPos+$i-1)]);
                } else {
                 :set smsTS ($smsTS.[:pick $sms ($curPos+$i+1)]);
                }
             }
           }
           :if ($pduPartNum = 7) do={ #UDL - User Data Length
              :set pduPartLen 2;
           }
           :if ($pduPartLen = 0) do={
             #:put ("could not detect Pdu len for part:".$pduPartNum);
             :set pduPartLen 2;
           }
           #:put ("PDU PART ".$pduPartNum.": ".[:pick $sms $curPos ($curPos+$pduPartLen)]);
           :set curPos ($curPos+$pduPartLen);
           :set pduPartNum ($pduPartNum+1);
           :set pduEnd $curPos;
         }
         #:log info ("PDU END: ".$pduEnd);
         :if ($pduEnd = 0) do={
           :log error ("Не удалось определить конец PDU в следующем сообщении: ".[:pick $sms 0 72]);
           :set pduEnd 58;
         }

         #:put ("PDU:".[:pick $sms 0 $pduEnd]);
         :set sms [:pick $sms $pduEnd [:len $sms]];

         :local udh [:pick $sms 0 2];
         #Если у сообщения есть UDH - декодируем его, заодно заполняя необходимые номера сообщений
         :if ($udh = "05") do={
           #:put ("UDH: ".[:pick $sms 0 12]);
           :set smsNum ([:pick $sms 6 8].[:pick $sms 10 12]); #4-й байт - уникальный ИД, 6-й номер сообщения
           :set sms [:pick $sms 12 [:len $sms]];
         }
         #Если это UTF16 сообщение - делаем транслит
         :if ($dcs = "08") do={
           :local decodedSMS "";
           :for i from=0 to=([:len $sms]-1) step=4 do={
             :local char [:pick $sms $i ($i+4)];
             :local ind [:find $symbolsRusUTF $char];
             :if ([:typeof $ind]="num") do={
               :set decodedSMS ($decodedSMS.[:pick $symbolsRus $ind]);
             } else={
               :set ind [:find $symbolsUTF $char];
               :if ([:typeof $ind]="num") do={
                 :set decodedSMS ($decodedSMS.[:pick $symbols $ind]);
               } else={
                 :if ($char!="\r") do={
                   :set decodedSMS ($decodedSMS."[".$char."]");
                 }
               }
             }
           }
           :set sms $decodedSMS;
         }
         #Если это 7-byte сообщение - делаем распаковку
         :if ($dcs = "00") do={
            #:put ("UCS2: ".$sms);
            :local curbit 0;
            :local nextpart 0;
            :local smsDecoded "";
            :for i from=0 to=([:len $sms]-1) step=2 do={
              :local tmp [:pick $sms $i];
              :local charcode ([:find "0123456789ABCDEF" $tmp]*16);
              :set tmp [:pick $sms ($i+1)];
              :set charcode ($charcode+[:find "0123456789ABCDEF" $tmp]);
              :if ($curbit<7) do={
                :set tmp ($charcode & (127>>$curbit)); #Отсекаем левую часть битов (столько битов, сколько нужно отсечь)
                :set tmp ($tmp<<$curbit);
                :set tmp ($tmp + $nextpart); #Прибавляем то, что осталось от предыдущей части
                :set nextpart ($charcode>>(7-$curbit));
                :set curbit ($curbit+1);
              }
              :set smsDecoded ($smsDecoded.[:pick $ascii ($tmp-32)]);
              :if ($curbit=7) do={
                :set tmp $nextpart;
                :set curbit 0;
                :set nextpart 0;
                :set smsDecoded ($smsDecoded.[:pick $ascii ($tmp-32)]);
              }
            }
            :set sms $smsDecoded;
         }
         #:log info ("SET ".($smsTS.$smsNum)." ".$sms);
         :set ($smsArray->($smsTS.$smsFrom.$smsNum)) $sms;
        }
      }
   }

  :if ($autodelete=1) do={
    :set inboxCursor 0;
    :do {
      :set inboxCursor ($inboxCursor+1);
      /interface lte at-chat lte1 input="AT+CMGD=$inboxCursor";
     } while ($inboxCursor < $inboxCount);
   };

 }; #if inboxCount>0

#Отправляем полученные sms:
:local minBalance 370;
:local emailFrom "lte-router@domain.ru";
:local emailTo "admin@domain.ru";
:local emailServer "mail.domain.ru";
:local emailPort 25;
:local emailUser "lte-router@domain.ru";
:local emailPassword "lte-router-password";


:local buffer ({});
:foreach k,v in=$smsArray do={
  :local smsId [:pick $k 0 ([:len $k]-4)];
  :local smsFrom [:pick $k 0 ([:len $k]-4)];
  :set ($buffer->$smsId) ($buffer->$smsId . $v);
  :set ($smsArray->$k);
}
:foreach k,v in=$buffer do={
  :local smsFrom [:pick $k 14 [:len $k]];
  :local sysName [/system identity get name];
  :local datePrefix ("[".[:pick $k 6 8].".".[:pick $k 4 6].".".[:pick $k 0 4]." ".[:pick $k 8 10].":".[:pick $k 10 12].":".[:pick $k 12 14]."]");
  :log warning ($datePrefix." SMS from ".$smsFrom.": ".$v);
  :put ($sysName." ". $datePrefix . " SMS from " . $smsFrom . ": ". $v . "\n");
  #Проверяем баланс тут. Привязано к строке вида "balance vashego scheta XXX.XX rub..."
  :if (([:typeof [:find $v "scheta "]] = "num") and ([:typeof [:find $v " rub" [:find $v "scheta "]]] = "num")) do={
    :local balance [:pick $v ([:find $v "scheta "]+7) [:find $v " rub" [:find $v "scheta "]] ]
    #Отсекаем все, что после точки
    :set balance [:pick $balance 0 [:find $balance "."]];
    :if ($minBalance<$balance) do={
      :log warning ("SIM account balance is ".$balance." rub."); 
    } else={
      :log error ("SIM account balance is ".$balance." rub and it lower than needed (".$minBalance.").");
      :local need ($minBalance-$balance);
      #Отправляем почту только после 24-го числа
      :local dm [:pick [/system clock get date] 4 6];
      :if ($dm>23) do={
        /tool e-mail send from=$emailFrom to=$emailTo server=$emailServer port=$emailPort user=$emailUser password=$emailPassword start-tls=yes subject=($sysName." SIM balance low (".$balance.")") body=($sysName. " " . $datePrefix ." SIM Balance = ".$balance." rub, nuzno eshe: ".$need." rub!");
      };
    }
  } else={ #Отправляем все остальные смс как есть в телеграм.
  #Отправляем почту
   /tool e-mail send from=$emailFrom to=$emailTo server=$emailServer port=$emailPort user=$emailUser password=$emailPassword start-tls=yes subject=($sysName." - SMS From ".$smsFrom) body=($sysName." ".$datePrefix." SMS from ".$smsFrom.": ".$v);
  }

  :set ($buffer->$k);

}


Re: RouterOS и кодировка SMS UCS-2

Добавлено: 02 июн 2020, 21:54
pepelxl
Вторую версию своего скрипта выложил здесь

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 02 мар 2023, 07:07
SergeyN
Может кто в курсе, использую скрипт с первой страницы.
Все работало, но сегодня на одно сообщение от Билайна получил вот такое:
02.03.2023 06:56:53 SMS from Beeline: [0608][0461][7504][0304][3E04][3B04][4304
][4704][3804][4204][4C00][2004][3404][3504][4204][3004][3B04]...
Модем R11e-LTE6.

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 03 мар 2023, 08:05
pepelxl
Скрипт не учёл смещения в заголовках и съехала разметка

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 04 мар 2023, 09:33
SergeyN
Этому есть какое-то решение?

Re: RouterOS и кодировка SMS UCS-2

Добавлено: 20 июл 2026, 01:05
EagleNN
Комрад pepelxl писал скрипт отправки СМС на email.
Ранее он у меня работал с небольшими доработками.
Однако при обновлении на последнюю версию (7.23.2) все отказалось работать, от слова совсем.

В результате много чего было переписано и дополнено. Нормально работает на актуальной, на данный момент, прошивке.
Проверено на Mikrotik LHG LTE6 с модемом
Model "R11e-LTE6"
Revision R11e-LTE6_V039

Скрипт состоит из 4 частей:
  • extractModem
    functionMTI
    functionPDU
    PDUtoEMAIL

 
extractModem

Код: Выделить всё

# extractModem - extract SMS PDUs from modem via AT+CMGL / извлечение PDU из модема

:global extractModem do={
  :local output [:toarray ""]

  # AT-command helper / Функция AT-запроса
  :local chat do={
    :local t {"r"="";"f"=""}
    :if ($1 = "lte") do={:set ($t->"r") ([/interface lte at-chat $2 input=$3 wait=yes as-value]->"output")}
    :if ($1 = "ppp-client") do={:set ($t->"r") ([/interface ppp-client at-chat $2 input=$3 as-value]->"output")}
    :if (($t->"r")~"(^|\n)OK(\$|\r)" != true) do={:set ($t->"f") true} else={:set ($t->"f") false}
    :return $t
  }

  # Find modems / Ищем модемы
  :local nameFind [:toarray ""]
  :foreach i in=[/interface lte find] do={
    :if ([/interface lte get $i value-name=disabled] = false) do={
      :set $nameFind ($nameFind, {{"name"=[/interface lte get $i value-name=name];"type"="lte"}})
    }
  }
  :foreach i in=[/interface ppp-client find] do={
    :if ([/interface ppp-client get $i value-name=disabled] = false) do={
      :set $nameFind ($nameFind, {{"name"=[/interface ppp-client get $i value-name=name];"type"="ppp-client"}})
    }
  }

  :if ([:len $nameFind] = 0) do={:return $output}

  # Query each modem / Опрашиваем каждый модем
  :foreach m in=$nameFind do={
    :local tmp [$chat ($m->"type") ($m->"name") "AT+CMGL=4"]
    :if (!($tmp->"f")) do={
      :local flagend true
      :local stStart [:find ($tmp->"r") "+CMGL"]
      :if ([:typeof $stStart] != "num") do={:set $flagend false}
      :while ($flagend) do={
        :local stEnd [:find ($tmp->"r") "\r\n" $stStart]
        :if ([:typeof $stEnd] != "num") do={:set $flagend false} else={
          :local cmglLine [:pick ($tmp->"r") $stStart $stEnd]
          :local stat [:tonum [:pick $cmglLine ([:find $cmglLine ","] + 1)]]
          :if (($stat = 0) or ($stat = 1)) do={
            :local dsStart [:find $cmglLine ",,"]
            :if ([:typeof $dsStart] != "num") do={
              :set $dsStart ([:find $cmglLine ",\"\","] + 4)
            } else={
              :set $dsStart ($dsStart + 2)
            }
            :local pduLen [:tonum [pick $cmglLine $dsStart [:len $cmglLine]]]

            :set $stStart ($stEnd + 2)
            :local coretka true
            :while ($coretka) do={
              :if (([:pick ($tmp->"r") $stStart] = "\r") or ([:pick ($tmp->"r") $stStart] = "\n")) do={
                :set $stStart ($stStart + 1)} else={:set $coretka false}
            }
            :set $stEnd [:find ($tmp->"r") "\r\n" $stStart]

            :if ([:typeof $stEnd] != "num") do={:set $flagend false} else={
              :local pduHex [:pick ($tmp->"r") $stStart $stEnd]
              :local calcLen (($pduLen + 1 + [:tonum ("0x".[pick $pduHex 0 2])]) * 2)
              :if ($calcLen = [:len $pduHex]) do={
                :set $output ($output, {{"pdu"=$pduHex;"name"=($m->"name");"mode"="sms"}})
              }
            }
          }
        }
        :set $stStart [:find ($tmp->"r") "+CMGL" $stEnd]
        :if ([:typeof $stStart] != "num") do={:set $flagend false}
      }
    }
  }

  :return $output
}

:log info "extractModem loaded"
functionMTI

Код: Выделить всё

# functionMTI - SMS-DELIVER TPDU parser / парсер SMS-DELIVER

:global mti0 do={
  :local retArg [:toarray ""]
  :local replyPath
  :local udhi
  :local sri
  :local mms
  :local oaLen
  :local oaLine
  :local pid
  :local dcs
  :local dcsModel
  :local dcsSubModel
  :local compressedUd
  :local dcsFlagClass
  :local dcsCode
  :local dcsClass
  :local dcsMwig
  :local dcsMwigIndication
  :local dcsMwigIndicationType
  :local sctsLine
  :local udl
  :local udhl
  :local structUdh {"concatenated"=false;"size"=0}
  :local udh
  :local udhIEI
  :local udhLIE
  :local udhIE
  :local udText
  :local udhOctet1
  :local udhOctet2
  :local udhOctet3
  
  do {
    :set $replyPath ($pduType >> 7)
    :if ($replyPath = 0) do={:set $replyPath false} else={:set $replyPath true}
    
    :set $udhi (($pduType >> 6) & 1)
    :if ($udhi = 0) do={:set $udhi false} else={:set $udhi true}
    
    :set $sri (($pduType >> 5) & 1)
    :if ($sri = 0) do={:set $sri false} else={:set $sri true}
    
    :set $mms (($pduType >> 2) & 1)
    :if ($mms = 0) do={:set $mms false} else={:set $mms true}
    
    :set $oaLen [:tonum ("0x".[:pick $tpduLine 2 4])]
    :if ($oaLen % 2 != 0) do={:set $oaLen ($oaLen + 1)}
    :if ($oaLen > 20) do={:set $retArg "Error parse, incorrect len OA"; throw;}
    
    :set $oaLine [:pick $tpduLine 4 (6 + $oaLen)]
    
    :set $pid [:tonum ("0x".[:pick $tpduLine (6 + $oaLen) (8 + $oaLen)])]
    :if ($pid != 0) do={:set $retArg "Error parse, unknown PID"; throw;}
    
    :set $dcs [:tonum ("0x".[:pick $tpduLine (8 + $oaLen) (10 + $oaLen)])]
    :set $dcsModel ($dcs >> 6)
    
    :if ($dcsModel = 0) do={
      :set $compressedUd (($dcs >> 5) & 1)
      :if ($compressedUd = 0) do={:set $compressedUd false} else={:set $compressedUd true}
      
      :set $dcsFlagClass (($dcs >> 4) & 1)
      :if ($dcsFlagClass = 0) do={:set $dcsFlagClass false} else={:set $dcsFlagClass true}
      
      :set $dcsCode (($dcs >> 2) & 3)
      :set $dcsClass ($dcs & 3)
    }
    
    :if ($dcsModel = 3) do={
      :set $dcsSubModel (($dcs >> 4) & 3)
      :if ($dcsSubModel = 3) do={
        :if ((($dcs >> 3) & 1) != 0) do={:set $retArg "Error parse, incorrect DCS"; throw;}
        :set $dcsCode (($dcs >> 2) & 1)
        :set $dcsFlagClass true
        :set $dcsClass ($dcs & 3)
      } else={
        :set $dcsMwig $dcsSubModel
        :set $dcsMwigIndicationType ($dcs & 3)
        :set $dcsMwigIndication (($dcs >> 2) & 1)
        :if ($dcsMwigIndication = 0) do={:set $dcsMwigIndication false} else={:set $dcsMwigIndication true}
        :if ($dcsMwig = 2) do={:set $dcsCode 2} else={:set $dcsCode 0}
      }
    }
    
    :set $sctsLine [:pick $tpduLine (10 + $oaLen) (24 + $oaLen)]
    
    :set $udl [:tonum ("0x".[:pick $tpduLine (24 + $oaLen) (26 + $oaLen)])]
    
    :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, length mismatch"; throw;
    }
    
    :if ($udhi) do={
      :set $udhl [:tonum ("0x".[:pick $tpduLine (26 + $oaLen) (28 + $oaLen)])]
      :set $udh [:pick $tpduLine (28 + $oaLen) (28 + $oaLen + $udhl * 2)]
      
      :while ([:typeof $udh] = "str") do={
        :set $udhIEI [:pick $udh 0 2]
        :set $udhLIE [:tonum ("0x".[:pick $udh 2 4])]
        :set $udhIE [:pick $udh 4 (4 + $udhLIE * 2)]
        
        :set ($structUdh->"size") (($structUdh->"size") + 2 + $udhLIE)
        
        :if ([:typeof [:find $structUdh $udhIEI]] != num) do={
          :set $structUdh ($structUdh , [[:parse "({\"$udhIEI\"=\"$udhIE\"})"]])
        } else={
          :set ($structUdh->$udhIEI) $udhIE
        }
        
        :if ($udhIEI = "00") do={
          :set ($structUdh->"concatenated") true
          :if ([:len $udhIE] != 6) do={:set $retArg "Error parse, wrong concat length"; throw;}
          :set $udhOctet1 [:tonum ("0x".[:pick $udhIE 0 2])]
          :set $udhOctet2 [:tonum ("0x".[:pick $udhIE 2 4])]
          :set $udhOctet3 [:tonum ("0x".[:pick $udhIE 4 6])]
        }
        
        :if ($udhIEI = "08") do={
          :set ($structUdh->"concatenated") true
          :if ([:len $udhIE] != 8) do={:set $retArg "Error parse, wrong concat length"; throw;}
          :set $udhOctet1 [:tonum ("0x".[:pick $udhIE 0 4])]
          :set $udhOctet2 [:tonum ("0x".[:pick $udhIE 4 6])]
          :set $udhOctet3 [:tonum ("0x".[:pick $udhIE 6 8])]
        }
        
        :set $udh [:pick $udh ($udhLIE * 2 + 4) [:len $udh]]
      }
    }
    
    :if ($udhi) do={
      :set $udText [:pick $tpduLine (28 + $oaLen + $udhl * 2) [:len $tpduLine]]
    } else={
      :set $udText [:pick $tpduLine (26 + $oaLen) [:len $tpduLine]]
    }
    
    :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;"structUdh"=$structUdh;"udhOctet1"=$udhOctet1;"udhOctet2"=$udhOctet2;"udhOctet3"=$udhOctet3;"udText"=$udText}
  } on-error={}
  :return $retArg
};

:log info "functionMTI loaded"
functionPDU

Код: Выделить всё

# functionPDU - PDU decoding functions (UCS2, 7-bit, address) / декодеры PDU

# ---------- convert7bitToUtf8 ----------
:global convert7bitToUtf8 do={
  :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;
  :local nextpart 0;
  :local escape false;
  :local decodedLine "";
  do {
    :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)]);
          :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)]);
              :set $decodedLine ($decodedLine.$tmp2);
              :set $escape false;
            } else={
              :set $decodedLine ($decodedLine.($alphabetToUtf8->[:tostr $tmp]));
            };
          };
          :set $curbit 0;
          :set $nextpart 0;
        };
      };
      # remove trailing fill character (0x00 = '\40' in GSM alphabet) / убрать заполнитель
      :if (($curbit = 0) and ([:pick $decodedLine ([:len $decodedLine] - 1)] = "\40")) do={
        :set $decodedLine [:pick $decodedLine 0 ([:len $decodedLine] - 1)];
      }
    } on-error={}
    :return $decodedLine;
};

# ---------- convertAddress ----------
:global convertAddress do={
  :local decodedLine "";
  :local instring $1
  :local typeOfAddress [:tonum ("0x".[:pick $instring 0 2])];
  :local typeOfNumber (($typeOfAddress >> 4) & 7);
  :local nameNumber {"Unknown";"International number";"National number";"Network specific number";"Subscriber number";"Alphanumeric";"Abbreviated number";"Reserved for extension"};
  :local numberingPlanIdentification ($typeOfAddress & 15);
  :local addressValue [:pick $instring 2 [:len $instring]];
  :local size [:len $addressValue];
  :local rotare "";
  :local bcd {"A"="*";"B"="#";"C"="a";"D"="b";"E"="c"};
  do {
    :if (($typeOfAddress >> 7) != 1) do={:set $decodedLine "Error: 7 bits not set"; throw;};
    :if (($typeOfNumber = 3) or ($typeOfNumber = 4) or ($typeOfNumber = 6) or ($typeOfNumber = 7)) do={
      :set $decodedLine ("Error: unsupported number type - ".($nameNumber->$typeOfNumber)); throw;
    };
    :if (($size % 2) = 1) do={:set $decodedLine "Error: length not byte aligned"; throw;};
    :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));
            };
          };
        };
      }
      :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).")");
      };
    };
  } on-error={}
  :return $decodedLine;
};

# ---------- symbolsHex (256 bytes mapping) ----------
:global 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"};

# ---------- convertUcs2ToUtf8 (UCS2/UTF-16BE -> UTF-8) ----------
:global convertUcs2ToUtf8 do={
  :local decodedLine "";
  :global symbolsHex;
  :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;
};

# ---------- UnixTimeToFormat ----------
:global UnixTimeToFormat do={
  :local decodedLine ""
  :if ($timeStamp < 0) do={:set $timeStamp ($timeStamp * -1)}
  :local timeS ($timeStamp % 86400)
  :local timeH ($timeS / 3600)
  :local timeM ($timeS % 3600 / 60)
  :set $timeS ($timeS - $timeH * 3600 - $timeM * 60)
  :local dateD ($timeStamp / 86400)
  :local dateM 2
  :local dateY 1970
  :local leap false
  :while (($dateD / 365) > 0) do={
    :set $dateD ($dateD - 365)
    :set $dateY ($dateY + 1)
    :set $dateM ($dateM + 1)
    :if ($dateM = 4) do={:set $dateM 0
      :if (($dateY % 400 = 0) or ($dateY % 100 != 0)) do={:set $leap true; :set $dateD ($dateD - 1)}} else={:set $leap false}
  }
  :local months [:toarray (0,31,28,31,30,31,30,31,31,30,31,30,31)]
  :if ($leap) do={:set $dateD ($dateD + 1); :set ($months->2) 29}
  do {
    :for i from=1 to=12 do={:if (($months->$i) > $dateD) do={:set $dateM $i; :set $dateD ($dateD + 1); break;} else={:set $dateD ($dateD - ($months->$i))}}
  } on-error={}
  :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;
};

# ---------- convertScts (decode SMS timestamp) ----------
:global convertScts do={
  :local decodedLine "";
  do {
    :local tmp [:tonum ("0x".[:pick $sctsLine 0 1])]
    :local byteY [:tonum ("0x".[:pick $sctsLine 1 2])]
    :if (($tmp > 9) or ($byteY > 9)) do={:set $decodedLine $sctsLine; throw;}
    :set $byteY ($byteY * 10 + $tmp)
    :set $tmp [:tonum ("0x".[:pick $sctsLine 2 3])]
    :local byteMn [:tonum ("0x".[:pick $sctsLine 3 4])]
    :if (($tmp > 9) or ($byteMn > 1)) do={:set $decodedLine $sctsLine; throw;}
    :set $byteMn ($byteMn * 10 + $tmp)
    :if ($byteMn > 12) do={:set $decodedLine $sctsLine; throw;}
    :set $tmp [:tonum ("0x".[:pick $sctsLine 4 5])]
    :local byteD [:tonum ("0x".[:pick $sctsLine 5 6])]
    :if (($tmp > 9) or ($byteD > 3)) do={:set $decodedLine $sctsLine; throw;}
    :set $byteD ($byteD * 10 + $tmp)
    :if ($byteD > 31) do={:set $decodedLine $sctsLine; throw;}
    :set $tmp [:tonum ("0x".[:pick $sctsLine 6 7])]
    :local byteH [:tonum ("0x".[:pick $sctsLine 7 8])]
    :if (($tmp > 9) or ($byteH > 2)) do={:set $decodedLine $sctsLine; throw;}
    :set $byteH ($byteH * 10 + $tmp)
    :if ($byteH > 23) do={:set $decodedLine $sctsLine; throw;}
    :set $tmp [:tonum ("0x".[:pick $sctsLine 8 9])]
    :local byteM [:tonum ("0x".[:pick $sctsLine 9 10])]
    :if (($tmp > 9) or ($byteM > 5)) do={:set $decodedLine $sctsLine; throw;}
    :set $byteM ($byteM * 10 + $tmp)
    :if ($byteM > 59) do={:set $decodedLine $sctsLine; throw;}
    :set $tmp [:tonum ("0x".[:pick $sctsLine 10 11])]
    :local byteS [:tonum ("0x".[:pick $sctsLine 11 12])]
    :if (($tmp > 9) or ($byteS > 5)) do={:set $decodedLine $sctsLine; throw;}
    :set $byteS ($byteS * 10 + $tmp)
    :if ($byteS > 59) do={:set $decodedLine $sctsLine; throw;}
    :set $tmp [:tonum ("0x".[:pick $sctsLine 12 13])]
    :local byteO [:tonum ("0x".[:pick $sctsLine 13 14])]
    :if ($tmp > 9) do={:set $decodedLine $sctsLine; 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 offsetR [/system clock get gmt-offset]
    :if (($offsetR >> 31) = 1) do={:set $offsetR ($offsetR - 4294967296)}
    :local hd "Date: "
    :if ([:typeof $headerDate] = "str") do={:set $hd $headerDate}
    :set $tmp [$UnixTimeToFormat timeStamp=$unixTime timeStruct=$timeStruct]
    :if ([:len $tmp] = 0) do={:set $decodedLine $sctsLine; throw;}
    :set $decodedLine ($hd.$tmp)
    :if ($byteO = $offsetR) do={:set $decodedLine ($decodedLine."\r\n")} else={
      :set $decodedLine ($decodedLine." (SMS time)\r\n")
      :set $tmp ($unixTime + $byteO - $offsetR)
      :set $tmp [$UnixTimeToFormat timeStamp=$tmp timeStruct=$timeStruct]
      :if ([:len $tmp] = 0) do={:set $decodedLine $sctsLine; throw;}
      :set $decodedLine ($decodedLine.$hd.$tmp." (OS time) \r\n")
    }
  } on-error={}
  :if ([:len $decodedLine] = 0) do={:set $decodedLine $sctsLine}
  :return $decodedLine;
};

:log info "functionPDU loaded"
PDUtoEMAIL

Код: Выделить всё

:global PDUtoEMAIL do={
  :local emailAdr "!!!YOUR_ADDRESS!!!@gmail.com"
  :local headerFrom "From: "
  :local headerDate "Date: "

  :log info "=== PDUtoEMAIL START ==="
  /system script run functionPDU; :delay 200ms
  /system script run functionMTI; :delay 200ms
  /system script run extractModem; :delay 200ms

  :if ([:typeof $mti0] = "nothing") do={:log error "mti0 missing"; :return false}
  :if ([:typeof $extractModem] = "nothing") do={:log error "extractModem missing"; :return false}

  :local flatPdus [$extractModem]
  :local pduCount [:len $flatPdus]
  :log info ("Extracted PDUs: ".$pduCount)

  :local emailBody ""
  :local rawPdus ""

  # === PASS 1: decode all PDUs into flat arrays === / ПРОХОД 1: декодируем все PDU в плоские массивы
  :local allSn     [:toarray ""]
  :local allTm     [:toarray ""]
  :local allTxt    [:toarray ""]
  :local allInfo   [:toarray ""]
  :local allConcat [:toarray ""]
  :local allKey    [:toarray ""]
  :local allTotal  [:toarray ""]
  :local allSeq    [:toarray ""]
  :local used      [:toarray ""]

  :local pduIdx 0
  :while ($pduIdx < $pduCount) do={
    :local pduLine (($flatPdus->$pduIdx)->"pdu")
    :if ([:len $pduLine] > 0) do={
      :set $rawPdus ($rawPdus . "=== PDU $pduIdx ===\r\n" . $pduLine . "\r\n\r\n")
      :local scaLen [:tonum ("0x".[:pick $pduLine 0 2])]
      :local tpduLine ""
      :if ($scaLen > 0) do={:set $tpduLine [:pick $pduLine (($scaLen * 2) + 2) [:len $pduLine]]} else={:set $tpduLine [:pick $pduLine 2 [:len $pduLine]]}
      :local pduType [:tonum ("0x".[:pick $tpduLine 0 2])]
      :local mti ($pduType & 3)

      :if ($mti = 0) do={
        :local parsedSms [$mti0 pduType=$pduType debugPduParse=false tpduLine=$tpduLine]
        :if ([:typeof $parsedSms] = "array") do={
          :local ud ($parsedSms->"udText")
          :local dc ($parsedSms->"dcsCode")
          :local txt $ud
          :if ([:len $ud] > 0) do={
            :if ($dc = 0) do={:if ([:typeof $convert7bitToUtf8] != "nothing") do={:set $txt [$convert7bitToUtf8 instring=$ud]}}
            :if ($dc = 2) do={:if ([:typeof $convertUcs2ToUtf8] != "nothing") do={:set $txt [$convertUcs2ToUtf8 instring=$ud]}}
          }
          :local sn ($headerFrom.($parsedSms->"oaLine"))
          :local oa ($parsedSms->"oaLine")
          :if ([:len $oa] > 0) do={:if ([:typeof $convertAddress] != "nothing") do={:set $sn ($headerFrom.[$convertAddress $oa])}}
          :local tm $headerDate
          :local sc ($parsedSms->"sctsLine")
          :if ([:len $sc] >= 12) do={
            :local y ([:tonum ("0x".[:pick $sc 1 2])]*10 + [:tonum ("0x".[:pick $sc 0 1])])
            :local mo ([:tonum ("0x".[:pick $sc 3 4])]*10 + [:tonum ("0x".[:pick $sc 2 3])])
            :local d ([:tonum ("0x".[:pick $sc 5 6])]*10 + [:tonum ("0x".[:pick $sc 4 5])])
            :local h ([:tonum ("0x".[:pick $sc 7 8])]*10 + [:tonum ("0x".[:pick $sc 6 7])])
            :local mi ([:tonum ("0x".[:pick $sc 9 10])]*10 + [:tonum ("0x".[:pick $sc 8 9])])
            :local se ([:tonum ("0x".[:pick $sc 11 12])]*10 + [:tonum ("0x".[:pick $sc 10 11])])
            :local hs [:tostr $h]; :if ($h < 10) do={:set $hs ("0".$hs)}
            :local ms [:tostr $mo]; :if ($mo < 10) do={:set $ms ("0".$ms)}
            :local ds [:tostr $d]; :if ($d < 10) do={:set $ds ("0".$ds)}
            :local mis [:tostr $mi]; :if ($mi < 10) do={:set $mis ("0".$mis)}
            :local ss [:tostr $se]; :if ($se < 10) do={:set $ss ("0".$ss)}
            :set $tm ($headerDate."20".[:tostr $y].".".$ms.".".$ds." ".$hs.":".$mis.":".$ss)
          }
          :local isConcat ($parsedSms->"structUdh"->"concatenated")
          :local mtiNames {"SMS-DELIVER";"SMS-SUBMIT-REPORT";"SMS-STATUS-REPORT";"RESERVED"}
          :local info ("PDU type: ".($mtiNames->$mti).", Class ".($parsedSms->"dcsClass"))
          :local ck ""
          :local tot 0
          :local sq 0
          :if ([:typeof $isConcat] = "bool" and $isConcat) do={
            :set $ck (($parsedSms->"oaLine")."_".($parsedSms->"udhOctet1"))
            :set $tot ($parsedSms->"udhOctet2")
            :set $sq ($parsedSms->"udhOctet3")
          }
          :set $allSn     ($allSn,     $sn)
          :set $allTm     ($allTm,     $tm)
          :set $allTxt    ($allTxt,    $txt)
          :set $allInfo   ($allInfo,   $info)
          :set $allConcat ($allConcat, ([:typeof $isConcat] = "bool" and $isConcat))
          :set $allKey    ($allKey,    $ck)
          :set $allTotal  ($allTotal,  $tot)
          :set $allSeq    ($allSeq,    $sq)
          :set $used      ($used,      false)
        }
      }
    }
    :set $pduIdx ($pduIdx + 1)
  }

  # === PASS 2: assemble groups in sequence order === / ПРОХОД 2: выводим сообщения группами
  :local count [:len $allSn]
  :local gi 0
  :while ($gi < $count) do={
    :if (!($used->$gi)) do={
      :if (($allConcat->$gi)) do={
        :local key ($allKey->$gi)
        :local total ($allTotal->$gi)
        :local assembled ""
        :local oi 1
        :while ($oi <= $total) do={
          :local found false
          :for si from=0 to=($count - 1) do={
            :if (!($used->$si) and ($allConcat->$si) and ($allKey->$si) = $key and ($allSeq->$si) = $oi) do={
              :set $assembled ($assembled . ($allTxt->$si))
              :set ($used->$si) true
              :set $found true
            }
          }
          :if (!$found) do={:set $oi ($total + 1)}
          :set $oi ($oi + 1)
        }
        :set $emailBody ($emailBody . ($allSn->$gi)."\r\n".($allTm->$gi)."\r\n".($allInfo->$gi)."\r\n".$assembled."\r\n\r\n")
      } else={
        :set $emailBody ($emailBody . ($allSn->$gi)."\r\n".($allTm->$gi)."\r\n".($allInfo->$gi)."\r\n".($allTxt->$gi)."\r\n\r\n")
        :set ($used->$gi) true
      }
    }
    :set $gi ($gi + 1)
  }

  :if ([:len $rawPdus] > 0) do={:set $emailBody ($emailBody . "\r\n=== RAW PDUs ===\r\n\r\n" . $rawPdus)}
  :local sendOk false
  :if ([:len $emailBody] > 0) do={
    /tool e-mail send to=$emailAdr subject=("SMS from ".[/system identity get name]) body=$emailBody
    :log info "Email sent"
    :set $sendOk true
  } else={:log info "No SMS"}

  # Delete SMS after successful send / Удаление SMS после успешной отправки
  :if ($sendOk) do={
    :local mName ""
    :foreach i in=[/interface lte find] do={:if ([/interface lte get $i value-name=disabled] = false) do={:set $mName [/interface lte get $i value-name=name]}}
    :if ([:len $mName] > 0) do={
      :local deleted false
      :local r
      :set $r ([/interface lte at-chat $mName input="AT+CMGD=1,4" wait=yes as-value]->"output")
      :if ($r~"OK") do={:set $deleted true; :log info "SMS deleted: CMGD=1,4"}
      :if (!$deleted) do={
        :set $r ([/interface lte at-chat $mName input="AT+CMGD=1,1" wait=yes as-value]->"output")
        :if ($r~"OK") do={:set $deleted true; :log info "SMS deleted: CMGD=1,1"}
      }
      :if (!$deleted) do={
        :set $r ([/interface lte at-chat $mName input="AT+CMGD=0" wait=yes as-value]->"output")
        :if ($r~"OK") do={:set $deleted true; :log info "SMS deleted: CMGD=0"}
      }
      :if (!$deleted) do={
        :local idx 0
        :while ($idx < 20 and !$deleted) do={
          :set $idx ($idx + 1)
          :set $r ([/interface lte at-chat $mName input=("AT+CMGD=".[:tostr $idx]) wait=yes as-value]->"output")
          :if ($r~"OK") do={:set $deleted true}
        }
        :if ($deleted) do={:log info "SMS deleted: individual"} else={:log info "Delete failed"}
      }
    }
  }
  :log info "=== PDUtoEMAIL END ==="
  :return true
}
$PDUtoEMAIL