文章目录
前言
最近刚刚找完工作,总想鼓捣些小玩意,想到了自己有台坏掉的台灯,于是就用ESP8266对台灯进行了物联网改造,实现了通过局域网控制台灯的功能。
由于这套改造方案可以套用在除了台灯之外的其他家居设备上,因此我就大言不惭地将这套方案称为:家居设备物联网改造的解决方案。
一、详细介绍
整个方案分为三个部分:
- 硬件电路
- IOT设备固件(Arduino)
- 客户端app(Android)
下面针对不同部分进行介绍。
1.硬件电路
台灯里面有一张电路板负责控制,如图所示:
电路板上有四个微动按钮,从左到右分别对应了 亮度调高、色温、开关、亮度调低 这四个操作。
这些微动按钮的一侧引脚连接电源负极,另一侧按钮连接板上的控制芯片,按下按钮后控制芯片引脚检测到低电平,就会执行相应的操作。
因此,只需要向控制芯片的对应引脚发送低电平脉冲就可以模拟出点击微动按钮的效果。而发送脉冲对于ESP8266来说十分简单,具体的接线方案就不再赘述了。
2.IOT设备固件
IOT设备固件是整个解决方案的重中之重,由于本方案是通过局域网实现设备的控制,因此IOT设备既是用户请求的接收者,又是控制信号的发送者。除此之外,考虑到用户在第一次使用时需要对IOT设备的参数进行配置,因此还需要在固件中暴露IOT设备的配置接口。
下面我将以 配置接口->配置数据持久化->请求的接收与响应 的顺序介绍IOT设备固件。
1) 配置接口
a. 启动流程
ESP8266可以通过简单的几行代码实现http服务器的功能,这为我们暴露配置接口提供了便利。结合该单片机的WiFi热点、WiFi连接、EEPROM数据持久化功能,可以设计一套逻辑来实现设备的初始化配置。流程如下:
- step1:设备启动时,检测本地EEPROM中是否存在配置:
- 若存在配置,则读取配置,跳转到step2;
- 若不存在配置,则表明是第一次启动,跳转到step3;
- step2:尝试根据配置信息连接对应的WiFi:
- 若连接失败,则跳转到step3;
- 若连接成功,则跳转到step4;
- step3:开启设备的WiFi热点和配置服务器,等待接收用户发送的配置,在接收配置后,将配置信息存储到EEPROM中并重启设备,从step1重新开始;
- step4:开启控制服务器,等待接收用户的控制信号并执行相应的操作。
经过上述一套流程,IOT设备已经可以成功连接目标WiFi并开始接收控制信号和执行操作,用户可以在同一局域网中,通过客户端app扫描并连接设备,对设备发起控制。由于配置信息保存在设备的EEPROM中,因此就算设备重启,也不需要重新进行配置,除非需要切换WiFi环境。
b. 重点问题
下面对上述流程中的几个重点问题进行介绍
(1). 配置信息包含哪些?
- 设备名称
- WiFi的SSID
- WiFi的密码
(2). 如何发送配置信息?
- 还是依靠http服务器,在用户访问服务器首页时,返回一个html页面,用户通过html页面填写配置信息,并将配置信息作为表单提交给服务器;
- 当然,在服务器中需要设置表单接收和处理的相关逻辑。
- 配置界面中还添加了对于控制接口的测试按钮,如图所示:
(3). 客户端app如何扫描设备?
- 通过访问自身所处的网段和相邻网段中所有可能主机的指定路径(/identity/
),通过响应来判断该主机是否就是我们要找的设备;
- 若在访问某主机的指定路径时,得到了带有指定前缀(remote-controller
)的响应,则就可认定该主机是我们要找的设备。
(4). 数据如何在EEPROM中存储?
- 针对EEPROM的读写问题,官方提供了对应的头文件,只需要调用头文件中的函数就可以实现字节数据的存储和读取;
- 由于配置信息全都是由字符串组成,因此可以将字符串以字节的形式进行存储和读取。这样就可以实现配置信息的持久化,具体细节将在下一部分详细介绍。
c. 相关代码
- 初始化网络
void initNetwork(int timeout){
// 处理wifi, getSign()是读取本地配置标记的函数
if (getSign() == "Y")
{
String ssid = getSsid();
String passwd = getPasswd();
// 尝试连接wifi
if (connectWifi(timeout, ssid.c_str(), passwd.c_str()))
return;
}
// 连接wifi失败,开启ap服务
startAp();
}
- 初始化服务器
// 声明服务器
ESP8266WebServer server(80);
// 设置服务器对应路径的handler
void setupServer(){
server.on("/",handleIndex);
server.on("/identity/",handleIdentity); // for scanning
server.on("/switch/", handleSwitch);
// save wifi info
server.on("/saveinfo/", handleSave);
// handle power
server.on("/power/", handlePower);
// handle light mode
server.on("/mode/", handleMode);
// handle light up
server.on("/up/", handleLightUp);
// handle light down
server.on("/down/", handleLightDown);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("Server started...");
}
- 处理配置信息的handler以及保存函数
// 服务器中用于处理配置信息的handler
void handleSave(){
if (!handlePost())
return;
// 解析参数
const char* ssid = server.arg("ssid").c_str();
const char* passwd = server.arg("passwd").c_str();
Serial.printf("Receive data: \nssid: %s\t password: %s\n\n",ssid,passwd);
// 存储wifi信息
server.send(200, "text/plain", "update success, device will reboot in 5 seconds...");
saveWifiInfoAndReset(ssid,passwd);
}
// 保存配置信息并重启
void saveWifiInfoAndReset(const char* ssid, const char* passwd){
// 清空数据
Serial.printf("Writing data: %s, %s\n", ssid, passwd);
// 写入新数据
saveSsid(ssid);
savePasswd(passwd);
saveSign();
delay(5000);
// 重启
Serial.println("\nRebooting...");
ESP.restart();
}
2) 配置数据持久化
ESP8266上有4KB大小的EEPROM,因此可以将配置信息写入EEPROM来实现配置数据的持久化。
但是要注意的是,官方头文件只提供了字节读写操作的函数,因此在数据读写时,需要自行进行序列化和反序列化。
这一部分话不多说,直接展示写入和读取的关键代码。
// 初始化EEPROM
void initEeprom(){
EEPROM.begin(128);
}
/*
@Time : 2022/11/13 20:45:07
@Author : victor2022
@Desc : 写入字符串
*/
bool set_string(int addr, int size, String str)
{
// 保证长度不超过最大限制
size = size<str.length()?size:str.length();
EEPROM.write(addr, size); // EEPROM第addr位,写入str字符串的长度
//通过一个for循环,把str所有数据,逐个保存在EEPROM
for (int i = 0; i < size; i++)
{
EEPROM.write(addr + i +1, char(str[i]));
EEPROM.commit();
}
Serial.println("Data has been saved to EEPROM!");
return true; //执行保存EEPROM
}
/*
@Time : 2022/11/13 20:45:24
@Author : victor2022
@Desc : 读取字符串
*/
String get_string(int addr, int size)
{
// 读取长度
byte tempSize = EEPROM.read(addr);
if(tempSize>0){
size = tempSize;
}
addr++;
// 读取数据
String data = "";
//通过一个for循环,从EEPROM中逐个取出每一位的值,并连接起来
for (int i = 0; i < size; i++)
{
data += char(EEPROM.read(addr + i));
}
return data;
}
/*
@Time : 2022/11/13 22:50:57
@Author : victor2022
@Desc : 清空数据
*/
void clear(int addr, int size)
{
for (int i = addr; i <= addr+size; i++)
{
EEPROM.write(i, 0);
}
EEPROM.commit();
}
3) 请求的接收与响应
控制请求依然依靠http协议在客户端app和IOT设备之间进行传递,控制原理如下:
- 在http服务器中定义不同路径对应的的handler;
- 在handler中定义相应的操作(如:发射脉冲、修改某个io口的输出电平);
- 当客户端app向某个路径发送http请求时,设备就可以执行该路径对应handler中定义的逻辑。
拿开关操作举例,当服务器的/power/
路径被访问时,就调用其对应的handler,向控制开关的io口发送一个低电平脉冲。代码如下:
// /power/路径对应的handler
void handlePower(){
Serial.println("power is visited...");
switchPower();
sendString("success");
}
// 开关灯函数
void switchPower(){
pulse(powerPin);
}
// 向某io口发送低电平脉冲
void pulse(int pinNum){
digitalWrite(pinNum, LOW);
delay(30);
flash();
digitalWrite(pinNum, HIGH);
}
3.客户端app
客户端app需要做到下面几件事:
- 扫描局域网中的IOT设备
- 保存已经扫描到的IOT设备信息
- 向IOT设备发送控制信号
app的页面布局如下图所示:
下面针对app的不同功能分别进行介绍。
1) 扫描IOT设备
扫描必须在手机连接wifi的情况下才能开始,在断开连接的情况下,点击上方的状态条开始扫描。设备扫描步骤如下:
- step1:获取手机在局域网中分配到的ip地址
- step2:解析ip地址,获取网段信息
- step3:向当前及相邻网段中所有主机的
/identity/
路径发送get请求,尝试获取信息- 若某个请求得到了正常响应,且响应以
remote-controller
开头,则表明此主机就是我们要找的IOT设备,扫描结束,进入已连接状态。 - 若扫描进行30s之后仍然没有找到目标设备,则停止扫描,仍为未连接状态。
- 若某个请求得到了正常响应,且响应以
由于安卓不允许在主线程上进行网络请求操作,因此需要使用另一个线程处理扫描过程。另外,为了快速地扫描大量的主机,还需要使用线程池进行扫描速度优化。
2) 保存IOT设备信息
在完成设备扫描后,可以通过规定好的api读取IOT设备的信息,并将这些信息保存在手机本地,方便下次开启应用时能够迅速地重新连接。
IOT设备信息包含连接状态、设备名称和设备ip地址,这些信息都比较简短,可以通过安卓原生提供的sharedPreference
来进行存储和读取。
除此之外,为了能够实时更新连接状态,app中还实现了心跳机制来检测连接的有效性,在检测到连接失效时,就会及时更新连接状态。
3) 向IOT设备发送控制信号
在完成连接之后,就可以通过Control Panel
中的操作按钮发送控制信号了。控制信号依然通过访问指定api的方式由http协议发送给IOT设备。
若想要进行进一步改造,需要在IOT设备中添加新的控制api,实现控制逻辑,并在客户端app中增加新的按钮,添加点击事件即可。
二、成品展示和演示
1. 硬件展示
- 正面,采用WemosD1mini(ESP8266)作为IOT控制板,18650锂电池作为电源,如图所示:
- 背面,台灯自己的电路板,加上一堆乱七八糟的热熔胶(没加胶之前挺好看的),如图所示:
2. App使用演示
三、总结
整个项目涉及了嵌入式、cpp开发(Arduino)、安卓开发等技术,但是整体难度不大。相关代码已经托管在了github上,带有签名的app也发布在了release中,项目地址:remote-controller。
有兴趣的同学可以玩一玩(拿来做课程设计也是极好的)。
由于我本人是Java选手,所以cpp代码都是最基础的函数调用,如果有同学有改进的想法,也欢迎一起参与这个项目。