SMS server v Pythonu
10.08.2011
SMS server je software, který umožňuje v rámci lokální sítě odesílat SMS zprávy pomocí HW GSM modemu. Hlavním účelem jeho vzniku byla moje potřeba se trošku zorientovat v Pythonu a nejlépe to jde na nečem, co má smysl a funkční potenciál. Nejedná se o žádné veledílo, ale spiše o softwarový bastl, který ale funguje a plná svůj účel k maximální spokojenosti.
Intro
Pokud máte ve správě nějaké Windows servery a na nich naplánované úlohy, tak se vám zcela jistě už někdy stalo, že nějaká úloha selhala - tedy vyskytla se taková chyba, jejíž neřešení by mohlo vést závažným problémům (poškozená data, nefunkční aplikace, neprovedá záloha apod.). V takovém případě je jistě namístě, aby byla správci odeslána varovná SMSka.
Jenže jak na to? Vynechám-li použití webové SMS brány pro závislost na připojení do Internetu, je jediným řešením HW GSM modem. Pokud máte ve správě pouze jeden fyzický server, je situace poměrně snadná - stačí modem připojit k sériovému portu a pomocí několika AT příkazů SMS odeslat. V případě, že vaše "portfolio" čítá desitky virtuálních mašin a to v různých lokalitách, je jediným schůdným řešením vytvořit nějakou síťovou službu, která bude schopna SMS zprávy centralizovaně odesílat - a právě to řeší tento SMS server.
Jak to funguje?
Pro síťové a administrační věci preferuji Linux - je to tak trochu srdcovka :-) Čili základem je Debian a k němu na sériový port připojený GSM modem. V mém případě se jedná o fyzický stroj, neboť je mimo SMS serveru, využíván k několika dalším síťovým službám. Pokud by však měl řešit pouze SMS server, tak by byl realizován jako virtuální mašina s tím, že by k němu byl namapován sériový port fyzického host serveru. Každopádně, pokud si lze s modemem povídat pomocí AT příkazů je vše v naprostém pořádku.
Na uvedeném linuxovém stroji je spuštěn TCP server napsaný v Pythonu. Jedná se o primitivní implementaci práce se sockety, ale vzhledem k tomu, že síťová komunikace probíhá pouze v rámci lokální sítě a spojení je povoleno jen pro definovanou množinu klientů (firewall), tak toto řešení postačuje. Navíc četnost použití není příliš vysoké - SMS zpráva obvykle znamená problém a práci :-) TCP server poslouchá na porti 2021 a komunikuje v plaintextu - tedy nešifrovaně v lidsky čitelném textu.
Běžící server čeka na připojení od klientů. Pokud se tak stane, tak očekává, že mu klient předá požadavek na odeslání SMS - tedy svým způsobem formátovaný řetězec, který obsahuje telefonní číslo a zprávu. Přijatý požadavek server uloží do databáze - konkrétně MySQL. Jiný proces pak požadavek z databáze vyjme a pomocí připojeného GSM modemu zprávu odešle. Tento asynchronní způsob je zvolen proto, že požadavků na odeslání zpráv může TCP serveru přijít více, přičemž rychlost odesílání zpráv modemem je malá. Databáze je tedy něco jako buffer. V databázi pak zůstává i informace, že zpráva byla odeslána a kdy.
Komentář
Pro přenos požadavku od klienta k SMS serveru lze samozřejmě použít mnohem robustnější metody - xinetd, snmp trapy, http požadavky, zabezpečení SSL, ale jak jsem psal v úvodu, šlo o to se trochu naučit základům Pythonu. SMS server je tedy něco jako "by the way" :-)
Dokumentace
Celý SMS server je realizován na serveru takto:
- GSM Modul
- Ovládací skripty
- Webové rozhraní - není však nutné
- Klienti
GSM Modul
Jedná se o GSM modul Cinterion TC35i (původní výrobce Siemens), připojený k serveru na fyzický sériový port /dev/ttyS0 (COM1). Alternativně lze použít i jiný modul, který lze ovládat pomocí AT příkazů, avšak tento je ověřen a určen i pro průmyslové použití.
V modulu je vložena tarifní SIM karta.
Ovládací skripty
Jejich činnost je založena na vzájemné provázanosti následujících elementů:
- Tabulky v MySQL
- TCP server - slouží k příjmu požadavků na odeslání SMS zprávy od síťových klientů (Windows nebo Linux).
- Check_and_send skript - slouží ke realizaci příjmu a odeslání zpráv ve spolupráci s GSM modulem.
Tabulky v MySQL
Databáze: sms
- mgs - tabulka pro ukládání požadavků od TCP serveru
- rcv - tabulka přijatých SMS zpráv
- sms - tabulka odeslaných zpráv
Zobrazit/skrýt create table SQL
CREATE TABLE IF NOT EXISTS `msg` (
`idm` int(11) NOT NULL AUTO_INCREMENT,
`msg` text COLLATE utf8_czech_ci NOT NULL,
`ulozeno` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`idm`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci ;
CREATE TABLE IF NOT EXISTS `rcv` (
`idrcv` int(11) NOT NULL AUTO_INCREMENT,
`tel` varchar(20) COLLATE utf8_czech_ci NOT NULL,
`text` varchar(160) COLLATE utf8_czech_ci NOT NULL,
`prijato` datetime NOT NULL,
PRIMARY KEY (`idrcv`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci ;
CREATE TABLE IF NOT EXISTS `sms` (
`idsms` int(11) NOT NULL AUTO_INCREMENT,
`tel` varchar(15) COLLATE utf8_czech_ci NOT NULL,
`text` varchar(160) COLLATE utf8_czech_ci NOT NULL,
`zpracovano` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`idsms`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;
TCP Server
Principem činnosti je otevření portu 2021 pro konzolovou TCP komunikaci (telnet). Na tomto portu očekává připojení od klientů a předání požadavků na odeslání SMS zpráv. Pokud klient zašle požadavek ve správném formátu, server uloží jeho obsah do tabulky msg a následně zavolá skript check_and_send. Tímto dojde k okamžitému odeslání zprávy. Klient s TCP serverem komunikuje plain-textem.
- Umístění: /usr/local/smss/smss
- Spouštění: /usr/local/smss/start_stop
- PID file: /var/run/smss.pid
- Logy: /var/log/smss/smss.log
Zobrazit/skrýt soubor /usr/local/smss/smss
#!/usr/bin/env python
# -*- coding: utf-8 -*-
####################################################################################################
# PROCESY: SMS SERVER #
# Server na prijem zadosti o odeslani SMS zprav #
# Jan Matuska, walda@starhill.org #
####################################################################################################
logfile = "/var/log/smss/smss.log"
pidfile = "/var/run/smss.pid"
sender_script = "nohup /usr/local/smss/check_and_send&"
import sys, socket, select, time, os
import MySQLdb
import logger, daemon
# Seznam socketu prichozich spojeni
open_sockets = []
# Slovnik bufferu na prichozi zpravy
messages = dict()
# Instance tridy na logovani
log = logger.logger(logfile)
# Instance tridy daemon a nastaveni parametru
proc = daemon.daemon(pidfile)
proc.logcl = log
# Funkce na ulozeni zpravy do DB -------------------------------------------------------------------
def todb(zprava):
# pripojeni k MySQL
try:
db=MySQLdb.connect(host="localhost",user="uzivatel",passwd="heslo",db="smss");
except Exception:
log.log("Cannot to connnect database")
return
# provedeni vlozenoi
try:
cursor = db.cursor()
cursor.execute("INSERT INTO msg(msg) VALUES('%s')" % zprava);
cursor.close()
except Exception:
log.log("Error in SQL query")
db.close()
# --------------------------------------------------------------------------------------------------
# Funkce na zpracovani bufferu ---------------------------------------------------------------------
def ceb(msgid, socket):
if messages[msgid].find('\n') == -1:
return False
pocet=0
msg = messages[msgid][0:messages[msgid].rfind('\n')]
for value in msg.split('\n'):
if value.find('#')==0:
socket.send("Command(s) stored\n\r");
for prikaz in value.split('#'):
if len(prikaz)>0:
log.log("Buffer "+str(msgid)+" stored ["+prikaz+"]")
todb(prikaz)
pocet=pocet+1
else:
socket.send("Syntax error\n\r");
if pocet>0:
os.system(sender_script)
return True
# --------------------------------------------------------------------------------------------------
# Pokus o spusteni serveru
log.log("Trying to start server")
# Test, zda-li process jiz bezi nebo ne
if proc.IsProcessRunning():
sys.exit()
# Deamonizujeme proces
proc.DaemonizeProcess()
# Regulerne spustime (pidfile)
proc.RunProcess()
# Naslouchajici socket
listening_socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
# Binding
binded=False
while binded==False:
try:
listening_socket.bind( ("", 2021) )
binded = True
except Exception:
log.log("Unable to bind. Waiting 30 seconds.")
time.sleep(30)
# Pocet cekajicich spojeni v TCP stacku na obsluhu
listening_socket.listen(20)
# Serverova smycka
log.log("Server running. PID %s" % os.getpid())
while True:
# Cekame na prichozi paket nebo udalost
try:
rlist, wlist, xlist = select.select( [listening_socket] + open_sockets, [], [] )
except Exception:
#log.log("Stopping server")
listening_socket.close()
proc.ExitProcess()
for i in rlist:
if i is listening_socket:
try:
new_socket, addr = listening_socket.accept()
open_sockets.append(new_socket)
ip, port = addr;
msgid = open_sockets.index(new_socket);
messages[msgid]="";
new_socket.send("Vema SMS Server v.1.0\r\n")
log.log("New connection from "+ip+":"+str(port)+", created buffer "+str(msgid))
except Exception:
try:
msgid = open_sockets.index(new_socket)
del messages[msgid]
except:
None
open_sockets.remove(new_socket);
log.log("Broken connection from "+ip+":"+str(port)+", buffer "+str(msgid)+" created and removed")
new_socket.close()
else:
try:
data, adresa = i.recvfrom(1024)
addr = i.getpeername();
ip, port = addr;
msgid = open_sockets.index(i);
if data == "":
del messages[msgid];
open_sockets.remove(i)
log.log("Connection from "+ip+":"+str(port)+" terminated by host, buffer "+str(msgid)+" removed")
else:
data = data.replace('\r','\n').replace('\n\n','\n');
messages[msgid] = messages[msgid] + data;
log.log("Data received from "+ip+" added to buffer "+str(msgid)+": "+repr(data))
if ceb(msgid, i) is True:
del messages[msgid];
open_sockets.remove(i)
#i.send("\n\rHello goodbye\n\r");
i.close()
log.log("Connection from "+ip+":"+str(port)+" terminated by server, buffer "+str(msgid)+" removed")
except Exception:
try:
del messages[open_sockets.index(i)]
except:
None
open_sockets.remove(i);
i.close()
log.log("Connection from "+ip+":"+str(port)+" terminated by host, buffer "+str(msgid)+" removed")
Skript /usr/local/smss/smsspotřebuje ke svému běhu třídu daemon. Obsahuje metody na daemononizaci procesu, tedy to, že se tcp server spustí na pozadí a zůstane běžet.
Zobrazit/skrýt třídu /usr/local/smss/daemon.py
#!/usr/bin/env python
########################################################################################################################
# D A E M O N C L A S S #
# Tato trida je urcena pro daemonizaci a spravu procesu pomoci PID souboru (lze vyuzit nezavisle) #
########################################################################################################################
import os, sys
from signal import *
class daemon:
# aktualni cislo procesu
pid = None
# soubor s pid beziciho procesu
pidfile = "/tmp/none"
# instance na tridu s logovanim
logcl = None
# ------------------------------------------------------------------------------------------------------------------
# Konstruktor
# Vstup: pidfile
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, pidfile):
self.pid = os.getpid()
self.pidfile = pidfile
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Logovani na terminal a do tridy pro logovani
# Vstup: retezec logu
# ------------------------------------------------------------------------------------------------------------------
def __log(self, text):
print text
try:
self.logcl.log(text)
except Exception:
None
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Test, zda-li proces bezi nebo ne
# Vystup: true/false podle toho, zda-li proces bezi nebo ne
# sys.exit() pokud se vyskytne problem s pidfile (prava)
# ------------------------------------------------------------------------------------------------------------------
def IsProcessRunning(self):
if os.path.exists(self.pidfile):
try:
file = open(self.pidfile, "r")
lines = file.readlines()
file.close()
if os.path.exists("/proc/%s" % str(lines[0])):
self.__log("Process is running, try kill him or delete pidfile. PID=%s" % lines[0])
return True
else:
os.remove(self.pidfile)
except Exception:
self.__log("Cannot open existing pidfile. Check permissions.")
sys.exit()
return False
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Zalozeni PID file s aktualnim pid a nastaveni reakce na TERM signal
# Vystup: sys.exit() pokud se vyskytne problem s pidfile (prava)
# ------------------------------------------------------------------------------------------------------------------
def RunProcess(self):
try:
file = open(self.pidfile,'w')
file.write(str(self.pid))
file.close()
except Exception:
self.__log("Cannot create pidfile. Check permissions.")
sys.exit()
signal(SIGTERM, self.__eventterm)
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Obsluha po prichodu TERM signalu
# Vystup: sys.exit() - koreknti ukonceni
# ------------------------------------------------------------------------------------------------------------------
def __eventterm(self, signum, frame):
self.__log("Stopping server SIGTERM")
self.Exit()
sys.exit()
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Korektni ukonceni
# Vystup: sys.exit()
# ------------------------------------------------------------------------------------------------------------------
def ExitProcess(self):
try:
os.remove(self.pidfile)
except Exception:
self.__log("Cannot delete pidfile. Check permissions.")
sys.exit()
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Daemonizace procesu
# Vystup: nic
# ------------------------------------------------------------------------------------------------------------------
def DaemonizeProcess(self):
# Udelame fork, tedy klon procesu - rodic (pid=cislo potomka) a prvni potomek (pid=0)
try:
pid = os.fork()
except OSError, e:
raise Exception, "%s [%d]" % (e.strerror, e.errno)
if (pid == 0): # Pokud je proces prvni potomek
os.setsid() # Odpojime od terminalu
try:
pid = os.fork() # Vytvorime druheho potomka z prvniho potomka
except OSError, e:
raise Exception, "%s [%d]" % (e.strerror, e.errno)
if (pid == 0): # Pokud je proces druhy potomek
os.chdir("/") # Nastaveni pracovniho adresare
os.umask(0) # Nastaveni masky
else:
os._exit(0) # Ukonceni rodice druheho potomka (=prvniho potomka)
else:
os._exit(0) # Ukonceni rodice prvniho potomka
# Nyni by mel zustat bezet druhy potomek procesu a to je to, co chceme
# Preventivni uzavreni vsech filedektriptoru - preventivne
import resource
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
if (maxfd == resource.RLIM_INFINITY):
maxfd = 1024
# Iterate through and close all file descriptors.
for fd in range(0, maxfd):
try:
os.close(fd)
except OSError: # ERROR, fd wasn't open to begin with (ignored)
pass
# Presmerovani vstupu a vystupu
if (hasattr(os, "devnull")):
REDIRECT_TO = os.devnull
else:
REDIRECT_TO = "/dev/null"
os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
os.dup2(0, 1) # standard output (1)
os.dup2(0, 2) # standard error (2)
# Hotovo"
self.pid = os.getpid()
# ------------------------------------------------------------------------------------------------------------------
Skript /usr/local/smss/smss potřebuje ke svému běhu třídu logger. Obsahuje jednoduch0 metody logování do souboru.
Zobrazit/skrýt třídu /usr/local/smss/logger.py
#!/usr/bin/env python
########################################################################################################################
# L O G G E R C L A S S #
# Tato trida je urcena pro realizaci logovani na terminal a do souboru #
########################################################################################################################
from time import gmtime, strftime, localtime
class logger:
# logovy soubor
logfile = "/tmp/nonelog"
# logovani na terminal
log_to_console = False
# ------------------------------------------------------------------------------------------------------------------
# Konstruktor
# Vstup: logfile
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, logfile, truncatefile=False):
self.logfile = logfile
if truncatefile==True:
FW = open(self.logfile,'w').close()
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Metoda na logovani
# Vstup: text logu
# Vystup: nic
# ------------------------------------------------------------------------------------------------------------------
def log(self, text):
if self.log_to_console:
print strftime("%Y.%m.%d %H:%M:%S ", localtime()) + text
try:
FW = open(self.logfile,'a')
FW.write(strftime("%Y.%m.%d %H:%M:%S ", localtime()) + str(text) + '\n');
FW.close()
except:
print "Cannot open logfile"
# ------------------------------------------------------------------------------------------------------------------
TCP se dá spustit nebo restartovat následujícím způsobem.
Zobrazit/skrýt soubor /usr/local/smss/stop_start
#!/bin/bash
# Zastavi bezici proces serveru, resp. posle SIGTERM
kill `cat /var/run/smss.pid`
# Spusti server
/usr/local/smss/smss
Pokud Python zakřičí, že nemá nějaký modul, stačí ho doinstalovat z debianích repozitářů.
PID file je implementován pro snadnou identifikaci procesu, neboť TCP server je po spuštění daemonizován. Obsahem PID file je číslo procesu. V případě problémů se spuštěním je třeba PID soubor smazat.
Formát požadavku na odeslání SMS zprávy: #phone1;text1#phone2;tex2...#phonen;textn
- Telefonní číslo musí být v mezinárodním tvaru, tedy např. 420608337726
- Zpráva musí být bez diakritiky
Př. #420608337726;AHOJ#420777123456;Kolotoc
Skript check_and_send
Skript přečte obsah tabulky msg a v případě, že nalezne přijaté požadavky na odeslání SMS zpráv, tyto zprávy odešle zapíše do tabulky sms jako odeslané. Poté otestuje existenci nějakých přijatých SMS zpráv a pokud takové jsou, zapíše je do tabulky rcv a z GSM modulu odstraní.
- Umístění: /usr/local/smss/check_and_send
- PID file: /var/run/check_and_send.pid
- Logy: /var/log/smss/check_and_send.log
Skript je spouštěn periodicky cronem (každou minutu) a případně TCP serverem. PID file je zde implementován jako ochrana proti vícenásobnému spuštění (TCP server a následně cron).
Zobrazit/skrýt soubor /usr/local/smss/check_and_send
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##########################################################################################################
# SMS SERVER: CHECK & SEND #
# Skript detekuje nove prichozi zpravu a odesle ji. Zaroven smaze pripadne prichozi zpravy #
# Pouziti: check_and_send (spousteni kazdou minutu z CRONu) #
##########################################################################################################
logfile = "/var/log/smss/check_and_send.log"
pidfile = "/var/run/check_and_send.pid"
import sys, os, MySQLdb
import logger, daemon
import sms
from time import gmtime, strftime, localtime
# Instance tridy na logovani
log = logger.logger(logfile)
# Instance tridy daemon a nastaveni parametru
proc = daemon.daemon(pidfile)
proc.logcl = log
# Test, zda-li process jiz bezi nebo ne
if proc.IsProcessRunning():
sys.exit()
# Regulerne spustime (pidfile)
proc.RunProcess()
# pripojeni k MySQL
try:
db=MySQLdb.connect(host="localhost",user="uzivatel",passwd="heslo",db="smss");
except Exception:
log.log("ERROR: Nelze se pripojit k MySQL")
proc.ExitProcess()
# vytvoreni instance tridy SMS a pripojeni k GSM modulu
s = sms.sms('/dev/ttyS0')
if not s.Connect():
log.log("ERROR: Nelze se pripojit k GSM modulu")
log.log("[GSM]" + s.logprefix)
log.log("[GSM]" + repr(s.loglist))
proc.ExitProcess()
# --- BEGIN: detekce zprav a odeslani ----
# SQL dotaz na seznam vsech prijatych zprav
try:
cursor = db.cursor()
cursor.execute("SELECT msg, idm FROM msg ORDER BY ulozeno ASC")
result = cursor.fetchall()
except Exception:
log.log("ERROR: Chyba v SQL dotazu 1 - fatalita - KONEC")
proc.ExitProcess()
# prochazeni seznamem prijatych zprav
for record in result:
ProcessThis = False
# test korektnosti zpravy
try:
(msg, idm) = record;
phone = msg.split(';')[0]
message = msg.split(';')[1]
if (not phone.isdigit()) or (len(phone) != 12):
raise
if len(message) <= 0:
raise
ProcessThis = True
except Exception:
log.log("ERROR: Spatny format zpravy [" + msg + "] -> mazu")
# zpracovani zpravy
if ProcessThis:
try:
cursor2 = db.cursor()
log.log("SMS: Odesilani zpravy na cislo %s, text: %s" % (phone, message))
if not s.SendSMS(phone, message):
log.log("[GSM]" + s.logprefix)
log.log("[GSM]" + repr(s.loglist))
raise
cursor2.execute("INSERT INTO sms(tel, text)\
VALUES('"+str(phone)+"', '"+str(message)+"')")
except Exception:
log.log("ERROR: Chyba pri zpracovani zpravy " + str(idm) + " [" + msg + "]")
finally:
cursor2.close()
# smazani zpravy z tabulky prijatych zprav
try:
cursor2 = db.cursor()
cursor2.execute("DELETE FROM msg WHERE idm="+str(idm))
cursor2.close()
except Exception:
log.log("ERROR: Chyba pri mazani zpravy z tabulky")
# uzavreni kurzoru
cursor.close()
# --- END: detekce zprav a odeslani ----
# --- BEGIN: prijem zprav ----
# otevreni kurzoru
cursor = db.cursor()
# nacteni prijatych SMS, ulozeni do DB a smazani
for i in s.GetListOfSMS():
cursor.execute("INSERT INTO rcv(tel, text, prijato)\
VALUES('"+i['sender']+"', '"+i['sms']+"','"+i['time']+"')")
s.DeleteSMS(i['id'])
# uzavreni kurzoru
cursor.close()
# --- END: prijem zprav ----
# Ukonceni spojeni do DB
db.close()
# Ukonceni procesu
proc.ExitProcess()
Skript /usr/local/smss/check_and_send potřebuje ke svému běhu třídu sms a PDU. Třídy obsahují metody pro komunikaci s GSM modemem a vytváření zpráv.
Zobrazit/skrýt soubor třídy/usr/local/smss/sms.py
#!/usr/bin/env python
########################################################################################################################
# S M S C L A S S #
# Tato trida je urcena pro komunikaci s GSM modemem Siemens GSM modem TC35 apod. #
# Umi cist, posilat, mazat SMS a prozvanet. #
# Soucasti je i trida PDU, ktera dovede enkodovat a dekodovat SMS zpravy z/do PDU formatu #
########################################################################################################################
# Import variuos libraries
import sys, serial, re, time, string
from time import gmtime, strftime, localtime
########################################################################################################################
# Trida implementuje komunikacni metody pro GSM modem co se SMS tyce #
########################################################################################################################
class sms:
# atribut pro instanci tridy serial
port = None
#
logprefix = ""
loglist = []
# ------------------------------------------------------------------------------------------------------------------
# Konstruktor
# Vstup: cislo nebo nazev portu
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, port):
self.port = serial.Serial()
self.port.port = port
self.port.baudrate = 9600
self.port.bytesize = 8
self.port.parity = 'N'
self.port.stopbits = 1
self.port.timeout = 10
self.port.xonxoff = 0
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Privatni metoda na cteni odpovedi od modemu s vyhodnocovanim
# Vstup: vzor - regex vzor pro true vystup
# timeout - cas na cteni dalsiho radku z portu
# Vystup: True/False dle vyhodnoceni vzoru v prijatem radku
# ------------------------------------------------------------------------------------------------------------------
def __ReadOneAnswer(self, vzor, timeout):
self.port.timeout = timeout
ret = False
while True:
x = self.port.readline()
if x=="":
break
self.__ToLog('RX',repr(x))
if re.match(vzor, x):
ret = True
break
return ret
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Privatni metody pro logovani
# Princip je ten, aby po ukonceni nejake verejne metody zustal v atributu loglist seznam radku s logem prubehu
# ------------------------------------------------------------------------------------------------------------------
def __ToLog(self, prefix, text):
self.loglist.append(strftime("%Y.%m.%d %H:%M:%S ", localtime()) + '[SMSCLASS]['+self.logprefix+']['+prefix+'] '+text)
def __ClearAndSetLog(self, prefix):
self.loglist = []
self.logprefix = prefix
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Privatni metoda na odeslani prikazu modemu
# Vstup: text - retezec, ktery bude odeslan na port
# ------------------------------------------------------------------------------------------------------------------
def __Send(self, text):
self.__ToLog('TX', repr(text))
self.port.flushInput()
self.port.write(text)
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Metoda na pripojeni k modemu a nastaveni pracovnich pameti
# Vystup: True/False dle toho, zda-li se podarilo pripojit k portu a detekovat zivy modem
# ------------------------------------------------------------------------------------------------------------------
def Connect(self):
self.__ClearAndSetLog('CONNECT')
self.__ToLog('', 'Zkousim otevrit port')
self.port.open()
if self.port.isOpen()==0:
self.__ToLog('ERR', 'Nemohu otevrit port -> fatalni chyba')
return False
# dotaz na dostupnost
self.__ToLog('OK', 'Port byl otevren, testuji dostupnost modemu prikazem AT')
self.__Send('AT\r')
if not self.__ReadOneAnswer('^OK',2):
self.__ToLog('ERR', 'Modem neodpovida')
return False
self.__ToLog('OK', 'Modem je dostupny, nastavuji pameti pro ukladani SMS')
self.__Send('AT+CPMS="MT","MT","MT"\r')
self.__ReadOneAnswer('^OK',10)
self.__Send('AT+CMGF=0\r')
self.__ReadOneAnswer('^OK',10)
self.__ToLog('OK', 'Pripojeno')
return True
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Metoda na smazani SMS
# Vstup: id - identifikator zpravy v GSM modemu
# Vystup: True/False dle toho, zda-li operace mazani probehla v poradku ci nikoliv
# ------------------------------------------------------------------------------------------------------------------
def DeleteSMS(self, id):
self.__ClearAndSetLog('DELETESMS')
self.__Send('AT+CMGD=%s\r' % str(id))
self.port.timeout = 2
if self.__ReadOneAnswer('^OK',3):
self.__ToLog('OK', 'Zprava smazana')
return True
else:
log += "ERROR"
self.__ToLog('ERR', 'Chyba pri mazani zpravy')
return False
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Metoda na ziskani seznamu SMS
# Vystup: seznam hash poli s prijatymi SMS zpravami
# ------------------------------------------------------------------------------------------------------------------
def GetListOfSMS(self):
self.__ClearAndSetLog('GETLISTOFSMS')
msgs = []
opdu = PDU()
self.__Send('AT+CMGL=4\r')
self.port.timeout = 2
while True:
x = self.port.readline()
self.__ToLog('RX', repr(x))
if (x=="") or (re.match('^OK', x)):
break
try:
x = x.strip()
if re.match("^\+CMGL.*", x):
id = int(x.split(':')[1].split(',')[0])
if len(x)>40:
t = opdu.decodeSMS(x)
t['id'] = id
self.__ToLog('SMS', str(t))
msgs.append(t)
except Exception:
self.__ToLog('ERR', 'Chybny format PDU')
return msgs
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Metoda na odeslani SMS
# Vstup: dest - cislo prijemce v mezinarovnim formatu (pr. 420608337726)
# text - text SMS zpravy (max. 160 znaku)
# Vystup: True/False dle prinaku, zda-li byla SMS zprava odeslana ci nikoliv
# ------------------------------------------------------------------------------------------------------------------
def SendSMS(self, dest, text):
self.__ClearAndSetLog('SENDSMS')
opdu = PDU()
try:
sms = opdu.encodeSMS(dest, text[:160])
if len(sms)!=2:
raise
except Exception:
self.__ToLog('ERR', 'Chyba prevodu do PDU')
return False
self.__ToLog('PDU', str(sms))
self.__Send('AT+CMGS=%s\r' % str(sms['velikost']))
if not self.__ReadOneAnswer('^>',2):
self.__ToLog('ERR', 'Chyba prikazu AT+CMGS')
return False
self.__Send("%s\x1a" % sms['pdu'])
if self.__ReadOneAnswer('.*OK.*',10):
self.__ToLog('OK', 'SMS odeslana')
return True
else:
self.__ToLog('ERR', 'SMS nebyla odeslana')
return False
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Metoda na prozvoneni
# Vstup: dest - cislo prijemce v mezinarovnim formatu (pr. 420608337726)
# cas - doba prozvaneni v sekundach (zde je nutne pocitat s dobou navazani spojeni)
# ------------------------------------------------------------------------------------------------------------------
def Ring(self, dest, cas):
self.__ClearAndSetLog('RING')
self.__Send('ATD+%s;\r' % dest)
time.sleep(cas)
self.__Send('ATH\r')
# ------------------------------------------------------------------------------------------------------------------
########################################################################################################################
# Trida implementuje kodovani a dekodovani PDU formatu SMS zprav #
########################################################################################################################
class PDU:
# Hex to dec conversion array
__hex2dec = {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'A':10,'B':11,'C':12,'D':13,'E':14,'F':15 }
# Konstruktor ------------------------------------------------------------------------------------------------------
def __init__(self):
None
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
# Prevede hex na dec
# ------------------------------------------------------------------------------------------------------------------
def __hex2int(self,n):
c1 = n[0]
c2 = n[1]
c3 = (self.__hex2dec[c1] * 16) + (self.__hex2dec[c2])
return int("%s" % c3)
# ------------------------------------------------------------------------------------------------------------------
# Prevede dec na hex
# ------------------------------------------------------------------------------------------------------------------
def __int2hex(self,n):
hex = ""
q = n
while q > 0:
r = q % 16
if r == 10: hex = 'A' + hex
elif r == 11: hex = 'B' + hex
elif r == 12: hex = 'C' + hex
elif r == 13: hex = 'D' + hex
elif r == 14: hex = 'E' + hex
elif r == 15: hex = 'F' + hex
else:
hex = str(r) + hex
q = int(q/16)
if len(hex) % 2 == 1: hex = '0' + hex
return hex
# ------------------------------------------------------------------------------------------------------------------
# Prohodi horni a spodni pulbyte
# ------------------------------------------------------------------------------------------------------------------
def __byteSwap(self,byte):
return "%c%c" % (byte[1], byte[0])
# ------------------------------------------------------------------------------------------------------------------
# Precte datum a cas z PDU zpravy
# ------------------------------------------------------------------------------------------------------------------
def __parseTimeStamp(self,time):
y = self.__byteSwap(time[0:2])
m = self.__byteSwap(time[2:4])
d = self.__byteSwap(time[4:6])
hour = self.__byteSwap(time[6:8])
min = self.__byteSwap(time[8:10])
sec = self.__byteSwap(time[10:12])
# korekce roku
if int(y) < 70:
y = "20" + y
return "%s-%s-%s %s:%s:%s" % (y, m, d, hour, min, sec)
# ------------------------------------------------------------------------------------------------------------------
# Dekoduje 7mi bitove kodovani zpravy
# ------------------------------------------------------------------------------------------------------------------
def __decodeText7Bit(self, src):
bits = ''
i = 0
l = len(src) - 1
# First, get the bit stream, concatenating all binary represented chars
while i < l:
bits += self.__char2bits(src[i:i+2])
i += 2
# Now decode those pseudo-8bit octets
char_nr = 0
i = 1
tmp_out = ''
acumul = ''
decoded = ''
while char_nr <= len(bits):
byte = bits[char_nr + i:char_nr + 8]
tmp_out += byte + "+" + acumul + " "
byte += acumul
c = chr(self.__bits2int(byte))
decoded += c
acumul = bits[char_nr:char_nr + i]
i += 1
char_nr += 8
if i==8:
i = 1
char_nr
decoded += chr(self.__bits2int(acumul))
acumul=''
tmp_out += "\n"
return decoded.strip("\x00")
# ------------------------------------------------------------------------------------------------------------------
# Konvertuje text na 7-mi bitove kodovani
# ------------------------------------------------------------------------------------------------------------------
def __encodeText7Bit(self,src):
result = []
count = 0
last = 0
for c in src:
this = ord(c) << (8 - count)
if count:
result.append('%02X' % ((last >> 8) | (this & 0xFF)))
count = (count + 1) % 8
last = this
result.append('%02X' % (last >> 8))
return ''.join(result)
# ------------------------------------------------------------------------------------------------------------------
# Znak na bin
# ------------------------------------------------------------------------------------------------------------------
def __char2bits(self,char):
inputChar = self.__hex2int(char)
mask = 1
output = ''
bitNo = 1
while bitNo <= 8:
if inputChar & mask > 0:
output = '1' + output
else:
output = '0' + output
mask = mask<<1
bitNo += 1
return output
# ------------------------------------------------------------------------------------------------------------------
# Bin na int
# ------------------------------------------------------------------------------------------------------------------
def __bits2int(self,bits):
mask = 1
i = 0
end = len(bits) - 1
result = 0
while i <= end:
if bits[end - i] == "1":
result += mask
mask = mask << 1
i += 1
return result
# ------------------------------------------------------------------------------------------------------------------
# Dekodovani PDU zpravy
# Vstup: sms - prijata PDU zprava
# Vystup: pole odesilatel - cislo odesilatele zpravy v mezinarodnim formatu
# cas - datum a cas doruceni
# zprava - citelna forma zpravy
# ------------------------------------------------------------------------------------------------------------------
def decodeSMS(self, sms):
self.SMS = {}
# Zjistime pocet oktetu SMSC informace
smsc_len = self.__hex2int(sms[0:2])
# Presun na pozici PDU flagu
i = (smsc_len * 2) + 2
# Presun na pozici s delkou cisla odesilatele
i += 2
sender_len = self.__hex2int(sms[i:i+2])
# Presun na pozici cisla odesilatele
i += 4
# Preskladani cisla odesilatele
tmp_sender='';
if sender_len % 2 == 1:
sender_len += 1;
for n in range(i, i+sender_len, 2):
tmp_sender += sms[n+1] + sms[n]
self.SMS['sender'] = tmp_sender.strip('F')
# Presun na pozici datumu a casu, skrz cislo odesilatele a 2 bytes flagu
i += sender_len + 4
# Get the sending timestamp
self.SMS['time'] = self.__parseTimeStamp(sms[i:i+14])
# Presun na pozici delky textu
i += 14
mlength = self.__hex2int(sms[i:i+2])
# Dekodovani zpravy
if mlength > 0:
message_enc = sms[i+2:len(sms)]
zprava = self.__decodeText7Bit(message_enc)
else:
zprava = ""
self.SMS['sms'] = zprava
return(self.SMS)
# ------------------------------------------------------------------------------------------------------------------
# Vytvoreni PDU zpravy
# Vstup: dest_number - telefonni cislo v mezinarodnim formatu, napr. 420608337726
# text - text SMS zpravy, max. 160 znaku, bez diakritiky
# Vystup: pole velikost - velikost PDU retezce pro prikaz AT+CMGS
# pdu - PDU retezec
# ------------------------------------------------------------------------------------------------------------------
def encodeSMS(self, dest_number, text):
self.SMS = {}
# Uvodni byte + hlavicka
result = "000100"
# Prida delku cisla prijemce a format cisla na mezinarodni
result += self.__int2hex(len(dest_number)) + '91'
# Prida cislo prijemce
tmp_number = dest_number
if len(tmp_number) % 2 == 1: tmp_number += 'F'
i = 0
while i < len(tmp_number):
result = result + self.__byteSwap(tmp_number[i:i+2])
i += 2
# Prida dalsi blbosti
result += '0000'
# Prida delku zpravy a text zpravy
result += self.__int2hex(len(text))
result += self.__encodeText7Bit(text)
# Vrati pole dvojice velikost a PDU retezec
self.SMS['pdu'] = result
self.SMS['velikost'] = (len(result)-2) / 2
return(self.SMS)
Dále je potřeba třída daemon a logger - popsány jsou výše. Opět, pokud Python zakřičí, že nemá nějaký modul, stačí ho doinstalovat z debianích repozitářů.
Webové rozhraní
Slouží k získání přehledu o odeslaných a přijatých zprávách. Je rovněž možné pomocí jednoduchého formuláře SMS odeslat. Webové rozhraní využívá pouze obsah MySQL tabulek. Jinak klasika Apache + PHP.
Klienti
Popis formátu požadavku, který akceptuje TCP server
#phone1;text1#phone2;text2...#phonen;textn
phone
je telefonní číslo v mezinárodním formátu, např. 420608337726
text
je max. 160 znaků dlouhá zpráva bez diakritiky
Každý požadavek začíná vždy znakem #
a při jednom připojení k serveru lze požadavky zřetězit a odeslat tak více SMS zpráv na různá telefonní čísla.
Příklad:
#420608337726;AHOJ
odešle text AHOJ na číslo 420608337726
#420608337726;AHOJ#420777123456;Cau
odešle text AHOJ na číslo 420608337726 a CAU na 420777123456
Klienti
- SMSSClient (Windows) ke stažení zde
- smss_client (Linux)
- telnet 192.168.1.1 2021 - ruční otestování
Zobrazit/skrýt soubor /usr/local/smss/smss_client
#!/usr/bin/env python
# -*- coding: utf-8 -*-
####################################################################################################
# PROCESY: SMS SERVER CLIENT #
# Skript na odesilani pozadavku na odeslani SMS zpravy #
# Jan Matuska, walda@starhill.org #
####################################################################################################
import socket
import sys
import time
import re
# Kontrola argumentu
if len(sys.argv)!=3:
print "To send command to SMS Server"
print "Usage: smss_client \"\"\n"
sys.exit(1)
host = sys.argv[1]
cmd = sys.argv[2]
# Vytvoreni socketu
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
print "ERROR: Failed to create socket"
sys.exit(1)
# Pripojeni k SMS Serveru
try:
sock.connect((host, 2021))
time.sleep(1)
except socket.error, msg:
print "ERROR: Failed to connect"
sys.exit(1)
# Komunikace
data = ""
try:
# Nastaveni timeoutu pro komunikacni metody
sock.settimeout(2)
# Prijeti hlavicky
data = sock.recv(1024)
# Odeslani prikazu
sock.send("%s\n" % cmd)
# Prijeti odpovedi
data = sock.recv(1024)
#print "INFO: Server answer", repr(data)
except Exception:
print "ERROR: Communication error"
sys.exit(1)
# Test odpovedi od Vema State Serveru
if (re.match(".*stored.*", data) != None):
print "OK: Successfully sent"
sys.exit(0)
else:
print "ERROR: Syntax error"
sys.exit(1)
Typické použití SMSSClienta:
SMSSClient.exe -a 192.168.1.1 #420608337726;Zprava
Vrací exitcode 0 v případě úspěšného odeslání požadavku a 1 v případě neúspěchu. Exitcode ovšem neříká nic o tom, zda SMS skutečně odešla nebo ne, pouze potvrzuje přijetí požadavku TCP serverem. V případě nedoručení je třeba nahlédnout do logu TCP serveru nebo skriptu check_and_send.