feat: basic joystick feature
This commit is contained in:
parent
5e3433cbdc
commit
29ecffb220
|
@ -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">
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue