Bogusław Kempny

sms

×
Autor English
Początek HC-SR04 LCD Kamera fork() sms strfry() GPIO impulsy Klawiatura Brama GPIO PWM SG90 RFID Grafolog RCP Shutdown Temperatura ....











Jak wysłać z naszego Raspberry sms?

Potrzebujemy modem i oczywiście kartę SIM:


Modem z tradycyjnym interfejsem, jeśli będzie to modem HiLink, opisaną tu metodą sms'a nie wyślemy. będziemy skazani na ręczne klikanie myszka w przeglądarce internetowej.

Włączamy nasz modem do portu USB Raspberry. Po kilku sekundach w zobaczymy nowe urządzenie USB i nowe porty szeregowe w /dev:

root@mobile3:~# lsusb
Bus 001 Device 005: ID 046d:081b Logitech, Inc. Webcam C310
Bus 001 Device 007: ID 0e8d:00a5 MediaTek Inc. 
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. 
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

root@mobile3:~# ls /dev autofs loop4 ram12 tty0 tty32 tty56 vc-mem . . fuse net sda tty22 tty46 ttyUSB0 vcsa6 gpiomem network_latency serial tty23 tty47 ttyUSB1 vcsa7 hwrng network_throughput serial0 tty24 tty48 ttyUSB2 vcsm initctl null serial1 tty25 tty49 ttyUSB3 vhci . .

Portów ttyUSB może być mniej, zależnie od modelu modemu, nas jednak interesuje wyłącznie ttyUSB0 (chyba, że przed podłączeniem modemu mieliśmy już jakieś porty ttyUSB, wtedy pierwszy z tych które się teraz pojawiły).

Jeśli lsusb pokazało nowe urządzenie, ale w /dev nie pojawiły się nowe urządzenia szeregowe, to albo nasz Raspbian nie zna tego sprzętu, albo jest to modem HiLink.

W tym drugim przypadku możemy spróbować przełączyć go do trybu tradycyjnego, szukając fachowych rad na rozmaitych forach internetowych, choć raczej będzie to strata czasu, a zniszczenie modemu też się może przytrafić.

Jeśli mamy do czynienia z pierwszym przypadkiem, to najprawdopodobniej brakuje nam usb-modeswitch. Instalujemy ten pakiet: apt-get install usb-modeswitch usb-modeswitch-data i próbujemy ponownie.

Jeśli nadal brak sukcesu, to czeka nas śledztwo, jakich reguł opisujących nasz modem w /usr/share/usb_modeswitch brakuje.

Ale mamy pisać o wysyłaniu sms'ow, więc wrócimy do właściwego tematu, wątek śledczy zostawimy do być może innego artykułu.

No to stanowisko do wysyłania smsów gotowe:

Warto sprawdzić, czy nasz modem chce z nami współpracować.

Uruchamiamy minicom -D /dev/ttyUSB0 i piszemy AT <enter>. Modem powinien odpowiedzieć OK.
Możemy też spróbować innych komend AT standardu Hayes, możemy nawet 'ręcznie' wysłać sms.

Welcome to minicom 2.7

OPTIONS: I18n                                                                
Compiled on Jan 12 2014, 05:42:53.                                           
Port /dev/ttyUSB0, 10:47:58                                                  
                                                                             
Press CTRL-A Z for help on special keys                                      
                                                                             
at                                                                           
OK                                                                           
at+cgmi                                                                      
+CGMI: MTK1     



Jeśli coś takiego zobaczymy na ekranie, to program, który za chwilę omówimy, też będzie umiał dogadać się z modemem i wysłac sms.

No to teraz oprogramowanie. Jak wykle przykładowy program i prosty skrypt do jego kompilowania, ściągnąć, skompilować, uruchomić:

./sendsms numer_telefonu treść

Numer telefonu pełny, z numerem kierunkowym kraju, a więc na przykład +48605223344

Sms zostanie wysłany, a w pliku /var/log/sms.log pojawi się wpis co, kiedy i na jaki numer telefonu zostało wysłane.
2018-05-23 15:00:   Nietrzezwy pracownik JAN SEBASTIAN BACH,
zawartosc alkoholu: 0.1 | Status (+48605223344): +CMGS: 1 OK

Jeśli nie interesuje Cię jak ten program działa, to w tym miejscu możesz juz zakończyć lekturę.

Program wysyła do modemu standardową sekwencję komend AT, nic ciekawego, ale po każdej komendzie musi poczekać na odpowiedź modemu i nie zawisnąć albo wybuchnąć jak jej nie dostanie.

Musi też wcześniej zaprogramować port szeregowy. I to jest ciekawe.

