]> git.neil.brown.name Git - freerunner.git/commitdiff
Initial checkin of 'gsmd.py' and support code.
authorNeil Brown <neilb@suse.de>
Sun, 8 Feb 2009 02:54:27 +0000 (13:54 +1100)
committerNeil Brown <neilb@suse.de>
Sun, 8 Feb 2009 02:54:27 +0000 (13:54 +1100)
gsmd.py uses gsm0710muxd to talk to the modem in the Freerunner.
Its role is largely of monitoring and switching between major states
of:
  - idle/incoming/oncall : for normal operation of the phone
  - suspend : makes the modem quiet for suspend
  - flightmode : turns of the modem.

flightmode is determined if /var/lib/misc/flightmode/active is non-empty.
suspend is detected using the 'apm/events.d/interlock' script.

gsmd makes various state available via files in /var/run/gsm-state/

gsmd does not answer phone calls or retrieve SMS messages or anything like that.
Those tasks are achieved by other programs that make other connections to
gsm0710muxd.

Tracing can be enabled by setting PYTRACE=1 in the environment

TODO:
  work happily when no SIM is present
  work happily when there is no cell to register to
  allow alternate supplier to be set to support roaming.

Signed-off-by: NeilBrown <neilb@suse.de>
apm/interlock [new file with mode: 0644]
gsm/gsmd.py [new file with mode: 0644]
lib/dnotify.py [new file with mode: 0644]
lib/suspend.py [new file with mode: 0644]
lib/trace.py [new file with mode: 0644]

