refactor(serial): 清理废弃类并复用现有工具函数
- 删除 SerialProtocolHandler(旧版协议解析器,已无任何引用) - 删除 SerialFrameParser(仅被 SerialProtocolHandler 使用,随其废弃) - ScaleFrameParser.parseFrame 改用项目已有的 String.hexToBytes(),移除重复的私有 hexToByteArray() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user