业务目标:产品面板按钮状态发生变化,依靠事件桥完成跨进程上报,最终组装 protobuf 报文发送给中心站。 比喻对照:事件桥 = 内部邮件系统;业务账本 = 内部笔记本;SDK 结构体 = 翻译草稿纸;Protobuf = 对外快递包裹(发送完就销毁) 数据流向:①业务账本 → ②通信层 sdk → ③proto 协议 ,开发严格遵循该顺序。 底层设计三原则:组件解耦、单写者、跨进程契约。
0. 整体流程一览
SystemManager(采集产生按钮状态)
↓ 组装CoreEvent邮件,dispatch_event交给事件桥(邮件系统)
ClientProxy(接收跨进程事件邮件)
↓ 步骤5:把状态写入①业务账本(内部笔记本,常驻内存保存真实状态)
↓ 步骤6:数据拷贝:①业务账本 → ②通信层sdk结构体(翻译草稿纸)
↓ 步骤7:数据拷贝:②通信层sdk结构体 → ③protobuf协议报文(快递包裹)
↓ 序列化为网络字节流,发送至中心站
关键注意:SystemManager 不能直接修改 ClientProxy 内部成员变量,进程隔离,只能发事件消息;proto 是对外通信契约,要放在流程最后实现。
第一部分:发送端 SystemManager,产生并向外投递事件邮件
步骤 1:定义全新事件 ID(头文件 .h)
通俗解释:定义邮件编号,用来标识这是一条 "面板按钮状态变更" 的消息,收发双方靠这个编号识别消息类型。 在原有事件枚举列表中,原有内部温度事件下方,添加新事件枚举:
EV_MCP_UPDATE_INTERNAL_TEMP, //原有:内部温度
EV_MCP_UPDATE_BOARD_BUTTON_STATUS, //新增:面板按钮状态事件(邮件编号)
步骤 2:函数末尾触发发送事件(实现文件 .cpp)
通俗解释:硬件检测到按钮按下 / 松开,组装一封 CoreEvent 邮件,填上收件人、邮件编号、附件数据,调用 dispatch_event 把邮件交给事件桥(邮件系统)完成跨进程投递。 在按钮状态更新函数的函数末尾,增加事件组装与发送代码:
// button_state:硬件读到的值,int,1按下,0松开
CoreEvent ev;
ev.type = EV_TYPE_LOCAL_CLIENT_PROXY; //收件人:ClientProxy组件
ev.id = EV_MCP_UPDATE_BOARD_BUTTON_STATUS; //邮件编号
ev.payload = std::move(button_state); //邮件附件:携带按钮状态数值
dispatch_event(ev); //把邮件交给【事件桥】,底层负责跨进程分发
提示:此处只向外发送消息,不能直接操作其他组件内部数据;事件邮件处理完成之后就会销毁,不会保存状态。
第二部分:三层数据容器定义(账本、通信层、协议层)
步骤 3:①业务账本结构体(头文件 .h)
通俗解释:准备好内部笔记本,增加一栏记录面板按钮状态;账本是类的成员变量,常驻内存,就算断网、邮件销毁,设备当前真实状态仍然保存在这里。 在业务状态结构体
TechMaintainStatus内部,新增按钮状态成员变量:
struct TechMaintainStatus {
...
std::optional<bool> nursecall_status; //原有
std::optional<bool> button_status; //新增:①业务账本保存按钮状态(内部笔记本)
};
作用:
std::optional<bool>区分三种状态:未收到上报、按下 true、松开 false。事件处理函数收到消息后,把值存入该账本。
步骤 4:②通信层 SDK 过渡结构体(头文件 .h)
通俗解释:准备翻译草稿纸;业务账本是固件内部格式,proto 是对外网络格式,用这一层做中转隔离。后续协议修改,只改草稿纸到 proto 的逻辑,内部业务账本代码不用改动。 在 SDK 通信中间结构体
nx::sdk::TechMaintainStatus中增加字段:
struct TechMaintainStatus {
...
std::optional<bool> nursecall_status; //原有
std::optional<MFDStatus> mfd_status; //原有
std::optional<bool> button_status; //新增:②通信过渡层(翻译草稿纸)
};
作用:实现业务逻辑与网络报文解耦;业务层只管维护状态,通信层只管组装报文,互不耦合。
步骤 5:③protobuf 对外协议契约(.proto 协议文件)
通俗解释:定义对外快递包裹格式,和外部中心站约定好数据包长什么样;BoolValue 包装类型可以区分 "无数据、true、false",普通 bool 做不到。 在 proto 的
TechMaintainStatus消息体中添加字段:
google.protobuf.BoolValue button_status = 11;
作用:设备和外部中心站之间的网络通信契约;编译后自动生成
has_button_status()判断字段是否有效。protobuf 对象是临时局部变量,报文发送完成就销毁,不能用来长期保存设备状态。
第三部分:接收端 ClientProxy,接收事件、逐层搬运数据
步骤 6:事件处理 handler 声明与实现(.h 头文件 + .cpp 实现文件)
通俗解释:收到事件邮件,拆开邮件取出附件数据,把按钮状态写入内部笔记本(业务账本 tech_maintain_status_)。事件本身是一次性消息,用完销毁,真正状态保存在账本。
-
头文件添加函数声明
ClientProxyEvResult handle_update_board_button_status_event(
const CoreEvent& in, CoreEvent* out); -
cpp 实现文件编写处理函数
ClientProxyEvResult ClientProxy::handle_update_board_button_status_event(
const CoreEvent &in, CoreEvent *out)
{
bool is_fslev{false};
//从事件payload取出邮件附件int类型按钮状态
if (const auto &data = std::any_cast<int32_t>(&in.payload)) {
//【对应动作:把数据写进内部笔记本‑业务账本】
tech_maintain_status_.button_status = *data;
}
return {HANDLED, is_fslev};
}
作用:事件实际处理入口,只负责更新内部业务状态,不直接做网络发送。int 数值自动转换:0 对应 false,1 对应 true。
步骤 7:注册事件处理函数路由(.cpp 实现文件)
通俗解释:告诉事件桥(邮件系统):收到编号为 EV_MCP_UPDATE_BOARD_BUTTON_STATUS 的邮件,请交给上面这个 handler 函数处理。 ⚠️重点坑点:忘记注册,代码编译完全通过,但运行时邮件直接静默丢弃,处理函数永远不会执行。
register_client_proxy_ev_handler(
EV_MCP_UPDATE_BOARD_BUTTON_STATUS,
std::bind(&ClientProxy::handle_update_board_button_status_event, this,
std::placeholders::_1, std::placeholders::_2));
事件完整内部调用链路
dispatch_event()
→ 根据目标组件找到 ClientProxy::handle_event()
→ handle_clientproxy_event()
→ 在路由map中根据事件ID查找注册的处理函数
→ 执行我们编写的handler业务逻辑
步骤 8:数据搬运①业务账本 → ②通信层 SDK 结构体(.cpp 转换文件)
通俗解释:要发送报文时,把笔记本(业务账本)上面记录的数据,抄写一份到翻译草稿纸(SDK 结构体)。 参照现有其他状态拷贝代码,增加如下赋值逻辑:
// src = 内部账本 tech_maintain_status_
// data = SDK草稿纸对象
if (src.button_status) {
//【对应动作:把笔记本内容抄写至草稿纸】
data->button_status = src.button_status;
}
踩坑提醒:遗漏该步骤,账本数据正常,但通信结构体为空,最终报文不带该字段。
步骤 9:数据搬运②通信层 SDK → ③protobuf 报文(.cpp 类型转换文件)
通俗解释:拿着草稿纸上的数据,按照约定格式填充到临时快递包裹(protobuf 对象)。 增加代码把 SDK 数据填充到 protobuf 对象:
// sdkdata:草稿纸 nx::sdk::TechMaintainStatus
// proto:待填充的protobuf对象(临时快递包裹)
if (sdkdata.button_status) {
//【对应动作:把草稿纸上内容填进对外快递包裹】
// mutable_xxx()是protoc自动生成接口,获取proto里面可修改的子对象
proto->mutable_button_status()->set_value(sdkdata.button_status.value());
}
填充完成之后,调用框架原有序列化、发送逻辑即可:
std::string bin = proto.SerializeAsString();
send_network_data(bin);
报文发送完毕,proto 局部对象销毁;业务账本
tech_maintain_status_仍然保留设备最新状态。
交付验证与问题排查
-
对外接口说明 复用已有
TechMaintainStatus通信通道; 字段语义:true按钮按下,false按钮松开;调用has_button_status()判断本条报文是否携带按钮状态数据。 -
调试日志埋点(仅开发调试使用,正式提交删除)
- 日志 1:SystemManager 成功发出事件
- 日志 2:handler 收到事件,业务账本写入数据
- 日志 3:数据从账本拷贝到通信 SDK 结构体
- 日志 4:数据填充进 protobuf 报文
四条日志全部打印,代表整条数据流完整跑通。
对象生命周期对照表
| 对象 | 对应代码变量名 | 生命周期 |
|---|---|---|
| 事件邮件 CoreEvent | CoreEvent ev |
投递处理完直接销毁,不存状态 |
| 业务账本(内部笔记本) | tech_maintain_status_ |
类成员变量,常驻内存,设备运行一直存在 |
| SDK 草稿纸 | nx::sdk::TechMaintainStatus |
每次上报临时生成拷贝 |
| Protobuf 快递包裹 | proto 局部对象 | 发送完毕函数结束就销毁 |
整体工作知识点总结
1. 属于什么工作
属于嵌入式 Linux 医疗设备固件开发,做设备内部业务状态跨进程上报、上层业务到网络协议的数据适配开发;是设备业务与外部上位机通信链路的新增业务字段开发。
2. 使用到的技术原理 & C++ 知识点
- 跨进程事件驱动架构(事件桥 CoreEvent):不同进程之间不能直接访问对方内部变量,通过消息完成通信,实现组件解耦。事件只是 "状态发生变化" 的通知信号,事件本身不长期保存业务数据。
- 分层设计、关注点分离思想
- 业务账本层:只关心设备当前真实业务状态;
- SDK 中间过渡层:隔离内部业务逻辑与外部网络协议;
- protobuf 协议层:只负责网络传输二进制格式。 每一层职责单一,修改其中一层尽量不影响其他层。
- C++17 std::optional:可以表达 "值不存在" 语义,区分「状态从未更新」和「布尔值 false」两种完全不同场景。
- std::any / std::any_cast :事件载荷通用容器,可以存放任意类型数据,接收端做安全类型提取。
- std::bind + std::placeholders:将类的成员函数包装成回调,绑定 this 指针,注册到事件路由表。
- Protobuf 协议 :IDL 接口描述语言,编译自动生成 C++ 访问接口;
mutable_xxx()获取可修改子对象,has_xxx()判断字段是否存在;协议字段编号属于网络契约,旧编号不能随意修改。 - 逐层数据拷贝流转:每层使用独立结构体对象,逐层赋值搬运,避免业务逻辑和网络逻辑耦合。
3. 开发踩坑经验
- 编译通过 ≠ 运行正常:事件 handler 漏注册不会编译报错,只会运行静默丢消息;
- 分层拷贝任何一层遗漏赋值,都会造成下游报文字段丢失;
- proto 不要最先写,要按照数据流顺序,先业务层、中间层,最后再定义协议,防止逻辑顺序颠倒;
BoolValue和普通 bool 不一样:普通 bool 无法区分 "没有上报" 和 "主动 false",对外协议必须使用包装类型;- ❌禁止:收到事件 payload 直接组装 proto 发送。事件消息是瞬时一次性的,网络丢包、定时重传时没有地方获取设备当前状态;
- ✅正确:事件只做通知,真实业务状态必须保存在常驻内存的业务账本。
4. 一句话概括整个业务
设备采集主机面板按钮状态,通过事件桥跨进程传递变更通知;接收组件将状态保存至本地业务账本,上报时分层拷贝中转,最终转换成 protobuf 网络报文,上报给外部中心站。