This commit is contained in:
玖叁 2025-01-02 02:19:16 +08:00
parent 33bd3eed44
commit 4e977f496b
15 changed files with 469 additions and 135 deletions

View File

@ -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 {

View File

@ -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

View File

@ -0,0 +1,6 @@
package icu.fur93.esp32_car.const
object RunModeConsts {
const val MODE_MANUAL = 0x00u
const val MODE_TRACKING = 0x01u
}

View File

@ -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(),

View File

@ -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,
)

View File

@ -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
)

View File

@ -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)
}
)
)

View File

@ -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 "开始")
}
}
)
}

View File

@ -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,55 +75,84 @@ fun ControlSingleJoystickModeControlPanel(
viewModel: CarViewModel,
onJoystickStateChange: (JoystickState) -> Unit
) {
// 添加Y轴限位模式的状态
var yAxisOnly by remember { mutableStateOf(false) }
Spacer(Modifier.height(24.dp))
Row(
Column(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
horizontalAlignment = Alignment.CenterHorizontally
) {
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.sendXYR(0, 0, -50)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
// 添加开关控件
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转")
Text(
text = "Y 轴限位模式",
style = MaterialTheme.typography.bodyLarge
)
Switch(
checked = yAxisOnly,
onCheckedChange = { yAxisOnly = it }
)
}
ControlJoystick(
viewModel = viewModel,
onJoystickStateChange = onJoystickStateChange
)
Spacer(Modifier.height(16.dp))
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.sendXYR(0, 0, 50)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
// 原有的控制面板布局
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转")
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.rotateLeft(100)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转")
}
ControlJoystick(
viewModel = viewModel,
onJoystickStateChange = onJoystickStateChange,
yAxisOnly = yAxisOnly // 传递Y轴限位模式状态
)
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.rotateRight(100)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转")
}
}
}
}
@ -128,7 +160,8 @@ fun ControlSingleJoystickModeControlPanel(
@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)

View File

@ -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,16 +98,122 @@ 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("取消")
}
}
)
}
}

View File

@ -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]
))
)
)
}

View File

@ -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 "未进行")
}
)
}

View File

@ -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)

View File

@ -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)
)
}
}
}

View File

@ -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)
}