From: NeilBrown Date: Fri, 13 Dec 2013 09:30:58 +0000 (+1100) Subject: New gsmd2 X-Git-Url: http://git.neil.brown.name/?a=commitdiff_plain;h=1f7e648d1a7b95cd08b3ed451583a9018d0fd79f;p=plato.git New gsmd2 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. --- diff --git a/gsm/gsmd2.py b/gsm/gsmd2.py new file mode 100644 index 0000000..5683eeb --- /dev/null +++ b/gsm/gsmd2.py @@ -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()