Como armar un gateway SMS con tu celular

jueves, 17 de septiembre de 2009
Estuve averiguando y al parecer la única manera de hacer una aplicación que reciba mensajes de texto y pueda responder a los mismos, es apersonarse muy bien trajeado en una empresa que brinde servicios de alertas SMS y cuestiones afines, y firmar un jugoso contrato para obtener el muy preciado servicio. Ojo, enviar mensajes SMS desde una computadora es mucho más sencillo: la mayoría de los carriers permiten hacerlo enviando un correo electrónico a una dirección conveniente. El problema está en recibir los benditos textos: ¿qué pasa si quiero hacer el Twitter argentino, y quiero que mis usuarios posteen actualizaciones desde su celular? ¿qué pasa si quiero preguntarle a mi máquina como va la descarga de mi torrent?

El servicio necesario para enviar o recibir SMS desde una computadora conectada a Internet se denomina gateway SMS, y si bien el servicio de envío lo proveen las mismas compañías de telefonía celular, la recepción es más delicada, ya que nadie provee un servicio de acceso sencillo para forwardearme los mensajes que lleguen a un determinado número a mi aplicación. A continuación voy a explicar como se puede fabricar un gateway SMS fatto in casa utilizando un celular común y silvestre.

Ingredientes
  • Una computadora con un adaptador Bluetooth (o no, ver nota a continuación)
  • Un celular con Bluetooth
  • Python + python-bluez
En mi caso utilicé mi maltrecho Nokia 6103, y la solución que propongo debería funcionar en cualquier celular con dos dedos de frente (o sea, la gran mayoría de los Nokia y Sony Ericcsson, y tal vez algún que otro Motorola).

Técnicamente no es necesario usar bluetooth puesto que lo que vamos a hacer es enviar comandos AT estándar sobre un enlace serial. Esto se puede hacer tanto por Bluetooth como con el cable adaptador USB o serial correspondiente, artículo del que no dispongo. Debería ser trivial portar las ideas de mi programa para funcionar con un enlace cableado.

Como sistema operativo utilicé Ubuntu 9.04, pero debería funcionar en casi cualquier sistema actual puesto que las librerías necesarias funcionan también en Windows.

Pasos a seguir

El funcionamiento del gateway que propongo es sencillo; se conecta al teléfono mediante la conexión inalámbrica y puede realizar una de tres acciones:
  • Enviar un mensaje de texto a un número determinado
  • Obtener todos los mensajes de texto almacenados en el teléfono
  • Borrar todos los mensajes de texto almacenados en el teléfono
El único paso manual consiste en identificar la dirección de hardware del teléfono para indicarla al conectarse. En Linux esto puede hacerse habilitando Bluetooth tanto en el celular como en la computadora, activando en el teléfono el modo "visible", y finalmente ejecutando el comando hcitool scan.

Tras ingresar dicho valor en el script que pongo a continuación, se realizará la conexión con el celular. Es probable que el celular requiera una aprobación manual para cada vez que se realice una conexión (un mensaje del tipo "¿Desea conectarse con el dispositivo XXX?"). Como esto no es muy cómodo en un servidor, en los teléfonos Nokia esta opción puede desactivarse una vez que se ha establecido la conexión (en el apartado "Conexiones activas" del menú Bluetooth, se puede indicar que un cierto dispositivo no requiere confirmación).

El código que adjunto a continuación hace el resto del trabajo, gran parte del cual consiste en parsear el extraño formato PDU en que el teléfono entrega los mensajes de texto cuando se le pide. El código que hace esto está fuertemente basado en smspdu, que modifiqué para hacer un poco más amigable.

Seguir leyendo >>

gateway.py

#!/usr/bin/python

#Copyright (c) 2009 Gonzalo Sainz-Trapaga
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.

import bluetooth
import select
import pdu

DEBUG = False

class Nokia6103:
    def __init__(self, hwaddr, port):
        self.sockfd = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
        self.sockfd.connect((hwaddr, port))
        self._send('ATZ')

    def _log(self, s):
        if DEBUG:
            print s

    def _read(self):
        i = [self.sockfd]
        w = []
        e = [self.sockfd]
        out = ''
        while True:
            ir, wr, er = select.select(i, w, e, 3)
            if len(ir) == 0:
                self._log("Select: espera finalizada - saliendo.")
                break
            if len(er) > 0:
                self._log("Select: condicion de excepcion - saliendo.")
                break
    
            out += i[0].recv(1000)
            if out.find('OK\r\n') != -1:
                self._log("Select: OK alcanzado - saliendo.")
                break
        return out
    
    
    def _send(self, s):
        self.sockfd.send('%s\r' % s)
        out = self._read()
        self._log(out)
        return out

    def sendSMS(self, num, txt):
        self._send('AT+CMGF=1')
        self._send('AT+CMGS="%s"' % num)
        self.sockfd.send(txt + "\n")
        self.sockfd.send(chr(26))

    def getAllSMS(self):
        s = self._send('AT+CMGL=4')
        lines = s.split('\r\n')
        lines.pop(0)
        msgs = []
        for i, msg in enumerate(lines):
            if i % 2 == 0:
                if not msg.startswith('+CMGL'):
                    break
            else:
                msgs.append(pdu.decodePdu(msg))
        return msgs

    def deleteAllSMS(self):
        self._send('AT+CMGD=1,4')

    def close(self):
        self.sockfd.close()

if __name__ == '__main__':
    port = 1
    hwaddr = '00:19:B7:XX:XX:XX'
    n = Nokia6103(hwaddr, port)
    print n.sendSMS('+541150501234', 'Mensaje de prueba!')
    print n.getAllSMS()

