12 Commits
Author SHA1 Message Date
lvmeng 8145eb588b refactor(serial): 将串口配置常量重命名为大写格式
- 将 portName 常量重命名为 PORT_NAME
- 将 baudRate 常量重命名为 BAUD_RATE
- 更新代码中对这些常量的引用以使用新的命名约定
- 保持串口通信功能完全不变
2026-05-21 09:01:27 +08:00
mazengfei c91d5656aa 修改旧版设备秤数量导致数据显示不准的问题 2026-05-20 16:50:53 +08:00
mazengfei 532b22a5c6 设备支持3列显示,统一处理显示和提交数据顺序 2026-05-20 15:36:28 +08:00
mazengfei e9d04bfb70 优化启动页面样式 2026-04-28 16:52:54 +08:00
mazengfei afba5220b3 首页设置禁止返回按钮 2026-04-28 16:41:21 +08:00
lvmeng 4ace12c6c7 style(ui): 调整加载动画尺寸并替换为矢量资源
- 将加载动画ImageView尺寸从160dp调整为120dp
- 替换loading_spinner为新的loading矢量资源
- 添加loading.xml矢量动画资源文件
- 注释掉HomeActivity中的临时报告对话框逻辑
- 简化HomeActivity中的RespData类型转换代码
2026-04-28 15:37:47 +08:00
mazengfei ee36869003 优化 2026-04-07 17:19:12 +08:00
lvmengandClaude Sonnet 4.6 c23c215941 fix: 修复 noDataWatchdogTask 潜在的内存泄漏问题
在 Runnable 中添加生命周期检查,防止 Activity 销毁后任务继续执行。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:58:22 +08:00
lvmengandClaude Sonnet 4.6 3d67dd2c2d refactor: 优化货架列表转换逻辑并支持奇数长度处理
- 移除 subList 创建,直接通过索引访问元素
- 修复奇数长度列表的处理逻辑
- 保持单次循环,提升性能

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:53:00 +08:00
mazengfeiandClaude Sonnet 4.6 133f309d4b refactor: 统一时钟管理并优化状态栏显示逻辑
- 在 BaseActivity 中实现统一的时钟管理机制,避免各子类重复实现
- LogActivity 和 ShelfActivity 覆写 onClockTick() 自定义状态栏显示
- HomeActivity 移除重复的时间更新逻辑,启用右侧状态栏菜单
- 提取协议常量到 ProtocolConstants,提高代码可维护性
- 优化开锁逻辑判断,改为基于 deviceId 的动态判断
- 新增右侧状态栏弹出菜单布局文件

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:07:41 +08:00
mazengfeiandClaude Sonnet 4.6 3e5c7c6caa refactor(HomeActivity): 修复内存泄漏并优化错误处理
1. 修复内存泄漏问题
   - 在 onDestroy 中清理所有 Handler 回调
   - 在 onDestroy 中释放 Dialog 引用

2. 增强类型安全
   - updateUI 中增加类型检查和日志
   - updateOverdueState 中增加完整的类型检查

3. 优化串口异常处理
   - 串口数据处理异常时向用户提示

4. 消除代码重复
   - 删除冗余的 saveShelfGoodsList 方法
   - 定时任务直接调用 syncShelfGoodsToServer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:19:45 +08:00
