Страница 4 из 4
Re: RouterOS и кодировка SMS UCS-2
Добавлено: 21 июл 2026, 09:32
stpa
EagleNN писал(а): ↑20 июл 2026, 01:05
В результате много чего было переписано и дополнено. Нормально работает на актуальной, на данный момент, прошивке.
Проверено на Mikrotik LHG LTE6 с модемом
Model "R11e-LTE6"
Revision R11e-LTE6_V039
Скрипт состоит из 4 частей:
- extractModem
functionMTI
functionPDU
PDUtoEMAIL
На RBS_XTR_LTE4 c модемом EP06E на RouterOS 7.23.2
Дает ошибку mti0 missing
Не подскажете в каком блоке ковырять?
Re: RouterOS и кодировка SMS UCS-2
Добавлено: 26 июл 2026, 01:38
EagleNN
Обновления скрипта, т.к. нашлись недочеты.
Проверено на Mikrotik LHG LTE6 с модемом
Model "R11e-LTE6"
Revision R11e-LTE6_V039
Скрипт состоит из 4 частей:
- extractModem
functionMTI
functionPDU
PDUtoEMAIL
extractModem
Код: Выделить всё
# extractModem — извлечение PDU из модема / extract SMS PDUs from modem
# Сначала ME, потом SM / Tries ME storage first, then SM
:global extractModem do={
:local output [:toarray ""]
# Хелпер AT-команд / AT-command helper
: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 active 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}
# Опрос: ME, затем SM / Query each modem: ME then SM
:foreach m in=$nameFind do={
:foreach storage in={"ME";"SM"} do={
:local tmp [$chat ($m->"type") ($m->"name") \
("AT+CPMS=\"".[:tostr $storage]."\"")]
:set $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={
# Смещение PDU / Find PDU data offset
: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)
# Пропуск \r\n / Skip \r\n
: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";"storage"=$storage}})
}
}
}
}
# Следующий +CMGL / Find next +CMGL
: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 / SMS-DELIVER TPDU parser
: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 {
# Разбор флагов TP-MTI / Parse TP-MTI flags
: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}
# Разбор адреса отправителя / Parse originating address
:set $oaLen [:tonum ("0x".[:pick $tpduLine 2 4])]
:local oaType (([:tonum ("0x".[:pick $tpduLine 4 6])] >> 4) & 7)
:if ($oaType = 5) do={
:set $oaLen (($oaLen * 7 + 7) / 8 * 2)
} else={
:if ($oaLen % 2 != 0) do={:set $oaLen ($oaLen + 1)}
}
:if ($oaLen > 28) do={
:set $retArg "Error parse, incorrect len OA"; throw;
}
:set $oaLine [:pick $tpduLine 4 (6 + $oaLen)]
# Разбор PID: 0, 0x21, 0x41, 0x42, 0x7F, 0x80-0x9F
:set $pid [:tonum ("0x".[:pick $tpduLine (6 + $oaLen) (8 + $oaLen)])]
:if ($pid != 0 and $pid != 0x21 and $pid != 0x41 \
and $pid != 0x42 and $pid != 0x7F) do={
:if ($pid < 0x80 or $pid > 0x9F) do={
:set $retArg "Error parse, unknown PID"; throw;
}
}
# Разбор DCS / Parse DCS
: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}
}
}
:if (($dcsModel = 1) or ($dcsModel = 2)) do={
:set $dcsCode 2; :set $compressedUd false
}
:if ($dcsModel = 1) do={:set $dcsClass ($dcs & 3)}
# Разбор SCTS (метка времени) / Parse SCTS (timestamp)
:set $sctsLine [:pick $tpduLine (10 + $oaLen) (24 + $oaLen)]
# Разбор UDL (длина данных) / Parse UDL
:set $udl [:tonum ("0x".[:pick $tpduLine (24 + $oaLen) (26 + $oaLen)])]
# Проверка длины TPDU / Validate TPDU length
: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;
}
# Разбор UDH / Parse UDH if present
: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])]
:if ($udhLIE = 0) do={:set $udh ""}
: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
}
# Конкатенация: 8-битная ссылка / 8-bit concat
: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])]
}
# Конкатенация: 16-битная ссылка / 16-bit concat
: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]]
}
}
# Извлечение текста / Extract user data text
:if ($udhi) do={
:set $udText [:pick $tpduLine \
(28 + $oaLen + $udhl * 2) [:len $tpduLine]]
} else={
:set $udText [:pick $tpduLine (26 + $oaLen) [:len $tpduLine]]
}
# Сборка результата / Build result array
: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 / PDU decoding functions
# UCS2->UTF-8, 7-bit GSM->UTF-8, address, timestamp
# ---------- convert7bitToUtf8 (GSM 03.38 alphabet) ----------
: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 (CR = \40 in GSM)
: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
Код: Выделить всё
# PDUtoEMAIL — извлечение, декодирование, сборка, отправка, удаление
# PDUtoEMAIL — extract, decode, assemble, send, delete
:global PDUtoEMAIL do={
:local emailAdr "!!!!EMAIL_ADDRESS!!!@gmail.com"
:local hdrFrom "From: "
:local hdrDate "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 ""
# ПРОХОД 1 / PASS 1: collect all into flat arrays
: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 ($hdrFrom.($parsedSms->"oaLine"))
:local oa ($parsedSms->"oaLine")
:if ([:len $oa] > 0) do={
:if ([:typeof $convertAddress] != "nothing") do={
:set $sn ($hdrFrom.[$convertAddress $oa])
}
}
:local tm $hdrDate
: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 ($hdrDate."20".[:tostr $y]."." \
.$ms.".".$ds." ".$hs.":".$mis.":".$ss)
}
:local isc ($parsedSms->"structUdh"->"concatenated")
:local ctrue false
:if ([:typeof $isc] = "bool") do={
:if ($isc) do={:set $ctrue true}
}
:local mt {"SMS-DELIVER";"SMS-SUBMIT-REPORT";\
"SMS-STATUS-REPORT";"RESERVED"}
:local info ("PDU type: ".($mt->$mti) \
.", Class ".($parsedSms->"dcsClass"))
:local ck ""
:local tot 0
:local sq 0
:if ($ctrue) 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, $ctrue)
:set $allKey ($allKey, $ck)
:set $allTotal ($allTotal, $tot)
:set $allSeq ($allSeq, $sq)
:set $used ($used, 0)
}
}
}
:set $pduIdx ($pduIdx + 1)
}
# ПРОХОД 2 / PASS 2: assemble concatenated groups by seq
:local count [:len $allSn]
:local gi 0
:while ($gi < $count) do={
:if (($used->$gi) = 0) do={
:if (($allConcat->$gi) = true) do={
:local key ($allKey->$gi)
:local total ($allTotal->$gi)
:local assembled ""
:local oi 1
:while ($oi <= $total) do={
:local found 0
:for si from=0 to=($count - 1) do={
:if (($used->$si) = 0) do={
:if (($allConcat->$si) = true) do={
:if (($allKey->$si) = $key) do={
:if (($allSeq->$si) = $oi) do={
:set $assembled ($assembled . ($allTxt->$si))
:set ($used->$si) 1
:set $found 1
}
}
}
}
}
:if ($found = 0) 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")
:set ($used->$gi) 1
} 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) 1
}
}
: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"}
# Удаление из ME и SM / Delete from both storages
: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={
:foreach storage in={"ME";"SM"} do={
:local sr ([/interface lte at-chat $mName \
input=("AT+CPMS=\"".$storage."\"") \
wait=yes as-value]->"output")
:local del 0; :local r
:set $r ([/interface lte at-chat $mName \
input="AT+CMGD=1,4" wait=yes as-value]->"output")
:if ($r~"OK") do={:set $del 1}
:if ($del = 0) do={
:set $r ([/interface lte at-chat $mName \
input="AT+CMGD=1,1" wait=yes as-value]->"output")
:if ($r~"OK") do={:set $del 1}
}
:if ($del = 0) do={
:set $r ([/interface lte at-chat $mName \
input="AT+CMGD=0" wait=yes as-value]->"output")
:if ($r~"OK") do={:set $del 1}
}
:if ($del = 0) do={
:local idx 0
:while ($idx < 20) do={
:set $idx ($idx + 1)
:if ($del = 0) do={
:set $r ([/interface lte at-chat $mName \
input=("AT+CMGD=".[:tostr $idx]) \
wait=yes as-value]->"output")
:if ($r~"OK") do={:set $del 1}
}
}
}
:if ($del = 1) do={
:log info ("SMS deleted from ".$storage)
} else={
:log info ("Delete failed: ".$storage)
}
}
}
}
:log info "=== PDUtoEMAIL END ==="
:return true
}
$PDUtoEMAIL
Re: RouterOS и кодировка SMS UCS-2
Добавлено: 26 июл 2026, 01:41
EagleNN
stpa писал(а): ↑21 июл 2026, 09:32
EagleNN писал(а): ↑20 июл 2026, 01:05
В результате много чего было переписано и дополнено. Нормально работает на актуальной, на данный момент, прошивке.
Проверено на Mikrotik LHG LTE6 с модемом
Model "R11e-LTE6"
Revision R11e-LTE6_V039
Скрипт состоит из 4 частей:
- extractModem
functionMTI
functionPDU
PDUtoEMAIL
На RBS_XTR_LTE4 c модемом EP06E на RouterOS 7.23.2
Дает ошибку mti0 missing
Не подскажете в каком блоке ковырять?
Попробуйте новую версию (пост выше), возможно уже поправил.
Если нет, попробуйте выполнить скрипт ниже и прислать логи.
Самое простое, после выполнения скрипта, в терминале выполнить:
/log print without-paging where topics~"script"
Код: Выделить всё
# diag_mti — диагностика mti0 missing / mti0 missing diagnostic
:log info "=== diag_mti START ==="
# 1. Проверим наличие всех глобалов
:local funcs {"mti0";"extractModem";"convertUcs2ToUtf8";"convert7bitToUtf8";"convertAddress"}
:foreach f in=$funcs do={
:local t [:typeof ($f)]
:log info ("$f = ".[:tostr $t])
}
# 2. Посмотрим source functionMTI
:local srcLen 0
:do {
:set srcLen [:len [/system script get functionMTI source]]
:log info ("functionMTI source length = ".[:tostr $srcLen])
} on-error={
:log error "Cannot read functionMTI source"
}
# 3. Попробуем запустить functionMTI отдельно
:do {
/system script run functionMTI
:delay 200ms
:local t [:typeof $mti0]
:log info ("After run: mti0 = ".[:tostr $t])
} on-error={
:log error "functionMTI run failed"
}
# 4. Проверим версию RouterOS
:log info ("ROS: ".[/system resource get version])
# 5. Проверим длину строк в source (нет ли обрезанных)
:do {
:local src [/system script get functionMTI source]
:local lines 0; :local max 0
:local pos 0; :local len [:len $src]
:while ($pos < $len) do={
:local nl [:find $src "\n" $pos]
:if ([:typeof $nl] != "num") do={:set $nl $len}
:local llen ($nl - $pos)
:if ($llen > $max) do={:set $max $llen}
:set $lines ($lines + 1)
:set $pos ($nl + 1)
}
:log info ("functionMTI: lines=".[:tostr $lines]." max=".[:tostr $max])
} on-error={
:log error "Line analysis failed"
}
:log info "=== diag_mti END ==="