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
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
|
@ -11,7 +12,6 @@ import android.bluetooth.le.ScanCallback
|
|||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -22,22 +22,16 @@ import android.util.Log
|
|||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
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.ControlPathfinderModePage
|
||||
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.SettingsPage
|
||||
import icu.fur93.esp32_car.ui.component.Joystick
|
||||
import icu.fur93.esp32_car.ui.component.JoystickState
|
||||
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import icu.fur93.esp32_car.viewmodel.CarControlUseCase
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
import java.util.UUID
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
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 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)
|
||||
}
|
||||
private val bluetoothRepository by lazy {
|
||||
BluetoothRepositoryImpl(this, lifecycleScope)
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
super.onScanFailed(errorCode)
|
||||
Log.e("MainActivity", "扫描失败,错误码: $errorCode")
|
||||
}
|
||||
private val carControlUseCase by lazy {
|
||||
CarControlUseCase(bluetoothRepository)
|
||||
}
|
||||
|
||||
private val carController by lazy {
|
||||
CarController { }
|
||||
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?) {
|
||||
|
@ -100,18 +73,6 @@ class MainActivity : ComponentActivity() {
|
|||
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(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
|
@ -130,158 +91,15 @@ class MainActivity : ComponentActivity() {
|
|||
missingPermissions.toTypedArray(),
|
||||
BLUETOOTH_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
} else {
|
||||
// 如果已经有所有权限,直接开始扫描
|
||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||
bleScanner = bluetoothAdapter?.bluetoothLeScanner
|
||||
startBleScan()
|
||||
}
|
||||
|
||||
setContent {
|
||||
Esp32carTheme {
|
||||
// GamepadScreen(
|
||||
// deviceList = deviceList,
|
||||
// isConnected = isConnected.value,
|
||||
// onStartScan = ::startBleScan,
|
||||
// onConnect = ::connectToDevice,
|
||||
// carController = carController
|
||||
// )
|
||||
App()
|
||||
App(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
requestCode: Int,
|
||||
|
@ -292,28 +110,19 @@ class MainActivity : ComponentActivity() {
|
|||
if (requestCode == BLUETOOTH_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||
Log.d("MainActivity", "所有权限已获得")
|
||||
// 权限获得后重新初始化蓝牙
|
||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||
bleScanner = bluetoothAdapter?.bluetoothLeScanner
|
||||
} else {
|
||||
Log.e("MainActivity", "部分权限未获得")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// 确保在Activity销毁时停止扫描
|
||||
stopBleScan()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BLUETOOTH_PERMISSION_REQUEST_CODE = 1
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun App() {
|
||||
fun App(viewModel: CarViewModel) {
|
||||
val activity = LocalContext.current as MainActivity
|
||||
activity.window.statusBarColor = MaterialTheme.colorScheme.background.toArgb()
|
||||
|
||||
|
@ -323,9 +132,9 @@ fun App() {
|
|||
navController = navController,
|
||||
startDestination = Route.Home.route
|
||||
) {
|
||||
composable(Route.Home.route) { HomePage(navController) }
|
||||
composable(Route.Control.route) { ControlPage(navController) }
|
||||
composable(Route.Settings.route) { SettingsPage(navController) }
|
||||
composable(Route.Home.route) { HomePage(navController, viewModel) }
|
||||
composable(Route.Control.route) { ControlPage(navController, viewModel) }
|
||||
composable(Route.Settings.route) { SettingsPage(navController, viewModel) }
|
||||
composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) }
|
||||
composable(Route.ControlSingleJoystickMode.route) { ControlSingleJoystickModePage(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.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
|
@ -12,20 +13,29 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.navigation.NavHostController
|
||||
import icu.fur93.esp32_car.R
|
||||
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.CardButtonGroup
|
||||
import icu.fur93.esp32_car.ui.component.CardButtonItem
|
||||
import icu.fur93.esp32_car.ui.component.PageTitle
|
||||
import icu.fur93.esp32_car.ui.layout.MainLayout
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
|
||||
@Composable
|
||||
fun ControlPage(navController: NavHostController) {
|
||||
fun ControlPage(navController: NavHostController, viewModel: CarViewModel) {
|
||||
val connectionState by viewModel.connectionState.collectAsState()
|
||||
|
||||
MainLayout(
|
||||
content = {
|
||||
Column (LayoutContentModifier) {
|
||||
PageTitle("控制")
|
||||
if (connectionState == ConnectionState.CONNECTED) {
|
||||
ControlPageStatusCard()
|
||||
} else {
|
||||
UnconnectedStatusCard(viewModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package icu.fur93.esp32_car.page
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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 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.carditem.StatusCardInfo
|
||||
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.layout.MainLayout
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
|
||||
@Composable
|
||||
fun HomePage(navController: NavHostController) {
|
||||
fun HomePage(navController: NavHostController, viewModel: CarViewModel) {
|
||||
val connectionState by viewModel.connectionState.collectAsState()
|
||||
|
||||
MainLayout(
|
||||
content = {
|
||||
Column (LayoutContentModifier) {
|
||||
Column(LayoutContentModifier) {
|
||||
PageTitle("标题还没想好")
|
||||
HomePageStatusCard()
|
||||
// UnconnectedStatusCard()
|
||||
if (connectionState == ConnectionState.CONNECTED) {
|
||||
HomePageStatusCard(viewModel)
|
||||
} else {
|
||||
UnconnectedStatusCard(viewModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
|
@ -37,14 +44,16 @@ fun HomePage(navController: NavHostController) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun HomePageStatusCard() {
|
||||
fun HomePageStatusCard(viewModel: CarViewModel) {
|
||||
val deviceInfo by viewModel.deviceInfo.collectAsState()
|
||||
val carState by viewModel.carState.collectAsState()
|
||||
|
||||
StatusCard(
|
||||
cardItems = listOf(
|
||||
StatusCardInfo(),
|
||||
StatusCardMotorStatus(),
|
||||
StatusCardInfraredStatus(),
|
||||
StatusCardUltrasoundStatus()
|
||||
StatusCardInfo(deviceInfo),
|
||||
StatusCardMotorStatus(carState),
|
||||
StatusCardInfraredStatus(carState.infraredState),
|
||||
StatusCardUltrasoundStatus(carState.ultrasoundState)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
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.JoystickState
|
||||
import kotlinx.coroutines.delay
|
|
@ -1,27 +1,39 @@
|
|||
package icu.fur93.esp32_car.page
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Switch
|
||||
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.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.navigation.NavHostController
|
||||
import icu.fur93.esp32_car.R
|
||||
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.PageTitle
|
||||
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
|
||||
import icu.fur93.esp32_car.ui.layout.MainLayout
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutContentPadding
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutTopPadding
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
|
||||
@Composable
|
||||
fun SettingsPage(navController: NavHostController) {
|
||||
fun SettingsPage(navController: NavHostController, viewModel: CarViewModel) {
|
||||
MainLayout(
|
||||
content = {
|
||||
Column {
|
||||
|
@ -34,7 +46,7 @@ fun SettingsPage(navController: NavHostController) {
|
|||
) {
|
||||
PageTitle("设置")
|
||||
}
|
||||
SettingsList()
|
||||
SettingsList(viewModel)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
|
@ -47,16 +59,10 @@ fun SettingsPage(navController: NavHostController) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsList() {
|
||||
fun SettingsList(viewModel: CarViewModel) {
|
||||
LazyColumn {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text("连接设备") },
|
||||
supportingContent = { Text("已连接") },
|
||||
trailingContent = {
|
||||
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "连接设备")
|
||||
}
|
||||
)
|
||||
SettingConnectDeviceItem(viewModel)
|
||||
}
|
||||
item {
|
||||
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.Text
|
||||
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.StatusCard
|
||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
|
||||
@Composable
|
||||
fun UnconnectedStatusCard() {
|
||||
fun UnconnectedStatusCard(viewModel: CarViewModel) {
|
||||
var showScanDialog by remember { mutableStateOf(false) }
|
||||
|
||||
StatusCard(
|
||||
cardItems = listOf(
|
||||
CardItem(
|
||||
|
@ -21,11 +29,17 @@ fun UnconnectedStatusCard() {
|
|||
bottomControl = {
|
||||
Button(
|
||||
onClick = {
|
||||
|
||||
showScanDialog = true
|
||||
}
|
||||
) {
|
||||
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 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.StatusCardItemText
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardInfo () : CardItem {
|
||||
fun StatusCardInfo(deviceInfo: ConnectedDeviceInfo) : CardItem {
|
||||
return CardItem(
|
||||
title = "当前连接",
|
||||
content = {
|
||||
StatusCardItemText("名称 Name")
|
||||
StatusCardItemText("Mac XX:XX:XX:XX:XX:XX")
|
||||
StatusCardItemText("版本 001")
|
||||
StatusCardItemText("名称 ${deviceInfo.name}")
|
||||
StatusCardItemText("Mac ${deviceInfo.address}")
|
||||
StatusCardItemText("版本 ${deviceInfo.version}")
|
||||
}
|
||||
)
|
||||
}
|
|
@ -8,21 +8,26 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import icu.fur93.esp32_car.ui.component.CardItem
|
||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||
import icu.fur93.esp32_car.viewmodel.InfraredState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardInfraredStatus () : CardItem {
|
||||
fun StatusCardInfraredStatus(infraredState: InfraredState): CardItem {
|
||||
return CardItem(
|
||||
title = "红外模块状态",
|
||||
content = {
|
||||
if (infraredState.enable) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
listOf(0, 0, 0, 0, 0).forEach { status ->
|
||||
infraredState.statusList.forEach { status ->
|
||||
StatusCardItemText(status.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StatusCardItemText("未启用")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -8,23 +8,28 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import icu.fur93.esp32_car.ui.component.CardItem
|
||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||
import icu.fur93.esp32_car.viewmodel.UltrasoundState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardUltrasoundStatus () : CardItem {
|
||||
fun StatusCardUltrasoundStatus(ultrasoundState: UltrasoundState): CardItem {
|
||||
return CardItem(
|
||||
title = "超声波状态",
|
||||
content = {
|
||||
if (ultrasoundState.enable) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
StatusCardItemText("舵机角度 90")
|
||||
StatusCardItemText("舵机角度 ${ultrasoundState.servoAngle}")
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
StatusCardItemText("舵机角度 20mm")
|
||||
StatusCardItemText("障碍距离 ${ultrasoundState.distance}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StatusCardItemText("未启用")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -6,10 +6,11 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||
import androidx.compose.runtime.Composable
|
||||
import icu.fur93.esp32_car.ui.component.CardItem
|
||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||
import icu.fur93.esp32_car.viewmodel.CarState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardMotorStatus () : CardItem {
|
||||
fun StatusCardMotorStatus (carState: CarState) : CardItem {
|
||||
return CardItem(
|
||||
title = "电机状态",
|
||||
content = {
|
||||
|
@ -17,16 +18,16 @@ fun StatusCardMotorStatus () : CardItem {
|
|||
columns = GridCells.Fixed(2)
|
||||
) {
|
||||
item {
|
||||
StatusCardItemText("A 0 1 255")
|
||||
StatusCardItemText("A ${carState.motorAState.in1} ${carState.motorAState.in2} ${carState.motorAState.pwm}")
|
||||
}
|
||||
item {
|
||||
StatusCardItemText("D 0 1 255")
|
||||
StatusCardItemText("D ${carState.motorDState.in1} ${carState.motorDState.in2} ${carState.motorDState.pwm}")
|
||||
}
|
||||
item {
|
||||
StatusCardItemText("B 1 0 255")
|
||||
StatusCardItemText("B ${carState.motorBState.in1} ${carState.motorBState.in2} ${carState.motorBState.pwm}")
|
||||
}
|
||||
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