From 4d545ceb4683cda500f12af95f390cd2fec2e491 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Thu, 21 May 2026 14:59:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(serial):=20=E5=B0=86=20SerialPortManag?= =?UTF-8?q?er=20=E5=90=88=E5=B9=B6=E5=88=B0=20ScaleManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 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 --- .../shelves/activity/HomeActivity.kt | 88 ++++----- .../shelves/activity/ShelfActivity.kt | 6 +- .../intelligent/shelves/base/BaseActivity.kt | 9 +- .../shelves/serial/ScaleManager.kt | 167 +++++++++++++----- .../shelves/serial/SerialPortManager.kt | 77 -------- 5 files changed, 155 insertions(+), 192 deletions(-) delete mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt index 0a042cd..399e14f 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt @@ -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.START_TEMP_CTRL_CMD 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.IntervalExecutor import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard @@ -77,33 +76,34 @@ class HomeActivity : BaseActivity() { // 串口协议解析器,通过回调通知本 Activity private fun initScaleManager() { - ScaleManager.instance - .init( - sendAction = { cmd -> - // ScaleManager 的自动应答(心跳/对时等)直接写串口,不经过 BaseActivity.sendCmd - lifecycleScope.launch { SerialPortManager.send(cmd) } - }, - deviceId = App.deviceId ?: "", - activateMap = ProtocolConstants.ACTIVE_MAP - ) - .onHeartbeat { _, data -> - data.weights.forEach { weight -> - val shelfNo = weight.index - if (shelfNo in 1..list.size) { - 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)) + lifecycleScope.launch { + val ok = ScaleManager + .init(activateCode = ProtocolConstants.ACTIVE_MAP[App.deviceId] ?: "") + .onHeartbeat { _, data -> + data.weights.forEach { weight -> + val shelfNo = weight.index + if (shelfNo in 1..list.size) { + 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 { _ -> - updateLeftStatus(deviceName) - } - .onLog { message -> log(message) } + .onTempReport { _ -> + updateLeftStatus(deviceName) + } + .onLog { message -> log(message) } + .start { data -> + lastDataReceivedTime = System.currentTimeMillis() + noDataWarningDialog?.takeIf { it.isShowing }?.dismiss() + log("receiveSerialPortData: $data") + } + log("onCreate: openState=$ok") + } } @SuppressLint("NotifyDataSetChanged") @@ -119,28 +119,9 @@ class HomeActivity : BaseActivity() { updateLeftStatus("") initRecyclerView() - // 初始化 ScaleManager(串口初始化前注册回调) + // 初始化 ScaleManager:注册回调、打开串口、启动接收 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 { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -250,14 +231,6 @@ class HomeActivity : BaseActivity() { 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 /** 最后一次成功收到串口数据的时间戳(毫秒) */ @@ -309,8 +282,7 @@ class HomeActivity : BaseActivity() { noDataWarningDialog?.dismiss() noDataWarningDialog = null - runBlocking { SerialPortManager.close() } - ScaleManager.instance.release() + runBlocking { ScaleManager.release() } saveTaskJob?.cancel() overdueTaskJob?.cancel() EventBus.getDefault().unregister(this) @@ -333,10 +305,10 @@ class HomeActivity : BaseActivity() { lifecycleScope.launch { if (isNeedOpenLock()) { val openCmd = if (list[position].deviceNo in 1..5) - ScaleManager.instance.buildOpenLock1Cmd() + ScaleManager.buildOpenLock1Cmd() else - ScaleManager.instance.buildOpenLock2Cmd() - ScaleManager.instance.sendCmd(openCmd) + ScaleManager.buildOpenLock2Cmd() + ScaleManager.sendCmd(openCmd) } launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { it.putExtra(ShelfActivity.SHELF_MODEL, list[position]) diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfActivity.kt index 778ab0d..3be5c18 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfActivity.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/activity/ShelfActivity.kt @@ -146,7 +146,7 @@ class ShelfActivity : BaseActivity() { //binding.tvFoodWeight.text = "0克" EventBus.getDefault().post(ClearShelfEvent(shelfModel!!.deviceNo, tempWeight)) } - sendCmd(ScaleManager.instance.buildGetStatusCmd()) + sendCmd(ScaleManager.buildGetStatusCmd()) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.getGoodsListUiState.collect { state -> @@ -359,7 +359,7 @@ class ShelfActivity : BaseActivity() { private fun clearZero() { lifecycleScope.launch { val shelfNo = shelfModel?.deviceNo ?: 0 - val zeroClearingCmd = ScaleManager.instance.buildScaleZeroCmd(shelfNo) + val zeroClearingCmd = ScaleManager.buildScaleZeroCmd(shelfNo) log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}") sendCmd(zeroClearingCmd) // sendCmd( @@ -369,7 +369,7 @@ class ShelfActivity : BaseActivity() { realWeight = 0 // binding.tvFoodWeight.text = "${realWeight}克" - sendCmd(ScaleManager.instance.buildGetStatusCmd()) + sendCmd(ScaleManager.buildGetStatusCmd()) startTime = System.currentTimeMillis() Loading.show(this@ShelfActivity) window.decorView.postDelayed({ Loading.dismiss() }, 10000) diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt index 5b624f5..930004b 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/base/BaseActivity.kt @@ -12,14 +12,13 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope import com.shuwei.intelligent.shelves.activity.LogActivity import com.shuwei.intelligent.shelves.R import com.shuwei.intelligent.shelves.databinding.ActivityBaseBinding import com.shuwei.intelligent.shelves.databinding.PopupRightStatusMenuBinding import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_CLOSE import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_OPEN -import com.shuwei.intelligent.shelves.serial.SerialPortManager +import com.shuwei.intelligent.shelves.serial.ScaleManager import com.shuwei.intelligent.shelves.utils.FileLogger import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce import com.shuwei.intelligent.shelves.utils.ext.dp @@ -215,10 +214,8 @@ open class BaseActivity : AppCompatActivity() { fun sendCmd(cmd: String) { - lifecycleScope.launch { - val result = SerialPortManager.send(cmd) - log("发送命令${cmd}结果:${result}") - } + ScaleManager.sendCmd(cmd) + log("发送命令: $cmd") } val fileLogger by lazy { FileLogger(this) } diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleManager.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleManager.kt index d64f3b8..7e2b240 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleManager.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/serial/ScaleManager.kt @@ -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.FOOTER_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 /** * 称重控制板协议管理器(SDK 入口) * * 职责: + * - 管理串口连接(open/startReceive/release) * - 接收串口原始分片数据,自动拼装帧并分发到对应回调 * - 内置各命令的自动应答逻辑(心跳/对时/卡号等) * - 提供链式回调注册、常用指令构建、资源释放 * - * 设计原则: - * - 不依赖具体串口实现,通过 init(sendAction) 注入发送函数 - * - 不依赖任何业务类,可直接复制到其他项目使用 - * - 非线程安全,建议在同一线程(如串口IO线程或主线程)中使用 - * * 典型用法: * ```kotlin - * // 初始化(Activity/Application中) - * ScaleManager.instance - * .init { hexStr -> scope.launch { SerialPortManager.send(hexStr) } } + * // 初始化并注册回调 + * ScaleManager + * .init(deviceId = App.deviceId, activateMap = ACTIVE_MAP) * .onHeartbeat { frame, data -> * 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) } * - * // 在串口数据回调中调用 - * 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 { - /** 默认单例,适用于单设备场景;多设备场景请自行实例化 */ - @JvmStatic - val instance: ScaleManager by lazy { ScaleManager() } - } + /** 串口端口名,默认 /dev/ttyS1 */ + var portName: String = "/dev/ttyS1" + /** 串口波特率,默认 115200 */ + var baudRate: Int = 115200 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 = emptyMap() - - /** 当前设备ID,用于激活码查表 */ - private var currentDeviceId: String = "" + /** 当前设备激活码,非空时收到激活请求自动应答 */ + private var activateCode: String = "" /** 心跳应答:心跳上报间隔(秒),默认5秒 */ var heartbeatInterval: Int = 5 @@ -91,25 +101,78 @@ class ScaleManager { // ---------------------------------------------------------------- /** - * 初始化,注入串口发送函数与激活码映射表 + * 初始化,设置设备激活码 * - * @param sendAction 发送十六进制字符串指令的函数,在该函数内完成实际串口写入 - * @param deviceId 当前设备ID,用于激活码查表 - * @param activateMap 激活码映射表:key=deviceId,value=完整激活应答帧十六进制字符串; - * 传入后终端激活请求将由 SDK 内部自动应答,无需再注册 onActivate 回调 + * @param activateCode 当前设备的激活应答帧十六进制字符串,非空时收到激活请求自动应答 * @return this,支持链式调用 */ - fun init( - sendAction: (String) -> Unit, - deviceId: String = "", - activateMap: Map = emptyMap() - ): ScaleManager { - this.sendAction = sendAction - this.currentDeviceId = deviceId - this.activateMap = activateMap + fun init(activateCode: String = ""): ScaleManager { + this.activateCode = activateCode 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 完整帧十六进制字符串,含协议头尾 */ fun sendCmd(hexStr: String) { - val action = sendAction ?: run { - log("警告:ScaleManager 尚未初始化,请先调用 init(sendAction)") + if (!scope.isActive) { + log("警告:ScaleManager 串口未打开或已释放") 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 销毁或不再使用时调用 */ - fun release() { + suspend fun release() { + scope.cancel() + serialPort?.close() + serialPort = null heartbeatListeners.clear() statusListeners.clear() lockListeners.clear() @@ -364,9 +437,7 @@ class ScaleManager { activateListeners.clear() rawListeners.clear() logListeners.clear() - sendAction = null - activateMap = emptyMap() - currentDeviceId = "" + activateCode = "" parser.reset() } @@ -400,7 +471,7 @@ class ScaleManager { */ private fun handleActivate(frame: ScaleFrame, rawHex: String) { log("收到[终端激活]指令,终端ID:${frame.terminalId},固件版本:${frame.firmwareVersion},原始数据:$rawHex") - val activeCmd = activateMap[currentDeviceId] + val activeCmd = activateCode if (!activeCmd.isNullOrBlank()) { sendCmd(activeCmd) log("激活码自动应答:$activeCmd") diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt b/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt deleted file mode 100644 index bdeaa23..0000000 --- a/app/src/main/java/com/shuwei/intelligent/shelves/serial/SerialPortManager.kt +++ /dev/null @@ -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() - } -}