diff --git a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt index 3a31255..8c3fa31 100644 --- a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt +++ b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt @@ -117,15 +117,15 @@ class MainActivity : ComponentActivity() { ) } - // 检查权限后尝试自动连接 - lifecycleScope.launch { - preferencesDataStore.getLastConnectedDevice().collect { address -> - if (address != null) { - viewModel.connectToDeviceByAddress(address) - } - } + // // 检查权限后尝试自动连接 + // lifecycleScope.launch { + // preferencesDataStore.getLastConnectedDevice().collect { address -> + // if (address != null) { + // viewModel.connectToDeviceByAddress(address) + // } + // } - } + // } setContent { Esp32carTheme { diff --git a/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt b/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt index cf80c6a..3155ed8 100644 --- a/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt +++ b/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt @@ -11,6 +11,8 @@ object CarCommands { const val CMD_GET_SPIFFS_STATUS = 0x11u const val CMD_GET_DISTANCE = 0x12u + const val CMD_SET_MODE = 0x30u + const val CMD_SET_NAME = 0xa1u const val CMD_SET_PID = 0xa2u diff --git a/app/src/main/java/icu/fur93/esp32_car/const/RunModeConsts.kt b/app/src/main/java/icu/fur93/esp32_car/const/RunModeConsts.kt new file mode 100644 index 0000000..95f2c45 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/const/RunModeConsts.kt @@ -0,0 +1,6 @@ +package icu.fur93.esp32_car.const + +object RunModeConsts { + const val MODE_MANUAL = 0x00u + const val MODE_TRACKING = 0x01u +} \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt index afbfa21..bb6d605 100644 --- a/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt @@ -2,6 +2,7 @@ package icu.fur93.esp32_car.entity.car data class CarState( val controlState: CarControlState = CarControlState(), + val runModeState: RunModeState = RunModeState(), val motorAState: MotorState = MotorState(), val motorBState: MotorState = MotorState(), val motorCState: MotorState = MotorState(), diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/RunModeState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/RunModeState.kt new file mode 100644 index 0000000..10ae24b --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/RunModeState.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt index 35779ba..2bf534c 100644 --- a/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt @@ -3,5 +3,5 @@ package icu.fur93.esp32_car.entity.car data class UltrasoundState( val enable: Boolean = false, val servoAngle: UInt = 0u, - val distance: UInt = 0u + val distance: Float = 0f ) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt b/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt index 661a1ff..3091848 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt @@ -46,7 +46,7 @@ fun ControlPageStatusCard() { @Composable fun ControlPageAutoControl() { - val context = LocalContext.current + val navController = LocalNavigation.current return CardButtonGroup( title = "自动控制", buttons = listOf( @@ -54,7 +54,7 @@ fun ControlPageAutoControl() { text = Route.ControlPathfinderMode.title, icon = ImageVector.vectorResource(R.drawable.outline_route_24), onClick = { - Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show() + navController.navigate(Route.ControlPathfinderMode.route) } ) ) diff --git a/app/src/main/java/icu/fur93/esp32_car/page/ControlPathfinderModePage.kt b/app/src/main/java/icu/fur93/esp32_car/page/ControlPathfinderModePage.kt index d8e27d3..cbbe458 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/ControlPathfinderModePage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/ControlPathfinderModePage.kt @@ -1,8 +1,56 @@ package icu.fur93.esp32_car.page +import androidx.compose.foundation.layout.Column 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.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 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 "开始") + } + } + ) } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt b/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt index 89b5aca..376eb1f 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt @@ -10,6 +10,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.FilledIconButton 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.collectAsState import androidx.compose.runtime.getValue @@ -72,55 +75,84 @@ fun ControlSingleJoystickModeControlPanel( viewModel: CarViewModel, onJoystickStateChange: (JoystickState) -> Unit ) { + // 添加Y轴限位模式的状态 + var yAxisOnly by remember { mutableStateOf(false) } Spacer(Modifier.height(24.dp)) - Row( + Column( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + horizontalAlignment = Alignment.CenterHorizontally ) { - FilledIconButton( - onClick = {}, - modifier = Modifier.pointerInteropFilter { event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - viewModel.sendXYR(0, 0, -50) - true - } - MotionEvent.ACTION_UP -> { - viewModel.stop() - true - } - else -> false - } - } + // 添加开关控件 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转") + Text( + text = "Y 轴限位模式", + style = MaterialTheme.typography.bodyLarge + ) + Switch( + checked = yAxisOnly, + onCheckedChange = { yAxisOnly = it } + ) } - ControlJoystick( - viewModel = viewModel, - onJoystickStateChange = onJoystickStateChange - ) + Spacer(Modifier.height(16.dp)) - FilledIconButton( - onClick = {}, - modifier = Modifier.pointerInteropFilter { event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - viewModel.sendXYR(0, 0, 50) - true - } - MotionEvent.ACTION_UP -> { - viewModel.stop() - true - } - else -> false - } - } + // 原有的控制面板布局 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - 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 fun ControlJoystick( viewModel: CarViewModel, - onJoystickStateChange: (JoystickState) -> Unit + onJoystickStateChange: (JoystickState) -> Unit, + yAxisOnly: Boolean = false // 添加参数 ) { // 记住上一次发送的状态和时间 val lastSentState = remember { mutableStateOf(JoystickState()) } @@ -142,7 +175,8 @@ fun ControlJoystick( Joystick( size = 175.dp, - knobScale = 0.25f + knobScale = 0.25f, + yAxisOnly = yAxisOnly, // 使用传入的参数 ) { state -> // 更新状态 onJoystickStateChange(state) diff --git a/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt b/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt index ed3cf0d..a07aaa1 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext 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.R import icu.fur93.esp32_car.Route @@ -63,6 +65,8 @@ fun SettingsList(viewModel: CarViewModel) { val context = LocalContext.current var debugMode by remember { mutableStateOf(DebugWindowManager.isShowing()) } + var showSensitivityDialog by remember { mutableStateOf(false) } + var showSpeedDialog by remember { mutableStateOf(false) } val onDebugModeChange = { enable: Boolean -> if (enable) { @@ -94,16 +98,122 @@ fun SettingsList(viewModel: CarViewModel) { } item { ListItem( - headlineContent = { Text("PID 参数") }, + headlineContent = { Text("循迹灵敏度设置") }, trailingContent = { Icon( 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("取消") + } + } + ) + } } diff --git a/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt b/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt index e5e82c2..641272c 100644 --- a/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt +++ b/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt @@ -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.InfraredState 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 { @@ -91,6 +94,48 @@ class BluetoothRepositoryImpl( private val _deviceAddress = MutableStateFlow(null) override val deviceAddress: StateFlow = _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() { override fun onScanResult(callbackType: Int, result: ScanResult) { val bleDevice = BleDevice( @@ -213,33 +258,45 @@ class BluetoothRepositoryImpl( override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { super.onServicesDiscovered(gatt, status) - if (status == BluetoothGatt.GATT_SUCCESS) { + if (status == BluetoothGatt.GATT_SUCCESS && gatt != null) { // 读取设备名称 - val genericService = - gatt?.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb")) - val deviceNameChar = - genericService?.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb")) + val genericService = gatt.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb")) + val deviceNameChar = genericService?.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb")) if (deviceNameChar != null) { gatt.readCharacteristic(deviceNameChar) } - gatt?.getService(UUID.fromString(serviceUUID))?.let { service -> + gatt.getService(UUID.fromString(serviceUUID))?.let { service -> bluetoothGatt = gatt - rxCharacteristic = - service.getCharacteristic(UUID.fromString(rxCharUUID)) + 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) + // 使用新的通知启用方法 + val txCharacteristic = service.getCharacteristic(UUID.fromString(txCharUUID)) + if (txCharacteristic != null) { + enableNotifications(gatt, txCharacteristic) } } } } + 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( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, @@ -260,6 +317,7 @@ class BluetoothRepositoryImpl( value: ByteArray ) { super.onCharacteristicChanged(gatt, characteristic, value) + Log.d("onCharacteristicChanged", "characteristic: ${characteristic.uuid}, value: ${value.joinToString(" ")}") if (characteristic.uuid == UUID.fromString(txCharUUID)) { 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") override fun sendCommand(command: ByteArray) { rxCharacteristic?.let { characteristic -> @@ -345,16 +363,17 @@ class BluetoothRepositoryImpl( 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 | | PWM | 无符号整数 | - | | 红外引脚数量 | 无符号整数 | - | | 红外数据 | 无符号整数 | 按位表示红外循迹模块数据, 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()) { @@ -373,11 +392,22 @@ class BluetoothRepositoryImpl( private fun updateStatus(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), - infraredState = createInfraredState(data[8].toUByte().toInt(), data[9].toUByte()) + runModeState = RunModeState(data[0].toUByte().toUInt()), + motorAState = createMotorState(data, 1), + motorBState = createMotorState(data, 3), + motorCState = createMotorState(data, 5), + 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] + )) + ) ) } diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardPathfinderStatus.kt b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardPathfinderStatus.kt index 2f0ee77..c22b4ee 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardPathfinderStatus.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardPathfinderStatus.kt @@ -4,14 +4,16 @@ import android.annotation.SuppressLint 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.const.RunModeConsts +import icu.fur93.esp32_car.entity.car.RunModeState @SuppressLint("ComposableNaming") @Composable -fun StatusCardPathfinderStatus () : CardItem { +fun StatusCardPathfinderStatus (runModeState: RunModeState) : CardItem { return CardItem( title = "循迹状态", content = { - StatusCardItemText("进行中") + StatusCardItemText(if (runModeState.runMode == RunModeConsts.MODE_TRACKING) "进行中" else "未进行") } ) } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt b/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt index 8e30449..e9e46a5 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt @@ -22,6 +22,7 @@ fun Joystick( modifier: Modifier = Modifier, size: Dp = 200.dp, knobScale: Float = 0.4f, + yAxisOnly: Boolean = false, onJoystickMove: (JoystickState) -> Unit ) { // 状态管理 @@ -46,14 +47,26 @@ fun Joystick( onDragStart = { offset -> isDragging = true centerPoint = Offset(sizePx / 2, sizePx / 2) - updateStickPosition(offset, centerPoint, radius, innerRadius) { newPos -> + updateStickPosition( + offset, + centerPoint, + radius, + innerRadius, + yAxisOnly + ) { newPos -> stickPosition = newPos val state = calculateJoystickState(centerPoint, newPos, radius) onJoystickMove(state) } }, onDrag = { change, _ -> - updateStickPosition(change.position, centerPoint, radius, innerRadius) { newPos -> + updateStickPosition( + change.position, + centerPoint, + radius, + innerRadius, + yAxisOnly + ) { newPos -> stickPosition = newPos val state = calculateJoystickState(centerPoint, newPos, radius) onJoystickMove(state) @@ -93,25 +106,32 @@ private fun updateStickPosition( centerPoint: Offset, maxRadius: Float, knobRadius: Float, + yAxisOnly: Boolean, 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( - pointerPosition.x - centerPoint.x, - pointerPosition.y - centerPoint.y + adjustedPosition.x - centerPoint.x, + adjustedPosition.y - centerPoint.y ) val newPosition = if (distance > maxRadius) { - // 限制在最大范围内 val angle = atan2( - pointerPosition.y - centerPoint.y, - pointerPosition.x - centerPoint.x + adjustedPosition.y - centerPoint.y, + adjustedPosition.x - centerPoint.x ) Offset( centerPoint.x + (maxRadius * kotlin.math.cos(angle)), centerPoint.y + (maxRadius * kotlin.math.sin(angle)) ) } else { - pointerPosition + adjustedPosition } onUpdate(newPosition) diff --git a/app/src/main/java/icu/fur93/esp32_car/utils/Utils.kt b/app/src/main/java/icu/fur93/esp32_car/utils/Utils.kt new file mode 100644 index 0000000..b3b3643 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/utils/Utils.kt @@ -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) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt b/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt index efe7c10..645f3ce 100644 --- a/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt +++ b/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt @@ -2,11 +2,10 @@ package icu.fur93.esp32_car.viewmodel import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice -import android.util.Log -import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.LogEntry 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.stateIn import kotlinx.coroutines.launch -import android.view.WindowManager import kotlinx.coroutines.flow.combine @@ -44,10 +42,28 @@ class CarControlUseCase( 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( 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( byteArrayOf( 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( CarCommands.PACKET_T_HEAD.toByte(), 0x06, @@ -88,11 +125,6 @@ class CarViewModel( (repository as BluetoothRepositoryImpl).connectToDevice(device) } - @SuppressLint("MissingPermission") - fun connectToDeviceByAddress(address: String) { - (repository as BluetoothRepositoryImpl).connectToDeviceByAddress(address) - } - val carState: StateFlow = repository.observeCarState() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CarState()) @@ -157,6 +189,15 @@ class CarViewModel( fun turnLeft(speed: Int = 255) = carControlUseCase.turnLeft(speed) fun turnRight(speed: Int = 255) = carControlUseCase.turnRight(speed) 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 clearLogs() = repository.clearLogs() + + fun setPathfinderSpeed(baseSpeed: Int, turnSpeed: Int) = + carControlUseCase.setPathfinderSpeed(baseSpeed, turnSpeed) + + fun setPathfinderSensitivity(sensitivity: Int) = + carControlUseCase.setPathfinderSensitivity(sensitivity) } \ No newline at end of file