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:
@@ -27,9 +27,12 @@ import com.shuwei.dish.match.databinding.ListTypeScale1Binding
|
||||
import com.shuwei.dish.match.databinding.ListTypeScale18Binding
|
||||
import com.shuwei.dish.match.databinding.ListTypeScale2Binding
|
||||
import com.shuwei.dish.match.databinding.ListTypeScale22Binding
|
||||
import com.shuwei.dish.match.base.BaseApp
|
||||
import com.shuwei.dish.match.scale.ScaleData
|
||||
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.shuwei.dish.match.utils.NetworkUtil
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
@@ -59,6 +62,9 @@ class MasterScaleActivity : BaseActivity() {
|
||||
private val groupList = mutableListOf<DeviceGroup>()
|
||||
private val adapter = DeviceGroupAdapter(groupList)
|
||||
|
||||
/** key = "$deviceId#$address",value = 调料名称,用于注入 ScaleData.name */
|
||||
private val slotNameMap = mutableMapOf<String, String>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMasterScaleBinding.inflate(layoutInflater)
|
||||
@@ -70,7 +76,26 @@ class MasterScaleActivity : BaseActivity() {
|
||||
binding.rvScaleList.itemAnimator = null
|
||||
binding.rvScaleList.adapter = adapter
|
||||
|
||||
observeScaleData()
|
||||
loadSlotNames()
|
||||
}
|
||||
|
||||
/** 从 Room 加载槽位配置到 slotNameMap,加载完成后启动数据观测 */
|
||||
private fun loadSlotNames() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
|
||||
withContext(Dispatchers.Main) {
|
||||
slots.forEach { slot ->
|
||||
slotNameMap["${slot.deviceId}#${slot.address}"] = slot.goodsName
|
||||
}
|
||||
observeScaleData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 用 slotNameMap 为 ScaleData 注入名称 */
|
||||
private fun ScaleData.withSlotName(): ScaleData {
|
||||
val name = slotNameMap["$deviceId#$address"]
|
||||
return if (name != null) copy(name = name) else this
|
||||
}
|
||||
|
||||
private fun observeScaleData() {
|
||||
@@ -90,7 +115,7 @@ class MasterScaleActivity : BaseActivity() {
|
||||
deviceId = deviceId,
|
||||
ip = if (deviceId == localId) localIp
|
||||
else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "",
|
||||
scales = scales.sortedBy { it.address }
|
||||
scales = scales.map { it.withSlotName() }.sortedBy { it.address }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -269,7 +294,7 @@ class MasterScaleActivity : BaseActivity() {
|
||||
alignItems = AlignItems.FLEX_START
|
||||
}
|
||||
b.rvScales.itemAnimator = null
|
||||
innerAdapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false).apply {
|
||||
innerAdapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false, showAddress = true).apply {
|
||||
onItemClick = { scale, _ ->
|
||||
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
|
||||
}
|
||||
@@ -303,7 +328,7 @@ class MasterScaleActivity : BaseActivity() {
|
||||
}
|
||||
b.rvScales.layoutManager = gridLayoutManager
|
||||
b.rvScales.itemAnimator = null
|
||||
innerAdapter = Seasoning18GridAdapter(showNameOnly = false).apply {
|
||||
innerAdapter = Seasoning18GridAdapter(showNameOnly = false, showAddress = true).apply {
|
||||
onItemClick = { scale, _ ->
|
||||
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
|
||||
}
|
||||
@@ -369,6 +394,8 @@ class MasterScaleActivity : BaseActivity() {
|
||||
setBackgroundResource(R.drawable.shape_scale_cell)
|
||||
val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt()
|
||||
addView(makeCellText("秤${scale.address}", (baseSize * 0.16f).coerceIn(9f, 14f).toInt()))
|
||||
val foodType = if (scale.address == 1) "净材" else "熟食"
|
||||
addView(makeCellText(foodType, (baseSize * 0.16f).coerceIn(9f, 14f).toInt()))
|
||||
addView(makeCellText("${scale.weight}g", (baseSize * 0.22f).coerceIn(12f, 20f).toInt(), bold = true))
|
||||
addView(makeCellText(stateStr(scale.state), (baseSize * 0.14f).coerceIn(8f, 12f).toInt()))
|
||||
tag = scale
|
||||
@@ -379,8 +406,9 @@ class MasterScaleActivity : BaseActivity() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) {
|
||||
(cell.getChildAt(0) as? TextView)?.text = "秤${scale.address}"
|
||||
(cell.getChildAt(1) as? TextView)?.text = "${scale.weight}g"
|
||||
(cell.getChildAt(2) as? TextView)?.text = stateStr(scale.state)
|
||||
(cell.getChildAt(1) as? TextView)?.text = if (scale.address == 1) "净材" else "熟食"
|
||||
(cell.getChildAt(2) as? TextView)?.text = "${scale.weight}g"
|
||||
(cell.getChildAt(3) as? TextView)?.text = stateStr(scale.state)
|
||||
}
|
||||
|
||||
private fun makeCellText(text: String, spSize: Int, bold: Boolean = false): TextView =
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.addCallback
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.flexbox.AlignItems
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexWrap
|
||||
@@ -17,15 +18,20 @@ 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
|
||||
import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
||||
import com.shuwei.dish.match.scale.ScaleData
|
||||
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.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* 子设备专属页面(小屏幕设备使用)
|
||||
@@ -49,6 +55,8 @@ class SlaveActivity : BaseActivity() {
|
||||
|
||||
/** 各秤地址的上一次重量快照,用于计算 delta */
|
||||
private val weightSnapshot = mutableMapOf<Int, Double>()
|
||||
/** key = address,value = 调料名称,用于注入 ScaleData.name,避免重量更新时覆盖名称 */
|
||||
private val slotNameMap = mutableMapOf<Int, String>()
|
||||
/** 主线程 Handler,用于延迟清除高亮 */
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
|
||||
@@ -65,8 +73,10 @@ class SlaveActivity : BaseActivity() {
|
||||
|
||||
setupScaleList()
|
||||
updateMasterStatus(ScaleServiceManager.isMasterConnected)
|
||||
loadSlotsFromDb()
|
||||
listenLocalScales()
|
||||
listenMasterConnection()
|
||||
listenSeasoningConfig()
|
||||
addBackKeyListener()
|
||||
}
|
||||
|
||||
@@ -140,6 +150,55 @@ class SlaveActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地 Room 加载本机设备的槽位配置并更新 adapter
|
||||
* 主设备未开机时也能正常展示上次配置
|
||||
*/
|
||||
private fun loadSlotsFromDb() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val dao = BaseApp.instance!!.database.seasoningSlotDao()
|
||||
val slots = dao.queryByDeviceId(GlobalData.deviceId)
|
||||
withContext(Dispatchers.Main) { applySlots(slots) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听主设备下发的调料配置同步事件
|
||||
* 收到后更新 adapter 并写入本地 Room
|
||||
*/
|
||||
private fun listenSeasoningConfig() {
|
||||
ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event ->
|
||||
val mySlots = event.slots
|
||||
?.filter { it.deviceId == GlobalData.deviceId }
|
||||
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
|
||||
?: return@onSeasoningConfig
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
BaseApp.instance!!.database.seasoningSlotDao().upsertAll(mySlots)
|
||||
withContext(Dispatchers.Main) { applySlots(mySlots) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
|
||||
* 同时更新 slotNameMap,确保后续重量回调构造 ScaleData 时携带正确名称
|
||||
*/
|
||||
private fun applySlots(slots: List<SeasoningSlotEntity>) {
|
||||
slots.forEach { slot ->
|
||||
slotNameMap[slot.address] = slot.goodsName
|
||||
when {
|
||||
scale22Adapter != null -> {
|
||||
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
|
||||
if (pos >= 0) scale22Adapter!!.updateItemName(pos, slot.goodsName)
|
||||
}
|
||||
scale18Adapter != null -> {
|
||||
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
|
||||
if (pos >= 0) scale18Adapter!!.updateItemName(pos, slot.goodsName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听本机 WeightUtil 回调,实时刷新秤列表
|
||||
* 22/18个秤走网格 adapter,其余走线性列表
|
||||
@@ -164,14 +223,14 @@ class SlaveActivity : BaseActivity() {
|
||||
// 用最新数据更新缓存后整体刷新网格
|
||||
val existing = scale22Adapter!!.items.toMutableList()
|
||||
val idx = existing.indexOfFirst { it.address == address }
|
||||
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis())
|
||||
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis(), name = slotNameMap[address])
|
||||
if (idx >= 0) existing[idx] = newData else existing.add(newData)
|
||||
scale22Adapter!!.updateByAddress(existing)
|
||||
}
|
||||
scale18Adapter != null -> {
|
||||
val existing = scale18Adapter!!.items.toMutableList()
|
||||
val idx = existing.indexOfFirst { it.address == address }
|
||||
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis())
|
||||
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis(), name = slotNameMap[address])
|
||||
if (idx >= 0) existing[idx] = newData else existing.add(newData)
|
||||
scale18Adapter!!.updateByAddress(existing)
|
||||
}
|
||||
@@ -219,6 +278,7 @@ class SlaveActivity : BaseActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
ScaleServiceManager.onMasterConnectionChanged = null
|
||||
ScaleServiceManager.onSeasoningConfig = null
|
||||
WeightUtil.removeWeightListener(TAG)
|
||||
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
super.onDestroy()
|
||||
|
||||
@@ -13,18 +13,23 @@ import com.google.android.flexbox.FlexWrap
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
|
||||
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
|
||||
import com.shuwei.dish.match.base.BaseApp
|
||||
import com.shuwei.dish.match.base.BaseFragment
|
||||
import com.shuwei.dish.match.base.DeviceRole
|
||||
import com.shuwei.dish.match.base.GlobalData
|
||||
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
|
||||
import com.shuwei.dish.match.dialog.SeasoningSelectDialog
|
||||
import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
||||
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
||||
import com.shuwei.dish.match.scale.ScaleEvent
|
||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
||||
import com.shuwei.dish.match.scale.ScaleEvent.SlotConfig
|
||||
import com.shuwei.dish.match.ui.SettingActivity
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
||||
|
||||
@@ -65,26 +70,58 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
||||
|
||||
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
|
||||
private fun bindAdapterClicks() {
|
||||
adapter22.onItemClick = { _, position ->
|
||||
adapter22.onItemClick = { scaleData, position ->
|
||||
SeasoningSelectDialog(
|
||||
activity = settingActivity,
|
||||
clickIndex = position,
|
||||
onItemSelected = { item ->
|
||||
adapter22.updateItemName(position, item.goodsName ?: "")
|
||||
saveSlotAndBroadcast(
|
||||
SeasoningSlotEntity(
|
||||
deviceId = ScaleDeviceConfig.DEVICE_ID_22,
|
||||
address = scaleData.address,
|
||||
goodsId = item.goodsId,
|
||||
goodsName = item.goodsName ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
).show()
|
||||
}
|
||||
adapter18.onItemClick = { _, position ->
|
||||
adapter18.onItemClick = { scaleData, position ->
|
||||
SeasoningSelectDialog(
|
||||
activity = settingActivity,
|
||||
clickIndex = position,
|
||||
onItemSelected = { item ->
|
||||
adapter18.updateItemName(position, item.goodsName ?: "")
|
||||
saveSlotAndBroadcast(
|
||||
SeasoningSlotEntity(
|
||||
deviceId = ScaleDeviceConfig.DEVICE_ID_18,
|
||||
address = scaleData.address,
|
||||
goodsId = item.goodsId,
|
||||
goodsName = item.goodsName ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个槽位写入 Room,再读取全量配置广播给子设备
|
||||
* 仅主设备执行广播,子设备不调用此方法
|
||||
*/
|
||||
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val dao = BaseApp.instance!!.database.seasoningSlotDao()
|
||||
dao.upsert(slot)
|
||||
// 读取全量配置,通过 wsClient 推送给所有已连接子设备
|
||||
val allSlots = dao.queryAll().map {
|
||||
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
|
||||
}
|
||||
ScaleServiceManager.sendSeasoningConfig(allSlots)
|
||||
}
|
||||
}
|
||||
|
||||
// /** 初始化 ViewModel */
|
||||
// private fun initViewModel() {
|
||||
// val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao()))
|
||||
@@ -126,10 +163,25 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载默认空数据,使两个列表完整展示所有格子 */
|
||||
/** 从 Room 加载已配置的槽位名称,填充到对应格子 */
|
||||
private fun loadData() {
|
||||
adapter22.update(emptyList())
|
||||
adapter18.update(emptyList())
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val dao = BaseApp.instance!!.database.seasoningSlotDao()
|
||||
val slots22 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_22)
|
||||
val slots18 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_18)
|
||||
withContext(Dispatchers.Main) {
|
||||
slots22.forEach { slot ->
|
||||
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
|
||||
if (pos >= 0) adapter22.updateItemName(pos, slot.goodsName)
|
||||
}
|
||||
slots18.forEach { slot ->
|
||||
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
|
||||
if (pos >= 0) adapter18.updateItemName(pos, slot.goodsName)
|
||||
}
|
||||
}
|
||||
}
|
||||
// appViewModel.loadSeasoning { list ->
|
||||
// adapter22.update(list ?: emptyList())
|
||||
// adapter18.update(list ?: emptyList())
|
||||
|
||||
Reference in New Issue
Block a user