3 Commits
Author SHA1 Message Date
lvmeng 428852a07e refactor(activity): 优化启动页网络检测逻辑,有网络时直接跳转并隐藏检测区域 2026-04-13 18:17:17 +08:00
lvmeng 96863e2078 feat(activity): 新增菜品识别功能,重构FoodRecognizeDialog并临时跳过调料配置检查
- 新增 FoodRecognizeDialog(继承 BottomSheetDialog),只初始化一次,暴露 loadData() 方法
- 新增 dialog_food_recognize.xml 布局(标题 + 2列 GridLayoutManager RecyclerView)
- PrepareCookActivity 集成相机初始化、拍照识别流程(重量稳定 ≥10g 时触发)
- PrepareCookActivity 新增 flCameraContainer(1dp 隐藏容器)用于相机预览绑定
- SelectDishActivity 临时注释 judgeDeviceConfig 逻辑,直接调用 block()
- 修正 GpioUtils/L/LightManager 包名为 com.shuwei.dish.match.utils
2026-04-13 17:31:31 +08:00
lvmeng 41036cdbf9 feat(layout): 更新主秤监控界面布局并启用PyTorch模型功能
- 将主秤监控界面从LinearLayout改为ConstraintLayout布局
- 添加返回按钮并实现点击关闭页面功能
- 在BaseApp中初始化ObjectBox数据库
- 启用PyTorch模型依赖库并移除注释代码
- 在CollectFragment中激活食物图像向量化功能
- 初始化FoodModule中的PyTorch模型
- 临时忽略assets下的.pt模型文件到.gitignore
2026-04-13 15:11:47 +08:00
18 changed files with 875 additions and 368 deletions
+2
View File
@@ -11,3 +11,5 @@ local.properties
.claude/ .claude/
app/debug/ app/debug/
app/release/ app/release/
# 临时忽略 assets 下的 .pt 模型文件,正式文件就绪后取消此行
app/src/main/assets/*.pt
+2 -2
View File
@@ -137,8 +137,8 @@ dependencies {
implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.extensions)
//pytorch //pytorch
// implementation (libs.pytorch.android) implementation (libs.pytorch.android)
// implementation (libs.pytorch.android.torchvision) implementation (libs.pytorch.android.torchvision)
// objectbox // objectbox
if (isDebug) { if (isDebug) {
@@ -41,6 +41,7 @@ class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>)
): VH { ): VH {
val binding = val binding =
ListItemSearchGoodsInfoBinding.inflate(LayoutInflater.from(context), parent, false) ListItemSearchGoodsInfoBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding) return VH(binding)
} }
@@ -8,6 +8,7 @@ import android.content.SharedPreferences
import android.util.Log import android.util.Log
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.db.DatabaseProvider import com.shuwei.dish.match.db.DatabaseProvider
import com.shuwei.dish.match.objbox.ObjectBox
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.ui.InitActivity.Companion.TAG import com.shuwei.dish.match.ui.InitActivity.Companion.TAG
import com.shuwei.dish.match.utils.AppUtil import com.shuwei.dish.match.utils.AppUtil
@@ -44,7 +45,7 @@ class BaseApp : Application() {
// GlobalData.deviceId = "39a7abdd06b3c7ab" // GlobalData.deviceId = "39a7abdd06b3c7ab"
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED) val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
registerReceiver(BootReceiver(), filter) registerReceiver(BootReceiver(), filter)
// ObjectBox.init(this) ObjectBox.init(this)
} }
override fun onTerminate() { override fun onTerminate() {
@@ -0,0 +1,60 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogFoodRecognizeBinding
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
/**
* 菜品识别结果弹窗,继承 BottomSheetDialog 确保只初始化一次
* @param activity 宿主 Activity
* @param onItemSelected 用户点击某一菜品时的回调
*/
class FoodRecognizeDialog(
private val activity: BaseActivity,
private val onItemSelected: (item: CookFoodGoodsEntity) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
private val binding = DialogFoodRecognizeBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<CookFoodGoodsEntity>()
private val adapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
list[position].isClicked = true
notifyItemChanged(position)
Handler(Looper.getMainLooper()).postDelayed({
onItemSelected(list[position])
dismiss()
}, 300)
}
}
init {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false
setOnDismissListener { activity.hideStatusBar() }
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = this@FoodRecognizeDialog.adapter
}
}
/**
* 加载识别结果数据并显示弹窗
* @param items 识别到的菜品列表
*/
@SuppressLint("NotifyDataSetChanged")
fun loadData(items: List<CookFoodGoodsEntity>) {
list.clear()
list.addAll(items)
adapter.notifyDataSetChanged()
if (!isShowing) show()
}
}
@@ -1,308 +1,308 @@
//package com.shuwei.dish.match.objbox package com.shuwei.dish.match.objbox
//
//import android.content.Context import android.content.Context
//import android.graphics.Bitmap import android.graphics.Bitmap
//import android.util.Log import android.util.Log
//import androidx.core.graphics.scale import androidx.core.graphics.scale
//import com.google.gson.Gson import com.google.gson.Gson
//import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
//import com.shuwei.dish.match.utils.AssetsTool import com.shuwei.dish.match.utils.AssetsTool
//import com.shuwei.dish.match.utils.LogSaveUtil import com.shuwei.dish.match.utils.LogSaveUtil
//import com.shuwei.dish.match.utils.ext.toJsonString import com.shuwei.dish.match.utils.ext.toJsonString
//import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
//import org.pytorch.IValue import org.pytorch.IValue
//import org.pytorch.Module import org.pytorch.Module
//import org.pytorch.torchvision.TensorImageUtils import org.pytorch.torchvision.TensorImageUtils
//import java.io.File import java.io.File
//import java.io.FileOutputStream import java.io.FileOutputStream
//import java.io.IOException import java.io.IOException
//import java.io.InputStream import java.io.InputStream
//
//
//object FoodModule { object FoodModule {
//
// private lateinit var module_mobile: Module private lateinit var module_mobile: Module
//
// private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
// private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
//
// // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224) // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
// private const val MODEL_INPUT_WIDTH = 300 private const val MODEL_INPUT_WIDTH = 300
// private const val MODEL_INPUT_HEIGHT = 300 private const val MODEL_INPUT_HEIGHT = 300
// const val DEFAULT_FOOD_INDEX = -1 const val DEFAULT_FOOD_INDEX = -1
// const val DEFAULT_QUERY_COUNT = 50 const val DEFAULT_QUERY_COUNT = 50
//
// const val BAG_RATE = 0.05 const val BAG_RATE = 0.05
//
// suspend fun init(context: Context) { suspend fun init(context: Context) {
// withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
// initDefFoodData(context) initDefFoodData(context)
// } }
// } }
//
//// fun uri2FloatArray(uri: Uri): FloatArray? { // fun uri2FloatArray(uri: Uri): FloatArray? {
//// return MyApp.instance?.let { context -> // return MyApp.instance?.let { context ->
//// ImageUtil.uriToBitmap(context, uri)?.let { // ImageUtil.uriToBitmap(context, uri)?.let {
//// bitmap2FloatArray(it) // bitmap2FloatArray(it)
//// }
//// }
//// }
//
// fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
// var rgb565Bitmap: Bitmap? = null
// try {
// val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
// if (isRecycle) {
// originBitmap.recycle()
// } // }
// rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
// scaledBitmap.recycle()
// val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
// rgb565Bitmap,
// NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
// NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
// )
// val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
// return outputTensor.dataAsFloatArray
// } catch (e: OutOfMemoryError) {
// e.printStackTrace()
// logInfo("bitmap2FloatArray异常:${e.message}")
// } finally {
// if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
// rgb565Bitmap.recycle()
// }
// //System.gc()
// //System.runFinalization()
// }
// return null
// }
//
//// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
//// return uri2FloatArray(uri)?.let {
//// queryFood(it, queryCount)
//// }
//// }
//
//// /**
//// * 返回识别物品名称列表
//// */
//// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
//// val floatArray = bitmap2FloatArray(bitmap)
//// return queryFood(floatArray, queryCount)
//// }
//
//// /**
//// * 返回识别物品IdNameScore对象列表
//// */
//// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
//// val floatArray = bitmap2FloatArray(bitmap)
//// return queryFoodNameScore(floatArray, queryCount)
//// }
//
// suspend fun queryFoodNameScore(
// floatArray: FloatArray?,
// queryCount: Int = DEFAULT_QUERY_COUNT
// ): List<IdNameScore> {
// if (floatArray == null) return emptyList()
//// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//// //查询比较分数
////// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
//// var idScoreList: List<IdWithScore>
//// try {
//// idScoreList = query.findIdsWithScores();
//// } finally {
//// // 先关闭Query,释放Cursor
//// query.close()
//// }
//
// logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
// val idScoreList = ObjectBox.query(floatArray, queryCount)
// // 批量加载所有实体,一次DB操作替代循环单条查询
// val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
// val nameScoreList = mutableListOf<IdNameScore>()
// idScoreList.forEach {
// val name = foodMap[it.id]?.name ?: ""
// nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
// }
// logInfo("queryFood数据:${nameScoreList.toJsonString()}")
// return nameScoreList
// }
//
// suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
// val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
// logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// if (nameScoreList.isEmpty()) {
// return emptyList()
// }
// //暂时不过滤
// val maxScoreList = nameScoreList
// //.filter { it.score < 0.15 }
// .groupBy { it.name }
// .map { (_, value) -> value.minByOrNull { it.score }!! }
// .toMutableList()
//// val map = mutableMapOf<String, Int>()
//// nameScoreList.forEach {
//// val key = it.name
//// val count = map[key] ?: 0
//// map[key] = count + 1
//// }
//// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
//// val firstFood = nameScoreList[0].name
//// orderList.remove(firstFood)
//// orderList.add(0, firstFood)
////
//// val sortedScoreList = maxScoreList.sortedWith(compareBy {
//// orderList.indexOf(it.name)
//// })
// val sortedScoreList = maxScoreList.sortedBy { it.score }
// logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
// return sortedScoreList
// }
//
//// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
//// val map = mutableMapOf<String, Int>()
//// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
//// nameScoreList.filter { it.score < 0.05 }.forEach {
//// val count = map[it.name] ?: 0
//// map[it.name] = count + 1
//// }
//// val list = map.entries.sortedByDescending { it.value }.map { it.key }
//// return list
//// }
//
// data class IdNameScore(
// val id: Long,
// var name: String,
// val score: Double
// )
//
// suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
// try {
// val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX)
// if (count > 0) {
// return
// }
// val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
// val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
// val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
//
// val embeddingsList: List<List<Float>> =
// Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
// val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
// val classInfo: FoodClassInfo =
// Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
//
// val foodMap = classInfo.idx_to_class
// val list = mutableListOf<Food>()
// val size = embeddingsList.size
// val size2 = labelsList.size
// val size3 = foodMap.size
// logInfo("initDefFoodData: $size,$size2,$size3")
// embeddingsList.forEachIndexed { index, floatList ->
// val classIdx = labelsList[index]
// val foodName = foodMap["$classIdx"]
// val array = floatList.toFloatArray()
// // ObjectBox.boxStore.runInTx {
// // box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
// // }
// val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
// list.add(food)
// }
//// val tempList = list.chunked(10)
// ObjectBox.putAll(list)
// action()
// } catch (e: Exception) {
// e.printStackTrace()
// logInfo("initDefFoodData: ---${e.message}--")
// } // }
// } // }
//
// fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
var rgb565Bitmap: Bitmap? = null
try {
val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
if (isRecycle) {
originBitmap.recycle()
}
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
scaledBitmap.recycle()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
rgb565Bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
logInfo("bitmap2FloatArray异常:${e.message}")
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
// return uri2FloatArray(uri)?.let {
// queryFood(it, queryCount)
// }
// }
// /** // /**
// * ,此方法的主要目的是:从assets 拷贝到 app的cache目录 // * 返回识别物品名称列表
// * @param context
// * @param fileName
// * @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
// *
// * 不可能反正SD
// */ // */
//// fun copyAssetToCache(context: Context, fileName: String): String? { // fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
//// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦 // val floatArray = bitmap2FloatArray(bitmap)
//// val cacheDir = context.getCacheDir() // return queryFood(floatArray, queryCount)
//// if (!cacheDir.exists()) { // }
//// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
//// } // /**
//// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置 // * 返回识别物品IdNameScore对象列表
//// if (outPath.exists()) { // */
//// outPath.delete() // TODO 如果该文件已经存在,就删掉 // fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
//// } // val floatArray = bitmap2FloatArray(bitmap)
//// var `is`: InputStream? = null // 读取 // return queryFoodNameScore(floatArray, queryCount)
//// var fos: FileOutputStream? = null // 写入 // }
//// try {
//// // 创建文件,如果创建成功,就返回true suspend fun queryFoodNameScore(
//// val res = outPath.createNewFile() floatArray: FloatArray?,
//// if (res) { queryCount: Int = DEFAULT_QUERY_COUNT
//// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节 ): List<IdNameScore> {
//// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath if (floatArray == null) return emptyList()
//// val buf = ByteArray(`is`.available()) // 缓存区 // val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//// var byteCount: Int // //查询比较分数
//// //// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
//// // 开始循环读取 // var idScoreList: List<IdWithScore>
//// while ((`is`.read(buf).also { byteCount = it }) != -1) {
//// fos.write(buf, 0, byteCount)
//// }
//// return outPath.getAbsolutePath()
//// }
//// } catch (e: IOException) {
//// e.printStackTrace()
//// } finally {
//// try {
//// // TODO 一定要记得关闭资源,为了不去性能的磨损
//// fos?.flush()
//// `is`?.close()
//// fos?.close()
//// } catch (e: IOException) {
//// e.printStackTrace()
//// }
//// }
//// return null
//// }
//
// fun copyAssetToCache(context: Context, fileName: String): String? {
// val cacheFile = File(context.cacheDir, fileName)
// val buffer = ByteArray(8 * 1024)
// var inputStream: InputStream? = null
// var outputStream: FileOutputStream? = null
//
// try { // try {
// inputStream = context.assets.open(fileName) // idScoreList = query.findIdsWithScores();
// outputStream = FileOutputStream(cacheFile) // } finally {
// var byteCount: Int // // 先关闭Query,释放Cursor
// while (inputStream.read(buffer).also { byteCount = it } != -1) { // query.close()
// outputStream.write(buffer, 0, byteCount) // }
logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
val idScoreList = ObjectBox.query(floatArray, queryCount)
// 批量加载所有实体,一次DB操作替代循环单条查询
val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
val name = foodMap[it.id]?.name ?: ""
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
}
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
return nameScoreList
}
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
}
//暂时不过滤
val maxScoreList = nameScoreList
//.filter { it.score < 0.15 }
.groupBy { it.name }
.map { (_, value) -> value.minByOrNull { it.score }!! }
.toMutableList()
// val map = mutableMapOf<String, Int>()
// nameScoreList.forEach {
// val key = it.name
// val count = map[key] ?: 0
// map[key] = count + 1
// }
// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
// val firstFood = nameScoreList[0].name
// orderList.remove(firstFood)
// orderList.add(0, firstFood)
//
// val sortedScoreList = maxScoreList.sortedWith(compareBy {
// orderList.indexOf(it.name)
// })
val sortedScoreList = maxScoreList.sortedBy { it.score }
logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
// val map = mutableMapOf<String, Int>()
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// nameScoreList.filter { it.score < 0.05 }.forEach {
// val count = map[it.name] ?: 0
// map[it.name] = count + 1
// }
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
// return list
// }
data class IdNameScore(
val id: Long,
var name: String,
val score: Double
)
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
try {
val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX)
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
val embeddingsList: List<List<Float>> =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
val classInfo: FoodClassInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
val list = mutableListOf<Food>()
val size = embeddingsList.size
val size2 = labelsList.size
val size3 = foodMap.size
logInfo("initDefFoodData: $size,$size2,$size3")
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
// ObjectBox.boxStore.runInTx {
// box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
// }
val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
list.add(food)
}
// val tempList = list.chunked(10)
ObjectBox.putAll(list)
action()
} catch (e: Exception) {
e.printStackTrace()
logInfo("initDefFoodData: ---${e.message}--")
}
}
/**
* ,此方法的主要目的是:从assets 拷贝到 app的cache目录
* @param context
* @param fileName
* @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
*
* 不可能反正SD
*/
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.getCacheDir()
// if (!cacheDir.exists()) {
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
// }
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
// if (outPath.exists()) {
// outPath.delete() // TODO 如果该文件已经存在,就删掉
// }
// var `is`: InputStream? = null // 读取
// var fos: FileOutputStream? = null // 写入
// try {
// // 创建文件,如果创建成功,就返回true
// val res = outPath.createNewFile()
// if (res) {
// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
// val buf = ByteArray(`is`.available()) // 缓存区
// var byteCount: Int
//
// // 开始循环读取
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
// fos.write(buf, 0, byteCount)
// }
// return outPath.getAbsolutePath()
// } // }
// outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
// return cacheFile.absolutePath
// } catch (e: IOException) { // } catch (e: IOException) {
// e.printStackTrace() // e.printStackTrace()
// logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
// // 拷贝失败时删除残缺文件,避免下次读取到损坏文件
// if (cacheFile.exists()) {
// cacheFile.delete()
// }
// } finally { // } finally {
// outputStream?.close() // try {
// inputStream?.close() // // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos?.flush()
// `is`?.close()
// fos?.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// } // }
// return null // return null
// } // }
//
// private fun logInfo(msg:String) { fun copyAssetToCache(context: Context, fileName: String): String? {
// Log.d("FoodModule", msg) val cacheFile = File(context.cacheDir, fileName)
// LogSaveUtil.saveLogFile(msg) val buffer = ByteArray(8 * 1024)
// } var inputStream: InputStream? = null
// var outputStream: FileOutputStream? = null
//}
try {
inputStream = context.assets.open(fileName)
outputStream = FileOutputStream(cacheFile)
var byteCount: Int
while (inputStream.read(buffer).also { byteCount = it } != -1) {
outputStream.write(buffer, 0, byteCount)
}
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
return cacheFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
private fun logInfo(msg:String) {
Log.d("FoodModule", msg)
LogSaveUtil.saveLogFile(msg)
}
}
@@ -2,7 +2,6 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.util.Log
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
@@ -18,12 +17,15 @@ import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivityInitBinding import com.shuwei.dish.match.databinding.ActivityInitBinding
import com.shuwei.dish.match.db.AppRepository import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AppUtil import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.NetworkUtils import com.shuwei.dish.match.utils.NetworkUtils
import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.Weigher2 import com.shuwei.dish.match.utils.Weigher2
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory import com.shuwei.dish.match.viewmodel.factory.AppFactory
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -47,9 +49,9 @@ class InitActivity : BaseActivity() {
// useBackground(false) // useBackground(false)
binding = ActivityInitBinding.inflate(layoutInflater) binding = ActivityInitBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// lifecycleScope.launch { lifecycleScope.launch {
// FoodModule.init(this@InitActivity) FoodModule.init(this@InitActivity)
// } }
// setHeaderBackground(isHomePage = true) // setHeaderBackground(isHomePage = true)
setHeaderBgVisible(false) setHeaderBgVisible(false)
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString() BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
@@ -63,9 +65,6 @@ class InitActivity : BaseActivity() {
WeightUtil.startContinuousRead() WeightUtil.startContinuousRead()
initViews() initViews()
val dpi = resources.displayMetrics.density
val metrics = resources.displayMetrics.toString()
Log.d(TAG, "dpi=$dpi, $metrics")
} }
private fun initViewModel() { private fun initViewModel() {
@@ -74,9 +73,9 @@ class InitActivity : BaseActivity() {
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java] appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
} }
private fun startNextPage() { private fun startNextPage(withDelay: Boolean = false) {
lifecycleScope.launch { lifecycleScope.launch {
delay(2000) if (withDelay) delay(2000)
// 子设备:直接进入小屏专属页面,不走原有大屏业务流程 // 子设备:直接进入小屏专属页面,不走原有大屏业务流程
if (GlobalData.deviceRole == DeviceRole.SLAVE) { if (GlobalData.deviceRole == DeviceRole.SLAVE) {
startActivity<SlaveActivity>() startActivity<SlaveActivity>()
@@ -135,9 +134,6 @@ class InitActivity : BaseActivity() {
binding.btnConnectNetwork.setOnClickListener { binding.btnConnectNetwork.setOnClickListener {
openNetworkSettings() openNetworkSettings()
} }
// 启动加载动画 - 持续旋转
startLoadingAnimation()
} }
/** /**
@@ -159,9 +155,12 @@ class InitActivity : BaseActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (NetworkUtils.isNetworkConnected(this)) { if (NetworkUtils.isNetworkConnected(this)) {
startNextPage() // 有网络时隐藏网络检测区域,直接跳转
binding.llNetwork.gone()
startNextPage(withDelay = false)
return return
} }
binding.llNetwork.visible()
startCountdown() startCountdown()
} }
@@ -178,9 +177,11 @@ class InitActivity : BaseActivity() {
startTime = SystemClock.elapsedRealtime() startTime = SystemClock.elapsedRealtime()
lastNetworkCheckTime = 0 lastNetworkCheckTime = 0
// binding.llNetwork.setBackgroundColor(Color.TRANSPARENT) // binding.llNetwork.setBackgroundColor(Color.TRANSPARENT)
binding.llLoading.visibility = android.view.View.VISIBLE binding.llLoading.visible()
binding.llNetworkButton.visibility = android.view.View.GONE binding.llNetworkButton.gone()
binding.tvCountdown.text = "60秒" binding.tvCountdown.text = "60秒"
// 无网络时才启动加载动画
startLoadingAnimation()
scheduleCountdown() scheduleCountdown()
} }
@@ -233,8 +234,8 @@ class InitActivity : BaseActivity() {
*/ */
private fun showNetworkButton() { private fun showNetworkButton() {
// binding.llNetwork.setBackgroundColor("#5C77F7".toColorInt()) // binding.llNetwork.setBackgroundColor("#5C77F7".toColorInt())
binding.llLoading.visibility = android.view.View.GONE binding.llLoading.gone()
binding.llNetworkButton.visibility = android.view.View.VISIBLE binding.llNetworkButton.visible()
} }
/** /**
@@ -52,6 +52,7 @@ class MasterScaleActivity : BaseActivity() {
setContentView(binding.root) setContentView(binding.root)
setHeaderBackground() setHeaderBackground()
binding.ivMasterBack.setOnClickListener { finish() }
binding.rvScaleList.layoutManager = LinearLayoutManager(this) binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱 binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
binding.rvScaleList.adapter = adapter binding.rvScaleList.adapter = adapter
@@ -1,10 +1,11 @@
package com.shuwei.dish.match.ui package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
@@ -12,23 +13,34 @@ import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodRecognizeDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.net.NetViewModel import android.view.ViewGroup
import androidx.camera.view.PreviewView
import androidx.lifecycle.lifecycleScope
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.SwipeCallback import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable import java.io.Serializable
import kotlin.getValue import java.util.concurrent.atomic.AtomicBoolean
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class PrepareCookActivity : BaseActivity() { class PrepareCookActivity : BaseActivity() {
@@ -40,6 +52,8 @@ class PrepareCookActivity : BaseActivity() {
private lateinit var binding: ActivityPrepareCookBinding private lateinit var binding: ActivityPrepareCookBinding
private val cameraUtils: CameraUtils by lazy { CameraUtils(this) }
private var food: FoodRecord? = null private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null private var goodsList: MutableList<CookFoodGoodsEntity>? = null
@@ -69,6 +83,7 @@ class PrepareCookActivity : BaseActivity() {
binding.tvDishName.text = food?.foodName ?: "" binding.tvDishName.text = food?.foodName ?: ""
addViewClickListener() addViewClickListener()
initCamera(binding.flCameraContainer)
initRecyclerView() initRecyclerView()
getDishDetail() getDishDetail()
@@ -81,6 +96,9 @@ class PrepareCookActivity : BaseActivity() {
//Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight") //Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight")
if (address == AddressUtil.ONE) { if (address == AddressUtil.ONE) {
binding.tvDishPartWeight.text = "${weight}" binding.tvDishPartWeight.text = "${weight}"
if (state == WeightUtil.STATE_STABLE) {
recognizeFood(weight)
}
} }
}) })
binding.ivWeightClear.setOnClickListener { binding.ivWeightClear.setOnClickListener {
@@ -134,6 +152,25 @@ class PrepareCookActivity : BaseActivity() {
} }
} }
private val isTakingPhoto = AtomicBoolean(false)
/**
* 重量稳定时触发菜品识别
* @param weight 当前稳定重量
*/
private fun recognizeFood(weight: Double) {
if (weight < 10) {
if (recognizeDialog.isShowing) recognizeDialog.dismiss()
return
}
if (isTakingPhoto.get() || recognizeDialog.isShowing) return
isTakingPhoto.set(true)
cameraUtils.takePhoto(
succCallback = cameraSuccessCallback,
failCallback = cameraFailureCallback
)
}
private fun addFood() { private fun addFood() {
FoodSearchDialog().show(this) { item -> FoodSearchDialog().show(this) { item ->
val filterResult = list.firstOrNull { it.goodsId == item.goodsId } val filterResult = list.firstOrNull { it.goodsId == item.goodsId }
@@ -251,6 +288,16 @@ class PrepareCookActivity : BaseActivity() {
private var firstReqSize = 0 private var firstReqSize = 0
private val list = mutableListOf<CookFoodGoodsEntity>() private val list = mutableListOf<CookFoodGoodsEntity>()
private val recognizeDialog by lazy {
FoodRecognizeDialog(this) { item ->
// TODO: 处理识别结果选中
}
.apply {
setOnDismissListener {
isTakingPhoto.set(false)
}
}
}
private val dishPartAdapter by lazy { private val dishPartAdapter by lazy {
DishPartAdapter(list).apply { DishPartAdapter(list).apply {
setOnItemClickListener { _, _, positon -> setOnItemClickListener { _, _, positon ->
@@ -326,6 +373,26 @@ class PrepareCookActivity : BaseActivity() {
} }
} }
override fun onResume() {
super.onResume()
cameraUtils.bind()
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
}
/**
* 初始化相机并绑定预览容器
* @param container 相机预览容器
*/
fun initCamera(container: ViewGroup) {
cameraUtils.initCamera()
val previewBinding = LayoutCameraPreviewBinding.inflate(layoutInflater, container)
cameraUtils.setPreviewController(previewBinding.previewView)
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@SuppressLint("GestureBackNavigation") @SuppressLint("GestureBackNavigation")
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
@@ -367,4 +434,49 @@ class PrepareCookActivity : BaseActivity() {
}.show() }.show()
} }
private val cameraSuccessCallback: (Uri) -> Unit = { uri ->
Log.d(TAG, "takePhoto success")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val bitmap = ImageUtil.uriToBitmap(this@PrepareCookActivity, uri)
if (bitmap == null) {
Log.d(TAG, "takePhoto bitmap is null")
isTakingPhoto.set(false)
return@withContext
}
val foodScoreList = FoodModule.getFoodScoreList(bitmap)
if (foodScoreList.isEmpty()) {
Log.d(TAG, "takePhoto foodScoreList is empty")
isTakingPhoto.set(false)
return@withContext
}
withContext(Dispatchers.Main) {
queryFood(foodScoreList)
}
}
}
}
private val cameraFailureCallback: (String) -> Unit = { errMsg ->
Log.d(TAG, "takePhoto failure")
isTakingPhoto.set(false)
}
private fun queryFood(list: List<FoodModule.IdNameScore>) {
Log.d(TAG, "takePhoto queryFood, list=${list.toString()}")
//调用接口成功,返回食材列表设置isTakingPhoto.set(false),暂时写死数据
val foodList = mutableListOf<CookFoodGoodsEntity>()
list.forEachIndexed { index, food ->
val foodName = food.name.split("WP").first()
val foodScore = ((1-food.score) * 100).roundedDecimalPlace(2)
foodList.add(CookFoodGoodsEntity(
goodsId = index.toString(),
goodsName = "${foodName}-${foodScore}%",
foodId = index.toString()
))
}
recognizeDialog.loadData(foodList)
isTakingPhoto.set(false)
}
} }
@@ -82,18 +82,19 @@ class SelectDishActivity : BaseActivity() {
private var configFinished = false private var configFinished = false
fun judgeDeviceConfig(block: () -> Unit) { fun judgeDeviceConfig(block: () -> Unit) {
if (configFinished) { // if (configFinished) {
block() // block()
return // return
} // }
appViewModel.loadSeasoning { // appViewModel.loadSeasoning {
if (it.isEmpty()) { // if (it.isEmpty()) {
showDeviceConfigDialog() // showDeviceConfigDialog()
return@loadSeasoning // return@loadSeasoning
} // }
configFinished = true // configFinished = true
block() // block()
} // }
block()
} }
private fun showDeviceConfigDialog() { private fun showDeviceConfigDialog() {
@@ -17,6 +17,7 @@ import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentCollectBinding import com.shuwei.dish.match.databinding.FragmentCollectBinding
import com.shuwei.dish.match.dialog.Loading import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.objbox.FoodCollectionBean import com.shuwei.dish.match.objbox.FoodCollectionBean
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.ui.SettingActivity import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.utils.BitmapSaver import com.shuwei.dish.match.utils.BitmapSaver
import com.shuwei.dish.match.utils.Debouncer import com.shuwei.dish.match.utils.Debouncer
@@ -119,38 +120,38 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
} }
private fun getImageVector(index: Int, bitmap: Bitmap) { private fun getImageVector(index: Int, bitmap: Bitmap) {
//// val bitmap = BitmapCropper.cropCenter( // val bitmap = BitmapCropper.cropCenter(
//// original = srcBmp, // original = srcBmp,
//// targetWidth = 900, targetHeight = 900, // targetWidth = 900, targetHeight = 900,
////// offsetX = 30, offsetY = 100 //// offsetX = 30, offsetY = 100
//// )
// val imageVector = try {
// FoodModule.bitmap2FloatArray(bitmap, false)
// } catch (e: Exception) {
// e.printStackTrace()
// toast("操作失败")
// log("操作失败:${e.message}")
// hideWaitingDialog()
// return
// }
// val file = BitmapSaver.saveToAppFilesDir(
// bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
// ) // )
// log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") val imageVector = try {
// FoodModule.bitmap2FloatArray(bitmap, false)
// activity?.runOnUiThread { } catch (e: Exception) {
// foodCollectionList[index].let { e.printStackTrace()
// it.imageVector = imageVector toast("操作失败")
// it.bitmap = null log("操作失败:${e.message}")
// it.isShowCamera = false hideWaitingDialog()
// it.imageFile = file return
// } }
// collectionAdapter.notifyItemChanged(index) val file = BitmapSaver.saveToAppFilesDir(
// } bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
// if (bitmap.isRecycled.not()) { )
// bitmap.recycle() log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
// }
// hideWaitingDialog() activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
collectionAdapter.notifyItemChanged(index)
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
hideWaitingDialog()
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -0,0 +1,175 @@
package com.shuwei.dish.match.utils;
import android.os.SystemClock;
import android.util.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* Created by Administrator on 2018/11/9.
*/
public class GpioUtils {
private static final String TAG = "GpioUtils";
/*
给export申请权限
*/
public static void upgradeRootPermissionForExport() {
upgradeRootPermission("/sys/class/gpio/export");
}
/*
配置一个gpio路径
*/
public static boolean exportGpio(int gpio) {
return writeNode("/sys/class/gpio/export", "" + gpio);
}
/*
给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用
*/
public static void upgradeRootPermissionForGpio(int gpio) {
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction");
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value");
}
/*
设置io口为输入或输出
*/
public static boolean setGpioDirection(int gpio, int arg) {
String gpioDirection = "";
if (arg == 0) gpioDirection = "out";
else if (arg == 1) gpioDirection = "in";
else return false;
return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection);
}
/*
获取io口的状态为输出还是输入
*/
public static String getGpioDirection(int gpio) {
String gpioDirection = "";
gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction");
return gpioDirection;
}
/*
给输出io口写值,高电平或低电平
*/
public static boolean writeGpioValue(int gpio, String arg) {
return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg);
}
//获取当前gpio是高还是低
public static String getGpioValue(int gpio) {
return readNode("/sys/class/gpio/gpio" + gpio + "/value");
}
private static boolean upgradeRootPermission(String path) {
Process process = null;
DataOutputStream os = null;
try {
String cmd = "chmod 777 " + path;
process = Runtime.getRuntime().exec("su"); //切换到root帐号
os = new DataOutputStream(process.getOutputStream());
os.writeBytes(cmd + "\n");
os.writeBytes("exit\n");
os.flush();
process.waitFor();
} catch (Exception e) {
} finally {
try {
if (os != null) {
os.close();
}
process.destroy();
} catch (Exception e) {
}
}
try {
return process.waitFor() == 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
private static boolean writeNode(String path, String arg) {
Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg);
if (path == null || arg == null) {
Log.e(TAG, "set node error");
return false;
}
FileWriter fileWriter = null;
BufferedWriter bufferedWriter = null;
try {
fileWriter = new FileWriter(path);
fileWriter.write(arg);
} catch (Exception e) {
Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg);
e.printStackTrace();
return false;
} finally {
try {
if (fileWriter != null) {
fileWriter.close();
}
if (bufferedWriter != null) {
bufferedWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private static long mTime = 0;
private static int mFailTimes = 0;
private static String readNode(String path) {
String result = "";
FileReader fread = null;
BufferedReader buffer = null;
try {
fread = new FileReader(path);
buffer = new BufferedReader(fread);
String str = null;
while ((str = buffer.readLine()) != null) {
result = str;
break;
}
mFailTimes = 0;
} catch (IOException e) {
Log.e(TAG, "IO Exception");
e.printStackTrace();
if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) {
mFailTimes++;
}
if (mFailTimes >= 3) {
Log.d(TAG, "read format node continuous failed three times, exist thread");
}
} finally {
try {
if (buffer != null) {
buffer.close();
}
if (fread != null) {
fread.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
@@ -0,0 +1,57 @@
package com.shuwei.dish.match.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -0,0 +1,25 @@
package com.shuwei.dish.match.utils;
public class LightManager {
private static final String TAG = "LightManager";
public static void openGreenLight() {
L.e(TAG, "openGreenLight");
GpioUtils.writeGpioValue(41, "1");
}
public static void closeGreenLight() {
L.e(TAG, "closeGreenLight");
GpioUtils.writeGpioValue(41, "0");
}
public static void openRedLight() {
L.e(TAG, "openRedLight");
GpioUtils.writeGpioValue(40, "1");
}
public static void closeRedLight() {
L.e(TAG, "closeRedLight");
GpioUtils.writeGpioValue(40, "0");
}
}
+2 -1
View File
@@ -59,7 +59,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:gravity="center"
android:visibility="gone">
<!-- Loading 动画容器 --> <!-- Loading 动画容器 -->
<LinearLayout <LinearLayout
@@ -1,19 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:background="@color/bg_color"> tools:background="@color/bg_color">
<!-- 顶部标题 --> <!-- 顶部标题 -->
<TextView <TextView
android:id="@+id/tvMasterTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:text="全设备秤数据监控" android:text="全设备秤数据监控"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textColor="@color/home_title" android:textColor="@color/home_title"
android:textSize="36sp" android:textSize="36sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -23,20 +27,36 @@
android:id="@+id/tvConnectionStatus" android:id="@+id/tvConnectionStatus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" app:layout_constraintTop_toBottomOf="@id/tvMasterTitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:textColor="@color/home_sub_title" android:textColor="@color/home_sub_title"
android:textSize="24sp" android:textSize="24sp"
tools:text="已发现设备:3 台" /> tools:text="已发现设备:3 台" />
<ImageView
android:id="@+id/ivMasterBack"
android:layout_width="70dp"
android:layout_height="60dp"
android:layout_gravity="start"
android:paddingStart="30dp"
android:paddingEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvMasterTitle"
app:layout_constraintBottom_toBottomOf="@id/tvMasterTitle"
android:src="@drawable/ic_back"
tools:ignore="ContentDescription" />
<!-- 秤数据列表 --> <!-- 秤数据列表 -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvScaleList" android:id="@+id/rvScaleList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus"
app:layout_constraintBottom_toBottomOf="parent"
android:overScrollMode="never" android:overScrollMode="never"
android:padding="16dp" android:padding="16dp"
tools:listitem="@layout/list_item_scale_data"/> tools:listitem="@layout/list_item_scale_data"/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@@ -262,6 +262,11 @@
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/flCameraContainer"
android:layout_width="1dp"
android:layout_height="1dp" />
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCook" android:id="@+id/btnCook"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -0,0 +1,44 @@
<?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="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:background="@color/white">
<View
android:id="@+id/viewLine"
android:layout_width="90dp"
android:layout_height="10dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_gray_dc_5" />
<TextView
android:id="@+id/tvSheetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="菜品识别结果"
android:textColor="@color/black333"
android:textSize="36sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="45dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="45dp"
android:layout_marginBottom="15dp"
android:minHeight="500dp"
android:nestedScrollingEnabled="true"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_search_goods_info" />
</LinearLayout>