Index: pyneod.py =================================================================== --- pyneod.py (revision 111) +++ 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 GpsLocation Index: setup.py =================================================================== --- setup.py (revision 111) +++ 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,503 @@ +#!/usr/bin/env python2.5 +# -*- coding: latin1 -*- + +# SMS Storage Daemon +# License: GPL, copyright Bernhard Kaindl , 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" +# dbus-query --dest=org.mobile /org/mobile/Storage/SMS org.mobile.Storage.ListEntries +# Returns: +# array [ +# string "5" +# ] +# 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, ...) +# 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') + +__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=2: Buffer unsolicited result codes while link is reserved + # mt=1: Enable getting new SMS indication with +CMTI: ,: + # bm=2: New CBMs are routed directly to the TE using + # +CBM: ,,,, + # 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 + '+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: ,,[],[,,,,,,,] + # if text mode (+CMGF=1), command successful and SMS-SUBMIT: + # +CMGR: ,,[][,,,,,[],,,] + # CMGL spec: + # if text mode (+CMGF=1), command successful and SMS-SUBMITs and/or SMS-DELIVERs: + # +CMGL: ,,,[],[][,,][ + # +CMGL: ,,,[],[][,,][...]] + ####################################################### + ### 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: + # + # filters may include also: "REC READ", "REC UNREAD", "ALL", ... + # + @method(DIN_STORAGE, 's', 'a{sv}') + def RetrieveEntry(self, primary_key): + # TODO: raise defined 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, '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[=[,[,]]] + #text is entered + #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: + # +CMS ERROR: e.g.: +CMS ERROR: operation not allowed + line = self.readline().strip() # Response + LOG(LOG_INFO, __name__, 'read:', 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 = 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 == '+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:nowrap Index: pyneod.ini =================================================================== --- pyneod.ini (revision 111) +++ pyneod.ini (working copy) @@ -6,6 +6,9 @@ [ppp] apn = web.vodafone.de +[sms] +poll_interval = 10 + [gsm] poll_interval = 7