19 Commits
Author SHA1 Message Date
lvmeng 8145eb588b refactor(serial): 将串口配置常量重命名为大写格式
- 将 portName 常量重命名为 PORT_NAME
- 将 baudRate 常量重命名为 BAUD_RATE
- 更新代码中对这些常量的引用以使用新的命名约定
- 保持串口通信功能完全不变
2026-05-21 09:01:27 +08:00
mazengfei c91d5656aa 修改旧版设备秤数量导致数据显示不准的问题 2026-05-20 16:50:53 +08:00
mazengfei 532b22a5c6 设备支持3列显示,统一处理显示和提交数据顺序 2026-05-20 15:36:28 +08:00
mazengfei e9d04bfb70 优化启动页面样式 2026-04-28 16:52:54 +08:00
mazengfei afba5220b3 首页设置禁止返回按钮 2026-04-28 16:41:21 +08:00
lvmeng 4ace12c6c7 style(ui): 调整加载动画尺寸并替换为矢量资源
- 将加载动画ImageView尺寸从160dp调整为120dp
- 替换loading_spinner为新的loading矢量资源
- 添加loading.xml矢量动画资源文件
- 注释掉HomeActivity中的临时报告对话框逻辑
- 简化HomeActivity中的RespData类型转换代码
2026-04-28 15:37:47 +08:00
mazengfei ee36869003 优化 2026-04-07 17:19:12 +08:00
lvmengandClaude Sonnet 4.6 c23c215941 fix: 修复 noDataWatchdogTask 潜在的内存泄漏问题
在 Runnable 中添加生命周期检查,防止 Activity 销毁后任务继续执行。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:58:22 +08:00
lvmengandClaude Sonnet 4.6 3d67dd2c2d refactor: 优化货架列表转换逻辑并支持奇数长度处理
- 移除 subList 创建,直接通过索引访问元素
- 修复奇数长度列表的处理逻辑
- 保持单次循环,提升性能

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:53:00 +08:00
mazengfeiandClaude Sonnet 4.6 133f309d4b refactor: 统一时钟管理并优化状态栏显示逻辑
- 在 BaseActivity 中实现统一的时钟管理机制,避免各子类重复实现
- LogActivity 和 ShelfActivity 覆写 onClockTick() 自定义状态栏显示
- HomeActivity 移除重复的时间更新逻辑,启用右侧状态栏菜单
- 提取协议常量到 ProtocolConstants,提高代码可维护性
- 优化开锁逻辑判断,改为基于 deviceId 的动态判断
- 新增右侧状态栏弹出菜单布局文件

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:07:41 +08:00
mazengfeiandClaude Sonnet 4.6 3e5c7c6caa refactor(HomeActivity): 修复内存泄漏并优化错误处理
1. 修复内存泄漏问题
   - 在 onDestroy 中清理所有 Handler 回调
   - 在 onDestroy 中释放 Dialog 引用

2. 增强类型安全
   - updateUI 中增加类型检查和日志
   - updateOverdueState 中增加完整的类型检查

3. 优化串口异常处理
   - 串口数据处理异常时向用户提示

4. 消除代码重复
   - 删除冗余的 saveShelfGoodsList 方法
   - 定时任务直接调用 syncShelfGoodsToServer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:19:45 +08:00
