feat: basic joystick feature

This commit is contained in:
玖叁 2024-12-23 10:22:09 +08:00
parent 5e3433cbdc
commit 29ecffb220
5 changed files with 240 additions and 129 deletions

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

View File

@ -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">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -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<List<LogEntry>>(emptyList())
val logs: State<List<LogEntry>> = _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 {

View File

@ -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"
@ -73,6 +78,16 @@ 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) }
Column(
verticalArrangement = Arrangement.Center
) {
Joystick(
modifier = Modifier.padding(16.dp),
size = 200f
) { state ->
joystickState = state
// 在这里处理摇杆状态变化
// 发送蓝牙数据
val x = (state.x * 100).toInt() // 转换为 -100 到 100
// 用于停止命令重发的计数器
val stopCommandCount = remember { mutableStateOf(0) }
val coroutineScope = rememberCoroutineScope()
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
}
// 显示摇杆状态(可选)
Text("X: ${joystickState.x.format(2)}")
Text("Y: ${joystickState.y.format(2)}")
Text("Angle: ${(joystickState.angle * 180 / PI)}°")
Text("Distance: ${joystickState.distance.format(2)}")
// 更新上次发送的状态和时间
lastSentState.value = state
lastSentTime.value = currentTime
}
}
}
@ -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,8 +382,15 @@ 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}")
@ -330,7 +398,7 @@ fun GamepadScreen(
}
// 电机B状态
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Column {
Text("电机B")
Text("PWM: ${carController.carState.value.motorBState.pwm}")
Text("IN1: ${carController.carState.value.motorBState.in1}")
@ -338,7 +406,7 @@ fun GamepadScreen(
}
// 电机C状态
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Column {
Text("电机C")
Text("PWM: ${carController.carState.value.motorCState.pwm}")
Text("IN1: ${carController.carState.value.motorCState.in1}")
@ -346,78 +414,116 @@ fun GamepadScreen(
}
// 电机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)
// 设备选择<E98089><E68BA9><EFBFBD>
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)
)
}
}
}

View File

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