--- /dev/null
+#!/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()