diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..c2d1443 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6eeb3d7..cf9c930 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8b73cd9..6601d5a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,10 @@
+
+
Unit) {
+ // 使用 MutableState 来存储小车状态
+ private val _carState = mutableStateOf(CarState())
+ val carState: State = _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
+}
diff --git a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt
index 6c3c9ff..db5185b 100644
--- a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt
+++ b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt
@@ -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
+ 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("扫描设备")
}
},
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2732d54..d38ea32 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }