воскресенье, 12 августа 2012 г.

SMS+python - закрывая тему

Наконец-то руки дошли разобраться с получением списка SMS-сообщений из моего Huawei E1550. Всё это дело (и отправку, и прием) свёл в один маленький модуль с претенциозным названием smsutils.py.

Пользоваться им совсем просто:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import smsutils

# отправляем SMS
smsutils.SendSMS('Привет, мир!', '79031234567', '/dev/ttyUSB0')

# читаем SMS с карты
result = smsutils.GetSMS('/dev/ttyUSB0')

for r in result:
    print r[0], ' ', r[1], ' ', r[2], '\n', r[3]

Исходный код smsutils.py таков:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
#
# Набор вспомогательных функций для работы с SMS
#
# большое спасибо:
#     http://www.varesano.net/blog/fabio/serial%20rs232%20connections%20python
#     http://www.dreamfabric.com/sms/

import serial, datetime, time, random


# Преобразование номера телефона в международном формате в формат SMS
#
# Исходная строка, содержащая телефон в международном формате 79130123456,
# дополняется справа символом F                                                            - 79130123456F,
# разбивается на пары символов                                                             - 79 13 01 23 45 6F,
# в каждой паре символы меняются местами                                                   - 97 31 10 32 54 F6,
# слева приписывается идентификатор международного формата (91)                            - 91 97 31 10 32 54 F6,
# слева приписывается количество цифр в телефоне, т.е. 11 в шестнадцатеричном формате (0B) - 0B 91 97 31 10 32 54 F6
#
# Возвращаемое значение - строка, содержащая закодированный номер телефона 0B919731103254F6
#
def PhoneNumberToSMS(number):
    number += 'F'
    result = '0B' + '91'
    i = 0
    while i < len(number):
        result += number[i+1] + number[i]
        i += 2
    return result


# Преобразование строки символов в формат SMS
#
# Каждый двухбайтовый юникодный символ в строке разбивается на пару байт,
# и формируется новая строка, состоящая из шестнадцатеричных представлений этих байтов
#
# Возвращаемое значение - строка, содержащая строку символов в формате SMS
#
def TextToSMS(text):
    b = text
    result = ''
    i = 0
    while i < len(b):
        o = ord(b[i])
        result += ("%0.2X" % (o/256)) + ("%0.2X" % (o%256))
        i += 1
    return result


# Восстановление номера телефона в международном формате в из формата SMS
#
# Исходная строка, содержащая закодированный телефон 9731103254F6
# разбивается на пары символов           - 97 31 10 32 54 F6,
# в каждой паре символы меняются местами - 79 13 01 23 46 6F,
# убирается символ F                     - 79130123456
#
# Возвращаемое значение - строка, содержащая номер телефона 79130123456
#
def SMSToPhoneNumber(data):
    result = ""
    i = 0
    while i < len(data):
        result += data[i+1] + data[i]
        i += 2
    return result[:-1]


# Восстановление строки символов в из формата SMS
#
# Исходная строка разбивается на четверки символов, которые преобразуются в целые числа
# и формируется строка, состоящая из соответствующих этим числам символов
#
# Возвращаемое значение - раскодированная строка
#
def SMSToText(text):
    result = u''
    i = 0
    while i+3 < len(text):
        result += unichr(int(text[i] + text[i+1] + text[i+2] + text[i+3],16))
        i += 4
    return result


# преобразование целого числа в строку из нулей и единиц, соответствующую его двоичной записи
# (использовалось для отладки)
def ByteToBitsString(byte, n):
    result = ''
    for i in range(0,n):
        if byte & (1 << i) != 0:
            result = '1' + result
        else:
            result = '0' + result
    return result


# Восстановление строки из её семибитного кода
#
# На вход подается закодированная строка  - 4DEA10
# эта строка разбивается на пары символов - 4D EA 10,
# каждая пара трактуется как шестнадцатиричное представление байта - 0x4D 0xEA 0x10 = 01001101 11101010 00010000
# из первого байта 01001101 берутся семь младших битов 1001101 и преобразуются в соответствующий символ ASCII - M
# оставшийся бит 0 дополняется слева шестью младшими битами второго байта 101010: 1010100 - T
# оставшиеся два бита 11 дополняются слева пятью младшими битами третьего байта 10000: 1000011 - С
# и т.д.
#
# Возвращаемое значение - раскодированная строка MTC
#
def Decode7bit(text):
    result = ''

    bytes = [int(text[i*2:i*2+2],16) for i in range(0,len(text)/2)]

    symbol = 0
    bits   = 0
    n      = 0

    while n < len(bytes):

        if bits == 7:
            result += chr(symbol)
            symbol = 0
            bits   = 0
        else:
            symbol += (bytes[n] & (0x7F >> bits)) << bits
            result += chr(symbol)
            symbol = (bytes[n] & (0x7F << (7-bits))) >> (7-bits)
            bits   = (8-7) + bits
            n += 1

    if bits > 0 and symbol != 0:
        result += chr(symbol)

    return result


