Files
DishMatch/scale包架构分析.md
T
lvmeng 6595c4bde0 feat(dialog): FoodSearchDialog 新增空页面效果及 Window.Callback 手势冲突修复
- FoodSearchDialog:adapter 启用 isStateViewEnable,新增 loadEmptyView() 通过 adapter.stateView 展示空视图,替换原 toast 提示;覆写 show() 通过 Window.Callback.dispatchTouchEvent 动态控制 behavior.isDraggable,解决列表滑动与 BottomSheet 拖拽的手势冲突;注释旧 addOnScrollListener 方案保留备查
- SeasoningSelectDialog:同步补齐空页面效果,修复网络错误时也触发 loadEmptyView,优化 Handler 为 View.postDelayed
- FoodRecognizeActivity:修复识别结果回填后列表未显示的问题
- 新增 scale包架构分析.md 文档
2026-05-06 17:35:29 +08:00

195 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 的兜底发现机制。
广播包结构:
```json
{ "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
整个包的统一入口(单例),根据设备角色决定启动哪些服务,并将各组件串联起来。
**外部使用方式:**
```kotlin
// 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` 交互,无需感知内部任何组件。