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.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.SparseIntArray
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope 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.NetViewModel
import com.shuwei.intelligent.shelves.net.RespData import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.UiState 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.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.LEFT_SHELF_OPEN_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.RIGHT_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.ProtocolConstants.START_TEMP_CTRL_CMD
import com.shuwei.intelligent.shelves.serial.SerialFrameParser import com.shuwei.intelligent.shelves.serial.SerialFrameParser
import com.shuwei.intelligent.shelves.serial.SerialPortManager 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.IntervalExecutor
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.gone import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toJsonString import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import com.shuwei.intelligent.shelves.utils.ext.toast import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible import com.shuwei.intelligent.shelves.utils.ext.visible
import com.shuwei.intelligent.shelves.utils.hexToBinary
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@@ -53,22 +49,16 @@ import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.TimeZone
class HomeActivity : BaseActivity() { class HomeActivity : BaseActivity() {
companion object { 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 const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
} }
// 货架列表数据 // 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf() private val list: MutableList<ShelfModel> = mutableListOf()
// 各传感器重量数据,key 为货架编号
private val weightArray = SparseIntArray()
// 当前选中货架下标 // 当前选中货架下标
private var shelfIndex = 0 private var shelfIndex = 0
@@ -81,10 +71,47 @@ class HomeActivity : BaseActivity() {
// 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false // 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false
private var isNeedOpenLock = true private var isNeedOpenLock = true
private var deviceName = ""
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels() 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") @SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -94,8 +121,8 @@ class HomeActivity : BaseActivity() {
setContentView(binding.root) setContentView(binding.root)
updateLeftStatus("") updateLeftStatus("")
initRecyclerView() initRecyclerView()
// 串口初始化,独立协程,不阻塞网络状态监听 // 串口初始化,独立协程,不阻塞网络状态监听
lifecycleScope.launch { lifecycleScope.launch {
val openState = SerialPortManager.open() val openState = SerialPortManager.open()
@@ -110,9 +137,10 @@ class HomeActivity : BaseActivity() {
} }
} }
} }
// 网络状态监听,独立协程,与串口初始化并行执行 // 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { launch {
viewModel.getAccessTokenUiState.collect { state -> viewModel.getAccessTokenUiState.collect { state ->
when (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({ binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD) sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD) sendCmd(C_TEMP_CMD)
@@ -167,8 +205,7 @@ class HomeActivity : BaseActivity() {
return return
} }
deviceName = data.data.deviceName deviceName = data.data.deviceName
updateLeftStatus(deviceName)
updateLeftStatus("$deviceName")
App.canteenId = data.data.placeId App.canteenId = data.data.placeId
val tempList = data.data.containerGoodsList val tempList = data.data.containerGoodsList
@@ -192,135 +229,13 @@ class HomeActivity : BaseActivity() {
loadEmptyView() loadEmptyView()
} }
/**
* 终端id
*/
private var terminalId: String = ""
/**
* 固件版本
*/
private var terminalVersion: String = ""
private val serialFrameParser = SerialFrameParser()
private fun receiveSerialPortData(srcData: String) { private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData") log("receiveSerialPortData: $srcData")
// 重置无数据计时,并关闭已显示的超时提示弹窗 // 重置无数据计时,并关闭已显示的超时提示弹窗
lastDataReceivedTime = System.currentTimeMillis() lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss() noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
val frame = serialFrameParser.parse(srcData) ?: return val frame = serialFrameParser.parse(srcData) ?: return
handleReceivedData(frame) serialProtocolHandler.handle(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 -> {}
}
} }
private var tipDialog: CommonDialog? = null 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) { override fun onUserInactive() {
try { switchLight(false)
// 已通过激活指令成功解析过,无需重复解析
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 onDestroy() { override fun onDestroy() {
@@ -504,9 +315,13 @@ class HomeActivity : BaseActivity() {
/** /**
* 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端 * 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端
*/ */
@Suppress("DEPRECATION")
private fun onShelfActivityResult(position: Int, intent: Intent) { 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 { list[position].apply {
goodsId = model.goodsId goodsId = model.goodsId
goodsName = model.goodsName goodsName = model.goodsName
@@ -516,29 +331,13 @@ class HomeActivity : BaseActivity() {
} }
} }
shelfAdapter.notifyItemChanged(position) shelfAdapter.notifyItemChanged(position)
syncShelfGoodsToServer()
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()
}
} }
private fun initRecyclerView() { private fun initRecyclerView() {
binding.rvShelf.run { binding.rvShelf.run {
layoutManager = layoutManager = GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false)
GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false)
adapter = shelfAdapter adapter = shelfAdapter
itemAnimator.let { itemAnimator.let {
if (it is DefaultItemAnimator) { if (it is DefaultItemAnimator) {
it.supportsChangeAnimations = false 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 val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) }
private fun updateDateTime() { private fun updateDateTime() {
val dateTime = dateTimeFormatter.format(Date()) updateRightStatus(dateTimeFormatter.format(Date()))
updateRightStatus(dateTime)
} }
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@@ -581,7 +379,6 @@ class HomeActivity : BaseActivity() {
private fun loadEmptyView() { private fun loadEmptyView() {
list.clear() list.clear()
shelfAdapter.notifyDataSetChanged() shelfAdapter.notifyDataSetChanged()
binding.include?.let { binding.include?.let {
it.root.visible() it.root.visible()
it.root.setOnClickListener { it.root.setOnClickListener {
@@ -593,7 +390,7 @@ class HomeActivity : BaseActivity() {
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) { fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo } val item = list.firstOrNull { it.deviceNo == event.shelfNo }
item?.apply { item?.apply {
goodsId = "" goodsId = ""
@@ -607,7 +404,7 @@ class HomeActivity : BaseActivity() {
} }
private val taskExecutor by lazy { IntervalExecutor() } private val taskExecutor by lazy { IntervalExecutor() }
private var saveTaskJob: Job?=null private var saveTaskJob: Job? = null
/** /**
* 保存定时任务 * 保存定时任务
@@ -618,23 +415,28 @@ class HomeActivity : BaseActivity() {
} }
} }
private var overdueTaskJob: Job?=null private var overdueTaskJob: Job? = null
/** /**
* 过期时间定时任务 * 过期时间定时任务
*/ */
private fun overdueTask() { private fun overdueTask() {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60* 1000L) { overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) {
updateOverdueState() updateOverdueState()
} }
} }
private fun updateOverdueState() { private fun updateOverdueState() {
viewModel.updateOverdueState { state, items -> viewModel.updateOverdueState()
if (items.isNullOrEmpty()) return@updateOverdueState }
if (items.size != list.size) return@updateOverdueState
/**
* 将接口返回的过期状态应用到本地列表,仅在状态变化时刷新对应 item
*/
private fun applyOverdueState(items: List<ShelfModel>) {
if (items.size != list.size) return
list.forEachIndexed { index, model -> list.forEachIndexed { index, model ->
//接口过期状态变化后更新状态 // 接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) { if (items[index].overdue != model.overdue) {
model.overdue = items[index].overdue model.overdue = items[index].overdue
shelfAdapter.notifyItemChanged(index) shelfAdapter.notifyItemChanged(index)
@@ -642,10 +444,11 @@ class HomeActivity : BaseActivity() {
} }
} }
} }
}
private fun saveShelfGoodsList() { /**
log("performSync: list:${list.toJsonString()}") * 构建 ShelfBody 并提交到服务端(被定时任务与 ShelfActivity 返回共用)
*/
private fun syncShelfGoodsToServer() {
val submitList = list.sortedBy { it.deviceNo } val submitList = list.sortedBy { it.deviceNo }
submitList.forEach { submitList.forEach {
if (it.goodsId.isNullOrBlank()) { if (it.goodsId.isNullOrBlank()) {
@@ -653,7 +456,7 @@ class HomeActivity : BaseActivity() {
} }
} }
val body = ShelfBody().also { val body = ShelfBody().also {
it.deviceId = App.deviceId it.deviceId = App.deviceId ?: ""
it.canteenId = App.canteenId it.canteenId = App.canteenId
it.temperature = showTemperatureC it.temperature = showTemperatureC
it.humidity = showHumidity it.humidity = showHumidity
@@ -663,5 +466,8 @@ class HomeActivity : BaseActivity() {
viewModel.saveShelfGoodsList(body) 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.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.PopupWindow
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.shuwei.intelligent.shelves.activity.HomeActivity import com.shuwei.intelligent.shelves.activity.LogActivity
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.R import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.databinding.ActivityBaseBinding 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.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.FileLogger import com.shuwei.intelligent.shelves.utils.FileLogger
import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.core.view.WindowCompat 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() lightSwitchClick()
binding.tvLeftStatus.setOnDoubleClickListener { binding.tvLeftStatus.setOnDoubleClickListener {
finish() finish()
} }
binding.tvRightStatus.setOnDoubleClickListener { binding.tvRightStatus.setOnDoubleClickListener {
EnvSwitchDialog(this).show() showRightStatusPopup(binding.tvRightStatus)
} }
} }
fun hideStatusBar() { 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 statusBarDarkFont() { fun statusBarDarkFont() {
//window.decorView.systemUiVisibility = if (enable) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0
val color = ContextCompat.getColor(this, R.color.white) val color = ContextCompat.getColor(this, R.color.white)
binding.tvLeftStatus.setTextColor(color) binding.tvLeftStatus.setTextColor(color)
binding.tvRightStatus.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 TIME_OUT: Long = (5 * 60 * 1000).toLong()
private val mHandler by lazy { Handler(Looper.getMainLooper()) } private val mHandler by lazy { Handler(Looper.getMainLooper()) }
private val mTimeoutRunnable = Runnable { private val mTimeoutRunnable = Runnable {
if (this is HomeActivity) { // 用户长时间无操作时回调,子类可覆写实现具体逻辑(如关灯)
this.switchLight(false) onUserInactive()
}
} }
/**
* 用户超时无操作时回调,默认空实现,子类按需覆写
*/
open fun onUserInactive() {}
override fun onUserInteraction() { override fun onUserInteraction() {
super.onUserInteraction() super.onUserInteraction()
//开灯 //开灯
@@ -164,15 +153,27 @@ open class BaseActivity : AppCompatActivity() {
val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true
switchLight(tag.not()) 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.App 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.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 com.shuwei.intelligent.shelves.utils.ext.toJsonString
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class NetViewModel : ViewModel() { 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) private val _getShelfListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getShelfListUiState: StateFlow<UiState> = _getShelfListUiState val getShelfListUiState: StateFlow<UiState> = _getShelfListUiState
// private val _saveShelfGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
// val saveShelfGoodsListUiState: StateFlow<UiState> = _saveShelfGoodsListUiState
private val _getGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial) private val _getGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getGoodsListUiState: StateFlow<UiState> = _getGoodsListUiState val getGoodsListUiState: StateFlow<UiState> = _getGoodsListUiState
@@ -43,6 +27,8 @@ class NetViewModel : ViewModel() {
private val _getDeviceConfigUiState = MutableStateFlow<UiState>(UiState.Initial) private val _getDeviceConfigUiState = MutableStateFlow<UiState>(UiState.Initial)
val getDeviceConfigUiState: StateFlow<UiState> = _getDeviceConfigUiState val getDeviceConfigUiState: StateFlow<UiState> = _getDeviceConfigUiState
private val _updateOverdueStateUiState = MutableStateFlow<UiState>(UiState.Initial)
val updateOverdueStateUiState: StateFlow<UiState> = _updateOverdueStateUiState
fun getDeviceToken(deviceId: String) { fun getDeviceToken(deviceId: String) {
viewModelScope.launch { viewModelScope.launch {
@@ -62,17 +48,16 @@ class NetViewModel : ViewModel() {
fun getDeviceConfig(deviceId: String, deviceToken: String) { fun getDeviceConfig(deviceId: String, deviceToken: String) {
viewModelScope.launch { viewModelScope.launch {
_getDeviceTokenUiState.value = UiState.Loading _getDeviceConfigUiState.value = UiState.Loading
runCatching { runCatching {
val response = val response = apiService.getDeviceInfo(equipmentCode = deviceId)
apiService.getDeviceInfo(equipmentCode = deviceId)
if (response.isSuccess()) { if (response.isSuccess()) {
_getDeviceTokenUiState.value = UiState.Success(response) _getDeviceConfigUiState.value = UiState.Success(response)
} else { } else {
_getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败") _getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败")
} }
}.onFailure { }.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 { viewModelScope.launch {
runCatching { runCatching {
val response = apiService.getShelfList(deviceId = App.deviceId) val response = apiService.getShelfList(deviceId = App.deviceId)
if (response.isSuccess()) { if (response.isSuccess()) {
block(true, response.data?.containerGoodsList) _updateOverdueStateUiState.value = UiState.Success(response)
} else { } else {
block(false, emptyList()) _updateOverdueStateUiState.value = UiState.Error(response.msg ?: "请求失败")
} }
}.onFailure { }.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( fun getGoodsList(
canteenId: String = "", canteenId: String = "",
goodsName: String = "", goodsName: String = "",
@@ -176,7 +145,5 @@ class NetViewModel : ViewModel() {
Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}") Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}")
} }
} }
} }
} }
@@ -2,7 +2,7 @@ package com.shuwei.intelligent.shelves.serial
/** /**
* 串口通信协议常量定义 * 串口通信协议常量定义
* 统一管理所有协议头、协议尾各类指令 * 统一管理所有协议头、协议尾各类指令及帧解析偏移量
*/ */
object ProtocolConstants { object ProtocolConstants {
@@ -12,6 +12,81 @@ object ProtocolConstants {
/** 协议尾 */ /** 协议尾 */
const val FOOTER = "FFFF" 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楼餐厅) */ /** 激活指令(1楼餐厅) */
const val ACTIVE_CMD = const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}" "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
@@ -36,7 +111,7 @@ object ProtocolConstants {
/** 摄氏温度单位指令 */ /** 摄氏温度单位指令 */
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}" const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
/** 灯指令(继电器1关) */ /** 灯指令(继电器1关) */
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}" const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
/** 开灯指令(继电器1开) */ /** 开灯指令(继电器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()
}
}
}