Jak na SMS v PERLu

15.05.2005

V jednom projektu jsem byl postaven před úkol, jak jednoduše, elelegantně a hlavně neinteraktivně posílat SMS zprávy z telefonu, který je připojen k sériovému portu počítače s FreeBSD. Pod Windows existuje kupa programů, bohužel vetšina z nich je závislá na interaktivitě, pod linux jsem objevil 2, nicméně jeden pro X-ka, druhý by byl použitelný i v příkazové řádce, nicméně ukrutně komplikovaný a bez dokumentace. Jelikož tvorba vlastní SMS zprávy a odeslání je, jak jsem později zjistit, velmi jednoduchou záležitostí, rozdhodl jsem se napsat program vlastní - a to nejsem žádný programátor. Celý tento výtvor je napsaný v programovacím jazyce PERL, který bývá vetšinou standartní součástí linuxových distrubucí, to samé platí i pro FreeBSD.

Komunikace s telefonem

Telefon, ideálně můj oblíbený Siemens, je připojený přes datový kabel k sériovému portu. Datový kabel obsahuje převodník úrovní s RS232, zapojení lze najít na mnoha místech nebo zajít do nějakého bazaru, kde určitě hromadu takových kabelů mají. S telefonem se poté komunikuje pomocí AT příkazů, v tomto případě budou potřeba jen 2. Princip je takový, že připojenému telefonu odešleme příkaz a telefon vrátí odpověď, jednoduché. Parametry nastavení sériového portu by měly být v souladu s tím, co umí telefon. Já jsem to testoval na ME45 a fungovalo bezvadně jak 9600bps, tak i 19200 a 38400bps. Co zvládne třebas Siemens C35 nebo telefon jiného výrobce, to opravdu netuším, 9600bps by však měl zcela určitě. Další parametry jsou 8 bitů, 1 stop bit a bez parity, tedy defaultní hodnoty.

Sériový port pod Linuxem nebo FreeBSD

Sériové porty pod linuxem se tváří jako obyčejné soubory /dev/ttyS0 pro COM1 a /dev/ttyS1 pro COM2. Ve FreeBSD je to stejné, jen jinak pojmenované a to /dev/cuaa0 pro COM1 a /dev/cuaa1 pro COM2. S těmito "soubory" lze pracovat velmi elegatně. Napíšete li příkaz echo AT > /dev/cuaa0, odešle se na COM1 příkaz AT, napíšete-li příkaz cat /dev/cuaa0, bude se vám na terminál vypisovat všechna komunikace, která bude přicházet od telefonu. Defautní nastavení sériového portu ve FreeBSD nebo linuxu pro komunikaci s telefonem není příliš vhodné (=nefunguje) a to z toho důvodu, že unixový systém předpokládá na svém sériovém portu spíše nějaký znakový terminál než mobil. Řešením je program stty, který dokáže se sériovým portem kouzla. Především je nutné vypnout kanonický režim, tedy režim, který odesílá data na sériový port po řádcích, což není příliš dobré, vzhledem k tomu, že při odesílání SMS se znak ukončující řádek nikde nevyskytuje. Další nutností je vypnout echo, tedy snyčka, která znak z výstupu pošle znovu na vstup - toto je dobré u terminálu, pač vidíte, co píšete, ale v tomto případě to v PERLu dělalo problémy. Tak toto stačí pro linux, pro FreeBSD jsem musel přidat ještě daší 4 paramtery, jejich význam je tak trošku nelogický, ale blíže jsem to nezkoumal.
Takže parametry pro sériový port se nastaví takto:
Linux: stty -F /dev/ttyS0 -echo -icanon speed 19200
FreeBSD: stty -f /dev/cuaa0 -echo -icanon isig iexten opost onlcr speed 19200
Aktuální parametry lze vypsat příkazem: stty -F /dev/ttyS0 nebo stty -f /dev/cuaa0

Jak na SMS?

Krátká textová zpráva se posílá jednoduše? Na telefon pošleme AT příkaz tvaru: AT+CMGS=x<CR>, kde x je číslo reprezentující velikost tzv. PDU rámce (v něm je zakódovaná zpráva) mínus jedna. Telefon za krátkou dobu (cca 0-2 sekundy) odpoví znakem > a poté odešleme PDU rámec v ASCII formě, tedy nikoliv v binární. PDU rámec je totiž řetezec čísel v šestnáctkové soustavě a do telefonu se musí poslat právě jako tyto znaky. Například máme řetezec 2AFF3C a na telefon musí jít ASCII tvar: 2, pak A, následně F atd.. Binární tvar by představoval odeslání hodnoty 2A, jakožto jeden byte (znak s číslem 42) - to je chybné. Po odeslání posledního znaku PDU retězce se musí odeslat znak <CTRL+Z>. Pokud hloubáte, co to je <CR> nebo <CTRL+Z>, tak v prvním případě se jedná o znak Enteru, tedy v ASCII znak pod číslem 13 a v druhém případě o ASCII znak pod číslem 26.

Tvorba PDU

PDU rámec obsahuje v sobě základní instrukce pro mobilní telefon, číslo příjemce a zakódovanou zprávu. Lzde do něj přidat spoustu dalších věcí, jako je například číslo SMS centra daného operátora, požadavek na doručenku atd. Vše lze najít v dokumentaci, v tomto případě popíšu pouze nejzákladnější variantu, která bezproblémů funguje.

Mějme telefonní číslo v mezinárodním (nutné) formátu: 420775801456
Mějme zprávu, kterou chceme poslat: Ahoj svete

PDU začíná hlavičkou 000100 - toto berte jako dogma

Poté následuje délka telefonního čísla v hexa, v tomto případě 12 číslic, tedy 0Ch

PDU tedy narostlo na 0001000C

Číslo je v mezinárodním formátu, proto musíme přidat tuto informaci do PDU a to 1 bytem o hodnotě 91h

PDU je tedy už 0001000C91

Nyní se podívejme na telefonní číslo, má 12 znaků, to je sudé číslo a to je v pořádku. Pokud by mělo lichý počet číslic, musíme na konec přídat znak F. Příklad: mám-li číslo 42123214121, to je 13 znaků., liché, musím přidat nakonec F, číslo bude pak vypadat takto 42123214121F a počet číslic bude 14, tedy sudý. Nicméně všechny čísla v ČR mají 12 znaků, takže problém doplnění se týká většinou zahraničních čísel.
Takže máme číslo, které má sudý počet číslic, teď ho musíme zkonvertovat pro PDU. Konverze spočívá v pouhém vzájemném přesunu polohy číslic ve dvojiích. Tedy číslo 420775801456 si rozdělíme na dvojice 42 07 75 80 14 56 a u těchto dvojic vzájemně prohodímě pozici číslic 42->24, 07->70, 75->57, 80->08, 14->41, 56->65. Dostaneme tedz 247057084165 a tento tvar přidáme do PDU. Zkuste se zamyslet, co by se stalo, kdyby číslo mělo lichý počet číslic? V poslední dvojici bychom dostali pouze jednu číslici a nevěděli bychom, s čím ji prohodit. Doplněním písmene F se daná číslice prohodí právě s písmenem F a je vše v pořádku. Telefon ví (protože jsme mu to v minulém kroku řekli), že číslic je lichý počet, tak si přidaného Fka vůbec nevšimne, ale být tam musí.

PDU je po doplnění o telefonní číslo již 0001000C91247057084165

Dále do PDU doplníme 0000 - toto je opět dogma.

PDU je opět narostlo na 0001000C912470570841650000

Teď se podívejme na text zprávy, má 10 znaků, což je 0Ah. Šup s tím do PDU. Textovka může mít maximálně 160 znaků.

PDU po přidání délky textu je 0001000C9124705708416500000A

Teď příjde část nejnáročnější, musíme překonvertovat text zprávy na 7-mi bitový tvar. Postup je takový: vezmeme 7bitů prvního znaku, před něj umístíme 7 bitů následujícího znaku a zase před něj 7 bitů dalšího znaku. Celý bitový řetězec doplníme zepředu nulami tak, aby byl bezezbytku dělitelný 8. Poté odzadu vezmene 8-bitů, převedeme na hexa a vložíme do PDU. Nejasné? Ani se nedivím, následuje podrobnější a barevnější vysvětlení.

Takže převedeme si znaky na ASCII kódy a do binárního tvaru, barevně jsem označil pouze 7 bitů.

A - podle ASCII je 93, tedy 01000001
h - podle ASCII je 104, tedy 01101000
o - podle ASCII je 139, tedy 01101111
j - podle ASCII je 106, tedy 01101010
mezera - podle ASCII je 32, tedy 00100000
s - podle ASCII je 115, tedy 01110011
v - podle ASCII je 118, tedy 01110110
e - podle ASCII je 101, tedy 01100101
t - podle ASCII je 116, tedy 01110100
e - podle ASCII je 101, tedy 01100101

Znaky poskládáme stylem první dozadu a poslední dopředu. Doufám, že nejste arabové a nemáte začátek věty vpravo :-)

1100101111010011001011110110111001101000001101010110111111010001000001

Tuto skládačku rozdělíme odzadu na 8-mice

110010 11110100 11001011 11011011 10011010 00001101 01011011 11110100 01000001

Úplně vlevo chybí do osmice 2 bity a proto je tam doplníme jako nuly (tučné bílé), vznikne tedy bitový řetezec dělitelný 8. V podstatě bychom nic doplňovat nemuseli, protože numerická hodnota zůstane stejná, ale je to tu jen pro úplnost.

00110010 11110100 11001011 11011011 10011010 00001101 01011011 11110100 01000001

Teď vezmeme tyto osmice odzadu a převedeme je na hexa tvar:

01011101=41h
11110100=F4h
01011011=5Bh
00001101=0Dh
10011010=9Ah
11011011=DBh
11001011=CBh
11110100=F4h
00110010=32h

Poté tyto hodnoty napíšeme vedle sebe 41F45B0D9ADBCBF432 a tento řetezec přidáme do PDU.

PDU má tedy finální tvar 0001000C9124705708416500000A41F45B0D9ADBCBF432

Odeslání SMS

Princip odeslání jsem již zmínil o něco řádků výše, ale opakování je matkou moudrosti. Takže nejprve musíme spočítat, kolik byte má PDU řetězec. Ten náš má 23 byte, na telefon se posílá 23 - 1 = 22.

Počítač: AT+CMGS=22<CR>
Telefon: >
Počítač: 0001000C9124705708416500000A41F45B0D9ADBCBF432<CTRL+Z>

To je vše, sežere to 2,-Kč a máme radost :)

Perlivý skript sendsms.pl

#!/usr/bin/perl -w

# Tento skript umi odeslat SMS z telefonu pripojenem naseriovy port
# Pouziti: sendsms.pl <cislo> '<zprava>'
# Cislo musi byt v mezinarodnim formatu a zprava vapostrofech
# Priklad: sendsms.pl 420775234123 'Ahoj clovece sOskarem.'

# Nastaveni portu, kde je telefon zapojeny
$port="/dev/cuaa0";


# Nacteni hodnot z parametru
$cislo=$ARGV[0];
$text=$ARGV[1];

# otestuje cislo na velikost a numericke znaky
print "\nCislo: ";
if(length($cislo)==0) {
  print "cislo nebylo zadano\n\n";
  exit;
}
for($n=0;$n<length($cislo);$n++) {
  $tmp=substr($cislo,$n,1);
  if((ord($tmp)<48) || (ord($tmp)>57)) {
    print $tmp."->chybny znak\n\n";
    exit;
  } else { 
    print $tmp; 
  }
}

# otestuje cislo na sudy pocet a lichemu prida F
if((length($cislo)%2)!=0) {
  $cislo.="F";
}

# prehazi cisla v telefonnim cisle
$pcislo="";
for($n=0;$n<length($cislo);$n=$n+2) {
  $pcislo=$pcislo.substr($cislo,$n+1,1);
  $pcislo=$pcislo.substr($cislo,$n,1);
}

# otestuje delku textu
print "\nText: ";
if((length($text))>160 or (length($text))==0 ) {
  print " -> text je prilis dlouhy nebo nebyl zadan";
  exit;
  } else {
  print $text;
  }

# zacatek tvorby PDU, hlavicka, delka cisla a sluzba
$pdu="000100".&tohex(length($cislo))."91";
# pridani delky nezakodovane zpravy
$pdu=$pdu.$pcislo."0000".&tohex(length($text));

# vytvori binarni 7-bitovy stream zpravy
$stream="";
for($i=0;$i<length($text);$i++){
  $znak=ord(substr($text,$i,1));
  if($znak<32 or $znak>127) {
    $znak=63;
  }
  $vystup="";
  for($n=0;$n<7;$n++){
    $vystup=($znak%2).$vystup;
    $znak=int($znak/2);
  }
  $stream=$vystup.$stream;
}
while (length($stream)%8!=0) {
  $stream="0".$stream;
}
for($n=length($stream)-8;$n>=0;$n=$n-8) {
  $pdu=$pdu.&tohex(&btod(substr($stream,$n,8)));
}

# vypocet delky PDU formatu bez uvodniho byte, v BCD
$delka=(length($pdu)-2)/2;
print "\n\nDelka PDU zpravy bez uvodniho byte: ".$delka;
print "\nTedy musi se poslat prikaz AT+CMGS=".$delka."<CR>";
print "\nPDU(hex): ".$pdu." <CTRL+Z>\n\n";

# odeslani SMS na mobilni telefon
print "Otevirani portu $port: ";
open(SERIAL,">$port") or die ("Portnejde otevrit");
system("stty -f $port -echo -icanon isig iexten opost onlcr");
system("stty -f $port speed 19200 > /dev/null");
print SERIAL "AT\n";
sleep 2;
print "OK\n";
print "Odeslalni SMS: ";
print SERIAL "AT+CMGS=$delka\n";
sleep 2;
print SERIAL ($pdu.chr(26));
close(SERIAL);
sleep 8;
print "OK\n\n";


# fce: prevede 8bit dec na hex
sub tohex {
  my ($arg)=@_;
  my ($out,@values);
  @values=qw(0 1 2 3 4 5 6 7 8 9 A B C D E F);
  $out="";
  $out=$values[int($arg/16)].$values[($arg%16)];
  return ($out);
}

# fce: prevede full8bit bin na dec
sub btod {
  my ($arg)=@_;
  my ($out,$n);
  $out=0;
  for($n=0;$n<8;$n++) {
    $out=$out+(2**$n)*substr($arg,(7-$n),1);
  }
  return ($out);
}