python

Бюджетная рассылка СМС

  • воскресенье, 8 ноября 2015 г. в 02:10:40
http://habrahabr.ru/post/261387/

Приветствую всех хаброжителей!

Конечно, зализанная тема про рассылку смс сообщений, но как говориться: «много — не мало». Как-то так получилось, что именно она меня постоянно преследует: то одни, то другие добрые люди попросят принять участие (советом, например) в реализации бюджетной рассылки сообщений. И поэтому чтобы не пропадать накопленному добру, оставлю здесь, а вдруг кому-то пригодится…


Итак-с… Опускаем все варианты реализации на базе обычного компа и оси семейства NT. А перейдем сразу к «автономным» системам.

Чем может похвастаться arduino в этом направлении? Отвечу сразу, ОНО работает, но есть нюансы, о которых напишу ниже. Вообщем, имеем китайский вариант arduino 2560 (было перепробовано практически вся линейка) и два дополнительных модуля — сеть W5100 (наиболее стабильный вариант) и GSM SIM 900. Выглядит это все дело как-то так.

image

Задача была следующая:
— устройство должно уметь общаться по http
— отправлять сообщение
— выдавать результат в формате json

Гугл делится всей необходимой информацией, и на выходе получаем следующий код:

Скетч
#include <SPI.h>
#include <Ethernet.h>

#include <String.h>

#include "SIM900.h"
#include <SoftwareSerial.h>
#include "sms.h"

#include <LiquidCrystal_I2C.h>
#include <Wire.h>

byte mac[] = { 0x90, 0xA2, 0x00, 0x00, 0x00, 0x01 };    
IPAddress ip(192,168,34,139);                              

EthernetServer server(80);

char char_in = 0;    
String HTTP_req;    

SMSGSM sms;

boolean started=false;
bool power = false;

LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

void setup() {

  Serial.begin(9600);       
  
  lcd.begin(16,2);
  lcd.setCursor(0,0);
  lcd.print("INIT GSM...");
  lcd.setCursor(0,1);
  lcd.print("WAIT!!!");
  
  //powerUp();
  gsm.forceON();
  
  if (gsm.begin(4800)) {
    Serial.println("\nstatus=READY");
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("READY");    
    started=true;  
  }
  else {
    Serial.println("\nstatus=IDLE");
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("IDLE");
  }
  
  Ethernet.begin(mac, ip); 
  server.begin();         
  
}

void software_reset() {
  asm volatile ("  jmp 0");  
} 

void loop() {
  EthernetClient client = server.available();

  if (client) {
    while (client.connected()) {
      if (client.available()) {
        char_in = client.read();  //
        HTTP_req += char_in;      

        if (char_in == '\n') {  
          
          Serial.println(HTTP_req);
          
          if(HTTP_req.indexOf("GET /res") >= 0) {
            reset_processing(&HTTP_req, &client);
            break;
          }

          if(HTTP_req.indexOf("GET /sms") >= 0) {
            sms_processing(&HTTP_req, &client);
            break;
          }    

          if(HTTP_req.indexOf("GET /test") >= 0) {
            test_processing(&HTTP_req, &client);
            break;
          }   
          else {
            client_header(&client);  
            break;
          }     
        }
      }
    }
    HTTP_req = "";    
    client.stop();
  } 
  
  if(power) {
    delay(1000);
    software_reset();
  }
}

char* string2char(String command) {
  if(command.length()!=0){
    char *p = const_cast<char*>(command.c_str());
    return p;
  }
}

void parse_data(String *data) {
  data->replace("GET /sms/","");
  data->replace("GET /test/", "");

  int lastPost = data->indexOf("\r");
  *data = data->substring(0, lastPost);
  data->replace(" HTTP/1.1", "");
  data->replace(" HTTP/1.0", "");
  data->trim();
}

