营养数据计算优化;人脸增加数据处理;其它优化;

This commit is contained in:
2025-12-25 18:57:26 +08:00
parent f080a8b73c
commit dfb734e25d
29 changed files with 1350 additions and 64 deletions
@@ -20,7 +20,8 @@ object GlobalData {
/** /**
* 具体业务baseurl * 具体业务baseurl
*/ */
var appBaseUrl: String = "" var appBaseUrl: String = "http://192.168.1.201:14801"
//var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/** /**
* 横排数量 * 横排数量
+1 -2
View File
@@ -21,10 +21,9 @@ class MyApp : App() {
instance = this instance = this
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
var deviceId = AppUtil.getUDID(this) var deviceId = AppUtil.getUDID(this)
Timber.d("UDID = ${AppUtil.getUDID(this)}") Timber.d("deviceId = $deviceId")
// deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e" // deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
GlobalData.deviceId = deviceId GlobalData.deviceId = deviceId
GlobalData.appBaseUrl = "http://192.168.1.201:14801"
ObjectBox.init(this) ObjectBox.init(this)
// 初始化崩溃处理器 // 初始化崩溃处理器
CrashHandler.init(this) CrashHandler.init(this)
@@ -36,23 +36,35 @@ import com.sw.dualscreen.model.response.ClickBackEvent
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.PaySuccessEvent import com.sw.dualscreen.model.response.PaySuccessEvent
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.objbox.FoodModule import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.FoodModule.IdNameScore import com.sw.dualscreen.objbox.FoodModule.IdNameScore
import com.sw.dualscreen.presentation.MainScreenPresentation import com.sw.dualscreen.presentation.MainScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.socket.LanServer
import com.sw.dualscreen.socket.TcpClient
import com.sw.dualscreen.utils.BitmapSaver import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.GsonUtils import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.utils.IntervalExecutor
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.dualscreen.view.CustomBottomSheetDialog import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.face.collect.view.LanServerListenerImpl
import com.sw.face.collect.view.TcpClientListenerImpl
import com.sw.plate.utils.Base64
import com.sw.plate.utils.LightManager import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils 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.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -127,6 +139,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
isPageVisible = true
addBackEventListener() addBackEventListener()
FoodModule.init(this) FoodModule.init(this)
initView() initView()
@@ -140,6 +153,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun initData() { private fun initData() {
// viewModel.getEquipmentToken() // viewModel.getEquipmentToken()
//viewModel.getUserFaceCache2() //viewModel.getUserFaceCache2()
startFaceTask()
// addSocketListener()
} }
private fun initView() { private fun initView() {
@@ -150,7 +167,18 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// //
// Log.d("ScreenInfo", "DPI: $screenDPI, density: $density"); // Log.d("ScreenInfo", "DPI: $screenDPI, density: $density");
viewModel.activeEngine() viewModel.getDeviceConfig { deviceConfig ->
runOnUiThread {
if (deviceConfig == null) {
ToastUtils.showToast("获取设备配置数据失败")
return@runOnUiThread
}
GlobalData.appId = deviceConfig.arcsoftAppId ?: ""
GlobalData.sdkKey = deviceConfig.arcsoftSdkKey ?: ""
GlobalData.activeKey = deviceConfig.arcsoftActiveKey ?: ""
viewModel.activeEngine()
}
}
updateDateTime() updateDateTime()
binding.ivSetting.setOnClickListener { binding.ivSetting.setOnClickListener {
startActivity(Intent(this, SettingActivity::class.java)) startActivity(Intent(this, SettingActivity::class.java))
@@ -292,7 +320,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
SensorScaleUtils.addWeightListener { weight -> SensorScaleUtils.addWeightListener { weight ->
Timber.d("registerDataChange weight = $weight") Timber.d("registerDataChange weight = $weight")
if (isPageVisible.not() || isStartRecognize.not()) return@addWeightListener if (isPageVisible.not() || isStartRecognize.not()) {
return@addWeightListener
}
val isWeightChange = weight - lastWeight > 0.05 val isWeightChange = weight - lastWeight > 0.05
recognizeByWeight(weight, isWeightChange) recognizeByWeight(weight, isWeightChange)
} }
@@ -607,6 +637,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (displays.size > 1) return displays[1] if (displays.size > 1) return displays[1]
return null return null
} }
private val recognizeViewModel by lazy { viewModels<RecognizeViewModel>().value }
private fun setupSecondaryDisplay() { private fun setupSecondaryDisplay() {
val secondaryDisplay = getSecondaryDisplay() val secondaryDisplay = getSecondaryDisplay()
@@ -619,7 +650,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
activity = this, activity = this,
display = secondaryDisplay, display = secondaryDisplay,
userViewModel = viewModel, userViewModel = viewModel,
recognizeViewModel = viewModels<RecognizeViewModel>().value recognizeViewModel = recognizeViewModel
).apply { ).apply {
setStepChangeCallback { step -> setStepChangeCallback { step ->
if (step == 1) { if (step == 1) {
@@ -653,13 +684,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
override fun onDestroy() {
//SensorScaleUtils.closeScale()
presentation?.cancel()
super.onDestroy()
shutdownCamera()
}
private fun updateDateTime() { private fun updateDateTime() {
val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 E", Locale.CHINA) val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 E", Locale.CHINA)
val dateTime = formatter.format(LocalDateTime.now(ZoneId.of("Asia/Shanghai"))) val dateTime = formatter.format(LocalDateTime.now(ZoneId.of("Asia/Shanghai")))
@@ -696,7 +720,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
binding.root.postDelayed({ binding.root.postDelayed({
recognizeByWeight(weight, true) recognizeByWeight(weight, true)
},500) }, 500)
} }
isRefreshPage = !isRefreshPage isRefreshPage = !isRefreshPage
} }
@@ -763,6 +787,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
return return
} }
} }
if (eatNum == 0) {
ToastUtils.showToast("您好,当前重量不足一份")
return
}
val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0)
val order = FoodOrder( val order = FoodOrder(
deviceId = GlobalData.deviceId, deviceId = GlobalData.deviceId,
@@ -836,7 +864,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
} }
} }
private var isRefreshPage = false private var isRefreshPage = false
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onClickBackEvent(event: ClickBackEvent) { fun onClickBackEvent(event: ClickBackEvent) {
isRefreshPage = false isRefreshPage = false
@@ -893,4 +923,168 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
} }
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 = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 60 * 1000L
private val dealyMillis = 30 * 1000L
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
viewModel.getFaceIncrementList(
pageNo = taskPageNo,
timestamp = SpTool.getLastFaceTimestamp()
) { list ->
runOnUiThread {
if (list.isEmpty()) {
return@runOnUiThread
}
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
updateFaceData(list)
}
}
}
}
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
val faceList = mutableListOf<FaceEntity>()
try {
list.forEach { model ->
if (model.faceDeleted == true) {
//删除数据
FaceDatabase.getInstance(this).faceDao().deleteFaceById(model.userId)
} else {
//保存数据
val faceEntity = FaceEntity(
model.userId,
null,
Base64.decode(model.faceFeatureStr)
).also {
it.userType = "1"
}
faceList.add(faceEntity)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
if (faceList.isNotEmpty()) {
FaceDatabase.getInstance(this).faceDao().insert(faceList)
}
recognizeViewModel.refreshFaceList();
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
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 fun addSocketListener() {
// //val ipAddress = NetworkUtils.getIPAddress(true)
// //Log.d(TAG, "addSocketListener,ipAddress: $ipAddress");
// tcpClient = TcpClient(
// "192.168.1.95",
// 5000,
// GlobalData.deviceId, // clientId
// 5000, // connectTimeoutMs
// 10 * 1000, // heartbeatIntervalMs
// 30 * 1000 // heartbeatTimeoutMs
// )
//
// tcpClient?.setListener(object : TcpClientListenerImpl() {
// override fun onSendSuccess(json: JSONObject?) {
// super.onSendSuccess(json)
//// toast("发送成功")
// }
//
// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
// super.onSendFailed(json, e)
// ToastUtils.showToast("发送失败${e?.message}")
// }
//
// override fun onMessage(json: JSONObject?) {
// super.onMessage(json)
// json?.let { receiveMessage(json!!) }
// }
// })
// tcpClient?.start();
//
// try {
// LanServer.getInstance().let {
// it.setListener(object : LanServerListenerImpl() {
// override fun onMessageReceived(clientId: String?, message: JSONObject?) {
// super.onMessageReceived(clientId, message)
// //toast("收到消息clientId=$clientId")
// }
// })
// it.start()
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// /**
// * 接收服务端发送的消息
// *
// * @param json 人脸数据
// */
// private fun receiveMessage(json: JSONObject) {
// try {
// val faceId = json.getString("faceId");
// val faceFeature = json.getString("faceFeature");
// queryByUserName(faceId, { faceEntity ->
// if (faceEntity != null) {
// //已存在用户faceId的人脸数据
// return@queryByUserName
// }
// //不存在用户faceId,保存数据
// val faceEntity2 = FaceEntity(faceId, null, Base64.decode(faceFeature))
// //设置用户类型:1-内部员工、2-临时用户、3-普通会员、或者其它待定类型
// faceEntity2.setUserType("2")
// saveFaceData(faceEntity2)
// })
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// private fun saveFaceData(faceEntity: FaceEntity) {
// Thread {
// FaceDatabase.getInstance(this).faceDao().insert(faceEntity)
// }.start()
// }
//
// private fun queryByUserName(userName: String, block: (FaceEntity) -> Unit) {
// Thread {
// val faceEntity = FaceDatabase.getInstance(this).faceDao().queryByUserName(userName)
// runOnUiThread {
// block(faceEntity)
// }
// }.start()
// }
} }
@@ -166,7 +166,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据") ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return return
} }
settingActivity?.showWaitingDialog("拍照中……") settingActivity?.showWaitingDialog("采集中……")
cameraUtils.takePhoto(cameraCallback) { errMsg -> cameraUtils.takePhoto(cameraCallback) { errMsg ->
ToastUtils.showToast("拍照异常,请重新操作") ToastUtils.showToast("拍照异常,请重新操作")
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
@@ -374,10 +374,8 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
clickIndex = -1 clickIndex = -1
binding.editFoodName.setText("") binding.editFoodName.setText("")
// foodList.clear() searchFoodList.clear()
// foodAdapter.updateData(mutableListOf()) searchFoodAdapter.notifyDataSetChanged()
// foodAdapter.notifyDataSetChanged()
//loadEmptyView()
} }
override fun onResume() { override fun onResume() {
@@ -48,8 +48,10 @@ class NumberPayFragment : BaseFragment<FragmentNumberPayBinding>() {
payActivity.getMemberInfoByPhone(phone, key) { memberInfo -> payActivity.getMemberInfoByPhone(phone, key) { memberInfo ->
if (memberInfo == null) { if (memberInfo == null) {
payActivity.hideWaitingDialog() binding.root.postDelayed({
//ToastUtils.showToast("查询会员信息失败,请稍后重试") payActivity.hideWaitingDialog()
ToastUtils.showToast("未查询到会员信息,请稍后重试")
}, 1000)
return@getMemberInfoByPhone return@getMemberInfoByPhone
} }
//绑定订单使用faceUserId //绑定订单使用faceUserId
@@ -137,7 +137,8 @@ class PayResultFragment : BaseFragment<FragmentPayResultBinding>() {
payActivity.getQrCodeImg( payActivity.getQrCodeImg(
orderId = payActivity.foodOrderId, orderId = payActivity.foodOrderId,
userId = memberInfo!!.id, userId = memberInfo!!.id,
totalFee = payAmount.format2String(2) //混合支付,实际支付金额
//totalFee = payAmount.format2String(2)
) { qrCodeImg -> ) { qrCodeImg ->
binding.ivPayQrCode.load(qrCodeImg) binding.ivPayQrCode.load(qrCodeImg)
//更新副屏二维码 //更新副屏二维码
@@ -44,7 +44,8 @@ class ScanQrCodePayFragment : BaseFragment<FragmentScanQrcodePayBinding>() {
//不需要绑定会员id //不需要绑定会员id
payActivity.getQrCodeImg( payActivity.getQrCodeImg(
orderId = payActivity.foodOrderId, orderId = payActivity.foodOrderId,
totalFee = payAmount.format2String(2) //非会员扫码支付,不用传金额,后端处理,混合支付要传
//totalFee = payAmount.format2String(2)
) { ) {
payQrCodePic = it payQrCodePic = it
binding.ivPayQrCode.load(payQrCodePic) binding.ivPayQrCode.load(payQrCodePic)
@@ -20,6 +20,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.math.BigDecimal
import java.math.RoundingMode
/** /**
* 将 Int 值转换为 dp 值 * 将 Int 值转换为 dp 值
@@ -65,6 +67,10 @@ val Float.sp: Float
Resources.getSystem().displayMetrics Resources.getSystem().displayMetrics
) )
fun Double.roundedDecimalPlace(num: Int = 2): Double {
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
}
fun Double?.format2String(): String = this.format2String(1) fun Double?.format2String(): String = this.format2String(1)
fun Double?.format2String(num:Int): String = "%.${num}f".format(this?:0.0) fun Double?.format2String(num:Int): String = "%.${num}f".format(this?:0.0)
@@ -130,4 +130,10 @@ data class ChargeModeEvent(
data class ClickBackEvent( data class ClickBackEvent(
var name: String = "" var name: String = ""
)
data class DeviceConfig(
var arcsoftAppId:String?=null,
var arcsoftSdkKey:String?=null,
var arcsoftActiveKey:String?=null
) )
@@ -20,7 +20,9 @@ data class UserFaceModel(
// val userFaceId: String? = "", // val userFaceId: String? = "",
// @SerializedName("userId") // @SerializedName("userId")
val userId: String? = "", val userId: String? = "",
val faceFeatureStr: String? = "" val faceFeatureStr: String? = "",
val faceDeleted: Boolean? = false
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
@@ -3,6 +3,7 @@ package com.sw.dualscreen.network.api
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
@@ -66,7 +67,7 @@ interface ApiService {
@Body param: Map<String, Int> @Body param: Map<String, Int>
// @Query("pageNum") pageNum: Int, // @Query("pageNum") pageNum: Int,
// @Query("pageSize") pageSize: Int // @Query("pageSize") pageSize: Int
): ApiResponse<List<UserFaceModel>> ): ApiResponse<List<UserFaceModel>?>
@GET @GET
suspend fun getUserFaceCache2( suspend fun getUserFaceCache2(
@@ -161,7 +162,7 @@ interface ApiService {
// @Url url: String = "${GlobalData.appBaseUrl}/pay/yx-check-out-pay/turnOrderInfo", // @Url url: String = "${GlobalData.appBaseUrl}/pay/yx-check-out-pay/turnOrderInfo",
// @Body param: HashMap<String, String> // @Body param: HashMap<String, String>
// ): ApiResponse<PayResult?> // ): ApiResponse<PayResult?>
/** /**
* 查询支付结果 * 查询支付结果
*/ */
@GET @GET
@@ -197,7 +198,7 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/getQrCodeImg", @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/pay/app/getQrCodeImg",
@Query("orderNo") orderNo: String, @Query("orderNo") orderNo: String,
@Query("memberId") memberId: String?, @Query("memberId") memberId: String?,
@Query("totalFee") totalFee: String? @Query("totalFee") totalFee: String? = null
): ApiResponse<String?> ): ApiResponse<String?>
/** /**
@@ -251,4 +252,20 @@ interface ApiService {
@Part foodPics: List<MultipartBody.Part> @Part foodPics: List<MultipartBody.Part>
): ApiResponse<String> ): ApiResponse<String>
/**
* 获取设备配置数据
*/
@GET
suspend fun getDeviceConfig(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getYxEquipmentByEquipmentCode"
): ApiResponse<DeviceConfig?>
/**
* 获取人脸增量数据
*/
@POST
suspend fun getFaceIncrementList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
} }
@@ -35,6 +35,7 @@ import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.load import com.sw.dualscreen.ext.load
import com.sw.dualscreen.ext.maskName import com.sw.dualscreen.ext.maskName
import com.sw.dualscreen.ext.roundedDecimalPlace
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
@@ -61,6 +62,7 @@ import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.text.DecimalFormat
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -391,7 +393,7 @@ class MainScreenPresentation(
} }
private var isLoadUnbilledMode = false private var isLoadUnbilledMode = false
private val df by lazy { DecimalFormat("#.##") }
/** /**
* 餐品识别成功,不计费模式 * 餐品识别成功,不计费模式
*/ */
@@ -411,8 +413,10 @@ class MainScreenPresentation(
//updateFoodInfo(foodInfo) //updateFoodInfo(foodInfo)
binding.nutritionInclude.tvUserName.text = userNutrition?.name.maskName() binding.nutritionInclude.tvUserName.text = userNutrition?.name.maskName()
binding.nutritionInclude.tvRecommendHeat.text = val recommendCalorie = userNutrition?.recommendCalorie ?: 0.0
"推荐热量:${userNutrition?.recommendCalorie.format2String(2)}kcal"
binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${df.format(recommendCalorie)}kcal"
// binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${recommendCalorie.removeTrailingZeros()}kcal"
//updateWeight(lastWeight / 1000) //updateWeight(lastWeight / 1000)
// ------------------------------------------------- // -------------------------------------------------
// 删除userNutritionData,改为userNutrition // 删除userNutritionData,改为userNutrition
@@ -24,14 +24,16 @@ object UserNutritionUtils {
dinnerType: String dinnerType: String
): UserEnergy { ): UserEnergy {
// 初始化变量 // 初始化变量
var totalKcal = 0.0 //var totalKcal = 0.0
var (calorie, grain, fruitsVegetables, meatEggs) = List(4) { 0.0 } var (calorie, grain, fruitsVegetables, meatEggs) = List(4) { 0.0 }
// 处理食物信息 // 处理食物信息
calorie = foodInfo.calorie ?: 0.0 //calculateValue(foodInfo.calorie, weight) if (weight > 0.0) {
grain = foodInfo.stapleFood ?: 0.0 //calculateValue(foodInfo.stapleFood, weight) calorie = calculateValue2(foodInfo.calorie,foodInfo.specWeight, weight)
fruitsVegetables = foodInfo.fruitsVegetables ?: 0.0 //calculateValue(foodInfo.fruitsVegetables, weight) grain = calculateValue2(foodInfo.stapleFood, foodInfo.specWeight,weight)
meatEggs = foodInfo.meatEggs ?: 0.0 //calculateValue(foodInfo.meatEggs, weight) fruitsVegetables = calculateValue2(foodInfo.fruitsVegetables, foodInfo.specWeight,weight)
meatEggs = calculateValue2(foodInfo.meatEggs, foodInfo.specWeight,weight)
}
// 合并用户数据 // 合并用户数据
//val maxKcal = (nutrition.calorie?:0.0) * calculateDinnerTypeRatio(dinnerType) / 10.0 //val maxKcal = (nutrition.calorie?:0.0) * calculateDinnerTypeRatio(dinnerType) / 10.0
@@ -40,7 +42,7 @@ object UserNutritionUtils {
fruitsVegetables += nutrition.fruitsVegetables ?: 0.0 fruitsVegetables += nutrition.fruitsVegetables ?: 0.0
meatEggs += nutrition.meatEggs ?: 0.0 meatEggs += nutrition.meatEggs ?: 0.0
totalKcal = max(calorie, 0.0) //totalKcal = max(calorie, 0.0)
// 判断当餐最大热量 // 判断当餐最大热量
//val maxKcal = userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10 //val maxKcal = userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
//Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}") //Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
@@ -55,7 +57,7 @@ object UserNutritionUtils {
// meat = parseRecommend(foodInfo.meatEggsRecommend, meatEggs) // meat = parseRecommend(foodInfo.meatEggsRecommend, meatEggs)
// ) // )
return UserEnergy( return UserEnergy(
calorie = totalKcal, calorie = calorie,
grain = grain, grain = grain,
fruitsVegetables = fruitsVegetables, fruitsVegetables = fruitsVegetables,
meatEggs = meatEggs meatEggs = meatEggs
@@ -140,7 +142,10 @@ object UserNutritionUtils {
else -> 0 else -> 0
} }
} }
private fun calculateValue2(nutrient: Double?, specWeight: Double?, weight: Double): Double {
if ((nutrient?:0.0) == 0.0 || (specWeight?:0.0) == 0.0) return 0.0
return nutrient!! * weight / specWeight!!
}
/** /**
* 计算每百克含量 * 计算每百克含量
*/ */
@@ -3,6 +3,7 @@ package com.sw.dualscreen.repository
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
@@ -63,7 +64,7 @@ class RemoteRepository constructor(
suspend fun getUserFaceCache( suspend fun getUserFaceCache(
pageNum: Int, pageNum: Int,
pageSize: Int = 100, pageSize: Int = 100,
): ApiResponse<List<UserFaceModel>> { ): ApiResponse<List<UserFaceModel>?> {
return safeApiCall { return safeApiCall {
apiService.getUserFaceCache( apiService.getUserFaceCache(
param = mapOf( param = mapOf(
@@ -73,6 +74,24 @@ class RemoteRepository constructor(
) )
} }
} }
/**
* 获取人脸数据
*/
suspend fun getFaceIncrementList(
pageNum: Long,
pageSize: Long = 100L,
timestamp: Long
): ApiResponse<List<UserFaceModel>?> {
return safeApiCall {
apiService.getFaceIncrementList(
param = mapOf(
"pageNum" to pageNum,
"pageSize" to pageSize,
"timestamp" to timestamp
)
)
}
}
suspend fun getUserFaceCache2( suspend fun getUserFaceCache2(
pageNum: Int, pageNum: Int,
@@ -151,7 +170,11 @@ class RemoteRepository constructor(
totalFee: String? = null totalFee: String? = null
): ApiResponse<String?> { ): ApiResponse<String?> {
return safeApiCall { return safeApiCall {
apiService.getQrCodeImg(orderNo = orderNo, memberId = memberId, totalFee = totalFee) apiService.getQrCodeImg(
orderNo = orderNo,
memberId = memberId,
totalFee = totalFee
)
} }
} }
@@ -275,4 +298,8 @@ class RemoteRepository constructor(
apiService.uploadCollectFoodPics(params = params, foodPics = fileParts) apiService.uploadCollectFoodPics(params = params, foodPics = fileParts)
} }
} }
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
} }
@@ -0,0 +1,300 @@
package com.sw.dualscreen.socket;
import android.annotation.SuppressLint;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送
* 可用于 Android 和 JVM 程序。
*/
public class LanCommunicationManager {
// ============ 监听配置 ============
private final int port;
private final long HEARTBEAT_TIMEOUT_MS;
private final int MAX_CLIENT_THREADS;
// ============ 状态 ============
private volatile boolean running = false;
private ServerSocket serverSocket;
// ============ 线程池 ============
private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService clientExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// ============ 客户端会话 ============
private final ConcurrentHashMap<String, ClientSession> clients = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Socket, ClientSession> unAuthSessions = new ConcurrentHashMap<>();
// ============ 回调接口 ============
public interface Listener {
// 新客户端完成 AUTH / 认证
void onClientConnected(String clientId);
// 客户端断开
void onClientDisconnected(String clientId);
// 收到业务消息(type != heartbeat/auth)
void onMessageReceived(String clientId, JSONObject message);
}
private Listener listener;
// ============ 构造 ============
public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) {
this.port = port;
this.MAX_CLIENT_THREADS = maxClientThreads;
this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs;
this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads));
}
public void setListener(Listener listener) {
this.listener = listener;
}
// ============ 启动服务端 ============
@SuppressLint("DiscouragedApi")
public void start() throws IOException {
if (running) return;
running = true;
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(2000);
acceptExecutor.execute(this::acceptLoop);
scheduler.scheduleAtFixedRate(this::heartbeatCheck,
HEARTBEAT_TIMEOUT_MS,
HEARTBEAT_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
System.out.println("LanCommunicationManager started on port " + port);
}
private void acceptLoop() {
while (running) {
try {
Socket socket = serverSocket.accept();
socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2);
ClientSession session = new ClientSession(socket);
unAuthSessions.put(socket, session);
clientExecutor.execute(() -> clientReadLoop(session));
} catch (SocketTimeoutException ignore) {
} catch (Exception e) {
if (running) e.printStackTrace();
}
}
}
// ============ 处理客户端数据读取 ============
private void clientReadLoop(ClientSession session) {
Socket socket = session.socket;
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
while (running && !socket.isClosed()) {
int len;
try {
len = in.readInt();
} catch (SocketTimeoutException ste) {
continue;
}
if (len <= 0 || len > 10 * 1024 * 1024) break;
byte[] buf = new byte[len];
in.readFully(buf);
session.updateLastSeen();
JSONObject msg = new JSONObject(new String(buf));
handleMessage(session, msg);
}
} catch (Exception ignored) {
} finally {
closeSession(session);
}
}
private void handleMessage(ClientSession session, JSONObject msg) {
String type = msg.optString("type", "");
switch (type) {
case "auth":
handleAuth(session, msg);
break;
case "heartbeat":
session.updateLastSeen();
break;
default:
if (listener != null && session.clientId != null) {
listener.onMessageReceived(session.clientId, msg);
}
break;
}
}
private void handleAuth(ClientSession session, JSONObject msg) {
String clientId = msg.optString("clientId", null);
if (clientId == null) return;
session.clientId = clientId;
// 移动到已认证 map
unAuthSessions.remove(session.socket);
clients.put(clientId, session);
if (listener != null) listener.onClientConnected(clientId);
sendToSession(session, ack("auth_ok"));
}
// ============ 心跳超时 ============
private void heartbeatCheck() {
long now = System.currentTimeMillis();
for (Map.Entry<String, ClientSession> e : clients.entrySet()) {
ClientSession s = e.getValue();
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) {
closeSession(s);
}
}
for (ClientSession s : unAuthSessions.values()) {
if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) {
closeSession(s);
}
}
}
// ============ 发送 ============
public boolean sendToClient(String clientId, JSONObject json) {
ClientSession s = clients.get(clientId);
return s != null && sendToSession(s, json);
}
public void broadcast(JSONObject json) {
for (ClientSession s : clients.values()) {
sendToSession(s, json);
}
}
private boolean sendToSession(ClientSession s, JSONObject json) {
try {
DataOutputStream out = s.out;
synchronized (out) {
byte[] data = json.toString().getBytes();
out.writeInt(data.length);
out.write(data);
out.flush();
}
return true;
} catch (Exception e) {
closeSession(s);
return false;
}
}
// ============ ACK ============
private JSONObject ack(String type) {
JSONObject j = new JSONObject();
try {
j.put("type", "ack");
j.put("ack", type);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return j;
}
// ============ 停止 ============
public void stop() {
running = false;
try {
serverSocket.close();
} catch (Exception ignored) {
}
for (ClientSession s : clients.values()) closeSession(s);
for (ClientSession s : unAuthSessions.values()) closeSession(s);
acceptExecutor.shutdownNow();
clientExecutor.shutdownNow();
scheduler.shutdownNow();
System.out.println("LanCommunicationManager stopped");
}
// ============ 会话类 ============
public static class ClientSession {
public final Socket socket;
public final DataOutputStream out;
public volatile long lastSeen = System.currentTimeMillis();
public volatile String clientId;
public ClientSession(Socket socket) throws IOException {
this.socket = socket;
this.out = new DataOutputStream(socket.getOutputStream());
}
public void updateLastSeen() {
lastSeen = System.currentTimeMillis();
}
}
public void closeSession(ClientSession session) {
if (session == null) return;
try {
Socket socket = session.socket;
// 1. 从已认证表移除
if (session.clientId != null) {
ClientSession removed = clients.remove(session.clientId);
if (removed != null && listener != null) {
listener.onClientDisconnected(session.clientId);
}
}
// 2. 从未认证表移除
unAuthSessions.remove(socket);
// 3. 关闭输出流
try {
session.out.close();
} catch (Exception ignored) {
}
// 4. 关闭 socket
try {
if (!socket.isClosed()) socket.close();
} catch (Exception ignored) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,21 @@
package com.sw.dualscreen.socket;
public class LanServer {
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
if (instance == null) {
synchronized (LanServer.class) {
if (instance == null) {
instance = new LanCommunicationManager(
5000, // 监听端口
20, // 最大客户端数
10_000 // 心跳超时时间 10 秒
);
}
}
}
return instance;
}
}
@@ -0,0 +1,22 @@
package com.sw.face.collect.view
import android.util.Log
import com.sw.dualscreen.socket.LanCommunicationManager
import org.json.JSONObject
open class LanServerListenerImpl: LanCommunicationManager.Listener {
companion object {
private const val TAG = "LanServerListenerImpl"
}
override fun onClientConnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId")
}
override fun onClientDisconnected(clientId: String?) {
Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId")
}
override fun onMessageReceived(clientId: String?, message: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientIdmessage = $message")
}
}
@@ -0,0 +1,330 @@
package com.sw.dualscreen.socket;
import android.util.Log;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* TcpClient - Android ready
* <p>
* Features:
* - length-prefix protocol (int length + bytes)
* - separate read thread and write queue & writer thread
* - auto-reconnect with exponential backoff
* - heartbeat scheduler
* - send queue with optional callback for send result
* - auto send "auth" JSON after connection
*/
public class TcpClient {
private static final String TAG = "TcpClient";
// configuration
private final String serverIp;
private final int serverPort;
private final String clientId; // will be sent in auth message
private final int connectTimeoutMs;
private final long heartbeatIntervalMs;
private final long heartbeatTimeoutMs;
// socket + streams
private Socket socket;
private DataOutputStream out;
private DataInputStream in;
// threads & executors
private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer"));
private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader"));
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler"));
private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect"));
// send queue
private final BlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<>();
// state
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicBoolean connected = new AtomicBoolean(false);
private final AtomicBoolean authSent = new AtomicBoolean(false);
// reconnection/backoff
private final long baseReconnectDelayMs = 1000; // 1s
private final long maxReconnectDelayMs = 30_000; // 30s
private final AtomicInteger reconnectAttempt = new AtomicInteger(0);
// heartbeat task future
private ScheduledFuture<?> heartbeatFuture;
// listener
public interface Listener {
void onConnected();
void onDisconnected(Exception e);
void onMessage(JSONObject json);
void onSendSuccess(JSONObject json);
void onSendFailed(JSONObject json, Exception e);
}
private Listener listener;
public void setListener(Listener l) {
this.listener = l;
}
// ctor
public TcpClient(String serverIp, int serverPort, String clientId,
int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) {
this.serverIp = serverIp;
this.serverPort = serverPort;
this.clientId = clientId;
this.connectTimeoutMs = connectTimeoutMs;
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
}
// start client (will attempt connect)
public void start() {
if (running.getAndSet(true)) return;
scheduleConnect(0);
// writer thread drains sendQueue
writerExecutor.execute(this::writerLoop);
}
// stop client and cleanup
public void stop() {
running.set(false);
cancelHeartbeat();
closeSocketQuiet();
writerExecutor.shutdownNow();
readerExecutor.shutdownNow();
scheduler.shutdownNow();
connectExecutor.shutdownNow();
sendQueue.clear();
}
// send JSON (queued). Non-blocking.
public void send(JSONObject json) {
if (!running.get()) return;
sendQueue.offer(json);
}
// AUTH shortcut (immediately send auth JSON)
private void sendAuth() {
try {
JSONObject auth = new JSONObject();
auth.put("type", "auth");
auth.put("clientId", clientId);
sendQueue.offer(auth);
authSent.set(true);
} catch (Exception ignored) {
}
}
// writer thread loop (serializes sends)
private void writerLoop() {
while (running.get()) {
try {
JSONObject json = sendQueue.take(); // blocks
if (connected.get() && out != null) {
try {
byte[] data = json.toString().getBytes();
synchronized (out) {
out.writeInt(data.length);
out.write(data);
out.flush();
}
if (listener != null) listener.onSendSuccess(json);
} catch (Exception e) {
if (listener != null) listener.onSendFailed(json, e);
// on write failure, attempt reconnect
safeCloseAndScheduleReconnect(e);
}
} else {
// not connected: requeue it and wait for connection
sendQueue.offer(json);
Thread.sleep(500); // avoid busy loop
}
} catch (InterruptedException ignored) {
break;
}
}
}
// reader loop (runs in readerExecutor)
private void startReaderLoop() {
readerExecutor.execute(() -> {
try {
while (running.get() && connected.get() && in != null) {
int length;
try {
length = in.readInt(); // will throw SocketTimeoutException if set
} catch (SocketTimeoutException ste) {
// used to detect socket liveness; continue loop
continue;
}
if (length <= 0 || length > 10 * 1024 * 1024) {
// invalid length, break
throw new RuntimeException("Invalid message length: " + length);
}
byte[] buf = new byte[length];
in.readFully(buf);
String s = new String(buf);
try {
JSONObject json = new JSONObject(s);
// update last seen time via heartbeat ack if needed
if ("heartbeat".equals(json.optString("type"))) {
// optionally respond or update time
} else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) {
// ignore or process ack
} else {
if (listener != null) listener.onMessage(json);
}
} catch (Exception je) {
Log.w(TAG, "Invalid JSON from server: " + s, je);
}
}
} catch (Exception e) {
if (running.get()) {
safeCloseAndScheduleReconnect(e);
}
}
});
}
// schedule connect attempt with delay (ms)
private void scheduleConnect(long delayMs) {
connectExecutor.execute(() -> {
try {
if (delayMs > 0) Thread.sleep(delayMs);
} catch (InterruptedException ignored) {
}
if (!running.get()) return;
tryConnect();
});
}
// connect logic
private void tryConnect() {
if (!running.get()) return;
closeSocketQuiet(); // ensure closed
try {
Socket s = new Socket();
s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs);
s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000));
socket = s;
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
connected.set(true);
reconnectAttempt.set(0);
authSent.set(false);
// start reader
startReaderLoop();
// send auth immediately
sendAuth();
// start heartbeat
startHeartbeat();
if (listener != null) listener.onConnected();
Log.i(TAG, "Connected to " + serverIp + ":" + serverPort);
} catch (Exception e) {
Log.w(TAG, "Connect failed: " + e.getMessage());
scheduleReconnectWithBackoff();
}
}
// start heartbeat scheduler
private void startHeartbeat() {
cancelHeartbeat();
heartbeatFuture = scheduler.scheduleAtFixedRate(() -> {
if (!running.get() || !connected.get()) return;
try {
JSONObject hb = new JSONObject();
hb.put("type", "heartbeat");
hb.put("time", System.currentTimeMillis());
sendQueue.offer(hb);
} catch (Exception ignored) {
}
}, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS);
}
private void cancelHeartbeat() {
if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) {
heartbeatFuture.cancel(true);
heartbeatFuture = null;
}
}
// close socket quietly and notify listener
private void safeCloseAndScheduleReconnect(Exception cause) {
closeSocketQuiet();
if (listener != null) listener.onDisconnected(cause);
scheduleReconnectWithBackoff();
}
private void scheduleReconnectWithBackoff() {
int attempt = reconnectAttempt.incrementAndGet();
long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs);
Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms");
scheduleConnect(delay);
}
// compute exponential backoff
private long maxReconnectDelayMsFromAttempt(int attempt) {
long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30));
if (d < 0) d = maxReconnectDelayMs;
return Math.min(d, maxReconnectDelayMs);
}
// close socket and streams
private void closeSocketQuiet() {
connected.set(false);
cancelHeartbeat();
try {
if (out != null) {
out.close();
}
} catch (Exception ignored) {
}
try {
if (in != null) {
in.close();
}
} catch (Exception ignored) {
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (Exception ignored) {
}
out = null;
in = null;
socket = null;
}
// helper: when manually call reconnect (immediately)
public void reconnectNow() {
scheduleConnect(0);
}
// helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered)
public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException {
return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS);
}
}
@@ -0,0 +1,33 @@
package com.sw.face.collect.view
import android.util.Log
import com.sw.dualscreen.socket.TcpClient
import org.json.JSONObject
import java.lang.Exception
open class TcpClientListenerImpl : TcpClient.Listener {
companion object {
private const val TAG = "TcpClientListenerImpl"
}
override fun onConnected() {
Log.d(TAG, "addSocketListener,onConnected: ")
}
override fun onDisconnected(e: Exception?) {
Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}")
}
override fun onMessage(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onMessage: $json")
}
override fun onSendSuccess(json: JSONObject?) {
Log.d(TAG, "addSocketListener,onSendSuccess: $json")
}
override fun onSendFailed(json: JSONObject?, e: Exception?) {
Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}")
}
}
@@ -0,0 +1,47 @@
package com.sw.dualscreen.socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* Simple UDP discovery client:
* Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip"
*/
public class UdpDiscoveryClient {
public interface Listener {
void onFound(String ip, String serverName);
void onError(Exception e);
}
public void discover(int discoveryPort, int timeoutMs, Listener listener) {
new Thread(() -> {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
byte[] data = "DISCOVER_SERVER".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort);
socket.send(packet);
socket.setSoTimeout(timeoutMs);
byte[] buf = new byte[512];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
socket.receive(resp);
String msg = new String(resp.getData(), 0, resp.getLength());
if (msg.startsWith("SERVER_FOUND")) {
// format: SERVER_FOUND:serverName:ip
String[] parts = msg.split(":", 3);
if (parts.length >= 3) {
listener.onFound(parts[2], parts[1]);
return;
}
}
listener.onError(new Exception("Invalid response"));
} catch (Exception e) {
listener.onError(e);
}
}).start();
}
}
@@ -0,0 +1,113 @@
package com.sw.dualscreen.socket;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class test {
LanCommunicationManager manager = new LanCommunicationManager(
9999, // 端口
20, // 最大客户端线程
30_000 // 心跳超时 30 秒
);
public void test() {
manager.setListener(new LanCommunicationManager.Listener() {
@Override
public void onClientConnected(String clientId) {
System.out.println("新的客户端上线:" + clientId);
}
@Override
public void onClientDisconnected(String clientId) {
System.out.println("客户端离线:" + clientId);
}
@Override
public void onMessageReceived(String clientId, JSONObject msg) {
System.out.println("收到 " + clientId + " 的消息:" + msg);
}
});
// 启动
try {
manager.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 给某个客户端发送
JSONObject j = new JSONObject();
try {
j.put("type", "cmd");
j.put("content", "hello");
} catch (JSONException e) {
throw new RuntimeException(e);
}
manager.sendToClient("device123", j);
// 广播
manager.broadcast(j);
}
public void clientTets() {
// 1) discover server (optional)
UdpDiscoveryClient disc = new UdpDiscoveryClient();
disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() {
@Override
public void onFound(String ip, String serverName) {
startClient(ip);
}
@Override
public void onError(Exception e) { /* fallback to manual IP */ }
});
}
// 2) start client
private TcpClient client;
private void startClient(String serverIp) {
client = new TcpClient(
serverIp,
9999,
"device123", // clientId
5000, // connectTimeoutMs
10_000, // heartbeatIntervalMs
30_000 // heartbeatTimeoutMs
);
client.setListener(new TcpClient.Listener() {
@Override
public void onConnected() {
Log.i("APP", "connected");
}
@Override
public void onDisconnected(Exception e) {
Log.i("APP", "disconnected", e);
}
@Override
public void onMessage(JSONObject json) {
Log.i("APP", "msg:" + json);
}
@Override
public void onSendSuccess(JSONObject json) {
}
@Override
public void onSendFailed(JSONObject json, Exception e) {
}
});
client.start();
}
}
@@ -0,0 +1,66 @@
package com.sw.dualscreen.utils
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.text.contains
import kotlin.text.format
import kotlin.text.replace
/**
* 去除小数点后无效零的工具类
* 功能示例:
* 1230.00 => 1230
* 1230.10 => 1230.1
* 3.40 => 3.4
* 3.0 => 3
*/
class RemoveZeroUtils {
companion object {
/**
* 方法1:使用字符串格式化(最简单)
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByFormat(number: Double): String {
return "%.10f".format(number) // 先格式化为固定小数位
.replace(Regex("0*$"), "") // 移除末尾的零
.replace(Regex("\\.$"), "") // 如果小数点后全为零,移除小数点
}
/**
* 方法2:使用DecimalFormat(推荐)
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByDecimalFormat(number: Double): String {
val format = DecimalFormat("0.##########")
format.roundingMode = RoundingMode.FLOOR
return format.format(number)
}
/**
* 方法3:使用正则表达式处理字符串
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByRegex(number: Double): String {
var str = number.toString()
// 如果包含小数点,处理末尾的零
if (str.contains(".")) {
str = str.replace(Regex("0+?$"), "") // 移除末尾的零
.replace(Regex("[.]$"), "") // 如果小数点后全为零,移除小数点
}
return str
}
}
}
// 扩展函数方式,更符合Kotlin风格
fun Double?.removeTrailingZeros(): String {
if (this == null) return ""
return RemoveZeroUtils.removeZeroByDecimalFormat(this)
}
@@ -0,0 +1,19 @@
package com.sw.dualscreen.utils;
import com.sw.dualscreen.MyApp;
public class SpTool {
public static final String LAST_FACE_TIMESTAMP = "faceTimestamp";
public static long getLastFaceTimestamp() {
return SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").get(LAST_FACE_TIMESTAMP, 0L);
}
public static void setLastFaceTimestamp(long timestamp) {
SPUtil.Companion.getInstance(MyApp.getContext(), "default_sp").put(LAST_FACE_TIMESTAMP, timestamp);
}
}
@@ -9,21 +9,21 @@ object StorageUtils {
activity.requestSinglePermissionResult(Manifest.permission.WRITE_EXTERNAL_STORAGE) { activity.requestSinglePermissionResult(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
callback() callback()
} }
// activity.requestSinglePermissionResult(Manifest.permission.CAMERA) { activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//// if (Environment.isExternalStorageManager()) { // if (Environment.isExternalStorageManager()) {
//// callback() // callback()
//// } else { // } else {
//// val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) // val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
////// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE) //// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE)
//// activity.launchActivityResult(intent) { // activity.launchActivityResult(intent) {
//// callback() // callback()
//// } // }
//// } // }
//// } else { // } else {
//
//// } // }
// } }
} }
fun requestCameraPermission(activity: BaseActivity<*>, callback: () -> Unit) { fun requestCameraPermission(activity: BaseActivity<*>, callback: () -> Unit) {
@@ -5,6 +5,7 @@ import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.model.request.UserNutritionParam import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder import com.sw.dualscreen.model.response.FoodOrder
@@ -14,6 +15,7 @@ import com.sw.dualscreen.model.response.UserFaceModel2
import com.sw.dualscreen.model.response.UserNutrition import com.sw.dualscreen.model.response.UserNutrition
import com.sw.dualscreen.objbox.CollectedFoodInfo import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.utils.SpTool
import com.sw.plate.App import com.sw.plate.App
import com.sw.plate.utils.Base64 import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
@@ -98,6 +100,7 @@ class UserViewModel : BaseViewModel() {
ToastUtils.showToast("查询人脸数据为空") ToastUtils.showToast("查询人脸数据为空")
return@withContext return@withContext
} }
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
// val item = list.firstOrNull { it.userId == "1951105919342936066" } // val item = list.firstOrNull { it.userId == "1951105919342936066" }
// Timber.tag(TAG).d("getUserFaceCache userId = ${item?.userId}") // Timber.tag(TAG).d("getUserFaceCache userId = ${item?.userId}")
val faceEntity = list.map { val faceEntity = list.map {
@@ -146,6 +149,30 @@ class UserViewModel : BaseViewModel() {
// } // }
// } // }
// } // }
/**
* 获取人脸数据
*/
fun getFaceIncrementList(
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
block: (List<UserFaceModel>) -> Unit
) {
Timber.tag(TAG).d("getFaceIncrementList")
launch {
val response = repository.getFaceIncrementList(
pageNum = pageNo.toLong(),
pageSize = pageSize.toLong(),
timestamp = timestamp
)
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
block(list)
}
}
}
}
fun getCollectedFoodList( fun getCollectedFoodList(
pageNo: Int = 1, pageNo: Int = 1,
@@ -426,7 +453,7 @@ class UserViewModel : BaseViewModel() {
fun bindOrder(userId: String, orderId: String, mode: Int, block: (Boolean) -> Unit) { fun bindOrder(userId: String, orderId: String, mode: Int, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("bindOrder") Timber.tag(TAG).d("bindOrder")
launchWithLoading { launchWithLoading {
val response = repository.bindOrder(userId = userId, orderId = orderId, mode =mode) val response = repository.bindOrder(userId = userId, orderId = orderId, mode = mode)
if (parseResponse(response)) { if (parseResponse(response)) {
block(true) block(true)
} else { } else {
@@ -492,4 +519,16 @@ class UserViewModel : BaseViewModel() {
// } // }
return resp.code == "00000" return resp.code == "00000"
} }
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
Timber.tag(TAG).d("getDeviceConfig")
launchWithLoading {
val response = repository.getDeviceConfig()
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
} }
@@ -39,7 +39,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginHorizontal="64dp" android:layout_marginHorizontal="64dp"
android:inputType="text" android:inputType="textVisiblePassword"
android:background="@drawable/setting_border_gray3" android:background="@drawable/setting_border_gray3"
android:hint="输入消费码" android:hint="输入消费码"
android:textColorHint="#ffc4cfda" android:textColorHint="#ffc4cfda"
@@ -48,6 +48,7 @@
android:textSize="36sp" android:textSize="36sp"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:text=""
android:paddingHorizontal="50dp" android:paddingHorizontal="50dp"
android:textStyle="bold" android:textStyle="bold"
tools:ignore="Autofill,HardcodedText,LabelFor,TextFields" /> tools:ignore="Autofill,HardcodedText,LabelFor,TextFields" />
+1 -1
View File
@@ -56,7 +56,7 @@ dependencies {
// implementation("com.licheedev:android-serialport:2.1.5") // implementation("com.licheedev:android-serialport:2.1.5")
val roomVersion = "2.2.5" val roomVersion = "2.2.5"
implementation("androidx.room:room-runtime:$roomVersion") api("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion") annotationProcessor("androidx.room:room-compiler:$roomVersion")
implementation("io.reactivex.rxjava2:rxandroid:2.0.1") implementation("io.reactivex.rxjava2:rxandroid:2.0.1")
@@ -88,4 +88,7 @@ public interface FaceDao {
@Query("UPDATE sqlite_sequence SET seq = 0 WHERE name ='face'") @Query("UPDATE sqlite_sequence SET seq = 0 WHERE name ='face'")
void resetId(); void resetId();
@Query("SELECT * FROM face WHERE user_name = :userName limit 1")
FaceEntity queryByUserName(String userName);
} }
@@ -2,9 +2,11 @@ package com.sw.plate.utils.arcface.facedb.entity;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.TextUtils;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import java.util.Arrays; import java.util.Arrays;
@@ -42,7 +44,14 @@ public class FaceEntity implements Parcelable {
*/ */
@ColumnInfo(name = "register_time") @ColumnInfo(name = "register_time")
private long registerTime; private long registerTime;
/**
* 用户类型:1-普通会员、2-临时用户、3-内部员工、或者其它待定类型
*/
@ColumnInfo(name = "user_type")
private String userType;
@Ignore
private int trackId;//人脸追踪ID
public FaceEntity(String userName, String imagePath, byte[] featureData) { public FaceEntity(String userName, String imagePath, byte[] featureData) {
this.userName = userName; this.userName = userName;
@@ -120,6 +129,22 @@ public class FaceEntity implements Parcelable {
this.registerTime = registerTime; this.registerTime = registerTime;
} }
public int getTrackId() {
return trackId;
}
public void setTrackId(int trackId) {
this.trackId = trackId;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;
@@ -132,8 +157,11 @@ public class FaceEntity implements Parcelable {
dest.writeString(userName); dest.writeString(userName);
dest.writeString(imagePath); dest.writeString(imagePath);
dest.writeByteArray(featureData); dest.writeByteArray(featureData);
dest.writeString(userType);
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {
@@ -143,16 +171,17 @@ public class FaceEntity implements Parcelable {
return false; return false;
} }
FaceEntity that = (FaceEntity) o; FaceEntity that = (FaceEntity) o;
return faceId == that.faceId && return this.faceId == that.faceId &&
registerTime == that.registerTime && this.registerTime == that.registerTime &&
userName.equals(that.userName) && TextUtils.equals(this.userName, that.userName) &&
imagePath.equals(that.imagePath) && TextUtils.equals(this.imagePath, that.imagePath) &&
Arrays.equals(featureData, that.featureData); Arrays.equals(featureData, that.featureData) &&
TextUtils.equals(this.userType, that.userType);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath); int result = Objects.hash(faceId, registerTime, userName, imagePath, userType);
result = 31 * result + Arrays.hashCode(featureData); result = 31 * result + Arrays.hashCode(featureData);
return result; return result;
} }