Index: pyneod.py===================================================================--- pyneod.py (revision 116)+++ pyneod.py (working copy)@@ -33,6 +33,11 @@ from pygsmd import GsmPhone daemons.append(GsmPhone(bus)) except Exception, e: LOG(LOG_ERR, __name__, 'pygsmd', e)+if has_section('sms'):+ try:+ from pysmsd import GsmSmsStorage+ daemons.append(GsmSmsStorage(bus))+ except Exception, e: LOG(LOG_ERR, __name__, 'pysmsd', e) if has_section('gps'): try: from pygpsd import GpsLocationIndex: setup.py===================================================================--- setup.py (revision 116)+++ setup.py (working copy)@@ -19,6 +19,7 @@ 'pyggld.py', 'pygpsd.py', 'pygsmd.py',+ 'pysmsd.py', 'pypppd.py', 'pypwrd.py', ]Index: pysmsd.py===================================================================--- pysmsd.py (revision 0)+++ pysmsd.py (revision 0)@@ -0,0 +1,528 @@+#!/usr/bin/env python2.5+# -*- coding: latin1 -*-++# SMS Storage Daemon+# License: GPL, copyright Bernhard Kaindl <bkaindl@ffii.org>, 2008+# Derived from pygsmd.py, subject to:+# copyright : m. dietrich+# license: gpl++# Current status: In development:+#+# only ListEntries, RetrieveEntry and StoreEntry is implemented, but StoreEntrys+# successfully tested, in my setup, the Calypso always refused writing to the SIM.+#+# org.mobile.Storage.ListEntries gives all entries, no filters, no parameters.++# Biggest bug: Errors from the modem are not handled yet, needed if SIM not unlocked+# (if PIN/PUK is needed) to access SMS.++# Usage:+# for standalone testing (after starting the muxer):+# ./pysmsd.py &+# for deployment (again after starting the muxer):+# ./pyneod.py &++# Calling:+# alias dbus-query="dbus-send --system --print-reply --type=method_call"+## List all SMS:+# dbus-query --dest=org.mobile /org/mobile/Storage/SMS org.mobile.Storage.ListEntries +# Returns:+# array [+# string "5"+# ]+## Retrieve an SMS:+# dbus-query --dest=org.mobile /org/mobile/Storage/SMS org.mobile.Storage.RetrieveEntry string:5+# Returns:+# array [+# dict entry(+# string "index"+# variant int32 5+# )+# dict entry(+# string "stat"+# variant string "REC READ"+# )+# dict entry(+# string "book"+# variant string "Ingrid Steger"+# )+# dict entry(+# string "number"+# variant string "+491790123456"+# )+# dict entry(+# string "time"+# variant string "11:45:18+04"+# )+# dict entry(+# string "date"+# variant string "08/03/23"+# )+# dict entry(+# string "message"+# variant string "Danke! Ja schaud guat aus ... Buszale hdvl"+# )+# ]+# TODO for this output: Maybe bring time and date into a standard format (to be decided)+# (could use datetime for it) and convert GSM timezone to common one (+04 -> GMT+1, ...)+#+## Storing an SMS in SIM memory:+# dbus-query --dest=org.mobile /org/mobile/Storage/SMS org.mobile.Storage.StoreEntry uint32:+491795965727 string:Hi+# Error org.freedesktop.DBus.Python.pysmsd.GsmError: Traceback (most recent call last):+# File "/usr/lib64/python2.5/site-packages/dbus/service.py", line 655, in _message_cb+# retval = candidate_method(self, *args, **keywords)+# File "/windows/D/moko/pyneod/pysmsd.py", line 453, in StoreEntry+# # FIXME: Test this, it always fails on my Neo with my SIM...+# GsmError: ('Writing SMS failed with:', '+CMS ERROR: operation not allowed')+#+## Deleting an SMS:+# dbus-query --dest=org.mobile /org/mobile/Storage/SMS org.mobile.Storage.DeleteEntry string:5+# (Untested, should not give a return value, gives error exception if it fails)++__revision = '$Rev: 99 $'++# the serial does all the initialization of the serial port for this+# module. this includes line speed, hw flow control and the like.+import serial+from gobject import source_remove, io_add_watch, IO_IN, timeout_add+from time import sleep+# datetime can be used to parse the date and time returned in the SMS data:+from datetime import datetime+from base import log_info, log_debug, config, LOG_ERR, LOG_WARNING, LOG_INFO, LOG_DEBUG, LOG+from freesmartphone import *++init_commands = ( # base settings+ # echo off, verbose result on, report mobile equipment error,+ # cellular result codes and single numbering scheme:+ 'Z', # Being really slow...+ 'E0V1', # Start sloowly...+ '+CMEE=2;+CRC=1;+CSNS=0',+ '+CFUN=1',+ #'+CSMS=0', # response is +CSMS: 1,1,1 (all MT, MO and CBM supported)+ #'+CPMS=?',# response is +CPMS: ("ME","SM"),("ME","SM"),("ME","SM")+ # (ME and SIM memories reading and writing, but reading ME fails..)+ '+CPMS="SM","SM","SM"', #(set SIM memory)+ '+CPIN?',+ '+CPMS?', #(query current memories)+ #+CPMS: "ME",5,99,"ME",5,99,"ME",5,99 (five messages in ME, 99 total space)+ ########################### CHARSET SELECTION ########################+ '+CSCS="IRA"', # Set IRA charset (only for now, USC2 later) #+ #'+CSCS="UCS2"', # Set UCS-2 charset (later, but sure to be done) #+ ######################################################################+ '+CSDH=1;' # Show Text Mode Parameters (show additional info in CMGL/CMGR)+ '+CSCB=1', # (Just for interest: all CBMs are accepted)+ ######################################################################+ # Set the New Message Indications properties:+ # +CNMI=[<mode>[,<mt>[,<bm>[,<ds>[,<bfr>]]]]]+ # mode=2: Buffer unsolicited result codes while link is reserved + # mt=1: Enable getting new SMS indication with +CMTI: <mem>,<index>:+ # bm=2: New CBMs are routed directly to the TE using+ # +CBM: <sn>,<mid>,<dcs>,<page>,<pages><CR><LF><data>+ # ds=1: If SMS-STATUS-REPORT is forwarded to us also+ # (just for interest, can be disabled if we are not implementing+ # SMS status here)+ # TODO: Use AT+CNMI=? -> +CNMI: (0-2),(0-3),(0-3),(0,1),(0,1)+ # to check if what you want is actually supported by the TE/TA!+ '+CNMI=2,1,2,1',+ '+CMGL="ALL"', # List all SMS+ )+statuscommand = ';'.join((+# '+CMGF=0', # pdu mode sms enable + '+CPIN?',+ '+CMGF=1', # text mode sms enable+ '+CSCS="IRA"', # Set IRA charset (only for now, USC2 later)+ '+CMGL="ALL"', # List all SMS+ ))++import dbus+class GsmError(dbus.DBusException):+ _dbus_error_name = 'org.freesmartphone.Storage.SMS.GsmError'+class EntryNotfound(dbus.DBusException):+ _dbus_error_name = 'org.freesmartphone.Storage.SMS.NotFound'+class Busy(dbus.DBusException):+ _dbus_error_name = 'org.freesmartphone.Storage.SMS.Busy'+class NotReady(dbus.DBusException):+ _dbus_error_name = 'org.freesmartphone.Storage.SMS.NotReady'+class InternalError(dbus.DBusException):+ _dbus_error_name = 'org.freesmartphone.Storage.SMS.InternalError'++class GsmSmsStorage(serial.Serial, NotifyObject):+ '''+ this class just fires requests against the modem. if the modem+ answers, those responses are parsed and if the answers are state+ responses methods with the same name are called if a subclass+ implements those.++ this class does not contain gsm-state managment or interpretation+ of responses. it only controls if the modem is alive and reponses+ to standard status requests (see statuscommand which is fired+ every 3000 ms).++ TODO the statuscommand is currently constant. it should be+ possible to alter the states that are requested periodically.+ '''+ def __init__(self, bus):+ self.bus = bus+ # glib's waiters+ self.tow = None+ self.iow = None+ serial.Serial.__init__(self)+ NotifyObject.__init__(self,+ object_path='/org/mobile/Storage/SMS',+ bus_name=BusName(DBUS_NAME, bus=self.bus),+ )+ self.__ctor()+ LOG(LOG_INFO, __name__, 'waiting for muxer to tell us a channel')+ timeout_add(100, self.__connect_to_muxer)++ def __ctor(self):+ # reset div. info+ #+ self.baudrate = 115200+ self.command = None+ self.commands = []+ self._device_info = dict()+ self.in_command = False+ self.ready = False+ self.update_in_progress = True+ self.pin_required = False+ self._sms = dict()+ self._sms_idx = -1+ self._sms_list_new = []+ self._sms_indices_new = []+ self.poll_interval = int(config.get('gsm', 'poll_interval'))+ self.port = '/dev/null'+ self.rtscts = True+ self.silence = 0+ self._sim_auth_status = dict()+ self.timeout = 7+ self.writeTimeout = 1+ self.xonxoff = False+ # serial config++ def __connect_to_muxer(self):+ '''+ if gsm does not control the device itsself it will inquire one from the+ muxer. glib calls this method every x seconds to try that.+ '''+ LOG(LOG_DEBUG, __name__, '__connect_to_muxer')+ if self.isOpen():+ LOG(LOG_WARNING, 'serial port still open')+ try:+ obj = self.bus.get_object('org.mobile.mux', '/org/mobile/mux/RemoteObject')+ obj.connect_to_signal('deactivate', self.__deactivate, dbus_interface='org.mobile.mux.RemoteInterface')+ obj = Interface(obj, 'org.mobile.mux.RemoteInterface')+ gsm_port = str(obj.alloc_channel('gsm')) # str() needed because pyserial doesnt use isinstance!+ if gsm_port:+ LOG(LOG_DEBUG, __name__, '__connect_to_muxer got port', gsm_port)+ # reset div. info+ self.__ctor()+ self.port = gsm_port+ self.open()+ self.iow = io_add_watch(self, IO_IN, self.__read)+ self.tow = timeout_add(self.poll_interval * 1000, self.__poll)+ self.commands = list(init_commands)+ self.write('\n\r') # channel wakeup+ #self.write('ATZE0V1\n\r') # channel wakeup+ #line = self.readline().strip()+ #LOG(LOG_DEBUG, __name__, 'AAAAT response', line)+ self.__write(self.commands.pop(0))+ return False+ except Exception, e:+ LOG(LOG_ERR, __name__, 'dbus error', e)+ timeout_add(self.poll_interval * 1000, self.__connect_to_muxer)+ return False++ def __deactivate(self, s):+ '''+ method that gets called if the muxer deactivates the channel+ '''+ self.__close()+ timeout_add(self.poll_interval * 1000, self.__connect_to_muxer)++ def __poll(self):+ '''+ method that is called regulary to check if the modem is alive.+ in the end a hard reset will be tried.+ '''+ if self.isOpen():+ self.silence += 1+ if self.in_command:+ if self.silence > 100:+ self.__write("")+ else:+ LOG(LOG_DEBUG, __name__, 'polling modem', self.silence)+ if self.commands:+ self.__write(self.commands.pop(0))+ #elif self.silence > 4:+ #LOG(LOG_ERR, __name__, 'resetting due to silence')+ #self.__write('+CFUN=4') # function disable+ #self.__reset()+ elif self.silence > 2:+ LOG(LOG_INFO, __name__, 'issue empty request to wakeup')+ self.__write("")+ else:+ if statuscommand not in self.commands:+ LOG(LOG_DEBUG, __name__, 'status request queued')+ self.__push(statuscommand)+ return True++ def __close(self, hard=False):+ '''+ switch off the modem+ '''+ LOG(LOG_DEBUG, __name__, '__close')+ if self.iow:+ source_remove(self.iow)+ self.iow = None+ if self.tow:+ source_remove(self.tow)+ self.tow = None+ if self.isOpen():+ self.close()+ self.__ctor()++ def __push(self, command):+ '''+ push a new command to the queue of commands. if no command is+ pending, this command will be written directly to the modem.+ '''+ if self.in_command:+ self.commands.append(command)+ else:+ self.__write(command)+++ def __write(self, command):+ '''+ write a command to the modem.+ '''+ try:+ self.command = command+ if command is not None:+ command = 'AT%s\r\n'% command+ LOG(LOG_DEBUG, __name__, 'write', command.strip())+ self.write(command)+ self.in_command = True+ #sleep(0.03)+ except Exception, e:+ LOG(LOG_ERR, __name__, 'error', e)++ def __read(self, source, condition):+ '''+ this method is called if the fd is readable (as glib tells+ us). it reads one line and interprets it.+ '''+ try:+ line = self.readline().strip()+ self.silence = 0+ if line: # ignore empty lines+ LOG(LOG_DEBUG, __name__, 'read', line.__repr__())+ if line == 'AT%s'% self.command: # ignore my echo...+ pass+ elif line in ('NO CARRIER', 'BUSY', 'ERROR', 'OK', ):+ name = 'gsm%s'% line+ name = name.replace(' ', '_')+ fnctn = getattr(self, name, None)+ if fnctn: fnctn(line)+ else: LOG(LOG_ERR, __name__, 'no fnctn', name, self.command)+ self.in_command = False+ # This only works with charset != IRA as we might get "OK" also as SMS Text,+ # But we would interpret it as OK response above -> Use UCS2 instead!+ elif self._sms_idx >= 0:+ self.CMGL_saveLineToSMS(line)+ elif line[0] == '+' or line[0] == '%': # state responses+ name, values = line[1:].split(': ', 1)+ values = values.split(',')+ if not 'ERROR' in name:+ name = 'gsm%s'% name+ fnctn = getattr(self, name, None)+ if fnctn: fnctn(*values)+ else: LOG(LOG_ERR, __name__, 'no fnctn', name, values)+ else:+ self.__error(self.command, name, *values)+ self.in_command = False+ else:+ LOG(LOG_ERR, __name__, 'read unknown', line.__repr__(), self.command)+ if self.command:+ values = line.split(',')+ name = 'gsm%s'% self.command[1:]+ fnctn = getattr(self, name, None)+ if fnctn: fnctn(*values)+ else: LOG(LOG_ERR, __name__, 'no fnctn', name, values)+ if not self.in_command and self.commands:+ self.__write(self.commands.pop(0))+ except Exception, e:+ LOG(LOG_ERR, __name__, e, line)+ return True+ def __del__(self):+ self.__close()+ ### errors+ @notify(DIN_STORAGE, 'av')+ def error(self, texts):+ LOG(LOG_INFO, __name__, texts, 'returned from', self.command)+ def __error(self, *texts):+ self.error(texts)+ ####################################################### + ### Called by all dbus methods to check if we are ready+ def check_ready(self):+ # We retrieve all the data beforehand and only check here if we have+ # the latest SMS data:+ if self.ready:+ return+ elif self.pin_required:+ raise NotReady('SIM is locked, needs PIN!')+ elif self.update_in_progress:+ raise Busy('SMS list is in progress of being updated, try again!')+ else:+ raise InternalError('Internal error, not ready but no explanation for it!')+ ####################################################### + # CMGR spec:+ # if text mode (+CMGF=1), command successful and SMS-DELIVER:+ # +CMGR: <stat>,<oa>,[<alpha>],<scts>[,<tooa>,<fo>,<pid>,<dcs>,<sca>,<tosca>,<length>]<CR><LF><data>+ # if text mode (+CMGF=1), command successful and SMS-SUBMIT:+ # +CMGR: <stat>,<da>,[<alpha>][,<toda>,<fo>,<pid>,<dcs>,[<vp>],<sca>,<tosca>,<length>]<CR><LF><data>+ # CMGL spec:+ # if text mode (+CMGF=1), command successful and SMS-SUBMITs and/or SMS-DELIVERs:+ # +CMGL: <index>,<stat>,<oa/da>,[<alpha>],[<scts>][,<tooa/toda>,<length>]<CR><LF><data>[<CR><LF>+ # +CMGL: <index>,<stat>,<da/oa>,[<alpha>],[<scts>][,<tooa/toda>,<length>]<CR><LF><data>[...]]+ ####################################################### + ### LIST SMS format as we get it from the modem:+ # ['5', '"REC READ"', '"436644490292"', '', '"08/03/23', '11:45:18+04"', '145', '91']'+ def gsmCMGL(self,index,stat,oada,alph,date,time,tooatoda,length):+ self.update_in_progress = True+ LOG(LOG_INFO, __name__, 'sms metadata:', index,stat,oada,alph,date,time,tooatoda,length)+ # Put the metadata into a dict as we get the message only with the next line:+ if (tooatoda == '145'): # Number should start with "+" (first char of oada is: '"')+ oada = oada.replace('"', '+', 1)+ self._sms = dict(self._sms,+ index = int(index),+ number = oada.replace('"', ''),+ # FIXME: Result parser may find some "," in the alph from the+ # Phone book here and we may have messed up already!: Fix parser+ # to not look for "," within double-quoted result strings!+ book = alph.replace('"', ''),+ date = date.replace('"', ''),+ # FIXME: Convert GSM timezone to common format (+04 => GMT+1 and so on)+ time = time.replace('"', ''),+ stat = stat.replace('"', ''),+ )+ self._sms_indices_new.append(index);+ self._sms_idx = index+ ####################################################### + ### SMS Helper:+ def CMGL_saveLineToSMS(self, line):+ # We stored the SMS metadata, now add the message and append the+ # complete SMS dict record to the (so far received SMS list):+ LOG(LOG_INFO, __name__, 'got smstext:', line)+ self._sms = dict(self._sms, message=line)+ self._sms_list_new.append([int(self._sms.get('index')), self._sms])+ self._sms_idx = -1+ ####################################################### + ### RetrieveEntry: gets the SMS index of an SMS, returns the SMS record+ # TODO: Implement additional org.freesmartphone.Storage parameters:+ # <arg type="s" name="filter" direction="in" /><!-- later -->+ # filters may include also: "REC READ", "REC UNREAD", "ALL", ...+ # <arg type="i" name="limit" direction="in" />+ @method(DIN_STORAGE, 's', 'a{sv}')+ def RetrieveEntry(self, primary_key):+ # TODO: raise defined API exception if primary_key does not convert to int:+ index = int(primary_key)+ for sms in self.sms_list:+ if sms[0] == index:+ return sms[1]+ raise EntryNotfound('The requested SMS index could not be found!')+ @method(DIN_STORAGE, '', 'as')+ def ListEntries(self):+# If the main loop were in a separate thread, this could work (in theory, if it was C):+# while (not self.ready) or self.in_command:+# LOG(LOG_INFO, __name__, 'sleep a bit....', self.ready, self.in_command)+# sleep(1)+ self.check_ready()+ return self.sms_indices+ #@method(DIN_STORAGE, 'a{sv}', 's')+ #def StoreEntry(self, sms):+ @method(DIN_STORAGE, 's', '')+ def DeleteEntry(self, primary_key):+ # TODO: raise defined API exception if primary_key does not convert to int:+ index = int(primary_key)+ self.check_ready()+ # Possible addition check:+ #for sms in self.sms_list:+ # if sms[0] == index:+ # -> Ok, SMS exists, go on, else raise NOTEXIST:+ #raise EntryNotfound('The SMS index to delete could not be found!')+ self.write('AT+CMGD=%d\r\n'% index)+ line = self.readline().strip() # Response+ LOG(LOG_INFO, __name__, 'resp:', line)+ if line.find('OK') == -1:+ raise GsmError('Deleting SMS failed with:', line)+ @method(DIN_STORAGE, 'is', 's')+ def StoreEntry(self, number, text):+ LOG(LOG_INFO, __name__, 'StoreEntry called with:', number, text)+ # For catching SIM not ready, needs to be improved:+ self.check_ready()+# while (not self.ready) or self.in_command:+# LOG(LOG_INFO, __name__, 'sleep a bit....', self.ready, self.in_command)+# sleep(1)+ if self.in_command:+ raise Busy('This should not be possible!!!! AT command in progress, try again!')+ # Or, read the data, store it in a list, and make readline calls from status loop read it+ #number = sms.get('number')+ #text = sms.get('text')+ #+CMGW[=<oa/da>[,<tooa/toda>[,<stat>]]]<CR>+ #text is entered<ctrl-Z/ESC>+ #self.write('AT+CMGW=%s\r\n%s%c'% (number, text, 26))+ self.write('AT+CMGW=%s\r%s%c'% (number, text, 26))+ line = self.readline() # One Empty line if we sent one line.+ # Responses:+ # +CMGW: <index>+ # +CMS ERROR: <err> e.g.: +CMS ERROR: operation not allowed+ line = self.readline().strip() # Response+ LOG(LOG_INFO, __name__, 'resp:', line)+ # FIXME: Test this, it always fails on my Neo with my SIM...+ if line.find('+CMGW:') == -1:+ raise GsmError('Writing SMS failed with:', line)+ primary_key = line.replace('+CMGW:', '').strip()+ return primary_key+ ####################################################### + ### STATUS CHECKS+ def gsmCPIN(self, pin_state):+ LOG(LOG_INFO, __name__, 'pin state', pin_state)+ if (pin_state == 'READY'):+ self.pin_required = False+ else:+ self.pin_required = True+ ####################################################### + ### RESPONSE HANDLERS+ # TODO: Add Handler for '+CMS ERROR: SIM PIN required' and set self.pin_required then.+ def gsmOK(self, line):+ #print "OK for command:", command+ if self.command == '+CMGL="ALL"' or self.command == '+CPIN?;+CMGF=1;+CSCS="IRA";+CMGL="ALL"':+ self.sms_indices = self._sms_indices_new; # New SMS indices+ self.sms_list = self._sms_list_new+ self._sms_indices_new = []; # Clean SMS indices+ self._sms_list_new = []+ self.ready = True+ self.update_in_progress = False+ def gsmERROR(self, line):+ self.error(line)+ def gsmCRING(self, state):+ self.error(line)+ def gsmNO_CARRIER(self, line):+ self.error(line)+ def gsmBUSY(self, line):+ self.error(line)++if __name__ == '__main__':+ from syslog import openlog, syslog, closelog, LOG_ERR, LOG_WARNING, LOG_INFO, LOG_DEBUG, LOG_DAEMON, LOG_NDELAY, LOG_PID, LOG_PERROR+ from gobject import MainLoop+ from dbus.mainloop.glib import DBusGMainLoop+ openlog('pygsmd', LOG_NDELAY|LOG_PID|LOG_PERROR, LOG_DAEMON,)+ DBusGMainLoop(set_as_default=True)+ mainloop = MainLoop()+ daemon = GsmSmsStorage(InitBus())+ mainloop.run()+ closelog()+# vim:tw=0:nowrapIndex: pyneod.ini===================================================================--- pyneod.ini (revision 116)+++ pyneod.ini (working copy)@@ -6,6 +6,9 @@ [ppp] apn = web.vodafone.de+[sms]+poll_interval = 10+ [gsm] poll_interval = 7