【开发者成长激励计划-基于TencentOS Tiny的非侵入式86型智能开关】
1 产品介绍
非侵入式86型智能开关装置是针对遵循86型安装盒(JB/T 8593-2013)的开关面板简易接入物联网的一款辅助装置. 它能在用户不改动原有正常的86型开关面板的情况下, 实现对开关面板接入到物联网, 并通过物联网实现用户对不同开关面板的适配, 实现对远程开关, 定时开关和光感开关等功能. 免打孔, 免接线的特点以求让用户以最低的适应成本使用物联网技术.
设备演示视频
2 产品亮点
- 支持腾讯连连小程序扫描二维码添加设备.
- 腾讯连连小程序中实现自定义H5页面: 设备上报的按键状态会在UI界面上直观地显现出来.
- 通过TOS Tiny快速接入腾讯云IoT Explorer平台.
- 设备完成开关操作后, 按键触手会自动回到起始位置, 不妨碍原开关面板的正常使用.
- 基于事件驱动的设计, 设备支持在移动过程中插入执行新的按键操作.
- 未来将通过小程序配置设备以适应不同按键个数的开关面板和触键幅度.
- 未来将接入homeassistant和各种智能音箱以实现联动.
3 产品硬件架构
3.1 组成架构
3.2 沁恒CH32V307_EVB简介
CH32V_EVB是腾讯物联网操作系统TencentOS tiny 团队联合沁恒微电子设计的一款物联网评估板, 其采用沁恒RISC-V MCU CH32V307VCT6芯片. 而该芯片是采用基于沁恒自研RISC-V架构微处理器青稞V4架构. 在该开发板上可充分学习评估TencentOS tiny 基础内核、RISC-V IP核架构和IoT组件功能.
ESP8266
作为TOS Tiny系列开发板的一员, CH32V_EVB自然少不了板载一个ESP8266 WIFI模块. 通过开发板上的简单跳线的即可进行烧录和测试. 将腾讯云物联网平台提供的ESP8266固件烧录进ESP8266模块, 在加上TOS Tiny系统加持下, 可以实现在几乎在无代码添加的情况下让MCU快速上云.
3.3 THB6128步进电机驱动模块
THB6128是一款双全桥MOSFET, 低导通电阻的电机驱动IC. 该模块为电机提供独立电源供应, 并接受主控芯片输出的转速脉冲信号, 方向信号和使能信号以驱动电机运转, 且模块对主控的信号均采取光耦隔离的保护措施.
该模块采用信号:共阳极接线法, 即CP+/DIR+/EN+接到开发板+3.3V端. 当CP-/DIR-/EN-针脚为低电平时, 方为输出有效信号.
模块电流设定拨码开关(1/2/3/4): ON/ON/ON/ON(0.7A)
模块细分设定拨码开关(1/2/3): ON/OFF/OFF(64细分)
3.4 霍尔传感器3144以及LM393比较器
霍尔传感模块是基于霍尔元件3144设计而成, 元件是一种磁传感器, 用于计算丝杆转动的圈数. 由于丝杆运动与该传感器没有机械干涉, 它相比于限位开关寿命更长且更稳定. 使用宽电压LM393比较器, 通过对霍尔元件信号与电位器比较, 直接输出稳定的高低电平信号到主控芯片. 需要注意该传感器只对某一极磁场敏感.
4 产品方案设计
4.1 产品组成
主要抽象出两个概念:
- 触手(tentacle)
- 滑台(slider)
4.2 端侧架构
主要流程
核心代码
主要基于事件驱动回调的方式来实现:
// g_sw_status: uint16_t. 下发的最新的设备状态.
// g_sw_status_pst: uint16_t. 已操作的缓存的设备状态
// cur_key_pos: 0对应按键位置为起始位置; 1对应开关面板左①号键位...
// 当触手处在 cur_key_pos 号按键位置上时, 进入该回调
void on_over_key_pos(int cur_key_pos)
{
printf("当前处于%d号按键上:", cur_key_pos);
int tmp = 1 << (cur_key_pos - 1);
if (((g_sw_status ^ g_sw_status_pst) & tmp) != 0)
{
if ((g_sw_status & tmp) != 0 && (g_sw_status_pst & tmp) == 0)
{
// 按键从关闭-->打开
printf("打开按键\\n");
tentable_push(PUSH_METHOD_ON);
g_sw_status_pst = g_sw_status_pst | (g_sw_status & tmp);
}
else //if (g_sw_status & tmp == 0 && g_sw_status_pst & tmp != 0)
{
// 按键从打开-->关闭
printf("关闭按键\\n");
tentable_push(PUSH_METHOD_OFF);
g_sw_status_pst = g_sw_status_pst & ~(g_sw_status_pst & tmp);
}
tos_task_delay(1000);
}
}
监控任务:
void monitor_task_entry(void* arg)
{
... // 一些值的初始化
while (1)
{
switch (get_key()) // 获取测试按键
{...}
...
LCD_ShowString(...); // 通过LCD输出测试信息
// 当按键开关量发生变化
while(g_sw_status != g_sw_status_pst)
{
// 阻塞方法. 滑台移动到下一个键位, 并返回到达键位值.
// 若滑台处在末尾键位, 则改变滑台的运动方向
ret = slider_moveNextStep();
if (ret < 0)
{
printf("moveNextStep=>%d\\n", ret);
break;
}
// 触发到达键位时间
on_over_key_pos(ret);
}
// 空出CPU给其它任务执行
tos_task_delay(100);
}
通信代码:
由于挤出的时间比较仓促, 通信部分代码是直接在智能电灯的demo上改出来的:
连带物模型也懒得改(
就借用了brightness属性进行开关状态量的更新和同步
void default_message_handler(mqtt_message_t* msg) {
... // 一些初始化操作
len = strlen(msg->payload);
if (len <= sizeof (msg->payload) && msg->payload[len-1] == '"')
msg->payload[len-1] = '\\0';
cjson_root = cJSON_Parse((char*)(msg->payload +1));
if (cjson_root == NULL) {
...
goto exit;
}
jmethod = cJSON_GetObjectItem(cjson_root, "method");
method = cJSON_GetStringValue(jmethod);
jparams = cJSON_GetObjectItem(cjson_root, "params");
jbrightness = cJSON_GetObjectItem(jparams, "brightness");
brightness = jbrightness->valueint;
if (jmethod == NULL || method == NULL ||
jparams == NULL || jbrightness == NULL)
{
printf("report reply status parser fail:%X\\t%X\\t%X\\t%X\\t%X\\t\\r\\n",
cjson_root, jmethod, method, jparams, jbrightness);
event_flag = report_fail;
goto exit;
}
if (strstr(method, "control") == NULL)
{
printf("method:%s\\r\\n", method);
event_flag = report_fail;
goto exit;
}
g_sw_status = brightness & 0x07;
printf("g_sw_status:%d\\n", g_sw_status);
exit:
// FIXME: cJSON_Delete只需要对根句柄操作, 否则会崩
cJSON_Delete(cjson_root);
.. // 一些归0操作
return;
}
上报开关状态量部分代码:
#define EVENT_DATA_TEMPLATE "{\\\\\\"method\\\\\\":\\\\\\"report\\\\\\"\\\\,\\\\\\"clientToken\\\\\\":\\\\\\"00000001\\\\\\"\\\\,\\\\\\"params\\\\\\":{\\\\\\"brightness\\\\\\":%d}}"
void mqtt_task_entry(void) {
...
while (1) {
tos_sleep_ms(5000);
/* use AT+PUB AT command */
memset(payload, 0, sizeof(payload));
// 就改了这句↓
snprintf(payload, sizeof(payload), EVENT_DATA_TEMPLATE, g_sw_status_pst & 0x0F);
printf("pub payload:%s\\n", payload);
if (tos_tf_module_mqtt_pub(event_topic_name, QOS0, payload) != 0)
{
printf("module mqtt pub fail\\n");
break;
}
else
{
printf("module mqtt pub success\\n");
}
}
}
4.3 用户交互
采用腾讯连连小程序实现自定义H5页面.
只要大致理解腾讯连连H5面板原理: HTML和NodeJS是相对独立的关系, 就不难制作出想要的H5页面.
核心代码
前端代码是基于腾讯物联网开发平台提供示例项目魔改而成:https://github.com/tencentyun/iotexplorer-h5-panel-demo
首先是魔改了大按钮为3个大按钮:
// 渲染顶部大按钮
const renderHeadPanel = () => {
...
// 将id=="brightness"和类型为int的属性值渲染成3个大按钮
switch (type) {
case "int":
case "float":
console.log('int/float');
if (id == "brightness")
{
return (
<HeadBoolPanel
templateConfig={headTemplateConfig}
onChange={(value) => doControlDeviceProperty(id, value)}
value={value as number}
disabled={disabled}
/>
);
}
}
}
大按钮由大块的按钮和icon组成, 其中icon的样式反映设备已操作的开关量状态.
// 单个按钮的核心代码
<div id="power-switch-body_1"
className="power-switch-body off"
onClick={() => {
if (disabled)
return;
if ((value & 0x01) != 0)
{
value &= ~0x01;
}
else
{
value |= 0x01;
}
onChange(value);
}}
>
<div id="power-switch-icon_1" className="power-switch-icon off" />
</div>
按钮按下的处理函数与icon更新:
// 设备属性控制
const doControlDeviceProperty = async (propertyId: string, value: unknown) => {
// renderHeadPanel();
// 控制报文
const payload = {
[propertyId]: value,
};
if (propertyId == 'brightness')
{
let val_num = value as number;
let tmp = 0x01;
for (let index = 0; index < 3; index++)
{
let id_str = "#power-switch-body_" + (index + 1);
const body = document.querySelector(id_str);
if ((val_num & tmp) != 0)
{
body?.setAttribute("class", "power-switch-body");
}
else
{
body?.setAttribute("class", "power-switch-body off");
}
tmp <<= 1;
}
}
try {
const res = await sdk.controlDeviceData(payload);
console.log("[controlDeviceData]", payload, "成功", res);
} catch (err) {
console.log("[controlDeviceData]", payload, "失败", err);
}
};
通过sdk监听wsEventReport事件, 获取设备上报的已操作的开关量状态信息:
sdk.on('wsEventReport', (data) => {
console.log('wsEventReport', data);
let isHasBrightness = false;
let brightness = 0;
const {Payload, deviceId} = data;
if (deviceId == sdk.deviceId)
{
console.log(`发现设备:${deviceId}`);
const {params} = Payload;
if ('params' in Payload)
{
if ('brightness' in params)
{
brightness = params.brightness;
isHasBrightness = true;
}
}
if (isHasBrightness)
{
let val_num = brightness as number;
// val_num = Math.round(Math.random() * 100) % 0x08;
console.log(`val_num:${val_num}`);
let tmp = 0x01;
for (let index = 0; index < 3; index++)
{
let id_str = "#power-switch-icon_" + (index + 1);
const body = document.querySelector(id_str);
if ((val_num & tmp) != 0)
{
body?.setAttribute("class", "power-switch-icon");
}
else
{
body?.setAttribute("class", "power-switch-icon off");
}
tmp <<= 1;
}
}
}
});
编写完所有面板代码和样式后, 可以通过"命令控制台"cd到项目目录, 运行
npm run release
即可在iotexplorer-h5-panel-demo\\dist\\release下找到:
index.*.js
index.*.css
即可上传到IoT平台:
4.4 使用微信开发工具开发腾讯连连自定义H5页面
参考:https://cloud.tencent.com/document/product/1081/67441
安装好NodeJS和whistleJS代理程序并加入环境变量PATH后, 在"命令提示符"中运行:
w2 start
配置好证书后, cd到项目目录:
npm run dev
....
# 当看到:
「wdm」: Compiled successfully.
# 即成功运行项目, 不要关闭控制台
每当修改项目代码保存时, 后台会自动编译新的程序. 通过"微信开发者工具"输入正确地址并登陆, 即可预览H5页面:
# 请替换括号内容
https://iot.cloud.tencent.com/h5panel/developing?deviceName=(deviceName)&productId=(productId)
4.5 使用真机腾讯连连进行调试
根据4.3的介绍, 将生成的H5页面上传后, 即可:
使用腾讯连连小程序扫描, 即可在真机调试H5页面和设备.
5 使用TOS Tiny和腾讯云物联网开发平台全栈开发感受
无论从端侧开发还是前端交互开发, 都有充分的技术支持. 技术栈的设计均是相应领域的所擅长的, 取各家之所长, 所以非常利于嵌入式工程师进行全栈开发. 从主控到ESP8266 的硬件联动, 再到腾讯物联网开发平台和腾讯连连的软件联动, 环环相扣, 代码利用率相当高.
端侧固件代码
https://gitee.com/YJHmath/NI-86-box-ISW
腾讯连连自定义H5页面代码
https://gitee.com/YJHmath/non-invasive86-box-intelligent-switch-h5-pannel