# Класс для работы с часовыми поясами (см следующую функцию)
class smsTZ(datetime.tzinfo):
    hours = 0
    def __init__(self, h):
        self.hours = h
    def utcoffset(self, dt):
        return datetime.timedelta(hours=self.hours)
    def dst(self, dt):
        return datetime.timedelta(0)

# Восстановление даты и времени из их представления SMS
#
# На вход подается закодированная строка  - 11113131516461
# эта строка разбивается на пары символов - 11 11 31 31 51 64 61,
# в каждой паре символы меняются местами  - 11 11 13 13 15 46 16,
# получившиеся строки трактуются как шестнадцатиричные представления байтов - 0x11 0x11 0x13 0x13 0x15 0x46 0x16
# эти байты представляют собой соответственно год, месяц, день, часы, минуты, секунды, часовой пояс
# часовой пояс представляется как количество четвертей часа, т.е. 0x16 = GMT+4 (седьмой бит отвечает за знак)
#
# Возвращаемое значение - дата и время 2011-11-13 13:15:46+04:00
#
def SMSToTimeStamp(text):
    year   = int(text[1] + text[0]) + 2000
    month  = int(text[3] + text[2])
    day    = int(text[5] + text[4])
    hour   = int(text[7] + text[6])
    minute = int(text[9] + text[8])
    second = int(text[11] + text[10])
    tz     = int(text[13] + text[12])
    tz = ( (tz & 0x7F) if (tz & 0x80 == 0) else -(tz & 0x7F) ) / 4
    return datetime.datetime(year, month, day, hour, minute, second, 0, smsTZ(tz))


# Обмен с последовательным портом
def str_send (ser, textline):
    ser.write(textline)

    out = ''
    # let's wait one second before reading output (let's give device time to answer)
    N = 10
    while N > 0:
        time.sleep(1)
        while ser.inWaiting() > 0:
            out += ser.read(1)

        if ('OK' in out) or ('ERROR' in out) or ('>' in out):
            N = 1

        N -= 1

    return out


# отправка пин-кода в открытый порт
def SendPINToPort(ser, pin):
    str_send(ser, 'AT+CPIN="%s"\r' % (pin))


# отправка пин-кода модему
def SendPIN(serial_name, pin):

    # подключаемся к порту
    ser = serial.Serial(serial_name, 115200, timeout=1)
    ser.open()

    # отправляем пин-код
    SendPINToPort(ser, pin)

    # закрываем порт
    ser.close()


# отправка SMS-сообщения
def SendSMS(msg, phone, serial_name, pin=None):

    # если нечего или некуда отправлять, выходим
    if msg == '' or len(phone) != 11:
        return

    # декодируем сообщение в utf-8
    message = msg.decode('utf-8')

    # разрезаем сообщение на кусочки по 66 символов
    chunks = []
    if len(message) > 70:
        while len(message) > 66:
            chunks.append(message[:66])
            message = message[66:]
    if len(message) > 0:
        chunks.append(message)

    # инициализируем служебную информацию
    SMS_SUBMIT_PDU = "11"
    CSMS_reference_number = ""

    # если сообщение требует конкатенации SMS, то подправляем служебную информацию
    # и генерируем четырехсимвольный номер сообщения
    if len(chunks) > 1:
        SMS_SUBMIT_PDU = "51"
        CSMS_reference_number = "%0.4X" % random.randrange(1,65536)

    # подключаемся к порту
    ser = serial.Serial(serial_name, 115200, timeout=1)
    ser.open()

    # устанавливаем формат передачи сообщения - PDU
    status = str_send(ser, 'AT+CMGF=0\r')

    # если в ответ пришел текст, содержащий SIM PIN REQUIRED, значит, модему нужен пин-код
    if 'SIM PIN' in status:
        SendPINToPort(ser, pin)
        str_send(ser, 'AT+CMGF=0\r')


    # отсылаем сообщение по кусочкам
    i = 1
    for chunk in chunks:

        # кодируем кусочек
        emessage = TextToSMS(chunk)

        # если сообщение состоит из нескольких кусочков, то в каждом кусочке надо указать
        # номер сообщения, количество кусочков и порядковый номер кусочка (1,2,3 и т.д.)
        if CSMS_reference_number != "":
            emessage = "06" + "08" + "04" + CSMS_reference_number + \
            ("%0.2X" % len(chunks)) + ("%0.2X" % i) + emessage

        # готовим строку для отправки в порт
        sms =                             \
            "00" +                        \
            SMS_SUBMIT_PDU +              \
            "00" +                        \
            PhoneNumberToSMS(phone) +     \
            "00" +                        \
            "08" +                        \
            "AA" +                        \
            "%0.2X" % (len(emessage)/2) + \
            emessage

        # подготавливаем модем - передаем ему длину отправляемой строки
        str_send(ser, 'AT+CMGS=' + str(len(sms)/2-1) + '\r')

        # отправляем строку
        str_send(ser, sms + '\x1A')

        i += 1

    # закрываем порт
    ser.close()



