feat: joystick

This commit is contained in:
玖叁 2024-12-20 01:28:23 +08:00
parent 88f618ac55
commit 5e3433cbdc
3 changed files with 236 additions and 11 deletions

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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)