Author SHA1 Message Date
lvmengandClaude Sonnet 4.6 9a795b9710 feat(HomeActivity): 新增串口无数据超时提示弹窗
超过5分钟未收到串口数据时,自动弹窗提示用户考虑断电重启设备;
收到新数据后自动关闭弹窗并重置计时。
同步提交串口协议常量(ProtocolConstants)与帧解析器(SerialFrameParser)重构代码。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 10:18:00 +08:00
mazengfei fff6a2a3b3 配置生产域名 2026-03-31 19:16:56 +08:00
lvmeng 3b79f6d6aa feat(config): 实现应用基础URL配置管理与环境切换功能
- 引入GlobalData对象统一管理应用基础URL配置
- 添加TEST/UAT/PROD三套环境切换弹窗功能
- 在BaseActivity中集成双击右上角状态栏显示环境切换对话框
- 将网络请求配置从App.configUrl迁移至GlobalData.appBaseUrl
- 扩展SpTool工具类支持baseUrl本地存储
- 增加HTTP连接和写入超时时间至30秒
- 更新构建配置启用BuildConfig支持
- 添加环境切换相关UI资源文件和样式定义
2026-03-31 14:06:04 +08:00
lvmeng 76b67b545d Merge remote-tracking branch 'origin/dev_优化_基于分支代码备份' 2026-03-31 10:58:32 +08:00
lvmeng 3bd60990b9 Revert "重构 HomeActivity:修复多项稳定性与架构问题"
This reverts commit 85903eb646.
2026-03-31 10:57:46 +08:00
lvmeng a116c4fc6c Revert "重构:拆分 HomeActivity 职责,分层解耦串口与业务逻辑"
This reverts commit e6cfc4dff2.
2026-03-31 10:57:40 +08:00
lvmengandClaude Sonnet 4.6 e6cfc4dff2 重构:拆分 HomeActivity 职责,分层解耦串口与业务逻辑
- 新建 SerialEventListener:定义串口协议解析回调接口契约
- 新建 SerialProtocolParser:迁移全部协议解析逻辑(拼包、12种命令分发、重量解析),无 Android 依赖,可单元测试
- 新建 SerialViewModel:管理串口生命周期(open/close)、Channel 数据传递,暴露 weightFlow / tempCtrlEventFlow / deviceStatusRefreshFlow
- 扩展 HomeViewModel:迁入定时任务(保存货架数据、刷新过期状态),改用 viewModelScope 管理,新增 deviceName 属性与 overdueUpdateFlow
- 精简 HomeActivity:从 835 行削减至 255 行,仅保留 UI 协调职责,所有串口与业务逻辑由 ViewModel 层处理

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 13:41:01 +08:00
lvmengandClaude Sonnet 4.6 85903eb646 重构 HomeActivity:修复多项稳定性与架构问题
架构优化:
- 新增 HomeViewModel,将 companion object 中的共享状态(shelfItems、
  weightMap、selectedShelfIndex、temperatureC、humidity)迁移至 ViewModel,
  消除 Activity 重建时的状态残留与全局状态污染问题

协程修复:
- repeatOnLifecycle(CREATED) 改为 STARTED,确保 Flow 收集与 UI 可见性
  生命周期对齐,进入后台时自动停止订阅
- 串口数据接收改用 Channel<String> + 单一消费协程,替代多 launch 并发模式,
  彻底消除 StringBuilder(serialReader)的并发竞争问题

稳定性修复:
- 修复 tipDialog 从未赋值导致 Dialog 功能失效及 onDestroy 窗口泄漏问题
- 修复 weightHex.toInt() 十进制解析错误,改为 toIntOrNull(16) 并添加兜底
- 修复 updateOverdueState 按索引访问接口数据导致的越界风险,
  改为按 deviceNo 精确匹配,同时修正交叉排列导致的状态串位问题
- 废弃 API getSerializableExtra 替换为版本分支兼容写法(minSdk 25+)