Unix (a więc w pewnym sensie i Linux) ma już prawie 60 lat. W czasach jego powstawania komputery i ich interfejsy były całkiem inne.

To, co teraz wydaje się jedynym rozwiązaniem, tak oczywistym, że nie przychodzi nam do głowy, że mogłoby być inaczej, w tamtych archaicznych, licząc czas w skali czasu informatyki, wcale nie było oczywiste.

Weźmy na przykład bajt. 8 bitów. Jakie to piękne i proste dla informatyka. Potęga liczby 2. I jak łatwo to zapisać szesnastkowo!

Pamiętajcie jednak, że w tamtych czasach, kiedy procesor komputera wykonany na pojedynczych tranzystorach, często germanowych, ważył tonę, pamięć (na przykład ferrytowa) miała pojemność liczoną w kilobajtach, każdy bit kosztował koszmarne pieniądze!

Na zdjęciu pamięci ferrytowej w powiększeniu widać to wyraźnie. Każdy z tych ferrytowych koralików ma średnicę okolo 1 milimetra, przewleczono przez niego trzy druciki (ręcznie!), do każdego drucika układ sterowania na tranzystorach germanowych. Pamiętał jeden bit. Ile mógł kosztować hardware obsługujący jeden bit?

Słowo maszynowe jakie teraz znamy, 4, 8, 16, 32 itd. bitów upowszechniło się w latach 70 ubiegłego wieku, wraz z pojawieniem się pierwszych mikroprocesorów. Wcześniej panowała pełna dowolnośc, 22, 50, 27, 48...

Kodowanie znaków też było wynikiem decyzji konstruktora komputera, nie tylko nie było UNICODE, co może nie dziwić, używa się przecież jeszcze ISO, cp1250 itp., ale nawet ASCII o którym z pewnością słyszeliście nie używał 8 bitów, a 7. Kod EBCDIC firmy IBM był ośmiobitowy, ale kody znaków w EBCDIC były inne, niż w ASCII.

W takich to warunkach powstawał Unix. Do teraz widać to w niektórych starych jego elementach, na przykład w obsłudze portu szeregowego. Wydawałoby się niemożliwe, że koncepcje opracowane 50 lat temu mogą beż problemu pasować do współczesnych rozwiązań. A jednak. Te opracowane dla ubsługi portu szeregowego (znak 7 lub 8 bitowy, różne prędkość transmisji przychodzącej i wychodzącej, konwersja transmitowanycgh znaków itd.) świetnie obsługują porty USB, modemy GSM!

No to teraz jak tem program wysyłający smsy działa.

Pełny tekst źródłowy jest w pliku wskazywanym linkiem na poprzedniej stronie, więc pominiemy tu takie jego elementy, które nie są istotne dla jego zrozumienia.

Na początek otwieramy plik logów, gdzie będą zapisywane informacje o wysłanych smsach, wpisujemy do niego bieżący czas, numer telefonu, na który wysyłamy sms i jego treść. Plik logów otwieramy w trybie "a", wszystko będzie dopisywane na jego końcu, bez niczszenia zastanej zawartości.

fp=fopen("/var/log/sms.log", "a");
time (&czas);
loctime = localtime (&czas);
strftime (data, 50, "%Y-%m-%d %H:%M: ", loctime);
fprintf(fp, "%s %s ", data, argv[1]); fflush(0);
fprintf(fp, "%s ", argv[2]); fflush(0);

No i otwieramy port szeregowy, do którego podpięliśmy modem.
Nie chcemy, żeby w tym samym czasie jakiś inny proces też coś z naszym modemem kombinował, więc na cas kiedy wysyłamy sms zabraniamy innym procesom korzystać z naszego portu szeregowego.

inicjujemy w tym celu strukturę fl:

fl.l_type   = F_WRLCK;  /* F_RDLCK, F_WRLCK, F_UNLCK    */
fl.l_whence = SEEK_SET; /* SEEK_SET, SEEK_CUR, SEEK_END */
fl.l_start  = 0;        /* Offset from l_whence         */
fl.l_len    = 0;        /* length, 0 = to EOF           */
fl.l_pid    = getpid(); /* our PID                      */

