Bot SMS pod Linuksem

Ostatnio w firmie pojawił się temat stworzenia SMS bota. Zacząłem więc szukać rozwiązań. Jeden z pomysłów, to postawienienie komputera z Linuksem na pokładzie, podłączenie do niego telefonu z ofertą, w której jest miliard darmowych SMS-ów i uruchomienie czegoś magicznego, co zajmie się odbieraniem i wysyłaniem wiadomości. I to magiczne coś właśnie zamierzam opisać.

Rozwiązanie które można zastosować, okazało się prostsze niż myślałem. Można użyć do tego świetnego programu, który jest dostępny zarówno pod Linuksem jak i Windowsem - mowa o Gammu. Jest to program, który komunikuje się z telefonem, lub modemem i służy do różnego rodzaju cznności, takich jak odbieranie/wysyłanie SMS-ów, zarządzanie kontaktami, zadaniami, kalenarzem itp. Ja oczywiście skupię się na odbieraniu i wysyłaniu SMS-ów.

Tak więc do komunikacji użytkownika z botem posłużę się Gammu - to już ustaliliśmy. Żeby było ciekawiej, Gammu posiada demona do wysyłania i odbierania SMS-ów - gammu-smsd. Jako backend do przechowywania danych można użyć zwykłych plików lub bazy danych (obsługiwany jest MySQL, PostgreSQL i SQLite). Pliki odradzam, bo to nieco ogranicza funkcjonalność - przykładowo nie jest możliwe wysyłanie wiadomości przy użyciu polecenia gammu-smsd-inject lub nie można przekazać żadnego id wiadomości do skryptu, który można uruchomić po otrzymaniu wiadomości (o tym w dalszej części). Lepiej zatem skorzystać z bazy danych. Ja wybrałem PostgreSQL.

Konfiguracja Gammu oraz SMSD

Najpierw trzeba zacząć od komunikacji Gammu z telefonem. Mój telefon to Sony Ericsson k800i. Pomijam fakt, że trzeba podłączyć telefon do komputera przez kabel USB ;) Poniżej zamieszczam źródła plików konfiguracyjnych:

~/.gammurc

[gammu] 
port = /dev/ttyACM0 
model = 
connection = at19200 
synchronizetime = no 
logfile = 
logformat = errorsdate 
use_locking = 
gammuloc =

/etc/gammu_smsdrc

[gammu] 
port = /dev/ttyACM0 
model = 
connection = at19200 
synchronizetime = no 
logfile = 
logformat = errorsdate 
use_locking = 
gammuloc = 

[smsd] 
Service = pgsql 
LogFile = syslog 
DebugLevel = 255 
DeliveryReport = log 
RunOnReceive = /home/jaro/bin/sms_trigger.py 
User = uzytkownik_bazy 
Password = haslo_bazy 
PC = localhost 
Database = gammu

Poprawność konfiguracji połączenia z telefonem można sprawdzić przez polecenie gammu identify. U mnie wyjście wyglądało tak:

# gammu identify 
Manufacturer : Sony Ericsson 
Model : K800i (AAD-3022031-BV) 
Firmware : R8BF003 080130 2133 CXC1250212_ORANGE_WI 
IMEI : WOLE_NIE_POKAZYWAĆ 
Kod produktu : AAD-3022031-BV 
SIM IMSI : WOLE_NIE_POKAZYWAĆ

Przed uruchomieniem demona smsd, trzeba utworzyć odpowiednie tabele w bazie danych. W dokumentacji znajduje się przykładowy plik ze skryptem SQL, który tworzy te tabele. U mnie znajduje się on w lokalizacji: /usr/share/doc/gammu/examples/sql/pgsql.sql Wystarczy uruchomić skrypt i voila!

Skrypt (bot)

W pliku gammu_smsdrc jest opcja RunOnReceive, która, jak się można domyślić, wskazuje skrypt uruchamiany po odebraniu wiadomości. Skrypt jest uruchamiany z parametrem, który zawiera id wiersza w bazie danych w tabeli inbox. Wiersz zawiera dane przesłanej wiadomości. Napisałem w Pythonie przykładowy skrypt, który odczytuje odebraną wiadomość, sprawdza jaki tekst wysłano i na jego podstawie wysyła odpowiedź.

#!/usr/bin/python

import psycopg2 as db, sys, os

db_user = 'nazwa_uzytkownika'
db_password = 'haslo_do_bazy'
db_host = 'localhost'
db_name = 'gammu'


