--- /dev/null
+
+import re, time, gobject
+from atchan import AtChannel
+import dnotify, suspend, trace
+
+def record(key, value):
+ f = open('/var/run/gsm-state/' + key, 'w')
+ f.write(value)
+
+class Task:
+ def __init__(self, repeat):
+ self.repeat = repeat
+ pass
+ def start(self, channel):
+ # take the first action for this task
+ pass
+ def takeline(self, channel, line):
+ # a line has arrived that might is presumably for us
+ pass
+ def timeout(self, channel):
+ # we asked for a timeout and got it
+ pass
+
+
+class AtAction(Task):
+ # An AtAction involves:
+ # optional sending an AT command to check some value
+ # matching the result against a string, possibly storing the value
+ # if there is no match send some other AT command, probably to set a value
+ #
+ # States are 'init' 'checking', 'setting', 'done'
+ ok = re.compile("^OK")
+ not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)")
+ def __init__(self, check = None, ok = None, record = None, at = None,
+ timeout=None, handle = None, repeat = None):
+ Task.__init__(self, repeat)
+ self.check = check
+ self.okstr = ok
+ if ok:
+ self.okre = re.compile(ok)
+ self.record = record
+ self.at = at
+ self.timeout_time = timeout
+ self.handle = handle
+
+ def start(self, channel):
+ channel.state['retries'] = 0
+ channel.state['stage'] = 'init'
+ self.advance(channel)
+
+ def takeline(self, channel, line):
+ m = self.ok.match(line)
+ if m:
+ channel.cancel_timeout()
+ if self.handle:
+ self.handle(channel, line, None)
+ return self.advance(channel)
+ if self.not_ok.match(line):
+ channel.cancel_timeout()
+ return self.timeout(channel)
+
+ if channel.state['stage'] == 'checking':
+ m = self.okre.match(line)
+ if m:
+ channel.state['matched'] = True
+ if self.record:
+ record(self.record[0], m.expand(self.record[1]))
+ if self.handle:
+ self.handle(channel, line, m)
+ return
+
+ if channel.state['stage'] == 'setting':
+ # didn't really expect anything here..
+ pass
+
+ def timeout(self, channel):
+ if channel.state['retries'] >= 5:
+ channel.state['stage'] = 'failed'
+ channel.advance()
+ return
+ channel.state['retries'] += 1
+ channel.state['stage'] = 'init'
+ channel.atcmd('')
+
+ def advance(self, channel):
+ st = channel.state['stage']
+ if st == 'init' and self.check:
+ channel.state['stage'] = 'checking'
+ if self.timeout_time:
+ channel.atcmd(self.check, timeout = self.timeout_time)
+ else:
+ channel.atcmd(self.check)
+ elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state:
+ channel.state['stage'] = 'setting'
+ if self.timeout_time:
+ channel.atcmd(self.at, timeout = self.timeout_time)
+ else:
+ channel.atcmd(self.at)
+ else:
+ channel.state['stage'] = 'done'
+ channel.advance()
+
+class PowerAction(Task):
+ # A PowerAction ensure that we have a connection to the modem
+ # and sets the power on or off, or resets the modem
+ def __init__(self, cmd):
+ Task.__init__(self, None)
+ self.cmd = cmd
+
+ def start(self, channel):
+ if not channel.connected:
+ channel.connect()
+
+ channel.state['stage'] = 'setting'
+ if self.cmd == "on":
+ channel.set_power(True)
+ elif self.cmd == "off":
+ channel.set_power(False)
+ record('carrier', '')
+ record('cell', '')
+ record('signal_strength','0/32')
+ elif self.cmd == "reset":
+ channel.reset()
+
+ def takeline(self, channel, line):
+ # really a 'power_done' callback
+ if channel.state['stage'] == 'setting':
+ channel.state['stage'] = 'connecting'
+ return channel.atconnect()
+ if channel.state['stage'] == 'connecting':
+ channel.state['stage'] = 'done'
+ return channel.advance()
+ raise
+
+class SuspendAction(Task):
+ # This action simply allows suspend to continue
+ def __init__(self):
+ Task.__init__(self, None)
+
+ def start(self, channel):
+ channel.state['stage'] = 'done'
+ channel.suspend_handle.release()
+ return channel.advance()
+
+class Async:
+ def __init__(self, msg, handle, handle_extra = None):
+ self.msg = msg
+ self.msgre = re.compile(msg)
+ self.handle = handle
+ self.handle_extra = handle_extra
+
+ def match(self, line):
+ return self.msgre.match(line)
+
+# async handlers...
+LAC=0
+CELLID=0
+cellnames={}
+def status_update(channel, line, m):
+ if m and m.groups()[3] != None:
+ global LAC, CELLID, cellnames
+ LAC = int(m.groups()[2],16)
+ CELLID = int(m.groups()[3],16)
+ if CELLID in cellnames:
+ record('cell', cellnames[CELLID])
+ log("That one is", cellnames[CELLID])
+ # rerun final task which should be 'get signal strength'
+ channel.lastrun[-1] = 0
+ channel.abort_timeout()
+
+def new_sms(channel, line, m):
+ if m:
+ record('newsms', m.groups()[1])
+
+def cellid_update(channel, line, m):
+ # get something like +CBM: 1568,50,1,1,1
+ # don't know what that means, just collect the 'extra' line
+ pass
+def cellid_new(channel, line):
+ global CELLID, cellnames
+ line = line.strip()
+ if CELLID:
+ cellnames[CELLID] = line
+ record('cell', line)
+
+incoming_num = None
+def incoming(channel, line, m):
+ global incoming_num
+ if incoming_num:
+ record('incoming', incoming_num)
+ else:
+ record('incoming', '-')
+ if channel.gstate != 'incoming':
+ channel.set_state('incoming')
+def incoming_number(channel, line, m):
+ global incoming_num
+ if m:
+ incoming_num = m.groups()[0]
+ record('incoming', incoming_num)
+
+def call_status(channel, line, m):
+ log("call_status got", line)
+ if not m:
+ return
+ s = int(m.groups()[0])
+ log("s = %d" % s)
+ if s == 0:
+ # idle
+ global incoming_num
+ incoming_num = None
+ if channel.gstate == 'incoming':
+ record('incoming', '')
+ if channel.gstate != 'idle':
+ channel.set_state('idle')
+ if s == 3:
+ # incoming call
+ if channel.gstate != 'incoming':
+ # strange ..
+ channel.set_state('incoming')
+ record('incoming', '-')
+ if s == 4:
+ # on a call
+ if channel.gstate != 'on-call':
+ channel.set_state('on-call')
+
+control = {}
+
+# For flight mode, we turn the power off.
+control['flight'] = [
+ PowerAction('off')
+ ]
+
+# For suspend, we want power on, but no wakups for status or cellid
+control['suspend'] = [
+ AtAction(at='+CNMI=1,1,0,0,0'),
+ AtAction(at='+CREG=0'),
+ SuspendAction()
+ ]
+
+control['idle'] = [
+ PowerAction('on'),
+ AtAction(at='V1E0'),
+ AtAction(at='+CMEE=2;+CRC=1'),
+ # Turn the device on.
+ AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+ # register with a carrier
+ AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+ record=('carrier', '\\1'), timeout=10000),
+ AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+ record=('carrier', '\\1'), timeout=10000, repeat=37000),
+ # fix a bug
+ AtAction(at='%SLEEP=2'),
+ # text format for various messages such SMS
+ AtAction(check='+CMGF?', ok='\+CMGF: 1', at='+CMGF=1'),
+ # get location status updates
+ AtAction(at='+CREG=2'),
+ AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', handle=status_update),
+ # Enable collection of Cell Info message
+ #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'),
+ AtAction(at='+CSCB=0'),
+ AtAction(at='+CSCB=1'),
+ # Enable async reporting of TXT and Cell info messages
+ #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'),
+ AtAction(at='+CNMI=1,0,0,0,0'),
+ AtAction(at='+CNMI=1,1,2,0,0'),
+ # Enable reporting of Caller number id.
+ AtAction(check='+CLIP?', ok='\+CLIP: 1,2', at='+CLIP=1', timeout=5000),
+
+ # Must be last: get signal string
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=29000)
+ ]
+
+control['incoming'] = [
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500),
+
+ # Must be last: get signal strength
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=30000)
+ ]
+control['on-call'] = [
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000),
+
+ # Must be last: get signal strength
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=30000)
+ ]
+async = [
+ Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update),
+ Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms),
+ Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update,
+ handle_extra = cellid_new),
+ Async(msg='\+CRING: (.*)', handle = incoming),
+ Async(msg='\+CLIP: "([^"]*)",[0-9,]*', handle = incoming_number),
+ ]
+
+class GsmD(AtChannel):
+
+ # gsmd works like a state machine
+ # the high level states are: flight suspend idle incoming on-call
+ # Note that the whole 'call-waiting' experience is not coverred here.
+ # That needs to be handled by whoever answers calls and allows interaction
+ # between user and phone system.
+ #
+ # Each state contains a list of tasks such as setting and checking config options
+ # and monitoring state (e.g. signal strength)
+ # Some tasks are single-shot and only need to complete each time the state is
+ # entered. Others are repeating (such as status monitoring).
+ # We take the first task of the current list and execute it, or wait
+ # until one will be ready.
+ # Tasks them selves can be state machine, so we keep track of what 'stage'
+ # we are up to in the current task.
+ #
+ # The system is (naturally) event driven. The main two events that we receive
+ # 'takeline' which presents one line of text from the GSM device, and
+ # 'timeout' which indicates that a timeout set when a command was sent has
+ # expired.
+ # Other events are:
+ # 'taskready' when the time of the next pending task arrives.
+ # 'flight' when the state of the 'flight mode' has changed
+ # 'suspend' when a suspend has been requested.
+ #
+ # Each event does some event specific processing to modify the state,
+ # Then calls 'self.advance' to progress the state machine.
+ # When high level state changes are requested, any pending task is discarded.
+ #
+ # If a task detects an error (gsm device not responding properly) it might
+ # request a reset. This involves sending a modem_reset command and then
+ # restarting the current state from the top.
+ # A task can also indicate:
+ # The next stage to try
+ # How long to wait before trying (or None)
+ #
+
+
+ def __init__(self):
+ AtChannel.__init__(self, master = True)
+
+ record('carrier','')
+ record('cell','')
+ record('incoming','')
+ record('signal_strength','')
+
+ self.extra = None
+ self.flightmode = True
+ # Monitor other external events which affect us
+ d = dnotify.dir('/var/lib/misc/flightmode')
+ self.flightmode_watcher = d.watch('active', self.check_flightmode)
+
+ self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume)
+
+ self.state = None
+ # set the initial state
+ self.set_state('flight')
+
+ # Check the externally imposed state
+ self.check_flightmode(self.flightmode_watcher)
+ # and GO!
+ self.advance()
+
+ def check_flightmode(self, f):
+ try:
+ fd = open("/var/lib/misc/flightmode/active")
+ l = fd.read(1)
+ fd.close()
+ except IOError:
+ l = ""
+ log("check flightmode got", len(l))
+ if len(l) == 0:
+ self.flightmode = False
+ if self.gstate == 'flight':
+ if self.suspend_handle.suspended:
+ self.set_state('suspend')
+ else:
+ self.set_state('idle')
+ else:
+ self.flightmode = True
+ if self.gstate != 'flight':
+ self.set_state('flight')
+
+ def do_suspend(self):
+ if self.gstate == 'flight':
+ return True
+ self.set_state('suspend')
+ return False
+
+ def do_resume(self):
+ if self.gstate == 'suspend':
+ self.set_state('idle')
+
+ def set_state(self, state):
+ log("state becomes", state)
+ n = len(control[state])
+ self.lastrun = n * [0]
+ self.gstate = state
+ self.state = None
+ self.tasknum = None
+ self.abort_timeout()
+
+
+ def advance(self):
+ now = int(time.time()*1000)
+ if self.state != None:
+ if self.state['stage'] == 'done':
+ self.lastrun[self.tasknum] = now
+ else:
+ self.need_reset()
+ self.state = None
+ self.tasknum = None
+ (t, delay) = self.next_cmd()
+ if delay:
+ log("Sleeping for %f seconds" % (delay/1000))
+ self.set_timeout(delay)
+ else:
+ self.tasknum = t
+ self.state = {}
+ control[self.gstate][t].start(self)
+
+
+ def takeline(self, line):
+
+ if not line:
+ return False
+
+ if self.extra:
+ self.extra.handle_extra(self, line)
+ self.extra = None
+ return False
+
+ # Check for an async message
+ for m in async:
+ mt = m.match(line)
+ if mt:
+ m.handle(self, line, mt)
+ if m.handle_extra:
+ self.extra = m
+ return False
+
+ # else pass it to the task
+ if self.tasknum != None:
+ control[self.gstate][self.tasknum].takeline(self, line)
+
+ def power_done(self):
+ if self.tasknum != None:
+ control[self.gstate][self.tasknum].takeline(self, None)
+
+
+ def timedout(self):
+ if self.tasknum == None:
+ self.advance()
+ else:
+ control[self.gstate][self.tasknum].timeout(self)
+
+
+ def next_cmd(self):
+ # Find a command to execute, or a delay
+ # return (cmd,time)
+ # cmd is an index into control[state], or -1 for reset
+ # time is seconds until try something
+ mindelay = 60*60*1000
+ cs = control[self.gstate]
+ n = len(cs)
+ now = int(time.time()*1000)
+ for i in range(n):
+ if self.lastrun[i] == 0 or (cs[i].repeat and
+ self.lastrun[i] + cs[i].repeat <= now):
+ return (i, 0)
+ if cs[i].repeat:
+ delay = (self.lastrun[i] + cs[i].repeat) - now;
+ if delay < mindelay:
+ mindelay = delay
+ return (0, mindelay)
+
+ def next(self):
+ (cmd, delay) = self.next_cmd()
+ if cmd == -1:
+ self.close()
+ self.open()
+ self.command("reset_modem")
+ return False
+ if delay > 0:
+ gobject.timeout_add(int(delay * 1000), self.next)
+ return False
+ self.cmd = cmd
+ self.stage = None
+ self.advance()
+ return False
+
+GsmD()
+c = gobject.main_context_default()
+while True:
+ c.iteration()