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:
@@ -21,9 +21,11 @@ import com.shuwei.dish.match.utils.ext.dp
|
|||||||
*
|
*
|
||||||
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState;
|
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState;
|
||||||
* false = 秤总览模式,三行全部显示
|
* false = 秤总览模式,三行全部显示
|
||||||
|
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
|
||||||
*/
|
*/
|
||||||
class Seasoning18GridAdapter(
|
class Seasoning18GridAdapter(
|
||||||
private val showNameOnly: Boolean = false
|
private val showNameOnly: Boolean = false,
|
||||||
|
private val showAddress: Boolean = false
|
||||||
) : BaseQuickAdapter<ScaleData, Seasoning18GridAdapter.VH>(mutableListOf()) {
|
) : BaseQuickAdapter<ScaleData, Seasoning18GridAdapter.VH>(mutableListOf()) {
|
||||||
|
|
||||||
/** 空位哨兵,address=0 表示无效 */
|
/** 空位哨兵,address=0 表示无效 */
|
||||||
@@ -53,9 +55,12 @@ class Seasoning18GridAdapter(
|
|||||||
else it.setMargins(margin, margin, margin, margin)
|
else it.setMargins(margin, margin, margin, margin)
|
||||||
}
|
}
|
||||||
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
|
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
|
||||||
|
b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
|
||||||
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
|
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
|
||||||
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
|
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
|
||||||
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
|
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
|
||||||
|
// 秤地址行:仅 MasterScaleActivity 显示
|
||||||
|
b.tvAddress.visibility = if (showAddress) View.VISIBLE else View.GONE
|
||||||
// 调料配置模式隐藏重量和状态行
|
// 调料配置模式隐藏重量和状态行
|
||||||
if (showNameOnly) {
|
if (showNameOnly) {
|
||||||
b.tvWeight.visibility = View.GONE
|
b.tvWeight.visibility = View.GONE
|
||||||
@@ -75,31 +80,31 @@ class Seasoning18GridAdapter(
|
|||||||
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
|
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
|
||||||
val valid = (item?.address ?: 0) != 0
|
val valid = (item?.address ?: 0) != 0
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
holder.b.tvName.text = if (showNameOnly) "-" else ""
|
holder.b.tvAddress.text = ""
|
||||||
|
holder.b.tvName.text = "-"
|
||||||
holder.b.tvWeight.text = ""
|
holder.b.tvWeight.text = ""
|
||||||
holder.b.tvState.text = ""
|
holder.b.tvState.text = ""
|
||||||
} else {
|
} else {
|
||||||
holder.b.tvName.text = when {
|
holder.b.tvAddress.text = "秤${item!!.address}"
|
||||||
!item!!.name.isNullOrBlank() -> item.name
|
holder.b.tvName.text = if (!item.name.isNullOrBlank()) item.name else "-"
|
||||||
showNameOnly -> "-"
|
holder.b.tvWeight.text = "${item.weight}g"
|
||||||
else -> "秤${item.address}"
|
|
||||||
}
|
|
||||||
holder.b.tvWeight.text = "${item!!.weight}g"
|
|
||||||
holder.b.tvState.text = stateStr(item.state)
|
holder.b.tvState.text = stateStr(item.state)
|
||||||
}
|
}
|
||||||
// 高亮格子:红底白字;其余恢复默认
|
|
||||||
if (position == highlightedPosition) {
|
if (position == highlightedPosition) {
|
||||||
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
|
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
|
||||||
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
|
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
|
||||||
|
holder.b.tvAddress.setTextColor(white)
|
||||||
holder.b.tvName.setTextColor(white)
|
holder.b.tvName.setTextColor(white)
|
||||||
holder.b.tvWeight.setTextColor(white)
|
holder.b.tvWeight.setTextColor(white)
|
||||||
holder.b.tvState.setTextColor(white)
|
holder.b.tvState.setTextColor(white)
|
||||||
} else {
|
} else {
|
||||||
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
|
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
|
||||||
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
|
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
|
||||||
holder.b.tvName.setTextColor(normal)
|
val sub = ContextCompat.getColor(holder.b.root.context, R.color.home_sub_title)
|
||||||
|
holder.b.tvAddress.setTextColor(sub)
|
||||||
|
holder.b.tvName.setTextColor(sub)
|
||||||
holder.b.tvWeight.setTextColor(normal)
|
holder.b.tvWeight.setTextColor(normal)
|
||||||
holder.b.tvState.setTextColor(normal)
|
holder.b.tvState.setTextColor(sub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import com.shuwei.dish.match.utils.ext.dp
|
|||||||
* @param smallSize 其余小格子的尺寸(px)
|
* @param smallSize 其余小格子的尺寸(px)
|
||||||
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState;
|
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState;
|
||||||
* false = 秤总览模式,三行全部显示
|
* false = 秤总览模式,三行全部显示
|
||||||
|
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
|
||||||
*/
|
*/
|
||||||
class Seasoning22GridAdapter(
|
class Seasoning22GridAdapter(
|
||||||
private val largeSize: Int,
|
private val largeSize: Int,
|
||||||
private val smallSize: Int,
|
private val smallSize: Int,
|
||||||
private val showNameOnly: Boolean = false
|
private val showNameOnly: Boolean = false,
|
||||||
|
private val showAddress: Boolean = false
|
||||||
) : BaseQuickAdapter<ScaleData, Seasoning22GridAdapter.VH>(mutableListOf()) {
|
) : BaseQuickAdapter<ScaleData, Seasoning22GridAdapter.VH>(mutableListOf()) {
|
||||||
|
|
||||||
/** 空位哨兵,address=0 表示无效 */
|
/** 空位哨兵,address=0 表示无效 */
|
||||||
@@ -55,9 +57,12 @@ class Seasoning22GridAdapter(
|
|||||||
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
|
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
|
||||||
}
|
}
|
||||||
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
|
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
|
||||||
|
b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
|
||||||
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
|
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
|
||||||
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
|
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
|
||||||
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
|
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
|
||||||
|
// 秤地址行:仅 MasterScaleActivity 显示
|
||||||
|
b.tvAddress.visibility = if (showAddress) View.VISIBLE else View.GONE
|
||||||
// 调料配置模式隐藏重量和状态行
|
// 调料配置模式隐藏重量和状态行
|
||||||
if (showNameOnly) {
|
if (showNameOnly) {
|
||||||
b.tvWeight.visibility = View.GONE
|
b.tvWeight.visibility = View.GONE
|
||||||
@@ -77,32 +82,32 @@ class Seasoning22GridAdapter(
|
|||||||
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
|
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
|
||||||
val valid = (item?.address ?: 0) != 0
|
val valid = (item?.address ?: 0) != 0
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
holder.b.tvName.text = if (showNameOnly) "-" else ""
|
holder.b.tvAddress.text = ""
|
||||||
|
holder.b.tvName.text = "-"
|
||||||
holder.b.tvWeight.text = ""
|
holder.b.tvWeight.text = ""
|
||||||
holder.b.tvState.text = ""
|
holder.b.tvState.text = ""
|
||||||
} else {
|
} else {
|
||||||
// 有名称显示名称,无名称:调料配置显示"-",秤总览显示秤地址编号
|
holder.b.tvAddress.text = "秤${item!!.address}"
|
||||||
holder.b.tvName.text = when {
|
holder.b.tvName.text = if (!item.name.isNullOrBlank()) item.name else "-"
|
||||||
!item!!.name.isNullOrBlank() -> item.name
|
holder.b.tvWeight.text = "${item.weight}g"
|
||||||
showNameOnly -> "-"
|
|
||||||
else -> "秤${item.address}"
|
|
||||||
}
|
|
||||||
holder.b.tvWeight.text = "${item!!.weight}g"
|
|
||||||
holder.b.tvState.text = stateStr(item.state)
|
holder.b.tvState.text = stateStr(item.state)
|
||||||
}
|
}
|
||||||
// 高亮格子:红底白字;其余恢复默认
|
// 高亮格子:红底白字;其余恢复默认
|
||||||
if (position == highlightedPosition) {
|
if (position == highlightedPosition) {
|
||||||
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
|
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
|
||||||
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
|
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
|
||||||
|
holder.b.tvAddress.setTextColor(white)
|
||||||
holder.b.tvName.setTextColor(white)
|
holder.b.tvName.setTextColor(white)
|
||||||
holder.b.tvWeight.setTextColor(white)
|
holder.b.tvWeight.setTextColor(white)
|
||||||
holder.b.tvState.setTextColor(white)
|
holder.b.tvState.setTextColor(white)
|
||||||
} else {
|
} else {
|
||||||
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
|
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
|
||||||
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
|
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
|
||||||
holder.b.tvName.setTextColor(normal)
|
val sub = ContextCompat.getColor(holder.b.root.context, R.color.home_sub_title)
|
||||||
|
holder.b.tvAddress.setTextColor(sub)
|
||||||
|
holder.b.tvName.setTextColor(sub)
|
||||||
holder.b.tvWeight.setTextColor(normal)
|
holder.b.tvWeight.setTextColor(normal)
|
||||||
holder.b.tvState.setTextColor(normal)
|
holder.b.tvState.setTextColor(sub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import androidx.room.RoomDatabase
|
|||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.shuwei.dish.match.db.dao.AppDao
|
import com.shuwei.dish.match.db.dao.AppDao
|
||||||
|
import com.shuwei.dish.match.db.dao.SeasoningSlotDao
|
||||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||||
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
|
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
|
||||||
import com.shuwei.dish.match.entity.SeasoningEntity
|
import com.shuwei.dish.match.entity.SeasoningEntity
|
||||||
|
import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
||||||
|
|
||||||
|
|
||||||
// 步骤1:更新版本号
|
// 步骤1:更新版本号
|
||||||
@@ -19,8 +21,9 @@ import com.shuwei.dish.match.entity.SeasoningEntity
|
|||||||
SeasoningEntity::class,
|
SeasoningEntity::class,
|
||||||
CookFoodEntity::class,
|
CookFoodEntity::class,
|
||||||
CookFoodGoodsEntity::class,
|
CookFoodGoodsEntity::class,
|
||||||
|
SeasoningSlotEntity::class,
|
||||||
],
|
],
|
||||||
version = 7,
|
version = 8,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -30,6 +33,8 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
abstract fun appDao(): AppDao
|
abstract fun appDao(): AppDao
|
||||||
|
|
||||||
|
abstract fun seasoningSlotDao(): SeasoningSlotDao
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤2:创建Migration对象
|
// 步骤2:创建Migration对象
|
||||||
@@ -140,6 +145,20 @@ val MIGRATION_5_6 = MigrationImpl(5, 6) { db ->
|
|||||||
db.endTransaction()
|
db.endTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_7_8 = MigrationImpl(7, 8) { db ->
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `dm_seasoning_slot` (
|
||||||
|
`deviceId` TEXT NOT NULL,
|
||||||
|
`address` INTEGER NOT NULL,
|
||||||
|
`goodsId` TEXT NOT NULL,
|
||||||
|
`goodsName` TEXT NOT NULL,
|
||||||
|
PRIMARY KEY(`deviceId`, `address`)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val MIGRATION_6_7 = MigrationImpl(6, 7) {db ->
|
val MIGRATION_6_7 = MigrationImpl(6, 7) {db ->
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|
||||||
@@ -210,6 +229,7 @@ class DatabaseProvider(private val context: Context) {
|
|||||||
// .addMigrations(MIGRATION_4_5)
|
// .addMigrations(MIGRATION_4_5)
|
||||||
// .addMigrations(MIGRATION_5_6)
|
// .addMigrations(MIGRATION_5_6)
|
||||||
// .addMigrations(MIGRATION_6_7)
|
// .addMigrations(MIGRATION_6_7)
|
||||||
|
.addMigrations(MIGRATION_7_8)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.shuwei.dish.match.db.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.shuwei.dish.match.entity.SeasoningSlotEntity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调料槽位配置 DAO
|
||||||
|
* 以 (deviceId, address) 为主键做 upsert,保证同一槽位只保留最新配置
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface SeasoningSlotDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入或更新单个槽位配置(主键冲突时覆盖)
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsert(slot: SeasoningSlotEntity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量插入或更新槽位配置(主设备广播全量时使用)
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertAll(slots: List<SeasoningSlotEntity>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定设备的全部槽位配置
|
||||||
|
* @param deviceId 目标设备 ID
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM dm_seasoning_slot WHERE deviceId = :deviceId")
|
||||||
|
suspend fun queryByDeviceId(deviceId: String): List<SeasoningSlotEntity>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有设备的全部槽位配置(主设备广播时使用)
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM dm_seasoning_slot")
|
||||||
|
suspend fun queryAll(): List<SeasoningSlotEntity>
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.shuwei.dish.match.entity
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调料槽位配置表,记录每个格子对应哪种调料
|
||||||
|
* 主键为 (deviceId, address) 组合,唯一标识一个物理秤槽位
|
||||||
|
*
|
||||||
|
* @param deviceId 所属设备 ID,用于区分 22格/18格 设备
|
||||||
|
* @param address 秤硬件地址,对应 ScaleDeviceConfig.SCALE_ORDER_22/18 中的值
|
||||||
|
* @param goodsId 调料 ID
|
||||||
|
* @param goodsName 调料名称,用于 adapter 展示
|
||||||
|
*/
|
||||||
|
@Entity(
|
||||||
|
tableName = "dm_seasoning_slot",
|
||||||
|
primaryKeys = ["deviceId", "address"]
|
||||||
|
)
|
||||||
|
data class SeasoningSlotEntity(
|
||||||
|
val deviceId: String,
|
||||||
|
val address: Int,
|
||||||
|
val goodsId: String,
|
||||||
|
val goodsName: String
|
||||||
|
)
|
||||||
@@ -1,20 +1,38 @@
|
|||||||
package com.shuwei.dish.match.scale
|
package com.shuwei.dish.match.scale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 秤事件数据类,用于子设备向主设备发送非重量类通知
|
* 秤事件数据类,用于主设备和子设备之间的非重量类通知
|
||||||
* @param type 事件类型,参见 companion object 中的常量
|
* @param type 事件类型,参见 companion object 中的常量
|
||||||
* @param deviceId 发送方设备 ID
|
* @param deviceId 发送方设备 ID
|
||||||
* @param address 触发事件的秤地址
|
* @param address 触发事件的秤地址,TYPE_SEASONING_CONFIG 时为 0(无意义)
|
||||||
* @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义
|
* @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义
|
||||||
|
* @param slots 调料槽位配置列表,仅 TYPE_SEASONING_CONFIG 时有效
|
||||||
*/
|
*/
|
||||||
data class ScaleEvent(
|
data class ScaleEvent(
|
||||||
val type: String,
|
val type: String,
|
||||||
val deviceId: String,
|
val deviceId: String,
|
||||||
val address: Int,
|
val address: Int = 0,
|
||||||
val delta: Double = 0.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 {
|
companion object {
|
||||||
/** 调料添加事件:某秤重量增加超过阈值 */
|
/** 调料添加事件:某秤重量增加超过阈值 */
|
||||||
const val TYPE_SEASONING_ADDED = "seasoning_added"
|
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.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.shuwei.dish.match.base.BaseApp
|
||||||
import com.shuwei.dish.match.base.DeviceRole
|
import com.shuwei.dish.match.base.DeviceRole
|
||||||
import com.shuwei.dish.match.base.GlobalData
|
import com.shuwei.dish.match.base.GlobalData
|
||||||
import com.shuwei.dish.match.utils.NetworkUtil
|
import com.shuwei.dish.match.utils.NetworkUtil
|
||||||
import com.shuwei.dish.match.utils.WeightUtil
|
import com.shuwei.dish.match.utils.WeightUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 秤服务统一门面(单例)
|
* 秤服务统一门面(单例)
|
||||||
@@ -59,6 +63,36 @@ object ScaleServiceManager {
|
|||||||
wsClient?.onScaleEvent = value
|
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 要广播的事件
|
* @param event 要广播的事件
|
||||||
@@ -123,6 +157,19 @@ object ScaleServiceManager {
|
|||||||
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
|
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
|
||||||
it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) }
|
it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) }
|
||||||
it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) }
|
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
|
wsClient = client
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class ScaleWebSocketClient {
|
|||||||
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */
|
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */
|
||||||
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
|
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
|
||||||
|
|
||||||
|
/** 子设备首次连接成功时的回调,在子线程调用;可用于主设备主动推送全量配置 */
|
||||||
|
var onDeviceConnected: ((deviceId: String) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接到指定子设备
|
* 连接到指定子设备
|
||||||
* @param deviceId 子设备 ID
|
* @param deviceId 子设备 ID
|
||||||
@@ -79,6 +82,7 @@ class ScaleWebSocketClient {
|
|||||||
connections[deviceId] = webSocket
|
connections[deviceId] = webSocket
|
||||||
reconnectDelays[deviceId] = RECONNECT_BASE_MS
|
reconnectDelays[deviceId] = RECONNECT_BASE_MS
|
||||||
reconnectTasks.remove(deviceId)?.cancel(false)
|
reconnectTasks.remove(deviceId)?.cancel(false)
|
||||||
|
onDeviceConnected?.invoke(deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
@@ -148,6 +152,31 @@ class ScaleWebSocketClient {
|
|||||||
return ws.send(gson.toJson(command))
|
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
|
* @param deviceId 子设备 ID
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
|||||||
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
|
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
|
||||||
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
|
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
|
||||||
|
|
||||||
|
/** 收到主设备下发的调料配置同步事件时的回调,在子线程调用 */
|
||||||
|
var onSeasoningConfig: ((event: ScaleEvent) -> Unit)? = null
|
||||||
|
|
||||||
/** 当前是否有主设备连接 */
|
/** 当前是否有主设备连接 */
|
||||||
val isConnected: Boolean
|
val isConnected: Boolean
|
||||||
get() = server?.connections?.isNotEmpty() == true
|
get() = server?.connections?.isNotEmpty() == true
|
||||||
@@ -72,6 +75,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
|||||||
*/
|
*/
|
||||||
fun stop() {
|
fun stop() {
|
||||||
WeightUtil.removeWeightListener(TAG)
|
WeightUtil.removeWeightListener(TAG)
|
||||||
|
onConnectionChanged = null
|
||||||
|
onSeasoningConfig = null
|
||||||
try {
|
try {
|
||||||
server?.stop(1000)
|
server?.stop(1000)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -135,6 +140,15 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
|||||||
|
|
||||||
override fun onMessage(conn: WebSocket, message: String) {
|
override fun onMessage(conn: WebSocket, message: String) {
|
||||||
try {
|
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)
|
val cmd = gson.fromJson(message, ScaleCommand::class.java)
|
||||||
// 校验指令目标设备是否为本机
|
// 校验指令目标设备是否为本机
|
||||||
if (cmd.deviceId != deviceId) {
|
if (cmd.deviceId != deviceId) {
|
||||||
@@ -148,7 +162,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "收到清零指令, 解析指令失败: ${e.message}")
|
Log.w(TAG, "收到消息, 解析失败: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ import com.shuwei.dish.match.databinding.ListTypeScale1Binding
|
|||||||
import com.shuwei.dish.match.databinding.ListTypeScale18Binding
|
import com.shuwei.dish.match.databinding.ListTypeScale18Binding
|
||||||
import com.shuwei.dish.match.databinding.ListTypeScale2Binding
|
import com.shuwei.dish.match.databinding.ListTypeScale2Binding
|
||||||
import com.shuwei.dish.match.databinding.ListTypeScale22Binding
|
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.ScaleData
|
||||||
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
||||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
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.NetworkUtil
|
||||||
import com.shuwei.dish.match.utils.WeightUtil
|
import com.shuwei.dish.match.utils.WeightUtil
|
||||||
import com.shuwei.dish.match.utils.ext.dp
|
import com.shuwei.dish.match.utils.ext.dp
|
||||||
@@ -59,6 +62,9 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
private val groupList = mutableListOf<DeviceGroup>()
|
private val groupList = mutableListOf<DeviceGroup>()
|
||||||
private val adapter = DeviceGroupAdapter(groupList)
|
private val adapter = DeviceGroupAdapter(groupList)
|
||||||
|
|
||||||
|
/** key = "$deviceId#$address",value = 调料名称,用于注入 ScaleData.name */
|
||||||
|
private val slotNameMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMasterScaleBinding.inflate(layoutInflater)
|
binding = ActivityMasterScaleBinding.inflate(layoutInflater)
|
||||||
@@ -70,8 +76,27 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
binding.rvScaleList.itemAnimator = null
|
binding.rvScaleList.itemAnimator = null
|
||||||
binding.rvScaleList.adapter = adapter
|
binding.rvScaleList.adapter = adapter
|
||||||
|
|
||||||
|
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()
|
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() {
|
private fun observeScaleData() {
|
||||||
val flow = ScaleServiceManager.allScales ?: return
|
val flow = ScaleServiceManager.allScales ?: return
|
||||||
@@ -90,7 +115,7 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
ip = if (deviceId == localId) localIp
|
ip = if (deviceId == localId) localIp
|
||||||
else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "",
|
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
|
alignItems = AlignItems.FLEX_START
|
||||||
}
|
}
|
||||||
b.rvScales.itemAnimator = null
|
b.rvScales.itemAnimator = null
|
||||||
innerAdapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false).apply {
|
innerAdapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false, showAddress = true).apply {
|
||||||
onItemClick = { scale, _ ->
|
onItemClick = { scale, _ ->
|
||||||
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
|
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
|
||||||
}
|
}
|
||||||
@@ -303,7 +328,7 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
b.rvScales.layoutManager = gridLayoutManager
|
b.rvScales.layoutManager = gridLayoutManager
|
||||||
b.rvScales.itemAnimator = null
|
b.rvScales.itemAnimator = null
|
||||||
innerAdapter = Seasoning18GridAdapter(showNameOnly = false).apply {
|
innerAdapter = Seasoning18GridAdapter(showNameOnly = false, showAddress = true).apply {
|
||||||
onItemClick = { scale, _ ->
|
onItemClick = { scale, _ ->
|
||||||
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
|
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
|
||||||
}
|
}
|
||||||
@@ -369,6 +394,8 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
setBackgroundResource(R.drawable.shape_scale_cell)
|
setBackgroundResource(R.drawable.shape_scale_cell)
|
||||||
val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt()
|
val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt()
|
||||||
addView(makeCellText("秤${scale.address}", (baseSize * 0.16f).coerceIn(9f, 14f).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("${scale.weight}g", (baseSize * 0.22f).coerceIn(12f, 20f).toInt(), bold = true))
|
||||||
addView(makeCellText(stateStr(scale.state), (baseSize * 0.14f).coerceIn(8f, 12f).toInt()))
|
addView(makeCellText(stateStr(scale.state), (baseSize * 0.14f).coerceIn(8f, 12f).toInt()))
|
||||||
tag = scale
|
tag = scale
|
||||||
@@ -379,8 +406,9 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) {
|
private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) {
|
||||||
(cell.getChildAt(0) as? TextView)?.text = "秤${scale.address}"
|
(cell.getChildAt(0) as? TextView)?.text = "秤${scale.address}"
|
||||||
(cell.getChildAt(1) as? TextView)?.text = "${scale.weight}g"
|
(cell.getChildAt(1) as? TextView)?.text = if (scale.address == 1) "净材" else "熟食"
|
||||||
(cell.getChildAt(2) as? TextView)?.text = stateStr(scale.state)
|
(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 =
|
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 androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.flexbox.AlignItems
|
import com.google.android.flexbox.AlignItems
|
||||||
import com.google.android.flexbox.FlexDirection
|
import com.google.android.flexbox.FlexDirection
|
||||||
import com.google.android.flexbox.FlexWrap
|
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.Seasoning22GridAdapter
|
||||||
import com.shuwei.dish.match.adapter.ScaleRowAdapter
|
import com.shuwei.dish.match.adapter.ScaleRowAdapter
|
||||||
import com.shuwei.dish.match.base.BaseActivity
|
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.base.GlobalData
|
||||||
import com.shuwei.dish.match.databinding.ActivitySlaveBinding
|
import com.shuwei.dish.match.databinding.ActivitySlaveBinding
|
||||||
import com.shuwei.dish.match.dialog.CommonDialog
|
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.ScaleData
|
||||||
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
import com.shuwei.dish.match.scale.ScaleDeviceConfig
|
||||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
import com.shuwei.dish.match.scale.ScaleServiceManager
|
||||||
import com.shuwei.dish.match.utils.NetworkUtil
|
import com.shuwei.dish.match.utils.NetworkUtil
|
||||||
import com.shuwei.dish.match.utils.WeightUtil
|
import com.shuwei.dish.match.utils.WeightUtil
|
||||||
import com.shuwei.dish.match.utils.ext.dp
|
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 */
|
/** 各秤地址的上一次重量快照,用于计算 delta */
|
||||||
private val weightSnapshot = mutableMapOf<Int, Double>()
|
private val weightSnapshot = mutableMapOf<Int, Double>()
|
||||||
|
/** key = address,value = 调料名称,用于注入 ScaleData.name,避免重量更新时覆盖名称 */
|
||||||
|
private val slotNameMap = mutableMapOf<Int, String>()
|
||||||
/** 主线程 Handler,用于延迟清除高亮 */
|
/** 主线程 Handler,用于延迟清除高亮 */
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
|
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
|
||||||
@@ -65,8 +73,10 @@ class SlaveActivity : BaseActivity() {
|
|||||||
|
|
||||||
setupScaleList()
|
setupScaleList()
|
||||||
updateMasterStatus(ScaleServiceManager.isMasterConnected)
|
updateMasterStatus(ScaleServiceManager.isMasterConnected)
|
||||||
|
loadSlotsFromDb()
|
||||||
listenLocalScales()
|
listenLocalScales()
|
||||||
listenMasterConnection()
|
listenMasterConnection()
|
||||||
|
listenSeasoningConfig()
|
||||||
addBackKeyListener()
|
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 回调,实时刷新秤列表
|
* 监听本机 WeightUtil 回调,实时刷新秤列表
|
||||||
* 22/18个秤走网格 adapter,其余走线性列表
|
* 22/18个秤走网格 adapter,其余走线性列表
|
||||||
@@ -164,14 +223,14 @@ class SlaveActivity : BaseActivity() {
|
|||||||
// 用最新数据更新缓存后整体刷新网格
|
// 用最新数据更新缓存后整体刷新网格
|
||||||
val existing = scale22Adapter!!.items.toMutableList()
|
val existing = scale22Adapter!!.items.toMutableList()
|
||||||
val idx = existing.indexOfFirst { it.address == address }
|
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)
|
if (idx >= 0) existing[idx] = newData else existing.add(newData)
|
||||||
scale22Adapter!!.updateByAddress(existing)
|
scale22Adapter!!.updateByAddress(existing)
|
||||||
}
|
}
|
||||||
scale18Adapter != null -> {
|
scale18Adapter != null -> {
|
||||||
val existing = scale18Adapter!!.items.toMutableList()
|
val existing = scale18Adapter!!.items.toMutableList()
|
||||||
val idx = existing.indexOfFirst { it.address == address }
|
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)
|
if (idx >= 0) existing[idx] = newData else existing.add(newData)
|
||||||
scale18Adapter!!.updateByAddress(existing)
|
scale18Adapter!!.updateByAddress(existing)
|
||||||
}
|
}
|
||||||
@@ -219,6 +278,7 @@ class SlaveActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
ScaleServiceManager.onMasterConnectionChanged = null
|
ScaleServiceManager.onMasterConnectionChanged = null
|
||||||
|
ScaleServiceManager.onSeasoningConfig = null
|
||||||
WeightUtil.removeWeightListener(TAG)
|
WeightUtil.removeWeightListener(TAG)
|
||||||
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
|
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -13,18 +13,23 @@ import com.google.android.flexbox.FlexWrap
|
|||||||
import com.google.android.flexbox.FlexboxLayoutManager
|
import com.google.android.flexbox.FlexboxLayoutManager
|
||||||
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
|
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
|
||||||
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
|
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.BaseFragment
|
||||||
import com.shuwei.dish.match.base.DeviceRole
|
import com.shuwei.dish.match.base.DeviceRole
|
||||||
import com.shuwei.dish.match.base.GlobalData
|
import com.shuwei.dish.match.base.GlobalData
|
||||||
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
|
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
|
||||||
import com.shuwei.dish.match.dialog.SeasoningSelectDialog
|
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.ScaleDeviceConfig
|
||||||
import com.shuwei.dish.match.scale.ScaleEvent
|
import com.shuwei.dish.match.scale.ScaleEvent
|
||||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
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.ui.SettingActivity
|
||||||
import com.shuwei.dish.match.utils.WeightUtil
|
import com.shuwei.dish.match.utils.WeightUtil
|
||||||
import com.shuwei.dish.match.utils.ext.dp
|
import com.shuwei.dish.match.utils.ext.dp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
||||||
|
|
||||||
@@ -65,26 +70,58 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
|||||||
|
|
||||||
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
|
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
|
||||||
private fun bindAdapterClicks() {
|
private fun bindAdapterClicks() {
|
||||||
adapter22.onItemClick = { _, position ->
|
adapter22.onItemClick = { scaleData, position ->
|
||||||
SeasoningSelectDialog(
|
SeasoningSelectDialog(
|
||||||
activity = settingActivity,
|
activity = settingActivity,
|
||||||
clickIndex = position,
|
clickIndex = position,
|
||||||
onItemSelected = { item ->
|
onItemSelected = { item ->
|
||||||
adapter22.updateItemName(position, item.goodsName ?: "")
|
adapter22.updateItemName(position, item.goodsName ?: "")
|
||||||
|
saveSlotAndBroadcast(
|
||||||
|
SeasoningSlotEntity(
|
||||||
|
deviceId = ScaleDeviceConfig.DEVICE_ID_22,
|
||||||
|
address = scaleData.address,
|
||||||
|
goodsId = item.goodsId,
|
||||||
|
goodsName = item.goodsName ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
adapter18.onItemClick = { _, position ->
|
adapter18.onItemClick = { scaleData, position ->
|
||||||
SeasoningSelectDialog(
|
SeasoningSelectDialog(
|
||||||
activity = settingActivity,
|
activity = settingActivity,
|
||||||
clickIndex = position,
|
clickIndex = position,
|
||||||
onItemSelected = { item ->
|
onItemSelected = { item ->
|
||||||
adapter18.updateItemName(position, item.goodsName ?: "")
|
adapter18.updateItemName(position, item.goodsName ?: "")
|
||||||
|
saveSlotAndBroadcast(
|
||||||
|
SeasoningSlotEntity(
|
||||||
|
deviceId = ScaleDeviceConfig.DEVICE_ID_18,
|
||||||
|
address = scaleData.address,
|
||||||
|
goodsId = item.goodsId,
|
||||||
|
goodsName = item.goodsName ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
).show()
|
).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 */
|
// /** 初始化 ViewModel */
|
||||||
// private fun initViewModel() {
|
// private fun initViewModel() {
|
||||||
// val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao()))
|
// val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao()))
|
||||||
@@ -126,10 +163,25 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加载默认空数据,使两个列表完整展示所有格子 */
|
/** 从 Room 加载已配置的槽位名称,填充到对应格子 */
|
||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
adapter22.update(emptyList())
|
adapter22.update(emptyList())
|
||||||
adapter18.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 ->
|
// appViewModel.loadSeasoning { list ->
|
||||||
// adapter22.update(list ?: emptyList())
|
// adapter22.update(list ?: emptyList())
|
||||||
// adapter18.update(list ?: emptyList())
|
// adapter18.update(list ?: emptyList())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- 单个秤格子:地址 + 重量 + 状态 -->
|
<!-- 单个秤格子:秤地址 + 调料名 + 重量 + 状态 -->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -8,6 +8,15 @@
|
|||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
android:background="@drawable/shape_scale_cell">
|
android:background="@drawable/shape_scale_cell">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAddress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textColor="@color/home_sub_title"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvName"
|
android:id="@+id/tvName"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
Reference in New Issue
Block a user