树莓派.GPRS.短信接收器
起因 曾经用过西门子出的短信猫,好处是直接有SDK开发包,不会硬件开发也能直接使用 缺点也是明显的,就是只支持Windows系统,另外就是在Windows下工作很不稳定,隔开几天就会出现收不到短信的毛病,要断电重启设备才有机会恢复(还不是必然恢复) 后来在地府(DFRobot)发现了新品"Gravity: UART A6 GSM & GPRS 无线通信模块",买来试了一下发现可用(不过不清楚地府的A6和外面常见的SIM800系列、SIM900系列有什么不同), 而且可以自己写驱动支持Linux下运行,完美 期间也碰到一些小坑, 记录一下。 需求清单 - 自动初始化GPRS模块 - 接收短信并能解析出重要元素(包括:来电号码/时间/短信内容) - 把解析到的短信内容上传到服务器保存 - 清除已阅短信 - 支持Linux系统 - 开发语言:JAVA 硬件清单 - 树莓派2代B型(3代串口使用上有区别,需要另外的方法处理) 接线方法 (树莓派40PIN引脚图) (A6引脚说明) 树莓派 A6 GPIO15 RX GPIO16 TX GND GND 重要: A6模块的电源需要单独供电!!! A6模块不能直接从树莓派上的GPIO 5V针脚接电,因为电流不足! 最开始的时候, 我是从树莓派上取电供给A6, 结果串口怎么都无法通信,刚开始还以为是波特率的问题,结果折腾了半天后, 留意到A6上有个蓝灯(上面写着SLEEP)有明暗变化, 不稳定,感觉像是电压不稳定一样, 果断试了一下把A6外接电源,然后A6才正常工作! 可以从蓝灯看得出来,亮度较高,且稳定(不闪烁) 资料准备 - 树莓派系统(用NOOBS或Raspbian都可以) - pi4j (JAVA支持包) - AT指令知识 树莓派系统安装方法可以自行搜索,或看我之前发过的文章 系统装好后还涉及到如何把GPIO15(TX)和GPIO16(RX)启用的问题, 见这篇文章:《两个树莓派通过串口通信》 pi4j是个能让JAVA访问树莓派40个GPIO的支持包, 可以上官网下载安装,传送门>>> 最重要和容易掉坑的是关于接收短信的AT指令部分,下面要详细讲解 这里需要做的功能是利用A6接收短信,涉及到以下指令 * 第一步:初始化GPRS.模块 * AT 握手 / SIM卡检测等 ------以上指令用于初始化模块,一般接线没问题,波特率设置没问题的话都是比较容易调通 * 第二步:初始化GPRS.设置短信模式及短信接收参数 * AT+CMGF=1 0-PDU,1-文本格式 极易掉坑系列,逐个讲解: 设置短信格式指令:AT+CMGF=1 分2种短信格式: 0-PDU,1-文本格式 如果设置的是PDU模式, 那么你收到的短信就是这样 的: +CIEV: "MESSAGE",1 +CMT:,32 如何读懂PDU要另外翻阅专业文章, 这里如果你不使用发短信功能的话,建议不要采用PDU格式 PDU格式的好处是可以发中文短信! 好, 如果设置是文本格式,收到的短信就类似下面这样的: +CIEV: "MESSAGE",1 +CMT: "test again ,中文也试试 直接就能看到短信内容,中文也一样可以显示出来(注意它可以GB2312或GBK编码) 然后就有问题产生了, 新短信来时是上面这样的格式, 短信内容是可以获取了, 但特么为什么看不出是谁(手机号)发来的呢?下面就是入坑的时候: 看不到手机号怎么办, 你可以试试这个指令: AT+CMGL="ALL" 它能读出存在SIM卡上的短信(包括已读和未读,以及外发时存着的短信),执行后收到的内容大概是这个样子: +CMGL: 2,"REC READ","106907931100","2017/06/02,10:15:21+08",160,134
但是这时你会发现刚收到的信息不一定在这个清单里! 这是怎么回事呢? 我反正查阅了很多资料,费了大量的时间也不知怎么回事 后来才发现这个指令(查看SIM卡内存情况): AT+CPMS? 执行后你可能会看到这个结果: +CPMS: "MT",50,"ME",50 OK 重点关注"SM"后面第1个数字"1"代表当前存了多少条短信,第2个数字"50"代表存储上限 "SM"表示SIM卡,其它2个一个代表手机设备,另一个是手机内存 然后你给A6发个新短信,有可能发现这个"1"不会增加! 为什么收到的新短信不存到SIM卡上呢? 然后就找到这个指令 AT+CNMI=<mode>,<mt>,<bm>,<ds>,<bfr> 这个指令比较复杂,它负责设置收到新短信后的处理机制,下面是参数含义 <mode>控制通知TE的方式. 相信看完你已经蒙圈,我就是,如果你看得懂,那恭喜了! 我在这里采用的参数是 AT+CNMI=2,1 收接通知,并存在指定位置 这时再测试一下发条新短信给A6 +CIEV: "MESSAGE",1 +CMTI: "SM",0 现在不显示短信内容了(反正显示也没用,因为没来电号码),但"SM"后面仍然是0! 这时再用AT+CMGL="ALL" 你会发现短信依然没存到卡上,结果当然也没法看到短信内容及来电号码等信息啦 这是怎么回事裂 后来发现这个AT+CNMI跟刚才说的指令(AT+CPMS)息息相关,再来查一下: AT+CPMS? OK 注意看,如果你看到的和上面差不多,会发现有"MT"和"ME"存在,这时收到短信虽然在CNMI告诉A6收到短信要存下来啊! 但是A6找不到"MT"和"ME",结果存储失败! 我们现在是希望它收到短信后能存在SIM卡上,所以要设置一下: AT+CPMS="SM","SM" 再发条短信试试效果: +CIEV: "MESSAGE",1 现在看到"SM"后面是1了! 后面这个1表示的是短信存储的SIM卡内存的位置 然后可以用指令查看短信内容了,这里有2种方法 方法1,单条读取(AT+CMGR=index) AT+CMGR=1 +CMGR: "REC UNREAD","18620671820","2017/10/26,11:37:03+08",161,17,"+8613010200500",145,25 方法2,全部读取(AT+CMGL="ALL") +CMGL: 2,134 小结一下正确获取短信的姿势(流程): AT+CMGF=1 PS: 其中有一条指令没解释:AT+CSDH=1 这个留给大家查资料 好,接下来只需要写出Java代码分析短信内容即可. 程序部分 import java.io.IOException; import java.util.Date; import com.common.DateTimeUtil; import com.common.StringUtil; import com.pi4j.io.serial.Baud; import com.pi4j.io.serial.DataBits; import com.pi4j.io.serial.FlowControl; import com.pi4j.io.serial.Parity; import com.pi4j.io.serial.Serial; import com.pi4j.io.serial.SerialConfig; import com.pi4j.io.serial.SerialFactory; import com.pi4j.io.serial.SerialPort; import com.pi4j.io.serial.StopBits; import com.pi4j.util.CommandArgumentParser; import com.pi4j.util.Console; /** * This example code demonstrates how to perform serial communications using the Raspberry Pi. * * @author Robert Savage */ public class SerialListenSMS { /** * This example program supports the following optional command arguments/options: * "--device (device-path)" [DEFAULT: /dev/ttyAMA0] * "--baud (baud-rate)" [DEFAULT: 38400] * "--data-bits (5|6|7|8)" [DEFAULT: 8] * "--parity (none|odd|even)" [DEFAULT: none] * "--stop-bits (1|2)" [DEFAULT: 1] * "--flow-control (none|hardware|software)" [DEFAULT: none] * * @param args * @throws InterruptedException * @throws IOException */ public static void main(String args[]) throws InterruptedException,IOException { // !! ATTENTION !! // By default,the serial port is configured as a console port // for interacting with the Linux OS shell. If you want to use // the serial port in a software program,you must disable the // OS from using this port. // // Please see this blog article for instructions on how to disable // the OS console for this port: // https://www.cube-controls.com/2015/11/02/disable-serial-port-terminal-output-on-raspbian/ // create Pi4J console wrapper/helper // (This is a utility class to abstract some of the boilerplate code) final Console console = new Console(); // print program title/header console.title("<-- The Pi4J Project -->","监听串口(GPIO15-Tx / GPIO16-Rx)数据并写入Memcached中"); // allow for user to exit program using CTRL-C console.promptForExit(); // create an instance of the serial communications class final Serial serial = SerialFactory.createInstance(); byte [] data = new byte[1024]; //数据缓冲区 try { // create serial config object SerialConfig config = new SerialConfig(); System.out.println(">>>"+SerialPort.getDefaultPort()); config.device(SerialPort.getDefaultPort()) // "/dev/ttyACM0" .baud(Baud._115200) .dataBits(DataBits._8) .parity(Parity.NONE) .stopBits(StopBits._1) .flowControl(FlowControl.NONE); // parse optional command argument options to override the default serial settings. if(args.length > 0){ config = CommandArgumentParser.getSerialConfig(config,args); } // display connection details console.box(" Connecting to: " + config.toString()," Data received on serial port will be displayed below."); // open the default serial device/port with the configuration settings serial.open(config); serial.flush(); System.out.println("serial.isOpen():"+serial.isOpen()); /**初始化GPRS模块**/ boolean isinit = initGPRS(serial); long trydelay = 2000; while(!isinit){ System.out.println("初始化GPRS模块不成功,请检查模块工作状态灯,以及SIM卡是否接触良好..."+trydelay); Thread.sleep(trydelay+=1000); isinit = initGPRS(serial); if(trydelay>(10*1000)){return;} //检测10次都不成功时,退出程序 } /**初始化短信参数**/ isinit = initGPRS_SMS(serial); trydelay = 2000; while(!isinit){ System.out.println("初始化短信参数不成功,以及SIM卡是否接触良好."); Thread.sleep(trydelay+=1000); isinit = initGPRS_SMS(serial); if(trydelay>(10*1000)){return;} //检测10次都不成功时,退出程序 } //每次开机时尝试读取一次存储卡中的短信 String res = new String(sendCMD(serial,"AT+CMGL="ALL""),"GBK"); System.out.println("AT+CMGL="REC READ".res:"+res); if(res.indexOf("OK")==-1){ System.out.println("设置失败!"); } //下面进入主程序 System.out.println("进入短信监听程序:"); long old_msg_delay = 60000; //设置旧短信搜索间隔时间(毫秒),在SIM卡内存中搜索数据 long old_msg_count = 0; //旧短信计时器 int index = 1; data = null; while(true){ System.out.print("."); if(!serial.isOpen()){ System.out.println("串口未打开,退出程序"); break; } if(old_msg_count>=old_msg_delay){ // System.out.println("发送获取SIM卡内存中的所有信息的指令"); sendCMD(serial,"AT+CMGL="ALL""); old_msg_count = 0; }else{ old_msg_count+=1000; //System.out.println("old_msg_count..."+old_msg_count); } if(serial.available()>0){ while(serial.available()>0){ data=serial.read(); //此处接收到的数据上限是1024 //System.out.print(new String(serial.read(),"utf-8")); } serial.flush(); } if(data!=null){ //接收到数据 String cc = new String(data,"GBK"); //处理中文 System.out.println("cc:"+cc); if(cc!=null && !cc.trim().equals("")){ //处理数据 /** * 有新短信时: * +CIEV: "MESSAGE",1 * * +CMTI: "SM",1 */ if(cc.indexOf("+CMTI")!=-1){ index = getIndexFromNewSMS(cc); System.out.println("发现新短信.index:"+index); sendCMD(serial,"AT+CMGR="+index); } if(cc.indexOf("+CMGR")!=-1){ String[] contents = getContentFromIndex(index,cc); System.out.println("[AT+CMGR=index]读取存在卡上的短信内容.分析后:"); if(contents!=null){ System.out.println("新短信内容:"); for(String tt : contents){ System.out.println(tt); } //保存读到的短信 -> 服务器 if(sendDataToServer(contents)){ //删除已读出的短信 System.out.println("删除已读出的新短信.index:"+contents[0]); delSMSByIndex(serial,Integer.parseInt(contents[0])); } }else{ System.out.println("新短信内容:null"); } } /** * 查询旧短信时: * AT+CMGL="ALL" * * +CMGL: 1,25 * just because the people11 * +CMGL: 2,25 * just because the people11 */ if(cc.indexOf("CMGL:")!=-1){ //获取第1条短信 String[] contents = getContentFromStorageSMS(cc); System.out.println("[AT+CMGL="ALL"]存在卡上的短信内容.分析后:"); for(String tt : contents){ System.out.println(tt); } //保存读到的短信 if(sendDataToServer(contents)){ //删除已读出的短信 System.out.println("删除已读出的旧短信.index:"+contents[0]); delSMSByIndex(serial,Integer.parseInt(contents[0])); } } }else{ System.out.println("data:"+new String(data)); System.out.println("data(byte[]) 转换成 String时出错"); } } //if(cc!=null && !cc.trim().equals(""))System.out.println(cc); data = null; Thread.sleep(1000); } } catch(IOException ex) { console.println(" ==>> SERIAL SETUP FAILED : " + ex.getMessage()); return; } } /** * 把短信上传到服务器中 * @param contents 数组 [0] - 短信位置索引 [1] - 电话号码 [2] - 日期+时间 2017/10/26 11:37:03+08 [3] - 短信内容 * @return */ public static boolean sendDataToServer(String[] contents){ System.out.println("尝试上传短信数据"); try{ //移除时间中的时区 +08 2017/10/26 12:38:14+08...2017-10-26 12:38:14 String d = contents[2].substring(0,contents[2].lastIndexOf("+")); d = d.replace("/","-").replace(" ","%20"); StringBuffer url = new StringBuffer("http://192.168.6.2:9080/webService.do?method=saveSMSBank"); String vno = DateTimeUtil.dateToString(new Date(),"yyyyMMdd"); vno = StringUtil.encodePassword(vno,"MD5"); url.append("&vno=").append(vno); url.append("&smstype=0"); url.append("&port=2"); url.append("&recTime=").append(d); //need: 2013-12-05%2014:35:20 url.append("&phone=").append(contents[1]); url.append("&serialNo=0"); url.append("&nums=0"); url.append("&submitPort=0"); url.append("&sendid=").append(contents[1]); url.append("&sendtype=0"); url.append("&sendNo=0"); String xx = new String(contents[3].getBytes(),"UTF-8"); url.append("&txt=").append(java.net.URLEncoder.encode(xx,"UTF-8")); System.out.println("sendDataToServer().url:"+url.toString()); String resurl = StringUtil.getContentByUrl2(url.toString()); System.out.println("sendDataToServer().resurl:"+resurl); if(resurl.trim().equals("200")){ System.out.println("数据上传成功!"); return true; }else if(resurl.trim().equals("401")){ System.out.println("这个电话号码和短信内容已上传过,数据重复!"); System.out.println("清除SIM卡上的短信!"); return true; } }catch(Exception e){ e.printStackTrace(); return false; } return false; } /** * 解析返回的短信内容 * @return */ public static String[] getContentFromIndex(int index,String res){ try{ System.out.println("尝试读取短信...getContentFromIndex.res:"+res); if(res.indexOf("OK")!=-1){ System.out.println("获取短信成功,解析内容..."); /** * +CMGR: "REC READ",25 * just because the people11 * * +CMGR: "REC READ",25 * ---------------- ------------- - ---------- ----------- --- -- - - ---------------- --- -- * [0] [1] [2] [3] [4] [5] [6][7][8] [9] [10][11] */ String[] ccs = res.split("rn"); String phone = new String(); String sendDate = new String(); String content = new String(); boolean isvalid = false; //数据获取成功 for(int i=0;i<ccs.length;i++){ if(ccs[i].indexOf("CMGR:")!=-1){ String[] temp1 = ccs[i].split(","); phone = temp1[1]; sendDate = temp1[3]+" "+temp1[4]; content = ccs[i+1]; isvalid = true; break; //只处理1条 } } if(!isvalid)return null; //处理双引号 phone = phone.substring(1,phone.length()-1); sendDate = sendDate.substring(1,sendDate.length()-1); String[] resu = new String[4]; resu[0] = String.valueOf(index); resu[1] = phone.trim(); resu[2] = sendDate; resu[3] = content; return resu; }else if(res.indexOf("CMS ERROR")!=-1){ //CMS ERROR:321 表示所读取的内存位置出错,一般是指定位置无短信内容所致 System.out.println("获取短信失败,错误内容..."); return null; } }catch(Exception e){ e.printStackTrace(); } return null; } /** * 有新短信时,获取短信内容: * +CIEV: "MESSAGE",1 * * +CMTI: "SM",1 * * @return index 短信所在的内存位置 index */ public static int getIndexFromNewSMS(String cc){ try{ String[] ccs = cc.split("rn"); for(String v : ccs){ if(v.indexOf("CMTI: "SM",")!=-1){ String c = v.substring(v.indexOf(",")+1); return Integer.parseInt(c); } } }catch(Exception e){ e.printStackTrace(); } return 0; } /** * 查询旧短信,每次只抓1条: * +CMGL: 4,10:15:19+08" * -------- ---------- -------------- ----------- ------------ * [0] [1] [2] [3] [4] [5] 【小米】[小米移动]您2017年6月共消费1.32元,当前余额97.88元。其中:数据流量费1.32元;语音通信费0元;短/彩信费0元 +CMGL: 5,10:15:30+08" 。查询账单 http://10046.mi.com 。 +CMGL: 6,10:15:30+08" 【小米】[小米移动]您2017年7月共消费0.81元,当前余额97.01元。其中:数据流量费0.81元;语音通信费0元;短/彩信费0元 OK @return 数组 [0] - 短信位置索引 [1] - 电话号码 [2] - 日期+时间 [3] - 短信内容 */ public static String[] getContentFromStorageSMS(String cc){ String[] ccs = cc.split("rn"); String smsIndex = new String(); String phone = new String(); String sendDate = new String(); String content = new String(); for(int i=0;i<ccs.length;i++){ if(ccs[i].indexOf("CMGL:")!=-1){ //smsIndex = Integer.parseInt(ccs[i].substring(ccs[i].indexOf("CMGL:")+5,ccs[i].indexOf(","))); smsIndex = ccs[i].substring(ccs[i].indexOf("CMGL:")+5,")); String[] temp1 = ccs[i].split(","); phone = temp1[2]; sendDate = temp1[4]+" "+temp1[5]; content = ccs[i+1]; break; //只处理1条 } } //处理双引号 phone = phone.substring(1,phone.length()-1); sendDate = sendDate.substring(1,sendDate.length()-1); String[] res = new String[4]; res[0] = smsIndex.trim(); res[1] = phone.trim(); res[2] = sendDate; res[3] = content; return res; } /** * 删除指定位置上的短信 * AT+CMGD=4 * @param index 短信索引位置 * @return */ public static boolean delSMSByIndex(Serial serial,int index){ String res = new String(sendCMD(serial,"AT+CMGD="+index)); System.out.println("AT+CMGD="+index+":"+res); //if(res.indexOf("OK")==-1){ // System.out.println("删除["+index+"]位置的短信失败!"); // return false; //} return true; } /** * * 初始化GPRS.模块 * AT 100ms 握手 / SIM卡检测等 * AT+CPIN? 100ms 查询是否检测到SIM卡 * AT+CSQ 100ms 信号质量测试,值为0-31,31表示最好 * AT+CCID 100ms 读取SIM的CCID(SIM卡背面20位数字),可以检测是否有SIM卡或者是否接触良好 * AT+CREG? 500ms 检测是否注册网络 * @return */ public static boolean initGPRS(Serial serial){ if(!serial.isOpen()){return false;} //串口未准备好 byte[] buffs = new byte[128]; try{ System.out.println("try send AT to module..."); //char cmd[] = {'A','T'}; //byte cmd[] = "AT".getBytes(); //buffs = sendCMD(serial,"AT".getBytes()); System.out.print("rnGPRS模块检测中..."); buffs = sendCMD(serial,"AT"); String res = new String(buffs); if(res.indexOf("OK")==-1){ System.out.println("GPRS模块未准备好,请检查电源和串口波特率是否正确!"); return false; } System.out.println(" ...[正常]rn"); //System.out.println("AT.res:"+res); System.out.print("rn检测SIM卡..."); res = new String(sendCMD(serial,"AT+CPIN?")); if(res.indexOf("READY")==-1){ System.out.println("SIM卡未准备好!"); return false; } System.out.println(" ...[正常]rn"); //System.out.println("AT+CPIN?.res:"+res); System.out.print("rn信号质量测试,值为0-31,31表示最好..."); res = new String(sendCMD(serial,"AT+CSQ")); if(res.indexOf("ERROR")!=-1){ System.out.println("信号质量测试检测失败"); return false; } /** * +CSQ: 24,99 */ String[] vs = res.split("rn"); for(String v : vs){ if(v.indexOf(":")!=-1){ String x = v.substring(v.indexOf(":")+1); //System.out.println("x:"+x); System.out.println(" ...信号强度:["+x.trim()+"]rn"); } } //System.out.println("AT+CSQ.res:"+res); res = new String(sendCMD(serial,"AT+CCID")); System.out.println("AT+CCID.res:"+res); res = new String(sendCMD(serial,"AT+CREG?")); System.out.println("AT+CREG.res:"+res); }catch(Exception e){ e.printStackTrace(); return false; } return true; } /** * * 初始化GPRS.设置短信模式及短信接收参数 * AT+CMGF=1 0-PDU,1-文本格式 * AT+CSDH=1 * AT+CPMS="SM","SM" 将信息保存在SIM卡中,SM-表示存在SIM卡上 * AT+CNMI=2,1 收接通知,并存在指定位置(与AT+CPMS设置有关) * * 设置好后,收到短信: * +CIEV: "MESSAGE",1 * +CMTI: "SM",1 表示存储位置index=1 * @return */ public static boolean initGPRS_SMS(Serial serial){ if(!serial.isOpen()){return false;} //串口未准备好 String res = new String(); try{ System.out.print("rn设置短信格式..."); res = new String(sendCMD(serial,"AT+CMGF=1")); if(res.indexOf("OK")==-1){ System.out.println("设置失败!"); return false; } System.out.println(" ...[文本格式]rn"); Thread.sleep(100); System.out.print("rnAT+CSDH=1..."); res = new String(sendCMD(serial,"AT+CSDH=1")); if(res.indexOf("OK")==-1){ System.out.println("设置失败!"); return false; } System.out.println(" ...[DONE]rn"); Thread.sleep(100); System.out.print("rn设置信息保存位置..."); res = new String(sendCMD(serial,"AT+CPMS="SM","SM","SM"")); if(res.indexOf("OK")==-1){ System.out.println("设置失败!"); return false; } System.out.println(" ...[SIM卡]rn"); Thread.sleep(100); System.out.print("rn收接通知,并存在指定位置..."); res = new String(sendCMD(serial,"AT+CNMI=2,1")); if(res.indexOf("OK")==-1){ System.out.println("设置失败!"); return false; } System.out.println(" ...[DONE]rn"); Thread.sleep(100); }catch(Exception e){ e.printStackTrace(); return false; } return true; } //public static byte[] sendCMD(Serial serial,byte[] cmd){ public static byte[] sendCMD(Serial serial,String cmd){ long overtime = 10000; //每条指令超时上限 5秒 long timecount = 0; //计时器 byte[] buffs = new byte[128]; try { serial.writeln(cmd+"r"); //serial.writeln("ATr"); timecount = 0; while(timecount<overtime){ //System.out.print(serial.available()); if(serial.available()>0){ while(serial.available()>0){ buffs = serial.read(); //System.out.print(new String(serial.read())); //System.out.print(new String(buffs)); } serial.flush(); timecount = overtime; //exit while } timecount += 100; Thread.sleep(100); } //System.out.println("sendCMD:"+new String(buffs)); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return buffs; } } // END SNIPPET: serial-snippet 程序中的方法: sendDataToServer() 主要是用于上传保存短信,大家替换成自己的方式即可 总结 以上所述是小编给大家介绍的树莓派.GPRS.短信接收器,希望对大家有所帮助! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |