基于小熊派hi3863开发鸿蒙手机控制蜂鸣器
BearPi-HM Nano 2 实战:用 HarmonyOS 手机控制人体检测蜂鸣器
最近用 BearPi-HM Nano 2 和 E53_IS1 人体红外扩展板做了一个小实验:开发板检测到有人靠近时自动响蜂鸣器,同时可以在 HarmonyOS 手机上查看状态、手动鸣响和停止鸣响。
一开始功能看起来很简单,真正联调时却遇到了几个很典型的问题:HTTP 服务只能访问一次、手机换网后仍显示在线、蜂鸣器第一次能响但停止后第二次再也响不起来。本文把最终可用的实现思路和排查过程记录下来,希望能给做 WS63、OpenHarmony 和 ArkTS 联调的同学一些参考。
最终实现效果
这套方案完全运行在局域网内,不依赖公网服务器。
开发板连接 2.4 GHz Wi-Fi,通过 DHCP 获取 IP,然后监听 8888 端口。HarmonyOS 手机和开发板处于同一局域网时,可以完成下面这些操作:
- 查看开发板是否在线
- 查看人体检测状态
- 查看蜂鸣器是否正在鸣响
- 手动开启蜂鸣器
- 停止当前鸣响
- 开启或关闭人体自动报警
- 连续执行多次“鸣响、停止、再次鸣响”
手机与开发板不在同一局域网,或者当前 IP 无法访问时,应用会及时显示连接失败,不会继续使用上一次请求留下的在线状态。
演示视频可以按照下面的顺序录制:先展示串口 IP,再打开手机应用连接设备,连续执行两次鸣响和停止,最后切换手机网络,展示应用从在线变为离线。
整体通信流程
开发板内部主要运行两个任务。
AlarmTask 负责 E53_IS1 人体红外检测。GPIO10 检测到上升沿后只发送一个事件,真正的蜂鸣器控制放在线程中执行,避免在中断回调里执行耗时操作。
NetworkTask 负责连接 Wi-Fi、获取 IP,并启动 HTTP Server。手机发送控制指令后,开发板更新业务状态、控制 GPIO11/PWM3,然后返回 JSON。
当前使用的接口比较简单:
- GET /api/status:查询完整状态
- GET /api/manual/on:手动鸣响
- GET /api/silence:停止当前鸣响
- GET /api/alarm/enable:开启人体自动报警
- GET /api/alarm/disable:关闭人体自动报警
状态接口返回的内容如下:
{
"ok": true,
"alarmEnabled": true,
"manualOn": false,
"motionDetected": false,
"buzzerOn": false
}
这里需要区分“停止鸣响”和“关闭自动报警”。停止鸣响只结束当前声音,不能断开 Wi-Fi,也不能关闭 HTTP Server;关闭自动报警才表示以后检测到人体时不再自动响。
开发板端的关键实现
Wi-Fi 账号和密码在本地配置,实际项目中不要把真实密码提交到公开仓库。
#define WIFI_SSID "你的2.4G Wi-Fi名称"
#define WIFI_PASSWORD "你的Wi-Fi密码"
#define HTTP_SERVER_PORT 8888
Wi-Fi 连接成功后要输出 DHCP 分配的 IP:
printf("[WIFI] network ready\r\n");
WifiPrintIpAddress();
开发板复位或路由器重新分配租约后,IP 可能变化,所以手机端要以串口最新输出为准。为了减少每次修改 IP 的麻烦,可以在路由器后台根据开发板 MAC 地址设置 DHCP 地址保留。
HTTP Server 创建成功后,监听 socket 要一直保留。每次手机请求只关闭本次连接产生的 clientFd:
while (1) {
clientFd = lwip_accept(
serverFd,
(struct sockaddr *)&clientAddress,
&clientAddressLength
);
if (clientFd < 0) {
osDelay(10);
continue;
}
HandleHttpClient(clientFd);
lwip_close(clientFd);
}
这里使用 lwip_socket、lwip_accept、lwip_recv、lwip_send 和 lwip_close,避免混用不同层的 socket 接口。
收到控制请求后的路由处理如下:
if (strstr(request, "GET /api/manual/on ") != NULL) {
ManualBuzzerOn();
SendStatusResponse(clientFd);
return;
}
if (strstr(request, "GET /api/silence ") != NULL) {
SilenceBuzzer();
SendStatusResponse(clientFd);
return;
}
手动鸣响必须真正调用一次硬件控制,不能只修改软件变量:
static void ManualBuzzerOn(void)
{
osMutexAcquire(g_stateMutex, osWaitForever);
g_manualOn = true;
g_silenced = false;
SetBuzzerOutputLocked(true);
osMutexRelease(g_stateMutex);
}
停止鸣响则同步清除手动状态和蜂鸣器状态:
static void SilenceBuzzer(void)
{
osMutexAcquire(g_stateMutex, osWaitForever);
g_manualOn = false;
g_silenced = true;
SetBuzzerOutputLocked(false);
osMutexRelease(g_stateMutex);
}
蜂鸣器只能响一次的问题
这是整个联调过程中最值得记录的坑。
最初代码使用 IoTPwmStart 开启 PWM,使用 IoTPwmStop 停止 PWM。第一次完全正常,但停止以后再次调用 IoTPwmStart,串口返回:
[PWM] start result=4294967295
4294967295 是无符号形式的 -1,表示底层启动失败。HTTP 接口仍然可以返回成功,所以手机会显示“指令执行成功”,但蜂鸣器并没有真正发声。
最终没有继续反复 Stop 和 Start,而是让 PWM 在运行周期内只启动一次。需要静音时,把 GPIO11 从 PWM3 切换成普通 GPIO,并输出低电平;需要再次鸣响时,再把 GPIO11 切回 PWM3。
静音的关键代码:
static void MuteBuzzerByGpio(void)
{
IoTGpioSetFunc(
E53_IS1_BEEP_GPIO,
IOT_GPIO_FUNC_GPIO_11_GPIO
);
IoTGpioSetDir(
E53_IS1_BEEP_GPIO,
IOT_GPIO_DIR_OUT
);
IoTGpioSetOutputVal(
E53_IS1_BEEP_GPIO,
IOT_GPIO_VALUE0
);
}
再次鸣响时只恢复引脚复用:
IoTGpioSetFunc(
E53_IS1_BEEP_GPIO,
IOT_GPIO_FUNC_GPIO_11_PWM3_OUT
);
if (!g_pwmStarted) {
result = IoTPwmStart(
PWM_PORT_PWM3,
PWM_DUTY,
PWM_FREQ
);
if (result == IOT_SUCCESS) {
g_pwmStarted = true;
}
}
这样第一次鸣响会真正启动 PWM,后续停止只断开 PWM 与蜂鸣器引脚的输出关系,再次鸣响只恢复 GPIO11 的 PWM3 复用,因此不会再次触发底层 PWM 启动失败。
这种做法会让 PWM 外设在后台保持运行,不过静音期间 GPIO11 输出低电平,蜂鸣器不会发声。对于当前局域网控制和功能验证场景,这个方案比较稳定。
HarmonyOS 手机端实现
手机端使用 ArkTS 和 Network Kit,module.json5 中需要申请网络权限:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
每次请求都创建独立的 HttpRequest,请求结束后再销毁:
private async requestDevice(path: string): Promise<DeviceStatus> {
const request: http.HttpRequest = http.createHttp();
try {
const response: http.HttpResponse = await request.request(
`${this.getBaseUrl()}${path}`,
{
method: http.RequestMethod.GET,
expectDataType: http.HttpDataType.STRING,
usingCache: false,
connectTimeout: 1500,
readTimeout: 1500
}
);
if (response.responseCode !== 200) {
throw new Error(`HTTP ${response.responseCode}`);
}
return JSON.parse(response.result as string) as DeviceStatus;
} finally {
request.destroy();
}
}
页面每两秒查询一次 /api/status。轮询失败时立即清除旧状态,不能继续显示上一次的“设备连接正常”:
private markDisconnected(
statusText: string,
errorText: string
): void {
this.connected = false;
this.alarmEnabled = false;
this.manualOn = false;
this.motionDetected = false;
this.buzzerOn = false;
this.statusText = statusText;
this.errorText = errorText;
this.lastUpdateTime = '--';
}
private async refreshStatus(): Promise<void> {
if (this.requestBusy || this.commandWaiting) {
return;
}
this.requestBusy = true;
try {
const status: DeviceStatus =
await this.requestDevice('/api/status');
this.applyDeviceStatus(status);
} catch (_) {
this.markDisconnected(
'开发板连接失败',
'请确认手机和开发板处于同一局域网'
);
} finally {
this.requestBusy = false;
}
}
状态轮询和控制命令还需要串行执行。开发板的 socket 资源有限,如果手机同时发出大量状态请求和控制请求,容易造成连接异常或资源耗尽。
实际遇到的几个坑
第一,HTTP 请求结束后只关闭 clientFd,不要正常处理一次请求就关闭 serverFd。否则服务端只能短暂访问,后面会出现端口占用或监听失败。
第二,不要同时混用 shutdown、close 和 lwip_close。当前方案统一使用 lwIP 接口,每个客户端连接只执行一次 lwip_close。
第三,这套组合中使用 select 曾经触发 LOS_EventRead failed,后面改成阻塞式 lwip_accept,服务才稳定下来。
第四,errno 98 通常表示端口仍被旧监听占用;errno 105 通常表示 lwIP socket 或 netconn 资源被耗尽。遇到这两个错误,首先检查是否反复创建 server socket,以及 clientFd 是否真的关闭。
第五,手机显示“指令执行成功”只能证明 HTTP 请求收到了成功响应,不能证明硬件一定执行成功。调试阶段一定要同时打印 PWM 返回值。
第六,手机换到另一个网络后仍显示在线,一般不是开发板还能跨网访问,而是手机界面保留了旧状态。在线状态必须由当前一次 /api/status 请求决定。
第七,Ninja 最后显示的 Code 4000 或 Code 4003 只是汇总信息,真正的错误要看 error.log 中第一条 error。快速检查命令如下:
grep -nE "FAILED:|error:|undefined reference|multiple definition|not used" \
out/bearpi_hm_nano_2/bearpi_hm_nano_2/error.log | head -50
第八,完整替换 C 文件时不要把新代码追加在旧代码后面。否则很容易出现 redefinition 和 defined but not used。
第九,修改代码后如果串口看不到新增日志,要检查新字符串是否进入最终固件。源码改对了但烧录旧包,也是嵌入式开发中很常见的问题。
编译、烧录与验证
修改完成后执行完整构建:
cd /home/bearpi/project/bearpi-hm_nano_2
rm -rf out/bearpi_hm_nano_2
hb build -f
完整烧录使用当前构建生成的 ws63-liteos-app_all.fwpkg。烧录结束后再按一次 Reset,让新固件运行。
电脑端可以先使用 curl 验证,确认开发板稳定后再调试手机:
curl --max-time 3 http://开发板IP:8888/api/status
curl --max-time 3 http://开发板IP:8888/api/manual/on
curl --max-time 3 http://开发板IP:8888/api/silence
curl --max-time 3 http://开发板IP:8888/api/manual/on
curl --max-time 3 http://开发板IP:8888/api/silence
验收时至少连续执行两轮鸣响和停止。如果第一次正常、第二次失败,要重点观察串口中是否出现 4294967295,以及第二次是否真正进入蜂鸣器硬件控制函数。
演示视频怎么处理
CSDN 的创作中心支持上传视频,也支持在文章编辑器中插入账号下已经上传并审核通过的视频。录屏本身不是在 CSDN 编辑器里完成,先使用手机或电脑自带的屏幕录制功能生成视频,再上传到 CSDN 即可。
微信普通小程序可以播放视频,但没有面向普通小程序开放“录制整个手机屏幕”的通用接口。小游戏有单独的 GameRecorderManager,不能直接当作普通小程序录屏方案。普通小程序演示建议使用手机系统录屏,再把视频上传到对象存储、云点播或其他 HTTPS 地址,通过 video 组件播放。
为了兼顾 CSDN、微信小程序、Android、iOS 和 HarmonyOS,建议统一导出为 MP4,视频编码使用 H.264,音频编码使用 AAC。分辨率使用 1080p 或 720p,帧率 30 fps 即可。不要直接使用 macOS 默认生成的 MOV 作为最终发布文件,先转成 MP4 会更稳妥。
演示视频控制在两到四分钟比较合适,重点展示实际效果和关键串口日志,不需要把全部编译过程都录进去。录制前注意隐藏 Wi-Fi 密码、个人 IP、公网服务器密码、设备序列号和其他敏感信息。
最后
这个小项目真正麻烦的地方并不在页面按钮,而是在嵌入式状态、PWM 生命周期、Socket 资源和手机在线状态之间保持一致。
局域网版本跑通后,下一步可以增加 DHCP 地址保留或 mDNS 自动发现,解决每次输入 IP 的问题。如果需要跨网络控制,再考虑 MQTT 或 HTTPS 服务,不建议直接把开发板的 8888 端口暴露到公网。
相关资料:
- BearPi-HM Nano 2 官方仓库:https://gitee.com/bearpi/bearpi-hm_nano_2
- E53_IS1 官方示例:https://gitee.com/bearpi/bearpi-hm_nano_2/tree/master/device/board/bearpi/bearpi_hm_nano_2/app/C5_e53_is1_infrared
- HarmonyOS HTTP API:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-http
更多推荐



所有评论(0)