def main(argv=None):
        try:
                sms_id = sys.argv[1]
        except IndexError:
                print 'Za malo parametrow'
                sys.exit()
        
        try:
                conn = db.connect("dbname=%s user=%s password=%s host=%s" % (db_name, db_user, db_password, db_host))
        except:
                print 'Blad podczas polaczenia z baza'
                
        if not sms_id.isdigit():
                print 'Niepoprawny parametr'
                sys.exit()
        
        cur = conn.cursor()
        cur.execute("SELECT * FROM inbox WHERE id=%s" % sms_id)
        
        row = cur.fetchone()
        
        if not row:
                print 'Nie znaleziono wiadomosci sms o podanym id'
                sys.exit()
                
                
        rcp_num = row[3]
        command = row[8].strip().lower()
        
        if command == 'jezyk':
                response = "Python"
        elif command == 'system':
                response = "Linux"
        else:
                response = "Nieznana komenda"
        
        
        os.system("gammu-smsd-inject TEXT %s -text '%s'" % (rcp_num, response)) 
        


if __name__ == '__main__':
        main()
        

Finał - uruchomienie demona

Wreszcie przechodzimy do puenty - uruchomienia demona. Wystarczy krótkie:

gammu-smsd --daemon

I w ten oto sposób, telefon jest gotowy do odbierania i wysyłania wiadomości.

Debugowanie Django w Winpdb

Django posiada wbudowaną stronę służącą do zgłaszania błędów, dzięki której można zidentyfikować źródło błędu. Pojawia się zawsze gdy wystąpi nieobsłużony wyjątek, pod warunkiem, że w ustawieniach opcja DEBUG przyjmuje wartość True. Każdy developer Django pewnie widział ją nie raz. Jednak błędy oprogramowania, to nie tylko wyjątki, ale także niepoprawnie wyświetlane dane - tego Django już nie wyłapie. Czasami też zdaża się, że informacja ze strony błędu, to za mało, żeby zidentyfikować błąd. Można wtedy posiłkować się funkcją print, której zwróconą wartość widać w konsoli, na której został uruchomiony serwer developerski. Jest to jednak przydatne przy naprawie drobnych błędów, gdzie szybko można zorientować się co może być ich przyczyną. Korzystanie z print na pewno nie jest debugowaniem z prawdziwego zdarzenia. Żeby dokładnie prześledzić działanie stworzonego skryptu, najlepiej posłużyć się narzędziem przeznaczonym do tego. Takim narzędziem jest Winpdb - graficzna wersja pdb (Python Debugger).

Debugowanie aplikacji stworzonych w Django jest prostsze, niż mogłoby się to wydawać. Do prezentacji posłużyłem się bardzo prostym, testowym projektem:

test_project/
|-- __init__.py
|-- manage.py
|-- settings.py
|-- test_app
|   |-- __init__.py
|   |-- models.py
|   `-- views.py
`-- urls.py

Kod który chcę prześledzić w poszukiwaniu błędów znajduje się w pliku test_app/views.py

def test_view(request):
        a = 1
        b = 3
        c = a + b

        print c

Pierwszym krokiem do prześledzenia skryptu, jest uruchomienie developerskiego serwera Django z poziomu Winpdb. Najpierw wybieramy "File -> Launch" W oknie które się pojawi,trzeba wpisać polecenie, które uruchomi serwer developerski.

Launch command

W moim przypadku było to:

/home/jaro/strony/test_project/manage.py runserver --noreload

Opcja noreload umożliwia debugowanie i zatrzymywanie się wykonywania skryptu w breakpointach. Skrypt zostaje załadowany i żeby go uruchomić trzeba kliknąć na ikonkę play. Spowoduje to uruchomienie serwera developerskiego Django. W konsoli wyświetli się komunikat serwera.

Konsola Winpdb

Do zatrzymania wykonywania skryptu w wybranym miejscu, można posłużyć się breakpointem. W pierwszej kolejności trzeba otworzyć plik, w którym breakpoint będzie ustawiony (File -> Open Source). Plik załaduje się do okna "Source".

Breakpoint

Breakpoint ustawia się poprzez kliknięcie na linię kodu, która z kolei zostanie wyróżniona czerwonym kolorem, tak jak jest to widoczne na powyższym rysunku.

