feat
This commit is contained in:
parent
33bd3eed44
commit
4e977f496b
|
@ -117,15 +117,15 @@ class MainActivity : ComponentActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
// 检查权限后尝试自动连接
|
||||
lifecycleScope.launch {
|
||||
preferencesDataStore.getLastConnectedDevice().collect { address ->
|
||||
if (address != null) {
|
||||
viewModel.connectToDeviceByAddress(address)
|
||||
}
|
||||
}
|
||||
// // 检查权限后尝试自动连接
|
||||
// lifecycleScope.launch {
|
||||
// preferencesDataStore.getLastConnectedDevice().collect { address ->
|
||||
// if (address != null) {
|
||||
// viewModel.connectToDeviceByAddress(address)
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
setContent {
|
||||
Esp32carTheme {
|
||||
|
|
|
@ -11,6 +11,8 @@ object CarCommands {
|
|||
const val CMD_GET_SPIFFS_STATUS = 0x11u
|
||||
const val CMD_GET_DISTANCE = 0x12u
|
||||
|
||||
const val CMD_SET_MODE = 0x30u
|
||||
|
||||
const val CMD_SET_NAME = 0xa1u
|
||||
const val CMD_SET_PID = 0xa2u
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package icu.fur93.esp32_car.const
|
||||
|
||||
object RunModeConsts {
|
||||
const val MODE_MANUAL = 0x00u
|
||||
const val MODE_TRACKING = 0x01u
|
||||
}
|
|
@ -2,6 +2,7 @@ package icu.fur93.esp32_car.entity.car
|
|||
|
||||
data class CarState(
|
||||
val controlState: CarControlState = CarControlState(),
|
||||
val runModeState: RunModeState = RunModeState(),
|
||||
val motorAState: MotorState = MotorState(),
|
||||
val motorBState: MotorState = MotorState(),
|
||||
val motorCState: MotorState = MotorState(),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
import icu.fur93.esp32_car.const.RunModeConsts
|
||||
|
||||
data class RunModeState(
|
||||
val runMode: UInt = RunModeConsts.MODE_MANUAL,
|
||||
)
|
|
@ -3,5 +3,5 @@ package icu.fur93.esp32_car.entity.car
|
|||
data class UltrasoundState(
|
||||
val enable: Boolean = false,
|
||||
val servoAngle: UInt = 0u,
|
||||
val distance: UInt = 0u
|
||||
val distance: Float = 0f
|
||||
)
|
|
@ -46,7 +46,7 @@ fun ControlPageStatusCard() {
|
|||
|
||||
@Composable
|
||||
fun ControlPageAutoControl() {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavigation.current
|
||||
return CardButtonGroup(
|
||||
title = "自动控制",
|
||||
buttons = listOf(
|
||||
|
@ -54,7 +54,7 @@ fun ControlPageAutoControl() {
|
|||
text = Route.ControlPathfinderMode.title,
|
||||
icon = ImageVector.vectorResource(R.drawable.outline_route_24),
|
||||
onClick = {
|
||||
Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show()
|
||||
navController.navigate(Route.ControlPathfinderMode.route)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,8 +1,56 @@
|
|||
package icu.fur93.esp32_car.page
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import icu.fur93.esp32_car.Route
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
|
||||
import icu.fur93.esp32_car.ui.component.PageTitle
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardInfraredStatus
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardMotorStatus
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardPathfinderStatus
|
||||
import icu.fur93.esp32_car.ui.component.StatusCard
|
||||
import icu.fur93.esp32_car.const.RunModeConsts
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import icu.fur93.esp32_car.const.CarCommands
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardUltrasoundStatus
|
||||
|
||||
@Composable
|
||||
fun ControlPathfinderModePage(viewModel: CarViewModel) {
|
||||
|
||||
Column(LayoutContentModifier) {
|
||||
PageTitle(Route.ControlPathfinderMode.title)
|
||||
ControlPathfinderModeStatusCard(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ControlPathfinderModeStatusCard(viewModel: CarViewModel) {
|
||||
val deviceInfo by viewModel.connectionInfoState.collectAsState()
|
||||
val carState by viewModel.carState.collectAsState()
|
||||
|
||||
StatusCard(
|
||||
cardItems = listOf(
|
||||
StatusCardInfo(deviceInfo),
|
||||
StatusCardMotorStatus(carState),
|
||||
StatusCardInfraredStatus(carState.infraredState),
|
||||
StatusCardUltrasoundStatus(carState.ultrasoundState),
|
||||
StatusCardPathfinderStatus(carState.runModeState)
|
||||
),
|
||||
bottomControl = {
|
||||
Button(onClick = {
|
||||
if (carState.runModeState.runMode == RunModeConsts.MODE_TRACKING) {
|
||||
viewModel.setRunMode(RunModeConsts.MODE_MANUAL.toInt())
|
||||
} else {
|
||||
viewModel.setRunMode(RunModeConsts.MODE_TRACKING.toInt())
|
||||
}
|
||||
}) {
|
||||
Text(if (carState.runModeState.runMode == RunModeConsts.MODE_TRACKING) "停止" else "开始")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -10,6 +10,9 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -72,9 +75,36 @@ fun ControlSingleJoystickModeControlPanel(
|
|||
viewModel: CarViewModel,
|
||||
onJoystickStateChange: (JoystickState) -> Unit
|
||||
) {
|
||||
// 添加Y轴限位模式的状态
|
||||
var yAxisOnly by remember { mutableStateOf(false) }
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 添加开关控件
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Y 轴限位模式",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Switch(
|
||||
checked = yAxisOnly,
|
||||
onCheckedChange = { yAxisOnly = it }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// 原有的控制面板布局
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -85,7 +115,7 @@ fun ControlSingleJoystickModeControlPanel(
|
|||
modifier = Modifier.pointerInteropFilter { event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
viewModel.sendXYR(0, 0, -50)
|
||||
viewModel.rotateLeft(100)
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
|
@ -101,7 +131,8 @@ fun ControlSingleJoystickModeControlPanel(
|
|||
|
||||
ControlJoystick(
|
||||
viewModel = viewModel,
|
||||
onJoystickStateChange = onJoystickStateChange
|
||||
onJoystickStateChange = onJoystickStateChange,
|
||||
yAxisOnly = yAxisOnly // 传递Y轴限位模式状态
|
||||
)
|
||||
|
||||
FilledIconButton(
|
||||
|
@ -109,7 +140,7 @@ fun ControlSingleJoystickModeControlPanel(
|
|||
modifier = Modifier.pointerInteropFilter { event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
viewModel.sendXYR(0, 0, 50)
|
||||
viewModel.rotateRight(100)
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
|
@ -123,12 +154,14 @@ fun ControlSingleJoystickModeControlPanel(
|
|||
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ControlJoystick(
|
||||
viewModel: CarViewModel,
|
||||
onJoystickStateChange: (JoystickState) -> Unit
|
||||
onJoystickStateChange: (JoystickState) -> Unit,
|
||||
yAxisOnly: Boolean = false // 添加参数
|
||||
) {
|
||||
// 记住上一次发送的状态和时间
|
||||
val lastSentState = remember { mutableStateOf(JoystickState()) }
|
||||
|
@ -142,7 +175,8 @@ fun ControlJoystick(
|
|||
|
||||
Joystick(
|
||||
size = 175.dp,
|
||||
knobScale = 0.25f
|
||||
knobScale = 0.25f,
|
||||
yAxisOnly = yAxisOnly, // 使用传入的参数
|
||||
) { state ->
|
||||
// 更新状态
|
||||
onJoystickStateChange(state)
|
||||
|
|
|
@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import icu.fur93.esp32_car.BuildConfig
|
||||
import icu.fur93.esp32_car.R
|
||||
import icu.fur93.esp32_car.Route
|
||||
|
@ -63,6 +65,8 @@ fun SettingsList(viewModel: CarViewModel) {
|
|||
val context = LocalContext.current
|
||||
|
||||
var debugMode by remember { mutableStateOf(DebugWindowManager.isShowing()) }
|
||||
var showSensitivityDialog by remember { mutableStateOf(false) }
|
||||
var showSpeedDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val onDebugModeChange = { enable: Boolean ->
|
||||
if (enable) {
|
||||
|
@ -94,15 +98,121 @@ fun SettingsList(viewModel: CarViewModel) {
|
|||
}
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text("PID 参数") },
|
||||
headlineContent = { Text("循迹灵敏度设置") },
|
||||
trailingContent = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.arrow_right_24),
|
||||
contentDescription = "PID 参数"
|
||||
contentDescription = "循迹灵敏度设置"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { showSensitivityDialog = true }
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text("循迹速度设置") },
|
||||
trailingContent = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.arrow_right_24),
|
||||
contentDescription = "循迹速度设置"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { showSpeedDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 循迹灵敏度设置对话框
|
||||
if (showSensitivityDialog) {
|
||||
var sensitivity by remember { mutableStateOf("3") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSensitivityDialog = false },
|
||||
title = { Text("设置循迹灵敏度") },
|
||||
text = {
|
||||
Column {
|
||||
Text("请输入灵敏度值 (1-10):")
|
||||
androidx.compose.material3.TextField(
|
||||
value = sensitivity,
|
||||
onValueChange = {
|
||||
if (it.isEmpty() || (it.toIntOrNull() in 1..10)) {
|
||||
sensitivity = it
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
sensitivity.toIntOrNull()?.let { value ->
|
||||
viewModel.setPathfinderSensitivity(value)
|
||||
}
|
||||
showSensitivityDialog = false
|
||||
}
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showSensitivityDialog = false }) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 循迹速度设置对话框
|
||||
if (showSpeedDialog) {
|
||||
var baseSpeed by remember { mutableStateOf("50") }
|
||||
var turnSpeed by remember { mutableStateOf("50") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSpeedDialog = false },
|
||||
title = { Text("设置循迹速度") },
|
||||
text = {
|
||||
Column {
|
||||
Text("请输入基础速度 (0-255):")
|
||||
androidx.compose.material3.TextField(
|
||||
value = baseSpeed,
|
||||
onValueChange = {
|
||||
if (it.isEmpty() || (it.toIntOrNull() in 0..255)) {
|
||||
baseSpeed = it
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
Text("请输入转弯速度 (0-255):")
|
||||
androidx.compose.material3.TextField(
|
||||
value = turnSpeed,
|
||||
onValueChange = {
|
||||
if (it.isEmpty() || (it.toIntOrNull() in 0..255)) {
|
||||
turnSpeed = it
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val base = baseSpeed.toIntOrNull()
|
||||
val turn = turnSpeed.toIntOrNull()
|
||||
if (base != null && turn != null) {
|
||||
viewModel.setPathfinderSpeed(base, turn)
|
||||
}
|
||||
showSpeedDialog = false
|
||||
}
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showSpeedDialog = false }) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,9 @@ import icu.fur93.esp32_car.entity.LogEntry
|
|||
import icu.fur93.esp32_car.entity.car.CarState
|
||||
import icu.fur93.esp32_car.entity.car.InfraredState
|
||||
import icu.fur93.esp32_car.entity.car.MotorState
|
||||
import icu.fur93.esp32_car.entity.car.RunModeState
|
||||
import icu.fur93.esp32_car.entity.car.UltrasoundState
|
||||
import icu.fur93.esp32_car.utils.Utils
|
||||
|
||||
// 蓝牙通信接口
|
||||
interface BluetoothRepository {
|
||||
|
@ -91,6 +94,48 @@ class BluetoothRepositoryImpl(
|
|||
private val _deviceAddress = MutableStateFlow<String?>(null)
|
||||
override val deviceAddress: StateFlow<String?> = _deviceAddress.asStateFlow()
|
||||
|
||||
private var notificationRetryCount = 0
|
||||
private val MAX_NOTIFICATION_RETRIES = 3
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun enableNotifications(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||
scope.launch {
|
||||
var success = false
|
||||
var retryCount = 0
|
||||
|
||||
while (!success && retryCount < MAX_NOTIFICATION_RETRIES) {
|
||||
success = try {
|
||||
gatt.setCharacteristicNotification(characteristic, true)
|
||||
|
||||
// 等待一小段时间确保设置生效
|
||||
delay(100)
|
||||
|
||||
// 写入描述符
|
||||
characteristic.descriptors.forEach { descriptor ->
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
gatt.writeDescriptor(descriptor)
|
||||
// 等待描述符写入完成
|
||||
delay(100)
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("BluetoothRepository", "启用通知失败: ${e.message}")
|
||||
delay(200) // 失败后等待一段时间再重试
|
||||
false
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
retryCount++
|
||||
Log.d("BluetoothRepository", "重试启用通知 #$retryCount")
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Log.e("BluetoothRepository", "无法启用通知,已达到最大重试次数")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
val bleDevice = BleDevice(
|
||||
|
@ -213,33 +258,45 @@ class BluetoothRepositoryImpl(
|
|||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
||||
super.onServicesDiscovered(gatt, status)
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS && gatt != null) {
|
||||
// 读取设备名称
|
||||
val genericService =
|
||||
gatt?.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"))
|
||||
val deviceNameChar =
|
||||
genericService?.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"))
|
||||
val genericService = gatt.getService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"))
|
||||
val deviceNameChar = genericService?.getCharacteristic(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"))
|
||||
if (deviceNameChar != null) {
|
||||
gatt.readCharacteristic(deviceNameChar)
|
||||
}
|
||||
|
||||
gatt?.getService(UUID.fromString(serviceUUID))?.let { service ->
|
||||
gatt.getService(UUID.fromString(serviceUUID))?.let { service ->
|
||||
bluetoothGatt = gatt
|
||||
rxCharacteristic =
|
||||
service.getCharacteristic(UUID.fromString(rxCharUUID))
|
||||
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)
|
||||
// 使用新的通知启用方法
|
||||
val txCharacteristic = service.getCharacteristic(UUID.fromString(txCharUUID))
|
||||
if (txCharacteristic != null) {
|
||||
enableNotifications(gatt, txCharacteristic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDescriptorWrite(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
status: Int
|
||||
) {
|
||||
super.onDescriptorWrite(gatt, descriptor, status)
|
||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||
Log.e("BluetoothRepository", "描述符写入失败: $status")
|
||||
// 如果写入失败,尝试重新启用通知
|
||||
val characteristic = descriptor.characteristic
|
||||
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
|
||||
enableNotifications(gatt, characteristic)
|
||||
}
|
||||
} else {
|
||||
Log.d("BluetoothRepository", "描述符写入成功")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
|
@ -260,6 +317,7 @@ class BluetoothRepositoryImpl(
|
|||
value: ByteArray
|
||||
) {
|
||||
super.onCharacteristicChanged(gatt, characteristic, value)
|
||||
Log.d("onCharacteristicChanged", "characteristic: ${characteristic.uuid}, value: ${value.joinToString(" ")}")
|
||||
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
|
||||
onReceivePacket(value)
|
||||
}
|
||||
|
@ -274,46 +332,6 @@ class BluetoothRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connectToDeviceByAddress(
|
||||
address: String,
|
||||
callback: ((Boolean, BluetoothDevice) -> Unit)? = null
|
||||
) {
|
||||
val bluetoothManager =
|
||||
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
val device = bluetoothManager.adapter.getRemoteDevice(address)
|
||||
if (callback != null) {
|
||||
connectToDevice(device) { isConnected, deviceDetail ->
|
||||
callback(isConnected, deviceDetail ?: device)
|
||||
}
|
||||
} else {
|
||||
connectToDevice(device)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun setupDeviceNameNotification(gatt: BluetoothGatt) {
|
||||
val service = gatt.getService(UUID.fromString(deviceInfoServiceUUID))
|
||||
val characteristic = service?.getCharacteristic(UUID.fromString(deviceNameCharUUID))
|
||||
|
||||
characteristic?.let { char ->
|
||||
// 启用通知
|
||||
gatt.setCharacteristicNotification(char, true)
|
||||
|
||||
// 获取 descriptor 并写入通知使能值
|
||||
val descriptor =
|
||||
char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
|
||||
descriptor?.let {
|
||||
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
gatt.writeDescriptor(it)
|
||||
}
|
||||
|
||||
// 同时也读取一次当前值
|
||||
gatt.readCharacteristic(char)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun sendCommand(command: ByteArray) {
|
||||
rxCharacteristic?.let { characteristic ->
|
||||
|
@ -345,16 +363,17 @@ class BluetoothRepositoryImpl(
|
|||
if (!isValidPacket(packet)) return
|
||||
|
||||
/**
|
||||
包体 `01 0E 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`
|
||||
包体 `01 0F 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`
|
||||
|
||||
| 数据 | 类型 | 表示 |
|
||||
| -------- | ---- | ---- |
|
||||
| 运行模式 | 无符号整数 | 0x00 手动模式,0x01 巡线模式 |
|
||||
| IN_1_2 | 无符号整数 | 按位表示 IN1 IN2 的值, 0000 00 IN2 IN1 |
|
||||
| PWM | 无符号整数 | - |
|
||||
| 红外引脚数量 | 无符号整数 | - |
|
||||
| 红外数据 | 无符号整数 | 按位表示红外循迹模块数据, 0001 1111 |
|
||||
|
||||
示例 `01 0E E0 01 FF 02 FF 02 FF 01 FF 05 1F FE`
|
||||
示例 `01 0F E0 00 01 FF 02 FF 02 FF 01 FF 05 1F FE`
|
||||
**/
|
||||
|
||||
when (packet[2].toUByte().toUInt()) {
|
||||
|
@ -373,11 +392,22 @@ class BluetoothRepositoryImpl(
|
|||
private fun updateStatus(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),
|
||||
infraredState = createInfraredState(data[8].toUByte().toInt(), data[9].toUByte())
|
||||
runModeState = RunModeState(data[0].toUByte().toUInt()),
|
||||
motorAState = createMotorState(data, 1),
|
||||
motorBState = createMotorState(data, 3),
|
||||
motorCState = createMotorState(data, 5),
|
||||
motorDState = createMotorState(data, 7),
|
||||
infraredState = createInfraredState(data[9].toUByte().toInt(), data[10].toUByte()),
|
||||
ultrasoundState = UltrasoundState(
|
||||
true,
|
||||
0u,
|
||||
Utils.bytesToFloat(byteArrayOf(
|
||||
data[11],
|
||||
data[12],
|
||||
data[13],
|
||||
data[14]
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,16 @@ import android.annotation.SuppressLint
|
|||
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.const.RunModeConsts
|
||||
import icu.fur93.esp32_car.entity.car.RunModeState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardPathfinderStatus () : CardItem {
|
||||
fun StatusCardPathfinderStatus (runModeState: RunModeState) : CardItem {
|
||||
return CardItem(
|
||||
title = "循迹状态",
|
||||
content = {
|
||||
StatusCardItemText("进行中")
|
||||
StatusCardItemText(if (runModeState.runMode == RunModeConsts.MODE_TRACKING) "进行中" else "未进行")
|
||||
}
|
||||
)
|
||||
}
|
|
@ -22,6 +22,7 @@ fun Joystick(
|
|||
modifier: Modifier = Modifier,
|
||||
size: Dp = 200.dp,
|
||||
knobScale: Float = 0.4f,
|
||||
yAxisOnly: Boolean = false,
|
||||
onJoystickMove: (JoystickState) -> Unit
|
||||
) {
|
||||
// 状态管理
|
||||
|
@ -46,14 +47,26 @@ fun Joystick(
|
|||
onDragStart = { offset ->
|
||||
isDragging = true
|
||||
centerPoint = Offset(sizePx / 2, sizePx / 2)
|
||||
updateStickPosition(offset, centerPoint, radius, innerRadius) { newPos ->
|
||||
updateStickPosition(
|
||||
offset,
|
||||
centerPoint,
|
||||
radius,
|
||||
innerRadius,
|
||||
yAxisOnly
|
||||
) { newPos ->
|
||||
stickPosition = newPos
|
||||
val state = calculateJoystickState(centerPoint, newPos, radius)
|
||||
onJoystickMove(state)
|
||||
}
|
||||
},
|
||||
onDrag = { change, _ ->
|
||||
updateStickPosition(change.position, centerPoint, radius, innerRadius) { newPos ->
|
||||
updateStickPosition(
|
||||
change.position,
|
||||
centerPoint,
|
||||
radius,
|
||||
innerRadius,
|
||||
yAxisOnly
|
||||
) { newPos ->
|
||||
stickPosition = newPos
|
||||
val state = calculateJoystickState(centerPoint, newPos, radius)
|
||||
onJoystickMove(state)
|
||||
|
@ -93,25 +106,32 @@ private fun updateStickPosition(
|
|||
centerPoint: Offset,
|
||||
maxRadius: Float,
|
||||
knobRadius: Float,
|
||||
yAxisOnly: Boolean,
|
||||
onUpdate: (Offset) -> Unit
|
||||
) {
|
||||
val deltaX = pointerPosition.x - centerPoint.x
|
||||
val deltaY = pointerPosition.y - centerPoint.y
|
||||
|
||||
// 如果是 Y 轴限位模式,将 X 坐标固定在中心
|
||||
val adjustedX = if (yAxisOnly) centerPoint.x else pointerPosition.x
|
||||
val adjustedPosition = Offset(adjustedX, pointerPosition.y)
|
||||
|
||||
val distance = hypot(
|
||||
pointerPosition.x - centerPoint.x,
|
||||
pointerPosition.y - centerPoint.y
|
||||
adjustedPosition.x - centerPoint.x,
|
||||
adjustedPosition.y - centerPoint.y
|
||||
)
|
||||
|
||||
val newPosition = if (distance > maxRadius) {
|
||||
// 限制在最大范围内
|
||||
val angle = atan2(
|
||||
pointerPosition.y - centerPoint.y,
|
||||
pointerPosition.x - centerPoint.x
|
||||
adjustedPosition.y - centerPoint.y,
|
||||
adjustedPosition.x - centerPoint.x
|
||||
)
|
||||
Offset(
|
||||
centerPoint.x + (maxRadius * kotlin.math.cos(angle)),
|
||||
centerPoint.y + (maxRadius * kotlin.math.sin(angle))
|
||||
)
|
||||
} else {
|
||||
pointerPosition
|
||||
adjustedPosition
|
||||
}
|
||||
|
||||
onUpdate(newPosition)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package icu.fur93.esp32_car.utils
|
||||
|
||||
class Utils {
|
||||
companion object {
|
||||
/**
|
||||
* 将 Float 转换为字节数组
|
||||
*/
|
||||
fun floatToBytes(value: Float): ByteArray {
|
||||
return value.toBits().let { bits ->
|
||||
byteArrayOf(
|
||||
(bits and 0xFF).toByte(),
|
||||
((bits shr 8) and 0xFF).toByte(),
|
||||
((bits shr 16) and 0xFF).toByte(),
|
||||
((bits shr 24) and 0xFF).toByte()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组转换为 Float
|
||||
*/
|
||||
fun bytesToFloat(bytes: ByteArray): Float {
|
||||
require(bytes.size >= 4) { "字节数组长度必须大于等于4" }
|
||||
|
||||
return Float.fromBits(
|
||||
(bytes[3].toInt() and 0xFF shl 24) or
|
||||
(bytes[2].toInt() and 0xFF shl 16) or
|
||||
(bytes[1].toInt() and 0xFF shl 8) or
|
||||
(bytes[0].toInt() and 0xFF)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,11 +2,10 @@ package icu.fur93.esp32_car.viewmodel
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import icu.fur93.esp32_car.const.CarCommands
|
||||
import icu.fur93.esp32_car.const.RunModeConsts
|
||||
import icu.fur93.esp32_car.entity.BleDevice
|
||||
import icu.fur93.esp32_car.entity.LogEntry
|
||||
import icu.fur93.esp32_car.entity.car.CarState
|
||||
|
@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import android.view.WindowManager
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
|
||||
|
@ -44,10 +42,28 @@ class CarControlUseCase(
|
|||
buildCommand(CarCommands.CMD_MOTOR_STEER_CONTROL, 0x01, speed)
|
||||
)
|
||||
|
||||
fun rotateLeft(speed: Int = 255) = repository.sendCommand(
|
||||
buildCommand(CarCommands.CMD_MOTOR_ROTATE_CONTROL, 0x01, speed)
|
||||
)
|
||||
|
||||
fun rotateRight(speed: Int = 255) = repository.sendCommand(
|
||||
buildCommand(CarCommands.CMD_MOTOR_ROTATE_CONTROL, 0x00, speed)
|
||||
)
|
||||
|
||||
fun stop() = repository.sendCommand(
|
||||
buildCommand(CarCommands.CMD_MOTOR_MOVE_CONTROL, 0x00, 0x00)
|
||||
)
|
||||
|
||||
fun setRunMode(mode: Int) = repository.sendCommand(
|
||||
byteArrayOf(
|
||||
CarCommands.PACKET_T_HEAD.toByte(),
|
||||
0x05,
|
||||
CarCommands.CMD_SET_MODE.toByte(),
|
||||
mode.toByte(),
|
||||
CarCommands.PACKET_T_TAIL.toByte()
|
||||
)
|
||||
)
|
||||
|
||||
fun sendXYR(x: Int, y: Int, r: Int) = repository.sendCommand(
|
||||
byteArrayOf(
|
||||
CarCommands.PACKET_T_HEAD.toByte(),
|
||||
|
@ -60,6 +76,27 @@ class CarControlUseCase(
|
|||
)
|
||||
)
|
||||
|
||||
fun setPathfinderSpeed(baseSpeed: Int, turnSpeed: Int) = repository.sendCommand(
|
||||
byteArrayOf(
|
||||
CarCommands.PACKET_T_HEAD.toByte(),
|
||||
0x06,
|
||||
0xA3.toByte(),
|
||||
baseSpeed.toByte(),
|
||||
turnSpeed.toByte(),
|
||||
CarCommands.PACKET_T_TAIL.toByte()
|
||||
)
|
||||
)
|
||||
|
||||
fun setPathfinderSensitivity(sensitivity: Int) = repository.sendCommand(
|
||||
byteArrayOf(
|
||||
CarCommands.PACKET_T_HEAD.toByte(),
|
||||
0x05,
|
||||
0xA4.toByte(),
|
||||
sensitivity.toByte(),
|
||||
CarCommands.PACKET_T_TAIL.toByte()
|
||||
)
|
||||
)
|
||||
|
||||
private fun buildCommand(cmd: UInt, param1: Int, param2: Int) = byteArrayOf(
|
||||
CarCommands.PACKET_T_HEAD.toByte(),
|
||||
0x06,
|
||||
|
@ -88,11 +125,6 @@ class CarViewModel(
|
|||
(repository as BluetoothRepositoryImpl).connectToDevice(device)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connectToDeviceByAddress(address: String) {
|
||||
(repository as BluetoothRepositoryImpl).connectToDeviceByAddress(address)
|
||||
}
|
||||
|
||||
val carState: StateFlow<CarState> = repository.observeCarState()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CarState())
|
||||
|
||||
|
@ -157,6 +189,15 @@ class CarViewModel(
|
|||
fun turnLeft(speed: Int = 255) = carControlUseCase.turnLeft(speed)
|
||||
fun turnRight(speed: Int = 255) = carControlUseCase.turnRight(speed)
|
||||
fun stop() = carControlUseCase.stop()
|
||||
fun rotateLeft(speed: Int = 255) = carControlUseCase.rotateLeft(speed)
|
||||
fun rotateRight(speed: Int = 255) = carControlUseCase.rotateRight(speed)
|
||||
fun setRunMode(mode: Int) = carControlUseCase.setRunMode(mode)
|
||||
fun sendXYR(x: Int, y: Int, r: Int) = carControlUseCase.sendXYR(x, y, r)
|
||||
fun clearLogs() = repository.clearLogs()
|
||||
|
||||
fun setPathfinderSpeed(baseSpeed: Int, turnSpeed: Int) =
|
||||
carControlUseCase.setPathfinderSpeed(baseSpeed, turnSpeed)
|
||||
|
||||
fun setPathfinderSensitivity(sensitivity: Int) =
|
||||
carControlUseCase.setPathfinderSensitivity(sensitivity)
|
||||
}
|
Loading…
Reference in New Issue