From a116c4fc6cc45a92208e211308343ba2b67c9f99 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Tue, 31 Mar 2026 10:57:40 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=E9=87=8D=E6=9E=84=EF=BC=9A=E6=8B=86?= =?UTF-8?q?=E5=88=86=20HomeActivity=20=E8=81=8C=E8=B4=A3=EF=BC=8C=E5=88=86?= =?UTF-8?q?=E5=B1=82=E8=A7=A3=E8=80=A6=E4=B8=B2=E5=8F=A3=E4=B8=8E=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E9=80=BB=E8=BE=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e6cfc4dff2ad2ea50ed6002d6df56baf3fdf5b6d. --- .../shelves/activity/HomeActivity.kt | 633 +++++++++++++++--- .../intelligent/shelves/net/HomeViewModel.kt | 165 +---- .../shelves/net/SerialViewModel.kt | 157 ----- .../shelves/serial/SerialEventListener.kt | 40 -- .../shelves/serial/SerialProtocolParser.kt | 326 --------- app/src/main/res/layout/dialog_layout.xml | 50 -- 6 files changed, 540 insertions(+), 831 deletions(-) delete mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/net/SerialViewModel.kt delete mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialEventListener.kt delete mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolParser.kt delete mode 100644 app/src/main/res/layout/dialog_layout.xml 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 72adb97..6975b5b 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 @@ -30,13 +30,20 @@ import com.shuwei.intelligent.shelves.net.HomeViewModel 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.SerialViewModel import com.shuwei.intelligent.shelves.net.UiState +import com.shuwei.intelligent.shelves.net.apiService +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 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.channels.Channel import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -49,47 +56,58 @@ 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}" - // 1楼餐厅激活码 + //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) + //开灯(开 继电器1) const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}" const val LIGHT_OPEN = "${HEADER}020102020002020000000000000000000000000000${FOOTER}" const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}" + } - // 401-false,1楼餐厅-true + //401-false,1楼餐厅-true private var isNeedOpenLock = true private lateinit var binding: ActivityHomeBinding private val viewModel: NetViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels() - private val serialViewModel: SerialViewModel by viewModels() @SuppressLint("NotifyDataSetChanged", "HardwareIds") override fun onCreate(savedInstanceState: Bundle?) { @@ -99,37 +117,38 @@ class HomeActivity : BaseActivity() { setBackground() setContentView(binding.root) +// updateLeftStatus("-°C / -%") updateLeftStatus("") + initRecyclerView() - // 打开串口,串口生命周期由 SerialViewModel 管理 - serialViewModel.openSerial() + // 串口初始化:独立协程,仅执行一次 + lifecycleScope.launch { + val openState = SerialPortManager.open() + log("onCreate: openState=$openState") + if (openState) { + // 串口回调在 IO 线程,通过 Channel 无锁传递数据,不再嵌套 launch + SerialPortManager.startReceive { data -> + serialDataChannel.trySend(data) + } + //SerialPortManager.send(DEVICE_INFO_CMD) + } + } - // 统一注册所有 ViewModel Flow 观察 - observeViewModels() + // 串口数据消费:单一协程串行处理,serialReader 访问不存在并发竞争 + lifecycleScope.launch { + for (data in serialDataChannel) { + try { + receiveSerialPortData(data) + } catch (e: Exception) { + e.printStackTrace() + } + } + } - // 延迟发送温控初始化命令 - binding.root.postDelayed({ - serialViewModel.sendCmd(START_TEMP_CTRL_CMD) - serialViewModel.sendCmd(C_TEMP_CMD) - }, 5000) - - // 启动定时任务(保存货架数据 + 刷新过期状态),生命周期由 viewModelScope 管理 - homeViewModel.startPeriodicTasks() - - // 拉取货架列表 - viewModel.getShelfList(deviceId = App.deviceId) - } - - /** - * 统一注册所有 ViewModel Flow 观察。 - * 在 STARTED 状态下收集,onStop 时自动暂停,onStart 时恢复。 - */ - private fun observeViewModels() { + // ViewModel 状态观察:STARTED 状态下收集,onStop 时自动取消,onStart 时恢复 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - - // 获取 Token 结果 launch { viewModel.getAccessTokenUiState.collect { state -> when (state) { @@ -140,8 +159,6 @@ class HomeActivity : BaseActivity() { } } } - - // 货架列表加载结果 launch { viewModel.getShelfListUiState.collect { state -> when (state) { @@ -152,52 +169,61 @@ class HomeActivity : BaseActivity() { } } } - - // 串口重量更新:更新 ViewModel 缓存 + 局部刷新 Adapter - launch { - serialViewModel.weightFlow.collect { (deviceNo, weight) -> - homeViewModel.putWeight(deviceNo, weight) - if (deviceNo in 1..10 && homeViewModel.shelfItems.size == 10) { - homeViewModel.shelfItems.firstOrNull { it.deviceNo == deviceNo } - ?.let { model -> - model.weight = weight.toDouble() - val pos = homeViewModel.shelfItems.indexOf(model) - shelfAdapter.notifyItemChanged(pos) - log("weightFlow: deviceNo=${model.deviceNo}, weight=$weight") - EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight)) - } - } - } - } - - // 串口温控上报:弹窗展示日志 - launch { - serialViewModel.tempCtrlEventFlow.collect { logMsg -> - showTipDialog(logMsg) - } - } - - // 串口触发顶部状态栏刷新 - launch { - serialViewModel.deviceStatusRefreshFlow.collect { - updateLeftStatus(homeViewModel.deviceName) - } - } - - // 过期状态变化:局部刷新对应 position - launch { - homeViewModel.overdueUpdateFlow.collect { positions -> - positions.forEach { shelfAdapter.notifyItemChanged(it) } - } - } } } + //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() { + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 4, goodsName = "梨", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 9, goodsName = "梨", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.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) } } @@ -214,9 +240,10 @@ class HomeActivity : BaseActivity() { loadEmptyView() return } - // 将 deviceName 写入 HomeViewModel,跨配置变更保持 - homeViewModel.deviceName = data.data.deviceName - updateLeftStatus(homeViewModel.deviceName) + deviceName = data.data.deviceName + +// updateLeftStatus("$deviceName -°C / -%") + updateLeftStatus("$deviceName") App.canteenId = data.data.placeId val tempList = data.data.containerGoodsList @@ -224,6 +251,10 @@ class HomeActivity : BaseActivity() { loadEmptyView() return } +// if (tempList.size < 10) { +// toast("数据错误") +// return +// } homeViewModel.shelfItems.clear() val subList01 = tempList.subList(0, tempList.size / 2) val subList02 = tempList.subList(tempList.size / 2, tempList.size) @@ -241,30 +272,363 @@ class HomeActivity : BaseActivity() { } /** - * 显示温控信息弹窗。 - * 若弹窗已在展示中则不重复打开,仅更新内容。 + * 终端id */ - private fun showTipDialog(logMsg: String) { - if (tipDialog == null) { - tipDialog = CommonDialog(this).apply { - dialogTitle = "温馨提示" - confirmBlock = { logMsg.copyTextToClipboard(this@HomeActivity) } + private var terminalId: String = "" + + /** + * 固件版本 + */ + private var terminalVersion: String = "" + + /** + * 串口数据接收通道,无界缓冲。 + * IO 线程(串口回调)通过 trySend 写入,单一消费协程串行读取, + * 确保 serialReader 始终在同一协程中访问,规避并发竞争。 + */ + private val serialDataChannel = Channel(Channel.UNLIMITED) + + /** + * 串口数据拼包缓冲器。 + * 仅由 serialDataChannel 的单一消费协程访问,线程安全。 + */ + private val serialReader = StringBuilder() + + 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 (tipDialog?.isShowing == false) { - tipDialog?.dialogContent = logMsg - tipDialog?.show() + 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) { + 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) + //lockFeedback(data) + } + + "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 timeLong = System.currentTimeMillis() / 1000 + (8 * 3600) + 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) + //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) { + tipDialog = CommonDialog(this).apply { + dialogTitle = "温馨提示" +// dialogContent = logMsg + 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}") + //D6DADEDB0F020331323136334238423134303233303456322E31313EFFFF00000055010000000000002707000001000000220100000000000091040000120100000100000095140080611400800000000000000000000000000000000000000000010100005031323136334238423134303233303456322E31313EFFFF + runOnUiThread { } + } + + else -> {} } } private var tipDialog: CommonDialog? = null + private var fixedDataBody = "" + +// 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 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 +// 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 { + 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")) { + //正数:按十六进制解析,失败时兜底为 0 + realWeight = weightHex.toIntOrNull(16) ?: 0 + } else { + //负数:清除符号位后取绝对值,再取反 + firstByteBinary = "0${firstByteBinary.substring(1)}" + val firstByteHex = binaryToHex(firstByteBinary) + weightHex = firstByteHex + weightHex.substring(2) + realWeight = -1 * (weightHex.toIntOrNull(16) ?: 0) + } + homeViewModel.putWeight(index, realWeight) + + if (index in 1..10 && homeViewModel.shelfItems.size == 10) { + homeViewModel.shelfItems.firstOrNull { it.deviceNo == index }?.let { + it.weight = realWeight.toDouble() + val pos = homeViewModel.shelfItems.indexOf(it) + shelfAdapter.notifyItemChanged(pos) + 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) { + 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 + } +// 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( + start = weightStartIndex, + end = temperatureStartIndex, + data = data + ) + + getFixDataBody(data, temperatureEndIndex) + } + + 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) + if (fixDataBody.length == 42) { + terminalId = fixDataBody.substring(0, 30) + terminalVersion = fixDataBody.substring(30, 40) + log("receiveSerialPortData:从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}") + } + } catch (e: Exception) { + e.printStackTrace() + } + } + override fun onDestroy() { - // 弹窗需在 Activity 销毁前手动关闭,防止窗口泄漏 + lifecycleScope.launch { + SerialPortManager.close() + } +// TaskManager.cancelTask() + saveTaskJob?.cancel() + overdueTaskJob?.cancel() + // 关闭 Channel,消费协程的 for 循环将随之结束 + serialDataChannel.close() + // 销毁前关闭 Dialog,防止 Activity 已销毁时窗口仍附着导致泄漏 tipDialog?.dismiss() tipDialog = null EventBus.getDefault().unregister(this) - // 串口关闭、Channel 关闭、定时任务取消均由各 ViewModel.onCleared() 自动管理 super.onDestroy() } @@ -273,14 +637,18 @@ class HomeActivity : BaseActivity() { setOnItemClickListener { _, _, position -> homeViewModel.selectedShelfIndex = position lifecycleScope.launch { - // 根据货架编号决定开启左侧或右侧柜锁 if (isNeedOpenLock) { val openCmd = if (homeViewModel.shelfItems[position].deviceNo in 1..5) LEFT_SHELF_OPEN_CMD else RIGHT_SHELF_OPEN_CMD - serialViewModel.sendCmd(openCmd) + val openState = SerialPortManager.send(openCmd) } +// if (openState.not()) { +// toast("${homeViewModel.shelfItems[position].goodsName}柜门开启失败") +// return@launch +// } + //SerialPortManager.send(START_CMD) launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { it.putExtra(ShelfActivity.SHELF_MODEL, homeViewModel.shelfItems[position]) }) { @@ -292,8 +660,6 @@ class HomeActivity : BaseActivity() { @Suppress("DEPRECATION") getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel } ?: return@run - - // 将 ShelfActivity 返回的数据同步回列表 homeViewModel.shelfItems[position].apply { goodsId = model.goodsId goodsName = model.goodsName @@ -304,8 +670,8 @@ class HomeActivity : BaseActivity() { } notifyItemChanged(position) - // 货架操作后立即将最新数据同步到服务端 try { + val submitList = homeViewModel.shelfItems.sortedBy { item -> item.deviceNo } val body = ShelfBody().also { body -> body.deviceId = App.deviceId ?: "" @@ -314,8 +680,11 @@ class HomeActivity : BaseActivity() { body.humidity = homeViewModel.humidity body.goodsList = submitList } - Log.d(TAG, "shelfItemClick sync: body=${body.toJsonString()}") - viewModel.saveShelfGoodsList(body) + 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() } @@ -331,6 +700,7 @@ class HomeActivity : BaseActivity() { layoutManager = GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false) adapter = shelfAdapter + itemAnimator.let { if (it is DefaultItemAnimator) { it.supportsChangeAnimations = false @@ -357,6 +727,8 @@ class HomeActivity : BaseActivity() { super.onResume() hideStatusBar() handler.post(updateTask) +// D6DADEDB00010031323136334238423134303233303456322E313100FFFF +// sendCmd("${HEADER}00010031323136334238423134303233303456322E313152${FOOTER}") } override fun onPause() { @@ -368,9 +740,11 @@ class HomeActivity : BaseActivity() { private fun loadEmptyView() { homeViewModel.shelfItems.clear() shelfAdapter.notifyDataSetChanged() + 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) @@ -379,11 +753,12 @@ class HomeActivity : BaseActivity() { } @Subscribe(threadMode = ThreadMode.MAIN) - fun weightEvent(event: WeightEvent) { + public fun weightEvent(event: WeightEvent) { + } @Subscribe(threadMode = ThreadMode.MAIN) - fun clearEmptyShelf(event: ClearShelfEvent) { + public fun clearEmptyShelf(event: ClearShelfEvent) { val item = homeViewModel.shelfItems.firstOrNull { it.deviceNo == event.shelfNo } item?.apply { goodsId = "" @@ -395,4 +770,66 @@ class HomeActivity : BaseActivity() { } shelfAdapter.notifyItemChanged(homeViewModel.shelfItems.indexOf(item)) } -} + + private val taskExecutor by lazy { IntervalExecutor() } + private var saveTaskJob: Job?=null + + /** + * 保存定时任务 + */ + private fun saveGoodsTask() { + saveTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) { + saveShelfGoodsList() + } + } + + private var overdueTaskJob: Job?=null + + /** + * 过期时间定时任务 + */ + private fun overdueTask() { + overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60* 1000L) { + updateOverdueState() + } + } + + private fun updateOverdueState() { + viewModel.updateOverdueState { state, items -> + if (items.isNullOrEmpty()) return@updateOverdueState + homeViewModel.shelfItems.forEachIndexed { index, model -> + // 按 deviceNo 精确匹配接口数据,避免: + // 1. 两端列表大小不一致时的 IndexOutOfBoundsException + // 2. 本地列表交叉排列与接口顺序不一致导致的状态串位 + val serverModel = items.find { it.deviceNo == model.deviceNo } + ?: return@forEachIndexed + if (serverModel.overdue != model.overdue) { + model.overdue = serverModel.overdue + shelfAdapter.notifyItemChanged(index) + log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}") + } + } + } + } + + private fun saveShelfGoodsList() { + log("performSync: list:${homeViewModel.shelfItems.toJsonString()}") + val submitList = homeViewModel.shelfItems.sortedBy { it.deviceNo } + submitList.forEach { + if (it.goodsId.isNullOrBlank()) { + it.weight = it.weightBak + } + } + val body = ShelfBody().also { + it.deviceId = App.deviceId + it.canteenId = App.canteenId + it.temperature = homeViewModel.temperatureC + it.humidity = homeViewModel.humidity + it.goodsList = submitList + } + log("performSync: body:${body.toJsonString()}") + viewModel.saveShelfGoodsList(body) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt index 17274e9..d228d8b 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt @@ -1,48 +1,18 @@ package com.shuwei.intelligent.shelves.net -import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.shuwei.intelligent.shelves.App -import com.shuwei.intelligent.shelves.model.ShelfBody import com.shuwei.intelligent.shelves.model.ShelfModel -import com.shuwei.intelligent.shelves.utils.ext.toJsonString -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.launch /** * HomeActivity 专用 ViewModel。 * - * 职责: - * 1. 管理货架列表、重量、温湿度等 UI 状态数据; - * 2. 持有 deviceName,供 Activity 更新顶部状态栏; - * 3. 管理定时任务(保存货架数据、刷新过期状态),生命周期由 viewModelScope 保障, - * 跨配置变更存活,Activity 销毁时自动取消,无需手动持有 Job 引用。 + * 职责:管理原本散落在 HomeActivity.companion object 中的 UI 状态数据, + * 避免静态共享状态导致的生命周期污染与状态残留问题。 + * + * 包含:货架列表、重量数据、选中索引、温湿度。 */ class HomeViewModel : ViewModel() { - companion object { - private const val TAG = "HomeViewModel" - - /** 首次保存延迟(1 分钟) */ - private const val SAVE_INITIAL_DELAY = 1 * 60 * 1000L - - /** 保存间隔(5 分钟) */ - private const val SAVE_INTERVAL = 5 * 60 * 1000L - - /** 过期检查首次延迟(5 秒) */ - private const val OVERDUE_INITIAL_DELAY = 5 * 1000L - - /** 过期检查间隔(5 分钟) */ - private const val OVERDUE_INTERVAL = 5 * 60 * 1000L - } - - // ---------------------------------------------------------------- - // UI 状态数据 - // ---------------------------------------------------------------- - /** * 货架列表。 * Adapter 持有同一个列表引用,修改后需调用 notifyDataSetChanged / notifyItemChanged 触发刷新。 @@ -52,6 +22,7 @@ class HomeViewModel : ViewModel() { /** * 重量数据缓存。 * key:deviceNo(货架编号),value:重量(克)。 + * 替代原 SparseIntArray,使用更符合 Kotlin 习惯的 MutableMap。 */ private val weightMap: MutableMap = mutableMapOf() @@ -70,28 +41,6 @@ class HomeViewModel : ViewModel() { */ var humidity: String = "0" - /** - * 设备名称,由网络接口响应写入,供顶部状态栏显示。 - * 原散落于 HomeActivity 局部变量,现迁入 ViewModel 以跨配置变更保持。 - */ - var deviceName: String = "" - - // ---------------------------------------------------------------- - // 对外暴露的 Flow - // ---------------------------------------------------------------- - - /** - * 过期状态变化通知。 - * 发射需要局部刷新的 adapter position 列表,Activity 收到后调用 notifyItemChanged。 - * 仅在状态实际发生变化时发射,避免无效刷新。 - */ - private val _overdueUpdateFlow = MutableSharedFlow>(extraBufferCapacity = 4) - val overdueUpdateFlow: SharedFlow> = _overdueUpdateFlow - - // ---------------------------------------------------------------- - // 重量操作 - // ---------------------------------------------------------------- - /** * 更新指定货架编号的重量。 * @param deviceNo 货架编号 @@ -107,108 +56,4 @@ class HomeViewModel : ViewModel() { * @return 重量(克),若无记录返回 0 */ fun getWeight(deviceNo: Int): Int = weightMap[deviceNo] ?: 0 - - // ---------------------------------------------------------------- - // 定时任务 - // ---------------------------------------------------------------- - - /** - * 启动所有定时任务。 - * 应在 Activity.onCreate 中调用一次(ViewModel 跨配置变更存活,不会重复启动)。 - * 任务在 viewModelScope 中运行,ViewModel 销毁时自动取消,无需手动管理 Job。 - */ - fun startPeriodicTasks() { - startSaveTask() - startOverdueTask() - } - - /** - * 启动货架数据定时保存任务(延迟1分钟后,每5分钟执行一次)。 - */ - private fun startSaveTask() { - viewModelScope.launch { - delay(SAVE_INITIAL_DELAY) - while (true) { - periodicSave() - delay(SAVE_INTERVAL) - } - } - } - - /** - * 启动过期状态定时检查任务(延迟5秒后,每5分钟执行一次)。 - */ - private fun startOverdueTask() { - viewModelScope.launch { - delay(OVERDUE_INITIAL_DELAY) - while (true) { - periodicUpdateOverdue() - delay(OVERDUE_INTERVAL) - } - } - } - - // ---------------------------------------------------------------- - // 定时任务内部业务逻辑 - // ---------------------------------------------------------------- - - /** - * 将当前货架数据同步到服务端。 - * 若货架无商品(goodsId 为空),则上报备份重量(weightBak)。 - */ - private suspend fun periodicSave() { - Log.d(TAG, "periodicSave: list=${shelfItems.toJsonString()}") - val submitList = shelfItems.sortedBy { it.deviceNo } - submitList.forEach { - if (it.goodsId.isNullOrBlank()) { - it.weight = it.weightBak - } - } - val body = ShelfBody().also { - it.deviceId = App.deviceId - it.canteenId = App.canteenId - it.temperature = temperatureC - it.humidity = humidity - it.goodsList = submitList - } - Log.d(TAG, "periodicSave: body=${body.toJsonString()}") - runCatching { - val resp = apiService.saveShelfGoodsList(body = body) - Log.d(TAG, "periodicSave: resp=${resp.toJsonString()}") - }.onFailure { - it.printStackTrace() - Log.d(TAG, "periodicSave 异常:${it.message}") - } - } - - /** - * 从服务端拉取货架列表,对比本地数据,将变化的过期状态通过 [overdueUpdateFlow] 发射。 - * 按 deviceNo 精确匹配,避免列表顺序不一致导致的状态串位。 - */ - private suspend fun periodicUpdateOverdue() { - runCatching { - val response = apiService.getShelfList(deviceId = App.deviceId) - if (!response.isSuccess()) return - val serverItems = response.data?.containerGoodsList ?: return - if (serverItems.isEmpty()) return - - // 找出过期状态发生变化的 position 列表 - val changedPositions = mutableListOf() - shelfItems.forEachIndexed { index, model -> - val serverModel = serverItems.find { it.deviceNo == model.deviceNo } - ?: return@forEachIndexed - if (serverModel.overdue != model.overdue) { - model.overdue = serverModel.overdue - changedPositions.add(index) - Log.d(TAG, "过期状态变更:deviceNo=${model.deviceNo}, 商品=${model.goodsName}, overdue=${model.overdue}") - } - } - - if (changedPositions.isNotEmpty()) { - _overdueUpdateFlow.emit(changedPositions) - } - }.onFailure { - it.printStackTrace() - } - } } diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/SerialViewModel.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/SerialViewModel.kt deleted file mode 100644 index d271384..0000000 --- a/app/src/main/java/com/shuwei/intelligent/shelves/net/SerialViewModel.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.shuwei.intelligent.shelves.net - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.shuwei.intelligent.shelves.serial.SerialEventListener -import com.shuwei.intelligent.shelves.serial.SerialPortManager -import com.shuwei.intelligent.shelves.serial.SerialProtocolParser -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.launch - -/** - * 串口通信 ViewModel。 - * 职责: - * 1. 管理串口的生命周期(open / close); - * 2. 持有 [SerialProtocolParser] 实例,实现 [SerialEventListener] 接收解析结果; - * 3. 通过 [weightFlow]、[tempCtrlEventFlow]、[deviceStatusRefreshFlow] 将事件暴露给 UI 层。 - * - * 不持有任何 View / Context 引用,可安全跨配置变更存活。 - */ -class SerialViewModel : ViewModel() { - - companion object { - private const val TAG = "SerialViewModel" - } - - // ---------------------------------------------------------------- - // 对外暴露的 Flow(Activity 通过 repeatOnLifecycle 收集) - // ---------------------------------------------------------------- - - /** - * 货架重量更新事件。 - * Pair.first = 货架编号(deviceNo),Pair.second = 重量(克)。 - */ - private val _weightFlow = MutableSharedFlow>(extraBufferCapacity = 64) - val weightFlow: SharedFlow> = _weightFlow - - /** - * 温控信息上报事件,携带完整日志字符串,Activity 收到后弹窗展示。 - */ - private val _tempCtrlEventFlow = MutableSharedFlow(extraBufferCapacity = 8) - val tempCtrlEventFlow: SharedFlow = _tempCtrlEventFlow - - /** - * 设备状态刷新信号。 - * 串口收到需要刷新顶部状态栏的命令时触发,Activity 收到后用 deviceName 重绘。 - */ - private val _deviceStatusRefreshFlow = MutableSharedFlow(extraBufferCapacity = 4) - val deviceStatusRefreshFlow: SharedFlow = _deviceStatusRefreshFlow - - // ---------------------------------------------------------------- - // 内部:串口数据通道 + 协议解析器 - // ---------------------------------------------------------------- - - /** - * 串口数据接收通道,无界缓冲。 - * IO 线程(串口回调)通过 trySend 写入,单一消费协程串行读取, - * 确保 parser 内部 serialReader 始终在同一协程中访问,规避并发竞争。 - */ - private val serialDataChannel = Channel(Channel.UNLIMITED) - - /** - * 协议解析器实例。 - * 持有 terminalId / terminalVersion / serialReader 等协议状态。 - */ - private val parser = SerialProtocolParser(object : SerialEventListener { - - /** 解析器需要发送命令时,通过 viewModelScope 异步发送 */ - override fun onSendCommand(cmd: String) { - viewModelScope.launch { SerialPortManager.send(cmd) } - } - - /** 终端信息就绪(激活或设备状态上报时),仅打印日志,信息已在 parser 内持有 */ - override fun onTerminalInfoReady(terminalId: String, terminalVersion: String) { - Log.d(TAG, "终端信息就绪: id=$terminalId, version=$terminalVersion") - } - - /** 货架重量更新,发射到 weightFlow */ - override fun onWeightUpdate(deviceNo: Int, weight: Int) { - _weightFlow.tryEmit(deviceNo to weight) - } - - /** 设备状态需要刷新,发射刷新信号 */ - override fun onDeviceStatusUpdate(text: String) { - _deviceStatusRefreshFlow.tryEmit(Unit) - } - - /** 温控信息上报,发射日志到 tempCtrlEventFlow */ - override fun onTempCtrlReport(logMsg: String) { - _tempCtrlEventFlow.tryEmit(logMsg) - } - }) - - // ---------------------------------------------------------------- - // 对外操作 - // ---------------------------------------------------------------- - - /** - * 打开串口并启动数据接收。 - * 应在 Activity.onCreate 中调用一次。 - * 串口数据通过 Channel 无锁传递给消费协程,由消费协程串行调用 parser。 - */ - fun openSerial() { - viewModelScope.launch { - val opened = SerialPortManager.open() - Log.d(TAG, "openSerial: opened=$opened") - if (!opened) return@launch - - // 串口回调在 IO 线程,通过 Channel 传递数据,不嵌套 launch - SerialPortManager.startReceive { data -> - serialDataChannel.trySend(data) - } - - // 单一消费协程,串行处理所有串口数据 - launch { - for (data in serialDataChannel) { - try { - parser.parseIncoming(data) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - } - } - - /** - * 向串口发送命令。 - * @param cmd 十六进制命令字符串 - */ - fun sendCmd(cmd: String) { - viewModelScope.launch { - SerialPortManager.send(cmd) - } - } - - // ---------------------------------------------------------------- - // 生命周期清理 - // ---------------------------------------------------------------- - - /** - * ViewModel 销毁时关闭 Channel 和串口。 - * Channel 关闭后,消费协程的 for 循环自然结束。 - * 串口关闭在独立 IO 协程中执行,避免阻塞主线程。 - */ - override fun onCleared() { - serialDataChannel.close() - CoroutineScope(Dispatchers.IO).launch { - SerialPortManager.close() - } - super.onCleared() - } -} diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialEventListener.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialEventListener.kt deleted file mode 100644 index 7733716..0000000 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialEventListener.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.shuwei.intelligent.shelves.serial - -/** - * 串口协议解析事件监听器。 - * [SerialProtocolParser] 解析完成后通过此接口将结果回调给上层(ViewModel 或 Activity)。 - */ -interface SerialEventListener { - - /** - * 需要向串口发送命令。 - * @param cmd 十六进制命令字符串 - */ - fun onSendCommand(cmd: String) - - /** - * 获取到终端信息(激活指令或设备状态上报时触发)。 - * @param terminalId 终端 ID(15字节) - * @param terminalVersion 固件版本(5字节) - */ - fun onTerminalInfoReady(terminalId: String, terminalVersion: String) - - /** - * 重量数据更新。 - * @param deviceNo 货架编号(从 1 开始) - * @param weight 重量(克) - */ - fun onWeightUpdate(deviceNo: Int, weight: Int) - - /** - * 设备状态文本更新(用于顶部状态栏左侧显示)。 - * @param text 显示内容 - */ - fun onDeviceStatusUpdate(text: String) - - /** - * 温控信息上报(需要弹窗提示用户)。 - * @param logMsg 温控日志信息 - */ - fun onTempCtrlReport(logMsg: String) -} diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolParser.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolParser.kt deleted file mode 100644 index 5f33668..0000000 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolParser.kt +++ /dev/null @@ -1,326 +0,0 @@ -package com.shuwei.intelligent.shelves.serial - -import android.util.Log -import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.ACTIVE_CMD -import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.FOOTER -import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.HEADER -import com.shuwei.intelligent.shelves.utils.binaryToHex -import com.shuwei.intelligent.shelves.utils.hexToBinary - -/** - * 串口协议解析器。 - * 职责:将原始串口字节流解析为结构化协议事件,通过 [SerialEventListener] 回调通知上层。 - * 无 Android UI 依赖,可进行单元测试。 - * - * @param listener 协议事件监听器,由 SerialViewModel 实现并传入 - */ -class SerialProtocolParser(private val listener: SerialEventListener) { - - companion object { - private const val TAG = "SerialProtocolParser" - private const val LOG_PREFIX = "receiveSerialPortData:收到" - } - - /** - * 终端 ID(15字节),由激活指令或设备状态上报时获取。 - * 对外只读,内部可写。 - */ - var terminalId: String = "" - private set - - /** - * 固件版本(5字节),由激活指令或设备状态上报时获取。 - * 对外只读,内部可写。 - */ - var terminalVersion: String = "" - private set - - /** - * 串口拼包缓冲器。 - * 仅在单一消费协程中调用 [parseIncoming],线程安全。 - */ - private val serialReader = StringBuilder() - - // ---------------------------------------------------------------- - // 对外入口:拼包 - // ---------------------------------------------------------------- - - /** - * 处理一条原始串口数据,执行拼包逻辑后交由 [handleReceivedData] 分发命令。 - * @param srcData 原始十六进制字符串(大写) - */ - fun parseIncoming(srcData: String) { - log("parseIncoming: $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 = data.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)) { - // 只有头,没有尾,等待后续分片 - serialReader.clear() - val startIndex = srcData.lastIndexOf(HEADER) - serialReader.append(srcData.substring(startIndex)).append(",") - return - } - - if (!srcData.contains(HEADER) && !srcData.contains(FOOTER)) { - // 中间分片 - serialReader.append(srcData).append(",") - return - } - - // 只有尾,没有头:拼接并处理 - val endIndex = srcData.indexOf(FOOTER) + FOOTER.length - serialReader.append(srcData.substring(0, endIndex)) - log("parseIncoming:serialReader: $serialReader") - data = serialReader.toString().replace(",", "") - handleReceivedData(data) - } - - // ---------------------------------------------------------------- - // 命令分发 - // ---------------------------------------------------------------- - - /** - * 根据命令码分发处理各类串口协议。 - */ - 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" -> handleActivation(data, cmd, cmdFlag) - "0101", "8101" -> handleHeartBeat(data, cmd, cmdFlag) - "0202" -> handleLockFeedback(data, cmd, cmdFlag) - "0302" -> handleDeviceStatus(data, cmd, cmdFlag) - "0501" -> handleCardUpload(data, cmd, cmdFlag) - "0B01" -> handleOperationRecord(data, cmd, cmdFlag) - "0C01" -> handleTimeSync(data, cmd, cmdFlag) - "0E01" -> handlePositionReport(data, cmd, cmdFlag) - "1001" -> handleTempCtrlReport(data, cmd, cmdFlag) - "1002" -> log("${LOG_PREFIX}[温控信息应答]1002数据:$data") - "0F02" -> log("${LOG_PREFIX}[温控信息应答]0F02数据:$data") - else -> {} - } - } - - // ---------------------------------------------------------------- - // 各命令处理 - // ---------------------------------------------------------------- - - /** - * 处理终端发起的激活指令(0001)。 - * 解析终端 ID 和固件版本,并发送激活命令。 - */ - private fun handleActivation(data: String, cmd: String, cmdFlag: String) { - 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") - listener.onSendCommand(ACTIVE_CMD) - listener.onTerminalInfoReady(terminalId, terminalVersion) - } - } - - /** - * 处理心跳指令(0101 / 8101)。 - * 发送心跳应答,并解析重量数据。 - */ - private fun handleHeartBeat(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER" - log("${LOG_PREFIX}[心跳]${cmdFlag}指令:$data,发送应答指令:$respCmd") - listener.onSendCommand(respCmd) - parseHeartBeat(data) - } - - /** - * 处理开关锁反馈(0202)。 - * 发送开锁应答。 - */ - private fun handleLockFeedback(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER" - log("${LOG_PREFIX}[开锁]${cmdFlag}指令:$data,发送应答指令:$respCmd") - listener.onSendCommand(respCmd) - } - - /** - * 处理获取设备状态(0302)。 - * 解析设备重量和终端信息。 - */ - private fun handleDeviceStatus(data: String, cmd: String, cmdFlag: String) { - log("${LOG_PREFIX}[获取设备状态]${cmdFlag}指令:$data") - parseDeviceInfo(data) - } - - /** - * 处理卡号上发(0501)。 - */ - private fun handleCardUpload(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}050203$FOOTER" - log("${LOG_PREFIX}[卡号上发]${cmdFlag}指令:$data,发送应答指令:$respCmd") - listener.onSendCommand(respCmd) - } - - /** - * 处理操作记录上传(0B01)。 - */ - private fun handleOperationRecord(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}0B0203$FOOTER" - log("${LOG_PREFIX}[操作记录上传]${cmdFlag}指令:$data,发送应答指令:$respCmd") - listener.onSendCommand(respCmd) - } - - /** - * 处理对时指令(0C01)。 - * 将当前系统时间转换为小端序十六进制并应答。 - */ - private fun handleTimeSync(data: String, cmd: String, cmdFlag: String) { - val timeLong = System.currentTimeMillis() / 1000 + (8 * 3600) - 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") - listener.onSendCommand(respCmd) - } - - /** - * 处理终端位置上报(0E01)。 - */ - private fun handlePositionReport(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}0E0200$FOOTER" - log("${LOG_PREFIX}[终端位置上报]${cmdFlag}指令:$data,发送应答指令:$respCmd") - listener.onSendCommand(respCmd) - } - - /** - * 处理温控信息上报(1001)。 - * 发送应答,并通过回调通知上层弹窗展示日志。 - */ - private fun handleTempCtrlReport(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}100200$FOOTER" - val logMsg = "${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:$data,发送应答指令:$respCmd" - log(logMsg) - listener.onSendCommand(respCmd) - listener.onDeviceStatusUpdate("") - listener.onTempCtrlReport(logMsg) - } - - // ---------------------------------------------------------------- - // 数据解析辅助 - // ---------------------------------------------------------------- - - /** - * 解析心跳报文中的重量和固定数据体。 - */ - private fun parseHeartBeat(data: String) { - runCatching { - if (data.length < 200) return - val weightStartIndex = 14 - val weightEndIndex = weightStartIndex + 12 * 8 - parseWeightInfo(start = weightStartIndex, end = weightEndIndex, data = data) - val temperatureEndIndex = 158 + 2 - parseFixDataBody(data, temperatureEndIndex) - }.onFailure { it.printStackTrace() } - } - - /** - * 解析设备状态报文(0302)中的重量和固定数据体。 - */ - private fun parseDeviceInfo(data: String) { - val weightStartIndex = HEADER.length + 14 - val temperatureStartIndex = weightStartIndex + 12 * 8 - val temperatureEndIndex = temperatureStartIndex + 2 - try { - data.substring(temperatureStartIndex, temperatureEndIndex) - } catch (e: Exception) { - e.printStackTrace() - return - } - listener.onDeviceStatusUpdate("") - parseWeightInfo(start = weightStartIndex, end = temperatureStartIndex, data = data) - parseFixDataBody(data, temperatureEndIndex) - } - - /** - * 解析重量数据区,逐格读取并通过 [listener] 回调。 - * @param start 重量数据起始索引 - * @param end 重量数据结束索引 - * @param data 完整协议报文(十六进制字符串) - */ - fun parseWeightInfo(start: Int, end: Int, data: String) { - var deviceNo = 1 - var count = start - while (count < end) { - val weightInfo = data.substring(count, count + 8) - // 将小端序 4 字节转换为大端序 - var weightHex = weightInfo.run { - substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2) - } - var realWeight: Int - var firstByteBinary = hexToBinary(weightHex.substring(0, 2)) - if (firstByteBinary.startsWith("0")) { - // 正数:按十六进制解析,失败时兜底为 0 - realWeight = weightHex.toIntOrNull(16) ?: 0 - } else { - // 负数:清除符号位后取绝对值,再取反 - firstByteBinary = "0${firstByteBinary.substring(1)}" - val firstByteHex = binaryToHex(firstByteBinary) - weightHex = firstByteHex + weightHex.substring(2) - realWeight = -1 * (weightHex.toIntOrNull(16) ?: 0) - } - listener.onWeightUpdate(deviceNo, realWeight) - count += 8 - deviceNo++ - } - } - - /** - * 从数据体尾部提取 terminalId / terminalVersion(首次获取时有效)。 - * @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 == 42) { - terminalId = fixDataBody.substring(0, 30) - terminalVersion = fixDataBody.substring(30, 40) - log("从设备状态获取数据,终端id:$terminalId,固件版本:$terminalVersion") - listener.onTerminalInfoReady(terminalId, terminalVersion) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun log(msg: String) { - Log.d(TAG, msg) - } -} diff --git a/app/src/main/res/layout/dialog_layout.xml b/app/src/main/res/layout/dialog_layout.xml deleted file mode 100644 index 1d184ee..0000000 --- a/app/src/main/res/layout/dialog_layout.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file