Otwieramy nasz port szeregowy ustawiając flagi NOCTTY (nie twórz terminala sterującego), RDWR (na odczyt i zapis) NONBLOCK (jeśli nie czekaj az znak nadejdzie, jeśli nie ma niczego do odczytania, wróć natychmiast z błędem).
Flaga O_NOCTTY ma znaczenie, choć nie we wszystkich systemach Unix, przy otwieraniu urządzeń terminalowych. Lepiej ją ustawić. Pełny opis, o co chodzi znajdziesz w dokumentacji glibc

 
if ((fd2 = open ("/dev/ttyUSB0",
                   O_NOCTTY | O_RDWR | O_NONBLOCK)) != -1)
 {

Zakładamy natychmiast lock na nasz otwarty plik.
Do czasu jego zdjęcia, albo zakończenia naszego procesu nikt inny nie będzie miał do naszego portu dostępu.

  fcntl(fd2,F_SETLKW,&fl); // set lock, czekaj, jesli ktos inny to zrobil

Teraz musimy nasz port szeregowy zaprogramować. I tu kontynuacja rozważań o historii Linuxa z poprzedniej strony. Pu programowanie portu szeregowego to nie tylko ustawienie prędkości, liczby bitów stopu i sposobu sterowania transmisją. Ustawienia naszego portu szeregowego będą na przykład takie:

root@mobile3:/home/rcp# stty -a -F /dev/ttyUSB0
speed 9600 baud; rows 0; columns 0; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>;
swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 5;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread clocal -crtscts
ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon -ixoff
-iuclc -ixany -imaxbel -iutf8
-opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
-isig -icanon -iexten -echo -echoe -echok -echonl -noflsh -xcase -tostop -echoprt
-echoctl -echoke

Driver naszego portu szeregowego to jakby część oprogramowania terminala znakowego podpiętego do niego, potrafi zamieniać znaki sterujące (np. CR i LF), wstrzymywać transmisję, produkować lokalne echo wysyłanych znaków. Nie jest nam to potrzebne, ale trzeba powyłączać to, co może nam przeszkadzać, na przykład zamianę CR na LF.

No to programujemy. Czytamy zastane ostawienia (tcgetattr), ustawiamy prędkość (cfsetspeed), ustawiamy flagi i programujemy port (tcgetattr)

  tcgetattr (fd2, &opcje);
  predkosc = B4800;
  cfsetspeed (&opcje, predkosc);
  opcje.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
  opcje.c_cflag &= ~PARENB;
  opcje.c_cflag &= ~PARODD;
  opcje.c_cflag &= ~CSTOPB;
  opcje.c_cflag &= ~CSIZE;
  opcje.c_cflag |= CS8;
  opcje.c_cflag &= ~CRTSCTS;
  opcje.c_iflag &= ~IXON;
  opcje.c_iflag &= ~IXOFF;
  opcje.c_iflag &= ~INLCR;
  opcje.c_iflag &= ~ICRNL;
  opcje.c_oflag &= ~ONLCR;
  opcje.c_oflag &= ~OCRNL;
  tcsetattr (fd2, TCSANOW, &opcje);

No to port szeregowy załatwiony, możemy wysyłać smsy.

Na początek programujemy modem. Reset do ustawień fabrycznych, włączamy wyświetlanie odpowiedzi modemu, włączamy echo komend (przyda się przy uruchamianiu programu). Po każdej wysłanej komendzie czekamy na odpowiedź modemu (checkok())


       write (fd2, "AT&f\r", 5);
       checkok();
       write (fd2, "ATq0\r", 5);
       checkok();
       write (fd2, "ATe1\r", 5);
       checkok();

Przełączamy modem na wysyłanie smsów w trybie tekstowym. Alternatywą jest wysłanie smsa w trybie PDU, ale musielibyśmy wcześniej z naszego tekstu i numeru telefonu zrobić zakodowany binarny blok. Może kiedyś opiszę mój programik który to robi, tu zostaniemy przy prostszym trybie znakowym.

       write (fd2, "AT+CMGF=1\r", 10);
       checkok();

I wysyłamy sms. Na początek komenda AT+CMGS=numer_telefonu, CR, potem treść i CTRL Z
A więc komenda:


       write (fd2, "AT+CMGS=", 8);

Numer telefonu (koniecznie z prefixem numeru kierunkowego kraju, a więc np. +48223344):

       sprintf(buf, "\"%s\"\r", argv[1]);
       write (fd2, buf , strlen(buf));

Teraz modem czeka na treść smsa, wyświetli znak >, który znajdzie procedura checkprompt()

       checkprompt();

Zanim wyślemy nasz tekst, musimy się upewnić, że występują w nim wyłącznie znaki pisarskie tabeli ASCII. Wysłanie czegoś innego może nam zawiesić modem, tak paskudnie, że tylko wyjęcie z portu USB i włożenie z powrotem pomoże.

Można wysłać tekst zakodowany w UTF, ale ja nie wiem jak, nie znalazłem właściwej dokumentacji, a moje eksperymenty zakończyły się kompletną klęską.

Przekodujemy więc wszystkie polskie znaki z ogonkami na ASCII bez ogonków, jak znajdziemy jakiś inny nielegalny kod, w to miejsce wstawimy spację. Ta konwersja jest strasznie toporna, radzi sobie tylko z polskim alfabetem, ale jest prosta i skuteczna.

Przepiszemy znak po znaku tekst z łańcucha w argv[2] do buf zamieniając krzaczki na ASCII:

       j=0;
       for (i=0; i< strlen(argv[2]); i++)
       {
         znak=argv[2][i];
        if (znak < 0x80)
          {buf[j]=znak; j++;}
        else
         {
          i++;
          znak1=argv[2][i];
          if (znak == 0xc3)
             switch (znak1)
              {
               case (0x93): { znak='O';buf[j]=znak; j++;;
                          break; }// O ogonek
               case (0xb3): { znak='o';buf[j]=znak; j++;;
                          break; }// o ogonek
              }
          if (znak == 0xc4)
             switch (znak1)
              {
               case (0x84): { znak='A';buf[j]=znak; j++;;
                          break; }// A ogonek
               case (0x86): { znak='C';buf[j]=znak; j++;;
                          break; }// C ogonek
               case (0x98): { znak='E';buf[j]=znak; j++;;
                          break; }// E ogonek
               case (0x85): { znak='a';buf[j]=znak; j++;;
                          break; }// a ogonek
               case (0x87): { znak='c';buf[j]=znak; j++;;
                          break; }// c ogonek
               case (0x99): { znak='e';buf[j]=znak; j++;;
                          break; }// e ogonek
              }
          if (znak == 0xc5)
             switch (znak1)
              {
               case (0x81): { znak='L';buf[j]=znak; j++;;
                          break; }// L ogonek
               case (0x83): { znak='N';buf[j]=znak; j++;;
                          break; }// N ogonek
               case (0x9a): { znak='S';buf[j]=znak; j++;;
                          break; }// S ogonek
               case (0xb9): { znak='Z';buf[j]=znak; j++;;
                          break; }// Z kreska
               case (0xbb): { znak='Z';buf[j]=znak; j++;;
                          break; }// Z kropka
               case (0x82): { znak='l';buf[j]=znak; j++;;
                          break; }// l ogonek
               case (0x84): { znak='n';buf[j]=znak; j++;;
                          break; }// n ogonek
               case (0x9b): { znak='s';buf[j]=znak; j++;;
                          break; }// s ogonek
               case (0xba): { znak='z';buf[j]=znak; j++;;
                          break; }// z kreska
               case (0xbc): { znak='z';buf[j]=znak; j++;;
                          break; }// z kropka
              }
          if (znak > 0x7f) {buf[j] = ' '; i--; j++;}  // nieprawidlowy krzaczek 

         }
       }
       buf[j]=0x00;
       

Wysyłamy do modemu nasz przekodowany tekst, po nim znak 1A szesnastkowo (CTRL Z), czekamy na odpowiedź modemu co z tym naszym smsem zrobił, zamykamy otwarty na początku port szeregowy (lock zostanie zdjęty), zamykamy plik logu i kończymy działanie:

       
       write (fd2, buf , strlen(buf));
       write (fd2, "\x1a", 1);
       checkresp();
    close(fd2);
 } //od open fd
