]> git.neil.brown.name Git - freerunner.git/commitdiff
gsm: various updates and additions.
authorNeilBrown <neilb@suse.de>
Sun, 6 Feb 2011 09:15:45 +0000 (20:15 +1100)
committerNeilBrown <neilb@suse.de>
Sun, 6 Feb 2011 09:15:45 +0000 (20:15 +1100)
Signed-off-by: NeilBrown <neilb@suse.de>
gsm/atchan.py
gsm/gsm-getsms.py [new file with mode: 0644]
gsm/gsm-sms.py [new file with mode: 0644]
gsm/gsmd.py
gsm/launch_gsm.py [new file with mode: 0644]
gsm/smsdecode.py [new file with mode: 0644]
lib/tracing.py [moved from lib/trace.py with 80% similarity]

index cb40dc1f3be26ddcc5f61550330ebe0b9e4cef9a..88e528551203c3a02527ce4ad34026c85022f6f4 100644 (file)
@@ -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 (file)
index 0000000..48a0790
--- /dev/null
@@ -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 (file)
index 0000000..7a718a5
--- /dev/null
@@ -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<<pad)-1)
+            b += (c << pad) & 255
+            code += '%02X' % b
+            pad -= 1
+            carry = c >> (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<<pad)-1)
+        code += '%02X' % b
+    return code
+
+def add_len(mesg):
+    return ('%02X' % (len(mesg)/2)) + mesg
+
+def send(chan, dest, mesg):
+    n,c = chan.chat('AT+CMGS=%d' % (len(mesg)/2), ['OK','ERROR','>'])
+    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)
+
index 912c07abb2d2f7c3232cee317635c7eb53c546a6..775e914aacafd2e8f1b0eca50cdf710f6ec3ba64 100644 (file)
@@ -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 (file)
index 0000000..be9a226
--- /dev/null
@@ -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('&', '&amp;')
+    txt = txt.replace('<', '&lt;')
+    txt = txt.replace('>', '&gt;')
+    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'] = '<span size="10000">'+txt+'</span>'
+
+    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("&", "&amp;", 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', ('<span size="12000">%s</span>\n<span size="10000">%s %s</span>'
+                       % (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 (file)
index 0000000..dc0f5ed
--- /dev/null
@@ -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])
similarity index 80%
rename from lib/trace.py
rename to lib/tracing.py
index 81acf7d02d513098a618b7b1e1f637af9b1f6cf3..142437708190595eac7aa2504a313a93c465e171 100644 (file)
@@ -2,7 +2,7 @@
 # trivial library for including tracing in programs
 # It can be turned on with PYTRACE=1 in environment
 
-import os
+import os,time,sys
 
 tracing = False
 
@@ -12,8 +12,8 @@ if 'PYTRACE' in os.environ:
 
 def log(*mesg):
     if tracing:
+        print time.ctime(),
         for m in mesg:
             print m,
         print
-
-
+        sys.stdout.flush()