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