// explode
 String request_value(String *data, char separator, int index) {
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data->length()-1;

  for(int i=0; i<=maxIndex && found<=index; i++) {
    if(data->charAt(i)==separator || i==maxIndex) {
      found++;
      strIndex[0] = strIndex[1]+1;
      strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data->substring(strIndex[0], strIndex[1]) : "";
}

bool gsm_status() {
  bool result = false;
  switch(gsm.CheckRegistration()) {
    case 1:
      result = true;
      break;
    default:
      break;
  }
  return result;
}

bool gsm_send(char *number_str, char *message_str) {
  bool result = false;
  switch(sms.SendSMS(number_str, message_str)) {
    case 1:
      result = true;
      break;
    default:
      break;
  } 
  return result; 
}

void reset_processing(String *data, EthernetClient *cl) {
  client_header(cl);    
  cl->println("\{\"error\": 0, \"message\": \"restarting...\"\}");  

  power = true;   
}

void test_processing(String *data, EthernetClient *cl) {
  parse_data(data);
  
  if(started) {
    client_header(cl);
    cl->println("\{\"id\":" + request_value(data, '/',0) + ",\"error\":0" + ",\"message\":\"test success\"\}");   
  }
}

void sms_processing(String *data, EthernetClient *cl) {
  parse_data(data);

  if(started) {
    if (gsm_send(string2char(request_value(data, '/', 1)), string2char(request_value(data, '/', 2)))) {
      client_header(cl);
      cl->println("\{\"id\":" + request_value(data, '/',0) + ",\"error\":0" + ",\"message\":\"success\"\}");
    }
    else {
      if(!gsm_status()) {
        client_header(cl);
        cl->println("\{\"id\":" + request_value(data, '/',0) + ",\"error\":2" + ",\"message\":\"gsm not registered\"\}");   
        power = true;
      }
      else {
        client_header(cl);
        cl->println("\{\"id\":" + request_value(data, '/',0) + ",\"error\":1" + ",\"message\":\"fail\"\}");     
      }
    }
  }
}

void client_header(EthernetClient *cl) {
  cl->println("HTTP/1.1 200 OK");
  cl->println("Content-Type: text/plain");
  cl->println("Connection: close");  
  cl->println();
}



Заливаем, упаковываем в коробочку. Вроде бы выглядит красиво, отдаем добрым людям.

image

Что получилось в коде:
— подняли простенький http
— обрабатываем простые GET
— отправляем полученные данные через SERIAL на SIM 900
— отвечаем с помощью «JSON»

И вот тут есть один большой нюанс, перед умными людьми стоит задача реализовать какой-нибудь сервис, чтобы научиться отправлять через это устройство сразу пачку сообщений, но это уже не мои проблемы. В производстве устройство себя повело удовлетворительно.

Наращиваем мощности… Задача полностью аналогичная: повторение — мать учения. Умные люди уже создали классный сервис для работы с предыдущим устройством: очередь, история и прочие полезности.

Итак, на руках имеем raspberry pi, такой же модуль SIM 900 (был взят только ради экспериментов, потому что линукс прекрасно работает с 3g-модемами через USB) и сам 3g-modem huawei e-линейки

image

Снова задаем гуглу нужные вопросы, читаем результаты, определяемся с языком реализации — python — быстро, просто, надежно…

скрипт
import serial, time
from flask import Flask
import RPi.GPIO as GPIO

app = Flask(__name__)


def sim900_on():
    gsm = serial.Serial('/dev/ttyAMA0', 115200, timeout=1)
    gsm.write('ATZ\r')
    time.sleep(0.05)

    abort_after = 5
    start = time.time()
    output = ""

    while True:
        output = output + gsm.readline()
        if 'OK' in output:
            gsm.close()
            return True
        delta = time.time() - start
        if delta >= abort_after:
            gsm.close()
            break


    #GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BOARD)                
    GPIO.setup(11, GPIO.OUT)                
    GPIO.output(11, True)                 
    time.sleep(1.2)
    GPIO.output(11, False) 
    return False


def gsm_send(id, port, phone, msg):

    delay = False
    if 'AMA' in port:
        delay = True


    msg = msg.replace('\\n', '\n')
    msg = msg.replace('\s', ' ')

    gsm = serial.Serial('/dev/tty%s' % port, 115200, timeout=1)
    gsm.write('ATZ\r')

    if delay:
        time.sleep(0.05)

    gsm.write('AT+CMGF=1\r\n')

    if delay:
        time.sleep(0.05)

    gsm.write('AT+CMGS="%s"\r\n' % phone)

    if delay:
        time.sleep(0.05)

    gsm.write(msg + '\r\n')

    if delay:
        time.sleep(0.05)

    gsm.write(chr(26))

    if delay:
        time.sleep(0.05)

    abort_after = 15
    start = time.time()
    output = ""

    while True:

        output = output + gsm.readline()
        #print output
        if '+CMGS:' in output:
            print output
            gsm.close()
            return '{"id":%s,"error":0,"message":"success", "raw":"%s"}' % (id, output)
        if 'ERROR' in output:
            print output
            gsm.close()
            return '{"id":%s,"error":0,"message":"fail", "raw":"%s"}' % (id, output)

        delta = time.time() - start
        if delta >= abort_after:
            gsm.close()
            return '{"id":%s,"error":1,"message":"timeout", "raw":"%s"}' % (id, output)

@app.route('/sms/<id>/<port>/<phone>/<msg>',methods=['GET'])
def get_data(id, port, phone, msg):
    return gsm_send(id, port, phone, msg)

@app.route('/',methods=['GET'])
def index():
    return "Hello World"

if __name__ == "__main__":
    sim900_on()
    app.run(host="0.0.0.0", port=8080, threaded=True)



Скармливаем питону, запускаем с помощью «start-stop-daemon», придаем дружелюбный вид, отдаем добрым людям…

image

Получилось практически один в один, только за счет шины USB систему можно расширять. В производстве претензий вообще не оказалось — все были ОЧЕНЬ довольны.

Устройство получилось настолько удачным, что появилось желание использовать это дело в «личных» интересах, а именно внедрить в систему мониторинга данный аппарат. Но надо было избавиться от главного нюанса — отсутствие очереди сообщений. Принцип реализации я взял у одного известного вендора (он предлагал программно-аппаратный комлекс, часть которого поднимала smtp-сервер для обработки уведомлений и отправки ее на gsm-устройство). Такая схема встраивается в любую систему мониторинга.

Итак, нужные знания уже давно получены, приступаем к реализации.

SMTP-демон
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

import smtpd
import asyncore
import email
import MySQLdb
import subprocess

def InsertNewMessage(phone, msg):
    conn = MySQLdb.connect(host="localhost", # your host, usually localhost
                     user="sms", # your username
                      passwd="sms", # your password
                      db="sms") # name of the data base
    c = conn.cursor()
    c.execute('insert into message_queue (phone, message) values ("%s", "%s")' % (phone, msg))
    conn.commit()
    conn.close()

class CustomSMTPServer(smtpd.SMTPServer):

    def process_message(self, peer, mailfrom, rcpttos, data):

        msg = email.message_from_string(data)
        phone = rcpttos[0].split('@',1)[0]
        addr = mailfrom

        for part in msg.walk():
            if part.get_content_type() == "text/plain": # ignore attachments/html
                body = part.get_payload(decode=True)


        InsertNewMessage(phone, str(body))

subprocess.Popen("/home/pi/daemons/sms/pygsmd.py", shell=True)

server = CustomSMTPServer(('0.0.0.0', 25), None)

asyncore.loop()



Демонизация происходит, как я уже писал выше, с помощью «start-stop-daemon», а сам smtp скрипт запускает подпроцесс для работы с базой сообщений.

gsm скрипт
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

import serial
import time
import MySQLdb
import commands


def gsm_send(port, phone, msg):

    print 'Sending message: %s to: %s' % (msg, phone)
    gsm = serial.Serial('/dev/tty%s' % port,
                        460800,
                        timeout=5,
                        xonxoff = False,
                        rtscts = False,
                        bytesize = serial.EIGHTBITS,
                        parity = serial.PARITY_NONE,
                        stopbits = serial.STOPBITS_ONE )
    gsm.write('ATZ\r\n')
    time.sleep(0.05)
    gsm.write('AT+CMGF=1\r\n')
    time.sleep(0.05)
    gsm.write('''AT+CMGS="''' + phone + '''"\r''')
    time.sleep(0.05)
    gsm.write(msg + '\r\n')
    time.sleep(0.05)
    gsm.write(chr(26))
    time.sleep(0.05)

    abort_after = 15
    start = time.time()
    output = ""


    while True:

        output = output + gsm.readline()
        #print output
        if '+CMGS:' in output:
            #print output
            gsm.close()
            return 0
        if 'ERROR' in output:
            #print output
            gsm.close()
            return 1

        delta = time.time() - start
        if delta >= abort_after:
            gsm.close()
            return 1

def msg_delete(list):
    conn = MySQLdb.connect(host="localhost",
                     user="sms",
                      passwd="sms",
                      db="sms")
    c = conn.cursor()
    c.execute("delete from  message_queue where id in %s;" % list)
    conn.commit()
    conn.close()

def msg_hadle():
    list = tuple()
    conn = MySQLdb.connect(host="localhost",
                     user="sms",
                      passwd="sms",
                      db="sms")
    c = conn.cursor()
    c.execute("select * from message_queue")

    numrows = int(c.rowcount)
    if numrows > 0:
        for row in c.fetchall():
            result = gsm_send('USB0', ('+' +  row[1]),  row[2])
            if result == 0:
                list +=(str(row[0]),)

    conn.close()

    if len(list) == 1:
        qlist = str(list).replace(',','')

    if len(list) > 1:
        qlist = str(list)

    if len(list) > 0:
        msg_delete(qlist)

    del list

while True:
    try:
        msg_hadle()
    except:
        print "mysql error"

    time.sleep(10)



В связке с моей системой мониторинга устройство ведет себя адекватно, хотя работает не так давно. Надеюсь, материал будет полезен кому-нибудь.