#!/usr/bin/env python

#############################################################################
##                                                                         ##
##   This is python-AT, a python module to send AT commands.               ##
##   There is no other doc for now than this file. If you are looking for  ##
## an AT commands reference, see :                                         ##
## <http://www.mipot.ch/MTM-120_AT_Commands.pdf>                           ##
##   If you want examples of use, have a look at the end of this file.     ##
##                                                                         ##
##                                                                         ##
## Copyright (C) 2007 Droids Corporation                                   ##
##                                                                         ##
## This program is free software; you can redistribute it and/or modify it ##
## under the terms of the GNU General Public License version 2 as          ##
## published by the Free Software Foundation; version 2.                   ##
##                                                                         ##
## This program is distributed in the hope that it will be useful, but     ##
## WITHOUT ANY WARRANTY; without even the implied warranty of              ##
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       ##
## General Public License for more details.                                ##
##                                                                         ##
#############################################################################

# TODO:
#   - handle unsolicited codes (RING, +CREG, ...)
#   - feed buf (see TODO in the code, needed for the first TODO)
#   - handle lines with no echo (for now, we assume that we recv() the
#   data we have sent ; actually, sometimes this happens, and
#   sometimes not. See ATE command.)

# BUGS: look for FIXME tags in the code ;-)

# * 0.0.3: Added Serial Socket support: one item less in the TODO list ;-)
# * 0.0.2: Generic CODE handling
#          Added TCP Socket support
#          Fixed bug 'answer without "cmd:" not handled'
#          Fixed several bugs around clean_result() and split() 
# * 0.0.1: initial revision

import re

RECVLEN=100
CODES=[ 'OK',
        'ERROR',
        'NO CARRIER',
        'NO DIALTONE',
        'NO ANSWER',
        'BUSY' ]

class SuperSocket:
    def __init__(self):
        self.s = None
        # not (yet) handled data
        self.buf = []
    
    def close(self):
        self.s.close()
    
    def srcmd(self, cmd):
        rdata = ''
        sdata = 'AT' + cmd + '\r'
        self.s.send(sdata)
        # When we have this regexp, we have got everything we need:
        expr = re.compile('(^|\n)' + re.escape(sdata) + '\r\n.*('
                          + '|'.join(CODES) + ')\r\n', re.DOTALL)
        while not re.search(expr, rdata):
            rdata += self.s.recv(RECVLEN)
        rdata = rdata.split('\r\n')
        # we go to the beginning [TODO: send the rest to buf] 
        rdata = rdata[rdata.index(sdata) + 1:]
        # let's clean cmd (answers to 'CMD?', 'CMD=' and 'CMD=?'
        # commands start with 'CMD: ', without '?' or '=')
        if cmd[-1] == '?':
            cmd = cmd[:-1]
        if '=' in cmd:
            cmd = cmd[:cmd.index('=')]

        codeindex = min([ rdata.index(x)
                          for x in filter(lambda x: x in rdata,
                                          CODES) ])
        code = rdata[codeindex]
        # we remove anything after the result code [TODO: send the rest to buf]
        rdata = rdata[:codeindex]
        
        # we only consider non-empty lines without ':' (AT+CGMI /
        # AT+CGMM, for example, produce such lines ; but [FIXME] we
        # might include unsolicited RING, for example), and lines with
        # cmd + ': ' (most commands produce such results) ; we might
        # also include unsolicited messages, but they would be related
        # to the command we have just issued, and I'm not sure there
        # is a way to tell wether a message has been solicited.  TODO:
        # send the rest to buf
        
        rdata = filter(lambda x: (x != "" and ":" not in x) or
                       x.startswith(cmd + ': '), rdata)
        def remove_cmd(x):
            if x.startswith(cmd + ': '):
                return x[len(cmd)+2:]
            else:
                return x
        rdata = map(remove_cmd, rdata)
        
        # we pass the coma-separated fields to clean_result(), and we
        # return the resulting array (of arrays of fields), with the
        # error / ok code in a tuple
        
        #return (code, map(lambda x: map(lambda r: clean_result(cmd, r),
        #                                split(x)),
        #                  rdata))
        return (code, map(lambda x: map(clean_result, split(x)), rdata))

