From 436de88d7382a591a1914f4811eba2bcbc775f49 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Fri, 8 May 2026 15:48:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(scale):=20=E4=BF=AE=E5=A4=8D=E5=AD=90?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E9=87=8D=E5=90=AF=E5=90=8E=E4=B8=BB=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=9C=8B=E4=B8=8D=E5=88=B0=E8=BF=9E=E6=8E=A5=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B8=85=E9=99=A4=E6=95=B0=E6=8D=AE=E5=B9=BF=E6=92=AD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../com/shuwei/dish/match/db/DbRepository.kt | 2 +- .../com/shuwei/dish/match/db/DbViewModel.kt | 4 +- .../com/shuwei/dish/match/scale/ScaleEvent.kt | 2 + .../dish/match/scale/ScaleServiceManager.kt | 18 ++++++++ .../dish/match/scale/ScaleWebSocketClient.kt | 43 ++++++++++++++----- .../dish/match/scale/ScaleWebSocketServer.kt | 41 +++++++++++++++--- .../shuwei/dish/match/ui/SettingActivity.kt | 20 +++++---- .../com/shuwei/dish/match/ui/SlaveActivity.kt | 28 +++++++++++- app/src/main/res/layout/activity_setting.xml | 4 +- scale包架构分析.md | 16 ++++++- 10 files changed, 147 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt b/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt index 4240d3c..ae7385c 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt @@ -227,7 +227,7 @@ class DbRepository { * 物理清空全部 4 张表的测试数据 * 清除顺序:先删子表(食材),再删主表(菜品),最后删配置表(调料、槽位) */ - suspend fun clearAllTestData() = withContext(Dispatchers.IO) { + suspend fun clearAllData() = withContext(Dispatchers.IO) { db.cookFoodGoodsDao().deleteAll() db.cookFoodDao().deleteAll() db.seasoningDao().deleteAll() diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt index e864cb8..189acd3 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt @@ -243,9 +243,9 @@ class DbViewModel : ViewModel() { * 物理清空全部 4 张表的测试数据,完成后通过回调通知 UI * @param onDone 清除完成后在主线程执行的回调 */ - fun clearAllTestData(onDone: () -> Unit) { + fun clearAllData(onDone: () -> Unit) { viewModelScope.launch { - rep.clearAllTestData() + rep.clearAllData() onDone() } } diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleEvent.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleEvent.kt index 4e4160c..fe097a1 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleEvent.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleEvent.kt @@ -34,5 +34,7 @@ data class ScaleEvent( const val TYPE_SEASONING_ADDED = "seasoning_added" /** 调料槽位配置同步事件:主设备配置变更后广播给子设备 */ const val TYPE_SEASONING_CONFIG = "seasoning_config" + /** 清除数据事件:主设备通知所有子设备清除本机测试数据 */ + const val TYPE_CLEAR_DATA = "clear_data" } } diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt index 173288b..c140bda 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt @@ -71,6 +71,14 @@ object ScaleServiceManager { get() = wsServer?.onSeasoningConfig set(value) { wsServer?.onSeasoningConfig = value } + /** + * 收到主设备下发的清除数据事件时的回调,仅子设备有效 + * 在子线程中调用,需自行切换到主线程更新 UI + */ + var onClearData: ((ScaleEvent) -> Unit)? + get() = wsServer?.onClearData + set(value) { wsServer?.onClearData = value } + /** * 向所有已连接子设备广播调料配置(主设备调用) * @param slots 全量槽位配置列表 @@ -79,6 +87,16 @@ object ScaleServiceManager { wsClient?.sendToAllSlaves(buildConfigEvent(slots)) } + /** + * 通知所有已连接子设备清除本机数据(主设备调用) + * 子设备收到后执行与主设备相同的清除逻辑 + */ + fun sendClearData() { + wsClient?.sendToAllSlaves( + ScaleEvent(type = ScaleEvent.TYPE_CLEAR_DATA, deviceId = GlobalData.deviceId) + ) + } + /** * 向单台刚连接的子设备推送调料配置(连接时按需调用) * @param deviceId 目标子设备 ID diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketClient.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketClient.kt index 8bf1d9f..3ab7a41 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketClient.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketClient.kt @@ -43,6 +43,11 @@ class ScaleWebSocketClient { private val reconnectDelays = ConcurrentHashMap() /** 已主动移除的设备集合,不对其触发重连 */ private val removedDevices = ConcurrentHashMap.newKeySet() + /** + * 各设备的连接版本号,每次调用 connect() 时递增 + * doConnect 执行前校验版本,版本不匹配说明已有更新的连接请求,直接放弃本次连接 + */ + private val connectVersions = ConcurrentHashMap() private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() @@ -66,13 +71,21 @@ class ScaleWebSocketClient { */ fun connect(deviceId: String, host: String, port: Int) { removedDevices.remove(deviceId) - // 取消旧 IP 的重连任务,防止 IP 变化时新旧任务并发竞争 + // 取消旧重连任务,防止 IP 变化时新旧任务并发竞争 reconnectTasks.remove(deviceId)?.cancel(false) reconnectDelays[deviceId] = RECONNECT_BASE_MS - doConnect(deviceId, host, port) + // 递增版本号:doConnect 执行前会校验版本,旧任务版本不匹配时直接放弃 + val version = (connectVersions[deviceId] ?: 0) + 1 + connectVersions[deviceId] = version + doConnect(deviceId, host, port, version) } - private fun doConnect(deviceId: String, host: String, port: Int) { + private fun doConnect(deviceId: String, host: String, port: Int, version: Int = connectVersions[deviceId] ?: 0) { + // 版本校验:若当前版本已被更新的 connect() 调用覆盖,放弃本次连接 + if (connectVersions[deviceId] != version) { + Log.d(TAG, "放弃过期连接任务: $deviceId, version=$version") + return + } val url = "ws://$host:$port" val request = Request.Builder().url(url).build() @@ -103,16 +116,23 @@ class ScaleWebSocketClient { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { Log.w(TAG, "连接子设备失败: $deviceId, ${t.message}") - connections.remove(deviceId) - onDeviceDisconnected?.invoke(deviceId) - scheduleReconnect(deviceId, host, port) + // 使用 remove(key, value) 确保只移除自己的实例 + // 避免旧连接超时触发时误删新连接引用,导致 aggregator 错误清空新连接数据 + val removed = connections.remove(deviceId, webSocket) + if (removed) { + onDeviceDisconnected?.invoke(deviceId) + scheduleReconnect(deviceId, host, port) + } } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { Log.d(TAG, "子设备连接已关闭: $deviceId, reason=$reason") - connections.remove(deviceId) - onDeviceDisconnected?.invoke(deviceId) - scheduleReconnect(deviceId, host, port) + // 同上:只有移除的是自己,才触发断线回调和重连 + val removed = connections.remove(deviceId, webSocket) + if (removed) { + onDeviceDisconnected?.invoke(deviceId) + scheduleReconnect(deviceId, host, port) + } } }) @@ -129,10 +149,12 @@ class ScaleWebSocketClient { if (scheduler.isShutdown) return val delay = reconnectDelays[deviceId] ?: RECONNECT_BASE_MS Log.d(TAG, "将在 ${delay}ms 后重连子设备: $deviceId") + // 记录当前版本,延迟任务执行时再次校验,防止期间有新的 connect() 调用 + val version = connectVersions[deviceId] ?: 0 val future = scheduler.schedule({ if (!removedDevices.contains(deviceId)) { - doConnect(deviceId, host, port) + doConnect(deviceId, host, port, version) } }, delay, TimeUnit.MILLISECONDS) @@ -188,6 +210,7 @@ class ScaleWebSocketClient { reconnectTasks.remove(deviceId)?.cancel(false) connections.remove(deviceId)?.close(1000, "设备离线") reconnectDelays.remove(deviceId) + connectVersions.remove(deviceId) Log.d(TAG, "已断开子设备: $deviceId") } diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt index 1578439..674eed2 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt @@ -9,6 +9,8 @@ import org.java_websocket.WebSocket import org.java_websocket.handshake.ClientHandshake import org.java_websocket.server.WebSocketServer import java.net.InetSocketAddress +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit /** * 秤数据 WebSocket 服务端(主设备和子设备均运行) @@ -20,10 +22,14 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co private const val TAG = "ScaleWebSocketServer" /** 节流间隔:两次推送之间的最小间隔(毫秒) */ private const val THROTTLE_MS = 100L + /** latestData 为空时延迟补推的时间(毫秒),给 WeightUtil 时间产生第一次读数 */ + private const val RETRY_PUSH_DELAY_MS = 3_000L } private val gson = Gson() private var server: InternalServer? = null + /** 延迟补推任务的调度器 */ + private val scheduler = Executors.newSingleThreadScheduledExecutor() /** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */ var onConnectionChanged: ((connected: Boolean) -> Unit)? = null @@ -31,6 +37,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co /** 收到主设备下发的调料配置同步事件时的回调,在子线程调用 */ var onSeasoningConfig: ((event: ScaleEvent) -> Unit)? = null + /** 收到主设备下发的清除数据事件时的回调,在子线程调用 */ + var onClearData: ((event: ScaleEvent) -> Unit)? = null + /** 当前是否有主设备连接 */ val isConnected: Boolean get() = server?.connections?.isNotEmpty() == true @@ -77,6 +86,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co WeightUtil.removeWeightListener(TAG) onConnectionChanged = null onSeasoningConfig = null + onClearData = null + scheduler.shutdownNow() try { server?.stop(1000) } catch (e: Exception) { @@ -124,13 +135,32 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co connectionCount++ Log.d(TAG, "主设备已连接: ${conn.remoteSocketAddress}") onConnectionChanged?.invoke(true) + // 新客户端连接后,立即推送所有秤的最新数据 - latestData.values.forEach { data -> - try { - conn.send(gson.toJson(data)) - } catch (e: Exception) { - Log.w(TAG, "初始推送失败: ${e.message}") + if (latestData.isNotEmpty()) { + latestData.values.forEach { data -> + try { + conn.send(gson.toJson(data)) + } catch (e: Exception) { + Log.w(TAG, "初始推送失败: ${e.message}") + } } + } else { + // latestData 为空说明刚重启,WeightUtil 还未产生读数 + // 延迟 3 秒后补推一次,确保主设备能看到本机秤 + Log.d(TAG, "latestData 为空,${RETRY_PUSH_DELAY_MS}ms 后补推") + scheduler.schedule({ + if (conn.isOpen && latestData.isNotEmpty()) { + latestData.values.forEach { data -> + try { + conn.send(gson.toJson(data)) + Log.d(TAG, "补推数据: address=${data.address}, weight=${data.weight}") + } catch (e: Exception) { + Log.w(TAG, "补推失败: ${e.message}") + } + } + } + }, RETRY_PUSH_DELAY_MS, TimeUnit.MILLISECONDS) } } @@ -151,6 +181,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co val event = gson.fromJson(message, ScaleEvent::class.java) when (event.type) { ScaleEvent.TYPE_SEASONING_CONFIG -> onSeasoningConfig?.invoke(event) + ScaleEvent.TYPE_CLEAR_DATA -> onClearData?.invoke(event) } return } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt index 9093248..f72660a 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt @@ -12,9 +12,9 @@ import androidx.core.content.ContextCompat import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.databinding.ActivitySettingBinding import com.shuwei.dish.match.dialog.CommonDialog +import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.ext.clickWithDebounce -import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.visible @@ -49,7 +49,7 @@ class SettingActivity : BaseActivity() { it.clickWithDebounce { binding.llDbInspect.visible() binding.llScaleObserve.visible() - binding.llClearTestData.visible() + binding.llClearData.visible() } }) @@ -85,24 +85,28 @@ class SettingActivity : BaseActivity() { binding.llScaleObserve.clickWithDebounce { startActivity() } - binding.llClearTestData.clickWithDebounce { - showClearTestDataDialog() + binding.llClearData.clickWithDebounce { + showClearDataDialog() } } /** * 弹出清除测试数据的二次确认弹窗 */ - private fun showClearTestDataDialog() { + private fun showClearDataDialog() { CommonDialog(this) - .setTitle("测试数据清除") + .setTitle("数据清除") .setContent("将物理删除全部 4 张表数据(菜品、食材、调料、槽位),此操作不可恢复,同时重置菜品模式和食堂id,确认继续?") .setNegativeButton("取消") .setPositiveButton("确认清除") { - appViewModel.clearAllTestData { + appViewModel.clearAllData { SpTool.cookMode = -1 SpTool.canteenId = "0" - toast("测试数据已清除") + // 通知所有已连接子设备同步清除数据 + ScaleServiceManager.sendClearData() + startActivity { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } } } .show() diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt index 1ef5707..b591d2d 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SlaveActivity.kt @@ -18,7 +18,6 @@ import com.shuwei.dish.match.adapter.Seasoning18GridAdapter import com.shuwei.dish.match.adapter.Seasoning22GridAdapter import com.shuwei.dish.match.adapter.ScaleRowAdapter import com.shuwei.dish.match.base.BaseActivity -import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.databinding.ActivitySlaveBinding import com.shuwei.dish.match.dialog.CommonDialog @@ -28,6 +27,7 @@ import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.SizeTool +import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.ext.dp import kotlinx.coroutines.launch @@ -77,6 +77,7 @@ class SlaveActivity : BaseActivity() { listenLocalScales() listenMasterConnection() listenSeasoningConfig() + listenClearData() addBackKeyListener() } @@ -109,6 +110,8 @@ class SlaveActivity : BaseActivity() { it.onItemClick = { scale, _ -> showTareDialog(scale.address) } } binding.rvScaleList.adapter = scale22Adapter + // 启动时用空哨兵预填全部格子,不依赖 WeightUtil 回调才显示结构 + scale22Adapter!!.updateByAddress(emptyList()) } ScaleDeviceConfig.DEVICE_ID_18 -> { binding.rvScaleList.updateLayoutParams { topMargin = 0 } @@ -121,12 +124,18 @@ class SlaveActivity : BaseActivity() { it.onItemClick = { scale, _ -> showTareDialog(scale.address) } } binding.rvScaleList.adapter = scale18Adapter + // 启动时用空哨兵预填全部格子,不依赖 WeightUtil 回调才显示结构 + scale18Adapter!!.updateByAddress(emptyList()) } else -> { binding.rvScaleList.updateLayoutParams { topMargin = 20.dp } binding.rvScaleList.layoutManager = LinearLayoutManager(this) linearAdapter.onItemClick = { item -> showTareDialog(item.address) } binding.rvScaleList.adapter = linearAdapter + // 启动时预填空占位,不依赖 WeightUtil 回调才显示列表结构 + ScaleDeviceConfig.SCALE_ORDER_1.forEach { addr -> + linearAdapter.updateItem(ScaleRowAdapter.ScaleItem(addr, 0.0, WeightUtil.STATE_STABLE)) + } } } } @@ -177,6 +186,23 @@ class SlaveActivity : BaseActivity() { } } + /** + * 监听主设备下发的清除数据事件 + * 收到后清空本机全部 4 张表数据并重置 SP 状态,与主设备保持一致 + * 清除完成后刷新 UI,清空所有秤的调料名称显示 + */ + private fun listenClearData() { + ScaleServiceManager.onClearData = { + lifecycleScope.launch { + appViewModel.clearAllData { + SpTool.cookMode = -1 + SpTool.canteenId = "0" + runOnUiThread { applySlots(emptyList()) } + } + } + } + } + /** * 将槽位配置应用到对应 adapter(按地址匹配格子位置) * 同时以传入列表为准全量刷新 slotNameMap: diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml index c422f81..cd6417c 100644 --- a/app/src/main/res/layout/activity_setting.xml +++ b/app/src/main/res/layout/activity_setting.xml @@ -174,7 +174,7 @@ diff --git a/scale包架构分析.md b/scale包架构分析.md index 404a2da..f5dcc42 100644 --- a/scale包架构分析.md +++ b/scale包架构分析.md @@ -63,6 +63,7 @@ scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 * |---|---|---| | `seasoning_added` | 子设备 → 主设备 | 某秤检测到调料添加,携带 `delta`(重量变化量) | | `seasoning_config` | 主设备 → 子设备 | 调料槽位配置同步,携带 `List` | +| `clear_data` | 主设备 → 子设备 | 通知所有子设备清除本机全部测试数据并重置 SP 状态 | ### ScaleCommand 主设备向子设备发送的**控制指令**,子设备收到后校验 `deviceId` 是否匹配自身再执行。 @@ -112,9 +113,10 @@ scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 * **职责:** - 监听本机 `WeightUtil` 回调,将秤数据实时推送给所有已连接客户端(节流 100ms) -- 新客户端连接时,立即推送所有秤的最新快照(`latestData` 缓存) -- 接收主设备下发的 `ScaleCommand`(清零)和 `ScaleEvent`(配置同步) +- 新客户端连接时,立即推送所有秤的最新快照(`latestData` 缓存);若 `latestData` 为空(刚重启尚无读数),延迟 3 秒后补推一次 +- 接收主设备下发的 `ScaleCommand`(清零)和 `ScaleEvent`(配置同步、清除数据) - 通过 `connectionCount` 计数器跟踪连接数,避免 `connections` 集合竞态问题 +- 内置单线程 `scheduler`,用于延迟补推任务调度;`stop()` 时同步关闭 ### ScaleWebSocketClient(仅主设备运行) 管理主设备与多台子设备的 WebSocket 长连接。 @@ -125,6 +127,10 @@ scale 包负责多设备秤数据的采集、发现、传输与聚合,采用 * - 向指定设备或全部设备发送指令/事件 - 子设备首次连接成功时触发 `onDeviceConnected`,供主设备推送全量配置 +**并发安全机制:** +- **版本号防重复连接**:每次调用 `connect()` 时递增 `connectVersions[deviceId]`,`doConnect` 执行前校验版本号,版本不匹配(说明已有更新的连接请求)则直接放弃,避免 UDP 触发的新连接与指数退避重连任务并发建立两条连接 +- **原子移除防误删**:`onFailure`/`onClosed` 使用 `connections.remove(deviceId, webSocket)` 原子操作,只有移除的是自己的实例时才触发 `onDeviceDisconnected` 和重连,避免旧连接超时回调误删新连接引用 + --- ## 数据聚合(1个) @@ -172,12 +178,18 @@ ScaleServiceManager.sendTare(deviceId, address) // 主设备广播调料配置 ScaleServiceManager.sendSeasoningConfig(slots) +// 主设备通知所有子设备清除数据 +ScaleServiceManager.sendClearData() + // 主设备监听子设备秤事件(如调料添加) ScaleServiceManager.onScaleEvent = { event -> ... } // 子设备监听主设备下发的调料配置同步 ScaleServiceManager.onSeasoningConfig = { event -> ... } +// 子设备监听主设备下发的清除数据指令 +ScaleServiceManager.onClearData = { ... } + // 子设备向主设备广播秤事件 ScaleServiceManager.broadcastEvent(event)