From 29ecffb2208814eadabf9e983fc1ed0a4e51bfb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=96=E5=8F=81?= Date: Mon, 23 Dec 2024 10:22:09 +0800 Subject: [PATCH] feat: basic joystick feature --- .idea/misc.xml | 1 - app/src/main/AndroidManifest.xml | 3 +- .../java/icu/fur93/esp32_car/CarController.kt | 44 ++- .../java/icu/fur93/esp32_car/MainActivity.kt | 292 ++++++++++++------ .../fur93/esp32_car/ui/component/Joystick.kt | 29 -- 5 files changed, 240 insertions(+), 129 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6601d5a..547aa6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,7 +29,8 @@ android:name=".MainActivity" android:exported="true" android:label="@string/app_name" - android:theme="@style/Theme.Esp32car"> + android:theme="@style/Theme.Esp32car" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/icu/fur93/esp32_car/CarController.kt b/app/src/main/java/icu/fur93/esp32_car/CarController.kt index 968e70c..c8b30e0 100644 --- a/app/src/main/java/icu/fur93/esp32_car/CarController.kt +++ b/app/src/main/java/icu/fur93/esp32_car/CarController.kt @@ -46,6 +46,19 @@ data class CarState( 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()) @@ -54,6 +67,10 @@ class CarController(private val onStateChange: () -> Unit) { var bluetoothGatt: BluetoothGatt? = null var rxCharacteristic: BluetoothGattCharacteristic? = null + // 添加日志列表状态 + private val _logs = mutableStateOf>(emptyList()) + val logs: State> = _logs + // 更新电机状态的方法 private fun updateMotorState( motorA: MotorState? = null, @@ -68,11 +85,18 @@ class CarController(private val onStateChange: () -> Unit) { } private fun sendCommand(command: ByteArray) { - rxCharacteristic?.let { characteristic -> - characteristic.value = command; - bluetoothGatt?.writeCharacteristic(characteristic) - } - } + 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())) } @@ -98,6 +122,11 @@ class CarController(private val onStateChange: () -> Unit) { } 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() && @@ -142,6 +171,11 @@ class CarController(private val onStateChange: () -> Unit) { } } } + + // 清除日志 + fun clearLogs() { + _logs.value = emptyList() + } } class CarControlState { 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 8663552..f04ec7d 100644 --- a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt +++ b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt @@ -30,12 +30,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import icu.fur93.esp32_car.ui.component.Joystick -import icu.fur93.esp32_car.ui.component.JoystickDemo import icu.fur93.esp32_car.ui.component.JoystickState -import icu.fur93.esp32_car.ui.component.format import java.util.UUID -import kotlin.math.PI +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { private val serviceUUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" @@ -72,7 +77,17 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + + // 设置全屏显示 + WindowCompat.setDecorFitsSystemWindows(window, false) + + // 隐藏状态栏和导航栏 + 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 @@ -112,6 +127,18 @@ class MainActivity : ComponentActivity() { } } + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + // 重新应用全屏设置 + WindowInsetsControllerCompat(window, window.decorView).let { controller -> + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + } + private fun connectToDevice(result: ScanResult) { if (ActivityCompat.checkSelfPermission( this, @@ -148,10 +175,12 @@ class MainActivity : ComponentActivity() { if (status == BluetoothGatt.GATT_SUCCESS) { gatt?.getService(UUID.fromString(serviceUUID))?.let { service -> carController.bluetoothGatt = gatt - carController.rxCharacteristic = service.getCharacteristic(UUID.fromString(rxCharUUID)) - + carController.rxCharacteristic = + service.getCharacteristic(UUID.fromString(rxCharUUID)) + // 注册通知监听器 - val txCharacteristic = service.getCharacteristic(UUID.fromString(txCharUUID)) + val txCharacteristic = + service.getCharacteristic(UUID.fromString(txCharUUID)) gatt.setCharacteristicNotification(txCharacteristic, true) txCharacteristic.descriptors.forEach { descriptor -> descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE @@ -273,31 +302,62 @@ class MainActivity : ComponentActivity() { @Composable fun ControlJoystick( - carController: CarController + modifier: Modifier, + carController: CarController, + joystickState: JoystickState, + onJoystickStateChange: (JoystickState) -> Unit ) { - var joystickState by remember { mutableStateOf(JoystickState()) } + // 记住上一次发送的状态和时间 + val lastSentState = remember { mutableStateOf(JoystickState()) } + val lastSentTime = remember { mutableStateOf(0L) } + + // 用于停止命令重发的计数器 + val stopCommandCount = remember { mutableStateOf(0) } + val coroutineScope = rememberCoroutineScope() - Column( - verticalArrangement = Arrangement.Center - ) { - Joystick( - modifier = Modifier.padding(16.dp), - size = 200f - ) { state -> - joystickState = state - // 在这里处理摇杆状态变化 - // 发送蓝牙数据 - val x = (state.x * 100).toInt() // 转换为 -100 到 100 + Joystick( + modifier = modifier, + size = 350f + ) { state -> + // 更新状态 + onJoystickStateChange(state) + + val currentTime = System.currentTimeMillis() + + // 检查是否是停止状态(摇杆回正) + val isStopCommand = state.x == 0f && state.y == 0f + + // 如果是停止命令,启动重发机制 + // 否则检查时间间隔和状态变化 + if (isStopCommand) { + if (stopCommandCount.value == 0) { + // 第一次发送停止命令 + carController.stop() + stopCommandCount.value = 1 + + // 启动延时重发 + coroutineScope.launch { + delay(50) // 延时50ms + carController.stop() // 再次发送停止命令 + stopCommandCount.value = 2 + + delay(50) // 再次延时50ms + carController.stop() // 第三次发送停止命令 + stopCommandCount.value = 0 // 重置计数器 + } + } + } else if ((state.x != lastSentState.value.x || state.y != lastSentState.value.y) && + currentTime - lastSentTime.value >= 50) { + // 非停止命令的正常处理 + stopCommandCount.value = 0 // 重置停止命令计数器 + val x = (state.x * 100).toInt() val y = (state.y * 100).toInt() carController.sendXYR(x, y, 0) - // sendBluetoothCommand(x, y, 0) // R设为0 + + // 更新上次发送的状态和时间 + lastSentState.value = state + lastSentTime.value = currentTime } - - // 显示摇杆状态(可选) - Text("X: ${joystickState.x.format(2)}") - Text("Y: ${joystickState.y.format(2)}") - Text("Angle: ${(joystickState.angle * 180 / PI)}°") - Text("Distance: ${joystickState.distance.format(2)}") } } @@ -311,6 +371,7 @@ fun GamepadScreen( ) { var showDeviceList by remember { mutableStateOf(false) } var sliderValue by remember { mutableStateOf(0f) } + var joystickState by remember { mutableStateOf(JoystickState()) } Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxWidth()) { @@ -321,103 +382,148 @@ fun GamepadScreen( .padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly ) { + // 摇杆状态显示 + Column { + Text("摇杆") + Text("X: %.2f".format(joystickState.x)) + Text("Y: %.2f".format(joystickState.y)) + } + // 电机A状态 - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column { Text("电机A") Text("PWM: ${carController.carState.value.motorAState.pwm}") Text("IN1: ${carController.carState.value.motorAState.in1}") Text("IN2: ${carController.carState.value.motorAState.in2}") } - + // 电机B状态 - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column { Text("电机B") Text("PWM: ${carController.carState.value.motorBState.pwm}") Text("IN1: ${carController.carState.value.motorBState.in1}") Text("IN2: ${carController.carState.value.motorBState.in2}") } - + // 电机C状态 - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column { Text("电机C") Text("PWM: ${carController.carState.value.motorCState.pwm}") Text("IN1: ${carController.carState.value.motorCState.in1}") Text("IN2: ${carController.carState.value.motorCState.in2}") } - + // 电机D状态 - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column { Text("电机D") Text("PWM: ${carController.carState.value.motorDState.pwm}") Text("IN1: ${carController.carState.value.motorDState.in1}") Text("IN2: ${carController.carState.value.motorDState.in2}") } - } - } - // 设备选择按钮 - Button( - onClick = { showDeviceList = true }, - modifier = Modifier - .align(Alignment.TopEnd) - .padding(16.dp) - ) { - Text(if (isConnected) "已连接" else "选择设备") - } - - // 方向控制布局 - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically - ) { - // 左侧控制区 - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.weight(1f) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(vertical = 16.dp) + // 设备选择���钮 + Button( + onClick = { showDeviceList = true }, ) { -// Button(onClick = { carController.turnLeft() }) { -// Text("左转") -// } -// Button(onClick = { carController.turnRight() }) { -// Text("右转") -// } - ControlJoystick(carController) + Text(if (isConnected) "已连接" else "选择设备") } } - // 右侧控制区 - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.weight(1f) + // 方向控制布局 + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically ) { - // 垂直滑杆 - Slider( - value = sliderValue, - onValueChange = { newValue -> - sliderValue = newValue - val speed = (newValue * 255).toInt() - if (speed > 0) { - carController.moveForward(speed) - } else if (speed < 0) { - carController.moveBackward(-speed) - } else { - carController.stop() - } - }, - valueRange = -1f..1f, + // 左侧控制区 + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, modifier = Modifier - .height(20.dp) - .width(200.dp) - .padding(vertical = 16.dp) - .graphicsLayer(rotationZ = 270f) - ) + .weight(1f) + .fillMaxHeight() + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + ControlJoystick( + modifier = Modifier.padding(16.dp), + carController = carController, + joystickState = joystickState, + onJoystickStateChange = { newState -> + joystickState = newState + } + ) + } + } + + // 中间 Log 区 + // 用于显示发出的蓝牙指令 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .weight(1f), + reverseLayout = true // 反转布局顺序 + ) { + items(carController.logs.value.filter { it.direction == LogDirection.SEND }.reversed()) { log -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "[${SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()) + .format(Date(log.timestamp))}]", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = log.data, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f) + ) + } + } + } + + // 右侧控制区 + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.weight(1f) + ) { + // 垂直滑杆 + Slider( + value = sliderValue, + onValueChange = { newValue -> + // 在0附近添加吸附效果 + val snapValue = when { + newValue > -0.1f && newValue < 0.1f -> 0f + else -> newValue + } + sliderValue = snapValue + val speed = (snapValue * 255).toInt() + if (speed > 0) { + carController.moveForward(speed) + } else if (speed < 0) { + carController.moveBackward(-speed) + } else { + carController.stop() + } + }, + valueRange = -1f..1f, + modifier = Modifier + .height(10.dp) + .width(200.dp) + .padding(vertical = 16.dp) + .graphicsLayer(rotationZ = 270f) + ) + } } } 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 8add7d3..1c3059f 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 @@ -151,34 +151,5 @@ private fun DrawScope.drawJoystickKnob(position: Offset, radius: Float) { ) } -// 使用示例 -@Composable -fun JoystickDemo() { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center - ) { - var joystickState by remember { mutableStateOf(JoystickState()) } - - Joystick( - modifier = Modifier.padding(16.dp), - size = 200f - ) { state -> - joystickState = state - // 在这里处理摇杆状态变化 - // 发送蓝牙数据 - val x = (state.x * 100).toInt() // 转换为 -100 到 100 - val y = (state.y * 100).toInt() - // sendBluetoothCommand(x, y, 0) // R设为0 - } - - // 显示摇杆状态(可选) - Text("X: ${joystickState.x.format(2)}") - Text("Y: ${joystickState.y.format(2)}") - Text("Angle: ${(joystickState.angle * 180 / PI)}°") - Text("Distance: ${joystickState.distance.format(2)}") - } -} - // 格式化浮点数 fun Float.format(digits: Int) = "%.${digits}f".format(this) \ No newline at end of file