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 文档
This commit is contained in:
2026-05-06 17:35:29 +08:00
parent 2de5ec39b5
commit 6595c4bde0
4 changed files with 365 additions and 33 deletions
+194
View File
@@ -0,0 +1,194 @@
# 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` 交互,无需感知内部任何组件。