diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleFrameParser.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleFrameParser.kt index 1849edc..387859a 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleFrameParser.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleFrameParser.kt @@ -9,6 +9,7 @@ import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.FOOTER_LEN import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.HEADER_HEX import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.SIM_CARD_NO_LEN import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.TERMINAL_ID_LEN +import com.shuwei.intelligent.shelves.utils.hexToBytes /** * 协议帧组装与解析器 @@ -108,7 +109,7 @@ class ScaleFrameParser { if (!hexFrame.startsWith(HEADER_HEX) || !hexFrame.endsWith(FOOTER_HEX)) return null return runCatching { - val bytes = hexFrame.hexToByteArray() + val bytes = hexFrame.hexToBytes() val totalLen = bytes.size val cmd = bytes[ScaleProtocolConstants.HEADER_LEN] @@ -277,18 +278,4 @@ class ScaleFrameParser { return if (isNegative) -absValue.toInt() else absValue.toInt() } - // ---------------------------------------------------------------- - // 私有工具 - // ---------------------------------------------------------------- - - /** - * 十六进制字符串转字节数组 - * 例:"D6DADEDB" → [0xD6, 0xDA, 0xDE, 0xDB] - */ - private fun String.hexToByteArray(): ByteArray { - require(length % 2 == 0) { "十六进制字符串长度必须为偶数" } - return ByteArray(length / 2) { i -> - substring(i * 2, i * 2 + 2).toInt(16).toByte() - } - } } 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 deleted file mode 100644 index b6c915e..0000000 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialFrameParser.kt +++ /dev/null @@ -1,80 +0,0 @@ -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/SerialProtocolHandler.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolHandler.kt deleted file mode 100644 index 50b7d67..0000000 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolHandler.kt +++ /dev/null @@ -1,247 +0,0 @@ -package com.shuwei.intelligent.shelves.serial - -import com.shuwei.intelligent.shelves.App -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVATE_BODY_LENGTH -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVE_MAP -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.BYTES_PER_WEIGHT -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_ACTIVATE -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_CARD_UPLOAD -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_DEVICE_STATUS -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_HEARTBEAT -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_HEARTBEAT_TERMINAL -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_LOCK_FEEDBACK -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_OPERATION_RECORD -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_POSITION_REPORT -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_REPORT -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_RESPONSE -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_RESPONSE_ALT -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TIME_SYNC -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.DEVICE_STATUS_WEIGHT_OFFSET -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FIX_BODY_LENGTH -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_WEIGHT_OFFSET -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.TEMPERATURE_FIELD_LEN -import com.shuwei.intelligent.shelves.utils.binaryToHex -import com.shuwei.intelligent.shelves.utils.hexToBinary -import java.util.TimeZone - -/** - * 串口协议解析处理器 - * - * 负责将原始协议帧字符串解析为业务事件,通过 [Callback] 接口通知调用方。 - * 设计为无状态(除 terminalId/terminalVersion 外),便于单元测试。 - * - * @param callback 业务回调,由 Activity/ViewModel 实现 - */ -class SerialProtocolHandler(private val callback: Callback) { - - /** 终端 ID(15字节/30字符),由激活指令或心跳兜底填充 */ - var terminalId: String = "" - private set - - /** 固件版本(5字节/10字符),由激活指令或心跳兜底填充 */ - var terminalVersion: String = "" - private set - - /** - * 协议解析结果回调接口 - */ - interface Callback { - /** 需要向串口发送一条指令 */ - fun onSendCmd(cmd: String) - - /** 解析到一组重量数据:货架编号 → 重量(克) */ - fun onWeightUpdated(shelfNo: Int, weight: Int) - - /** 温控信息上报,附带完整帧日志用于显示/复制 */ - fun onTempReport(logMsg: String) - - /** 记录日志 */ - fun onLog(message: String) - } - - /** - * 处理一帧完整的协议数据 - * - * @param data 经 SerialFrameParser 解析后的完整帧字符串 - */ - fun handle(data: String) { - if (data.isEmpty()) return - - val cmd = data.substring(HEADER.length, HEADER.length + 4) - val cmdFlag = cmd.substring(0, 2) - - when (cmd) { - CMD_ACTIVATE -> handleActivate(data, cmd, cmdFlag) - CMD_HEARTBEAT, CMD_HEARTBEAT_TERMINAL -> handleHeartbeat(data, cmd, cmdFlag) - CMD_LOCK_FEEDBACK -> handleLockFeedback(data, cmd, cmdFlag) - CMD_DEVICE_STATUS -> handleDeviceStatus(data, cmdFlag) - CMD_CARD_UPLOAD -> handleCardUpload(data, cmd, cmdFlag) - CMD_OPERATION_RECORD -> handleOperationRecord(data, cmd, cmdFlag) - CMD_TIME_SYNC -> handleTimeSync(data, cmd, cmdFlag) - CMD_POSITION_REPORT -> handlePositionReport(data, cmd, cmdFlag) - CMD_TEMP_REPORT -> handleTempReport(data, cmd, cmdFlag) - CMD_TEMP_RESPONSE -> callback.onLog("收到[温控信息应答]${CMD_TEMP_RESPONSE}数据:$data") - CMD_TEMP_RESPONSE_ALT -> callback.onLog("收到[温控信息应答]${CMD_TEMP_RESPONSE_ALT}数据:$data") - else -> {} - } - } - - /** 处理终端激活指令 */ - private fun handleActivate(data: String, cmd: String, cmdFlag: String) { - val reqBody = data.substring(HEADER.length + cmd.length, data.length - FOOTER.length) - if (reqBody.length == ACTIVATE_BODY_LENGTH) { - terminalId = reqBody.substring(2, 32) - terminalVersion = reqBody.substring(32, 42) - val activeCode = ACTIVE_MAP[App.deviceId]?:"" - callback.onLog("收到[激活]${cmdFlag}指令:${data},终端id:${terminalId},固件版本:${terminalVersion},发送激活命令:$activeCode") - callback.onSendCmd(activeCode) - } - } - - /** 处理心跳指令(0101 / 8101) */ - private fun handleHeartbeat(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER" - callback.onLog("收到[心跳]${cmdFlag}指令:${data},发送应答指令:${respCmd}") - callback.onSendCmd(respCmd) - parseHeartbeatData(data) - } - - /** 处理开关锁反馈 */ - private fun handleLockFeedback(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER" - callback.onLog("收到[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}") - callback.onSendCmd(respCmd) - } - - /** 处理获取设备状态(0302) */ - private fun handleDeviceStatus(data: String, cmdFlag: String) { - callback.onLog("收到[获取设备状态]${cmdFlag}指令:${data}") - val weightStartIndex = DEVICE_STATUS_WEIGHT_OFFSET - val weightEndIndex = weightStartIndex + ProtocolConstants.shelfSensorCount() * BYTES_PER_WEIGHT - val temperatureEndIndex = weightEndIndex + TEMPERATURE_FIELD_LEN - try { - parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data) - parseFixDataBody(data, temperatureEndIndex) - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** 处理卡号上发 */ - private fun handleCardUpload(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}050203$FOOTER" - callback.onLog("收到[卡号上发]${cmdFlag}指令:${data},发送应答指令:${respCmd}") - callback.onSendCmd(respCmd) - } - - /** 处理操作记录上传 */ - private fun handleOperationRecord(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}0B0203$FOOTER" - callback.onLog("收到[操作记录上传]${cmdFlag}指令:${data},发送应答指令:${respCmd}") - callback.onSendCmd(respCmd) - } - - /** 处理对时请求 */ - private fun handleTimeSync(data: String, cmd: String, cmdFlag: String) { - val nowMs = System.currentTimeMillis() - val timeLong = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000 - val timeStr = "0000000" + String.format("%X", timeLong) - val time = timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("") - val respCmd = "${HEADER}0B02${time}$FOOTER" - callback.onLog("收到[对时]${cmdFlag}指令:${data},发送应答指令:${respCmd}") - callback.onSendCmd(respCmd) - } - - /** 处理终端位置上报 */ - private fun handlePositionReport(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}0E0200$FOOTER" - callback.onLog("收到[终端位置上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}") - callback.onSendCmd(respCmd) - } - - /** 处理温控信息上报 */ - private fun handleTempReport(data: String, cmd: String, cmdFlag: String) { - val respCmd = "${HEADER}100200$FOOTER" - val logMsg = "收到[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}" - callback.onLog(logMsg) - callback.onSendCmd(respCmd) - callback.onTempReport(logMsg) - } - - /** - * 解析心跳帧中的重量与固定数据体 - */ - private fun parseHeartbeatData(data: String) { - runCatching { - if (data.length < ProtocolConstants.heartbeatMinLength()) return - val weightStartIndex = HEARTBEAT_WEIGHT_OFFSET - val weightEndIndex = weightStartIndex + ProtocolConstants.shelfSensorCount() * BYTES_PER_WEIGHT - parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data) - val temperatureEndIndex = ProtocolConstants.heartbeatTemperatureStart() + TEMPERATURE_FIELD_LEN - parseFixDataBody(data, temperatureEndIndex) - }.onFailure { it.printStackTrace() } - } - - /** - * 解析连续的重量数据段,每 [BYTES_PER_WEIGHT] 字符一组,依次对应货架 1..N - * - * @param start 数据段起始索引 - * @param end 数据段结束索引(不含) - * @param data 完整帧字符串 - */ - fun parseWeightData(start: Int, end: Int, data: String) { - var index = 1 - var count = start - while (count < end) { - val weightInfo = data.substring(count, count + BYTES_PER_WEIGHT) - // 小端字节序转换:将4字节按字节倒序拼接 - var weightHex = weightInfo.run { - substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2) - } - var firstByteBinary = hexToBinary(weightHex.substring(0, 2)) - val realWeight = if (firstByteBinary.startsWith("0")) { - // 正数:使用 toLongOrNull(16) 转换,避免超出 Int 范围 - weightHex.toLongOrNull(16)?.toInt() ?: 0 - } else { - // 负数:清除符号位后计算绝对值,再取反 - firstByteBinary = "0${firstByteBinary.substring(1)}" - val firstByteHex = binaryToHex(firstByteBinary) - weightHex = firstByteHex + weightHex.substring(2) - //-1 * (weightHex.toLongOrNull(16)?.toInt() ?: 0) - // 直接用 Long 解析,然后减去 2^32 得到有符号值 - val unsigned = weightHex.toLongOrNull(16) ?: 0L - val signed = if (unsigned >= 0x80000000L) unsigned - 0x100000000L else unsigned - signed.toInt() - } - callback.onWeightUpdated(index, realWeight) - count += BYTES_PER_WEIGHT - index++ - } - } - - /** - * 从心跳帧或设备状态帧中补充解析 terminalId / terminalVersion。 - * 激活指令(CMD_ACTIVATE)是获取这两个字段的首选来源; - * 若激活指令尚未到来,则通过此方法从心跳/设备状态帧中兜底解析。 - * - * @param data 完整协议帧字符串 - * @param temperatureEndIndex 温度字段结束位置(固定数据体紧随其后) - */ - private fun parseFixDataBody(data: String, temperatureEndIndex: Int) { - try { - // 已通过激活指令成功解析过,无需重复解析 - if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) return - val fixDataBodyStartIndex = temperatureEndIndex + 1 - val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length) - if (fixDataBody.length == FIX_BODY_LENGTH) { - terminalId = fixDataBody.substring(0, 30) - terminalVersion = fixDataBody.substring(30, 40) - callback.onLog("从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}") - } - } catch (e: Exception) { - e.printStackTrace() - } - } -}