Już wszystko jest gotowe do debugowania, teraz wystarczy wejść w przeglądarce na stronę, która uruchomi interesujący nas widok (np. http://localhost:8000/). Strona nie załaduje się, a jak przejdziemy do debuggera, to zobaczymy skrypt zatrzymany we wskazanej wcześniej linii.

Debugowanie skryptu

W widocznych oknach można zobaczyć aktualne wartości zmiennych, uruchomione wątki, stos, konsolę oraz kod źródłowy, którego kolejne linie można wykonywać interaktywnie, korzystając z przycisków w pasku narzędzi.

Internet przez komórkę pod Linuxem

Zazwyczaj jak korzystam z Internetu na laptopie, to albo w domu, albo gdzieś gdzie jest sieć bezprzewodowa. Czasami jednak pojawiają się sytuacje, w których potrzebuję dostępu do Internetu w miejscach, w których nie łapię żadnej sieci bezprzewodowej i nie ma gdzie się wpiąć kablem. Z pomocą przychodzi GPRS.

Co jest potrzebne żeby uruchomić GPRS pod Linuxem:

  1. telefon, który obsługuje tę technologię komunikacyjną
  2. włączoną opcję połączeń z Internetem przez kabel USB (w SE k800i ustawienia->łączność->USB->Włącz)
  3. kabel do telefonu lub blutetooth (przez IrDA chyba też się da)
  4. skompilowany i załadowany moduł jądra do obsługi modemów przez USB (USB_ACM)
  5. program global 3g

Modem w jajku

Po podłączeniu telefonu do komputera przez kabel, w katalogu /dev, powinny znajdować się pliki ACM*. Jeżeli są, to znaczy że moduł USB_ACM jest załadowany. W przeciwnym wypadku, najpierw można spróbować załadować go ręcznie (być może z jakiegoś powodu nie powiodło się automatyczne ładowanie):

# modprobe cdc_acm

Jeżeli to nie pomogło, to trzeba skompilować moduł USB_ACM. Nie będę opisywał szczegółowo jak to się robi, bo to temat na osobny wpis. W Internecie jest dużo informacji o kompilacji jądra. Podpowiem tylko gdzie i jakiej opcji w menu konfiguracyjnym trzeba szukać:

Location:
    -> Device Drivers
      -> USB Support
        -> Support for Host-side USB 
          -> USB Modem (CDC ACM) support

Po skompilowaniu modułu, powinien on automatycznie ładować się gdy telefon zostanie podłączony do komputera.

Połączenie ze światem

Czas na punkt kulminacyjny. Do połączenia użyłem świetnego programu Global 3G Jego wielkimi zaletami są wbudowane ustawienia do najpopularniejszych operatorów sieci komórkowych (w tym także polskich) oraz sterowniki do wielu telefonów. W konfiguracji programu wybrałem swojego operatora z listy dostawców, natomiast z listy urządzeń odpowiedni telefon. Posiadam Sony Ericsson k800i, który widnieje na liście obsługiwanych urządzeń, ale jak go wybrałem, to nie łączył się z Internetem. Dopiero jak wybrałem opcję "Sony Ericsson - Standard Models (2)", to zadziałało. W przypadku połączenia telefonu przez kabel, jako port trzeba wybrać USB -> ACM0 lub ACM1 jeżeli nie zadziała. Piszę tylko o kablu, bo nie próbowałem łączyć się przez blutetooth.

To by było na tyle jeśli chodzi o konfigurację. Wystarczy kliknąć "połącz" i wrota Internetu otworzą się przed nami. Wolnego, bo wolnego, ale w sytuacji awaryjnej wystarczy. Polecam wyłączyć w przeglądarce pobieranie obrazków, animacji, javascriptu i apletów javy. Niestety jest jeden szkopuł. Global 3G w darmowej wersji przed nawiązaniem połączenia z Internetem, zmusza nas do odczekania 6 minut (tylko za pierwszym razem w ciągu pojedynczego uruchomienia programu) i nie pamięta konfiguracji. Jeżeli chcemy pozbyć się tej uciążliwości, to trzeba zakupić od autora płatną wersję za 29 zł. Wydaje mi się, ze nie są to duże pieniądze i czuję, że fajnie jest zapłacić komuś drobną kwotę za dobrze wykonaną robotę.

Wysyłanie maila z konsoli - skrypt Pythona

Jakiś czas temu postawiłem subversion na serwerze developerskim, który służył do testowania aplikacji którą piszę. Chciałem żeby informacje o każdym commit-cie były wysyłane na maila. Postanowiłem zrobić to przy pomocy skryptu w Pythonie.

import smtplib
import getopt
import email.Message
import sys

def main(argv):
    opts, args = getopt.getopt(argv, "u:p:s:h:t:r:t:", ["server=", "port=", "sender=", password=", "to=", "subject=", "text="])
    for opt, arg in opts:
        if opt in ["-u", "--server"]:
            serverURL = arg
        elif opt in ["-p", "--port"]:
            port = arg
        elif opt in ["-s", "--sender"]:
            sender = arg
        elif opt in ["-h", "--password"]:
            password = arg
        elif opt in ["-t", "--to"]:
            to = arg
        elif opt in ["-r", "--subject"]:
            subject = arg
        elif opt in ["-t", "--text"]:
            text = arg

    message = email.Message.Message()
    message["To"]      = to
    message["From"]    = sender
    message["Subject"] = subject
    message.set_payload(text)
    if port:
        mailServer = smtplib.SMTP(serverURL, port)
    else:
        mailServer = smtplib.SMTP(serverURL)
    mailServer.ehlo()
    mailServer.starttls()
    mailServer.ehlo()
    mailServer.login(sender, password)
    
    mailServer.sendmail(sender, to, message.as_string())
    mailServer.quit()

Wiadomość ma wysyłać się z serwera SMTP na gmailu, więc istotna jest tu część kodu:

mailServer.ehlo()
mailServer.starttls()
mailServer.ehlo()

Żeby wysłać maila, wystarczy wpisać polecenie: python /srv/mail.py --server=smtp.gmail.com --port=587 --sender=<adres_nadawcy> --password=<hasło_nadawcy> --to=<adres_odbiorcy> --subject="temat" --text="tresc wiadomosci"

Zenity - bash i okna GTK

Jakiś czas temu przy aktualizacji paru pakietów, zwróciłem, nie wiem czemu, uwagę na jeden z nich. Jego nazwa nic mi nie mówiła, przeczytałem więc krótki opis, który zachęcił mnie do "googlowania". Mowa o Zenity, umożliwiającym uruchamianie zdefiniowanych okienek GTK o określonej funkcjonalności, z przeznaczeniem do wykorzystania w skryptach bash-a. Nie będę opisywał dostępnych okienek, bo to można wyczytać z dokumentacji. Skupię się zatem na konkretnym przykładzie wykorzystania Zenity.

Na szybko napisałem skrypt, który zastąpił moją metodę pomniejszania obrazków z wykorzystaniem z linii poleceń, opisaną w jednym z wcześniejszych wpisów.

#!/bin/bash

files=(`zenity --file-selection --title "Wybierz pliki do zmniejszenia" --multiple --separator " "`)

if [ $files == "" ]
then
        exit
fi

fnum=${#files[@]}
percent=$[100/$fnum]
current=$percent

scale=(`zenity --scale --title "Rozmiar miniatury" --text "Wybierz maksymalny rozmiar wysokości lub szerokości - obraz będzie zmniejszony proporcjonalnie" --max-value 1024`)

if [ $scale == 0 ]
then
        exit
fi

(for file in ${files[*]};
do
        type=`file -b -i $file`
        if [ ${type:0:5} == 'image' ]
        then            
                convert $file -resize "$scale"x"$scale" $file
        fi
        
        current=$[$current+$percent]
        echo $current
        
done) | zenity --progress --title "Zmniejszanie obrazków" --text "Postęp" --auto-close

Jest bardzo prosty, ale mi w zupełności wystarcza. Wykorzystuje polecenie "convert" z pakietu ImageMagick.

Debian & Broadcom Wireless & ndiswrapper

Ostatnio zakupiłem sobie router TP-Link TL-WR542G. Wcześniej używałem starego komputera z Pentium III 500 MHz, 96 RAM, z Gentoo w środku. Fajnie jest mieć router, którym można sobie zarządzać z konsoli, ale ma kilka wad - głośno chodzi, brak wi-fi, większy pobór prądu, zajmuje więcej miejsca. Ale starczy narzekania, do rzeczy. Pierwsze uruchomienie, szybka konfiguracja i coś tu nie gra. Internet strasznie zamula przez wi-fi. Wyniki testu łącza strasznie słabe - 300 kb przy pobieraniu danych, podczas gdy przy połączeniu przez kabel wychodziło około 2Mb. Jak nie trudno domyślić się, przyczyna tkwiła w sterowniku (BCM43xx). Z pomocą przychodzi ndiswrapper, który umożliwia instalację sterowników kart sieciowych, napisanych dla Windowsa. Sterowniki oraz listę obsługiwanych kart można znaleźć na stronie projektu. Przy wyborze sterownika należy zwrócić uwagę na pciid karty, które można odczytać przy pomocy polecenia lspci -nn. W moim przypadku jest to 14e4:4320
$ lspci -nn | grep Broadcom
02:04.0 Network controller [0280]: Broadcom Corporation BCM4306 802.11b/g Wireless LAN Controller [14e4:4320] (rev 03)
więc dla mojej karty odpowiedni jest sterownik bcmwl5. Czas na istalację: # aptitude install ndiswrapper-common
# ndiswrapper -i bcmwl5.inf
Jeszcze tylko załadowanie modułu: # modprobe ndiswrapper
FATAL: Module ndiswrapper not found
Jak widać, moduł który chcę załadować nie istnieje. Trzeba więc taki moduł stworzyć. Potrzebne do tego będą pakiety: build-essential, module-assistant, ndiswrapper-source oraz linux-headers-2.[wersja_jądra], w przypadku korzystania z jądra z repozytorium. # aptitude build-essential module-assistant ndiswrapper-source
# m-a a-i ndiswrapper
# modprobe ndiswrapper
Teraz już można w pełni cieszyć się bezprzewodowym dostępem do internetu.
Żeby moduł uruchamiał się przy starcie systemu, wystarczy dopisać go do listy /etc/modules: # echo "ndiswrapper" >> /etc/modules i dodać moduł bcm43xx do czarnej listy, żeby nie ładował się przy starcie systemu i nie kolidował z ndiswrapperem: # echo "blacklist bcm43xx" >> /etc/modprobe.d/blacklist

Tworzenie miniaturek z linii poleceń

Już wspominałem o tym w moim starym blogu, ale skoro ten blog ma zawierać głównie wpisy o tematyce technicznej, postanowiłem go umieścić również tutaj.

Czasami trzeba szybko zmniejszyć dużą ilość zdjęć, żeby np. umieścić je w galerii internetowej. Pod Linuxem jest na to prosty sposób. Wszystko czego potrzebujemy to pakiet ImageMagick.
Zakładam że będą pomniejszane wszystkie zdjęcia w formacie jpg z bieżącego katalogu.

W linii poleceń wystarczy wpisać: find . -maxdepth 1 -name '*.jpg' -exec convert {} -resize 800x800 {} ';' No i co to robi? Polecenie find wyszukuje w bieżącym katalogu wszystkie pliki o rozszerzeniu .jpg i wykonuje polecenie convert (z pakietu ImageMagick). Nawiasy "{}" oznaczają nazwę znalezionego pliku. Powyższe polecenie nadpisze oryginalne zdjęcia zmniejszonymi. Jeżeli chcemy uniknąć nadpisania, to można to zrobić np. tak find . -maxdepth 1 -name '*.jpg' -exec convert {} -resize 800x800 'min/{}' ';' Spowoduje to zapisanie miniaturek do katalogu min, który jest podkatalogiem bieżącego katalogu. Oczywiście "min" trzeba wcześniej utworzyć.
Jeżeli chcemy uniknąć nadpisania oryginalnych plików i zapisać miniaturki w bieżącym katalogu, można dodać prefiks przed nazwą miniatury: find * -maxdepth 1 -name '*.jpg' -exec convert {} -resize 800x800 "min"{} ';' Czemu akurat 800x800? Polecenie convert pomniejsza zdjęcia proporcjonalnie, więc jeżeli szerokość jest większa od wysokości to zmniejszy do rozdzielczości 800x..., a jeżeli jest odwrotnie to do rozdzielczości ...x800. Jeżeli nie chcemy zmniejszać wszystkich zdjęć do tego samego rozmiaru, tylko proporcjonalnie, to można użyć procentów, np. zamiast 800x800, 50%x50%. Opcja maxdepth określa ile poziomów podkatalogów ma być przeszukiwanych. Dla wartości 1 przeszukiwany jest tylko bieżący katalog.

W zasadzie powyższe przykłady prezentują cząstkę możliwości zarówno polecenia convert jak i find. Używając tego drugiego w analogiczny sposób można robić wiele innych rzeczy, np. przekonwertować wiele plików wav na mp3 przy użyciu lame.
ImageMagick jest wyposażony w narzędzie mogrify, które może pomniejszyć wiele zdjęć wg podanego filtra, więc te nieco długie powyższe przykłady, można zapisać krócej: mogrify -resize 800x800 *.jpg Mogrify nadpisuje oryginały, więc jeżeli nie chcemy ich utracić, trzeba pracować na kopiach.