最近闲来无事,对硬件控制产生了兴趣。看到家里的盆栽,我突然萌生了制作一个自动浇水工具的想法。通过在淘宝搜索并查找相关资料,我了解了需要的硬件和通信协议。接下来,我们先看看需要做哪些准备工作(如安装 Arduino、.NET、EMQX 工具等,请自行搜索并完成安装)。
准备工作
硬件清单(淘宝可购买):
-
湿度传感器
-
湿度传感器继电器
-
WiFi 控制继电器(我用的是 ESP32。注意,这两种继电器是否能合并为一个?目前没有找到这样的硬件,只能用2个继电器了)
-
抽水器
-
电池(可选,也可以直接接家用电路,但要注意安全)
软件清单:
-
.NET 6
-
EMQX MQTT
-
服务器(Linux + Docker)
硬件和软件模块功能简介
1. 继电器的作用
继电器是一种电磁开关设备,能够在低电压或低电流的控制信号下,控制高电压或大电流的电路开关。主要功能包括:
-
电路隔离:保护低电压控制电路免受高电压或高电流的影响。
-
信号放大:将小电流控制信号转化为高功率输出。
-
多路控制:通过一个控制信号切换多个电路。
-
自动化控制:与传感器和控制器协作,实现自动化。
-
远程控制:通过 WiFi、蓝牙等信号远程控制。
2. 湿度传感器的作用
湿度传感器用于检测和测量环境湿度,并将湿度值转换为电信号,供继电器和控制系统读取。它是实现自动化浇水的核心输入模块。
3. EMQX 的作用
EMQX 是一种开源的 MQTT 消息服务器,基于 Erlang/OTP 构建,专为高并发、低延迟和高可靠性设计。它的核心作用是实现物联网(IoT)设备间的消息通信,支持通过 MQTT 协议连接大量设备并在它们之间传递消息。
实现思路
其余不做一一描述了,我们的目标是制作一个网页或手机 App,通过操作前端界面向服务器发送信号,服务器将信号转发给继电器,从而实现远程控制硬件。
实操步骤
第一步:电路连接
我们需要先把电路按照以下图片连接起来,期间接线时注意短路的,通电后测试每块硬件是否能正常亮灯(本人不太熟悉硬件,所以硬件接线过程中会有接错的情况,一般接错了灯是不会亮的);
第二步:服务端配置与代码编写
-
配置 EMQX
-
在服务器上安装并配置 EMQX(推荐 Linux + Docker 环境,简单高效)。
-
配置完成后,通过网页管理界面验证是否成功运行(注意mqtt的消息端口和管理地址端口是分开的)。成功界面如下图所示:
-
-
-
调用 EMQX 工具的代码
-
请使用MQTTnet的包,然后以下是调用 EMQX 的示例代码:
-
public class MqttBackgroundService : BackgroundService { private readonly MqttService _mqttService; private readonly ILogger<MqttBackgroundService> _logger; private readonly IConfiguration _configuration; public MqttBackgroundService(MqttService mqttService, ILogger<MqttBackgroundService> logger, IConfiguration configuration) { _mqttService = mqttService; _logger = logger; _configuration = configuration; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { // 启动 MQTT 客户端并连接 await _mqttService.ConnectAsync(); // 读取配置文件中的 MqttClients 配置 var mqttClientsConfig = _configuration.GetSection("MqttClients").Get<List<MqttClientConfig>>(); foreach (var clientConfig in mqttClientsConfig) { await _mqttService.SubscribeAsync(clientConfig.Topic); } // 保持连接,直到应用程序停止 while (!stoppingToken.IsCancellationRequested) { await Task.Delay(1000); // 可执行心跳检测或其他后台任务 } } catch (Exception ex) { _logger.LogError($"Error in MQTT connection: {ex.Message}"); } } }
public class MqttService { private IMqttClient _mqttClient; private string _brokerAddress = ""; // 设置EMQX服务器地址 private int _brokerPort = 1883; // 默认MQTT端口 private readonly SemaphoreSlim _mqttLock = new SemaphoreSlim(1, 1); // 线程锁 public MqttService() { var mqttFactory = new MqttFactory(); _mqttClient = mqttFactory.CreateMqttClient(); } // 启动时连接 public async Task ConnectAsync() { await _mqttLock.WaitAsync(); try { if (!_mqttClient.IsConnected) { var options = new MqttClientOptionsBuilder() .WithTcpServer(_brokerAddress, _brokerPort) .WithCredentials("", "") // 添加用户名和密码 .WithCleanSession() .Build(); _mqttClient.ConnectedAsync += ConnectedHandler; _mqttClient.DisconnectedAsync += DisconnectedHandler; _mqttClient.ApplicationMessageReceivedAsync += ApplicationMessageReceivedHandler; await _mqttClient.ConnectAsync(options); Console.WriteLine("MQTT Connected."); } } finally { _mqttLock.Release(); } } // 连接事件处理 private async Task ConnectedHandler(MqttClientConnectedEventArgs e) { Console.WriteLine("Connected to MQTT broker."); // 连接成功后订阅主题 //await SubscribeAsync("test/topic"); //_mqttLock.Release(); } // 断开连接事件处理 private Task DisconnectedHandler(MqttClientDisconnectedEventArgs e) { Console.WriteLine("Disconnected from MQTT broker."); return Task.CompletedTask; } // 接收到消息事件处理 private async Task ApplicationMessageReceivedHandler(MqttApplicationMessageReceivedEventArgs e) { var payload = e.ApplicationMessage.PayloadSegment.ToArray(); // 获取消息的 topic var topic = e.ApplicationMessage.Topic; var msg = Encoding.UTF8.GetString(payload); Console.WriteLine($"MessageReceive: {msg}"); //await Task.CompletedTask.Wait(); } // 订阅主题 public async Task SubscribeAsync(string topic) { await _mqttLock.WaitAsync(); try { await _mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).Build()); Console.WriteLine($"Subscribed to topic: {topic}"); } finally { _mqttLock.Release(); } } // 发布消息 public async Task PublishAsync(string topic, string message) { await _mqttLock.WaitAsync(); try { var mqttMessage = new MqttApplicationMessageBuilder() .WithTopic(topic) .WithPayload(message) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) // 改为 QoS 0 .Build(); if (!_mqttClient.IsConnected) { Console.WriteLine("MQTT client is not connected."); await ConnectAsync(); // 尝试重新连接 } await _mqttClient.PublishAsync(mqttMessage); Console.WriteLine($"Message sent: {message}"); } finally { _mqttLock.Release(); } } // 断开连接 public async Task DisconnectAsync() { await _mqttLock.WaitAsync(); try { await _mqttClient.DisconnectAsync(); Console.WriteLine("Disconnected from MQTT broker."); } finally { _mqttLock.Release(); } } }
-
-
编写 API
-
实现继电器开关的接口。
-
可加入定时控制功能(通过调度任务定时开关继电器)。
-
[Route("api/[controller]")] [ApiController] public class MqttController : ControllerBase { private readonly MqttService _mqttService; private readonly FileService _fileService; public MqttController(MqttService mqttService, FileService fileService) { _mqttService = mqttService; _fileService = fileService; } [HttpPost("sendMessage")] public async Task<IActionResult> SendMessage(MqttMessageRequest request) { try { await _fileService.SaveFileContentAsync(request.Message); // 2. 发布消息到主题 await _mqttService.PublishAsync(request.Topic, JsonSerializer.Serialize( request.Message)); Console.WriteLine($"Message sent to topic {request.Topic}: {JsonSerializer.Serialize(request.Message)}"); return Ok(request.Message); } catch (Exception ex) { return StatusCode(500, $"Failed to send message: {ex.Message}"); } } // 根据 ClientId 获取文件内容 [HttpGet("GetFile/{clientId}")] public async Task<IActionResult> GetFileContent(string clientId) { try { var model = await _fileService.GetFileContentAsync(clientId); return Ok(model); // 返回对应的文件内容 } catch (FileNotFoundException ex) { return NotFound(ex.Message); // 如果找不到文件,返回 404 } catch (Exception ex) { return StatusCode(500, ex.Message); // 其他异常返回 500 } } }
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MQTT 控制开关</title> <style> body { font-family: Arial, sans-serif; background-color: #f4f7fa; margin: 0; padding: 0; } .container { width: 100%; max-width: 600px; margin: 50px auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: #333; } .form-group { margin-bottom: 20px; } .form-group label { display: block; font-size: 16px; color: #555; margin-bottom: 8px; } .form-group input[type="time"] { width: 100%; padding: 10px; font-size: 14px; border: 1px solid #ddd; border-radius: 4px; background-color: #f9f9f9; box-sizing: border-box; /* 保证padding包含在宽度内 */ } /* 自定义开关按钮样式 */ .switch { position: relative; display: inline-block; width: 60px; height: 34px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 26px; width: 26px; border-radius: 50%; left: 4px; bottom: 4px; background-color: white; transition: .4s; } input:checked + .slider { background-color: #4CAF50; } input:checked + .slider:before { transform: translateX(26px); } button { width: 100%; padding: 12px; font-size: 16px; color: #fff; background-color: #007bff; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } button:hover { background-color: #0056b3; } /* 弹框样式 */ .modal { display: none; position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } .modal-content { background-color: #fff; padding: 20px; border-radius: 8px; width: 80%; max-width: 400px; text-align: center; } .modal .message { font-size: 18px; color: #28a745; } .modal .error-message { color: #dc3545; } .modal-close { margin-top: 20px; padding: 10px 20px; background-color: #007bff; color: #fff; border: none; border-radius: 4px; cursor: pointer; } .modal-close:hover { background-color: #0056b3; } </style> <script> // 函数:当开关按钮被点击时执行POST请求 function toggleSwitch() { var topic = "Client3"; var message = { ClientId: "Client3", DailyScheduledTime: document.getElementById('scheduledTime').value, CurrentSwitch: document.getElementById('switch').checked }; var data = { Topic: topic, Message: message }; // 使用fetch发送POST请求 fetch('/api/Mqtt/sendMessage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { console.log('成功:', data); showModal('操作成功!', 'message'); }) .catch((error) => { console.error('错误:', error); showModal('操作失败,请重试!', 'error-message'); }); } // 弹框显示函数 function showModal(message, messageType) { const modal = document.getElementById('responseModal'); const modalMessage = document.getElementById('modalMessage'); const modalContent = document.getElementById('modalContent'); modalMessage.textContent = message; modalContent.classList.remove('message', 'error-message'); modalContent.classList.add(messageType); modal.style.display = 'flex'; // 显示弹框 } // 关闭弹框 function closeModal() { const modal = document.getElementById('responseModal'); modal.style.display = 'none'; } </script> </head> <body> <div class="container"> <h1>MQTT 控制开关</h1> <div class="form-group"> <label for="scheduledTime">每天定时: </label> <input type="time" id="scheduledTime" required /> </div> <div class="form-group"> <label for="switch">当前开关状态: </label> <!-- 自定义开关按钮 --> <label class="switch"> <input type="checkbox" id="switch" /> <span class="slider"></span> </label> </div> <button onclick="toggleSwitch()">提交</button> </div> <!-- 弹框 --> <div id="responseModal" class="modal"> <div class="modal-content" id="modalContent"> <div id="modalMessage" class="message">操作成功!</div> <button class="modal-close" onclick="closeModal()">关闭</button> </div> </div> </body> </html>
-
第三步:Arduino 嵌入式代码编写
过程本人踩过的坑:1.要了解继电器的GPIO用是哪个,不然你无法控制;2.Wifi控制的情况下,由于继电器是有一个虚拟WiFi的,你需要链接到继电器的虚拟WiFi后,才能配置自家的WiFi;当然你也可以连接蓝牙来配置;3.需要把设置好的WiFi账号密码记录到继电器否则每次启动都要配置WiFi(代码写入即可);
以下是我这次的代码可做参考:
#include <WiFi.h> #include <ESPAsyncWebServer.h> #include <PubSubClient.h> #include <ArduinoJson.h> #include <HTTPClient.h> #include <Preferences.h> // Include Preferences library #define RELAY_PIN 4 // Relay control pin #define LED_ALWAYS_ON 3 // LED pin for other devices like lights const char* ssid = "ESP32_Config"; // Wi-Fi hotspot name for initial setup const char* password = "123456"; // Wi-Fi password for the hotspot String targetSSID = ""; String targetPassword = ""; const char* mqttServer = ""; // MQTT server address const int mqttPort = ; // MQTT port const char* mqttUser = ""; // MQTT username const char* mqttPassword = ""; // MQTT password WiFiClient espClient; PubSubClient client(espClient); // MQTT client AsyncWebServer server(80); // Web server object listening on port 80 const int relayPin = 5; // Relay control pin (modify as necessary) // HTTP request URL String apiUrl = "";//API地址 // Preferences instance for storing Wi-Fi credentials Preferences preferences; // MQTT callback function void mqttCallback(char* topic, byte* payload, unsigned int length) { String message = ""; for (int i = 0; i < length; i++) { message += (char)payload[i]; } // Create a JSON document to parse the incoming message StaticJsonDocument<200> doc; DeserializationError error = deserializeJson(doc, message); if (error) { Serial.println("Failed to parse JSON"); return; } String clientId = doc["ClientId"].as<String>(); // Extract clientId from JSON bool currentSwitch = doc["CurrentSwitch"].as<bool>(); // Extract currentSwitch value if (clientId == "Client3") { digitalWrite(RELAY_PIN, currentSwitch ? HIGH : LOW); // Control relay based on currentSwitch } } // Wi-Fi connection function bool connectWiFi() { Serial.println("Connecting to Wi-Fi..."); WiFi.begin(targetSSID.c_str(), targetPassword.c_str()); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWi-Fi connected successfully!"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); digitalWrite(LED_ALWAYS_ON, HIGH); // Turn on LED return true; } else { Serial.println("\nWi-Fi connection failed!"); return false; } } // MQTT connection function bool connectMQTT() { while (!client.connected()) { Serial.print("Connecting to MQTT..."); if (client.connect("ESP32Client", mqttUser, mqttPassword)) { Serial.println("Connected to MQTT Broker."); return true; } else { Serial.print("."); delay(1000); // Retry if connection fails } } return false; } // Function to get data from API and subscribe to MQTT topics void getDataFromAPI() { HTTPClient http; http.begin(apiUrl); // Specify URL int httpCode = http.GET(); // Send GET request if (httpCode > 0) { String payload = http.getString(); Serial.println("Received Data:"); Serial.println(payload); // Parse the JSON response from the API DynamicJsonDocument doc(1024); deserializeJson(doc, payload); String clientId = doc["clientId"]; client.subscribe(clientId.c_str()); // Subscribe to the topic } else { Serial.println("Failed to get HTTP data"); } http.end(); // End HTTP request } void setup() { Serial.begin(115200); pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW); // Initialize relay to LOW pinMode(LED_ALWAYS_ON, OUTPUT); digitalWrite(LED_ALWAYS_ON, LOW); // Initialize LED to LOW // Initialize Preferences to read and write Wi-Fi credentials preferences.begin("wifi_config", false); // Open preferences in read-write mode // Read saved Wi-Fi credentials from Preferences targetSSID = preferences.getString("SSID", ""); targetPassword = preferences.getString("PASSWORD", ""); // If the credentials are saved, try to connect to Wi-Fi if (targetSSID != "" && targetPassword != "") { if (connectWiFi()) { // Once connected to Wi-Fi, connect to the MQTT broker client.setServer(mqttServer, mqttPort); client.setCallback(mqttCallback); if (connectMQTT()) { getDataFromAPI(); // Fetch data from the API if MQTT connection is successful } else { Serial.println("Failed to connect to MQTT Broker."); } } } else { Serial.println("No saved Wi-Fi credentials. Keeping the device in AP mode."); // Set up the device in AP mode for Wi-Fi configuration WiFi.softAP(ssid, password); Serial.println("Wi-Fi hotspot started."); Serial.print("IP Address: "); Serial.println(WiFi.softAPIP()); } // Web server to handle Wi-Fi credentials saving server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request) { targetSSID = request->arg("SSID"); targetPassword = request->arg("PASSWORD"); // Save Wi-Fi credentials in Preferences preferences.putString("SSID", targetSSID); preferences.putString("PASSWORD", targetPassword); String response = "{\"status\":\"success\",\"message\":\"Wi-Fi configuration saved!\"}"; request->send(200, "application/json", response); // After saving the credentials, connect to Wi-Fi if (connectWiFi()) { // Connect to MQTT broker client.setServer(mqttServer, mqttPort); client.setCallback(mqttCallback); if (connectMQTT()) { getDataFromAPI(); // Fetch data from API } } }); // Start the web server server.begin(); } void loop() { // If Wi-Fi connection is lost, attempt to reconnect if (WiFi.status() != WL_CONNECTED) { Serial.println("Wi-Fi connection lost, reconnecting..."); connectWiFi(); } // Ensure the MQTT client loop is running client.loop(); }
最终效果展示
通过网页或手机 App 操作,可以实现:
-
远程控制继电器开关。
-
根据湿度传感器数据,自动开启或关闭抽水器。
整个项目非常简单,适合入门硬件控制的朋友。DIY 成本约 20 元,而购买成品价格至少 50 元。
经验总结
通过这个项目,我从对硬件一无所知逐渐熟悉了硬件控制,深刻感受到今天硬件发展的迅速。希望本文能对有一定软件基础并想尝试硬件开发的朋友有所帮助!
标签:Arduino,color,NET,Wi,MQTT,Fi,Serial,message From: https://www.cnblogs.com/weirun/p/18617489