refactor(scale): 优化串口数据处理逻辑和自动应答机制
- 在串口数据读取中增加帧拼装与解析,统一自动应答发送 - 实现心跳日志降频,减少重复日志,避免主线程无效计算 - 将数据回调分派至主线程,确保线程安全 - 缓存时间格式化器,避免重复创建对象 - 增加秤重数据变更判断,避免无变化时刷新UI和日志 - 异步优化文件日志写入,使用协程通道和单线程写入,避免多线程SimpleDateFormat竞争 - 修复文件路径和上下文使用,防止Activity内存泄漏 - 精简部分注释和代码未使用段落,提高代码清晰度
This commit is contained in:
@@ -166,10 +166,23 @@ object ScaleManager {
|
||||
val flow = serialPort?.read()
|
||||
flow?.collect { buffer ->
|
||||
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) {
|
||||
try {
|
||||
preHook?.invoke(data)
|
||||
onDataReceived(data)
|
||||
if (parsed != null) {
|
||||
invokeCallbacks(parsed.first)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
log("串口数据处理异常: ${e.message}")
|
||||
@@ -179,7 +192,7 @@ object ScaleManager {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
delay(50)
|
||||
delay(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,19 +202,117 @@ object ScaleManager {
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 接收串口原始数据(在串口回调中调用此方法)
|
||||
* 接收串口原始数据(公开 API,主线程安全)
|
||||
*
|
||||
* 内部自动完成分片拼装 → 帧解析 → 命令分发 → 自动应答
|
||||
* 内部自动完成分片拼装 → 帧解析 → 自动应答 → 回调分发
|
||||
*
|
||||
* @param hexData 串口收到的十六进制字符串(可能为分片数据)
|
||||
*/
|
||||
fun onDataReceived(hexData: String) {
|
||||
val completeFrame = parser.appendData(hexData) ?: return
|
||||
val frame = parser.parseFrame(completeFrame) ?: run {
|
||||
val parsed = tryParseCompleteFrame(hexData) ?: return
|
||||
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")
|
||||
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()
|
||||
logListeners.clear()
|
||||
activateCode = ""
|
||||
lastHeartbeatLogSecond = 0
|
||||
parser.reset()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 内部命令分发与自动应答
|
||||
// 日志与工具
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private fun dispatch(frame: ScaleFrame, rawHex: String) {
|
||||
when (frame.cmd) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 上次心跳日志时间(秒),用于降频:同一秒内不重复记录心跳日志 */
|
||||
private var lastHeartbeatLogSecond: Long = 0
|
||||
|
||||
/**
|
||||
* 处理终端激活(CMD=0x00)
|
||||
* 若 activateMap 中存在当前 deviceId 对应的激活码,则自动应答;
|
||||
* 否则仍回调 onActivate,由业务方自行处理
|
||||
* IO 线程:心跳日志降频 — 每秒最多记录一次
|
||||
*/
|
||||
private fun handleActivate(frame: ScaleFrame, rawHex: String) {
|
||||
log("收到[终端激活]指令,终端ID:${frame.terminalId},固件版本:${frame.firmwareVersion},原始数据:$rawHex")
|
||||
val activeCmd = activateCode
|
||||
if (!activeCmd.isNullOrBlank()) {
|
||||
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)
|
||||
|
||||
private fun logHeartbeatIfNeeded(frame: ScaleFrame, rawHex: String) {
|
||||
val nowSec = System.currentTimeMillis() / 1000
|
||||
if (nowSec == lastHeartbeatLogSecond) return
|
||||
lastHeartbeatLogSecond = nowSec
|
||||
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")
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理开锁反馈(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) {
|
||||
logListeners.forEach { it(message) }
|
||||
}
|
||||
|
||||
@@ -1,131 +1,91 @@
|
||||
package com.shuwei.intelligent.shelves.utils
|
||||
|
||||
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.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* 文件日志记录器
|
||||
* 功能:保存日志到本地文件,每条数据一行,每天生成新文件
|
||||
* 文件日志记录器(异步版)
|
||||
*
|
||||
* log() 仅投递原始消息到 Channel(任意线程安全),
|
||||
* 格式化与文件写入在单一后台协程顺序执行,避免 SimpleDateFormat 多线程竞争。
|
||||
*/
|
||||
class FileLogger(private val context: Context) {
|
||||
class FileLogger(context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val LOG_DIR = "logs"
|
||||
private const val FILE_PREFIX = "log_"
|
||||
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) {
|
||||
try {
|
||||
val logEntry = formatLogEntry(message, level)
|
||||
writeToFile(logEntry)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
logChannel.trySend(RawEntry(appContext, message, level))
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日志条目
|
||||
*/
|
||||
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> {
|
||||
val fileName = "${FILE_PREFIX}${date}${FILE_EXTENSION}"
|
||||
return readLogsByName(fileName)
|
||||
return readLogsByName("${FILE_PREFIX}${date}${FILE_EXTENSION}")
|
||||
}
|
||||
|
||||
fun readLogsByName(fileName: String): List<String> {
|
||||
try {
|
||||
val input = context.openFileInput(fileName)
|
||||
val input = appContext.openFileInput(fileName)
|
||||
val reader = BufferedReader(InputStreamReader(input))
|
||||
val list: MutableList<String> = reader.useLines { lines ->
|
||||
lines.toList().toMutableList()
|
||||
}
|
||||
|
||||
// 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 reader.useLines { lines -> lines.toList().toMutableList() }
|
||||
} catch (_: FileNotFoundException) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
public fun getCrashDir(context: Context):File {
|
||||
//val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
|
||||
var crashDir = File(context.filesDir, CRASH_REPORTS_DIR)
|
||||
if (!crashDir.exists()) {
|
||||
crashDir.mkdirs()
|
||||
}
|
||||
|
||||
if (!(crashDir.exists())) {
|
||||
crashDir = File(context.cacheDir, CRASH_REPORTS_DIR)
|
||||
}
|
||||
fun getCrashDir(context: Context): File {
|
||||
var crashDir = File(context.filesDir, "crash_reports")
|
||||
if (!crashDir.exists()) crashDir.mkdirs()
|
||||
if (!crashDir.exists()) crashDir = File(context.cacheDir, "crash_reports")
|
||||
return crashDir
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有日志文件列表
|
||||
*/
|
||||
fun getLogFiles(): List<String> {
|
||||
return context.fileList()
|
||||
return appContext.fileList()
|
||||
.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()
|
||||
|
||||
/** 记录每台秤的上次重量(克),用于跳过无变化的 UI 刷新和日志 */
|
||||
private val lastWeightGrams = mutableMapOf<Int, Int>()
|
||||
|
||||
private fun initScaleManager() {
|
||||
lifecycleScope.launch {
|
||||
val ok = ScaleManager
|
||||
@@ -81,6 +84,9 @@ class HomeV3Activity : BaseActivity() {
|
||||
.onHeartbeat { _, data ->
|
||||
data.weights.forEach { weight ->
|
||||
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]
|
||||
model.weight = weight.grams / 1000.0
|
||||
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"
|
||||
}
|
||||
|
||||
/** 缓存的日期格式化器,避免 onClockTick 每秒 new 对象 */
|
||||
private val clockFormatter by lazy {
|
||||
SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityShelfV3Binding
|
||||
|
||||
private var shelfModel: SlotModel? = null
|
||||
@@ -327,8 +332,7 @@ class ShelfV3Activity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onClockTick() {
|
||||
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
|
||||
val arr = sdf.format(Date()).split("***")
|
||||
val arr = clockFormatter.format(Date()).split("***")
|
||||
updateLeftStatus(arr[0])
|
||||
updateRightStatus(arr[1])
|
||||
}
|
||||
|
||||
@@ -42,10 +42,6 @@ class ShelfV3Adapter(list: MutableList<SlotModel>) :
|
||||
// 格口编号
|
||||
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
|
||||
binding.tvFoodWeight.run {
|
||||
setTextColor(getColor(R.color.food_weight_orange))
|
||||
|
||||
Reference in New Issue
Block a user