fclose(fp);
} //od main

Funkcje checkok() i checkprompt() chyba nie wymagają specjalnego opisu, checkresp() jednak o parę słów komentarza się prosi. Odbiera ona odpowiedź modemu, co stało się z naszym sms-em i zapisuje tę informację w logu.

Po wysłaniu sms modem odpowie czymś takim:

+CMGS: 200

OK

Trzy linie, w pierwszej numer kolejny wysłanego smsa, druga pusta, w trzeciej rezultat. Niestety komunikaty te mogą się różnić nieznacznie w zależności od modelu modemu, jeśli nie udało się wysłać smsa drugiego i trzeciego wiersza nie będzie...

Tak ogólnie, odbieramy trzy wiersze, kończąc timeoutem jak się nie udało i zapisujemy to do pliku z logiem wysłanych smsów:

W logu znajdzie się coś w tym rodzaju:

2018-07-09 16:49:  +48605223344 test | Status:  +CMGS: 202   OK

Nie zamieścimy tu zródeł funkcji checkok(), checkprompt(), ani checkresp(), są w zródłowym programie wskazywanym linkiem na stronie 3.

Niczego ciekawego w nich nie ma.

Noże kiedyś jeszcze opiszę trochę bardziej skomplikowany program do wysyłania smsów, taki serwer sms, numer telefonu i treść sms odbiera przez sieć, trochę na wzór opisanego już serwera kamer.