diff --git a/apm/interlock b/apm/interlock
new file mode 100644 (file)
index 0000000..063ca91
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+# This script should be run from /etc/apm/event.d/interlock
+# It uses files in /var/lock/suspend to allow interlock with
+# daemons that might need to know respond to suspend/resume events.
+# Said daemon should:
+#   1/  get a flock(LOCK_SH) on /var/lock/suspend/suspend,
+#        looping if the file is found, after lock, to have no links
+#   2/  use dnotify or similar to watch for changes to this file
+#   3/  when the file has size > 0, prepare for suspend
+#   3a/  take a flock(LOCK_SH) on /var/lock/suspend/next_suspend
+#   3b/  releaes the lock on .../suspend
+#   4/  use dnotify or similar to watch for next_suspend to be renamed to
+#       suspend.  At that point wake-from-suspend processing can happen.
+
+mkdir -p /var/lock/suspend
+
+case $1 in
+
+  suspend)
+    # prepare for next cycle
+    > /var/lock/suspend/next_suspend
+    {
+       # wakeup daemons that care
+       echo suspending >& 9
+       # wait for those daemons to be ready
+       flock --exclusive 9
+       sleep 1000000000 &
+       echo $! > /var/lock/suspend/.pid
+    } 9>> /var/lock/suspend/suspend
+  ;;
+  resume )
+    > /var/lock/suspend/next_suspend
+    mv /var/lock/suspend/next_suspend /var/lock/suspend/suspend
+    pid=`cat /var/lock/suspend/.pid`
+    rm -f /var/lock/suspend/.pid
+    if [ "$pid" -gt 1 ]; then
+        kill -9 "$pid"
+    fi
+
+  ;;
+esac
diff --git a/gsm/gsmd.py b/gsm/gsmd.py
new file mode 100644 (file)
index 0000000..06ebc79
--- /dev/null
@@ -0,0 +1,492 @@
+
+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()
diff --git a/lib/dnotify.py b/lib/dnotify.py
new file mode 100644 (file)
index 0000000..7ae7aea
--- /dev/null
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+
+# class to allow watching multiple files and
+# calling a callback when any change (size or mtime)
+#
+# We take exclusive use of SIGIO and maintain a global list of
+# watched files.
+# As we cannot get siginfo in python, we check every file
+# every time we get a signal.
+# we report change is size, mtime, or ino of the file (given by name)
+
+
+import os, fcntl, signal
+
+
+dirlist = []
+def notified(sig, stack):
+    for d in dirlist:
+        fcntl.fcntl(d.fd, fcntl.F_NOTIFY, (fcntl.DN_MODIFY|fcntl.DN_RENAME|
+                                           fcntl.DN_CREATE|fcntl.DN_DELETE))
+        d.check()
+
+class dir():
+    def __init__(self, dname):
+        self.dname = dname
+        self.fd = os.open(dname, 0)
+        self.files = []
+        fcntl.fcntl(self.fd, fcntl.F_NOTIFY, (fcntl.DN_MODIFY|fcntl.DN_RENAME|
+                                              fcntl.DN_CREATE|fcntl.DN_DELETE))
+        if not dirlist:
+            signal.signal(signal.SIGIO, notified)
+        dirlist.append(self)
+
+    def watch(self, fname, callback):
+        self.files.append(file(os.path.join(self.dname, fname), callback))
+
+    def check(self):
+        for f in self.files:
+            f.check()
+
+class file():
+    def __init__(self, fname, callback):
+        self.name = fname
+        stat = os.stat(self.name)
+        self.ino = stat.st_ino
+        self.size = stat.st_size
+        self.mtime = stat.st_mtime
+        self.callback = callback
+
+    def check(self):
+        stat = os.stat(self.name)
+        if stat.st_size == self.size and stat.st_mtime == self.mtime \
+           and stat.st_ino == self.ino:
+            return False
+        self.size = stat.st_size
+        self.mtime = stat.st_mtime
+        self.ino = stat.st_ino
+        self.callback(self)
+        return True
+
+if __name__ == "__main__" :
+    import signal
+
+
+    ##
+    def ping(f): print "got ", f.name
+
+    d = dir("/tmp/n")
+    a = d.watch("a", ping)
+    b = d.watch("b", ping)
+    c = d.watch("c", ping)
+
+    while True:
+        signal.pause()
diff --git a/lib/suspend.py b/lib/suspend.py
new file mode 100644 (file)
index 0000000..c84fbfd
--- /dev/null
@@ -0,0 +1,73 @@
+
+#
+# interact with apm/events.d/interlock to provide
+# suspend notification
+
+import dnotify, fcntl, os
+
+lock_watcher = None
+
+class monitor:
+    def __init__(self, suspend_callback, resume_callback):
+        """
+        Arrange that suspend_callback is called before we suspend, and
+        resume_callback is called when we resume.
+        If suspend_callback returns False, it must have arranged for
+        'release' to be called soon to allow suspend to continue.
+        """
+        global lock_watcher
+        if not lock_watcher:
+            lock_watcher = dnotify.dir('/var/lock/suspend')
+
+        self.f = open('/var/lock/suspend/suspend', 'r')
+        self.getlock()
+        while os.fstat(self.f.fileno()).st_nlink == 0:
+            self.f.close()
+            self.f = open('/var/lock/suspend/suspend', 'r')
+            self.getlock()
+
+        self.suspended = False
+        self.suspend = suspend_callback
+        self.resume = resume_callback
+        self.watch = lock_watcher.watch("suspend", self.change)
+
+    def getlock(self):
+        # lock file, protecting againt getting IOError when we get signalled.
+        locked = False
+        while not locked:
+            try:
+                fcntl.flock(self.f, fcntl.LOCK_SH)
+                locked = True
+            except IOError:
+                pass
+    
+
+    def change(self, watched):
+        if os.fstat(self.f.fileno()).st_size == 0:
+            if self.suspended and os.stat('/var/lock/suspend/suspend').st_size == 0:
+                self.suspended = False
+                if self.resume:
+                    self.resume()
+            return
+        if not self.suspended and (not self.suspend or self.suspend()):
+            # ready for suspend
+            self.release()
+
+    def release(self):
+        # ready for suspend
+        old = self.f
+        self.f = open('/var/lock/suspend/next_suspend', 'r')
+        self.getlock()
+        self.suspended = True
+        fcntl.flock(old, fcntl.LOCK_UN)
+        old.close()
+
+
+if __name__ == '__main__':
+    import signal
+    def sus(): print "Suspending"; return True
+    def res(): print "Resuming"
+    monitor(sus, res)
+    print "ready"
+    while True:
+        signal.pause()
diff --git a/lib/trace.py b/lib/trace.py
new file mode 100644 (file)
index 0000000..81acf7d
--- /dev/null
@@ -0,0 +1,19 @@
+
+# trivial library for including tracing in programs
+# It can be turned on with PYTRACE=1 in environment
+
+import os
+
+tracing = False
+
+if 'PYTRACE' in os.environ:
+    if os.environ['PYTRACE']:
+        tracing = True
+
+def log(*mesg):
+    if tracing:
+        for m in mesg:
+            print m,
+        print
+
+