基于Rust与ESP32的Wi-Fi红外空调遥控器开发实战

📅 2026/7/25 2:41:52
基于Rust与ESP32的Wi-Fi红外空调遥控器开发实战
你有没有遇到过这样的情况炎热的夏天回到家空调遥控器却找不到了或者想要在床上调整温度却懒得起身找遥控器今天我要分享的就是如何用一块小小的M5StickC开发板结合Rust语言打造一个可以通过Wi-Fi远程控制的空调遥控器。这个项目看似简单但背后涉及的技术栈却很有意思ESP32芯片的嵌入式开发、Rust语言在嵌入式领域的应用、红外信号编解码、以及网络通信协议。更重要的是它展示了如何用现代开发工具解决日常生活中的实际问题。1. 这个项目真正解决了什么问题传统的空调遥控器有几个明显的痛点容易丢失、控制距离有限、无法远程操作。而市面上的智能空调虽然支持手机控制但价格昂贵且需要依赖厂商的云服务。这个项目的核心价值在于低成本解决方案M5StickC开发板价格在百元以内远低于智能空调的差价完全本地控制不依赖任何云服务数据完全在本地网络流转高度可定制可以根据自己的需求定制控制逻辑和界面技术学习价值同时学习嵌入式开发、Rust语言和网络编程特别适合以下场景的开发者想要入门嵌入式Rust开发的初学者需要对现有家电进行智能化改造的DIY爱好者希望了解物联网设备本地化解决方案的技术人员2. 技术栈选择背后的考量2.1 为什么选择M5StickCM5StickC是一款基于ESP32芯片的微型开发板它具备以下优势特性说明对本项目的价值小巧尺寸48mm × 25mm × 13mm便于安装在任意位置内置显示屏0.96寸彩色LCD可以显示状态和温度信息红外发射内置IR LED直接控制空调无需额外硬件Wi-Fi支持基于ESP32的Wi-Fi模块实现网络远程控制电池供电内置80mAh锂电池可以便携使用2.2 为什么选择Rust语言在嵌入式领域C/C一直是主流选择。但Rust提供了独特优势内存安全编译时检查避免内存错误对于长期运行的设备至关重要现代工具链cargo包管理器让依赖管理变得简单强大的类型系统减少运行时错误提高代码可靠性嵌入式生态成熟esp-rs项目为ESP32提供了完整的支持// Rust在嵌入式中的优势示例强大的类型检查 #[derive(Debug)] enum AirConditionerMode { Cool, Heat, Fan, Dry, } impl AirConditionerMode { fn from_str(s: str) - ResultSelf, static str { match s { cool Ok(AirConditionerMode::Cool), heat Ok(AirConditionerMode::Heat), fan Ok(AirConditionerMode::Fan), dry Ok(AirConditionerMode::Dry), _ Err(Invalid mode), } } }3. 环境准备与工具链配置3.1 硬件准备清单M5StickC开发板 × 1USB Type-C数据线 × 1空调用于测试红外信号电脑Windows/macOS/Linux均可3.2 软件环境搭建Rust嵌入式开发环境配置相对复杂但遵循以下步骤可以避免很多坑# 1. 安装Rust工具链 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env # 2. 添加ESP32目标支持 rustup target add riscv32imc-unknown-none-elf # 针对ESP32-C3 # 或者 rustup target add xtensa-esp32-none-elf # 针对ESP32 # 3. 安装espup工具ESP32开发工具链管理 cargo install espup # 4. 安装ESP32工具链 espup install # 5. 配置环境变量添加到~/.bashrc或~/.zshrc source ~/export-esp.sh3.3 项目依赖配置创建新的Rust项目并配置依赖cargo new ac-wifi-remote cd ac-wifi-remote在Cargo.toml中添加依赖[package] name ac-wifi-remote version 0.1.0 edition 2021 [dependencies] esp-idf-hal 0.42 esp-idf-svc 0.42 embedded-svc 0.25 anyhow 1.0 log 0.4 [dependencies.esp-idf-sys] version 0.33 features [binstart]4. 红外信号编解码原理4.1 空调红外协议基础空调红外信号比电视遥控器复杂得多因为它需要传输更多信息开关状态运行模式制冷/制热/除湿/送风温度设定通常16-30℃风速设定扫风模式常见的空调红外协议有NEC协议最常用但空调通常使用扩展版本RC-5协议飞利浦系列自定义协议各品牌厂商的自定义格式4.2 信号捕获与分析首先需要捕获原装遥控器的红外信号use esp_idf_hal::prelude::*; use esp_idf_hal::gpio; use esp_idf_hal::delay::FreeRtos; struct IrReceiver { pin: gpio::PinDriverstatic, gpio::Gpio4, gpio::Input, } impl IrReceiver { fn new() - anyhow::ResultSelf { let peripherals Peripherals::take().unwrap(); let pin gpio::PinDriver::input(peripherals.pins.gpio4)?; Ok(IrReceiver { pin }) } fn capture_signal(mut self) - anyhow::ResultVecu32 { let mut timings Vec::new(); let mut last_state self.pin.is_high(); let mut start_time FreeRtos::get_tick_count(); // 捕获持续时间的信号 for _ in 0..1000 { let current_state self.pin.is_high(); if current_state ! last_state { let duration FreeRtos::get_tick_count() - start_time; timings.push(duration); start_time FreeRtos::get_tick_count(); last_state current_state; } FreeRtos::delay_ms(1); } Ok(timings) } }4.3 信号解析与生成解析捕获到的红外信号struct IrSignal { header_mark: u32, header_space: u32, data_bits: Vecbool, bit_duration_mark: u32, bit_duration_space_0: u32, bit_duration_space_1: u32, } impl IrSignal { fn decode(timings: [u32]) - anyhow::ResultSelf { // 分析信号时序识别协议格式 // 这里以NEC协议为例 if timings.len() 4 { return Err(anyhow::anyhow!(Signal too short)); } let header_mark timings[0]; let header_space timings[1]; // 解析数据位 let mut data_bits Vec::new(); for chunk in timings[2..].chunks(2) { if chunk.len() 2 { let mark_duration chunk[0]; let space_duration chunk[1]; // 根据空间持续时间判断是0还是1 if space_duration mark_duration * 2 { data_bits.push(true); } else { data_bits.push(false); } } } Ok(IrSignal { header_mark, header_space, data_bits, bit_duration_mark: 560, // 典型NEC协议值 bit_duration_space_0: 560, bit_duration_space_1: 1690, }) } fn encode(self) - Vecu32 { let mut signal Vec::new(); // 添加头部 signal.push(self.header_mark); signal.push(self.header_space); // 添加数据位 for bit in self.data_bits { signal.push(self.bit_duration_mark); if bit { signal.push(self.bit_duration_space_1); } else { signal.push(self.bit_duration_space_0); } } signal } }5. Wi-Fi网络通信实现5.1 网络连接配置实现Wi-Fi连接功能use esp_idf_svc::wifi::{BlockingWifi, EspWifi}; use esp_idf_svc::eventloop::EspSystemEventLoop; use embedded_svc::wifi::{AuthMethod, ClientConfiguration, Configuration}; struct WiFiManager { wifi: BlockingWifiEspWifistatic, } impl WiFiManager { fn new() - anyhow::ResultSelf { let peripherals Peripherals::take().unwrap(); let sys_loop EspSystemEventLoop::take()?; let wifi EspWifi::new(peripherals.modem, sys_loop.clone(), None)?; let wifi BlockingWifi::wrap(wifi, sys_loop)?; Ok(WiFiManager { wifi }) } fn connect(mut self, ssid: str, password: str) - anyhow::Result() { // 配置Wi-Fi客户端 let wifi_config Configuration::Client(ClientConfiguration { ssid: ssid.try_into().unwrap(), bssid: None, auth_method: AuthMethod::WPA2Personal, password: password.try_into().unwrap(), channel: None, }); self.wifi.set_configuration(wifi_config)?; // 启动Wi-Fi self.wifi.start()?; // 连接网络 self.wifi.connect()?; // 等待连接成功 self.wifi.wait_netif_up()?; log::info!(Wi-Fi connected successfully); Ok(()) } }5.2 HTTP服务器实现创建一个简单的HTTP服务器来处理控制请求use embedded_svc::http::Method; use embedded_svc::http::server::Registry; use embedded_svc::io::Write; use std::collections::HashMap; struct ControlServer { ir_emitter: IrEmitter, } impl ControlServer { fn handle_request(mut self, method: Method, path: str, body: str) - String { match (method, path) { (Method::Get, /status) { serde_json::json!({ status: online, device: AC Remote }).to_string() } (Method::Post, /control) { let params: HashMapString, String serde_json::from_str(body).unwrap(); self.handle_control_command(params) } _ { serde_json::json!({ error: Not found }).to_string() } } } fn handle_control_command(mut self, params: HashMapString, String) - String { if let (Some(mode), Some(temp)) (params.get(mode), params.get(temperature)) { match mode.as_str() { cool { self.ir_emitter.send_cool_command(temp.parse().unwrap()); serde_json::json!({status: cooling, temperature: temp}).to_string() } heat { self.ir_emitter.send_heat_command(temp.parse().unwrap()); serde_json::json!({status: heating, temperature: temp}).to_string() } _ serde_json::json!({error: Invalid mode}).to_string() } } else { serde_json::json!({error: Missing parameters}).to_string() } } }6. 完整的系统集成6.1 主程序逻辑将各个模块整合到主程序中use anyhow::Result; use log::info; fn main() - Result() { // 初始化日志系统 esp_idf_svc::log::EspLogger::initialize_default(); info!(Starting AC Wi-Fi Remote...); // 初始化Wi-Fi let mut wifi_manager WiFiManager::new()?; wifi_manager.connect(YourWiFiSSID, YourWiFiPassword)?; // 初始化红外发射器 let mut ir_emitter IrEmitter::new()?; // 初始化控制服务器 let mut control_server ControlServer::new(ir_emitter); // 启动HTTP服务器 start_http_server(control_server)?; info!(AC Wi-Fi Remote is ready!); // 主循环 loop { // 处理网络请求 handle_network_requests()?; // 其他后台任务 FreeRtos::delay_ms(100); } } fn start_http_server(control_server: ControlServer) - Result() { // 实现HTTP服务器启动逻辑 // 这里简化实现实际需要使用esp-idf-svc中的http服务器组件 info!(HTTP server started on port 80); Ok(()) }6.2 用户界面设计利用M5StickC的显示屏提供本地交互struct DisplayManager { display: DisplayDriver, } impl DisplayManager { fn show_status(mut self, status: str, temperature: i32) - Result() { self.display.clear(); // 显示状态信息 self.display.draw_text(10, 10, status, Color::White, Color::Black); // 显示温度 let temp_text format!({}°C, temperature); self.display.draw_text(10, 30, temp_text, Color::Red, Color::Black); // 显示连接状态 self.display.draw_text(10, 50, Wi-Fi: Connected, Color::Green, Color::Black); self.display.flush(); Ok(()) } fn show_error(mut self, error: str) - Result() { self.display.clear(); self.display.draw_text(10, 10, Error:, Color::Red, Color::Black); self.display.draw_text(10, 30, error, Color::White, Color::Black); self.display.flush(); Ok(()) } }7. 实际部署与测试7.1 硬件连接与配置M5StickC的引脚配置struct HardwarePins { ir_led: gpio::Gpio26, // 红外发射LED button_a: gpio::Gpio37, // 按钮A用于本地控制 button_b: gpio::Gpio39, // 按钮B用于模式切换 } impl HardwarePins { fn configure() - ResultSelf { let peripherals Peripherals::take().unwrap(); Ok(HardwarePins { ir_led: peripherals.pins.gpio26, button_a: peripherals.pins.gpio37, button_b: peripherals.pins.gpio39, }) } }7.2 功能测试流程完整的测试步骤编译和烧录cargo build --release espflash /dev/ttyUSB0 target/riscv32imc-unknown-none-elf/release/ac-wifi-remoteWi-Fi连接测试观察M5StickC显示屏是否显示Wi-Fi Connected检查路由器客户端列表确认设备在线红外信号测试用手机摄像头观察IR LED是否闪烁红外光在摄像头下可见测试空调是否响应控制命令网络控制测试# 测试HTTP API curl -X GET http://192.168.1.100/status curl -X POST http://192.168.1.100/control \ -H Content-Type: application/json \ -d {mode:cool,temperature:25}7.3 预期运行结果成功运行后你应该看到M5StickC显示屏显示连接状态和当前温度设置通过HTTP请求可以控制空调开关和模式切换红外信号正常发射空调响应控制命令8. 常见问题与解决方案问题现象可能原因排查方法解决方案编译失败提示找不到esp-idf环境变量未正确设置检查export-esp.sh是否已source手动设置环境变量source ~/export-esp.shWi-Fi连接失败密码错误或信号弱检查SSID和密码是否正确确保设备在路由器信号范围内红外控制无响应红外频率不匹配用手机摄像头检查IR LED是否工作调整红外载波频率通常38kHzHTTP请求超时IP地址错误或防火墙阻挡检查设备获取的IP地址在路由器中为设备设置静态IP设备频繁重启内存不足或代码错误查看串口日志输出优化内存使用检查数组越界8.1 红外信号调试技巧红外信号调试是项目中最容易出问题的环节// 添加调试功能通过串口输出信号时序 fn debug_ir_signal(timings: [u32]) { log::info!(IR Signal timings:); for (i, duration) in timings.iter().enumerate() { log::info!({}: {}us, i, duration); } } // 测试不同的载波频率 fn test_carrier_frequencies() { let frequencies [36000, 38000, 40000]; // 常见红外载波频率 for freq in frequencies { log::info!(Testing frequency: {}Hz, freq); // 发送测试信号并检查空调响应 } }8.2 网络稳定性优化对于需要长期运行的设备网络稳定性很重要impl WiFiManager { fn check_connection(mut self) - bool { // 定期检查网络连接状态 self.wifi.is_up().unwrap_or(false) } fn reconnect_if_needed(mut self) - Result() { if !self.check_connection() { log::warn!(Wi-Fi connection lost, attempting reconnect...); self.wifi.disconnect()?; FreeRtos::delay_ms(5000); // 等待5秒 self.connect(YourWiFiSSID, YourWiFiPassword)?; } Ok(()) } }9. 进阶功能与扩展思路9.1 温度自动化控制实现基于环境温度的智能控制struct TemperatureAutomation { target_temp: i32, hysteresis: i32, // 迟滞范围避免频繁开关 sensor: TemperatureSensor, } impl TemperatureAutomation { fn should_turn_on(self, current_temp: i32) - bool { current_temp self.target_temp self.hysteresis } fn should_turn_off(self, current_temp: i32) - bool { current_temp self.target_temp - self.hysteresis } fn auto_control(mut self) { let current_temp self.sensor.read_temperature(); if self.should_turn_on(current_temp) { self.send_cool_command(self.target_temp); } else if self.should_turn_off(current_temp) { self.send_power_off(); } } }9.2 多设备支持与场景模式扩展支持控制多个空调设备#[derive(Clone)] struct AirConditioner { name: String, ir_code: IrSignal, location: String, } struct MultiACController { ac_units: HashMapString, AirConditioner, current_scene: SceneMode, } impl MultiACController { fn create_scene(mut self, name: str, settings: HashMapString, ACSetting) { // 创建场景模式如回家模式、睡眠模式 } fn execute_scene(mut self, scene_name: str) { if let Some(scene) self.scenes.get(scene_name) { for (ac_name, setting) in scene { if let Some(ac) self.ac_units.get(ac_name) { self.send_command(ac, setting); } } } } }9.3 能耗统计与优化添加用电量统计功能struct EnergyMonitor { power_consumption: f32, // 当前功率 total_energy: f32, // 总耗电量 start_time: u64, // 开始时间 } impl EnergyMonitor { fn calculate_energy_saving(self, manual_mode: f32, auto_mode: f32) - f32 { // 计算自动化控制相比手动控制的节能效果 ((manual_mode - auto_mode) / manual_mode) * 100.0 } fn generate_report(self) - EnergyReport { EnergyReport { total_energy: self.total_energy, average_power: self.power_consumption, cost: self.total_energy * 0.6, // 假设电费0.6元/度 carbon_footprint: self.total_energy * 0.785, // 碳排放系数 } } }这个项目展示了如何用现代技术栈解决实际问题从硬件控制到网络通信从信号处理到用户交互涵盖了嵌入式开发的多个重要方面。最重要的是它证明了Rust在嵌入式领域的可行性为想要尝试嵌入式Rust开发的开发者提供了一个很好的起点。在实际部署时建议先在小范围内测试所有功能确保稳定性后再正式使用。对于生产环境还需要考虑电源管理、故障恢复、安全认证等更多工程化问题。