小题大做系列II(PM2.5微信控)炒鸡长文体虚慎入

2015-11-253.8万
精华项目
AI 快速预览详细 收起
本文介绍了一个基于Arduino的项目,名为PM2.5微信控。该项目可以采集办公室的PM1、PM2.5、PM10、温湿度、照明和噪声数据,并通过微信公众号实时查询。终端实物已安装在作者的办公桌上。此项目的技术储备较广,适合对空气质量监测感兴趣的用户。
去年这会儿,老夫在Arduino中文社区首发《小题大做之远程LED控制》长文(https://www.arduino.cn/thread-8463-1-1.html)。据说,栏目主办者准备了WIZnet网络板、T恤、马克杯等奖品。老夫心不黑,冲着马克杯去的,于是乎就发了一篇。现如今该文点击达到35165,但是连马克杯的照片也没给我。这个骗纸。

今年4月间,就把该文转到咱地府萝卜头来(https://mc.dfrobot.com.cn/thread-10590-1-1.html),蒙编辑老爷厚爱,侥幸列入上半年度编辑推荐奖(此处当有掌声)。
小题大做系列II(PM2.5微信控)炒鸡长文体虚慎入图1

看看2015年已临近岁末,又老一岁。借宝地汇报一下老夫一年来的学习成果,风格仍延续小题大做、技术跨界、干湿货通吃的模式。

此文篇幅较长、技术储备较广、创作时断时续,故而体虚肾亏胸闷气急的读者,千万服药后阅读或遵医嘱。

此项目采集办公室空气质量(包括:PM1、PM2.5、PM10),温湿度、照明和噪声,并可通过微信公众号实时查询7个数据。
小题大做系列II(PM2.5微信控)炒鸡长文体虚慎入图2


终端实物参考下图。原先照片在家里拍的,如今已安装在老夫办公桌上了。
小题大做系列II(PM2.5微信控)炒鸡长文体虚慎入图3

以下篇幅,陆续展开全文。

创作许可协议

本项目采用 None(不开放任何权利,保留所有权利) 进行许可。

评论(33)
20060606
20060606
好创意,赞一个
kevinzhang19701
kevinzhang19701
作者
哇塞,萝卜头又送来惊喜。:lol:lol

qing1987
qing1987
学习~
swanglei
swanglei
这篇真的挺不错。。。
dsweiliang
dsweiliang
哇,好赞啊,果断收藏
刹那ZF清英
刹那ZF清英
:loveliness:哇哦对代码啥的一窍不通的我竟然把前辈的文章看完了————不对,是翻完了。果然还是要回家吃药啊,前辈已经带着萝卜头的小伙伴们(除我之外)在大神的道路上渐行渐远了——连模仿都模仿不来滴 !!!好吧 感谢分享,期待新作——
kevinzhang19701
kevinzhang19701
作者
回复刹那ZF清英
您的鱼菜共生炒鸡棒:D:D:handshake
kevinzhang19701
kevinzhang19701
作者
非常感谢地府萝卜头的厚爱。

尝鲜包内有Romeo Mini BLE和两枚微马达,外加一个环保袋:D:D



kevinzhang19701
kevinzhang19701
作者
回复Ash1
:loveliness::loveliness::handshake:handshake
Ash1
Ash1回复kevinzhang19701
喜欢就好
期待你带来更多优秀的作品~
kevinzhang19701
kevinzhang19701
作者
17. 结束语
从上个月25号到今天,陆陆续续把这个项目扯完了。篇幅有点长,话也有点啰嗦,不过,其中着实还有不少东西说得挺含糊甚至没来得及提及的,大家要是恶心想吐就跟帖吐槽吧。

科技行业分析公司Linley Group发布了最新的调研报告。报告显示,到2020年,全球使用的物联网设备将达到19亿个。所以,如果有孩子打算上大学的话,请一定选择IT专业(特别是她们)。未来生活中将充斥着无数的传感器,它们的功耗更小、体积更小、更稳定、更灵敏,我们的生活将因为传感器和智能算法而发生重大的变化。不过数据汇合点仍将在云端,这就是为什么西方敌对势力都极其重视云端的原因。
本项目还存在安全方面的问题,比如终端设备提交给项目服务器的数据没有加密、项目服务器与微信服务器数据交换时也没有加密、获取传感器数据后写入数据库前没有做防注入验证等等,这条数据传输链是不稳定不安全的。如果是生产环境的话,这些都要再加以调整。
这个项目还可以进一步扩展成一个志愿者联盟性质的活动。这意味着有兴趣的地府同好可以人手一台,将实时数据提交到地府某个志愿者的服务器上,数据可供社会查询和进一步加工使用(加工后的数据的使用可能需要支付一定费用;P)。这有个好处,因为花纳税人钱的政府只会提供有限的几个点的某些时间点的空气质量数据,而地府同好来自于世界各地,且覆盖范围更广、数据采集更密集,因此提供的数据量、覆盖面、实时性将更好,社会效益和经济效益都有收获(老夫顺便问问,这个粉尘传感器输出数据是否可以信赖?)
(以上灵感来自于:http://www.leiphone.com/news/201508/zxYBDeC5khgC26Zz.html
谨借宝地谨献此文,预祝地府君们圣诞快乐、新年快乐。
(全文完)
kevinzhang19701
kevinzhang19701
作者
16. 其他一些注意事项
请允许老夫再啰嗦几句。
本项目只使用了文本消息查询,回复消息也是文本消息。
有小盆友问是否可以接受用户发来的语音、图像、甚至地理位置信息?回答是完全可以接收。不过,由于个人申请的无认证订阅号受到其权限限制,能接受字符串并不意味着可以接受这些语音、图像和地理位置的源数据(微信团队还是很奸诈的,将这些源数据使用MediaId来标识和引用)。这也就是解释了为什么Arduino中文社区里有朋友发的帖子,是使用测试账号制作的自定义菜单来发起查询的(http://www.arduino.cn/forum.php? ... =%E5%BE%AE%E4%BF%A1)。

因为微信团队给测试账号的权限很大很大(顺便说一下上图的自定义菜单是使用JSON而不是XML实现的,需要注意),不过,测试账号就是测试账号,自己用用可以,推广成正式项目就非常可疑了。是吧,这年头骗子太多。
从另外的角度看,如果需要拥有更多的权限、更高大上的形象的话,那么还是需要以公司名义,申请一个乃至多个认证的服务号,或者最高大的企业号。
==
大家可能不太知道,鳗鱼年幼的时候对水温、水质、含氧量等环境要求很高。一旦生活环境稍有不适,会成批死去,严重影响养殖场的收益。几年前,韩国鳗鱼养殖场主们开始使用点状分布的传感器来监控鳗鱼网箱的环境数据,以取代原来每隔一段时间人工测量的繁琐工作。这些数据上传至云端,可以实时从手机上获得。当个别敏感数据的变化超过警戒范围时,手机App会发出警告,及时通知养殖场主是哪个网箱发生了问题,且是什么样的问题。

kevinzhang19701
kevinzhang19701
作者
15. 完整的wxtest.class源代码

此为本项目核心代码之三,技术涉及非常广泛,如果您有疑问,欢迎跟帖咨询。
[code]import java.io.IOException;
import java.io.PrintWriter;
import java.io.InputStream;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.dom4j.*;
import org.dom4j.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.ResultSet;
@WebServlet(urlPatterns = {"/wxtest"})
public class wxtest extends HttpServlet
{
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
String myVal01 = ""; //7个变量对应7个参数
String myVal02 = "";
String myVal03 = "";
String myVal04 = "";
String myVal05 = "";
String myVal06 = "";
String myVal07 = "";
Connection conn = null; // 涉及数据库打交道的变量
Statement stmt = null;
ResultSet rs1 = null;
int i = 0;
String mySQL = "SELECT * FROM t01 ORDER BY tid DESC LIMIT 1;"; // 从数据表里读取最后那条记录的SQL语句
try
{
conn = DriverManager.getConnection("jdbc:mysql://localhost/mypm?" +
"user=root&password=15186okia");
stmt = conn.createStatement();
rs1 = stmt.executeQuery(mySQL);
while(rs1.next())
{
myVal01 = rs1.getString(2); //分别将获得的数据写入7个变量
myVal02 = rs1.getString(3);
myVal03 = rs1.getString(4);
myVal04 = rs1.getString(5);
myVal05 = rs1.getString(6);
myVal06 = rs1.getString(7);
myVal07 = rs1.getString(8);
}
}
catch (SQLException ex)
{
System.out.println("Error in connection: " + ex.toString());
System.out.println("SQLException: " + ex.getMessage());
System.out.println("SQLState: " + ex.getSQLState());
System.out.println("VendorError: " + ex.getErrorCode());
}
InputStream postIn = request.getInputStream(); //从输入流获取微信服务器传过来的整个字符串
try
{
SAXReader reader = new SAXReader();
Document myDocument = reader.read(postIn); // 转成dom4j可以处理的格式
Element root = myDocument.getRootElement(); // 从XML字符串内获取全部元素,组成一棵树
Element myToUserName = root.element("ToUserName"); // 分别获取需要的值
Element myFromUserName = root.element("FromUserName");
Element myCreateTime = root.element("CreateTime");
Element myMsgType = root.element("MsgType");
Element myContent = root.element("Content");
Element myMsgId = root.element("MsgId");
String sToUserName = myToUserName.getText(); // 转成String类型
String sFromUserName = myFromUserName.getText();
String sCreateTime = myCreateTime.getText();
String sMsgType = myMsgType.getText();
String sContent = myContent.getText();
String sMsgId = myMsgId.getText();
if (sMsgType.equals("text") && sContent.equals("/??")) // 用户发来的是文本消息且是查询数据关键词吗?
{
long lCreateTime = System.currentTimeMillis()/1000L; // 消息创建的时间
String mySendContent = "";
// 以下手工封装回复文本消息的XML字符串
mySendContent = "PM1:" + myVal01 + "ug/m3\nPM2.5:" + myVal02 + "ug/m3\nPM10:" + myVal03 + "ug/m3\n温度:" + myVal04 +
"℃\n湿度:" + myVal05 + "%\n亮度:" + myVal06 + "\n噪声:" + myVal07;
String xmlString01 = "";
String xmlString02 = "";
String xmlString03 = "";
String xmlString04 = "";
String xmlString05 = "";
String xmlString06 = "";
String xmlString07 = "
";
String xmlAll = xmlString01 + xmlString02 + xmlString03 + xmlString04 + xmlString05 + xmlString06
+ xmlString07;
System.out.println(xmlAll);
out.print(xmlAll); // 回复给微信服务器,由其转发给用户手机
out.close();
}
System.out.println("== Done ==");
}
catch (DocumentException ex)
{
System.out.println("dom4j Document: " + ex.toString());
}
}
[/code]正常情况下,正确反馈应该是下面酱紫滴:

kevinzhang19701
kevinzhang19701
作者
14. 微信消息的XML格式

14.1 接受消息
我们在微信客户端里发一个言,站在微信服务器的角度来看,就是发送了一个消息(可能是文本消息、语音消息、图像消息、音乐视频消息、地理位置消息等)。这个消息传到微信服务器后,服务器会判断传过来的微信公众号运作在编辑模式还是开发模式,一旦确认是开发模式,那么微信服务器就直接把消息打包成XML字符串转给项目服务器,全部由项目服务器接着去做后续处理。
老夫定义了"/??"为用户获取设备数据的关键词。
项目服务器必须要处理这个叫做“文本消息”的XML字符串。
请大家注意下列连接(接受消息):
http://mp.weixin.qq.com/wiki/17/ ... 44d9c96eaf51f9.html
这是开发者文档中重要的一部分,我们关注的是“文本消息”和她的格式。其他格式的消息本项目暂时不使用,如您有兴趣,比如想做个语音消息,可以参考文档的其他部分。
文本消息的格式如下图:

各位看到上面的文本消息格式和解释了吧,我就不啰嗦了。对我们的项目来说,需要提取的是两个值。前者我们限定在"text"(老夫只看文本消息,忽略其他的),后者我们限定在"/??"(老夫只看用户是不是发送了/??查询数据,其他忽略)。这两个地方,可以根据您的项目的要求灵活运用。
14.2 回复消息
现在我们已可以判断用户是否发送了查询关键词/??,那么接下来怎么回复给用户呢?请阅读如下链接:
http://mp.weixin.qq.com/wiki/18/ ... 46dc39de20f672.html
“回复文本消息”说明了我们要回复给微信服务器的也是一串XML格式的字符串,格式如下:

看上去好像和传过来的消息挺像的。这里的ToUserName和FromUserName只需要简单地将传过来的文本消息的To和From交换一下,即可实现回复给发言的那个用户了。
这里的Content参数,就要组织好回复的7个数据的XML串。整个回复文本的样子应该是下面这个样子滴:
[code]





[/code]那么,上面的7个参数数据从哪里来?对,从我们的数据库里来,而且我们只取数据库最后那一条最新鲜的值。
kevinzhang19701
kevinzhang19701
作者
13. dom4j开源项目
dom4j的快速入门教程链接如下:
http://dom4j.sourceforge.net/dom4j-1.6.1/guide.html
但是我担心翻不了墙的小伙伴会无法看到。

下载地址在Sourceforge,别忘记下载完成后的jar文件要放入Java的lib目录,或者直接在NetBeans里导入这个jar。
http://sourceforge.net/projects/dom4j/files/
[align=left]

[/align]
我们的重点在解析上,也就是我们需要从微信服务器传给项目服务器的一长串XML字符串里,提取我们想要的数据值,其他我们就不关心了。整个解析过程可以分如下三步:
13.1 从输入流读取微信服务器传过来的一长串字符串(XML格式)。
13.2 将字符串转换成dom4j可以处理的类型,赋给变量。
13.3 从这个变量中提取需要的参数值(即微信服务器传过来的数据),这一步我们仰赖dom4j自动帮我们完成。
别着急,后面代码我会有解释,先让老夫卖个关子。
kevinzhang19701
kevinzhang19701
作者
12.3 XML长啥样
[code]



Harry Potter
J K. Rowling
2005
29.99


Learning XML
Erik T. Ray
2003
39.95

[/code]如上样例,这些用"<>"和""成对括起来的就是正宗的XML代码。
我们不用了解得很深入,对本项目有用的概念,就是记住微信在回复消息的时候,使用的就是XML格式的字符串。数据来往,就是接受和发送这些符合格式要求的串(字符串含有“元素”、“属性”等)。不过,有一点大家要注意:XML字符串,需要我们自己进行编码和解析。
关于XML的引申阅读链接:http://www.w3school.com.cn/xml/index.asp
12.4 XML的编码和解析
让我们自己编码,问题不大,可以先摸清楚微信文本消息的格式,然后用字符串拼接出来。
然而让我们解析XML字符串,就有一定困难了。不过,老夫这个矮子仍旧可以站在巨人的肩上,借助一个开源项目:dom4j,来完成。
kevinzhang19701
kevinzhang19701
作者
加油加油加油(重要的事情说三遍);P



luna
luna
坐等完整版啊~~前几天北京雾霾,还想找你的文章发一下呢~~T T 可惜你没有更完~我可等着下次雾霾微信公众号发这篇呢
luna
luna回复kevinzhang19701
评论内容
kevinzhang19701
kevinzhang19701
作者
回复luna
不好意思,休假去日本一周。回来后会尽快写完整:lol
kevinzhang19701
kevinzhang19701
作者
休假不忘发帖

kevinzhang19701
kevinzhang19701
作者
今天在仙台,休假中
kevinzhang19701
kevinzhang19701
作者
12. 关于XML
12.1 什么是XML
[list]
[*]XML 指可扩展标记语言(EXtensible Markup Language)
[*]XML 是一种标记语言,很类似 HTML
[*]XML 的设计宗旨是传输数据,而非显示数据
[*]XML 标签没有被预定义。您需要自行定义标签。
[*]XML 被设计为具有自我描述性。
[*]XML 是 W3C 的推荐标准
[/list]
12.2 为什么使用XML
大家都知道Web Service吧,一种分布式传输数据的方式。考虑到分布式设备使用不同的系统,他们要相互传输数据有一定难度,后来大家发现传输文本类型的数据比较简单,因此就使用XML技术,将需要传输的数据打包成XML格式。
kevinzhang19701
kevinzhang19701
作者
11. 第二个Servlet,mypm.class
这是核心代码之二,主要实现:
1. 监听http请求,抓取7个传递来的参数。
2. 写入t01数据表。
[code]import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// MySQL connection package
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.ResultSet;
@WebServlet(urlPatterns = {"/mypm"})
public class mypm extends HttpServlet
{
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
String myPm01 = request.getParameter("unoVar01"); // 抓取7个参数
String myPm25 = request.getParameter("unoVar02");
String myPm10 = request.getParameter("unoVar03");
String myTemp = request.getParameter("unoVar04");
String myHumi = request.getParameter("unoVar05");
String myLight = request.getParameter("unoVar06");
String mySound = request.getParameter("unoVar07");
Connection conn = null;
Statement stmt = null;
ResultSet rs1 = null;
int i;
String mySQL = "INSERT INTO t01 (tpm01, tpm25, tpm10, ttemp, thumi, tlight, tsound) VALUES('" +
myPm01 + "','" +
myPm25 + "','" +
myPm10 + "','" +
myTemp + "','" +
myHumi + "','" +
myLight + "','" +
mySound + "')";
try
{
conn = DriverManager.getConnection("jdbc:mysql://localhost/mypm?" +
"user=root&password=15186okia");
stmt = conn.createStatement();
i = stmt.executeUpdate(mySQL); // 写入数据表
System.out.println("inserting...");
}
catch (SQLException ex)
{
System.out.println("Error in connection: " + ex.toString());
System.out.println("SQLException: " + ex.getMessage());
System.out.println("SQLState: " + ex.getSQLState());
System.out.println("VendorError: " + ex.getErrorCode());
}
System.out.println("Written...");
System.out.println("PM 1: " + myPm01);
System.out.println("PM2.5:" + myPm25);
System.out.println("PM10: " + myPm10);
System.out.println("Temperature:" + myTemp + "C");
System.out.println("Humidity: " + myHumi + "%");
System.out.println("Light: " + myLight);
System.out.println("Sound: " + mySound);
System.out.println("==");
}
}
[/code]
kevinzhang19701
kevinzhang19701
作者
10. MySQL数据库
我们使用开源的MySQL存储数据。多年来,MySQL已经做了大量的改进,用于生产系统毫无违和感。配合WorkBench图形化界面,可以不用输入命令来维护整个数据库。

这里建了一个简单的库mypm,里面建了一个数据表t01,数据表结构和字段类型,请参考上图。
再次强调,Servlet与MySQL的连接桥梁是ConnectorJ。所以,一定不要忘记在Tomcat的lib目录,存放这个驱动。下载链接:
http://dev.mysql.com/downloads/connector/j/