From 04fe3ec2c11525ecd21a04d4f79e371705f0086b Mon Sep 17 00:00:00 2001 From: mazengfei <331023091@qq.com> Date: Wed, 1 Apr 2026 11:26:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=85=A8=E9=9D=A2=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20HomeActivity=20=E6=9E=B6=E6=9E=84=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=A4=9A=E9=A1=B9=20Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SerialProtocolHandler 类,将串口协议解析逻辑从 HomeActivity 剥离 - ProtocolConstants 补充命令码常量(CMD_*)及帧解析偏移常量 - 修复 NetViewModel.getDeviceConfig 更新错误 StateFlow 的 Bug - 修复 getWeightInfo 中 toInt() 整数溢出崩溃风险,改用 toLongOrNull(16) - 升级废弃的 getSerializableExtra,使用 Build.VERSION.SDK_INT 版本分支 - BaseActivity 提取 onUserInactive() 虚方法,消除对子类 HomeActivity 的直接引用 - 修复 BaseActivity 中错误的 LIGHT_OPEN/CLOSE 导入(改从 ProtocolConstants 导入) - repeatOnLifecycle 由 CREATED 改为 STARTED,避免 UI 不可见时无效更新 - updateOverdueState 从回调风格改为 StateFlow,与项目其他接口保持一致 - 提取 syncShelfGoodsToServer() 消除重复的 ShelfBody 构建逻辑 - 清理 NetViewModel 和 BaseActivity 中大段注释死代码 Co-Authored-By: Claude Sonnet 4.6 --- .../shelves/activity/HomeActivity.kt | 382 +++++------------- .../intelligent/shelves/base/BaseActivity.kt | 71 ++-- .../intelligent/shelves/net/NetViewModel.kt | 55 +-- .../shelves/serial/ProtocolConstants.kt | 79 +++- .../shelves/serial/SerialProtocolHandler.kt | 244 +++++++++++ 5 files changed, 462 insertions(+), 369 deletions(-) create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolHandler.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 99d1937..baf908a 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 @@ -3,10 +3,10 @@ package com.shuwei.intelligent.shelves.activity import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.SparseIntArray import androidx.activity.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -28,23 +28,19 @@ 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.serial.ProtocolConstants.ACTIVE_CMD import com.shuwei.intelligent.shelves.serial.ProtocolConstants.C_TEMP_CMD -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER -import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LEFT_SHELF_OPEN_CMD import com.shuwei.intelligent.shelves.serial.ProtocolConstants.RIGHT_SHELF_OPEN_CMD import com.shuwei.intelligent.shelves.serial.ProtocolConstants.START_TEMP_CTRL_CMD import com.shuwei.intelligent.shelves.serial.SerialFrameParser import com.shuwei.intelligent.shelves.serial.SerialPortManager +import com.shuwei.intelligent.shelves.serial.SerialProtocolHandler 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.launch import org.greenrobot.eventbus.EventBus @@ -53,22 +49,16 @@ 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" } // 货架列表数据 private val list: MutableList = mutableListOf() - // 各传感器重量数据,key 为货架编号 - private val weightArray = SparseIntArray() - // 当前选中货架下标 private var shelfIndex = 0 @@ -81,10 +71,47 @@ class HomeActivity : BaseActivity() { // 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false private var isNeedOpenLock = true + private var deviceName = "" + private lateinit var binding: ActivityHomeBinding private val viewModel: NetViewModel by viewModels() + // 串口协议解析器,通过回调通知本 Activity + private val serialProtocolHandler = SerialProtocolHandler(object : SerialProtocolHandler.Callback { + override fun onSendCmd(cmd: String) = sendCmd(cmd) + + override fun onWeightUpdated(shelfNo: Int, weight: Int) { + if (shelfNo in 1..10 && list.size == 10) { + list.firstOrNull { it.deviceNo == shelfNo }?.let { model -> + model.weight = weight.toDouble() + val pos = list.indexOf(model) + shelfAdapter.notifyItemChanged(pos) + log("getWeightInfo: deviceNo=${model.deviceNo},realWeight=$weight") + EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight)) + } + } + } + + override fun onTempReport(logMsg: String) { + updateLeftStatus(deviceName) + if (tipDialog == null) { + tipDialog = CommonDialog(this@HomeActivity).apply { + dialogTitle = "温馨提示" + confirmBlock = { logMsg.copyTextToClipboard(this@HomeActivity) } + } + } + if (tipDialog?.isShowing == false) { + tipDialog?.dialogContent = logMsg + tipDialog?.show() + } + } + + override fun onLog(message: String) = log(message) + }) + + private val serialFrameParser = SerialFrameParser() + @SuppressLint("NotifyDataSetChanged") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -94,8 +121,8 @@ class HomeActivity : BaseActivity() { setContentView(binding.root) updateLeftStatus("") - initRecyclerView() + // 串口初始化,独立协程,不阻塞网络状态监听 lifecycleScope.launch { val openState = SerialPortManager.open() @@ -110,9 +137,10 @@ class HomeActivity : BaseActivity() { } } } + // 网络状态监听,独立协程,与串口初始化并行执行 lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { + repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.getAccessTokenUiState.collect { state -> when (state) { @@ -133,8 +161,18 @@ class HomeActivity : BaseActivity() { } } } + launch { + viewModel.updateOverdueStateUiState.collect { state -> + if (state is UiState.Success<*>) { + val respData = state.data as? com.shuwei.intelligent.shelves.net.RespData<*> ?: return@collect + val items = (respData.data as? ShelfResult)?.containerGoodsList ?: return@collect + applyOverdueState(items) + } + } + } } } + binding.root.postDelayed({ sendCmd(START_TEMP_CTRL_CMD) sendCmd(C_TEMP_CMD) @@ -167,8 +205,7 @@ class HomeActivity : BaseActivity() { return } deviceName = data.data.deviceName - - updateLeftStatus("$deviceName") + updateLeftStatus(deviceName) App.canteenId = data.data.placeId val tempList = data.data.containerGoodsList @@ -192,135 +229,13 @@ class HomeActivity : BaseActivity() { loadEmptyView() } - /** - * 终端id - */ - private var terminalId: String = "" - - /** - * 固件版本 - */ - private var terminalVersion: String = "" - - private val serialFrameParser = SerialFrameParser() - private fun receiveSerialPortData(srcData: String) { log("receiveSerialPortData: $srcData") // 重置无数据计时,并关闭已显示的超时提示弹窗 lastDataReceivedTime = System.currentTimeMillis() noDataWarningDialog?.takeIf { it.isShowing }?.dismiss() val frame = serialFrameParser.parse(srcData) ?: return - handleReceivedData(frame) - } - - 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) - } - - "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 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" - 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) - updateLeftStatus("$deviceName") - - if (tipDialog == null) { - tipDialog = CommonDialog(this).apply { - dialogTitle = "温馨提示" - 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}") - } - - else -> {} - } + serialProtocolHandler.handle(frame) } private var tipDialog: CommonDialog? = null @@ -353,115 +268,11 @@ class HomeActivity : BaseActivity() { } } - 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 - - 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")) { - //正数 - realWeight = weightHex.toInt() - } else { - //负数 - firstByteBinary = "0${firstByteBinary.substring(1)}" - val firstByteHex = binaryToHex(firstByteBinary) - weightHex = firstByteHex + weightHex.substring(2) - realWeight = -1 * weightHex.toInt() - } - weightArray.put(index, realWeight) - - if (index in 1..10 && list.size == 10) { - list.firstOrNull { it.deviceNo == index }?.let { - it.weight = realWeight.toDouble() - val pos = list.indexOf(it) - shelfAdapter.notifyItemChanged(pos) - log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}") - EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight)) - } - } - - count += 8 - index++ - } - } - - 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 - } - updateLeftStatus("$deviceName") - - getWeightInfo( - start = weightStartIndex, - end = temperatureStartIndex, - data = data - ) - - getFixDataBody(data, temperatureEndIndex) - } - /** - * 从心跳帧或设备状态帧中补充解析 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) - log("receiveSerialPortData:从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}") - } - } catch (e: Exception) { - e.printStackTrace() - } + override fun onUserInactive() { + switchLight(false) } override fun onDestroy() { @@ -504,9 +315,13 @@ class HomeActivity : BaseActivity() { /** * 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端 */ - @Suppress("DEPRECATION") private fun onShelfActivityResult(position: Int, intent: Intent) { - val model = intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel + @Suppress("DEPRECATION") + val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(ShelfActivity.SHELF_MODEL, ShelfModel::class.java) + } else { + intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel + } ?: return list[position].apply { goodsId = model.goodsId goodsName = model.goodsName @@ -516,29 +331,13 @@ class HomeActivity : BaseActivity() { } } 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() - } + syncShelfGoodsToServer() } private fun initRecyclerView() { binding.rvShelf.run { - layoutManager = - GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false) + layoutManager = GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false) adapter = shelfAdapter - itemAnimator.let { if (it is DefaultItemAnimator) { it.supportsChangeAnimations = false @@ -550,8 +349,7 @@ class HomeActivity : BaseActivity() { private val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) } private fun updateDateTime() { - val dateTime = dateTimeFormatter.format(Date()) - updateRightStatus(dateTime) + updateRightStatus(dateTimeFormatter.format(Date())) } private val handler = Handler(Looper.getMainLooper()) @@ -581,7 +379,6 @@ class HomeActivity : BaseActivity() { private fun loadEmptyView() { list.clear() shelfAdapter.notifyDataSetChanged() - binding.include?.let { it.root.visible() it.root.setOnClickListener { @@ -593,7 +390,7 @@ class HomeActivity : BaseActivity() { } @Subscribe(threadMode = ThreadMode.MAIN) - public fun clearEmptyShelf(event: ClearShelfEvent) { + fun clearEmptyShelf(event: ClearShelfEvent) { val item = list.firstOrNull { it.deviceNo == event.shelfNo } item?.apply { goodsId = "" @@ -607,7 +404,7 @@ class HomeActivity : BaseActivity() { } private val taskExecutor by lazy { IntervalExecutor() } - private var saveTaskJob: Job?=null + private var saveTaskJob: Job? = null /** * 保存定时任务 @@ -618,34 +415,40 @@ class HomeActivity : BaseActivity() { } } - private var overdueTaskJob: Job?=null + private var overdueTaskJob: Job? = null /** * 过期时间定时任务 */ private fun overdueTask() { - overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60* 1000L) { + overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) { updateOverdueState() } } 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) { - model.overdue = items[index].overdue - shelfAdapter.notifyItemChanged(index) - log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}") - } + viewModel.updateOverdueState() + } + + /** + * 将接口返回的过期状态应用到本地列表,仅在状态变化时刷新对应 item + */ + private fun applyOverdueState(items: List) { + if (items.size != list.size) return + list.forEachIndexed { index, model -> + // 接口过期状态变化后更新状态 + if (items[index].overdue != model.overdue) { + model.overdue = items[index].overdue + shelfAdapter.notifyItemChanged(index) + log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}") } } } - private fun saveShelfGoodsList() { - log("performSync: list:${list.toJsonString()}") + /** + * 构建 ShelfBody 并提交到服务端(被定时任务与 ShelfActivity 返回共用) + */ + private fun syncShelfGoodsToServer() { val submitList = list.sortedBy { it.deviceNo } submitList.forEach { if (it.goodsId.isNullOrBlank()) { @@ -653,7 +456,7 @@ class HomeActivity : BaseActivity() { } } val body = ShelfBody().also { - it.deviceId = App.deviceId + it.deviceId = App.deviceId ?: "" it.canteenId = App.canteenId it.temperature = showTemperatureC it.humidity = showHumidity @@ -663,5 +466,8 @@ class HomeActivity : BaseActivity() { viewModel.saveShelfGoodsList(body) } - -} \ No newline at end of file + private fun saveShelfGoodsList() { + log("performSync: list:${list.toJsonString()}") + syncShelfGoodsToServer() + } +} diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt index 2e0bad1..cf8f133 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt @@ -5,20 +5,24 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log +import android.view.LayoutInflater import android.view.View +import android.widget.PopupWindow import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope -import com.shuwei.intelligent.shelves.activity.HomeActivity -import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_CLOSE -import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_OPEN +import com.shuwei.intelligent.shelves.activity.LogActivity import com.shuwei.intelligent.shelves.R import com.shuwei.intelligent.shelves.databinding.ActivityBaseBinding +import com.shuwei.intelligent.shelves.databinding.PopupRightStatusMenuBinding +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_CLOSE +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_OPEN import com.shuwei.intelligent.shelves.serial.SerialPortManager import com.shuwei.intelligent.shelves.utils.FileLogger import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce +import com.shuwei.intelligent.shelves.utils.ext.startActivity import kotlinx.coroutines.launch import androidx.core.view.WindowCompat @@ -57,38 +61,19 @@ open class BaseActivity : AppCompatActivity() { } } -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { -// window.setDecorFitsSystemWindows(false) -// window.insetsController?.apply { -// hide(WindowInsets.Type.statusBars()) // 隐藏状态栏 -// systemBarsBehavior = -// WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // 滑动时临时显示 -// } -// } - lightSwitchClick() binding.tvLeftStatus.setOnDoubleClickListener { finish() } binding.tvRightStatus.setOnDoubleClickListener { - EnvSwitchDialog(this).show() + showRightStatusPopup(binding.tvRightStatus) } } - fun hideStatusBar() { -//// enableEdgeToEdge() -//// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏 -//// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选) -//// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN -// -// ImmersionBar.with(this) -// .hideBar(BarHide.FLAG_HIDE_STATUS_BAR) -// .init() - } + fun hideStatusBar() {} fun statusBarDarkFont() { - //window.decorView.systemUiVisibility = if (enable) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0 val color = ContextCompat.getColor(this, R.color.white) binding.tvLeftStatus.setTextColor(color) binding.tvRightStatus.setTextColor(color) @@ -118,11 +103,15 @@ open class BaseActivity : AppCompatActivity() { private val TIME_OUT: Long = (5 * 60 * 1000).toLong() private val mHandler by lazy { Handler(Looper.getMainLooper()) } private val mTimeoutRunnable = Runnable { - if (this is HomeActivity) { - this.switchLight(false) - } + // 用户长时间无操作时回调,子类可覆写实现具体逻辑(如关灯) + onUserInactive() } + /** + * 用户超时无操作时回调,默认空实现,子类按需覆写 + */ + open fun onUserInactive() {} + override fun onUserInteraction() { super.onUserInteraction() //开灯 @@ -164,15 +153,27 @@ open class BaseActivity : AppCompatActivity() { val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true switchLight(tag.not()) } - binding.tvRightStatus.clickWithDebounce { -// if (this is HomeActivity) { -// startActivity { -// putExtra(LogActivity.IS_LOG_DIR, true) -// } -// return@clickWithDebounce -// } -// startActivity { } + } + + private fun showRightStatusPopup(anchor: View) { + val menuBinding = PopupRightStatusMenuBinding.inflate(LayoutInflater.from(this)) + val popup = PopupWindow( + menuBinding.root, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + true + ) + menuBinding.tvEnvSwitch.setOnClickListener { + popup.dismiss() + EnvSwitchDialog(this).show() } + menuBinding.tvLog.setOnClickListener { + popup.dismiss() + startActivity { + putExtra(LogActivity.IS_LOG_DIR, true) + } + } + popup.showAsDropDown(anchor) } diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModel.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModel.kt index 5a3c332..62159df 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModel.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/NetViewModel.kt @@ -4,33 +4,17 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.shuwei.intelligent.shelves.App -import com.shuwei.intelligent.shelves.GlobalData import com.shuwei.intelligent.shelves.model.ShelfBody -import com.shuwei.intelligent.shelves.model.ShelfModel -import com.shuwei.intelligent.shelves.net.HttpManager.Companion.instance import com.shuwei.intelligent.shelves.utils.ext.toJsonString import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory class NetViewModel : ViewModel() { -// val apiService: ApiService = HttpManager.instance.apiService -// val apiService: ApiService = Retrofit.Builder() -// .baseUrl(UrlConfig.BASE_URL) -// .client(HttpManager.instance.client) -// .addConverterFactory(GsonConverterFactory.create()) -// .build() -// .create(ApiService::class.java) - private val _getShelfListUiState = MutableStateFlow(UiState.Initial) val getShelfListUiState: StateFlow = _getShelfListUiState -// private val _saveShelfGoodsListUiState = MutableStateFlow(UiState.Initial) -// val saveShelfGoodsListUiState: StateFlow = _saveShelfGoodsListUiState - private val _getGoodsListUiState = MutableStateFlow(UiState.Initial) val getGoodsListUiState: StateFlow = _getGoodsListUiState @@ -43,6 +27,8 @@ class NetViewModel : ViewModel() { private val _getDeviceConfigUiState = MutableStateFlow(UiState.Initial) val getDeviceConfigUiState: StateFlow = _getDeviceConfigUiState + private val _updateOverdueStateUiState = MutableStateFlow(UiState.Initial) + val updateOverdueStateUiState: StateFlow = _updateOverdueStateUiState fun getDeviceToken(deviceId: String) { viewModelScope.launch { @@ -62,17 +48,16 @@ class NetViewModel : ViewModel() { fun getDeviceConfig(deviceId: String, deviceToken: String) { viewModelScope.launch { - _getDeviceTokenUiState.value = UiState.Loading + _getDeviceConfigUiState.value = UiState.Loading runCatching { - val response = - apiService.getDeviceInfo(equipmentCode = deviceId) + val response = apiService.getDeviceInfo(equipmentCode = deviceId) if (response.isSuccess()) { - _getDeviceTokenUiState.value = UiState.Success(response) + _getDeviceConfigUiState.value = UiState.Success(response) } else { - _getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败") + _getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败") } }.onFailure { - _getDeviceTokenUiState.value = UiState.Error(it.message ?: "请求异常") + _getDeviceConfigUiState.value = UiState.Error(it.message ?: "请求异常") } } } @@ -109,37 +94,21 @@ class NetViewModel : ViewModel() { } } - fun updateOverdueState(block:(Boolean, List?)-> Unit) { + fun updateOverdueState() { viewModelScope.launch { runCatching { val response = apiService.getShelfList(deviceId = App.deviceId) if (response.isSuccess()) { - block(true, response.data?.containerGoodsList) + _updateOverdueStateUiState.value = UiState.Success(response) } else { - block(false, emptyList()) + _updateOverdueStateUiState.value = UiState.Error(response.msg ?: "请求失败") } }.onFailure { - block(false, null) + _updateOverdueStateUiState.value = UiState.Error(it.message ?: "请求异常") } } } -// fun saveShelfGoodsList(body: ShelfBody) { -// viewModelScope.launch { -// _saveShelfGoodsListUiState.value = UiState.Loading -// runCatching { -// val response = apiService.saveShelfGoodsList(body) -// if (response.success) { -// _saveShelfGoodsListUiState.value = UiState.Success(response) -// } else { -// _saveShelfGoodsListUiState.value = UiState.Error(response.message ?: "请求失败") -// } -// }.onFailure { -// _saveShelfGoodsListUiState.value = UiState.Error(it.message ?: "请求异常") -// } -// } -// } - fun getGoodsList( canteenId: String = "", goodsName: String = "", @@ -176,7 +145,5 @@ class NetViewModel : ViewModel() { Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}") } } - } - } 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 index d8fc60a..bf3f2a2 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/ProtocolConstants.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ProtocolConstants.kt @@ -2,7 +2,7 @@ package com.shuwei.intelligent.shelves.serial /** * 串口通信协议常量定义 - * 统一管理所有协议头、协议尾及各类指令 + * 统一管理所有协议头、协议尾、各类指令及帧解析偏移量 */ object ProtocolConstants { @@ -12,6 +12,81 @@ object ProtocolConstants { /** 协议尾 */ const val FOOTER = "FFFF" + // ---------------------------------------------------------------- + // 指令码(cmd 字段,4字符) + // ---------------------------------------------------------------- + + /** 终端激活指令 */ + const val CMD_ACTIVATE = "0001" + + /** 心跳指令(服务端发起) */ + const val CMD_HEARTBEAT = "0101" + + /** 心跳指令(终端发起) */ + const val CMD_HEARTBEAT_TERMINAL = "8101" + + /** 开关锁反馈 */ + const val CMD_LOCK_FEEDBACK = "0202" + + /** 获取设备状态 */ + const val CMD_DEVICE_STATUS = "0302" + + /** 卡号上发 */ + const val CMD_CARD_UPLOAD = "0501" + + /** 操作记录上传 */ + const val CMD_OPERATION_RECORD = "0B01" + + /** 对时 */ + const val CMD_TIME_SYNC = "0C01" + + /** 终端位置上报 */ + const val CMD_POSITION_REPORT = "0E01" + + /** 温控信息上报 */ + const val CMD_TEMP_REPORT = "1001" + + /** 温控信息应答 */ + const val CMD_TEMP_RESPONSE = "1002" + + /** 温控应答(备用) */ + const val CMD_TEMP_RESPONSE_ALT = "0F02" + + // ---------------------------------------------------------------- + // 帧数据偏移 / 尺寸常量 + // ---------------------------------------------------------------- + + /** 心跳帧中重量数据起始偏移(相对帧起始) */ + const val HEARTBEAT_WEIGHT_OFFSET = 14 + + /** 设备状态帧中重量数据起始偏移(含协议头长度) */ + const val DEVICE_STATUS_WEIGHT_OFFSET = HEADER.length + 14 + + /** 货架数量(每侧5格,共10格,协议中12组) */ + const val SHELF_SENSOR_COUNT = 12 + + /** 每组重量数据字节数(十六进制字符数) */ + const val BYTES_PER_WEIGHT = 8 + + /** 温度字段字符长度 */ + const val TEMPERATURE_FIELD_LEN = 2 + + /** 心跳帧最小有效长度 */ + const val HEARTBEAT_MIN_LENGTH = 200 + + /** 心跳帧中温度字段起始偏移(经协议分析得出,前面含重量+其他传感器数据) */ + const val HEARTBEAT_TEMPERATURE_START = 158 + + /** 激活请求体有效长度 */ + const val ACTIVATE_BODY_LENGTH = 44 + + /** 固定数据体有效长度(terminalId 30 + terminalVersion 10 + 其他 2) */ + const val FIX_BODY_LENGTH = 42 + + // ---------------------------------------------------------------- + // 完整指令 + // ---------------------------------------------------------------- + /** 激活指令(1楼餐厅) */ const val ACTIVE_CMD = "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}" @@ -36,7 +111,7 @@ object ProtocolConstants { /** 摄氏温度单位指令 */ const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}" - /** 开灯指令(继电器1关) */ + /** 关灯指令(继电器1关) */ const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}" /** 开灯指令(继电器1开) */ 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 new file mode 100644 index 0000000..53f5d47 --- /dev/null +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialProtocolHandler.kt @@ -0,0 +1,244 @@ +package com.shuwei.intelligent.shelves.serial + +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVATE_BODY_LENGTH +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVE_CMD +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_MIN_LENGTH +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_TEMPERATURE_START +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_WEIGHT_OFFSET +import com.shuwei.intelligent.shelves.serial.ProtocolConstants.SHELF_SENSOR_COUNT +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) + callback.onLog("收到[激活]${cmdFlag}指令:${data},终端id:${terminalId},固件版本:${terminalVersion},发送激活命令:$ACTIVE_CMD") + callback.onSendCmd(ACTIVE_CMD) + } + } + + /** 处理心跳指令(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 + SHELF_SENSOR_COUNT * 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 < HEARTBEAT_MIN_LENGTH) return + val weightStartIndex = HEARTBEAT_WEIGHT_OFFSET + val weightEndIndex = weightStartIndex + SHELF_SENSOR_COUNT * BYTES_PER_WEIGHT + parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data) + val temperatureEndIndex = HEARTBEAT_TEMPERATURE_START + 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) + } + 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() + } + } +}