依赖升级:
- androidx.core-ktx 1.10.1 → 1.12.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:45:44 +08:00
17 changed files with 562 additions and 320 deletions
+1
View File
@@ -44,6 +44,7 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
buildConfig = true
} }
ndkVersion = "29.0.13846066" ndkVersion = "29.0.13846066"
@@ -0,0 +1,38 @@
package com.shuwei.intelligent.shelves
object GlobalData {
// /**
// * 设备id
// */
// var deviceId: String = ""
// /**
// * app版本号
// */
// var appVersion: String = "1"
var appBaseUrl: String = ""
/**
* 具体业务 BaseUrl
*/
const val TEST_BASE_URL = "http://192.168.1.201:14801"
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
}
/**
* 全局常量
*/
object Constants {
}
/**
* key
*/
object GlobalKey {
const val KEY_BASE_URL = "baseUrlKey"
}
@@ -6,7 +6,6 @@ import android.graphics.Color
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.Log
import android.util.SparseIntArray import android.util.SparseIntArray
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -25,14 +24,19 @@ import com.shuwei.intelligent.shelves.model.SendWeightEvent
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.model.ShelfModel
import com.shuwei.intelligent.shelves.model.ShelfResult import com.shuwei.intelligent.shelves.model.ShelfResult
import com.shuwei.intelligent.shelves.model.WeightEvent
import com.shuwei.intelligent.shelves.net.Loading 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.net.apiService 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.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LEFT_SHELF_OPEN_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.RIGHT_SHELF_OPEN_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.SerialPortManager import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.HexUtils
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.binaryToHex
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
@@ -49,74 +53,39 @@ import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale 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 TAG = "HomeActivity"
private const val LOG_PREFIX = "receiveSerialPortData:收到" private const val LOG_PREFIX = "receiveSerialPortData:收到"
private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss" private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
//协议头
const val HEADER = "D6DADEDB"
//协议尾
const val FOOTER = "FFFF"
// const val ACTIVE_CMD =
// "${HEADER}000281C5D70A7D428A4FA67B50F7DACA241938938B4B9CBD73673E9C8E7F2E15062D${FOOTER}"
const val LEFT_SHELF_OPEN_CMD =
"${HEADER}020101000000000000000000000000000000000000${FOOTER}"
const val RIGHT_SHELF_OPEN_CMD =
"${HEADER}020100010000000000000000000000000000000000${FOOTER}"
//const val TEMPERATURE_CMD = "${HEADER}1001${FOOTER}"
const val DEVICE_INFO_CMD = "${HEADER}0301000000000000000000000000${FOOTER}"
const val START_TEMP_CTRL_CMD = "${HEADER}0F01360000${FOOTER}"
//const val START_CMD= "${HEADER}0F01050020${FOOTER}"
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D428948AD7254FADBB1516738938B4B9CBD73673E9D8E7F2E15062D$FOOTER"
// 410激活码
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${FOOTER}"
//1楼餐厅激活码
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
//华氏温度单位
const val F_TEMP_CMD = "${HEADER}0F01390000${FOOTER}"
//摄氏温度单位
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
//开灯(开 继电器1
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
const val LIGHT_OPEN = "${HEADER}020102020002020000000000000000000000000000${FOOTER}"
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}"
val list: MutableList<ShelfModel> = mutableListOf()
val weightArray = SparseIntArray()
var shelfIndex = 0
// public fun getRealWeight():Double = weightArray[shelfIndex]?:0.0
// 温度
var showTemperatureC = "0"
// 湿度
var showHumidity = "0"
} }
//401-false1楼餐厅-true // 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf()
// 各传感器重量数据,key 为货架编号
private val weightArray = SparseIntArray()
// 当前选中货架下标
private var shelfIndex = 0
// 当前温度(摄氏)
private var showTemperatureC = "0"
// 当前湿度
private var showHumidity = "0"
// 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false
private var isNeedOpenLock = true private var isNeedOpenLock = true
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels() private val viewModel: NetViewModel by viewModels()
@SuppressLint("NotifyDataSetChanged", "HardwareIds") @SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
@@ -124,26 +93,25 @@ class HomeActivity : BaseActivity() {
setBackground() setBackground()
setContentView(binding.root) setContentView(binding.root)
// updateLeftStatus("-°C / -%")
updateLeftStatus("") updateLeftStatus("")
initRecyclerView() initRecyclerView()
// 串口初始化,独立协程,不阻塞网络状态监听
lifecycleScope.launch { lifecycleScope.launch {
val openState = SerialPortManager.open() val openState = SerialPortManager.open()
log("onCreate: openState=$openState") log("onCreate: openState=$openState")
if (openState) { if (openState) {
SerialPortManager.startReceive { data -> SerialPortManager.startReceive { data ->
// 更新UI显示接收数据 // 切换到主线程协程处理串口数据
lifecycleScope.launch { lifecycleScope.launch {
try { runCatching { receiveSerialPortData(data) }
receiveSerialPortData(data) .onFailure { it.printStackTrace() }
} catch (e: Exception) {
e.printStackTrace()
} }
} }
} }
//SerialPortManager.send(DEVICE_INFO_CMD)
} }
// 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
launch { launch {
viewModel.getAccessTokenUiState.collect { state -> viewModel.getAccessTokenUiState.collect { state ->
@@ -167,59 +135,21 @@ class HomeActivity : BaseActivity() {
} }
} }
} }
//sendCmd(ACTIVE_CMD)
binding.root.postDelayed({ binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD) sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD) sendCmd(C_TEMP_CMD)
}, 5000) }, 5000)
// TaskManager.startTask()
saveGoodsTask() saveGoodsTask()
overdueTask() overdueTask()
// var androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
//// androidId = "d0e7a2d5f25c775c"
// log("onCreate: androidId=${androidId}")
// if (androidId == "65e1012638f78260" || androidId == "40f89e91ee40611d") {
// androidId = "d0e7a2d5f25c775c"
// }
// App.deviceId = androidId
// //"53babf69-0d2f-3875-a649-f2e12f80ad4c1"
//
// val swDpValue = resources?.configuration?.smallestScreenWidthDp
// log("onCreate: swDpValue=$swDpValue")
// viewModel.getAccessToken(App.deviceId)
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
// binding.root.postDelayed({
// loadTestData()
// }, 30*1000)
}
private fun loadTestData() {
list.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 4, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 9, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId))
shelfAdapter.notifyDataSetChanged()
} }
private fun getTokenSuccess(data: RespData<*>) { private fun getTokenSuccess(data: RespData<*>) {
log("getTokenSuccess: $data") log("getTokenSuccess: $data")
data.data?.let { data.data?.let {
App.accessToken = it.toString() App.accessToken = it.toString()
//UIUtils.toast(it.toString())
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
} }
} }
@@ -238,7 +168,6 @@ class HomeActivity : BaseActivity() {
} }
deviceName = data.data.deviceName deviceName = data.data.deviceName
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName") updateLeftStatus("$deviceName")
App.canteenId = data.data.placeId App.canteenId = data.data.placeId
@@ -247,10 +176,6 @@ class HomeActivity : BaseActivity() {
loadEmptyView() loadEmptyView()
return return
} }
// if (tempList.size < 10) {
// toast("数据错误")
// return
// }
list.clear() list.clear()
val subList01 = tempList.subList(0, tempList.size / 2) val subList01 = tempList.subList(0, tempList.size / 2)
val subList02 = tempList.subList(tempList.size / 2, tempList.size) val subList02 = tempList.subList(tempList.size / 2, tempList.size)
@@ -277,51 +202,15 @@ class HomeActivity : BaseActivity() {
*/ */
private var terminalVersion: String = "" private var terminalVersion: String = ""
private val serialReader by lazy { StringBuilder() } private val serialFrameParser = SerialFrameParser()
private fun receiveSerialPortData(srcData: String) { private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData") log("receiveSerialPortData: $srcData")
var data = "" // 重置无数据计时,并关闭已显示的超时提示弹窗
if (srcData.contains(HEADER) && srcData.contains(FOOTER)) { lastDataReceivedTime = System.currentTimeMillis()
serialReader.clear() noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
val startIndex = srcData.lastIndexOf(HEADER) val frame = serialFrameParser.parse(srcData) ?: return
var endIndex = srcData.indexOf(FOOTER) + FOOTER.length handleReceivedData(frame)
if (endIndex < startIndex) {
//类似这种:31314FFFFFD6DADEDB01010CFOOTER在HEADER前
data = srcData.substring(startIndex)
if (data.contains(FOOTER)) {
endIndex = srcData.indexOf(FOOTER) + FOOTER.length
data = data.substring(0, endIndex)
handleReceivedData(data)
serialReader.clear()
return
}
serialReader.clear()
serialReader.append(data).append(",")
return
}
serialReader.append(srcData.substring(startIndex, endIndex))
data = serialReader.toString()
handleReceivedData(data)
serialReader.clear()
return
}
if (srcData.contains(HEADER) && srcData.contains(FOOTER).not()) {
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
serialReader.append(srcData.substring(startIndex)).append(",")
return
}
if (srcData.contains(HEADER).not() && srcData.contains(FOOTER).not()) {
serialReader.append(srcData).append(",")
return
}
//if (srcData.contains(HEADER).not() && srcData.contains(FOOTER))
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
serialReader.append(srcData.substring(0, endIndex))
log("receiveSerialPortData:serialReader: $serialReader")
data = serialReader.toString().replace(",", "")
handleReceivedData(data)
} }
private fun handleReceivedData(data: String) { private fun handleReceivedData(data: String) {
@@ -358,7 +247,6 @@ class HomeActivity : BaseActivity() {
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER" val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}") log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd) sendCmd(respCmd)
//lockFeedback(data)
} }
"0302" -> { "0302" -> {
@@ -383,7 +271,8 @@ class HomeActivity : BaseActivity() {
"0C01" -> { "0C01" -> {
//对时 //对时
val timeLong = System.currentTimeMillis() / 1000 + (8 * 3600) val nowMs = System.currentTimeMillis()
val timeLong = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
val timeStr = "0000000" + String.format("%X", timeLong) val timeStr = "0000000" + String.format("%X", timeLong)
val time = val time =
timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("") timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
@@ -406,19 +295,13 @@ class HomeActivity : BaseActivity() {
"${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}" "${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}"
log(logMsg) log(logMsg)
sendCmd(respCmd) sendCmd(respCmd)
//val tempC = HexUtils.signedHexToDec(data.take(4)) * 0.1
// updateLeftStatus("$deviceName ${"%.1f".format(tempC)}°C / -%")
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName") updateLeftStatus("$deviceName")
//D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF
runOnUiThread {
if (tipDialog == null) { if (tipDialog == null) {
CommonDialog(this).apply { tipDialog = CommonDialog(this).apply {
dialogTitle = "温馨提示" dialogTitle = "温馨提示"
// dialogContent = logMsg
confirmBlock = { confirmBlock = {
logMsg?.copyTextToClipboard(this@HomeActivity) logMsg.copyTextToClipboard(this@HomeActivity)
} }
} }
} }
@@ -427,7 +310,6 @@ class HomeActivity : BaseActivity() {
tipDialog?.show() tipDialog?.show()
} }
} }
}
"1002" -> { "1002" -> {
log("${LOG_PREFIX}[温控信息应答]1002数据:${data}") log("${LOG_PREFIX}[温控信息应答]1002数据:${data}")
@@ -435,8 +317,6 @@ class HomeActivity : BaseActivity() {
"0F02" -> { "0F02" -> {
log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}") log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}")
//D6DADEDB0F020331323136334238423134303233303456322E31313EFFFF00000055010000000000002707000001000000220100000000000091040000120100000100000095140080611400800000000000000000000000000000000000000000010100005031323136334238423134303233303456322E31313EFFFF
runOnUiThread { }
} }
else -> {} else -> {}
@@ -445,29 +325,33 @@ class HomeActivity : BaseActivity() {
private var tipDialog: CommonDialog? = null private var tipDialog: CommonDialog? = null
private var fixedDataBody = "" /** 最后一次成功收到串口数据的时间戳(毫秒) */
private var lastDataReceivedTime = System.currentTimeMillis()
// private fun lockFeedback(data: String) { /** 无数据超时提示弹窗 */
// runCatching { private var noDataWarningDialog: CommonDialog? = null
// val fixedDataBodyEndIndex = data.length - FOOTER.length
// val fixedDataBodyStartIndex = HEADER.length + "0202".length + 42 /**
// fixedDataBody = data.substring(fixedDataBodyStartIndex, fixedDataBodyEndIndex) * 无数据看门狗:每30秒检查一次,若超过5分钟未收到数据则弹出提示。
// * 收到数据后会自动重置计时并关闭弹窗。
// val lockNo = if (list[shelfIndex].deviceNo in 1..5) "01" else "02" */
// val lockOneState = data.substring(HEADER.length + 4, HEADER.length + 6) private val noDataWatchdogTask = object : Runnable {
// if (lockOneState == "00") { override fun run() {
// //锁1关 val elapsed = System.currentTimeMillis() - lastDataReceivedTime
// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}") if (elapsed >= 5 * 60 * 1000L) {
// } if (noDataWarningDialog == null) {
// val lockTwoState = data.substring(HEADER.length + 6, HEADER.length + 8) noDataWarningDialog = CommonDialog(this@HomeActivity).apply {
// if (lockTwoState == "00") { dialogTitle = "设备提示"
// //锁2关 dialogContent = "长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备"
// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}") }
// } }
// }.onFailure { if (noDataWarningDialog?.isShowing == false) {
// it.printStackTrace() noDataWarningDialog?.show()
// } }
// } }
handler.postDelayed(this, 30 * 1000L)
}
}
private fun heartBeat(data: String) { private fun heartBeat(data: String) {
runCatching { runCatching {
@@ -483,14 +367,6 @@ class HomeActivity : BaseActivity() {
) )
val temperatureStartIndex = 158 val temperatureStartIndex = 158
val temperatureEndIndex = temperatureStartIndex + 2 val temperatureEndIndex = temperatureStartIndex + 2
// val temperatureHex = data.substring(temperatureStartIndex, temperatureEndIndex)
// val temperature = hexToDec(temperatureHex)
// val tempC = f2C(temperature.toDouble())
// showTemperatureC = "%.1f".format(tempC)
//val percent = 10 * list.count { it.goodsName.isNullOrBlank().not() }
// updateLeftStatus("${deviceName} [$temperatureHex] ${showTemperatureC}°C / -%")
getFixDataBody(data, temperatureEndIndex) getFixDataBody(data, temperatureEndIndex)
}.onFailure { }.onFailure {
@@ -532,23 +408,11 @@ class HomeActivity : BaseActivity() {
log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}") log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}")
EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight)) EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight))
} }
// val pos = index - 1
// val item = list[pos]
// item.weight = realWeight.toDouble()
// shelfAdapter.notifyItemChanged(pos)
// log("getWeightInfo: deviceNo=${item.deviceNo},realWeight=${realWeight}")
// EventBus.getDefault().post(SendWeightEvent(item.deviceNo, realWeight))
} }
count += 8 count += 8
index++ index++
} }
// ShelfActivity.updateWeight(getRealWeight())
// if (list.isNotEmpty()) {
// val intent = Intent(ShelfActivity.RECEIVER_DEVICE_INFO + list[shelfIndex].deviceNo)
// intent.putExtra(ShelfActivity.SHELF_WEIGHT, weightArray[shelfIndex])
// LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
// }
} }
private fun getDeviceInfo(data: String) { private fun getDeviceInfo(data: String) {
@@ -562,17 +426,6 @@ class HomeActivity : BaseActivity() {
e.printStackTrace() e.printStackTrace()
return return
} }
// val temperature = hexToDec(temperatureHex)
// val tempC = f2C(temperature.toDouble())
// showTemperatureC = "%.1f".format(tempC)
//val percent = 10 * list.count { it.goodsName.isNullOrBlank().not() }
// updateLeftStatus("${deviceName} ${showTemperatureC}°C / -%")
// val tempC = HexUtils.signedHexToDec(temperatureHex) * 0.1
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName") updateLeftStatus("$deviceName")
getWeightInfo( getWeightInfo(
@@ -584,13 +437,23 @@ class HomeActivity : BaseActivity() {
getFixDataBody(data, temperatureEndIndex) getFixDataBody(data, temperatureEndIndex)
} }
fun getFixDataBody(data: String, temperatureEndIndex: Int) { /**
* 从心跳帧或设备状态帧中补充解析 terminalId / terminalVersion。
* 激活指令(0001)是获取这两个字段的首选来源;
* 若激活指令尚未到来,则通过此方法从心跳/设备状态帧中兜底解析。
*
* @param data 完整协议帧字符串
* @param temperatureEndIndex 温度字段结束位置(固定数据体紧随其后)
*/
private fun getFixDataBody(data: String, temperatureEndIndex: Int) {
try { try {
// 已通过激活指令成功解析过,无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) { if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) {
return return
} }
val fixDataBodyStartIndex = temperatureEndIndex + 1 val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length) val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
// 固定数据体长度:terminalId 30字节 + terminalVersion 10字节 + 其他2字节 = 42字节
if (fixDataBody.length == 42) { if (fixDataBody.length == 42) {
terminalId = fixDataBody.substring(0, 30) terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40) terminalVersion = fixDataBody.substring(30, 40)
@@ -602,20 +465,25 @@ class HomeActivity : BaseActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
lifecycleScope.launch {
SerialPortManager.close() SerialPortManager.close()
}
// TaskManager.cancelTask()
saveTaskJob?.cancel() saveTaskJob?.cancel()
overdueTaskJob?.cancel() overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
super.onDestroy() super.onDestroy()
} }
@Suppress("DEPRECATION")
private val shelfAdapter by lazy { private val shelfAdapter by lazy {
ShelfAdapter(list).apply { ShelfAdapter(list).apply {
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
onShelfItemClicked(position)
}
}
}
/**
* 处理货架格子点击事件:按需开锁,然后跳转 ShelfActivity
*/
private fun onShelfItemClicked(position: Int) {
shelfIndex = position shelfIndex = position
lifecycleScope.launch { lifecycleScope.launch {
if (isNeedOpenLock) { if (isNeedOpenLock) {
@@ -623,19 +491,22 @@ class HomeActivity : BaseActivity() {
LEFT_SHELF_OPEN_CMD LEFT_SHELF_OPEN_CMD
else else
RIGHT_SHELF_OPEN_CMD RIGHT_SHELF_OPEN_CMD
val openState = SerialPortManager.send(openCmd) SerialPortManager.send(openCmd)
} }
// if (openState.not()) {
// toast("${list[position].goodsName}柜门开启失败")
// return@launch
// }
//SerialPortManager.send(START_CMD)
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])
}) { }) { result ->
it?.run { result?.let { onShelfActivityResult(position, it) }
val model = }
getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel }
}
/**
* 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端
*/
@Suppress("DEPRECATION")
private fun onShelfActivityResult(position: Int, intent: Intent) {
val model = intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel
list[position].apply { list[position].apply {
goodsId = model.goodsId goodsId = model.goodsId
goodsName = model.goodsName goodsName = model.goodsName
@@ -644,11 +515,10 @@ class HomeActivity : BaseActivity() {
putCabinetDay = model.putCabinetDay putCabinetDay = model.putCabinetDay
} }
} }
notifyItemChanged(position) shelfAdapter.notifyItemChanged(position)
try { try {
val submitList = list.sortedBy { it.deviceNo }
val submitList = list.sortedBy { item -> item.deviceNo }
val body = ShelfBody().also { body -> val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: "" body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId body.canteenId = App.canteenId
@@ -656,20 +526,12 @@ class HomeActivity : BaseActivity() {
body.humidity = showHumidity body.humidity = showHumidity
body.goodsList = submitList body.goodsList = submitList
} }
Log.d("mzf", "performSync: body:${body.toJsonString()}") log("performSync: body:${body.toJsonString()}")
lifecycleScope.launch { viewModel.saveShelfGoodsList(body)
val resp = apiService.saveShelfGoodsList(body = body)
Log.d("mzf", "performSync: resp:${resp.toJsonString()}")
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
}
}
}
}
}
private fun initRecyclerView() { private fun initRecyclerView() {
binding.rvShelf.run { binding.rvShelf.run {
@@ -685,9 +547,10 @@ class HomeActivity : BaseActivity() {
} }
} }
private val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) }
private fun updateDateTime() { private fun updateDateTime() {
val sdf = SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) val dateTime = dateTimeFormatter.format(Date())
val dateTime = sdf.format(Date())
updateRightStatus(dateTime) updateRightStatus(dateTime)
} }
@@ -703,13 +566,15 @@ class HomeActivity : BaseActivity() {
super.onResume() super.onResume()
hideStatusBar() hideStatusBar()
handler.post(updateTask) handler.post(updateTask)
// D6DADEDB00010031323136334238423134303233303456322E313100FFFF // 启动无数据看门狗(每30秒检查一次)
// sendCmd("${HEADER}00010031323136334238423134303233303456322E313152${FOOTER}") handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
handler.removeCallbacks(updateTask) handler.removeCallbacks(updateTask)
// 停止无数据看门狗
handler.removeCallbacks(noDataWatchdogTask)
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -720,7 +585,6 @@ class HomeActivity : BaseActivity() {
binding.include?.let { binding.include?.let {
it.root.visible() it.root.visible()
it.root.setOnClickListener { it.root.setOnClickListener {
//viewModel.getAccessToken(App.deviceId)
viewModel.getShelfList(deviceId = App.deviceId) viewModel.getShelfList(deviceId = App.deviceId)
} }
it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white) it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white)
@@ -728,11 +592,6 @@ class HomeActivity : BaseActivity() {
} }
} }
@Subscribe(threadMode = ThreadMode.MAIN)
public fun weightEvent(event: WeightEvent) {
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) { public fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo } val item = list.firstOrNull { it.deviceNo == event.shelfNo }
@@ -773,6 +632,7 @@ class HomeActivity : BaseActivity() {
private fun updateOverdueState() { private fun updateOverdueState() {
viewModel.updateOverdueState { state, items -> viewModel.updateOverdueState { state, items ->
if (items.isNullOrEmpty()) return@updateOverdueState if (items.isNullOrEmpty()) return@updateOverdueState
if (items.size != list.size) return@updateOverdueState
list.forEachIndexed { index, model -> list.forEachIndexed { index, model ->
//接口过期状态变化后更新状态 //接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) { if (items[index].overdue != model.overdue) {
@@ -792,10 +652,6 @@ class HomeActivity : BaseActivity() {
it.weight = it.weightBak it.weight = it.weightBak
} }
} }
// submitList.forEach {
// it.temperature = HomeActivity.showTemperatureC
// it.humidity = HomeActivity.showHumidity
// }
val body = ShelfBody().also { val body = ShelfBody().also {
it.deviceId = App.deviceId it.deviceId = App.deviceId
it.canteenId = App.canteenId it.canteenId = App.canteenId
@@ -16,6 +16,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.shuwei.intelligent.shelves.App import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalData
import com.shuwei.intelligent.shelves.base.BaseActivity import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityInitBinding import com.shuwei.intelligent.shelves.databinding.ActivityInitBinding
import com.shuwei.intelligent.shelves.model.DeviceConfigInfo import com.shuwei.intelligent.shelves.model.DeviceConfigInfo
@@ -63,7 +64,12 @@ class InitActivity : BaseActivity() {
// App.deviceId = "4787e213-90ab-3e32-88e0-ac271a937751" // App.deviceId = "4787e213-90ab-3e32-88e0-ac271a937751"
Log.d(TAG, "onCreate: deviceId = ${App.deviceId}") Log.d(TAG, "onCreate: deviceId = ${App.deviceId}")
SpTool.put(SpTool.DEVICE_ID, App.deviceId) SpTool.put(SpTool.DEVICE_ID, App.deviceId)
App.configUrl = UrlConfig.BASE_URL val appBaseUrl = SpTool.baseUrl
if (!appBaseUrl.isBlank()) {
GlobalData.appBaseUrl = appBaseUrl
} else {
GlobalData.appBaseUrl = GlobalData.PROD_BASE_URL
}
App.canteenId = "0" App.canteenId = "0"
// var deviceId = AppUtil.getUDID(this) // var deviceId = AppUtil.getUDID(this)
@@ -144,7 +150,7 @@ class InitActivity : BaseActivity() {
state.data.data?.let { state.data.data?.let {
if (it is DeviceConfigInfo) { if (it is DeviceConfigInfo) {
SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString()) SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString())
App.configUrl = it.appPackageUrl?:"" GlobalData.appBaseUrl = it.appPackageUrl?:""
App.canteenId = it.canteenId?:"" App.canteenId = it.canteenId?:""
startActivity<HomeActivity>() startActivity<HomeActivity>()
@@ -169,7 +175,7 @@ class InitActivity : BaseActivity() {
if (config == null) { if (config == null) {
return false return false
} }
App.configUrl = config.appPackageUrl?:"" GlobalData.appBaseUrl = config.appPackageUrl?:""
App.canteenId = config.canteenId?:"" App.canteenId = config.canteenId?:""
return true return true
} }
@@ -71,6 +71,9 @@ open class BaseActivity : AppCompatActivity() {
binding.tvLeftStatus.setOnDoubleClickListener { binding.tvLeftStatus.setOnDoubleClickListener {
finish() finish()
} }
binding.tvRightStatus.setOnDoubleClickListener {
EnvSwitchDialog(this).show()
}
} }
fun hideStatusBar() { fun hideStatusBar() {
@@ -0,0 +1,69 @@
package com.shuwei.intelligent.shelves.base
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.shuwei.intelligent.shelves.GlobalData
import com.shuwei.intelligent.shelves.databinding.DialogEnvSwitchBinding
import com.shuwei.intelligent.shelves.utils.SpTool
import com.shuwei.intelligent.shelves.utils.ToastUtil
import com.shuwei.intelligent.shelves.utils.ext.dp
/**
* 环境切换弹窗
*
* 提供 TEST / UAT / PROD 三套环境的切换功能,
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
*
* @param context 上下文
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
*/
class EnvSwitchDialog(
context: Context,
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
) : BaseDialog(
context,
defWidth = 600.dp,
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
) {
private lateinit var binding: DialogEnvSwitchBinding
override fun getRootView(): View {
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.TEST_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.UAT_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
// 取消按钮
binding.btnCancel.setOnClickListener { dismiss() }
// 确认按钮
binding.btnConfirm.setOnClickListener {
// 未选中任何选项时提示用户
if (binding.rgEnv.checkedRadioButtonId == -1) {
ToastUtil.show(context,"请选择环境")
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.TEST_BASE_URL
binding.rbUat.id -> GlobalData.UAT_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SpTool.baseUrl = newUrl
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -2,7 +2,8 @@ package com.shuwei.intelligent.shelves.net
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import com.shuwei.intelligent.shelves.App import com.shuwei.intelligent.shelves.BuildConfig
import com.shuwei.intelligent.shelves.GlobalData
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
@@ -18,7 +19,7 @@ import kotlin.apply
val apiService: ApiService = Retrofit.Builder() val apiService: ApiService = Retrofit.Builder()
// .baseUrl(UrlConfig.BASE_URL) // .baseUrl(UrlConfig.BASE_URL)
.baseUrl(UrlConfig.DEVICE_BASE_URL) .baseUrl(GlobalData.appBaseUrl)
.client(HttpManager.instance.client) .client(HttpManager.instance.client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
@@ -28,9 +29,9 @@ class HttpManager private constructor() {
val client: OkHttpClient by lazy { val client: OkHttpClient by lazy {
OkHttpClient.Builder() OkHttpClient.Builder()
.apply { .apply {
connectTimeout(15, TimeUnit.SECONDS) connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS) readTimeout(30, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS) writeTimeout(30, TimeUnit.SECONDS)
sslSocketFactory(createSSLSocketFactory(), TrustAllCerts()) sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
hostnameVerifier { _, _ -> true } hostnameVerifier { _, _ -> true }
// addInterceptor(LoggingInterceptor()) // addInterceptor(LoggingInterceptor())
@@ -38,11 +39,11 @@ class HttpManager private constructor() {
Log.d("HttpManager","okhttp logger ==>${it}") Log.d("HttpManager","okhttp logger ==>${it}")
}).apply { }).apply {
level = level =
// if (MyApp.DEBUG) { if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY HttpLoggingInterceptor.Level.BODY
// } else { } else {
// HttpLoggingInterceptor.Level.NONE HttpLoggingInterceptor.Level.NONE
// } }
}) })
// addInterceptor { chain -> // addInterceptor { chain ->
// val request = chain.request().newBuilder() // val request = chain.request().newBuilder()
@@ -4,6 +4,7 @@ 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.model.ShelfModel
import com.shuwei.intelligent.shelves.net.HttpManager.Companion.instance import com.shuwei.intelligent.shelves.net.HttpManager.Companion.instance
@@ -148,9 +149,6 @@ class NetViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
_getGoodsListUiState.value = UiState.Loading _getGoodsListUiState.value = UiState.Loading
runCatching { runCatching {
if (App.configUrl.isNullOrBlank()) {
App.configUrl = UrlConfig.BASE_URL
}
val response = apiService.getGoodsList( val response = apiService.getGoodsList(
canteenId = canteenId, canteenId = canteenId,
goodsName = goodsName, goodsName = goodsName,
@@ -1,6 +1,7 @@
package com.shuwei.intelligent.shelves.net package com.shuwei.intelligent.shelves.net
import com.shuwei.intelligent.shelves.App import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalData
object UrlConfig { object UrlConfig {
// const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889" // const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889"
@@ -13,18 +14,18 @@ object UrlConfig {
// */ // */
// const val DEVICE_CONFIG = "${DEVICE_BASE_URL}/equipment/stEquipment/queryByEquipmentCode" // const val DEVICE_CONFIG = "${DEVICE_BASE_URL}/equipment/stEquipment/queryByEquipmentCode"
const val BASE_URL = "https://dev.yixiong-tech.com:8081" // const val BASE_URL = "https://dev.yixiong-tech.com:8081"
// const val BASE_URL = "https://yyjk.shuziweidao.com/gateway" //// const val BASE_URL = "https://yyjk.shuziweidao.com/gateway"
// const val BASE_URL = "http://192.168.1.201:14801" //// const val BASE_URL = "http://192.168.1.201:14801"
const val DEVICE_BASE_URL = BASE_URL // const val DEVICE_BASE_URL = BASE_URL
var GET_ACCESS_TOKEN = "${App.configUrl}/restaurant/equipment/stEquipment/getEquipmentToken" var GET_ACCESS_TOKEN = "${GlobalData.appBaseUrl}/restaurant/equipment/stEquipment/getEquipmentToken"
var GET_SHELF_LIST = var GET_SHELF_LIST =
"${App.configUrl}/terminal/neglect/smartShelves/app/smartShelves/app/getShelvesListByDeviceId" "${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/getShelvesListByDeviceId"
var GET_GOODS_LIST = var GET_GOODS_LIST =
"${App.configUrl}/terminal/neglect/smartShelves/app/smartShelves/app/getGoodsPageList" "${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/getGoodsPageList"
var SAVE_SHELF_GOODS_LIST = var SAVE_SHELF_GOODS_LIST =
"${App.configUrl}/terminal/neglect/smartShelves/app/smartShelves/app/saveShelvesGoodsList" "${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/saveShelvesGoodsList"
} }
@@ -0,0 +1,47 @@
package com.shuwei.intelligent.shelves.serial
/**
* 串口通信协议常量定义
* 统一管理所有协议头、协议尾及各类指令
*/
object ProtocolConstants {
/** 协议头 */
const val HEADER = "D6DADEDB"
/** 协议尾 */
const val FOOTER = "FFFF"
/** 激活指令(1楼餐厅) */
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
/** 左货架开锁指令 */
const val LEFT_SHELF_OPEN_CMD =
"${HEADER}020101000000000000000000000000000000000000${FOOTER}"
/** 右货架开锁指令 */
const val RIGHT_SHELF_OPEN_CMD =
"${HEADER}020100010000000000000000000000000000000000${FOOTER}"
/** 获取设备信息指令 */
const val DEVICE_INFO_CMD = "${HEADER}0301000000000000000000000000${FOOTER}"
/** 启动温控指令 */
const val START_TEMP_CTRL_CMD = "${HEADER}0F01360000${FOOTER}"
/** 华氏温度单位指令 */
const val F_TEMP_CMD = "${HEADER}0F01390000${FOOTER}"
/** 摄氏温度单位指令 */
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
/** 开灯指令(继电器1关) */
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
/** 开灯指令(继电器1开) */
const val LIGHT_OPEN = "${HEADER}020102020002020000000000000000000000000000${FOOTER}"
/** 温控指令 */
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}"
}
@@ -0,0 +1,80 @@
package com.shuwei.intelligent.shelves.serial
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
/**
* 串口数据帧解析器
* 负责将串口原始分片数据拼装为完整协议帧
* 完整帧格式:[HEADER][CMD][BODY][FOOTER]
*/
class SerialFrameParser {
/** 数据帧缓冲区,用于跨分片拼装 */
private val buffer = StringBuilder()
/**
* 解析原始串口分片数据,尝试拼装为完整帧
* @param srcData 原始串口数据片段
* @return 完整数据帧字符串;帧不完整时返回 null
*/
fun parse(srcData: String): String? {
val hasHeader = srcData.contains(HEADER)
val hasFooter = srcData.contains(FOOTER)
return when {
hasHeader && hasFooter -> parseCompleteInSingleChunk(srcData)
hasHeader && !hasFooter -> {
// 仅含帧头,等待后续分片
buffer.clear()
buffer.append(srcData.substring(srcData.lastIndexOf(HEADER))).append(",")
null
}
!hasHeader && !hasFooter -> {
// 中间分片,追加缓冲
buffer.append(srcData).append(",")
null
}
else -> {
// 仅含帧尾,与缓冲区拼接完整帧
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
buffer.append(srcData.substring(0, endIndex))
val frame = buffer.toString().replace(",", "")
buffer.clear()
frame.takeIf { it.isNotEmpty() }
}
}
}
/**
* 单个分片中同时含有帧头和帧尾的情况
*/
private fun parseCompleteInSingleChunk(srcData: String): String? {
buffer.clear()
val startIndex = srcData.lastIndexOf(HEADER)
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
return if (endIndex < startIndex) {
// 帧尾在帧头之前,说明是上一帧的残留尾部 + 新帧的头部
// 例:31314FFFFFD6DADEDB01010C
val partial = srcData.substring(startIndex)
if (partial.contains(FOOTER)) {
// 当前片段中新帧已完整
val newEnd = partial.indexOf(FOOTER) + FOOTER.length
partial.substring(0, newEnd)
} else {
// 新帧不完整,缓存等待后续分片
buffer.append(partial).append(",")
null
}
} else {
// 帧头在帧尾之前,正常完整帧
srcData.substring(startIndex, endIndex)
}
}
/** 清空缓冲区 */
fun reset() {
buffer.clear()
}
}
@@ -51,15 +51,13 @@ object SerialPortManager {
} }
// 启动接收协程 // 启动接收协程
@Suppress("DEPRECATION")
@SuppressLint("DefaultLocale")
fun startReceive(callback: (String) -> Unit) { fun startReceive(callback: (String) -> Unit) {
scope.launch { scope.launch {
while (isActive) { while (isActive) {
try { try {
val flow = serialPort?.read() val flow = serialPort?.read()
flow?.collect { buffer -> flow?.collect { buffer ->
val data = buffer.toHexString2().toUpperCase() val data = buffer.toHexString2().uppercase(Locale.ROOT)
callback(data) callback(data)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -71,7 +69,7 @@ object SerialPortManager {
} }
// 关闭串口 // 关闭串口
suspend fun close() { fun close() {
scope.cancel() scope.cancel()
serialPort?.close() serialPort?.close()
} }
@@ -1,6 +1,7 @@
package com.shuwei.intelligent.shelves.utils package com.shuwei.intelligent.shelves.utils
import com.shuwei.intelligent.shelves.App import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalKey
import com.shuwei.intelligent.shelves.utils.ext.put import com.shuwei.intelligent.shelves.utils.ext.put
import kotlin.to import kotlin.to
@@ -28,4 +29,10 @@ object SpTool {
fun getBoolean(key: String, defValue: Boolean = false): Boolean { fun getBoolean(key: String, defValue: Boolean = false): Boolean {
return pref.getBoolean(key, defValue) return pref.getBoolean(key, defValue)
} }
var baseUrl: String
get() = getString(GlobalKey.KEY_BASE_URL, "")
set(value) {
put(GlobalKey.KEY_BASE_URL, value)
}
} }
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/bg_card_blue" />
</shape>
</item>
</ripple>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/bg_card_blue" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="12dp"/>
</shape>
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="600dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius12"
android:orientation="vertical"
android:paddingHorizontal="40dp"
android:paddingVertical="36dp">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="环境切换"
android:textColor="#ff141428"
android:textSize="36sp"
android:textStyle="bold" />
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="24dp"
android:background="#FFDCDCF0" />
<!-- 环境选项 -->
<RadioGroup
android:id="@+id/rgEnv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbTest"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="测试环境"
android:textColor="#ff141428"
android:textSize="28sp" />
<RadioButton
android:id="@+id/rbUat"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="UAT 环境"
android:textColor="#ff141428"
android:textSize="28sp" />
<RadioButton
android:id="@+id/rbProd"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="生产环境"
android:textColor="#ff141428"
android:textSize="28sp" />
</RadioGroup>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="24dp"
android:background="#FFDCDCF0" />
<!-- 底部按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="20dp"
android:layout_weight="1"
android:background="@drawable/bg_cancel"
android:text="取消"
android:textColor="@color/bg_card_blue"
android:textSize="28sp" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:background="@drawable/bg_confirm"
android:text="确认"
android:textColor="#FFFFFF"
android:textSize="28sp" />
</LinearLayout>
</LinearLayout>