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:
co-authored by
Claude Sonnet 4.6
parent
9a795b9710
commit
04fe3ec2c1
@@ -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 = ""
|
||||
@@ -630,9 +427,14 @@ class HomeActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun updateOverdueState() {
|
||||
viewModel.updateOverdueState { state, items ->
|
||||
if (items.isNullOrEmpty()) return@updateOverdueState
|
||||
if (items.size != list.size) return@updateOverdueState
|
||||
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) {
|
||||
@@ -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 }
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user