feat: joystick
This commit is contained in:
parent
88f618ac55
commit
5e3433cbdc
|
@ -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) {
|
||||
|
|
|
@ -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<ScanResult>,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue