feat(HomeActivity): 新增串口无数据超时提示弹窗

超过5分钟未收到串口数据时,自动弹窗提示用户考虑断电重启设备;
收到新数据后自动关闭弹窗并重置计时。
同步提交串口协议常量(ProtocolConstants)与帧解析器(SerialFrameParser)重构代码。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 10:18:00 +08:00
co-authored by Claude Sonnet 4.6
parent fff6a2a3b3
commit 9a795b9710
4 changed files with 279 additions and 298 deletions
@@ -6,7 +6,6 @@ import android.graphics.Color
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.Log
import android.util.SparseIntArray import android.util.SparseIntArray
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -25,14 +24,19 @@ import com.shuwei.intelligent.shelves.model.SendWeightEvent
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.model.ShelfModel
import com.shuwei.intelligent.shelves.model.ShelfResult import com.shuwei.intelligent.shelves.model.ShelfResult
import com.shuwei.intelligent.shelves.model.WeightEvent
import com.shuwei.intelligent.shelves.net.Loading 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.net.apiService 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.SerialPortManager
import com.shuwei.intelligent.shelves.utils.HexUtils
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.binaryToHex
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
@@ -49,74 +53,39 @@ 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 TAG = "HomeActivity"
private const val LOG_PREFIX = "receiveSerialPortData:收到" 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"
//协议头
const val HEADER = "D6DADEDB"
//协议尾
const val FOOTER = "FFFF"
// const val ACTIVE_CMD =
// "${HEADER}000281C5D70A7D428A4FA67B50F7DACA241938938B4B9CBD73673E9C8E7F2E15062D${FOOTER}"
const val LEFT_SHELF_OPEN_CMD =
"${HEADER}020101000000000000000000000000000000000000${FOOTER}"
const val RIGHT_SHELF_OPEN_CMD =
"${HEADER}020100010000000000000000000000000000000000${FOOTER}"
//const val TEMPERATURE_CMD = "${HEADER}1001${FOOTER}"
const val DEVICE_INFO_CMD = "${HEADER}0301000000000000000000000000${FOOTER}"
const val START_TEMP_CTRL_CMD = "${HEADER}0F01360000${FOOTER}"
//const val START_CMD= "${HEADER}0F01050020${FOOTER}"
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D428948AD7254FADBB1516738938B4B9CBD73673E9D8E7F2E15062D$FOOTER"
// 410激活码
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${FOOTER}"
//1楼餐厅激活码
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
//华氏温度单位
const val F_TEMP_CMD = "${HEADER}0F01390000${FOOTER}"
//摄氏温度单位
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
//开灯(开 继电器1
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
const val LIGHT_OPEN = "${HEADER}020102020002020000000000000000000000000000${FOOTER}"
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}"
val list: MutableList<ShelfModel> = mutableListOf()
val weightArray = SparseIntArray()
var shelfIndex = 0
// public fun getRealWeight():Double = weightArray[shelfIndex]?:0.0
// 温度
var showTemperatureC = "0"
// 湿度
var showHumidity = "0"
} }
//401-false1楼餐厅-true // 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf()
// 各传感器重量数据,key 为货架编号
private val weightArray = SparseIntArray()
// 当前选中货架下标
private var shelfIndex = 0
// 当前温度(摄氏)
private var showTemperatureC = "0"
// 当前湿度
private var showHumidity = "0"
// 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false
private var isNeedOpenLock = true private var isNeedOpenLock = true
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels() private val viewModel: NetViewModel by viewModels()
@SuppressLint("NotifyDataSetChanged", "HardwareIds") @SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
@@ -124,26 +93,25 @@ class HomeActivity : BaseActivity() {
setBackground() setBackground()
setContentView(binding.root) setContentView(binding.root)
// updateLeftStatus("-°C / -%")
updateLeftStatus("") updateLeftStatus("")
initRecyclerView() initRecyclerView()
// 串口初始化,独立协程,不阻塞网络状态监听
lifecycleScope.launch { lifecycleScope.launch {
val openState = SerialPortManager.open() val openState = SerialPortManager.open()
log("onCreate: openState=$openState") log("onCreate: openState=$openState")
if (openState) { if (openState) {
SerialPortManager.startReceive { data -> SerialPortManager.startReceive { data ->
// 更新UI显示接收数据 // 切换到主线程协程处理串口数据
lifecycleScope.launch { lifecycleScope.launch {
try { runCatching { receiveSerialPortData(data) }
receiveSerialPortData(data) .onFailure { it.printStackTrace() }
} catch (e: Exception) {
e.printStackTrace()
} }
} }
} }
//SerialPortManager.send(DEVICE_INFO_CMD)
} }
// 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
launch { launch {
viewModel.getAccessTokenUiState.collect { state -> viewModel.getAccessTokenUiState.collect { state ->
@@ -167,59 +135,21 @@ class HomeActivity : BaseActivity() {
} }
} }
} }
//sendCmd(ACTIVE_CMD)
binding.root.postDelayed({ binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD) sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD) sendCmd(C_TEMP_CMD)
}, 5000) }, 5000)
// TaskManager.startTask()
saveGoodsTask() saveGoodsTask()
overdueTask() overdueTask()
// var androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
//// androidId = "d0e7a2d5f25c775c"
// log("onCreate: androidId=${androidId}")
// if (androidId == "65e1012638f78260" || androidId == "40f89e91ee40611d") {
// androidId = "d0e7a2d5f25c775c"
// }
// App.deviceId = androidId
// //"53babf69-0d2f-3875-a649-f2e12f80ad4c1"
//
// val swDpValue = resources?.configuration?.smallestScreenWidthDp
// log("onCreate: swDpValue=$swDpValue")
// viewModel.getAccessToken(App.deviceId)
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
// binding.root.postDelayed({
// loadTestData()
// }, 30*1000)
}
private fun loadTestData() {
list.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 4, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 9, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId))
shelfAdapter.notifyDataSetChanged()
} }
private fun getTokenSuccess(data: RespData<*>) { private fun getTokenSuccess(data: RespData<*>) {
log("getTokenSuccess: $data") log("getTokenSuccess: $data")
data.data?.let { data.data?.let {
App.accessToken = it.toString() App.accessToken = it.toString()
//UIUtils.toast(it.toString())
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
} }
} }
@@ -238,7 +168,6 @@ class HomeActivity : BaseActivity() {
} }
deviceName = data.data.deviceName deviceName = data.data.deviceName
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName") updateLeftStatus("$deviceName")
App.canteenId = data.data.placeId App.canteenId = data.data.placeId
@@ -247,10 +176,6 @@ class HomeActivity : BaseActivity() {
loadEmptyView() loadEmptyView()
return return
} }
// if (tempList.size < 10) {
// toast("数据错误")
// return
// }
list.clear() list.clear()
val subList01 = tempList.subList(0, tempList.size / 2) val subList01 = tempList.subList(0, tempList.size / 2)
val subList02 = tempList.subList(tempList.size / 2, tempList.size) val subList02 = tempList.subList(tempList.size / 2, tempList.size)
@@ -277,51 +202,15 @@ class HomeActivity : BaseActivity() {
*/ */
private var terminalVersion: String = "" private var terminalVersion: String = ""
private val serialReader by lazy { StringBuilder() } private val serialFrameParser = SerialFrameParser()
private fun receiveSerialPortData(srcData: String) { private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData") log("receiveSerialPortData: $srcData")
var data = "" // 重置无数据计时,并关闭已显示的超时提示弹窗
if (srcData.contains(HEADER) && srcData.contains(FOOTER)) { lastDataReceivedTime = System.currentTimeMillis()
serialReader.clear() noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
val startIndex = srcData.lastIndexOf(HEADER) val frame = serialFrameParser.parse(srcData) ?: return
var endIndex = srcData.indexOf(FOOTER) + FOOTER.length handleReceivedData(frame)
if (endIndex < startIndex) {
//类似这种:31314FFFFFD6DADEDB01010CFOOTER在HEADER前
data = srcData.substring(startIndex)
if (data.contains(FOOTER)) {
endIndex = srcData.indexOf(FOOTER) + FOOTER.length
data = data.substring(0, endIndex)
handleReceivedData(data)
serialReader.clear()
return
}
serialReader.clear()
serialReader.append(data).append(",")
return
}
serialReader.append(srcData.substring(startIndex, endIndex))
data = serialReader.toString()
handleReceivedData(data)
serialReader.clear()
return
}
if (srcData.contains(HEADER) && srcData.contains(FOOTER).not()) {
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
serialReader.append(srcData.substring(startIndex)).append(",")
return
}
if (srcData.contains(HEADER).not() && srcData.contains(FOOTER).not()) {
serialReader.append(srcData).append(",")
return
}
//if (srcData.contains(HEADER).not() && srcData.contains(FOOTER))
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
serialReader.append(srcData.substring(0, endIndex))
log("receiveSerialPortData:serialReader: $serialReader")
data = serialReader.toString().replace(",", "")
handleReceivedData(data)
} }
private fun handleReceivedData(data: String) { private fun handleReceivedData(data: String) {
@@ -358,7 +247,6 @@ class HomeActivity : BaseActivity() {
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER" val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}") log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd) sendCmd(respCmd)
//lockFeedback(data)
} }
"0302" -> { "0302" -> {
@@ -383,7 +271,8 @@ class HomeActivity : BaseActivity() {
"0C01" -> { "0C01" -> {
//对时 //对时
val timeLong = System.currentTimeMillis() / 1000 + (8 * 3600) val nowMs = System.currentTimeMillis()
val timeLong = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
val timeStr = "0000000" + String.format("%X", timeLong) val timeStr = "0000000" + String.format("%X", timeLong)
val time = val time =
timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("") timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
@@ -406,19 +295,13 @@ class HomeActivity : BaseActivity() {
"${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}" "${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}"
log(logMsg) log(logMsg)
sendCmd(respCmd) sendCmd(respCmd)
//val tempC = HexUtils.signedHexToDec(data.take(4)) * 0.1
// updateLeftStatus("$deviceName ${"%.1f".format(tempC)}°C / -%")
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName") updateLeftStatus("$deviceName")
//D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF
runOnUiThread {
if (tipDialog == null) { if (tipDialog == null) {
CommonDialog(this).apply { tipDialog = CommonDialog(this).apply {
dialogTitle = "温馨提示" dialogTitle = "温馨提示"
// dialogContent = logMsg
confirmBlock = { confirmBlock = {
logMsg?.copyTextToClipboard(this@HomeActivity) logMsg.copyTextToClipboard(this@HomeActivity)
} }
} }
} }
@@ -427,7 +310,6 @@ class HomeActivity : BaseActivity() {
tipDialog?.show() tipDialog?.show()
} }
} }
}
"1002" -> { "1002" -> {
log("${LOG_PREFIX}[温控信息应答]1002数据:${data}") log("${LOG_PREFIX}[温控信息应答]1002数据:${data}")
@@ -435,8 +317,6 @@ class HomeActivity : BaseActivity() {
"0F02" -> { "0F02" -> {
log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}") log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}")
//D6DADEDB0F020331323136334238423134303233303456322E31313EFFFF00000055010000000000002707000001000000220100000000000091040000120100000100000095140080611400800000000000000000000000000000000000000000010100005031323136334238423134303233303456322E31313EFFFF
runOnUiThread { }
} }
else -> {} else -> {}
@@ -445,29 +325,33 @@ class HomeActivity : BaseActivity() {
private var tipDialog: CommonDialog? = null private var tipDialog: CommonDialog? = null
private var fixedDataBody = "" /** 最后一次成功收到串口数据的时间戳(毫秒) */
private var lastDataReceivedTime = System.currentTimeMillis()
// private fun lockFeedback(data: String) { /** 无数据超时提示弹窗 */
// runCatching { private var noDataWarningDialog: CommonDialog? = null
// val fixedDataBodyEndIndex = data.length - FOOTER.length
// val fixedDataBodyStartIndex = HEADER.length + "0202".length + 42 /**
// fixedDataBody = data.substring(fixedDataBodyStartIndex, fixedDataBodyEndIndex) * 无数据看门狗:每30秒检查一次,若超过5分钟未收到数据则弹出提示。
// * 收到数据后会自动重置计时并关闭弹窗。
// val lockNo = if (list[shelfIndex].deviceNo in 1..5) "01" else "02" */
// val lockOneState = data.substring(HEADER.length + 4, HEADER.length + 6) private val noDataWatchdogTask = object : Runnable {
// if (lockOneState == "00") { override fun run() {
// //锁1关 val elapsed = System.currentTimeMillis() - lastDataReceivedTime
// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}") if (elapsed >= 5 * 60 * 1000L) {
// } if (noDataWarningDialog == null) {
// val lockTwoState = data.substring(HEADER.length + 6, HEADER.length + 8) noDataWarningDialog = CommonDialog(this@HomeActivity).apply {
// if (lockTwoState == "00") { dialogTitle = "设备提示"
// //锁2关 dialogContent = "长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备"
// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}") }
// } }
// }.onFailure { if (noDataWarningDialog?.isShowing == false) {
// it.printStackTrace() noDataWarningDialog?.show()
// } }
// } }
handler.postDelayed(this, 30 * 1000L)
}
}
private fun heartBeat(data: String) { private fun heartBeat(data: String) {
runCatching { runCatching {
@@ -483,14 +367,6 @@ class HomeActivity : BaseActivity() {
) )
val temperatureStartIndex = 158 val temperatureStartIndex = 158
val temperatureEndIndex = temperatureStartIndex + 2 val temperatureEndIndex = temperatureStartIndex + 2
// val temperatureHex = data.substring(temperatureStartIndex, temperatureEndIndex)
// val temperature = hexToDec(temperatureHex)
// val tempC = f2C(temperature.toDouble())
// showTemperatureC = "%.1f".format(tempC)
//val percent = 10 * list.count { it.goodsName.isNullOrBlank().not() }
// updateLeftStatus("${deviceName} [$temperatureHex] ${showTemperatureC}°C / -%")
getFixDataBody(data, temperatureEndIndex) getFixDataBody(data, temperatureEndIndex)
}.onFailure { }.onFailure {
@@ -532,23 +408,11 @@ class HomeActivity : BaseActivity() {
log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}") log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}")
EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight)) EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight))
} }
// val pos = index - 1
// val item = list[pos]
// item.weight = realWeight.toDouble()
// shelfAdapter.notifyItemChanged(pos)
// log("getWeightInfo: deviceNo=${item.deviceNo},realWeight=${realWeight}")
// EventBus.getDefault().post(SendWeightEvent(item.deviceNo, realWeight))
} }
count += 8 count += 8
index++ index++
} }
// ShelfActivity.updateWeight(getRealWeight())
// if (list.isNotEmpty()) {
// val intent = Intent(ShelfActivity.RECEIVER_DEVICE_INFO + list[shelfIndex].deviceNo)
// intent.putExtra(ShelfActivity.SHELF_WEIGHT, weightArray[shelfIndex])
// LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
// }
} }
private fun getDeviceInfo(data: String) { private fun getDeviceInfo(data: String) {
@@ -562,17 +426,6 @@ class HomeActivity : BaseActivity() {
e.printStackTrace() e.printStackTrace()
return return
} }
// val temperature = hexToDec(temperatureHex)
// val tempC = f2C(temperature.toDouble())
// showTemperatureC = "%.1f".format(tempC)
//val percent = 10 * list.count { it.goodsName.isNullOrBlank().not() }
// updateLeftStatus("${deviceName} ${showTemperatureC}°C / -%")
// val tempC = HexUtils.signedHexToDec(temperatureHex) * 0.1
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName") updateLeftStatus("$deviceName")
getWeightInfo( getWeightInfo(
@@ -584,13 +437,23 @@ class HomeActivity : BaseActivity() {
getFixDataBody(data, temperatureEndIndex) getFixDataBody(data, temperatureEndIndex)
} }
fun getFixDataBody(data: String, temperatureEndIndex: Int) { /**
* 从心跳帧或设备状态帧中补充解析 terminalId / terminalVersion。
* 激活指令(0001)是获取这两个字段的首选来源;
* 若激活指令尚未到来,则通过此方法从心跳/设备状态帧中兜底解析。
*
* @param data 完整协议帧字符串
* @param temperatureEndIndex 温度字段结束位置(固定数据体紧随其后)
*/
private fun getFixDataBody(data: String, temperatureEndIndex: Int) {
try { try {
// 已通过激活指令成功解析过,无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) { if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) {
return return
} }
val fixDataBodyStartIndex = temperatureEndIndex + 1 val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length) val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
// 固定数据体长度:terminalId 30字节 + terminalVersion 10字节 + 其他2字节 = 42字节
if (fixDataBody.length == 42) { if (fixDataBody.length == 42) {
terminalId = fixDataBody.substring(0, 30) terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40) terminalVersion = fixDataBody.substring(30, 40)
@@ -602,20 +465,25 @@ class HomeActivity : BaseActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
lifecycleScope.launch {
SerialPortManager.close() SerialPortManager.close()
}
// TaskManager.cancelTask()
saveTaskJob?.cancel() saveTaskJob?.cancel()
overdueTaskJob?.cancel() overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
super.onDestroy() super.onDestroy()
} }
@Suppress("DEPRECATION")
private val shelfAdapter by lazy { private val shelfAdapter by lazy {
ShelfAdapter(list).apply { ShelfAdapter(list).apply {
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
onShelfItemClicked(position)
}
}
}
/**
* 处理货架格子点击事件:按需开锁,然后跳转 ShelfActivity
*/
private fun onShelfItemClicked(position: Int) {
shelfIndex = position shelfIndex = position
lifecycleScope.launch { lifecycleScope.launch {
if (isNeedOpenLock) { if (isNeedOpenLock) {
@@ -623,19 +491,22 @@ class HomeActivity : BaseActivity() {
LEFT_SHELF_OPEN_CMD LEFT_SHELF_OPEN_CMD
else else
RIGHT_SHELF_OPEN_CMD RIGHT_SHELF_OPEN_CMD
val openState = SerialPortManager.send(openCmd) SerialPortManager.send(openCmd)
} }
// if (openState.not()) {
// toast("${list[position].goodsName}柜门开启失败")
// return@launch
// }
//SerialPortManager.send(START_CMD)
launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { launch(Intent(this@HomeActivity, ShelfActivity::class.java).also {
it.putExtra(ShelfActivity.SHELF_MODEL, list[position]) it.putExtra(ShelfActivity.SHELF_MODEL, list[position])
}) { }) { result ->
it?.run { result?.let { onShelfActivityResult(position, it) }
val model = }
getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel }
}
/**
* 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端
*/
@Suppress("DEPRECATION")
private fun onShelfActivityResult(position: Int, intent: Intent) {
val model = intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel
list[position].apply { list[position].apply {
goodsId = model.goodsId goodsId = model.goodsId
goodsName = model.goodsName goodsName = model.goodsName
@@ -644,11 +515,10 @@ class HomeActivity : BaseActivity() {
putCabinetDay = model.putCabinetDay putCabinetDay = model.putCabinetDay
} }
} }
notifyItemChanged(position) shelfAdapter.notifyItemChanged(position)
try { try {
val submitList = list.sortedBy { it.deviceNo }
val submitList = list.sortedBy { item -> item.deviceNo }
val body = ShelfBody().also { body -> val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: "" body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId body.canteenId = App.canteenId
@@ -656,20 +526,12 @@ class HomeActivity : BaseActivity() {
body.humidity = showHumidity body.humidity = showHumidity
body.goodsList = submitList body.goodsList = submitList
} }
Log.d("mzf", "performSync: body:${body.toJsonString()}") log("performSync: body:${body.toJsonString()}")
lifecycleScope.launch { viewModel.saveShelfGoodsList(body)
val resp = apiService.saveShelfGoodsList(body = body)
Log.d("mzf", "performSync: resp:${resp.toJsonString()}")
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
}
}
}
}
}
private fun initRecyclerView() { private fun initRecyclerView() {
binding.rvShelf.run { binding.rvShelf.run {
@@ -685,9 +547,10 @@ class HomeActivity : BaseActivity() {
} }
} }
private val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) }
private fun updateDateTime() { private fun updateDateTime() {
val sdf = SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) val dateTime = dateTimeFormatter.format(Date())
val dateTime = sdf.format(Date())
updateRightStatus(dateTime) updateRightStatus(dateTime)
} }
@@ -703,13 +566,15 @@ class HomeActivity : BaseActivity() {
super.onResume() super.onResume()
hideStatusBar() hideStatusBar()
handler.post(updateTask) handler.post(updateTask)
// D6DADEDB00010031323136334238423134303233303456322E313100FFFF // 启动无数据看门狗(每30秒检查一次)
// sendCmd("${HEADER}00010031323136334238423134303233303456322E313152${FOOTER}") handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
handler.removeCallbacks(updateTask) handler.removeCallbacks(updateTask)
// 停止无数据看门狗
handler.removeCallbacks(noDataWatchdogTask)
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -720,7 +585,6 @@ class HomeActivity : BaseActivity() {
binding.include?.let { binding.include?.let {
it.root.visible() it.root.visible()
it.root.setOnClickListener { it.root.setOnClickListener {
//viewModel.getAccessToken(App.deviceId)
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
} }
it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white) it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white)
@@ -728,11 +592,6 @@ class HomeActivity : BaseActivity() {
} }
} }
@Subscribe(threadMode = ThreadMode.MAIN)
public fun weightEvent(event: WeightEvent) {
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) { public fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo } val item = list.firstOrNull { it.deviceNo == event.shelfNo }
@@ -773,6 +632,7 @@ class HomeActivity : BaseActivity() {
private fun updateOverdueState() { private fun updateOverdueState() {
viewModel.updateOverdueState { state, items -> viewModel.updateOverdueState { state, items ->
if (items.isNullOrEmpty()) return@updateOverdueState if (items.isNullOrEmpty()) return@updateOverdueState
if (items.size != list.size) return@updateOverdueState
list.forEachIndexed { index, model -> list.forEachIndexed { index, model ->
//接口过期状态变化后更新状态 //接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) { if (items[index].overdue != model.overdue) {
@@ -792,10 +652,6 @@ class HomeActivity : BaseActivity() {
it.weight = it.weightBak it.weight = it.weightBak
} }
} }
// submitList.forEach {
// it.temperature = HomeActivity.showTemperatureC
// it.humidity = HomeActivity.showHumidity
// }
val body = ShelfBody().also { val body = ShelfBody().also {
it.deviceId = App.deviceId it.deviceId = App.deviceId
it.canteenId = App.canteenId it.canteenId = App.canteenId
@@ -0,0 +1,47 @@
package com.shuwei.intelligent.shelves.serial
/**
* 串口通信协议常量定义
* 统一管理所有协议头、协议尾及各类指令
*/
object ProtocolConstants {
/** 协议头 */
const val HEADER = "D6DADEDB"
/** 协议尾 */
const val FOOTER = "FFFF"
/** 激活指令(1楼餐厅) */
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
/** 左货架开锁指令 */
const val LEFT_SHELF_OPEN_CMD =
"${HEADER}020101000000000000000000000000000000000000${FOOTER}"
/** 右货架开锁指令 */
const val RIGHT_SHELF_OPEN_CMD =
"${HEADER}020100010000000000000000000000000000000000${FOOTER}"
/** 获取设备信息指令 */
const val DEVICE_INFO_CMD = "${HEADER}0301000000000000000000000000${FOOTER}"
/** 启动温控指令 */
const val START_TEMP_CTRL_CMD = "${HEADER}0F01360000${FOOTER}"
/** 华氏温度单位指令 */
const val F_TEMP_CMD = "${HEADER}0F01390000${FOOTER}"
/** 摄氏温度单位指令 */
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
/** 开灯指令(继电器1关) */
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
/** 开灯指令(继电器1开) */
const val LIGHT_OPEN = "${HEADER}020102020002020000000000000000000000000000${FOOTER}"
/** 温控指令 */
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}"
}
@@ -0,0 +1,80 @@
package com.shuwei.intelligent.shelves.serial
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
/**
* 串口数据帧解析器
* 负责将串口原始分片数据拼装为完整协议帧
* 完整帧格式:[HEADER][CMD][BODY][FOOTER]
*/
class SerialFrameParser {
/** 数据帧缓冲区,用于跨分片拼装 */
private val buffer = StringBuilder()
/**
* 解析原始串口分片数据,尝试拼装为完整帧
* @param srcData 原始串口数据片段
* @return 完整数据帧字符串;帧不完整时返回 null
*/
fun parse(srcData: String): String? {
val hasHeader = srcData.contains(HEADER)
val hasFooter = srcData.contains(FOOTER)
return when {
hasHeader && hasFooter -> parseCompleteInSingleChunk(srcData)
hasHeader && !hasFooter -> {
// 仅含帧头,等待后续分片
buffer.clear()
buffer.append(srcData.substring(srcData.lastIndexOf(HEADER))).append(",")
null
}
!hasHeader && !hasFooter -> {
// 中间分片,追加缓冲
buffer.append(srcData).append(",")
null
}
else -> {
// 仅含帧尾,与缓冲区拼接完整帧
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
buffer.append(srcData.substring(0, endIndex))
val frame = buffer.toString().replace(",", "")
buffer.clear()
frame.takeIf { it.isNotEmpty() }
}
}
}
/**
* 单个分片中同时含有帧头和帧尾的情况
*/
private fun parseCompleteInSingleChunk(srcData: String): String? {
buffer.clear()
val startIndex = srcData.lastIndexOf(HEADER)
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
return if (endIndex < startIndex) {
// 帧尾在帧头之前,说明是上一帧的残留尾部 + 新帧的头部
// 例:31314FFFFFD6DADEDB01010C
val partial = srcData.substring(startIndex)
if (partial.contains(FOOTER)) {
// 当前片段中新帧已完整
val newEnd = partial.indexOf(FOOTER) + FOOTER.length
partial.substring(0, newEnd)
} else {
// 新帧不完整,缓存等待后续分片
buffer.append(partial).append(",")
null
}
} else {
// 帧头在帧尾之前,正常完整帧
srcData.substring(startIndex, endIndex)
}
}
/** 清空缓冲区 */
fun reset() {
buffer.clear()
}
}
@@ -51,15 +51,13 @@ object SerialPortManager {
} }
// 启动接收协程 // 启动接收协程
@Suppress("DEPRECATION")
@SuppressLint("DefaultLocale")
fun startReceive(callback: (String) -> Unit) { fun startReceive(callback: (String) -> Unit) {
scope.launch { scope.launch {
while (isActive) { while (isActive) {
try { try {
val flow = serialPort?.read() val flow = serialPort?.read()
flow?.collect { buffer -> flow?.collect { buffer ->
val data = buffer.toHexString2().toUpperCase() val data = buffer.toHexString2().uppercase(Locale.ROOT)
callback(data) callback(data)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -71,7 +69,7 @@ object SerialPortManager {
} }
// 关闭串口 // 关闭串口
suspend fun close() { fun close() {
scope.cancel() scope.cancel()
serialPort?.close() serialPort?.close()
} }