From 9a795b9710be30fd6f2a09c4bf7611c282b3950e Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Wed, 1 Apr 2026 10:18:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(HomeActivity):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=B2=E5=8F=A3=E6=97=A0=E6=95=B0=E6=8D=AE=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 超过5分钟未收到串口数据时,自动弹窗提示用户考虑断电重启设备; 收到新数据后自动关闭弹窗并重置计时。 同步提交串口协议常量(ProtocolConstants)与帧解析器(SerialFrameParser)重构代码。 Co-Authored-By: Claude Sonnet 4.6 --- .../shelves/activity/HomeActivity.kt | 444 ++++++------------ .../shelves/serial/ProtocolConstants.kt | 47 ++ .../shelves/serial/SerialFrameParser.kt | 80 ++++ .../shelves/serial/SerialPortManager.kt | 6 +- 4 files changed, 279 insertions(+), 298 deletions(-) create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/serial/ProtocolConstants.kt create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialFrameParser.kt diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt index a03be83..99d1937 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt @@ -6,7 +6,6 @@ import android.graphics.Color import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.Log import android.util.SparseIntArray import androidx.activity.viewModels 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.ShelfModel 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.NetViewModel import com.shuwei.intelligent.shelves.net.RespData 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.utils.HexUtils import com.shuwei.intelligent.shelves.utils.IntervalExecutor import com.shuwei.intelligent.shelves.utils.binaryToHex import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard @@ -49,74 +53,39 @@ 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" - - //协议头 - 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 = mutableListOf() - val weightArray = SparseIntArray() - var shelfIndex = 0 - - // public fun getRealWeight():Double = weightArray[shelfIndex]?:0.0 - // 温度 - var showTemperatureC = "0" - - // 湿度 - var showHumidity = "0" } - //401-false,1楼餐厅-true + // 货架列表数据 + private val list: MutableList = 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 lateinit var binding: ActivityHomeBinding private val viewModel: NetViewModel by viewModels() - @SuppressLint("NotifyDataSetChanged", "HardwareIds") + @SuppressLint("NotifyDataSetChanged") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityHomeBinding.inflate(layoutInflater) @@ -124,26 +93,25 @@ class HomeActivity : BaseActivity() { setBackground() setContentView(binding.root) -// updateLeftStatus("-°C / -%") updateLeftStatus("") initRecyclerView() + // 串口初始化,独立协程,不阻塞网络状态监听 lifecycleScope.launch { val openState = SerialPortManager.open() log("onCreate: openState=$openState") if (openState) { SerialPortManager.startReceive { data -> - // 更新UI显示接收数据 + // 切换到主线程协程处理串口数据 lifecycleScope.launch { - try { - receiveSerialPortData(data) - } catch (e: Exception) { - e.printStackTrace() - } + runCatching { receiveSerialPortData(data) } + .onFailure { it.printStackTrace() } } } - //SerialPortManager.send(DEVICE_INFO_CMD) } + } + // 网络状态监听,独立协程,与串口初始化并行执行 + lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.getAccessTokenUiState.collect { state -> @@ -167,59 +135,21 @@ class HomeActivity : BaseActivity() { } } } - //sendCmd(ACTIVE_CMD) - binding.root.postDelayed({ sendCmd(START_TEMP_CTRL_CMD) sendCmd(C_TEMP_CMD) }, 5000) -// TaskManager.startTask() - saveGoodsTask() 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) - -// 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<*>) { log("getTokenSuccess: $data") data.data?.let { App.accessToken = it.toString() - //UIUtils.toast(it.toString()) viewModel.getShelfList(deviceId = App.deviceId) } } @@ -238,7 +168,6 @@ class HomeActivity : BaseActivity() { } deviceName = data.data.deviceName -// updateLeftStatus("$deviceName -°C / -%") updateLeftStatus("$deviceName") App.canteenId = data.data.placeId @@ -247,10 +176,6 @@ class HomeActivity : BaseActivity() { loadEmptyView() return } -// if (tempList.size < 10) { -// toast("数据错误") -// return -// } list.clear() val subList01 = tempList.subList(0, tempList.size / 2) val subList02 = tempList.subList(tempList.size / 2, tempList.size) @@ -277,51 +202,15 @@ class HomeActivity : BaseActivity() { */ private var terminalVersion: String = "" - private val serialReader by lazy { StringBuilder() } + private val serialFrameParser = SerialFrameParser() private fun receiveSerialPortData(srcData: String) { log("receiveSerialPortData: $srcData") - var data = "" - if (srcData.contains(HEADER) && srcData.contains(FOOTER)) { - serialReader.clear() - val startIndex = srcData.lastIndexOf(HEADER) - var endIndex = srcData.indexOf(FOOTER) + FOOTER.length - if (endIndex < startIndex) { - //类似这种:31314FFFFFD6DADEDB01010C,FOOTER在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) + // 重置无数据计时,并关闭已显示的超时提示弹窗 + lastDataReceivedTime = System.currentTimeMillis() + noDataWarningDialog?.takeIf { it.isShowing }?.dismiss() + val frame = serialFrameParser.parse(srcData) ?: return + handleReceivedData(frame) } private fun handleReceivedData(data: String) { @@ -358,7 +247,6 @@ class HomeActivity : BaseActivity() { val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER" log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}") sendCmd(respCmd) - //lockFeedback(data) } "0302" -> { @@ -383,7 +271,8 @@ class HomeActivity : BaseActivity() { "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 time = timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("") @@ -406,26 +295,19 @@ class HomeActivity : BaseActivity() { "${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}" log(logMsg) sendCmd(respCmd) - //val tempC = HexUtils.signedHexToDec(data.take(4)) * 0.1 -// updateLeftStatus("$deviceName ${"%.1f".format(tempC)}°C / -%") -// updateLeftStatus("$deviceName -°C / -%") updateLeftStatus("$deviceName") - //D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF - runOnUiThread { - if (tipDialog == null) { - CommonDialog(this).apply { - dialogTitle = "温馨提示" -// dialogContent = logMsg - confirmBlock = { - logMsg?.copyTextToClipboard(this@HomeActivity) - } + if (tipDialog == null) { + tipDialog = CommonDialog(this).apply { + dialogTitle = "温馨提示" + confirmBlock = { + logMsg.copyTextToClipboard(this@HomeActivity) } } - if (tipDialog?.isShowing == false) { - tipDialog?.dialogContent = logMsg - tipDialog?.show() - } + } + if (tipDialog?.isShowing == false) { + tipDialog?.dialogContent = logMsg + tipDialog?.show() } } @@ -435,8 +317,6 @@ class HomeActivity : BaseActivity() { "0F02" -> { log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}") - //D6DADEDB0F020331323136334238423134303233303456322E31313EFFFF00000055010000000000002707000001000000220100000000000091040000120100000100000095140080611400800000000000000000000000000000000000000000010100005031323136334238423134303233303456322E31313EFFFF - runOnUiThread { } } else -> {} @@ -445,29 +325,33 @@ class HomeActivity : BaseActivity() { private var tipDialog: CommonDialog? = null - private var fixedDataBody = "" + /** 最后一次成功收到串口数据的时间戳(毫秒) */ + private var lastDataReceivedTime = System.currentTimeMillis() -// private fun lockFeedback(data: String) { -// runCatching { -// val fixedDataBodyEndIndex = data.length - FOOTER.length -// val fixedDataBodyStartIndex = HEADER.length + "0202".length + 42 -// fixedDataBody = data.substring(fixedDataBodyStartIndex, fixedDataBodyEndIndex) -// -// val lockNo = if (list[shelfIndex].deviceNo in 1..5) "01" else "02" -// val lockOneState = data.substring(HEADER.length + 4, HEADER.length + 6) -// if (lockOneState == "00") { -// //锁1关 -// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}") -// } -// val lockTwoState = data.substring(HEADER.length + 6, HEADER.length + 8) -// if (lockTwoState == "00") { -// //锁2关 -// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}") -// } -// }.onFailure { -// it.printStackTrace() -// } -// } + /** 无数据超时提示弹窗 */ + private var noDataWarningDialog: CommonDialog? = null + + /** + * 无数据看门狗:每30秒检查一次,若超过5分钟未收到数据则弹出提示。 + * 收到数据后会自动重置计时并关闭弹窗。 + */ + private val noDataWatchdogTask = object : Runnable { + override fun run() { + val elapsed = System.currentTimeMillis() - lastDataReceivedTime + if (elapsed >= 5 * 60 * 1000L) { + if (noDataWarningDialog == null) { + noDataWarningDialog = CommonDialog(this@HomeActivity).apply { + dialogTitle = "设备提示" + dialogContent = "长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备" + } + } + if (noDataWarningDialog?.isShowing == false) { + noDataWarningDialog?.show() + } + } + handler.postDelayed(this, 30 * 1000L) + } + } private fun heartBeat(data: String) { runCatching { @@ -483,14 +367,6 @@ class HomeActivity : BaseActivity() { ) val temperatureStartIndex = 158 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) }.onFailure { @@ -532,23 +408,11 @@ class HomeActivity : BaseActivity() { log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${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 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) { @@ -562,17 +426,6 @@ class HomeActivity : BaseActivity() { e.printStackTrace() 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") getWeightInfo( @@ -584,13 +437,23 @@ class HomeActivity : BaseActivity() { getFixDataBody(data, temperatureEndIndex) } - fun getFixDataBody(data: String, temperatureEndIndex: Int) { + /** + * 从心跳帧或设备状态帧中补充解析 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) @@ -602,75 +465,74 @@ class HomeActivity : BaseActivity() { } override fun onDestroy() { - lifecycleScope.launch { - SerialPortManager.close() - } -// TaskManager.cancelTask() + SerialPortManager.close() saveTaskJob?.cancel() overdueTaskJob?.cancel() EventBus.getDefault().unregister(this) super.onDestroy() } - @Suppress("DEPRECATION") private val shelfAdapter by lazy { ShelfAdapter(list).apply { setOnItemClickListener { _, _, position -> - shelfIndex = position - lifecycleScope.launch { - if (isNeedOpenLock) { - val openCmd = if (list[position].deviceNo in 1..5) - LEFT_SHELF_OPEN_CMD - else - RIGHT_SHELF_OPEN_CMD - val openState = SerialPortManager.send(openCmd) - } -// if (openState.not()) { -// toast("${list[position].goodsName}柜门开启失败") -// return@launch -// } - //SerialPortManager.send(START_CMD) - launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { - it.putExtra(ShelfActivity.SHELF_MODEL, list[position]) - }) { - it?.run { - val model = - getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel - list[position].apply { - goodsId = model.goodsId - goodsName = model.goodsName - weight = model.weight - if (model.putCabinetDay.isNullOrBlank().not()) { - putCabinetDay = model.putCabinetDay - } - } - notifyItemChanged(position) - - try { - - val submitList = list.sortedBy { item -> item.deviceNo } - val body = ShelfBody().also { body -> - body.deviceId = App.deviceId ?: "" - body.canteenId = App.canteenId - body.temperature = showTemperatureC - body.humidity = showHumidity - body.goodsList = submitList - } - Log.d("mzf", "performSync: body:${body.toJsonString()}") - lifecycleScope.launch { - val resp = apiService.saveShelfGoodsList(body = body) - Log.d("mzf", "performSync: resp:${resp.toJsonString()}") - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - } + onShelfItemClicked(position) } } } + /** + * 处理货架格子点击事件:按需开锁,然后跳转 ShelfActivity + */ + private fun onShelfItemClicked(position: Int) { + shelfIndex = position + lifecycleScope.launch { + if (isNeedOpenLock) { + val openCmd = if (list[position].deviceNo in 1..5) + LEFT_SHELF_OPEN_CMD + else + RIGHT_SHELF_OPEN_CMD + SerialPortManager.send(openCmd) + } + launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { + it.putExtra(ShelfActivity.SHELF_MODEL, list[position]) + }) { result -> + result?.let { onShelfActivityResult(position, it) } + } + } + } + + /** + * 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端 + */ + @Suppress("DEPRECATION") + private fun onShelfActivityResult(position: Int, intent: Intent) { + val model = intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel + list[position].apply { + goodsId = model.goodsId + goodsName = model.goodsName + weight = model.weight + if (model.putCabinetDay.isNullOrBlank().not()) { + putCabinetDay = model.putCabinetDay + } + } + 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() + } + } + private fun initRecyclerView() { binding.rvShelf.run { layoutManager = @@ -685,9 +547,10 @@ class HomeActivity : BaseActivity() { } } + private val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) } + private fun updateDateTime() { - val sdf = SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) - val dateTime = sdf.format(Date()) + val dateTime = dateTimeFormatter.format(Date()) updateRightStatus(dateTime) } @@ -703,13 +566,15 @@ class HomeActivity : BaseActivity() { super.onResume() hideStatusBar() handler.post(updateTask) -// D6DADEDB00010031323136334238423134303233303456322E313100FFFF -// sendCmd("${HEADER}00010031323136334238423134303233303456322E313152${FOOTER}") + // 启动无数据看门狗(每30秒检查一次) + handler.postDelayed(noDataWatchdogTask, 30 * 1000L) } override fun onPause() { super.onPause() handler.removeCallbacks(updateTask) + // 停止无数据看门狗 + handler.removeCallbacks(noDataWatchdogTask) } @SuppressLint("NotifyDataSetChanged") @@ -720,7 +585,6 @@ class HomeActivity : BaseActivity() { binding.include?.let { it.root.visible() it.root.setOnClickListener { - //viewModel.getAccessToken(App.deviceId) viewModel.getShelfList(deviceId = App.deviceId) } 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) public fun clearEmptyShelf(event: ClearShelfEvent) { val item = list.firstOrNull { it.deviceNo == event.shelfNo } @@ -773,6 +632,7 @@ class HomeActivity : BaseActivity() { private fun updateOverdueState() { viewModel.updateOverdueState { state, items -> if (items.isNullOrEmpty()) return@updateOverdueState + if (items.size != list.size) return@updateOverdueState list.forEachIndexed { index, model -> //接口过期状态变化后更新状态 if (items[index].overdue != model.overdue) { @@ -792,10 +652,6 @@ class HomeActivity : BaseActivity() { it.weight = it.weightBak } } -// submitList.forEach { -// it.temperature = HomeActivity.showTemperatureC -// it.humidity = HomeActivity.showHumidity -// } val body = ShelfBody().also { it.deviceId = App.deviceId it.canteenId = App.canteenId diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/ProtocolConstants.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ProtocolConstants.kt new file mode 100644 index 0000000..d8fc60a --- /dev/null +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ProtocolConstants.kt @@ -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}" +} diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialFrameParser.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialFrameParser.kt new file mode 100644 index 0000000..b6c915e --- /dev/null +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialFrameParser.kt @@ -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() + } +} diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt index 3727f44..9d7aa5c 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt @@ -51,15 +51,13 @@ object SerialPortManager { } // 启动接收协程 - @Suppress("DEPRECATION") - @SuppressLint("DefaultLocale") fun startReceive(callback: (String) -> Unit) { scope.launch { while (isActive) { try { val flow = serialPort?.read() flow?.collect { buffer -> - val data = buffer.toHexString2().toUpperCase() + val data = buffer.toHexString2().uppercase(Locale.ROOT) callback(data) } } catch (e: Exception) { @@ -71,7 +69,7 @@ object SerialPortManager { } // 关闭串口 - suspend fun close() { + fun close() { scope.cancel() serialPort?.close() }