feat: basic frame

This commit is contained in:
玖叁 2024-12-24 12:15:53 +08:00
parent 1f60b1f2ce
commit 55299209fd
17 changed files with 780 additions and 456 deletions

View File

@ -1,184 +0,0 @@
package icu.fur93.esp32_car
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.room.util.copy
class CarCommand {
companion object {
// 数据包头尾定义
const val PACKET_T_HEAD = 0x00u
const val PACKET_T_TAIL = 0xFFu
const val PACKET_R_HEAD = 0x01u
const val PACKET_R_TAIL = 0xFEu
const val PACKET_MAX_LENGTH = 32u // 数据包最大长度
// 指令定义
const val CMD_GET_BT_STATUS = 0x10u
const val CMD_GET_SPIFFS_STATUS = 0x11u
const val CMD_GET_DISTANCE = 0x12u
const val CMD_MOTOR_MOVE_CONTROL = 0x20u
const val CMD_MOTOR_STEER_CONTROL = 0x21u
const val CMD_MOTOR_SINGLE_CONTROL = 0x22u
const val CMD_MOTOR_ROTATE_CONTROL = 0x23u
const val CMD_MOTOR_XYR_CONTROL = 0x24u
const val CMD_DEMO_PID = 0xf0u
const val CMD_DEMO_PATH = 0xf1u
const val CMD_STATUS_MOTOR = 0xE0u
}
}
data class MotorState(
var pwm: UInt = 0u,
var in1: UInt = 0u,
var in2: UInt = 0u
)
data class CarState(
var controlState: CarControlState = CarControlState(),
var motorAState: MotorState = MotorState(),
var motorBState: MotorState = MotorState(),
var motorCState: MotorState = MotorState(),
var motorDState: MotorState = MotorState()
)
enum class LogDirection {
SEND, // 发送
RECEIVE // 接收
}
// 添加日志数据类
data class LogEntry(
val timestamp: Long = System.currentTimeMillis(),
val direction: LogDirection, // "发送" 或 "接收"
val data: String
)
class CarController(private val onStateChange: () -> Unit) {
// 使用 MutableState 来存储小车状态
private val _carState = mutableStateOf(CarState())
val carState: State<CarState> = _carState
var bluetoothGatt: BluetoothGatt? = null
var rxCharacteristic: BluetoothGattCharacteristic? = null
// 添加日志列表状态
private val _logs = mutableStateOf<List<LogEntry>>(emptyList())
val logs: State<List<LogEntry>> = _logs
// 更新电机状态的方法
private fun updateMotorState(
motorA: MotorState? = null,
motorB: MotorState? = null,
motorC: MotorState? = null,
motorD: MotorState? = null
) {
_carState.value.motorAState = motorA ?: _carState.value.motorAState
_carState.value.motorBState = motorB ?: _carState.value.motorBState
_carState.value.motorCState = motorC ?: _carState.value.motorCState
_carState.value.motorDState = motorD ?: _carState.value.motorDState
}
private fun sendCommand(command: ByteArray) {
rxCharacteristic?.let { characteristic ->
characteristic.value = command
bluetoothGatt?.writeCharacteristic(characteristic)
// 添加发送日志
_logs.value = _logs.value + LogEntry(
direction = LogDirection.SEND,
data = command.joinToString(" ") { "0x%02X".format(it) }
)
}
}
fun moveForward(speed: Int = 255) {
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_MOVE_CONTROL.toByte(), 0x01, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
}
fun moveBackward(speed: Int = 255) {
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_MOVE_CONTROL.toByte(), 0x02, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
}
fun turnLeft(speed: Int = 255) {
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_STEER_CONTROL.toByte(), 0x00, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
}
fun turnRight(speed: Int = 255) {
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_STEER_CONTROL.toByte(), 0x01, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
}
fun stop() {
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_MOVE_CONTROL.toByte(), 0x00, 0x00, CarCommand.PACKET_T_TAIL.toByte()))
}
fun sendXYR(x: Int, y: Int, r: Int) {
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x07, CarCommand.CMD_MOTOR_XYR_CONTROL.toByte(), x.toByte(), y.toByte(), r.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
}
fun onReceivePacket(packet: ByteArray) {
// 添加接收日志
_logs.value = _logs.value + LogEntry(
direction = LogDirection.RECEIVE,
data = packet.joinToString(" ") { "0x%02X".format(it) }
)
// 判断数据包格式时使用 toUByte()
if (packet[0].toUByte() == CarCommand.PACKET_R_HEAD.toUByte() &&
packet.size == packet[1].toUByte().toInt() &&
packet[packet[1].toUByte().toInt() - 1].toUByte() == CarCommand.PACKET_R_TAIL.toUByte()) {
val command = packet[2].toUByte()
val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1)
Log.d("CarController", "command: 0x%02X".format(command.toInt() and 0xFF))
// 解析数据包
when (command.toUInt()) {
// `01 0C E0 电机A_IN_1_2 电机A_PWM 电机B_IN_1_2 电机B_PWM 电机C_IN_1_2 电机C_PWM 电机D_IN_1_2 电机D_PWM FE`
CarCommand.CMD_STATUS_MOTOR -> {
Log.d("CarController", "status motor data: ${data.joinToString(" ") { "0x%02X".format(it) }}")
// 创建新的状态对象
_carState.value = _carState.value.copy(
motorAState = MotorState(
pwm = data[1].toUByte().toUInt(),
in1 = data[0].toUByte().toUInt() and 1u,
in2 = (data[0].toUByte().toUInt() shr 1) and 1u
),
motorBState = MotorState(
pwm = data[3].toUByte().toUInt(),
in1 = data[2].toUByte().toUInt() and 1u,
in2 = (data[2].toUByte().toUInt() shr 1) and 1u
),
motorCState = MotorState(
pwm = data[5].toUByte().toUInt(),
in1 = data[4].toUByte().toUInt() and 1u,
in2 = (data[4].toUByte().toUInt() shr 1) and 1u
),
motorDState = MotorState(
pwm = data[7].toUByte().toUInt(),
in1 = data[6].toUByte().toUInt() and 1u,
in2 = (data[6].toUByte().toUInt() shr 1) and 1u
)
)
}
}
}
}
// 清除日志
fun clearLogs() {
_logs.value = emptyList()
}
}
class CarControlState {
public var speed: UInt = 0u
public var direction: UInt = 0u
}