pdu.py

#Copyright (c) 2009 Eric Gradman, Gonzalo Sainz-Trapaga
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
from cStringIO import StringIO
from math import ceil
from binascii import unhexlify, hexlify
from itertools import cycle
from datetime import datetime


def decodePdu(s):
    s = unhexlify(s)
    d = StringIO(s)

    # parse SMSC information
    p = {}
    p['smsc_len'] = d.read(1)
    p['type_of_address'] = d.read(1)
    p['sc_num'] = unsemi(d.read(ord(p['smsc_len'])-1))

    p['msg_type'] = d.read(1)
    p['address_len'] = d.read(1)
    p['type_of_address'] = d.read(1)

    p['sender_num'] = unsemi(d.read(int(ceil(ord(p['address_len'])/2.0))))
    p['pid'] = d.read(1)
    p['dcs'] = d.read(1)
    ts = d.read(7)
    p['ts'], p['tz'] = parseTimeStamp(ts)

    p['udl'] = d.read(1)
    p['user_data'] = d.read(ord(p['udl']))
    p['user_data'] = decodeUserData(p['user_data'])

    
    for f in ['sc_num', 'sender_num']:
        if p[f].endswith('f'):
            p[f] = p[f][:-1]
    
    return p

def unnibleSwapChar(c):
    c = ord(c)
    d1 = c & 0x0F
    d2 = c >> 4
    return int(str(d1) + str(d2))

def parseTimeZone(c):
    c = ord(c)
    d1 = c & 0x0F
    d2 = c >> 4

    neg = d1 >> 3
    d1 = d1 & 0x7
    
    units = int(str(d1) + str(d2))
    if neg:
        zona = '-'
    else:
        zona = ''
    zona += str(units // 4)
    zona += ':'
    zona += "%.02d" % ((units % 4) * 15)

    return zona


def parseTimeStamp(s):
    ts = s[:6]
    tz = s[-1:]

    f = [unnibleSwapChar(c) for c in ts]
    f[0] = f[0] + 2000

    zona = parseTimeZone(tz)
    return datetime(*f), zona

def decodeUserData(s):
    bytes = map(ord, s)
    strips = cycle(range(1,9))
    out = ""
    c = 0    # carry
    clen = 0 # carry length in bits
    while len(bytes):
      strip = strips.next()
      if strip == 8:
        byte = 0
        ms = 0
        ls = 0
      else:
        byte = bytes.pop(0)
        # take strip bytes off the top
        ms = byte >> (8-strip)
        ls = byte & (0xff >> strip)
      #print "%d byte %x ms %x ls %x" % (strip, byte, ms, ls)

      # append the previous
      byte = ((ls << clen) | c) & 0xff
      out += chr(byte)

      c = ms
      clen = strip % 8

    if strip == 7:  out += chr(ls) # changed 6/11/09 to incorporate Carl's suggestion in comments
    return out

def unsemi(s):
    """turn PDU semi-octets into a string"""
    l = list(hexlify(s))
    out = ""
    while len(l):
      out += l.pop(1)
      out += l.pop(0)
    return out

8 comentarios:

Marcelo Fernández dijo...

Groso, te felicito!

Saludos

kaki dijo...

justo con un amigo estabamos comenzando a investigar el tema... estabamos en su casa y empezamos a jugar mas que nada con el identificador de llamadas, realizar llamadas, y creo que llegamos a jugar un poco con sms, via bluetooth y comando at.
Ahora, mucho no recuerdo porque lo dejamos así nomas, pero creo que existia la posibilidad de obtener el string, sin ese formato feo pdu.

Chequeo y vuelvo.

Cuando arranque mi portatil para seguir jugando pero desde un entorno gnu/linux sinceramente me mareé, asique tu codigo me va a ser muy valioso.

Gracias por la data.

kaki dijo...

bueno, voli...
AT+CMGF=? retorna 1, 0 o ambos.
Ahora, lo que importa:
0: permite trabajar en modo PDU.
1: permite trabajar con texto.

En mi caso, mi nokia 6131, retorna (0,1).

Para cambiar entre un y otro modo: AT+CMGF=1

seguiremos jugando con este temilla.

Solo falta la pasarela de audio over bluetooth jajajaja

GomoX dijo...

Hola, mi celular también retorna esas 2 opciones, pero me daba error si intentaba usar el formato texto para leer mensajes (no así para enviarlos). Esa es la razón por la que utilicé el decoder de formato PDU.

En caso de que les interese acá hay mucha información sobre comandos AT (y las miles de posibilidades que hay, de las cuales lo que yo hice es apenas una probadita):
http://www.developershome.com/sms/atCommandsIntro.asp

kaki dijo...

yo la opcion PDU no se porque pero no la estoy podiendo utilizar... la opcion texto tampoco, pero como llueve yd ecidi no ir a la facu me colgue toqueteando con esto... :)

Anónimo dijo...

Hola, una consulta.
Estaba probando esto en un 6131 tambien.

Pero el AT+CMGL=4 o bien en modo texto con "ALL", me devuelve ok con hyperterminal, pero nunca lee ningun mensaje.

Mis mensajes se almacenan en la memoria del equipo, no en el SIM. Y segun encontre en los foros de Nokia este telefono esta preparado para leer los mensajes solo del SIM. Tambien vi un comando para cambiar la ubicacion de lectura del storage pero no me da bola.

Cuando hiciste esta prueba tu Nokia permitia guardar los mensajes en la SIM?

Muchas gracias!

Anónimo dijo...

Cual es l SMS Gateway de la compañia Open Mobile

Anónimo dijo...

como lo ejecuto?? soy nuevo en esto O_o

Publicar un comentario