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_STEER_CONTROL = 0x21u
|
||||||
const val CMD_MOTOR_SINGLE_CONTROL = 0x22u
|
const val CMD_MOTOR_SINGLE_CONTROL = 0x22u
|
||||||
const val CMD_MOTOR_ROTATE_CONTROL = 0x23u
|
const val CMD_MOTOR_ROTATE_CONTROL = 0x23u
|
||||||
|
const val CMD_MOTOR_XYR_CONTROL = 0x24u
|
||||||
const val CMD_DEMO_PID = 0xf0u
|
const val CMD_DEMO_PID = 0xf0u
|
||||||
const val CMD_DEMO_PATH = 0xf1u
|
const val CMD_DEMO_PATH = 0xf1u
|
||||||
|
|
||||||
|
@ -73,23 +74,27 @@ class CarController(private val onStateChange: () -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun moveForward(speed: Int = 255) {
|
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) {
|
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) {
|
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) {
|
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() {
|
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) {
|
fun onReceivePacket(packet: ByteArray) {
|
||||||
|
|
|
@ -30,7 +30,12 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.ActivityCompat
|
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 java.util.UUID
|
||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val serviceUUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
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
|
@Composable
|
||||||
fun GamepadScreen(
|
fun GamepadScreen(
|
||||||
deviceList: List<ScanResult>,
|
deviceList: List<ScanResult>,
|
||||||
|
@ -346,12 +381,13 @@ fun GamepadScreen(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier.padding(vertical = 16.dp)
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Button(onClick = { carController.turnLeft() }) {
|
// Button(onClick = { carController.turnLeft() }) {
|
||||||
Text("左转")
|
// Text("左转")
|
||||||
}
|
// }
|
||||||
Button(onClick = { carController.turnRight() }) {
|
// Button(onClick = { carController.turnRight() }) {
|
||||||
Text("右转")
|
// 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