- FoodSearchDialog:adapter 启用 isStateViewEnable,新增 loadEmptyView() 通过 adapter.stateView 展示空视图,替换原 toast 提示;覆写 show() 通过 Window.Callback.dispatchTouchEvent 动态控制 behavior.isDraggable,解决列表滑动与 BottomSheet 拖拽的手势冲突;注释旧 addOnScrollListener 方案保留备查 - SeasoningSelectDialog:同步补齐空页面效果,修复网络错误时也触发 loadEmptyView,优化 Handler 为 View.postDelayed - FoodRecognizeActivity:修复识别结果回填后列表未显示的问题 - 新增 scale包架构分析.md 文档
7.1 KiB
scale 包架构分析
概览
scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 mDNS + UDP 双路冗余发现 + WebSocket 长连接的架构。
根据设备角色(MASTER/SLAVE)运行不同的服务组合:
| 类 | 子设备 | 主设备 |
|---|---|---|
| MdnsRegisterManager | ✅ | ✅ |
| ScaleWebSocketServer | ✅ | ✅ |
| UdpBroadcastSender | ✅ | ❌ |
| MdnsDiscoveryManager | ❌ | ✅ |
| UdpBroadcastReceiver | ❌ | ✅ |
| ScaleWebSocketClient | ❌ | ✅ |
| ScaleDataAggregator | ❌ | ✅ |
整体数据流
子设备 主设备
│ │
├─ UdpBroadcastSender ──UDP广播(8766)──▶ UdpBroadcastReceiver ─┐
├─ MdnsRegisterManager ──mDNS注册──▶ MdnsDiscoveryManager ─┤
│ │ │
│ onDeviceFound │
│ │ │
├─ ScaleWebSocketServer ◀──WS连接(8765)── ScaleWebSocketClient ◀┘
│ │ 秤数据推送 │
│ └──────────────────────────────▶ ScaleDataAggregator
│ │
│ StateFlow → UI
│
│ ◀── ScaleCommand(清零指令)─────────────────┤
│ ◀── ScaleEvent(配置同步)──────────────────┤
│ ──▶ ScaleEvent(调料添加通知)──────────────▶│
数据模型(3个)
ScaleData
单个秤的数据快照,是整个包内流转的核心数据结构。
| 字段 | 类型 | 说明 |
|---|---|---|
| deviceId | String | 所属设备 ID |
| address | Int | 秤硬件地址编号 |
| weight | Double | 重量(克) |
| state | Int | 1=稳定,0=不稳定,2=量程溢出 |
| ts | Long | 数据时间戳(毫秒) |
| ip | String | 所属设备 IP,用于 UI 展示;网络传输数据中可能为空 |
| name | String? | 秤槽位名称(可选),默认为 null |
ScaleEvent
主子设备之间的非重量类通知,通过 WebSocket 传输。
| 事件类型 | 方向 | 说明 |
|---|---|---|
seasoning_added |
子设备 → 主设备 | 某秤检测到调料添加,携带 delta(重量变化量) |
seasoning_config |
主设备 → 子设备 | 调料槽位配置同步,携带 List<SlotConfig> |
ScaleCommand
主设备向子设备发送的控制指令,子设备收到后校验 deviceId 是否匹配自身再执行。
| 指令 | 说明 |
|---|---|
tare |
清零指定地址的秤 |
设备发现(4个)
采用 mDNS + UDP 双路冗余,任意一路发现子设备均可触发连接。
MdnsRegisterManager(主设备和子设备均运行)
将本机 WebSocket 服务以 mDNS 形式注册到局域网。
- 服务名格式:
DishMatch-{deviceId} - 服务类型:
_dishmatch._tcp. - 端口:
8765
MdnsDiscoveryManager(仅主设备运行)
持续扫描局域网中所有 DishMatch-* 的 mDNS 服务,解析出 IP:PORT 后触发 onDeviceFound。
关键细节:Android
NsdManager.resolveService不支持并发调用,多台子设备同时被发现时会报FAILURE_ALREADY_ACTIVE(3)。内部使用串行队列(resolveQueue)逐一解析,避免解析失败。
UdpBroadcastSender(仅子设备运行)
每 5 秒向 255.255.255.255:8766 广播一个 JSON 包,作为 mDNS 的兜底发现机制。
广播包结构:
{ "deviceId": "xxx", "ip": "192.168.1.x", "port": 8765 }
UdpBroadcastReceiver(仅主设备运行)
监听 8766 端口,接收子设备的 UDP 广播包,解析后触发 onDeviceFound。
- 内部用
knownDevices(ConcurrentHashMap)缓存已发现的设备,避免每 5 秒重复触发连接 - 设备断线时需调用
removeDevice()清除缓存,才能在重连时重新触发onDeviceFound
数据传输(2个)
ScaleWebSocketServer(主设备和子设备均运行)
基于 java-websocket 的服务端,监听 8765 端口。
职责:
- 监听本机
WeightUtil回调,将秤数据实时推送给所有已连接客户端(节流 100ms) - 新客户端连接时,立即推送所有秤的最新快照(
latestData缓存) - 接收主设备下发的
ScaleCommand(清零)和ScaleEvent(配置同步) - 通过
connectionCount计数器跟踪连接数,避免connections集合竞态问题
ScaleWebSocketClient(仅主设备运行)
管理主设备与多台子设备的 WebSocket 长连接。
职责:
- 多设备并发连接(
ConcurrentHashMap管理) - 断线自动重连(指数退避:2s → 4s → 8s → ... → 30s)
- 向指定设备或全部设备发送指令/事件
- 子设备首次连接成功时触发
onDeviceConnected,供主设备推送全量配置
数据聚合(1个)
ScaleDataAggregator(仅主设备运行)
将本机秤和所有子设备秤的数据统一汇总,以 StateFlow 暴露给 UI 层。
- 本机秤:直接监听
WeightUtil回调,无需经过网络 - 子设备秤:由
ScaleWebSocketClient.onScaleData回调写入 - Map key 格式:
{deviceId}#{address},便于 UI 按设备分组展示 - IP 回填:若数据包先于 IP 信息到达,
setDeviceIp()会回填已缓存数据中的空 IP 字段
配置(1个)
ScaleDeviceConfig
硬编码各子设备的固定 UUID 和秤地址显示顺序。
| 常量 | 说明 |
|---|---|
DEVICE_ID_2/22/18/1 |
各子设备固定 UUID |
SCALE_ORDER_22 |
22个秤的物理位置排列顺序 |
SCALE_ORDER_18 |
18个秤的物理位置排列顺序 |
DEVICE_ORDER |
设备在列表中的显示顺序 |
门面(1个)
ScaleServiceManager
整个包的统一入口(单例),根据设备角色决定启动哪些服务,并将各组件串联起来。
外部使用方式:
// Application.onCreate()
ScaleServiceManager.start(context)
// 主设备 UI 订阅全量秤数据
ScaleServiceManager.allScales?.collect { scales -> ... }
// 主设备发送清零指令
ScaleServiceManager.sendTare(deviceId, address)
// 主设备广播调料配置
ScaleServiceManager.sendSeasoningConfig(slots)
// 主设备监听子设备秤事件(如调料添加)
ScaleServiceManager.onScaleEvent = { event -> ... }
// 子设备监听主设备下发的调料配置同步
ScaleServiceManager.onSeasoningConfig = { event -> ... }
// 子设备向主设备广播秤事件
ScaleServiceManager.broadcastEvent(event)
// 子设备监听主设备连接状态变化
ScaleServiceManager.onMasterConnectionChanged = { connected -> ... }
// 子设备查询当前是否有主设备连接
ScaleServiceManager.isMasterConnected
// Application.onTerminate()
ScaleServiceManager.stop()
外部代码只需与 ScaleServiceManager 交互,无需感知内部任何组件。