Files
DishMatch/scale包架构分析.md
T
lvmengandClaude Sonnet 4.6 436de88d73 fix(scale): 修复子设备重启后主设备看不到连接数据的问题,新增清除数据广播功能
- ScaleWebSocketClient:引入版本号机制防止 UDP 触发的新连接与指数退避重连任务并发建立两条连接;onFailure/onClosed 改用 remove(key, value) 原子操作,避免旧连接超时回调误删新连接引用
- ScaleWebSocketServer:新增 latestData 为空时延迟 3 秒补推机制,解决子设备刚重启时 WeightUtil 尚无读数导致主设备看不到数据的问题;新增 onClearData 回调处理清除数据事件;stop() 时同步关闭 scheduler
- ScaleEvent:新增 TYPE_CLEAR_DATA 事件类型
- ScaleServiceManager:新增 onClearData 属性和 sendClearData() 方法
- SlaveActivity:监听 onClearData 事件,收到后清空本机数据并重置 SP 状态;启动时预填空占位格子
- SettingActivity/DbViewModel/DbRepository:clearAllTestData 重命名为 clearAllData,清除完成后通知子设备并跳转 HomeActivity
- scale包架构分析.md:同步更新文档

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:48:59 +08:00

8.1 KiB
Raw Blame History

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>
clear_data 主设备 → 子设备 通知所有子设备清除本机全部测试数据并重置 SP 状态

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

  • 内部用 knownDevicesConcurrentHashMap)缓存已发现的设备,避免每 5 秒重复触发连接
  • 设备断线时需调用 removeDevice() 清除缓存,才能在重连时重新触发 onDeviceFound

数据传输(2个)

ScaleWebSocketServer(主设备和子设备均运行)

基于 java-websocket 的服务端,监听 8765 端口。

职责:

  • 监听本机 WeightUtil 回调,将秤数据实时推送给所有已连接客户端(节流 100ms)
  • 新客户端连接时,立即推送所有秤的最新快照(latestData 缓存);若 latestData 为空(刚重启尚无读数),延迟 3 秒后补推一次
  • 接收主设备下发的 ScaleCommand(清零)和 ScaleEvent(配置同步、清除数据)
  • 通过 connectionCount 计数器跟踪连接数,避免 connections 集合竞态问题
  • 内置单线程 scheduler,用于延迟补推任务调度;stop() 时同步关闭

ScaleWebSocketClient(仅主设备运行)

管理主设备与多台子设备的 WebSocket 长连接。

职责:

  • 多设备并发连接(ConcurrentHashMap 管理)
  • 断线自动重连(指数退避:2s → 4s → 8s → ... → 30s
  • 向指定设备或全部设备发送指令/事件
  • 子设备首次连接成功时触发 onDeviceConnected,供主设备推送全量配置

并发安全机制:

  • 版本号防重复连接:每次调用 connect() 时递增 connectVersions[deviceId]doConnect 执行前校验版本号,版本不匹配(说明已有更新的连接请求)则直接放弃,避免 UDP 触发的新连接与指数退避重连任务并发建立两条连接
  • 原子移除防误删onFailure/onClosed 使用 connections.remove(deviceId, webSocket) 原子操作,只有移除的是自己的实例时才触发 onDeviceDisconnected 和重连,避免旧连接超时回调误删新连接引用

数据聚合(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.sendClearData()

// 主设备监听子设备秤事件(如调料添加)
ScaleServiceManager.onScaleEvent = { event -> ... }

// 子设备监听主设备下发的调料配置同步
ScaleServiceManager.onSeasoningConfig = { event -> ... }

// 子设备监听主设备下发的清除数据指令
ScaleServiceManager.onClearData = { ... }

// 子设备向主设备广播秤事件
ScaleServiceManager.broadcastEvent(event)

// 子设备监听主设备连接状态变化
ScaleServiceManager.onMasterConnectionChanged = { connected -> ... }

// 子设备查询当前是否有主设备连接
ScaleServiceManager.isMasterConnected

// Application.onTerminate()
ScaleServiceManager.stop()

外部代码只需与 ScaleServiceManager 交互,无需感知内部任何组件。