refactor(scale): 优化串口数据处理逻辑和自动应答机制
- 在串口数据读取中增加帧拼装与解析,统一自动应答发送 - 实现心跳日志降频,减少重复日志,避免主线程无效计算 - 将数据回调分派至主线程,确保线程安全 - 缓存时间格式化器,避免重复创建对象 - 增加秤重数据变更判断,避免无变化时刷新UI和日志 - 异步优化文件日志写入,使用协程通道和单线程写入,避免多线程SimpleDateFormat竞争 - 修复文件路径和上下文使用,防止Activity内存泄漏 - 精简部分注释和代码未使用段落,提高代码清晰度
This commit is contained in:
@@ -166,10 +166,23 @@ object ScaleManager {
|
|||||||
val flow = serialPort?.read()
|
val flow = serialPort?.read()
|
||||||
flow?.collect { buffer ->
|
flow?.collect { buffer ->
|
||||||
val data = buffer.toHexString2().uppercase(Locale.ROOT)
|
val data = buffer.toHexString2().uppercase(Locale.ROOT)
|
||||||
|
// IO 线程:帧拼装与解析
|
||||||
|
val parsed = tryParseCompleteFrame(data)
|
||||||
|
if (parsed != null) {
|
||||||
|
// IO 线程:发送自动应答(不占用主线程)
|
||||||
|
sendAutoResponse(parsed.first)
|
||||||
|
// IO 线程:心跳日志降频
|
||||||
|
if (parsed.first.cmd == CMD_HEARTBEAT || parsed.first.cmd == CMD_HEARTBEAT_STORE) {
|
||||||
|
logHeartbeatIfNeeded(parsed.first, parsed.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 仅回调分发和 preHook 需要主线程
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
preHook?.invoke(data)
|
preHook?.invoke(data)
|
||||||
onDataReceived(data)
|
if (parsed != null) {
|
||||||
|
invokeCallbacks(parsed.first)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
log("串口数据处理异常: ${e.message}")
|
log("串口数据处理异常: ${e.message}")
|
||||||
@@ -179,7 +192,7 @@ object ScaleManager {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
delay(50)
|
delay(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,19 +202,117 @@ object ScaleManager {
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收串口原始数据(在串口回调中调用此方法)
|
* 接收串口原始数据(公开 API,主线程安全)
|
||||||
*
|
*
|
||||||
* 内部自动完成分片拼装 → 帧解析 → 命令分发 → 自动应答
|
* 内部自动完成分片拼装 → 帧解析 → 自动应答 → 回调分发
|
||||||
*
|
*
|
||||||
* @param hexData 串口收到的十六进制字符串(可能为分片数据)
|
* @param hexData 串口收到的十六进制字符串(可能为分片数据)
|
||||||
*/
|
*/
|
||||||
fun onDataReceived(hexData: String) {
|
fun onDataReceived(hexData: String) {
|
||||||
val completeFrame = parser.appendData(hexData) ?: return
|
val parsed = tryParseCompleteFrame(hexData) ?: return
|
||||||
val frame = parser.parseFrame(completeFrame) ?: run {
|
sendAutoResponse(parsed.first)
|
||||||
|
invokeCallbacks(parsed.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IO 线程安全的帧拼装与解析(纯计算,无副作用)
|
||||||
|
*
|
||||||
|
* @return Pair<ScaleFrame, rawHex> 或 null(分片未完整/解析失败)
|
||||||
|
*/
|
||||||
|
private fun tryParseCompleteFrame(hexData: String): Pair<ScaleFrame, String>? {
|
||||||
|
val completeFrame = parser.appendData(hexData) ?: return null
|
||||||
|
val frame = parser.parseFrame(completeFrame)
|
||||||
|
if (frame == null) {
|
||||||
log("帧解析失败,原始数据:$completeFrame")
|
log("帧解析失败,原始数据:$completeFrame")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
dispatch(frame, completeFrame)
|
return frame?.let { it to completeFrame }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IO 线程:发送协议自动应答(不阻塞主线程)
|
||||||
|
*/
|
||||||
|
private fun sendAutoResponse(frame: ScaleFrame) {
|
||||||
|
when (frame.cmd) {
|
||||||
|
CMD_HEARTBEAT, CMD_HEARTBEAT_STORE -> {
|
||||||
|
val firmwareHex = frame.firmwareVersion.toAsciiHex()
|
||||||
|
val resp = "${HEADER_HEX}0102%02X%02X${firmwareHex}${FOOTER_HEX}".format(
|
||||||
|
heartbeatInterval and 0xFF, lockTimeout and 0xFF
|
||||||
|
)
|
||||||
|
sendCmd(resp)
|
||||||
|
}
|
||||||
|
CMD_UNLOCK -> {
|
||||||
|
val terminalIdHex = frame.terminalId.toAsciiHex()
|
||||||
|
val firmwareHex = frame.firmwareVersion.toAsciiHex()
|
||||||
|
sendCmd("${HEADER_HEX}020200${terminalIdHex}${firmwareHex}${FOOTER_HEX}")
|
||||||
|
}
|
||||||
|
CMD_CARD_UPLOAD -> sendCmd("${HEADER_HEX}050203${FOOTER_HEX}")
|
||||||
|
CMD_OPERATION_RECORD -> sendCmd("${HEADER_HEX}0B0203${FOOTER_HEX}")
|
||||||
|
CMD_TEMP_REPORT -> sendCmd("${HEADER_HEX}100200${FOOTER_HEX}")
|
||||||
|
CMD_POSITION_REPORT -> sendCmd("${HEADER_HEX}0E0200${FOOTER_HEX}")
|
||||||
|
CMD_TIME_SYNC -> {
|
||||||
|
val nowMs = System.currentTimeMillis()
|
||||||
|
val timeSec = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
|
||||||
|
val timeHex = "%08X".format(timeSec.toInt())
|
||||||
|
.chunked(2).reversed().joinToString("")
|
||||||
|
sendCmd("${HEADER_HEX}0C02${timeHex}${FOOTER_HEX}")
|
||||||
|
}
|
||||||
|
CMD_ACTIVATE -> {
|
||||||
|
if (activateCode.isNotBlank()) sendCmd(activateCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主线程:仅回调分发(不含 sendCmd 和日志)
|
||||||
|
*/
|
||||||
|
private fun invokeCallbacks(frame: ScaleFrame) {
|
||||||
|
when (frame.cmd) {
|
||||||
|
CMD_ACTIVATE -> {
|
||||||
|
activateListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_HEARTBEAT, CMD_HEARTBEAT_STORE -> {
|
||||||
|
val data = parser.parseHeartbeat(frame.rawBody)
|
||||||
|
if (data != null) {
|
||||||
|
heartbeatListeners.forEach { it(frame, data) }
|
||||||
|
}
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_UNLOCK -> {
|
||||||
|
lockListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_GET_STATUS -> {
|
||||||
|
val data = parser.parseStatus(frame.rawBody)
|
||||||
|
if (data != null) {
|
||||||
|
statusListeners.forEach { it(frame, data) }
|
||||||
|
}
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_CARD_UPLOAD -> {
|
||||||
|
cardListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_OPERATION_RECORD -> {
|
||||||
|
operationListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_TIME_SYNC -> {
|
||||||
|
timeSyncListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_POSITION_REPORT -> {
|
||||||
|
positionListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
CMD_TEMP_REPORT -> {
|
||||||
|
tempReportListeners.forEach { it(frame) }
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
rawListeners[frame.cmd]?.forEach { it(frame) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -443,164 +554,28 @@ object ScaleManager {
|
|||||||
rawListeners.clear()
|
rawListeners.clear()
|
||||||
logListeners.clear()
|
logListeners.clear()
|
||||||
activateCode = ""
|
activateCode = ""
|
||||||
|
lastHeartbeatLogSecond = 0
|
||||||
parser.reset()
|
parser.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// 内部命令分发与自动应答
|
// 日志与工具
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
private fun dispatch(frame: ScaleFrame, rawHex: String) {
|
/** 上次心跳日志时间(秒),用于降频:同一秒内不重复记录心跳日志 */
|
||||||
when (frame.cmd) {
|
private var lastHeartbeatLogSecond: Long = 0
|
||||||
CMD_ACTIVATE -> handleActivate(frame, rawHex)
|
|
||||||
CMD_HEARTBEAT,
|
|
||||||
CMD_HEARTBEAT_STORE -> handleHeartbeat(frame, rawHex)
|
|
||||||
CMD_UNLOCK -> handleLockFeedback(frame, rawHex)
|
|
||||||
CMD_GET_STATUS -> handleStatus(frame, rawHex)
|
|
||||||
CMD_CARD_UPLOAD -> handleCardUpload(frame, rawHex)
|
|
||||||
CMD_OPERATION_RECORD -> handleOperationRecord(frame, rawHex)
|
|
||||||
CMD_TIME_SYNC -> handleTimeSync(frame, rawHex)
|
|
||||||
CMD_POSITION_REPORT -> handlePositionReport(frame, rawHex)
|
|
||||||
CMD_TEMP_REPORT -> handleTempReport(frame, rawHex)
|
|
||||||
else -> {
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到未处理命令:0x%02X,终端ID:${frame.terminalId},原始数据:$rawHex".format(frame.cmd))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理终端激活(CMD=0x00)
|
* IO 线程:心跳日志降频 — 每秒最多记录一次
|
||||||
* 若 activateMap 中存在当前 deviceId 对应的激活码,则自动应答;
|
|
||||||
* 否则仍回调 onActivate,由业务方自行处理
|
|
||||||
*/
|
*/
|
||||||
private fun handleActivate(frame: ScaleFrame, rawHex: String) {
|
private fun logHeartbeatIfNeeded(frame: ScaleFrame, rawHex: String) {
|
||||||
log("收到[终端激活]指令,终端ID:${frame.terminalId},固件版本:${frame.firmwareVersion},原始数据:$rawHex")
|
val nowSec = System.currentTimeMillis() / 1000
|
||||||
val activeCmd = activateCode
|
if (nowSec == lastHeartbeatLogSecond) return
|
||||||
if (!activeCmd.isNullOrBlank()) {
|
lastHeartbeatLogSecond = nowSec
|
||||||
sendCmd(activeCmd)
|
|
||||||
log("激活码自动应答:$activeCmd")
|
|
||||||
}
|
|
||||||
activateListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理心跳(CMD=0x01 / 0x81)
|
|
||||||
* 自动应答:[心跳间隔] [上锁时间] [设备固件版本5字节]
|
|
||||||
*/
|
|
||||||
private fun handleHeartbeat(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val firmwareHex = frame.firmwareVersion.toAsciiHex()
|
|
||||||
val resp = "${HEADER_HEX}0102%02X%02X${firmwareHex}${FOOTER_HEX}".format(
|
|
||||||
heartbeatInterval and 0xFF,
|
|
||||||
lockTimeout and 0xFF
|
|
||||||
)
|
|
||||||
sendCmd(resp)
|
|
||||||
|
|
||||||
val data = parser.parseHeartbeat(frame.rawBody)
|
val data = parser.parseHeartbeat(frame.rawBody)
|
||||||
if (data != null) {
|
|
||||||
heartbeatListeners.forEach { it(frame, data) }
|
|
||||||
}
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[心跳]指令,终端ID:${frame.terminalId},秤数量:${data?.scaleCount ?: 0},原始数据:$rawHex")
|
log("收到[心跳]指令,终端ID:${frame.terminalId},秤数量:${data?.scaleCount ?: 0},原始数据:$rawHex")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理开锁反馈(CMD=0x02,终端发起,关门后上报)
|
|
||||||
* 自动应答:[锁号00] [终端ID 15字节] [固件版本 5字节]
|
|
||||||
*/
|
|
||||||
private fun handleLockFeedback(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val terminalIdHex = frame.terminalId.toAsciiHex()
|
|
||||||
val firmwareHex = frame.firmwareVersion.toAsciiHex()
|
|
||||||
val resp = "${HEADER_HEX}020200${terminalIdHex}${firmwareHex}${FOOTER_HEX}"
|
|
||||||
sendCmd(resp)
|
|
||||||
lockListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[开锁反馈]指令,终端ID:${frame.terminalId},原始数据:$rawHex")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理获取状态响应(CMD=0x03,终端响应)
|
|
||||||
* 无需应答,直接解析并分发
|
|
||||||
*/
|
|
||||||
private fun handleStatus(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val data = parser.parseStatus(frame.rawBody)
|
|
||||||
if (data != null) {
|
|
||||||
statusListeners.forEach { it(frame, data) }
|
|
||||||
}
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[获取状态]响应,终端ID:${frame.terminalId},秤数量:${data?.scaleCount ?: 0},原始数据:$rawHex")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理卡号上发(CMD=0x05,终端发起)
|
|
||||||
* 自动应答:成功 0x03
|
|
||||||
*/
|
|
||||||
private fun handleCardUpload(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val resp = "${HEADER_HEX}050203${FOOTER_HEX}"
|
|
||||||
sendCmd(resp)
|
|
||||||
cardListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[卡号上发]指令,终端ID:${frame.terminalId},原始数据:$rawHex")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理操作记录上传(CMD=0x0B,终端发起)
|
|
||||||
* 自动应答:成功 0x03
|
|
||||||
*/
|
|
||||||
private fun handleOperationRecord(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val resp = "${HEADER_HEX}0B0203${FOOTER_HEX}"
|
|
||||||
sendCmd(resp)
|
|
||||||
operationListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[操作记录上传]指令,终端ID:${frame.terminalId},原始数据:$rawHex")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理对时请求(CMD=0x0C,终端发起,仅以太网版本)
|
|
||||||
* 自动应答:当前 Unix 时间戳(含时区偏移),小端序4字节
|
|
||||||
*/
|
|
||||||
private fun handleTimeSync(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val nowMs = System.currentTimeMillis()
|
|
||||||
val timeSec = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
|
|
||||||
// 取低32位,转为小端序4字节十六进制
|
|
||||||
val timeHex = "%08X".format(timeSec.toInt())
|
|
||||||
.chunked(2).reversed().joinToString("")
|
|
||||||
val resp = "${HEADER_HEX}0C02${timeHex}${FOOTER_HEX}"
|
|
||||||
sendCmd(resp)
|
|
||||||
timeSyncListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[对时]请求,终端ID:${frame.terminalId},时间戳:${timeSec},应答:$resp")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理终端位置上报(CMD=0x0E,终端发起,仅4G版本)
|
|
||||||
* 自动应答:保留字节 0x00
|
|
||||||
*/
|
|
||||||
private fun handlePositionReport(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val resp = "${HEADER_HEX}0E0200${FOOTER_HEX}"
|
|
||||||
sendCmd(resp)
|
|
||||||
positionListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[终端位置上报]指令,终端ID:${frame.terminalId},原始数据:$rawHex")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理温控信息上报(CMD=0x10,终端发起)
|
|
||||||
* 自动应答:保留字节 0x00
|
|
||||||
*/
|
|
||||||
private fun handleTempReport(frame: ScaleFrame, rawHex: String) {
|
|
||||||
val resp = "${HEADER_HEX}100200${FOOTER_HEX}"
|
|
||||||
sendCmd(resp)
|
|
||||||
tempReportListeners.forEach { it(frame) }
|
|
||||||
rawListeners[frame.cmd]?.forEach { it(frame) }
|
|
||||||
log("收到[温控信息上报]指令,终端ID:${frame.terminalId},原始数据:$rawHex")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// 私有工具
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
|
|
||||||
private fun log(message: String) {
|
private fun log(message: String) {
|
||||||
logListeners.forEach { it(message) }
|
logListeners.forEach { it(message) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +1,91 @@
|
|||||||
package com.shuwei.intelligent.shelves.utils
|
package com.shuwei.intelligent.shelves.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.shuwei.intelligent.shelves.utils.CrashHandler.Companion.CRASH_REPORTS_DIR
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件日志记录器
|
* 文件日志记录器(异步版)
|
||||||
* 功能:保存日志到本地文件,每条数据一行,每天生成新文件
|
*
|
||||||
|
* log() 仅投递原始消息到 Channel(任意线程安全),
|
||||||
|
* 格式化与文件写入在单一后台协程顺序执行,避免 SimpleDateFormat 多线程竞争。
|
||||||
*/
|
*/
|
||||||
class FileLogger(private val context: Context) {
|
class FileLogger(context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_DIR = "logs"
|
|
||||||
private const val FILE_PREFIX = "log_"
|
private const val FILE_PREFIX = "log_"
|
||||||
private const val FILE_EXTENSION = ".txt"
|
private const val FILE_EXTENSION = ".txt"
|
||||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
|
||||||
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
/** 全局单线程日志写入 */
|
||||||
|
private val logChannel = Channel<RawEntry>(capacity = Channel.UNLIMITED)
|
||||||
|
private val writerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
init {
|
||||||
|
writerScope.launch {
|
||||||
|
// 单协程顺序消费:SimpleDateFormat 在此线程安全
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
for (entry in logChannel) {
|
||||||
|
try {
|
||||||
|
val fileName = "${FILE_PREFIX}${dateFormat.format(Date())}${FILE_EXTENSION}"
|
||||||
|
val timestamp = timeFormat.format(Date())
|
||||||
|
val logLine = "[$timestamp] [${entry.level}] ${entry.message}\n"
|
||||||
|
val output = entry.appContext.openFileOutput(
|
||||||
|
fileName, Context.MODE_APPEND
|
||||||
|
)
|
||||||
|
BufferedWriter(OutputStreamWriter(output)).use { it.write(logLine) }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// 写入失败静默丢弃
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class RawEntry(
|
||||||
|
val appContext: Context,
|
||||||
|
val message: String,
|
||||||
|
val level: LogLevel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 持有 ApplicationContext 避免 Activity 泄漏 */
|
||||||
|
private val appContext: Context = context.applicationContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录日志
|
* 记录日志(异步,线程安全)
|
||||||
* @param message 日志消息
|
|
||||||
* @param level 日志级别
|
|
||||||
*/
|
*/
|
||||||
fun log(message: String, level: LogLevel = LogLevel.INFO) {
|
fun log(message: String, level: LogLevel = LogLevel.INFO) {
|
||||||
try {
|
logChannel.trySend(RawEntry(appContext, message, level))
|
||||||
val logEntry = formatLogEntry(message, level)
|
|
||||||
writeToFile(logEntry)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化日志条目
|
|
||||||
*/
|
|
||||||
private fun formatLogEntry(message: String, level: LogLevel): String {
|
|
||||||
val timestamp = timeFormat.format(Date())
|
|
||||||
return "[$timestamp] [$level] $message\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写入文件
|
|
||||||
*/
|
|
||||||
private fun writeToFile(logEntry: String) {
|
|
||||||
// val path = File(context.filesDir, LOG_DIR)
|
|
||||||
// if (path.exists().not()) {
|
|
||||||
// path.mkdirs()
|
|
||||||
// }
|
|
||||||
val fileName = getCurrentDateFileName()
|
|
||||||
// val file = File(path, fileName)
|
|
||||||
// if (file.exists().not()) {
|
|
||||||
// file.createNewFile()
|
|
||||||
// }
|
|
||||||
val output = context.openFileOutput(fileName, Context.MODE_APPEND)
|
|
||||||
|
|
||||||
val writer = BufferedWriter(OutputStreamWriter(output))
|
|
||||||
writer.use {
|
|
||||||
it.write(logEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前日期对应的文件名
|
|
||||||
*/
|
|
||||||
private fun getCurrentDateFileName(): String {
|
|
||||||
val date = dateFormat.format(Date())
|
|
||||||
return "${FILE_PREFIX}${date}${FILE_EXTENSION}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取指定日期的日志文件
|
|
||||||
* @param date 日期字符串,格式:yyyy-MM-dd
|
|
||||||
*/
|
|
||||||
fun readLogsByDate(date: String): List<String> {
|
fun readLogsByDate(date: String): List<String> {
|
||||||
val fileName = "${FILE_PREFIX}${date}${FILE_EXTENSION}"
|
return readLogsByName("${FILE_PREFIX}${date}${FILE_EXTENSION}")
|
||||||
return readLogsByName(fileName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readLogsByName(fileName: String): List<String> {
|
fun readLogsByName(fileName: String): List<String> {
|
||||||
try {
|
try {
|
||||||
val input = context.openFileInput(fileName)
|
val input = appContext.openFileInput(fileName)
|
||||||
val reader = BufferedReader(InputStreamReader(input))
|
val reader = BufferedReader(InputStreamReader(input))
|
||||||
val list: MutableList<String> = reader.useLines { lines ->
|
return reader.useLines { lines -> lines.toList().toMutableList() }
|
||||||
lines.toList().toMutableList()
|
} catch (_: FileNotFoundException) {
|
||||||
}
|
|
||||||
|
|
||||||
// val input2 = FileInputStream(getCrashDir(context))
|
|
||||||
// val reader2 = BufferedReader(InputStreamReader(input2))
|
|
||||||
// val list2: MutableList<String> = reader2.useLines {lines ->
|
|
||||||
// lines.toList().toMutableList()
|
|
||||||
// }
|
|
||||||
// list.addAll(list2)
|
|
||||||
return list
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun getCrashDir(context: Context):File {
|
fun getCrashDir(context: Context): File {
|
||||||
//val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
|
var crashDir = File(context.filesDir, "crash_reports")
|
||||||
var crashDir = File(context.filesDir, CRASH_REPORTS_DIR)
|
if (!crashDir.exists()) crashDir.mkdirs()
|
||||||
if (!crashDir.exists()) {
|
if (!crashDir.exists()) crashDir = File(context.cacheDir, "crash_reports")
|
||||||
crashDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(crashDir.exists())) {
|
|
||||||
crashDir = File(context.cacheDir, CRASH_REPORTS_DIR)
|
|
||||||
}
|
|
||||||
return crashDir
|
return crashDir
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有日志文件列表
|
|
||||||
*/
|
|
||||||
fun getLogFiles(): List<String> {
|
fun getLogFiles(): List<String> {
|
||||||
return context.fileList()
|
return appContext.fileList()
|
||||||
.filter { it.startsWith(FILE_PREFIX) && it.endsWith(FILE_EXTENSION) }
|
.filter { it.startsWith(FILE_PREFIX) && it.endsWith(FILE_EXTENSION) }
|
||||||
//.map { it.replace(FILE_EXTENSION, "") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
enum class LogLevel { DEBUG, INFO, WARN, ERROR }
|
||||||
* 日志级别枚举
|
|
||||||
*/
|
|
||||||
enum class LogLevel {
|
|
||||||
DEBUG, INFO, WARN, ERROR
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ class HomeV3Activity : BaseActivity() {
|
|||||||
|
|
||||||
private val viewModel: NetViewModelV3 by viewModels()
|
private val viewModel: NetViewModelV3 by viewModels()
|
||||||
|
|
||||||
|
/** 记录每台秤的上次重量(克),用于跳过无变化的 UI 刷新和日志 */
|
||||||
|
private val lastWeightGrams = mutableMapOf<Int, Int>()
|
||||||
|
|
||||||
private fun initScaleManager() {
|
private fun initScaleManager() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val ok = ScaleManager
|
val ok = ScaleManager
|
||||||
@@ -81,6 +84,9 @@ class HomeV3Activity : BaseActivity() {
|
|||||||
.onHeartbeat { _, data ->
|
.onHeartbeat { _, data ->
|
||||||
data.weights.forEach { weight ->
|
data.weights.forEach { weight ->
|
||||||
val pos = scaleIndexToPosition[weight.index] ?: return@forEach
|
val pos = scaleIndexToPosition[weight.index] ?: return@forEach
|
||||||
|
// 重量不变则跳过 UI 刷新、日志和 EventBus,避免主线程无效计算
|
||||||
|
if (lastWeightGrams[weight.index] == weight.grams) return@forEach
|
||||||
|
lastWeightGrams[weight.index] = weight.grams
|
||||||
val model = list[pos]
|
val model = list[pos]
|
||||||
model.weight = weight.grams / 1000.0
|
model.weight = weight.grams / 1000.0
|
||||||
shelfAdapter.notifyItemChanged(pos)
|
shelfAdapter.notifyItemChanged(pos)
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ class ShelfV3Activity : BaseActivity() {
|
|||||||
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
|
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 缓存的日期格式化器,避免 onClockTick 每秒 new 对象 */
|
||||||
|
private val clockFormatter by lazy {
|
||||||
|
SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var binding: ActivityShelfV3Binding
|
private lateinit var binding: ActivityShelfV3Binding
|
||||||
|
|
||||||
private var shelfModel: SlotModel? = null
|
private var shelfModel: SlotModel? = null
|
||||||
@@ -327,8 +332,7 @@ class ShelfV3Activity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClockTick() {
|
override fun onClockTick() {
|
||||||
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
|
val arr = clockFormatter.format(Date()).split("***")
|
||||||
val arr = sdf.format(Date()).split("***")
|
|
||||||
updateLeftStatus(arr[0])
|
updateLeftStatus(arr[0])
|
||||||
updateRightStatus(arr[1])
|
updateRightStatus(arr[1])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ class ShelfV3Adapter(list: MutableList<SlotModel>) :
|
|||||||
// 格口编号
|
// 格口编号
|
||||||
binding.tvShelfName.text = item.slotNo.padStart(2, '0')
|
binding.tvShelfName.text = item.slotNo.padStart(2, '0')
|
||||||
|
|
||||||
// // 临期预警(slotStatus=2)显示过期图标
|
|
||||||
// if (item.slotStatus.toInt() == 2) binding.ivStaleFood.visible()
|
|
||||||
// else binding.ivStaleFood.gone()
|
|
||||||
|
|
||||||
val weightG = item.weight ?: 0.0
|
val weightG = item.weight ?: 0.0
|
||||||
binding.tvFoodWeight.run {
|
binding.tvFoodWeight.run {
|
||||||
setTextColor(getColor(R.color.food_weight_orange))
|
setTextColor(getColor(R.color.food_weight_orange))
|
||||||
|
|||||||
Reference in New Issue
Block a user