# Bluetooth:

#  - socket implementation
class BTSocket(SuperSocket):
    def __init__(self, addr, port=None):
        SuperSocket.__init__(self)
        import bluetooth
        if port is None:
            services = bluetooth.find_service(address=addr)
            dialup = filter(lambda x: x['protocol'] == 'RFCOMM' and
                            bluetooth.DIALUP_NET_CLASS in x['service-classes'],
                            services)
            if dialup:
                port = dialup[0]['port']
            else:
                raise bluetooth.BluetoothError('No dialup service discovered, and no port selected.')
        self.peer = 'bt://' + addr + ':' + str(port)
        self.s = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
        self.s.connect((addr, port))

#  - some usefull functions
#    - bluetooth.discover_devices() -> device list (mac addresses)
#    - bluetooth.lookup_name(mac address) -> device name
#    - bluetooth.find_service([address=mac address]) -> services list

# IP Socket (TCP for now):
class InetSocket(SuperSocket):
    def __init__(self, addr, proto='tcp', port=23):
        SuperSocket.__init__(self)
        import socket
        self.peer = type.lower() + '://' + addr + ':' + str(port)
        if proto == 'tcp':
            self.s = socket.socket(socket.AF_INET, type = socket.SOCK_STREAM)
            self.s.connect((addr, port))
        else:
            raise socket.error('Protocol not supported.')

# Serial / USB Socket
class SerialSocket(SuperSocket):
    def __init__(self, *arg, **kargs):
        SuperSocket.__init__(self)
        import serial
        self.s = serial.Serial(timeout=0, *arg, **kargs)
        self.s.recv = self.s.read
        self.s.send = self.s.write

# Generic socket creation
def AT(uri):
    if not '://' in uri:
        print "Unsupported URI (%s)" % uri
        return None
    proto = uri.split('://')[0]
    dev = uri.split('://')[1]
    if proto == 'bt':
        # BT URIs: bt://<addr>[:port]
        if dev.count(':') > 5:
            return BTSocket(':'.join(dev.split(':')[0:6]),
                            port = int(dev.split(':')[6]))
        else:
            return BTSocket(dev)
    elif proto == 'tcp':
        if ':' in dev:
            return InetSocket(dev.split(':')[0],
                              port = int(dev.split(':')[1]),
                              proto = proto)
        else:
            return InetSocket(dev, proto = proto)
    elif proto == 'serial':
        # for now, we don't have a way to describe parameters such as
        # speed, parity, etc.
        return SerialSocket(dev)
    else:
        print "Unsupported URI (%s)" % uri
        return None

## Functions

#def clean_result(r, cmd):
def clean_result(r):
    if type(r) is not str:
        return r
    #if r.startswith(cmd + ': '):
    #    r = r[len(cmd)+2:]
    if r[0] == '"' and r[-1] == '"':
        return r[1:-1]
    if r[0] == '(' and r[-1] == ')':
        res = []
        for x in r[1:-1].split(','):
            if x.isdigit():
                res.append(int(x))
            elif x[0] == '"' and x[-1] == '"':
                res.append(x[1:-1])
            elif '-' in x:
                x = x.split('-')
                if len(x) == 2 and x[0].isdigit() and x[1].isdigit():
                    res += range(int(x[0]), int(x[1])+1)
                else:
                    res.append('-'.join(x))
            else:
                res.append(x)
        return tuple(res)
    if r.isdigit():
        return int(r)
    else:
        return r

def split(inp):
    # splits a string that way :
    # 'a,b,(c,(d)),(e,f),(g)' -> ['a', 'b', '(c,(d))', '(e,f)', '(g)']
    outp = []
    while inp:
            if '(' in inp:
                outp += inp[:inp.find('(')].split(',')
                if outp[-1] == '':
                    outp = outp[:-1]
                inp = inp[inp.find('(')+1:]
                i = 1
                tmp = '('
                while i != 0:
                    if ')' in inp:
                        i += inp[:inp.find(')') + 1].count('(') - 1
                        tmp += inp[:inp.find(')') + 1]
                        inp = inp[inp.find(')') + 1:]
                        if inp != '' and inp[0] == ',':
                            inp = inp[1:]
                    else:
                        # unbalanced (
                        tmp = inp
                        inp = ''
                        break
                outp += [ tmp ]
            else:
                outp += inp.split(',')
                inp = ''
    return outp

## Command class

class Command:
    def __init__(self, cmd, desc):
        self.cmd = cmd
        self.desc = desc
        self.fields_desc = None
        self.fields_disp = None
    def display_field_value(self, field, value):
        try:
            return self.fields_disp[field](value)
        except:
            return value

CNUM = Command('+CNUM', 'Subscriber number')
CNUM.fields_desc = [ "alpha", "number", "type", "speed", "service", "itc" ]
CNUM.services = {
    0: "Async modem",
    1: "Sync modem",
    2: "PAD access (async)",
    3: "Packety access (sync)",
    4: "Voice",
    5: "Fax"
    }
CNUM.fields_disp = { "service": lambda x: CNUM.services[x] }

CSQ = Command('+CSQ', 'Signal quality')
CSQ.fields_desc = ['rssi', 'ber']

def CSQ_fields_disp_rssi(val):
    if val == 99:
        return 'not known / not detectable'
    elif type(val) is int and val >= 0 and val <= 31:
        return str(2*val-113)+' dBm'

CSQ.fields_disp = {
    "rssi": CSQ_fields_disp_rssi,
    "ber": lambda x: {99:'not known / not detectable'}[x]
    }

CBC = Command('+CBC', 'Battery charge')
CBC.fields_desc = [ 'bcs', 'bcl' ]
CBC.BCS_values = {
    0: 'Powered by the battery',
    1: 'Battery connected, not powered by it',
    2: 'No battery connected',
    3: 'Power faults, calls inhibited',
    }

CBC.fields_disp = {
    'bcs': lambda x: CBC.BCS_values[x],
    'bcl': lambda x: str(x) + ' %'
    }

COMMANDS = {
    "+CNUM": CNUM,
    "+CSQ": CSQ,
    "+CBC": CBC
    }

def print_command(s,cmd):
    def print_ans(ans):
        for i in range(len(ans)):
            try:
                print cmd.fields_desc[i] + ":",
            except IndexError:
                print "UNKNOWN FIELD:",
            print cmd.display_field_value(cmd.fields_desc[i],
                                          ans[i])
        print
    ans = s.srcmd(cmd.cmd)
    print "<- AT" + cmd.cmd + " (" + cmd.desc + ')'
    print "-> " + ans[0]
    print
    map(print_ans, ans[1])


## Examples of use

if __name__ == "__main__":
    import bluetooth
    btphones = [] #bluetooth.discover_devices()
    serialmodem = '/dev/ttyACM0'
    phones = map(lambda x: 'bt://'+x, btphones) + [ 'serial://' + serialmodem ]

    for phone in phones:
        s = AT(phone)

        # Two ways to send / receive commands:

        ## The first one uses ASCII strings as commands (without the
        ## 'AT' prefix, and you get the results as Python objects:
        
        ### Manufacturer identification
        print s.srcmd("+CGMI")
        ### Model identification
        print s.srcmd("+CGMM")
        ### Subscriber number
        print s.srcmd("+CNUM")
        ### Signal quality
        print s.srcmd("+CSQ")
        # ('OK', [[27, 99]])
        ### Network registration:
        ###  - 2: enable network registration info and location info
        print s.srcmd("+CREG=2")
        ###  - read value
        print s.srcmd("+CREG?")
        
        ## the second one uses Command instances as commands, and you
        ## get the results displayed on you standard output, with an
        ## interpretation when it's possible and implemented
        print_command(s, CNUM)
        print_command(s, CSQ)
        # <- AT+CSQ (Signal quality)
        # -> OK
        # 
        # rssi: -69 dBm
        # ber: not known / not detectable
        print_command(s, CBC)
        # <- AT+CBC (Battery charge)
        # -> OK
        #
        # bcs: Powered by the battery
        # bcl: 96 %
        s.close()
