# readSMS 0.1 Copyright © 2025 lelic # License GPLv3+: GNU GPL version 3 or later . # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # :global toChar {"\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"}; :global i2str3 do={ :return (($val/100).($val/10).($val%10)); }; :global pdu2Num do={ :local buf ""; :for idx from=0 to=[:len $pdu] step=2 do={ :local byte [:tonum ("0x".[:pick $pdu $idx ($idx+2)])]; :local digA (byte & 0x0f); :local digB (byte >> 4); :if ($digA != 0xf) do={ :set buf ($buf.$digA); }; :if ($digB != 0xf) do={ :set buf ($buf.$digB); }; }; :return $buf; }; :global pdu2Date do={ :global pdu2Num; :local buf [$pdu2Num pdu=$pdu]; :local tz [:tonum ([:pick $buf 12 13].[:pick $buf 13 14])]; :local sign; :if (($tz & 0x8000) != 0) do={ :set sign "-"; } else={ :set sign "+"; }; :set tz (($tz & 0x7ffff) * 15); :local h ($tz/60); :local m ($tz%60); if ($tz = 0) do={ :set sign ""}; :return ([:pick $buf 4 6]."-".[:pick $buf 2 4]."-20".[:pick $buf 0 2]." ".[:pick $buf 6 8].":".[:pick $buf 8 10].":".[:pick $buf 10 12]." ".$sign.($h/10).($h%10).":".($m/10).($m%10)); }; :global pdu2Char do={ :global toChar; :local bitCount 0; :local number 0; :local buf ""; :for idx from=0 to=[:len $pdu] step=2 do={ :local byte [:tonum ("0x".[:pick $pdu $idx ($idx+2)])]; :set number ($number + ($byte << $bitCount)); :set bitCount ($bitCount + 1); :set buf ($buf.$toChar->($number & 0x7F)); :set number ($number >> 7); :if ($bitCount = 7) do={ :set buf ($buf.$toChar->$number); :set bitCount 0; :set number 0; }; }; :return $buf; }; :global pdu2Utf do={ :global pdu2Char; :global toChar; :local buf ""; :if ($dcs = 0) do={ :for idx from=0 to=[:len $pdu] step=2 do={ :set buf [$pdu2Char pdu=$pdu]; }; } else={ :if ($dcs = 8) do={ :local end ([:len $pdu]-1); :for idx from=0 to=$end step=4 do={ :local i [:tonum ("0x".[:pick $pdu $idx ($idx +4)])]; :if ($i < 0x80) do={ :set buf ($buf.($toChar->$i)); }; :if (($i >= 0x80) and ($i < 0x800)) do={ :local byteA (($i >> 6) | 192); :local byteB (($i & 63) | 128); :set buf ($buf.($toChar->$byteA).($toChar->$byteB)); }; :if ($i >= 0x800) do={ :local byteA (($i >> 12) | 224); :local byteB ((($i >> 6) & 63) | 128); :local byteC (($i & 63) | 128); :set buf ($buf.($toChar->$byteA).($toChar->$byteB).($toChar->$byteC)); }; }; } else={ :set buf "Unknown DCS type ($dcs)"; }; }; :return $buf; }; :global pdu2SMS do={ :global toChar; :global pdu2Num; :global pdu2Date; :global pdu2Utf; :local buf $pdu; :local pduInfoLen [:tonum ("0x".[:pick $buf 0 2])]; :local smsSCLen (($pduInfoLen-1)*2); :set buf [:pick $buf 2 ([:len $buf]+1)]; :local pduAddrType [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local smsSC [$pdu2Num pdu=[:pick $buf 0 $smsSCLen]]; :set buf [:pick $buf $smsSCLen ([:len $buf]+1)]; :local tpduFirst [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local smsUDH false; :if ($tpduFirst & 0x40 != 0) do={ :set smsUDH true; }; :local smsFromLen [:tonum ("0x".[:pick $buf 0 2])]; :set smsFromLen ($smsFromLen+$smsFromLen%2); :set buf [:pick $buf 2 ([:len $buf]+1)]; :local smsFromType [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local smsFrom [$pdu2Num pdu=[:pick $buf 0 $smsFromLen]]; :set buf [:pick $buf $smsFromLen ([:len $buf]+1)]; :local smsTPPID [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local smsTPDCS [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local smsDate [$pdu2Date pdu=[:pick $buf 0 14]]; :set buf [:pick $buf 14 ([:len $buf]+1)]; :local smsLen [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local udhLen; :local isConcat false; :local partRn 0; :local partTotal 0; :local partNum 0; :if ($smsUDH) do={ :set udhLen [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local ieId [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :local ieLen [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :if ($ieId = 8) do={ :set partRn [:tonum ("0x".[:pick $buf 0 4])]; :set buf [:pick $buf 4 ([:len $buf]+1)]; } else={ :set partRn [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; }; :set partTotal [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :set partNum [:tonum ("0x".[:pick $buf 0 2])]; :set buf [:pick $buf 2 ([:len $buf]+1)]; :set udhLen ($udhLen-$ieLen-2); :if ($udhLen > 0) do={ :local udhData; :set udhData [:pick $buf 0 ($udhLen*2)]; :set buf [:pick $buf ($udhLen*2) ([:len $buf]+1)]; }; :set smsLen ($smsLen-$udhLen); :if ($partRn >0) do={ :set isConcat true; }; }; :local smsBody [$pdu2Utf pdu=$buf dcs=$smsTPDCS]; :return {"isConcat"=$isConcat;"partRn"=$partRn;"partTotal"=$partTotal;"partNum"=$partNum;"from"=$smsFrom;"date"=$smsDate;"message"=$smsBody}; }; /interface lte at-chat lte1 input="AT+CMGF=0" as-value; :local buf [/interface lte at-chat lte1 input="AT+CMGD=?" as-value]; :set buf ($buf->"output"); :local pos [:find $buf "+CMGD:"]; :local msgs; :local msgsIdx; :local partsIdx; :if ([:typeof $pos]="num") do={ :local spos [:find $buf "(" $pos]; :local epos [:find $buf ")" $pos]; :if (([:typeof $spos]="num") && ([:typeof $epos]="num")) do={ :local list [:pick $buf ($spos+1) $epos]; while ([:len $list] > 0) do={ :set epos [:find $list "," 0]; if ([:typeof $epos]="nil") do={ :set epos ([:len $list]+1); }; :local smsIdx [:tonum [:pick $list 0 $epos]]; :set list [:pick $list ($epos+1) ([:len $list]+1)]; :local content [/interface lte at-chat lte1 input="AT+CMGL=4" as-value]; :set content ($content->"output"); :if ([:len $content]>2) do={ :local lineEnd; :local lineStart; :local line; :local flagEnd true; :while ($flagEnd) do={ :set lineStart [find $content "+CMGL:" $lineStart]; :if ([:typeof $lineStart ]="num") do={ :set lineEnd [find $content "+CMGL:" $lineStart]; :if ([:typeof $lineEnd] = "nil") do={ :set $lineEnd [ find $content "OK" $lineStart ]; if ([:typeof $lineEnd] = "nil") do={ :set lineEnd ([:len $content]+1); }; }; :set line [:pick $content $lineStart ($lineEnd - 1)]; :set line [:pick $line ([:find $line "\n"]+1) ([:len $line]+1)]; :local sms [$pdu2SMS pdu=$line]; :local idx [$i2str3 val=$smsIdx]; :if ($sms->"isConcat") do={ :local partIdx [$i2str3 val=($sms->"partNum")]; :local parts; :if ([:typeof ($partsIdx->"rn$($sms->"partRn")")] != "nothing") do={ :set idx ($partsIdx->"rn$($sms->"partRn")"); :local parts [:serialize ($msgs->"parts$idx",[:deserialize "{\"p$partIdx\":\"$($sms->"message")\"}" from json options json.no-string-conversion]) to json]; :set msgs ($msgs,[:deserialize "{\"parts$idx\":$parts}" from json]); } else={ :set partsIdx ($partsIdx,[:deserialize "{\"rn$($sms->"partRn")\":\"$idx\"}" from json options json.no-string-conversion]); :local parts [:serialize [:deserialize "{\"p$partIdx\":\"$($sms->"message")\"}" from json options json.no-string-conversion] to json]; :set msgs ($msgs,[:deserialize "{\"from$idx\":\"$($sms->"from")\",\"date$idx\":\"$($sms->"date")\",\"parts$idx\":$parts}" from json options json.no-string-conversion]); :set msgsIdx ($msgsIdx,{$idx}); }; } else={ :set msgs ($msgs,[:deserialize "{\"from$idx\":\"$($sms->"from")\",\"date$idx\":\"$($sms->"date")\",\"message$idx\":\"$($sms->"message")\"}" from json]); :set msgsIdx ($msgsIdx,{$idx}); }; } else={ :set flagEnd false; }; }; }; }; }; }; :local text; :foreach idx in $msgsIdx do={ :if ([:typeof ($msgs->"parts$idx")] != "nothing") do={ :set text ""; foreach msg in ($msgs->"parts$idx") do={ :set text ($text.$msg); }; } else={ :set text ($msgs->"message$idx"); }; :put "From: $($msgs->"from$idx")\nDate: $($msgs->"date$idx")\nMessage: $text\n\n"; }; # PDU format https://www.gsmfavorites.com/documents/sms/pdutext/ # 07 917238010010F5 040BC87238880900F100009930925161958003C16010 # Octet(s) Description # 0x7F Length of the SMSC information (in this case 7 octets) # 91 Type-of-address of the SMSC. (91 means international format of the phone number) # 72 38 01 00 10 F5 Service center number(in decimal semi-octets). The length of the phone number is odd (11), so a trailing F # has been added to form proper octets. The phone number of this service center is "+27831000015". See below. # 04 First octet of this SMS-DELIVER message. # 0B Address-Length. Length of the sender number (0B hex = 11 dec) # C8 Type-of-address of the sender number # 72 38 88 09 00 F1 Sender number (decimal semi-octets), with a trailing F # 00 TP-PID. Protocol identifier. # 00 TP-DCS Data coding scheme # 99 30 92 51 61 95 80 TP-SCTS. Time stamp (semi-octets) DDMMYYHHMMSSTZ # 0A TP-UDL. User data length, length of message. The TP-DCS field indicated 7-bit data, so the length here is the # number of septets (10). If the TP-DCS field were set to indicate 8-bit data or Unicode, the length would be # the number of octets (9). # E8329BFD4697D9EC37 TP-UD. Message "hellohello" , 8-bit octets representing 7-bit data. # # Concatenated SMS https://en.wikipedia.org/wiki/Concatenated_SMS # One way of sending concatenated SMS (CSMS) is to split the message into 153 7-bit character parts (134 octets), and sending each part with a User Data Header (UDH) tacked onto the beginning. A UDH can be # used for various purposes and its contents and size varies accordingly, but a UDH for concatenating SMSes look like this: # Field 1 (1 octet): Length of User Data Header, in this case 05. # Field 2 (1 octet): Information Element Identifier (IEI), equal to 00 (Concatenated short messages, 8-bit reference number) # Field 3 (1 octet): Length of the Information Element (IEL), excluding the IEI and the IEL; equal to 03 # Field 4 (1 octet): 00-FF, CSMS reference number, must be same for all the SMS parts forming the concatenated message # Field 5 (1 octet): 00-FF, total number of parts. The value shall remain constant for every short message which makes up the concatenated short message. If the value is zero then the receiving entity shall # ignore the whole information element # Field 6 (1 octet): 00-FF, this part's number in the sequence. The value shall start at 1 and increment for every short message which makes up the concatenated short message. If the value is zero or greater # than the value in Field 5 then the receiving entity shall ignore the whole information element. [ETSI Specification: GSM 03.40 Version 5.3.0: July 1996] # It is possible to use a 16 bit CSMS reference number in order to reduce the probability that two different concatenated messages are sent with identical reference numbers to a receiver. In this case, the # User Data Header shall be: # # Field 1 (1 octet): Length of User Data Header (UDL), in this case 06. # Field 2 (1 octet): Information Element Identifier (IEI), equal to 08 (Concatenated short messages, 16-bit reference number) # Field 3 (1 octet): Length of the Information Element (IEL), excluding the IEI and the IEL; equal to 04 # Field 4 (2 octets): 0000-FFFF, CSMS reference number, must be same for all the SMS parts forming the concatenated message # Field 5 (1 octet): 00-FF, total number of parts. The value shall remain constant for every short message which makes up the concatenated short message. If the value is zero then the receiving entity shall # ignore the whole information element # Field 6 (1 octet): 00-FF, this part's number in the sequence. The value shall start at 1 and increment for every short message which makes up the concatenated short message. If the value is zero or greater # than the value in Field 5 then the receiving entity shall ignore the whole information element. [ETSI Specification: GSM 03.40 Version 5.3.0: July 1996]