Watcher 功能模块开发指南
本文档由 AI 翻译。如您发现内容有误或有改进建议,欢迎通过页面下方的评论区,或在以下 Issue 页面中告诉我们:https://github.com/Seeed-Studio/wiki-documents/issues
建议您首先阅读 Watcher 软件框架,以了解功能模块的工作原理。
在本文档中,我们将逐步指导您如何开发一个新的功能模块。我们将以 UART Alarm
模块为例。
1. 安装和首次构建
请先完成 构建 Watcher 开发环境 中的步骤,如果您已经跳过了这些步骤。
# 您当前位于 PROJ_ROOT_DIR/examples/factory_firmware/
cd main/task_flow_module
2. 选择合适的模板
在 Watcher 软件框架 中,我们介绍了现有的功能模块(在本文档中简称为 FM)及其用途。当我们开发一个新的 FM 时,最好从一个最接近的现有 FM 开始作为参考。在本教程中,我们将开发一个报警器 FM,因此我们选择一个报警器 FM,local alarmer
是最简单的,我们将以它为例。
cp tf_module_local_alarm.h tf_module_uart_alarm.h
cp tf_module_local_alarm.c tf_module_uart_alarm.c
文件的名称并不重要,任何 .h
和 .c
文件都会被构建系统扫描并纳入编译代码树。但仍然建议使用有意义的文件名。
3. 实现注册
TFE(任务流引擎)提供了一个 API 函数来注册新的 FM。
esp_err_t tf_module_register(const char *p_name,
const char *p_desc,
const char *p_version,
tf_module_mgmt_t *mgmt_handle);
前三个参数分别是 FM 的名称、描述和版本,它们目前主要用于内部,例如从注册表中匹配 FM、日志打印等,但未来会用于 FM 与本地服务通信。
// 在 tf_module_uart_alarm.h 中
#define TF_MODULE_UART_ALARM_NAME "uart alarm"
#define TF_MODULE_UART_ALARM_VERSION "1.0.0"
#define TF_MODULE_UART_ALARM_DESC "uart alarm function module"
// 在 tf_module_uart_alarm.c 中
esp_err_t tf_module_uart_alarm_register(void)
{
return tf_module_register(TF_MODULE_UART_ALARM_NAME,
TF_MODULE_UART_ALARM_DESC,
TF_MODULE_UART_ALARM_VERSION,
&__g_module_management);
}
第四个参数是一个结构体,包含管理此 FM 生命周期所需的 API 函数。
// 在 tf_module.h 中
typedef struct tf_module_mgmt {
tf_module_t *(*tf_module_instance)(void);
void (*tf_module_destroy)(tf_module_t *p_module);
}tf_module_mgmt_t;
tf_module_instance
是一个函数,当引擎初始化任务流中指定的所有 FM 时会调用它,基本上这意味着引擎刚刚收到任务流创建请求并开始流。tf_module_destroy
是一个函数,当引擎停止任务流时会调用它。
3.1 实例化
tf_module_t *tf_module_uart_alarm_instance(void)
{
tf_module_uart_alarm_t *p_module_ins = (tf_module_uart_alarm_t *) tf_malloc(sizeof(tf_module_uart_alarm_t));
if (p_module_ins == NULL)
{
return NULL;
}
p_module_ins->module_base.p_module = p_module_ins;
p_module_ins->module_base.ops = &__g_module_ops;
if (atomic_fetch_add(&g_ins_cnt, 1) == 0) {
// 第一次实例化时,我们需要初始化硬件
esp_err_t ret;
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
const int buffer_size = 2 * 1024;
ESP_GOTO_ON_ERROR(uart_param_config(UART_NUM_2, &uart_config), err, TAG, "uart_param_config 失败");
ESP_GOTO_ON_ERROR(uart_set_pin(UART_NUM_2, GPIO_NUM_19, GPIO_NUM_20, -1, -1), err, TAG, "uart_set_pin 失败");
ESP_GOTO_ON_ERROR(uart_driver_install(UART_NUM_2, buffer_size, buffer_size, 0, NULL, ESP_INTR_FLAG_SHARED), err, TAG, "uart_driver_install 失败");
}
return &p_module_ins->module_base;
err:
free(p_module_ins);
return NULL;
}
以上是我们对 instance
函数的实现。它为一个结构体 tf_module_uart_alarm_t
分配内存,该结构体用于保存此 FM 的参数,就像 C++ 类的成员一样。在结构体 tf_module_uart_alarm_t
中,第一个字段非常重要——tf_module_t module_base
,从 C++ 编程的角度来看,tf_module_t
是所有 FM 的父类。instance
函数只是向 TFE 提供一个指向 tf_module_t
结构体的指针。
// 在 tf_module_uart_alarm.h 中
typedef struct {
tf_module_t module_base;
int input_evt_id; // 这也可以是模块实例 ID
int output_format; // 默认值为 0,详见上方注释
bool include_big_image; // 默认值:false
bool include_small_image; // 默认值:false
bool include_boxes; // 默认值:false,功能即将推出
} tf_module_uart_alarm_t;
// 在 tf_module_uart_alarm.c 中
tf_module_t *tf_module_uart_alarm_instance(void)
{
...
return &p_module_ins->module_base;
...
}
tf_module_t
的两个成员必须被赋值。
// 在 tf_module_uart_alarm.c 中
tf_module_t *tf_module_uart_alarm_instance(void)
{
...
p_module_ins->module_base.p_module = p_module_ins;
p_module_ins->module_base.ops = &__g_module_ops;
p_module
- 一个指针,指向 FM 实例本身,用于 destroy
函数获取实例的句柄并释放其内存。
ops
- 一个结构体,包含由 TFE 操作 FM 的 API 函数,我们稍后会讨论这一点。
实例函数的其余部分是用于初始化硬件以及与您的 FM(功能模块)逻辑相关的内容。
需要提到的一点是,FM 可能会被多次实例化。您需要处理 instance
函数的重新进入逻辑,如果您的 FM 不支持多实例化,则需要在第二次调用 instance
函数时返回一个 NULL 指针。
在这个 uart alarmer
示例中,我们将使用引用计数器来处理重新进入的逻辑。
if (atomic_fetch_add(&g_ins_cnt, 1) == 0) {
// 第一次实例化时,我们需要初始化硬件
esp_err_t ret;
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
const int buffer_size = 2 * 1024;
ESP_GOTO_ON_ERROR(uart_param_config(UART_NUM_2, &uart_config), err, TAG, "uart_param_config 失败");
ESP_GOTO_ON_ERROR(uart_set_pin(UART_NUM_2, GPIO_NUM_19, GPIO_NUM_20, -1, -1), err, TAG, "uart_set_pin 失败");
ESP_GOTO_ON_ERROR(uart_driver_install(UART_NUM_2, buffer_size, buffer_size, 0, NULL, ESP_INTR_FLAG_SHARED), err, TAG, "uart_driver_install 失败");
}
3.2 销毁
void tf_module_uart_alarm_destroy(tf_module_t *p_module_base)
{
if (p_module_base) {
if (atomic_fetch_sub(&g_ins_cnt, 1) <= 1) {
// 这是最后一次销毁调用,反初始化 UART
uart_driver_delete(UART_NUM_2);
ESP_LOGI(TAG, "UART 驱动已删除。");
}
if (p_module_base->p_module) {
free(p_module_base->p_module);
}
}
}
destroy
通常很简单 😂 我们只需要释放内存,并在必要时反初始化硬件。
4. 实现操作
父类的 ops
成员定义如下:
struct tf_module_ops
{
int (*start)(void *p_module);
int (*stop)(void *p_module);
int (*cfg)(void *p_module, cJSON *p_json);
int (*msgs_sub_set)(void *p_module, int evt_id);
int (*msgs_pub_set)(void *p_module, int output_index, int *p_evt_id, int num);
};
当 TFE 初始化 FM 时,它会按以下顺序调用这些函数:cfg
-> msgs_sub_set
-> msgs_pub_set
-> start
-> stop
。
cfg
- 从任务流 JSON 中获取参数,使用这些参数配置您的 FM。msgs_sub_set
- 通过向上游 FM 的事件 ID 注册事件处理程序来创建与上游 FM 的连接。输入参数evt_id
是由 TFE 从任务流 JSON 中提取并准备的。第一个参数p_module
是 FM 实例本身的指针。msgs_pub_set
- 存储与下游 FM 的连接。如果此 FM 没有输出,我们可以将此函数留空。第一个参数p_module
是 FM 实例本身的指针。第二个参数output_index
是端口号,例如此 FM 有两个输出,msgs_pub_set
将被调用两次,output_index
分别为 0 和 1。第三个参数p_evt_id
是一个指向数组的指针,该数组包含此端口上所有下游 FM 的事件 ID,数组的大小由最后一个参数num
指定。start
和stop
- 顾名思义,它们分别启动和停止 FM。它们都接收p_module
作为参数,即 FM 实例本身的指针。
4.1 配置(cfg)
static int __cfg(void *p_module, cJSON *p_json)
{
tf_module_uart_alarm_t *p_module_ins = (tf_module_uart_alarm_t *)p_module;
cJSON *output_format = cJSON_GetObjectItem(p_json, "output_format");
if (output_format == NULL || !cJSON_IsNumber(output_format))
{
ESP_LOGE(TAG, "参数 output_format 缺失,默认值为 0(二进制输出)");
p_module_ins->output_format = 0;
} else {
ESP_LOGI(TAG, "参数 output_format=%d", output_format->valueint);
p_module_ins->output_format = output_format->valueint;
}
cJSON *include_big_image = cJSON_GetObjectItem(p_json, "include_big_image");
if (include_big_image == NULL || !cJSON_IsBool(include_big_image))
{
ESP_LOGE(TAG, "参数 include_big_image 缺失,默认值为 false");
p_module_ins->include_big_image = false;
} else {
ESP_LOGI(TAG, "参数 include_big_image=%s", cJSON_IsTrue(include_big_image)?"true":"false");
p_module_ins->include_big_image = cJSON_IsTrue(include_big_image);
}
cJSON *include_small_image = cJSON_GetObjectItem(p_json, "include_small_image");
if (include_small_image == NULL || !cJSON_IsBool(include_small_image))
{
ESP_LOGE(TAG, "参数 include_small_image 缺失,默认值为 false");
p_module_ins->include_small_image = false;
} else {
ESP_LOGI(TAG, "参数 include_small_image=%s", cJSON_IsTrue(include_small_image)?"true":"false");
p_module_ins->include_small_image = cJSON_IsTrue(include_small_image);
}
cJSON *include_boxes = cJSON_GetObjectItem(p_json, "include_boxes");
if (include_boxes == NULL || !cJSON_IsBool(include_boxes))
{
ESP_LOGE(TAG, "参数 include_boxes 缺失,默认值为 false");
p_module_ins->include_boxes = false;
} else {
ESP_LOGI(TAG, "参数 include_boxes=%s", cJSON_IsTrue(include_boxes)?"true":"false");
p_module_ins->include_boxes = cJSON_IsTrue(include_boxes);
}
return 0;
}
如您所见,cfg
函数只是从 cJSON 对象中提取字段值,该对象来自任务流中 FM 对象的 params
字段。例如,以下是一个包含 uart alarmer
FM 的简单任务流:
{
"tlid": 3,
"ctd": 3,
"tn": "本地人类检测",
"type": 0,
"task_flow": [
{
"id": 1,
"type": "ai camera",
"index": 0,
"version": "1.0.0",
"params": {
"model_type": 1,
"modes": 0,
"model": {
"arguments": {
"iou": 45,
"conf": 50
}
},
"conditions": [
{
"class": "person",
"mode": 1,
"type": 2,
"num": 0
}
],
"conditions_combo": 0,
"silent_period": {
"silence_duration": 5
},
"output_type": 0,
"shutter": 0
},
"wires": [
[2]
]
},
{
"id": 2,
"type": "alarm trigger",
"index": 1,
"version": "1.0.0",
"params": {
"text": "检测到人类",
"audio": ""
},
"wires": [
[3]
]
},
{
"id": 3,
"type": "uart alarm",
"index": 2,
"version": "1.0.0",
"params": {
"output_format": 1,
"include_big_image": false,
"include_small_image": false
},
"wires": []
}
]
}
在上述任务流中,uart alarmer
的 params
参数为:
{
"output_format": 1,
"include_big_image": false,
"include_small_image": false
}
我们分析了 cJSON,提取了所需的值,并将它们通常存储到模块实例中。
4.2 msgs_sub_set
static int __msgs_sub_set(void *p_module, int evt_id)
{
tf_module_uart_alarm_t *p_module_ins = (tf_module_uart_alarm_t *)p_module;
p_module_ins->input_evt_id = evt_id;
return tf_event_handler_register(evt_id, __event_handler, p_module_ins);
}
记录上游 FM 的事件 ID 以供将来使用,并为该事件注册一个事件处理程序。
4.3 事件处理程序
在 Watcher 软件框架 中,我们了解到数据流是由事件循环驱动的。基本上,一个 FM 会从其事件处理程序接收数据,然后消费这些数据,进行计算并得到一些结果。最后,它需要将结果发布到事件循环中,目标是那些对该 FM 数据感兴趣的下游 FM。
在这个 uart alarmer
示例中,我们消费来自一个报警触发 FM 的数据,该 FM 的输出数据类型为 TF_DATA_TYPE_DUALIMAGE_WITH_INFERENCE_AUDIO_TEXT
。由于 UART 数据准备相对简单,我们在事件循环处理程序中完成所有数据生成。然而,这种做法并不推荐,如果你的数据处理耗时较长或 IO 密集,建议创建一个工作任务(线程)来进行后台处理。
我们根据输入参数 output_format
准备一个二进制输出缓冲区或 JSON 字符串。最后,我们将这些数据写入 UART。我们的 FM 只有一个输出,即硬件,而不是另一个 FM,因此我们的 msgs_pub_set
是一个空操作。最后,我们需要释放来自事件循环的数据,原因将在下一节中解释。
4.4 msgs_pub_set
在这个示例中,msgs_pub_set
是空操作,因为我们的 FM 没有下游消费者。以下是 ai camera
FM 的一个示例。
// 在 tf_module_ai_camera.c 中
static int __msgs_pub_set(void *p_module, int output_index, int *p_evt_id, int num)
{
tf_module_ai_camera_t *p_module_ins = (tf_module_ai_camera_t *)p_module;
__data_lock(p_module_ins);
if (output_index == 0 && num > 0)
{
p_module_ins->p_output_evt_id = (int *)tf_malloc(sizeof(int) * num);
if (p_module_ins->p_output_evt_id )
{
memcpy(p_module_ins->p_output_evt_id, p_evt_id, sizeof(int) * num);
p_module_ins->output_evt_num = num;
} else {
ESP_LOGE(TAG, "Failed to malloc p_output_evt_id");
p_module_ins->output_evt_num = 0;
}
}
else
{
ESP_LOGW(TAG, "Only support output port 0, ignore %d", output_index);
}
__data_unlock(p_module_ins);
return 0;
}
这段代码并不复杂,只是将事件 ID 存储到 FM 实例的结构中。这是你需要在 FM 的类型结构中添加一个成员字段的地方,在这个例子中是 tf_module_ai_camera_t
。
我们什么时候会使用这些事件 ID?当数据生成并通过时间门控时。在 ai camera
的示例中,数据来源于 Himax SoC 的 SPI 输出,该 SoC 运行本地 AI 推理,并通过一些条件门控。如果所有条件都满足,数据就会到达需要发布到事件循环的时间点。
// 在 tf_module_ai_camera.c 中
...
for (int i = 0; i < p_module_ins->output_evt_num; i++)
{
tf_data_image_copy(&p_module_ins->output_data.img_small, &info.img);
tf_data_inference_copy(&p_module_ins->output_data.inference, &info.inference);
ret = tf_event_post(p_module_ins->p_output_evt_id[i], &p_module_ins->output_data, sizeof(p_module_ins->output_data), pdMS_TO_TICKS(100));
if( ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to post event %d", p_module_ins->p_output_evt_id[i]);
tf_data_free(&p_module_ins->output_data);
} else {
ESP_LOGI(TAG, "Output -> %d", p_module_ins->p_output_evt_id[i]);
}
}
...
我们需要将数据发布给每个输出的订阅者。如你所见,我们为每个订阅者复制了一份数据。
内存分配和释放规则
- 数据生成 FM 为每个订阅者分配内存。
- 数据消费 FM 在数据使用完毕后释放内存。
4.5 启动和停止
这些是 FM 的运行时控制,用于支持未来的流程暂停/恢复功能。目前,你可以在实例化 FM 后让其运行,但我们仍然建议将逻辑分为 FM 的生命周期管理和 FM 的运行时控制。
5. 测试
现在我们有了 uart alarmer
FM,在提交请求之前,如何在本地测试它?
我们实现了一个控制台命令来本地发起任务流。
SenseCAP> help taskflow
taskflow [-iej] [-f <string>]
import taskflow by json string or SD file, eg:taskflow -i -f "test.json".
export taskflow to stdout or SD file, eg: taskflow -e -f "test.json"
-i, --import import taskflow
-e, --export export taskflow
-f, --file=<string> File path, import or export taskflow json string by SD, eg: test.json
-j, --json import taskflow json string by stdin
请参考 构建 Watcher 开发环境 - 5. 监控日志输出
获取控制台。准备一个去掉空格和空白字符的任务流,并通过以下命令发起任务流:
taskflow -i -j<enter>
Please input taskflow json:
#<在此处粘贴你的任务流 json,例如>
{"tlid":3,"ctd":3,"tn":"Local Human Detection","type":0,"task_flow":[{"id":1,"type":"ai camera","index":0,"version":"1.0.0","params":{"model_type":1,"modes":0,"model":{"arguments":{"iou":45,"conf":50}},"conditions":[{"class":"person","mode":1,"type":2,"num":0}],"conditions_combo":0,"silent_period":{"silence_duration":5},"output_type":0,"shutter":0},"wires":[[2]]},{"id":2,"type":"alarm trigger","index":1,"version":"1.0.0","params":{"text":"human detected","audio":""},"wires":[[3]]},{"id":3,"type":"uart alarm","index":2,"version":"1.0.0","params":{"output_format":1},"wires":[]}]}
如何编写任务流?在 Watcher 软件框架 中,我们介绍了每个 FM 及其参数。编写任务流的过程类似于在 Node-RED 中绘制 FM 块之间的连线。
在我们拥有用于编写任务流的 GUI 之前,可以使用 export
命令来收集示例。只需使用移动应用程序发布一个启用了本地报警功能(RGB 灯)的任务流,当任务流运行时,使用以下命令导出任务流:
taskflow -e
此命令会将运行中的任务流导出到控制台。如果任务流非常长,其输出可能会被其他日志中断。在这种情况下,我们需要一张 TF 卡。将 TF 卡格式化为 FAT/exFAT 文件系统,并将其插入 Watcher。现在我们可以将运行中的任务流导出到 TF 卡:
taskflow -e -f tf1.json
# 仅支持根目录下的文件名
# 请不要在路径中指定前置目录,该命令无法创建目录
现在你已经有了示例,修改其中一个报警器 FM(通常是最后一个 FM),将其替换为你的 uart alarmer
FM,向你的 FM 的 JSON 对象中添加一些参数,使用 JSON 编辑器删除空白字符,然后使用上述 taskflow -i -j
命令导入。
就是这样,享受探索的乐趣吧!
附录 - 更多任务流示例
以下是一些可以开始使用的任务流示例。
{"tlid":3,"ctd":3,"tn":"Local Human Detection","type":0,"task_flow":[{"id":1,"type":"ai camera","index":0,"version":"1.0.0","params":{"model_type":1,"modes":0,"model":{"arguments":{"iou":45,"conf":50}},"conditions":[{"class":"person","mode":1,"type":2,"num":0}],"conditions_combo":0,"silent_period":{"silence_duration":5},"output_type":0,"shutter":0},"wires":[[2]]},{"id":2,"type":"alarm trigger","index":1,"version":"1.0.0","params":{"text":"human detected","audio":""},"wires":[[3,4]]},{"id":3,"type":"local alarm","index":2,"version":"1.0.0","params":{"sound":1,"rgb":1,"img":0,"text":0,"duration":1},"wires":[]},{"id":4,"type":"sensecraft alarm","index":3,"version":"1.0.0","params":{"silence_duration":30},"wires":[]}]}
{"tlid":1,"ctd":1,"tn":"Local Gesture Detection","type":0,"task_flow":[{"id":1,"type":"ai camera","index":0,"version":"1.0.0","params":{"model_type":3,"modes":0,"model":{"arguments":{"iou":45,"conf":65}},"conditions":[{"class":"paper","mode":1,"type":2,"num":0}],"conditions_combo":0,"silent_period":{"silence_duration":5},"output_type":0,"shutter":0},"wires":[[2]]},{"id":2,"type":"alarm trigger","index":1,"version":"1.0.0","params":{"text":"scissors detected","audio":""},"wires":[[3,4]]},{"id":3,"type":"local alarm","index":2,"version":"1.0.0","params":{"sound":1,"rgb":1,"img":0,"text":0,"duration":1},"wires":[]},{"id":4,"type":"sensecraft alarm","index":3,"version":"1.0.0","params":{"silence_duration":30},"wires":[]}]}
{"tlid":1719396404172,"ctd":1719396419707,"tn":"Man with glasses spotted, notify immediately","task_flow":[{"id":753589649,"type":"ai camera","type_id":0,"index":0,"vision":"0.0.1","params":{"model_type":0,"model":{"model_id":"60086","version":"1.0.0","arguments":{"size":1644.08,"url":"https://sensecraft-statics.oss-accelerate.aliyuncs.com/refer/model/1705306215159_jVQf4u_swift_yolo_nano_person_192_int8_vela(2).tflite","icon":"https://sensecraft-statics.oss-accelerate.aliyuncs.com/refer/pic/1705306138275_iykYXV_detection_person.png","task":"detect","createdAt":1705306231,"updatedAt":null},"model_name":"Person Detection--Swift YOLO","model_format":"tfLite","ai_framework":"6","author":"SenseCraft AI","description":"The model is a Swift-YOLO model trained on the person detection dataset. It can detect human body existence.","task":1,"algorithm":"Object Dectect(TensorRT,SMALL,COCO)","classes":["person"]},"modes":0,"conditions":[{"class":"person","mode":1,"type":2,"num":0}],"conditions_combo":0,"silent_period":{"time_period":{"repeat":[1,1,1,1,1,1,1],"time_start":"00:00:00","time_end":"23:59:59"},"silence_duration":60},"output_type":1,"shutter":0},"wires":[[193818631]]},{"id":193818631,"type":"image analyzer","type_id":3,"index":1,"version":"0.0.1","params":{"url":"","header":"","body":{"prompt":"Is there a man with glasses?","type":1,"audio_txt":"Man with glasses"}},"wires":[[420037647,452707375]]},{"id":452707375,"type_id":99,"type":"sensecraft alarm","index":2,"version":"0.0.1","params":{"silence_duration":10,"text":"Man with glasses"},"wires":[]},{"id":420037647,"type_id":5,"type":"local alarm","index":3,"version":"0.0.1","params":{"sound":1,"rgb":1,"img":1,"text":1,"duration":10},"wires":[]}],"type":0}
技术支持与产品讨论
感谢您选择我们的产品!我们为您提供多种支持渠道,确保您在使用我们的产品时获得顺畅的体验。我们提供多种沟通方式,以满足不同的偏好和需求。