feat: status display
This commit is contained in:
parent
2a6712381f
commit
88f618ac55
|
@ -4,6 +4,14 @@
|
|||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-12-19T11:42:03.594785900Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="Default" identifier="serial=192.168.31.62:42449;connection=c8a0adf4" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
|
|
@ -59,6 +59,7 @@ dependencies {
|
|||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
package icu.fur93.esp32_car
|
||||
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.room.util.copy
|
||||
|
||||
class CarCommand {
|
||||
companion object {
|
||||
// 数据包头尾定义
|
||||
const val PACKET_T_HEAD = 0x00u
|
||||
const val PACKET_T_TAIL = 0xFFu
|
||||
const val PACKET_R_HEAD = 0x01u
|
||||
const val PACKET_R_TAIL = 0xFEu
|
||||
const val PACKET_MAX_LENGTH = 32u // 数据包最大长度
|
||||
|
||||
// 指令定义
|
||||
const val CMD_GET_BT_STATUS = 0x10u
|
||||
const val CMD_GET_SPIFFS_STATUS = 0x11u
|
||||
const val CMD_GET_DISTANCE = 0x12u
|
||||
const val CMD_MOTOR_MOVE_CONTROL = 0x20u
|
||||
const val CMD_MOTOR_STEER_CONTROL = 0x21u
|
||||
const val CMD_MOTOR_SINGLE_CONTROL = 0x22u
|
||||
const val CMD_MOTOR_ROTATE_CONTROL = 0x23u
|
||||
const val CMD_DEMO_PID = 0xf0u
|
||||
const val CMD_DEMO_PATH = 0xf1u
|
||||
|
||||
const val CMD_STATUS_MOTOR = 0xE0u
|
||||
}
|
||||
}
|
||||
|
||||
data class MotorState(
|
||||
var pwm: UInt = 0u,
|
||||
var in1: UInt = 0u,
|
||||
var in2: UInt = 0u
|
||||
)
|
||||
|
||||
data class CarState(
|
||||
var controlState: CarControlState = CarControlState(),
|
||||
var motorAState: MotorState = MotorState(),
|
||||
var motorBState: MotorState = MotorState(),
|
||||
var motorCState: MotorState = MotorState(),
|
||||
var motorDState: MotorState = MotorState()
|
||||
)
|
||||
|
||||
class CarController(private val onStateChange: () -> Unit) {
|
||||
// 使用 MutableState 来存储小车状态
|
||||
private val _carState = mutableStateOf(CarState())
|
||||
val carState: State<CarState> = _carState
|
||||
|
||||
var bluetoothGatt: BluetoothGatt? = null
|
||||
var rxCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
||||
// 更新电机状态的方法
|
||||
private fun updateMotorState(
|
||||
motorA: MotorState? = null,
|
||||
motorB: MotorState? = null,
|
||||
motorC: MotorState? = null,
|
||||
motorD: MotorState? = null
|
||||
) {
|
||||
_carState.value.motorAState = motorA ?: _carState.value.motorAState
|
||||
_carState.value.motorBState = motorB ?: _carState.value.motorBState
|
||||
_carState.value.motorCState = motorC ?: _carState.value.motorCState
|
||||
_carState.value.motorDState = motorD ?: _carState.value.motorDState
|
||||
}
|
||||
|
||||
private fun sendCommand(command: ByteArray) {
|
||||
rxCharacteristic?.let { characteristic ->
|
||||
characteristic.value = command;
|
||||
bluetoothGatt?.writeCharacteristic(characteristic)
|
||||
}
|
||||
}
|
||||
fun moveForward(speed: Int = 255) {
|
||||
sendCommand(byteArrayOf(0x00, 0x06, 0x20, 0x01, speed.toByte(), 0xff.toByte()))
|
||||
}
|
||||
|
||||
fun moveBackward(speed: Int = 255) {
|
||||
sendCommand(byteArrayOf(0x00, 0x06, 0x20, 0x02, speed.toByte(), 0xff.toByte()))
|
||||
}
|
||||
|
||||
fun turnLeft(speed: Int = 255) {
|
||||
sendCommand(byteArrayOf(0x00, 0x06, 0x21, 0x00, speed.toByte(), 0xff.toByte()))
|
||||
}
|
||||
|
||||
fun turnRight(speed: Int = 255) {
|
||||
sendCommand(byteArrayOf(0x00, 0x06, 0x20, 0x01, speed.toByte(), 0xff.toByte()))
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
sendCommand(byteArrayOf(0x00, 0x06, 0x20, 0x00, 0x00, 0xff.toByte()))
|
||||
}
|
||||
|
||||
fun onReceivePacket(packet: ByteArray) {
|
||||
|
||||
// 判断数据包格式时使用 toUByte()
|
||||
if (packet[0].toUByte() == CarCommand.PACKET_R_HEAD.toUByte() &&
|
||||
packet.size == packet[1].toUByte().toInt() &&
|
||||
packet[packet[1].toUByte().toInt() - 1].toUByte() == CarCommand.PACKET_R_TAIL.toUByte()) {
|
||||
val command = packet[2].toUByte()
|
||||
val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1)
|
||||
|
||||
Log.d("CarController", "command: 0x%02X".format(command.toInt() and 0xFF))
|
||||
|
||||
// 解析数据包
|
||||
when (command.toUInt()) {
|
||||
// `01 0C E0 电机A_IN_1_2 电机A_PWM 电机B_IN_1_2 电机B_PWM 电机C_IN_1_2 电机C_PWM 电机D_IN_1_2 电机D_PWM FE`
|
||||
CarCommand.CMD_STATUS_MOTOR -> {
|
||||
|
||||
Log.d("CarController", "status motor data: ${data.joinToString(" ") { "0x%02X".format(it) }}")
|
||||
|
||||
// 创建新的状态对象
|
||||
_carState.value = _carState.value.copy(
|
||||
motorAState = MotorState(
|
||||
pwm = data[1].toUByte().toUInt(),
|
||||
in1 = data[0].toUByte().toUInt() and 1u,
|
||||
in2 = (data[0].toUByte().toUInt() shr 1) and 1u
|
||||
),
|
||||
motorBState = MotorState(
|
||||
pwm = data[3].toUByte().toUInt(),
|
||||
in1 = data[2].toUByte().toUInt() and 1u,
|
||||
in2 = (data[2].toUByte().toUInt() shr 1) and 1u
|
||||
),
|
||||
motorCState = MotorState(
|
||||
pwm = data[5].toUByte().toUInt(),
|
||||
in1 = data[4].toUByte().toUInt() and 1u,
|
||||
in2 = (data[4].toUByte().toUInt() shr 1) and 1u
|
||||
),
|
||||
motorDState = MotorState(
|
||||
pwm = data[7].toUByte().toUInt(),
|
||||
in1 = data[6].toUByte().toUInt() and 1u,
|
||||
in2 = (data[6].toUByte().toUInt() shr 1) and 1u
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CarControlState {
|
||||
public var speed: UInt = 0u
|
||||
public var direction: UInt = 0u
|
||||
}
|
|
@ -5,6 +5,7 @@ import android.bluetooth.BluetoothAdapter
|
|||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.le.BluetoothLeScanner
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
|
@ -19,7 +20,6 @@ import android.os.ParcelUuid
|
|||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
|
@ -31,15 +31,12 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.KFunction1
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val serviceUUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
private val rxCharUUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
private val txCharUUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
|
||||
private var bluetoothGatt: BluetoothGatt? = null
|
||||
private var rxCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var bleScanner: BluetoothLeScanner? = null
|
||||
|
||||
// 将deviceList移动到Activity作用域内并使用MutableList
|
||||
|
@ -64,6 +61,10 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private val carController by lazy {
|
||||
CarController { }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -101,31 +102,11 @@ class MainActivity : ComponentActivity() {
|
|||
isConnected = isConnected.value,
|
||||
onStartScan = ::startBleScan,
|
||||
onConnect = ::connectToDevice,
|
||||
onSendCommand = ::sendCommand
|
||||
carController = carController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendCommand(command: ByteArray) {
|
||||
rxCharacteristic?.let { characteristic ->
|
||||
characteristic.value = command;
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.BLUETOOTH_CONNECT
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// 请求蓝牙连接权限
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
|
||||
BLUETOOTH_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
bluetoothGatt?.writeCharacteristic(characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToDevice(result: ScanResult) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
|
@ -145,14 +126,14 @@ class MainActivity : ComponentActivity() {
|
|||
super.onConnectionStateChange(gatt, status, newState)
|
||||
when (newState) {
|
||||
BluetoothGatt.STATE_CONNECTED -> {
|
||||
bluetoothGatt = gatt
|
||||
carController.bluetoothGatt = gatt
|
||||
_isConnected.value = true
|
||||
gatt?.discoverServices()
|
||||
}
|
||||
|
||||
BluetoothGatt.STATE_DISCONNECTED -> {
|
||||
_isConnected.value = false
|
||||
bluetoothGatt = null
|
||||
carController.bluetoothGatt = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,10 +142,30 @@ class MainActivity : ComponentActivity() {
|
|||
super.onServicesDiscovered(gatt, status)
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
gatt?.getService(UUID.fromString(serviceUUID))?.let { service ->
|
||||
rxCharacteristic = service.getCharacteristic(UUID.fromString(rxCharUUID))
|
||||
carController.bluetoothGatt = gatt
|
||||
carController.rxCharacteristic = service.getCharacteristic(UUID.fromString(rxCharUUID))
|
||||
|
||||
// 注册通知监听器
|
||||
val txCharacteristic = service.getCharacteristic(UUID.fromString(txCharUUID))
|
||||
gatt.setCharacteristicNotification(txCharacteristic, true)
|
||||
txCharacteristic.descriptors.forEach { descriptor ->
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
gatt.writeDescriptor(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
) {
|
||||
super.onCharacteristicChanged(gatt, characteristic, value)
|
||||
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
|
||||
carController.onReceivePacket(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -271,12 +272,54 @@ fun GamepadScreen(
|
|||
isConnected: Boolean,
|
||||
onStartScan: () -> Unit,
|
||||
onConnect: (ScanResult) -> Unit,
|
||||
onSendCommand: KFunction1<ByteArray, Unit>
|
||||
carController: CarController
|
||||
) {
|
||||
var showDeviceList by remember { mutableStateOf(false) }
|
||||
var sliderValue by remember { mutableStateOf(0f) }
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
// 电机状态显示区域
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
// 电机A状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("电机A")
|
||||
Text("PWM: ${carController.carState.value.motorAState.pwm}")
|
||||
Text("IN1: ${carController.carState.value.motorAState.in1}")
|
||||
Text("IN2: ${carController.carState.value.motorAState.in2}")
|
||||
}
|
||||
|
||||
// 电机B状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("电机B")
|
||||
Text("PWM: ${carController.carState.value.motorBState.pwm}")
|
||||
Text("IN1: ${carController.carState.value.motorBState.in1}")
|
||||
Text("IN2: ${carController.carState.value.motorBState.in2}")
|
||||
}
|
||||
|
||||
// 电机C状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("电机C")
|
||||
Text("PWM: ${carController.carState.value.motorCState.pwm}")
|
||||
Text("IN1: ${carController.carState.value.motorCState.in1}")
|
||||
Text("IN2: ${carController.carState.value.motorCState.in2}")
|
||||
}
|
||||
|
||||
// 电机D状态
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
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 },
|
||||
|
@ -303,22 +346,12 @@ fun GamepadScreen(
|
|||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
Button(onClick = {
|
||||
onSendCommand(
|
||||
byteArrayOf(
|
||||
0x00, 0x06, 0x21, 0x00, 0x10, 0xff.toByte(),
|
||||
0xff.toByte()
|
||||
)
|
||||
)
|
||||
}) { Text("左转") }
|
||||
Button(onClick = {
|
||||
onSendCommand(
|
||||
byteArrayOf(
|
||||
0x00, 0x06, 0x20, 0x01, 0x10, 0xff.toByte(),
|
||||
0xff.toByte()
|
||||
)
|
||||
)
|
||||
}) { Text("右转") }
|
||||
Button(onClick = { carController.turnLeft() }) {
|
||||
Text("左转")
|
||||
}
|
||||
Button(onClick = { carController.turnRight() }) {
|
||||
Text("右转")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,28 +367,22 @@ fun GamepadScreen(
|
|||
onValueChange = { newValue ->
|
||||
sliderValue = newValue
|
||||
val speed = (newValue * 255).toInt()
|
||||
val command = if (speed >= 0) {
|
||||
byteArrayOf(
|
||||
0x00, 0x06, 0x20, 0x01, speed.toByte(),
|
||||
0xff.toByte()
|
||||
)
|
||||
if (speed > 0) {
|
||||
carController.moveForward(speed)
|
||||
} else if (speed < 0) {
|
||||
carController.moveBackward(-speed)
|
||||
} else {
|
||||
byteArrayOf(
|
||||
0x00, 0x06, 0x20, 0x02, (-speed).toByte(),
|
||||
0xff.toByte()
|
||||
)
|
||||
carController.stop()
|
||||
}
|
||||
onSendCommand(command)
|
||||
},
|
||||
valueRange = -1f..1f,
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.width(200.dp)
|
||||
.padding(vertical = 16.dp)
|
||||
.graphicsLayer(rotationZ = 270f) // 旋转 270 度
|
||||
.graphicsLayer(rotationZ = 270f)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 设备列表对话框
|
||||
|
@ -389,9 +416,7 @@ fun GamepadScreen(
|
|||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { onStartScan() }
|
||||
) {
|
||||
TextButton(onClick = { onStartScan() }) {
|
||||
Text("扫描设备")
|
||||
}
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ espressoCore = "3.6.1"
|
|||
lifecycleRuntimeKtx = "2.8.6"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.04.01"
|
||||
roomKtx = "2.6.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
@ -24,6 +25,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
|||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
Loading…
Reference in New Issue