From: NeilBrown Date: Sun, 6 Feb 2011 09:15:45 +0000 (+1100) Subject: gsm: various updates and additions. X-Git-Url: http://git.neil.brown.name/?a=commitdiff_plain;h=fb15c7bcb546478f0f15f6bd75a30cd98a45b31c;p=freerunner.git gsm: various updates and additions. Signed-off-by: NeilBrown --- diff --git a/gsm/atchan.py b/gsm/atchan.py index cb40dc1..88e5285 100644 --- a/gsm/atchan.py +++ b/gsm/atchan.py @@ -13,7 +13,7 @@ # This is usually subclassed by code with an agenda. import gobject, sys, os, time -from trace import log +from tracing import log from socket import * class AtChannel: @@ -44,6 +44,7 @@ class AtChannel: log("connect to", self.path) s = socket(AF_UNIX, SOCK_STREAM) s.connect(self.path) + s.setblocking(0) self.watcher = gobject.io_add_watch(s, gobject.IO_IN, self.readdata) self.sock = s self.connected = True @@ -56,11 +57,22 @@ class AtChannel: log("send command", str) if not self.command_pending: self.command_pending = str - self.sock.sendall(str + '\n') - self.set_timeout(30000) + try: + self.sock.sendall(str + '\n') + except error: + self.set_timeout(10) + else: + self.set_timeout(30000) def readdata(self, io, arg): - r = self.sock.recv(1000) + try: + r = self.sock.recv(1000) + except error: + # no data there really. + return True + if not r: + # pipe closed + return False r = self.buf + r ra = r.split('\n') self.buf = ra[-1]; @@ -68,6 +80,10 @@ class AtChannel: for ln in ra: ln = ln.strip('\r') self.getline(ln) + # FIXME this should be configurable + if self.buf == '> ': + self.getline(self.buf) + self.buf = '' return True def getline(self, line): @@ -137,7 +153,11 @@ class AtChannel: """ self.set_timeout(timeout) log("send AT command", cmd, timeout) - self.sock.sendall('AT' + cmd + '\r') + try: + self.sock.sendall('AT' + cmd + '\r') + except error: + self.cancel_timeout() + self.set_timeout(10) def timer_fired(self): log("Timer Fired") @@ -170,9 +190,79 @@ class AtChannel: def wait_line(self, timeout): self.cancel_timeout() + self.set_timeout(timeout) c = gobject.main_context_default() - while not self.linelist: + while not self.linelist and self.pending: c.iteration() - l = self.linelist[0] - del self.linelist[0] - return l + if self.linelist: + self.cancel_timeout() + l = self.linelist[0] + del self.linelist[0] + return l + else: + return None + def timedout(self): + pass + + + def chat(self, mesg, resp, timeout = 1000): + """ + Send the message (if not 'None') and wait up to + 'timeout' for one of the responses (regexp) + Return None on timeout, or number of response. + combined with an array of the messages received. + """ + if mesg: + log("send command", mesg) + try: + self.sock.sendall(mesg + '\r\n') + except error: + timeout = 10 + + conv = [] + while True: + l = self.wait_line(timeout) + if l == None: + return (None, conv) + conv.append(l) + for i in range(len(resp)): + ptn = resp[i] + if type(ptn) == str: + if ptn == l.strip(): + return (i, conv) + else: + if resp[i].match(l): + return (i, conv) + + def chat1(self, mesg, resp, timeout=1000): + n,c = self.chat(mesg, resp, timeout = timeout) + return n + + def cmdchat(self, mesg): + self.command(mesg) + conv = [] + while self.command_pending: + c = gobject.main_context_default() + while not self.linelist and self.pending: + c.iteration() + if self.linelist: + l = self.linelist[0] + del self.linelist[0] + conv.append(l) + return conv + + +def found(list, patn): + """ + see if patn can be found in the list of strings + """ + for l in list: + l = l.strip() + if type(patn) == str: + if l == patn: + return True + else: + if patn.match(l): + return True + return False + diff --git a/gsm/gsm-getsms.py b/gsm/gsm-getsms.py new file mode 100644 index 0000000..48a0790 --- /dev/null +++ b/gsm/gsm-getsms.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python + +# Collect SMS messages from the GSM device. +# We store a list of messages that are thought to be +# in the SIM card: number from date +# e.g. 17 61403xxxxxx 09/02/17,20:28:36+44 +# As we read messages, if we find one that is not in that list, +# we record it in the SMS store, then update the list +# +# An option can specify either 'new' or 'all. +# 'new' will only ask for 'REC UNREAD' and so will be faster and so +# is appropriate when we know that a new message has arrived. +# 'all' reads all messages and so is appropriate for an occasional +# 'sync' like when first turning the phone on. +# +# If we discover that the SMS card is more than half full, we +# deleted the oldest messages. +# We discover this by 'all' finding lots of messages, or 'new' +# finding a message with a high index. +# For now, we "know" that the SIM card can hold 30 messages. +# +# We need to be careful about long messages. A multi-part message +# looks like e.g. +#+CMGL: 19,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:46+44",145,140 +#0500031C0201A8E8F41C949E83C2207B599E07B1DFEE33A85D9ECFC3E732888E0ED34165FCB85C26CF41747419344FBBCFEC32A85D9ECFC3E732889D6EA7E9A0F91B444787E9A024681C7683E6E532E88E0ED341E939485E1E97D3F6321914A683E8E832E84D4797E5A0B29B0C7ABB41ED3CC8282F9741F2BADB5D96BB40D7329B0D9AD3D36C36887E2FBBE9 +#+CMGL: 20,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:47+44",145,30 +#0500031C0202F2A0B71C347F83D8653ABD2C9F83E86FD0F90D72B95C2E17 + +# If that was just hex you could use +# perl -e 'print pack("H*","050....")' +# to print it.. but no... +# Looks like it decodes as: +# 05 - length of header, not including this byte +# 00 - concatentated SMS with 8 bit ref number (08 means 16 bit ref number) +# 03 - length of rest of header +# 1C - ref number for this concat-SMS +# 02 - number of parts in this SMS +# 01 - number of this part - counting starts from 1 +# A8E8F41C949E83C22.... message, 7 bits per char. so: +# A8 - 54 *2 + 0 54 == T 1010100 0 1010100 +# 0E8 - 68 *2 + 0 68 == h 1 1101000 1101000 +# F4 - 69 == i 11 110100 1101001 1 +# 1C 73 == s 000 11100 1110011 11 +# 94 20 == space 1001 0100 0100000 000 +# 9E 69 == i 10011 110 1101001 1001 +# 83 73 == s 100000 11 1110011 10011 +# 20 == space 0100000 0100000 + +# 153 characters in first message. 19*8 + 1 +# that uses 19*7+1 == 134 octets +# There are 6 in the header so a total of 140 +# second message has 27 letters - 3*8+3 +# That requires 3*7+3 == 24 octets. 30 with the 6 octet header. + +# then there are VCARD messages that look lie e.g. +#+CMGL: 2,"REC READ","61403xxxxxx",,"09/01/29,13:01:26+44",145,137 +#06050423F40000424547494E3A56434152440D0A56455253494F4E3A322E310D0A4E3A....0D0A454E443A56434152440D0A +#which is +#06050423F40000 +#then +#BEGIN:VCARD +#VERSION:2.1 +#N: ... +#... +#END:VCARD +# The 06050423f40000 +# might decode like: +# 06 - length of rest of header +# 05 - magic code meaning 'user data' +# 04 - length of rest of header... +# 23 - +# f4 - destination port '23f4' means 'vcard' +# 00 - +# 00 - 0000 is the origin port. +# +#in hex/ascii +# +# For now, ignore anything longer than the specified length. + + +import atchan, sys, re, os +from storesms import SMSmesg, SMSstore + + +def load_mirror(filename): + # load an array of index address date + # from the file and store in a hash + rv = {} + try: + f = file(filename) + except IOError: + return rv + l = f.readline() + while l: + fields = l.strip().split(None, 1) + rv[fields[0]] = fields[1] + l = f.readline() + return rv + +def save_mirror(filename, hash): + n = filename + '.new' + f = open(n, 'w') + for i in hash: + f.write(i + ' ' + hash[i] + '\n') + f.close() + os.rename(n, filename) + + +def sms_decode(msg): + #msg is a 7-in-8 encoding of a longer message. + pos = 0 + carry = 0 + str = '' + while msg: + c = msg[0:2] + msg = msg[2:] + b = int(c, 16) + + if pos == 0: + if carry: + str += chr(carry + (b&1)*64) + carry = 0 + b /= 2 + else: + b = (b << (pos-1)) | carry + carry = (b & 0xff80) >> 7 + b &= 0x7f + if (b & 0x7f) != 0: + str += chr(b&0x7f) + pos = (pos+1) % 7 + return str + +def main(): + mode = 'all' + for a in sys.argv[1:]: + if a == '-n': + mode = 'new' + else: + print "Unknown option:", a + sys.exit(1) + + pth = None + for p in ['/media/card','/media/disk','/var/tmp']: + if os.path.exists(os.path.join(p,'SMS')): + pth = p + break + + if not pth: + print "Cannot find SMS directory" + sys.exit(1) + + dir = os.path.join(pth, 'SMS') + + store = SMSstore(dir) + + chan = atchan.AtChannel() + chan.connect() + + if not atchan.found(chan.cmdchat('get_power'), 'OK on'): + sys.exit(1) + + if not atchan.found(chan.cmdchat('connect'), 'OK'): + sys.exit(1) + + chan.chat1('', ['OK', 'ERROR']); + if chan.chat1('ATE0', ['OK', 'ERROR']) != 0: + sys.exit(1) + + # get ID of SIM card + n,c = chan.chat('AT+CIMI', ['OK', 'ERROR']) + CIMI='unknown' + for l in c: + l = l.strip() + if re.match('^\d+$', l): + CIMI = l + + mfile = os.path.join(dir, '.sim-mirror-'+CIMI) + #FIXME lock mirror file + mirror = load_mirror(mfile) + mirror_changed = False + + chan.chat('AT+CMFG=1', ['OK','ERROR']) + if mode == 'new': + chan.atcmd('+CMGL="REC UNREAD"') + else: + chan.atcmd('+CMGL="ALL"') + + # reading the msg list might fail for some reason, so + # we always prime the mirror list with the previous version + # and only replace things, never assume they aren't there + # because we cannot see them + newmirror = mirror + + l = '' + state = 'waiting' + msg = '' + # indx state from name date type len + want = re.compile('^\+CMGL: (\d+),("[^"]*")?,("([^"]*)")?,("[^"]*")?,("([^"]*)")?,(\d+),(\d+)$') + + while state == 'reading' or not (l[0:2] == 'OK' or l[0:5] == 'ERROR' or + l[0:10] == '+CMS ERROR'): + l = chan.wait_line(1000) + if l == None: + sys.exit(1) + if state == 'reading': + if msg: + msg += '\n' + msg += l + if len(msg) >= msg_len: + state = 'waiting' + ref = None; part = None + if len(msg) > msg_len: + if msg[0:6] == '050003': + # concatenated message with 8bit ref number + ref = msg[6:8] + part = ( int(msg[10:12],16), int(msg[8:10], 16)) + msg = sms_decode(msg[12:]) + else: + print "ignoring", index, sender, date, msg + continue + print "found", index, sender, date, msg + if index in mirror and mirror[index] == date + ' ' + sender: + print "Already have that one" + else: + sms = SMSmesg(source='GSM', time=date, sender=sender, + text = msg, state = 'NEW', + ref= ref, part = part) + store.store(sms) + newmirror[index] = date + ' ' + sender + else: + m = want.match(l) + if m: + g = m.groups() + index = g[0] + sender = g[3] + date = g[6] + typ = int(g[7]) + if typ & 16: + sender = '+' + sender + msg_len = int(g[8]) + msg = '' + state = 'reading' + + mirror = newmirror + + if len(mirror) > 10: + rev = {} + dlist = [] + for i in mirror: + rev[mirror[i]] = i + dlist.append(mirror[i]) + dlist.sort() + for i in range(len(mirror) - 10): + dt=dlist[i] + ind = rev[dt] + resp = chan.chat1('AT+CMGD=%s' % ind, ['OK', 'ERROR', '+CMS ERROR'], + timeout=3000) + if resp == 0: + del mirror[ind] + + save_mirror(mfile, mirror) + +main() diff --git a/gsm/gsm-sms.py b/gsm/gsm-sms.py new file mode 100644 index 0000000..7a718a5 --- /dev/null +++ b/gsm/gsm-sms.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +# Send an SMS message using GSM. +# Args are: +# sender - ignored +# recipient - phone number +# message - no newlines +# +# We simply connect to the GSM module, +# check for a registration +# and +# AT+CMGS="recipient" +# > message +# > control-Z +# +# Sending multipart sms messages: +# ref: http://www.developershome.com/sms/cmgsCommand4.asp +# 1/ set PDU mode with AT+CMGF=0 +# 2/ split message into 153-char bundles +# 3/ create messages as follows: +# +# 00 - this says we aren't providing an SMSC number +# 41 - TPDU header - type is SMS-SUBMIT, user-data header present +# 00 - please assign a message reference number +# xx - length in digits of phone number +# 91 for IDD, 81 for "don't know what sort of number this is" +# 164030... BCD phone number, nibble-swapped, pad with F at end if needed +# 00 - protocol identifier?? +# 00 - encoding - 7 bit ascii +# XX - length of message in septets +# Then the message which starts with 7 septets of header that looks like 6 octets. +# 05 - length of rest of header +# 00 - multi-path with 1 byte id number +# 03 - length of rest +# idnumber - random id number +# parts - number of parts +# this part - this part, starts from '1' +# +# then AT+CMGS=len-of-TPDU in octets +# +# TPDU header byte: +# Structure: (n) = bits +# +--------+----------+---------+-------+-------+--------+ +# | RP (1) | UDHI (1) | SRI (1) | X (1) | X (1) | MTI(2) | +# +--------+----------+---------+-------+-------+--------+ +# RP: +# Reply path +# UDHI: +# User Data Header Indicator = Does the UD contains a header +# 0 : Only the Short Message +# 1 : Beginning of UD containsheader information +# SRI: +# Status Report Indication. +# The SME (Short Message Entity) has requested a status report. +# MTI: +# 00 for SMS-Deliver +# 01 for SMS-SUBMIT + + +import atchan, sys, re, random + +def encode_number(recipient): + # encoded number is + # number of digits + # 91 for international, 81 for local interpretation + # BCD digits, nibble-swapped, F padded. + if recipient[0] == '+': + type = '91' + recipient = recipient[1:] + else: + type = '81' + leng = '%02X' % len(recipient) + if len(recipient) % 2 == 1: + recipient += 'F' + swap = '' + while recipient: + swap += recipient[1] + recipient[0] + recipient = recipient[2:] + return leng + type + swap + +def code7(pad, mesg): + # Encode the message as 8 chars in 7 bytes. + # pad with 'pad' 0 bits at the start (low in the byte) + carry = 0 + # we have 'pad' low bits stored low in 'carry' + code = '' + while mesg: + c = ord(mesg[0]) + mesg = mesg[1:] + if pad + 7 >= 8: + # have a full byte + b = carry & ((1<> (7-pad) + else: + # not a full byte yet, just a septet + pad = 7 + carry = c + if pad: + # a few bits still to emit + b = carry & ((1<']) + if n == 2: + n,c = chan.chat('%s%s\r\n\032' % (dest, mesg), ['OK','ERROR'], 10000) + if n == 0 and atchan.found(c, re.compile('^\+CMGS: \d+') ): + return True + return False + +recipient = sys.argv[2] +mesg = sys.argv[3] + +chan = atchan.AtChannel() +chan.connect() + +if not atchan.found(chan.cmdchat('get_power'), 'OK on'): + print 'GSM disabled - message not sent' + sys.exit(1) + +if not atchan.found(chan.cmdchat('connect'), 'OK'): + print 'system error - message not sent' + sys.exit(1) +chan.chat1(None, [ 'AT-Command Interpreter ready' ], 500) +# clear any pending error status +chan.chat1('ATE0', ['OK', 'ERROR']) +if chan.chat1('ATE0', ['OK', 'ERROR']) != 0: + print 'system error - message not sent' + sys.exit(1) + +want = re.compile('^\+COPS:.*".+"') +n,c = chan.chat('AT+COPS?', ['OK', 'ERROR'], 2000 ) +if n != 0 or not atchan.found(c, want): + print 'No Service - message not sent' + sys.exit(1) + +# use PDU mode +n,c = chan.chat('AT+CMGF=0', ['OK', 'ERROR']) +if n != 0: + print 'Unknown error' + sys.exit(1) + +# SMSC header and TPDU header +SMSC = '00' # No SMSC number given, use default +REF = '00' # ask network to assign ref number +phone_num = encode_number(recipient) +proto = '00' # don't know what this means +encode = '00' # 7 bit ascii +if len(mesg) <= 160: + # single SMS mesg + m1 = code7(0,mesg) + m2 = '%02X'%len(mesg) + m1 + coded = "01" + REF + phone_num + proto + encode + m2 + if send(chan, SMSC, coded): + print "OK message sent" + sys.exit(0) + else: + print "ERROR message not sent" + sys.exit(1) + +elif len(mesg) <= 5 * 153: + # Multiple messsage + packets = (len(mesg) + 152) / 153 + packet = 0 + mesgid = random.getrandbits(8) + while len(mesg) > 0: + m = mesg[0:153] + mesg = mesg[153:] + id = mesgid + packet = packet + 1 + UDH = add_len('00' + add_len('%02X%02X%02X'%(id, packets, packet))) + m1 = UDH + code7(1, m) + m2 = '%02X'%(7+len(m)) + m1 + coded = "41" + REF + phone_num + proto + encode + m2 + if not send(chan, SMSC, coded): + print "ERROR message not sent at part %d/%d" % (packet,packets) + sys.exit(1) + print 'OK message sent in %d parts' % packets + sys.exit(0) +else: + print 'Message is too long' + sys.exit(1) + diff --git a/gsm/gsmd.py b/gsm/gsmd.py index 912c07a..775e914 100644 --- a/gsm/gsmd.py +++ b/gsm/gsmd.py @@ -1,13 +1,28 @@ #!/usr/bin/env python -import re, time, gobject +## FIXME +# e.g. receive AT response +CREG: 1,"08A7","6E48" +# show that SIM is now ready +# cope with /var/lock/suspend not existing yet +# define 'reset' + +import re, time, gobject, os from atchan import AtChannel import dnotify, suspend -from trace import log +from tracing import log def record(key, value): - f = open('/var/run/gsm-state/' + key, 'w') + f = open('/var/run/gsm-state/.new.' + key, 'w') f.write(value) + f.close() + os.rename('/var/run/gsm-state/.new.' + key, + '/var/run/gsm-state/' + key) + +def calllog(key, msg): + f = open('/var/log/' + key, 'a') + now = time.strftime("%Y-%m-%d %H:%M:%S") + f.write(now + ' ' + msg + "\n") + f.close() class Task: def __init__(self, repeat): @@ -32,6 +47,7 @@ class AtAction(Task): # # States are 'init' 'checking', 'setting', 'done' ok = re.compile("^OK") + busy = re.compile("\+CMS ERROR.*SIM busy") not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)") def __init__(self, check = None, ok = None, record = None, at = None, timeout=None, handle = None, repeat = None): @@ -51,12 +67,19 @@ class AtAction(Task): self.advance(channel) def takeline(self, channel, line): + if line == None: + return m = self.ok.match(line) if m: channel.cancel_timeout() if self.handle: self.handle(channel, line, None) return self.advance(channel) + + if self.busy.match(line): + channel.cancel_timeout() + channel.set_timeout(5000) + return if self.not_ok.match(line): channel.cancel_timeout() return self.timeout(channel) @@ -113,20 +136,25 @@ class PowerAction(Task): if not channel.connected: channel.connect() - channel.state['stage'] = 'setting' + channel.state['stage'] = 'connecting' if self.cmd == "on": + channel.state['stage'] = 'needconnect' channel.set_power(True) + channel.check_flightmode() elif self.cmd == "off": channel.set_power(False) record('carrier', '') record('cell', '') record('signal_strength','0/32') - elif self.cmd == "reset": - channel.reset() + elif self.cmd == 'reopen': + channel.disconnect() + channel.connect() + channel.state['stage'] = 'done' + return channel.advance() def takeline(self, channel, line): # really a 'power_done' callback - if channel.state['stage'] == 'setting': + if channel.state['stage'] == 'needconnect': channel.state['stage'] = 'connecting' return channel.atconnect() if channel.state['stage'] == 'connecting': @@ -134,6 +162,11 @@ class PowerAction(Task): return channel.advance() raise + def timeout(self, channel): + # Hopefully a reset will work + channel.set_state('reset') + channel.advance() + class SuspendAction(Task): # This action simply allows suspend to continue def __init__(self): @@ -144,6 +177,16 @@ class SuspendAction(Task): channel.suspend_handle.release() return channel.advance() +class ChangeStateAction(Task): + # This action changes to a new state, like a goto + def __init__(self, state): + Task.__init__(self, None) + self.newstate = state + def start(self, channel): + channel.state['stage'] = 'done' + channel.set_state(self.newstate) + return channel.advance() + class Async: def __init__(self, msg, handle, handle_extra = None): self.msg = msg @@ -163,6 +206,7 @@ def status_update(channel, line, m): global LAC, CELLID, cellnames LAC = int(m.groups()[2],16) CELLID = int(m.groups()[3],16) + record("cellid", "%04X %04X" % (LAC, CELLID)); if CELLID in cellnames: record('cell', cellnames[CELLID]) log("That one is", cellnames[CELLID]) @@ -174,16 +218,33 @@ def new_sms(channel, line, m): if m: record('newsms', m.groups()[1]) +global incoming_cell_id def cellid_update(channel, line, m): # get something like +CBM: 1568,50,1,1,1 # don't know what that means, just collect the 'extra' line - pass + # I think the '50' means 'this is a cell id'. I should + # probably test for that. + # + # response can be multi-line + global incoming_cell_id + incoming_cell_id = "" + def cellid_new(channel, line): - global CELLID, cellnames + global CELLID, cellnames, incoming_cell_id + if not line: + # end of message + if incoming_cell_id: + l = re.sub('[^!-~]+',' ',incoming_cell_id) + if CELLID: + cellnames[CELLID] = l + record('cell', l) + return False line = line.strip() - if CELLID: - cellnames[CELLID] = line - record('cell', line) + if incoming_cell_id: + incoming_cell_id += ' ' + line + else: + incoming_cell_id = line + return True incoming_num = None def incoming(channel, line, m): @@ -193,27 +254,38 @@ def incoming(channel, line, m): else: record('incoming', '-') if channel.gstate != 'incoming': + calllog('incoming', '-call-') channel.set_state('incoming') def incoming_number(channel, line, m): global incoming_num if m: - incoming_num = m.groups()[0] + num = m.groups()[0] + if incoming_num == None: + calllog('incoming', num); + incoming_num = num record('incoming', incoming_num) +cpas_zero_cnt = 0 def call_status(channel, line, m): + global cpas_zero_cnt log("call_status got", line) if not m: return s = int(m.groups()[0]) log("s = %d" % s) if s == 0: + cpas_zero_cnt += 1 + if cpas_zero_cnt == 1: + return # idle global incoming_num incoming_num = None + record('incoming', '') if channel.gstate == 'incoming': - record('incoming', '') + calllog('incoming','-end-') if channel.gstate != 'idle': channel.set_state('idle') + cpas_zero_cnt = 0 if s == 3: # incoming call if channel.gstate != 'incoming': @@ -232,6 +304,14 @@ control['flight'] = [ PowerAction('off') ] +control['reset'] = [ + # turning power off just kills everything!!! + #PowerAction('reopen'), + #PowerAction('off'), + AtAction(at='E0', timeout=30000), + ChangeStateAction('idle'), + ] + # For suspend, we want power on, but no wakups for status or cellid control['suspend'] = [ AtAction(at='+CNMI=1,1,0,0,0'), @@ -239,16 +319,23 @@ control['suspend'] = [ SuspendAction() ] +control['listenerr'] = [ + PowerAction('on'), + AtAction(at='V1E0'), + AtAction(at='+CMEE=2;+CRC=1') + ] control['idle'] = [ PowerAction('on'), AtAction(at='V1E0'), AtAction(at='+CMEE=2;+CRC=1'), # Turn the device on. AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000), + # Report carrier as long name + AtAction(at='+COPS=3,0'), # register with a carrier - AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS', - record=('carrier', '\\1'), timeout=10000), - AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS', + #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS', + # record=('carrier', '\\1'), timeout=10000), + AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0', record=('carrier', '\\1'), timeout=10000, repeat=37000), # fix a bug AtAction(at='%SLEEP=2'), @@ -256,7 +343,8 @@ control['idle'] = [ AtAction(check='+CMGF?', ok='\+CMGF: 1', at='+CMGF=1'), # get location status updates AtAction(at='+CREG=2'), - AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', handle=status_update), + AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', + handle=status_update, timeout=4000), # Enable collection of Cell Info message #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'), AtAction(at='+CSCB=0'), @@ -265,8 +353,9 @@ control['idle'] = [ #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'), AtAction(at='+CNMI=1,0,0,0,0'), AtAction(at='+CNMI=1,1,2,0,0'), + # Enable reporting of Caller number id. - AtAction(check='+CLIP?', ok='\+CLIP: 1,2', at='+CLIP=1', timeout=5000), + AtAction(check='+CLIP?', ok='\+CLIP: 1,2', at='+CLIP=1', timeout=10000), # Must be last: get signal string AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', @@ -293,7 +382,7 @@ async = [ Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update, handle_extra = cellid_new), Async(msg='\+CRING: (.*)', handle = incoming), - Async(msg='\+CLIP: "([^"]*)",[0-9,]*', handle = incoming_number), + Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number), ] class GsmD(AtChannel): @@ -335,32 +424,38 @@ class GsmD(AtChannel): # - def __init__(self): + def __init__(self, dostuff): AtChannel.__init__(self, master = True) - record('carrier','') - record('cell','') - record('incoming','') - record('signal_strength','') - self.extra = None self.flightmode = True - # Monitor other external events which affect us - d = dnotify.dir('/var/lib/misc/flightmode') - self.flightmode_watcher = d.watch('active', self.check_flightmode) + self.state = None - self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume) + if dostuff: + record('carrier','') + record('cell','') + record('incoming','') + record('signal_strength','') - self.state = None - # set the initial state - self.set_state('flight') + # Monitor other external events which affect us + d = dnotify.dir('/var/lib/misc/flightmode') + self.flightmode_watcher = d.watch('active', self.check_flightmode) + + self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume) + + # set the initial state + self.set_state('flight') - # Check the externally imposed state - self.check_flightmode(self.flightmode_watcher) + # Check the externally imposed state + self.check_flightmode(self.flightmode_watcher) + else: + self.set_state('listenerr') + + # and GO! self.advance() - def check_flightmode(self, f): + def check_flightmode(self, f = None): try: fd = open("/var/lib/misc/flightmode/active") l = fd.read(1) @@ -406,12 +501,13 @@ class GsmD(AtChannel): if self.state['stage'] == 'done': self.lastrun[self.tasknum] = now else: - self.need_reset() + self.set_state('reset') self.state = None self.tasknum = None (t, delay) = self.next_cmd() + log("advance %s chooses %d, %d" % (self.gstate, t, delay)) if delay: - log("Sleeping for %f seconds" % (delay/1000)) + log("Sleeping for %f seconds" % (delay/1000.0)) self.set_timeout(delay) else: self.tasknum = t @@ -421,12 +517,12 @@ class GsmD(AtChannel): def takeline(self, line): - if not line: + if self.extra: + if not self.extra.handle_extra(self, line): + self.extra = None return False - if self.extra: - self.extra.handle_extra(self, line) - self.extra = None + if not line: return False # Check for an async message @@ -488,7 +584,8 @@ class GsmD(AtChannel): self.advance() return False -GsmD() +a = GsmD(True) +#b = GsmD(False) c = gobject.main_context_default() while True: c.iteration() diff --git a/gsm/launch_gsm.py b/gsm/launch_gsm.py new file mode 100644 index 0000000..be9a226 --- /dev/null +++ b/gsm/launch_gsm.py @@ -0,0 +1,970 @@ + +# +# launcher plugin for watching for new sms messages. +# +# We watch /var/run/gsm-state/newsms and if that changes, +# run "gsm-getsms -n" +# When that completes, or after a timer, load new messages +# and display from/time +# Allow 'run sendsms -n' + +import dnotify +from subprocess import Popen +from storesms import SMSstore +import gobject +import sys,os,fcntl +import re, time + +global gsmstate +gsmstate = {}; +# +# lastxt : time stamp of mos recent text to arrive, so we know if +# current new text is actually new. +gsmstate['lastxt'] = 0 +# watchsms: watch on 'newsms' file +# watchmsg: watch on 'newmesg' file +# watchcall: watch on 'incoming' file +# store: the sms store +# value: BUSY / name of incoming / +# oncall: Boolean if making or receiving call +gsmstate['oncall'] = False +# tonestr: last string from which we had touch-tone chars +# tonesent: list of touch-tone chars sent +# channel: channel to GSMd for make/answer call. Holds 'call' object +# book: phone book +# newname +# fullname +# num +# from +# suspend-lock +# buzz +gsmstate['carriers'] = [] +gsmstate['carriers_valid'] = 0 +gsmstate['searching'] = False + +def suspend_lock(): + try: + f = open("/var/run/suspend_disabled", "w+") + except: + f = None + if f: + try: + fcntl.lockf(f, fcntl.LOCK_SH | fcntl.LOCK_NB) + except: + f.close() + f = None + return f +def suspend_unlock(f): + if f: + fcntl.lockf(f, fcntl.LOCK_UN) + f.close() + return None + + +def sysfs_write(file, val): + try: + f = open(file, "w") + f.write(val) + f.close() + except: + pass + +def buzz_in(msec): + return gobject.timeout_add(msec, buzz_on) +def buzz_off(ev): + if (ev): + gobject.source_remove(ev) + return None + +def buzz_on(): + vib = "/sys/class/leds/neo1973:vibrator" + sysfs_write(vib+"/trigger", "none") + sysfs_write(vib+"/trigger", "timer") + sysfs_write(vib+"/delay_on", "50") + sysfs_write(vib+"/delay_off", "100") + vib_timer = gobject.timeout_add(1000, buzz_cancel) + return False +def buzz_cancel(): + vib = "/sys/class/leds/neo1973:vibrator" + sysfs_write(vib+"/trigger", "none") + return False + + +def newmesg(cmd, obj): + global gsmstate + if len(obj.state) == 0: + init(obj) + if cmd == '_name': + return ('cmd', gsmstate['name']) + if cmd == '_options': + return obj.state['cmd'].options() + return obj.state['cmd'].event(cmd) + +def init(obj): + global gsmstate + obj.state['cmd'] = sys.modules['__main__'].CmdTask(',/usr/local/bin/sendsms -n,SendSMS') + gsmstate['name'] = '-' + d = dnotify.dir('/var/run/gsm-state') + w = d.watch('newsms', lambda f : got_sms(obj)) + gsmstate['watchsms'] = w + for p in ['/media/card','/media/disk','/var/tmp']: + if os.path.exists(p): + pth = p+"/SMS" + break + d = dnotify.dir(pth) + w = d.watch('newmesg', lambda f : load_new(obj)) + gsmstate['watchmsg'] = w + s = SMSstore(pth) + gsmstate['store'] = s + load_new(obj) + +def wrap(txt): + ln = 0 + n = '' + w = '' + for l in txt: + if l != ' ' and l != '\n': + # still in word + w += l + continue + if ln + len(w) < 22: + # this word fits + if ln: + n += ' ' + w + else: + n += w + ln = 1 + ln += len(w) + w = '' + continue + if ln == 0: + # line otherwise empty + n += w + '\n' + w = '' + continue + n += '\n' + w; + ln = len(w) + 1 + w = '' + if w: + if ln: + n += ' ' + w + else: + n += w + return n + +def protect(txt): + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + return txt + +def get_book(): + global gsmstate + if not 'book' in gsmstate: + gsmstate['book'] = load_book("/media/card/address-book") + gsmstate['bookstamp'] = time.time() + return gsmstate['book'] + +def load_new(obj): + global gsmstate + s = gsmstate['store'] + (next, l) = s.lookup(None, 'NEW') + if len(l) == 0: + if gsmstate['name'] != '-': + obj.refresh(False) + gsmstate['name'] = '-' + gsmstate['lastxt'] = 0 + return + cor = book_name(get_book(), l[0].correspondent) + if not cor: + cor = l[0].correspondent + else: + cor = cor[0] + txt = cor + if len(l) > 1: + txt += " (+%d)"% (len(l)-1) + txt += ': ' + l[0].text + txt = wrap(txt) + txt = protect(txt) + gsmstate['name'] = ''+txt+'' + + wake = (l[0].stamp > gsmstate['lastxt']) + if wake: + gsmstate['lastxt'] = l[0].stamp + obj.refresh(wake) + return False + +def got_sms(obj): + Popen('gsm-getsms -n', shell=True, close_fds = True) + #the watcher does this.. gobject.timeout_add(1000, lambda *a : (load_new(obj))) + try: + f = open("/var/run/alert/sms", "w") + f.write("new") + f.close() + except: + pass + +## +## incoming and outgoing calls +## + +import atchan + +class Phone(atchan.AtChannel): + def __init__(self, refresh): + self.refresh = refresh + self.action = '' + atchan.AtChannel.__init__(self, master = False) + self.atconnect() + + def power_done(self, line = None): + if line == "OK": + self.do_cmd() + + def do_cmd(self): + if self.action == 'disconnect': + self.disconnect() + self.action = '' + if self.action == 'answer': + self.atcmd('%N0187;A') + self.action = '' + if self.action[0:5] == 'call:': + self.atcmd('%N0187;D'+self.action[5:]+';') + self.action = '' + if self.action == 'hangup': + self.atcmd('H') + self.action = 'disconnect' + if self.action[0:5] == 'tone:': + self.atcmd('+VTS='+self.action[5:]) + self.action = '' + if self.action == 'rq-clear': + self.atcmd('+CUSD=0;H') + self.action = '' + if self.action[0:3] == 'rq ': + pre = '' + if self.action[3] == 'not0': + pre = "+CUSD=0;H;" + self.atcmd(pre + '+CUSD=' + self.action[3:]) + self.action = '' + + if self.action == 'search': + self.atcmd('+COPS=?', timeout = 40000) + self.action = '' + + if self.action[0:7] == 'select:': + c = self.action[7:] + self.atcmd('+COPS=4,2,"%s"'%c, timeout=15000) + self.action = '' + + + def takeline(self, line): + global gsmstate + + if line == 'AT-Command Interpreter ready': + return False + + gsmstate['searching'] = False + if line == 'OK': + if self.pending: + self.pending = False + gobject.source_remove(self.timer) + self.timer = None + self.do_cmd() + return False + + if line == 'BUSY': + gsmstate['value'] = 'BUSY' + self.refresh() + if line == 'NO CARRIER': + if gsmstate['oncall']: + reject() + + if line[0:7] == "+CUSD: ": + m = re.match('^\+CUSD: ([012]),"(.*)"(,[0-9]+)?$', line) + if m: + if m.group(1) == '0': + gsmstate['request'] = 3 + else: + gsmstate['request'] = 2 + gsmstate['rqmsg'] = m.group(2) + self.refresh() + + if line[0:7] == "+COPS: ": + gsmstate['carriers'] = re.findall('\((\d+),"([^"]*)","([^"]*)","([^"]*)"\)',line[7:]) + gsmstate['carriers_valid'] = time.time() + self.refresh() + + return False + + + def act(self, cmd): + self.action = cmd + if not self.pending and not self.command_pending: + self.do_cmd() + + +def load_book(file): + try: + f = open(file) + except: + f = open("/home/neilb/home/mobile-numbers-jan-08") + rv = [] + for l in f: + x = l.split(';') + rv.append([x[0],x[1]]) + rv.sort(lambda x,y: cmp(x[0],y[0])) + return rv + +def book_lookup(book, name, num): + st=[]; mid=[] + for l in book: + if name.lower() == l[0][0:len(name)].lower(): + st.append(l) + elif l[0].lower().find(name.lower()) >= 0: + mid.append(l) + st += mid + if len(st) == 0: + return [None, None] + if num >= len(st): + num = -1 + return st[num] + +def book_speed(book, sym): + i = book_lookup(book, sym, 0) + if i[0] == None or i[0] != sym: + return None + j = book_lookup(book, i[1], 0) + if j[0] == None: + return (i[1], i[0]) + return (j[1], j[0]) + +def book_name(book, num): + if len(num) < 8: + return None + for ad in book: + if len(ad[1]) >= 8 and num[-8:] == ad[1][-8:]: + return ad + return None + +incoming = [] +def incoming_flush(start, end, number): + global incoming + if start: + incoming.append((start, end, number)) +def load_incoming(): + global incoming + incoming = [] + + f = open("/var/log/incoming") + start = None; end=None; number=None + for l in f: + w = l.split() + if len(w) != 3: + continue + if w[2] == '-call-': + incoming_flush(start, end, number) + start = (w[0], w[1]) + number = None; end = None + elif w[2] == '-end-': + end = (w[0], w[1]) + incoming_flush(start, end, number) + start = None; end = None; number = None + else: + number = w[2] + if not start: + start = (w[0], w[1]) + f.close() + +outgoing = [] +def outgoing_flush(start, end, number): + global outgoing + if start: + outgoing.append((start, end, number)) +def load_outgoing(): + global outgoing + outgoing = [] + + f = open("/var/log/outgoing") + start = None; end=None; number=None + for l in f: + w = l.split() + if len(w) != 3: + continue + if w[2] == '-end-': + end = (w[0], w[1]) + outgoing_flush(start, end, number) + start = None; end = None; number = None + else: + outgoing_flush(start, end, number) + start = (w[0], w[1]) + number = w[2] + f.close() + +incoming_map = [] +def incoming_lookup(num): + global incoming, incoming_map + if num == 1: + load_incoming() + ln = 0 + a = {} + for start, end, number in incoming: + if number == None: + continue + ln += 1 + a[number] = ln + b = {} + for x in a: + b[a[x]] = x + b = b.keys() + k.sort() + incoming_map = [] + for n in k: + incoming_map.append(b[n]) + if num > len(incoming_map): + num = len(incoming_map) + return ["Caller %d" %num, incoming_map[-num]] + +def name_lookup(book, str): + # We need to report + # - a number - to dial + # - optionally a name that is associated with that number + # - optionally a new name to save the number as + # The name is normally alpha, but can be a single digit for + # speed-dial + # Dots following a name allow us to stop through multiple matches. + # So input can be: + # A single symbol. + # This is a speed dial. It maps to name, then number + # A string of >1 digits + # This is a literal number, we look up name if we can + # A string of dots + # This is a look up against recent incoming calls + # We look up name in phone book + # A string starting with alpha, possibly ending with dots + # This is a regular lookup in the phone book + # A number followed by a string + # This provides the string as a new name for saving + # A string of dots followed by a string + # This also provides the string as a newname + # An alpha string, with dots, followed by '+'then a single symbol + # This saves the match as a speed dial + # + # We return a triple of (number,oldname,newname) + if re.match('^[A-Za-z0-9]$', str): + # Speed dial lookup + s = book_speed(book, str) + print "speed", str, s + if s: + return (s[0], s[1], None) + return None + m = re.match('^(\+?\d+|[*#][*#0-9]*)([A-Za-z][A-Za-z0-9 ]*)?$', str) + if m: + # Number and possible newname + s = book_name(book, m.group(1)) + if s: + return (m.group(1), s[0], m.group(2)) + else: + return (m.group(1), None, m.group(2)) + m = re.match('^(\.+)([A-Za-z][A-Za-z0-9 ]*)?$', str) + if m: + # dots and possible newname + i = incoming_lookup(len(m.group(1))) + s = book_name(book, i[1]) + if s: + return (i[1], s[0], m.group(2)) + else: + return (i[1], i[0], m.group(2)) + m = re.match('^([A-Za-z][A-Za-z0-9 ]*)(\.*)(\+[A-Za-z0-9])?$', str) + if m: + # name and dots + speed = None + if m.group(3): + speed = m.group(3)[1] + i = book_lookup(book, m.group(1), len(m.group(2))) + if i[0]: + return (i[1], i[0], speed) + return None + +def encode(str): + return re.sub("&", "&", str) + +def incoming(cmd, obj): + global gsmstate + + if len(obj.state) == 0: + init_incoming(obj) + gsmstate['obj'] = obj + havename = False; havenum = False + if gsmstate['oncall']: + # any new characters get sent via GSM if valid + ts = gsmstate['tonestr'] + if obj.current_input[0:len(ts)] != ts: + # something has changed, reset + ts = obj.current_input + + xtra = obj.current_input[len(ts):] + for c in xtra: + if c in "0123456789#*": + gsmstate['channel'].act('tone:' + c) + gsmstate['tonesent'] += c + gsmstate['tonestr'] = obj.current_input + + x = name_lookup(gsmstate['book'], obj.current_input) + if x: + havenum = x[0] + havename= x[1] + gsmstate['newname'] = x[2] + gsmstate['fullname'] = havename + gsmstate['num'] = havenum + else: + havenum = False + + if cmd == '_name': + if 'from' in gsmstate: + v = gsmstate['from'] + elif 'value' in gsmstate: + v = gsmstate['value'] + else: + v = False + if gsmstate['oncall'] and gsmstate['tonesent']: + v = 'Sent:' + gsmstate['tonesent'] + if not v and gsmstate['request'] > 0: + if gsmstate['request'] == 1: + v = "... awaiting response ..." + else: + v = protect(wrap(gsmstate['rqmsg'])) + if not v and havenum: + if gsmstate['newname']: + v = 'Save: ' + encode(gsmstate['newname']) + elif havename: + v = 'Call: ' + encode(havename) + else: + v = 'Call: ' + encode(havenum) + print 'now v = ', v + if not v and gsmstate['lastcaller']: + v = '(' + gsmstate['lastcaller'] + ')' + return ('cmd', v) + if cmd == '_options': + if gsmstate['oncall']: + o = ["Mute", "Hang Up"] + elif gsmstate['value']: + o = ["Answer", "Reject"] + elif gsmstate['request'] > 0: + if gsmstate['request'] == 2: + o = ["Quit", "Send Answer (%s)" % havenum] + else: + o = ["Quit"] + elif havenum: + if gsmstate['newname']: + o = ['Save ' + havenum ] + elif re.match('^[*#][*#0-9]*#$', havenum): + o = ["Request " + havenum ] + else: + o = ["Call " + havenum ] + elif gsmstate['lastcallnum']: + o = [ "Call-back", "Discard" ] + else: + o = ['Speed\nDial', 'Received\ncalls', 'Dialed\nNumbers'] + return o + if gsmstate['oncall']: + if cmd == 0: + # Mute + return + if cmd == 1: + # Hang up + reject() + return + elif gsmstate['value']: + # incoming + gsmstate['lastcaller'] = '' + gsmstate['lastcallnum'] = '' + if cmd == 0: + answer(obj) + elif cmd == 1: + reject() + elif gsmstate['request'] > 0: + if cmd == 0: + # quit - abort and forget + gsmstate['request'] = 0 + gsmstate['rqmsg'] = '' + if 'channel' in gsmstate: + chan = gsmstate['channel'] + if chan: + chan.act('rq-clear') + elif cmd == 1: + # send update message + if 'channel' in gsmstate: + chan = gsmstate['channel'] + if chan: + chan.act('rq 1,"%s"' % havenum) + gsmstate['request'] = 1 + + elif havenum and gsmstate['newname']: + f = open('/media/card/address-book', 'a') + nn = gsmstate['newname'] + if len(nn) == 1 and havename: + # new speed dial + f.write(nn + ';' + havename + ';\n') + else: + f.write(nn + ';' + havenum + ';\n') + f.close() + gsmstate['book'] = load_book("/media/card/address-book") + gsmstate['bookstamp'] = time.time() + elif havenum: + if cmd == 0: + num = gsmstate['num'] + if re.match('^[*#][*#0-9]*#$', num): + place_request(obj, num) + else: + place_call(num, lambda: obj.refresh(False), obj.current_input) + elif gsmstate['lastcallnum']: + if cmd == 0: + place_call(gsmstate['lastcallnum'], + lambda: obj.refresh(False), obj.current_input) + elif cmd == 1: + gsmstate['lastcaller'] = '' + gsmstate['lastcallnum'] = '' + obj.refresh(False) + + elif cmd == 0: + obj.set_tasks(tasklist_speed_dial(), 0) + elif cmd == 1: + obj.set_tasks(tasklist_received(True), 0) + elif cmd == 2: + obj.set_tasks(tasklist_received(False), 0) + + +def init_incoming(obj): + global gsmstate + print "init incoming" + obj.state['cmd'] = None + gsmstate['value'] = '' + gsmstate['num'] = False + gsmstate['name'] = False + gsmstate['suspend-lock'] = False + gsmstate['buzz'] = False + gsmstate['request'] = 0 + gsmstate['lastcaller'] = '' + gsmstate['lastcallnum'] = '' + # request values: + # 1 == waiting reply + # 2 == have intermediate reply + # 3 == have final reply + gsmstate['rqmsg'] = '' + get_book() + d = dnotify.dir('/var/run/gsm-state') + w = d.watch('incoming', lambda f : got_incoming_later(obj)) + gsmstate['watchcall'] = w + +def got_incoming_later(obj): + gobject.idle_add(lambda : got_incoming(obj)) + +def got_incoming(obj): + global gsmstate + try: + f = open("/var/run/gsm-state/incoming") + l = f.readline().strip() + f.close() + except: + l = "" + print 'got l=', l + if l != gsmstate['value']: + obj.refresh(not not l) + gsmstate['value'] = l + if l: + try: + f = open("/var/run/alert/ring", "w") + f.write("ring") + f.close() + except: + pass + if 'channel' not in gsmstate: + try: + chan = Phone(lambda : obj.refresh(False)) + gsmstate['channel'] = chan + except: + gsmstate['channel'] = None + else: + chan = gsmstate['channel'] + else: + try: + os.unlink("/var/run/alert/ring") + except: + pass + reject() + fromnum = book_name(gsmstate['book'], l) + if fromnum == None: + gsmstate['from'] = l + else: + gsmstate['from'] = fromnum[0] + if len(gsmstate['from']) > 1: + gsmstate['lastcaller'] = gsmstate['from'] + gsmstate['lastcallnum'] = l + return False + +def answer(obj): + # Need to + # lock against suspend + # alsactl -f /root/usr/share/openmoko/scenarios/gsmhandset.state restore + # silence 'sound' + # send command AT%N0187 + # send command ATA + # set flag that we are on a call ['oncall'] = True + # pop up a touch-tone thing + # listen for carrier to drop + global gsmstate + try: + os.unlink("/var/run/alert/ring") + except: + pass + gsmstate['suspend-lock'] = suspend_lock() + Popen(['alsactl', '-f', '/root/usr/share/openmoko/scenarios/gsmhandset.state', + 'restore' ], shell=False, close_fds = True) + if 'channel' in gsmstate: + chan = gsmstate['channel'] + if chan: + gsmstate['buzz'] = buzz_in(105000) + chan.act('answer') + gsmstate['oncall'] = True + gsmstate['tonestr'] = obj.current_input + gsmstate['tonesent'] = '' + +def calllog(key, msg): + f = open('/var/log/' + key, 'a') + now = time.strftime("%Y-%m-%d %H:%M:%S") + f.write(now + ' ' + msg + "\n") + f.close() + +def place_call(num, refresh, input): + # Need to + # lock against suspend + # alsactl -f /root/usr/share/openmoko/scenarios/gsmhandset.state restore + # silence 'sound' + # send command AT%N0187 + # send command ATDwhatever; + # set flag that we are on a call ['oncall'] = True + # pop up a touch-tone thing + # listen for carrier to drop + global gsmstate + gsmstate['suspend-lock'] = suspend_lock() + Popen(['alsactl', '-f', '/root/usr/share/openmoko/scenarios/gsmhandset.state', + 'restore' ], shell=False, close_fds = True) + if 'channel' not in gsmstate: + chan = Phone(refresh) + gsmstate['channel'] = chan + + gsmstate['buzz'] = buzz_in(105000) + chan = gsmstate['channel'] + gsmstate['oncall'] = True + gsmstate['tonestr'] = input + gsmstate['tonesent'] = '' + chan.act('call:'+ num) + calllog('outgoing',num) + +def place_request(obj, num): + global gsmstate + if 'channel' not in gsmstate: + chan = Phone(lambda : obj.refresh(False)) + gsmstate['channel'] = chan + chan = gsmstate['channel'] + gsmstate['request'] = 1 + chan.act('rq 0,"%s"'% num) + + + +def reject(): + # Need to + # alsactl -f /root/usr/share/openmoko/scenarios/stereoout.state restore + # remove silence + # sent ATH + # remove touchtone thing + global gsmstate + if 'channel' in gsmstate: + chan = gsmstate['channel'] + if chan: + chan.act('hangup') + del gsmstate['channel'] + Popen(['alsactl', '-f', '/root/usr/share/openmoko/scenarios/stereoout.state', + 'restore' ], shell=False, close_fds = True) + calllog('outgoing','-end-') + gsmstate['oncall'] = False + gsmstate['value'] = '' + gsmstate['suspend-lock'] = suspend_unlock(gsmstate['suspend-lock']) + gsmstate['buzz'] = buzz_off(gsmstate['buzz']) + + + +def speed(n): + return lambda cmd, obj: speed_dial(cmd, obj, n) + +def empty(cmd, obj): + if cmd == '_name': + return ('cmd', '') + if cmd == '_options': + return [] + +def speed_dial(cmd, obj, n): + global gsmstate + if 'stamp' not in obj.state or obj.state['stamp'] != gsmstate['bookstamp']: + s = book_speed(get_book(), n) + obj.state['num'] = s + obj.state['stamp'] = gsmstate['bookstamp'] + else: + s = obj.state['num'] + if s == None: + return empty(cmd, obj) + (num, name) = s + if cmd == '_name': + return ('cmd', n+': '+name) + if cmd == '_options': + if gsmstate['oncall']: + return ['Mute', 'Hang Up'] + return [ 'Call ' + num] + if gsmstate['oncall']: + if cmd == 0: + # Mute + return + if cmd == 1: + # Hang up + reject() + return + if cmd == 0: + place_call(num, lambda: obj.refresh(False), obj.current_input) + + +############################################ +# operator selecting +# Display current operator (from /var/run/gsm-state/carrier) +# If we know options, then one button for each carrier. +# If we don't, then one button for 'search carriers' + +def carrier(cmd, obj): + global gsmstate + + if len(obj.state) == 0: + obj.state['carrier_name'] = '' + + if cmd == '_name' : + if gsmstate['searching']: + return ('cmd', '(searching)') + else: + try: + f = open("/var/run/gsm-state/carrier") + l = f.readline().strip() + f.close() + except OSError: + l = '' + if not l: + l = '(unknown)' + return ('cmd', l) + + if cmd == '_options': + if gsmstate['carriers_valid'] + 5*60 < time.time(): + return [ 'Search Carriers' ] + rv = [] + for v in gsmstate['carriers']: + rv.append(v[2]) + return rv + + if 'channel' not in gsmstate: + try: + chan = Phone(lambda : obj.refresh(False)) + gsmstate['channel'] = chan + except: + gsmstate['channel'] = None + else: + chan = gsmstate['channel'] + + if gsmstate['carriers_valid'] + 5*60 < time.time(): + chan.act('search') + gsmstate['searching'] = True + return + if cmd >= len(gsmstate['carriers']): + return + c = gsmstate['carriers'][cmd] + chan.act('select:' + c[3]) + +############################################ +# 'tasklist' for received calls +class tasklist_received(sys.modules['__main__'].tasklist): + def __init__(self, income): + sys.modules['__main__'].tasklist.__init__(self) + self.incoming = income + if income: + self.name = 'Received Calls' + else: + self.name = 'Dialled Numbers' + + def start_refresh(self): + global incoming, outgoing + if self.incoming: + load_incoming() + self.list = incoming + else: + load_outgoing() + self.list = outgoing + + def info(self, n): + global incoming, gsmstate + (start, end, number) = self.list[-1-n] + if number == None: + number = "-private-number-" + else: + s = book_name(gsmstate['book'], number) + if s != None: + number = s[0] + return 'cmd', ('%s\n%s %s' + % (number, start[0], start[1])) + def options(self, n): + global gsmstate + if gsmstate['oncall']: + return [] + (start, end, number) = self.list[-1-n] + if number == None: + return [] + return ['Call back %s' % number] + def event(self, n, ev): + global gsmstate + if gsmstate['oncall']: + if ev == 0: + # Mute + return + if ev == 1: + reject() + if ev == 0: + (start, end, number) = self.list[-1-n] + gsmstate['lastcallnum'] = number + gsmstate['lastcaller'] = number + gsmstate['obj'].refresh(True) + + + +class tasklist_speed_dial(sys.modules['__main__'].tasklist): + def __init__(self): + sys.modules['__main__'].tasklist.__init__(self) + self.name = 'Speed Dials' + + def start_refresh(self): + self.list = [] + b = gsmstate['book'] + print "Start Refresh" + for i in range(32,96): + i = book_speed(b, chr(i)) + if i == None: + continue + self.list.append(i) + + def info(self, n): + num, name = self.list[n] + return 'cmd', name + + def options(self, n): + num, name = self.list[n] + return ['Call ' + num] + def event(self, n, ev): + if ev == 0: + global gsmstate + num, name = self.list[n] + gsmstate['lastcallnum'] = num + gsmstate['lastcaller'] = name + gsmstate['obj'].refresh(True) diff --git a/gsm/smsdecode.py b/gsm/smsdecode.py new file mode 100644 index 0000000..dc0f5ed --- /dev/null +++ b/gsm/smsdecode.py @@ -0,0 +1,26 @@ +def sms_decode(msg): + #msg is a 7-in-8 encoding of a longer message. + pos = 0 + carry = 0 + str = '' + while msg: + c = msg[0:2] + msg = msg[2:] + b = int(c, 16) + + if pos == 0: + if carry: + str += chr(carry + (b&1)*64) + carry = 0 + b /= 2 + else: + b = (b << (pos-1)) | carry + carry = (b & 0xff80) >> 7 + b &= 0x7f + if (b & 0x7f) != 0: + str += chr(b&0x7f) + pos = (pos+1) % 7 + return str + +import sys +print sms_decode(sys.argv[1]) diff --git a/lib/trace.py b/lib/trace.py deleted file mode 100644 index 81acf7d..0000000 --- a/lib/trace.py +++ /dev/null @@ -1,19 +0,0 @@ - -# trivial library for including tracing in programs -# It can be turned on with PYTRACE=1 in environment - -import os - -tracing = False - -if 'PYTRACE' in os.environ: - if os.environ['PYTRACE']: - tracing = True - -def log(*mesg): - if tracing: - for m in mesg: - print m, - print - - diff --git a/lib/tracing.py b/lib/tracing.py new file mode 100644 index 0000000..1424377 --- /dev/null +++ b/lib/tracing.py @@ -0,0 +1,19 @@ + +# trivial library for including tracing in programs +# It can be turned on with PYTRACE=1 in environment + +import os,time,sys + +tracing = False + +if 'PYTRACE' in os.environ: + if os.environ['PYTRACE']: + tracing = True + +def log(*mesg): + if tracing: + print time.ctime(), + for m in mesg: + print m, + print + sys.stdout.flush()