Страница 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 ==="