# чтение SMS сообщений с сим-карты
#
# возвращаемое значение: список, состоящий из кортежей, каждый из которых содержит
# номер слота на сим-карте, в котором находится сообщение (0-19),
# номер телефона отправителя,
# дату отправления сообщения,
# текст сообщения
#
def GetSMS(serial_name, pin=None):

    result = []

    # подключаемся к порту
    ser = serial.Serial(serial_name, 115200, timeout=1)
    ser.open()

    # устанавливаем формат передачи сообщения - PDU
    status = str_send(ser, 'AT+CMGF=0\r')

    # если в ответ пришел текст, содержащий SIM PIN REQUIRED, значит, модему нужен пин-код
    if 'SIM PIN' in status:
        SendPINToPort(ser, pin)
        str_send(ser, 'AT+CMGF=0\r')

    # запрашиваем список сообщений (4 - все сообщения)
    messages = str_send(ser, 'AT+CMGL=4\r')

    if 'ERROR' not in messages:

        strings = messages.split('\n')

        i = 0

        while i < len(strings):

            if '+CMGL: ' in strings[i]:

                message_header = strings[i][7:]
                message_body = strings[i+1]

                offset = 0

                SMSC_length = int(message_body[offset:offset+2],16)
                offset += 2

                SMSC_address = message_body[offset:offset+2*SMSC_length]
                SMSC_typeOfAddress = SMSC_address[:2]
                SMSC_serviceCenterNumber = SMSToPhoneNumber( SMSC_address[2:] )
                offset += 2*SMSC_length

                SMS_deliverBits = int(message_body[offset:offset+2],16)
                offset += 2

                SMS_senderNumberLength = int(message_body[offset:offset+2],16)
                offset += 2

                SMS_senderNumberType = message_body[offset:offset+2]
                offset += 2

                SMS_senderNumber = message_body[offset:offset+SMS_senderNumberLength+(1 if SMS_senderNumberLength & 1 != 0 else 0) ]
                if SMS_senderNumberType == '91':
                    SMS_senderNumber = SMSToPhoneNumber(SMS_senderNumber)
                if int(SMS_senderNumberType[0],16) & 5 == 5:
                    SMS_senderNumber = Decode7bit(SMS_senderNumber)
                offset += SMS_senderNumberLength+(1 if SMS_senderNumberLength & 1 != 0 else 0)

                TP_protocolIdentifier = message_body[offset:offset+2]
                offset += 2

                TP_dataCodingScheme = int(message_body[offset:offset+2],16)
                offset += 2

                TP_serviceCenterTimeStamp = SMSToTimeStamp(message_body[offset:offset+14])
                offset += 14

                TP_userDataLength = int(message_body[offset:offset+2],16)
                offset += 2

                if SMS_deliverBits & 64 != 0:
                    SMS_userDataHeaderLength = int(message_body[offset:offset+2],16)
                    offset += 2
                    SMS_userDataHeader = message_body[offset:offset+2*SMS_userDataHeaderLength]
                    offset += 2*SMS_userDataHeaderLength

                message_text = None
                if (TP_dataCodingScheme == 0):
                    message_text = Decode7bit(message_body[offset:])
                if (TP_dataCodingScheme & 8 != 0):
                    message_text = SMSToText(message_body[offset:])
                if message_text is None:
                    message_text = message_body[offset:]

                # добавляем в результирующий список кортеж, содержащий
                # номер слота на сим-карте, в котором находится сообщение (0-19),
                # номер телефона отправителя,
                # дату отправления сообщения,
                # текст сообщения
                result.append((message_header.split(',')[0], SMS_senderNumber, TP_serviceCenterTimeStamp, message_text))

                i += 2

            else:

                i += 1

    # закрываем порт
    ser.close()

    return result



# удаление SMS сообщения в слоте с номером slot с сим-карты
def DeleteSMS(serial_name, slot, pin=None):

    # подключаемся к порту
    ser = serial.Serial(serial_name, 115200, timeout=1)
    ser.open()

    # удаляем сообщение
    status = str_send(ser, 'AT+CMGD=%s\r' % (slot))

    # если в ответ пришел текст, содержащий SIM PIN REQUIRED, значит, модему нужен пин-код
    if 'SIM PIN' in status:
        SendPINToPort(ser, pin)
        str_send(ser, 'AT+CMGD=%s\r' % (slot))

    # закрываем порт
    ser.close()