refactor(serial): 将 SerialPortManager 合并到 ScaleManager

- 删除 SerialPortManager,串口生命周期(open/startReceive/release)内嵌到 ScaleManager
- ScaleManager 改为 object 单例,去除 .instance 调用
- init() 简化为只接收 activateCode,去除 deviceId + activateMap 查表逻辑
- 新增 start(preHook) 方法合并 open + startReceive,支持链式调用到末尾
- BaseActivity.sendCmd 改为直接调用 ScaleManager.sendCmd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 14:59:19 +08:00
co-authored by Claude Sonnet 4.6
parent 0eede1059c
commit 4d545ceb46
5 changed files with 155 additions and 192 deletions
@@ -33,7 +33,6 @@ import com.shuwei.intelligent.shelves.serial.ProtocolConstants
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.START_TEMP_CTRL_CMD import com.shuwei.intelligent.shelves.serial.ProtocolConstants.START_TEMP_CTRL_CMD
import com.shuwei.intelligent.shelves.serial.ScaleManager import com.shuwei.intelligent.shelves.serial.ScaleManager
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.GridLayoutTool 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.ext.copyTextToClipboard import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
@@ -77,33 +76,34 @@ class HomeActivity : BaseActivity() {
// 串口协议解析器,通过回调通知本 Activity // 串口协议解析器,通过回调通知本 Activity
private fun initScaleManager() { private fun initScaleManager() {
ScaleManager.instance lifecycleScope.launch {
.init( val ok = ScaleManager
sendAction = { cmd -> .init(activateCode = ProtocolConstants.ACTIVE_MAP[App.deviceId] ?: "")
// ScaleManager 的自动应答(心跳/对时等)直接写串口,不经过 BaseActivity.sendCmd .onHeartbeat { _, data ->
lifecycleScope.launch { SerialPortManager.send(cmd) } data.weights.forEach { weight ->
}, val shelfNo = weight.index
deviceId = App.deviceId ?: "", if (shelfNo in 1..list.size) {
activateMap = ProtocolConstants.ACTIVE_MAP list.firstOrNull { it.deviceNo == shelfNo }?.let { model ->
) model.weight = weight.grams.toDouble()
.onHeartbeat { _, data -> val pos = list.indexOf(model)
data.weights.forEach { weight -> shelfAdapter.notifyItemChanged(pos)
val shelfNo = weight.index log("getWeightInfo: deviceNo=${model.deviceNo},realWeight=${weight.grams}")
if (shelfNo in 1..list.size) { EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight.grams))
list.firstOrNull { it.deviceNo == shelfNo }?.let { model -> }
model.weight = weight.grams.toDouble()
val pos = list.indexOf(model)
shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: deviceNo=${model.deviceNo},realWeight=${weight.grams}")
EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight.grams))
} }
} }
} }
} .onTempReport { _ ->
.onTempReport { _ -> updateLeftStatus(deviceName)
updateLeftStatus(deviceName) }
} .onLog { message -> log(message) }
.onLog { message -> log(message) } .start { data ->
lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
log("receiveSerialPortData: $data")
}
log("onCreate: openState=$ok")
}
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -119,28 +119,9 @@ class HomeActivity : BaseActivity() {
updateLeftStatus("") updateLeftStatus("")
initRecyclerView() initRecyclerView()
// 初始化 ScaleManager(串口初始化前注册回调) // 初始化 ScaleManager:注册回调、打开串口、启动接收
initScaleManager() initScaleManager()
// 串口初始化,独立协程,不阻塞网络状态监听
lifecycleScope.launch {
val openState = SerialPortManager.open()
log("onCreate: openState=$openState")
if (openState) {
SerialPortManager.startReceive { data ->
// 切换到主线程协程处理串口数据
lifecycleScope.launch {
runCatching { receiveSerialPortData(data) }
.onFailure {
it.printStackTrace()
log("串口数据处理异常: ${it.message}")
toast("串口数据处理异常,请检查设备连接")
}
}
}
}
}
// 网络状态监听,独立协程,与串口初始化并行执行 // 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -250,14 +231,6 @@ class HomeActivity : BaseActivity() {
loadEmptyView() loadEmptyView()
} }
private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData")
// 重置无数据计时,并关闭已显示的超时提示弹窗
lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
ScaleManager.instance.onDataReceived(srcData)
}
private var tipDialog: CommonDialog? = null private var tipDialog: CommonDialog? = null
/** 最后一次成功收到串口数据的时间戳(毫秒) */ /** 最后一次成功收到串口数据的时间戳(毫秒) */
@@ -309,8 +282,7 @@ class HomeActivity : BaseActivity() {
noDataWarningDialog?.dismiss() noDataWarningDialog?.dismiss()
noDataWarningDialog = null noDataWarningDialog = null
runBlocking { SerialPortManager.close() } runBlocking { ScaleManager.release() }
ScaleManager.instance.release()
saveTaskJob?.cancel() saveTaskJob?.cancel()
overdueTaskJob?.cancel() overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
@@ -333,10 +305,10 @@ class HomeActivity : BaseActivity() {
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)
ScaleManager.instance.buildOpenLock1Cmd() ScaleManager.buildOpenLock1Cmd()
else else
ScaleManager.instance.buildOpenLock2Cmd() ScaleManager.buildOpenLock2Cmd()
ScaleManager.instance.sendCmd(openCmd) ScaleManager.sendCmd(openCmd)
} }
launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { launch(Intent(this@HomeActivity, ShelfActivity::class.java).also {
it.putExtra(ShelfActivity.SHELF_MODEL, list[position]) it.putExtra(ShelfActivity.SHELF_MODEL, list[position])
@@ -146,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(ScaleManager.instance.buildGetStatusCmd()) sendCmd(ScaleManager.buildGetStatusCmd())
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.getGoodsListUiState.collect { state -> viewModel.getGoodsListUiState.collect { state ->
@@ -359,7 +359,7 @@ class ShelfActivity : BaseActivity() {
private fun clearZero() { private fun clearZero() {
lifecycleScope.launch { lifecycleScope.launch {
val shelfNo = shelfModel?.deviceNo ?: 0 val shelfNo = shelfModel?.deviceNo ?: 0
val zeroClearingCmd = ScaleManager.instance.buildScaleZeroCmd(shelfNo) val zeroClearingCmd = ScaleManager.buildScaleZeroCmd(shelfNo)
log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}") log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}")
sendCmd(zeroClearingCmd) sendCmd(zeroClearingCmd)
// sendCmd( // sendCmd(
@@ -369,7 +369,7 @@ class ShelfActivity : BaseActivity() {
realWeight = 0 realWeight = 0
// binding.tvFoodWeight.text = "${realWeight}克" // binding.tvFoodWeight.text = "${realWeight}克"
sendCmd(ScaleManager.instance.buildGetStatusCmd()) sendCmd(ScaleManager.buildGetStatusCmd())
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
Loading.show(this@ShelfActivity) Loading.show(this@ShelfActivity)
window.decorView.postDelayed({ Loading.dismiss() }, 10000) window.decorView.postDelayed({ Loading.dismiss() }, 10000)
@@ -12,14 +12,13 @@ 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 com.shuwei.intelligent.shelves.activity.LogActivity import com.shuwei.intelligent.shelves.activity.LogActivity
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.databinding.PopupRightStatusMenuBinding
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_CLOSE import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_CLOSE
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_OPEN import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_OPEN
import com.shuwei.intelligent.shelves.serial.SerialPortManager import com.shuwei.intelligent.shelves.serial.ScaleManager
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.dp
@@ -215,10 +214,8 @@ open class BaseActivity : AppCompatActivity() {
fun sendCmd(cmd: String) { fun sendCmd(cmd: String) {
lifecycleScope.launch { ScaleManager.sendCmd(cmd)
val result = SerialPortManager.send(cmd) log("发送命令: $cmd")
log("发送命令${cmd}结果:${result}")
}
} }
val fileLogger by lazy { FileLogger(this) } val fileLogger by lazy { FileLogger(this) }
@@ -12,59 +12,69 @@ import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.CMD_TIME_SYN
import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.CMD_UNLOCK import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.CMD_UNLOCK
import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.FOOTER_HEX import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.FOOTER_HEX
import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.HEADER_HEX import com.shuwei.intelligent.shelves.serial.ScaleProtocolConstants.HEADER_HEX
import com.shuwei.intelligent.shelves.utils.hexToBytes
import com.shuwei.intelligent.shelves.utils.toHexString2
import io.github.jeadyx.jserialport.AndroidSerialPort
import io.github.jeadyx.jserialport.SerialPort
import io.github.jeadyx.jserialport.SerialPortFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
/** /**
* 称重控制板协议管理器(SDK 入口) * 称重控制板协议管理器(SDK 入口)
* *
* 职责: * 职责:
* - 管理串口连接(open/startReceive/release
* - 接收串口原始分片数据,自动拼装帧并分发到对应回调 * - 接收串口原始分片数据,自动拼装帧并分发到对应回调
* - 内置各命令的自动应答逻辑(心跳/对时/卡号等) * - 内置各命令的自动应答逻辑(心跳/对时/卡号等)
* - 提供链式回调注册、常用指令构建、资源释放 * - 提供链式回调注册、常用指令构建、资源释放
* *
* 设计原则:
* - 不依赖具体串口实现,通过 init(sendAction) 注入发送函数
* - 不依赖任何业务类,可直接复制到其他项目使用
* - 非线程安全,建议在同一线程(如串口IO线程或主线程)中使用
*
* 典型用法: * 典型用法:
* ```kotlin * ```kotlin
* // 初始化Activity/Application中) * // 初始化并注册回调
* ScaleManager.instance * ScaleManager
* .init { hexStr -> scope.launch { SerialPortManager.send(hexStr) } } * .init(deviceId = App.deviceId, activateMap = ACTIVE_MAP)
* .onHeartbeat { frame, data -> * .onHeartbeat { frame, data ->
* data.weights.forEach { Log.d("Scale", "秤${it.index}: ${it.grams}g") } * data.weights.forEach { Log.d("Scale", "秤${it.index}: ${it.grams}g") }
* } * }
* .onStatus { frame, data ->
* Log.d("Scale", "锁1状态: ${data.lock1Lock}")
* }
* .onLog { msg -> Log.d("Scale", msg) } * .onLog { msg -> Log.d("Scale", msg) }
* *
* // 在串口数据回调中调用 * // 打开串口并启动接收
* ScaleManager.instance.onDataReceived(hexData) * lifecycleScope.launch {
* if (ScaleManager.open()) {
* ScaleManager.startReceive { data ->
* // 主线程前置钩子,处理看门狗等副作用
* }
* }
* }
* *
* // 主动发送指令 * // 主动发送指令
* ScaleManager.instance.sendCmd(ScaleManager.instance.buildGetStatusCmd()) * ScaleManager.sendCmd(ScaleManager.buildGetStatusCmd())
* *
* // 页面销毁时释放回调(不影响其他页面注册的回调,若需完全释放则调用 release()) * // 页面销毁时释放
* runBlocking { ScaleManager.release() }
* ``` * ```
*/ */
class ScaleManager { object ScaleManager {
companion object { /** 串口端口名,默认 /dev/ttyS1 */
/** 默认单例,适用于单设备场景;多设备场景请自行实例化 */ var portName: String = "/dev/ttyS1"
@JvmStatic /** 串口波特率,默认 115200 */
val instance: ScaleManager by lazy { ScaleManager() } var baudRate: Int = 115200
}
private val parser = ScaleFrameParser() private val parser = ScaleFrameParser()
private var sendAction: ((String) -> Unit)? = null private var serialPort: AndroidSerialPort? = null
private var scope = CoroutineScope(Dispatchers.IO)
/** 激活码映射表:key=deviceId,value=完整激活应答帧十六进制字符串 */ /** 当前设备激活码,非空时收到激活请求自动应答 */
private var activateMap: Map<String, String> = emptyMap() private var activateCode: String = ""
/** 当前设备ID,用于激活码查表 */
private var currentDeviceId: String = ""
/** 心跳应答:心跳上报间隔(秒),默认5秒 */ /** 心跳应答:心跳上报间隔(秒),默认5秒 */
var heartbeatInterval: Int = 5 var heartbeatInterval: Int = 5
@@ -91,25 +101,78 @@ class ScaleManager {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
/** /**
* 初始化,注入串口发送函数与激活码映射表 * 初始化,设置设备激活码
* *
* @param sendAction 发送十六进制字符串指令的函数,在该函数内完成实际串口写入 * @param activateCode 当前设备的激活应答帧十六进制字符串,非空时收到激活请求自动应答
* @param deviceId 当前设备ID,用于激活码查表
* @param activateMap 激活码映射表:key=deviceId,value=完整激活应答帧十六进制字符串;
* 传入后终端激活请求将由 SDK 内部自动应答,无需再注册 onActivate 回调
* @return this,支持链式调用 * @return this,支持链式调用
*/ */
fun init( fun init(activateCode: String = ""): ScaleManager {
sendAction: (String) -> Unit, this.activateCode = activateCode
deviceId: String = "",
activateMap: Map<String, String> = emptyMap()
): ScaleManager {
this.sendAction = sendAction
this.currentDeviceId = deviceId
this.activateMap = activateMap
return this return this
} }
// ----------------------------------------------------------------
// 串口生命周期
// ----------------------------------------------------------------
/**
* 打开串口并启动接收,在回调注册完成后调用(链式末尾)
*
* @param preHook 每帧数据到达时的前置钩子(主线程),用于看门狗计时、UI副作用等
* @return true=串口打开成功并已启动接收,false=串口打开失败
*/
suspend fun start(preHook: ((String) -> Unit)? = null): Boolean {
if (!openPort()) return false
startReceive(preHook)
return true
}
private suspend fun openPort(): Boolean {
if (!scope.isActive) {
scope = CoroutineScope(Dispatchers.IO)
}
return try {
serialPort = SerialPortFactory.create().apply {
open(
portName = portName,
baudRate = baudRate,
dataBits = 8,
stopBits = 1,
parity = SerialPort.PARITY_NONE
)
} as AndroidSerialPort?
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
private fun startReceive(preHook: ((String) -> Unit)? = null) {
scope.launch {
while (isActive) {
try {
val flow = serialPort?.read()
flow?.collect { buffer ->
val data = buffer.toHexString2().uppercase(Locale.ROOT)
withContext(Dispatchers.Main) {
try {
preHook?.invoke(data)
onDataReceived(data)
} catch (e: Exception) {
e.printStackTrace()
log("串口数据处理异常: ${e.message}")
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
delay(50)
}
}
}
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 数据接收入口 // 数据接收入口
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -243,11 +306,18 @@ class ScaleManager {
* @param hexStr 完整帧十六进制字符串,含协议头尾 * @param hexStr 完整帧十六进制字符串,含协议头尾
*/ */
fun sendCmd(hexStr: String) { fun sendCmd(hexStr: String) {
val action = sendAction ?: run { if (!scope.isActive) {
log("警告:ScaleManager 尚未初始化,请先调用 init(sendAction)") log("警告:ScaleManager 串口未打开或已释放")
return return
} }
action(hexStr) scope.launch {
try {
serialPort?.write(hexStr.hexToBytes())
} catch (e: Exception) {
e.printStackTrace()
log("发送命令失败: ${e.message}")
}
}
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -352,7 +422,10 @@ class ScaleManager {
* 释放所有资源:清空回调列表、发送函数及帧缓冲 * 释放所有资源:清空回调列表、发送函数及帧缓冲
* 通常在 Activity/Fragment 销毁或不再使用时调用 * 通常在 Activity/Fragment 销毁或不再使用时调用
*/ */
fun release() { suspend fun release() {
scope.cancel()
serialPort?.close()
serialPort = null
heartbeatListeners.clear() heartbeatListeners.clear()
statusListeners.clear() statusListeners.clear()
lockListeners.clear() lockListeners.clear()
@@ -364,9 +437,7 @@ class ScaleManager {
activateListeners.clear() activateListeners.clear()
rawListeners.clear() rawListeners.clear()
logListeners.clear() logListeners.clear()
sendAction = null activateCode = ""
activateMap = emptyMap()
currentDeviceId = ""
parser.reset() parser.reset()
} }
@@ -400,7 +471,7 @@ class ScaleManager {
*/ */
private fun handleActivate(frame: ScaleFrame, rawHex: String) { private fun handleActivate(frame: ScaleFrame, rawHex: String) {
log("收到[终端激活]指令,终端ID${frame.terminalId},固件版本:${frame.firmwareVersion},原始数据:$rawHex") log("收到[终端激活]指令,终端ID${frame.terminalId},固件版本:${frame.firmwareVersion},原始数据:$rawHex")
val activeCmd = activateMap[currentDeviceId] val activeCmd = activateCode
if (!activeCmd.isNullOrBlank()) { if (!activeCmd.isNullOrBlank()) {
sendCmd(activeCmd) sendCmd(activeCmd)
log("激活码自动应答:$activeCmd") log("激活码自动应答:$activeCmd")
@@ -1,77 +0,0 @@
package com.shuwei.intelligent.shelves.serial
import android.annotation.SuppressLint
import androidx.lifecycle.lifecycleScope
import com.shuwei.intelligent.shelves.utils.hexToBytes
import com.shuwei.intelligent.shelves.utils.toHexString2
import io.github.jeadyx.jserialport.AndroidSerialPort
import io.github.jeadyx.jserialport.SerialPort
import io.github.jeadyx.jserialport.SerialPortFactory
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import java.nio.charset.Charset
import java.util.Locale
object SerialPortManager {
private const val PORT_NAME: String = "/dev/ttyS1"
private const val BAUD_RATE: Int = 115200
private var serialPort: AndroidSerialPort? = null
// private var serialPort: SerialPort? = null
private val scope = CoroutineScope(Dispatchers.IO)
// 打开串口
suspend fun open(): Boolean {
return try {
serialPort = SerialPortFactory.create().apply {
open(
portName = PORT_NAME,
baudRate = BAUD_RATE,
dataBits = 8,
stopBits = 1,
parity = SerialPort.PARITY_NONE
)
} as AndroidSerialPort?
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
// 发送数据
suspend fun send(data: String): Boolean {
return try {
serialPort?.write(data.hexToBytes())
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
// 启动接收协程
fun startReceive(callback: (String) -> Unit) {
scope.launch {
while (isActive) {
try {
val flow = serialPort?.read()
flow?.collect { buffer ->
val data = buffer.toHexString2().uppercase(Locale.ROOT)
callback(data)
}
} catch (e: Exception) {
e.printStackTrace()
}
delay(50)
}
}
}
// 关闭串口
suspend fun close() {
scope.cancel()
serialPort?.close()
}
}