先说结论
Odoo IoT 的本质,不是“把外设接到 Odoo 上”,而是建立一条浏览器、Odoo 服务器、IoT Box、设备驱动之间的双向控制链。
从 /home/ubuntu/odoo-temp/addons/iot_base/static/src/network_utils/longpolling.js、addons/iot_drivers/websocket_client.py 和 addons/iot_drivers/interface.py 来看,官方把两种通信方向拆得很清楚:
- 浏览器 / 前端 → IoT Box:主要通过 longpolling / HTTP action
- Odoo 服务器 → IoT Box:主要通过 WebSocket 下发远程消息
- IoT Box → 本地设备:通过 interface 检测设备,再分配给具体 driver
所以它真正解决的问题不是“设备在线没有”,而是:
一个前台动作如何跨过网络与盒子,最终变成某个具体硬件驱动上的执行动作,并把结果再带回来。
第一层:为什么前端侧用的是 Longpolling,而不是只发一次 HTTP
IoTLongpolling 这个类名称已经很说明问题。
它有两条固定路由:
actionRoute = '/iot_drivers/action'pollRoute = '/iot_drivers/event'
同时维护:
_session_id_listenerslast_eventabortController- 重试延迟和退避逻辑
这表示浏览器和 IoT Box 的关系不是“按一次按钮发一次请求”这么简单。
前端除了发 action,还要持续维持一个监听关系,接收设备事件返回。
这很重要,因为硬件交互不是纯同步世界:
- 扫码枪什么时候扫到码,不由前端控制
- 打印机执行结果可能要稍后返回
- 秤、屏幕、脚踏板这类设备会不断产生事件流
如果只靠单次 RPC,浏览器根本没法建立稳定的设备事件通道。
所以 longpolling 在这里承担的其实是:
把 Web 界面的请求-响应世界,拓展成“可等待设备事件”的半实时通道。
第二层:监听器为什么要按 iot_ip 和 device_identifier 分层
addListener(iot_ip, devices, listener_id, callback, fallback) 会把监听关系挂到:
- 某个 IoT Box IP
- 其下若干 device_identifier
每个设备还有自己的 callback。
这套结构特别关键,因为一个 IoT Box 往往不只接一个设备:
- 打印机
- 扫码枪
- 客显
- 电子秤
- 键盘类输入设备
如果监听不按设备标识拆开,返回事件一多,前端根本不知道该把哪条结果交给哪个组件。
而源码里每个 listener 既保存 session,又保存 device map,本质上是在做一个轻量设备事件路由表。
这也是为什么 _onSuccess() 里会根据 result.device_identifier 去调用对应 callback——
Odoo IoT 不是只在“连通性”层面集成设备,而是在“事件语义”层面集成设备。
第三层:为什么要同时保留 Longpolling 和 WebSocket 两条链
很多人会问:既然已经有 longpolling,为什么 IoT Box 侧还要跑一个 WebsocketClient?
答案是它们服务的方向不同。
Longpolling 更偏前端与盒子的局部交互
比如:
- 浏览器组件监听扫码事件
- 前端向某设备发 action
- 页面维持和本地盒子的短链路
WebSocket 更偏服务器与盒子的远程控制
websocket_client.py 里,IoT Box 连接到 Odoo 服务端的 websocket 通道,并订阅自身 channel。
收到消息后会处理:
iot_actionserver_clearserver_updaterestart_odoowebrtc_offerremote_debugtest_connectionbundle_changed
这说明 WebSocket 的角色更像“远程控制总线”。
也就是说,前端 longpolling 解决的是“当前页面如何和盒子对话”; 而服务端 websocket 解决的是“数据库如何远程管理和调度这台盒子”。
这两条链互补,而不是重复。
第四层:设备动作为什么最终都要落到 driver 上
在 WebSocket 的 on_message() 里,处理 iot_action 时会:
- 遍历
device_identifiers - 判断
device_identifier是否在main.iot_devices - 找到后执行
main.iot_devices[device_identifier].action(payload)
这一步特别重要。
它说明 IoT Box 并不是直接“认识打印机、认识扫码枪”,而是先把设备抽象成 driver 对象,再把动作交给 driver 执行。
这就带来三个好处:
1. 设备接入与设备动作解耦
只要 driver 实现统一 action 协议,前端和服务端就不必关心底层硬件细节。
2. 多设备类型可以共存
不同驱动只要都注册进 iot_devices,消息分发路径就统一了。
3. 未连接设备可以被明确回报
如果找不到对应 device_identifier,源码会回传 status = disconnected,而不是默默失败。
这类失败显式化,对现场运维特别关键。
第五层:Interface 为什么是 IoT 体系里最容易被忽略的一层
interface.py 展示了 IoT Box 如何把“物理世界”接进软件世界。
每个 Interface:
- 带一个
connection_type - 周期性
get_devices() - 跟踪
_detected_devices - 根据支持的 driver 自动
add_device() - 消失时
remove_device()
而 add_device() 并不是见到设备就接入,而是:
- 根据 connection type 过滤可用 driver
- 找到第一个
supported(device)为真的 driver - 用它实例化真正的设备对象
- 放入
iot_devices - 启动设备线程
这说明 Interface 做的不是业务动作,而是设备发现与驱动匹配。
很多人会把 IoT 理解成“写几个控制接口”,但如果没有 Interface 这一层:
- 新设备插上来不会被发现
- 同一连接类型下无法自动挑选驱动
- 断连事件不能及时清理
所以从架构上看,Interface 更像硬件即插即用的入口。
第六层:为什么系统要认真处理失败、超时和重连
无论 longpolling 还是 websocket,源码里都花了不少篇幅处理异常:
Longpolling 侧
- 失败会
_doWarnFail()弹通知 - timeout 时会指数退避式重启 polling
AbortController用于及时取消监听
WebSocket 侧
- 记录
last_message_id - 断开后持久化消息位置
run_forever(reconnect=10)持续重连- 启动初期对旧
server_clear做防抖处理
这说明官方非常清楚:
IoT 集成最真实的敌人不是功能缺失,而是网络不稳定、设备断连和时序错位。
所以能不能稳定重连、能不能避免重复消息、能不能清楚提示失败,和“能不能打印”一样重要。
第七层:为什么 IoT 不是“浏览器直连设备”
longpolling.js 里其实已经透露了一个现实:浏览器只是在和 IoT Box 的 HTTP 接口对话。
真正复杂的设备世界被挡在盒子后面。
这样做的好处很明显:
- 浏览器不需要直接访问 USB / 串口 / 本地驱动
- 设备兼容性问题集中到 IoT Box 处理
- Odoo 服务端也能通过 websocket 和盒子做远程协同
于是整个体系被拆成了三层边界:
- 浏览器:界面与业务动作发起者
- IoT Box:本地设备代理与驱动宿主
- Odoo Server:远程控制、配置与状态协调中心
这才是 Odoo IoT 能在复杂现场环境里落地的根本原因。
最容易误解的三个点
误区一:IoT 就是把设备 IP 填进系统
错。核心不在登记地址,而在事件监听、动作分发、驱动执行和失败反馈。
误区二:Longpolling 和 WebSocket 二选一
也不对。它们分工不同,分别解决前端监听与服务端远控。
误区三:驱动只是实现细节,不重要
恰恰相反。没有 driver / interface 分层,所有设备集成都会退化成不可维护的硬编码。
最后一句话
Odoo IoT 真正厉害的地方,不是“能连打印机”,而是它把页面动作、网络消息、盒子代理和设备驱动串成了一条完整控制链。
所以理解它最好的方式不是“硬件接入插件”,而是:
它是一套让 Odoo 业务动作落地到真实设备世界的中间层。
DISCUSSION
评论区