mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 17:08:01 +00:00
feat: 单插件重载等 PluginMgr 能力导出到外部 SDK
- meta: CORE_PLUGIN_RELOAD_ONE(48) / LIST_LOADED(49) / IS_DISABLED(50) - sdk: PluginMgrAPI 接口(ReloadOne/ListLoadedPlugins/IsPluginDisabled) + PluginSDK.SetPluginMgrAPI/PluginMgr() 访问器 - plugindev 模板: dispatchPluginMgr 桥接注入,走 C ABI 48/49/50
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
# Build artifacts
|
||||
*.so
|
||||
*.dll
|
||||
*.o
|
||||
*.exe
|
||||
*.hmap
|
||||
plugin.json
|
||||
|
||||
|
||||
318
README.md
318
README.md
@ -308,6 +308,324 @@ enabled := sdk.AutoRestart()
|
||||
| [rss](example/rss) | Go | RSS 订阅 |
|
||||
| [sanitizer](example/sanitizer) | Go | 内容清洗/安全过滤 |
|
||||
|
||||
## Remote Device SDK
|
||||
|
||||
用于开发**远程设备接入适配器**的 C 语言 SDK,零外部依赖,兼容嵌入式平台。
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ha_remotedevice (C SDK) │
|
||||
│ 协议引擎 │ WS 帧 │ JSON │ 状态机 │ 传输抽象 │
|
||||
└──────────┬──────────────────────────────────────┘
|
||||
│ 同一份 C 代码,设备端和 App 端共用
|
||||
┌──────┴──────────────────┐
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────────────────┐
|
||||
│ ESP32 裸机 │ │ Linux 设备上的 App │
|
||||
│ 纯 C 直调 │ │ (Python ctypes / Go CGo / │
|
||||
│ 简单命令处理 │ │ Node addon / C# P/Invoke) │
|
||||
└──────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
### 声明式 API 设计
|
||||
|
||||
设备在代码中声明**自己是什么**、**能做什么**、**支持哪些命令**,每个命令对应独立处理函数,SDK 自动分发并回执结果:
|
||||
|
||||
```c
|
||||
#include "ha_remotedevice.h"
|
||||
|
||||
/* 声明能力 */
|
||||
const char *caps[] = {"camera", "status", NULL};
|
||||
|
||||
/* 声明式命令处理表:每个命令绑定独立处理函数 */
|
||||
static ha_status_t handle_camerasue(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
int duration = args[0] ? atoi(args) : 0;
|
||||
// 拍照/录像...
|
||||
result->status = 0;
|
||||
result->output = "data:image/jpeg;base64,..."; // SDK 自动回执
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
ha_cmd_handler_def_t handlers[] = {
|
||||
{.command = "shell", .handler = handle_shell},
|
||||
{.command = "camerasue", .handler = handle_camerasue},
|
||||
{.command = "screensee", .handler = handle_screensee},
|
||||
{.command = "speakeruse", .handler = handle_speakeruse},
|
||||
{.command = NULL}, /* 标记结束 */
|
||||
};
|
||||
|
||||
ha_config_t config = {
|
||||
.transport = my_transport, // 用户实现 4 个函数
|
||||
.server = "192.168.1.100:9890",
|
||||
.token = "my-token",
|
||||
.device = {
|
||||
.device_id = "esp32-cam-1",
|
||||
.name = "门口摄像头",
|
||||
.kind = "camera",
|
||||
.caps = caps,
|
||||
},
|
||||
.handlers = handlers, // 声明式命令处理表
|
||||
.on_state = my_state_handler,
|
||||
};
|
||||
|
||||
ha_client_t *client = ha_client_new(&config);
|
||||
ha_client_start(client);
|
||||
while (1) {
|
||||
ha_client_process(client); // 主循环处理
|
||||
}
|
||||
```
|
||||
|
||||
### 传输层抽象
|
||||
|
||||
用户只需实现 4 个函数,适配不同平台:
|
||||
|
||||
```c
|
||||
ha_transport_t my_transport = {
|
||||
.connect = my_tcp_connect, // 建立 TCP 连接
|
||||
.send = my_tcp_send, // 发送数据
|
||||
.recv = my_tcp_recv, // 接收数据(阻塞)
|
||||
.close = my_tcp_close, // 关闭连接
|
||||
.ctx = &my_platform_ctx,
|
||||
};
|
||||
```
|
||||
|
||||
### 支持的协议
|
||||
|
||||
| 功能 | API |
|
||||
|------|-----|
|
||||
| WS 连接 + 握手 | `ha_client_start` 自动完成 |
|
||||
| 设备注册 (hello/bind) | 启动时自动发送 |
|
||||
| 命令接收 (shell/homeagent) | `handlers` 表声明式注册,SDK 自动分发 |
|
||||
| 命令回执 | `ha_client_send_result` |
|
||||
| 二进制分块(录像等) | `ha_client_send_data_chunked` |
|
||||
| TTS 音频接收 | `on_binary` 回调 |
|
||||
| 事件上报 | `ha_client_send_event` |
|
||||
| 状态上报 | `ha_client_send_status` |
|
||||
| 心跳保持 | 自动 ping/pong |
|
||||
|
||||
### 使用方式
|
||||
|
||||
通过 `plugindev` 工具链初始化项目:
|
||||
|
||||
```bash
|
||||
plugindev init my-adapter --type remotedevice
|
||||
```
|
||||
|
||||
生成 `main.c` + `CMakeLists.txt`,可直接编译或作为三方库引入:
|
||||
|
||||
```cmake
|
||||
add_subdirectory(path/to/ha_remotedevice)
|
||||
target_link_libraries(my_app ha_remotedevice)
|
||||
target_include_directories(my_app PRIVATE ${HA_REMOTEDEVICE_INCLUDE_DIR})
|
||||
```
|
||||
|
||||
### 快速接入指南
|
||||
|
||||
以下是从零到设备成功接入 HomeAgent 的完整步骤。
|
||||
|
||||
#### 1. 准备工作
|
||||
|
||||
在 HomeAgent 平台上创建接入令牌:
|
||||
|
||||
```bash
|
||||
# 在 HomeAgent 服务端创建一个设备接入令牌
|
||||
curl -X POST http://<homeagent-server>:8080/api/v1/device/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_id":"esp32-cam-1","name":"门口摄像头","kind":"camera"}'
|
||||
# 返回: {"token":"ha-dev-token-xxxxx"}
|
||||
```
|
||||
|
||||
记录下返回的 `token`,设备端配置时使用。
|
||||
|
||||
#### 2. 实现传输层(4 个函数)
|
||||
|
||||
根据你的平台实现 `ha_transport_t` 的 4 个函数指针。以下是几种常见场景:
|
||||
|
||||
**场景 A:带 TCP/IP 栈的嵌入式设备(如 ESP32 + lwIP)**
|
||||
|
||||
```c
|
||||
#include "ha_remotedevice.h"
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
static int esp_connect(void *ctx, const char *host, uint16_t port) {
|
||||
struct sockaddr_in addr;
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) return -1;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
inet_pton(AF_INET, host, &addr.sin_addr);
|
||||
int ret = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (ret < 0) { closesocket(sock); return -1; }
|
||||
*(int *)ctx = sock;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int esp_send(void *ctx, const uint8_t *data, int len) {
|
||||
int sock = *(int *)ctx;
|
||||
return send(sock, (const char *)data, len, 0);
|
||||
}
|
||||
|
||||
static int esp_recv(void *ctx, uint8_t *buf, int len) {
|
||||
int sock = *(int *)ctx;
|
||||
return recv(sock, (char *)buf, len, 0);
|
||||
}
|
||||
|
||||
static void esp_close(void *ctx) {
|
||||
int sock = *(int *)ctx;
|
||||
closesocket(sock);
|
||||
}
|
||||
|
||||
int esp_ctx = -1;
|
||||
ha_transport_t transport = {
|
||||
.connect = esp_connect,
|
||||
.send = esp_send,
|
||||
.recv = esp_recv,
|
||||
.close = esp_close,
|
||||
.ctx = &esp_ctx,
|
||||
};
|
||||
```
|
||||
|
||||
**场景 B:通过串口(UART)连接透传模块**
|
||||
|
||||
```c
|
||||
static int uart_connect(void *ctx, const char *host, uint16_t port) {
|
||||
(void)host; (void)port;
|
||||
// 初始化 UART,波特率 115200
|
||||
return uart_init((uart_ctx_t *)ctx, 115200);
|
||||
}
|
||||
|
||||
static int uart_send(void *ctx, const uint8_t *data, int len) {
|
||||
return uart_write((uart_ctx_t *)ctx, data, len);
|
||||
}
|
||||
|
||||
static int uart_recv(void *ctx, uint8_t *buf, int len) {
|
||||
return uart_read((uart_ctx_t *)ctx, buf, len);
|
||||
}
|
||||
|
||||
static void uart_close(void *ctx) {
|
||||
uart_deinit((uart_ctx_t *)ctx);
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:UART 透传时,另一端需运行一个 TCP 桥接程序,将串口数据转发到 HomeAgent 的 WebSocket 端口。
|
||||
|
||||
#### 3. 声明设备能力和命令处理
|
||||
|
||||
```c
|
||||
#include "ha_remotedevice.h"
|
||||
|
||||
/* 声明设备能力 */
|
||||
const char *caps[] = {"camera", "speaker", "status", NULL};
|
||||
|
||||
/* 处理 camerasue 命令(拍照) */
|
||||
static ha_status_t handle_camera(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
int duration = args[0] ? atoi(args) : 0; // 参数:录像时长
|
||||
|
||||
// 拍照或录像,将结果填入 result
|
||||
result->status = 0;
|
||||
result->output = "data:image/jpeg;base64,/9j/4AAQ..."; // base64 图像数据
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 处理 shell 命令 */
|
||||
static ha_status_t handle_shell(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
// 执行 shell 命令,args 为完整命令字符串
|
||||
result->status = 0;
|
||||
result->output = "command executed";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 声明式命令处理表 */
|
||||
ha_cmd_handler_def_t handlers[] = {
|
||||
{.command = "shell", .handler = handle_shell},
|
||||
{.command = "camerasue", .handler = handle_camera},
|
||||
{.command = "screensee", .handler = handle_camera},
|
||||
{.command = "speakeruse", .handler = handle_speaker},
|
||||
{.command = NULL}, /* 标记结束 */
|
||||
};
|
||||
```
|
||||
|
||||
#### 4. 配置并启动客户端
|
||||
|
||||
```c
|
||||
ha_config_t config = {
|
||||
.transport = transport, // 传输层实现
|
||||
.server = "192.168.1.100:9890", // HomeAgent 服务端地址
|
||||
.token = "ha-dev-token-xxxxx", // 第 1 步获取的令牌
|
||||
.device = {
|
||||
.device_id = "esp32-cam-1",
|
||||
.name = "门口摄像头",
|
||||
.kind = "camera",
|
||||
.caps = caps,
|
||||
.info_json = "{\"chip\":\"ESP32-S3\",\"firmware\":\"v1.0\"}",
|
||||
},
|
||||
.handlers = handlers, // 命令处理表
|
||||
.on_binary = on_binary_data, // 接收 TTS 音频等二进制数据
|
||||
.on_state = on_state_change, // 连接状态变化回调
|
||||
.ping_interval = 30, // 心跳间隔秒数
|
||||
};
|
||||
|
||||
ha_client_t *client = ha_client_new(&config);
|
||||
ha_status_t ret = ha_client_start(client);
|
||||
if (ret != HA_OK) {
|
||||
printf("设备接入失败: %d\n", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 主循环 */
|
||||
while (1) {
|
||||
ha_client_process(client); // 处理协议帧、心跳、命令分发
|
||||
|
||||
/* 可选:设备主动上报事件 */
|
||||
ha_client_send_event(client, "motion_detected",
|
||||
"{\"zone\":\"front_door\",\"confidence\":0.95}");
|
||||
|
||||
/* 可选:上报设备状态 */
|
||||
ha_client_send_status(client, "online");
|
||||
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS); // 嵌入式 RTOS 风格延时
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 验证连接
|
||||
|
||||
在 HomeAgent 服务端检查设备是否在线:
|
||||
|
||||
```bash
|
||||
# 查看已注册设备列表
|
||||
curl http://<homeagent-server>:8080/api/v1/device/list
|
||||
# 预期输出包含: {"device_id":"esp32-cam-1","status":"online",...}
|
||||
|
||||
# 向设备发送命令(测试 camerasue)
|
||||
curl -X POST http://<homeagent-server>:8080/api/v1/device/esp32-cam-1/cmd \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cmd":"camerasue","args":"3"}'
|
||||
# 预期返回: {"status":"ok","result":"data:image/jpeg;base64,..."}
|
||||
```
|
||||
|
||||
#### 6. 调试技巧
|
||||
|
||||
| 问题 | 检查点 |
|
||||
|------|--------|
|
||||
| 连接失败 | 确认 `server` 地址和端口可通;检查 `token` 是否正确 |
|
||||
| WS 握手失败 | 确认 HomeAgent 服务端已开启 WebSocket 支持 |
|
||||
| 命令无响应 | 确认 `handlers` 表中注册了对应命令名;检查 `on_binary` 是否配置 |
|
||||
| 断线重连 | `max_reconnect` 控制重连次数,-1 为无限重连 |
|
||||
| 内存不足(嵌入式) | 定义 `HA_NO_ALLOC` 宏禁用动态内存分配 |
|
||||
|
||||
### 位置
|
||||
|
||||
- **SDK 源码**: `remotedevice/`
|
||||
- **plugindev 模板**: `plugindev init --type remotedevice`
|
||||
|
||||
## 构建与安装
|
||||
|
||||
### 构建
|
||||
|
||||
299
README_EN.md
299
README_EN.md
@ -265,6 +265,305 @@ Internal plugins (platform built-in) have full SDK access including SocialAPI wr
|
||||
| [rss](example/rss) | Go | RSS subscriptions |
|
||||
| [sanitizer](example/sanitizer) | Go | Content sanitization / safety filtering |
|
||||
|
||||
## Remote Device SDK
|
||||
|
||||
A C language SDK for developing **remote device access adapters** with zero external dependencies, compatible with embedded platforms.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ha_remotedevice (C SDK) │
|
||||
│ Protocol Engine │ WS Frames │ JSON │ State │
|
||||
│ Machine │ Transport Abstraction │
|
||||
└──────────┬──────────────────────────────────────┘
|
||||
│ Same C code, shared by device & app
|
||||
┌──────┴──────────────────┐
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────────────────┐
|
||||
│ ESP32 Bare │ │ Linux App │
|
||||
│ Pure C │ │ (Python ctypes / Go CGo /│
|
||||
│ Simple Cmd │ │ Node addon / C# P/Invoke)│
|
||||
└──────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
### Declarative API Design
|
||||
|
||||
The device declares **what it is** and **what it can do** in code. The SDK handles all protocol details automatically:
|
||||
|
||||
```c
|
||||
#include "ha_remotedevice.h"
|
||||
|
||||
/* Declare capabilities */
|
||||
const char *caps[] = {"camera", "status", NULL};
|
||||
|
||||
ha_config_t config = {
|
||||
.transport = my_transport, // User implements 4 functions
|
||||
.server = "192.168.1.100:9890",
|
||||
.token = "my-token",
|
||||
.device = {
|
||||
.device_id = "esp32-cam-1",
|
||||
.name = "Front Door Camera",
|
||||
.kind = "camera",
|
||||
.caps = caps,
|
||||
},
|
||||
.on_cmd = my_cmd_handler, // Called when receiving commands
|
||||
.on_binary = my_data_handler, // Called on binary data (TTS audio, etc.)
|
||||
.on_state = my_state_handler, // Connection state changes
|
||||
};
|
||||
|
||||
ha_client_t *client = ha_client_new(&config);
|
||||
ha_client_start(client);
|
||||
while (1) {
|
||||
ha_client_process(client); // Main loop processing
|
||||
}
|
||||
```
|
||||
|
||||
### Transport Layer Abstraction
|
||||
|
||||
Users only need to implement 4 functions to adapt to different platforms:
|
||||
|
||||
```c
|
||||
ha_transport_t my_transport = {
|
||||
.connect = my_tcp_connect, // Establish TCP connection
|
||||
.send = my_tcp_send, // Send data
|
||||
.recv = my_tcp_recv, // Receive data (blocking)
|
||||
.close = my_tcp_close, // Close connection
|
||||
.ctx = &my_platform_ctx,
|
||||
};
|
||||
```
|
||||
|
||||
### Protocol Support
|
||||
|
||||
| Feature | API |
|
||||
|---------|-----|
|
||||
| WS connection + handshake | Automatic via `ha_client_start` |
|
||||
| Device registration (hello/bind) | Automatic on startup |
|
||||
| Command receive (shell/homeagent) | `on_cmd` callback |
|
||||
| Command result | `ha_client_send_result` |
|
||||
| Binary chunked transfer (video) | `ha_client_send_data_chunked` |
|
||||
| TTS audio receive | `on_binary` callback |
|
||||
| Event reporting | `ha_client_send_event` |
|
||||
| Status reporting | `ha_client_send_status` |
|
||||
| Heartbeat keepalive | Automatic ping/pong |
|
||||
|
||||
### Usage
|
||||
|
||||
Initialize a project via the `plugindev` toolchain:
|
||||
|
||||
```bash
|
||||
plugindev init my-adapter --type remotedevice
|
||||
```
|
||||
|
||||
Generates `main.c` + `CMakeLists.txt`, can be built directly or used as a third-party library:
|
||||
|
||||
```cmake
|
||||
add_subdirectory(path/to/ha_remotedevice)
|
||||
target_link_libraries(my_app ha_remotedevice)
|
||||
target_include_directories(my_app PRIVATE ${HA_REMOTEDEVICE_INCLUDE_DIR})
|
||||
```
|
||||
|
||||
### Quick Start Guide
|
||||
|
||||
A complete step-by-step guide from zero to a device successfully connected to HomeAgent.
|
||||
|
||||
#### Step 1: Preparation
|
||||
|
||||
Create an access token on the HomeAgent platform:
|
||||
|
||||
```bash
|
||||
# Create a device access token on the HomeAgent server
|
||||
curl -X POST http://<homeagent-server>:8080/api/v1/device/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_id":"esp32-cam-1","name":"Front Door Camera","kind":"camera"}'
|
||||
# Returns: {"token":"ha-dev-token-xxxxx"}
|
||||
```
|
||||
|
||||
Save the returned `token` — you'll need it in the device configuration.
|
||||
|
||||
#### Step 2: Implement the Transport Layer (4 functions)
|
||||
|
||||
Implement the 4 function pointers of `ha_transport_t` for your platform. Here are common scenarios:
|
||||
|
||||
**Scenario A: Embedded device with TCP/IP stack (e.g., ESP32 + lwIP)**
|
||||
|
||||
```c
|
||||
#include "ha_remotedevice.h"
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
static int esp_connect(void *ctx, const char *host, uint16_t port) {
|
||||
struct sockaddr_in addr;
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) return -1;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
inet_pton(AF_INET, host, &addr.sin_addr);
|
||||
int ret = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (ret < 0) { closesocket(sock); return -1; }
|
||||
*(int *)ctx = sock;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int esp_send(void *ctx, const uint8_t *data, int len) {
|
||||
int sock = *(int *)ctx;
|
||||
return send(sock, (const char *)data, len, 0);
|
||||
}
|
||||
|
||||
static int esp_recv(void *ctx, uint8_t *buf, int len) {
|
||||
int sock = *(int *)ctx;
|
||||
return recv(sock, (char *)buf, len, 0);
|
||||
}
|
||||
|
||||
static void esp_close(void *ctx) {
|
||||
int sock = *(int *)ctx;
|
||||
closesocket(sock);
|
||||
}
|
||||
|
||||
int esp_ctx = -1;
|
||||
ha_transport_t transport = {
|
||||
.connect = esp_connect,
|
||||
.send = esp_send,
|
||||
.recv = esp_recv,
|
||||
.close = esp_close,
|
||||
.ctx = &esp_ctx,
|
||||
};
|
||||
```
|
||||
|
||||
**Scenario B: Serial (UART) passthrough module**
|
||||
|
||||
```c
|
||||
static int uart_connect(void *ctx, const char *host, uint16_t port) {
|
||||
(void)host; (void)port;
|
||||
return uart_init((uart_ctx_t *)ctx, 115200);
|
||||
}
|
||||
|
||||
static int uart_send(void *ctx, const uint8_t *data, int len) {
|
||||
return uart_write((uart_ctx_t *)ctx, data, len);
|
||||
}
|
||||
|
||||
static int uart_recv(void *ctx, uint8_t *buf, int len) {
|
||||
return uart_read((uart_ctx_t *)ctx, buf, len);
|
||||
}
|
||||
|
||||
static void uart_close(void *ctx) {
|
||||
uart_deinit((uart_ctx_t *)ctx);
|
||||
}
|
||||
```
|
||||
|
||||
> Note: For UART passthrough, a TCP bridge program must run on the other end to forward serial data to the HomeAgent WebSocket port.
|
||||
|
||||
#### Step 3: Declare Device Capabilities and Command Handlers
|
||||
|
||||
```c
|
||||
#include "ha_remotedevice.h"
|
||||
|
||||
/* Declare device capabilities */
|
||||
const char *caps[] = {"camera", "speaker", "status", NULL};
|
||||
|
||||
/* Handle camerasue command (take photo) */
|
||||
static ha_status_t handle_camera(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
int duration = args[0] ? atoi(args) : 0;
|
||||
|
||||
// Capture image, fill the result
|
||||
result->status = 0;
|
||||
result->output = "data:image/jpeg;base64,/9j/4AAQ..."; // base64 image data
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* Handle shell command */
|
||||
static ha_status_t handle_shell(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
result->status = 0;
|
||||
result->output = "command executed";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* Declarative command handler table */
|
||||
ha_cmd_handler_def_t handlers[] = {
|
||||
{.command = "shell", .handler = handle_shell},
|
||||
{.command = "camerasue", .handler = handle_camera},
|
||||
{.command = "screensee", .handler = handle_camera},
|
||||
{.command = "speakeruse", .handler = handle_speaker},
|
||||
{.command = NULL}, /* terminator */
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 4: Configure and Start the Client
|
||||
|
||||
```c
|
||||
ha_config_t config = {
|
||||
.transport = transport, // Transport layer implementation
|
||||
.server = "192.168.1.100:9890", // HomeAgent server address
|
||||
.token = "ha-dev-token-xxxxx", // Token from Step 1
|
||||
.device = {
|
||||
.device_id = "esp32-cam-1",
|
||||
.name = "Front Door Camera",
|
||||
.kind = "camera",
|
||||
.caps = caps,
|
||||
.info_json = "{\"chip\":\"ESP32-S3\",\"firmware\":\"v1.0\"}",
|
||||
},
|
||||
.handlers = handlers, // Command handler table
|
||||
.on_binary = on_binary_data, // Receive TTS audio etc.
|
||||
.on_state = on_state_change, // Connection state callback
|
||||
.ping_interval = 30,
|
||||
};
|
||||
|
||||
ha_client_t *client = ha_client_new(&config);
|
||||
ha_status_t ret = ha_client_start(client);
|
||||
if (ret != HA_OK) {
|
||||
printf("Device connection failed: %d\n", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Main loop */
|
||||
while (1) {
|
||||
ha_client_process(client); // Process protocol frames, heartbeats, commands
|
||||
|
||||
/* Optional: device-initiated event reporting */
|
||||
ha_client_send_event(client, "motion_detected",
|
||||
"{\"zone\":\"front_door\",\"confidence\":0.95}");
|
||||
|
||||
/* Optional: report device status */
|
||||
ha_client_send_status(client, "online");
|
||||
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS); // RTOS-style delay
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 5: Verify the Connection
|
||||
|
||||
Check if the device is online on the HomeAgent server:
|
||||
|
||||
```bash
|
||||
# List registered devices
|
||||
curl http://<homeagent-server>:8080/api/v1/device/list
|
||||
# Expected output includes: {"device_id":"esp32-cam-1","status":"online",...}
|
||||
|
||||
# Send a command to the device (test camerasue)
|
||||
curl -X POST http://<homeagent-server>:8080/api/v1/device/esp32-cam-1/cmd \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cmd":"camerasue","args":"3"}'
|
||||
# Expected: {"status":"ok","result":"data:image/jpeg;base64,..."}
|
||||
```
|
||||
|
||||
#### Step 6: Debugging Tips
|
||||
|
||||
| Issue | Check |
|
||||
|-------|-------|
|
||||
| Connection failed | Verify `server` address and port are reachable; check `token` |
|
||||
| WS handshake failed | Verify HomeAgent server WebSocket support is enabled |
|
||||
| Command not responding | Confirm the command name is registered in `handlers` table; check `on_binary` |
|
||||
| Reconnection issues | `max_reconnect` controls retry count; -1 = infinite |
|
||||
| Low memory (embedded) | Define `HA_NO_ALLOC` to disable dynamic memory allocation |
|
||||
|
||||
### Location
|
||||
|
||||
- **SDK Source**: `remotedevice/`
|
||||
- **plugindev template**: `plugindev init --type remotedevice`
|
||||
|
||||
## Building & Installing
|
||||
|
||||
### Build
|
||||
|
||||
@ -99,4 +99,9 @@ const (
|
||||
CoreSettingsDefs = 43
|
||||
CoreSettingsDump = 44
|
||||
CoreSettingsPlugins = 45
|
||||
CoreRegisterInputCh = 46
|
||||
CoreInjectInputSync = 47
|
||||
CorePluginReloadOne = 48
|
||||
CorePluginListLoaded = 49
|
||||
CorePluginIsDisabled = 50
|
||||
)
|
||||
|
||||
116
remotedevice/CMakeLists.txt
Normal file
116
remotedevice/CMakeLists.txt
Normal file
@ -0,0 +1,116 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(ha_remotedevice VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
# ============================================================
|
||||
# ha_remotedevice — HomeAgent 远程设备接入 C SDK
|
||||
# 零外部依赖,纯 C 实现,兼容嵌入式平台。
|
||||
#
|
||||
# 使用方式:
|
||||
# add_subdirectory(path/to/ha_remotedevice)
|
||||
# target_link_libraries(my_app ha_remotedevice)
|
||||
# target_include_directories(my_app PRIVATE
|
||||
# ${HA_REMOTEDEVICE_INCLUDE_DIR})
|
||||
# ============================================================
|
||||
|
||||
# 选项: 构建为静态库或动态库
|
||||
option(BUILD_SHARED_LIBS "Build ha_remotedevice as shared library" OFF)
|
||||
|
||||
# 选项: 禁用 malloc/free(用于裸机环境,用户需提供 alloc 回调)
|
||||
option(HA_NO_ALLOC "Disable dynamic memory allocation" OFF)
|
||||
|
||||
# 选项: 日志级别
|
||||
set(HA_LOG_LEVEL 2 CACHE STRING "Log level: 0=none, 1=error, 2=info, 3=debug")
|
||||
|
||||
# 源文件
|
||||
set(HA_REMOTEDEVICE_SRC
|
||||
src/ha_remotedevice.c
|
||||
src/ha_json.c
|
||||
src/ha_ws.c
|
||||
)
|
||||
|
||||
# 头文件
|
||||
set(HA_REMOTEDEVICE_INCLUDE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
# 编译选项
|
||||
if(HA_NO_ALLOC)
|
||||
add_definitions(-DHA_NO_ALLOC)
|
||||
endif()
|
||||
add_definitions(-DHA_LOG_LEVEL=${HA_LOG_LEVEL})
|
||||
|
||||
# 创建库
|
||||
if(BUILD_SHARED_LIBS)
|
||||
add_library(ha_remotedevice SHARED ${HA_REMOTEDEVICE_SRC})
|
||||
if(WIN32)
|
||||
# Windows 需要导出符号
|
||||
set_target_properties(ha_remotedevice PROPERTIES
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
endif()
|
||||
else()
|
||||
add_library(ha_remotedevice STATIC ${HA_REMOTEDEVICE_SRC})
|
||||
endif()
|
||||
|
||||
# 包含目录
|
||||
target_include_directories(ha_remotedevice
|
||||
PUBLIC ${HA_REMOTEDEVICE_INCLUDE}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
# 不链接任何外部库
|
||||
target_link_libraries(ha_remotedevice PRIVATE)
|
||||
|
||||
# 导出包含目录供外部项目使用
|
||||
set(HA_REMOTEDEVICE_INCLUDE_DIR
|
||||
${HA_REMOTEDEVICE_INCLUDE}
|
||||
CACHE INTERNAL "ha_remotedevice include directories")
|
||||
|
||||
# 安装规则
|
||||
install(TARGETS ha_remotedevice
|
||||
EXPORT ha_remotedevice-targets
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
INCLUDES DESTINATION include
|
||||
)
|
||||
|
||||
install(DIRECTORY include/
|
||||
DESTINATION include
|
||||
)
|
||||
|
||||
install(EXPORT ha_remotedevice-targets
|
||||
DESTINATION lib/cmake/ha_remotedevice
|
||||
NAMESPACE ha_remotedevice::
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 测试(可选)
|
||||
# ============================================================
|
||||
option(BUILD_TESTS "Build ha_remotedevice tests" OFF)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_executable(ha_remotedevice_test
|
||||
test/test_ha_remotedevice.c
|
||||
)
|
||||
target_link_libraries(ha_remotedevice_test
|
||||
PRIVATE ha_remotedevice Threads::Threads
|
||||
)
|
||||
target_include_directories(ha_remotedevice_test
|
||||
PRIVATE ${HA_REMOTEDEVICE_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
# 添加测试
|
||||
add_test(NAME ha_remotedevice_test
|
||||
COMMAND ha_remotedevice_test
|
||||
)
|
||||
endif()
|
||||
|
||||
# ============================================================
|
||||
# 编译信息
|
||||
# ============================================================
|
||||
message(STATUS "ha_remotedevice ${PROJECT_VERSION}")
|
||||
message(STATUS " Build type: $<CONFIG>")
|
||||
message(STATUS " Shared lib: ${BUILD_SHARED_LIBS}")
|
||||
message(STATUS " No alloc: ${HA_NO_ALLOC}")
|
||||
216
remotedevice/include/ha_remotedevice.h
Normal file
216
remotedevice/include/ha_remotedevice.h
Normal file
@ -0,0 +1,216 @@
|
||||
#ifndef HA_REMOTEDEVICE_H
|
||||
#define HA_REMOTEDEVICE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================================================================
|
||||
* ha_remotedevice — 远程设备接入 C SDK
|
||||
*
|
||||
* 零外部依赖,纯 C 实现,兼容嵌入式平台。
|
||||
* 传输层由用户实现(4 个函数指针),SDK 处理所有协议细节。
|
||||
*
|
||||
* 声明式设计:
|
||||
* 设备在代码中声明自己是什么(kind)和能做什么(caps),
|
||||
* 声明支持哪些命令(shell/camerasue/screensee/...)并注册对应处理函数,
|
||||
* SDK 自动处理协议握手、心跳、消息路由、结果回执。
|
||||
*
|
||||
* 协议流程:
|
||||
* TCP 连接 → WS 升级 → hello(设备声明) → bind(令牌) → 就绪
|
||||
* 就绪后循环:读帧 → 按 handlers 表分发命令 → 自动回执结果
|
||||
* ================================================================== */
|
||||
|
||||
/* ======================== 状态码 ======================== */
|
||||
typedef enum {
|
||||
HA_OK = 0,
|
||||
HA_ERR_GENERIC = -1,
|
||||
HA_ERR_NOMEM = -2,
|
||||
HA_ERR_INVALID = -3,
|
||||
HA_ERR_TIMEOUT = -4,
|
||||
HA_ERR_DISCONNECTED = -5,
|
||||
HA_ERR_PROTOCOL = -6,
|
||||
HA_ERR_TRANSPORT = -7,
|
||||
HA_ERR_NOT_FOUND = -8,
|
||||
} ha_status_t;
|
||||
|
||||
/* ======================== 传输层抽象 ========================
|
||||
*
|
||||
* 用户必须实现这 4 个函数,适配不同平台(FreeRTOS+lwIP、Zephyr、裸机等)。
|
||||
*
|
||||
* connect(ctx, host, port) → 建立 TCP 连接,返回 0 成功
|
||||
* send(ctx, data, len) → 发送 len 字节,返回实际发送字节数,-1 失败
|
||||
* recv(ctx, buf, len) → 接收最多 len 字节,返回实际接收字节数,0 断开,-1 失败
|
||||
* close(ctx) → 关闭连接
|
||||
*/
|
||||
typedef struct {
|
||||
int (*connect)(void *ctx, const char *host, uint16_t port);
|
||||
int (*send)(void *ctx, const uint8_t *data, int len);
|
||||
int (*recv)(void *ctx, uint8_t *buf, int len);
|
||||
void (*close)(void *ctx);
|
||||
void *ctx;
|
||||
} ha_transport_t;
|
||||
|
||||
/* ======================== 设备声明 ========================
|
||||
*
|
||||
* 声明式配置:设备在代码中声明自己的类型和能力。
|
||||
* 这些信息通过 hello 消息发送给网关。
|
||||
*
|
||||
* device_id — 唯一标识,如 "esp32-cam-1"
|
||||
* name — 设备显示名,如 "门口摄像头"
|
||||
* kind — 设备种类,如 "camera"、"computer"、"speaker"、"light"
|
||||
* caps — 能力数组,以 NULL 结尾,如 {"camera","status",NULL}
|
||||
* info_json — 额外信息(JSON 字符串),可选,如 '{"chip":"ESP32-S3","psram":8}'
|
||||
*/
|
||||
typedef struct {
|
||||
const char *device_id;
|
||||
const char *name;
|
||||
const char *kind;
|
||||
const char **caps; /* NULL 结尾 */
|
||||
const char *info_json; /* 可选,NULL 或 JSON 字符串 */
|
||||
} ha_device_info_t;
|
||||
|
||||
/* ======================== 命令结果 ========================
|
||||
*
|
||||
* 命令处理函数通过填写此结构体返回数据。
|
||||
* SDK 收到结果后自动发送回执(文本或二进制分块)。
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 简单文本:设置 status=0, output="结果文本"
|
||||
* 2. 二进制数据:设置 has_binary=1, binary_data/binary_len/mime
|
||||
* 3. 错误:设置 status=1, error="错误信息"
|
||||
*
|
||||
* 注意:output 字符串由 SDK 内部 strdup 后发送,handler 返回后即可释放。
|
||||
* 我们约定 handler 不负责分配,由 SDK 在内部做好拷贝。
|
||||
* 所以 handler 可以返回栈上或静态字符串。
|
||||
*/
|
||||
typedef struct {
|
||||
int status; /* 0=ok, 非0=error */
|
||||
const char *output; /* 输出文本(如 base64 图像数据),SDK 内部拷贝 */
|
||||
const char *error; /* 错误信息 */
|
||||
int has_binary; /* 1=通过二进制分块回传 */
|
||||
const char *binary_mime; /* 二进制 MIME 类型 */
|
||||
const uint8_t *binary_data; /* 二进制数据指针 */
|
||||
int binary_len; /* 二进制数据长度 */
|
||||
} ha_cmd_result_t;
|
||||
|
||||
/* ======================== 命令处理声明 ========================
|
||||
*
|
||||
* 声明式命令注册:设备在配置中声明支持哪些命令,并绑定处理函数。
|
||||
*
|
||||
* command 值说明:
|
||||
* - "shell" → 处理 shell 类型命令,args 为完整命令字符串
|
||||
* - "camerasue" → 处理 homeagent-camerasue 命令,args 为参数
|
||||
* - "screensee" → 处理 homeagent-screensee 命令
|
||||
* - "speakeruse" → 处理 homeagent-speakeruse 命令
|
||||
* - "computeruse" → 处理 homeagent-computeruse 命令
|
||||
* - "clipboardsee" → 处理 homeagent-clipboardsee 命令
|
||||
* - "clipboardsue" → 处理 homeagent-clipboardsue 命令
|
||||
* - "screensue" → 处理 homeagent-screensue 命令
|
||||
* - "deviceinfo" → 处理设备信息查询
|
||||
* - 其他自定义命令名 → 按字符串匹配分发
|
||||
*
|
||||
* handler 处理完毕后只需填写 result 结构体,SDK 自动回执。
|
||||
*/
|
||||
typedef ha_status_t (*ha_cmd_handler_t)(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata);
|
||||
|
||||
typedef struct {
|
||||
const char *command; /* 命令名,如 "camerasue"、"shell" */
|
||||
ha_cmd_handler_t handler; /* 处理函数 */
|
||||
} ha_cmd_handler_def_t;
|
||||
|
||||
/* 二进制数据接收回调:收到服务端推送的二进制数据(如 TTS 音频)时调用。
|
||||
* data 指针在回调返回后失效,如需保存请拷贝。 */
|
||||
typedef void (*ha_binary_handler_t)(const char *req_id, const char *kind,
|
||||
const char *mime, const uint8_t *data,
|
||||
int len, void *userdata);
|
||||
|
||||
/* 连接状态变化回调 */
|
||||
typedef void (*ha_state_callback_t)(int connected, void *userdata);
|
||||
|
||||
/* ======================== 客户端配置 ========================
|
||||
*
|
||||
* 所有配置在 ha_client_new() 时一次性声明。
|
||||
* 声明式核心:handlers 表声明了设备支持的所有命令及其处理函数。
|
||||
*/
|
||||
typedef struct {
|
||||
ha_transport_t transport; /* 传输层实现(必须) */
|
||||
ha_device_info_t device; /* 设备声明(必须) */
|
||||
const char *server; /* 服务端地址,如 "192.168.1.100:9890"(必须) */
|
||||
const char *token; /* 接入令牌(必须) */
|
||||
|
||||
ha_cmd_handler_def_t *handlers; /* 声明式命令处理表,.command=NULL 标记结束 */
|
||||
ha_binary_handler_t on_binary; /* 二进制数据接收回调(可选) */
|
||||
ha_state_callback_t on_state; /* 状态变化回调(可选) */
|
||||
void *userdata; /* 用户自定义数据,传给所有回调 */
|
||||
|
||||
int ping_interval; /* 心跳间隔秒数,0 则默认 30 */
|
||||
int max_reconnect; /* 最大重连次数,-1 无限重连(默认),0 不重连 */
|
||||
} ha_config_t;
|
||||
|
||||
/* ======================== 客户端 API ======================== */
|
||||
|
||||
typedef struct ha_client ha_client_t;
|
||||
|
||||
/* 创建客户端实例。config 数据会在内部拷贝,外部可释放。 */
|
||||
ha_client_t *ha_client_new(const ha_config_t *config);
|
||||
|
||||
/* 启动连接:TCP 连接 → WS 升级 → hello → bind → 就绪。阻塞直到完成或失败。 */
|
||||
ha_status_t ha_client_start(ha_client_t *client);
|
||||
|
||||
/* 主循环处理:必须在用户的主循环中周期性调用。
|
||||
* - 读取 WS 帧并分发
|
||||
* - 按 handlers 表查找命令处理函数,自动回执结果
|
||||
* - 处理心跳 ping/pong
|
||||
* - 处理断线重连
|
||||
* 返回 HA_OK 表示正常,HA_ERR_DISCONNECTED 表示正在重连。 */
|
||||
ha_status_t ha_client_process(ha_client_t *client);
|
||||
|
||||
/* ===== 主动上报(设备主动推送,非命令响应) ===== */
|
||||
|
||||
/* 发送设备主动上报事件。type 如 "motion_detected",detail 为 JSON 字符串。 */
|
||||
void ha_client_send_event(ha_client_t *client, const char *type,
|
||||
const char *detail);
|
||||
|
||||
/* 发送设备状态更新。status: "online"、"offline"、"busy" 等。 */
|
||||
void ha_client_send_status(ha_client_t *client, const char *status);
|
||||
|
||||
/* ===== 生命周期 ===== */
|
||||
|
||||
/* 停止客户端,断开连接。 */
|
||||
void ha_client_stop(ha_client_t *client);
|
||||
|
||||
/* 销毁客户端,释放所有资源。 */
|
||||
void ha_client_destroy(ha_client_t *client);
|
||||
|
||||
/* ======================== 工具函数 ======================== */
|
||||
|
||||
/* 解析 homeagent-* 命令,返回能力名和参数。
|
||||
* command = "camerasue 5" → cap="camerasue", args="5"
|
||||
* command = "screensee" → cap="screensee", args=""
|
||||
* command = "computeruse {...}" → cap="computeruse", args="..." */
|
||||
void ha_cmd_parse_homeagent(const char *command, const char **cap,
|
||||
const char **args);
|
||||
|
||||
/* 解析 JSON 格式的命令参数,提取 action 和 JSON 字符串。
|
||||
* command = "computeruse {\"action\":\"click\",\"x\":100}"
|
||||
* → action="computeruse", json_str="{\"action\":\"click\",...}" */
|
||||
void ha_cmd_parse_json(const char *command, const char **action,
|
||||
const char **json_str);
|
||||
|
||||
/* Base64 编码(用于将二进制数据编码为文本回传)。
|
||||
* 返回写入 out 的字节数(不含 \0),out 不足时返回所需长度。 */
|
||||
int ha_base64_encode(const uint8_t *data, int len, char *out, int out_len);
|
||||
|
||||
/* 获取版本号 */
|
||||
const char *ha_version(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_REMOTEDEVICE_H */
|
||||
369
remotedevice/src/ha_json.c
Normal file
369
remotedevice/src/ha_json.c
Normal file
@ -0,0 +1,369 @@
|
||||
#include "ha_json.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ======================== 解析器 ======================== */
|
||||
|
||||
/* 前向声明 */
|
||||
static ha_json_node_t *parse_value(const char **pp);
|
||||
|
||||
/* 跳过空白 */
|
||||
static const char *skip_ws(const char *p) {
|
||||
while (*p && (unsigned char)*p <= ' ') p++;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* 解析字符串("..."),返回新分配的字符串,p 更新到结束引号后 */
|
||||
static char *parse_string(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p != '"') return NULL;
|
||||
p++;
|
||||
int len = 0;
|
||||
const char *q = p;
|
||||
while (*q && *q != '"') {
|
||||
if (*q == '\\') { q++; if (*q) q++; }
|
||||
else q++;
|
||||
len++;
|
||||
}
|
||||
if (*q != '"') return NULL;
|
||||
char *s = (char *)malloc(len + 1);
|
||||
if (!s) return NULL;
|
||||
q = p;
|
||||
int i = 0;
|
||||
while (*q && *q != '"') {
|
||||
if (*q == '\\') {
|
||||
q++;
|
||||
switch (*q) {
|
||||
case '"': s[i++] = '"'; break;
|
||||
case '\\': s[i++] = '\\'; break;
|
||||
case '/': s[i++] = '/'; break;
|
||||
case 'b': s[i++] = '\b'; break;
|
||||
case 'f': s[i++] = '\f'; break;
|
||||
case 'n': s[i++] = '\n'; break;
|
||||
case 'r': s[i++] = '\r'; break;
|
||||
case 't': s[i++] = '\t'; break;
|
||||
case 'u': q += 4; s[i++] = '?'; continue;
|
||||
default: s[i++] = *q; break;
|
||||
}
|
||||
q++;
|
||||
} else {
|
||||
s[i++] = *q++;
|
||||
}
|
||||
}
|
||||
s[i] = '\0';
|
||||
*pp = q + 1;
|
||||
return s;
|
||||
}
|
||||
|
||||
static ha_json_node_t *new_node(ha_json_type_t type) {
|
||||
ha_json_node_t *n = (ha_json_node_t *)calloc(1, sizeof(ha_json_node_t));
|
||||
if (n) n->type = type;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 解析数字 */
|
||||
static ha_json_node_t *parse_number(const char **pp) {
|
||||
const char *p = *pp;
|
||||
int neg = 0;
|
||||
if (*p == '-') { neg = 1; p++; }
|
||||
if (!isdigit((unsigned char)*p)) return NULL;
|
||||
int val = 0;
|
||||
while (isdigit((unsigned char)*p)) {
|
||||
val = val * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
if (*p == '.') { p++; while (isdigit((unsigned char)*p)) p++; }
|
||||
if (*p == 'e' || *p == 'E') {
|
||||
p++;
|
||||
if (*p == '+' || *p == '-') p++;
|
||||
while (isdigit((unsigned char)*p)) p++;
|
||||
}
|
||||
*pp = p;
|
||||
ha_json_node_t *n = new_node(HA_JSON_INT);
|
||||
if (n) n->int_val = neg ? -val : val;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 解析 true/false/null */
|
||||
static ha_json_node_t *parse_keyword(const char **pp) {
|
||||
const char *p = *pp;
|
||||
ha_json_node_t *n = NULL;
|
||||
if (strncmp(p, "true", 4) == 0 && !isalnum((unsigned char)p[4])) {
|
||||
n = new_node(HA_JSON_BOOL); if (n) n->bool_val = 1;
|
||||
*pp = p + 4;
|
||||
} else if (strncmp(p, "false", 5) == 0 && !isalnum((unsigned char)p[5])) {
|
||||
n = new_node(HA_JSON_BOOL); if (n) n->bool_val = 0;
|
||||
*pp = p + 5;
|
||||
} else if (strncmp(p, "null", 4) == 0 && !isalnum((unsigned char)p[4])) {
|
||||
n = new_node(HA_JSON_NULL);
|
||||
*pp = p + 4;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 解析对象 */
|
||||
static ha_json_node_t *parse_object(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p != '{') return NULL;
|
||||
p++;
|
||||
ha_json_node_t *obj = new_node(HA_JSON_OBJECT);
|
||||
if (!obj) return NULL;
|
||||
ha_json_node_t **tail = &obj->child;
|
||||
p = skip_ws(p);
|
||||
if (*p == '}') { *pp = p + 1; return obj; }
|
||||
while (*p) {
|
||||
p = skip_ws(p);
|
||||
char *key = parse_string(&p);
|
||||
if (!key) break;
|
||||
p = skip_ws(p);
|
||||
if (*p != ':') { free(key); break; }
|
||||
p++;
|
||||
ha_json_node_t *val = parse_value(&p);
|
||||
if (!val) { free(key); break; }
|
||||
val->key = key;
|
||||
*tail = val;
|
||||
tail = &val->next;
|
||||
p = skip_ws(p);
|
||||
if (*p == ',') { p++; continue; }
|
||||
if (*p == '}') break;
|
||||
}
|
||||
p = skip_ws(p);
|
||||
if (*p == '}') { *pp = p + 1; return obj; }
|
||||
ha_json_free(obj);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 解析数组 */
|
||||
static ha_json_node_t *parse_array(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p != '[') return NULL;
|
||||
p++;
|
||||
ha_json_node_t *arr = new_node(HA_JSON_ARRAY);
|
||||
if (!arr) return NULL;
|
||||
ha_json_node_t **tail = &arr->child;
|
||||
p = skip_ws(p);
|
||||
if (*p == ']') { *pp = p + 1; return arr; }
|
||||
while (*p) {
|
||||
ha_json_node_t *val = parse_value(&p);
|
||||
if (!val) break;
|
||||
*tail = val;
|
||||
tail = &val->next;
|
||||
p = skip_ws(p);
|
||||
if (*p == ',') { p++; continue; }
|
||||
if (*p == ']') break;
|
||||
}
|
||||
p = skip_ws(p);
|
||||
if (*p == ']') { *pp = p + 1; return arr; }
|
||||
ha_json_free(arr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 解析值(主入口) */
|
||||
static ha_json_node_t *parse_value(const char **pp) {
|
||||
const char *p = skip_ws(*pp);
|
||||
if (*p == '{') return parse_object(pp);
|
||||
if (*p == '[') return parse_array(pp);
|
||||
if (*p == '"') {
|
||||
char *s = parse_string(pp);
|
||||
if (!s) return NULL;
|
||||
ha_json_node_t *n = new_node(HA_JSON_STRING);
|
||||
if (!n) { free(s); return NULL; }
|
||||
n->str_val = s;
|
||||
return n;
|
||||
}
|
||||
if (*p == '-' || isdigit((unsigned char)*p)) return parse_number(pp);
|
||||
return parse_keyword(pp);
|
||||
}
|
||||
|
||||
/* ======================== 公共 API ======================== */
|
||||
|
||||
ha_json_node_t *ha_json_parse(const char *str) {
|
||||
if (!str) return NULL;
|
||||
const char *p = str;
|
||||
return parse_value(&p);
|
||||
}
|
||||
|
||||
const char *ha_json_get_string(const ha_json_node_t *obj, const char *key) {
|
||||
ha_json_node_t *n = ha_json_get(obj, key);
|
||||
if (!n || n->type != HA_JSON_STRING) return NULL;
|
||||
return n->str_val;
|
||||
}
|
||||
|
||||
int ha_json_get_int(const ha_json_node_t *obj, const char *key, int def) {
|
||||
ha_json_node_t *n = ha_json_get(obj, key);
|
||||
if (!n || n->type != HA_JSON_INT) return def;
|
||||
return n->int_val;
|
||||
}
|
||||
|
||||
ha_json_node_t *ha_json_get(const ha_json_node_t *obj, const char *key) {
|
||||
if (!obj || obj->type != HA_JSON_OBJECT) return NULL;
|
||||
ha_json_node_t *c = obj->child;
|
||||
while (c) {
|
||||
if (c->key && strcmp(c->key, key) == 0) return c;
|
||||
c = c->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int ha_json_array_len(const ha_json_node_t *arr) {
|
||||
if (!arr || arr->type != HA_JSON_ARRAY) return 0;
|
||||
int n = 0;
|
||||
ha_json_node_t *c = arr->child;
|
||||
while (c) { n++; c = c->next; }
|
||||
return n;
|
||||
}
|
||||
|
||||
ha_json_node_t *ha_json_array_get(const ha_json_node_t *arr, int index) {
|
||||
if (!arr || arr->type != HA_JSON_ARRAY) return NULL;
|
||||
ha_json_node_t *c = arr->child;
|
||||
int i = 0;
|
||||
while (c) {
|
||||
if (i == index) return c;
|
||||
i++; c = c->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ha_json_free(ha_json_node_t *root) {
|
||||
if (!root) return;
|
||||
ha_json_node_t *c = root->child;
|
||||
while (c) {
|
||||
ha_json_node_t *next = c->next;
|
||||
free(c->key);
|
||||
if (c->type == HA_JSON_STRING) free(c->str_val);
|
||||
ha_json_free(c);
|
||||
c = next;
|
||||
}
|
||||
free(root);
|
||||
}
|
||||
|
||||
/* ======================== 构建器 ======================== */
|
||||
|
||||
static void json_escape(ha_json_builder_t *jb, const char *s) {
|
||||
if (!s) { ha_json_builder_raw(jb, "null"); return; }
|
||||
ha_json_builder_raw(jb, "\"");
|
||||
for (const char *p = s; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
switch (c) {
|
||||
case '"': ha_json_builder_raw(jb, "\\\""); break;
|
||||
case '\\': ha_json_builder_raw(jb, "\\\\"); break;
|
||||
case '\b': ha_json_builder_raw(jb, "\\b"); break;
|
||||
case '\f': ha_json_builder_raw(jb, "\\f"); break;
|
||||
case '\n': ha_json_builder_raw(jb, "\\n"); break;
|
||||
case '\r': ha_json_builder_raw(jb, "\\r"); break;
|
||||
case '\t': ha_json_builder_raw(jb, "\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", c);
|
||||
ha_json_builder_raw(jb, buf);
|
||||
} else {
|
||||
char buf[2] = { (char)c, 0 };
|
||||
ha_json_builder_raw(jb, buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
ha_json_builder_raw(jb, "\"");
|
||||
}
|
||||
|
||||
void ha_json_builder_init(ha_json_builder_t *jb, char *buf, int cap) {
|
||||
jb->buf = buf;
|
||||
jb->len = 0;
|
||||
jb->cap = cap;
|
||||
jb->depth = 0;
|
||||
if (cap > 0) buf[0] = '\0';
|
||||
}
|
||||
|
||||
void ha_json_builder_reset(ha_json_builder_t *jb) {
|
||||
jb->len = 0;
|
||||
jb->depth = 0;
|
||||
if (jb->cap > 0) jb->buf[0] = '\0';
|
||||
}
|
||||
|
||||
void ha_json_builder_raw(ha_json_builder_t *jb, const char *s) {
|
||||
while (*s && jb->len < jb->cap - 1) {
|
||||
jb->buf[jb->len++] = *s++;
|
||||
}
|
||||
jb->buf[jb->len] = '\0';
|
||||
}
|
||||
|
||||
void ha_json_builder_comma(ha_json_builder_t *jb) {
|
||||
if (jb->depth > 0 && jb->item_count[jb->depth - 1] > 0) {
|
||||
ha_json_builder_raw(jb, ",");
|
||||
}
|
||||
if (jb->depth > 0) jb->item_count[jb->depth - 1]++;
|
||||
}
|
||||
|
||||
void ha_json_builder_begin_object(ha_json_builder_t *jb) {
|
||||
ha_json_builder_comma(jb);
|
||||
ha_json_builder_raw(jb, "{");
|
||||
if (jb->depth < 16) jb->item_count[jb->depth] = 0;
|
||||
jb->depth++;
|
||||
}
|
||||
|
||||
void ha_json_builder_end_object(ha_json_builder_t *jb) {
|
||||
jb->depth--;
|
||||
ha_json_builder_raw(jb, "}");
|
||||
}
|
||||
|
||||
void ha_json_builder_begin_array(ha_json_builder_t *jb) {
|
||||
ha_json_builder_comma(jb);
|
||||
ha_json_builder_raw(jb, "[");
|
||||
if (jb->depth < 16) jb->item_count[jb->depth] = 0;
|
||||
jb->depth++;
|
||||
}
|
||||
|
||||
void ha_json_builder_end_array(ha_json_builder_t *jb) {
|
||||
jb->depth--;
|
||||
ha_json_builder_raw(jb, "]");
|
||||
}
|
||||
|
||||
void ha_json_builder_key(ha_json_builder_t *jb, const char *key) {
|
||||
ha_json_builder_comma(jb);
|
||||
json_escape(jb, key);
|
||||
ha_json_builder_raw(jb, ":");
|
||||
}
|
||||
|
||||
void ha_json_builder_add_string(ha_json_builder_t *jb, const char *val) {
|
||||
json_escape(jb, val);
|
||||
}
|
||||
|
||||
void ha_json_builder_add_int(ha_json_builder_t *jb, int val) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d", val);
|
||||
ha_json_builder_raw(jb, buf);
|
||||
}
|
||||
|
||||
void ha_json_builder_add_bool(ha_json_builder_t *jb, int val) {
|
||||
ha_json_builder_raw(jb, val ? "true" : "false");
|
||||
}
|
||||
|
||||
void ha_json_builder_add_null(ha_json_builder_t *jb) {
|
||||
ha_json_builder_raw(jb, "null");
|
||||
}
|
||||
|
||||
void ha_json_builder_string(ha_json_builder_t *jb, const char *key, const char *val) {
|
||||
ha_json_builder_key(jb, key);
|
||||
json_escape(jb, val);
|
||||
}
|
||||
|
||||
void ha_json_builder_int(ha_json_builder_t *jb, const char *key, int val) {
|
||||
ha_json_builder_key(jb, key);
|
||||
ha_json_builder_add_int(jb, val);
|
||||
}
|
||||
|
||||
void ha_json_builder_bool(ha_json_builder_t *jb, const char *key, int val) {
|
||||
ha_json_builder_key(jb, key);
|
||||
ha_json_builder_add_bool(jb, val);
|
||||
}
|
||||
|
||||
const char *ha_json_builder_str(ha_json_builder_t *jb) {
|
||||
return jb->buf;
|
||||
}
|
||||
|
||||
int ha_json_builder_len(ha_json_builder_t *jb) {
|
||||
return jb->len;
|
||||
}
|
||||
107
remotedevice/src/ha_json.h
Normal file
107
remotedevice/src/ha_json.h
Normal file
@ -0,0 +1,107 @@
|
||||
#ifndef HA_JSON_H
|
||||
#define HA_JSON_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ======================== JSON 解析器(DOM 风格) ======================== */
|
||||
typedef enum {
|
||||
HA_JSON_NULL,
|
||||
HA_JSON_BOOL,
|
||||
HA_JSON_INT,
|
||||
HA_JSON_STRING,
|
||||
HA_JSON_ARRAY,
|
||||
HA_JSON_OBJECT,
|
||||
} ha_json_type_t;
|
||||
|
||||
typedef struct ha_json_node {
|
||||
ha_json_type_t type;
|
||||
union {
|
||||
int bool_val;
|
||||
int int_val;
|
||||
char *str_val;
|
||||
};
|
||||
struct ha_json_node *next; /* linked list for array/object items */
|
||||
struct ha_json_node *child; /* first child for array/object */
|
||||
char *key; /* key for object members */
|
||||
} ha_json_node_t;
|
||||
|
||||
/* 解析 JSON 字符串,返回根节点。失败返回 NULL。 */
|
||||
ha_json_node_t *ha_json_parse(const char *str);
|
||||
|
||||
/* 从对象中按 key 获取字符串值,不存在返回 NULL */
|
||||
const char *ha_json_get_string(const ha_json_node_t *obj, const char *key);
|
||||
|
||||
/* 从对象中按 key 获取 int 值,不存在返回 def */
|
||||
int ha_json_get_int(const ha_json_node_t *obj, const char *key, int def);
|
||||
|
||||
/* 从对象中按 key 获取子节点,不存在返回 NULL */
|
||||
ha_json_node_t *ha_json_get(const ha_json_node_t *obj, const char *key);
|
||||
|
||||
/* 获取数组长度 */
|
||||
int ha_json_array_len(const ha_json_node_t *arr);
|
||||
|
||||
/* 获取数组第 index 个元素,越界返回 NULL */
|
||||
ha_json_node_t *ha_json_array_get(const ha_json_node_t *arr, int index);
|
||||
|
||||
/* 释放整个 JSON 树 */
|
||||
void ha_json_free(ha_json_node_t *root);
|
||||
|
||||
/* ======================== JSON 构建器(直接写缓冲区) ======================== */
|
||||
typedef struct {
|
||||
char *buf;
|
||||
int len;
|
||||
int cap;
|
||||
int depth;
|
||||
int item_count[16]; /* 每层已添加元素数,用于逗号判断 */
|
||||
} ha_json_builder_t;
|
||||
|
||||
/* 初始化构建器 */
|
||||
void ha_json_builder_init(ha_json_builder_t *jb, char *buf, int cap);
|
||||
|
||||
/* 清空构建器 */
|
||||
void ha_json_builder_reset(ha_json_builder_t *jb);
|
||||
|
||||
/* 基础写入 */
|
||||
void ha_json_builder_raw(ha_json_builder_t *jb, const char *s);
|
||||
|
||||
/* 逗号(自动判断是否需要加) */
|
||||
void ha_json_builder_comma(ha_json_builder_t *jb);
|
||||
|
||||
/* 对象 */
|
||||
void ha_json_builder_begin_object(ha_json_builder_t *jb);
|
||||
void ha_json_builder_end_object(ha_json_builder_t *jb);
|
||||
|
||||
/* 数组 */
|
||||
void ha_json_builder_begin_array(ha_json_builder_t *jb);
|
||||
void ha_json_builder_end_array(ha_json_builder_t *jb);
|
||||
|
||||
/* 键名 */
|
||||
void ha_json_builder_key(ha_json_builder_t *jb, const char *key);
|
||||
|
||||
/* 值 */
|
||||
void ha_json_builder_add_string(ha_json_builder_t *jb, const char *val);
|
||||
void ha_json_builder_add_int(ha_json_builder_t *jb, int val);
|
||||
void ha_json_builder_add_bool(ha_json_builder_t *jb, int val);
|
||||
void ha_json_builder_add_null(ha_json_builder_t *jb);
|
||||
|
||||
/* 快捷方法:直接写 "key":"val" */
|
||||
void ha_json_builder_string(ha_json_builder_t *jb, const char *key, const char *val);
|
||||
void ha_json_builder_int(ha_json_builder_t *jb, const char *key, int val);
|
||||
void ha_json_builder_bool(ha_json_builder_t *jb, const char *key, int val);
|
||||
|
||||
/* 获取当前构建的字符串指针 */
|
||||
const char *ha_json_builder_str(ha_json_builder_t *jb);
|
||||
|
||||
/* 获取当前长度 */
|
||||
int ha_json_builder_len(ha_json_builder_t *jb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_JSON_H */
|
||||
628
remotedevice/src/ha_remotedevice.c
Normal file
628
remotedevice/src/ha_remotedevice.c
Normal file
@ -0,0 +1,628 @@
|
||||
#include "ha_remotedevice.h"
|
||||
#include "ha_json.h"
|
||||
#include "ha_ws.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define HA_VERSION "0.1.0"
|
||||
|
||||
/* 前向声明(因 handle_cmd_msg 需要调用这些函数,而它们定义在后面) */
|
||||
void ha_client_send_result(ha_client_t *client, const char *req_id,
|
||||
const char *status, const char *output,
|
||||
const char *error);
|
||||
void ha_client_send_data_chunked(ha_client_t *client, const char *req_id,
|
||||
const char *kind, const char *mime,
|
||||
const uint8_t *data, int len);
|
||||
|
||||
/* ======================== 内部状态 ======================== */
|
||||
typedef enum {
|
||||
HA_STATE_INIT,
|
||||
HA_STATE_DISCONNECTED,
|
||||
HA_STATE_CONNECTING,
|
||||
HA_STATE_WS_UPGRADING,
|
||||
HA_STATE_HELLO_SENT,
|
||||
HA_STATE_BIND_SENT,
|
||||
HA_STATE_READY,
|
||||
HA_STATE_STOPPING,
|
||||
} ha_state_t;
|
||||
|
||||
/* 语音数据聚合缓冲区 */
|
||||
typedef struct {
|
||||
char req_id[128];
|
||||
char kind[64];
|
||||
char mime[64];
|
||||
int total;
|
||||
uint8_t *data;
|
||||
int len;
|
||||
int cap;
|
||||
} ha_speech_accum_t;
|
||||
|
||||
struct ha_client {
|
||||
ha_config_t config; /* 拷贝的配置 */
|
||||
ha_state_t state;
|
||||
int reconnect_cnt; /* 当前重连次数 */
|
||||
ha_ws_t ws; /* WS 连接 */
|
||||
|
||||
/* JSON 构建缓冲区 */
|
||||
char json_buf[4096];
|
||||
ha_json_builder_t jb;
|
||||
|
||||
/* 语音数据聚合 */
|
||||
ha_speech_accum_t speech;
|
||||
};
|
||||
|
||||
/* ======================== 辅助函数 ======================== */
|
||||
|
||||
static void set_sockbuf(ha_client_t *c, int i) { (void)c; (void)i; }
|
||||
|
||||
/* Base64 编码表 */
|
||||
static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
int ha_base64_encode(const uint8_t *data, int len, char *out, int out_len) {
|
||||
int needed = ((len + 2) / 3) * 4 + 1;
|
||||
if (out_len < needed) {
|
||||
if (out_len > 0) out[0] = '\0';
|
||||
return needed;
|
||||
}
|
||||
int i = 0, j = 0;
|
||||
while (i < len) {
|
||||
int rem = len - i;
|
||||
uint8_t b0 = data[i++];
|
||||
uint8_t b1 = (rem > 1) ? data[i++] : 0;
|
||||
uint8_t b2 = (rem > 2) ? data[i++] : 0;
|
||||
out[j++] = b64[b0 >> 2];
|
||||
out[j++] = b64[((b0 & 0x03) << 4) | (b1 >> 4)];
|
||||
out[j++] = (rem > 1) ? b64[((b1 & 0x0F) << 2) | (b2 >> 6)] : '=';
|
||||
out[j++] = (rem > 2) ? b64[b2 & 0x3F] : '=';
|
||||
}
|
||||
out[j] = '\0';
|
||||
return j;
|
||||
}
|
||||
|
||||
/* ======================== JSON 构建辅助 ======================== */
|
||||
static void json_init(ha_client_t *c) {
|
||||
ha_json_builder_init(&c->jb, c->json_buf, sizeof(c->json_buf));
|
||||
}
|
||||
|
||||
/* ======================== WS 发送 JSON ======================== */
|
||||
static int ws_send_json(ha_client_t *c) {
|
||||
return ha_ws_send_text(&c->ws, c->json_buf);
|
||||
}
|
||||
|
||||
/* ======================== 协议消息构造 ======================== */
|
||||
|
||||
/* 构建 hello 消息 */
|
||||
static int send_hello(ha_client_t *c) {
|
||||
json_init(c);
|
||||
ha_json_builder_begin_object(&c->jb);
|
||||
ha_json_builder_string(&c->jb, "op", "hello");
|
||||
ha_json_builder_key(&c->jb, "device");
|
||||
ha_json_builder_begin_object(&c->jb);
|
||||
ha_json_builder_string(&c->jb, "device_id", c->config.device.device_id);
|
||||
ha_json_builder_string(&c->jb, "name", c->config.device.name);
|
||||
ha_json_builder_string(&c->jb, "kind", c->config.device.kind);
|
||||
/* caps */
|
||||
ha_json_builder_key(&c->jb, "caps");
|
||||
ha_json_builder_begin_array(&c->jb);
|
||||
if (c->config.device.caps) {
|
||||
for (const char **p = c->config.device.caps; *p; p++) {
|
||||
ha_json_builder_add_string(&c->jb, *p);
|
||||
}
|
||||
}
|
||||
ha_json_builder_end_array(&c->jb);
|
||||
/* info 可选 */
|
||||
if (c->config.device.info_json && c->config.device.info_json[0]) {
|
||||
ha_json_builder_string(&c->jb, "info", c->config.device.info_json);
|
||||
}
|
||||
ha_json_builder_end_object(&c->jb); /* device */
|
||||
ha_json_builder_end_object(&c->jb); /* root */
|
||||
return ws_send_json(c);
|
||||
}
|
||||
|
||||
/* 构建 bind 消息 */
|
||||
static int send_bind(ha_client_t *c) {
|
||||
json_init(c);
|
||||
ha_json_builder_begin_object(&c->jb);
|
||||
ha_json_builder_string(&c->jb, "op", "bind");
|
||||
ha_json_builder_string(&c->jb, "device_id", c->config.device.device_id);
|
||||
ha_json_builder_string(&c->jb, "token", c->config.token);
|
||||
ha_json_builder_end_object(&c->jb);
|
||||
return ws_send_json(c);
|
||||
}
|
||||
|
||||
/* ======================== 消息处理 ======================== */
|
||||
|
||||
/* 在 handlers 表中查找命令处理函数 */
|
||||
static ha_cmd_handler_def_t *find_handler(ha_client_t *c, const char *name) {
|
||||
if (!name || !c->config.handlers) return NULL;
|
||||
for (ha_cmd_handler_def_t *h = c->config.handlers; h->command; h++) {
|
||||
if (strcmp(h->command, name) == 0) return h;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 声明式命令分发:查找 handlers 表 → 调用 handler → 自动回执 */
|
||||
static void handle_cmd_msg(ha_client_t *c, ha_json_node_t *msg) {
|
||||
const char *req_id = ha_json_get_string(msg, "req_id");
|
||||
const char *command = ha_json_get_string(msg, "command");
|
||||
const char *cmd_type = ha_json_get_string(msg, "cmd_type");
|
||||
if (!req_id || !command) return;
|
||||
if (!cmd_type) cmd_type = "homeagent";
|
||||
|
||||
const char *handler_name = NULL;
|
||||
const char *args = command;
|
||||
|
||||
if (strcmp(cmd_type, "shell") == 0) {
|
||||
handler_name = "shell";
|
||||
/* args 保持为完整命令字符串 */
|
||||
} else {
|
||||
/* homeagent-* 命令:提取能力名作为 handler 名 */
|
||||
const char *cap = command;
|
||||
const char *p = command;
|
||||
if (strncmp(p, "homeagent-", 10) == 0) p += 10;
|
||||
const char *space = strchr(p, ' ');
|
||||
if (space) {
|
||||
args = space + 1;
|
||||
/* handler_name 用静态缓冲区 */
|
||||
static char name_buf[128];
|
||||
int n = (int)(space - p);
|
||||
if (n > 127) n = 127;
|
||||
strncpy(name_buf, p, n);
|
||||
name_buf[n] = '\0';
|
||||
handler_name = name_buf;
|
||||
} else {
|
||||
handler_name = p;
|
||||
args = "";
|
||||
}
|
||||
}
|
||||
|
||||
ha_cmd_handler_def_t *def = find_handler(c, handler_name);
|
||||
if (!def) {
|
||||
ha_client_send_result(c, req_id, "error", NULL,
|
||||
"unsupported command");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 调用 handler,填写 result */
|
||||
ha_cmd_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
ha_status_t st = def->handler(req_id, args, &result, c->config.userdata);
|
||||
|
||||
/* 自动回执 */
|
||||
if (st != HA_OK) {
|
||||
ha_client_send_result(c, req_id, "error", NULL,
|
||||
result.error ? result.error : "handler failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.has_binary && result.binary_data && result.binary_len > 0) {
|
||||
/* 二进制分块回传 */
|
||||
ha_client_send_data_chunked(c, req_id,
|
||||
handler_name, result.binary_mime ? result.binary_mime : "application/octet-stream",
|
||||
result.binary_data, result.binary_len);
|
||||
} else {
|
||||
/* 文本回传 */
|
||||
ha_client_send_result(c, req_id, result.status == 0 ? "ok" : "error",
|
||||
result.output, result.error);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_speech_start(ha_client_t *c, ha_json_node_t *msg) {
|
||||
const char *req_id = ha_json_get_string(msg, "req_id");
|
||||
const char *kind = ha_json_get_string(msg, "kind");
|
||||
const char *mime = ha_json_get_string(msg, "mime");
|
||||
if (!req_id) return;
|
||||
|
||||
/* 释放旧的聚合数据 */
|
||||
free(c->speech.data);
|
||||
memset(&c->speech, 0, sizeof(c->speech));
|
||||
|
||||
strncpy(c->speech.req_id, req_id, sizeof(c->speech.req_id) - 1);
|
||||
if (kind) strncpy(c->speech.kind, kind, sizeof(c->speech.kind) - 1);
|
||||
if (mime) strncpy(c->speech.mime, mime, sizeof(c->speech.mime) - 1);
|
||||
c->speech.total = ha_json_get_int(msg, "total", 0);
|
||||
}
|
||||
|
||||
static void handle_speech_end(ha_client_t *c, ha_json_node_t *msg) {
|
||||
const char *req_id = ha_json_get_string(msg, "req_id");
|
||||
if (!req_id || strcmp(req_id, c->speech.req_id) != 0) return;
|
||||
|
||||
if (c->config.on_binary && c->speech.data && c->speech.len > 0) {
|
||||
c->config.on_binary(c->speech.req_id, c->speech.kind,
|
||||
c->speech.mime, c->speech.data,
|
||||
c->speech.len, c->config.userdata);
|
||||
}
|
||||
|
||||
free(c->speech.data);
|
||||
memset(&c->speech, 0, sizeof(c->speech));
|
||||
}
|
||||
|
||||
static void handle_text_message(ha_client_t *c, const uint8_t *payload, int len) {
|
||||
/* 解析 JSON */
|
||||
char *tmp = (char *)malloc(len + 1);
|
||||
if (!tmp) return;
|
||||
memcpy(tmp, payload, len);
|
||||
tmp[len] = '\0';
|
||||
|
||||
ha_json_node_t *root = ha_json_parse(tmp);
|
||||
if (!root) { free(tmp); return; }
|
||||
|
||||
const char *op = ha_json_get_string(root, "op");
|
||||
if (!op) { ha_json_free(root); free(tmp); return; }
|
||||
|
||||
switch (c->state) {
|
||||
case HA_STATE_HELLO_SENT:
|
||||
if (strcmp(op, "hello_ack") == 0) {
|
||||
c->state = HA_STATE_BIND_SENT;
|
||||
send_bind(c);
|
||||
}
|
||||
break;
|
||||
case HA_STATE_BIND_SENT:
|
||||
if (strcmp(op, "bind_ack") == 0) {
|
||||
c->state = HA_STATE_READY;
|
||||
if (c->config.on_state) {
|
||||
c->config.on_state(1, c->config.userdata);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HA_STATE_READY:
|
||||
if (strcmp(op, "cmd") == 0) {
|
||||
handle_cmd_msg(c, root);
|
||||
} else if (strcmp(op, "cmd_speech_start") == 0) {
|
||||
handle_speech_start(c, root);
|
||||
} else if (strcmp(op, "cmd_speech_end") == 0) {
|
||||
handle_speech_end(c, root);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ha_json_free(root);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
/* ======================== 连接管理 ======================== */
|
||||
|
||||
static int do_connect(ha_client_t *c) {
|
||||
c->state = HA_STATE_CONNECTING;
|
||||
c->reconnect_cnt++;
|
||||
|
||||
/* 解析 server 地址 */
|
||||
char host[256] = {0};
|
||||
uint16_t port = 9890;
|
||||
const char *p = c->config.server;
|
||||
if (!p) return -1;
|
||||
|
||||
/* 去掉 ws:// 前缀 */
|
||||
if (strncmp(p, "ws://", 5) == 0) p += 5;
|
||||
else if (strncmp(p, "wss://", 6) == 0) p += 6;
|
||||
|
||||
/* 提取 host:port */
|
||||
const char *colon = strchr(p, ':');
|
||||
const char *slash = strchr(p, '/');
|
||||
if (colon && (!slash || colon < slash)) {
|
||||
int host_len = (int)(colon - p);
|
||||
if (host_len > (int)sizeof(host) - 1) host_len = sizeof(host) - 1;
|
||||
memcpy(host, p, host_len);
|
||||
host[host_len] = '\0';
|
||||
port = (uint16_t)atoi(colon + 1);
|
||||
} else {
|
||||
int host_len = (slash ? (int)(slash - p) : (int)strlen(p));
|
||||
if (host_len > (int)sizeof(host) - 1) host_len = sizeof(host) - 1;
|
||||
memcpy(host, p, host_len);
|
||||
host[host_len] = '\0';
|
||||
}
|
||||
|
||||
c->state = HA_STATE_WS_UPGRADING;
|
||||
if (ha_ws_connect(&c->ws, &c->config.transport, host, port,
|
||||
"/api/v1/device/ws", c->config.token) != 0) {
|
||||
c->state = HA_STATE_DISCONNECTED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 发送 hello */
|
||||
c->state = HA_STATE_HELLO_SENT;
|
||||
if (send_hello(c) != 0) {
|
||||
ha_ws_close(&c->ws);
|
||||
c->state = HA_STATE_DISCONNECTED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================== 公共 API ======================== */
|
||||
|
||||
ha_client_t *ha_client_new(const ha_config_t *config) {
|
||||
ha_client_t *c = (ha_client_t *)calloc(1, sizeof(ha_client_t));
|
||||
if (!c) return NULL;
|
||||
memcpy(&c->config, config, sizeof(ha_config_t));
|
||||
c->state = HA_STATE_INIT;
|
||||
c->reconnect_cnt = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
ha_status_t ha_client_start(ha_client_t *client) {
|
||||
if (!client) return HA_ERR_INVALID;
|
||||
if (client->state != HA_STATE_INIT) return HA_ERR_GENERIC;
|
||||
|
||||
/* 默认心跳间隔 30 秒 */
|
||||
if (client->config.ping_interval <= 0) {
|
||||
client->config.ping_interval = 30;
|
||||
}
|
||||
|
||||
if (do_connect(client) != 0) {
|
||||
return HA_ERR_TRANSPORT;
|
||||
}
|
||||
|
||||
/* 等待 bind_ack(最多 5 秒) */
|
||||
int wait_ms = 5000;
|
||||
int step = 50;
|
||||
while (wait_ms > 0 && client->state != HA_STATE_READY) {
|
||||
/* 处理一帧 */
|
||||
ha_status_t st = ha_client_process(client);
|
||||
if (st != HA_OK && st != HA_ERR_DISCONNECTED) {
|
||||
return st;
|
||||
}
|
||||
if (client->state == HA_STATE_READY) return HA_OK;
|
||||
|
||||
/* 简单延时:靠 process 中的 recv 阻塞 */
|
||||
wait_ms -= step;
|
||||
}
|
||||
|
||||
return (client->state == HA_STATE_READY) ? HA_OK : HA_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
ha_status_t ha_client_process(ha_client_t *client) {
|
||||
if (!client) return HA_ERR_INVALID;
|
||||
|
||||
if (client->state == HA_STATE_STOPPING) {
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
/* 断线重连 */
|
||||
if (client->state == HA_STATE_DISCONNECTED ||
|
||||
client->state == HA_STATE_INIT) {
|
||||
if (client->config.max_reconnect >= 0 &&
|
||||
client->reconnect_cnt > client->config.max_reconnect) {
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
/* 非阻塞模式:不在这里阻塞等待重连,返回 HA_ERR_DISCONNECTED */
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
if (!client->ws.connected) {
|
||||
client->state = HA_STATE_DISCONNECTED;
|
||||
if (client->config.on_state) {
|
||||
client->config.on_state(0, client->config.userdata);
|
||||
}
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
/* 尝试读取一帧 */
|
||||
const uint8_t *payload = NULL;
|
||||
int len = 0;
|
||||
int ret = ha_ws_read_frame(&client->ws, &payload, &len);
|
||||
|
||||
if (ret < 0) {
|
||||
/* 连接断开 */
|
||||
client->state = HA_STATE_DISCONNECTED;
|
||||
if (client->config.on_state) {
|
||||
client->config.on_state(0, client->config.userdata);
|
||||
}
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
switch (ret) {
|
||||
case WS_OPCODE_TEXT:
|
||||
handle_text_message(client, payload, len);
|
||||
break;
|
||||
case WS_OPCODE_BINARY:
|
||||
/* 二进制帧:如果处于语音聚合状态,追加数据 */
|
||||
if (client->speech.req_id[0] && payload) {
|
||||
int new_len = client->speech.len + len;
|
||||
if (new_len > client->speech.cap) {
|
||||
int new_cap = client->speech.cap ? client->speech.cap * 2 : 4096;
|
||||
while (new_cap < new_len) new_cap *= 2;
|
||||
uint8_t *nd = (uint8_t *)realloc(client->speech.data, new_cap);
|
||||
if (!nd) break;
|
||||
client->speech.data = nd;
|
||||
client->speech.cap = new_cap;
|
||||
}
|
||||
memcpy(client->speech.data + client->speech.len, payload, len);
|
||||
client->speech.len = new_len;
|
||||
}
|
||||
break;
|
||||
case WS_OPCODE_PING:
|
||||
/* 回复 pong */
|
||||
ha_ws_send_frame(&client->ws, WS_OPCODE_PONG, NULL, 0);
|
||||
break;
|
||||
case WS_OPCODE_PONG:
|
||||
/* 收到 pong,忽略 */
|
||||
break;
|
||||
case WS_OPCODE_CLOSE:
|
||||
client->state = HA_STATE_DISCONNECTED;
|
||||
if (client->config.on_state) {
|
||||
client->config.on_state(0, client->config.userdata);
|
||||
}
|
||||
return HA_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
void ha_client_send_result(ha_client_t *client, const char *req_id,
|
||||
const char *status, const char *output,
|
||||
const char *error) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "cmd_result");
|
||||
ha_json_builder_string(&client->jb, "req_id", req_id);
|
||||
ha_json_builder_string(&client->jb, "status", status ? status : "ok");
|
||||
ha_json_builder_string(&client->jb, "device_id", client->config.device.device_id);
|
||||
if (output && output[0]) {
|
||||
ha_json_builder_string(&client->jb, "output", output);
|
||||
}
|
||||
if (error && error[0]) {
|
||||
ha_json_builder_string(&client->jb, "error", error);
|
||||
}
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_send_data_chunked(ha_client_t *client, const char *req_id,
|
||||
const char *kind, const char *mime,
|
||||
const uint8_t *data, int len) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
|
||||
/* cmd_data_start */
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "cmd_data_start");
|
||||
ha_json_builder_string(&client->jb, "req_id", req_id);
|
||||
ha_json_builder_string(&client->jb, "kind", kind ? kind : "data");
|
||||
ha_json_builder_string(&client->jb, "mime", mime ? mime : "application/octet-stream");
|
||||
ha_json_builder_int(&client->jb, "total", len);
|
||||
ha_json_builder_int(&client->jb, "chunk_size", 8192);
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
|
||||
/* 二进制帧分块发送 */
|
||||
int off = 0;
|
||||
while (off < len) {
|
||||
int chunk = len - off;
|
||||
if (chunk > 8192) chunk = 8192;
|
||||
if (ha_ws_send_binary(&client->ws, data + off, chunk) != 0) return;
|
||||
off += chunk;
|
||||
}
|
||||
|
||||
/* cmd_data_end */
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "cmd_data_end");
|
||||
ha_json_builder_string(&client->jb, "req_id", req_id);
|
||||
ha_json_builder_string(&client->jb, "status", "ok");
|
||||
ha_json_builder_int(&client->jb, "total", len);
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_send_event(ha_client_t *client, const char *type,
|
||||
const char *detail) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "event");
|
||||
ha_json_builder_string(&client->jb, "device_id", client->config.device.device_id);
|
||||
ha_json_builder_string(&client->jb, "type", type ? type : "");
|
||||
if (detail && detail[0]) {
|
||||
ha_json_builder_string(&client->jb, "payload", detail);
|
||||
}
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_send_status(ha_client_t *client, const char *status) {
|
||||
if (!client || client->state != HA_STATE_READY) return;
|
||||
json_init(client);
|
||||
ha_json_builder_begin_object(&client->jb);
|
||||
ha_json_builder_string(&client->jb, "op", "status");
|
||||
ha_json_builder_string(&client->jb, "device_id", client->config.device.device_id);
|
||||
ha_json_builder_string(&client->jb, "status", status ? status : "online");
|
||||
ha_json_builder_end_object(&client->jb);
|
||||
ws_send_json(client);
|
||||
}
|
||||
|
||||
void ha_client_stop(ha_client_t *client) {
|
||||
if (!client) return;
|
||||
client->state = HA_STATE_STOPPING;
|
||||
if (client->ws.connected) {
|
||||
ha_ws_close(&client->ws);
|
||||
}
|
||||
}
|
||||
|
||||
void ha_client_destroy(ha_client_t *client) {
|
||||
if (!client) return;
|
||||
ha_client_stop(client);
|
||||
free(client->speech.data);
|
||||
free(client);
|
||||
}
|
||||
|
||||
/* ======================== 工具函数 ======================== */
|
||||
|
||||
void ha_cmd_parse_homeagent(const char *command, const char **cap,
|
||||
const char **args) {
|
||||
*cap = command;
|
||||
*args = "";
|
||||
|
||||
if (!command) {
|
||||
*cap = "";
|
||||
return;
|
||||
}
|
||||
|
||||
/* 去掉 homeagent- 前缀 */
|
||||
const char *p = command;
|
||||
if (strncmp(p, "homeagent-", 10) == 0) {
|
||||
p += 10;
|
||||
}
|
||||
|
||||
/* 按空格分割 */
|
||||
const char *space = strchr(p, ' ');
|
||||
if (space) {
|
||||
/* cap 指向 p 但不包含空格,需要临时拷贝 */
|
||||
/* 返回指针到原始字符串,调用方用 strncpy 取出 */
|
||||
*cap = command; /* 调用方应使用 ha_cmd_parse_homeagent 的要小心 */
|
||||
/* 实际上,最简单的方式是原地修改,但 const 不允许 */
|
||||
/* 用静态缓冲区或让调用方自己处理 */
|
||||
static char cap_buf[256];
|
||||
int n = (int)(space - p);
|
||||
if (n > 255) n = 255;
|
||||
strncpy(cap_buf, p, n);
|
||||
cap_buf[n] = '\0';
|
||||
*cap = cap_buf;
|
||||
*args = space + 1;
|
||||
} else {
|
||||
static char cap_buf[256];
|
||||
strncpy(cap_buf, p, sizeof(cap_buf) - 1);
|
||||
cap_buf[sizeof(cap_buf) - 1] = '\0';
|
||||
*cap = cap_buf;
|
||||
*args = "";
|
||||
}
|
||||
}
|
||||
|
||||
void ha_cmd_parse_json(const char *command, const char **action,
|
||||
const char **json_str) {
|
||||
*action = "";
|
||||
*json_str = "";
|
||||
|
||||
if (!command) return;
|
||||
|
||||
const char *p = command;
|
||||
if (strncmp(p, "homeagent-", 10) == 0) {
|
||||
p += 10;
|
||||
}
|
||||
|
||||
const char *brace = strchr(p, '{');
|
||||
if (brace) {
|
||||
static char act_buf[256];
|
||||
int n = (int)(brace - p);
|
||||
while (n > 0 && (p[n - 1] == ' ' || p[n - 1] == '\t')) n--;
|
||||
if (n > 255) n = 255;
|
||||
strncpy(act_buf, p, n);
|
||||
act_buf[n] = '\0';
|
||||
*action = act_buf;
|
||||
*json_str = brace;
|
||||
} else {
|
||||
static char act_buf[256];
|
||||
strncpy(act_buf, p, sizeof(act_buf) - 1);
|
||||
*action = act_buf;
|
||||
}
|
||||
}
|
||||
|
||||
const char *ha_version(void) {
|
||||
return HA_VERSION;
|
||||
}
|
||||
|
||||
325
remotedevice/src/ha_ws.c
Normal file
325
remotedevice/src/ha_ws.c
Normal file
@ -0,0 +1,325 @@
|
||||
#include "ha_ws.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* WS GUID 用于计算 Accept 值 */
|
||||
#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
/* ======================== Base64 编码(用于 WS key) ======================== */
|
||||
static const char b64t[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static void base64_encode_bin(const uint8_t *in, int in_len, char *out) {
|
||||
int i = 0, j = 0;
|
||||
uint8_t b[3];
|
||||
while (i < in_len) {
|
||||
int rem = in_len - i;
|
||||
if (rem >= 3) {
|
||||
b[0] = in[i++]; b[1] = in[i++]; b[2] = in[i++];
|
||||
out[j++] = b64t[b[0] >> 2];
|
||||
out[j++] = b64t[((b[0] & 0x03) << 4) | (b[1] >> 4)];
|
||||
out[j++] = b64t[((b[1] & 0x0F) << 2) | (b[2] >> 6)];
|
||||
out[j++] = b64t[b[2] & 0x3F];
|
||||
} else if (rem == 2) {
|
||||
b[0] = in[i++]; b[1] = in[i++];
|
||||
out[j++] = b64t[b[0] >> 2];
|
||||
out[j++] = b64t[((b[0] & 0x03) << 4) | (b[1] >> 4)];
|
||||
out[j++] = b64t[(b[1] & 0x0F) << 2];
|
||||
out[j++] = '=';
|
||||
} else {
|
||||
b[0] = in[i++];
|
||||
out[j++] = b64t[b[0] >> 2];
|
||||
out[j++] = b64t[(b[0] & 0x03) << 4];
|
||||
out[j++] = '=';
|
||||
out[j++] = '=';
|
||||
}
|
||||
}
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
/* 简单伪随机数生成器 */
|
||||
static uint32_t ws_rand_state = 0;
|
||||
static void ws_rand_seed(uint32_t seed) { ws_rand_state = seed; }
|
||||
static uint32_t ws_rand(void) {
|
||||
ws_rand_state = ws_rand_state * 1103515245 + 12345;
|
||||
return ws_rand_state;
|
||||
}
|
||||
|
||||
/* 生成 WS 握手 key */
|
||||
static void ws_gen_key(char *out) {
|
||||
uint8_t buf[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
buf[i] = (uint8_t)(ws_rand() & 0xFF);
|
||||
}
|
||||
base64_encode_bin(buf, 16, out);
|
||||
}
|
||||
|
||||
/* ======================== 从传输层接收指定字节数 ======================== */
|
||||
static int recv_all(ha_ws_t *ws, uint8_t *buf, int len) {
|
||||
int pos = 0;
|
||||
while (pos < len) {
|
||||
int n = ws->transport->recv(ws->transport->ctx, buf + pos, len - pos);
|
||||
if (n <= 0) return -1;
|
||||
pos += n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================== 发送 WS 帧 ======================== */
|
||||
int ha_ws_send_frame(ha_ws_t *ws, int opcode, const uint8_t *payload, int len) {
|
||||
uint8_t hdr[14]; /* 最大帧头:2 + 8 + 4 = 14 */
|
||||
int hdr_len = 0;
|
||||
|
||||
hdr[0] = 0x80 | opcode; /* FIN + opcode */
|
||||
hdr_len = 2;
|
||||
|
||||
int ext_len = 0;
|
||||
if (len < 126) {
|
||||
hdr[1] = 0x80 | len; /* mask bit + length */
|
||||
} else if (len < 65536) {
|
||||
hdr[1] = 0x80 | 126;
|
||||
hdr_len = 4;
|
||||
hdr[2] = (uint8_t)(len >> 8);
|
||||
hdr[3] = (uint8_t)(len & 0xFF);
|
||||
ext_len = 2;
|
||||
} else {
|
||||
hdr[1] = 0x80 | 127;
|
||||
hdr_len = 10;
|
||||
uint64_t l = (uint64_t)len;
|
||||
for (int i = 8; i > 0; i--) {
|
||||
hdr[1 + i] = (uint8_t)(l & 0xFF);
|
||||
l >>= 8;
|
||||
}
|
||||
ext_len = 8;
|
||||
}
|
||||
|
||||
/* mask key */
|
||||
uint8_t mask_key[4];
|
||||
mask_key[0] = (uint8_t)(ws_rand() & 0xFF);
|
||||
mask_key[1] = (uint8_t)(ws_rand() & 0xFF);
|
||||
mask_key[2] = (uint8_t)(ws_rand() & 0xFF);
|
||||
mask_key[3] = (uint8_t)(ws_rand() & 0xFF);
|
||||
|
||||
int mask_off = 2 + ext_len;
|
||||
hdr[mask_off] = mask_key[0];
|
||||
hdr[mask_off + 1] = mask_key[1];
|
||||
hdr[mask_off + 2] = mask_key[2];
|
||||
hdr[mask_off + 3] = mask_key[3];
|
||||
hdr_len = mask_off + 4;
|
||||
|
||||
/* 发送帧头 */
|
||||
if (ws->transport->send(ws->transport->ctx, hdr, hdr_len) != hdr_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 发送掩码后的 payload */
|
||||
if (len > 0) {
|
||||
/* 如果 payload 不大,用栈缓冲区 */
|
||||
uint8_t stack_buf[2048];
|
||||
uint8_t *masked = (len <= (int)sizeof(stack_buf)) ? stack_buf : (uint8_t *)malloc(len);
|
||||
if (!masked) return -1;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
masked[i] = payload[i] ^ mask_key[i & 3];
|
||||
}
|
||||
|
||||
int ret = (ws->transport->send(ws->transport->ctx, masked, len) == len) ? 0 : -1;
|
||||
|
||||
if (masked != stack_buf) free(masked);
|
||||
if (ret != 0) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ======================== 公共 API ======================== */
|
||||
|
||||
int ha_ws_connect(ha_ws_t *ws, ha_transport_t *transport,
|
||||
const char *host, uint16_t port,
|
||||
const char *path, const char *token) {
|
||||
memset(ws, 0, sizeof(ha_ws_t));
|
||||
ws->transport = transport;
|
||||
ws->connected = 0;
|
||||
|
||||
strncpy(ws->host, host, sizeof(ws->host) - 1);
|
||||
ws->port = port;
|
||||
strncpy(ws->path, path, sizeof(ws->path) - 1);
|
||||
if (token) strncpy(ws->token, token, sizeof(ws->token) - 1);
|
||||
|
||||
/* 种子 */
|
||||
ws_rand_seed((uint32_t)(uintptr_t)ws ^ (uint32_t)port);
|
||||
|
||||
/* 1. TCP 连接 */
|
||||
if (transport->connect(transport->ctx, host, port) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 2. 发送 WS 升级请求 */
|
||||
char key[32];
|
||||
ws_gen_key(key);
|
||||
|
||||
char req[1024];
|
||||
int n = snprintf(req, sizeof(req),
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"Host: %s:%u\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n",
|
||||
path, host, (unsigned)port, key);
|
||||
|
||||
/* 如果 token 存在,加到路径参数中 */
|
||||
if (token && token[0]) {
|
||||
n = snprintf(req, sizeof(req),
|
||||
"GET %s?token=%s HTTP/1.1\r\n"
|
||||
"Host: %s:%u\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n",
|
||||
path, token, host, (unsigned)port, key);
|
||||
}
|
||||
|
||||
if (transport->send(transport->ctx, (uint8_t *)req, n) != n) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 3. 读取响应头(直到 \r\n\r\n) */
|
||||
char resp[1024];
|
||||
int resp_len = 0;
|
||||
int found = 0;
|
||||
while (resp_len < (int)sizeof(resp) - 1) {
|
||||
int n = transport->recv(transport->ctx, (uint8_t *)(resp + resp_len), 1);
|
||||
if (n <= 0) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
resp_len += n;
|
||||
resp[resp_len] = '\0';
|
||||
if (resp_len >= 4 && strcmp(resp + resp_len - 4, "\r\n\r\n") == 0) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 4. 检查状态码 101 */
|
||||
if (strstr(resp, " 101 ") == NULL) {
|
||||
transport->close(transport->ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ws->connected = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ha_ws_send_text(ha_ws_t *ws, const char *text) {
|
||||
if (!ws->connected) return -1;
|
||||
return ha_ws_send_frame(ws, WS_OPCODE_TEXT, (const uint8_t *)text, (int)strlen(text));
|
||||
}
|
||||
|
||||
int ha_ws_send_binary(ha_ws_t *ws, const uint8_t *data, int len) {
|
||||
if (!ws->connected) return -1;
|
||||
return ha_ws_send_frame(ws, WS_OPCODE_BINARY, data, len);
|
||||
}
|
||||
|
||||
int ha_ws_send_ping(ha_ws_t *ws) {
|
||||
if (!ws->connected) return -1;
|
||||
return ha_ws_send_frame(ws, WS_OPCODE_PING, NULL, 0);
|
||||
}
|
||||
|
||||
int ha_ws_read_frame(ha_ws_t *ws, const uint8_t **payload, int *len) {
|
||||
if (!ws->connected) return -1;
|
||||
|
||||
*payload = NULL;
|
||||
*len = 0;
|
||||
|
||||
/* 读取帧头:2 字节 */
|
||||
uint8_t hdr[2];
|
||||
if (recv_all(ws, hdr, 2) != 0) {
|
||||
ws->connected = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int opcode = hdr[0] & 0x0F;
|
||||
int masked = (hdr[1] & 0x80) ? 1 : 0;
|
||||
uint64_t frame_len = hdr[1] & 0x7F;
|
||||
|
||||
if (frame_len == 126) {
|
||||
uint8_t ext[2];
|
||||
if (recv_all(ws, ext, 2) != 0) { ws->connected = 0; return -1; }
|
||||
frame_len = ((uint64_t)ext[0] << 8) | ext[1];
|
||||
} else if (frame_len == 127) {
|
||||
uint8_t ext[8];
|
||||
if (recv_all(ws, ext, 8) != 0) { ws->connected = 0; return -1; }
|
||||
frame_len = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
frame_len = (frame_len << 8) | ext[i];
|
||||
}
|
||||
}
|
||||
|
||||
/* 读取 mask key */
|
||||
uint8_t mask_key[4] = {0, 0, 0, 0};
|
||||
if (masked) {
|
||||
if (recv_all(ws, mask_key, 4) != 0) { ws->connected = 0; return -1; }
|
||||
}
|
||||
|
||||
/* 限制帧大小 */
|
||||
if (frame_len > sizeof(ws->read_buf)) {
|
||||
/* 帧太大,跳过 payload */
|
||||
uint64_t skip = frame_len;
|
||||
uint8_t tmp[256];
|
||||
while (skip > 0) {
|
||||
int to_skip = (skip > sizeof(tmp)) ? (int)sizeof(tmp) : (int)skip;
|
||||
if (recv_all(ws, tmp, to_skip) != 0) { ws->connected = 0; return -1; }
|
||||
skip -= to_skip;
|
||||
}
|
||||
return -1; /* 返回错误,帧太大 */
|
||||
}
|
||||
|
||||
/* 读取 payload */
|
||||
if (frame_len > 0) {
|
||||
if (recv_all(ws, ws->read_buf, (int)frame_len) != 0) {
|
||||
ws->connected = 0;
|
||||
return -1;
|
||||
}
|
||||
/* 如果有 mask,解掩码 */
|
||||
if (masked) {
|
||||
for (uint64_t i = 0; i < frame_len; i++) {
|
||||
ws->read_buf[i] ^= mask_key[i & 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*payload = ws->read_buf;
|
||||
*len = (int)frame_len;
|
||||
|
||||
switch (opcode) {
|
||||
case WS_OPCODE_CLOSE:
|
||||
ws->connected = 0;
|
||||
return WS_OPCODE_CLOSE;
|
||||
case WS_OPCODE_PING:
|
||||
return WS_OPCODE_PING;
|
||||
case WS_OPCODE_PONG:
|
||||
return WS_OPCODE_PONG;
|
||||
case WS_OPCODE_TEXT:
|
||||
case WS_OPCODE_BINARY:
|
||||
return opcode;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void ha_ws_close(ha_ws_t *ws) {
|
||||
if (ws->connected) {
|
||||
ha_ws_send_frame(ws, WS_OPCODE_CLOSE, NULL, 0);
|
||||
ws->connected = 0;
|
||||
}
|
||||
ws->transport->close(ws->transport->ctx);
|
||||
}
|
||||
62
remotedevice/src/ha_ws.h
Normal file
62
remotedevice/src/ha_ws.h
Normal file
@ -0,0 +1,62 @@
|
||||
#ifndef HA_WS_H
|
||||
#define HA_WS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "../include/ha_remotedevice.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ======================== WS 帧类型 ======================== */
|
||||
#define WS_OPCODE_CONTINUATION 0x0
|
||||
#define WS_OPCODE_TEXT 0x1
|
||||
#define WS_OPCODE_BINARY 0x2
|
||||
#define WS_OPCODE_CLOSE 0x8
|
||||
#define WS_OPCODE_PING 0x9
|
||||
#define WS_OPCODE_PONG 0xA
|
||||
|
||||
/* ======================== WS 连接 ======================== */
|
||||
typedef struct {
|
||||
ha_transport_t *transport; /* 用户实现的传输层 */
|
||||
int connected; /* 是否已连接 */
|
||||
uint8_t read_buf[8192]; /* 读缓冲区 */
|
||||
int read_pos; /* 缓冲区中有效数据起始位置 */
|
||||
int read_len; /* 缓冲区中有效数据长度 */
|
||||
char host[256]; /* 缓存目标地址 */
|
||||
uint16_t port;
|
||||
char path[256];
|
||||
char token[256];
|
||||
} ha_ws_t;
|
||||
|
||||
/* 创建 WS 连接。返回 0 成功,非 0 失败。 */
|
||||
int ha_ws_connect(ha_ws_t *ws, ha_transport_t *transport,
|
||||
const char *host, uint16_t port,
|
||||
const char *path, const char *token);
|
||||
|
||||
/* 发送文本帧。返回 0 成功。 */
|
||||
int ha_ws_send_text(ha_ws_t *ws, const char *text);
|
||||
|
||||
/* 发送二进制帧。返回 0 成功。 */
|
||||
int ha_ws_send_binary(ha_ws_t *ws, const uint8_t *data, int len);
|
||||
|
||||
/* 发送 ping。返回 0 成功。 */
|
||||
int ha_ws_send_ping(ha_ws_t *ws);
|
||||
|
||||
/* 读取一帧。
|
||||
* 返回 opcode (0x1/0x2/0x8/0x9/0xA),-1 表示关闭或错误。
|
||||
* payload 和 len 指向内部缓冲区,在下次调用前有效。 */
|
||||
int ha_ws_read_frame(ha_ws_t *ws, const uint8_t **payload, int *len);
|
||||
|
||||
/* 发送原始 WS 帧(内部使用,用于回复 ping) */
|
||||
int ha_ws_send_frame(ha_ws_t *ws, int opcode, const uint8_t *payload, int len);
|
||||
|
||||
/* 关闭 WS 连接 */
|
||||
void ha_ws_close(ha_ws_t *ws);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_WS_H */
|
||||
1509
remotedevice/test/test_ha_remotedevice.c
Normal file
1509
remotedevice/test/test_ha_remotedevice.c
Normal file
File diff suppressed because it is too large
Load Diff
@ -147,6 +147,17 @@ type EventSubscriber interface {
|
||||
Subscribe(eventType EventType, handler EventHandler) func()
|
||||
}
|
||||
|
||||
// PluginMgrAPI 提供插件管理能力(外部插件可调用)。
|
||||
// 由 bridge 注入 dispatch 实现,走 C ABI CORE_PLUGIN_RELOAD_ONE 等。
|
||||
type PluginMgrAPI interface {
|
||||
// ReloadOne 重载单个插件(停止后重新加载)。
|
||||
ReloadOne(name string) error
|
||||
// ListLoadedPlugins 列出已加载插件。
|
||||
ListLoadedPlugins() []string
|
||||
// IsPluginDisabled 查询插件是否被禁用。
|
||||
IsPluginDisabled(name string) bool
|
||||
}
|
||||
|
||||
// StageScope controls which events a stage handler receives.
|
||||
type StageScope int
|
||||
|
||||
@ -200,6 +211,7 @@ type PluginSDK struct {
|
||||
sett SettingsAPI
|
||||
social SocialAPI
|
||||
events EventSubscriber
|
||||
plgMgr PluginMgrAPI
|
||||
|
||||
autoRestart bool
|
||||
|
||||
@ -347,6 +359,13 @@ func (s *PluginSDK) SetLLMAPI(llm LLMAPI) { s.llm = llm }
|
||||
func (s *PluginSDK) SetSocialAPI(social SocialAPI) { s.social = social }
|
||||
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber) { s.events = es }
|
||||
|
||||
// SetPluginMgrAPI sets the plugin manager API (called by the bridge at startup).
|
||||
func (s *PluginSDK) SetPluginMgrAPI(pm PluginMgrAPI) { s.plgMgr = pm }
|
||||
|
||||
// PluginMgr returns the plugin manager API (ReloadOne / ReloadPlugins / list).
|
||||
// May be nil if the host did not wire it.
|
||||
func (s *PluginSDK) PluginMgr() PluginMgrAPI { return s.plgMgr }
|
||||
|
||||
// ---- IO Convenience Methods ----
|
||||
|
||||
// InjectInterruptText injects a text interrupt that can preempt current LLM processing.
|
||||
|
||||
@ -69,18 +69,45 @@ type TemplateData struct {
|
||||
|
||||
func cmdInit(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Usage: plugindev init <name> [--lua]")
|
||||
fmt.Println("Usage: plugindev init <name> [--lua] [--type remotedevice]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
isLua := false
|
||||
isRemoteDevice := false
|
||||
for _, a := range args[1:] {
|
||||
switch a {
|
||||
case "--lua":
|
||||
isLua = true
|
||||
case "--type", "-t":
|
||||
// handled in next iteration
|
||||
}
|
||||
}
|
||||
// also check --type remotedevice as a single arg
|
||||
for i, a := range args[1:] {
|
||||
if a == "--type" || a == "-t" {
|
||||
if i+1 < len(args[1:]) {
|
||||
if args[1:][i+1] == "remotedevice" {
|
||||
isRemoteDevice = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if a == "--type=remotedevice" || a == "-t=remotedevice" {
|
||||
isRemoteDevice = true
|
||||
}
|
||||
}
|
||||
|
||||
if isRemoteDevice && isLua {
|
||||
fmt.Println("error: --type remotedevice and --lua are mutually exclusive")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Remote device projects use different scaffold
|
||||
if isRemoteDevice {
|
||||
scaffoldRemoteDevice(name)
|
||||
return
|
||||
}
|
||||
|
||||
dir := name
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
@ -192,6 +219,57 @@ func detectSDKInfo() (modulePath, goVersion, sdkPath, sdkVersion string) {
|
||||
return modulePath, goVersion, root, sdkVersion
|
||||
}
|
||||
|
||||
// scaffoldRemoteDevice 创建远程设备适配器项目脚手架
|
||||
func scaffoldRemoteDevice(name string) {
|
||||
dir := name
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
fmt.Printf("error: directory %q already exists\n", dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
nameEn := strings.Title(strings.ReplaceAll(name, "-", " "))
|
||||
|
||||
data := TemplateData{
|
||||
Plg: PlgConfig{
|
||||
Name: name,
|
||||
NameZh: "中文名",
|
||||
NameEn: nameEn,
|
||||
Version: "0.1.0",
|
||||
Description: name + " remote device adapter",
|
||||
Author: "HomeAgent",
|
||||
Entry: name,
|
||||
Tags: []string{name, "remotedevice"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
fmt.Printf("error: create dir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 写入 main.c
|
||||
writeTemplate(filepath.Join(dir, "main.c"), tmplRemoteDeviceMain, data)
|
||||
|
||||
// 写入 CMakeLists.txt
|
||||
writeTemplate(filepath.Join(dir, "CMakeLists.txt"), tmplRemoteDeviceCMake, data)
|
||||
|
||||
// 创建 SDK 目录(symlink/copy)
|
||||
sdkSrc := filepath.Join("..", "remotedevice")
|
||||
sdkDst := filepath.Join(dir, "ha_remotedevice")
|
||||
if _, err := os.Stat(sdkDst); os.IsNotExist(err) {
|
||||
// 尝试创建符号链接,失败则提示
|
||||
if err := os.Symlink(sdkSrc, sdkDst); err != nil {
|
||||
fmt.Printf(" note: could not create symlink to SDK, copy manually:\n")
|
||||
fmt.Printf(" cp -r %s %s\n", sdkSrc, sdkDst)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Created remote device adapter project %q\n", dir)
|
||||
fmt.Printf(" cd %s && mkdir build && cd build && cmake .. && make\n", dir)
|
||||
fmt.Printf(" Or include as subdirectory in your project:\n")
|
||||
fmt.Printf(" add_subdirectory(%s)\n", dir)
|
||||
}
|
||||
|
||||
func writeTemplate(path, content string, data TemplateData) {
|
||||
tmpl, err := template.New("").Parse(content)
|
||||
if err != nil {
|
||||
|
||||
@ -30,15 +30,20 @@ func help() {
|
||||
fmt.Print(`HomeAgent Plugin Dev Tool
|
||||
|
||||
Usage:
|
||||
plugindev init <name> Scaffold a new plugin project
|
||||
plugindev build [flags] Compile and package plugin
|
||||
plugindev clean Clean build/dist artifacts
|
||||
plugindev debug [dir] Interpret and debug plugin source
|
||||
plugindev sdk <command> Manage SDK versions
|
||||
plugindev init <name> Scaffold a new plugin project
|
||||
plugindev init <name> --lua Create Lua plugin
|
||||
plugindev init <name> --type remotedevice
|
||||
Create C remote device adapter
|
||||
plugindev build [flags] Compile and package plugin
|
||||
plugindev clean Clean build/dist artifacts
|
||||
plugindev debug [dir] Interpret and debug plugin source
|
||||
plugindev sdk <command> Manage SDK versions
|
||||
|
||||
Flags:
|
||||
--outdir Output directory (default: dist)
|
||||
--target Target OS/arch (e.g. linux/amd64), repeatable
|
||||
--lua Create Lua plugin (for init)
|
||||
--type Project type: "remotedevice" (for init)
|
||||
-t Alias for --type
|
||||
`)
|
||||
}
|
||||
|
||||
@ -405,6 +405,10 @@ enum {
|
||||
CORE_SETTINGS_DUMP = 44,
|
||||
CORE_SETTINGS_PLUGINS = 45,
|
||||
CORE_REGISTER_INPUT_CH = 46,
|
||||
CORE_INJECT_INPUT_SYNC = 47,
|
||||
CORE_PLUGIN_RELOAD_ONE = 48,
|
||||
CORE_PLUGIN_LIST_LOADED = 49,
|
||||
CORE_PLUGIN_IS_DISABLED = 50,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -519,6 +523,7 @@ func buildPluginSDK(name string) *sdk.PluginSDK {
|
||||
base.SetLLMAPI(dispatchLLM{})
|
||||
base.SetSocialAPI(dispatchSocial{})
|
||||
base.SetTextMemoryAPI(dispatchTextMemory{})
|
||||
base.SetPluginMgrAPI(dispatchPluginMgr{})
|
||||
base.SetInputChannelRegistrar(
|
||||
func(name string, def sdk.ChannelDef) error {
|
||||
defJSON, _ := json.Marshal(def)
|
||||
@ -577,6 +582,31 @@ func (dispatchSocial) ListPersons() ([]string, error) { r, e := callString(40, "
|
||||
type dispatchTextMemory struct{}
|
||||
func (dispatchTextMemory) Append(evt sdk.TextEvent) error { b, _ := json.Marshal(evt); return callVoid(41, string(b), "", "", 0, 0) }
|
||||
|
||||
// ---- dispatchPluginMgr (CORE_PLUGIN_RELOAD_ONE = 48) ----
|
||||
|
||||
type dispatchPluginMgr struct{}
|
||||
|
||||
func (dispatchPluginMgr) ReloadOne(name string) error {
|
||||
return callVoid(48, name, "", "", 0, 0)
|
||||
}
|
||||
|
||||
func (dispatchPluginMgr) ListLoadedPlugins() []string {
|
||||
r, e := callString(49, "", "", "", 0, 0)
|
||||
if e != nil || r == "" {
|
||||
return nil
|
||||
}
|
||||
var list []string
|
||||
if json.Unmarshal([]byte(r), &list) != nil {
|
||||
return nil
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (dispatchPluginMgr) IsPluginDisabled(name string) bool {
|
||||
r, e := callString(50, name, "", "", 0, 0)
|
||||
return e == nil && r == "1"
|
||||
}
|
||||
|
||||
// ---- dispatchSettings (inline) ----
|
||||
|
||||
type dispatchSettings struct{}
|
||||
@ -831,6 +861,353 @@ PluginAPI* plugin_init(void) {
|
||||
}
|
||||
`
|
||||
|
||||
// ============================================================
|
||||
// Remote Device Adapter Templates
|
||||
// ============================================================
|
||||
|
||||
const tmplRemoteDeviceMain = `#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "ha_remotedevice.h"
|
||||
|
||||
/* ============================================================
|
||||
* {{.Plg.Name}} — Remote Device Adapter
|
||||
*
|
||||
* 声明式远程设备接入示例。
|
||||
* 用户只需实现:
|
||||
* 1. ha_transport_t 的 4 个函数
|
||||
* 2. 声明 handlers 表(设备支持哪些命令 + 对应的处理函数)
|
||||
* 其余协议细节(WS 握手、hello/bind、心跳、重连、命令分发、结果回执)由 SDK 自动处理。
|
||||
* ============================================================ */
|
||||
|
||||
/* ====================== 传输层实现 ======================
|
||||
*
|
||||
* 请为你的平台实现以下 4 个函数:
|
||||
* connect(ctx, host, port) — 建立 TCP 连接
|
||||
* send(ctx, data, len) — 发送数据
|
||||
* recv(ctx, buf, len) — 接收数据(阻塞,返回实际接收字节数)
|
||||
* close(ctx) — 关闭连接
|
||||
*
|
||||
* 示例:POSIX socket 实现
|
||||
*/
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
/* Windows 平台需包含 winsock2.h */
|
||||
#error "Please implement transport for your platform (see example below)"
|
||||
#else
|
||||
/* POSIX (Linux, macOS, ESP-IDF, Zephyr, etc.) */
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct transport_ctx {
|
||||
int sock;
|
||||
};
|
||||
|
||||
static int transport_connect(void *ctx, const char *host, uint16_t port) {
|
||||
struct transport_ctx *tc = (struct transport_ctx *)ctx;
|
||||
struct hostent *he = gethostbyname(host);
|
||||
if (!he) return -1;
|
||||
tc->sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (tc->sock < 0) return -1;
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
|
||||
if (connect(tc->sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
close(tc->sock);
|
||||
tc->sock = -1;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int transport_send(void *ctx, const uint8_t *data, int len) {
|
||||
struct transport_ctx *tc = (struct transport_ctx *)ctx;
|
||||
int sent = 0;
|
||||
while (sent < len) {
|
||||
int n = (int)send(tc->sock, data + sent, len - sent, 0);
|
||||
if (n <= 0) return -1;
|
||||
sent += n;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
static int transport_recv(void *ctx, uint8_t *buf, int len) {
|
||||
struct transport_ctx *tc = (struct transport_ctx *)ctx;
|
||||
int n = (int)recv(tc->sock, buf, len, 0);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void transport_close(void *ctx) {
|
||||
struct transport_ctx *tc = (struct transport_ctx *)ctx;
|
||||
if (tc->sock >= 0) {
|
||||
close(tc->sock);
|
||||
tc->sock = -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ====================== 声明式命令处理 ======================
|
||||
*
|
||||
* 每个命令对应一个处理函数,通过填写 ha_cmd_result_t 返回数据。
|
||||
* SDK 自动回执结果,无需手动调用 send_result。
|
||||
*
|
||||
* 返回方式:
|
||||
* 1. 文本输出:填写 result->output
|
||||
* 2. 二进制数据:设置 result->has_binary=1 并填写 binary_data/len/mime
|
||||
* 3. 错误:设置 result->status=1 并填写 result->error
|
||||
* 4. 返回 HA_OK 表示处理成功,其他值表示处理失败
|
||||
*/
|
||||
|
||||
/* ESP32-CAM 摄像头处理 */
|
||||
static ha_status_t handle_camerasue(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
int duration = 0;
|
||||
if (args && args[0]) duration = atoi(args);
|
||||
printf("[camera] %s (duration=%ds)\n", duration ? "record" : "snapshot", duration);
|
||||
|
||||
/* 返回文本结果(base64 图片) */
|
||||
result->status = 0;
|
||||
result->output = "data:image/jpeg;base64,/9j/4AAQ...";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 屏幕截图处理 */
|
||||
static ha_status_t handle_screensee(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)args; (void)userdata;
|
||||
printf("[screen] screenshot\n");
|
||||
result->status = 0;
|
||||
result->output = "data:image/png;base64,iVBORw0KGgo...";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 语音播报处理 */
|
||||
static ha_status_t handle_speakeruse(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
printf("[speaker] TTS: %s\n", args ? args : "");
|
||||
result->status = 0;
|
||||
result->output = "speakeruse done";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 远程操控处理(computeruse) */
|
||||
static ha_status_t handle_computeruse(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
const char *action = NULL;
|
||||
const char *json_str = NULL;
|
||||
ha_cmd_parse_json(args, &action, &json_str);
|
||||
printf("[computeruse] action=%s\n", action ? action : "unknown");
|
||||
result->status = 0;
|
||||
result->output = "computeruse done";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 剪贴板读取 */
|
||||
static ha_status_t handle_clipboardsee(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)args; (void)userdata;
|
||||
result->status = 0;
|
||||
result->output = "clipboard content";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 剪贴板写入 */
|
||||
static ha_status_t handle_clipboardsue(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
printf("[clipboard] write: %s\n", args ? args : "");
|
||||
result->status = 0;
|
||||
result->output = "clipboard written";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 屏幕显示 */
|
||||
static ha_status_t handle_screensue(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
printf("[screensue] show: %s\n", args ? args : "");
|
||||
result->status = 0;
|
||||
result->output = "screensue shown";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* Shell 命令处理 */
|
||||
static ha_status_t handle_shell(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)userdata;
|
||||
printf("[shell] cmd: %s\n", args ? args : "");
|
||||
result->status = 0;
|
||||
result->output = "shell output";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* 设备信息查询 */
|
||||
static ha_status_t handle_deviceinfo(const char *req_id, const char *args,
|
||||
ha_cmd_result_t *result, void *userdata) {
|
||||
(void)req_id; (void)args; (void)userdata;
|
||||
result->status = 0;
|
||||
result->output = "{\"platform\":\"linux\",\"arch\":\"x86_64\"}";
|
||||
return HA_OK;
|
||||
}
|
||||
|
||||
/* ====================== 连接状态回调 ====================== */
|
||||
|
||||
static void on_state(int connected, void *userdata) {
|
||||
(void)userdata;
|
||||
printf("[devicelink] state: %s\n", connected ? "connected" : "disconnected");
|
||||
}
|
||||
|
||||
/* ====================== 主函数 ====================== */
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
/* 传输层上下文 */
|
||||
struct transport_ctx tctx;
|
||||
tctx.sock = -1;
|
||||
|
||||
ha_transport_t transport = {
|
||||
.connect = transport_connect,
|
||||
.send = transport_send,
|
||||
.recv = transport_recv,
|
||||
.close = transport_close,
|
||||
.ctx = &tctx,
|
||||
};
|
||||
|
||||
/* ===== 声明式设备配置 ===== */
|
||||
|
||||
/* 声明设备能力 */
|
||||
const char *caps[] = {
|
||||
"status", "cmdrun", "deviceinfo",
|
||||
"camerasue", "screensee", "speakeruse",
|
||||
"computeruse", "clipboardsee", "clipboardsue",
|
||||
"screensue",
|
||||
NULL
|
||||
};
|
||||
|
||||
/* 声明命令处理表:设备支持哪些命令,以及对应的处理函数 */
|
||||
ha_cmd_handler_def_t handlers[] = {
|
||||
{.command = "shell", .handler = handle_shell},
|
||||
{.command = "camerasue", .handler = handle_camerasue},
|
||||
{.command = "screensee", .handler = handle_screensee},
|
||||
{.command = "speakeruse", .handler = handle_speakeruse},
|
||||
{.command = "computeruse", .handler = handle_computeruse},
|
||||
{.command = "clipboardsee", .handler = handle_clipboardsee},
|
||||
{.command = "clipboardsue", .handler = handle_clipboardsue},
|
||||
{.command = "screensue", .handler = handle_screensue},
|
||||
{.command = "deviceinfo", .handler = handle_deviceinfo},
|
||||
{.command = NULL}, /* 标记结束 */
|
||||
};
|
||||
|
||||
ha_config_t config = {
|
||||
.transport = transport,
|
||||
.server = "127.0.0.1:9890",
|
||||
.token = "your-token-here",
|
||||
.device = {
|
||||
.device_id = "{{.Plg.Name}}",
|
||||
.name = "{{.Plg.NameEn}}",
|
||||
.kind = "computer",
|
||||
.caps = caps,
|
||||
.info_json = "{\"platform\":\"linux\",\"arch\":\"x86_64\"}",
|
||||
},
|
||||
.handlers = handlers, /* 声明式命令处理表 */
|
||||
.on_state = on_state,
|
||||
.ping_interval = 30,
|
||||
};
|
||||
|
||||
ha_client_t *client = ha_client_new(&config);
|
||||
if (!client) {
|
||||
fprintf(stderr, "Failed to create client\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Starting remote device adapter: {{.Plg.Name}}\n");
|
||||
printf(" Server: %s\n", config.server);
|
||||
printf(" Device ID: %s\n", config.device.device_id);
|
||||
printf(" Kind: %s\n", config.device.kind);
|
||||
printf(" Caps: ");
|
||||
for (const char **p = caps; *p; p++) printf("%s ", *p);
|
||||
printf("\n");
|
||||
|
||||
ha_status_t st = ha_client_start(client);
|
||||
if (st != HA_OK) {
|
||||
fprintf(stderr, "Failed to connect: %d\n", st);
|
||||
ha_client_destroy(client);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Connected! Entering main loop...\n");
|
||||
|
||||
/* 主循环 */
|
||||
while (1) {
|
||||
ha_status_t st = ha_client_process(client);
|
||||
if (st == HA_ERR_DISCONNECTED) {
|
||||
printf("Disconnected, exiting.\n");
|
||||
break;
|
||||
}
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
Sleep(10);
|
||||
#else
|
||||
usleep(10000);
|
||||
#endif
|
||||
}
|
||||
|
||||
ha_client_stop(client);
|
||||
ha_client_destroy(client);
|
||||
return 0;
|
||||
}
|
||||
`
|
||||
|
||||
const tmplRemoteDeviceCMake = `cmake_minimum_required(VERSION 3.10)
|
||||
project({{.Plg.Name}} VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
# ============================================================
|
||||
# {{.Plg.Name}} — Remote Device Adapter
|
||||
# ============================================================
|
||||
|
||||
# 设置 SDK 路径(默认使用内置 SDK,也可通过 -DSDK_PATH=... 指定)
|
||||
set(SDK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ha_remotedevice"
|
||||
CACHE PATH "Path to ha_remotedevice SDK")
|
||||
|
||||
# 添加 SDK 子目录
|
||||
if(EXISTS "${SDK_PATH}/CMakeLists.txt")
|
||||
add_subdirectory(${SDK_PATH} ha_remotedevice)
|
||||
else()
|
||||
message(FATAL_ERROR "ha_remotedevice SDK not found at ${SDK_PATH}")
|
||||
endif()
|
||||
|
||||
# 创建设备适配器可执行文件
|
||||
add_executable(${PROJECT_NAME}
|
||||
main.c
|
||||
)
|
||||
|
||||
# 链接 SDK
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ha_remotedevice)
|
||||
|
||||
# 包含 SDK 头文件
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
${HA_REMOTEDEVICE_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
# 编译选项
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE
|
||||
-Wall -Wextra -Wpedantic
|
||||
-Wno-unused-parameter
|
||||
)
|
||||
endif()
|
||||
|
||||
# 安装
|
||||
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
|
||||
`
|
||||
|
||||
const tmplReadme = `# {{.Plg.Name}}
|
||||
|
||||
{{.Plg.Description}}
|
||||
|
||||
Reference in New Issue
Block a user