View File

@ -1,6 +1,7 @@
package icu.fur93.esp32_car
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
@ -11,7 +12,6 @@ import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@ -22,22 +22,16 @@ import android.util.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@ -45,50 +39,29 @@ import icu.fur93.esp32_car.page.ControlGamepadModePage
import icu.fur93.esp32_car.page.ControlPage
import icu.fur93.esp32_car.page.ControlPathfinderModePage
import icu.fur93.esp32_car.page.ControlSingleJoystickModePage
import icu.fur93.esp32_car.page.GamepadScreen
import icu.fur93.esp32_car.page.HomePage
import icu.fur93.esp32_car.page.SettingsPage
import icu.fur93.esp32_car.ui.component.Joystick
import icu.fur93.esp32_car.ui.component.JoystickState
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import icu.fur93.esp32_car.viewmodel.CarControlUseCase
import icu.fur93.esp32_car.viewmodel.CarViewModel
import java.util.UUID
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : ComponentActivity() {
private val serviceUUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
private val rxCharUUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
private val txCharUUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
private var bleScanner: BluetoothLeScanner? = null
// 将deviceList移动到Activity作用域内并使用MutableList
private val deviceList = mutableListOf<ScanResult>()
// 添加可观察的连接状态
private val _isConnected = mutableStateOf(false)
val isConnected: State<Boolean> = _isConnected
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
Log.d("MainActivity", "onScanResult: ${result.device.name} - ${result.device.address}")
if (!deviceList.any { it.device.address == result.device.address }) {
deviceList.add(result)
}
private val bluetoothRepository by lazy {
BluetoothRepositoryImpl(this, lifecycleScope)
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Log.e("MainActivity", "扫描失败,错误码: $errorCode")
}
private val carControlUseCase by lazy {
CarControlUseCase(bluetoothRepository)
}
private val carController by lazy {
CarController { }
private val viewModel by viewModels<CarViewModel> {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return CarViewModel(carControlUseCase, bluetoothRepository) as T
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -100,18 +73,6 @@ class MainActivity : ComponentActivity() {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
// window.statusBarColor = ContextCompat
//
// // 隐藏状态栏和导航栏
// WindowInsetsControllerCompat(window, window.decorView).let { controller ->
// controller.hide(WindowInsetsCompat.Type.systemBars())
// controller.systemBarsBehavior =
// WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// }
//
// // 设置横屏
// requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
// 检查并请求所有必要的权限
val requiredPermissions = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
@ -130,158 +91,15 @@ class MainActivity : ComponentActivity() {
missingPermissions.toTypedArray(),
BLUETOOTH_PERMISSION_REQUEST_CODE
)
} else {
// 如果已经有所有权限,直接开始扫描
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
bleScanner = bluetoothAdapter?.bluetoothLeScanner
startBleScan()
}
setContent {
Esp32carTheme {
// GamepadScreen(
// deviceList = deviceList,
// isConnected = isConnected.value,
// onStartScan = ::startBleScan,
// onConnect = ::connectToDevice,
// carController = carController
// )
App()
App(viewModel)
}
}
}
private fun connectToDevice(result: ScanResult) {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
BLUETOOTH_PERMISSION_REQUEST_CODE
)
return
}
result.device.connectGatt(this, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
when (newState) {
BluetoothGatt.STATE_CONNECTED -> {
carController.bluetoothGatt = gatt
_isConnected.value = true
gatt?.discoverServices()
}
BluetoothGatt.STATE_DISCONNECTED -> {
_isConnected.value = false
carController.bluetoothGatt = null
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt?.getService(UUID.fromString(serviceUUID))?.let { service ->
carController.bluetoothGatt = gatt
carController.rxCharacteristic =
service.getCharacteristic(UUID.fromString(rxCharUUID))
// 注册通知监听器
val txCharacteristic =
service.getCharacteristic(UUID.fromString(txCharUUID))
gatt.setCharacteristicNotification(txCharacteristic, true)
txCharacteristic.descriptors.forEach { descriptor ->
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
}
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
carController.onReceivePacket(value)
}
}
})
}
private fun startBleScan() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.ACCESS_FINE_LOCATION
),
BLUETOOTH_PERMISSION_REQUEST_CODE
)
return
}
// 先停止当前扫描
try {
bleScanner?.stopScan(scanCallback)
} catch (e: Exception) {
Log.e("MainActivity", "停止扫描失败: ${e.message}")
}
// 清空之前的扫描结果
deviceList.clear()
Log.d("MainActivity", "开始扫描...")
// 添加UUID过滤器
val scanFilter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString(serviceUUID))
.build()
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
try {
// bleScanner?.startScan(listOf(scanFilter), scanSettings, scanCallback)
bleScanner?.startScan(scanCallback)
} catch (e: Exception) {
Log.e("MainActivity", "开始扫描失败: ${e.message}")
}
// 10秒后停止扫描
Handler(Looper.getMainLooper()).postDelayed({
stopBleScan()
Log.d("MainActivity", "停止扫描,发现 ${deviceList.size} 个设备")
}, 10000)
}
private fun stopBleScan() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED
) {
return
}
try {
bleScanner?.stopScan(scanCallback)
} catch (e: Exception) {
Log.e("MainActivity", "停止扫描失败: ${e.message}")
}
}
// 添加权限请求结果处理
override fun onRequestPermissionsResult(
requestCode: Int,
@ -292,28 +110,19 @@ class MainActivity : ComponentActivity() {
if (requestCode == BLUETOOTH_PERMISSION_REQUEST_CODE) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
Log.d("MainActivity", "所有权限已获得")
// 权限获得后重新初始化蓝牙
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
bleScanner = bluetoothAdapter?.bluetoothLeScanner
} else {
Log.e("MainActivity", "部分权限未获得")
}
}
}
override fun onDestroy() {
super.onDestroy()
// 确保在Activity销毁时停止扫描
stopBleScan()
}
companion object {
private const val BLUETOOTH_PERMISSION_REQUEST_CODE = 1
}
}
@Composable
fun App() {
fun App(viewModel: CarViewModel) {
val activity = LocalContext.current as MainActivity
activity.window.statusBarColor = MaterialTheme.colorScheme.background.toArgb()
@ -323,9 +132,9 @@ fun App() {
navController = navController,
startDestination = Route.Home.route
) {
composable(Route.Home.route) { HomePage(navController) }
composable(Route.Control.route) { ControlPage(navController) }
composable(Route.Settings.route) { SettingsPage(navController) }
composable(Route.Home.route) { HomePage(navController, viewModel) }
composable(Route.Control.route) { ControlPage(navController, viewModel) }
composable(Route.Settings.route) { SettingsPage(navController, viewModel) }
composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) }
composable(Route.ControlSingleJoystickMode.route) { ControlSingleJoystickModePage(navController) }
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) }

View File

@ -0,0 +1,18 @@
package icu.fur93.esp32_car.entity
import android.bluetooth.BluetoothDevice
data class BleDevice(
val device: BluetoothDevice,
val rssi: Int,
val scanRecord: ByteArray? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BleDevice
return device.address == other.device.address
}
override fun hashCode(): Int = device.address.hashCode()
}

View File

@ -0,0 +1,7 @@
package icu.fur93.esp32_car.entity
data class ConnectedDeviceInfo(
val name: String = "",
val address: String = "",
val version: String = ""
)

View File

@ -3,8 +3,9 @@ package icu.fur93.esp32_car.page
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
@ -12,20 +13,29 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.repository.ConnectionState
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
import icu.fur93.esp32_car.ui.component.CardButtonGroup
import icu.fur93.esp32_car.ui.component.CardButtonItem
import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.layout.MainLayout
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
import icu.fur93.esp32_car.viewmodel.CarViewModel
@Composable
fun ControlPage(navController: NavHostController) {
fun ControlPage(navController: NavHostController, viewModel: CarViewModel) {
val connectionState by viewModel.connectionState.collectAsState()
MainLayout(
content = {
Column (LayoutContentModifier) {
PageTitle("控制")
if (connectionState == ConnectionState.CONNECTED) {
ControlPageStatusCard()
} else {
UnconnectedStatusCard(viewModel)
}
}
},
bottomBar = {

View File

@ -1,11 +1,12 @@
package icu.fur93.esp32_car.page
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.repository.ConnectionState
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
import icu.fur93.esp32_car.ui.carditem.StatusCardInfraredStatus
@ -16,15 +17,21 @@ import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.component.StatusCard
import icu.fur93.esp32_car.ui.layout.MainLayout
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
import icu.fur93.esp32_car.viewmodel.CarViewModel
@Composable
fun HomePage(navController: NavHostController) {
fun HomePage(navController: NavHostController, viewModel: CarViewModel) {
val connectionState by viewModel.connectionState.collectAsState()
MainLayout(
content = {
Column (LayoutContentModifier) {
Column(LayoutContentModifier) {
PageTitle("标题还没想好")
HomePageStatusCard()
// UnconnectedStatusCard()
if (connectionState == ConnectionState.CONNECTED) {
HomePageStatusCard(viewModel)
} else {
UnconnectedStatusCard(viewModel)
}
}
},
bottomBar = {
@ -37,14 +44,16 @@ fun HomePage(navController: NavHostController) {
}
@Composable
fun HomePageStatusCard() {
fun HomePageStatusCard(viewModel: CarViewModel) {
val deviceInfo by viewModel.deviceInfo.collectAsState()
val carState by viewModel.carState.collectAsState()
StatusCard(
cardItems = listOf(
StatusCardInfo(),
StatusCardMotorStatus(),
StatusCardInfraredStatus(),
StatusCardUltrasoundStatus()
StatusCardInfo(deviceInfo),
StatusCardMotorStatus(carState),
StatusCardInfraredStatus(carState.infraredState),
StatusCardUltrasoundStatus(carState.ultrasoundState)
)
)
}

View File

@ -29,8 +29,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.CarController
import icu.fur93.esp32_car.LogDirection
import icu.fur93.esp32_car.ui.component.Joystick
import icu.fur93.esp32_car.ui.component.JoystickState
import kotlinx.coroutines.delay

View File

@ -1,27 +1,39 @@
package icu.fur93.esp32_car.page
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
import icu.fur93.esp32_car.repository.ConnectionState
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
import icu.fur93.esp32_car.ui.layout.MainLayout
import icu.fur93.esp32_car.ui.theme.LayoutContentPadding
import icu.fur93.esp32_car.ui.theme.LayoutTopPadding
import icu.fur93.esp32_car.viewmodel.CarViewModel
@Composable
fun SettingsPage(navController: NavHostController) {
fun SettingsPage(navController: NavHostController, viewModel: CarViewModel) {
MainLayout(
content = {
Column {
@ -34,7 +46,7 @@ fun SettingsPage(navController: NavHostController) {
) {
PageTitle("设置")
}
SettingsList()
SettingsList(viewModel)
}
},
bottomBar = {
@ -47,16 +59,10 @@ fun SettingsPage(navController: NavHostController) {
}
@Composable
fun SettingsList() {
fun SettingsList(viewModel: CarViewModel) {
LazyColumn {
item {
ListItem(
headlineContent = { Text("连接设备") },
supportingContent = { Text("已连接") },
trailingContent = {
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "连接设备")
}
)
SettingConnectDeviceItem(viewModel)
}
item {
ListItem(
@ -80,5 +86,74 @@ fun SettingsList() {
}
)
}
item {
ListItem(
headlineContent = { Text("PID 参数") },
trailingContent = {
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "PID 参数")
}
)
}
}
}
@Composable
fun SettingConnectDeviceItem(viewModel: CarViewModel) {
var showScanDialog by remember { mutableStateOf(false) }
var showDisconnectDialog by remember { mutableStateOf(false) }
val connectionState by viewModel.connectionState.collectAsState()
val deviceInfo by viewModel.deviceInfo.collectAsState()
ListItem(
headlineContent = { Text("连接设备") },
supportingContent = { if (connectionState == ConnectionState.CONNECTED) Text("已连接") else Text("未连接") },
trailingContent = {
Icon(
ImageVector.vectorResource(R.drawable.arrow_right_24),
contentDescription = "连接设备"
)
},
modifier = Modifier.clickable {
if (connectionState == ConnectionState.CONNECTED) {
showDisconnectDialog = true
} else if (connectionState == ConnectionState.DISCONNECTED) {
showScanDialog = true
}
}
)
// 扫描设备对话框
BleDeviceScanDialog(
isVisible = showScanDialog,
onDismiss = { showScanDialog = false },
viewModel = viewModel
)
// 断开连接确认对话框
if (showDisconnectDialog) {
AlertDialog(
onDismissRequest = { showDisconnectDialog = false },
title = { Text("断开连接") },
text = { Text("确定要断开与 ${deviceInfo.name} 的连接吗?") },
confirmButton = {
TextButton(
onClick = {
(viewModel.repository as BluetoothRepositoryImpl).disconnect()
showDisconnectDialog = false
}
) {
Text("确定")
}
},
dismissButton = {
TextButton(
onClick = { showDisconnectDialog = false }
) {
Text("取消")
}
}
)
}
}

View File

@ -0,0 +1,245 @@
package icu.fur93.esp32_car.repository
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import icu.fur93.esp32_car.entity.BleDevice
import icu.fur93.esp32_car.viewmodel.CarCommands
import icu.fur93.esp32_car.viewmodel.CarState
import icu.fur93.esp32_car.viewmodel.LogDirection
import icu.fur93.esp32_car.viewmodel.LogEntry
import icu.fur93.esp32_car.viewmodel.MotorState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
// 蓝牙通信接口
interface BluetoothRepository {
fun sendCommand(command: ByteArray)
fun observeCarState(): Flow<CarState>
fun observeLogs(): Flow<List<LogEntry>>
fun clearLogs()
}
// 添加连接状态枚举
enum class ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
DISCONNECTING
}
// 蓝牙通信实现
class BluetoothRepositoryImpl(
private val context: Context,
private val scope: CoroutineScope
) : BluetoothRepository {
private var bluetoothGatt: BluetoothGatt? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null
private val _carState = MutableStateFlow(CarState())
private val _logs = MutableStateFlow<List<LogEntry>>(emptyList())
private val bluetoothAdapter: BluetoothAdapter? =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
private val _scanResults = MutableStateFlow<List<BleDevice>>(emptyList())
val scanResults: StateFlow<List<BleDevice>> = _scanResults.asStateFlow()
private val _isScanning = MutableStateFlow(false)
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
// 连接状态流
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val bleDevice = BleDevice(
device = result.device,
rssi = result.rssi,
scanRecord = result.scanRecord?.bytes
)
val currentList = _scanResults.value.toMutableList()
val index = currentList.indexOfFirst { it.device.address == bleDevice.device.address }
if (index >= 0) {
currentList[index] = bleDevice
} else {
currentList.add(bleDevice)
}
// 按信号强度排序
_scanResults.value = currentList.sortedByDescending { it.rssi }
}
}
@SuppressLint("MissingPermission")
fun startScan() {
if (_isScanning.value) return
_scanResults.value = emptyList()
_isScanning.value = true
val scanner = bluetoothAdapter?.bluetoothLeScanner
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
scanner?.startScan(null, settings, scanCallback)
// 10秒后自动停止扫描
scope.launch {
delay(10000)
stopScan()
}
}
@SuppressLint("MissingPermission")
fun stopScan() {
if (!_isScanning.value) return
_isScanning.value = false
bluetoothAdapter?.bluetoothLeScanner?.stopScan(scanCallback)
}
@SuppressLint("MissingPermission")
fun connectToDevice(device: BluetoothDevice) {
_connectionState.value = ConnectionState.CONNECTING
device.connectGatt(
context,
false,
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
bluetoothGatt = gatt
_connectionState.value = ConnectionState.CONNECTED
scope.launch(Dispatchers.Main) {
gatt.discoverServices()
}
}
BluetoothProfile.STATE_CONNECTING -> {
_connectionState.value = ConnectionState.CONNECTING
}
BluetoothProfile.STATE_DISCONNECTING -> {
_connectionState.value = ConnectionState.DISCONNECTING
}
BluetoothProfile.STATE_DISCONNECTED -> {
bluetoothGatt = null
rxCharacteristic = null
_connectionState.value = ConnectionState.DISCONNECTED
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 找到特定的服务和特征
gatt.services?.forEach { service ->
service.characteristics?.forEach { characteristic ->
// 这里需要根据你的设备具体的UUID来匹配
if (characteristic.uuid.toString() == "YOUR_CHARACTERISTIC_UUID") {
rxCharacteristic = characteristic
}
}
}
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
onReceivePacket(value)
}
}
)
}
@SuppressLint("MissingPermission")
override fun sendCommand(command: ByteArray) {
rxCharacteristic?.let { characteristic ->
characteristic.value = command
bluetoothGatt?.writeCharacteristic(characteristic)
addLog(LogDirection.SEND, command)
}
}
override fun observeCarState(): Flow<CarState> = _carState.asStateFlow()
override fun observeLogs(): Flow<List<LogEntry>> = _logs.asStateFlow()
override fun clearLogs() {
_logs.value = emptyList()
}
private fun addLog(direction: LogDirection, data: ByteArray) {
_logs.value += LogEntry(
direction = direction,
data = data.joinToString(" ") { "0x%02X".format(it) }
)
}
fun onReceivePacket(packet: ByteArray) {
addLog(LogDirection.RECEIVE, packet)
parsePacket(packet)
}
private fun parsePacket(packet: ByteArray) {
if (!isValidPacket(packet)) return
when (packet[2].toUByte().toUInt()) {
CarCommands.CMD_STATUS_MOTOR -> updateMotorStatus(packet)
}
}
private fun isValidPacket(packet: ByteArray): Boolean =
packet[0].toUByte() == CarCommands.PACKET_R_HEAD.toUByte() &&
packet.size == packet[1].toUByte().toInt() &&
packet[packet[1].toUByte().toInt() - 1].toUByte() == CarCommands.PACKET_R_TAIL.toUByte()
private fun updateMotorStatus(packet: ByteArray) {
val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1)
_carState.value = _carState.value.copy(
motorAState = createMotorState(data, 0),
motorBState = createMotorState(data, 2),
motorCState = createMotorState(data, 4),
motorDState = createMotorState(data, 6)
)
}
private fun createMotorState(data: ByteArray, offset: Int) = MotorState(
pwm = data[offset + 1].toUByte().toUInt(),
in1 = data[offset].toUByte().toUInt() and 1u,
in2 = (data[offset].toUByte().toUInt() shr 1) and 1u
)
// 断开连接方法
@SuppressLint("MissingPermission")
fun disconnect() {
bluetoothGatt?.let { gatt ->
_connectionState.value = ConnectionState.DISCONNECTING
gatt.disconnect()
}
}
}

View File

@ -3,12 +3,20 @@ package icu.fur93.esp32_car.ui.card
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCard
import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
import icu.fur93.esp32_car.viewmodel.CarViewModel
@Composable
fun UnconnectedStatusCard() {
fun UnconnectedStatusCard(viewModel: CarViewModel) {
var showScanDialog by remember { mutableStateOf(false) }
StatusCard(
cardItems = listOf(
CardItem(
@ -21,11 +29,17 @@ fun UnconnectedStatusCard() {
bottomControl = {
Button(
onClick = {
showScanDialog = true
}
) {
Text("连接设备")
}
}
)
BleDeviceScanDialog(
isVisible = showScanDialog,
onDismiss = { showScanDialog = false },
viewModel = viewModel
)
}

View File

@ -2,18 +2,19 @@ package icu.fur93.esp32_car.ui.carditem
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import icu.fur93.esp32_car.entity.ConnectedDeviceInfo
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText
@SuppressLint("ComposableNaming")
@Composable
fun StatusCardInfo () : CardItem {
fun StatusCardInfo(deviceInfo: ConnectedDeviceInfo) : CardItem {
return CardItem(
title = "当前连接",
content = {
StatusCardItemText("名称 Name")
StatusCardItemText("Mac XX:XX:XX:XX:XX:XX")
StatusCardItemText("版本 001")
StatusCardItemText("名称 ${deviceInfo.name}")
StatusCardItemText("Mac ${deviceInfo.address}")
StatusCardItemText("版本 ${deviceInfo.version}")
}
)
}

View File

@ -8,21 +8,26 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.viewmodel.InfraredState
@SuppressLint("ComposableNaming")
@Composable
fun StatusCardInfraredStatus () : CardItem {
fun StatusCardInfraredStatus(infraredState: InfraredState): CardItem {
return CardItem(
title = "红外模块状态",
content = {
if (infraredState.enable) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
listOf(0, 0, 0, 0, 0).forEach { status ->
infraredState.statusList.forEach { status ->
StatusCardItemText(status.toString())
}
}
} else {
StatusCardItemText("未启用")
}
}
)
}

View File

@ -8,23 +8,28 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.viewmodel.UltrasoundState
@SuppressLint("ComposableNaming")
@Composable
fun StatusCardUltrasoundStatus () : CardItem {
fun StatusCardUltrasoundStatus(ultrasoundState: UltrasoundState): CardItem {
return CardItem(
title = "超声波状态",
content = {
if (ultrasoundState.enable) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Column(Modifier.weight(1f)) {
StatusCardItemText("舵机角度 90")
StatusCardItemText("舵机角度 ${ultrasoundState.servoAngle}")
}
Column(Modifier.weight(1f)) {
StatusCardItemText("舵机角度 20mm")
StatusCardItemText("障碍距离 ${ultrasoundState.distance}")
}
}
} else {
StatusCardItemText("未启用")
}
}
)
}

View File

@ -6,10 +6,11 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.viewmodel.CarState
@SuppressLint("ComposableNaming")
@Composable
fun StatusCardMotorStatus () : CardItem {
fun StatusCardMotorStatus (carState: CarState) : CardItem {
return CardItem(
title = "电机状态",
content = {
@ -17,16 +18,16 @@ fun StatusCardMotorStatus () : CardItem {
columns = GridCells.Fixed(2)
) {
item {
StatusCardItemText("A 0 1 255")
StatusCardItemText("A ${carState.motorAState.in1} ${carState.motorAState.in2} ${carState.motorAState.pwm}")
}
item {
StatusCardItemText("D 0 1 255")
StatusCardItemText("D ${carState.motorDState.in1} ${carState.motorDState.in2} ${carState.motorDState.pwm}")
}
item {
StatusCardItemText("B 1 0 255")
StatusCardItemText("B ${carState.motorBState.in1} ${carState.motorBState.in2} ${carState.motorBState.pwm}")
}
item {
StatusCardItemText("C 1 0 255")
StatusCardItemText("C ${carState.motorCState.in1} ${carState.motorCState.in2} ${carState.motorCState.pwm}")
}
}
}

View File

@ -0,0 +1,118 @@
package icu.fur93.esp32_car.ui.dialog
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.entity.BleDevice
import icu.fur93.esp32_car.viewmodel.CarViewModel
@Composable
fun BleDeviceScanDialog(
isVisible: Boolean,
onDismiss: () -> Unit,
viewModel: CarViewModel
) {
val scanResults by viewModel.scanResults.collectAsState()
val isScanning by viewModel.isScanning.collectAsState()
if (isVisible) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("扫描蓝牙设备")
if (isScanning) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
}
}
},
text = {
LazyColumn {
scanResults.forEach { bleDevice ->
item {
DeviceItem(
bleDevice = bleDevice,
onClick = {
viewModel.connectToDevice(bleDevice.device)
onDismiss()
}
)
}
}
}
},
confirmButton = {
TextButton(
onClick = {
if (isScanning) viewModel.stopScan()
else viewModel.startScan()
}
) {
Text(if (isScanning) "停止扫描" else "开始扫描")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消")
}
}
)
}
}
@SuppressLint("MissingPermission")
@Composable
private fun DeviceItem(
bleDevice: BleDevice,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = bleDevice.device.name ?: "Unknown Device",
style = MaterialTheme.typography.bodyLarge
)
Text(
text = bleDevice.device.address,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = "${bleDevice.rssi} dBm",
style = MaterialTheme.typography.bodyMedium
)
}
}

View File

@ -0,0 +1,195 @@
package icu.fur93.esp32_car.viewmodel
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import icu.fur93.esp32_car.entity.BleDevice
import icu.fur93.esp32_car.entity.ConnectedDeviceInfo
import icu.fur93.esp32_car.repository.BluetoothRepository
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
import icu.fur93.esp32_car.repository.ConnectionState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
// 数据模型
data class MotorState(
val pwm: UInt = 0u,
val in1: UInt = 0u,
val in2: UInt = 0u
)
data class InfraredState(
val enable: Boolean = false,
val statusList: List<UInt> = listOf()
)
data class UltrasoundState(
val enable: Boolean = false,
val servoAngle: UInt = 0u,
val distance: UInt = 0u
)
data class CarState(
val controlState: CarControlState = CarControlState(),
val motorAState: MotorState = MotorState(),
val motorBState: MotorState = MotorState(),
val motorCState: MotorState = MotorState(),
val motorDState: MotorState = MotorState(),
val infraredState: InfraredState = InfraredState(),
val ultrasoundState: UltrasoundState = UltrasoundState()
)
data class CarControlState(
val speed: UInt = 0u,
val direction: UInt = 0u
)
data class LogEntry(
val timestamp: Long = System.currentTimeMillis(),
val direction: LogDirection,
val data: String
)
enum class LogDirection {
SEND,
RECEIVE
}
// 命令常量
object CarCommands {
const val PACKET_T_HEAD = 0x00u
const val PACKET_T_TAIL = 0xFFu
const val PACKET_R_HEAD = 0x01u
const val PACKET_R_TAIL = 0xFEu
const val PACKET_MAX_LENGTH = 32u
const val CMD_GET_BT_STATUS = 0x10u
const val CMD_GET_SPIFFS_STATUS = 0x11u
const val CMD_GET_DISTANCE = 0x12u
const val CMD_MOTOR_MOVE_CONTROL = 0x20u
const val CMD_MOTOR_STEER_CONTROL = 0x21u
const val CMD_MOTOR_SINGLE_CONTROL = 0x22u
const val CMD_MOTOR_ROTATE_CONTROL = 0x23u
const val CMD_MOTOR_XYR_CONTROL = 0x24u
const val CMD_DEMO_PID = 0xf0u
const val CMD_DEMO_PATH = 0xf1u
const val CMD_STATUS_MOTOR = 0xE0u
}
// 用例
class CarControlUseCase(
private val repository: BluetoothRepository
) {
fun moveForward(speed: Int = 255) = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x01, speed)
)
fun moveBackward(speed: Int = 255) = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x02, speed)
)
fun turnLeft(speed: Int = 255) = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x00, speed)
)
fun turnRight(speed: Int = 255) = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x01, speed)
)
fun stop() = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x00, 0x00)
)
fun sendXYR(x: Int, y: Int, r: Int) = repository.sendCommand(
byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(),
0x07,
CarCommands.CMD_MOTOR_XYR_CONTROL.toByte(),
x.toByte(),
y.toByte(),
r.toByte(),
CarCommands.PACKET_T_TAIL.toByte()
)
)
private fun buildCommand(cmd: UInt, param1: Int, param2: Int) = byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(),
0x06,
cmd.toByte(),
param1.toByte(),
param2.toByte(),
CarCommands.PACKET_T_TAIL.toByte()
)
}
class CarViewModel(
private val carControlUseCase: CarControlUseCase,
val repository: BluetoothRepository
) : ViewModel() {
val scanResults: StateFlow<List<BleDevice>> =
(repository as BluetoothRepositoryImpl).scanResults
val isScanning: StateFlow<Boolean> =
(repository as BluetoothRepositoryImpl).isScanning
fun startScan() = (repository as BluetoothRepositoryImpl).startScan()
fun stopScan() = (repository as BluetoothRepositoryImpl).stopScan()
fun connectToDevice(device: BluetoothDevice) {
updateDeviceInfo(device)
(repository as BluetoothRepositoryImpl).connectToDevice(device)
}
val carState: StateFlow<CarState> = repository.observeCarState()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CarState())
val logs: StateFlow<List<LogEntry>> = repository.observeLogs()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
// 设备信息状态
private val _deviceInfo = MutableStateFlow(ConnectedDeviceInfo())
val deviceInfo: StateFlow<ConnectedDeviceInfo> = _deviceInfo.asStateFlow()
val connectionState: StateFlow<ConnectionState> =
(repository as BluetoothRepositoryImpl).connectionState
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ConnectionState.DISCONNECTED
)
init {
// 监听连接状态变化
viewModelScope.launch {
connectionState.collect { state ->
if (state == ConnectionState.DISCONNECTED) {
// 断开连接时清空设备信息
_deviceInfo.value = ConnectedDeviceInfo()
}
}
}
}
// 更新设备信息的方法
@SuppressLint("MissingPermission")
fun updateDeviceInfo(device: BluetoothDevice) {
_deviceInfo.value = ConnectedDeviceInfo(
name = device.name ?: "未知设备",
address = device.address,
version = "001"
)
}
fun moveForward(speed: Int = 255) = carControlUseCase.moveForward(speed)
fun moveBackward(speed: Int = 255) = carControlUseCase.moveBackward(speed)
fun turnLeft(speed: Int = 255) = carControlUseCase.turnLeft(speed)
fun turnRight(speed: Int = 255) = carControlUseCase.turnRight(speed)
fun stop() = carControlUseCase.stop()
fun sendXYR(x: Int, y: Int, r: Int) = carControlUseCase.sendXYR(x, y, r)
fun clearLogs() = repository.clearLogs()
}

View File

@ -1,2 +0,0 @@
package icu.fur93.esp32_car.viewmodel