1409 lines
53 KiB
Kotlin
1409 lines
53 KiB
Kotlin
package com.sw.dualscreen.activity
|
||
|
||
import android.annotation.SuppressLint
|
||
import android.content.Context
|
||
import android.content.Intent
|
||
import android.graphics.Bitmap
|
||
import android.graphics.Outline
|
||
import android.hardware.display.DisplayManager
|
||
import android.net.Uri
|
||
import android.os.Handler
|
||
import android.os.Looper
|
||
import android.text.TextUtils
|
||
import android.view.Display
|
||
import android.view.View
|
||
import android.view.ViewOutlineProvider
|
||
import android.widget.TextView
|
||
import androidx.activity.OnBackPressedCallback
|
||
import androidx.activity.viewModels
|
||
import androidx.camera.core.CameraSelector
|
||
import androidx.camera.core.ImageAnalysis
|
||
import androidx.camera.core.ImageCapture
|
||
import androidx.camera.core.ImageCaptureException
|
||
import androidx.camera.core.Preview
|
||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||
import androidx.core.content.ContextCompat
|
||
import androidx.core.view.isVisible
|
||
import androidx.lifecycle.lifecycleScope
|
||
import androidx.recyclerview.widget.GridLayoutManager
|
||
import androidx.recyclerview.widget.RecyclerView
|
||
import com.google.common.util.concurrent.ListenableFuture
|
||
import com.sw.dualscreen.GlobalData
|
||
import com.sw.dualscreen.GlobalKey
|
||
import com.sw.dualscreen.adapter.MainFoodListAdapter
|
||
import com.sw.dualscreen.databinding.ActivityMainBinding
|
||
import com.sw.dualscreen.databinding.LayoutEatDetailBinding
|
||
import com.sw.dualscreen.databinding.LayoutMainFoodList2Binding
|
||
import com.sw.dualscreen.databinding.LayoutMainFoodListBinding
|
||
import com.sw.dualscreen.dialog.EnvSwitchDialog
|
||
import com.sw.dualscreen.dialog.RemindDialog
|
||
import com.sw.dualscreen.ext.dp
|
||
import com.sw.dualscreen.ext.gone
|
||
import com.sw.dualscreen.ext.invisible
|
||
import com.sw.dualscreen.ext.load
|
||
import com.sw.dualscreen.ext.visible
|
||
import com.sw.dualscreen.model.response.ChargeModeEvent
|
||
import com.sw.dualscreen.model.response.FoodInfo
|
||
import com.sw.dualscreen.model.request.v2.PlaceOrderRequest
|
||
import com.sw.dualscreen.model.response.PaySuccessEvent
|
||
import com.sw.dualscreen.model.response.ResetRecognizeEvent
|
||
import com.sw.dualscreen.model.response.UpdateRefreshEvent
|
||
import com.sw.dualscreen.model.response.v2.FaceVO
|
||
import com.sw.dualscreen.model.response.v2.NewFoodInfo
|
||
import com.sw.dualscreen.model.response.v2.toFoodInfo
|
||
import com.sw.dualscreen.objbox.Food
|
||
import com.sw.dualscreen.objbox.FoodModule
|
||
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
|
||
import com.sw.dualscreen.objbox.ObjectBox
|
||
import com.sw.dualscreen.presentation.MainScreenPresentation
|
||
import com.sw.dualscreen.sdk.SensorScaleUtils
|
||
import com.sw.dualscreen.socket.TcpClient
|
||
import com.sw.dualscreen.utils.ActivityManager
|
||
import com.sw.dualscreen.utils.BitmapSaver
|
||
import com.sw.dualscreen.utils.Debouncer
|
||
import com.sw.dualscreen.utils.FoodVectorTool
|
||
import com.sw.dualscreen.utils.GsonUtils
|
||
import com.sw.dualscreen.utils.ImageUtil
|
||
import com.sw.dualscreen.utils.IntervalExecutor
|
||
import com.sw.dualscreen.utils.SPUtil
|
||
import com.sw.dualscreen.utils.SpTool
|
||
import com.sw.dualscreen.view.CustomBottomSheetDialog
|
||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||
import com.sw.dualscreen.viewmodel.NetViewModelV2
|
||
import com.sw.plate.utils.Base64
|
||
import com.sw.plate.utils.LightManager
|
||
import com.sw.plate.utils.ToastUtils
|
||
import com.sw.plate.utils.arcface.facedb.FaceDatabase
|
||
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
|
||
import com.sw.plate.utils.arcface.faceserver.FaceServer
|
||
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.Job
|
||
import kotlinx.coroutines.delay
|
||
import kotlinx.coroutines.launch
|
||
import kotlinx.coroutines.runBlocking
|
||
import kotlinx.coroutines.withContext
|
||
import org.greenrobot.eventbus.Subscribe
|
||
import org.greenrobot.eventbus.ThreadMode
|
||
import org.json.JSONObject
|
||
import timber.log.Timber
|
||
import java.io.File
|
||
import java.time.LocalDateTime
|
||
import java.time.ZoneId
|
||
import java.time.format.DateTimeFormatter
|
||
import java.util.Locale
|
||
import java.util.concurrent.Executors
|
||
import kotlin.math.roundToInt
|
||
|
||
/**
|
||
* 服务员显示界面
|
||
*/
|
||
@SuppressLint("NotifyDataSetChanged")
|
||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||
|
||
companion object {
|
||
private const val TAG = "MainActivity"
|
||
const val TIME_OUT = 5 * 60 * 1000L
|
||
|
||
const val WEIGHT_CHANGE_VALUE = 20
|
||
const val WEIGHT_RESET_RECOGNIZE = 10
|
||
// 档口按重量计费的最小计费重量(克),净重超过此值才计费/下单
|
||
const val MIN_BILLABLE_WEIGHT = 10
|
||
}
|
||
|
||
private val viewModel by viewModels<NetViewModelV2>()
|
||
private var imageCapture: ImageCapture? = null
|
||
var isAnalyzing = true // 控制是否进行图像分析
|
||
|
||
// private var presentation: SecondaryScreenPresentation? = null
|
||
private var presentation: MainScreenPresentation? = null
|
||
private val executor = Executors.newSingleThreadExecutor()
|
||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||
private val adapter by lazy {
|
||
MainFoodListAdapter(mutableListOf()).apply {
|
||
setOnItemClickListener { _, _, position ->
|
||
if (items[position].isChecked) {
|
||
return@setOnItemClickListener
|
||
}
|
||
items.forEachIndexed { index, info ->
|
||
items[index].isChecked = index == position
|
||
}
|
||
val item = items[position]
|
||
log("itemClick ${item.foodName}, position = $position")
|
||
item.photoUri = null
|
||
checkedItem = item
|
||
notifyDataSetChanged()
|
||
resetSelectedFood(item)
|
||
}
|
||
}
|
||
}
|
||
var isSwitchFood = false
|
||
|
||
/**
|
||
* 用于切换菜品时重置数据
|
||
*/
|
||
fun resetSelectedFood(foodInfo: FoodInfo) {
|
||
isSwitchFood = true
|
||
recognizeViewModel.resetFaceState()
|
||
updateCurrentFood(foodInfo)
|
||
if (settlementMode == 0) {
|
||
//联合支付,切换菜品重新请求接口
|
||
presentation?.let {
|
||
val userId = presentation?.currentUserId
|
||
if (userId.isNullOrBlank()) {
|
||
//切换菜品时,用户已离开重新打开识别功能
|
||
it.pauseCamera()
|
||
it.step2FaceRecognizing(foodInfo, true)
|
||
} else {
|
||
//切换菜品时,用户未离开,重新请求接口
|
||
it.step3ShowRecognizeResult(userId)
|
||
}
|
||
}
|
||
} else {
|
||
//0-计费,1-不计费
|
||
val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
//不计费模式切换菜品,重新加载数据
|
||
if (mode == 1) {
|
||
presentation?.isAgainLoadData = false
|
||
presentation?.loadUnbilledMode()
|
||
}
|
||
}
|
||
}
|
||
|
||
var checkedItem: FoodInfo? = null
|
||
private var imageAnalysis: ImageAnalysis? = null
|
||
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
|
||
private var canIdentify: Boolean = false
|
||
private var bottomSheetDialog: CustomBottomSheetDialog? = null
|
||
private var lastWeight: Int = 0
|
||
var currentWeight: Int = 0
|
||
private var lastPhotoUri: Uri? = null // 最后拍照的图片
|
||
private var debouncer = Debouncer(2000)
|
||
private var isRecognitionFood = true
|
||
private var isRecognizing = false
|
||
private var isFirstOpen = true
|
||
|
||
private var isStartRecognize = false
|
||
|
||
//0-联合支付,1-独立支付
|
||
var settlementMode: Int = 1
|
||
// val settlementMode by lazy {
|
||
// SPUtil.getInstance().get(GlobalKey.KEY_SETTLEMENT_MODE, 1)
|
||
// }
|
||
|
||
private lateinit var rvFoodList: RecyclerView
|
||
private lateinit var tvToSearch: TextView
|
||
lateinit var layoutEatDetailBinding: LayoutEatDetailBinding
|
||
|
||
override fun getViewModel(): BaseViewModel {
|
||
return viewModel
|
||
}
|
||
|
||
override fun inflateViewBinding(): ActivityMainBinding {
|
||
return ActivityMainBinding.inflate(layoutInflater)
|
||
}
|
||
|
||
override fun initialize() {
|
||
super.initialize()
|
||
settlementMode = SpTool.settlementMode
|
||
isPageVisible = true
|
||
// 拦截扫码枪按键事件,防止触发页面按钮点击
|
||
registerKeyEvent()
|
||
addBackEventListener()
|
||
initView()
|
||
if (SpTool.firstGetFace) {
|
||
viewModel.getFacePage(pageNum = 1)
|
||
}
|
||
setupCamera()
|
||
initData()
|
||
}
|
||
|
||
private fun initLayoutEatDetailBinding() {
|
||
Timber.tag(TAG).d("initLayoutEatDetailBinding,settlementMode=$settlementMode")
|
||
binding.flFoodList.removeAllViews()
|
||
if (settlementMode == 0) {
|
||
//联合支付
|
||
val b = LayoutMainFoodList2Binding.inflate(layoutInflater, binding.flFoodList, true)
|
||
rvFoodList = b.rvFoodList
|
||
tvToSearch = b.tvToSearch
|
||
layoutEatDetailBinding = b.detailInclude
|
||
layoutEatDetailBinding.llFoodTitle.gone()
|
||
// layoutEatDetailBinding.root.invisible()
|
||
} else {
|
||
//独立支付
|
||
val b = LayoutMainFoodListBinding.inflate(layoutInflater, binding.flFoodList, true)
|
||
rvFoodList = b.rvFoodList
|
||
tvToSearch = b.tvToSearch
|
||
layoutEatDetailBinding = b.detailInclude
|
||
layoutEatDetailBinding.llFoodTitle.gone()
|
||
layoutEatDetailBinding.root.gone()
|
||
}
|
||
tvToSearch.setOnClickListener {
|
||
bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
|
||
checkedItem = it
|
||
it.photoUri = null
|
||
adapter.items.forEachIndexed { index, info ->
|
||
adapter.items[index].isChecked = false
|
||
}
|
||
adapter.notifyDataSetChanged()
|
||
resetSelectedFood(it)
|
||
}
|
||
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
|
||
}
|
||
|
||
rvFoodList.let {
|
||
it.layoutManager = GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false)
|
||
it.adapter = adapter
|
||
}
|
||
}
|
||
|
||
private fun initData() {
|
||
startFaceTask()
|
||
}
|
||
|
||
private fun initView() {
|
||
viewModel.activeEngine()
|
||
initLayoutEatDetailBinding()
|
||
updateDateTime()
|
||
binding.tvTitleTime.setOnClickListener {
|
||
EnvSwitchDialog(context = this, onEnvChanged = {
|
||
showWaitingDialog("重置人脸数据中……")
|
||
lifecycleScope.launch {
|
||
withContext(Dispatchers.IO) {
|
||
FaceServer.getInstance().clearAllFaces()
|
||
ObjectBox.removeAll()
|
||
}
|
||
FoodVectorTool.loadAndSaveFoodVector(
|
||
userViewModel = viewModel,
|
||
lifecycleScope = lifecycleScope,
|
||
successBlock = {
|
||
hideWaitingDialog()
|
||
},
|
||
failureBlock = {
|
||
hideWaitingDialog()
|
||
ToastUtils.showToast(it)
|
||
}
|
||
)
|
||
}
|
||
viewModel.getFacePage(pageNum = 1, onSuccess = {
|
||
recognizeViewModel.refreshFaceList()
|
||
hideWaitingDialog()
|
||
ToastUtils.showToast("人脸数据已更新")
|
||
}, onFailure = { errMsg ->
|
||
hideWaitingDialog()
|
||
ToastUtils.showToast(errMsg)
|
||
})
|
||
}).show()
|
||
}
|
||
binding.ivSetting.setOnClickListener {
|
||
isRefreshPage = false
|
||
startActivity(Intent(this, SettingActivity::class.java))
|
||
}
|
||
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
|
||
override fun getOutline(view: View, outline: Outline) {
|
||
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
|
||
}
|
||
}
|
||
binding.previewView.clipToOutline = true
|
||
binding.tvFoodName.setOnClickListener {
|
||
SensorScaleUtils.zero()
|
||
}
|
||
binding.btnPay.setOnClickListener {
|
||
clickPayButton()
|
||
}
|
||
}
|
||
|
||
private var recognizeWeight = 0
|
||
fun updateCurrentFood(foodInfo: FoodInfo?) {
|
||
if (foodInfo == null) {
|
||
binding.tvFoodName.text = "-"
|
||
binding.previewView.visibility = View.VISIBLE
|
||
binding.ivImg.visibility = View.GONE
|
||
resumeAnalysis()
|
||
return
|
||
}
|
||
pauseAnalysis()
|
||
recognizeWeight = currentWeight
|
||
log("registerDataChange,副屏updateFood调用前耗时:${System.currentTimeMillis() - startTime}, recognizeWeight = ${recognizeWeight}")
|
||
startTime = System.currentTimeMillis()
|
||
presentation?.updateFood(foodInfo)
|
||
binding.tvFoodName.text = foodInfo.foodName
|
||
binding.previewView.gone()
|
||
binding.ivImg.visible()
|
||
binding.ivImg.load(foodInfo.foodImg)
|
||
//pauseAnalysis()
|
||
}
|
||
|
||
fun updateImage(imgUrl: Any?) {
|
||
binding.ivImg.load(imgUrl)
|
||
}
|
||
|
||
/**
|
||
* 联合支付-即放即取模式
|
||
*/
|
||
private fun loadJFJQBySettlement(weight: Int) {
|
||
log("registerDataChange weight 联合支付-即放即取")
|
||
//即放即取+联合支付人脸查询订单完成
|
||
if (presentation!!.jointPaymentQueryFinish) {
|
||
log("registerDataChange weight 联合支付-即放即取,人脸查询订单完成")
|
||
if (weight > WEIGHT_RESET_RECOGNIZE) {
|
||
//已识别完成人脸并查询到订单数据,增加重量不再重新识别
|
||
log("registerDataChange weight 联合支付-即放即取,不再重新识别")
|
||
return
|
||
}
|
||
log("registerDataChange weight 联合支付-即放即取,step1FoodRecognizing")
|
||
//重量小于等于10克时改为识别中页面
|
||
isAnalyzing = false
|
||
presentation?.step1FoodRecognizing()
|
||
return
|
||
}
|
||
log("registerDataChange weight 联合支付-即放即取,人脸查询订单没有完成")
|
||
if (weight <= WEIGHT_RESET_RECOGNIZE) {
|
||
//秤上重量过小,显示识别中页面
|
||
log("registerDataChange weight 联合支付-即放即取,重量小于${WEIGHT_RESET_RECOGNIZE}g,step1FoodRecognizing")
|
||
presentation?.step1FoodRecognizing()
|
||
return
|
||
}
|
||
log("registerDataChange weight 联合支付-即放即取,recognizeByWeight")
|
||
// val isWeightChange = abs(weight - lastWeight) > 0.02
|
||
val isWeightChange = weight - lastWeight > WEIGHT_CHANGE_VALUE
|
||
if (weight != lastWeight) {
|
||
log("registerDataChange weight 联合支付-即放即取,recognizeByWeight---00001,weight=$weight,lastWeight=$lastWeight,isWeightChange=$isWeightChange")
|
||
}
|
||
recognizeByWeight(weight, isWeightChange)
|
||
}
|
||
|
||
/**
|
||
* 联合支付-余量计量模式
|
||
*/
|
||
private fun loadYLJLBySettlement(weight: Int) {
|
||
log("registerDataChange weight 联合支付-余量计量")
|
||
if (weight <= WEIGHT_RESET_RECOGNIZE) {
|
||
log("registerDataChange weight 联合支付-余量计量,重量小于${WEIGHT_RESET_RECOGNIZE}g,step1FoodRecognizing")
|
||
if (presentation?.currentUserId.isNullOrBlank()
|
||
.not() && presentation!!.jointPaymentQueryFinish
|
||
) {
|
||
//联合支付+余量计量,已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
||
if (lastWeight != weight) {
|
||
presentation?.updateWeight(weight)
|
||
lastWeight = weight
|
||
}
|
||
return
|
||
}
|
||
//秤上重量过小,显示识别中页面
|
||
presentation?.step1FoodRecognizing()
|
||
return
|
||
}
|
||
log("registerDataChange weight 联合支付-余量计量,订单完成状态:${presentation!!.jointPaymentQueryFinish}")
|
||
//余量计量+联合支付人脸查询订单完成
|
||
if (presentation!!.jointPaymentQueryFinish) {
|
||
log("registerDataChange weight 联合支付-余量计量,updateWeight")
|
||
if (lastWeight != weight) {
|
||
presentation?.updateWeight(weight)
|
||
log("registerDataChange weight 重置lastWeight与weigh:${weight}一致----------------1000006")
|
||
lastWeight = weight
|
||
}
|
||
return
|
||
}
|
||
val isWeightChange = weight - lastWeight > WEIGHT_CHANGE_VALUE
|
||
recognizeByWeight(weight, isWeightChange)
|
||
}
|
||
|
||
private fun readWeight(weight: Int) {
|
||
currentWeight = weight
|
||
log("registerDataChange weight = $weight, lastWeight = $lastWeight")
|
||
binding.tvShowWeight.let {
|
||
if (it.text.toString() != weight.toString()) {
|
||
it.text = "$weight"
|
||
}
|
||
}
|
||
if (settlementMode == 0) {
|
||
log("registerDataChange weight 联合支付模式")
|
||
//联合支付-------------------------------------------
|
||
//0-即放即取,1-余量计量
|
||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||
if (mealPickupMode == 1) {
|
||
loadYLJLBySettlement(weight)
|
||
return
|
||
}
|
||
if (presentation?.currentUserId.isNullOrBlank().not()) {
|
||
//联合支付+即放即取,已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
||
return
|
||
}
|
||
loadJFJQBySettlement(weight)
|
||
return
|
||
}
|
||
//独立支付-------------------------------------------
|
||
//0-即放即取,1-余量计量
|
||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||
//0-计费,1-不计费
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
if (mealPickupMode == 0 && chargeMode == 1) {
|
||
//即放即取+不计费模式,
|
||
if (presentation?.currentUserId.isNullOrBlank().not()) {
|
||
//已识别人脸的状态下,从秤上拿餐品后,不再读取数据,等待人脸离开提交订单
|
||
return
|
||
}
|
||
if (weight <= WEIGHT_RESET_RECOGNIZE) {
|
||
log("registerDataChange weight 秤重量小于${WEIGHT_RESET_RECOGNIZE}g")
|
||
//检测到秤上没有东西
|
||
presentation?.step1FoodRecognizing()
|
||
return
|
||
}
|
||
}
|
||
if (isPageVisible.not() || isStartRecognize.not()) {
|
||
if (ActivityManager.currentActivity() is MainActivity) {
|
||
isPageVisible = true
|
||
isStartRecognize = true
|
||
}
|
||
return
|
||
}
|
||
val isWeightChange = weight - lastWeight > WEIGHT_CHANGE_VALUE
|
||
recognizeByWeight(weight, isWeightChange)
|
||
}
|
||
|
||
override fun registerDataChange() {
|
||
super.registerDataChange()
|
||
SensorScaleUtils.addWeightListener { value ->
|
||
runOnUiThread {
|
||
if (foodRecognizeState.not()) {
|
||
if (value <= WEIGHT_RESET_RECOGNIZE) {
|
||
presentation?.step1FoodRecognizing()
|
||
}
|
||
return@runOnUiThread
|
||
}
|
||
readWeight(value)
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun recognizeByWeight(weight: Int, isWeightChange: Boolean, block: () -> Unit = {}) {
|
||
log("registerDataChange weight 开始识别")
|
||
if (weight != lastWeight) {
|
||
val payTypeDesc = if (settlementMode == 0) "联合支付" else "独立支付"
|
||
log("registerDataChange weight readWeight-${payTypeDesc},weight=$weight,lastWeight=$lastWeight,isWeightChange=$isWeightChange")
|
||
}
|
||
// presentation?.updateWeight(weight)
|
||
|
||
if (weight <= WEIGHT_RESET_RECOGNIZE) {
|
||
log("registerDataChange weight recognizeByWeight---00002,秤重未超过${WEIGHT_RESET_RECOGNIZE}克")
|
||
//检测到秤上没有东西,重新启动识别菜品
|
||
isRecognitionFood = true
|
||
return
|
||
}
|
||
|
||
if (isWeightChange && isRecognitionFood) {
|
||
log("registerDataChange weight recognizeByWeight---00003,mealPickupMode=${presentation?.mealPickupMode}")
|
||
if (presentation?.mealPickupMode == 1 && checkedItem != null) {
|
||
isRecognitionFood = false
|
||
return
|
||
}
|
||
debouncer.debounce {
|
||
log("registerDataChange weight recognizeByWeight---00004--recognizeFood")
|
||
//保存当前重量
|
||
currentWeight = weight
|
||
recognizeFood()
|
||
}
|
||
} else {
|
||
log("registerDataChange weight recognizeByWeight---block")
|
||
block()
|
||
}
|
||
if (presentation?.mealPickupMode == 1 || settlementMode == 0) {
|
||
log("registerDataChange weight recognizeByWeight---lastWeight=${lastWeight}, weight=${weight}, checkedItem=${checkedItem}")
|
||
if (weight - lastWeight > WEIGHT_CHANGE_VALUE && checkedItem == null) {
|
||
debouncer.debounce {
|
||
log("registerDataChange weight重新进行菜品识别 ")
|
||
recognizeFood()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
private var startTime = 0L
|
||
|
||
fun recognizeFood() {
|
||
// 防重入:识别进行中则跳过,避免重量变化触发多次并发识别
|
||
if (isRecognizing) {
|
||
log("recognizeFood 已在识别中,跳过重复调用")
|
||
return
|
||
}
|
||
isRecognizing = true
|
||
// 立即更新基准重量,缩小后续重量变化触发识别的窗口
|
||
lastWeight = currentWeight
|
||
|
||
startTime = System.currentTimeMillis()
|
||
// LightManager.openRedLight()
|
||
LightManager.closeRedLight()
|
||
LightManager.openGreenLight()
|
||
showWaitingDialog("识别中,请稍后……")
|
||
lifecycleScope.launch {
|
||
delay(5000)
|
||
withContext(Dispatchers.Main) {
|
||
hideWaitingDialog()
|
||
}
|
||
}
|
||
takePhoto(successCallback = { photoUri ->
|
||
lifecycleScope.launch {
|
||
onTakePhotoSuccess(photoUri)
|
||
}
|
||
failCount = 0
|
||
}, failureCallback = {
|
||
log("registerDataChange main,拍照异常,请重试")
|
||
//ToastUtils.showToast("拍照异常,请重试")
|
||
failCount++
|
||
hideWaitingDialog()
|
||
|
||
shutdownCamera()
|
||
setupCamera()
|
||
|
||
// 释放识别锁,允许递归重试时重新获取
|
||
isRecognizing = false
|
||
binding.root.postDelayed({
|
||
recognizeFood()
|
||
}, 1000)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 识别相机异常次数
|
||
*/
|
||
private var failCount = 0
|
||
|
||
/**
|
||
* 是否进行识别:true-识别,false-不识别,默认开启识别
|
||
*/
|
||
private var foodRecognizeState = true
|
||
|
||
@SuppressLint("NotifyDataSetChanged")
|
||
private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) {
|
||
val queryData = GsonUtils.toJson(list)
|
||
val recData = GsonUtils.toJson(scoreList)
|
||
log("registerDataChange识别后查询接口数据:$queryData,识别数据:$recData")
|
||
if (list.isEmpty()) {
|
||
foodRecognizeState = false
|
||
isRecognizing = false
|
||
tvToSearch.let {
|
||
it.text = "未查询到,手动搜索"
|
||
it.visible()
|
||
}
|
||
return
|
||
}
|
||
foodRecognizeState = true
|
||
isRecognizing = false
|
||
//查询到菜品信息,同步设置lastWeight=currentWeight
|
||
lastWeight = currentWeight
|
||
presentation?.updateWeight(currentWeight)
|
||
|
||
list.forEach { foodInfo ->
|
||
val scoreItem = scoreList.firstOrNull { it.name == foodInfo.foodName }
|
||
val score = scoreItem?.score ?: 0.0
|
||
foodInfo.score = ((1 - score) * 10000).roundToInt()
|
||
}
|
||
//接口已处理排序
|
||
// val list2 = try {
|
||
// val orderList = scoreList.map { it.name.trim() }
|
||
// list.sortedBy { orderList.indexOf(it.foodName) }
|
||
// } catch (e: Exception) {
|
||
// e.printStackTrace()
|
||
// list
|
||
// }
|
||
|
||
adapter.submitList(list)
|
||
rvFoodList.visible()
|
||
|
||
checkedItem = list[0].also {
|
||
it.photoUri = lastPhotoUri
|
||
it.isChecked = true
|
||
}
|
||
adapter.notifyItemChanged(0)
|
||
updateCurrentFood(checkedItem)
|
||
// lastPhotoUri = null
|
||
LightManager.closeRedLight()
|
||
|
||
tvToSearch.let {
|
||
it.text = "以上都不是,手动搜索"
|
||
it.visible()
|
||
}
|
||
log("registerDataChange识别完成:visible=${rvFoodList.isVisible},count=${rvFoodList.childCount}")
|
||
}
|
||
|
||
private fun setupCamera() {
|
||
log("setupCamera")
|
||
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
||
cameraProviderFuture!!.addListener({
|
||
val cameraProvider = cameraProviderFuture!!.get()
|
||
|
||
// 1. 创建预览用例
|
||
val preview = Preview.Builder().build().also {
|
||
it.setSurfaceProvider(binding.previewView.surfaceProvider)
|
||
}
|
||
|
||
// 2. 创建图像分析用例(用于副屏显示)
|
||
imageAnalysis = ImageAnalysis.Builder()
|
||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
|
||
.build().also { analysis ->
|
||
analysis.setAnalyzer(executor) { imageProxy ->
|
||
// 仅在步骤1时传递
|
||
if (isAnalyzing) {
|
||
val bitmap = imageProxy.toBitmap()
|
||
presentation?.updateImage(bitmap) // 更新副屏
|
||
isStartRecognize = true
|
||
}
|
||
imageProxy.close() // 必须关闭以释放资源
|
||
}
|
||
}
|
||
|
||
// 3. 创建拍照用例
|
||
imageCapture =
|
||
ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||
// .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率
|
||
.build()
|
||
|
||
// 4. 选择摄像头
|
||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||
|
||
try {
|
||
// 解绑所有用例
|
||
cameraProvider.unbindAll()
|
||
|
||
// 绑定三个用例到生命周期
|
||
cameraProvider.bindToLifecycle(
|
||
this, cameraSelector, preview, imageAnalysis, imageCapture
|
||
)
|
||
} catch (e: Exception) {
|
||
log("setupCamera方法异常:${e.message}")
|
||
}
|
||
}, ContextCompat.getMainExecutor(this))
|
||
}
|
||
|
||
fun pauseAnalysis() {
|
||
log("pauseAnalysis")
|
||
isAnalyzing = false
|
||
imageAnalysis?.clearAnalyzer()
|
||
}
|
||
|
||
fun resumeAnalysis() {
|
||
log("resumeAnalysis isAnalyzing = $isAnalyzing")
|
||
if (isAnalyzing) return
|
||
// isAnalyzing = true
|
||
imageAnalysis?.setAnalyzer(executor) { imageProxy ->
|
||
if (presentation?.currentStep == 1) {
|
||
val bitmap = imageProxy.toBitmap()
|
||
presentation?.updateImage(bitmap)
|
||
}
|
||
imageProxy.close()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止相机识别
|
||
*/
|
||
private fun shutdownCamera() {
|
||
log("shutdownCamera")
|
||
try {
|
||
// 1. 首先停止图像分析
|
||
imageAnalysis?.clearAnalyzer()
|
||
|
||
// 2. 解除所有绑定
|
||
cameraProviderFuture?.get()?.unbindAll()
|
||
|
||
// 3. 关闭相关资源
|
||
imageCapture = null
|
||
imageAnalysis = null
|
||
cameraProviderFuture = null
|
||
|
||
// 4. 停止副屏更新
|
||
isAnalyzing = false
|
||
} catch (e: Exception) {
|
||
log("Error shutting down camera:${e.message}")
|
||
}
|
||
}
|
||
|
||
// 拍照功能
|
||
private fun takePhoto(successCallback: (Uri) -> Unit, failureCallback: () -> Unit) {
|
||
log("takePhoto")
|
||
// 临时停止分析以避免干扰
|
||
isAnalyzing = false
|
||
|
||
// 创建输出选项
|
||
val executor = ContextCompat.getMainExecutor(this)
|
||
val cacheDir = cacheDir
|
||
val photoFile = File.createTempFile(
|
||
"IMG_${System.currentTimeMillis()}", ".jpg", cacheDir
|
||
)
|
||
|
||
val cacheOutputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
||
// 执行拍照
|
||
imageCapture?.takePicture(
|
||
cacheOutputOptions, executor, object : ImageCapture.OnImageSavedCallback {
|
||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||
val savedUri = outputFileResults.savedUri
|
||
log("takePhoto savedUri = $savedUri")
|
||
if (savedUri != null) {
|
||
successCallback(savedUri)
|
||
}
|
||
// 恢复图像分析
|
||
isAnalyzing = true
|
||
}
|
||
|
||
override fun onError(exception: ImageCaptureException) {
|
||
log("takePhoto拍照失败: ${exception.message}")
|
||
failureCallback()
|
||
//ToastUtils.showToast("拍照失败: ${exception.message}")
|
||
// 恢复图像分析
|
||
isAnalyzing = true
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun getSecondaryDisplay(): Display? {
|
||
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||
val displays = displayManager.displays
|
||
|
||
// 查找副屏(通常索引为1)
|
||
if (displays.size > 1) return displays[1]
|
||
return null
|
||
}
|
||
|
||
private val recognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
|
||
|
||
private fun setupSecondaryDisplay() {
|
||
val secondaryDisplay = getSecondaryDisplay()
|
||
if (secondaryDisplay == null) {
|
||
ToastUtils.showToast("获取副屏失败")
|
||
return
|
||
}
|
||
// 创建副屏的Presentation
|
||
presentation = MainScreenPresentation(
|
||
activity = this,
|
||
display = secondaryDisplay,
|
||
userViewModel = viewModel,
|
||
recognizeViewModel = recognizeViewModel
|
||
).apply {
|
||
setStepChangeCallback { step ->
|
||
if (step == 1) {
|
||
log("setStepChangeCallback${step}")
|
||
updateCurrentFood(null)
|
||
bottomSheetDialog?.dismiss()
|
||
}
|
||
}
|
||
show()
|
||
}
|
||
}
|
||
|
||
@SuppressLint("NotifyDataSetChanged")
|
||
fun clearRecognizeFood() {
|
||
log("clearRecognizeFood")
|
||
rvFoodList.invisible()
|
||
layoutEatDetailBinding.root.invisible()
|
||
adapter.submitList(null)
|
||
tvToSearch.gone()
|
||
|
||
binding.previewView.visible()
|
||
binding.ivImg.gone()
|
||
checkedItem = null
|
||
lastWeight = 0
|
||
foodRecognizeState = true
|
||
isRecognizing = false
|
||
isSwitchFood = false
|
||
}
|
||
|
||
private fun updateDateTime() {
|
||
val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 E", Locale.CHINA)
|
||
val dateTime = formatter.format(LocalDateTime.now(ZoneId.of("Asia/Shanghai")))
|
||
binding.tvTitleTime.text = dateTime
|
||
}
|
||
|
||
public var isPageVisible = true
|
||
|
||
override fun onResume() {
|
||
super.onResume()
|
||
isPageVisible = true
|
||
// shutdownCamera 后 cameraProviderFuture 置为 null,需重新初始化相机
|
||
if (cameraProviderFuture == null) {
|
||
setupCamera()
|
||
}
|
||
// 启动定时器
|
||
handler.postDelayed(timeoutRunnable, TIME_OUT)
|
||
// ToastUtils.showToast("isPageVisible=$isPageVisible")
|
||
if (isFirstOpen) {
|
||
//首次
|
||
setupSecondaryDisplay()
|
||
presentation?.step1FoodRecognizing()
|
||
isFirstOpen = false
|
||
} else {
|
||
//副屏未显示重新加载
|
||
isRecognitionFood = true
|
||
log("onResume-00001,isRefreshPage=$isRefreshPage")
|
||
if (presentation == null || !presentation!!.isShowing) {
|
||
log("onResume-00002,副屏未显示,加载副屏")
|
||
lastWeight = 0
|
||
setupSecondaryDisplay()
|
||
presentation?.step1FoodRecognizing()
|
||
} else {
|
||
log("onResume-00003,副屏显示,更新副屏, isRefreshPage=$isRefreshPage")
|
||
if (isRefreshPage) {
|
||
isRefreshPage = false
|
||
//已显示副屏,则进行更新
|
||
val weight = lastWeight
|
||
lastWeight = 0
|
||
presentation?.pauseCamera()
|
||
presentation?.hideRecImage()
|
||
presentation?.step1FoodRecognizing()
|
||
|
||
log("onResume-recognizeByWeight")
|
||
binding.root.postDelayed({
|
||
recognizeByWeight(weight, true)
|
||
}, 500)
|
||
}
|
||
}
|
||
}
|
||
//根据付费模式确认是否显示按钮
|
||
binding.btnPay.run {
|
||
//0-计费,1-不计费
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
if (settlementMode == 1 && chargeMode == 0) visible() else gone()
|
||
}
|
||
}
|
||
|
||
override fun onPause() {
|
||
super.onPause()
|
||
isPageVisible = false
|
||
// 取消定时器
|
||
handler.removeCallbacks(timeoutRunnable)
|
||
}
|
||
|
||
private var lastTouchTime = 0L
|
||
private val timeoutRunnable = Runnable {
|
||
//显示待机页面
|
||
presentation?.loadStandbyScreen()
|
||
}
|
||
|
||
override fun onUserInteraction() {
|
||
super.onUserInteraction()
|
||
if (System.currentTimeMillis() - lastTouchTime >= TIME_OUT) {
|
||
//隐藏待机页面
|
||
presentation?.hideStandbyScreen()
|
||
}
|
||
// 重置定时器
|
||
handler.removeCallbacks(timeoutRunnable)
|
||
handler.postDelayed(timeoutRunnable, TIME_OUT)
|
||
lastTouchTime = System.currentTimeMillis()
|
||
}
|
||
|
||
fun resetTouchTime() {
|
||
handler.removeCallbacks(timeoutRunnable)
|
||
handler.postDelayed(timeoutRunnable, TIME_OUT)
|
||
lastTouchTime = System.currentTimeMillis()
|
||
}
|
||
|
||
fun createOrder(
|
||
foodInfo: FoodInfo,
|
||
foodWeight: Int,
|
||
eatWeight: Int,
|
||
eatNum: Int,
|
||
userId: String?,
|
||
isMember: Boolean,
|
||
notEnoughOneBlock: () -> Unit = {},
|
||
successBlock: () -> Unit
|
||
) {
|
||
log("foodWeight=$foodWeight,eatWeight=$eatWeight")
|
||
//0-计费,1-不计费
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
//0-即放即取,1-余量计量
|
||
val pickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||
// 按重量计费:净重不足最小计费重量时,提示并拦截下单
|
||
if (eatWeight < MIN_BILLABLE_WEIGHT) {
|
||
ToastUtils.showToast("重量不足,无法下单")
|
||
notEnoughOneBlock()
|
||
return
|
||
}
|
||
showWaitingDialog("加载中,请稍候……")
|
||
val request = PlaceOrderRequest(
|
||
deviceId = GlobalData.deviceId,
|
||
foodId = foodInfo.foodId.toLong(),
|
||
foodName = foodInfo.foodName ?: "",
|
||
foodMaterialId = foodInfo.foodMaterialId?.toLongOrNull(),
|
||
specId = foodInfo.specId?.toLongOrNull(),
|
||
foodWeight = foodWeight.toBigDecimal(),
|
||
eatWeight = eatWeight.toBigDecimal(),
|
||
eatNum = eatNum,
|
||
userId = userId?.toLongOrNull(),
|
||
notPay = chargeMode != 0,
|
||
//即放即取-1,称重-2
|
||
mode = if (pickupMode == 1) 1 else 2,
|
||
paymentFrom = if (settlementMode == 0) 1 else 2,
|
||
member = isMember,
|
||
cookOrderId = foodInfo.cookOrderId
|
||
)
|
||
submitOrder(request, successBlock)
|
||
}
|
||
|
||
private fun submitOrder(request: PlaceOrderRequest, successBlock: () -> Unit) {
|
||
viewModel.placeOrder(
|
||
orderRequest = request,
|
||
onSuccess = { orderId ->
|
||
runOnUiThread {
|
||
binding.root.postDelayed({
|
||
hideWaitingDialog()
|
||
if (orderId.isNullOrBlank()) {
|
||
return@postDelayed
|
||
}
|
||
foodOrderId = orderId
|
||
successBlock()
|
||
|
||
if (checkedItem?.isFromSearch == true) {
|
||
lifecycleScope.launch {
|
||
saveFoodVector(checkedItem!!)
|
||
}
|
||
}
|
||
}, 300)
|
||
}
|
||
},
|
||
onFailure = { errorMsg ->
|
||
runOnUiThread {
|
||
hideWaitingDialog()
|
||
ToastUtils.showToast(errorMsg)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
fun getEatNum(realWeight: Double?, specWeight: Double?): Int {
|
||
if (realWeight == null || realWeight <= 0.0 || specWeight == null || specWeight <= 0.0) {
|
||
log("getEatNum:realWeight=$realWeight,specWeight=$specWeight")
|
||
return 0
|
||
}
|
||
val eatNum = (realWeight / specWeight).roundToInt()
|
||
log("getEatNum:eatNum=$eatNum,realWeight=$realWeight,specWeight=$specWeight,realWeight/$specWeight=${realWeight / specWeight}}")
|
||
return if (eatNum < 1) 1 else eatNum;
|
||
}
|
||
|
||
/**
|
||
* 读取已配置的餐具重量(克)。
|
||
* 用于取餐提交时从就餐重量中扣减,默认 0 表示未配置、不扣减。
|
||
*/
|
||
internal fun getDishWeight(): Int {
|
||
return SPUtil.getInstance().get(GlobalKey.KEY_DISH_WEIGHT, 0) ?: 0
|
||
}
|
||
|
||
private var foodOrderId: String = ""
|
||
private var isExist = false
|
||
fun addBackEventListener() {
|
||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||
override fun handleOnBackPressed() {
|
||
if (isExist.not()) {
|
||
ToastUtils.showToast("再按一次退出")
|
||
isExist = true
|
||
binding.root.postDelayed({
|
||
isExist = false
|
||
}, 2000)
|
||
return
|
||
}
|
||
finish()
|
||
}
|
||
})
|
||
}
|
||
|
||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||
fun onUpdateRefreshEvent(event: UpdateRefreshEvent) {
|
||
isRefreshPage = true
|
||
checkedItem = null
|
||
}
|
||
|
||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||
fun onPaySuccessEvent(event: PaySuccessEvent) {
|
||
isRefreshPage = true
|
||
checkedItem = null
|
||
//0-计费,1-不计费
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
|
||
presentation?.hideNutritionView(chargeMode)
|
||
presentation?.cancel()
|
||
}
|
||
|
||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||
fun onResetRecognizeState(event: ResetRecognizeEvent) {
|
||
// presentation?.step1FoodRecognizing()
|
||
isRefreshPage = true
|
||
checkedItem = null
|
||
}
|
||
|
||
|
||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||
fun onChargeModeEvent(event: ChargeModeEvent) {
|
||
isRefreshPage = true
|
||
isStartRecognize = false
|
||
presentation?.hideNutritionView(event.chargeMode)
|
||
// presentation?.cancel()
|
||
|
||
if (event.chargeMode == 1) {
|
||
presentation?.let {
|
||
it.onStop()
|
||
it.setupArcCamera()
|
||
}
|
||
}
|
||
}
|
||
|
||
private var isRefreshPage = false
|
||
|
||
// @Subscribe(threadMode = ThreadMode.MAIN)
|
||
// fun onClickBackEvent(event: ClickBackEvent) {
|
||
// isRefreshPage = false
|
||
// }
|
||
|
||
private fun clickPayButton() {
|
||
log("eatWeight:recognizeWeight=$recognizeWeight, currentWeight=$currentWeight, lastWeight=$lastWeight")
|
||
if (checkedItem == null) {
|
||
ToastUtils.showToast("暂无识别数据,请搜索选择")
|
||
return
|
||
}
|
||
|
||
//val realWeight = (lastWeight * 1000).roundToInt()
|
||
//0-即放即取,1-余量计量
|
||
val mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
|
||
//0-计费,1-不计费
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) ?: 0
|
||
// 餐具重量(克),提交就餐数据时从就餐重量中扣减
|
||
val dishWeight = getDishWeight()
|
||
var foodWeight = 0
|
||
var eatWeight = 0
|
||
if (chargeMode == 0) {
|
||
//计费
|
||
if (mealPickupMode == 0) {
|
||
//即放即取
|
||
eatWeight = currentWeight
|
||
foodWeight = currentWeight
|
||
} else {
|
||
//余量计量
|
||
eatWeight = recognizeWeight - currentWeight
|
||
foodWeight = recognizeWeight
|
||
log("eatWeight=$eatWeight, recognizeWeight=$recognizeWeight, currentWeight=$currentWeight, lastWeight=$lastWeight")
|
||
}
|
||
} else {
|
||
//不计费
|
||
foodWeight = recognizeWeight
|
||
eatWeight = if (mealPickupMode == 0) {
|
||
//即放即取
|
||
recognizeWeight
|
||
} else {
|
||
//余量计量
|
||
recognizeWeight - currentWeight
|
||
}
|
||
}
|
||
// 扣减餐具重量(仅影响就餐重量与份数,菜品原始重量 foodWeight 保持不变),不足则归零
|
||
val rawEatWeight = eatWeight
|
||
eatWeight = (rawEatWeight - dishWeight).coerceAtLeast(0)
|
||
log("clickPayButton dishWeight=$dishWeight, rawEatWeight=$rawEatWeight, deductedEatWeight=$eatWeight")
|
||
val eatNum = getEatNum(eatWeight.toDouble(), checkedItem!!.specWeight)
|
||
createOrder(
|
||
foodInfo = checkedItem!!,
|
||
foodWeight = foodWeight,
|
||
eatWeight = eatWeight,
|
||
eatNum = eatNum,
|
||
userId = null,
|
||
isMember = false
|
||
) {
|
||
isRefreshPage = false
|
||
shutdownCamera()
|
||
startActivity(Intent(this, PayActivity::class.java).apply {
|
||
putExtra(PayActivity.FOOD_INFO, checkedItem!!)
|
||
putExtra(PayActivity.FOOD_EAT_NUM, eatNum)
|
||
putExtra(PayActivity.FOOD_ORDER_ID, foodOrderId)
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将uri转为文件和向量数据并提交
|
||
*/
|
||
private fun saveFoodVector(foodInfo: FoodInfo) {
|
||
try {
|
||
if (lastPhotoUri == null) {
|
||
return
|
||
}
|
||
lifecycleScope.launch(Dispatchers.IO) {
|
||
uri2File(lastPhotoUri!!) { imageFile, imageVector ->
|
||
runBlocking {
|
||
uploadCollectFoodPics(foodInfo, imageFile, imageVector)
|
||
}
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传菜品信息
|
||
*/
|
||
private suspend fun uploadCollectFoodPics(
|
||
foodInfo: FoodInfo, imageFile: File?, imageVector: FloatArray?
|
||
) {
|
||
if (imageFile == null || imageVector == null) {
|
||
log("是否null判断,imageFile == null:${imageFile == null},imageVector == null:${imageVector == null}")
|
||
return
|
||
}
|
||
val foodId = foodInfo.foodId.toLongOrNull() ?: run {
|
||
loadRemindDialog("foodId 格式错误: ${foodInfo.foodId}")
|
||
return
|
||
}
|
||
val foodName = foodInfo.foodName ?: ""
|
||
val foodModelVersion = GlobalData.foodModelVersion
|
||
val vectors = listOf(imageVector)
|
||
val foodVectorList = vectors.map {
|
||
it.joinToString(
|
||
separator = ",", prefix = "[", postfix = "]"
|
||
)
|
||
}
|
||
val foodVectorJson =
|
||
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
|
||
log("json=$foodVectorJson")
|
||
val idList = viewModel.uploadCollect(
|
||
foodId = foodId,
|
||
foodName = foodName,
|
||
version = foodModelVersion,
|
||
foodVector = foodVectorJson,
|
||
fileList = listOf(imageFile)
|
||
)
|
||
if (idList.isNullOrEmpty()) {
|
||
loadRemindDialog("上传失败,未返回向量数据id")
|
||
log("idList为空")
|
||
return
|
||
}
|
||
val all = ObjectBox.getAll()
|
||
val size = all.filter { it.isDel.not() }.size
|
||
log("uploadCollectFoodPics-已采集向量总数:${size}条")
|
||
val filterList = all.filter { it.collectId == idList[0] }
|
||
if (filterList.isNotEmpty()) {
|
||
loadRemindDialog("返回的id:${idList[0]}已存在")
|
||
return
|
||
}
|
||
ObjectBox.put(
|
||
Food(
|
||
collectId = idList[0],
|
||
foodId = foodId.toString(),
|
||
foodName = foodName,
|
||
foodVector = imageVector,
|
||
version = foodModelVersion
|
||
)
|
||
)
|
||
}
|
||
|
||
private fun loadRemindDialog(msg: String) {
|
||
runOnUiThread {
|
||
RemindDialog(
|
||
context = context, content = msg, confirmBlock = {}).show()
|
||
}
|
||
}
|
||
|
||
private fun uri2File(uri: Uri, block: (File?, FloatArray?) -> Unit) {
|
||
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
|
||
val imageVector = try {
|
||
FoodModule.bitmap2FloatArray(bitmap, false)
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
return@let
|
||
}
|
||
val imageFile = BitmapSaver.saveToAppFilesDir(
|
||
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||
)
|
||
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${imageFile?.absolutePath}")
|
||
block(imageFile, imageVector)
|
||
if (bitmap.isRecycled.not()) {
|
||
bitmap.recycle()
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onDestroy() {
|
||
//SensorScaleUtils.closeScale()
|
||
faceTaskJob?.cancel()
|
||
presentation?.cancel()
|
||
shutdownCamera()
|
||
super.onDestroy()
|
||
}
|
||
|
||
private val intervalExecutor by lazy { IntervalExecutor() }
|
||
private var faceTaskJob: Job? = null
|
||
|
||
private val initialDelay = 60 * 1000L
|
||
private val dealyMillis = 30 * 1000L
|
||
|
||
// private var taskPageNo = 1
|
||
fun startFaceTask() {
|
||
faceTaskJob =
|
||
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
|
||
val timestamp = SpTool.lastFaceTimestamp
|
||
//if (timestamp == 0L) {
|
||
// return@startIntervalTaskWithInitialDelay
|
||
//}
|
||
viewModel.getFaceIncrement(
|
||
// pageNo = taskPageNo,
|
||
timestamp = timestamp,
|
||
onAllQueryFinished = {
|
||
// taskPageNo = 1
|
||
},
|
||
onPageQueryFinished = { list ->
|
||
runOnUiThread {
|
||
if (list.isEmpty()) {
|
||
return@runOnUiThread
|
||
}
|
||
lifecycleScope.launch {
|
||
updateFaceData(list)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
private fun updateFaceData(list: List<FaceVO>) {
|
||
val faceList = mutableListOf<FaceEntity>()
|
||
try {
|
||
list.forEach { vo ->
|
||
if (vo.faceDeleted == true) {
|
||
//删除数据
|
||
lifecycleScope.launch(Dispatchers.IO) {
|
||
FaceDatabase.getInstance(this@MainActivity).faceDao().deleteFaceById(vo.userId)
|
||
}
|
||
} else {
|
||
// 保存数据:使用完整构造函数,映射后端全部字段到数据库
|
||
val faceEntity = FaceEntity(
|
||
vo.userId, // userName
|
||
Base64.decode(vo.faceFeature), // featureData
|
||
vo.personType ?: if (vo.member == true) "1" else "2", // userType
|
||
vo.cardNo ?: "", // cardNo
|
||
vo.userId ?: "", // userId
|
||
vo.userFaceId ?: "", // userFaceId
|
||
vo.member ?: false, // member
|
||
vo.faceUpdateTimestamp?.toLongOrNull() ?: 0L // faceUpdateTimestamp
|
||
)
|
||
faceList.add(faceEntity)
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
}
|
||
try {
|
||
if (faceList.isNotEmpty()) {
|
||
lifecycleScope.launch(Dispatchers.IO) {
|
||
FaceDatabase.getInstance(this@MainActivity).faceDao().insert(faceList)
|
||
}
|
||
}
|
||
recognizeViewModel.refreshFaceList();
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
}
|
||
}
|
||
|
||
private fun sendFaceData(faceData: String) {
|
||
// 给某个客户端发送
|
||
val jsonObject = JSONObject().also {
|
||
it.put("type", "faceFeature")
|
||
it.put("content", faceData)
|
||
}
|
||
tcpClient?.send(jsonObject)
|
||
}
|
||
|
||
private var tcpClient: TcpClient? = null
|
||
|
||
private suspend fun onTakePhotoSuccess(photoUri: Uri) {
|
||
lastPhotoUri = photoUri
|
||
log("registerDataChange main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
|
||
startTime = System.currentTimeMillis()
|
||
log("registerDataChange photoUri = ${photoUri.path}")
|
||
// viewModel.getIdentifiedFoodList()
|
||
val bitmap = withContext(Dispatchers.IO) {
|
||
ImageUtil.uriToBitmap(this@MainActivity, photoUri)
|
||
}
|
||
bitmap?.let { queryFoodData(it) }
|
||
}
|
||
|
||
private suspend fun queryFoodData(bitmap: Bitmap) {
|
||
log("registerDataChange main,uriToBitmap耗时:${System.currentTimeMillis() - startTime}")
|
||
startTime = System.currentTimeMillis()
|
||
log("registerDataChange photoUri 拿到bitmap")
|
||
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
|
||
log("registerDataChange registerDataChange photoUri bitmap裁剪完成")
|
||
val file = BitmapSaver.saveToAppFilesDir(
|
||
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||
)
|
||
log("registerDataChange main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
|
||
startTime = System.currentTimeMillis()
|
||
log("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
|
||
//val nameList = FoodModule.queryFood(bitmap)
|
||
// 推理前同时暂停主屏相机和副屏人脸识别,让 ArcSoft 线程进入空闲后再启动 PyTorch
|
||
withContext(Dispatchers.Main) {
|
||
shutdownCamera()
|
||
presentation?.pauseCamera()
|
||
}
|
||
// 等待相机 HAL 在内核层(uvcvideo URB / DMC system_status)完成异步清理
|
||
// 实测:unbindAll() 之后 HAL 仍会在 ~200ms 内写 sysfs_dmc/system_status,
|
||
// 若此时 PyTorch forward() 同时占用大量 DRAM 带宽,RK3588 DMC 频率切换
|
||
// 与高带宽访问并发会触发硬件级故障导致设备重启。
|
||
// 1500ms 给 HAL 足够时间完成 DMC 带宽 hold 释放后再启动推理。
|
||
Timber.tag(TAG).d("queryFoodData: 等待相机HAL完成DMC清理 (1500ms)...")
|
||
kotlinx.coroutines.delay(1500)
|
||
Timber.tag(TAG).d("queryFoodData: 延迟结束,开始 forward()")
|
||
val scoreList = try {
|
||
FoodModule.getFoodScoreList(bitmap)
|
||
} finally {
|
||
withContext(Dispatchers.Main) { setupCamera() }
|
||
}
|
||
val foodName = if (scoreList.isNotEmpty()) {
|
||
val recDataJson = GsonUtils.toJson(scoreList)
|
||
log("registerDataChange main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
|
||
startTime = System.currentTimeMillis()
|
||
log("registerDataChange photoUri 拿到识别数据:$recDataJson")
|
||
val nameList = scoreList.map { it.name }
|
||
nameList.joinToString(separator = ",")
|
||
} else ""
|
||
|
||
hideWaitingDialog()
|
||
log("registerDataChange photoUri 识别数据名称:$foodName")
|
||
withContext(Dispatchers.Main) {
|
||
binding.flPay.run {
|
||
//0-计费,1-不计费
|
||
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
|
||
//独立支付+计费模式才显示“去结算”按钮,否则使用人脸识别
|
||
if (settlementMode == 1 && chargeMode == 0) visible() else gone()
|
||
}
|
||
}
|
||
if (TextUtils.isEmpty(foodName)) {
|
||
LightManager.closeGreenLight()
|
||
LightManager.closeRedLight()
|
||
//binding.layoutRescan.visibility = View.VISIBLE
|
||
|
||
withContext(Dispatchers.Main) {
|
||
isRecognizing = false
|
||
tvToSearch.let {
|
||
it.text = "未识别到,手动搜索"
|
||
it.visible()
|
||
}
|
||
}
|
||
} else {
|
||
//binding.layoutRescan.visibility = View.GONE
|
||
showWaitingDialog("正在查询菜品信息,请稍后……")
|
||
viewModel.getFoodByNames(names = foodName, onSuccess = { list ->
|
||
runOnUiThread {
|
||
hideWaitingDialog()
|
||
log("registerDataChange main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
|
||
startTime = System.currentTimeMillis()
|
||
updateFoodInfo(list.map { it.toFoodInfo() }.toMutableList(), scoreList)
|
||
}
|
||
}, onFailure = { errorMsg ->
|
||
hideWaitingDialog()
|
||
ToastUtils.showToast(errorMsg)
|
||
})
|
||
}
|
||
}
|
||
|
||
// @Subscribe(threadMode = ThreadMode.MAIN)
|
||
// public fun onResetBox(event: ResetBoxEvent) {
|
||
// ObjectBox.boxStore.close()
|
||
// ObjectBox.init(this)
|
||
// }
|
||
|
||
|
||
} |