feat(scale): 实现调料槽位配置持久化及主从设备数据同步
- 新增 SeasoningSlotEntity / SeasoningSlotDao,独立存储格子调料配置 - AppDatabase 升级至版本 8,添加 MIGRATION_7_8 - ScaleEvent 新增 TYPE_SEASONING_CONFIG 和 SlotConfig,用于主从同步 - SeasoningConfigFragment 选择调料后写 Room 并通过 wsClient 推送给子设备 - ScaleWebSocketClient 新增 sendToAllSlaves / sendToDevice / onDeviceConnected - ScaleWebSocketServer 扩展 onMessage 支持接收主设备下发的配置事件 - ScaleServiceManager 暴露 sendSeasoningConfig / onSeasoningConfig;子设备连接时自动推送全量配置 - MasterScaleActivity 启动时从 Room 加载 slotNameMap,注入 ScaleData.name 展示调料名 - SlaveActivity 新增 slotNameMap 防止重量回调覆盖调料名;监听 onSeasoningConfig 写 Room 并刷新 adapter - Adapter 新增 tvAddress 和 showAddress 参数,仅 MasterScaleActivity 显示秤地址行;tvName 空值统一显示"-" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,38 @@
|
||||
package com.shuwei.dish.match.scale
|
||||
|
||||
/**
|
||||
* 秤事件数据类,用于子设备向主设备发送非重量类通知
|
||||
* 秤事件数据类,用于主设备和子设备之间的非重量类通知
|
||||
* @param type 事件类型,参见 companion object 中的常量
|
||||
* @param deviceId 发送方设备 ID
|
||||
* @param address 触发事件的秤地址
|
||||
* @param address 触发事件的秤地址,TYPE_SEASONING_CONFIG 时为 0(无意义)
|
||||
* @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义
|
||||
* @param slots 调料槽位配置列表,仅 TYPE_SEASONING_CONFIG 时有效
|
||||
*/
|
||||
data class ScaleEvent(
|
||||
val type: String,
|
||||
val deviceId: String,
|
||||
val address: Int,
|
||||
val delta: Double = 0.0
|
||||
val address: Int = 0,
|
||||
val delta: Double = 0.0,
|
||||
val slots: List<SlotConfig>? = null
|
||||
) {
|
||||
/**
|
||||
* 单个槽位的配置信息,随 TYPE_SEASONING_CONFIG 事件一起传输
|
||||
* @param deviceId 所属设备 ID
|
||||
* @param address 秤硬件地址
|
||||
* @param goodsId 调料 ID
|
||||
* @param goodsName 调料名称
|
||||
*/
|
||||
data class SlotConfig(
|
||||
val deviceId: String,
|
||||
val address: Int,
|
||||
val goodsId: String,
|
||||
val goodsName: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
/** 调料添加事件:某秤重量增加超过阈值 */
|
||||
const val TYPE_SEASONING_ADDED = "seasoning_added"
|
||||
/** 调料槽位配置同步事件:主设备配置变更后广播给子设备 */
|
||||
const val TYPE_SEASONING_CONFIG = "seasoning_config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ package com.shuwei.dish.match.scale
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.shuwei.dish.match.base.BaseApp
|
||||
import com.shuwei.dish.match.base.DeviceRole
|
||||
import com.shuwei.dish.match.base.GlobalData
|
||||
import com.shuwei.dish.match.utils.NetworkUtil
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 秤服务统一门面(单例)
|
||||
@@ -59,6 +63,36 @@ object ScaleServiceManager {
|
||||
wsClient?.onScaleEvent = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到主设备下发的调料配置同步事件时的回调,仅子设备有效
|
||||
* 在子线程中调用,需自行切换到主线程更新 UI
|
||||
*/
|
||||
var onSeasoningConfig: ((ScaleEvent) -> Unit)?
|
||||
get() = wsServer?.onSeasoningConfig
|
||||
set(value) { wsServer?.onSeasoningConfig = value }
|
||||
|
||||
/**
|
||||
* 向所有已连接子设备广播调料配置(主设备调用)
|
||||
* @param slots 全量槽位配置列表
|
||||
*/
|
||||
fun sendSeasoningConfig(slots: List<ScaleEvent.SlotConfig>) {
|
||||
wsClient?.sendToAllSlaves(buildConfigEvent(slots))
|
||||
}
|
||||
|
||||
/**
|
||||
* 向单台刚连接的子设备推送调料配置(连接时按需调用)
|
||||
* @param deviceId 目标子设备 ID
|
||||
*/
|
||||
private fun sendSeasoningConfigTo(deviceId: String, slots: List<ScaleEvent.SlotConfig>) {
|
||||
wsClient?.sendToDevice(deviceId, buildConfigEvent(slots))
|
||||
}
|
||||
|
||||
private fun buildConfigEvent(slots: List<ScaleEvent.SlotConfig>) = ScaleEvent(
|
||||
type = ScaleEvent.TYPE_SEASONING_CONFIG,
|
||||
deviceId = GlobalData.deviceId,
|
||||
slots = slots
|
||||
)
|
||||
|
||||
/**
|
||||
* 向主设备广播秤事件(子设备调用)
|
||||
* @param event 要广播的事件
|
||||
@@ -123,6 +157,19 @@ object ScaleServiceManager {
|
||||
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
|
||||
it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) }
|
||||
it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) }
|
||||
it.onDeviceConnected = { remoteId ->
|
||||
// 子设备连接成功时,从 Room 读取全量配置并单独推送给该设备
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val slots = BaseApp.instance!!.database.seasoningSlotDao()
|
||||
.queryAll()
|
||||
.map { slot ->
|
||||
ScaleEvent.SlotConfig(
|
||||
slot.deviceId, slot.address, slot.goodsId, slot.goodsName
|
||||
)
|
||||
}
|
||||
if (slots.isNotEmpty()) sendSeasoningConfigTo(remoteId, slots)
|
||||
}
|
||||
}
|
||||
}
|
||||
wsClient = client
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ class ScaleWebSocketClient {
|
||||
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */
|
||||
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
|
||||
|
||||
/** 子设备首次连接成功时的回调,在子线程调用;可用于主设备主动推送全量配置 */
|
||||
var onDeviceConnected: ((deviceId: String) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* 连接到指定子设备
|
||||
* @param deviceId 子设备 ID
|
||||
@@ -79,6 +82,7 @@ class ScaleWebSocketClient {
|
||||
connections[deviceId] = webSocket
|
||||
reconnectDelays[deviceId] = RECONNECT_BASE_MS
|
||||
reconnectTasks.remove(deviceId)?.cancel(false)
|
||||
onDeviceConnected?.invoke(deviceId)
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
@@ -148,6 +152,31 @@ class ScaleWebSocketClient {
|
||||
return ws.send(gson.toJson(command))
|
||||
}
|
||||
|
||||
/**
|
||||
* 向所有已连接的子设备广播事件(主设备调用,用于配置同步等场景)
|
||||
* @param event 要广播的事件
|
||||
*/
|
||||
fun sendToAllSlaves(event: ScaleEvent) {
|
||||
val json = gson.toJson(event)
|
||||
connections.values.forEach { ws ->
|
||||
try { ws.send(json) } catch (e: Exception) {
|
||||
Log.w(TAG, "sendToAllSlaves 失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定子设备发送事件(用于连接时单独推送配置)
|
||||
* @param deviceId 目标子设备 ID
|
||||
* @param event 要发送的事件
|
||||
*/
|
||||
fun sendToDevice(deviceId: String, event: ScaleEvent) {
|
||||
val ws = connections[deviceId] ?: return
|
||||
try { ws.send(gson.toJson(event)) } catch (e: Exception) {
|
||||
Log.w(TAG, "sendToDevice $deviceId 失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定子设备连接(不再重连)
|
||||
* @param deviceId 子设备 ID
|
||||
|
||||
@@ -28,6 +28,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
|
||||
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
|
||||
|
||||
/** 收到主设备下发的调料配置同步事件时的回调,在子线程调用 */
|
||||
var onSeasoningConfig: ((event: ScaleEvent) -> Unit)? = null
|
||||
|
||||
/** 当前是否有主设备连接 */
|
||||
val isConnected: Boolean
|
||||
get() = server?.connections?.isNotEmpty() == true
|
||||
@@ -72,6 +75,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
*/
|
||||
fun stop() {
|
||||
WeightUtil.removeWeightListener(TAG)
|
||||
onConnectionChanged = null
|
||||
onSeasoningConfig = null
|
||||
try {
|
||||
server?.stop(1000)
|
||||
} catch (e: Exception) {
|
||||
@@ -135,6 +140,15 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
|
||||
override fun onMessage(conn: WebSocket, message: String) {
|
||||
try {
|
||||
val raw = gson.fromJson(message, Map::class.java)
|
||||
if (raw.containsKey("type")) {
|
||||
// 主设备下发的事件(如调料配置同步)
|
||||
val event = gson.fromJson(message, ScaleEvent::class.java)
|
||||
when (event.type) {
|
||||
ScaleEvent.TYPE_SEASONING_CONFIG -> onSeasoningConfig?.invoke(event)
|
||||
}
|
||||
return
|
||||
}
|
||||
val cmd = gson.fromJson(message, ScaleCommand::class.java)
|
||||
// 校验指令目标设备是否为本机
|
||||
if (cmd.deviceId != deviceId) {
|
||||
@@ -148,7 +162,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "收到清零指令, 解析指令失败: ${e.message}")
|
||||
Log.w(TAG, "收到消息, 解析失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user