目录
一、想法及需求
1.1最初设想
1.2需求分析
1.3方案设计
二、硬件
2.1器材选型
2.2原理图解释
2.3PCB绘制
2.4焊接及成品
三、软件
3.1NETTY自定义协议的TCP服务器
3.1.1使用原因
3.1.2制定协议
3.1.3编写解码器
3.1.4编写服务器
3.1.5部署服务器(将netty服务器部署到阿里云服务器)
3.1.6 netty连接数据库
3.2嵌入式代码
3.3基于Thymeleaf的前端展示
一、想法及需求 1.1最初设想 题源于物联网综合设计课设 , 要求做一套简易的物联网综合系统 。由于宿舍楼下就是操场 , 很经常举办一些类似运动会、草地弹唱会的活动 , 影响到同学们的休息 , 所以想做一套校园噪声监测系统 , 这篇文章记录了制作过程中的心路历程 。
首先来看看最后的效果 , 大屏展示出各个节点的实时状态以及位置 , 当出现噪声时进行报警并发送短信到工作人员处 , 由于短信需要费用 , 这里使用邮件代替:
1.2需求分析 校园噪声检测系统的用户是保卫科管理人员 , 对象的需求大致为以下三点:
①及时提示有噪声出现并且显示噪声出现的地点
②及时通知工作人员 , 保证消息传送的及时性
③记录噪声出现的地点、时间以及大小 , 归纳噪声规律 , 从而更好地解决噪声问题
1.3方案设计 在校园的各个容易产生噪声的地方安装传感器节点 , 但节点之间不进行互相通信(不构成WSN无线传感网) 。每个节点由四个单元组成 , 分别是感知单元、处理单元、通信单元和能量供给单元 , 最后所有节点的数据将在大屏上进行展示(如下图所示) 。
对于数据的传输 , 分为以下几个部分:传感器节点发送TCP数据包到使用NETTY搭建的数据服务器 , 数据服务器解析数据包为一个个数据对象 , 使用JDBC存储到数据库中 , 而数据服务器一样可以通过JDBC来获取数据库中的数据 , 以进行数据的处理及展示 。
本篇文章将分为以下几个部分进行讲解:硬件、软件以及成功展示 。
二、硬件 2.1器材选型 首先是关于器材的选型 , 在第一部分已经说过 , 每个传感器节点由四个单元组成 , 分别是感知单元、处理单元、通信单元和能量供给单元 。感知单元最后选型为LM386声音传感器 , 它实际上是一个AD转化器 , 将噪声模拟量转化为电压的大小;处理单元以及能量供给单元最后选用STM32F103C8T6的最小系统板 , 不论从IO输出 , 传输效率 , 处理效率上 , STM32相对于C51而言都占了较大的优势;通信模块选用了ESP8266进行wifi传输 , 由于节点之间距离太远 , 并不进行相互通信 , 而是各自通过wifi连上互联网 , 发送TCP数据包到数据服务器 。各个模块的图片如下:
STM32F103C8T6最小系统板:
ESP8266:
USB转串口模块CP2102:
LM386声音传感器模块:
OLED显示屏模块:
2.2原理图解释
按从左到右 , 从上至下的顺序进行说明:
首先是wifi模块 , 这里有一个需要注意的地方 , wifi模块必须要与串口相连 , 通过串口来进行收发数据 。对于SMT32F103C8T6而言 , 它的串口1为PA9和PA10 , 而串口2为PA2和PA3 , 这里注意ESP8266同串口1连接好像容易出问题 , 所以选择串口2 , 接着就是wifi模块的RXD要连在STM32的TXD上 , TXD连在RXD上 , 这是由于它的工作机制:STM32发送AT指令到wifi模块处 , wifi模块识别指令并进行数据的转发 , 通过互联网传到数据服务器处 。
接着是声音传感器模块 , 本质上是一个AD转化起 , 将模拟量噪声转化为电压的大小进行读取 , 这里注意 , 这个传感器并没有给出分贝与电压之间的转化公式 , 需要自己用声级计进行测试 , 这里不推荐使用这个传感器 , 最好去找一些精确度较高的传感器模块 。
接着是LED模块 , 当测量噪声达到阈值时 , LED发光进行报警 。
USB转串口模块方便于同电脑间进行信息的传输以获取芯片实时的状态 , 方便调试 。
OLED模块使用IIC接口 , 将7针的SPI接口缩小到了4针 , 合理利用了IO口的资源 。
BUTTON模块并没有用到 , 可以进行复位或者调解参数使用 , 但是注意:这里的BUTTON接法有误 , 由于没有用到就没有进行修改 , 正确的接法如下:
STM32最小系统板:注意绘制封装时将每一个引脚对应上 , 以及每一个模块的尺寸都要画正确 。
2.3PCB绘制 根据上图画好电路图后 , 转为PCB并进行布局、布线以及铺铜 , 布局及布线要进行合理规划 , 如果STM32的引脚的接入端被大致均匀的分到了左右两边 , 一般建议将STM32放在中间 , 就可以将连线错开 , 减少过孔的数量 。在板的四角还可以进行开孔 , 使用M3六角铜柱进行支撑 。
2.4焊接及成品 PCB的绘制只需最简单的对排母的绘制 , 这里可以进行一些改进:比如OLED屏是很小的排母支撑起较大的屏幕 , 重心不稳且容易断 , 可以在它的附近再焊上一个排母 , 起支撑作用 。CP2102由于没有用到就没有进行焊接 。
三、软件3.1NETTY自定义协议的TCP服务器 3.1.1使用原因 为什么要使用自定义的协议呢 , 原因有三:
①常规的物联网系统是连接到大公司搭好的平台上 , 如使用mqtt连接到中国移动的onenet平台 , 但缺点就是公司可以掌握你的所有数据 , 以及有一种受制于人的感觉 。
②自定义协议方便扩展其余节点 , 只需要所有节点统一协议即可 。
③想学一些新东西 , 比如这里用到的netty 。
3.1.2制定协议 由于本项目只需要传输声音值的大小 , 传输的内容简单 , 也不需要定义非常复杂的协议 , 协议结构如下:
魔数传感器节点序号数据部分的长度数据其中魔数用来第一时间判断出是否为无效包 , 是收发双方提前约定好的数据 , 这里我定义为‘0717’四个字节;由于传感器得到的数据时确定三位的 , 所以可以确定数据部分的长度 , 节点每次发送时只需修改对应的数据部分以及节点序号即可 。以下是iot_protocol 协议类:
package com.zhiqi.IOT.protocol;import java.util.Arrays;/*** * 自定义的协议如下 * | ---- | -------------- | -------------- | ---- | * | 魔数 | 传感器节点序号 | 数据部分的长度 | 数据 | * | ---- | -------------- | -------------- | ---- | */public class iot_protocol {/*** 魔数 4个字节*/private int head_data ;/*** 传感器节点序号 1个字节*/private int id ;/*** 数据的长度 3个字节*/private int contentLength;/*** 数据的内容*/private String content;@Overridepublic String toString() {return "iot_protocol{" +"head_data="https://tazarkount.com/read/+ head_data +", id=" + id +", contentLength=" + contentLength +", content='" + content + '\'' +'}';}public int getHead_data() {return head_data;}public void setHead_data(int head_data) {this.head_data = https://tazarkount.com/read/head_data;}public int getId() {return id;}public void setId(int id) {this.id = id;}public int getContentLength() {return contentLength;}public void setContentLength(int contentLength) {this.contentLength = contentLength;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public iot_protocol() {}public iot_protocol(int head_data, int id, int contentLength, String content) {this.head_data = head_data;this.id = id;this.contentLength = contentLength;this.content = content;}} 3.1.3编写解码器 对于解码器而言 , 判断正误 , 依次读出每一个数据即可 , 但是有几个值得关注的点:
①对于魔数0717 , 解码器使用buffer.readInt()方法是一次读取四个字节的(一个int占四个字节) , 所以这里的0717会被转化成16进制的ascii码:30 37 31 37 , 合起来看做一个数 , 使用计算机计算结果如下 , 为808923447:
②对于节点编号只有一个字节 , 发送端发送后转化为ascii码 , 在接收端解码时使用字节读出 , 读出的是十进制ascii码 , 将这个数字减去48后就是正确的编号 。
③接下来是3位的数据长度 , 这里我选择使用三次readByte()读出三位 , 读出的三位分别减去48后 , 换算成整体的十进制 。
④得到数据长度后 , 可以就可以创建一个数据长度大小的byte数组 , 使用buffer.getbytes方法直接获取所有数据 , 再使用String的构造函数转化为String 。
解码器代码如下:
package com.zhiqi.IOT.protocol;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import java.util.List;//自定义协议的解码器 根据协议的格式进行对发来的数据包的解码public class iot_protocol_decoder extends ByteToMessageDecoder {/**** 魔数 4个字节* 传感器节点序号 1个字节* 数据长度 3个字节* 最基本的长度为10个字节 如果数据包小于10则发生异常*/public final int BASE_LENGTH = 4 + 1 + 3;//重写解码方法protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf buffer, List 3.1.4编写服务器 遇到的几个注意点:
①在Netty开发当中 , 需要自定义ChannelInboundHandlerAdapter , 在重写channelRead时会传入msg对象 , 此对象在使用完毕后必须释放 , 否则会导致对象池泄露内存溢出(报错Discarded inbound message {} that reached at the tail of the pipeline.Please check your pipeline configuration.) 。可以直接使用SimpleChannelInboundHandler , 会自动释放对象 。
②可以使用sockettool来测试服务器 , 这里来演示一下:比如说之前说的魔数0717会被识别成一个新的int , 我们连接上localhost:8266 , 进行测试:
对整体进行测试 , 观察解码器是否正确 。
3.1.5部署服务器(将netty服务器部署到阿里云服务器) 首先对maven项目进行打包:
使用winscp将jar包上传至云端:
打开putty运行此jar包(记得将目录切换到jar包的位置):
运行后打开日志发现报错 , 是因为没有指定主类 , 回到idea中来指定(修改pom文件):
继续报错:
没想到大半夜的在这个地方卡了好久 , netty导出jar包不知道为什么不能用maven的package , 而要用idea自带的build , 先在project structure中设置:
接着继续设置:
再次运行 , 服务器启动:
接着进行测试:
这里又踩坑了 , 可以和这个端口建立连接但是一直接收不到数据 , 经过了好长时间的调试 , 修改了阿里云安全组、给netty绑定上公网ip等等 , 最后发现代码根本没错 , 是由于解码器中设置了发过来的数据的格式 , 而我一开始没有按这个格式发送而导致接收不到 , 被当做无用包舍弃了 , 查阅资料后有如下解释:
首先 , 对于一个程序来讲 , 它所绑定的IP只能是其所在机器(无论物理机还是虚拟机)上的某个网卡的IP地址 , 这个你可以到机器上运行ifconfig查看 。
其次 , 所谓绑定的含义是规定程序能够监听到哪个目的地IP的IP包 , 比如机器有两个网卡A和B , IP地址分别是AIP和BIP , 你的程序绑定AIP , 那么操作系统只会将目的地是AIP的IP包转发给你的程序 。0.0.0.0是特殊的 , 它代表着能够转发目的地IP是机器上任意IP的IP包到你的程序 。
最后 , 为何可以通过公网IP访问到你的机器?这是因为云服务商给你做了NAT , 而这个地址你的机器是不知道的 , 也不属于你的机器上的任意一张网卡 , 所以你无法绑定 。
按规定格式发送数据后可以通信:
3.1.6 netty连接数据库 数据库表格如下:
①position表:
id是每个节点的编号 , lat纬度 , lng经度 , address是文字版的详细地址 , ismail记录发生噪声干扰时是否已经发送邮件给管理员 。
②now_data表
记录每个节点的当前数据 , 即网页地图实时展示的数据 , 当每次接收到传感器节点传来的数据时 , 就会进行now_data数据的更新 , 并将之前的数据记录到history_data表中 。
③history_data表
此表结构与now_data相同 。
注意的几个点:
①super传播
super.channelRead(ctx, msg); 在每一个Handler中都应该让数据传输到下一个ChannelInboundHandler中 。如果没有其他处理程序在意msg , 则无需调用它 。
②业务逻辑
在netty的业务逻辑中 , 将IO操作和占用时间的数据库操作分开 , 以免造成IO进程的堵塞 。IO操作是放在NioEventLoopGroup中的 , 而数据库等业务是放在DefaultEventLoopGroup中的 。代码如下:
首先在server类中放上DefaultEventLoopGroup , 要在一个私有类中进行使用 。
//配置业务线程组final EventLoopGroup businessGroup=new DefaultEventLoopGroup(16); 接着在pipeline中添加businessGroup:
/*** 网络事件处理器*/private class ChildChannelHandler extends ChannelInitializer {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {// 添加自定义协议的解码工具 处理完会将一个iot_protocol类型的数据传给ServerHandlerch.pipeline().addLast(new iot_protocol_decoder());// 处理网络IOch.pipeline().addLast(new ServerHandler());//处理数据库业务逻辑ch.pipeline().addLast(bussinessGroup,new bussinessHandler());}} 然后定义绑定上的businessHandler类 , 重写方法进行对数据库的操作 , 记得要对错误进行捕获处理 , 避免服务器宕机:
private class bussinessHandler extendsChannelInboundHandlerAdapter{/*** 在这里写入对数据库的操作* @param ctx* @param msg* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {iot_protocol sound =(iot_protocol)msg;Data data=https://tazarkount.com/read/new Data();statement =connection.createStatement();//把原先的数据取出来ResultSet resultSet =statement.executeQuery("select * from now_data where id=" + sound.getId());if(resultSet.next()){data.setId((int)resultSet.getObject(1));data.setSound((int)resultSet.getObject(2));data.setTime(((Timestamp) resultSet.getObject(3)).toString());}if(data.getTime()!=null) {//有数据才进行这些步骤没有数据就直接插入//插入到历史记录中String sql = "insert into history_data values(" + data.getId() + "," + data.getSound() + ",'" + data.getTime() + "')";System.out.println(sql);statement.execute(sql);//删除原有数据statement.execute("delete from now_data where id=" + data.getId());}//更新新数据statement.execute("insert into now_data(id,sound) values("+sound.getId()+","+Integer.parseInt(sound.getContent().substring(sound.getContent().length()-3))+")");super.channelRead(ctx, msg);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {super.exceptionCaught(ctx, cause);Channel channel = ctx.channel();if (channel.isActive()) ctx.close();}}
③timestamp的插入
注意timestamp插入数据库中的格式 , 需要引号 。
④部署服务器报错
ClassLoader classLoader = JdbcUtils.class.getClassLoader();URL resource = classLoader.getResource("db.properties");System.out.println(resource);String path = resource.getPath();//2.加载文件try {pro.load(new FileReader(path));} catch (IOException e) {e.printStackTrace();}
原因是读取配置文件出错 , 在windows下可以 , 但是在linux下 , url.getPath()得到的是properties文件的绝对路径 , 所以出错 , 改用以下方法:
ResourceBundle rb = ResourceBundle.getBundle("db");url = rb.getString("url");username = rb.getString("username");password = rb.getString("password");driver = rb.getString("driver");
到此netty服务器已搭建完成 。
3.2嵌入式代码 写累了 , 就简单写写吧...
嵌入式的代码时一步步慢慢完善的 , 从一开始点一个灯来确定PCB是否可用 , 到oled的显示 , 到传感器数据的读取及显示 , 再到使用esp8266对其联网上传 。
点灯的程序过于简单就不说了 , 从oled开始说 , 导入中景园提供的oled代码后 , 首先修改oled.h头文件中对引脚的宏定义:先查看绘制的电路图
可以看到SCL接的是PB6 , SDA接的是PB7 , 修改宏定义:
//-----------------OLED端口定义---------------- #define OLED_SCL_Clr() GPIO_ResetBits(GPIOB,GPIO_Pin_6)//SCL#define OLED_SCL_Set() GPIO_SetBits(GPIOB,GPIO_Pin_6)#define OLED_SDA_Clr() GPIO_ResetBits(GPIOB,GPIO_Pin_7)//DIN#define OLED_SDA_Set() GPIO_SetBits(GPIOB,GPIO_Pin_7) 然后注意还有oled.c中的init函数有对GPIO的初始化 , 也需要进行修改:
void OLED_Init(void){ GPIO_InitTypeDefGPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能B端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHzGPIO_Init(GPIOB, &GPIO_InitStructure);//初始化PB6,7GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);} 接着是传感器数据的读取 , 使用了原子哥的adc代码 , 同样要修改GPIO引脚 , 这次在h头文件中没有宏定义 , 直接修改c文件的init函数:
voidAdc_Init(void){ADC_InitTypeDef ADC_InitStructure;GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE );//使能ADC1通道时钟RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M //PA4 作为模拟通道输入引脚GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入引脚 GPIO_Init(GPIOA, &GPIO_InitStructure);ADC_DeInit(ADC1);//复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值 ...} 同时 , 由于使用的是PA4引脚 , 查询数据手册可知是通道4 , 所以在调用读取函数时需要选择为channel4 。
adcx=Get_Adc_Average1(ADC_Channel_4,30);
最后是esp8266的代码
一开始使用onenet官方给的代码 , 遇到了wifi接不上的问题 , 查询后发现是命令错误 , 官方给的是AT+CWJAP= , 而实际上要是AT+CWJAP_DEF= 。
更改后就可以连接上wifi , 传输数据到netty成功:
输出重定向:不是printf而是sprintf , 需要引入头文件#include "stdio.h".
注:这里%3d也有错误 , 前面不会补0 , 使用%03d才可以
在使用wifi模块遇到了很严重的问题 , 在定时器中断内无法发送数据 , 原先设想在定时器中断时读取并显示数据 , 再上传至数据服务器 , 但是在中断内不知道为什么发送到服务器端的却是AT指令 , 尝试了好久(设置中断优先级、查阅AT指令原理)也没有找到解决方法 , 最后只能在main函数中使用while和delay对数据进行发送 , 这也就意味着每次测量的显示值和发送到云端的值不同 。
3.3基于Thymeleaf的前端展示 【物联网嵌入式 校园噪声监测系统 ESP8266 STM32 LM386声音传感器 NETTY自定义协议】原本想用VUE来进行前端的搭建 , 框架选用ElementUI , 一个界面已经搭建好了 , 但是发现如果想要部署到服务器上 , 需要使用Nginx 。但是!!现在是凌晨1点 , 距离答辩还有七小时 , 只能找到以前做的一个项目进行修改 , 调用了腾讯地图api , 在前端设置了定时器进行Ajax轮询来定时获取数据库 。
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
