feat: auto connect
This commit is contained in:
parent
9b8b922ea7
commit
61f37f71de
|
@ -61,6 +61,10 @@ dependencies {
|
|||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.datastore.core.android)
|
||||
implementation(libs.androidx.datastore.preferences.core.jvm)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.view.WindowManager
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
|
@ -35,6 +36,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import icu.fur93.esp32_car.data.PreferencesDataStore
|
||||
import icu.fur93.esp32_car.page.ControlGamepadModePage
|
||||
import icu.fur93.esp32_car.page.ControlPage
|
||||
import icu.fur93.esp32_car.page.ControlPathfinderModePage
|
||||
|
@ -46,10 +48,16 @@ import icu.fur93.esp32_car.ui.theme.Esp32carTheme
|
|||
import icu.fur93.esp32_car.viewmodel.CarControlUseCase
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val preferencesDataStore by lazy {
|
||||
PreferencesDataStore(this)
|
||||
}
|
||||
|
||||
private val bluetoothRepository by lazy {
|
||||
BluetoothRepositoryImpl(this, lifecycleScope)
|
||||
BluetoothRepositoryImpl(this, lifecycleScope, preferencesDataStore)
|
||||
}
|
||||
|
||||
private val carControlUseCase by lazy {
|
||||
|
@ -64,6 +72,7 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -93,6 +102,16 @@ class MainActivity : ComponentActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
// 检查权限后尝试自动连接
|
||||
lifecycleScope.launch {
|
||||
preferencesDataStore.getLastConnectedDevice().collect { address ->
|
||||
if (address != null) {
|
||||
viewModel.connectToDeviceByAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setContent {
|
||||
Esp32carTheme {
|
||||
App(viewModel)
|
||||
|
@ -136,7 +155,11 @@ fun App(viewModel: CarViewModel) {
|
|||
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.ControlSingleJoystickMode.route) {
|
||||
ControlSingleJoystickModePage(
|
||||
navController
|
||||
)
|
||||
}
|
||||
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package icu.fur93.esp32_car.const
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package icu.fur93.esp32_car.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||
|
||||
class PreferencesDataStore(private val context: Context) {
|
||||
private val lastConnectedDevice = stringPreferencesKey("last_connected_device")
|
||||
|
||||
suspend fun saveLastConnectedDevice(address: String) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[lastConnectedDevice] = address
|
||||
}
|
||||
}
|
||||
|
||||
fun getLastConnectedDevice(): Flow<String?> {
|
||||
return context.dataStore.data.map { preferences ->
|
||||
preferences[lastConnectedDevice]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package icu.fur93.esp32_car.entity
|
||||
|
||||
data class LogEntry(
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val direction: LogDirection,
|
||||
val data: String
|
||||
)
|
||||
|
||||
enum class LogDirection {
|
||||
SEND,
|
||||
RECEIVE
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
data class CarControlState(
|
||||
val speed: UInt = 0u,
|
||||
val direction: UInt = 0u
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
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()
|
||||
)
|
|
@ -1,6 +1,6 @@
|
|||
package icu.fur93.esp32_car.entity
|
||||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
data class ConnectedDeviceInfo(
|
||||
data class ConnectionInfoState(
|
||||
val name: String = "",
|
||||
val address: String = "",
|
||||
val version: String = ""
|
|
@ -0,0 +1,6 @@
|
|||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
data class InfraredState(
|
||||
val enable: Boolean = false,
|
||||
val statusList: List<UInt> = listOf()
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
data class MotorState(
|
||||
val pwm: UInt = 0u,
|
||||
val in1: UInt = 0u,
|
||||
val in2: UInt = 0u
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package icu.fur93.esp32_car.entity.car
|
||||
|
||||
data class UltrasoundState(
|
||||
val enable: Boolean = false,
|
||||
val servoAngle: UInt = 0u,
|
||||
val distance: UInt = 0u
|
||||
)
|
|
@ -45,7 +45,7 @@ fun HomePage(navController: NavHostController, viewModel: CarViewModel) {
|
|||
|
||||
@Composable
|
||||
fun HomePageStatusCard(viewModel: CarViewModel) {
|
||||
val deviceInfo by viewModel.deviceInfo.collectAsState()
|
||||
val deviceInfo by viewModel.connectionInfoState.collectAsState()
|
||||
val carState by viewModel.carState.collectAsState()
|
||||
|
||||
StatusCard(
|
||||
|
|
|
@ -103,7 +103,7 @@ fun SettingConnectDeviceItem(viewModel: CarViewModel) {
|
|||
var showDisconnectDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val connectionState by viewModel.connectionState.collectAsState()
|
||||
val deviceInfo by viewModel.deviceInfo.collectAsState()
|
||||
val connectionInfoState by viewModel.connectionInfoState.collectAsState()
|
||||
|
||||
|
||||
ListItem(
|
||||
|
@ -136,7 +136,7 @@ fun SettingConnectDeviceItem(viewModel: CarViewModel) {
|
|||
AlertDialog(
|
||||
onDismissRequest = { showDisconnectDialog = false },
|
||||
title = { Text("断开连接") },
|
||||
text = { Text("确定要断开与 ${deviceInfo.name} 的连接吗?") },
|
||||
text = { Text("确定要断开与 ${connectionInfoState.name} 的连接吗?") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
|
|
@ -14,12 +14,8 @@ import android.bluetooth.le.ScanResult
|
|||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import icu.fur93.esp32_car.const.CarCommands
|
||||
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
|
||||
|
@ -29,6 +25,11 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import icu.fur93.esp32_car.data.PreferencesDataStore
|
||||
import icu.fur93.esp32_car.entity.LogDirection
|
||||
import icu.fur93.esp32_car.entity.LogEntry
|
||||
import icu.fur93.esp32_car.entity.car.CarState
|
||||
import icu.fur93.esp32_car.entity.car.MotorState
|
||||
|
||||
// 蓝牙通信接口
|
||||
interface BluetoothRepository {
|
||||
|
@ -36,6 +37,7 @@ interface BluetoothRepository {
|
|||
fun observeCarState(): Flow<CarState>
|
||||
fun observeLogs(): Flow<List<LogEntry>>
|
||||
fun clearLogs()
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
}
|
||||
|
||||
// 添加连接状态枚举
|
||||
|
@ -53,7 +55,8 @@ val txCharUUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
|||
// 蓝牙通信实现
|
||||
class BluetoothRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope
|
||||
private val scope: CoroutineScope,
|
||||
private val preferencesDataStore: PreferencesDataStore
|
||||
) : BluetoothRepository {
|
||||
private var bluetoothGatt: BluetoothGatt? = null
|
||||
private var rxCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
@ -72,7 +75,7 @@ class BluetoothRepositoryImpl(
|
|||
|
||||
// 连接状态流
|
||||
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
@ -126,7 +129,7 @@ class BluetoothRepositoryImpl(
|
|||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connectToDevice(device: BluetoothDevice) {
|
||||
fun connectToDevice(device: BluetoothDevice, callback: ((Boolean) -> Unit)? = null) {
|
||||
_connectionState.value = ConnectionState.CONNECTING
|
||||
device.connectGatt(
|
||||
context,
|
||||
|
@ -139,7 +142,8 @@ class BluetoothRepositoryImpl(
|
|||
) {
|
||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, "连接失败,错误码: $status", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, "连接失败,错误码: $status", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -149,18 +153,38 @@ class BluetoothRepositoryImpl(
|
|||
_connectionState.value = ConnectionState.CONNECTED
|
||||
scope.launch(Dispatchers.Main) {
|
||||
gatt.discoverServices()
|
||||
_connectionState.collect { state ->
|
||||
if (state == ConnectionState.CONNECTED) {
|
||||
preferencesDataStore.saveLastConnectedDevice(device.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (callback != null) {
|
||||
callback(true)
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothProfile.STATE_CONNECTING -> {
|
||||
_connectionState.value = ConnectionState.CONNECTING
|
||||
if (callback != null) {
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothProfile.STATE_DISCONNECTING -> {
|
||||
_connectionState.value = ConnectionState.DISCONNECTING
|
||||
if (callback != null) {
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
bluetoothGatt = null
|
||||
rxCharacteristic = null
|
||||
_connectionState.value = ConnectionState.DISCONNECTED
|
||||
if (callback != null) {
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,6 +217,24 @@ 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 ->
|
||||
callback(isConnected, device)
|
||||
}
|
||||
} else {
|
||||
connectToDevice(device)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun sendCommand(command: ByteArray) {
|
||||
rxCharacteristic?.let { characteristic ->
|
||||
|
@ -233,7 +275,8 @@ class BluetoothRepositoryImpl(
|
|||
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()
|
||||
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)
|
||||
|
|
|
@ -2,13 +2,13 @@ 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.entity.car.ConnectionInfoState
|
||||
import icu.fur93.esp32_car.ui.component.CardItem
|
||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardInfo(deviceInfo: ConnectedDeviceInfo) : CardItem {
|
||||
fun StatusCardInfo(deviceInfo: ConnectionInfoState) : CardItem {
|
||||
return CardItem(
|
||||
title = "当前连接",
|
||||
content = {
|
||||
|
|
|
@ -8,7 +8,7 @@ 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
|
||||
import icu.fur93.esp32_car.entity.car.InfraredState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
|
|
|
@ -8,7 +8,7 @@ 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
|
||||
import icu.fur93.esp32_car.entity.car.UltrasoundState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
|
|
|
@ -6,7 +6,7 @@ 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
|
||||
import icu.fur93.esp32_car.entity.car.CarState
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
|
|
|
@ -2,8 +2,11 @@ package icu.fur93.esp32_car.ui.component
|
|||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.automirrored.outlined.Send
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
|
@ -29,19 +32,19 @@ fun BottomNavigationBar(
|
|||
val navItems = listOf(
|
||||
BottomNavigationItem(
|
||||
title = "主页",
|
||||
icon = Icons.Default.Home,
|
||||
icon = Icons.Outlined.Home,
|
||||
selectedIcon = Icons.Default.Home,
|
||||
route = Route.Home
|
||||
),
|
||||
BottomNavigationItem(
|
||||
title = "控制",
|
||||
icon = Icons.AutoMirrored.Filled.Send,
|
||||
icon = Icons.AutoMirrored.Outlined.Send,
|
||||
selectedIcon = Icons.AutoMirrored.Filled.Send,
|
||||
route = Route.Control
|
||||
),
|
||||
BottomNavigationItem(
|
||||
title = "设置",
|
||||
icon = Icons.Default.Settings,
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
route = Route.Settings
|
||||
)
|
||||
|
|
|
@ -4,22 +4,22 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import icu.fur93.esp32_car.ui.theme.CardButtonTypography
|
||||
|
||||
data class CardButtonItem(
|
||||
val text: String,
|
||||
|
@ -37,7 +37,7 @@ fun CardButtonGroup(
|
|||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = CardButtonTypography.CardButtonGroupTitleTextStyle
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(Modifier.height(CardButtonGroupGap))
|
||||
LazyVerticalGrid(
|
||||
|
@ -45,14 +45,47 @@ fun CardButtonGroup(
|
|||
horizontalArrangement = Arrangement.spacedBy(CardButtonGroupGap),
|
||||
verticalArrangement = Arrangement.spacedBy(CardButtonGroupGap)
|
||||
) {
|
||||
buttons.forEach { buttonItem ->
|
||||
item {
|
||||
CardButton(buttonItem)
|
||||
items(buttons.size) { index ->
|
||||
M3CardButton(buttons[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun M3CardButton(item: CardButtonItem) {
|
||||
ElevatedCard(
|
||||
modifier = item.modifier ?: Modifier
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { item.onClick?.invoke() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.text,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = item.text,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 原代码注释
|
||||
@Composable
|
||||
fun CardButton(
|
||||
item: CardButtonItem
|
||||
|
@ -79,4 +112,5 @@ fun CardButton(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -4,8 +4,11 @@ import android.annotation.SuppressLint
|
|||
import android.bluetooth.BluetoothDevice
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import icu.fur93.esp32_car.const.CarCommands
|
||||
import icu.fur93.esp32_car.entity.BleDevice
|
||||
import icu.fur93.esp32_car.entity.ConnectedDeviceInfo
|
||||
import icu.fur93.esp32_car.entity.LogEntry
|
||||
import icu.fur93.esp32_car.entity.car.CarState
|
||||
import icu.fur93.esp32_car.entity.car.ConnectionInfoState
|
||||
import icu.fur93.esp32_car.repository.BluetoothRepository
|
||||
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||
import icu.fur93.esp32_car.repository.ConnectionState
|
||||
|
@ -16,71 +19,6 @@ 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(
|
||||
|
@ -140,9 +78,21 @@ class CarViewModel(
|
|||
|
||||
fun startScan() = (repository as BluetoothRepositoryImpl).startScan()
|
||||
fun stopScan() = (repository as BluetoothRepositoryImpl).stopScan()
|
||||
|
||||
fun connectToDevice(device: BluetoothDevice) {
|
||||
updateDeviceInfo(device)
|
||||
(repository as BluetoothRepositoryImpl).connectToDevice(device)
|
||||
(repository as BluetoothRepositoryImpl).connectToDevice(device) { isConnected ->
|
||||
if (isConnected) {
|
||||
updateDeviceInfo(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connectToDeviceByAddress(address: String) {
|
||||
(repository as BluetoothRepositoryImpl).connectToDeviceByAddress(address) { isConnected, device ->
|
||||
if (isConnected) {
|
||||
updateDeviceInfo(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val carState: StateFlow<CarState> = repository.observeCarState()
|
||||
|
@ -152,8 +102,8 @@ class CarViewModel(
|
|||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
// 设备信息状态
|
||||
private val _deviceInfo = MutableStateFlow(ConnectedDeviceInfo())
|
||||
val deviceInfo: StateFlow<ConnectedDeviceInfo> = _deviceInfo.asStateFlow()
|
||||
private val _connectionInfoState = MutableStateFlow(ConnectionInfoState())
|
||||
val connectionInfoState: StateFlow<ConnectionInfoState> = _connectionInfoState.asStateFlow()
|
||||
|
||||
val connectionState: StateFlow<ConnectionState> =
|
||||
(repository as BluetoothRepositoryImpl).connectionState
|
||||
|
@ -169,7 +119,7 @@ class CarViewModel(
|
|||
connectionState.collect { state ->
|
||||
if (state == ConnectionState.DISCONNECTED) {
|
||||
// 断开连接时清空设备信息
|
||||
_deviceInfo.value = ConnectedDeviceInfo()
|
||||
_connectionInfoState.value = ConnectionInfoState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -178,7 +128,7 @@ class CarViewModel(
|
|||
// 更新设备信息的方法
|
||||
@SuppressLint("MissingPermission")
|
||||
fun updateDeviceInfo(device: BluetoothDevice) {
|
||||
_deviceInfo.value = ConnectedDeviceInfo(
|
||||
_connectionInfoState.value = ConnectionInfoState(
|
||||
name = device.name ?: "未知设备",
|
||||
address = device.address,
|
||||
version = "001"
|
||||
|
|
|
@ -10,6 +10,10 @@ activityCompose = "1.9.3"
|
|||
composeBom = "2024.04.01"
|
||||
roomKtx = "2.6.1"
|
||||
navigationCompose = "2.8.5"
|
||||
datastoreCoreAndroid = "1.1.1"
|
||||
datastorePreferencesCoreJvm = "1.1.1"
|
||||
datastorePreferences = "1.1.1"
|
||||
lifecycleViewmodelCompose = "2.9.0-alpha08"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
@ -28,6 +32,10 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
|
|||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastoreCoreAndroid" }
|
||||
androidx-datastore-preferences-core-jvm = { group = "androidx.datastore", name = "datastore-preferences-core-jvm", version.ref = "datastorePreferencesCoreJvm" }
|
||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version = "2.7.0" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
Loading…
Reference in New Issue