mazengfeiandClaude Sonnet 4.6 04fe3ec2c1 refactor: 全面重构 HomeActivity 架构并修复多项 Bug
- 新增 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 <noreply@anthropic.com>
2026-04-01 11:26:05 +08:00
15 changed files with 971 additions and 464 deletions
+1 -1
View File
@@ -43,7 +43,7 @@
android:name=".activity.ShelfActivity" android:name=".activity.ShelfActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" /> tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.intelligent.shelves.activity.SettingActivity" /> <!-- <activity android:name="com.shuwei.intelligent.shelves.activity.SettingActivity" />-->
<activity android:name="com.shuwei.intelligent.shelves.activity.LogActivity" /> <activity android:name="com.shuwei.intelligent.shelves.activity.LogActivity" />
<receiver <receiver
@@ -3,10 +3,11 @@ package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.SparseIntArray import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -28,7 +29,6 @@ import com.shuwei.intelligent.shelves.net.Loading
import com.shuwei.intelligent.shelves.net.NetViewModel import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.RespData import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.UiState 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.C_TEMP_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
@@ -37,38 +37,30 @@ import com.shuwei.intelligent.shelves.serial.ProtocolConstants.RIGHT_SHELF_OPEN_
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.START_TEMP_CTRL_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.SerialFrameParser
import com.shuwei.intelligent.shelves.serial.SerialPortManager import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.serial.SerialProtocolHandler
import com.shuwei.intelligent.shelves.utils.GridLayoutTool
import com.shuwei.intelligent.shelves.utils.IntervalExecutor 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.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.gone import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toJsonString import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import com.shuwei.intelligent.shelves.utils.ext.toast import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible import com.shuwei.intelligent.shelves.utils.ext.visible
import com.shuwei.intelligent.shelves.utils.hexToBinary
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
class HomeActivity : BaseActivity() { class HomeActivity : BaseActivity() {
companion object { companion object {}
private const val TAG = "HomeActivity"
private const val LOG_PREFIX = "receiveSerialPortData:收到" override val enableRightStatusMenu: Boolean = true
private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
}
// 货架列表数据 // 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf() private val list: MutableList<ShelfModel> = mutableListOf()
// 各传感器重量数据,key 为货架编号
private val weightArray = SparseIntArray()
// 当前选中货架下标 // 当前选中货架下标
private var shelfIndex = 0 private var shelfIndex = 0
@@ -78,13 +70,52 @@ class HomeActivity : BaseActivity() {
// 当前湿度 // 当前湿度
private var showHumidity = "0" private var showHumidity = "0"
// 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false // 仅特定 deviceId 的部署点需要发送开锁指令
private var isNeedOpenLock = true private fun isNeedOpenLock() = App.deviceId == "4787e213-90ab-3e32-88e0-ac271a937751"
private var deviceName = ""
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels() 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..list.size) {
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)
log(logMsg)
// 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") @SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -92,10 +123,12 @@ class HomeActivity : BaseActivity() {
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
setBackground() setBackground()
setContentView(binding.root) setContentView(binding.root)
onBackPressedDispatcher.addCallback(this) {
}
updateLeftStatus("") updateLeftStatus("")
initRecyclerView() initRecyclerView()
// 串口初始化,独立协程,不阻塞网络状态监听 // 串口初始化,独立协程,不阻塞网络状态监听
lifecycleScope.launch { lifecycleScope.launch {
val openState = SerialPortManager.open() val openState = SerialPortManager.open()
@@ -105,14 +138,19 @@ class HomeActivity : BaseActivity() {
// 切换到主线程协程处理串口数据 // 切换到主线程协程处理串口数据
lifecycleScope.launch { lifecycleScope.launch {
runCatching { receiveSerialPortData(data) } runCatching { receiveSerialPortData(data) }
.onFailure { it.printStackTrace() } .onFailure {
it.printStackTrace()
log("串口数据处理异常: ${it.message}")
toast("串口数据处理异常,请检查设备连接")
}
} }
} }
} }
} }
// 网络状态监听,独立协程,与串口初始化并行执行 // 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { launch {
viewModel.getAccessTokenUiState.collect { state -> viewModel.getAccessTokenUiState.collect { state ->
when (state) { when (state) {
@@ -133,8 +171,31 @@ class HomeActivity : BaseActivity() {
} }
} }
} }
launch {
viewModel.updateOverdueStateUiState.collect { state ->
if (state is UiState.Success<*>) {
val respData = state.data as? RespData<*>
if (respData == null) {
log("updateOverdueState: 数据类型错误,期望 RespData")
return@collect
}
val shelfResult = respData.data as? ShelfResult
if (shelfResult == null) {
log("updateOverdueState: 数据类型错误,期望 ShelfResult")
return@collect
}
val items = shelfResult.containerGoodsList
if (items.isNullOrEmpty()) {
log("updateOverdueState: 货架列表为空")
return@collect
}
applyOverdueState(items)
}
}
}
} }
} }
binding.root.postDelayed({ binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD) sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD) sendCmd(C_TEMP_CMD)
@@ -146,6 +207,13 @@ class HomeActivity : BaseActivity() {
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
} }
private val defDeviceList = listOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751"
)
private fun getTokenSuccess(data: RespData<*>) { private fun getTokenSuccess(data: RespData<*>) {
log("getTokenSuccess: $data") log("getTokenSuccess: $data")
data.data?.let { data.data?.let {
@@ -162,27 +230,24 @@ class HomeActivity : BaseActivity() {
private fun updateUI(data: RespData<*>) { private fun updateUI(data: RespData<*>) {
binding.include?.root?.gone() binding.include?.root?.gone()
window?.decorView?.postDelayed({ Loading.dismiss() }, 500) window?.decorView?.postDelayed({ Loading.dismiss() }, 500)
if (data.data !is ShelfResult) { val shelfResult = data.data as? ShelfResult
if (shelfResult == null) {
log("updateUI: 数据类型错误,期望 ShelfResult,实际类型:${data.data?.javaClass?.simpleName}")
loadEmptyView() loadEmptyView()
return return
} }
deviceName = data.data.deviceName deviceName = shelfResult.deviceName
updateLeftStatus(deviceName)
updateLeftStatus("$deviceName") App.canteenId = shelfResult.placeId
var tempList = shelfResult.containerGoodsList
App.canteenId = data.data.placeId
val tempList = data.data.containerGoodsList
if (tempList.isNullOrEmpty()) { if (tempList.isNullOrEmpty()) {
loadEmptyView() loadEmptyView()
return return
} }
list.clear() list.clear()
val subList01 = tempList.subList(0, tempList.size / 2) tempList = GridLayoutTool.initListSort(tempList)
val subList02 = tempList.subList(tempList.size / 2, tempList.size) list.addAll(tempList)
repeat(tempList.size / 2) { index ->
list.add(subList01[index].also { it.deviceId = App.deviceId })
list.add(subList02[index].also { it.deviceId = App.deviceId })
}
shelfAdapter.notifyDataSetChanged() shelfAdapter.notifyDataSetChanged()
} }
@@ -192,135 +257,13 @@ class HomeActivity : BaseActivity() {
loadEmptyView() loadEmptyView()
} }
/**
* 终端id
*/
private var terminalId: String = ""
/**
* 固件版本
*/
private var terminalVersion: String = ""
private val serialFrameParser = SerialFrameParser()
private fun receiveSerialPortData(srcData: String) { private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData") log("receiveSerialPortData: $srcData")
// 重置无数据计时,并关闭已显示的超时提示弹窗 // 重置无数据计时,并关闭已显示的超时提示弹窗
lastDataReceivedTime = System.currentTimeMillis() lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss() noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
val frame = serialFrameParser.parse(srcData) ?: return val frame = serialFrameParser.parse(srcData) ?: return
handleReceivedData(frame) serialProtocolHandler.handle(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 -> {}
}
} }
private var tipDialog: CommonDialog? = null private var tipDialog: CommonDialog? = null
@@ -337,12 +280,16 @@ class HomeActivity : BaseActivity() {
*/ */
private val noDataWatchdogTask = object : Runnable { private val noDataWatchdogTask = object : Runnable {
override fun run() { override fun run() {
// 生命周期检查,防止 Activity 销毁后继续执行
if (isFinishing || isDestroyed) return
val elapsed = System.currentTimeMillis() - lastDataReceivedTime val elapsed = System.currentTimeMillis() - lastDataReceivedTime
if (elapsed >= 5 * 60 * 1000L) { if (elapsed >= 5 * 60 * 1000L) {
if (noDataWarningDialog == null) { if (noDataWarningDialog == null) {
noDataWarningDialog = CommonDialog(this@HomeActivity).apply { noDataWarningDialog = CommonDialog(this@HomeActivity).apply {
dialogTitle = "设备提示" dialogTitle = "设备提示"
dialogContent = "长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备" dialogContent =
"长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备"
} }
} }
if (noDataWarningDialog?.isShowing == false) { if (noDataWarningDialog?.isShowing == false) {
@@ -353,119 +300,24 @@ 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) { override fun onUserInactive() {
try { switchLight(false)
// 已通过激活指令成功解析过,无需重复解析
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 onDestroy() { override fun onDestroy() {
SerialPortManager.close() // 清理 Handler 所有回调,防止内存泄漏
handler.removeCallbacksAndMessages(null)
// 关闭并释放 Dialog 引用,防止内存泄漏
tipDialog?.dismiss()
tipDialog = null
noDataWarningDialog?.dismiss()
noDataWarningDialog = null
runBlocking { SerialPortManager.close() }
saveTaskJob?.cancel() saveTaskJob?.cancel()
overdueTaskJob?.cancel() overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
@@ -486,7 +338,7 @@ class HomeActivity : BaseActivity() {
private fun onShelfItemClicked(position: Int) { private fun onShelfItemClicked(position: Int) {
shelfIndex = position shelfIndex = position
lifecycleScope.launch { lifecycleScope.launch {
if (isNeedOpenLock) { if (isNeedOpenLock()) {
val openCmd = if (list[position].deviceNo in 1..5) val openCmd = if (list[position].deviceNo in 1..5)
LEFT_SHELF_OPEN_CMD LEFT_SHELF_OPEN_CMD
else else
@@ -504,9 +356,13 @@ class HomeActivity : BaseActivity() {
/** /**
* 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端 * 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端
*/ */
@Suppress("DEPRECATION")
private fun onShelfActivityResult(position: Int, intent: Intent) { 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 { list[position].apply {
goodsId = model.goodsId goodsId = model.goodsId
goodsName = model.goodsName goodsName = model.goodsName
@@ -516,30 +372,15 @@ class HomeActivity : BaseActivity() {
} }
} }
shelfAdapter.notifyItemChanged(position) shelfAdapter.notifyItemChanged(position)
syncShelfGoodsToServer()
try {
val submitList = list.sortedBy { it.deviceNo }
val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId
body.temperature = showTemperatureC
body.humidity = showHumidity
body.goodsList = submitList
}
log("performSync: body:${body.toJsonString()}")
viewModel.saveShelfGoodsList(body)
} catch (e: Exception) {
e.printStackTrace()
}
} }
private fun initRecyclerView() { private fun initRecyclerView() {
binding.rvShelf.run { val layoutManager = GridLayoutTool.getLayoutManager(this@HomeActivity) ?: return
layoutManager = binding.rvShelf.let {
GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false) it.layoutManager = layoutManager
adapter = shelfAdapter it.adapter = shelfAdapter
it.itemAnimator.let {
itemAnimator.let {
if (it is DefaultItemAnimator) { if (it is DefaultItemAnimator) {
it.supportsChangeAnimations = false it.supportsChangeAnimations = false
} }
@@ -547,32 +388,17 @@ 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)
}
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val updateTask = object : Runnable {
override fun run() {
updateDateTime()
handler.postDelayed(this, 1000)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
hideStatusBar() hideStatusBar()
handler.post(updateTask)
// 启动无数据看门狗(每30秒检查一次) // 启动无数据看门狗(每30秒检查一次)
handler.postDelayed(noDataWatchdogTask, 30 * 1000L) handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
handler.removeCallbacks(updateTask)
// 停止无数据看门狗 // 停止无数据看门狗
handler.removeCallbacks(noDataWatchdogTask) handler.removeCallbacks(noDataWatchdogTask)
} }
@@ -581,7 +407,6 @@ class HomeActivity : BaseActivity() {
private fun loadEmptyView() { private fun loadEmptyView() {
list.clear() list.clear()
shelfAdapter.notifyDataSetChanged() shelfAdapter.notifyDataSetChanged()
binding.include?.let { binding.include?.let {
it.root.visible() it.root.visible()
it.root.setOnClickListener { it.root.setOnClickListener {
@@ -593,7 +418,7 @@ class HomeActivity : BaseActivity() {
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) { fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo } val item = list.firstOrNull { it.deviceNo == event.shelfNo }
item?.apply { item?.apply {
goodsId = "" goodsId = ""
@@ -607,61 +432,67 @@ class HomeActivity : BaseActivity() {
} }
private val taskExecutor by lazy { IntervalExecutor() } private val taskExecutor by lazy { IntervalExecutor() }
private var saveTaskJob: Job?=null private var saveTaskJob: Job? = null
/** /**
* 保存定时任务 * 保存定时任务
*/ */
private fun saveGoodsTask() { private fun saveGoodsTask() {
saveTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) { saveTaskJob =
saveShelfGoodsList() taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) {
} syncShelfGoodsToServer()
}
} }
private var overdueTaskJob: Job?=null private var overdueTaskJob: Job? = null
/** /**
* 过期时间定时任务 * 过期时间定时任务
*/ */
private fun overdueTask() { private fun overdueTask() {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60* 1000L) { overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) {
updateOverdueState() updateOverdueState()
} }
} }
private fun updateOverdueState() { private fun updateOverdueState() {
viewModel.updateOverdueState { state, items -> viewModel.updateOverdueState()
if (items.isNullOrEmpty()) return@updateOverdueState }
if (items.size != list.size) return@updateOverdueState
list.forEachIndexed { index, model -> /**
//接口过期状态变化后更新状态 * 将接口返回的过期状态应用到本地列表,仅在状态变化时刷新对应 item
if (items[index].overdue != model.overdue) { */
model.overdue = items[index].overdue private fun applyOverdueState(items: List<ShelfModel>) {
shelfAdapter.notifyItemChanged(index) if (items.size != list.size) return
log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}") 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 返回共用)
val submitList = list.sortedBy { it.deviceNo } */
private fun syncShelfGoodsToServer() {
log("performSync显示list:${list.toJsonString()}")
val submitList = GridLayoutTool.submitListSort(list)
submitList.forEach { submitList.forEach {
if (it.goodsId.isNullOrBlank()) { if (it.goodsId.isNullOrBlank()) {
it.weight = it.weightBak it.weight = it.weightBak
} }
} }
val body = ShelfBody().also { val body = ShelfBody().also {
it.deviceId = App.deviceId it.deviceId = App.deviceId ?: ""
it.canteenId = App.canteenId it.canteenId = App.canteenId
it.temperature = showTemperatureC it.temperature = showTemperatureC
it.humidity = showHumidity it.humidity = showHumidity
it.goodsList = submitList it.goodsList = submitList
} }
log("performSync: body:${body.toJsonString()}") log("performSync提交body:${body.toJsonString()}")
viewModel.saveShelfGoodsList(body) viewModel.saveShelfGoodsList(body)
} }
}
}
@@ -1,28 +1,53 @@
package com.shuwei.intelligent.shelves.activity package com.shuwei.intelligent.shelves.activity
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.shuwei.intelligent.shelves.adapter.LogAdapter import com.shuwei.intelligent.shelves.adapter.LogAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityLogListBinding import com.shuwei.intelligent.shelves.databinding.ActivityLogListBinding
import com.shuwei.intelligent.shelves.dialog.CommonDialog import com.shuwei.intelligent.shelves.dialog.CommonDialog
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.gone import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.startActivity import com.shuwei.intelligent.shelves.utils.ext.startActivity
import com.shuwei.intelligent.shelves.utils.ext.toast import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.utils.ext.visible import com.shuwei.intelligent.shelves.utils.ext.visible
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class LogActivity : BaseActivity() { class LogActivity : BaseActivity() {
companion object { companion object {
const val IS_LOG_DIR = "isLogDir" const val IS_LOG_DIR = "isLogDir"
const val LOG_DATE = "logDate" const val LOG_DATE = "logDate"
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
}
/**
* 覆写时钟回调:左侧状态栏显示年月日星期,右侧状态栏显示时分秒
*/
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val arr = sdf.format(Date()).split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
} }
private lateinit var binding:ActivityLogListBinding private lateinit var binding:ActivityLogListBinding
private var isLogDir = false private var isLogDir = false
private var logDate = "" private var logDate = ""
private val logList = mutableListOf<String>() private val logList = mutableListOf<String>()
private val originalLogList = mutableListOf<String>() // 原始日志备份
private val logAdapter by lazy { private val logAdapter by lazy {
LogAdapter(logList).apply { LogAdapter(logList).apply {
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
@@ -62,24 +87,20 @@ class LogActivity : BaseActivity() {
logAdapter.notifyDataSetChanged() logAdapter.notifyDataSetChanged()
} }
if (isLogDir) {
logList.addAll(fileLogger.getLogFiles())
} else {
logList.addAll(fileLogger.readLogsByName(logDate))
}
binding.rvLogList.let { binding.rvLogList.let {
it.layoutManager = LinearLayoutManager(this) it.layoutManager = LinearLayoutManager(this)
it.adapter = logAdapter it.adapter = logAdapter
} }
setupSearch()
setupJumpButton()
binding.btnFind.run { binding.btnFind.run {
if (isLogDir) gone() else visible() if (isLogDir) gone() else visible()
} }
binding.btnFind.setOnClickListener { binding.btnFind.setOnClickListener {
if (isLogDir) { if (isLogDir) return@setOnClickListener
return@setOnClickListener val list = logList.filter { it.contains("${ProtocolConstants.HEADER}${ProtocolConstants.CMD_TEMP_REPORT}") }
}
val list = logList.filter { it.contains("${HomeActivity.HEADER}1001") }
if (list.isEmpty()) { if (list.isEmpty()) {
toast("未找到到1001指令") toast("未找到到1001指令")
return@setOnClickListener return@setOnClickListener
@@ -88,6 +109,95 @@ class LogActivity : BaseActivity() {
logList.addAll(list) logList.addAll(list)
logAdapter.notifyDataSetChanged() logAdapter.notifyDataSetChanged()
} }
// 后台加载日志数据
lifecycleScope.launch {
binding.progressBar.visible()
val data = withContext(Dispatchers.IO) {
if (isLogDir) fileLogger.getLogFiles()
else fileLogger.readLogsByName(logDate)
}
logList.addAll(data)
originalLogList.addAll(data)
logAdapter.notifyDataSetChanged()
binding.progressBar.gone()
setupTimeNavigation()
}
}
// 搜索功能
private fun setupSearch() {
binding.etSearch.addTextChangedListener(afterTextChanged = { s ->
val keyword = s?.toString() ?: ""
logList.clear()
logList.addAll(if (keyword.isEmpty()) originalLogList else originalLogList.filter { it.contains(keyword, ignoreCase = true) })
logAdapter.notifyDataSetChanged()
})
}
// 快速跳转功能
private fun setupJumpButton() {
var isAtTop = true
binding.fabJump.setOnClickListener {
if (isAtTop) {
binding.rvLogList.scrollToPosition(logList.size - 1)
} else {
binding.rvLogList.scrollToPosition(0)
}
isAtTop = !isAtTop
}
}
// 时间导航功能
private fun setupTimeNavigation() {
if (isLogDir) {
binding.hsvTimeNav.gone()
return
}
val timeMap = mutableMapOf<String, Int>()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
originalLogList.forEachIndexed { index, log ->
val timeStr = log.substringAfter("[").substringBefore("]")
try {
val date = dateFormat.parse(timeStr)
date?.let {
val hourKey = SimpleDateFormat("HH:00", Locale.getDefault()).format(it)
if (!timeMap.containsKey(hourKey)) {
timeMap[hourKey] = index
}
}
} catch (e: Exception) {
// 忽略解析失败的日志
}
}
if (timeMap.isEmpty()) {
binding.hsvTimeNav.gone()
return
}
timeMap.toSortedMap().forEach { (hour, position) ->
val btn = TextView(this).apply {
text = hour
textSize = 14f
setTextColor(Color.BLACK)
setPadding(12.dp, 8.dp, 12.dp, 8.dp)
gravity = Gravity.CENTER
setBackgroundColor(Color.parseColor("#E0E0E0"))
setOnClickListener {
binding.rvLogList.scrollToPosition(position)
}
}
val params = android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
android.widget.LinearLayout.LayoutParams.MATCH_PARENT
).apply {
marginEnd = 8.dp
}
binding.llTimeNav.addView(btn, params)
}
} }
} }
@@ -3,8 +3,6 @@ package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -18,7 +16,7 @@ import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.scwang.smart.refresh.layout.constant.RefreshState import com.scwang.smart.refresh.layout.constant.RefreshState
import com.shuwei.intelligent.shelves.App import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.R import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.DEVICE_INFO_CMD import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.adapter.SearchAdapter import com.shuwei.intelligent.shelves.adapter.SearchAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityShelfBinding import com.shuwei.intelligent.shelves.databinding.ActivityShelfBinding
@@ -148,7 +146,7 @@ class ShelfActivity : BaseActivity() {
//binding.tvFoodWeight.text = "0克" //binding.tvFoodWeight.text = "0克"
EventBus.getDefault().post(ClearShelfEvent(shelfModel!!.deviceNo, tempWeight)) EventBus.getDefault().post(ClearShelfEvent(shelfModel!!.deviceNo, tempWeight))
} }
sendCmd(DEVICE_INFO_CMD) sendCmd(ProtocolConstants.DEVICE_INFO_CMD)
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.getGoodsListUiState.collect { state -> viewModel.getGoodsListUiState.collect { state ->
@@ -305,31 +303,23 @@ class ShelfActivity : BaseActivity() {
} }
} }
private fun updateDateTime() { /**
* 覆写时钟回调:左侧状态栏显示年月日星期,右侧状态栏显示时分秒
*/
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA) val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val dateTime = sdf.format(Date()) val arr = sdf.format(Date()).split("***")
val arr = dateTime.split("***")
updateLeftStatus(arr[0]) updateLeftStatus(arr[0])
updateRightStatus(arr[1]) updateRightStatus(arr[1])
} }
private val handler = Handler(Looper.getMainLooper())
private val updateTask = object : Runnable {
override fun run() {
updateDateTime()
handler.postDelayed(this, 1000)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
hideStatusBar() hideStatusBar()
handler.post(updateTask)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
handler.removeCallbacks(updateTask)
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -371,20 +361,20 @@ class ShelfActivity : BaseActivity() {
val shelfNo = shelfModel?.deviceNo ?: 0 val shelfNo = shelfModel?.deviceNo ?: 0
val hex = shelfNo.toString(16).padStart(2, '0').uppercase() val hex = shelfNo.toString(16).padStart(2, '0').uppercase()
Log.d(TAG, "onCreate: hex=$hex") Log.d(TAG, "onCreate: hex=$hex")
val zeroClearingCmd = "${HomeActivity.HEADER}0401${hex}${HomeActivity.FOOTER}" val zeroClearingCmd = "${ProtocolConstants.HEADER}0401${hex}${ProtocolConstants.FOOTER}"
log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}") log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}")
sendCmd(zeroClearingCmd) sendCmd(zeroClearingCmd)
sendCmd( // sendCmd(
if (shelfNo in 1..5) HomeActivity.LEFT_SHELF_OPEN_CMD else HomeActivity.RIGHT_SHELF_OPEN_CMD // if (shelfNo in 1..5) ProtocolConstants.LEFT_SHELF_OPEN_CMD else ProtocolConstants.RIGHT_SHELF_OPEN_CMD
) // )
//清零 //清零
realWeight = 0 realWeight = 0
// binding.tvFoodWeight.text = "${realWeight}克" // binding.tvFoodWeight.text = "${realWeight}克"
sendCmd(DEVICE_INFO_CMD) sendCmd(ProtocolConstants.DEVICE_INFO_CMD)
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
Loading.show(this@ShelfActivity) Loading.show(this@ShelfActivity)
// window.decorView.postDelayed({ Loading.dismiss() }, 5000) window.decorView.postDelayed({ Loading.dismiss() }, 10000)
} }
KeyboardUtil.hideKeyboard(window.decorView) KeyboardUtil.hideKeyboard(window.decorView)
} }
@@ -5,26 +5,34 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.PopupWindow
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.shuwei.intelligent.shelves.activity.HomeActivity import com.shuwei.intelligent.shelves.activity.LogActivity
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.R import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.databinding.ActivityBaseBinding 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.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.FileLogger import com.shuwei.intelligent.shelves.utils.FileLogger
import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import com.shuwei.intelligent.shelves.utils.ext.setOnDoubleClickListener import com.shuwei.intelligent.shelves.utils.ext.setOnDoubleClickListener
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
open class BaseActivity : AppCompatActivity() { open class BaseActivity : AppCompatActivity() {
@@ -57,38 +65,28 @@ 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() lightSwitchClick()
binding.tvLeftStatus.setOnDoubleClickListener { binding.tvLeftStatus.setOnDoubleClickListener {
finish() //finish()
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
} }
binding.tvRightStatus.setOnDoubleClickListener { binding.tvRightStatus.setOnDoubleClickListener {
EnvSwitchDialog(this).show() if (enableRightStatusMenu) showRightStatusPopup(binding.tvRightStatus)
} }
} }
fun hideStatusBar() { /** 是否允许右侧状态栏双击弹出菜单,默认关闭,仅 HomeActivity 开启 */
//// enableEdgeToEdge() protected open val enableRightStatusMenu: Boolean = false
//// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
//// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选) fun hideStatusBar() {}
//// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
//
// ImmersionBar.with(this)
// .hideBar(BarHide.FLAG_HIDE_STATUS_BAR)
// .init()
}
fun statusBarDarkFont() { fun statusBarDarkFont() {
//window.decorView.systemUiVisibility = if (enable) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0
val color = ContextCompat.getColor(this, R.color.white) val color = ContextCompat.getColor(this, R.color.white)
binding.tvLeftStatus.setTextColor(color) binding.tvLeftStatus.setTextColor(color)
binding.tvRightStatus.setTextColor(color) binding.tvRightStatus.setTextColor(color)
@@ -107,7 +105,7 @@ open class BaseActivity : AppCompatActivity() {
} }
fun updateLeftStatus(data: String) { fun updateLeftStatus(data: String) {
binding.tvLeftStatus.text = data binding.tvLeftStatus.text = data.ifBlank { " " }
} }
fun updateRightStatus(data: String) { fun updateRightStatus(data: String) {
@@ -118,11 +116,35 @@ open class BaseActivity : AppCompatActivity() {
private val TIME_OUT: Long = (5 * 60 * 1000).toLong() private val TIME_OUT: Long = (5 * 60 * 1000).toLong()
private val mHandler by lazy { Handler(Looper.getMainLooper()) } private val mHandler by lazy { Handler(Looper.getMainLooper()) }
private val mTimeoutRunnable = Runnable { private val mTimeoutRunnable = Runnable {
if (this is HomeActivity) { // 用户长时间无操作时回调,子类可覆写实现具体逻辑(如关灯)
this.switchLight(false) onUserInactive()
}
// ---- 右侧状态栏时钟 ----
// 默认时间格式:月日星期 时:分:秒
private val CLOCK_FORMAT = "MM月dd日 EEEE HH:mm:ss"
private val clockFormatter by lazy { SimpleDateFormat(CLOCK_FORMAT, Locale.CHINA) }
/**
* 每秒时钟回调,默认将当前时间显示在右侧状态栏。
* 子类可覆写以自定义格式或同时更新左侧状态栏。
*/
open fun onClockTick() {
updateRightStatus(clockFormatter.format(Date()))
}
private val clockTask = object : Runnable {
override fun run() {
onClockTick()
mHandler.postDelayed(this, 1000)
} }
} }
/**
* 用户超时无操作时回调,默认空实现,子类按需覆写
*/
open fun onUserInactive() {}
override fun onUserInteraction() { override fun onUserInteraction() {
super.onUserInteraction() super.onUserInteraction()
//开灯 //开灯
@@ -139,14 +161,18 @@ open class BaseActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// 启动定时器 // 启动超时定时器
mHandler.postDelayed(mTimeoutRunnable, TIME_OUT) mHandler.postDelayed(mTimeoutRunnable, TIME_OUT)
// 启动右侧状态栏时钟
mHandler.post(clockTask)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// 取消定时器 // 取消超时定时器
mHandler.removeCallbacks(mTimeoutRunnable) mHandler.removeCallbacks(mTimeoutRunnable)
// 停止右侧状态栏时钟
mHandler.removeCallbacks(clockTask)
} }
fun switchLight(isOpen: Boolean) { fun switchLight(isOpen: Boolean) {
@@ -164,15 +190,27 @@ open class BaseActivity : AppCompatActivity() {
val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true
switchLight(tag.not()) switchLight(tag.not())
} }
binding.tvRightStatus.clickWithDebounce { }
// if (this is HomeActivity) {
// startActivity<LogActivity> { private fun showRightStatusPopup(anchor: View) {
// putExtra(LogActivity.IS_LOG_DIR, true) val menuBinding = PopupRightStatusMenuBinding.inflate(LayoutInflater.from(this))
// } val popup = PopupWindow(
// return@clickWithDebounce menuBinding.root,
// } 120.dp,
// startActivity<SettingActivity> { } 101.dp,
true
)
menuBinding.tvEnvSwitch.setOnClickListener {
popup.dismiss()
EnvSwitchDialog(this).show()
} }
menuBinding.tvLog.setOnClickListener {
popup.dismiss()
startActivity<LogActivity> {
putExtra(LogActivity.IS_LOG_DIR, true)
}
}
popup.showAsDropDown(anchor)
} }
@@ -4,33 +4,17 @@ import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.App 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.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 com.shuwei.intelligent.shelves.utils.ext.toJsonString
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class NetViewModel : ViewModel() { 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>(UiState.Initial) private val _getShelfListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getShelfListUiState: StateFlow<UiState> = _getShelfListUiState val getShelfListUiState: StateFlow<UiState> = _getShelfListUiState
// private val _saveShelfGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
// val saveShelfGoodsListUiState: StateFlow<UiState> = _saveShelfGoodsListUiState
private val _getGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial) private val _getGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getGoodsListUiState: StateFlow<UiState> = _getGoodsListUiState val getGoodsListUiState: StateFlow<UiState> = _getGoodsListUiState
@@ -43,6 +27,8 @@ class NetViewModel : ViewModel() {
private val _getDeviceConfigUiState = MutableStateFlow<UiState>(UiState.Initial) private val _getDeviceConfigUiState = MutableStateFlow<UiState>(UiState.Initial)
val getDeviceConfigUiState: StateFlow<UiState> = _getDeviceConfigUiState val getDeviceConfigUiState: StateFlow<UiState> = _getDeviceConfigUiState
private val _updateOverdueStateUiState = MutableStateFlow<UiState>(UiState.Initial)
val updateOverdueStateUiState: StateFlow<UiState> = _updateOverdueStateUiState
fun getDeviceToken(deviceId: String) { fun getDeviceToken(deviceId: String) {
viewModelScope.launch { viewModelScope.launch {
@@ -62,17 +48,16 @@ class NetViewModel : ViewModel() {
fun getDeviceConfig(deviceId: String, deviceToken: String) { fun getDeviceConfig(deviceId: String, deviceToken: String) {
viewModelScope.launch { viewModelScope.launch {
_getDeviceTokenUiState.value = UiState.Loading _getDeviceConfigUiState.value = UiState.Loading
runCatching { runCatching {
val response = val response = apiService.getDeviceInfo(equipmentCode = deviceId)
apiService.getDeviceInfo(equipmentCode = deviceId)
if (response.isSuccess()) { if (response.isSuccess()) {
_getDeviceTokenUiState.value = UiState.Success(response) _getDeviceConfigUiState.value = UiState.Success(response)
} else { } else {
_getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败") _getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败")
} }
}.onFailure { }.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<ShelfModel>?)-> Unit) { fun updateOverdueState() {
viewModelScope.launch { viewModelScope.launch {
runCatching { runCatching {
val response = apiService.getShelfList(deviceId = App.deviceId) val response = apiService.getShelfList(deviceId = App.deviceId)
if (response.isSuccess()) { if (response.isSuccess()) {
block(true, response.data?.containerGoodsList) _updateOverdueStateUiState.value = UiState.Success(response)
} else { } else {
block(false, emptyList()) _updateOverdueStateUiState.value = UiState.Error(response.msg ?: "请求失败")
} }
}.onFailure { }.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( fun getGoodsList(
canteenId: String = "", canteenId: String = "",
goodsName: String = "", goodsName: String = "",
@@ -176,7 +145,5 @@ class NetViewModel : ViewModel() {
Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}") Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}")
} }
} }
} }
} }
@@ -1,8 +1,10 @@
package com.shuwei.intelligent.shelves.serial package com.shuwei.intelligent.shelves.serial
import com.shuwei.intelligent.shelves.App
/** /**
* 串口通信协议常量定义 * 串口通信协议常量定义
* 统一管理所有协议头、协议尾各类指令 * 统一管理所有协议头、协议尾各类指令及帧解析偏移量
*/ */
object ProtocolConstants { object ProtocolConstants {
@@ -12,9 +14,99 @@ object ProtocolConstants {
/** 协议尾 */ /** 协议尾 */
const val FOOTER = "FFFF" 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组) */
fun shelfSensorCount(): Int {
return if (device2Columns.contains(App.deviceId)) 12
else if (device3Columns.contains(App.deviceId)) 24
else 0
}
/** 每组重量数据字节数(十六进制字符数) */
const val BYTES_PER_WEIGHT = 8
/** 温度字段字符长度 */
const val TEMPERATURE_FIELD_LEN = 2
/** 心跳帧最小有效长度 */
fun heartbeatMinLength(): Int {
return if (device2Columns.contains(App.deviceId)) 200
else if (device3Columns.contains(App.deviceId)) 296
else 0
}
/** 心跳帧中温度字段起始偏移(经协议分析得出,前面含重量+其他传感器数据) */
fun heartbeatTemperatureStart(): Int {
return if (device2Columns.contains(App.deviceId)) 158
else if (device3Columns.contains(App.deviceId)) 158+96
else 0
}
/** 激活请求体有效长度 */
const val ACTIVATE_BODY_LENGTH = 44
/** 固定数据体有效长度(terminalId 30 + terminalVersion 10 + 其他 2 */
const val FIX_BODY_LENGTH = 42
// ----------------------------------------------------------------
// 完整指令
// ----------------------------------------------------------------
/** 激活指令(1楼餐厅) */ /** 激活指令(1楼餐厅) */
const val ACTIVE_CMD = // const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}" // "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
// 410激活码
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${FOOTER}"
/** 左货架开锁指令 */ /** 左货架开锁指令 */
const val LEFT_SHELF_OPEN_CMD = const val LEFT_SHELF_OPEN_CMD =
@@ -36,7 +128,7 @@ object ProtocolConstants {
/** 摄氏温度单位指令 */ /** 摄氏温度单位指令 */
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}" const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
/** 灯指令(继电器1关) */ /** 灯指令(继电器1关) */
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}" const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
/** 开灯指令(继电器1开) */ /** 开灯指令(继电器1开) */
@@ -44,4 +136,45 @@ object ProtocolConstants {
/** 温控指令 */ /** 温控指令 */
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}" const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}"
/**
* 设备2列,10格
*/
val device2Columns = listOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751"
)
/**
* 设备3列,15格,左侧1列竖向编号1、2、3、4、5,右侧2列5行编号为6-7、8-9、10-11、12-13、14-15
*/
val device3ColumnsOrder = listOf(1, 6, 7, 2, 8, 9, 3, 10, 11, 4, 12, 13, 5, 14, 15)
/**
* 设备2列,10格,左侧1列竖向编号1、2、3、4、5,右侧1列6,7,8,9,10
*/
val device2ColumnsOrder = listOf(1, 6, 2, 7, 3, 8, 4, 9, 5, 10)
/**
* 设备3列,15格,左侧1列竖向编号1、2、3、4、5,右侧2列5行编号为6-7、8-9、10-11、12-13、14-15
*/
val device3Columns = listOf(
//新设备1
"d369f9b0-1c5b-3066-ba72-988e1449cecc",
//新设备2
"505ce1b1-facc-3eb2-855a-3dadda5ba358"
)
/** 激活指令mapkey为设备号,value为激活码 */
val ACTIVE_MAP = mutableMapOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473" to "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${FOOTER}",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751" to "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}",
//新设备1,临时使用1楼餐厅激活码
"d369f9b0-1c5b-3066-ba72-988e1449cecc" to "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}",
//新设备2,临时使用1楼餐厅激活码
"505ce1b1-facc-3eb2-855a-3dadda5ba358" to "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}",
)
} }
@@ -13,8 +13,9 @@ import java.nio.charset.Charset
import java.util.Locale import java.util.Locale
object SerialPortManager { object SerialPortManager {
private const val portName: String = "/dev/ttyS1"
private const val baudRate: Int = 115200 private const val PORT_NAME: String = "/dev/ttyS1"
private const val BAUD_RATE: Int = 115200
private var serialPort: AndroidSerialPort? = null private var serialPort: AndroidSerialPort? = null
// private var serialPort: SerialPort? = null // private var serialPort: SerialPort? = null
@@ -25,8 +26,8 @@ object SerialPortManager {
return try { return try {
serialPort = SerialPortFactory.create().apply { serialPort = SerialPortFactory.create().apply {
open( open(
portName = portName, portName = PORT_NAME,
baudRate = baudRate, baudRate = BAUD_RATE,
dataBits = 8, dataBits = 8,
stopBits = 1, stopBits = 1,
parity = SerialPort.PARITY_NONE parity = SerialPort.PARITY_NONE
@@ -69,7 +70,7 @@ object SerialPortManager {
} }
// 关闭串口 // 关闭串口
fun close() { suspend fun close() {
scope.cancel() scope.cancel()
serialPort?.close() serialPort?.close()
} }
@@ -0,0 +1,247 @@
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()
}
}
}
@@ -0,0 +1,54 @@
package com.shuwei.intelligent.shelves.utils
import android.content.Context
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
object GridLayoutTool {
/**
* 设备2列,10格
*/
private val device2Columns = ProtocolConstants.device2Columns
/**
* 设备3列,15格,左侧1列竖向编号1、2、3、4、5,右侧2列5行编号为6-7、8-9、10-11、12-13、14-15
*/
private val device3Columns = ProtocolConstants.device3Columns
fun getLayoutManager(context: Context): RecyclerView.LayoutManager? {
return if (device2Columns.contains(App.deviceId)) {
GridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false)
} else if (device3Columns.contains(App.deviceId)) {
GridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false)
} else {
null
}
}
/**
* 初始化list显示顺序
*/
fun initListSort(source: List<ShelfModel>): List<ShelfModel> {
val map = source.associateBy { it.deviceNo }
return if (device3Columns.contains(App.deviceId)) {
// 按全局定义的显示顺序取对应数据,找不到则跳过
ProtocolConstants.device3ColumnsOrder.mapNotNull { map[it] }
} else if (device2Columns.contains(App.deviceId)) {
ProtocolConstants.device2ColumnsOrder.mapNotNull { map[it] }
} else {
source
}
}
/**
* 提交接口list顺序
*/
fun submitListSort(source: List<ShelfModel>): List<ShelfModel> {
return source.sortedBy { it.deviceNo }
}
}
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="240dp"
android:height="240dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<!-- 由小到大排列的圆点,构成旋转加载指示器 -->
<path
android:fillColor="#ffffff"
android:pathData="M876.864,782.592c3.264,0 6.272,-3.2 6.272,-6.656c0,-3.456 -3.008,-6.592 -6.272,-6.592c-3.264,0 -6.272,3.2 -6.272,6.592c0,3.456 3.008,6.656 6.272,6.656z" />
<path
android:fillColor="#ffffff"
android:pathData="M736.32,935.936c2.304,2.432 5.568,3.84 8.768,3.84a12.16,12.16 0,0 0,8.832 -3.84a13.76,13.76 0,0 0,0 -18.56a12.224,12.224 0,0 0,-8.832 -3.84a12.16,12.16 0,0 0,-8.768 3.84a13.696,13.696 0,0 0,0 18.56z" />
<path
android:fillColor="#ffffff"
android:pathData="M552.32,1018.24c3.456,3.648 8.32,5.76 13.184,5.76a18.368,18.368 0,0 0,13.184 -5.76a20.608,20.608 0,0 0,0 -27.968a18.368,18.368 0,0 0,-13.184 -5.824a18.368,18.368 0,0 0,-13.184 5.76a20.608,20.608 0,0 0,0 28.032z" />
<path
android:fillColor="#ffffff"
android:pathData="M353.984,1012.48c4.608,4.8 11.072,7.68 17.6,7.68a24.448,24.448 0,0 0,17.536 -7.68a27.456,27.456 0,0 0,0 -37.248a24.448,24.448 0,0 0,-17.536 -7.68a24.448,24.448 0,0 0,-17.6 7.68a27.52,27.52 0,0 0,0 37.184z" />
<path
android:fillColor="#ffffff"
android:pathData="M178.304,920.64c5.76,6.08 13.824,9.6 21.952,9.6a30.592,30.592 0,0 0,22.016 -9.6a34.368,34.368 0,0 0,0 -46.592a30.592,30.592 0,0 0,-22.016 -9.6a30.592,30.592 0,0 0,-21.952 9.6a34.368,34.368 0,0 0,0 46.592z" />
<path
android:fillColor="#ffffff"
android:pathData="M57.152,761.28c6.912,7.36 16.64,11.648 26.368,11.648a36.736,36.736 0,0 0,26.432 -11.584a41.28,41.28 0,0 0,0 -55.936a36.736,36.736 0,0 0,-26.432 -11.584a36.8,36.8 0,0 0,-26.368 11.52a41.28,41.28 0,0 0,0 56z" />
<path
android:fillColor="#ffffff"
android:pathData="M12.736,564.672a42.88,42.88 0,0 0,30.784 13.44a42.88,42.88 0,0 0,30.784 -13.44a48.128,48.128 0,0 0,0 -65.216a42.88,42.88 0,0 0,-30.72 -13.44a42.88,42.88 0,0 0,-30.848 13.44a48.128,48.128 0,0 0,0 65.216z" />
<path
android:fillColor="#ffffff"
android:pathData="M52.544,369.28a48.96,48.96 0,0 0,35.2 15.36a48.96,48.96 0,0 0,35.2 -15.36a54.976,54.976 0,0 0,0 -74.56a48.96,48.96 0,0 0,-35.2 -15.424a48.96,48.96 0,0 0,-35.2 15.424a54.976,54.976 0,0 0,0 74.56z" />
<path
android:fillColor="#ffffff"
android:pathData="M168.32,212.48c10.368,11.008 24.96,17.408 39.68,17.408c14.592,0 29.184,-6.4 39.552,-17.408a61.888,61.888 0,0 0,0 -83.84a55.104,55.104 0,0 0,-39.616 -17.408c-14.656,0 -29.248,6.4 -39.616,17.408a61.888,61.888 0,0 0,0 83.84z" />
<path
android:fillColor="#ffffff"
android:pathData="M337.344,124.8c11.52,12.16 27.712,19.264 43.968,19.264c16.256,0 32.448,-7.04 43.968,-19.264a68.672,68.672 0,0 0,0 -93.184a61.248,61.248 0,0 0,-43.968 -19.264a61.248,61.248 0,0 0,-43.968 19.264a68.736,68.736 0,0 0,0 93.184z" />
<path
android:fillColor="#ffffff"
android:pathData="M526.976,123.712c12.672,13.44 30.528,21.248 48.448,21.248s35.712,-7.808 48.384,-21.248a75.584,75.584 0,0 0,0 -102.464A67.392,67.392 0,0 0,575.36 0c-17.92,0 -35.776,7.808 -48.448,21.248a75.584,75.584 0,0 0,0 102.464z" />
<path
android:fillColor="#ffffff"
android:pathData="M700.8,210.304c13.824,14.592 33.28,23.104 52.736,23.104c19.584,0 39.04,-8.512 52.8,-23.104a82.432,82.432 0,0 0,0 -111.744a73.472,73.472 0,0 0,-52.8 -23.168c-19.52,0 -38.912,8.512 -52.736,23.168a82.432,82.432 0,0 0,0 111.744z" />
<path
android:fillColor="#ffffff"
android:pathData="M824.832,368.832c14.976,15.872 36.032,25.088 57.216,25.088c21.12,0 42.24,-9.216 57.152,-25.088a89.344,89.344 0,0 0,0 -121.088a79.616,79.616 0,0 0,-57.152 -25.088c-21.184,0 -42.24,9.216 -57.216,25.088a89.344,89.344 0,0 0,0 121.088z" />
<path
android:fillColor="#ffffff"
android:pathData="M875.264,572.864c16.128,17.088 38.784,27.008 61.632,27.008c22.784,0 45.44,-9.92 61.568,-27.008a96.256,96.256 0,0 0,0 -130.432a85.76,85.76 0,0 0,-61.568 -27.072c-22.848,0 -45.44,9.984 -61.632,27.072a96.192,96.192 0,0 0,0 130.432z" />
</vector>
+1 -1
View File
@@ -23,7 +23,7 @@
android:layout_gravity="start" android:layout_gravity="start"
android:textSize="24sp" android:textSize="24sp"
android:fontFamily="sans-serif-medium" android:fontFamily="sans-serif-medium"
tools:text="1-1冷藏柜 -2°C / 87%" /> android:text=" " />
<TextView <TextView
android:id="@+id/tv_right_status" android:id="@+id/tv_right_status"
+6 -6
View File
@@ -73,9 +73,9 @@
<!-- 加载动画 --> <!-- 加载动画 -->
<ImageView <ImageView
android:id="@+id/ivLoading" android:id="@+id/ivLoading"
android:layout_width="160dp" android:layout_width="120dp"
android:layout_height="160dp" android:layout_height="120dp"
android:src="@drawable/loading_spinner" android:src="@drawable/loading"
android:contentDescription="加载中" /> android:contentDescription="加载中" />
<!-- 倒计时文字 --> <!-- 倒计时文字 -->
@@ -85,7 +85,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="60秒" android:text="60秒"
android:textSize="40sp" android:textSize="40sp"
android:textColor="@color/black" android:textColor="@color/white"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginTop="50dp" /> android:layout_marginTop="50dp" />
@@ -95,7 +95,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="网络连接检测中,请稍后……" android:text="网络连接检测中,请稍后……"
android:textSize="22sp" android:textSize="22sp"
android:textColor="#555555" android:textColor="@color/white"
android:layout_marginTop="20dp" /> android:layout_marginTop="20dp" />
</LinearLayout> </LinearLayout>
@@ -123,7 +123,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="网络连接失败" android:text="网络连接失败"
android:textSize="36sp" android:textSize="36sp"
android:textColor="@color/black" android:textColor="@color/white"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="40dp" /> android:layout_marginBottom="40dp" />
+54 -2
View File
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@@ -39,8 +44,55 @@
android:text="查找1001指令"/> android:text="查找1001指令"/>
</FrameLayout> </FrameLayout>
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="8dp"
android:background="@android:color/white"
android:hint="搜索日志..."
android:paddingHorizontal="12dp"
android:textSize="14sp"
android:singleLine="true"
tools:ignore="Autofill,TextFields" />
<HorizontalScrollView
android:id="@+id/hsvTimeNav"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/llTimeNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingHorizontal="12dp" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLogList" android:id="@+id/rvLogList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</LinearLayout> </LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabJump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@android:drawable/ic_menu_sort_by_size"
app:fabSize="normal"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_env_switch"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="切换环境"
android:textColor="#333333"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#E0E0E0" />
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="查看日志"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>