最终效果
基于NodeMCU的物联网窗帘控制系统设计
项目介绍
该项目是"物联网实验室监测控制系统设计(仿智能家居)"项目中的"家电控制设计"中的"窗帘控制"子项目,最前者还包括"物联网设计"、"环境监测设计"、"门禁系统设计计"和"小程序设计"等内容。本文只介绍"窗帘控制"部分。
项目功能实现的大致思路为:当单片机接收到MQTT服务器传来的窗帘新位置时,驱动步进电机转动,使窗帘移动到指定位置。
硬件设计
接线
|---------|---------|----------|-----|
| NodeMCU | ULN2003 | 28BYJ-48 | 电源 |
| | OUT1 | 1 | |
| | OUT2 | 2 | |
| | OUT3 | 3 | |
| | OUT4 | 4 | |
| D4 | INT1 | | |
| D3 | INT2 | | |
| D2 | INT3 | | |
| D1 | INT4 | | |
| | + | 5 | 5V |
| GND | - | | GND |
成本
|---------|------------|
| NodeMCU | 28BYJ-48模组 |
| 27.9 | 8.53 |
其中共需36.5元左右来购买该项目所需的模块。此外还需1根数据线、若干杜邦线、能提供5~12V中间任意电压的电源。
机械模型搭建
为使演示更贴合实际,本系统制作了一个窗帘模型,模型图见下文。
模型左上方的黑色绝缘胶带表示窗帘的移动端,位于左侧时表示窗帘闭合(遮住窗户);位于右侧时表示窗帘打开(露出窗户)。在模型中,步进电机带动齿轮旋转,从而带动传送带转动,进而实现窗帘的移动。通过控制步进电机的旋转,便可将窗帘移动至指定位置。该模型中的两齿轮中心距为800mm,主动轮的周长约为74.61mm(比两齿轮中心距的10%略小),步进电机旋转10圈可将窗帘移动到另一侧(窗帘行程留有冗余)。
器件图
正面俯视图 正面右视图 正面左视图 正面右部分局部后视图 反面俯视图
器件尺寸
未完待续
皮带:未完待续
28BYJ-48型步进电机的轴:未完待续
木板:未完待续
软件设计
本次的开发环境为Arduino IDE,开发板型号为NodeMCU 0.9 (ESP-12 Module)。
本系统软件部分的流程如下图所示。在初始化之后,等待小程序下发窗帘位置,据此驱动步进电机旋转。
连接WiFi以及接收MQTT服务器传来的消息,可参考:利用ESP-01S中继实现STM32F103C8T6与MQTT服务器的串口双向通信_mqtt和stm32开发板通信-CSDN博客
解析JSON数据,可参考:Arduino中解析JSON数据-CSDN博客
驱动28BYJ-48型步进电机转动,可参考:NodeMCU驱动28BYJ-48型步进电机(Arduino)-CSDN博客
cpp
//选择NodeMCU 0.9 (ESP-12 module)
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Arduino.h>
// 设置wifi接入信息和MQTT服务器
const char* wifiname = "DOILMSBOIOT";
const char* password = "doilmsboiot";
const char* mqttServer = "broker.emqx.io";
bool receive_message_flag = 0; //1表示收到信息但还未处理,0表示未收到信息或已处理
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
// 待解析的json文件,所需空间:13~15个字节,正好初始值为最多的字节;若初始化时空间不足,收到信息后无法赋值
String json = "{\"curtain\":000}";
// 创建DynamicJsonDocument对象
const size_t capacity = JSON_OBJECT_SIZE(1) + 32 ; //1表示待解析的JSON对象中有1对数据,32为解析过程中需要的额外空间,可在此网站计算 https://arduinojson.org/v6/assistant/#/step1
DynamicJsonDocument doc(capacity);
int curtain_position ; // 解析后的窗帘位置
int curtain_now_position =0 ; // 窗帘现在的位置
void setup()
{
Serial.begin(9600); // 启动串口通讯
WiFi.mode(WIFI_STA); //设置ESP8266工作模式为无线终端模式
connectWifi(); // 连接WiFi
mqttClient.setServer(mqttServer, 1883); // 设置MQTT服务器和端口号
mqttClient.setCallback(receiveCallback); // 设置MQTT订阅回调函数
connectMQTTserver(); // 连接MQTT服务器
stepmotor_initial(); //步进电机初始化
}
void loop()
{
if (mqttClient.connected()) // 如果开发板成功连接服务器
{
mqttClient.loop(); // 处理信息(收到信息后的回调函数)以及心跳
}
else // 如果开发板未能成功连接服务器
{
connectMQTTserver(); // 则尝试连接服务器并订阅主题
}
if (receive_message_flag == 1) //收到信息但还未处理
{
deserializeJson(doc, json); // 反序列化数据
// 解析收到的数据信息
curtain_position = doc["curtain"].as<int>();
if(curtain_position - curtain_now_position > 0)
{
Serial.print("电机要转到的位置:");Serial.println(curtain_position);
Serial.print("电机现在的位置:");Serial.println(curtain_now_position);
int cycle = (int)(curtain_position - curtain_now_position)/10;
Serial.println("开始转动");
Serial.println(cycle);
for(int i=0; i < cycle; i++)
{
clockwise_turn_one_circle();
curtain_now_position += 10;
Serial.print("转过的圈数:");Serial.println(i);
}
Serial.println("结束转动");
Serial.print("电机现在的位置:");Serial.println(curtain_now_position);
Serial.println("");
}
if(curtain_position - curtain_now_position < 0)
{
Serial.print("电机要转到的位置:");Serial.println(curtain_position);
Serial.print("电机现在的位置:");Serial.println(curtain_now_position);
int cycle = (int)(curtain_now_position - curtain_position)/10;
Serial.println("开始转动");
Serial.println(-cycle);
for(int i=0; i < cycle; i++)
{
anti_clockwise_turn_one_circle();
curtain_now_position -= 10;
Serial.print("转过的圈数:");Serial.println(i);
}
Serial.println("结束转动");
Serial.print("电机现在的位置:");Serial.println(curtain_now_position);
Serial.println("");
}
receive_message_flag = 0; //已处理接收到的信息
}
}
// 连接MQTT服务器并订阅主题
void connectMQTTserver()
{
// 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
String clientId = "esp8266-" + WiFi.macAddress();
if (mqttClient.connect(clientId.c_str())) //如果成功连接MQTT服务器
{
Serial.print("MQTT Server Has Connected. ");
Serial.print("Server Address: ");
Serial.println(mqttServer);
Serial.print("ClientId: ");
Serial.println(clientId);
subscribeTopic(); // 订阅指定主题
}
else
{
Serial.print("MQTT Server Connect Failed. Client State:");
Serial.println(mqttClient.state());
delay(3000);
}
}
// 收到信息后的回调函数
void receiveCallback(char* topic, byte* payload, unsigned int length)
{
Serial.print("Message with the topic of [ ");
Serial.print(topic);
Serial.println(" ] has been received.");
Serial.print("Content: ");
for (int i = 0; i < length; i++)
{
Serial.print((char)payload[i]);
json[i] = (char)payload[i]; //将收到的信息赋给json,以便后续解析和发射信号
}
Serial.println("");
for (int i = length; i < 15; i++) //清除掉多余字符
{
json[i] = '\0';
}
receive_message_flag = 1; //表示收到信息但还未处理
Serial.print("Message Length (Bytes) : ");
Serial.println(length);
Serial.println(" ");
}
// 订阅指定主题
void subscribeTopic()
{
String topicString = "deviceControl3/curtain"; // 订阅主题的名称
char subTopic[topicString.length() + 1];
strcpy(subTopic, topicString.c_str());
if(mqttClient.subscribe(subTopic)) //如果成功订阅主题
{
Serial.print("Subscrib Topic: ");
Serial.println(subTopic);
Serial.println("");
} else
{
Serial.print("Subscribe Fail...");
}
}
// ESP8266连接wifi
void connectWifi()
{
WiFi.begin(wifiname, password);
Serial.println("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) //等待WiFi连接,当wifi未连接时,持续输出".";成功连接后输出连接成功信息
{
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected!");
Serial.println("");
}
void stepmotor_initial()
{
pinMode(D1, OUTPUT);
pinMode(D2, OUTPUT);
pinMode(D3, OUTPUT);
pinMode(D4, OUTPUT);
}
void clockwise_turn_one_circle()
{
for(int i=0;i<512;i++)
{
digitalWrite(D1, HIGH);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, HIGH);
digitalWrite(D2, HIGH);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, HIGH);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, HIGH);
digitalWrite(D3, HIGH);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, HIGH);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, HIGH);
digitalWrite(D4, HIGH);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, HIGH);
delay(1);
digitalWrite(D1, HIGH);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, HIGH);
delay(1);
}
}
void anti_clockwise_turn_one_circle()
{
for(int i=0;i<512;i++)
{
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, HIGH);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, HIGH);
digitalWrite(D4, HIGH);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, HIGH);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, HIGH);
digitalWrite(D3, HIGH);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, LOW);
digitalWrite(D2, HIGH);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, HIGH);
digitalWrite(D2, HIGH);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, HIGH);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);
delay(1);
digitalWrite(D1, HIGH);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, HIGH);
delay(1);
}
}
不足之处
- 电机转速太慢
- 缺少获取窗帘当前位置的功能,无法处理手动和打滑。