软硬件开源项目-红外遥控网关:IRext 红外码库+远程实现红外开关空调+红外学习
【下载地址】:红外遥控网关源码
本仓库提供了一个完整的红外遥控网关源码,旨在帮助开发者了解红外控制和红外学习的实现过程。项目包含了红外控制和红外学习的完整代码。
项目地址:https://gitee.com/daadadada/infrared 小程序源码地址:https://pan.baidu.com/s/17PZ9PxKzOpBtRuvA9ePOTw?pwd=4hgn
1.前言
当智能家居从概念走向现实,传统红外设备的“智能改造”却成了多数家庭的痛点:不同品牌空调的红外协议千差万别,遥控器堆成小山仍难实现统一控制。尽管市面上有很多智能控制 APP,但是这需要设备端支持网络,一些生产较早的空调等设备大多都不支持网络这一功能。如果有一个类似遥控器的产品能远程控制市面上大部分的空调品牌,并且支持红外学习实现控制,那么问题就迎刃而解了。由于 WiFi 信号容易受影响并且做通信功耗相对较高,因此我使用以太网做通讯。之前做项目用过 WIZnet 的芯片实现以太网功能,近期他们出了个带 MCU 的以太网芯片 W55MH32,找他们销售申请了一套开发板。计划是小程序做控制端,开发板做设备端,通过以太网进行通讯。红外学习到的红外码在设备端使用 w25q64 本地存储。
目前开发板用着可以,这款芯片集成了全硬件 TCP/IP 协议栈、MAC 及 PHY,开发板也已经集成了网络变压器和网口。Flash 和 SRAM 也足够大。一些不是很懂的网络协议也可以去他们[官网例程]去了解,里面也有一些讲如何配置 Keil 和烧录,资料很充足。
目前的功能已经实现了远程控制空调,但是红外学习机制是通过外界发射红外信号来触发,并在串口打印,红外学习的功能并没有完全实现。后续会通过小程序按键来触发红外学习,并且能下发命令实现控制。未被收录的红外码通过 w25q64 和 FatFs 文件系统,实现本地存储!另外,后面也会自己画原理图和 pcb 板,将所有的硬件集成到板子上,也会把代码、原理图以及 pcb 都开源出来。
2.红外
红外遥控码是一种基于 38000Hz 或者 56000Hz 载波频率的控制编码,接收方通过识别带有或不带有载波的时间间隔片进行编码识别。
在控制层通常使用高低物理电平对的序列来表示一个独立的控制信号,例如以下的 NEC 码:
它使用 9000us 载波+4500us 无载波时间序列表示引导码,并且使用 500us 载波+500us 无载波时间序列表示逻辑 0,使用 500us 载波+1500us 无载波时间序列表示逻辑 1,采用 2 字节地址码和 2 字节命令码构成,全码时间序列长度为 67.5ms。
IRext 红外库移植
IRext 是一个开源万能红外遥控码库、编解码压缩算法以及免费周边服务。它向智能家居开发者提供:
- 支持 16 类 1000 多品牌,上万个型号的家用电器遥控。
- 在线以及离线的万能红外码库,包括按照品牌分类的索引以及遥控编码。
- 灵活的服务部署方式,利用开源的服务端以及控制台代码在容器环境内 5 分钟快速搭建自己的码库服务。
- 码库和解码算法经过了极限压缩,能存储和运行在配置低至 51 MCU 的严苛硬件环境中。
- 丰富的平台适配支持和示例代码。
- 详尽的文档资源,覆盖万能遥控开发的每一个环节。
- 开发者可使用码库扩展工具自行扩展码库,可基于开源代码自由修改方案功能细节。
前往[IRext 官网]下载离线码库和 IRext 解码库,将 IRext 解码库放进单片机工程目录里并修改头文件目录,让其包含解码库中的头文件。根据 IRext 提供的品牌索引库从离线码库下载所需要的品牌红外码库。可存放到单片机 FLASH 或放到服务器上。IRext 解码库支持从文件系统解码或从内存解码。
从文件系统解码
ir_file_open(category, sub_category, "my_ir_code_file.bin");
ir_decode(key_code, user_data, ac_status, change_wind_dir);
ir_close();
加载到内存解码
ir_binary_open(category, sub_category, buffer, buffer_length);
ir_decode(key_code, user_data, &ac_status, change_wind_dir);
ir_close();
解码之后在 decoded_data 中获得可供输出的 IR 时间序列,将此序列递交给 IR 设备驱动即可实现红外发送。不过,目前 IRext 库只支持开关,风速,温度控制和扫风这些功能。
3.程序功能实现
小程序与设备通信功能
使用小程序可以做到远程开启空调而无需在设备旁边,并且小程序无需下载安装,通过微信等平台就能直接访问,因此我选择微信小程序来作为控制端。如图为小程序界面图:
小程序与设备通信的方式为,小程序调用 OneNET 平台提供的 HTTP 接口实现设备属性下发,云平台通过 mqtt 协议将数据发送给设备。小程序与云平台以及设备交互的数据格式如下,Command 用于下发控制或红外学习命令。
其中 AC_Status 是一个结构体,存储空调的状态,具体内容如下,其中的结构体成员分别是空调的品牌、型号、电源、模式、温度、风速和操作,其中操作是因为 IRext 库需要在解码的时候作为形参传入,所以必须定义,这个操作对应着小程序端按下的按键,比如,开关机,升温等。
在设备端我用到 OneNET 平台的通信主题有$sys/{pid}/{device-name}/thing/property/set
这个用于订阅,接收下发的控制命令;$sys/{pid}/{device-name}/thing/property/set_reply
用于应答下发的控制命令。一些其他的通信主题没有用到,感兴趣的小伙伴可以访问OneNET mqtt 通信主题了解。
小程序端使用的是 OneNET 平台提供的 http 接口,具体用到的接口为 https://iot-api.heclouds.com/thingmodel/set-device-property ,用于下发设备属性设置命令到设备,设备会返回设置结果。其他的 http 接口可以访问OneNET http 接口。
另外在发起 https 请求前,还需要在 Headers 中携带统一的安全鉴权信息 authorization 才能成功请求接口。具体方法需前往安全鉴权查看详细内容。
我们在 W55MH32 提供的开发套件下写代码,一些依赖的头文件和 pack 包都在里面。
将开发套件下的 do_mqtt.c 和 do_mqtt.h 文件复制到项目里,并修改 mqtt_params 里的参数为自己要连接的 mqtt 地址等参数。
do_mqtt()函数是一个基于状态机的 MQTT 客户端处理函数,负责管理 MQTT 连接、订阅、保活和消息接收等操作。当函数第一次运行时,会来到 conn,开始和 MQTT 服务器建立连接,之后再根据状态进行判断状态,进入到 SUB 订阅主题或者 KEEPALIVE 保活等。在状态为 ERR 时,代表着 mqtt 连接之间出现了错误,在 ERR 状态中,可以重新连接或者自定义一些处理。由于在 mqtt 的状态机中我不需要发布消息,所以我把发布消息这一节点删除了,代码如下:
void do_mqtt(void)
{
uint8_t ret;
switch (run_status)
{
case CONN:
{
ret = MQTTConnect(&c, &data); /* 连接到MQTT服务器 */
printf("Connect to the MQTT server: %d.%d.%d.%d:%drn", mqtt_params.server_ip[0], mqtt_params.server_ip[1], mqtt_params.server_ip[2], mqtt_params.server_ip[3], mqtt_params.port);
printf("Connected:%srnrn", ret == SUCCESSS ? "success" : "failed");
if (ret != SUCCESSS)
{
run_status = ERR;
}
else
{
run_status = SUB;
}
break;
}
case SUB:
{
ret = MQTTSubscribe(&c, mqtt_params.subtopic, mqtt_params.subQoS, message_Arrived); /* 订阅主题 */
printf("Subscribing to %srn", mqtt_params.subtopic);
printf("Subscribed:%srnrn", ret == SUCCESSS ? "success" : "failed");
if (ret != SUCCESSS)
{
run_status = ERR;
}
else
{
run_status = KEEPALIVE;
}
break;
}
case KEEPALIVE:
{
if (MQTTYield(&c, 30) != SUCCESSS) /* 保活 MQTT */
{
run_status = ERR;
}
delay_ms(100);
}
case RECV:
{
if (mqtt_recv_flag)
{
mqtt_recv_flag = 0;
json_decode(mqtt_recv_msg);
}
delay_ms(100);
break;
}
case ERR: /* 错误处理 */
printf("system ERROR!");
delay_ms(1000);
break;
default:
break;
}
}
messageArrived 函数是当我们订阅的主题发来消息时的回调函数,当有消息到达时,会触发此函数,并把消息内容作为参数传递给此函数。这里会打印一些参数,如主题、消息内容、QOS 等级等。代码如下:
void message_Arrived(MessageData *md)
{
char topicname[64] = {0};
char msg[512] = {0};
sprintf(topicname, "%.*s", (int)md- >topicName- >lenstring.len, md- >topicName- >lenstring.data);
sprintf(msg, "%.*s", (int)md- >message- >payloadlen, (char *)md- >message- >payload);
printf("recv data from %s", topicname);
if (strcmp(topicname, mqtt_params.subtopic) == 0)
{
mqtt_recv_flag = 1;
memset(mqtt_recv_msg, 0, sizeof(mqtt_recv_msg));
memcpy(mqtt_recv_msg, msg, strlen(msg));
}
}
json_decode 函数用于解析 JSON 数据,这里可以根据我们在云平台定义的数据来解析数据。解析完成后,会做一个回复,表示接收到数据,做应答。代码如下:
void json_decode(char *msg)
{
int ret;
char replymsg[128] = {0};
cJSON *jsondata = NULL;
cJSON *id = NULL;
cJSON *params = NULL;
cJSON *AC = NULL;
cJSON *data = NULL;
jsondata = cJSON_Parse(msg);
if (jsondata == NULL)
{
printf("json parse fail.rn");
return;
}
id = cJSON_GetObjectItem(jsondata, "id");
params = cJSON_GetObjectItem(jsondata, "params");
data = cJSON_GetObjectItem(params, "Command");
// 如果不是控制命令则返回
if (strcmp(data- >valuestring, "Control"))
{
return;
}
// 解析json并获取空调状态
AC = cJSON_GetObjectItem(params, "AC");
data = cJSON_GetObjectItem(AC, "ACBrand");
brand = (unsigned char)data- >valueint;
data = cJSON_GetObjectItem(AC, "ACType");
type = (unsigned char)data- >valueint;
data = cJSON_GetObjectItem(AC, "openFlag");
ac_status.ac_power = (t_ac_power)data- >valueint;
data = cJSON_GetObjectItem(AC, "modeGear");
ac_status.ac_mode = (t_ac_mode)data- >valueint;
data = cJSON_GetObjectItem(AC, "tempature");
ac_status.ac_temp = (t_ac_temperature)(data- >valueint - 16);
data = cJSON_GetObjectItem(AC, "fengsuGear");
ac_status.ac_wind_speed = (t_ac_wind_speed)data- >valueint;
data = cJSON_GetObjectItem(AC, "opertion");
operation = (unsigned char)data- >valueint;
// 回复
pubmessage.qos = QOS0;
sprintf(replymsg, "{"id":"%s","code":200,"msg":"success"}", id- >valuestring);
printf("reply:%srn", replymsg);
pubmessage.payload = replymsg;
pubmessage.payloadlen = strlen(replymsg);
ret = MQTTPublish(&c, mqtt_params.subtopic_reply, &pubmessage);
if (ret != SUCCESSS)
{
run_status = ERR;
}
else
{
printf("publish:%s,%srnrn", mqtt_params.subtopic_reply, (char *)pubmessage.payload);
}
cJSON_Delete(jsondata);
irSendFlag = 1;
}
红外发射功能
由于红外发射是一种基于 38000Hz 载波频率的控制编码,因此我们需要使用到 PWM 功能和 GPIO 口,创建 GPIOConfiguration 和 TIMConfiguration 函数,分别初始化 GPIO 口和 PWM。GPIO 口,即 PWM 输出口设为 PA3。定时器 2 采用的是 PCLK1 时钟,频率尽量最大,这样输出的红外误码率低。我选择频率为 216MHz,因此不需要分频。计数值设置为 5736,216MHz/5736≈38KHz。红外发射的占空比一般设为 1/3,因此将占空比,即 TIM_Pulse 设为 1912。PWM 代码如下:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Period = 5736 - 1; // 216Mhz/5736≈38Khz
TIM_TimeBaseStructure.TIM_Prescaler = 1 - 1; // 216Mhz/3=216Mhz
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 配置PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 1912; // 占空比计算
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC4Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
TIM_SetCompare4(TIM2, 0);
在 IRControl.c 文件中创建 getIrCode 函数,通过 mqtt 解析的 json 数据,即可知道要控制哪个品牌以及类型的空调,目前仅支持三个品牌。接着调用 IRext 提供的 API 解码,并将解码后的数据存放到先前定义的 user_data 变量里,代码如下:
unsigned char *number_point = NULL;
unsigned short *length_point = NULL;
switch (brand)
{
case 0:
{
number_point = Gree[type];
length_point = Gree_Length;
break;
}
case 1:
{
number_point = Midea[type];
length_point = Midea_Length;
break;
}
case 2:
{
number_point = Haier[type];
length_point = Haier_Length;
break;
}
default:
break;
}
unsigned char ret = ir_binary_open(1, 1, number_point, length_point[type]);
length = ir_decode(operation_list[operation], user_data, &ac_status, 1);
if (length == 0)
printf("Decode error");
ir_close();
在 IRControl.c 中创建 runIr 函数,用于驱动红外发射模块,根据解码后的时间序列,驱动 PWM 输出占空比为 33%或 0%的 PWM 波即可控制空调,代码如下。在最后输出一个 40ms 的占空比为 0%的无载波时间片,来终止红外。
for (unsigned short i = 0; i < length; i++)
{
if (!(i % 2))
TIM_SetCompare4(TIM2, 1912);
else
TIM_SetCompare4(TIM2, 0);
delay_us(user_data[i]);
}
TIM_SetCompare4(TIM2, 0);
delay_us(40000);
最后创建 ir_control 函数并调用 getIrCode 和 runIr 函数即可。
红外学习功能
在初始化阶段会配置 GPIO 口中断用于监听红外信号,定时器中断用于计时。由于定时器的重装值为 16 位,最大只能到 65535us,而红外信号会有重复码,很容易就超过了定时器的最大值,因此需要使用定时器溢出中断来转换成 32 位计数值。在每次触发 TIM4 更新中断时,就将溢出计数加 1,在需要获取时间时就可以将溢出计数左移 16 位并与上当前定时器 的计数值就是 32 位计数值。例如:
return (timer_overflow_count << 16) | TIM_GetCounter(TIM4);
红外接收有 2 个状态分别是 IR_IDLE(空闲)、IR_RECEIVING(接收完成)。当红外信号到来时,会进入到 EXTI0_IRQHandler 中断处理函数中,并将空闲状态改变为接收状态,之后开始接收数据,完整代码如下:
void EXTI0_IRQHandler(void)
{
uint32_t current_time = 0;
uint32_t time_interval = 0;
// 检查是否是该EXTI线产生的中断
if (EXTI_GetITStatus(IR_EXTI_LINE) != RESET)
{
// 获取当前时间
current_time = Get_32bit_Timer_Value();
// 计算时间间隔
time_interval = current_time - ir_last_time;
// 如果是第一个数据点或者是接收状态,则记录时间
if (ir_state == IR_IDLE)
{
// 第一个边沿,开始接收数据
ir_state = IR_RECEIVING;
ir_data_count = 0;
ir_data_ready = 0;
}
// 存储时间数据(如果缓冲区未满)
if (ir_data_count < IR_DATA_BUFFER_SIZE)
{
ir_time_data[ir_data_count] = time_interval;
ir_data_count++;
}
// 更新时间
ir_last_time = current_time;
// 清除中断标志位
EXTI_ClearITPendingBit(IR_EXTI_LINE);
}
}
我们还需要一个判断接收完毕的函数,函数内会声明两个全局静态变量,分别是用于记录上次进入函数的时间 last_activity_time 和上次记录的红外数据计数值 previous_count。首先,我们需要判断当前红外状态是否是接收状态,其次,我们需要判断红外数据计数值不为 0。当都满足时,开始计算时间间隔,如果当前的红外数据计数值和上次的不一样就重置 last_activity_time 并更新 previous_count,返回 0,否则判断当时间间隔超过 10ms 时就认为红外接收完成并返回 1,并重置一些参数。完整代码如下:
uint8_t IR_Is_Ready(void)
{
// 如果正在接收数据且有数据
if (ir_state == IR_RECEIVING && ir_data_count > 0)
{
// 使用全局静态变量来跟踪超时
static uint32_t last_activity_time = 0;
static uint16_t previous_count = 0;
uint32_t current_time = Get_32bit_Timer_Value();
// 如果数据计数发生变化,说明有新的数据到来
if (previous_count != ir_data_count)
{
previous_count = ir_data_count;
last_activity_time = current_time;
}
else
{
// 数据计数没有变化,检查是否超时
uint32_t time_since_last_activity = current_time - last_activity_time;
// 如果超过10ms没有新数据,认为接收完成
if (time_since_last_activity > 10000) // 10ms = 10000us
{
// 设置数据就绪标志
ir_data_ready = 1;
ir_state = IR_IDLE;
// 重置跟踪变量
previous_count = 0;
return 1;
}
}
return 0;
}
return 0;
}
在主循环里轮询 IR_Is_Ready,如果返回为 1,则说明数据就绪,开始红外学习,调用函数 ir_learn,ir_learn 函数用于重复获取红外信号,用于后续滤波。在函数内也会计时,当超过 30s 后,认为红外学习失败,若在 30s 内完成 3 次红外接收,则认为红外学习成功,并开始均值滤波。均值滤波即将数组相同下标的值相加并除以数组的个数。滤波后通过串口打印红外时间序列。完整代码如下:
/**
* @brief 对二维数组进行均值滤波处理
* @param dataCount: 每个一维数组中有效数据的个数
*/
void IR_Mean_Filter(uint8_t dataCount)
{
uint16_t i;
uint32_t sum;
uint16_t mean_value;
// 对每个数据位置进行均值滤波
for (i = 0; i < dataCount && i < IR_DATA_BUFFER_SIZE; i++)
{
sum = 0;
// 累加所有数组在同一位置的值
for (unsigned char j = 0; j < 3; j++)
{
sum += receive_data[j][i];
}
// 计算均值
mean_value = (uint16_t)(sum / 3);
// 存储到滤波后的数据数组中
filtering_data[i] = mean_value;
}
}
void ir_learn()
{
// 计数
unsigned char count = 0;
unsigned char successFlag = 0;
unsigned int dataCount = 0;
IR_Get_Time_Data(receive_data[count], IR_Get_Data_Count());
// 获取红外数据数量
dataCount = IR_Get_Data_Count();
IR_Clear_Data();
count++;
// 启动30s计时
RTC_30Sec_Start();
while (!RTC_30Sec_IsTimeout())
{
if (IR_Is_Ready())
{
IR_Get_Time_Data(receive_data[count], IR_Get_Data_Count());
IR_Clear_Data();
count++;
if (count == 3)
{
successFlag = 1;
break;
}
}
}
RTC_30Sec_Stop();
if (!successFlag)
{
// 超时,停止红外学习,并返回
printf("timeout return rn");
return;
}
// 均值滤波
IR_Mean_Filter(dataCount);
printf("IR learn success rn");
printf("length:%d,红外时间序列:rn", dataCount);
for (unsigned int i = 1; i < dataCount; i++)
{
printf("%drn", filtering_data[i]);
}
printf("rn");
}
主函数代码
主函数完整代码如下:
#include "bsp_rcc.h"
#include "bsp_tim.h"
#include "bsp_uart.h"
#include "delay.h"
#include "do_mqtt.h"
#include "socket.h"
#include "stdlib.h"
#include "wiz_interface.h"
#include "wizchip_conf.h"
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include "IR_Control.h"
#include "IR_Receive.h"
#include "RTC.h"
#include "ir_ac_control.h"
#define SOCKET_ID 0
#define ETHERNET_BUF_MAX_SIZE (1024 * 2)
/* 网络配置信息 */
wiz_NetInfo default_net_info = {
.mac = {0x00, 0x08, 0xdc, 0x12, 0x22, 0x05},
.ip = {192, 168, 2, 40},
.gw = {192, 168, 2, 1},
.sn = {255, 255, 255, 0},
.dns = {8, 8, 8, 8},
.dhcp = NETINFO_DHCP};
uint8_t ethernet_buf[ETHERNET_BUF_MAX_SIZE] = {0};
static uint8_t mqtt_send_ethernet_buf[ETHERNET_BUF_MAX_SIZE] = {0};
static uint8_t mqtt_recv_ethernet_buf[ETHERNET_BUF_MAX_SIZE] = {0};
// 第三方库IRext定义的结构体,用于存储空调状态
t_remote_ac_status ac_status;
// 品牌
unsigned char brand = 0;
// 型号
unsigned char type = 0;
// 操作类型
unsigned char operation = 0;
int main(void)
{
// 时钟初始化,使能外部高速晶振时钟,主频倍频至为216MHz,PCLK1、PCLK2都设为216MHz
rcc_clk_config();
delay_init();
console_usart_init(115200);
tim3_init();
printf("Infrared Remote Control Gatewayrn");
wiz_toe_init();
wiz_phy_link_check();
network_init(ethernet_buf, &default_net_info);
// 红外初始化
ir_Config();
IR_Receive_Init();
// rtc 时钟初始化
RTC_Init();
mqtt_init(SOCKET_ID, mqtt_send_ethernet_buf, mqtt_recv_ethernet_buf);
while (1)
{
do_mqtt();
// 检查是否收到红外发送命令
if (IR_is_send())
{
printf("start IR controlrn");
// 红外控制
ir_Control(brand, type, operation, &ac_status);
Reset_IR_Send();
// 清除红外接收到的数据,避免触发红外学习
IR_Clear_Data();
}
// 检查是否收到红外学习命令
if (IR_is_learn())
{
printf("start IR learnrn");
ir_Learn(getFileName());
Reset_IR_learn();
}
}
}
4.功能验证
经过验证,目前的功能能够稳定运行。
5.结语
基于 W55MH32Q-EVB 构建的红外遥控?关,以其独特的技术优势为智能家居领域的红外设备控制提供了全新解决?案。W55MH32Q 芯?的?性能内核与全硬件?络协议栈,确保了数据处理与通信的?效稳定,让微信?程序与?关的交互响应迅速。
IRext 红外库的灵活移植,突破了品牌壁垒,实现了多品牌空调的统?控制;JSON 数据解析则让指令传递更精准?效。从硬件设计到软件实现,整个系统?缝衔接,既解决了传统遥控器混乱的痛点,?降低了智能家居改造的门槛。
未来,随着功能的进?步拓展,该?关有望兼容更多类型的红外设备,为?户带来更便捷、智能的?活体验,也为红外设备的智能化升级提供了可借鉴的技术范式。
感谢?家的耐?阅读,想了解红外学习、irext 红外库,FatFs 文件系统的请关注我。后续会更新文章。另外,如果你在阅读过程中有任何疑问,欢迎随时通过私信留?。我会尽快回复消息。
审核编辑 黄宇
-
单片机
+关注
关注
6069文章
45075浏览量
653833 -
网关
+关注
关注
9文章
5876浏览量
53446 -
智能家居
+关注
关注
1934文章
9837浏览量
191199 -
红外控制
+关注
关注
0文章
26浏览量
11791
发布评论请先 登录
智能红外遥控开关控制器
云智能红外转WIFI网关
基于红外学习的离线语音控制低成本方案
用ESP8266实现的红外学习遥控器介绍
基于NiosⅡ的红外学习型遥控器设计
基于51系列单片机的红外遥控设计

ESP8266红外学习遥控器

评论