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:
+194
@@ -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` 交互,无需感知内部任何组件。
|
||||
Reference in New Issue
Block a user