mazengfeiandClaude Sonnet 4.6 04fe3ec2c1 refactor: 全面重构 HomeActivity 架构并修复多项 Bug
- 新增 SerialProtocolHandler 类,将串口协议解析逻辑从 HomeActivity 剥离
- ProtocolConstants 补充命令码常量(CMD_*)及帧解析偏移常量
- 修复 NetViewModel.getDeviceConfig 更新错误 StateFlow 的 Bug
- 修复 getWeightInfo 中 toInt() 整数溢出崩溃风险,改用 toLongOrNull(16)
- 升级废弃的 getSerializableExtra,使用 Build.VERSION.SDK_INT 版本分支
- BaseActivity 提取 onUserInactive() 虚方法,消除对子类 HomeActivity 的直接引用
- 修复 BaseActivity 中错误的 LIGHT_OPEN/CLOSE 导入(改从 ProtocolConstants 导入)
- repeatOnLifecycle 由 CREATED 改为 STARTED,避免 UI 不可见时无效更新
- updateOverdueState 从回调风格改为 StateFlow,与项目其他接口保持一致
- 提取 syncShelfGoodsToServer() 消除重复的 ShelfBody 构建逻辑
- 清理 NetViewModel 和 BaseActivity 中大段注释死代码

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 11:26:05 +08:00
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
mazengfei 40cd27fa7b 增加左上角点击关闭页面,增加启动网络检测和连接功能,删除手机端测试布局; 2026-03-30 11:18:11 +08:00
59 changed files with 2103 additions and 2291 deletions
+5 -4
View File
@@ -44,6 +44,7 @@ android {
}
buildFeatures {
viewBinding = true
buildConfig = true
}
ndkVersion = "29.0.13846066"
@@ -67,10 +68,10 @@ dependencies {
implementation("io.github.jeadyx:kmp-serialport:1.0.0")
// 基础依赖包,必须要依赖
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
// kotlin扩展(可选)
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
// // 基础依赖包,必须要依赖
// implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
// // kotlin扩展(可选)
// implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
+1 -1
View File
@@ -43,7 +43,7 @@
android:name=".activity.ShelfActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.intelligent.shelves.activity.SettingActivity" />
<!-- <activity android:name="com.shuwei.intelligent.shelves.activity.SettingActivity" />-->
<activity android:name="com.shuwei.intelligent.shelves.activity.LogActivity" />
<receiver
@@ -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"
}
@@ -7,7 +7,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@@ -25,111 +25,132 @@ import com.shuwei.intelligent.shelves.model.SendWeightEvent
import com.shuwei.intelligent.shelves.model.ShelfBody
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.model.ShelfResult
import com.shuwei.intelligent.shelves.model.WeightEvent
import com.shuwei.intelligent.shelves.net.HomeViewModel
import com.shuwei.intelligent.shelves.net.Loading
import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.SerialViewModel
import com.shuwei.intelligent.shelves.net.UiState
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.SerialProtocolHandler
import com.shuwei.intelligent.shelves.utils.GridLayoutTool
import com.shuwei.intelligent.shelves.utils.IntervalExecutor
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class HomeActivity : BaseActivity() {
companion object {
private const val TAG = "HomeActivity"
private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
companion object {}
// 协议头
const val HEADER = "D6DADEDB"
override val enableRightStatusMenu: Boolean = true
// 协议尾
const val FOOTER = "FFFF"
// 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf()
const val LEFT_SHELF_OPEN_CMD =
"${HEADER}020101000000000000000000000000000000000000${FOOTER}"
const val RIGHT_SHELF_OPEN_CMD =
"${HEADER}020100010000000000000000000000000000000000${FOOTER}"
// 当前选中货架下标
private var shelfIndex = 0
const val DEVICE_INFO_CMD = "${HEADER}0301000000000000000000000000${FOOTER}"
const val START_TEMP_CTRL_CMD = "${HEADER}0F01360000${FOOTER}"
// 当前温度(摄氏)
private var showTemperatureC = "0"
// 1楼餐厅激活码
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
// 当前湿度
private var showHumidity = "0"
// 华氏温度单位
const val F_TEMP_CMD = "${HEADER}0F01390000${FOOTER}"
// 仅特定 deviceId 的部署点需要发送开锁指令
private fun isNeedOpenLock() = App.deviceId == "4787e213-90ab-3e32-88e0-ac271a937751"
// 摄氏温度单位
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}"
}
// 401-false1楼餐厅-true
private var isNeedOpenLock = true
private var deviceName = ""
private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels()
private val homeViewModel: HomeViewModel by viewModels()
private val serialViewModel: SerialViewModel by viewModels()
@SuppressLint("NotifyDataSetChanged", "HardwareIds")
// 串口协议解析器,通过回调通知本 Activity
private val serialProtocolHandler =
SerialProtocolHandler(object : SerialProtocolHandler.Callback {
override fun onSendCmd(cmd: String) = sendCmd(cmd)
override fun onWeightUpdated(shelfNo: Int, weight: Int) {
if (shelfNo in 1..list.size) {
list.firstOrNull { it.deviceNo == shelfNo }?.let { model ->
model.weight = weight.toDouble()
val pos = list.indexOf(model)
shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: deviceNo=${model.deviceNo},realWeight=$weight")
EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight))
}
}
}
override fun onTempReport(logMsg: String) {
updateLeftStatus(deviceName)
log(logMsg)
// if (tipDialog == null) {
// tipDialog = CommonDialog(this@HomeActivity).apply {
// dialogTitle = "温馨提示"
// confirmBlock = { logMsg.copyTextToClipboard(this@HomeActivity) }
// }
// }
// if (tipDialog?.isShowing == false) {
// tipDialog?.dialogContent = logMsg
// tipDialog?.show()
// }
}
override fun onLog(message: String) = log(message)
})
private val serialFrameParser = SerialFrameParser()
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
EventBus.getDefault().register(this)
setBackground()
setContentView(binding.root)
onBackPressedDispatcher.addCallback(this) {
}
updateLeftStatus("")
initRecyclerView()
// 打开串口,串口生命周期由 SerialViewModel 管理
serialViewModel.openSerial()
// 串口初始化,独立协程,不阻塞网络状态监听
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("串口数据处理异常,请检查设备连接")
}
}
}
}
}
// 统一注册所有 ViewModel Flow 观察
observeViewModels()
// 延迟发送温控初始化命令
binding.root.postDelayed({
serialViewModel.sendCmd(START_TEMP_CTRL_CMD)
serialViewModel.sendCmd(C_TEMP_CMD)
}, 5000)
// 启动定时任务(保存货架数据 + 刷新过期状态),生命周期由 viewModelScope 管理
homeViewModel.startPeriodicTasks()
// 拉取货架列表
viewModel.getShelfList(deviceId = App.deviceId)
}
/**
* 统一注册所有 ViewModel Flow 观察。
* 在 STARTED 状态下收集,onStop 时自动暂停,onStart 时恢复。
*/
private fun observeViewModels() {
// 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 获取 Token 结果
launch {
viewModel.getAccessTokenUiState.collect { state ->
when (state) {
@@ -140,8 +161,6 @@ class HomeActivity : BaseActivity() {
}
}
}
// 货架列表加载结果
launch {
viewModel.getShelfListUiState.collect { state ->
when (state) {
@@ -152,48 +171,49 @@ class HomeActivity : BaseActivity() {
}
}
}
// 串口重量更新:更新 ViewModel 缓存 + 局部刷新 Adapter
launch {
serialViewModel.weightFlow.collect { (deviceNo, weight) ->
homeViewModel.putWeight(deviceNo, weight)
if (deviceNo in 1..10 && homeViewModel.shelfItems.size == 10) {
homeViewModel.shelfItems.firstOrNull { it.deviceNo == deviceNo }
?.let { model ->
model.weight = weight.toDouble()
val pos = homeViewModel.shelfItems.indexOf(model)
shelfAdapter.notifyItemChanged(pos)
log("weightFlow: deviceNo=${model.deviceNo}, weight=$weight")
EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight))
}
viewModel.updateOverdueStateUiState.collect { state ->
if (state is UiState.Success<*>) {
val respData = state.data as? RespData<*>
if (respData == null) {
log("updateOverdueState: 数据类型错误,期望 RespData")
return@collect
}
val shelfResult = respData.data as? ShelfResult
if (shelfResult == null) {
log("updateOverdueState: 数据类型错误,期望 ShelfResult")
return@collect
}
val items = shelfResult.containerGoodsList
if (items.isNullOrEmpty()) {
log("updateOverdueState: 货架列表为空")
return@collect
}
applyOverdueState(items)
}
}
}
// 串口温控上报:弹窗展示日志
launch {
serialViewModel.tempCtrlEventFlow.collect { logMsg ->
showTipDialog(logMsg)
}
}
// 串口触发顶部状态栏刷新
launch {
serialViewModel.deviceStatusRefreshFlow.collect {
updateLeftStatus(homeViewModel.deviceName)
}
}
// 过期状态变化:局部刷新对应 position
launch {
homeViewModel.overdueUpdateFlow.collect { positions ->
positions.forEach { shelfAdapter.notifyItemChanged(it) }
}
}
}
}
binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD)
}, 5000)
saveGoodsTask()
overdueTask()
viewModel.getShelfList(deviceId = App.deviceId)
}
private val defDeviceList = listOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751"
)
private fun getTokenSuccess(data: RespData<*>) {
log("getTokenSuccess: $data")
data.data?.let {
@@ -210,27 +230,24 @@ class HomeActivity : BaseActivity() {
private fun updateUI(data: RespData<*>) {
binding.include?.root?.gone()
window?.decorView?.postDelayed({ Loading.dismiss() }, 500)
if (data.data !is ShelfResult) {
val shelfResult = data.data as? ShelfResult
if (shelfResult == null) {
log("updateUI: 数据类型错误,期望 ShelfResult,实际类型:${data.data?.javaClass?.simpleName}")
loadEmptyView()
return
}
// 将 deviceName 写入 HomeViewModel,跨配置变更保持
homeViewModel.deviceName = data.data.deviceName
updateLeftStatus(homeViewModel.deviceName)
deviceName = shelfResult.deviceName
updateLeftStatus(deviceName)
App.canteenId = data.data.placeId
val tempList = data.data.containerGoodsList
App.canteenId = shelfResult.placeId
var tempList = shelfResult.containerGoodsList
if (tempList.isNullOrEmpty()) {
loadEmptyView()
return
}
homeViewModel.shelfItems.clear()
val subList01 = tempList.subList(0, tempList.size / 2)
val subList02 = tempList.subList(tempList.size / 2, tempList.size)
repeat(tempList.size / 2) { index ->
homeViewModel.shelfItems.add(subList01[index].also { it.deviceId = App.deviceId })
homeViewModel.shelfItems.add(subList02[index].also { it.deviceId = App.deviceId })
}
list.clear()
tempList = GridLayoutTool.initListSort(tempList)
list.addAll(tempList)
shelfAdapter.notifyDataSetChanged()
}
@@ -240,98 +257,130 @@ class HomeActivity : BaseActivity() {
loadEmptyView()
}
/**
* 显示温控信息弹窗。
* 若弹窗已在展示中则不重复打开,仅更新内容。
*/
private fun showTipDialog(logMsg: String) {
if (tipDialog == null) {
tipDialog = CommonDialog(this).apply {
dialogTitle = "温馨提示"
confirmBlock = { logMsg.copyTextToClipboard(this@HomeActivity) }
}
}
if (tipDialog?.isShowing == false) {
tipDialog?.dialogContent = logMsg
tipDialog?.show()
}
private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData")
// 重置无数据计时,并关闭已显示的超时提示弹窗
lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
val frame = serialFrameParser.parse(srcData) ?: return
serialProtocolHandler.handle(frame)
}
private var tipDialog: CommonDialog? = null
/** 最后一次成功收到串口数据的时间戳(毫秒) */
private var lastDataReceivedTime = System.currentTimeMillis()
/** 无数据超时提示弹窗 */
private var noDataWarningDialog: CommonDialog? = null
/**
* 无数据看门狗:每30秒检查一次,若超过5分钟未收到数据则弹出提示。
* 收到数据后会自动重置计时并关闭弹窗。
*/
private val noDataWatchdogTask = object : Runnable {
override fun run() {
// 生命周期检查,防止 Activity 销毁后继续执行
if (isFinishing || isDestroyed) return
val elapsed = System.currentTimeMillis() - lastDataReceivedTime
if (elapsed >= 5 * 60 * 1000L) {
if (noDataWarningDialog == null) {
noDataWarningDialog = CommonDialog(this@HomeActivity).apply {
dialogTitle = "设备提示"
dialogContent =
"长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备"
}
}
if (noDataWarningDialog?.isShowing == false) {
noDataWarningDialog?.show()
}
}
handler.postDelayed(this, 30 * 1000L)
}
}
/**
* 用户超时无操作时关灯
*/
override fun onUserInactive() {
switchLight(false)
}
override fun onDestroy() {
// 弹窗需在 Activity 销毁前手动关闭,防止窗口泄漏
// 清理 Handler 所有回调,防止内存泄漏
handler.removeCallbacksAndMessages(null)
// 关闭并释放 Dialog 引用,防止内存泄漏
tipDialog?.dismiss()
tipDialog = null
noDataWarningDialog?.dismiss()
noDataWarningDialog = null
runBlocking { SerialPortManager.close() }
saveTaskJob?.cancel()
overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this)
// 串口关闭、Channel 关闭、定时任务取消均由各 ViewModel.onCleared() 自动管理
super.onDestroy()
}
private val shelfAdapter by lazy {
ShelfAdapter(homeViewModel.shelfItems).apply {
ShelfAdapter(list).apply {
setOnItemClickListener { _, _, position ->
homeViewModel.selectedShelfIndex = position
lifecycleScope.launch {
// 根据货架编号决定开启左侧或右侧柜锁
if (isNeedOpenLock) {
val openCmd = if (homeViewModel.shelfItems[position].deviceNo in 1..5)
LEFT_SHELF_OPEN_CMD
else
RIGHT_SHELF_OPEN_CMD
serialViewModel.sendCmd(openCmd)
}
launch(Intent(this@HomeActivity, ShelfActivity::class.java).also {
it.putExtra(ShelfActivity.SHELF_MODEL, homeViewModel.shelfItems[position])
}) {
it?.run {
// API 33 起支持带类型参数的重载,低版本回退旧 API + as? 安全转换
val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializableExtra(ShelfActivity.SHELF_MODEL, ShelfModel::class.java)
} else {
@Suppress("DEPRECATION")
getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel
} ?: return@run
// 将 ShelfActivity 返回的数据同步回列表
homeViewModel.shelfItems[position].apply {
goodsId = model.goodsId
goodsName = model.goodsName
weight = model.weight
if (model.putCabinetDay.isNullOrBlank().not()) {
putCabinetDay = model.putCabinetDay
}
}
notifyItemChanged(position)
// 货架操作后立即将最新数据同步到服务端
try {
val submitList = homeViewModel.shelfItems.sortedBy { item -> item.deviceNo }
val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId
body.temperature = homeViewModel.temperatureC
body.humidity = homeViewModel.humidity
body.goodsList = submitList
}
Log.d(TAG, "shelfItemClick sync: body=${body.toJsonString()}")
viewModel.saveShelfGoodsList(body)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
onShelfItemClicked(position)
}
}
}
/**
* 处理货架格子点击事件:按需开锁,然后跳转 ShelfActivity
*/
private fun onShelfItemClicked(position: Int) {
shelfIndex = position
lifecycleScope.launch {
if (isNeedOpenLock()) {
val openCmd = if (list[position].deviceNo in 1..5)
LEFT_SHELF_OPEN_CMD
else
RIGHT_SHELF_OPEN_CMD
SerialPortManager.send(openCmd)
}
launch(Intent(this@HomeActivity, ShelfActivity::class.java).also {
it.putExtra(ShelfActivity.SHELF_MODEL, list[position])
}) { result ->
result?.let { onShelfActivityResult(position, it) }
}
}
}
/**
* 处理 ShelfActivity 返回结果:更新本地列表数据并同步到服务端
*/
private fun onShelfActivityResult(position: Int, intent: Intent) {
@Suppress("DEPRECATION")
val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra(ShelfActivity.SHELF_MODEL, ShelfModel::class.java)
} else {
intent.getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel
} ?: return
list[position].apply {
goodsId = model.goodsId
goodsName = model.goodsName
weight = model.weight
if (model.putCabinetDay.isNullOrBlank().not()) {
putCabinetDay = model.putCabinetDay
}
}
shelfAdapter.notifyItemChanged(position)
syncShelfGoodsToServer()
}
private fun initRecyclerView() {
binding.rvShelf.run {
layoutManager =
GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false)
adapter = shelfAdapter
itemAnimator.let {
val layoutManager = GridLayoutTool.getLayoutManager(this@HomeActivity) ?: return
binding.rvShelf.let {
it.layoutManager = layoutManager
it.adapter = shelfAdapter
it.itemAnimator.let {
if (it is DefaultItemAnimator) {
it.supportsChangeAnimations = false
}
@@ -339,34 +388,24 @@ class HomeActivity : BaseActivity() {
}
}
private fun updateDateTime() {
val sdf = SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA)
val dateTime = sdf.format(Date())
updateRightStatus(dateTime)
}
private val handler = Handler(Looper.getMainLooper())
private val updateTask = object : Runnable {
override fun run() {
updateDateTime()
handler.postDelayed(this, 1000)
}
}
override fun onResume() {
super.onResume()
hideStatusBar()
handler.post(updateTask)
// 启动无数据看门狗(每30秒检查一次)
handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(updateTask)
// 停止无数据看门狗
handler.removeCallbacks(noDataWatchdogTask)
}
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
homeViewModel.shelfItems.clear()
list.clear()
shelfAdapter.notifyDataSetChanged()
binding.include?.let {
it.root.visible()
@@ -378,13 +417,9 @@ class HomeActivity : BaseActivity() {
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun weightEvent(event: WeightEvent) {
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun clearEmptyShelf(event: ClearShelfEvent) {
val item = homeViewModel.shelfItems.firstOrNull { it.deviceNo == event.shelfNo }
val item = list.firstOrNull { it.deviceNo == event.shelfNo }
item?.apply {
goodsId = ""
goodsName = null
@@ -393,6 +428,71 @@ class HomeActivity : BaseActivity() {
createTime = null
overdueDay = null
}
shelfAdapter.notifyItemChanged(homeViewModel.shelfItems.indexOf(item))
shelfAdapter.notifyItemChanged(list.indexOf(item))
}
private val taskExecutor by lazy { IntervalExecutor() }
private var saveTaskJob: Job? = null
/**
* 保存定时任务
*/
private fun saveGoodsTask() {
saveTaskJob =
taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) {
syncShelfGoodsToServer()
}
}
private var overdueTaskJob: Job? = null
/**
* 过期时间定时任务
*/
private fun overdueTask() {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) {
updateOverdueState()
}
}
private fun updateOverdueState() {
viewModel.updateOverdueState()
}
/**
* 将接口返回的过期状态应用到本地列表,仅在状态变化时刷新对应 item
*/
private fun applyOverdueState(items: List<ShelfModel>) {
if (items.size != list.size) return
list.forEachIndexed { index, model ->
// 接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) {
model.overdue = items[index].overdue
shelfAdapter.notifyItemChanged(index)
log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}")
}
}
}
/**
* 构建 ShelfBody 并提交到服务端(被定时任务与 ShelfActivity 返回共用)
*/
private fun syncShelfGoodsToServer() {
log("performSync显示list:${list.toJsonString()}")
val submitList = GridLayoutTool.submitListSort(list)
submitList.forEach {
if (it.goodsId.isNullOrBlank()) {
it.weight = it.weightBak
}
}
val body = ShelfBody().also {
it.deviceId = App.deviceId ?: ""
it.canteenId = App.canteenId
it.temperature = showTemperatureC
it.humidity = showHumidity
it.goodsList = submitList
}
log("performSync提交body:${body.toJsonString()}")
viewModel.saveShelfGoodsList(body)
}
}
@@ -1,13 +1,22 @@
package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.KeyEvent
import android.view.animation.RotateAnimation
import androidx.activity.viewModels
import androidx.core.graphics.toColorInt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalData
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityInitBinding
import com.shuwei.intelligent.shelves.model.DeviceConfigInfo
@@ -15,6 +24,7 @@ import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.UiState
import com.shuwei.intelligent.shelves.net.UrlConfig
import com.shuwei.intelligent.shelves.utils.AppUtil
import com.shuwei.intelligent.shelves.utils.NetworkUtils
import com.shuwei.intelligent.shelves.utils.QRCodeUtil
import com.shuwei.intelligent.shelves.utils.SpTool
import com.shuwei.intelligent.shelves.utils.ext.dp
@@ -39,6 +49,9 @@ class InitActivity : BaseActivity() {
private lateinit var binding: ActivityInitBinding
private val viewModel: NetViewModel by viewModels()
private val handler = Handler(Looper.getMainLooper())
private var startTime = 0L // 倒计时开始时间
private var lastNetworkCheckTime = 0L // 上次检测网络的时间
@SuppressLint("HardwareIds")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,7 +64,12 @@ class InitActivity : BaseActivity() {
// App.deviceId = "4787e213-90ab-3e32-88e0-ac271a937751"
Log.d(TAG, "onCreate: deviceId = ${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"
// var deviceId = AppUtil.getUDID(this)
@@ -73,7 +91,8 @@ class InitActivity : BaseActivity() {
binding.ivQrCode.invisible()
binding.btnInit.invisible()
countDown()
// countDown()
initViews()
}
private fun countDown() {
@@ -131,7 +150,7 @@ class InitActivity : BaseActivity() {
state.data.data?.let {
if (it is DeviceConfigInfo) {
SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString())
App.configUrl = it.appPackageUrl?:""
GlobalData.appBaseUrl = it.appPackageUrl?:""
App.canteenId = it.canteenId?:""
startActivity<HomeActivity>()
@@ -156,9 +175,147 @@ class InitActivity : BaseActivity() {
if (config == null) {
return false
}
App.configUrl = config.appPackageUrl?:""
GlobalData.appBaseUrl = config.appPackageUrl?:""
App.canteenId = config.canteenId?:""
return true
}
/**
* 初始化控件事件
*/
private fun initViews() {
// 连接网络按钮点击事件
binding.btnConnectNetwork.setOnClickListener {
openNetworkSettings()
}
// 启动加载动画 - 持续旋转
startLoadingAnimation()
}
/**
* 启动加载动画 - ImageView 持续旋转
*/
private fun startLoadingAnimation() {
val rotateAnimation = RotateAnimation(
0f, 360f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f
).apply {
duration = 1200 // 旋转周期 1.2 秒
repeatCount = RotateAnimation.INFINITE // 无限循环
repeatMode = RotateAnimation.RESTART
}
binding.ivLoading.startAnimation(rotateAnimation)
}
override fun onResume() {
super.onResume()
if (NetworkUtils.isNetworkConnected(this)) {
navigateToHome()
return
}
startCountdown()
}
override fun onPause() {
super.onPause()
// 页面不可见时停止倒计时
handler.removeCallbacksAndMessages(null)
}
/**
* 启动 60 秒倒计时
*/
private fun startCountdown() {
startTime = SystemClock.elapsedRealtime()
lastNetworkCheckTime = 0
binding.llNetwork.setBackgroundColor(Color.TRANSPARENT)
binding.llLoading.visibility = android.view.View.VISIBLE
binding.llNetworkButton.visibility = android.view.View.GONE
binding.tvCountdown.text = "60秒"
scheduleCountdown()
}
/**
* 定时更新倒计时
* 使用 SystemClock.elapsedRealtime() 确保精确计时
*/
private fun scheduleCountdown() {
handler.postDelayed({
val elapsedTime = SystemClock.elapsedRealtime() - startTime
val remainingTime = 60 - (elapsedTime / 1000).toInt()
val currentNetworkCheckTime = (elapsedTime / 1000).toInt()
// 每 10 秒检测一次网络
if (currentNetworkCheckTime > 0 && currentNetworkCheckTime % 10 == 0 && currentNetworkCheckTime != lastNetworkCheckTime.toInt()) {
lastNetworkCheckTime = currentNetworkCheckTime.toLong()
checkNetworkConnection()
}
// 倒计时未结束,继续更新 UI
if (remainingTime > 0) {
binding.tvCountdown.text = "${remainingTime}"
scheduleCountdown()
} else {
// 倒计时结束,最后检测一次网络
if (NetworkUtils.isNetworkConnected(this)) {
navigateToHome()
} else {
// 网络未连接,显示"连接网络"按钮
showNetworkButton()
}
}
}, 100) // 每 100ms 检查一次,确保精确性
}
/**
* 检测网络连接
*/
private fun checkNetworkConnection() {
if (NetworkUtils.isNetworkConnected(this)) {
// 网络连接成功,停止倒计时并跳转到 HomeActivity
handler.removeCallbacksAndMessages(null)
navigateToHome()
}
}
/**
* 跳转到 HomeActivity
*/
private fun navigateToHome() {
startActivity<HomeActivity>()
finish()
}
/**
* 显示"连接网络"按钮
*/
private fun showNetworkButton() {
binding.llNetwork.setBackgroundColor("#5C77F7".toColorInt())
binding.llLoading.visibility = android.view.View.GONE
binding.llNetworkButton.visibility = android.view.View.VISIBLE
}
/**
* 打开系统网络设置页面
*/
private fun openNetworkSettings() {
val intent = Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)
startActivity(intent)
}
/**
* 禁用返回键
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return if (keyCode == KeyEvent.KEYCODE_BACK) {
// 禁用返回键,不执行任何操作
true
} else {
super.onKeyDown(keyCode, event)
}
}
}
@@ -1,28 +1,53 @@
package com.shuwei.intelligent.shelves.activity
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.shuwei.intelligent.shelves.adapter.LogAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityLogListBinding
import com.shuwei.intelligent.shelves.dialog.CommonDialog
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.utils.ext.visible
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class LogActivity : BaseActivity() {
companion object {
const val IS_LOG_DIR = "isLogDir"
const val LOG_DATE = "logDate"
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
}
/**
* 覆写时钟回调:左侧状态栏显示年月日星期,右侧状态栏显示时分秒
*/
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val arr = sdf.format(Date()).split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
}
private lateinit var binding:ActivityLogListBinding
private var isLogDir = false
private var logDate = ""
private val logList = mutableListOf<String>()
private val originalLogList = mutableListOf<String>() // 原始日志备份
private val logAdapter by lazy {
LogAdapter(logList).apply {
setOnItemClickListener { _, _, position ->
@@ -62,24 +87,20 @@ class LogActivity : BaseActivity() {
logAdapter.notifyDataSetChanged()
}
if (isLogDir) {
logList.addAll(fileLogger.getLogFiles())
} else {
logList.addAll(fileLogger.readLogsByName(logDate))
}
binding.rvLogList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = logAdapter
}
setupSearch()
setupJumpButton()
binding.btnFind.run {
if (isLogDir) gone() else visible()
}
binding.btnFind.setOnClickListener {
if (isLogDir) {
return@setOnClickListener
}
val list = logList.filter { it.contains("${HomeActivity.HEADER}1001") }
if (isLogDir) return@setOnClickListener
val list = logList.filter { it.contains("${ProtocolConstants.HEADER}${ProtocolConstants.CMD_TEMP_REPORT}") }
if (list.isEmpty()) {
toast("未找到到1001指令")
return@setOnClickListener
@@ -88,6 +109,95 @@ class LogActivity : BaseActivity() {
logList.addAll(list)
logAdapter.notifyDataSetChanged()
}
// 后台加载日志数据
lifecycleScope.launch {
binding.progressBar.visible()
val data = withContext(Dispatchers.IO) {
if (isLogDir) fileLogger.getLogFiles()
else fileLogger.readLogsByName(logDate)
}
logList.addAll(data)
originalLogList.addAll(data)
logAdapter.notifyDataSetChanged()
binding.progressBar.gone()
setupTimeNavigation()
}
}
// 搜索功能
private fun setupSearch() {
binding.etSearch.addTextChangedListener(afterTextChanged = { s ->
val keyword = s?.toString() ?: ""
logList.clear()
logList.addAll(if (keyword.isEmpty()) originalLogList else originalLogList.filter { it.contains(keyword, ignoreCase = true) })
logAdapter.notifyDataSetChanged()
})
}
// 快速跳转功能
private fun setupJumpButton() {
var isAtTop = true
binding.fabJump.setOnClickListener {
if (isAtTop) {
binding.rvLogList.scrollToPosition(logList.size - 1)
} else {
binding.rvLogList.scrollToPosition(0)
}
isAtTop = !isAtTop
}
}
// 时间导航功能
private fun setupTimeNavigation() {
if (isLogDir) {
binding.hsvTimeNav.gone()
return
}
val timeMap = mutableMapOf<String, Int>()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
originalLogList.forEachIndexed { index, log ->
val timeStr = log.substringAfter("[").substringBefore("]")
try {
val date = dateFormat.parse(timeStr)
date?.let {
val hourKey = SimpleDateFormat("HH:00", Locale.getDefault()).format(it)
if (!timeMap.containsKey(hourKey)) {
timeMap[hourKey] = index
}
}
} catch (e: Exception) {
// 忽略解析失败的日志
}
}
if (timeMap.isEmpty()) {
binding.hsvTimeNav.gone()
return
}
timeMap.toSortedMap().forEach { (hour, position) ->
val btn = TextView(this).apply {
text = hour
textSize = 14f
setTextColor(Color.BLACK)
setPadding(12.dp, 8.dp, 12.dp, 8.dp)
gravity = Gravity.CENTER
setBackgroundColor(Color.parseColor("#E0E0E0"))
setOnClickListener {
binding.rvLogList.scrollToPosition(position)
}
}
val params = android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
android.widget.LinearLayout.LayoutParams.MATCH_PARENT
).apply {
marginEnd = 8.dp
}
binding.llTimeNav.addView(btn, params)
}
}
}
@@ -1,66 +1,66 @@
package com.shuwei.intelligent.shelves.activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivitySettingBinding
import com.shuwei.intelligent.shelves.utils.KeyboardUtil
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class SettingActivity: BaseActivity() {
private lateinit var binding: ActivitySettingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySettingBinding.inflate(layoutInflater)
setBackground()
setContentView(binding.root)
binding.btnSendCmd.setOnClickListener{
val cmd = binding.etInputCmd.text.toString().trim()
if (cmd.isBlank()) {
return@setOnClickListener
}
var record = binding.tvCmdRecord.text.toString().trim()
record = cmd+"\n"+record
binding.tvCmdRecord.text = record
val realCmd = "${HomeActivity.HEADER}0F01${cmd}${HomeActivity.FOOTER}"
sendCmd(realCmd)
}
binding.ivBack.setOnClickListener { finish() }
binding.root.setOnClickListener { v ->
KeyboardUtil.hideKeyboard(v)
}
}
private fun updateDateTime() {
val sdf = SimpleDateFormat(ShelfActivity.YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val dateTime = sdf.format(Date())
val arr = dateTime.split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
}
private val handler = Handler(Looper.getMainLooper())
private val updateTask = object : Runnable {
override fun run() {
updateDateTime()
handler.postDelayed(this, 1000)
}
}
override fun onResume() {
super.onResume()
hideStatusBar()
handler.post(updateTask)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(updateTask)
}
}
//package com.shuwei.intelligent.shelves.activity
//
//import android.os.Bundle
//import android.os.Handler
//import android.os.Looper
//import com.shuwei.intelligent.shelves.base.BaseActivity
//import com.shuwei.intelligent.shelves.databinding.ActivitySettingBinding
//import com.shuwei.intelligent.shelves.utils.KeyboardUtil
//import java.text.SimpleDateFormat
//import java.util.Date
//import java.util.Locale
//
//class SettingActivity: BaseActivity() {
//
// private lateinit var binding: ActivitySettingBinding
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// binding = ActivitySettingBinding.inflate(layoutInflater)
// setBackground()
// setContentView(binding.root)
// binding.btnSendCmd.setOnClickListener{
// val cmd = binding.etInputCmd.text.toString().trim()
// if (cmd.isBlank()) {
// return@setOnClickListener
// }
// var record = binding.tvCmdRecord.text.toString().trim()
// record = cmd+"\n"+record
// binding.tvCmdRecord.text = record
// val realCmd = "${HomeActivity.HEADER}0F01${cmd}${HomeActivity.FOOTER}"
// sendCmd(realCmd)
// }
// binding.ivBack.setOnClickListener { finish() }
// binding.root.setOnClickListener { v ->
// KeyboardUtil.hideKeyboard(v)
// }
// }
//
//
// private fun updateDateTime() {
// val sdf = SimpleDateFormat(ShelfActivity.YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
// val dateTime = sdf.format(Date())
// val arr = dateTime.split("***")
// updateLeftStatus(arr[0])
// updateRightStatus(arr[1])
// }
//
// private val handler = Handler(Looper.getMainLooper())
// private val updateTask = object : Runnable {
// override fun run() {
// updateDateTime()
// handler.postDelayed(this, 1000)
// }
// }
//
// override fun onResume() {
// super.onResume()
// hideStatusBar()
// handler.post(updateTask)
// }
//
// override fun onPause() {
// super.onPause()
// handler.removeCallbacks(updateTask)
// }
//
//}
@@ -3,8 +3,6 @@ package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.LinearLayout
import androidx.activity.viewModels
@@ -18,7 +16,7 @@ import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.scwang.smart.refresh.layout.constant.RefreshState
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.DEVICE_INFO_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.adapter.SearchAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityShelfBinding
@@ -148,7 +146,7 @@ class ShelfActivity : BaseActivity() {
//binding.tvFoodWeight.text = "0克"
EventBus.getDefault().post(ClearShelfEvent(shelfModel!!.deviceNo, tempWeight))
}
sendCmd(DEVICE_INFO_CMD)
sendCmd(ProtocolConstants.DEVICE_INFO_CMD)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.getGoodsListUiState.collect { state ->
@@ -305,31 +303,23 @@ class ShelfActivity : BaseActivity() {
}
}
private fun updateDateTime() {
/**
* 覆写时钟回调:左侧状态栏显示年月日星期,右侧状态栏显示时分秒
*/
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val dateTime = sdf.format(Date())
val arr = dateTime.split("***")
val arr = sdf.format(Date()).split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
}
private val handler = Handler(Looper.getMainLooper())
private val updateTask = object : Runnable {
override fun run() {
updateDateTime()
handler.postDelayed(this, 1000)
}
}
override fun onResume() {
super.onResume()
hideStatusBar()
handler.post(updateTask)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(updateTask)
}
@SuppressLint("NotifyDataSetChanged")
@@ -371,20 +361,20 @@ class ShelfActivity : BaseActivity() {
val shelfNo = shelfModel?.deviceNo ?: 0
val hex = shelfNo.toString(16).padStart(2, '0').uppercase()
Log.d(TAG, "onCreate: hex=$hex")
val zeroClearingCmd = "${HomeActivity.HEADER}0401${hex}${HomeActivity.FOOTER}"
val zeroClearingCmd = "${ProtocolConstants.HEADER}0401${hex}${ProtocolConstants.FOOTER}"
log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}")
sendCmd(zeroClearingCmd)
sendCmd(
if (shelfNo in 1..5) HomeActivity.LEFT_SHELF_OPEN_CMD else HomeActivity.RIGHT_SHELF_OPEN_CMD
)
// sendCmd(
// if (shelfNo in 1..5) ProtocolConstants.LEFT_SHELF_OPEN_CMD else ProtocolConstants.RIGHT_SHELF_OPEN_CMD
// )
//清零
realWeight = 0
// binding.tvFoodWeight.text = "${realWeight}克"
sendCmd(DEVICE_INFO_CMD)
sendCmd(ProtocolConstants.DEVICE_INFO_CMD)
startTime = System.currentTimeMillis()
Loading.show(this@ShelfActivity)
// window.decorView.postDelayed({ Loading.dismiss() }, 5000)
window.decorView.postDelayed({ Loading.dismiss() }, 10000)
}
KeyboardUtil.hideKeyboard(window.decorView)
}
@@ -5,27 +5,35 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
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.gyf.immersionbar.BarHide
import com.gyf.immersionbar.ImmersionBar
import com.shuwei.intelligent.shelves.activity.HomeActivity
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_CLOSE
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_OPEN
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.activity.LogActivity
import com.shuwei.intelligent.shelves.activity.SettingActivity
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.utils.FileLogger
import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import kotlinx.coroutines.launch
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.shuwei.intelligent.shelves.utils.ext.setOnDoubleClickListener
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Suppress("DEPRECATION")
open class BaseActivity : AppCompatActivity() {
val TAG = this.javaClass.simpleName
@@ -46,6 +54,7 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
hideSystemBars()
binding = ActivityBaseBinding.inflate(layoutInflater)
setContentView(binding.root)
statusBarDarkFont()
@@ -56,31 +65,28 @@ open class BaseActivity : AppCompatActivity() {
}
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// window.setDecorFitsSystemWindows(false)
// window.insetsController?.apply {
// hide(WindowInsets.Type.statusBars()) // 隐藏状态栏
// systemBarsBehavior =
// WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // 滑动时临时显示
// }
// }
lightSwitchClick()
binding.tvLeftStatus.setOnDoubleClickListener {
//finish()
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
binding.tvRightStatus.setOnDoubleClickListener {
if (enableRightStatusMenu) showRightStatusPopup(binding.tvRightStatus)
}
}
fun hideStatusBar() {
// enableEdgeToEdge()
// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选)
// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
/** 是否允许右侧状态栏双击弹出菜单,默认关闭,仅 HomeActivity 开启 */
protected open val enableRightStatusMenu: Boolean = false
ImmersionBar.with(this)
.hideBar(BarHide.FLAG_HIDE_STATUS_BAR)
.init()
}
fun hideStatusBar() {}
fun statusBarDarkFont() {
//window.decorView.systemUiVisibility = if (enable) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0
val color = ContextCompat.getColor(this, R.color.white)
binding.tvLeftStatus.setTextColor(color)
binding.tvRightStatus.setTextColor(color)
@@ -99,7 +105,7 @@ open class BaseActivity : AppCompatActivity() {
}
fun updateLeftStatus(data: String) {
binding.tvLeftStatus.text = data
binding.tvLeftStatus.text = data.ifBlank { " " }
}
fun updateRightStatus(data: String) {
@@ -110,11 +116,35 @@ open class BaseActivity : AppCompatActivity() {
private val TIME_OUT: Long = (5 * 60 * 1000).toLong()
private val mHandler by lazy { Handler(Looper.getMainLooper()) }
private val mTimeoutRunnable = Runnable {
if (this is HomeActivity) {
this.switchLight(false)
// 用户长时间无操作时回调,子类可覆写实现具体逻辑(如关灯)
onUserInactive()
}
// ---- 右侧状态栏时钟 ----
// 默认时间格式:月日星期 时:分:秒
private val CLOCK_FORMAT = "MM月dd日 EEEE HH:mm:ss"
private val clockFormatter by lazy { SimpleDateFormat(CLOCK_FORMAT, Locale.CHINA) }
/**
* 每秒时钟回调,默认将当前时间显示在右侧状态栏。
* 子类可覆写以自定义格式或同时更新左侧状态栏。
*/
open fun onClockTick() {
updateRightStatus(clockFormatter.format(Date()))
}
private val clockTask = object : Runnable {
override fun run() {
onClockTick()
mHandler.postDelayed(this, 1000)
}
}
/**
* 用户超时无操作时回调,默认空实现,子类按需覆写
*/
open fun onUserInactive() {}
override fun onUserInteraction() {
super.onUserInteraction()
//开灯
@@ -131,14 +161,18 @@ open class BaseActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// 启动定时器
// 启动超时定时器
mHandler.postDelayed(mTimeoutRunnable, TIME_OUT)
// 启动右侧状态栏时钟
mHandler.post(clockTask)
}
override fun onPause() {
super.onPause()
// 取消定时器
// 取消超时定时器
mHandler.removeCallbacks(mTimeoutRunnable)
// 停止右侧状态栏时钟
mHandler.removeCallbacks(clockTask)
}
fun switchLight(isOpen: Boolean) {
@@ -156,15 +190,27 @@ open class BaseActivity : AppCompatActivity() {
val tag = binding.tvLeftStatus.tag?.toString()?.toBoolean() ?: true
switchLight(tag.not())
}
binding.tvRightStatus.clickWithDebounce {
if (this is HomeActivity) {
startActivity<LogActivity> {
putExtra(LogActivity.IS_LOG_DIR, true)
}
return@clickWithDebounce
}
startActivity<SettingActivity> { }
}
private fun showRightStatusPopup(anchor: View) {
val menuBinding = PopupRightStatusMenuBinding.inflate(LayoutInflater.from(this))
val popup = PopupWindow(
menuBinding.root,
120.dp,
101.dp,
true
)
menuBinding.tvEnvSwitch.setOnClickListener {
popup.dismiss()
EnvSwitchDialog(this).show()
}
menuBinding.tvLog.setOnClickListener {
popup.dismiss()
startActivity<LogActivity> {
putExtra(LogActivity.IS_LOG_DIR, true)
}
}
popup.showAsDropDown(anchor)
}
@@ -181,4 +227,34 @@ open class BaseActivity : AppCompatActivity() {
fileLogger.log(message)
}
/**
* 窗口焦点变化时回调
* 场景:弹出 Dialog 后焦点丢失,Dialog 消失后焦点恢复,此时需重新隐藏系统栏,
* 避免系统栏在 Dialog 关闭后意外"复活"
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideSystemBars()
}
}
/**
* 隐藏状态栏和导航栏
* 使用 WindowInsetsControllerCompatAndroidx 推荐方式,向下兼容性好)
* - 状态栏:完全隐藏
* - 导航栏:完全隐藏,底部上滑时临时出现,松手后自动收回(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
*/
private fun hideSystemBars() {
// 允许布局延伸到系统栏区域(edge-to-edge),避免内容被系统栏遮挡
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).apply {
// 同时隐藏状态栏和导航栏
hide(WindowInsetsCompat.Type.systemBars())
// 底部上滑时临时显示导航栏,松手后自动隐藏(沉浸式 Sticky 模式)
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@@ -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()
}
}
}
@@ -1,214 +0,0 @@
package com.shuwei.intelligent.shelves.net
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfBody
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
/**
* HomeActivity 专用 ViewModel。
*
* 职责:
* 1. 管理货架列表、重量、温湿度等 UI 状态数据;
* 2. 持有 deviceName,供 Activity 更新顶部状态栏;
* 3. 管理定时任务(保存货架数据、刷新过期状态),生命周期由 viewModelScope 保障,
* 跨配置变更存活,Activity 销毁时自动取消,无需手动持有 Job 引用。
*/
class HomeViewModel : ViewModel() {
companion object {
private const val TAG = "HomeViewModel"
/** 首次保存延迟(1 分钟) */
private const val SAVE_INITIAL_DELAY = 1 * 60 * 1000L
/** 保存间隔(5 分钟) */
private const val SAVE_INTERVAL = 5 * 60 * 1000L
/** 过期检查首次延迟(5 秒) */
private const val OVERDUE_INITIAL_DELAY = 5 * 1000L
/** 过期检查间隔(5 分钟) */
private const val OVERDUE_INTERVAL = 5 * 60 * 1000L
}
// ----------------------------------------------------------------
// UI 状态数据
// ----------------------------------------------------------------
/**
* 货架列表。
* Adapter 持有同一个列表引用,修改后需调用 notifyDataSetChanged / notifyItemChanged 触发刷新。
*/
val shelfItems: MutableList<ShelfModel> = mutableListOf()
/**
* 重量数据缓存。
* keydeviceNo(货架编号),value:重量(克)。
*/
private val weightMap: MutableMap<Int, Int> = mutableMapOf()
/**
* 当前选中货架的列表索引(点击 item 时更新)。
*/
var selectedShelfIndex: Int = 0
/**
* 当前摄氏温度,由串口心跳数据更新,字符串格式(如 "24.5")。
*/
var temperatureC: String = "0"
/**
* 当前湿度,由串口心跳数据更新,字符串格式(如 "60")。
*/
var humidity: String = "0"
/**
* 设备名称,由网络接口响应写入,供顶部状态栏显示。
* 原散落于 HomeActivity 局部变量,现迁入 ViewModel 以跨配置变更保持。
*/
var deviceName: String = ""
// ----------------------------------------------------------------
// 对外暴露的 Flow
// ----------------------------------------------------------------
/**
* 过期状态变化通知。
* 发射需要局部刷新的 adapter position 列表,Activity 收到后调用 notifyItemChanged。
* 仅在状态实际发生变化时发射,避免无效刷新。
*/
private val _overdueUpdateFlow = MutableSharedFlow<List<Int>>(extraBufferCapacity = 4)
val overdueUpdateFlow: SharedFlow<List<Int>> = _overdueUpdateFlow
// ----------------------------------------------------------------
// 重量操作
// ----------------------------------------------------------------
/**
* 更新指定货架编号的重量。
* @param deviceNo 货架编号
* @param weight 重量(克)
*/
fun putWeight(deviceNo: Int, weight: Int) {
weightMap[deviceNo] = weight
}
/**
* 获取指定货架编号的重量。
* @param deviceNo 货架编号
* @return 重量(克),若无记录返回 0
*/
fun getWeight(deviceNo: Int): Int = weightMap[deviceNo] ?: 0
// ----------------------------------------------------------------
// 定时任务
// ----------------------------------------------------------------
/**
* 启动所有定时任务。
* 应在 Activity.onCreate 中调用一次(ViewModel 跨配置变更存活,不会重复启动)。
* 任务在 viewModelScope 中运行,ViewModel 销毁时自动取消,无需手动管理 Job。
*/
fun startPeriodicTasks() {
startSaveTask()
startOverdueTask()
}
/**
* 启动货架数据定时保存任务(延迟1分钟后,每5分钟执行一次)。
*/
private fun startSaveTask() {
viewModelScope.launch {
delay(SAVE_INITIAL_DELAY)
while (true) {
periodicSave()
delay(SAVE_INTERVAL)
}
}
}
/**
* 启动过期状态定时检查任务(延迟5秒后,每5分钟执行一次)。
*/
private fun startOverdueTask() {
viewModelScope.launch {
delay(OVERDUE_INITIAL_DELAY)
while (true) {
periodicUpdateOverdue()
delay(OVERDUE_INTERVAL)
}
}
}
// ----------------------------------------------------------------
// 定时任务内部业务逻辑
// ----------------------------------------------------------------
/**
* 将当前货架数据同步到服务端。
* 若货架无商品(goodsId 为空),则上报备份重量(weightBak)。
*/
private suspend fun periodicSave() {
Log.d(TAG, "periodicSave: list=${shelfItems.toJsonString()}")
val submitList = shelfItems.sortedBy { it.deviceNo }
submitList.forEach {
if (it.goodsId.isNullOrBlank()) {
it.weight = it.weightBak
}
}
val body = ShelfBody().also {
it.deviceId = App.deviceId
it.canteenId = App.canteenId
it.temperature = temperatureC
it.humidity = humidity
it.goodsList = submitList
}
Log.d(TAG, "periodicSave: body=${body.toJsonString()}")
runCatching {
val resp = apiService.saveShelfGoodsList(body = body)
Log.d(TAG, "periodicSave: resp=${resp.toJsonString()}")
}.onFailure {
it.printStackTrace()
Log.d(TAG, "periodicSave 异常:${it.message}")
}
}
/**
* 从服务端拉取货架列表,对比本地数据,将变化的过期状态通过 [overdueUpdateFlow] 发射。
* 按 deviceNo 精确匹配,避免列表顺序不一致导致的状态串位。
*/
private suspend fun periodicUpdateOverdue() {
runCatching {
val response = apiService.getShelfList(deviceId = App.deviceId)
if (!response.isSuccess()) return
val serverItems = response.data?.containerGoodsList ?: return
if (serverItems.isEmpty()) return
// 找出过期状态发生变化的 position 列表
val changedPositions = mutableListOf<Int>()
shelfItems.forEachIndexed { index, model ->
val serverModel = serverItems.find { it.deviceNo == model.deviceNo }
?: return@forEachIndexed
if (serverModel.overdue != model.overdue) {
model.overdue = serverModel.overdue
changedPositions.add(index)
Log.d(TAG, "过期状态变更:deviceNo=${model.deviceNo}, 商品=${model.goodsName}, overdue=${model.overdue}")
}
}
if (changedPositions.isNotEmpty()) {
_overdueUpdateFlow.emit(changedPositions)
}
}.onFailure {
it.printStackTrace()
}
}
}
@@ -2,7 +2,8 @@ package com.shuwei.intelligent.shelves.net
import android.annotation.SuppressLint
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.OkHttpClient
import okhttp3.Response
@@ -18,7 +19,7 @@ import kotlin.apply
val apiService: ApiService = Retrofit.Builder()
// .baseUrl(UrlConfig.BASE_URL)
.baseUrl(UrlConfig.DEVICE_BASE_URL)
.baseUrl(GlobalData.appBaseUrl)
.client(HttpManager.instance.client)
.addConverterFactory(GsonConverterFactory.create())
.build()
@@ -28,9 +29,9 @@ class HttpManager private constructor() {
val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.apply {
connectTimeout(15, TimeUnit.SECONDS)
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
writeTimeout(30, TimeUnit.SECONDS)
sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
hostnameVerifier { _, _ -> true }
// addInterceptor(LoggingInterceptor())
@@ -38,11 +39,11 @@ class HttpManager private constructor() {
Log.d("HttpManager","okhttp logger ==>${it}")
}).apply {
level =
// if (MyApp.DEBUG) {
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
// } else {
// HttpLoggingInterceptor.Level.NONE
// }
} else {
HttpLoggingInterceptor.Level.NONE
}
})
// addInterceptor { chain ->
// val request = chain.request().newBuilder()
@@ -5,31 +5,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfBody
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.net.HttpManager.Companion.instance
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class NetViewModel : ViewModel() {
// val apiService: ApiService = HttpManager.instance.apiService
// val apiService: ApiService = Retrofit.Builder()
// .baseUrl(UrlConfig.BASE_URL)
// .client(HttpManager.instance.client)
// .addConverterFactory(GsonConverterFactory.create())
// .build()
// .create(ApiService::class.java)
private val _getShelfListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getShelfListUiState: StateFlow<UiState> = _getShelfListUiState
// private val _saveShelfGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
// val saveShelfGoodsListUiState: StateFlow<UiState> = _saveShelfGoodsListUiState
private val _getGoodsListUiState = MutableStateFlow<UiState>(UiState.Initial)
val getGoodsListUiState: StateFlow<UiState> = _getGoodsListUiState
@@ -42,6 +27,8 @@ class NetViewModel : ViewModel() {
private val _getDeviceConfigUiState = MutableStateFlow<UiState>(UiState.Initial)
val getDeviceConfigUiState: StateFlow<UiState> = _getDeviceConfigUiState
private val _updateOverdueStateUiState = MutableStateFlow<UiState>(UiState.Initial)
val updateOverdueStateUiState: StateFlow<UiState> = _updateOverdueStateUiState
fun getDeviceToken(deviceId: String) {
viewModelScope.launch {
@@ -61,17 +48,16 @@ class NetViewModel : ViewModel() {
fun getDeviceConfig(deviceId: String, deviceToken: String) {
viewModelScope.launch {
_getDeviceTokenUiState.value = UiState.Loading
_getDeviceConfigUiState.value = UiState.Loading
runCatching {
val response =
apiService.getDeviceInfo(equipmentCode = deviceId)
val response = apiService.getDeviceInfo(equipmentCode = deviceId)
if (response.isSuccess()) {
_getDeviceTokenUiState.value = UiState.Success(response)
_getDeviceConfigUiState.value = UiState.Success(response)
} else {
_getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败")
_getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_getDeviceTokenUiState.value = UiState.Error(it.message ?: "请求异常")
_getDeviceConfigUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
@@ -108,37 +94,21 @@ class NetViewModel : ViewModel() {
}
}
fun updateOverdueState(block:(Boolean, List<ShelfModel>?)-> Unit) {
fun updateOverdueState() {
viewModelScope.launch {
runCatching {
val response = apiService.getShelfList(deviceId = App.deviceId)
if (response.isSuccess()) {
block(true, response.data?.containerGoodsList)
_updateOverdueStateUiState.value = UiState.Success(response)
} else {
block(false, emptyList())
_updateOverdueStateUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
block(false, null)
_updateOverdueStateUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
// fun saveShelfGoodsList(body: ShelfBody) {
// viewModelScope.launch {
// _saveShelfGoodsListUiState.value = UiState.Loading
// runCatching {
// val response = apiService.saveShelfGoodsList(body)
// if (response.success) {
// _saveShelfGoodsListUiState.value = UiState.Success(response)
// } else {
// _saveShelfGoodsListUiState.value = UiState.Error(response.message ?: "请求失败")
// }
// }.onFailure {
// _saveShelfGoodsListUiState.value = UiState.Error(it.message ?: "请求异常")
// }
// }
// }
fun getGoodsList(
canteenId: String = "",
goodsName: String = "",
@@ -148,9 +118,6 @@ class NetViewModel : ViewModel() {
viewModelScope.launch {
_getGoodsListUiState.value = UiState.Loading
runCatching {
if (App.configUrl.isNullOrBlank()) {
App.configUrl = UrlConfig.BASE_URL
}
val response = apiService.getGoodsList(
canteenId = canteenId,
goodsName = goodsName,
@@ -178,7 +145,5 @@ class NetViewModel : ViewModel() {
Log.d("NetViewModel", "saveShelfGoodsList异常:${it.message}")
}
}
}
}
@@ -1,157 +0,0 @@
package com.shuwei.intelligent.shelves.net
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.serial.SerialEventListener
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.serial.SerialProtocolParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
/**
* 串口通信 ViewModel。
* 职责:
* 1. 管理串口的生命周期(open / close);
* 2. 持有 [SerialProtocolParser] 实例,实现 [SerialEventListener] 接收解析结果;
* 3. 通过 [weightFlow]、[tempCtrlEventFlow]、[deviceStatusRefreshFlow] 将事件暴露给 UI 层。
*
* 不持有任何 View / Context 引用,可安全跨配置变更存活。
*/
class SerialViewModel : ViewModel() {
companion object {
private const val TAG = "SerialViewModel"
}
// ----------------------------------------------------------------
// 对外暴露的 FlowActivity 通过 repeatOnLifecycle 收集)
// ----------------------------------------------------------------
/**
* 货架重量更新事件。
* Pair.first = 货架编号(deviceNo),Pair.second = 重量(克)。
*/
private val _weightFlow = MutableSharedFlow<Pair<Int, Int>>(extraBufferCapacity = 64)
val weightFlow: SharedFlow<Pair<Int, Int>> = _weightFlow
/**
* 温控信息上报事件,携带完整日志字符串,Activity 收到后弹窗展示。
*/
private val _tempCtrlEventFlow = MutableSharedFlow<String>(extraBufferCapacity = 8)
val tempCtrlEventFlow: SharedFlow<String> = _tempCtrlEventFlow
/**
* 设备状态刷新信号。
* 串口收到需要刷新顶部状态栏的命令时触发,Activity 收到后用 deviceName 重绘。
*/
private val _deviceStatusRefreshFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 4)
val deviceStatusRefreshFlow: SharedFlow<Unit> = _deviceStatusRefreshFlow
// ----------------------------------------------------------------
// 内部:串口数据通道 + 协议解析器
// ----------------------------------------------------------------
/**
* 串口数据接收通道,无界缓冲。
* IO 线程(串口回调)通过 trySend 写入,单一消费协程串行读取,
* 确保 parser 内部 serialReader 始终在同一协程中访问,规避并发竞争。
*/
private val serialDataChannel = Channel<String>(Channel.UNLIMITED)
/**
* 协议解析器实例。
* 持有 terminalId / terminalVersion / serialReader 等协议状态。
*/
private val parser = SerialProtocolParser(object : SerialEventListener {
/** 解析器需要发送命令时,通过 viewModelScope 异步发送 */
override fun onSendCommand(cmd: String) {
viewModelScope.launch { SerialPortManager.send(cmd) }
}
/** 终端信息就绪(激活或设备状态上报时),仅打印日志,信息已在 parser 内持有 */
override fun onTerminalInfoReady(terminalId: String, terminalVersion: String) {
Log.d(TAG, "终端信息就绪: id=$terminalId, version=$terminalVersion")
}
/** 货架重量更新,发射到 weightFlow */
override fun onWeightUpdate(deviceNo: Int, weight: Int) {
_weightFlow.tryEmit(deviceNo to weight)
}
/** 设备状态需要刷新,发射刷新信号 */
override fun onDeviceStatusUpdate(text: String) {
_deviceStatusRefreshFlow.tryEmit(Unit)
}
/** 温控信息上报,发射日志到 tempCtrlEventFlow */
override fun onTempCtrlReport(logMsg: String) {
_tempCtrlEventFlow.tryEmit(logMsg)
}
})
// ----------------------------------------------------------------
// 对外操作
// ----------------------------------------------------------------
/**
* 打开串口并启动数据接收。
* 应在 Activity.onCreate 中调用一次。
* 串口数据通过 Channel 无锁传递给消费协程,由消费协程串行调用 parser。
*/
fun openSerial() {
viewModelScope.launch {
val opened = SerialPortManager.open()
Log.d(TAG, "openSerial: opened=$opened")
if (!opened) return@launch
// 串口回调在 IO 线程,通过 Channel 传递数据,不嵌套 launch
SerialPortManager.startReceive { data ->
serialDataChannel.trySend(data)
}
// 单一消费协程,串行处理所有串口数据
launch {
for (data in serialDataChannel) {
try {
parser.parseIncoming(data)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
/**
* 向串口发送命令。
* @param cmd 十六进制命令字符串
*/
fun sendCmd(cmd: String) {
viewModelScope.launch {
SerialPortManager.send(cmd)
}
}
// ----------------------------------------------------------------
// 生命周期清理
// ----------------------------------------------------------------
/**
* ViewModel 销毁时关闭 Channel 和串口。
* Channel 关闭后,消费协程的 for 循环自然结束。
* 串口关闭在独立 IO 协程中执行,避免阻塞主线程。
*/
override fun onCleared() {
serialDataChannel.close()
CoroutineScope(Dispatchers.IO).launch {
SerialPortManager.close()
}
super.onCleared()
}
}
@@ -1,6 +1,7 @@
package com.shuwei.intelligent.shelves.net
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalData
object UrlConfig {
// 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 BASE_URL = "https://dev.yixiong-tech.com:8081"
// const val BASE_URL = "https://yyjk.shuziweidao.com/gateway"
// const val BASE_URL = "http://192.168.1.201:14801"
const val DEVICE_BASE_URL = BASE_URL
// const val BASE_URL = "https://dev.yixiong-tech.com:8081"
//// const val BASE_URL = "https://yyjk.shuziweidao.com/gateway"
//// const val BASE_URL = "http://192.168.1.201:14801"
// 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 =
"${App.configUrl}/terminal/neglect/smartShelves/app/smartShelves/app/getShelvesListByDeviceId"
"${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/getShelvesListByDeviceId"
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 =
"${App.configUrl}/terminal/neglect/smartShelves/app/smartShelves/app/saveShelvesGoodsList"
"${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/saveShelvesGoodsList"
}
@@ -0,0 +1,180 @@
package com.shuwei.intelligent.shelves.serial
import com.shuwei.intelligent.shelves.App
/**
* 串口通信协议常量定义
* 统一管理所有协议头、协议尾、各类指令及帧解析偏移量
*/
object ProtocolConstants {
/** 协议头 */
const val HEADER = "D6DADEDB"
/** 协议尾 */
const val FOOTER = "FFFF"
// ----------------------------------------------------------------
// 指令码(cmd 字段,4字符)
// ----------------------------------------------------------------
/** 终端激活指令 */
const val CMD_ACTIVATE = "0001"
/** 心跳指令(服务端发起) */
const val CMD_HEARTBEAT = "0101"
/** 心跳指令(终端发起) */
const val CMD_HEARTBEAT_TERMINAL = "8101"
/** 开关锁反馈 */
const val CMD_LOCK_FEEDBACK = "0202"
/** 获取设备状态 */
const val CMD_DEVICE_STATUS = "0302"
/** 卡号上发 */
const val CMD_CARD_UPLOAD = "0501"
/** 操作记录上传 */
const val CMD_OPERATION_RECORD = "0B01"
/** 对时 */
const val CMD_TIME_SYNC = "0C01"
/** 终端位置上报 */
const val CMD_POSITION_REPORT = "0E01"
/** 温控信息上报 */
const val CMD_TEMP_REPORT = "1001"
/** 温控信息应答 */
const val CMD_TEMP_RESPONSE = "1002"
/** 温控应答(备用) */
const val CMD_TEMP_RESPONSE_ALT = "0F02"
// ----------------------------------------------------------------
// 帧数据偏移 / 尺寸常量
// ----------------------------------------------------------------
/** 心跳帧中重量数据起始偏移(相对帧起始) */
const val HEARTBEAT_WEIGHT_OFFSET = 14
/** 设备状态帧中重量数据起始偏移(含协议头长度) */
const val DEVICE_STATUS_WEIGHT_OFFSET = HEADER.length + 14
/** 货架数量(每侧5格,共10格,协议中12组) */
fun shelfSensorCount(): Int {
return if (device2Columns.contains(App.deviceId)) 12
else if (device3Columns.contains(App.deviceId)) 24
else 0
}
/** 每组重量数据字节数(十六进制字符数) */
const val BYTES_PER_WEIGHT = 8
/** 温度字段字符长度 */
const val TEMPERATURE_FIELD_LEN = 2
/** 心跳帧最小有效长度 */
fun heartbeatMinLength(): Int {
return if (device2Columns.contains(App.deviceId)) 200
else if (device3Columns.contains(App.deviceId)) 296
else 0
}
/** 心跳帧中温度字段起始偏移(经协议分析得出,前面含重量+其他传感器数据) */
fun heartbeatTemperatureStart(): Int {
return if (device2Columns.contains(App.deviceId)) 158
else if (device3Columns.contains(App.deviceId)) 158+96
else 0
}
/** 激活请求体有效长度 */
const val ACTIVATE_BODY_LENGTH = 44
/** 固定数据体有效长度(terminalId 30 + terminalVersion 10 + 其他 2 */
const val FIX_BODY_LENGTH = 42
// ----------------------------------------------------------------
// 完整指令
// ----------------------------------------------------------------
/** 激活指令(1楼餐厅) */
// const val ACTIVE_CMD =
// "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
// 410激活码
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${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}"
/**
* 设备2列,10格
*/
val device2Columns = listOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751"
)
/**
* 设备3列,15格,左侧1列竖向编号1、2、3、4、5,右侧2列5行编号为6-7、8-9、10-11、12-13、14-15
*/
val device3ColumnsOrder = listOf(1, 6, 7, 2, 8, 9, 3, 10, 11, 4, 12, 13, 5, 14, 15)
/**
* 设备2列,10格,左侧1列竖向编号1、2、3、4、5,右侧1列6,7,8,9,10
*/
val device2ColumnsOrder = listOf(1, 6, 2, 7, 3, 8, 4, 9, 5, 10)
/**
* 设备3列,15格,左侧1列竖向编号1、2、3、4、5,右侧2列5行编号为6-7、8-9、10-11、12-13、14-15
*/
val device3Columns = listOf(
//新设备1
"d369f9b0-1c5b-3066-ba72-988e1449cecc",
//新设备2
"505ce1b1-facc-3eb2-855a-3dadda5ba358"
)
/** 激活指令mapkey为设备号,value为激活码 */
val ACTIVE_MAP = mutableMapOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473" to "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${FOOTER}",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751" to "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}",
//新设备1,临时使用1楼餐厅激活码
"d369f9b0-1c5b-3066-ba72-988e1449cecc" to "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}",
//新设备2,临时使用1楼餐厅激活码
"505ce1b1-facc-3eb2-855a-3dadda5ba358" to "${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}",
)
}
@@ -1,40 +0,0 @@
package com.shuwei.intelligent.shelves.serial
/**
* 串口协议解析事件监听器。
* [SerialProtocolParser] 解析完成后通过此接口将结果回调给上层(ViewModel 或 Activity)。
*/
interface SerialEventListener {
/**
* 需要向串口发送命令。
* @param cmd 十六进制命令字符串
*/
fun onSendCommand(cmd: String)
/**
* 获取到终端信息(激活指令或设备状态上报时触发)。
* @param terminalId 终端 ID15字节)
* @param terminalVersion 固件版本(5字节)
*/
fun onTerminalInfoReady(terminalId: String, terminalVersion: String)
/**
* 重量数据更新。
* @param deviceNo 货架编号(从 1 开始)
* @param weight 重量(克)
*/
fun onWeightUpdate(deviceNo: Int, weight: Int)
/**
* 设备状态文本更新(用于顶部状态栏左侧显示)。
* @param text 显示内容
*/
fun onDeviceStatusUpdate(text: String)
/**
* 温控信息上报(需要弹窗提示用户)。
* @param logMsg 温控日志信息
*/
fun onTempCtrlReport(logMsg: String)
}
@@ -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()
}
}
@@ -13,8 +13,9 @@ import java.nio.charset.Charset
import java.util.Locale
object SerialPortManager {
private const val portName: String = "/dev/ttyS1"
private const val baudRate: Int = 115200
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
@@ -25,8 +26,8 @@ object SerialPortManager {
return try {
serialPort = SerialPortFactory.create().apply {
open(
portName = portName,
baudRate = baudRate,
portName = PORT_NAME,
baudRate = BAUD_RATE,
dataBits = 8,
stopBits = 1,
parity = SerialPort.PARITY_NONE
@@ -51,15 +52,13 @@ object SerialPortManager {
}
// 启动接收协程
@Suppress("DEPRECATION")
@SuppressLint("DefaultLocale")
fun startReceive(callback: (String) -> Unit) {
scope.launch {
while (isActive) {
try {
val flow = serialPort?.read()
flow?.collect { buffer ->
val data = buffer.toHexString2().toUpperCase()
val data = buffer.toHexString2().uppercase(Locale.ROOT)
callback(data)
}
} catch (e: Exception) {
@@ -0,0 +1,247 @@
package com.shuwei.intelligent.shelves.serial
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVATE_BODY_LENGTH
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.ACTIVE_MAP
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.BYTES_PER_WEIGHT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_ACTIVATE
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_CARD_UPLOAD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_DEVICE_STATUS
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_HEARTBEAT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_HEARTBEAT_TERMINAL
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_LOCK_FEEDBACK
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_OPERATION_RECORD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_POSITION_REPORT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_REPORT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_RESPONSE
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TEMP_RESPONSE_ALT
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.CMD_TIME_SYNC
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.DEVICE_STATUS_WEIGHT_OFFSET
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FIX_BODY_LENGTH
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.FOOTER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEADER
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.HEARTBEAT_WEIGHT_OFFSET
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.TEMPERATURE_FIELD_LEN
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.hexToBinary
import java.util.TimeZone
/**
* 串口协议解析处理器
*
* 负责将原始协议帧字符串解析为业务事件,通过 [Callback] 接口通知调用方。
* 设计为无状态(除 terminalId/terminalVersion 外),便于单元测试。
*
* @param callback 业务回调,由 Activity/ViewModel 实现
*/
class SerialProtocolHandler(private val callback: Callback) {
/** 终端 ID(15字节/30字符),由激活指令或心跳兜底填充 */
var terminalId: String = ""
private set
/** 固件版本(5字节/10字符),由激活指令或心跳兜底填充 */
var terminalVersion: String = ""
private set
/**
* 协议解析结果回调接口
*/
interface Callback {
/** 需要向串口发送一条指令 */
fun onSendCmd(cmd: String)
/** 解析到一组重量数据:货架编号 → 重量(克) */
fun onWeightUpdated(shelfNo: Int, weight: Int)
/** 温控信息上报,附带完整帧日志用于显示/复制 */
fun onTempReport(logMsg: String)
/** 记录日志 */
fun onLog(message: String)
}
/**
* 处理一帧完整的协议数据
*
* @param data 经 SerialFrameParser 解析后的完整帧字符串
*/
fun handle(data: String) {
if (data.isEmpty()) return
val cmd = data.substring(HEADER.length, HEADER.length + 4)
val cmdFlag = cmd.substring(0, 2)
when (cmd) {
CMD_ACTIVATE -> handleActivate(data, cmd, cmdFlag)
CMD_HEARTBEAT, CMD_HEARTBEAT_TERMINAL -> handleHeartbeat(data, cmd, cmdFlag)
CMD_LOCK_FEEDBACK -> handleLockFeedback(data, cmd, cmdFlag)
CMD_DEVICE_STATUS -> handleDeviceStatus(data, cmdFlag)
CMD_CARD_UPLOAD -> handleCardUpload(data, cmd, cmdFlag)
CMD_OPERATION_RECORD -> handleOperationRecord(data, cmd, cmdFlag)
CMD_TIME_SYNC -> handleTimeSync(data, cmd, cmdFlag)
CMD_POSITION_REPORT -> handlePositionReport(data, cmd, cmdFlag)
CMD_TEMP_REPORT -> handleTempReport(data, cmd, cmdFlag)
CMD_TEMP_RESPONSE -> callback.onLog("收到[温控信息应答]${CMD_TEMP_RESPONSE}数据:$data")
CMD_TEMP_RESPONSE_ALT -> callback.onLog("收到[温控信息应答]${CMD_TEMP_RESPONSE_ALT}数据:$data")
else -> {}
}
}
/** 处理终端激活指令 */
private fun handleActivate(data: String, cmd: String, cmdFlag: String) {
val reqBody = data.substring(HEADER.length + cmd.length, data.length - FOOTER.length)
if (reqBody.length == ACTIVATE_BODY_LENGTH) {
terminalId = reqBody.substring(2, 32)
terminalVersion = reqBody.substring(32, 42)
val activeCode = ACTIVE_MAP[App.deviceId]?:""
callback.onLog("收到[激活]${cmdFlag}指令:${data},终端id:${terminalId},固件版本:${terminalVersion},发送激活命令:$activeCode")
callback.onSendCmd(activeCode)
}
}
/** 处理心跳指令(0101 / 8101 */
private fun handleHeartbeat(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER"
callback.onLog("收到[心跳]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
parseHeartbeatData(data)
}
/** 处理开关锁反馈 */
private fun handleLockFeedback(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
callback.onLog("收到[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理获取设备状态(0302) */
private fun handleDeviceStatus(data: String, cmdFlag: String) {
callback.onLog("收到[获取设备状态]${cmdFlag}指令:${data}")
val weightStartIndex = DEVICE_STATUS_WEIGHT_OFFSET
val weightEndIndex = weightStartIndex + ProtocolConstants.shelfSensorCount() * BYTES_PER_WEIGHT
val temperatureEndIndex = weightEndIndex + TEMPERATURE_FIELD_LEN
try {
parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data)
parseFixDataBody(data, temperatureEndIndex)
} catch (e: Exception) {
e.printStackTrace()
}
}
/** 处理卡号上发 */
private fun handleCardUpload(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}050203$FOOTER"
callback.onLog("收到[卡号上发]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理操作记录上传 */
private fun handleOperationRecord(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0B0203$FOOTER"
callback.onLog("收到[操作记录上传]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理对时请求 */
private fun handleTimeSync(data: String, cmd: String, cmdFlag: String) {
val nowMs = System.currentTimeMillis()
val timeLong = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
val timeStr = "0000000" + String.format("%X", timeLong)
val time = timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
val respCmd = "${HEADER}0B02${time}$FOOTER"
callback.onLog("收到[对时]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理终端位置上报 */
private fun handlePositionReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0E0200$FOOTER"
callback.onLog("收到[终端位置上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
callback.onSendCmd(respCmd)
}
/** 处理温控信息上报 */
private fun handleTempReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}100200$FOOTER"
val logMsg = "收到[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}"
callback.onLog(logMsg)
callback.onSendCmd(respCmd)
callback.onTempReport(logMsg)
}
/**
* 解析心跳帧中的重量与固定数据体
*/
private fun parseHeartbeatData(data: String) {
runCatching {
if (data.length < ProtocolConstants.heartbeatMinLength()) return
val weightStartIndex = HEARTBEAT_WEIGHT_OFFSET
val weightEndIndex = weightStartIndex + ProtocolConstants.shelfSensorCount() * BYTES_PER_WEIGHT
parseWeightData(start = weightStartIndex, end = weightEndIndex, data = data)
val temperatureEndIndex = ProtocolConstants.heartbeatTemperatureStart() + TEMPERATURE_FIELD_LEN
parseFixDataBody(data, temperatureEndIndex)
}.onFailure { it.printStackTrace() }
}
/**
* 解析连续的重量数据段,每 [BYTES_PER_WEIGHT] 字符一组,依次对应货架 1..N
*
* @param start 数据段起始索引
* @param end 数据段结束索引(不含)
* @param data 完整帧字符串
*/
fun parseWeightData(start: Int, end: Int, data: String) {
var index = 1
var count = start
while (count < end) {
val weightInfo = data.substring(count, count + BYTES_PER_WEIGHT)
// 小端字节序转换:将4字节按字节倒序拼接
var weightHex = weightInfo.run {
substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2)
}
var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
val realWeight = if (firstByteBinary.startsWith("0")) {
// 正数:使用 toLongOrNull(16) 转换,避免超出 Int 范围
weightHex.toLongOrNull(16)?.toInt() ?: 0
} else {
// 负数:清除符号位后计算绝对值,再取反
firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2)
//-1 * (weightHex.toLongOrNull(16)?.toInt() ?: 0)
// 直接用 Long 解析,然后减去 2^32 得到有符号值
val unsigned = weightHex.toLongOrNull(16) ?: 0L
val signed = if (unsigned >= 0x80000000L) unsigned - 0x100000000L else unsigned
signed.toInt()
}
callback.onWeightUpdated(index, realWeight)
count += BYTES_PER_WEIGHT
index++
}
}
/**
* 从心跳帧或设备状态帧中补充解析 terminalId / terminalVersion。
* 激活指令(CMD_ACTIVATE)是获取这两个字段的首选来源;
* 若激活指令尚未到来,则通过此方法从心跳/设备状态帧中兜底解析。
*
* @param data 完整协议帧字符串
* @param temperatureEndIndex 温度字段结束位置(固定数据体紧随其后)
*/
private fun parseFixDataBody(data: String, temperatureEndIndex: Int) {
try {
// 已通过激活指令成功解析过,无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) return
val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
if (fixDataBody.length == FIX_BODY_LENGTH) {
terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40)
callback.onLog("从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -1,326 +0,0 @@
package com.shuwei.intelligent.shelves.serial
import android.util.Log
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.ACTIVE_CMD
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.FOOTER
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.HEADER
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.hexToBinary
/**
* 串口协议解析器。
* 职责:将原始串口字节流解析为结构化协议事件,通过 [SerialEventListener] 回调通知上层。
* 无 Android UI 依赖,可进行单元测试。
*
* @param listener 协议事件监听器,由 SerialViewModel 实现并传入
*/
class SerialProtocolParser(private val listener: SerialEventListener) {
companion object {
private const val TAG = "SerialProtocolParser"
private const val LOG_PREFIX = "receiveSerialPortData:收到"
}
/**
* 终端 ID(15字节),由激活指令或设备状态上报时获取。
* 对外只读,内部可写。
*/
var terminalId: String = ""
private set
/**
* 固件版本(5字节),由激活指令或设备状态上报时获取。
* 对外只读,内部可写。
*/
var terminalVersion: String = ""
private set
/**
* 串口拼包缓冲器。
* 仅在单一消费协程中调用 [parseIncoming],线程安全。
*/
private val serialReader = StringBuilder()
// ----------------------------------------------------------------
// 对外入口:拼包
// ----------------------------------------------------------------
/**
* 处理一条原始串口数据,执行拼包逻辑后交由 [handleReceivedData] 分发命令。
* @param srcData 原始十六进制字符串(大写)
*/
fun parseIncoming(srcData: String) {
log("parseIncoming: $srcData")
var data = ""
if (srcData.contains(HEADER) && srcData.contains(FOOTER)) {
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
var endIndex = srcData.indexOf(FOOTER) + FOOTER.length
if (endIndex < startIndex) {
// 类似这种:31314FFFFFD6DADEDB01010CFOOTER 在 HEADER 前
data = srcData.substring(startIndex)
if (data.contains(FOOTER)) {
endIndex = data.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)) {
// 只有头,没有尾,等待后续分片
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
serialReader.append(srcData.substring(startIndex)).append(",")
return
}
if (!srcData.contains(HEADER) && !srcData.contains(FOOTER)) {
// 中间分片
serialReader.append(srcData).append(",")
return
}
// 只有尾,没有头:拼接并处理
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
serialReader.append(srcData.substring(0, endIndex))
log("parseIncoming:serialReader: $serialReader")
data = serialReader.toString().replace(",", "")
handleReceivedData(data)
}
// ----------------------------------------------------------------
// 命令分发
// ----------------------------------------------------------------
/**
* 根据命令码分发处理各类串口协议。
*/
private fun handleReceivedData(data: String) {
if (data.isEmpty()) return
val cmd = data.substring(HEADER.length, HEADER.length + 4)
val cmdFlag = cmd.substring(0, 2)
when (cmd) {
"0001" -> handleActivation(data, cmd, cmdFlag)
"0101", "8101" -> handleHeartBeat(data, cmd, cmdFlag)
"0202" -> handleLockFeedback(data, cmd, cmdFlag)
"0302" -> handleDeviceStatus(data, cmd, cmdFlag)
"0501" -> handleCardUpload(data, cmd, cmdFlag)
"0B01" -> handleOperationRecord(data, cmd, cmdFlag)
"0C01" -> handleTimeSync(data, cmd, cmdFlag)
"0E01" -> handlePositionReport(data, cmd, cmdFlag)
"1001" -> handleTempCtrlReport(data, cmd, cmdFlag)
"1002" -> log("${LOG_PREFIX}[温控信息应答]1002数据:$data")
"0F02" -> log("${LOG_PREFIX}[温控信息应答]0F02数据:$data")
else -> {}
}
}
// ----------------------------------------------------------------
// 各命令处理
// ----------------------------------------------------------------
/**
* 处理终端发起的激活指令(0001)。
* 解析终端 ID 和固件版本,并发送激活命令。
*/
private fun handleActivation(data: String, cmd: String, cmdFlag: String) {
val reqBody = data.substring(HEADER.length + cmd.length, data.length - FOOTER.length)
if (reqBody.length == 44) {
terminalId = reqBody.substring(2, 32)
terminalVersion = reqBody.substring(32, 42)
log("${LOG_PREFIX}[激活]${cmdFlag}指令:$data,终端id:$terminalId,固件版本:$terminalVersion,发送激活命令:$ACTIVE_CMD")
listener.onSendCommand(ACTIVE_CMD)
listener.onTerminalInfoReady(terminalId, terminalVersion)
}
}
/**
* 处理心跳指令(0101 / 8101)。
* 发送心跳应答,并解析重量数据。
*/
private fun handleHeartBeat(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[心跳]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
parseHeartBeat(data)
}
/**
* 处理开关锁反馈(0202)。
* 发送开锁应答。
*/
private fun handleLockFeedback(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[开锁]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理获取设备状态(0302)。
* 解析设备重量和终端信息。
*/
private fun handleDeviceStatus(data: String, cmd: String, cmdFlag: String) {
log("${LOG_PREFIX}[获取设备状态]${cmdFlag}指令:$data")
parseDeviceInfo(data)
}
/**
* 处理卡号上发(0501)。
*/
private fun handleCardUpload(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}050203$FOOTER"
log("${LOG_PREFIX}[卡号上发]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理操作记录上传(0B01)。
*/
private fun handleOperationRecord(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0B0203$FOOTER"
log("${LOG_PREFIX}[操作记录上传]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理对时指令(0C01)。
* 将当前系统时间转换为小端序十六进制并应答。
*/
private fun handleTimeSync(data: String, cmd: String, cmdFlag: String) {
val timeLong = System.currentTimeMillis() / 1000 + (8 * 3600)
val timeStr = "0000000" + String.format("%X", timeLong)
val time = timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
val respCmd = "${HEADER}0B02${time}$FOOTER"
log("${LOG_PREFIX}[对时]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理终端位置上报(0E01)。
*/
private fun handlePositionReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0E0200$FOOTER"
log("${LOG_PREFIX}[终端位置上报]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理温控信息上报(1001)。
* 发送应答,并通过回调通知上层弹窗展示日志。
*/
private fun handleTempCtrlReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}100200$FOOTER"
val logMsg = "${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:$data,发送应答指令:$respCmd"
log(logMsg)
listener.onSendCommand(respCmd)
listener.onDeviceStatusUpdate("")
listener.onTempCtrlReport(logMsg)
}
// ----------------------------------------------------------------
// 数据解析辅助
// ----------------------------------------------------------------
/**
* 解析心跳报文中的重量和固定数据体。
*/
private fun parseHeartBeat(data: String) {
runCatching {
if (data.length < 200) return
val weightStartIndex = 14
val weightEndIndex = weightStartIndex + 12 * 8
parseWeightInfo(start = weightStartIndex, end = weightEndIndex, data = data)
val temperatureEndIndex = 158 + 2
parseFixDataBody(data, temperatureEndIndex)
}.onFailure { it.printStackTrace() }
}
/**
* 解析设备状态报文(0302)中的重量和固定数据体。
*/
private fun parseDeviceInfo(data: String) {
val weightStartIndex = HEADER.length + 14
val temperatureStartIndex = weightStartIndex + 12 * 8
val temperatureEndIndex = temperatureStartIndex + 2
try {
data.substring(temperatureStartIndex, temperatureEndIndex)
} catch (e: Exception) {
e.printStackTrace()
return
}
listener.onDeviceStatusUpdate("")
parseWeightInfo(start = weightStartIndex, end = temperatureStartIndex, data = data)
parseFixDataBody(data, temperatureEndIndex)
}
/**
* 解析重量数据区,逐格读取并通过 [listener] 回调。
* @param start 重量数据起始索引
* @param end 重量数据结束索引
* @param data 完整协议报文(十六进制字符串)
*/
fun parseWeightInfo(start: Int, end: Int, data: String) {
var deviceNo = 1
var count = start
while (count < end) {
val weightInfo = data.substring(count, count + 8)
// 将小端序 4 字节转换为大端序
var weightHex = weightInfo.run {
substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2)
}
var realWeight: Int
var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
if (firstByteBinary.startsWith("0")) {
// 正数:按十六进制解析,失败时兜底为 0
realWeight = weightHex.toIntOrNull(16) ?: 0
} else {
// 负数:清除符号位后取绝对值,再取反
firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2)
realWeight = -1 * (weightHex.toIntOrNull(16) ?: 0)
}
listener.onWeightUpdate(deviceNo, realWeight)
count += 8
deviceNo++
}
}
/**
* 从数据体尾部提取 terminalId / terminalVersion(首次获取时有效)。
* @param data 完整协议报文
* @param temperatureEndIndex 温度字段结束索引,固定数据体从此之后开始
*/
private fun parseFixDataBody(data: String, temperatureEndIndex: Int) {
try {
// 已有终端信息则无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) return
val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
if (fixDataBody.length == 42) {
terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40)
log("从设备状态获取数据,终端id:$terminalId,固件版本:$terminalVersion")
listener.onTerminalInfoReady(terminalId, terminalVersion)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun log(msg: String) {
Log.d(TAG, msg)
}
}
@@ -0,0 +1,54 @@
package com.shuwei.intelligent.shelves.utils
import android.content.Context
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
object GridLayoutTool {
/**
* 设备2列,10格
*/
private val device2Columns = ProtocolConstants.device2Columns
/**
* 设备3列,15格,左侧1列竖向编号1、2、3、4、5,右侧2列5行编号为6-7、8-9、10-11、12-13、14-15
*/
private val device3Columns = ProtocolConstants.device3Columns
fun getLayoutManager(context: Context): RecyclerView.LayoutManager? {
return if (device2Columns.contains(App.deviceId)) {
GridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false)
} else if (device3Columns.contains(App.deviceId)) {
GridLayoutManager(context, 3, GridLayoutManager.VERTICAL, false)
} else {
null
}
}
/**
* 初始化list显示顺序
*/
fun initListSort(source: List<ShelfModel>): List<ShelfModel> {
val map = source.associateBy { it.deviceNo }
return if (device3Columns.contains(App.deviceId)) {
// 按全局定义的显示顺序取对应数据,找不到则跳过
ProtocolConstants.device3ColumnsOrder.mapNotNull { map[it] }
} else if (device2Columns.contains(App.deviceId)) {
ProtocolConstants.device2ColumnsOrder.mapNotNull { map[it] }
} else {
source
}
}
/**
* 提交接口list顺序
*/
fun submitListSort(source: List<ShelfModel>): List<ShelfModel> {
return source.sortedBy { it.deviceNo }
}
}
@@ -0,0 +1,24 @@
package com.shuwei.intelligent.shelves.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/**
* 网络连接检测工具类
* 提供检测设备网络连接状态的功能
*/
object NetworkUtils {
/**
* 检测设备是否连接到网络
* @param context 应用上下文
* @return true 表示已连接网络,false 表示未连接
*/
fun isNetworkConnected(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
@@ -1,6 +1,7 @@
package com.shuwei.intelligent.shelves.utils
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.GlobalKey
import com.shuwei.intelligent.shelves.utils.ext.put
import kotlin.to
@@ -28,4 +29,10 @@ object SpTool {
fun getBoolean(key: String, defValue: Boolean = false): Boolean {
return pref.getBoolean(key, defValue)
}
var baseUrl: String
get() = getString(GlobalKey.KEY_BASE_URL, "")
set(value) {
put(GlobalKey.KEY_BASE_URL, value)
}
}
@@ -25,6 +25,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import android.content.ClipData
import android.content.ClipboardManager
import android.os.SystemClock
fun Context.toast(
message: String?,
@@ -175,3 +176,22 @@ fun String.copyTextToClipboard(context: Context) {
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
/**
* 为View添加双击点击事件的扩展函数
*/
fun View.setOnDoubleClickListener(onDoubleClickListener: (View) -> Unit) {
var lastClickTime = 0L
var lastClickView: View? = null
this.setOnClickListener { view ->
val currentTime = SystemClock.elapsedRealtime()
if (lastClickView === view && currentTime - lastClickTime < 500) {
// 双击事件触发
onDoubleClickListener(view)
lastClickTime = 0
} else {
lastClickTime = currentTime
lastClickView = view
}
}
}
+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,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#4ECDC4" />
<corners android:radius="12dp" />
<stroke
android:width="2dp"
android:color="#2BA39F" />
</shape>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 网络断开的图标 -->
<path
android:fillColor="#FF6B6B"
android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2c-4.42,-4.42 -11.58,-4.42 -16,0zM9,17h6v2h-6z" />
</vector>
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="240dp"
android:height="240dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<!-- 由小到大排列的圆点,构成旋转加载指示器 -->
<path
android:fillColor="#ffffff"
android:pathData="M876.864,782.592c3.264,0 6.272,-3.2 6.272,-6.656c0,-3.456 -3.008,-6.592 -6.272,-6.592c-3.264,0 -6.272,3.2 -6.272,6.592c0,3.456 3.008,6.656 6.272,6.656z" />
<path
android:fillColor="#ffffff"
android:pathData="M736.32,935.936c2.304,2.432 5.568,3.84 8.768,3.84a12.16,12.16 0,0 0,8.832 -3.84a13.76,13.76 0,0 0,0 -18.56a12.224,12.224 0,0 0,-8.832 -3.84a12.16,12.16 0,0 0,-8.768 3.84a13.696,13.696 0,0 0,0 18.56z" />
<path
android:fillColor="#ffffff"
android:pathData="M552.32,1018.24c3.456,3.648 8.32,5.76 13.184,5.76a18.368,18.368 0,0 0,13.184 -5.76a20.608,20.608 0,0 0,0 -27.968a18.368,18.368 0,0 0,-13.184 -5.824a18.368,18.368 0,0 0,-13.184 5.76a20.608,20.608 0,0 0,0 28.032z" />
<path
android:fillColor="#ffffff"
android:pathData="M353.984,1012.48c4.608,4.8 11.072,7.68 17.6,7.68a24.448,24.448 0,0 0,17.536 -7.68a27.456,27.456 0,0 0,0 -37.248a24.448,24.448 0,0 0,-17.536 -7.68a24.448,24.448 0,0 0,-17.6 7.68a27.52,27.52 0,0 0,0 37.184z" />
<path
android:fillColor="#ffffff"
android:pathData="M178.304,920.64c5.76,6.08 13.824,9.6 21.952,9.6a30.592,30.592 0,0 0,22.016 -9.6a34.368,34.368 0,0 0,0 -46.592a30.592,30.592 0,0 0,-22.016 -9.6a30.592,30.592 0,0 0,-21.952 9.6a34.368,34.368 0,0 0,0 46.592z" />
<path
android:fillColor="#ffffff"
android:pathData="M57.152,761.28c6.912,7.36 16.64,11.648 26.368,11.648a36.736,36.736 0,0 0,26.432 -11.584a41.28,41.28 0,0 0,0 -55.936a36.736,36.736 0,0 0,-26.432 -11.584a36.8,36.8 0,0 0,-26.368 11.52a41.28,41.28 0,0 0,0 56z" />
<path
android:fillColor="#ffffff"
android:pathData="M12.736,564.672a42.88,42.88 0,0 0,30.784 13.44a42.88,42.88 0,0 0,30.784 -13.44a48.128,48.128 0,0 0,0 -65.216a42.88,42.88 0,0 0,-30.72 -13.44a42.88,42.88 0,0 0,-30.848 13.44a48.128,48.128 0,0 0,0 65.216z" />
<path
android:fillColor="#ffffff"
android:pathData="M52.544,369.28a48.96,48.96 0,0 0,35.2 15.36a48.96,48.96 0,0 0,35.2 -15.36a54.976,54.976 0,0 0,0 -74.56a48.96,48.96 0,0 0,-35.2 -15.424a48.96,48.96 0,0 0,-35.2 15.424a54.976,54.976 0,0 0,0 74.56z" />
<path
android:fillColor="#ffffff"
android:pathData="M168.32,212.48c10.368,11.008 24.96,17.408 39.68,17.408c14.592,0 29.184,-6.4 39.552,-17.408a61.888,61.888 0,0 0,0 -83.84a55.104,55.104 0,0 0,-39.616 -17.408c-14.656,0 -29.248,6.4 -39.616,17.408a61.888,61.888 0,0 0,0 83.84z" />
<path
android:fillColor="#ffffff"
android:pathData="M337.344,124.8c11.52,12.16 27.712,19.264 43.968,19.264c16.256,0 32.448,-7.04 43.968,-19.264a68.672,68.672 0,0 0,0 -93.184a61.248,61.248 0,0 0,-43.968 -19.264a61.248,61.248 0,0 0,-43.968 19.264a68.736,68.736 0,0 0,0 93.184z" />
<path
android:fillColor="#ffffff"
android:pathData="M526.976,123.712c12.672,13.44 30.528,21.248 48.448,21.248s35.712,-7.808 48.384,-21.248a75.584,75.584 0,0 0,0 -102.464A67.392,67.392 0,0 0,575.36 0c-17.92,0 -35.776,7.808 -48.448,21.248a75.584,75.584 0,0 0,0 102.464z" />
<path
android:fillColor="#ffffff"
android:pathData="M700.8,210.304c13.824,14.592 33.28,23.104 52.736,23.104c19.584,0 39.04,-8.512 52.8,-23.104a82.432,82.432 0,0 0,0 -111.744a73.472,73.472 0,0 0,-52.8 -23.168c-19.52,0 -38.912,8.512 -52.736,23.168a82.432,82.432 0,0 0,0 111.744z" />
<path
android:fillColor="#ffffff"
android:pathData="M824.832,368.832c14.976,15.872 36.032,25.088 57.216,25.088c21.12,0 42.24,-9.216 57.152,-25.088a89.344,89.344 0,0 0,0 -121.088a79.616,79.616 0,0 0,-57.152 -25.088c-21.184,0 -42.24,9.216 -57.216,25.088a89.344,89.344 0,0 0,0 121.088z" />
<path
android:fillColor="#ffffff"
android:pathData="M875.264,572.864c16.128,17.088 38.784,27.008 61.632,27.008c22.784,0 45.44,-9.92 61.568,-27.008a96.256,96.256 0,0 0,0 -130.432a85.76,85.76 0,0 0,-61.568 -27.072c-22.848,0 -45.44,9.984 -61.632,27.072a96.192,96.192 0,0 0,0 130.432z" />
</vector>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadiusRatio="3"
android:thicknessRatio="8"
android:useLevel="false">
<gradient
android:type="sweep"
android:startColor="#FF6B6B"
android:centerColor="#4ECDC4"
android:endColor="#FF6B6B"
android:angle="0" />
</shape>
@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<FrameLayout
android:id="@+id/flStatusBar"
android:layout_width="match_parent"
android:layout_height="68dp"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:paddingTop="25dp">
<TextView
android:id="@+id/tv_left_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="start"
android:textSize="24sp"
android:fontFamily="sans-serif-medium"
tools:text="1-1冷藏柜 -2°C / 87%" />
<TextView
android:id="@+id/tv_right_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="end"
android:fontFamily="sans-serif-medium"
android:textSize="24sp"
tools:text="6月22日 星期日 15:26:33" />
</FrameLayout>
<FrameLayout
android:id="@+id/flContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:background="@color/bg_page">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvShelf"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
android:overScrollMode="never"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
tools:listitem="@layout/list_item_shelf"/>
<include
android:id="@+id/include"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/layout_empty_view"
android:visibility="gone"/>
</FrameLayout>
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="450dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="150dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<TextView
android:id="@+id/btnInit"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="100dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:ignore="HardcodedText" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="50dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512" />
</LinearLayout>
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/ivBack"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="top|start"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="设置"
android:textColor="@color/black"
android:textSize="28sp" />
<Button
android:id="@+id/btnFind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:textSize="18sp"
android:layout_gravity="end|center_vertical"
android:text="查找1001指令"/>
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLogList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
@@ -1,269 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="376dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_marginTop="24dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textColor="@color/bg_page"
android:textSize="30sp"
tools:text="货架 - 03" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/food_weight_orange"
android:textSize="60sp"
android:textStyle="bold"
tools:text="1506克" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_name_black"
android:textSize="36sp"
android:textStyle="bold"
tools:text="金针菇" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearZero"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_gravity="top|end"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="清零"
android:textColor="@color/white"
android:textSize="30sp"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/ivBack"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="top|start"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="48dp"
android:background="@drawable/shape_search"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_search_food" />
<EditText
android:id="@+id/etInputFood"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginEnd="24dp"
android:background="@null"
android:fontFamily="sans-serif-medium"
android:hint="输入食材名称"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="1dp"
android:paddingEnd="1dp"
android:text=""
android:textColor="@color/food_name_black"
android:textColorHint="#B4BEC8"
android:textSize="30sp"
tools:ignore="Autofill,HardcodedText,TextFields" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:layout_weight="1">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSearch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingStart="36dp"
android:paddingTop="12dp"
android:paddingEnd="36dp"
android:paddingBottom="12dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_search" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<include
android:id="@+id/include"
layout="@layout/layout_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearEmpty"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="清空"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="确定"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_dialog">
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="25dp"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingEnd="25dp"
android:textColor="@color/white_f6"
android:textSize="32sp"
tools:text="暂无数据" />
</FrameLayout>
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="400dp"
android:layout_height="300dp"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"
xmlns:tools="http://schemas.android.com/tools"
app:cardBackgroundColor="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginVertical="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvDialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textColor="@color/black"
android:textSize="22sp"
tools:text="标题"/>
<TextView
android:id="@+id/tvDialogContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:textColor="@color/black"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:textSize="16sp"
tools:text="内容"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
android:layout_width="120dp"
android:layout_height="50dp"
android:text="确定"
android:background="@color/bg_card_blue"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingEnd="20dp"
android:paddingBottom="10dp"
android:background="@drawable/shape_dialog"
tools:ignore="MissingDefaultResource">
<ProgressBar
android:layout_width="60dp"
android:layout_height="60dp"
android:indeterminateTint="@color/white_f6" />
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="加载中……"
android:textColor="@color/white_f6"
android:textSize="26sp"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center"
android:orientation="vertical"
tools:ignore="MissingDefaultResource,UseCompoundDrawables">
<ImageView
android:id="@+id/ivEmptyIcon"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_empty_gray"
android:adjustViewBounds="true"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tvEmptyContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:text="暂无数据"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:id="@+id/tvLogName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="30dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="2025-11-18" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="bottom"
android:layout_marginHorizontal="30dp"
app:dividerColor="@color/gray_edit" />
</FrameLayout>
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<!-- xmlns:tools="http://schemas.android.com/tools"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="80dp"-->
<!-- android:layout_margin="12dp">-->
<!-- <TextView-->
<!-- android:id="@+id/btnFoodName"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="center"-->
<!-- android:ellipsize="end"-->
<!-- android:gravity="center"-->
<!-- android:maxLines="1"-->
<!-- android:paddingStart="5dp"-->
<!-- android:paddingEnd="5dp"-->
<!-- android:fontFamily="sans-serif-medium"-->
<!-- android:textColor="@color/black999"-->
<!-- android:textSize="30sp"-->
<!-- tools:text="土豆丝" />-->
<!--</FrameLayout>-->
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/btnFoodName"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_margin="12dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
app:paddingStart="5dp"
app:paddingEnd="5dp"
android:insetRight="0dp"
android:insetLeft="0dp"
android:paddingHorizontal="8dp"
android:textColor="@color/black999"
android:textSize="30sp"
app:cornerRadius="12dp"
app:strokeWidth="2dp"
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝"
/>
@@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="216dp"
android:layout_margin="12dp"
app:cardBackgroundColor="@color/bg_card_blue"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:rippleColor="#B0BEC5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/shelf_name_white"
android:textSize="20sp"
android:textStyle="bold"
tools:text="货架-01" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
tools:text="空" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.833" />
<TextView
android:id="@+id/tvFoodWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="24sp"
android:textStyle="bold"
tools:text="0千克" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.933" />
<TextView
android:id="@+id/tvStoreDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="18sp"
tools:text="-" />
</LinearLayout>
<ImageView
android:id="@+id/ivStaleFood"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_stale_food_512"
android:layout_gravity="top|end"
tools:ignore="ContentDescription"
android:visibility="gone"/>
</com.google.android.material.card.MaterialCardView>
+9 -14
View File
@@ -5,39 +5,34 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
tools:background="@color/bg_page">
<FrameLayout
android:id="@+id/flStatusBar"
android:layout_width="match_parent"
android:layout_height="30dp">
<!-- android:paddingTop="9dp"-->
android:layout_height="68dp"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:paddingTop="25dp">
<TextView
android:id="@+id/tv_left_status"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="start"
android:textSize="12sp"
android:paddingStart="12dp"
android:paddingEnd="0dp"
android:textSize="24sp"
android:fontFamily="sans-serif-medium"
tools:text="1-1冷藏柜 -2°C / 87%" />
android:text=" " />
<TextView
android:id="@+id/tv_right_status"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="end"
android:fontFamily="sans-serif-medium"
android:textSize="12sp"
android:paddingStart="0dp"
android:paddingEnd="12dp"
android:textSize="24sp"
tools:text="6月22日 星期日 15:26:33" />
</FrameLayout>
+2 -10
View File
@@ -13,8 +13,8 @@
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
android:overScrollMode="never"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
tools:listitem="@layout/list_item_shelf"/>
@@ -24,12 +24,4 @@
android:layout_height="match_parent"
layout="@layout/layout_empty_view"
android:visibility="gone"/>
<!-- <include-->
<!-- android:id="@+id/includeLight"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- layout="@layout/layout_light_switch"-->
<!-- android:visibility="gone"/>-->
</FrameLayout>
+136 -39
View File
@@ -1,48 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<ImageView
android:layout_width="225dp"
android:layout_height="215dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="75dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<LinearLayout
android:id="@+id/llDeviceInit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/btnInit"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="visible"
tools:ignore="HardcodedText" />
<ImageView
android:layout_width="450dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="150dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/btnInit"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="100dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="25dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="50dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512" />
</LinearLayout>
<LinearLayout
android:id="@+id/llNetwork"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<!-- Loading 动画容器 -->
<LinearLayout
android:id="@+id/llLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:visibility="visible"
android:layout_marginTop="260dp">
<!-- 加载动画 -->
<ImageView
android:id="@+id/ivLoading"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/loading"
android:contentDescription="加载中" />
<!-- 倒计时文字 -->
<TextView
android:id="@+id/tvCountdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="60秒"
android:textSize="40sp"
android:textColor="@color/white"
android:textStyle="bold"
android:layout_marginTop="50dp" />
<!-- 提示文字 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络连接检测中,请稍后……"
android:textSize="22sp"
android:textColor="@color/white"
android:layout_marginTop="20dp" />
</LinearLayout>
<!-- 连接网络按钮容器 -->
<LinearLayout
android:id="@+id/llNetworkButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone">
<!-- 网络错误图标 -->
<ImageView
android:layout_width="140dp"
android:layout_height="140dp"
android:src="@drawable/ic_network_error"
android:contentDescription="网络连接失败"
android:layout_marginBottom="30dp" />
<!-- 连接失败提示 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络连接失败"
android:textSize="36sp"
android:textColor="@color/white"
android:textStyle="bold"
android:layout_marginBottom="40dp" />
<!-- 连接网络按钮 -->
<Button
android:id="@+id/btnConnectNetwork"
android:layout_width="350dp"
android:layout_height="70dp"
android:text="连接网络"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:background="@drawable/btn_connect_network" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
+56 -4
View File
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -33,14 +38,61 @@
android:id="@+id/btnFind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="12sp"
android:layout_marginEnd="15dp"
android:textSize="18sp"
android:layout_gravity="end|center_vertical"
android:text="查找1001指令"/>
</FrameLayout>
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="8dp"
android:background="@android:color/white"
android:hint="搜索日志..."
android:paddingHorizontal="12dp"
android:textSize="14sp"
android:singleLine="true"
tools:ignore="Autofill,TextFields" />
<HorizontalScrollView
android:id="@+id/hsvTimeNav"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/llTimeNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingHorizontal="12dp" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLogList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabJump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@android:drawable/ic_menu_sort_by_size"
app:fabSize="normal"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
@@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="260dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="12dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/ivBack"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="top|start"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28sp"
android:layout_gravity="center"
android:text="设置"
android:textColor="@color/black"/>
</FrameLayout>
<!-- <EditText-->
<!-- android:id="@+id/etInputCmdPrefix"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="60dp"-->
<!-- android:hint="请输入命令"-->
<!-- android:maxLines="1"-->
<!-- android:text=""-->
<!-- android:textSize="22sp" />-->
<EditText
android:id="@+id/etInputCmd"
android:layout_width="match_parent"
android:layout_height="60dp"
android:hint="请输入命令内容"
android:maxLines="1"
android:textSize="22sp" />
<Button
android:id="@+id/btnSendCmd"
android:layout_width="180dp"
android:layout_height="60dp"
android:textSize="22sp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:text="发送命令" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvCmdRecord"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="20sp"
android:text=""/>
</LinearLayout>
</ScrollView>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
+61 -61
View File
@@ -9,12 +9,12 @@
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="190dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="12dp"
android:layout_height="376dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="6dp"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<FrameLayout
@@ -25,20 +25,20 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="12dp"
android:layout_height="60dp"
android:layout_marginTop="24dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textColor="@color/bg_page"
android:textSize="15sp"
android:textSize="30sp"
tools:text="货架 - 03" />
<Space
@@ -54,7 +54,7 @@
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/food_weight_orange"
android:textSize="30sp"
android:textSize="60sp"
android:textStyle="bold"
tools:text="1506克" />
@@ -70,7 +70,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_name_black"
android:textSize="18sp"
android:textSize="36sp"
android:textStyle="bold"
tools:text="金针菇" />
@@ -82,11 +82,11 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearZero"
android:layout_width="65dp"
android:layout_height="30dp"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_gravity="top|end"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
@@ -95,21 +95,20 @@
android:maxLines="1"
android:text="清零"
android:textColor="@color/white"
android:textSize="15sp"
app:cornerRadius="6dp"
android:textSize="30sp"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/ivBack"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="top|start"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
@@ -117,12 +116,12 @@
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="6dp"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
@@ -132,26 +131,26 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="48dp"
android:background="@drawable/shape_search"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_search_food" />
<EditText
android:id="@+id/etInputFood"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:layout_height="60dp"
android:layout_marginEnd="24dp"
android:background="@null"
android:fontFamily="sans-serif-medium"
android:hint="输入食材名称"
@@ -163,14 +162,14 @@
android:text=""
android:textColor="@color/food_name_black"
android:textColorHint="#B4BEC8"
android:textSize="15sp"
android:textSize="30sp"
tools:ignore="Autofill,HardcodedText,TextFields" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginTop="24dp"
android:layout_weight="1">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
@@ -188,10 +187,10 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
android:paddingStart="36dp"
android:paddingTop="12dp"
android:paddingEnd="36dp"
android:paddingBottom="12dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
@@ -209,23 +208,24 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearEmpty"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:layout_height="40dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
@@ -233,22 +233,21 @@
android:maxLines="1"
android:text="清空"
android:textColor="@color/white"
android:textSize="15sp"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="6dp"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="12dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
@@ -256,11 +255,12 @@
android:maxLines="1"
android:text="确定"
android:textColor="@color/white"
android:textSize="15sp"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="6dp"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
+5 -5
View File
@@ -11,12 +11,12 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:paddingStart="25dp"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingEnd="25dp"
android:textColor="@color/white_f6"
android:textSize="16sp"
android:textSize="32sp"
tools:text="暂无数据" />
</FrameLayout>
@@ -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>
+9 -9
View File
@@ -5,25 +5,25 @@
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="10dp"
android:paddingTop="5dp"
android:paddingEnd="10dp"
android:paddingBottom="5dp"
android:background="@drawable/shape_dialog_small"
android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingEnd="20dp"
android:paddingBottom="10dp"
android:background="@drawable/shape_dialog"
tools:ignore="MissingDefaultResource">
<ProgressBar
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_width="60dp"
android:layout_height="60dp"
android:indeterminateTint="@color/white_f6" />
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginStart="10dp"
android:text="加载中……"
android:textColor="@color/white_f6"
android:textSize="14sp"
android:textSize="26sp"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -10,7 +10,7 @@
<ImageView
android:id="@+id/ivEmptyIcon"
android:layout_width="120dp"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_empty_gray"
android:adjustViewBounds="true"
@@ -21,8 +21,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:text="暂无数据"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#DDFFFFFF">
<ImageView
android:id="@+id/ivLightHide"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginVertical="10dp"
android:layout_marginEnd="10dp"
android:layout_gravity="end|center_vertical"
android:src="@drawable/close"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="继电器1"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightOpen"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="开"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightClose"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="关"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="继电器2"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightOpen2"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="开"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightClose2"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="关"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="继电器3"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightOpen3"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="开"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightClose3"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="关"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
</LinearLayout>
</LinearLayout>
+11 -10
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<?xml version="1.0" encoding="utf-8"?>
<!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<!-- xmlns:tools="http://schemas.android.com/tools"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="80dp"-->
@@ -25,23 +26,23 @@
android:id="@+id/btnFoodName"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="6dp"
android:layout_height="80dp"
android:layout_margin="12dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
app:paddingStart="5dp"
app:paddingEnd="5dp"
android:insetRight="0dp"
android:insetLeft="0dp"
android:paddingHorizontal="8dp"
android:textColor="@color/black999"
android:textSize="15sp"
app:cornerRadius="6dp"
app:strokeWidth="1dp"
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝" />
<!-- android:paddingHorizontal="2dp"-->
<!-- android:paddingLeft="5dp"-->
<!-- android:paddingRight="5dp"-->
android:textSize="30sp"
app:cornerRadius="12dp"
app:strokeWidth="2dp"
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝"
/>
+11 -11
View File
@@ -4,11 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="140dp"
android:layout_margin="6dp"
android:layout_height="216dp"
android:layout_margin="12dp"
app:cardBackgroundColor="@color/bg_card_blue"
app:cardCornerRadius="6dp"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:rippleColor="#B0BEC5">
<LinearLayout
@@ -16,14 +16,14 @@
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="12dp">
android:padding="24dp">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/shelf_name_white"
android:textSize="12sp"
android:textSize="20sp"
android:textStyle="bold"
tools:text="货架-01" />
@@ -39,7 +39,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="16sp"
android:textSize="30sp"
android:textStyle="bold"
tools:text="空" />
@@ -55,7 +55,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="14sp"
android:textSize="24sp"
android:textStyle="bold"
tools:text="0千克" />
@@ -71,14 +71,14 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="12sp"
android:textSize="18sp"
tools:text="-" />
</LinearLayout>
<ImageView
android:id="@+id/ivStaleFood"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_stale_food_512"
android:layout_gravity="top|end"
tools:ignore="ContentDescription"
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_env_switch"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="切换环境"
android:textColor="#333333"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#E0E0E0" />
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="查看日志"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
+1 -1
View File
@@ -2,7 +2,7 @@
#agp = "8.11.1"
agp = "8.4.0"
kotlin = "2.0.21"
coreKtx = "1.12.0"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"