feat: basic frame
This commit is contained in:
parent
1f60b1f2ce
commit
55299209fd
|
@ -1,184 +0,0 @@
|
||||||
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_MOTOR_XYR_CONTROL = 0x24u
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
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())
|
|
||||||
val carState: State<CarState> = _carState
|
|
||||||
|
|
||||||
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,
|
|
||||||
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)
|
|
||||||
|
|
||||||
// 添加发送日志
|
|
||||||
_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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun moveBackward(speed: Int = 255) {
|
|
||||||
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_MOVE_CONTROL.toByte(), 0x02, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun turnLeft(speed: Int = 255) {
|
|
||||||
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_STEER_CONTROL.toByte(), 0x00, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun turnRight(speed: Int = 255) {
|
|
||||||
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_STEER_CONTROL.toByte(), 0x01, speed.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x06, CarCommand.CMD_MOTOR_MOVE_CONTROL.toByte(), 0x00, 0x00, CarCommand.PACKET_T_TAIL.toByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendXYR(x: Int, y: Int, r: Int) {
|
|
||||||
sendCommand(byteArrayOf(CarCommand.PACKET_T_HEAD.toByte(), 0x07, CarCommand.CMD_MOTOR_XYR_CONTROL.toByte(), x.toByte(), y.toByte(), r.toByte(), CarCommand.PACKET_T_TAIL.toByte()))
|
|
||||||
}
|
|
||||||
|
|
||||||
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() &&
|
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除日志
|
|
||||||
fun clearLogs() {
|
|
||||||
_logs.value = emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CarControlState {
|
|
||||||
public var speed: UInt = 0u
|
|
||||||
public var direction: UInt = 0u
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package icu.fur93.esp32_car
|
package icu.fur93.esp32_car
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothGatt
|
import android.bluetooth.BluetoothGatt
|
||||||
import android.bluetooth.BluetoothGattCallback
|
import android.bluetooth.BluetoothGattCallback
|
||||||
|
@ -11,7 +12,6 @@ import android.bluetooth.le.ScanCallback
|
||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
import android.bluetooth.le.ScanSettings
|
import android.bluetooth.le.ScanSettings
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -22,22 +22,16 @@ import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
@ -45,50 +39,29 @@ import icu.fur93.esp32_car.page.ControlGamepadModePage
|
||||||
import icu.fur93.esp32_car.page.ControlPage
|
import icu.fur93.esp32_car.page.ControlPage
|
||||||
import icu.fur93.esp32_car.page.ControlPathfinderModePage
|
import icu.fur93.esp32_car.page.ControlPathfinderModePage
|
||||||
import icu.fur93.esp32_car.page.ControlSingleJoystickModePage
|
import icu.fur93.esp32_car.page.ControlSingleJoystickModePage
|
||||||
import icu.fur93.esp32_car.page.GamepadScreen
|
|
||||||
import icu.fur93.esp32_car.page.HomePage
|
import icu.fur93.esp32_car.page.HomePage
|
||||||
import icu.fur93.esp32_car.page.SettingsPage
|
import icu.fur93.esp32_car.page.SettingsPage
|
||||||
import icu.fur93.esp32_car.ui.component.Joystick
|
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||||
import icu.fur93.esp32_car.ui.component.JoystickState
|
|
||||||
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
|
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
|
||||||
import kotlinx.coroutines.delay
|
import icu.fur93.esp32_car.viewmodel.CarControlUseCase
|
||||||
import kotlinx.coroutines.launch
|
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val serviceUUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
private val bluetoothRepository by lazy {
|
||||||
private val rxCharUUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
|
BluetoothRepositoryImpl(this, lifecycleScope)
|
||||||
private val txCharUUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
|
||||||
|
|
||||||
private var bleScanner: BluetoothLeScanner? = null
|
|
||||||
|
|
||||||
// 将deviceList移动到Activity作用域内并使用MutableList
|
|
||||||
private val deviceList = mutableListOf<ScanResult>()
|
|
||||||
|
|
||||||
// 添加可观察的连接状态
|
|
||||||
private val _isConnected = mutableStateOf(false)
|
|
||||||
val isConnected: State<Boolean> = _isConnected
|
|
||||||
|
|
||||||
private val scanCallback = object : ScanCallback() {
|
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
||||||
super.onScanResult(callbackType, result)
|
|
||||||
Log.d("MainActivity", "onScanResult: ${result.device.name} - ${result.device.address}")
|
|
||||||
if (!deviceList.any { it.device.address == result.device.address }) {
|
|
||||||
deviceList.add(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScanFailed(errorCode: Int) {
|
|
||||||
super.onScanFailed(errorCode)
|
|
||||||
Log.e("MainActivity", "扫描失败,错误码: $errorCode")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val carController by lazy {
|
private val carControlUseCase by lazy {
|
||||||
CarController { }
|
CarControlUseCase(bluetoothRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val viewModel by viewModels<CarViewModel> {
|
||||||
|
object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return CarViewModel(carControlUseCase, bluetoothRepository) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -100,18 +73,6 @@ class MainActivity : ComponentActivity() {
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
}
|
}
|
||||||
|
|
||||||
// window.statusBarColor = ContextCompat
|
|
||||||
//
|
|
||||||
// // 隐藏状态栏和导航栏
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// 检查并请求所有必要的权限
|
// 检查并请求所有必要的权限
|
||||||
val requiredPermissions = arrayOf(
|
val requiredPermissions = arrayOf(
|
||||||
Manifest.permission.BLUETOOTH_SCAN,
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
@ -130,158 +91,15 @@ class MainActivity : ComponentActivity() {
|
||||||
missingPermissions.toTypedArray(),
|
missingPermissions.toTypedArray(),
|
||||||
BLUETOOTH_PERMISSION_REQUEST_CODE
|
BLUETOOTH_PERMISSION_REQUEST_CODE
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// 如果已经有所有权限,直接开始扫描
|
|
||||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
|
||||||
bleScanner = bluetoothAdapter?.bluetoothLeScanner
|
|
||||||
startBleScan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
Esp32carTheme {
|
Esp32carTheme {
|
||||||
// GamepadScreen(
|
App(viewModel)
|
||||||
// deviceList = deviceList,
|
|
||||||
// isConnected = isConnected.value,
|
|
||||||
// onStartScan = ::startBleScan,
|
|
||||||
// onConnect = ::connectToDevice,
|
|
||||||
// carController = carController
|
|
||||||
// )
|
|
||||||
App()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectToDevice(result: ScanResult) {
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.BLUETOOTH_CONNECT
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
ActivityCompat.requestPermissions(
|
|
||||||
this,
|
|
||||||
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
|
|
||||||
BLUETOOTH_PERMISSION_REQUEST_CODE
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result.device.connectGatt(this, false, object : BluetoothGattCallback() {
|
|
||||||
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
|
|
||||||
super.onConnectionStateChange(gatt, status, newState)
|
|
||||||
when (newState) {
|
|
||||||
BluetoothGatt.STATE_CONNECTED -> {
|
|
||||||
carController.bluetoothGatt = gatt
|
|
||||||
_isConnected.value = true
|
|
||||||
gatt?.discoverServices()
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothGatt.STATE_DISCONNECTED -> {
|
|
||||||
_isConnected.value = false
|
|
||||||
carController.bluetoothGatt = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
|
||||||
super.onServicesDiscovered(gatt, status)
|
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
||||||
gatt?.getService(UUID.fromString(serviceUUID))?.let { service ->
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startBleScan() {
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.BLUETOOTH_SCAN
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
ActivityCompat.requestPermissions(
|
|
||||||
this,
|
|
||||||
arrayOf(
|
|
||||||
Manifest.permission.BLUETOOTH_SCAN,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
),
|
|
||||||
BLUETOOTH_PERMISSION_REQUEST_CODE
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先停止当前扫描
|
|
||||||
try {
|
|
||||||
bleScanner?.stopScan(scanCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "停止扫描失败: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空之前的扫描结果
|
|
||||||
deviceList.clear()
|
|
||||||
|
|
||||||
Log.d("MainActivity", "开始扫描...")
|
|
||||||
|
|
||||||
// 添加UUID过滤器
|
|
||||||
val scanFilter = ScanFilter.Builder()
|
|
||||||
.setServiceUuid(ParcelUuid.fromString(serviceUUID))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val scanSettings = ScanSettings.Builder()
|
|
||||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
try {
|
|
||||||
// bleScanner?.startScan(listOf(scanFilter), scanSettings, scanCallback)
|
|
||||||
bleScanner?.startScan(scanCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "开始扫描失败: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10秒后停止扫描
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
stopBleScan()
|
|
||||||
Log.d("MainActivity", "停止扫描,发现 ${deviceList.size} 个设备")
|
|
||||||
}, 10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopBleScan() {
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.BLUETOOTH_SCAN
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
bleScanner?.stopScan(scanCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MainActivity", "停止扫描失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加权限请求结果处理
|
// 添加权限请求结果处理
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
requestCode: Int,
|
requestCode: Int,
|
||||||
|
@ -292,28 +110,19 @@ class MainActivity : ComponentActivity() {
|
||||||
if (requestCode == BLUETOOTH_PERMISSION_REQUEST_CODE) {
|
if (requestCode == BLUETOOTH_PERMISSION_REQUEST_CODE) {
|
||||||
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||||
Log.d("MainActivity", "所有权限已获得")
|
Log.d("MainActivity", "所有权限已获得")
|
||||||
// 权限获得后重新初始化蓝牙
|
|
||||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
|
||||||
bleScanner = bluetoothAdapter?.bluetoothLeScanner
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("MainActivity", "部分权限未获得")
|
Log.e("MainActivity", "部分权限未获得")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
// 确保在Activity销毁时停止扫描
|
|
||||||
stopBleScan()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BLUETOOTH_PERMISSION_REQUEST_CODE = 1
|
private const val BLUETOOTH_PERMISSION_REQUEST_CODE = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App(viewModel: CarViewModel) {
|
||||||
val activity = LocalContext.current as MainActivity
|
val activity = LocalContext.current as MainActivity
|
||||||
activity.window.statusBarColor = MaterialTheme.colorScheme.background.toArgb()
|
activity.window.statusBarColor = MaterialTheme.colorScheme.background.toArgb()
|
||||||
|
|
||||||
|
@ -323,9 +132,9 @@ fun App() {
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Route.Home.route
|
startDestination = Route.Home.route
|
||||||
) {
|
) {
|
||||||
composable(Route.Home.route) { HomePage(navController) }
|
composable(Route.Home.route) { HomePage(navController, viewModel) }
|
||||||
composable(Route.Control.route) { ControlPage(navController) }
|
composable(Route.Control.route) { ControlPage(navController, viewModel) }
|
||||||
composable(Route.Settings.route) { SettingsPage(navController) }
|
composable(Route.Settings.route) { SettingsPage(navController, viewModel) }
|
||||||
composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) }
|
composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) }
|
||||||
composable(Route.ControlSingleJoystickMode.route) { ControlSingleJoystickModePage(navController) }
|
composable(Route.ControlSingleJoystickMode.route) { ControlSingleJoystickModePage(navController) }
|
||||||
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) }
|
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) }
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package icu.fur93.esp32_car.entity
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
|
||||||
|
data class BleDevice(
|
||||||
|
val device: BluetoothDevice,
|
||||||
|
val rssi: Int,
|
||||||
|
val scanRecord: ByteArray? = null
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as BleDevice
|
||||||
|
return device.address == other.device.address
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int = device.address.hashCode()
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package icu.fur93.esp32_car.entity
|
||||||
|
|
||||||
|
data class ConnectedDeviceInfo(
|
||||||
|
val name: String = "",
|
||||||
|
val address: String = "",
|
||||||
|
val version: String = ""
|
||||||
|
)
|
|
@ -3,8 +3,9 @@ package icu.fur93.esp32_car.page
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
@ -12,20 +13,29 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import icu.fur93.esp32_car.R
|
import icu.fur93.esp32_car.R
|
||||||
import icu.fur93.esp32_car.Route
|
import icu.fur93.esp32_car.Route
|
||||||
|
import icu.fur93.esp32_car.repository.ConnectionState
|
||||||
|
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
|
||||||
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
|
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
|
||||||
import icu.fur93.esp32_car.ui.component.CardButtonGroup
|
import icu.fur93.esp32_car.ui.component.CardButtonGroup
|
||||||
import icu.fur93.esp32_car.ui.component.CardButtonItem
|
import icu.fur93.esp32_car.ui.component.CardButtonItem
|
||||||
import icu.fur93.esp32_car.ui.component.PageTitle
|
import icu.fur93.esp32_car.ui.component.PageTitle
|
||||||
import icu.fur93.esp32_car.ui.layout.MainLayout
|
import icu.fur93.esp32_car.ui.layout.MainLayout
|
||||||
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ControlPage(navController: NavHostController) {
|
fun ControlPage(navController: NavHostController, viewModel: CarViewModel) {
|
||||||
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
|
|
||||||
MainLayout(
|
MainLayout(
|
||||||
content = {
|
content = {
|
||||||
Column (LayoutContentModifier) {
|
Column (LayoutContentModifier) {
|
||||||
PageTitle("控制")
|
PageTitle("控制")
|
||||||
ControlPageStatusCard()
|
if (connectionState == ConnectionState.CONNECTED) {
|
||||||
|
ControlPageStatusCard()
|
||||||
|
} else {
|
||||||
|
UnconnectedStatusCard(viewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package icu.fur93.esp32_car.page
|
package icu.fur93.esp32_car.page
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import icu.fur93.esp32_car.Route
|
import icu.fur93.esp32_car.Route
|
||||||
|
import icu.fur93.esp32_car.repository.ConnectionState
|
||||||
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
|
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
|
||||||
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
|
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
|
||||||
import icu.fur93.esp32_car.ui.carditem.StatusCardInfraredStatus
|
import icu.fur93.esp32_car.ui.carditem.StatusCardInfraredStatus
|
||||||
|
@ -16,15 +17,21 @@ import icu.fur93.esp32_car.ui.component.PageTitle
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCard
|
import icu.fur93.esp32_car.ui.component.StatusCard
|
||||||
import icu.fur93.esp32_car.ui.layout.MainLayout
|
import icu.fur93.esp32_car.ui.layout.MainLayout
|
||||||
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage(navController: NavHostController) {
|
fun HomePage(navController: NavHostController, viewModel: CarViewModel) {
|
||||||
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
|
|
||||||
MainLayout(
|
MainLayout(
|
||||||
content = {
|
content = {
|
||||||
Column (LayoutContentModifier) {
|
Column(LayoutContentModifier) {
|
||||||
PageTitle("标题还没想好")
|
PageTitle("标题还没想好")
|
||||||
HomePageStatusCard()
|
if (connectionState == ConnectionState.CONNECTED) {
|
||||||
// UnconnectedStatusCard()
|
HomePageStatusCard(viewModel)
|
||||||
|
} else {
|
||||||
|
UnconnectedStatusCard(viewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
@ -37,14 +44,16 @@ fun HomePage(navController: NavHostController) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePageStatusCard() {
|
fun HomePageStatusCard(viewModel: CarViewModel) {
|
||||||
|
val deviceInfo by viewModel.deviceInfo.collectAsState()
|
||||||
|
val carState by viewModel.carState.collectAsState()
|
||||||
|
|
||||||
StatusCard(
|
StatusCard(
|
||||||
cardItems = listOf(
|
cardItems = listOf(
|
||||||
StatusCardInfo(),
|
StatusCardInfo(deviceInfo),
|
||||||
StatusCardMotorStatus(),
|
StatusCardMotorStatus(carState),
|
||||||
StatusCardInfraredStatus(),
|
StatusCardInfraredStatus(carState.infraredState),
|
||||||
StatusCardUltrasoundStatus()
|
StatusCardUltrasoundStatus(carState.ultrasoundState)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import icu.fur93.esp32_car.CarController
|
|
||||||
import icu.fur93.esp32_car.LogDirection
|
|
||||||
import icu.fur93.esp32_car.ui.component.Joystick
|
import icu.fur93.esp32_car.ui.component.Joystick
|
||||||
import icu.fur93.esp32_car.ui.component.JoystickState
|
import icu.fur93.esp32_car.ui.component.JoystickState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
|
@ -1,27 +1,39 @@
|
||||||
package icu.fur93.esp32_car.page
|
package icu.fur93.esp32_car.page
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import icu.fur93.esp32_car.R
|
import icu.fur93.esp32_car.R
|
||||||
import icu.fur93.esp32_car.Route
|
import icu.fur93.esp32_car.Route
|
||||||
|
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||||
|
import icu.fur93.esp32_car.repository.ConnectionState
|
||||||
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
|
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
|
||||||
import icu.fur93.esp32_car.ui.component.PageTitle
|
import icu.fur93.esp32_car.ui.component.PageTitle
|
||||||
|
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
|
||||||
import icu.fur93.esp32_car.ui.layout.MainLayout
|
import icu.fur93.esp32_car.ui.layout.MainLayout
|
||||||
import icu.fur93.esp32_car.ui.theme.LayoutContentPadding
|
import icu.fur93.esp32_car.ui.theme.LayoutContentPadding
|
||||||
import icu.fur93.esp32_car.ui.theme.LayoutTopPadding
|
import icu.fur93.esp32_car.ui.theme.LayoutTopPadding
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsPage(navController: NavHostController) {
|
fun SettingsPage(navController: NavHostController, viewModel: CarViewModel) {
|
||||||
MainLayout(
|
MainLayout(
|
||||||
content = {
|
content = {
|
||||||
Column {
|
Column {
|
||||||
|
@ -34,7 +46,7 @@ fun SettingsPage(navController: NavHostController) {
|
||||||
) {
|
) {
|
||||||
PageTitle("设置")
|
PageTitle("设置")
|
||||||
}
|
}
|
||||||
SettingsList()
|
SettingsList(viewModel)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
@ -47,16 +59,10 @@ fun SettingsPage(navController: NavHostController) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsList() {
|
fun SettingsList(viewModel: CarViewModel) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
item {
|
item {
|
||||||
ListItem(
|
SettingConnectDeviceItem(viewModel)
|
||||||
headlineContent = { Text("连接设备") },
|
|
||||||
supportingContent = { Text("已连接") },
|
|
||||||
trailingContent = {
|
|
||||||
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "连接设备")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
|
@ -80,5 +86,74 @@ fun SettingsList() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("PID 参数") },
|
||||||
|
trailingContent = {
|
||||||
|
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "PID 参数")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingConnectDeviceItem(viewModel: CarViewModel) {
|
||||||
|
var showScanDialog by remember { mutableStateOf(false) }
|
||||||
|
var showDisconnectDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
|
val deviceInfo by viewModel.deviceInfo.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("连接设备") },
|
||||||
|
supportingContent = { if (connectionState == ConnectionState.CONNECTED) Text("已连接") else Text("未连接") },
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
ImageVector.vectorResource(R.drawable.arrow_right_24),
|
||||||
|
contentDescription = "连接设备"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
if (connectionState == ConnectionState.CONNECTED) {
|
||||||
|
showDisconnectDialog = true
|
||||||
|
} else if (connectionState == ConnectionState.DISCONNECTED) {
|
||||||
|
showScanDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 扫描设备对话框
|
||||||
|
BleDeviceScanDialog(
|
||||||
|
isVisible = showScanDialog,
|
||||||
|
onDismiss = { showScanDialog = false },
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
|
||||||
|
// 断开连接确认对话框
|
||||||
|
if (showDisconnectDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDisconnectDialog = false },
|
||||||
|
title = { Text("断开连接") },
|
||||||
|
text = { Text("确定要断开与 ${deviceInfo.name} 的连接吗?") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
(viewModel.repository as BluetoothRepositoryImpl).disconnect()
|
||||||
|
showDisconnectDialog = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("确定")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { showDisconnectDialog = false }
|
||||||
|
) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
package icu.fur93.esp32_car.repository
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCallback
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.bluetooth.le.ScanCallback
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.bluetooth.le.ScanSettings
|
||||||
|
import android.content.Context
|
||||||
|
import icu.fur93.esp32_car.entity.BleDevice
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarCommands
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarState
|
||||||
|
import icu.fur93.esp32_car.viewmodel.LogDirection
|
||||||
|
import icu.fur93.esp32_car.viewmodel.LogEntry
|
||||||
|
import icu.fur93.esp32_car.viewmodel.MotorState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
// 蓝牙通信接口
|
||||||
|
interface BluetoothRepository {
|
||||||
|
fun sendCommand(command: ByteArray)
|
||||||
|
fun observeCarState(): Flow<CarState>
|
||||||
|
fun observeLogs(): Flow<List<LogEntry>>
|
||||||
|
fun clearLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加连接状态枚举
|
||||||
|
enum class ConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
DISCONNECTING
|
||||||
|
}
|
||||||
|
|
||||||
|
// 蓝牙通信实现
|
||||||
|
class BluetoothRepositoryImpl(
|
||||||
|
private val context: Context,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) : BluetoothRepository {
|
||||||
|
private var bluetoothGatt: BluetoothGatt? = null
|
||||||
|
private var rxCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
|
private val _carState = MutableStateFlow(CarState())
|
||||||
|
private val _logs = MutableStateFlow<List<LogEntry>>(emptyList())
|
||||||
|
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter? =
|
||||||
|
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
|
||||||
|
|
||||||
|
private val _scanResults = MutableStateFlow<List<BleDevice>>(emptyList())
|
||||||
|
val scanResults: StateFlow<List<BleDevice>> = _scanResults.asStateFlow()
|
||||||
|
|
||||||
|
private val _isScanning = MutableStateFlow(false)
|
||||||
|
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||||
|
|
||||||
|
// 连接状态流
|
||||||
|
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||||
|
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
|
private val scanCallback = object : ScanCallback() {
|
||||||
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
val bleDevice = BleDevice(
|
||||||
|
device = result.device,
|
||||||
|
rssi = result.rssi,
|
||||||
|
scanRecord = result.scanRecord?.bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
val currentList = _scanResults.value.toMutableList()
|
||||||
|
val index = currentList.indexOfFirst { it.device.address == bleDevice.device.address }
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
currentList[index] = bleDevice
|
||||||
|
} else {
|
||||||
|
currentList.add(bleDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按信号强度排序
|
||||||
|
_scanResults.value = currentList.sortedByDescending { it.rssi }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun startScan() {
|
||||||
|
if (_isScanning.value) return
|
||||||
|
|
||||||
|
_scanResults.value = emptyList()
|
||||||
|
_isScanning.value = true
|
||||||
|
|
||||||
|
val scanner = bluetoothAdapter?.bluetoothLeScanner
|
||||||
|
val settings = ScanSettings.Builder()
|
||||||
|
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
scanner?.startScan(null, settings, scanCallback)
|
||||||
|
|
||||||
|
// 10秒后自动停止扫描
|
||||||
|
scope.launch {
|
||||||
|
delay(10000)
|
||||||
|
stopScan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun stopScan() {
|
||||||
|
if (!_isScanning.value) return
|
||||||
|
|
||||||
|
_isScanning.value = false
|
||||||
|
bluetoothAdapter?.bluetoothLeScanner?.stopScan(scanCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun connectToDevice(device: BluetoothDevice) {
|
||||||
|
_connectionState.value = ConnectionState.CONNECTING
|
||||||
|
|
||||||
|
device.connectGatt(
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
object : BluetoothGattCallback() {
|
||||||
|
override fun onConnectionStateChange(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
status: Int,
|
||||||
|
newState: Int
|
||||||
|
) {
|
||||||
|
when (newState) {
|
||||||
|
BluetoothProfile.STATE_CONNECTED -> {
|
||||||
|
bluetoothGatt = gatt
|
||||||
|
_connectionState.value = ConnectionState.CONNECTED
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
gatt.discoverServices()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BluetoothProfile.STATE_CONNECTING -> {
|
||||||
|
_connectionState.value = ConnectionState.CONNECTING
|
||||||
|
}
|
||||||
|
BluetoothProfile.STATE_DISCONNECTING -> {
|
||||||
|
_connectionState.value = ConnectionState.DISCONNECTING
|
||||||
|
}
|
||||||
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
|
bluetoothGatt = null
|
||||||
|
rxCharacteristic = null
|
||||||
|
_connectionState.value = ConnectionState.DISCONNECTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
// 找到特定的服务和特征
|
||||||
|
gatt.services?.forEach { service ->
|
||||||
|
service.characteristics?.forEach { characteristic ->
|
||||||
|
// 这里需要根据你的设备具体的UUID来匹配
|
||||||
|
if (characteristic.uuid.toString() == "YOUR_CHARACTERISTIC_UUID") {
|
||||||
|
rxCharacteristic = characteristic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicChanged(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
onReceivePacket(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun sendCommand(command: ByteArray) {
|
||||||
|
rxCharacteristic?.let { characteristic ->
|
||||||
|
characteristic.value = command
|
||||||
|
bluetoothGatt?.writeCharacteristic(characteristic)
|
||||||
|
addLog(LogDirection.SEND, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun observeCarState(): Flow<CarState> = _carState.asStateFlow()
|
||||||
|
override fun observeLogs(): Flow<List<LogEntry>> = _logs.asStateFlow()
|
||||||
|
override fun clearLogs() {
|
||||||
|
_logs.value = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLog(direction: LogDirection, data: ByteArray) {
|
||||||
|
_logs.value += LogEntry(
|
||||||
|
direction = direction,
|
||||||
|
data = data.joinToString(" ") { "0x%02X".format(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onReceivePacket(packet: ByteArray) {
|
||||||
|
addLog(LogDirection.RECEIVE, packet)
|
||||||
|
parsePacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePacket(packet: ByteArray) {
|
||||||
|
if (!isValidPacket(packet)) return
|
||||||
|
|
||||||
|
when (packet[2].toUByte().toUInt()) {
|
||||||
|
CarCommands.CMD_STATUS_MOTOR -> updateMotorStatus(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidPacket(packet: ByteArray): Boolean =
|
||||||
|
packet[0].toUByte() == CarCommands.PACKET_R_HEAD.toUByte() &&
|
||||||
|
packet.size == packet[1].toUByte().toInt() &&
|
||||||
|
packet[packet[1].toUByte().toInt() - 1].toUByte() == CarCommands.PACKET_R_TAIL.toUByte()
|
||||||
|
|
||||||
|
private fun updateMotorStatus(packet: ByteArray) {
|
||||||
|
val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1)
|
||||||
|
_carState.value = _carState.value.copy(
|
||||||
|
motorAState = createMotorState(data, 0),
|
||||||
|
motorBState = createMotorState(data, 2),
|
||||||
|
motorCState = createMotorState(data, 4),
|
||||||
|
motorDState = createMotorState(data, 6)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMotorState(data: ByteArray, offset: Int) = MotorState(
|
||||||
|
pwm = data[offset + 1].toUByte().toUInt(),
|
||||||
|
in1 = data[offset].toUByte().toUInt() and 1u,
|
||||||
|
in2 = (data[offset].toUByte().toUInt() shr 1) and 1u
|
||||||
|
)
|
||||||
|
|
||||||
|
// 断开连接方法
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun disconnect() {
|
||||||
|
bluetoothGatt?.let { gatt ->
|
||||||
|
_connectionState.value = ConnectionState.DISCONNECTING
|
||||||
|
gatt.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,20 @@ package icu.fur93.esp32_car.ui.card
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import icu.fur93.esp32_car.ui.component.CardItem
|
import icu.fur93.esp32_car.ui.component.CardItem
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCard
|
import icu.fur93.esp32_car.ui.component.StatusCard
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||||
|
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UnconnectedStatusCard() {
|
fun UnconnectedStatusCard(viewModel: CarViewModel) {
|
||||||
|
var showScanDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
StatusCard(
|
StatusCard(
|
||||||
cardItems = listOf(
|
cardItems = listOf(
|
||||||
CardItem(
|
CardItem(
|
||||||
|
@ -21,11 +29,17 @@ fun UnconnectedStatusCard() {
|
||||||
bottomControl = {
|
bottomControl = {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
showScanDialog = true
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text("连接设备")
|
Text("连接设备")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BleDeviceScanDialog(
|
||||||
|
isVisible = showScanDialog,
|
||||||
|
onDismiss = { showScanDialog = false },
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -2,18 +2,19 @@ package icu.fur93.esp32_car.ui.carditem
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import icu.fur93.esp32_car.entity.ConnectedDeviceInfo
|
||||||
import icu.fur93.esp32_car.ui.component.CardItem
|
import icu.fur93.esp32_car.ui.component.CardItem
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
@SuppressLint("ComposableNaming")
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusCardInfo () : CardItem {
|
fun StatusCardInfo(deviceInfo: ConnectedDeviceInfo) : CardItem {
|
||||||
return CardItem(
|
return CardItem(
|
||||||
title = "当前连接",
|
title = "当前连接",
|
||||||
content = {
|
content = {
|
||||||
StatusCardItemText("名称 Name")
|
StatusCardItemText("名称 ${deviceInfo.name}")
|
||||||
StatusCardItemText("Mac XX:XX:XX:XX:XX:XX")
|
StatusCardItemText("Mac ${deviceInfo.address}")
|
||||||
StatusCardItemText("版本 001")
|
StatusCardItemText("版本 ${deviceInfo.version}")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -8,20 +8,25 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import icu.fur93.esp32_car.ui.component.CardItem
|
import icu.fur93.esp32_car.ui.component.CardItem
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||||
|
import icu.fur93.esp32_car.viewmodel.InfraredState
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
@SuppressLint("ComposableNaming")
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusCardInfraredStatus () : CardItem {
|
fun StatusCardInfraredStatus(infraredState: InfraredState): CardItem {
|
||||||
return CardItem(
|
return CardItem(
|
||||||
title = "红外模块状态",
|
title = "红外模块状态",
|
||||||
content = {
|
content = {
|
||||||
Row(
|
if (infraredState.enable) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
listOf(0, 0, 0, 0, 0).forEach { status ->
|
) {
|
||||||
StatusCardItemText(status.toString())
|
infraredState.statusList.forEach { status ->
|
||||||
|
StatusCardItemText(status.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
StatusCardItemText("未启用")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,22 +8,27 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import icu.fur93.esp32_car.ui.component.CardItem
|
import icu.fur93.esp32_car.ui.component.CardItem
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||||
|
import icu.fur93.esp32_car.viewmodel.UltrasoundState
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
@SuppressLint("ComposableNaming")
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusCardUltrasoundStatus () : CardItem {
|
fun StatusCardUltrasoundStatus(ultrasoundState: UltrasoundState): CardItem {
|
||||||
return CardItem(
|
return CardItem(
|
||||||
title = "超声波状态",
|
title = "超声波状态",
|
||||||
content = {
|
content = {
|
||||||
Row(
|
if (ultrasoundState.enable) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
Row(
|
||||||
) {
|
modifier = Modifier.fillMaxWidth()
|
||||||
Column(Modifier.weight(1f)) {
|
) {
|
||||||
StatusCardItemText("舵机角度 90")
|
Column(Modifier.weight(1f)) {
|
||||||
}
|
StatusCardItemText("舵机角度 ${ultrasoundState.servoAngle}")
|
||||||
Column(Modifier.weight(1f)) {
|
}
|
||||||
StatusCardItemText("舵机角度 20mm")
|
Column(Modifier.weight(1f)) {
|
||||||
|
StatusCardItemText("障碍距离 ${ultrasoundState.distance}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
StatusCardItemText("未启用")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,10 +6,11 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import icu.fur93.esp32_car.ui.component.CardItem
|
import icu.fur93.esp32_car.ui.component.CardItem
|
||||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarState
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
@SuppressLint("ComposableNaming")
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusCardMotorStatus () : CardItem {
|
fun StatusCardMotorStatus (carState: CarState) : CardItem {
|
||||||
return CardItem(
|
return CardItem(
|
||||||
title = "电机状态",
|
title = "电机状态",
|
||||||
content = {
|
content = {
|
||||||
|
@ -17,16 +18,16 @@ fun StatusCardMotorStatus () : CardItem {
|
||||||
columns = GridCells.Fixed(2)
|
columns = GridCells.Fixed(2)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
StatusCardItemText("A 0 1 255")
|
StatusCardItemText("A ${carState.motorAState.in1} ${carState.motorAState.in2} ${carState.motorAState.pwm}")
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
StatusCardItemText("D 0 1 255")
|
StatusCardItemText("D ${carState.motorDState.in1} ${carState.motorDState.in2} ${carState.motorDState.pwm}")
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
StatusCardItemText("B 1 0 255")
|
StatusCardItemText("B ${carState.motorBState.in1} ${carState.motorBState.in2} ${carState.motorBState.pwm}")
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
StatusCardItemText("C 1 0 255")
|
StatusCardItemText("C ${carState.motorCState.in1} ${carState.motorCState.in2} ${carState.motorCState.pwm}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
package icu.fur93.esp32_car.ui.dialog
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import icu.fur93.esp32_car.entity.BleDevice
|
||||||
|
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BleDeviceScanDialog(
|
||||||
|
isVisible: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
viewModel: CarViewModel
|
||||||
|
) {
|
||||||
|
val scanResults by viewModel.scanResults.collectAsState()
|
||||||
|
val isScanning by viewModel.isScanning.collectAsState()
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("扫描蓝牙设备")
|
||||||
|
if (isScanning) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
LazyColumn {
|
||||||
|
scanResults.forEach { bleDevice ->
|
||||||
|
item {
|
||||||
|
DeviceItem(
|
||||||
|
bleDevice = bleDevice,
|
||||||
|
onClick = {
|
||||||
|
viewModel.connectToDevice(bleDevice.device)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (isScanning) viewModel.stopScan()
|
||||||
|
else viewModel.startScan()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(if (isScanning) "停止扫描" else "开始扫描")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Composable
|
||||||
|
private fun DeviceItem(
|
||||||
|
bleDevice: BleDevice,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = bleDevice.device.name ?: "Unknown Device",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = bleDevice.device.address,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "${bleDevice.rssi} dBm",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
package icu.fur93.esp32_car.viewmodel
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import icu.fur93.esp32_car.entity.BleDevice
|
||||||
|
import icu.fur93.esp32_car.entity.ConnectedDeviceInfo
|
||||||
|
import icu.fur93.esp32_car.repository.BluetoothRepository
|
||||||
|
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||||
|
import icu.fur93.esp32_car.repository.ConnectionState
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
// 数据模型
|
||||||
|
data class MotorState(
|
||||||
|
val pwm: UInt = 0u,
|
||||||
|
val in1: UInt = 0u,
|
||||||
|
val in2: UInt = 0u
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InfraredState(
|
||||||
|
val enable: Boolean = false,
|
||||||
|
val statusList: List<UInt> = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UltrasoundState(
|
||||||
|
val enable: Boolean = false,
|
||||||
|
val servoAngle: UInt = 0u,
|
||||||
|
val distance: UInt = 0u
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CarState(
|
||||||
|
val controlState: CarControlState = CarControlState(),
|
||||||
|
val motorAState: MotorState = MotorState(),
|
||||||
|
val motorBState: MotorState = MotorState(),
|
||||||
|
val motorCState: MotorState = MotorState(),
|
||||||
|
val motorDState: MotorState = MotorState(),
|
||||||
|
val infraredState: InfraredState = InfraredState(),
|
||||||
|
val ultrasoundState: UltrasoundState = UltrasoundState()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CarControlState(
|
||||||
|
val speed: UInt = 0u,
|
||||||
|
val direction: UInt = 0u
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LogEntry(
|
||||||
|
val timestamp: Long = System.currentTimeMillis(),
|
||||||
|
val direction: LogDirection,
|
||||||
|
val data: String
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class LogDirection {
|
||||||
|
SEND,
|
||||||
|
RECEIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令常量
|
||||||
|
object CarCommands {
|
||||||
|
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_MOTOR_XYR_CONTROL = 0x24u
|
||||||
|
const val CMD_DEMO_PID = 0xf0u
|
||||||
|
const val CMD_DEMO_PATH = 0xf1u
|
||||||
|
const val CMD_STATUS_MOTOR = 0xE0u
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 用例
|
||||||
|
class CarControlUseCase(
|
||||||
|
private val repository: BluetoothRepository
|
||||||
|
) {
|
||||||
|
fun moveForward(speed: Int = 255) = repository.sendCommand(
|
||||||
|
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x01, speed)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun moveBackward(speed: Int = 255) = repository.sendCommand(
|
||||||
|
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x02, speed)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun turnLeft(speed: Int = 255) = repository.sendCommand(
|
||||||
|
buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x00, speed)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun turnRight(speed: Int = 255) = repository.sendCommand(
|
||||||
|
buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x01, speed)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun stop() = repository.sendCommand(
|
||||||
|
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x00, 0x00)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun sendXYR(x: Int, y: Int, r: Int) = repository.sendCommand(
|
||||||
|
byteArrayOf(
|
||||||
|
CarCommands.PACKET_T_HEAD.toByte(),
|
||||||
|
0x07,
|
||||||
|
CarCommands.CMD_MOTOR_XYR_CONTROL.toByte(),
|
||||||
|
x.toByte(),
|
||||||
|
y.toByte(),
|
||||||
|
r.toByte(),
|
||||||
|
CarCommands.PACKET_T_TAIL.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun buildCommand(cmd: UInt, param1: Int, param2: Int) = byteArrayOf(
|
||||||
|
CarCommands.PACKET_T_HEAD.toByte(),
|
||||||
|
0x06,
|
||||||
|
cmd.toByte(),
|
||||||
|
param1.toByte(),
|
||||||
|
param2.toByte(),
|
||||||
|
CarCommands.PACKET_T_TAIL.toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CarViewModel(
|
||||||
|
private val carControlUseCase: CarControlUseCase,
|
||||||
|
val repository: BluetoothRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
val scanResults: StateFlow<List<BleDevice>> =
|
||||||
|
(repository as BluetoothRepositoryImpl).scanResults
|
||||||
|
|
||||||
|
val isScanning: StateFlow<Boolean> =
|
||||||
|
(repository as BluetoothRepositoryImpl).isScanning
|
||||||
|
|
||||||
|
fun startScan() = (repository as BluetoothRepositoryImpl).startScan()
|
||||||
|
fun stopScan() = (repository as BluetoothRepositoryImpl).stopScan()
|
||||||
|
fun connectToDevice(device: BluetoothDevice) {
|
||||||
|
updateDeviceInfo(device)
|
||||||
|
(repository as BluetoothRepositoryImpl).connectToDevice(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
val carState: StateFlow<CarState> = repository.observeCarState()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CarState())
|
||||||
|
|
||||||
|
val logs: StateFlow<List<LogEntry>> = repository.observeLogs()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||||
|
|
||||||
|
// 设备信息状态
|
||||||
|
private val _deviceInfo = MutableStateFlow(ConnectedDeviceInfo())
|
||||||
|
val deviceInfo: StateFlow<ConnectedDeviceInfo> = _deviceInfo.asStateFlow()
|
||||||
|
|
||||||
|
val connectionState: StateFlow<ConnectionState> =
|
||||||
|
(repository as BluetoothRepositoryImpl).connectionState
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = ConnectionState.DISCONNECTED
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// 监听连接状态变化
|
||||||
|
viewModelScope.launch {
|
||||||
|
connectionState.collect { state ->
|
||||||
|
if (state == ConnectionState.DISCONNECTED) {
|
||||||
|
// 断开连接时清空设备信息
|
||||||
|
_deviceInfo.value = ConnectedDeviceInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备信息的方法
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun updateDeviceInfo(device: BluetoothDevice) {
|
||||||
|
_deviceInfo.value = ConnectedDeviceInfo(
|
||||||
|
name = device.name ?: "未知设备",
|
||||||
|
address = device.address,
|
||||||
|
version = "001"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveForward(speed: Int = 255) = carControlUseCase.moveForward(speed)
|
||||||
|
fun moveBackward(speed: Int = 255) = carControlUseCase.moveBackward(speed)
|
||||||
|
fun turnLeft(speed: Int = 255) = carControlUseCase.turnLeft(speed)
|
||||||
|
fun turnRight(speed: Int = 255) = carControlUseCase.turnRight(speed)
|
||||||
|
fun stop() = carControlUseCase.stop()
|
||||||
|
fun sendXYR(x: Int, y: Int, r: Int) = carControlUseCase.sendXYR(x, y, r)
|
||||||
|
fun clearLogs() = repository.clearLogs()
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
package icu.fur93.esp32_car.viewmodel
|
|
||||||
|
|
Loading…
Reference in New Issue