refactor: 全面重构 HomeActivity 架构并修复多项 Bug

- 新增 SerialProtocolHandler 类,将串口协议解析逻辑从 HomeActivity 剥离
- ProtocolConstants 补充命令码常量(CMD_*)及帧解析偏移常量
- 修复 NetViewModel.getDeviceConfig 更新错误 StateFlow 的 Bug
- 修复 getWeightInfo 中 toInt() 整数溢出崩溃风险,改用 toLongOrNull(16)
- 升级废弃的 getSerializableExtra,使用 Build.VERSION.SDK_INT 版本分支
- BaseActivity 提取 onUserInactive() 虚方法,消除对子类 HomeActivity 的直接引用
- 修复 BaseActivity 中错误的 LIGHT_OPEN/CLOSE 导入(改从 ProtocolConstants 导入)
- repeatOnLifecycle 由 CREATED 改为 STARTED,避免 UI 不可见时无效更新
- updateOverdueState 从回调风格改为 StateFlow,与项目其他接口保持一致
- 提取 syncShelfGoodsToServer() 消除重复的 ShelfBody 构建逻辑
- 清理 NetViewModel 和 BaseActivity 中大段注释死代码

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mazengfei
2026-04-01 11:26:05 +08:00
co-authored by Claude Sonnet 4.6
parent 9a795b9710
commit 04fe3ec2c1
5 changed files with 462 additions and 369 deletions
@@ -3,10 +3,10 @@ package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.SparseIntArray
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@@ -28,23 +28,19 @@ import com.shuwei.intelligent.shelves.net.Loading
import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.UiState
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVE_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.C_TEMP_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LEFT_SHELF_OPEN_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.RIGHT_SHELF_OPEN_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.START_TEMP_CTRL_CMD
import com.shuwei.intelligent.shelves.serial.SerialFrameParser
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.serial.SerialProtocolHandler
import com.shuwei.intelligent.shelves.utils.IntervalExecutor
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible
import com.shuwei.intelligent.shelves.utils.hexToBinary
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
@@ -53,22 +49,16 @@ import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
class HomeActivity : BaseActivity() {
companion object {
private const val TAG = "HomeActivity"
private const val LOG_PREFIX = "receiveSerialPortData:收到"
private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
}
// 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf()
// 各传感器重量数据,key 为货架编号
private val weightArray = SparseIntArray()
// 当前选中货架下标
private var shelfIndex = 0
@@ -81,10 +71,47 @@ class HomeActivity : BaseActivity() {
// 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false
private var isNeedOpenLock = true
private var deviceName = ""
private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels()
// 串口协议解析器,通过回调通知本 Activity
private val serialProtocolHandler = SerialProtocolHandler(object : SerialProtocolHandler.Callback {
override fun onSendCmd(cmd: String) = sendCmd(cmd)
override fun onWeightUpdated(shelfNo: Int, weight: Int) {
if (shelfNo in 1..10 && list.size == 10) {
list.firstOrNull { it.deviceNo == shelfNo }?.let { model ->
model.weight = weight.toDouble()
val pos = list.indexOf(model)
shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: deviceNo=${model.deviceNo},realWeight=$weight")
EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight))
}
}
}
override fun onTempReport(logMsg: String) {
updateLeftStatus(deviceName)
if (tipDialog == null) {
tipDialog = CommonDialog(this@HomeActivity).apply {
dialogTitle = "温馨提示"
confirmBlock = { logMsg.copyTextToClipboard(this@HomeActivity) }
}
}
if (tipDialog?.isShowing == false) {
tipDialog?.dialogContent = logMsg
tipDialog?.show()
}
}
override fun onLog(message: String) = log(message)
})
private val serialFrameParser = SerialFrameParser()
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -94,8 +121,8 @@ class HomeActivity : BaseActivity() {
setContentView(binding.root)
updateLeftStatus("")
initRecyclerView()
// 串口初始化,独立协程,不阻塞网络状态监听
lifecycleScope.launch {
val openState = SerialPortManager.open()
@@ -110,9 +137,10 @@ class HomeActivity : BaseActivity() {
}
}
}
// 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.getAccessTokenUiState.collect { state ->
when (state) {
@@ -133,8 +161,18 @@ class HomeActivity : BaseActivity() {
}
}
}
launch {
viewModel.updateOverdueStateUiState.collect { state ->
if (state is UiState.Success<*>) {
val respData = state.data as? com.shuwei.intelligent.shelves.net.RespData<*> ?: return@collect
val items = (respData.data as? ShelfResult)?.containerGoodsList ?: return@collect
applyOverdueState(items)
}
}
}
}
}
binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD)
@@ -167,8 +205,7 @@ class HomeActivity : BaseActivity() {
return
}
deviceName = data.data.deviceName
updateLeftStatus("$deviceName")
updateLeftStatus(deviceName)
App.canteenId = data.data.placeId
val tempList = data.data.containerGoodsList
@@ -192,135 +229,13 @@ class HomeActivity : BaseActivity() {
loadEmptyView()
}
/**
* 终端id
*/
private var terminalId: String = ""
/**
* 固件版本
*/
private var terminalVersion: String = ""
private val serialFrameParser = SerialFrameParser()
private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData")
// 重置无数据计时,并关闭已显示的超时提示弹窗
lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
val frame = serialFrameParser.parse(srcData) ?: return
handleReceivedData(frame)
}
private fun handleReceivedData(data: String) {
if (data.isEmpty()) {
return
}
val cmd = data.substring(HEADER.length, HEADER.length + 4)
val cmdFlag = cmd.substring(0, 2)
when (cmd) {
"0001" -> {
//终端发起激活指令,拿到固定数据体(终端ID-15字节、固件版本-5字节、命令次数-1字节)
val reqBody =
data.substring(HEADER.length + cmd.length, data.length - FOOTER.length)
if (reqBody.length == 44) {
terminalId = reqBody.substring(2, 32)
terminalVersion = reqBody.substring(32, 42)
log("${LOG_PREFIX}[激活]${cmdFlag}指令:${data},终端id:${terminalId},固件版本:${terminalVersion},发送激活命令:$ACTIVE_CMD")
sendCmd(ACTIVE_CMD)
}
}
"0101", "8101" -> {
//心跳0102${p1}${p2}${version}
//心跳81(终端发起,当终端发送此命令时需要服务端存储串口屏显示信息)
val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[心跳]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
heartBeat(data)
}
"0202" -> {
//开关锁反馈020200${deviceId}${version}
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0302" -> {
//获取设备状态
log("${LOG_PREFIX}[获取设备状态]${cmdFlag}指令:${data}")
getDeviceInfo(data)
}
"0501" -> {
//卡号上发
val respCmd = "${HEADER}050203$FOOTER"
log("${LOG_PREFIX}[卡号上发]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0B01" -> {
//操作记录上传
val respCmd = "${HEADER}0B0203$FOOTER"
log("${LOG_PREFIX}[操作记录上传]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0C01" -> {
//对时
val nowMs = System.currentTimeMillis()
val timeLong = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
val timeStr = "0000000" + String.format("%X", timeLong)
val time =
timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
val respCmd = "${HEADER}0B02${time}$FOOTER"
log("${LOG_PREFIX}[对时]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0E01" -> {
//终端位置上报
val respCmd = "${HEADER}0E0200$FOOTER"
log("${LOG_PREFIX}[终端位置上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"1001" -> {
//温控信息上报
val respCmd = "${HEADER}100200$FOOTER"
val logMsg =
"${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}"
log(logMsg)
sendCmd(respCmd)
updateLeftStatus("$deviceName")
if (tipDialog == null) {
tipDialog = CommonDialog(this).apply {
dialogTitle = "温馨提示"
confirmBlock = {
logMsg.copyTextToClipboard(this@HomeActivity)
}
}
}
if (tipDialog?.isShowing == false) {
tipDialog?.dialogContent = logMsg
tipDialog?.show()
}
}
"1002" -> {
log("${LOG_PREFIX}[温控信息应答]1002数据:${data}")
}
"0F02" -> {
log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}")
}
else -> {}
}
serialProtocolHandler.handle(frame)
}
private var tipDialog: CommonDialog? = null
@@ -353,115 +268,11 @@ class HomeActivity : BaseActivity() {
}
}
private fun heartBeat(data: String) {
runCatching {
if (data.length < 200) {
return
}
val weightStartIndex = 14
val weightEndIndex = weightStartIndex + 12 * 8
getWeightInfo(
start = weightStartIndex,
end = weightEndIndex,
data = data
)
val temperatureStartIndex = 158
val temperatureEndIndex = temperatureStartIndex + 2
getFixDataBody(data, temperatureEndIndex)
}.onFailure {
it.printStackTrace()
}
}
private var deviceName = ""
private fun getWeightInfo(start: Int, end: Int, data: String) {
var index = 1
var count = start
while (count < end) {
val weightInfo = data.substring(count, count + 8)
var weightHex = ""
weightInfo.run {
weightHex =
substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2)
}
var realWeight = 0
var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
if (firstByteBinary.startsWith("0")) {
//正数
realWeight = weightHex.toInt()
} else {
//负数
firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2)
realWeight = -1 * weightHex.toInt()
}
weightArray.put(index, realWeight)
if (index in 1..10 && list.size == 10) {
list.firstOrNull { it.deviceNo == index }?.let {
it.weight = realWeight.toDouble()
val pos = list.indexOf(it)
shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}")
EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight))
}
}
count += 8
index++
}
}
private fun getDeviceInfo(data: String) {
log("getDeviceInfo: data=$data")
val weightStartIndex = HEADER.length + 14
val temperatureStartIndex = weightStartIndex + 12 * 8
val temperatureEndIndex = temperatureStartIndex + 2
val temperatureHex = try {
data.substring(temperatureStartIndex, temperatureEndIndex)
} catch (e: Exception) {
e.printStackTrace()
return
}
updateLeftStatus("$deviceName")
getWeightInfo(
start = weightStartIndex,
end = temperatureStartIndex,
data = data
)
getFixDataBody(data, temperatureEndIndex)
}
/**
* 从心跳帧或设备状态帧中补充解析 terminalId / terminalVersion。
* 激活指令(0001)是获取这两个字段的首选来源;
* 若激活指令尚未到来,则通过此方法从心跳/设备状态帧中兜底解析。
*
* @param data 完整协议帧字符串
* @param temperatureEndIndex 温度字段结束位置(固定数据体紧随其后)
* 用户超时无操作时关灯
*/
private fun getFixDataBody(data: String, temperatureEndIndex: Int) {
try {
// 已通过激活指令成功解析过,无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) {
return
}
val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
// 固定数据体长度:terminalId 30字节 + terminalVersion 10字节 + 其他2字节 = 42字节
if (fixDataBody.length == 42) {
terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40)
log("receiveSerialPortData:从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}")
}
} catch (e: Exception) {
e.printStackTrace()
}
override fun onUserInactive() {
switchLight(false)
}
override fun onDestroy() {
@@ -504,9 +315,13 @@ class HomeActivity : BaseActivity() {
/**
* 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端
*/
@Suppress("DEPRECATION")
private fun onShelfActivityResult(position: Int, intent: Intent) {
val model = intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel
@Suppress("DEPRECATION")
val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra(ShelfActivity.SHELF_MODEL, ShelfModel::class.java)
} else {
intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel
} ?: return
list[position].apply {
goodsId = model.goodsId
goodsName = model.goodsName
@@ -516,29 +331,13 @@ class HomeActivity : BaseActivity() {
}
}
shelfAdapter.notifyItemChanged(position)
try {
val submitList = list.sortedBy { it.deviceNo }
val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId
body.temperature = showTemperatureC
body.humidity = showHumidity
body.goodsList = submitList
}
log("performSync: body:${body.toJsonString()}")
viewModel.saveShelfGoodsList(body)
} catch (e: Exception) {
e.printStackTrace()
}
syncShelfGoodsToServer()
}
private fun initRecyclerView() {
binding.rvShelf.run {
layoutManager =
GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false)
layoutManager = GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false)
adapter = shelfAdapter
itemAnimator.let {
if (it is DefaultItemAnimator) {
it.supportsChangeAnimations = false
@@ -550,8 +349,7 @@ class HomeActivity : BaseActivity() {
private val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) }
private fun updateDateTime() {
val dateTime = dateTimeFormatter.format(Date())
updateRightStatus(dateTime)
updateRightStatus(dateTimeFormatter.format(Date()))
}
private val handler = Handler(Looper.getMainLooper())
@@ -581,7 +379,6 @@ class HomeActivity : BaseActivity() {
private fun loadEmptyView() {
list.clear()
shelfAdapter.notifyDataSetChanged()
binding.include?.let {
it.root.visible()
it.root.setOnClickListener {
@@ -593,7 +390,7 @@ class HomeActivity : BaseActivity() {
}
@Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) {
fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo }
item?.apply {
goodsId = ""
@@ -607,7 +404,7 @@ class HomeActivity : BaseActivity() {
}
private val taskExecutor by lazy { IntervalExecutor() }
private var saveTaskJob: Job?=null
private var saveTaskJob: Job? = null
/**
* 保存定时任务
@@ -618,34 +415,40 @@ class HomeActivity : BaseActivity() {
}
}
private var overdueTaskJob: Job?=null
private var overdueTaskJob: Job? = null
/**
* 过期时间定时任务
*/
private fun overdueTask() {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60* 1000L) {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) {
updateOverdueState()
}
}
private fun updateOverdueState() {
viewModel.updateOverdueState { state, items ->
if (items.isNullOrEmpty()) return@updateOverdueState
if (items.size != list.size) return@updateOverdueState
list.forEachIndexed { index, model ->
//接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) {
model.overdue = items[index].overdue
shelfAdapter.notifyItemChanged(index)
log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}")
}
viewModel.updateOverdueState()
}
/**
* 将接口返回的过期状态应用到本地列表,仅在状态变化时刷新对应 item
*/
private fun applyOverdueState(items: List<ShelfModel>) {
if (items.size != list.size) return
list.forEachIndexed { index, model ->
// 接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) {
model.overdue = items[index].overdue
shelfAdapter.notifyItemChanged(index)
log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}")
}
}
}
private fun saveShelfGoodsList() {
log("performSync: list:${list.toJsonString()}")
/**
* 构建 ShelfBody 并提交到服务端(被定时任务与 ShelfActivity 返回共用)
*/
private fun syncShelfGoodsToServer() {
val submitList = list.sortedBy { it.deviceNo }
submitList.forEach {
if (it.goodsId.isNullOrBlank()) {
@@ -653,7 +456,7 @@ class HomeActivity : BaseActivity() {
}
}
val body = ShelfBody().also {
it.deviceId = App.deviceId
it.deviceId = App.deviceId ?: ""
it.canteenId = App.canteenId
it.temperature = showTemperatureC
it.humidity = showHumidity
@@ -663,5 +466,8 @@ class HomeActivity : BaseActivity() {
viewModel.saveShelfGoodsList(body)
}
}
private fun saveShelfGoodsList() {
log("performSync: list:${list.toJsonString()}")
syncShelfGoodsToServer()
}
}
@@ -5,20 +5,24 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.shuwei.intelligent.shelves.activity.HomeActivity
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_CLOSE
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_OPEN
import com.shuwei.intelligent.shelves.activity.LogActivity
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.databinding.ActivityBaseBinding
import com.shuwei.intelligent.shelves.databinding.PopupRightStatusMenuBinding
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_CLOSE
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_OPEN
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.FileLogger
import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import kotlinx.coroutines.launch
import androidx.core.view.WindowCompat
@@ -57,38 +61,19 @@ open class BaseActivity : AppCompatActivity() {
}
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// window.setDecorFitsSystemWindows(false)
// window.insetsController?.apply {
// hide(WindowInsets.Type.statusBars()) // 隐藏状态栏
// systemBarsBehavior =
// WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // 滑动时临时显示
// }
// }
lightSwitchClick()
binding.tvLeftStatus.setOnDoubleClickListener {
finish()
}
binding.tvRightStatus.setOnDoubleClickListener {
EnvSwitchDialog(this).show()
showRightStatusPopup(binding.tvRightStatus)
}
}
fun hideStatusBar() {
//// enableEdgeToEdge()
//// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
//// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选)
//// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
//
// ImmersionBar.with(this)
// .hideBar(BarHide.FLAG_HIDE_STATUS_BAR)
// .init()
}
fun hideStatusBar() {}
fun statusBarDarkFont() {
//window.decorView.systemUiVisibility = if (enable) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0
val color = ContextCompat.getColor(this, R.color.white)
binding.tvLeftStatus.setTextColor(color)
binding.tvRightStatus.setTextColor(color)
@@ -118,11 +103,15 @@ open class BaseActivity : AppCompatActivity() {
private val TIME_OUT: Long = (5 * 60 * 1000).toLong()
private val mHandler by lazy { Handler(Looper.getMainLooper()) }
private val mTimeoutRunnable = Runnable {
if (this is HomeActivity) {
this.switchLight(false)
}
// 用户长时间无操作时回调,子类可覆写实现具体逻辑(如关灯)
onUserInactive()
}
/**
* 用户超时无操作时回调,默认空实现,子类按需覆写
*/
open fun onUserInactive() {}
override fun onUserInteraction() {
super.onUserInteraction()
//开灯
@@ -164,15 +153,27 @@ open class BaseActivity : AppCompatActivity() {
val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true
switchLight(tag.not())
}
binding.tvRightStatus.clickWithDebounce {
// if (this is HomeActivity) {
// startActivity<LogActivity> {
// putExtra(LogActivity.IS_LOG_DIR, true)
// }
// return@clickWithDebounce
// }
// startActivity<SettingActivity> { }
}
private fun showRightStatusPopup(anchor: View) {
val menuBinding = PopupRightStatusMenuBinding.inflate(LayoutInflater.from(this))
val popup = PopupWindow(
menuBinding.root,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
menuBinding.tvEnvSwitch.setOnClickListener {
popup.dismiss()
EnvSwitchDialog(this).show()
}
menuBinding.tvLog.setOnClickListener {
popup.dismiss()
startActivity<LogActivity> {
putExtra(LogActivity.IS_LOG_DIR, true)
}
}
popup.showAsDropDown(anchor)
}
@@ -4,33 +4,17 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalData
import com.shuwei.intelligent.shelves.model.ShelfBody
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.net.HttpManager.Companion.instance
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class NetViewModel : ViewModel() {
// val apiService: ApiService = HttpManager.instance.apiService
// val apiService: ApiService = Retrofit.Builder()
// .baseUrl(UrlConfig.BASE_URL)
// .client(HttpManager.instance.client)
// .addConverterFactory(GsonConverterFactory.create())
// .build()
// .create(ApiService::class.java)
private val _getShelfListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getShelfListUiState: StateFlow<UiState> = _getShelfListUiState
// private val _saveShelfGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
// val saveShelfGoodsListUiState: StateFlow<UiState> = _saveShelfGoodsListUiState
private val _getGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getGoodsListUiState: StateFlow<UiState> = _getGoodsListUiState
@@ -43,6 +27,8 @@ class NetViewModel : ViewModel() {
private val _getDeviceConfigUiState = MutableStateFlow<UiState>(UiState.Initial)
val getDeviceConfigUiState: StateFlow<UiState> = _getDeviceConfigUiState
private val _updateOverdueStateUiState = MutableStateFlow<UiState>(UiState.Initial)
val updateOverdueStateUiState: StateFlow<UiState> = _updateOverdueStateUiState
fun getDeviceToken(deviceId: String) {
viewModelScope.launch {
@@ -62,17 +48,16 @@ class NetViewModel : ViewModel() {
fun getDeviceConfig(deviceId: String, deviceToken: String) {
viewModelScope.launch {
_getDeviceTokenUiState.value = UiState.Loading
_getDeviceConfigUiState.value = UiState.Loading
runCatching {
val response =
apiService.getDeviceInfo(equipmentCode = deviceId)
val response = apiService.getDeviceInfo(equipmentCode = deviceId)
if (response.isSuccess()) {
_getDeviceTokenUiState.value = UiState.Success(response)
_getDeviceConfigUiState.value = UiState.Success(response)
} else {
_getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败")
_getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_getDeviceTokenUiState.value = UiState.Error(it.message ?: "请求异常")
_getDeviceConfigUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
@@ -109,37 +94,21 @@ class NetViewModel : ViewModel() {
}
}
fun updateOverdueState(block:(Boolean, List<ShelfModel>?)-> Unit) {
fun updateOverdueState() {
viewModelScope.launch {
runCatching {
val response = apiService.getShelfList(deviceId = App.deviceId)
if (response.isSuccess()) {
block(true, response.data?.containerGoodsList)
_updateOverdueStateUiState.value = UiState.Success(response)
} else {
block(false, emptyList())
_updateOverdueStateUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
block(false, null)
_updateOverdueStateUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
// fun saveShelfGoodsList(body: ShelfBody) {
// viewModelScope.launch {
// _saveShelfGoodsListUiState.value = UiState.Loading
// runCatching {
// val response = apiService.saveShelfGoodsList(body)
// if (response.success) {
// _saveShelfGoodsListUiState.value = UiState.Success(response)
// } else {
// _saveShelfGoodsListUiState.value = UiState.Error(response.message ?: "请求失败")
// }
// }.onFailure {
// _saveShelfGoodsListUiState.value = UiState.Error(it.message ?: "请求异常")
// }
// }
// }
fun getGoodsList(
canteenId: String = "",
goodsName: String = "",
@@ -176,7 +145,5 @@ class NetViewModel : ViewModel() {
Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}")
}
}
}
}
@@ -2,7 +2,7 @@ package com.shuwei.intelligent.shelves.serial
/**
* 串口通信协议常量定义
* 统一管理所有协议头、协议尾各类指令
* 统一管理所有协议头、协议尾各类指令及帧解析偏移量
*/
object ProtocolConstants {
@@ -12,6 +12,81 @@ object ProtocolConstants {
/** 协议尾 */
const val FOOTER = "FFFF"
// ----------------------------------------------------------------
// 指令码(cmd 字段,4字符)
// ----------------------------------------------------------------
/** 终端激活指令 */
const val CMD_ACTIVATE = "0001"
/** 心跳指令(服务端发起) */
const val CMD_HEARTBEAT = "0101"
/** 心跳指令(终端发起) */
const val CMD_HEARTBEAT_TERMINAL = "8101"
/** 开关锁反馈 */
const val CMD_LOCK_FEEDBACK = "0202"
/** 获取设备状态 */
const val CMD_DEVICE_STATUS = "0302"
/** 卡号上发 */
const val CMD_CARD_UPLOAD = "0501"
/** 操作记录上传 */
const val CMD_OPERATION_RECORD = "0B01"
/** 对时 */
const val CMD_TIME_SYNC = "0C01"
/** 终端位置上报 */
const val CMD_POSITION_REPORT = "0E01"
/** 温控信息上报 */
const val CMD_TEMP_REPORT = "1001"
/** 温控信息应答 */
const val CMD_TEMP_RESPONSE = "1002"
/** 温控应答(备用) */
const val CMD_TEMP_RESPONSE_ALT = "0F02"
// ----------------------------------------------------------------
// 帧数据偏移 / 尺寸常量
// ----------------------------------------------------------------
/** 心跳帧中重量数据起始偏移(相对帧起始) */
const val HEARTBEAT_WEIGHT_OFFSET = 14
/** 设备状态帧中重量数据起始偏移(含协议头长度) */
const val DEVICE_STATUS_WEIGHT_OFFSET = HEADER.length + 14
/** 货架数量(每侧5格,共10格,协议中12组) */
const val SHELF_SENSOR_COUNT = 12
/** 每组重量数据字节数(十六进制字符数) */
const val BYTES_PER_WEIGHT = 8
/** 温度字段字符长度 */
const val TEMPERATURE_FIELD_LEN = 2
/** 心跳帧最小有效长度 */
const val HEARTBEAT_MIN_LENGTH = 200
/** 心跳帧中温度字段起始偏移(经协议分析得出,前面含重量+其他传感器数据) */
const val HEARTBEAT_TEMPERATURE_START = 158
/** 激活请求体有效长度 */
const val ACTIVATE_BODY_LENGTH = 44
/** 固定数据体有效长度(terminalId 30 + terminalVersion 10 + 其他 2 */
const val FIX_BODY_LENGTH = 42
// ----------------------------------------------------------------
// 完整指令
// ----------------------------------------------------------------
/** 激活指令(1楼餐厅) */
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
@@ -36,7 +111,7 @@ object ProtocolConstants {
/** 摄氏温度单位指令 */
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
/** 灯指令(继电器1关) */
/** 灯指令(继电器1关) */
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
/** 开灯指令(继电器1开) */
@@ -0,0 +1,244 @@
package com.shuwei.intelligent.shelves.serial
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVATE_BODY_LENGTH
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVE_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.BYTES_PER_WEIGHT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_ACTIVATE
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_CARD_UPLOAD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_DEVICE_STATUS
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_HEARTBEAT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_HEARTBEAT_TERMINAL
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_LOCK_FEEDBACK
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_OPERATION_RECORD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_POSITION_REPORT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_REPORT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_RESPONSE
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_RESPONSE_ALT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TIME_SYNC
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.DEVICE_STATUS_WEIGHT_OFFSET
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FIX_BODY_LENGTH
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_MIN_LENGTH
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_TEMPERATURE_START
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_WEIGHT_OFFSET
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.SHELF_SENSOR_COUNT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.TEMPERATURE_FIELD_LEN
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.hexToBinary
import java.util.TimeZone
/**
* 串口协议解析处理器
*
* 负责将原始协议帧字符串解析为业务事件,通过 [Callback] 接口通知调用方。
* 设计为无状态(除 terminalId/terminalVersion 外),便于单元测试。
*
* @param callback 业务回调,由 Activity/ViewModel 实现
*/
class SerialProtocolHandler(private val callback: Callback) {
/** 终端 ID(15字节/30字符),由激活指令或心跳兜底填充 */
var terminalId: String = ""
private set
/** 固件版本(5字节/10字符),由激活指令或心跳兜底填充 */
var terminalVersion: String = ""
private set
/**
* 协议解析结果回调接口
*/
interface Callback {
/** 需要向串口发送一条指令 */
fun onSendCmd(cmd: String)
/** 解析到一组重量数据:货架编号 → 重量(克) */
fun onWeightUpdated(shelfNo: Int, weight: Int)
/** 温控信息上报,附带完整帧日志用于显示/复制 */
fun onTempReport(logMsg: String)
/** 记录日志 */
fun onLog(message: String)
}
/**
* 处理一帧完整的协议数据
*
* @param data 经 SerialFrameParser 解析后的完整帧字符串
*/
fun handle(data: String) {
if (data.isEmpty()) return
val cmd = data.substring(HEADER.length, HEADER.length + 4)
val cmdFlag = cmd.substring(0, 2)
when (cmd) {
CMD_ACTIVATE -> handleActivate(data, cmd, cmdFlag)
CMD_HEARTBEAT, CMD_HEARTBEAT_TERMINAL -> handleHeartbeat(data, cmd, cmdFlag)
CMD_LOCK_FEEDBACK -> handleLockFeedback(data, cmd, cmdFlag)
CMD_DEVICE_STATUS -> handleDeviceStatus(data, cmdFlag)
CMD_CARD_UPLOAD -> handleCardUpload(data, cmd, cmdFlag)
CMD_OPERATION_RECORD -> handleOperationRecord(data, cmd, cmdFlag)
CMD_TIME_SYNC -> handleTimeSync(data, cmd, cmdFlag)
CMD_POSITION_REPORT -> handlePositionReport(data, cmd, cmdFlag)
CMD_TEMP_REPORT -> handleTempReport(data, cmd, cmdFlag)
CMD_TEMP_RESPONSE -> callback.onLog("收到[温控信息应答]${CMD_TEMP_RESPONSE}数据:$data")
CMD_TEMP_RESPONSE_ALT -> callback.onLog("收到[温控信息应答]${CMD_TEMP_RESPONSE_ALT}数据:$data")
else -> {}
}
}
/** 处理终端激活指令 */
private fun handleActivate(data: String, cmd: String, cmdFlag: String) {
val reqBody = data.substring(HEADER.length + cmd.length, data.length - FOOTER.length)
if (reqBody.length == ACTIVATE_BODY_LENGTH) {
terminalId = reqBody.substring(2, 32)
terminalVersion = reqBody.substring(32, 42)
callback.onLog("收到[激活]${cmdFlag}指令:${data},终端id:${terminalId},固件版本:${terminalVersion},发送激活命令:$ACTIVE_CMD")
callback.onSendCmd(ACTIVE_CMD)
}
}
/** 处理心跳指令(0101 / 8101 */
private fun handleHeartbeat(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER"
callback.onLog("收到[心跳]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
parseHeartbeatData(data)
}
/** 处理开关锁反馈 */
private fun handleLockFeedback(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
callback.onLog("收到[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理获取设备状态(0302) */
private fun handleDeviceStatus(data: String, cmdFlag: String) {
callback.onLog("收到[获取设备状态]${cmdFlag}指令:${data}")
val weightStartIndex = DEVICE_STATUS_WEIGHT_OFFSET
val weightEndIndex = weightStartIndex + SHELF_SENSOR_COUNT * BYTES_PER_WEIGHT
val temperatureEndIndex = weightEndIndex + TEMPERATURE_FIELD_LEN
try {
parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data)
parseFixDataBody(data, temperatureEndIndex)
} catch (e: Exception) {
e.printStackTrace()
}
}
/** 处理卡号上发 */
private fun handleCardUpload(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}050203$FOOTER"
callback.onLog("收到[卡号上发]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理操作记录上传 */
private fun handleOperationRecord(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0B0203$FOOTER"
callback.onLog("收到[操作记录上传]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理对时请求 */
private fun handleTimeSync(data: String, cmd: String, cmdFlag: String) {
val nowMs = System.currentTimeMillis()
val timeLong = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
val timeStr = "0000000" + String.format("%X", timeLong)
val time = timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
val respCmd = "${HEADER}0B02${time}$FOOTER"
callback.onLog("收到[对时]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理终端位置上报 */
private fun handlePositionReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0E0200$FOOTER"
callback.onLog("收到[终端位置上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理温控信息上报 */
private fun handleTempReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}100200$FOOTER"
val logMsg = "收到[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}"
callback.onLog(logMsg)
callback.onSendCmd(respCmd)
callback.onTempReport(logMsg)
}
/**
* 解析心跳帧中的重量与固定数据体
*/
private fun parseHeartbeatData(data: String) {
runCatching {
if (data.length < HEARTBEAT_MIN_LENGTH) return
val weightStartIndex = HEARTBEAT_WEIGHT_OFFSET
val weightEndIndex = weightStartIndex + SHELF_SENSOR_COUNT * BYTES_PER_WEIGHT
parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data)
val temperatureEndIndex = HEARTBEAT_TEMPERATURE_START + TEMPERATURE_FIELD_LEN
parseFixDataBody(data, temperatureEndIndex)
}.onFailure { it.printStackTrace() }
}
/**
* 解析连续的重量数据段,每 [BYTES_PER_WEIGHT] 字符一组,依次对应货架 1..N
*
* @param start 数据段起始索引
* @param end 数据段结束索引(不含)
* @param data 完整帧字符串
*/
fun parseWeightData(start: Int, end: Int, data: String) {
var index = 1
var count = start
while (count < end) {
val weightInfo = data.substring(count, count + BYTES_PER_WEIGHT)
// 小端字节序转换:将4字节按字节倒序拼接
var weightHex = weightInfo.run {
substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2)
}
var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
val realWeight = if (firstByteBinary.startsWith("0")) {
// 正数:使用 toLongOrNull(16) 转换,避免超出 Int 范围
weightHex.toLongOrNull(16)?.toInt() ?: 0
} else {
// 负数:清除符号位后计算绝对值,再取反
firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2)
-1 * (weightHex.toLongOrNull(16)?.toInt() ?: 0)
}
callback.onWeightUpdated(index, realWeight)
count += BYTES_PER_WEIGHT
index++
}
}
/**
* 从心跳帧或设备状态帧中补充解析 terminalId / terminalVersion。
* 激活指令(CMD_ACTIVATE)是获取这两个字段的首选来源;
* 若激活指令尚未到来,则通过此方法从心跳/设备状态帧中兜底解析。
*
* @param data 完整协议帧字符串
* @param temperatureEndIndex 温度字段结束位置(固定数据体紧随其后)
*/
private fun parseFixDataBody(data: String, temperatureEndIndex: Int) {
try {
// 已通过激活指令成功解析过,无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) return
val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
if (fixDataBody.length == FIX_BODY_LENGTH) {
terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40)
callback.onLog("从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}