feat: status display

This commit is contained in:
玖叁 2024-12-19 19:48:07 +08:00
parent 2a6712381f
commit 88f618ac55
6 changed files with 245 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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("扫描设备")
}
},

View File

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