This commit is contained in:
玖叁 2025-01-02 02:19:16 +08:00
parent 33bd3eed44
commit 4e977f496b
15 changed files with 469 additions and 135 deletions

View File

@ -117,15 +117,15 @@ class MainActivity : ComponentActivity() {
) )
} }
// 检查权限后尝试自动连接 // // 检查权限后尝试自动连接
lifecycleScope.launch { // lifecycleScope.launch {
preferencesDataStore.getLastConnectedDevice().collect { address -> // preferencesDataStore.getLastConnectedDevice().collect { address ->
if (address != null) { // if (address != null) {
viewModel.connectToDeviceByAddress(address) // viewModel.connectToDeviceByAddress(address)
} // }
} // }
} // }
setContent { setContent {
Esp32carTheme { Esp32carTheme {

View File

@ -11,6 +11,8 @@ object CarCommands {
const val CMD_GET_SPIFFS_STATUS = 0x11u const val CMD_GET_SPIFFS_STATUS = 0x11u
const val CMD_GET_DISTANCE = 0x12u const val CMD_GET_DISTANCE = 0x12u
const val CMD_SET_MODE = 0x30u
const val CMD_SET_NAME = 0xa1u const val CMD_SET_NAME = 0xa1u
const val CMD_SET_PID = 0xa2u const val CMD_SET_PID = 0xa2u

View File

@ -0,0 +1,6 @@
package icu.fur93.esp32_car.const
object RunModeConsts {
const val MODE_MANUAL = 0x00u
const val MODE_TRACKING = 0x01u
}

View File

@ -2,6 +2,7 @@ package icu.fur93.esp32_car.entity.car
data class CarState( data class CarState(
val controlState: CarControlState = CarControlState(), val controlState: CarControlState = CarControlState(),
val runModeState: RunModeState = RunModeState(),
val motorAState: MotorState = MotorState(), val motorAState: MotorState = MotorState(),
val motorBState: MotorState = MotorState(), val motorBState: MotorState = MotorState(),
val motorCState: MotorState = MotorState(), val motorCState: MotorState = MotorState(),

View File

@ -0,0 +1,7 @@
package icu.fur93.esp32_car.entity.car
import icu.fur93.esp32_car.const.RunModeConsts
data class RunModeState(
val runMode: UInt = RunModeConsts.MODE_MANUAL,
)

View File

@ -3,5 +3,5 @@ package icu.fur93.esp32_car.entity.car
data class UltrasoundState( data class UltrasoundState(
val enable: Boolean = false, val enable: Boolean = false,
val servoAngle: UInt = 0u, val servoAngle: UInt = 0u,
val distance: UInt = 0u val distance: Float = 0f
) )

View File

@ -46,7 +46,7 @@ fun ControlPageStatusCard() {
@Composable @Composable
fun ControlPageAutoControl() { fun ControlPageAutoControl() {
val context = LocalContext.current val navController = LocalNavigation.current
return CardButtonGroup( return CardButtonGroup(
title = "自动控制", title = "自动控制",
buttons = listOf( buttons = listOf(
@ -54,7 +54,7 @@ fun ControlPageAutoControl() {
text = Route.ControlPathfinderMode.title, text = Route.ControlPathfinderMode.title,
icon = ImageVector.vectorResource(R.drawable.outline_route_24), icon = ImageVector.vectorResource(R.drawable.outline_route_24),
onClick = { onClick = {
Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show() navController.navigate(Route.ControlPathfinderMode.route)
} }
) )
) )

View File

@ -1,8 +1,56 @@
package icu.fur93.esp32_car.page package icu.fur93.esp32_car.page
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
import icu.fur93.esp32_car.viewmodel.CarViewModel import icu.fur93.esp32_car.viewmodel.CarViewModel
import icu.fur93.esp32_car.ui.carditem.StatusCardInfraredStatus
import icu.fur93.esp32_car.ui.carditem.StatusCardMotorStatus
import icu.fur93.esp32_car.ui.carditem.StatusCardPathfinderStatus
import icu.fur93.esp32_car.ui.component.StatusCard
import icu.fur93.esp32_car.const.RunModeConsts
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import icu.fur93.esp32_car.const.CarCommands
import icu.fur93.esp32_car.ui.carditem.StatusCardUltrasoundStatus
@Composable @Composable
fun ControlPathfinderModePage(viewModel: CarViewModel) { fun ControlPathfinderModePage(viewModel: CarViewModel) {
Column(LayoutContentModifier) {
PageTitle(Route.ControlPathfinderMode.title)
ControlPathfinderModeStatusCard(viewModel)
}
}
@Composable
fun ControlPathfinderModeStatusCard(viewModel: CarViewModel) {
val deviceInfo by viewModel.connectionInfoState.collectAsState()
val carState by viewModel.carState.collectAsState()
StatusCard(
cardItems = listOf(
StatusCardInfo(deviceInfo),
StatusCardMotorStatus(carState),
StatusCardInfraredStatus(carState.infraredState),
StatusCardUltrasoundStatus(carState.ultrasoundState),
StatusCardPathfinderStatus(carState.runModeState)
),
bottomControl = {
Button(onClick = {
if (carState.runModeState.runMode == RunModeConsts.MODE_TRACKING) {
viewModel.setRunMode(RunModeConsts.MODE_MANUAL.toInt())
} else {
viewModel.setRunMode(RunModeConsts.MODE_TRACKING.toInt())
}
}) {
Text(if (carState.runModeState.runMode == RunModeConsts.MODE_TRACKING) "停止" else "开始")
}
}
)
} }

View File

@ -10,6 +10,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -72,55 +75,84 @@ fun ControlSingleJoystickModeControlPanel(
viewModel: CarViewModel, viewModel: CarViewModel,
onJoystickStateChange: (JoystickState) -> Unit onJoystickStateChange: (JoystickState) -> Unit
) { ) {
// 添加Y轴限位模式的状态
var yAxisOnly by remember { mutableStateOf(false) }
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Row( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally
verticalAlignment = Alignment.CenterVertically
) { ) {
FilledIconButton( // 添加开关控件
onClick = {}, Row(
modifier = Modifier.pointerInteropFilter { event -> modifier = Modifier
when (event.action) { .fillMaxWidth()
MotionEvent.ACTION_DOWN -> { .padding(horizontal = 16.dp),
viewModel.sendXYR(0, 0, -50) horizontalArrangement = Arrangement.SpaceBetween,
true verticalAlignment = Alignment.CenterVertically
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) { ) {
Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转") Text(
text = "Y 轴限位模式",
style = MaterialTheme.typography.bodyLarge
)
Switch(
checked = yAxisOnly,
onCheckedChange = { yAxisOnly = it }
)
} }
ControlJoystick( Spacer(Modifier.height(16.dp))
viewModel = viewModel,
onJoystickStateChange = onJoystickStateChange
)
FilledIconButton( // 原有的控制面板布局
onClick = {}, Row(
modifier = Modifier.pointerInteropFilter { event -> modifier = Modifier.fillMaxWidth(),
when (event.action) { horizontalArrangement = Arrangement.SpaceBetween,
MotionEvent.ACTION_DOWN -> { verticalAlignment = Alignment.CenterVertically
viewModel.sendXYR(0, 0, 50)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) { ) {
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转") FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.rotateLeft(100)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转")
}
ControlJoystick(
viewModel = viewModel,
onJoystickStateChange = onJoystickStateChange,
yAxisOnly = yAxisOnly // 传递Y轴限位模式状态
)
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.rotateRight(100)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转")
}
} }
} }
} }
@ -128,7 +160,8 @@ fun ControlSingleJoystickModeControlPanel(
@Composable @Composable
fun ControlJoystick( fun ControlJoystick(
viewModel: CarViewModel, viewModel: CarViewModel,
onJoystickStateChange: (JoystickState) -> Unit onJoystickStateChange: (JoystickState) -> Unit,
yAxisOnly: Boolean = false // 添加参数
) { ) {
// 记住上一次发送的状态和时间 // 记住上一次发送的状态和时间
val lastSentState = remember { mutableStateOf(JoystickState()) } val lastSentState = remember { mutableStateOf(JoystickState()) }
@ -142,7 +175,8 @@ fun ControlJoystick(
Joystick( Joystick(
size = 175.dp, size = 175.dp,
knobScale = 0.25f knobScale = 0.25f,
yAxisOnly = yAxisOnly, // 使用传入的参数
) { state -> ) { state ->
// 更新状态 // 更新状态
onJoystickStateChange(state) onJoystickStateChange(state)

View File

@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import icu.fur93.esp32_car.BuildConfig import icu.fur93.esp32_car.BuildConfig
import icu.fur93.esp32_car.R import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.Route import icu.fur93.esp32_car.Route
@ -63,6 +65,8 @@ fun SettingsList(viewModel: CarViewModel) {
val context = LocalContext.current val context = LocalContext.current
var debugMode by remember { mutableStateOf(DebugWindowManager.isShowing()) } var debugMode by remember { mutableStateOf(DebugWindowManager.isShowing()) }
var showSensitivityDialog by remember { mutableStateOf(false) }
var showSpeedDialog by remember { mutableStateOf(false) }
val onDebugModeChange = { enable: Boolean -> val onDebugModeChange = { enable: Boolean ->
if (enable) { if (enable) {
@ -94,16 +98,122 @@ fun SettingsList(viewModel: CarViewModel) {
} }
item { item {
ListItem( ListItem(
headlineContent = { Text("PID 参数") }, headlineContent = { Text("循迹灵敏度设置") },
trailingContent = { trailingContent = {
Icon( Icon(
ImageVector.vectorResource(R.drawable.arrow_right_24), ImageVector.vectorResource(R.drawable.arrow_right_24),
contentDescription = "PID 参数" contentDescription = "循迹灵敏度设置"
) )
} },
modifier = Modifier.clickable { showSensitivityDialog = true }
)
ListItem(
headlineContent = { Text("循迹速度设置") },
trailingContent = {
Icon(
ImageVector.vectorResource(R.drawable.arrow_right_24),
contentDescription = "循迹速度设置"
)
},
modifier = Modifier.clickable { showSpeedDialog = true }
) )
} }
} }
// 循迹灵敏度设置对话框
if (showSensitivityDialog) {
var sensitivity by remember { mutableStateOf("3") }
AlertDialog(
onDismissRequest = { showSensitivityDialog = false },
title = { Text("设置循迹灵敏度") },
text = {
Column {
Text("请输入灵敏度值 (1-10)")
androidx.compose.material3.TextField(
value = sensitivity,
onValueChange = {
if (it.isEmpty() || (it.toIntOrNull() in 1..10)) {
sensitivity = it
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
},
confirmButton = {
TextButton(
onClick = {
sensitivity.toIntOrNull()?.let { value ->
viewModel.setPathfinderSensitivity(value)
}
showSensitivityDialog = false
}
) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = { showSensitivityDialog = false }) {
Text("取消")
}
}
)
}
// 循迹速度设置对话框
if (showSpeedDialog) {
var baseSpeed by remember { mutableStateOf("50") }
var turnSpeed by remember { mutableStateOf("50") }
AlertDialog(
onDismissRequest = { showSpeedDialog = false },
title = { Text("设置循迹速度") },
text = {
Column {
Text("请输入基础速度 (0-255)")
androidx.compose.material3.TextField(
value = baseSpeed,
onValueChange = {
if (it.isEmpty() || (it.toIntOrNull() in 0..255)) {
baseSpeed = it
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Text("请输入转弯速度 (0-255)")
androidx.compose.material3.TextField(
value = turnSpeed,
onValueChange = {
if (it.isEmpty() || (it.toIntOrNull() in 0..255)) {
turnSpeed = it
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
},
confirmButton = {
TextButton(
onClick = {
val base = baseSpeed.toIntOrNull()
val turn = turnSpeed.toIntOrNull()
if (base != null && turn != null) {
viewModel.setPathfinderSpeed(base, turn)
}
showSpeedDialog = false
}
) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = { showSpeedDialog = false }) {
Text("取消")
}
}
)
}
} }

View File

@ -33,6 +33,9 @@ import icu.fur93.esp32_car.entity.LogEntry
import icu.fur93.esp32_car.entity.car.CarState import icu.fur93.esp32_car.entity.car.CarState
import icu.fur93.esp32_car.entity.car.InfraredState import icu.fur93.esp32_car.entity.car.InfraredState
import icu.fur93.esp32_car.entity.car.MotorState import icu.fur93.esp32_car.entity.car.MotorState
import icu.fur93.esp32_car.entity.car.RunModeState
import icu.fur93.esp32_car.entity.car.UltrasoundState
import icu.fur93.esp32_car.utils.Utils
// 蓝牙通信接口 // 蓝牙通信接口
interface BluetoothRepository { interface BluetoothRepository {
@ -91,6 +94,48 @@ class BluetoothRepositoryImpl(
private val _deviceAddress = MutableStateFlow<String?>(null) private val _deviceAddress = MutableStateFlow<String?>(null)
override val deviceAddress: StateFlow<String?> = _deviceAddress.asStateFlow() override val deviceAddress: StateFlow<String?> = _deviceAddress.asStateFlow()
private var notificationRetryCount = 0
private val MAX_NOTIFICATION_RETRIES = 3
@SuppressLint("MissingPermission")
private fun enableNotifications(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
scope.launch {
var success = false
var retryCount = 0
while (!success && retryCount < MAX_NOTIFICATION_RETRIES) {
success = try {
gatt.setCharacteristicNotification(characteristic, true)
// 等待一小段时间确保设置生效
delay(100)
// 写入描述符
characteristic.descriptors.forEach { descriptor ->
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
// 等待描述符写入完成
delay(100)
}
true
} catch (e: Exception) {
Log.e("BluetoothRepository", "启用通知失败: ${e.message}")
delay(200) // 失败后等待一段时间再重试
false
}
if (!success) {
retryCount++
Log.d("BluetoothRepository", "重试启用通知 #$retryCount")
}
}
if (!success) {
Log.e("BluetoothRepository", "无法启用通知,已达到最大重试次数")
}
}
}
private val scanCallback = object : ScanCallback() { private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
val bleDevice = BleDevice( val bleDevice = BleDevice(
@ -213,33 +258,45 @@ class BluetoothRepositoryImpl(
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status) super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS && gatt != null) {
// 读取设备名称 // 读取设备名称
val genericService = val genericService = gatt.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"))
gatt?.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb")) val deviceNameChar = genericService?.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"))
val deviceNameChar =
genericService?.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"))
if (deviceNameChar != null) { if (deviceNameChar != null) {
gatt.readCharacteristic(deviceNameChar) gatt.readCharacteristic(deviceNameChar)
} }
gatt?.getService(UUID.fromString(serviceUUID))?.let { service -> gatt.getService(UUID.fromString(serviceUUID))?.let { service ->
bluetoothGatt = gatt bluetoothGatt = gatt
rxCharacteristic = rxCharacteristic = service.getCharacteristic(UUID.fromString(rxCharUUID))
service.getCharacteristic(UUID.fromString(rxCharUUID))
// 注册通知监听器 // 使用新的通知启用方法
val txCharacteristic = val txCharacteristic = service.getCharacteristic(UUID.fromString(txCharUUID))
service.getCharacteristic(UUID.fromString(txCharUUID)) if (txCharacteristic != null) {
gatt.setCharacteristicNotification(txCharacteristic, true) enableNotifications(gatt, txCharacteristic)
txCharacteristic.descriptors.forEach { descriptor ->
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
} }
} }
} }
} }
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorWrite(gatt, descriptor, status)
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e("BluetoothRepository", "描述符写入失败: $status")
// 如果写入失败,尝试重新启用通知
val characteristic = descriptor.characteristic
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
enableNotifications(gatt, characteristic)
}
} else {
Log.d("BluetoothRepository", "描述符写入成功")
}
}
override fun onCharacteristicRead( override fun onCharacteristicRead(
gatt: BluetoothGatt, gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic, characteristic: BluetoothGattCharacteristic,
@ -260,6 +317,7 @@ class BluetoothRepositoryImpl(
value: ByteArray value: ByteArray
) { ) {
super.onCharacteristicChanged(gatt, characteristic, value) super.onCharacteristicChanged(gatt, characteristic, value)
Log.d("onCharacteristicChanged", "characteristic: ${characteristic.uuid}, value: ${value.joinToString(" ")}")
if (characteristic.uuid == UUID.fromString(txCharUUID)) { if (characteristic.uuid == UUID.fromString(txCharUUID)) {
onReceivePacket(value) onReceivePacket(value)
} }
@ -274,46 +332,6 @@ class BluetoothRepositoryImpl(
) )
} }
@SuppressLint("MissingPermission")
fun connectToDeviceByAddress(
address: String,
callback: ((Boolean, BluetoothDevice) -> Unit)? = null
) {
val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val device = bluetoothManager.adapter.getRemoteDevice(address)
if (callback != null) {
connectToDevice(device) { isConnected, deviceDetail ->
callback(isConnected, deviceDetail ?: device)
}
} else {
connectToDevice(device)
}
}
@SuppressLint("MissingPermission")
private fun setupDeviceNameNotification(gatt: BluetoothGatt) {
val service = gatt.getService(UUID.fromString(deviceInfoServiceUUID))
val characteristic = service?.getCharacteristic(UUID.fromString(deviceNameCharUUID))
characteristic?.let { char ->
// 启用通知
gatt.setCharacteristicNotification(char, true)
// 获取 descriptor 并写入通知使能值
val descriptor =
char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor?.let {
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(it)
}
// 同时也读取一次当前值
gatt.readCharacteristic(char)
}
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun sendCommand(command: ByteArray) { override fun sendCommand(command: ByteArray) {
rxCharacteristic?.let { characteristic -> rxCharacteristic?.let { characteristic ->
@ -345,16 +363,17 @@ class BluetoothRepositoryImpl(
if (!isValidPacket(packet)) return if (!isValidPacket(packet)) return
/** /**
包体 `01 0E 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` 包体 `01 0F 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`
| 数据 | 类型 | 表示 | | 数据 | 类型 | 表示 |
| -------- | ---- | ---- | | -------- | ---- | ---- |
| 运行模式 | 无符号整数 | 0x00 手动模式0x01 巡线模式 |
| IN_1_2 | 无符号整数 | 按位表示 IN1 IN2 的值 0000 00 IN2 IN1 | | IN_1_2 | 无符号整数 | 按位表示 IN1 IN2 的值 0000 00 IN2 IN1 |
| PWM | 无符号整数 | - | | PWM | 无符号整数 | - |
| 红外引脚数量 | 无符号整数 | - | | 红外引脚数量 | 无符号整数 | - |
| 红外数据 | 无符号整数 | 按位表示红外循迹模块数据 0001 1111 | | 红外数据 | 无符号整数 | 按位表示红外循迹模块数据 0001 1111 |
示例 `01 0E E0 01 FF 02 FF 02 FF 01 FF 05 1F FE` 示例 `01 0F E0 00 01 FF 02 FF 02 FF 01 FF 05 1F FE`
**/ **/
when (packet[2].toUByte().toUInt()) { when (packet[2].toUByte().toUInt()) {
@ -373,11 +392,22 @@ class BluetoothRepositoryImpl(
private fun updateStatus(packet: ByteArray) { private fun updateStatus(packet: ByteArray) {
val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1) val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1)
_carState.value = _carState.value.copy( _carState.value = _carState.value.copy(
motorAState = createMotorState(data, 0), runModeState = RunModeState(data[0].toUByte().toUInt()),
motorBState = createMotorState(data, 2), motorAState = createMotorState(data, 1),
motorCState = createMotorState(data, 4), motorBState = createMotorState(data, 3),
motorDState = createMotorState(data, 6), motorCState = createMotorState(data, 5),
infraredState = createInfraredState(data[8].toUByte().toInt(), data[9].toUByte()) motorDState = createMotorState(data, 7),
infraredState = createInfraredState(data[9].toUByte().toInt(), data[10].toUByte()),
ultrasoundState = UltrasoundState(
true,
0u,
Utils.bytesToFloat(byteArrayOf(
data[11],
data[12],
data[13],
data[14]
))
)
) )
} }

View File

@ -4,14 +4,16 @@ import android.annotation.SuppressLint
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import icu.fur93.esp32_car.ui.component.CardItem import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.const.RunModeConsts
import icu.fur93.esp32_car.entity.car.RunModeState
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable
fun StatusCardPathfinderStatus () : CardItem { fun StatusCardPathfinderStatus (runModeState: RunModeState) : CardItem {
return CardItem( return CardItem(
title = "循迹状态", title = "循迹状态",
content = { content = {
StatusCardItemText("进行中") StatusCardItemText(if (runModeState.runMode == RunModeConsts.MODE_TRACKING) "进行中" else "未进行")
} }
) )
} }

View File

@ -22,6 +22,7 @@ fun Joystick(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: Dp = 200.dp, size: Dp = 200.dp,
knobScale: Float = 0.4f, knobScale: Float = 0.4f,
yAxisOnly: Boolean = false,
onJoystickMove: (JoystickState) -> Unit onJoystickMove: (JoystickState) -> Unit
) { ) {
// 状态管理 // 状态管理
@ -46,14 +47,26 @@ fun Joystick(
onDragStart = { offset -> onDragStart = { offset ->
isDragging = true isDragging = true
centerPoint = Offset(sizePx / 2, sizePx / 2) centerPoint = Offset(sizePx / 2, sizePx / 2)
updateStickPosition(offset, centerPoint, radius, innerRadius) { newPos -> updateStickPosition(
offset,
centerPoint,
radius,
innerRadius,
yAxisOnly
) { newPos ->
stickPosition = newPos stickPosition = newPos
val state = calculateJoystickState(centerPoint, newPos, radius) val state = calculateJoystickState(centerPoint, newPos, radius)
onJoystickMove(state) onJoystickMove(state)
} }
}, },
onDrag = { change, _ -> onDrag = { change, _ ->
updateStickPosition(change.position, centerPoint, radius, innerRadius) { newPos -> updateStickPosition(
change.position,
centerPoint,
radius,
innerRadius,
yAxisOnly
) { newPos ->
stickPosition = newPos stickPosition = newPos
val state = calculateJoystickState(centerPoint, newPos, radius) val state = calculateJoystickState(centerPoint, newPos, radius)
onJoystickMove(state) onJoystickMove(state)
@ -93,25 +106,32 @@ private fun updateStickPosition(
centerPoint: Offset, centerPoint: Offset,
maxRadius: Float, maxRadius: Float,
knobRadius: Float, knobRadius: Float,
yAxisOnly: Boolean,
onUpdate: (Offset) -> Unit onUpdate: (Offset) -> Unit
) { ) {
val deltaX = pointerPosition.x - centerPoint.x
val deltaY = pointerPosition.y - centerPoint.y
// 如果是 Y 轴限位模式,将 X 坐标固定在中心
val adjustedX = if (yAxisOnly) centerPoint.x else pointerPosition.x
val adjustedPosition = Offset(adjustedX, pointerPosition.y)
val distance = hypot( val distance = hypot(
pointerPosition.x - centerPoint.x, adjustedPosition.x - centerPoint.x,
pointerPosition.y - centerPoint.y adjustedPosition.y - centerPoint.y
) )
val newPosition = if (distance > maxRadius) { val newPosition = if (distance > maxRadius) {
// 限制在最大范围内
val angle = atan2( val angle = atan2(
pointerPosition.y - centerPoint.y, adjustedPosition.y - centerPoint.y,
pointerPosition.x - centerPoint.x adjustedPosition.x - centerPoint.x
) )
Offset( Offset(
centerPoint.x + (maxRadius * kotlin.math.cos(angle)), centerPoint.x + (maxRadius * kotlin.math.cos(angle)),
centerPoint.y + (maxRadius * kotlin.math.sin(angle)) centerPoint.y + (maxRadius * kotlin.math.sin(angle))
) )
} else { } else {
pointerPosition adjustedPosition
} }
onUpdate(newPosition) onUpdate(newPosition)

View File

@ -0,0 +1,33 @@
package icu.fur93.esp32_car.utils
class Utils {
companion object {
/**
* Float 转换为字节数组
*/
fun floatToBytes(value: Float): ByteArray {
return value.toBits().let { bits ->
byteArrayOf(
(bits and 0xFF).toByte(),
((bits shr 8) and 0xFF).toByte(),
((bits shr 16) and 0xFF).toByte(),
((bits shr 24) and 0xFF).toByte()
)
}
}
/**
* 将字节数组转换为 Float
*/
fun bytesToFloat(bytes: ByteArray): Float {
require(bytes.size >= 4) { "字节数组长度必须大于等于4" }
return Float.fromBits(
(bytes[3].toInt() and 0xFF shl 24) or
(bytes[2].toInt() and 0xFF shl 16) or
(bytes[1].toInt() and 0xFF shl 8) or
(bytes[0].toInt() and 0xFF)
)
}
}
}

View File

@ -2,11 +2,10 @@ package icu.fur93.esp32_car.viewmodel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.util.Log
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import icu.fur93.esp32_car.const.CarCommands import icu.fur93.esp32_car.const.CarCommands
import icu.fur93.esp32_car.const.RunModeConsts
import icu.fur93.esp32_car.entity.BleDevice import icu.fur93.esp32_car.entity.BleDevice
import icu.fur93.esp32_car.entity.LogEntry import icu.fur93.esp32_car.entity.LogEntry
import icu.fur93.esp32_car.entity.car.CarState import icu.fur93.esp32_car.entity.car.CarState
@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.view.WindowManager
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -44,10 +42,28 @@ class CarControlUseCase(
buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x01, speed) buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x01, speed)
) )
fun rotateLeft(speed: Int = 255) = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_ROTATE_CONTROL, 0x01, speed)
)
fun rotateRight(speed: Int = 255) = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_ROTATE_CONTROL, 0x00, speed)
)
fun stop() = repository.sendCommand( fun stop() = repository.sendCommand(
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x00, 0x00) buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x00, 0x00)
) )
fun setRunMode(mode: Int) = repository.sendCommand(
byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(),
0x05,
CarCommands.CMD_SET_MODE.toByte(),
mode.toByte(),
CarCommands.PACKET_T_TAIL.toByte()
)
)
fun sendXYR(x: Int, y: Int, r: Int) = repository.sendCommand( fun sendXYR(x: Int, y: Int, r: Int) = repository.sendCommand(
byteArrayOf( byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(), CarCommands.PACKET_T_HEAD.toByte(),
@ -60,6 +76,27 @@ class CarControlUseCase(
) )
) )
fun setPathfinderSpeed(baseSpeed: Int, turnSpeed: Int) = repository.sendCommand(
byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(),
0x06,
0xA3.toByte(),
baseSpeed.toByte(),
turnSpeed.toByte(),
CarCommands.PACKET_T_TAIL.toByte()
)
)
fun setPathfinderSensitivity(sensitivity: Int) = repository.sendCommand(
byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(),
0x05,
0xA4.toByte(),
sensitivity.toByte(),
CarCommands.PACKET_T_TAIL.toByte()
)
)
private fun buildCommand(cmd: UInt, param1: Int, param2: Int) = byteArrayOf( private fun buildCommand(cmd: UInt, param1: Int, param2: Int) = byteArrayOf(
CarCommands.PACKET_T_HEAD.toByte(), CarCommands.PACKET_T_HEAD.toByte(),
0x06, 0x06,
@ -88,11 +125,6 @@ class CarViewModel(
(repository as BluetoothRepositoryImpl).connectToDevice(device) (repository as BluetoothRepositoryImpl).connectToDevice(device)
} }
@SuppressLint("MissingPermission")
fun connectToDeviceByAddress(address: String) {
(repository as BluetoothRepositoryImpl).connectToDeviceByAddress(address)
}
val carState: StateFlow<CarState> = repository.observeCarState() val carState: StateFlow<CarState> = repository.observeCarState()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CarState()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CarState())
@ -157,6 +189,15 @@ class CarViewModel(
fun turnLeft(speed: Int = 255) = carControlUseCase.turnLeft(speed) fun turnLeft(speed: Int = 255) = carControlUseCase.turnLeft(speed)
fun turnRight(speed: Int = 255) = carControlUseCase.turnRight(speed) fun turnRight(speed: Int = 255) = carControlUseCase.turnRight(speed)
fun stop() = carControlUseCase.stop() fun stop() = carControlUseCase.stop()
fun rotateLeft(speed: Int = 255) = carControlUseCase.rotateLeft(speed)
fun rotateRight(speed: Int = 255) = carControlUseCase.rotateRight(speed)
fun setRunMode(mode: Int) = carControlUseCase.setRunMode(mode)
fun sendXYR(x: Int, y: Int, r: Int) = carControlUseCase.sendXYR(x, y, r) fun sendXYR(x: Int, y: Int, r: Int) = carControlUseCase.sendXYR(x, y, r)
fun clearLogs() = repository.clearLogs() fun clearLogs() = repository.clearLogs()
fun setPathfinderSpeed(baseSpeed: Int, turnSpeed: Int) =
carControlUseCase.setPathfinderSpeed(baseSpeed, turnSpeed)
fun setPathfinderSensitivity(sensitivity: Int) =
carControlUseCase.setPathfinderSensitivity(sensitivity)
} }