]> git.neil.brown.name Git - plato.git/commitdiff
New gsmd2
authorNeilBrown <neilb@suse.de>
Fri, 13 Dec 2013 09:30:58 +0000 (20:30 +1100)
committerNeilBrown <neilb@suse.de>
Fri, 13 Dec 2013 09:30:58 +0000 (20:30 +1100)
The state machine model is not quite different.
We have a number of state machines which all run in parallel
handling different aspects of the modem state.

gsm/gsmd2.py [new file with mode: 0644]

diff --git a/gsm/gsmd2.py b/gsm/gsmd2.py
new file mode 100644 (file)
index 0000000..5683eeb
--- /dev/null
@@ -0,0 +1,1387 @@
+#!/usr/bin/env python
+
+
+# Error cases to handle
+# if +CFUN:6, then "+CFUN=1" can produce "+CME ERROR: operation not supported"
+#   close/open sometimes fixes.
+# +CIMI  can produce +CME ERROR: operation not allowed
+#   close/open seems to fix.
+# +CLIP? can produce CME ERROR: network rejected request
+#   don't know what fixed it
+# +CSCB=1 can produce +CMS ERROR: 500
+#   just give up and retry later.
+# CFUN:4 can be fixed by writing CFUN=1
+# CFUN:6 needs close/open
+# _OPSYS=3,2 can produce ERROR.  Just try much later I guess.
+
+#TODO
+# send sms
+# USS
+#Handle COPS
+# repeat status until full success
+# Keep suspend blocked while any messages are queued.
+# Need to detect reset and reset configs
+# use CLCC to get number
+
+import gobject
+import re, time, os
+from atchan import AtChannel
+import dnotify, suspend
+from tracing import log
+from subprocess import Popen
+from evdev import EvDev
+import wakealarm
+import storesms
+import sms
+
+def safe_read(file, default=''):
+    try:
+        fd = open(file)
+        l = fd.read(1000)
+        l = l.strip()
+        fd.close()
+    except IOError:
+        l = default
+    return l
+
+recording = {}
+def record(key, value):
+    global recording
+    try:
+        f = open('/run/gsm-state/.new.' + key, 'w')
+        f.write(value)
+        f.close()
+        os.rename('/run/gsm-state/.new.' + key,
+                  '/run/gsm-state/' + key)
+    except OSError:
+        # I got this once on the rename, don't know why
+        pass
+    recording[key] = value
+
+def recall(key, nofile = ""):
+    return safe_read("/run/gsm-state/" + key, nofile)
+
+lastlog={}
+def call_log(key, msg):
+    f = open('/var/log/' + key, 'a')
+    now = time.strftime("%Y-%m-%d %H:%M:%S")
+    f.write(now + ' ' + msg + "\n")
+    f.close()
+    lastlog[key] = msg
+
+def call_log_end(key):
+    if key in lastlog:
+        call_log(key, '-end-')
+        del lastlog[key]
+
+def set_alert(key, value):
+    path = '/run/alert/' + key
+    if value == None:
+        try:
+            os.unlink(path)
+        except OSError:
+            pass
+    else:
+        try:
+            f = open(path, 'w')
+            f.write(value)
+            f.close()
+        except IOError:
+            pass
+        suspend.abort_cycle()
+
+def gpio_set(line, val):
+    file = "/sys/class/gpio/gpio%d/value" % line
+    try:
+        fd = open(file, "w")
+        fd.write("%d\n" % val)
+        fd.close()
+    except IOError:
+        pass
+
+##
+# suspend handling:
+# There are three ways we interact with suspend
+#  1/ we block suspend when something important is happening:
+#    - phone call active
+#    - during initialisation?
+#    - AT command with timeout longer than 10 seconds.
+#  2/ on suspend request we performs some checks before allowing the suspend,
+#    and send notificaitons  on resume
+#    - If an AT command or async is pending, we don't ack the suspend request
+#      until a reply comes.
+#  3/ Some timeouts can wake up from suspend - e.g. CFUN poll.
+#
+# Each Engine can individually block suspend.  The number that are
+# active is maintained separately and suspend is blocked when that number
+# is positive.  On turn-off, everything is forced to allow suspend.
+# When suspend is pending, "set_suspend" is called on each engine.
+# An engine an return False to say that it doesn't want to suspend just now.
+# It should have started blocking, or signalled something.
+# Engines are informed for Resume when it happens.
+#
+# A 'retry' can have a non-resuming timeout and a resuming timeout.
+# The resuming timeout should be set while suspend is blocked, but can be
+# extended at other times.
+#
+# Important things should happen with a clear chain of suspend blocking.
+# e.g. The 'incoming' must be checked in the presuspend handler and create a
+# suspend block if needed.  That should be retained until txt messages are read
+# or phone call completes.
+# CFUN resuming timeout should block suspend until an answer is read and it is
+# rescheduled.
+# On startup we block until everyone has registerd the power-on. etc.
+#
+# - DONE flightmode
+# - incoming
+#   SMS
+#   CALL
+# - CFUN timer
+
+# - I keep getting EOF - why is that?
+# - double close is bad
+# - modem delaying of suspend doesn't quite seem right.
+
+
+class SuspendMan():
+    def __init__(self):
+        self.handle = suspend.blocker(False)
+        self.count = 0;
+    def block(self):
+        if self.count == 0:
+            self.handle.block()
+            self.handle.abort()
+        self.count += 1
+    def unblock(self):
+        self.count -= 1
+        if self.count == 0:
+            self.handle.unblock()
+sus = SuspendMan()
+
+class Engine:
+    def __init__(self):
+        self.timer = None
+        self.wakealarm = None
+        # delay is the default timeout for 'retry'
+        self.delay = 60000
+        # resuming_delay is the default timeout if we suspend
+        self.resuming_delay = None
+        # 'blocked' if true if we asked to block suspend.
+        self.blocked = False
+        # 'blocking' is a handle if we refused to let suspend continue yet.
+        self.blocking = None
+
+    def set_on(self, state):
+        pass
+    def set_service(self, state):
+        pass
+    def set_resume(self):
+        pass
+    def set_suspend(self):
+        return True
+
+    def retry(self, delay = None, resuming = None):
+        if self.timer:
+            gobject.source_remove(self.timer)
+            self.timer = None
+        if not delay is False:
+            if delay == None:
+                delay = self.delay
+            self.timer = gobject.timeout_add(delay, self.call_retry)
+        if not resuming is False:
+            if resuming == None:
+                resuming = self.resuming_delay
+            if resuming != None:
+                when = time.time() + resuming
+                if self.wakealarm and not self.wakealarm.s:
+                    self.wakealarm = None
+                if self.wakealarm:
+                    self.wakealarm.realarm(when)
+                else:
+                    self.wakealarm = wakealarm.wakealarm(when,
+                                                         self.wake_retry)
+        elif self.wakealarm:
+            self.wakealarm.release()
+            self.wakealarm = None
+
+    def wake_retry(self, handle):
+        self.wakealarm = None
+        self.do_retry()
+        return True
+
+    def call_retry(self):
+        self.timer = None
+        self.do_retry()
+        # Must be manually reset
+        return False
+
+    def block(self):
+        if not self.blocked:
+            global sus
+            self.blocked = True
+            sus.block()
+            if self.blocking:
+                b = self.blocking
+                self.blocking = None
+                b.release()
+    def unblock(self):
+        if self.blocked:
+            global sus
+            self.blocked = False
+            sus.unblock()
+        if self.blocking:
+            b = self.blocking
+            self.blocking = None
+            b.release()
+
+engines = []
+def add_engine(e):
+    global engines
+    engines.append(e)
+
+class state:
+    on = False
+    service = False
+    suspend = False
+state = state()
+
+def set_on(value):
+    global engines, state, sus
+    if state.on == value:
+        return
+    state.on = value
+    if not value:
+        sus.block()
+    for e in engines:
+        if not value:
+            e.retry(False)
+            e.unblock()
+        e.set_on(value)
+    if not value:
+        sus.unblock()
+
+def set_service(value):
+    global engines, state
+    if state.service == value:
+        return
+    state.service = value
+    for e in engines:
+        e.set_service(value)
+
+def set_suspend(blocker):
+    global engines, state, sus
+    if state.suspend:
+        return
+    if sus.count:
+        suspend.abort_cycle()
+        return
+    state.suspend = True
+    for e in engines:
+        blocker.block()
+        if e.set_suspend():
+            blocker.release()
+        else:
+            e.blocking = blocker
+
+def set_resume():
+    global engines, state
+    if not state.suspend:
+        return
+    state.suspend = False
+    for e in engines:
+        e.set_resume()
+
+watchers = {}
+def watch(dir, base, handle):
+    global watchers
+    if not dir in watchers:
+        watchers[dir] = dnotify.dir(dir)
+    watchers[dir].watch(base, lambda x: gobject.idle_add(handle, x))
+
+
+###
+# modem
+#  manage a channel or two, allowing requests to be
+#  queued and handling async notifications.
+#  Each request has text to send, a reply handler, and
+#  a timeout.  The reply handler indicates if more is expected.
+#  Queued message might be marked 'ignore in suspend'.
+#
+# when told to switch on, raise the GPIO, open the devices
+# send ATE0V1+CMEE=2;+CRC=1;+CMGF=0
+# Then process queue.
+# Every message that looks like it is async is handled as such
+# and may have follow-ons.
+# Every command should eventually be followed by OK or ERROR
+# or +CM[ES] ERROR  or timeout.
+# After a timeout we probe with 'AT' to get an 'OK'
+# If no response and close/open doesn't work we rmmod ehci_omap and
+# modprobe it again.
+#
+# When told to switch off, we drop the GPIO and queue a $QCPWRDN
+
+# When an engine schedules an 'at' command, it either will get at
+# least one callback, or will get 'set_on' to False, and then True
+
+
+class CarrierDetect(AtChannel):
+    # on the hso modem in the GTA04, the 'NO CARRIER' signal
+    # arrives on the 'Modem' port, not on the 'Application' port.
+    # So we listen to the 'Modem' port, and report any
+    # 'NO CARRIER' we see - or indeed anything that we see.
+    def __init__(self, path, main):
+        AtChannel.__init__(self, path = path)
+        self.main = main
+
+    def takeline(self, line):
+        self.main.takeline(line)
+
+class modem(Engine,AtChannel):
+    def __init__(self):
+        Engine.__init__(self)
+        AtChannel.__init__(self, "/dev/ttyHS_Application")
+        self.altchan = CarrierDetect("/dev/ttyHS_Modem", self)
+        self.queue = []
+        self.async = []
+        self.async_pending = None
+        self.pending_command = None
+        self.suspended = False
+        self.open_queued = False
+
+    def set_on(self, state):
+        if state:
+            self.open()
+        else:
+            self.queue = []
+            self.async_pending = None
+            self.pending_command = None
+            gpio_set(186, 0)
+            self.atcmd("$QCPWRDN")
+            self.close()
+
+    def set_suspend(self):
+        self.suspended = True
+        if self.pending_command or self.async_pending:
+            log("Modem delays suspend")
+            return False
+        log("Modem allows suspend")
+        #self.close()
+        return True
+
+    def set_resume(self):
+        log("modem resumes")
+        self.suspended = False
+        #self.reopen()
+        self.pending_command = self.ignore
+        self.atcmd('')
+
+    def close(self):
+        self.disconnect()
+        self.cancel_timeout()
+        self.altchan.disconnect()
+
+    def open(self):
+        sleep_time=0.4
+        self.block()
+        gpio_set(186, 1)
+        self.close()
+        self.timedout()
+        while not self.connected:
+            time.sleep(sleep_time)
+            sleep_time *= 2
+            if self.connect(15):
+                if self.altchan.connect(5):
+                    break
+            self.close()
+            gpio_set(186, 0)
+            Popen('rmmod ehci_omap; rmmod ehci-hcd; modprobe ehci-hcd; modprobe ehci_omap', shell=True).wait()
+            time.sleep(1)
+            gpio_set(186, 1)
+            time.sleep(1)
+        l = self.wait_line(100)
+        while l != None:
+            l = self.wait_line(100)
+        self.pending_command = self.ignore
+        self.open_queued = False
+        self.atcmd('V1E0+CMEE=2;+CRC=1;+CMGF=0')
+
+    def reopen(self):
+        if not self.open_queued:
+            self.open_queued = True
+            gobject.idle_add(self.open)
+
+    def unblock(self):
+        if self.open_queued or self.pending_command or self.queue or self.async_pending:
+            print "cannot unblock:",self.open_queued, self.pending_command, self.queue, self.async_pending, self.suspended
+            return
+        print "modem unblock"
+        Engine.unblock(self)
+
+
+    def takeline(self, line):
+        if line == "":
+            # Just an extra '\r', ignore it.
+            return False
+        # Could be:
+        #  async message
+        #  async continuation
+        #  reply for recent command
+        #  final OK/ERR for recent command
+        #  error
+        if line == None:
+            self.reopen()
+            return False
+        if self.async_pending:
+            if self.async_pending(line):
+                return False
+            self.async_pending = None
+        else:
+            if self.async_match(line):
+                self.unblock()
+                return self.async_pending == None
+            if self.pending_command:
+                if not self.pending_command(line):
+                    self.pending_command = None
+                if re.match('^(OK|\+CM[ES] ERROR|ERROR)', line):
+                    self.pending_command = None
+        if self.pending_command:
+            return False
+        gobject.idle_add(self.check_queue)
+        return True
+
+    def timedout(self):
+        if self.pending_command:
+            self.pending_command(None)
+            self.pending_command = None
+        if self.async_pending:
+            self.async_pending(None)
+            self.async_pending = None
+        self.pending_command = self.probe
+        if self.connected:
+            self.atcmd('', 10000)
+
+    def probe(self, line):
+        if line == "OK":
+            return False
+        # timeout
+        self.reopen()
+
+    def check_queue(self):
+        if not self.queue or self.pending_command or self.async_pending:
+            self.unblock()
+            return
+        if not self.connected:
+            return
+        if self.suspended:
+            return
+        cmd, cb, timeout = self.queue.pop()
+        if not cb:
+            cb = self.ignore
+        self.pending_command = cb
+        self.atcmd(cmd, timeout)
+
+    def ignore(self, line):
+        ## assume more to come until we see OK or ERROR
+        return True
+
+    def at_queue(self, cmd, handle, timeout):
+        self.block()
+        self.queue.append((cmd, handle, timeout))
+        gobject.idle_add(self.check_queue)
+
+    def clear_queue(self):
+        while self.queue:
+            cmd, cb, timeout = self.queue.pop()
+            if cb:
+                cb(None)
+
+    def async_match(self, line):
+        for prefix, handle, extra in self.async:
+            if line[:len(prefix)] == prefix:
+                # found
+                if handle(line):
+                    self.async_pending = extra
+                    if extra:
+                        self.set_timeout(1000)
+                    return True
+        return False
+
+    def request_async(self, prefix, handle, extra):
+        self.async.append((prefix, handle, extra))
+
+
+mdm = modem()
+add_engine(mdm)
+def request_async(prefix, handle, extras = None):
+    """ 'handle' should return True for a real match,
+    False if it was a false positive.
+    'extras' should return True if more is expected, or
+    False if there are no more async extras
+    """
+    global mdm
+    mdm.request_async(prefix, handle, extras)
+
+def at_queue(cmd, handle, timeout = 5000):
+    global mdm
+    mdm.at_queue(cmd, handle, timeout)
+
+###
+# flight
+#  monitor the 'flightmode' file.  Powers the modem
+#  on or off.  Reports off or on to all handlers
+#  uses CFUN and PWRDN commands
+class flight(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        watch('/var/lib/misc/flightmode','active', self.check)
+        gobject.idle_add(self.check)
+
+    def check(self, f = None):
+        self.block()
+        l = safe_read('/var/lib/misc/flightmode/active')
+        gobject.idle_add(self.turn_on, len(l) == 0)
+
+    def turn_on(self, state):
+        set_on(state)
+        self.unblock()
+
+    def set_suspend(self):
+        global state
+        l = safe_read('/var/lib/misc/flightmode/active')
+        if len(l) == 0 and not state.on:
+            self.block()
+            gobject.idle_add(self.turn_on, True)
+        elif len(l) > 0 and state.on:
+            self.block()
+            gobject.idle_add(self.turn_on, False)
+        return True
+
+add_engine(flight())
+
+###
+# register
+#  transitions from 'on' to 'service' and reports
+#  'no-service' when 'off' or no signal.
+#  +CFUN=1  - turns on
+#  +COPS=0  - auto select
+#  +COPS=1,2,50502 - select specific (2 == numeric)
+#  +COPS=3,1 - report format is long (1 == long)
+#  +COPS=4,2,50502 - select specific with auto-fallback
+#  http://www.shapeshifter.se/2008/04/30/list-of-at-commands/
+class register(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.resuming_delay = 300
+
+    def set_on(self, state):
+        if state:
+            self.retry(0)
+        else:
+            set_service(False)
+
+    def do_retry(self):
+        at_queue('+CFUN?', self.gotline, 10000)
+
+    def wake_retry(self, handle):
+        log("Woke!")
+        self.block()
+        at_queue('+CFUN?', self.got_wake_line, 8000)
+        return True
+
+    def got_wake_line(self, line):
+        log("CFUN wake for %s" % line)
+        self.gotline(line)
+        return False
+
+    def gotline(self, line):
+        if not line:
+            print "retry 1000 gotline not line"
+            self.retry(1000)
+            self.unblock()
+            return False
+        m = re.match('\+CFUN: (\d)', line)
+        if m:
+            n = m.group(1)
+            if n == '0' or n == '4':
+                self.block()
+                at_queue('+CFUN=1', self.did_set, 10000)
+                return False
+            if n == '6':
+                global mdm
+                self.block()
+                mdm.reopen()
+                self.do_retry()
+            if n == '1':
+                set_service(True)
+        print "retry end gotline"
+        self.retry()
+        self.unblock()
+        return False
+    def did_set(self, line):
+        print "retry 100 did_set"
+        self.retry(100)
+        self.unblock()
+        return False
+
+add_engine(register())
+###
+# signal
+#  While there is service, monitor signal strength.
+class signal(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        request_async('_OSIGQ:', self.get_async)
+        self.delay = 120000
+        self.zero_count = 0
+
+    def set_service(self, state):
+        if state:
+            at_queue('_OSQI=1', None)
+        else:
+            record('signal_strength', '-/32')
+        self.retry()
+
+    def get_async(self, line):
+        m = re.match('_OSIGQ: ([0-9]+),([0-9]+)', line)
+        if m:
+            self.set(m.group(1))
+            return True
+        return False
+
+    def do_retry(self):
+        at_queue('+CSQ', self.get_csq)
+
+    def get_csq(self, line):
+        self.retry()
+        if not line:
+            return False
+        m = re.match('\+CSQ: ([0-9]+),([0-9]+)', line)
+        if m:
+            self.set(m.group(1))
+        return False
+    def set(self, strength):
+        record('signal_strength', '%s/32'%strength)
+        if strength == '0':
+            self.zero_count += 1
+            self.delay = 5000
+        else:
+            self.zero_count = 0
+            self.delay = 120000
+        if self.zero_count > 10:
+            set_service(False)
+        self.retry()
+
+add_engine(signal())
+
+###
+# suspend
+# There are three ways we interact with suspend
+#  1/ we block suspend when something important is happening:
+#    - any AT commands pending or active
+#    - phone call active
+#    - during initialisation?
+#  2/ on suspend request we performs some checks before allowing the suspend,
+#    and send notificaitons  on resume
+#  3/ Some timeouts can wake up from suspend - e.g. CFUN poll.
+#  When a suspend is pending, check call state and
+#  sms gpio state and possibly abort.
+
+class Blocker():
+    """initialise a counter to '1' and when it hits zero
+    call the callback
+    """
+    def __init__(self, cb):
+        self.cb = cb
+        self.count = 1
+    def block(self):
+        self.count += 1
+    def release(self):
+        self.count -= 1
+        if self.count == 0:
+            self.cb()
+
+class suspender(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.mon = suspend.monitor(self.want_suspend, self.resuming)
+
+    def want_suspend(self):
+        b = Blocker(lambda : self.mon.release())
+        set_suspend(b)
+        b.release()
+        return False
+
+    def resuming(self):
+        gobject.idle_add(set_resume)
+
+add_engine(suspender())
+###
+# location
+#  when service, monitor cellid etc.
+class Cellid(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        request_async('+CREG:', self.async)
+        request_async('+CBM:', self.cellname, extras=self.the_name)
+        self.delay = 60000
+        self.newname = ''
+        self.cellnames = {}
+        self.lac = None
+
+    def set_on(self, state):
+        if state:
+            self.retry(100)
+
+    def set_resume(self):
+        # might have moved while we slept
+        self.retry(0)
+
+    def set_service(self, state):
+        if not state:
+            record('cell', '-')
+            record('cellid','')
+            record('sid','')
+            record('carrier','-')
+
+    def do_retry(self):
+        at_queue('+CREG?', self.got, 5000)
+
+    def got(self, line):
+        self.retry()
+        if not line:
+            return False
+        if line[:9] == '+CREG: 0,':
+            at_queue('+CREG=2', None)
+        m = re.match('\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', line)
+        if m:
+            self.record(m)
+        return False
+
+    def async(self, line):
+        m = re.match('\+CREG: ([012])(,"([^"]*)","([^"]*)")?$', line)
+        if m:
+            if m.group(1) == '1' and m.group(2):
+                self.record(m)
+                self.retry()
+            if m.group(1) == '0':
+                self.retry(0)
+            return True
+        return False
+
+    def cellname(self, line):
+        # get something like +CBM: 1568,50,1,1,1
+        # don't know what that means, just collect the 'extra' line
+        # I think the '50' means 'this is a cell id'.  I should
+        # probably test for that.
+        # Subsequent lines are the cell name.
+        m = re.match('\+CBM: \d+,\d+,\d+,\d+,\d+', line)
+        if m:
+            #ignore CBM content for now.
+            self.newname = ''
+            return True
+        return False
+
+    def the_name(self, line):
+        if not line:
+            if not self.newname:
+                return
+            l = re.sub('[^!-~]+',' ', self.newname)
+            if self.cellid:
+                self.names[self.cellid] = l
+            record('cell', l)
+            return False
+        if self.newname:
+            self.newname += ' '
+        self.newname += line
+        return True
+
+
+    def record(self, m):
+        if m.groups()[3] != None:
+            lac = int(m.group(3), 16)
+            cellid = int(m.group(4), 16)
+            record('cellid', '%04X %06X' % (lac, cellid))
+            self.cellid = cellid;
+            if cellid in self.cellnames:
+                record('cell', self.cellnames[cellid])
+            if lac != self.lac:
+                self.lac = lac
+                # check we still have correct carrier
+                at_queue('_OSIMOP', self.got_carrier)
+                # Make sure we are getting async cell notifications
+                at_queue('+CSCB=1', None)
+    def got_carrier(self, line):
+        #_OSIMOP: "YES OPTUS","YES OPTUS","50502"
+        if not line:
+            return False
+        m = re.match('_OSIMOP: "(.*)",".*","(.*)"', line)
+        if m:
+            record('sid', m.group(2))
+            record('carrier', m.group(1))
+        return False
+
+add_engine(Cellid())
+
+
+###
+# CIMI
+#  get CIMI once per 'on'
+class SIM_ID(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.CIMI = None
+
+    def set_on(self, state):
+        if state:
+            self.retry(100)
+        else:
+            self.CIMI = None
+            record('sim', '')
+
+    def got(self, line):
+        if line:
+            m = re.match('(\d\d\d+)', line)
+            if m:
+                self.CIMI = m.group(1)
+                record('sim', self.CIMI)
+                self.retry(False)
+                return False
+        if not self.CIMI:
+            self.retry(10000)
+        return False
+
+    def do_retry(self):
+        if not self.CIMI:
+            at_queue("+CIMI", self.got, 5000)
+
+add_engine(SIM_ID())
+###
+# protocol
+#  monitor 2g/3g protocol and select preferred
+# 0=only2g 1=only3g 2=prefer2g 3=prefer3g 4=staywhereyouare 5=auto
+# _OPSYS=%d,2
+class proto(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.confirmed = False
+        self.mode = 'x'
+        watch('/var/lib/misc/gsm','mode', self.update)
+
+    def update(self, f):
+        global state
+        self.set_on(state.service)
+
+    def set_service(self, state):
+        if not state:
+            self.confirmed = False
+        if state:
+            self.check()
+
+    def check(self):
+        l = safe_read("/var/lib/misc/gsm/mode", "3")
+        if len(l) and l[0] in "012345":
+            if self.mode != l[0]:
+                self.mode = l[0]
+                self.confirmed = False
+        self.do_retry()
+
+    def do_retry(self):
+        if self.confirmed:
+            return
+        at_queue("_OPSYS=%s,2" % self.mode, self.got,  5000)
+
+    def got(self, line):
+        if line == "OK":
+            self.confirmed = True;
+        self.do_retry()
+        return False
+
+add_engine(proto())
+
+###
+# data
+# async _OWANCALL
+# _OWANDATA _OWANCALL +CGDCONT
+#
+# if /run/gsm-state/data-APN contains an APN, make a data call
+# else hangup any data.
+# Data call involves
+#    +CGDCONT=1,"IP","$APN"
+#    _OWANCALL=1,1,0
+# We then poll _OWANCALL?, get _OWANCALL: (\d), (\d)
+# first number is '1' for us, second is
+#     0 = Disconnected, 1 = Connected, 2 = In setup,  3 = Call setup failed.
+# once connected, _OWANDATA? results in
+#  _OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$'
+#  IP DNS1 DNS2
+# e.g.
+#_OWANDATA: 1, 115.70.17.232, 0.0.0.0, 58.96.1.28, 220.233.0.4, 0.0.0.0, 0.0.0.0,144000
+#  IP is stored in 'data' and used with 'ifconfig'
+# DNS are stored in 'dns'
+class data(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.apn = None
+        self.dns = None
+        self.ip = None
+        self.retry_state = ''
+        self.last_data_usage = None
+        self.last_data_time = time.time()
+        request_async('_OWANCALL:', self.call_status)
+        watch('/run/gsm-state', 'data-APN', self.check_apn)
+
+    def set_on(self, state):
+        if not state:
+            self.apn = None
+            self.hangup()
+
+    def set_service(self, state):
+        if state:
+            self.check_apn(None)
+        else:
+            self.hangup()
+
+    def check_apn(self, f):
+        global state
+        l = recall('data-APN')
+        if l == '':
+            l = None
+        if not state.service:
+            l = None
+
+        if self.apn != l:
+            self.apn = l
+            if self.ip:
+                self.hangup()
+                at_queue('_OWANCALL=1,0,0', None)
+
+            if self.apn:
+                self.connect()
+
+    def hangup(self):
+        if self.ip:
+            os.system('/sbin/ifconfig hso0 0.0.0.0 down')
+        record('dns', '')
+        record('data', '')
+        self.ip = None
+        self.dns = None
+        if self.apn:
+            self.retry_state = 'reconnect'
+        else:
+            self.retry_state = ''
+        self.do_retry()
+
+    def connect(self):
+        if not self.apn:
+            return
+        # reply to +CGDCONT isn't interesting, and reply to
+        # _OWANCALL is handle by async handler.
+        at_queue('+CGDCONT=1,"IP","%s"' % self.apn, None)
+        at_queue('_OWANCALL=1,1,0', None)
+        self.retry_state = 'calling'
+        self.delay = 2000
+        self.retry()
+
+    def do_retry(self):
+        if self.retry_state == 'calling':
+            at_queue('_OWANCALL?', None)
+            return
+        if self.retry_state == 'reconnect':
+            self.connect()
+        if self.retry_state == 'connected':
+            self.check_connect()
+
+    def check_connect(self):
+        self.retry_state = 'connected'
+        self.delay = 60000
+        self.retry()
+        at_queue('_OWANDATA=1', self.connect_data)
+
+    def connect_data(self, line):
+        m = re.match('_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+, \d+$', line)
+        if m:
+            dns = (m.group(2), m.group(3))
+            if self.dns != dns:
+                record('dns', '%s %s' % dns)
+                self.dns = dns
+            ip = m.group(1)
+            if self.ip != ip:
+                self.ip = ip
+                os.system('/sbin/ifconfig hso0 up %s' % ip)
+                record('data', ip)
+            self.retry()
+        if line == 'ERROR':
+            self.hangup()
+        return False
+
+    def call_status(self, line):
+        m = re.match('_OWANCALL: (\d+), (\d+)', line)
+        if not m:
+            return False
+        if m.group(1) != '1':
+            return True
+        s = int(m.group(2))
+        if s == 0:
+            # disconnected
+            self.hangup()
+        if s == 1:
+            # connected
+            self.check_connect()
+        if s == 2:
+            # in setup
+            self.retry()
+        if s == 3:
+            # call setup failed
+            self.apn = None
+            self.hangup()
+        return True
+
+    def log_update(self, force = False):
+        global recording
+        if 'sim' in recording and recording['sim']:
+            sim = recording['sim']
+        else:
+            sim = 'unknown'
+
+        data_usage = self.last_data_usage
+        data_time = self.last_data_time
+        self.usage_update()
+        if not data_usage or (not force and
+                              self.last_data_time - data_time < 10 * 60):
+            return
+        call_log('gsm-data', '%s %d %d' % (
+                sim,
+                data_usage[0] - self.last_data_usage[0],
+                data_usage[1] - self.last_data_usage[1]))
+
+    def usage_update(self):
+        self.last_data_usage = self.read_iface_usage()
+        self.last_data_time = time.time()
+        # data-last-usage allows app to add instantaneous current usage to
+        # value from logs
+        record('data-last-usage', '%s %s' % last_data_usage)
+
+
+add_engine(data())
+
+
+###
+# config
+# Make sure CMGF CNMI etc all happen once per 'service'.
+# +CNMI=1,1,2,0,0 +CLIP?
+# +COPS
+
+class config(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+    def set_service(self, state):
+        if state:
+            at_queue('+CLIP=1', None)
+            at_queue('+CNMI=1,1,2,0,0', None)
+
+add_engine(config())
+
+###
+# call
+#   ????
+# async +CRING RING +CLIP "NO CARRIER" "BUSY"
+# +CPAS
+# A   D
+# +VTS
+# +CHUP
+#
+# If we get '+CRING' or 'RING' we alert a call:
+#     record number to 'incoming', INCOMING to status and alert 'ring'
+#     and log the call
+# If we get +CLIP:, record and log the call detail
+# If we get 'NO CARRIER', clear 'status' and 'call'
+# If we get 'BUSY' clear 'call' and record status==BUSY
+#
+# Files to watch:
+#  'call' :might be 'answer', or a number or empty, to hang up
+#  'dtmf' : clear file and send DTMF tones
+#
+# Files we report:
+#  'incoming' is "-" for private, or "number" of incoming (or empty)
+#  'status' is "INCOMING" or 'BUSY' or 'on-call' (or empty)
+#
+# While 'on-call' we poll with +CPAS
+# 0=ready 1=unavailable 2=unknown 3=ringing 4=call-in-progress 5=asleep
+# need 4 '0' in a row before assume hang-up
+# Could use AT+CLCC ??
+# ringing:
+#    +CLCC: 1,1,4,0,0,"0403463349",128
+# answered:
+#    +CLCC: 1,1,0,0,0,"0403463349",128
+# outgoing calling:
+#    +CLCC: 1,0,3,0,0,"0403463349",129
+# outgoing, got hangup
+#    +CLCC: 1,0,0,0,0,"0403463349",129
+#
+# Transitions are:
+#   Call :  idle -> active
+#   hangup: active,incoming,busy -> idle
+#   ring:   idle -> incoming
+#   answer: incoming -> active
+#   BUSY:   active -> busy
+#
+#
+
+class voice(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.state = 'idle'
+        self.number = None
+        self.zero_cnt = 0
+        self.router = None
+        request_async('+CRING', self.incoming)
+        request_async('RING', self.incoming)
+        request_async('+CLIP:', self.incoming_number)
+        request_async('NO CARRIER', self.hangup)
+        request_async('BUSY', self.busy)
+        watch('/run/gsm-state', 'call', self.check_call)
+        watch('/run/gsm-state', 'dtmf', self.check_dtmf)
+        self.f = EvDev('/dev/input/incoming', self.incoming_wake)
+
+    def set_on(self, state):
+        record('call', '')
+        record('dtmf', '')
+        record('incoming', '')
+        record('status', '')
+
+    def set_suspend(self):
+        self.f.read(None, None)
+        print "voice allows suspend"
+        return True
+
+    def incoming_wake(self, dc, mom, typ, code, val):
+        if typ == 1 and val == 1:
+            self.block()
+            self.zero_cnt = 0
+            self.retry(0)
+    def do_retry(self):
+        at_queue('+CPAS', self.get_activity)
+    def get_activity(self, line):
+        m = re.match('\+CPAS: (\d)', line)
+        if m:
+            n = m.group(1)
+            if n == '0':
+                self.zero_cnt += 1
+                if self.zero_cnt >= 4 or self.state == 'idle':
+                    self.to_idle()
+            else:
+                self.zero_cnt = 0
+            if n == '3':
+                if self.state != 'incoming':
+                    self.to_incoming()
+            self.retry()
+        return False
+
+    def incoming(self, line):
+        self.to_incoming()
+        return True
+    def incoming_number(self, line):
+        m = re.match('\+CLIP: "([^"]+)",[0-9,]*', line)
+        if m:
+            self.to_incoming(m.group(1))
+        return True
+    def hangup(self, line):
+        self.to_idle()
+        return True
+    def busy(self, line):
+        if self.state == 'active':
+            record('status', 'BUSY')
+        return True
+
+    def check_call(self, f):
+        l = recall('call')
+        if l == '':
+            self.to_idle()
+        elif l == 'answer':
+            if self.state == 'incoming':
+                at_queue('A', None)
+                set_alert('ring', None)
+                self.to_active()
+        elif self.state == 'idle':
+            call_log('outgoing', l)
+            at_queue('D%s;' % l, None)
+            self.to_active()
+
+    def check_dtmf(self, f):
+        l = recall('dtmf')
+        if l:
+            record('dtmf', '')
+        if self.state == 'active' and l:
+            at_queue('+VTS=%s' % l, None)
+
+    def to_idle(self):
+        if self.state == 'incoming' or self.state == 'active':
+            call_log_end('incoming')
+            call_log_end('outgoing')
+        if self.state != 'idle':
+            at_queue('+CHUP', None)
+            record('incoming', '')
+            record('status', '')
+            self.state = 'idle'
+        if self.router:
+            self.router.send_signal(15)
+            self.router.wait()
+            self.router = None
+            try:
+                os.unlink('/run/sound/00-voicecall')
+            except OSError:
+                pass
+        self.number = None
+        self.delay = 30000
+        self.retry()
+        self.unblock()
+
+    def to_incoming(self, number = None):
+        self.block()
+        n = '-call-'
+        if number:
+            n = number
+        elif self.number:
+            n = self.number
+        if self.state != 'incoming' or (number and not self.number):
+            call_log('incoming', n)
+        if number:
+            self.number = number
+        record('incoming', n)
+        record('status', 'INCOMING')
+        set_alert('ring','new')
+        self.delay = 500
+        self.state = 'incoming'
+        self.retry()
+
+    def to_active(self):
+        if not self.router:
+            try:
+                open('/run/sound/00-voicecall','w').close()
+            except:
+                pass
+            self.router = Popen('/usr/local/bin/gsm-voice-routing',
+                                close_fds = True)
+        record('status', 'on-call')
+        self.state = 'active'
+        self.delay = 1500
+        self.retry()
+
+add_engine(voice())
+
+###
+# ussd
+# async +CUSD
+
+###
+# sms_recv
+# async +CMTI
+#
+# If we receive +CMTI, or a signal on /dev/input/incoming, then
+# we +CMGL=4 and collect messages an add them to the sms database
+class sms_recv(Engine):
+    def __init__(self):
+        Engine.__init__(self)
+        self.check_needed = True
+        request_async('+CMTI', self.must_check)
+        self.f = EvDev("/dev/input/incoming", self.incoming)
+        self.expecting_line = False
+        self.messages = {}
+        self.to_delete = []
+
+    def set_suspend(self):
+        self.f.read(None, None)
+        return True
+
+    def incoming(self, dc, mom, typ, code, val):
+        if typ == 1 and val == 1:
+            self.must_check('')
+
+    def must_check(self, line):
+        self.block()
+        self.check_needed = True
+        self.retry(100)
+        return True
+
+    def set_on(self, state):
+        if state and self.check_needed:
+            self.block()
+            self.retry(100)
+        if not state:
+            self.unblock()
+
+    def do_retry(self):
+        if not self.check_needed:
+            if not self.to_delete:
+                return
+            t = self.to_delete[0]
+            self.to_delete = self.to_delete[1:]
+            at_queue('+CMGD=%s' % t, self.did_delete)
+            return
+        global recording
+        if 'sim' not in recording or not recording['sim']:
+            self.retry(10000)
+            return
+        self.messages = {}
+        # must not check when there is an incoming call
+        at_queue('+CPAS', self.cpas)
+
+    def cpas(self, line):
+        m = re.match('\+CPAS: (\d)', line)
+        if m and m.group(1) == '3':
+            self.retry(2000)
+            return False
+        at_queue('+CMGL=4', self.one_line, 40000)
+        return False
+
+    def did_delete(self, line):
+        if line != 'OK':
+            return False
+        self.retry(1)
+        return False
+
+    def one_line(self, line):
+        if line == 'OK':
+            global recording
+            self.check_needed = False
+            found, res = storesms.sms_update(self.messages, recording['sim'])
+            if res != None and len(res) > 10:
+                self.to_delete = res[:-10]
+                self.retry(1)
+            if found:
+                set_alert('sms','new')
+            self.unblock()
+            return False
+        if not line or line[:5] == 'ERROR' or line[:10] == '+CMS ERROR':
+            self.check_needed = True
+            self.retry(60000)
+            return False
+        if self.expecting_line:
+            self.expecting_line = False
+            if self.designation != '0' and self.designation != '1':
+                # send, not recv !!
+                return True
+            if len(line) < self.msg_len:
+                return True
+            sender, date, ref, part, txt = sms.extract(line)
+            self.messages[self.index] = (sender, date, txt, ref, part)
+            return True
+        m = re.match('^\+CMGL: (\d+),(\d+),("[^"]*")?,(\d+)$', line)
+        if m:
+            self.expecting_line = True
+            self.index = m.group(1)
+            self.designation = m.group(2)
+            self.msg_len = int(m.group(4), 10)
+        return True
+
+
+add_engine(sms_recv())
+
+
+###
+# sms_send
+
+
+c = gobject.main_context_default()
+while True:
+    c.iteration()