From 5e3433cbdc07903ba9798ac37d3e941a72e2ad31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=96=E5=8F=81?= Date: Fri, 20 Dec 2024 01:28:23 +0800 Subject: [PATCH] feat: joystick --- .../java/icu/fur93/esp32_car/CarController.kt | 15 +- .../java/icu/fur93/esp32_car/MainActivity.kt | 48 ++++- .../fur93/esp32_car/ui/component/Joystick.kt | 184 ++++++++++++++++++ 3 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt 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 e3c2c2e..968e70c 100644 --- a/app/src/main/java/icu/fur93/esp32_car/CarController.kt +++ b/app/src/main/java/icu/fur93/esp32_car/CarController.kt @@ -24,6 +24,7 @@ class CarCommand { 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 @@ -73,23 +74,27 @@ class CarController(private val onStateChange: () -> Unit) { } } fun moveForward(speed: Int = 255) { - sendCommand(byteArrayOf(0x00, 0x06, 0x20, 0x01, speed.toByte(), 0xff.toByte())) + 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(0x00, 0x06, 0x20, 0x02, speed.toByte(), 0xff.toByte())) + 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(0x00, 0x06, 0x21, 0x00, speed.toByte(), 0xff.toByte())) + 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(0x00, 0x06, 0x20, 0x01, speed.toByte(), 0xff.toByte())) + 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(0x00, 0x06, 0x20, 0x00, 0x00, 0xff.toByte())) + 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) { 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 db5185b..8663552 100644 --- a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt +++ b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt @@ -30,7 +30,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat +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 class MainActivity : ComponentActivity() { private val serviceUUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" @@ -266,6 +271,36 @@ class MainActivity : ComponentActivity() { } } +@Composable +fun ControlJoystick( + carController: CarController +) { + var joystickState by remember { mutableStateOf(JoystickState()) } + + Column( + verticalArrangement = Arrangement.Center + ) { + 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() + carController.sendXYR(x, y, 0) + // 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)}") + } +} + @Composable fun GamepadScreen( deviceList: List, @@ -346,12 +381,13 @@ fun GamepadScreen( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding(vertical = 16.dp) ) { - Button(onClick = { carController.turnLeft() }) { - Text("左转") - } - Button(onClick = { carController.turnRight() }) { - Text("右转") - } +// Button(onClick = { carController.turnLeft() }) { +// Text("左转") +// } +// Button(onClick = { carController.turnRight() }) { +// Text("右转") +// } + ControlJoystick(carController) } } 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 new file mode 100644 index 0000000..8add7d3 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt @@ -0,0 +1,184 @@ +package icu.fur93.esp32_car.ui.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import kotlin.math.atan2 +import kotlin.math.hypot +import kotlin.math.PI + +// 摇杆数据类 +data class JoystickState( + val x: Float = 0f, // -1 到 1 + val y: Float = 0f, // -1 到 1 + val angle: Float = 0f, // 角度(弧度) + val distance: Float = 0f // 0 到 1 +) + +@Composable +fun Joystick( + modifier: Modifier = Modifier, + size: Float = 200f, + onJoystickMove: (JoystickState) -> Unit +) { + // 状态管理 + var centerPoint by remember { mutableStateOf(Offset.Zero) } + var stickPosition by remember { mutableStateOf(Offset.Zero) } + var isDragging by remember { mutableStateOf(false) } + + // 计算实际尺寸 + val radius = size / 2 + val innerRadius = radius * 0.4f + + // 创建摇杆 + Canvas( + modifier = modifier + .size(size.dp) + .padding(8.dp) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + isDragging = true + centerPoint = Offset(size / 2, size / 2) + updateStickPosition(offset, centerPoint, radius, innerRadius) { newPos -> + stickPosition = newPos + val state = calculateJoystickState(centerPoint, newPos, radius) + onJoystickMove(state) + } + }, + onDrag = { change, _ -> + updateStickPosition(change.position, centerPoint, radius, innerRadius) { newPos -> + stickPosition = newPos + val state = calculateJoystickState(centerPoint, newPos, radius) + onJoystickMove(state) + } + }, + onDragEnd = { + isDragging = false + // 回到中心位置 + stickPosition = centerPoint + onJoystickMove(JoystickState()) + } + ) + } + ) { + // 初始化中心点 + if (centerPoint == Offset.Zero) { + centerPoint = Offset(size / 2, size / 2) + stickPosition = centerPoint + } + + // 绘制底部圆形 + drawJoystickBase(centerPoint, radius) + + // 绘制摇杆 + drawJoystickKnob(stickPosition, innerRadius) + } +} + +// 更新摇杆位置 +private fun updateStickPosition( + pointerPosition: Offset, + centerPoint: Offset, + maxRadius: Float, + knobRadius: Float, + onUpdate: (Offset) -> Unit +) { + val distance = hypot( + pointerPosition.x - centerPoint.x, + pointerPosition.y - centerPoint.y + ) + + val newPosition = if (distance > maxRadius) { + // 限制在最大范围内 + val angle = atan2( + pointerPosition.y - centerPoint.y, + pointerPosition.x - centerPoint.x + ) + Offset( + centerPoint.x + (maxRadius * kotlin.math.cos(angle)), + centerPoint.y + (maxRadius * kotlin.math.sin(angle)) + ) + } else { + pointerPosition + } + + onUpdate(newPosition) +} + +// 计算摇杆状态 +private fun calculateJoystickState( + centerPoint: Offset, + stickPosition: Offset, + maxRadius: Float +): JoystickState { + val deltaX = centerPoint.x - stickPosition.x + val deltaY = centerPoint.y - stickPosition.y + val distance = hypot(deltaX, deltaY) + val angle = atan2(deltaY, deltaX) + + return JoystickState( + x = (deltaX / maxRadius).coerceIn(-1f, 1f), + y = (deltaY / maxRadius).coerceIn(-1f, 1f), + angle = angle.toFloat(), + distance = (distance / maxRadius).coerceIn(0f, 1f) + ) +} + +// 绘制底部圆形 +private fun DrawScope.drawJoystickBase(center: Offset, radius: Float) { + drawCircle( + color = Color.Gray.copy(alpha = 0.3f), + radius = radius, + center = center + ) +} + +// 绘制摇杆球 +private fun DrawScope.drawJoystickKnob(position: Offset, radius: Float) { + drawCircle( + color = Color.Blue.copy(alpha = 0.5f), + radius = radius, + center = position + ) +} + +// 使用示例 +@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