feat: auto connect

This commit is contained in:
玖叁 2024-12-24 15:23:13 +08:00
parent 9b8b922ea7
commit 61f37f71de
22 changed files with 268 additions and 105 deletions

View File

@ -61,6 +61,10 @@ dependencies {
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.androidx.navigation.compose) 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) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -23,6 +23,7 @@ import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -35,6 +36,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import icu.fur93.esp32_car.data.PreferencesDataStore
import icu.fur93.esp32_car.page.ControlGamepadModePage import icu.fur93.esp32_car.page.ControlGamepadModePage
import icu.fur93.esp32_car.page.ControlPage import icu.fur93.esp32_car.page.ControlPage
import icu.fur93.esp32_car.page.ControlPathfinderModePage import icu.fur93.esp32_car.page.ControlPathfinderModePage
@ -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.CarControlUseCase
import icu.fur93.esp32_car.viewmodel.CarViewModel import icu.fur93.esp32_car.viewmodel.CarViewModel
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val preferencesDataStore by lazy {
PreferencesDataStore(this)
}
private val bluetoothRepository by lazy { private val bluetoothRepository by lazy {
BluetoothRepositoryImpl(this, lifecycleScope) BluetoothRepositoryImpl(this, lifecycleScope, preferencesDataStore)
} }
private val carControlUseCase by lazy { private val carControlUseCase by lazy {
@ -64,6 +72,7 @@ class MainActivity : ComponentActivity() {
} }
} }
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -93,6 +102,16 @@ class MainActivity : ComponentActivity() {
) )
} }
// 检查权限后尝试自动连接
lifecycleScope.launch {
preferencesDataStore.getLastConnectedDevice().collect { address ->
if (address != null) {
viewModel.connectToDeviceByAddress(address)
}
}
}
setContent { setContent {
Esp32carTheme { Esp32carTheme {
App(viewModel) App(viewModel)
@ -136,7 +155,11 @@ fun App(viewModel: CarViewModel) {
composable(Route.Control.route) { ControlPage(navController, viewModel) } composable(Route.Control.route) { ControlPage(navController, viewModel) }
composable(Route.Settings.route) { SettingsPage(navController, viewModel) } composable(Route.Settings.route) { SettingsPage(navController, viewModel) }
composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) } composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) }
composable(Route.ControlSingleJoystickMode.route) { ControlSingleJoystickModePage(navController) } composable(Route.ControlSingleJoystickMode.route) {
ControlSingleJoystickModePage(
navController
)
}
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) } composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) }
} }
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package icu.fur93.esp32_car.entity.car
data class CarControlState(
val speed: UInt = 0u,
val direction: UInt = 0u
)

View File

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

View File

@ -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 name: String = "",
val address: String = "", val address: String = "",
val version: String = "" val version: String = ""

View File

@ -0,0 +1,6 @@
package icu.fur93.esp32_car.entity.car
data class InfraredState(
val enable: Boolean = false,
val statusList: List<UInt> = listOf()
)

View File

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

View File

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

View File

@ -45,7 +45,7 @@ fun HomePage(navController: NavHostController, viewModel: CarViewModel) {
@Composable @Composable
fun HomePageStatusCard(viewModel: CarViewModel) { fun HomePageStatusCard(viewModel: CarViewModel) {
val deviceInfo by viewModel.deviceInfo.collectAsState() val deviceInfo by viewModel.connectionInfoState.collectAsState()
val carState by viewModel.carState.collectAsState() val carState by viewModel.carState.collectAsState()
StatusCard( StatusCard(

View File

@ -103,7 +103,7 @@ fun SettingConnectDeviceItem(viewModel: CarViewModel) {
var showDisconnectDialog by remember { mutableStateOf(false) } var showDisconnectDialog by remember { mutableStateOf(false) }
val connectionState by viewModel.connectionState.collectAsState() val connectionState by viewModel.connectionState.collectAsState()
val deviceInfo by viewModel.deviceInfo.collectAsState() val connectionInfoState by viewModel.connectionInfoState.collectAsState()
ListItem( ListItem(
@ -136,7 +136,7 @@ fun SettingConnectDeviceItem(viewModel: CarViewModel) {
AlertDialog( AlertDialog(
onDismissRequest = { showDisconnectDialog = false }, onDismissRequest = { showDisconnectDialog = false },
title = { Text("断开连接") }, title = { Text("断开连接") },
text = { Text("确定要断开与 ${deviceInfo.name} 的连接吗?") }, text = { Text("确定要断开与 ${connectionInfoState.name} 的连接吗?") },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {

View File

@ -14,12 +14,8 @@ import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import icu.fur93.esp32_car.const.CarCommands
import icu.fur93.esp32_car.entity.BleDevice 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -29,6 +25,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID 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 { interface BluetoothRepository {
@ -36,6 +37,7 @@ interface BluetoothRepository {
fun observeCarState(): Flow<CarState> fun observeCarState(): Flow<CarState>
fun observeLogs(): Flow<List<LogEntry>> fun observeLogs(): Flow<List<LogEntry>>
fun clearLogs() fun clearLogs()
val connectionState: StateFlow<ConnectionState>
} }
// 添加连接状态枚举 // 添加连接状态枚举
@ -53,7 +55,8 @@ val txCharUUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
// 蓝牙通信实现 // 蓝牙通信实现
class BluetoothRepositoryImpl( class BluetoothRepositoryImpl(
private val context: Context, private val context: Context,
private val scope: CoroutineScope private val scope: CoroutineScope,
private val preferencesDataStore: PreferencesDataStore
) : BluetoothRepository { ) : BluetoothRepository {
private var bluetoothGatt: BluetoothGatt? = null private var bluetoothGatt: BluetoothGatt? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null private var rxCharacteristic: BluetoothGattCharacteristic? = null
@ -72,7 +75,7 @@ class BluetoothRepositoryImpl(
// 连接状态流 // 连接状态流
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED) private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow() override val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
private val scanCallback = object : ScanCallback() { private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
@ -126,7 +129,7 @@ class BluetoothRepositoryImpl(
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun connectToDevice(device: BluetoothDevice) { fun connectToDevice(device: BluetoothDevice, callback: ((Boolean) -> Unit)? = null) {
_connectionState.value = ConnectionState.CONNECTING _connectionState.value = ConnectionState.CONNECTING
device.connectGatt( device.connectGatt(
context, context,
@ -139,7 +142,8 @@ class BluetoothRepositoryImpl(
) { ) {
if (status != BluetoothGatt.GATT_SUCCESS) { if (status != BluetoothGatt.GATT_SUCCESS) {
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
Toast.makeText(context, "连接失败,错误码: $status", Toast.LENGTH_SHORT).show() Toast.makeText(context, "连接失败,错误码: $status", Toast.LENGTH_SHORT)
.show()
} }
return return
} }
@ -149,18 +153,38 @@ class BluetoothRepositoryImpl(
_connectionState.value = ConnectionState.CONNECTED _connectionState.value = ConnectionState.CONNECTED
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
gatt.discoverServices() gatt.discoverServices()
_connectionState.collect { state ->
if (state == ConnectionState.CONNECTED) {
preferencesDataStore.saveLastConnectedDevice(device.address)
}
}
}
if (callback != null) {
callback(true)
} }
} }
BluetoothProfile.STATE_CONNECTING -> { BluetoothProfile.STATE_CONNECTING -> {
_connectionState.value = ConnectionState.CONNECTING _connectionState.value = ConnectionState.CONNECTING
if (callback != null) {
callback(false)
}
} }
BluetoothProfile.STATE_DISCONNECTING -> { BluetoothProfile.STATE_DISCONNECTING -> {
_connectionState.value = ConnectionState.DISCONNECTING _connectionState.value = ConnectionState.DISCONNECTING
if (callback != null) {
callback(false)
}
} }
BluetoothProfile.STATE_DISCONNECTED -> { BluetoothProfile.STATE_DISCONNECTED -> {
bluetoothGatt = null bluetoothGatt = null
rxCharacteristic = null rxCharacteristic = null
_connectionState.value = ConnectionState.DISCONNECTED _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") @SuppressLint("MissingPermission")
override fun sendCommand(command: ByteArray) { override fun sendCommand(command: ByteArray) {
rxCharacteristic?.let { characteristic -> rxCharacteristic?.let { characteristic ->
@ -233,7 +275,8 @@ class BluetoothRepositoryImpl(
private fun isValidPacket(packet: ByteArray): Boolean = private fun isValidPacket(packet: ByteArray): Boolean =
packet[0].toUByte() == CarCommands.PACKET_R_HEAD.toUByte() && packet[0].toUByte() == CarCommands.PACKET_R_HEAD.toUByte() &&
packet.size == packet[1].toUByte().toInt() && 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) { private fun updateMotorStatus(packet: ByteArray) {
val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1) val data = packet.sliceArray(3 until packet[1].toUByte().toInt() - 1)

View File

@ -2,13 +2,13 @@ package icu.fur93.esp32_car.ui.carditem
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import icu.fur93.esp32_car.entity.ConnectedDeviceInfo import icu.fur93.esp32_car.entity.car.ConnectionInfoState
import icu.fur93.esp32_car.ui.component.CardItem import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText import icu.fur93.esp32_car.ui.component.StatusCardItemText
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable
fun StatusCardInfo(deviceInfo: ConnectedDeviceInfo) : CardItem { fun StatusCardInfo(deviceInfo: ConnectionInfoState) : CardItem {
return CardItem( return CardItem(
title = "当前连接", title = "当前连接",
content = { content = {

View File

@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import icu.fur93.esp32_car.ui.component.CardItem import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.viewmodel.InfraredState import icu.fur93.esp32_car.entity.car.InfraredState
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable

View File

@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import icu.fur93.esp32_car.ui.component.CardItem import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.viewmodel.UltrasoundState import icu.fur93.esp32_car.entity.car.UltrasoundState
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable

View File

@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import icu.fur93.esp32_car.ui.component.CardItem import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.viewmodel.CarState import icu.fur93.esp32_car.entity.car.CarState
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable

View File

@ -2,8 +2,11 @@ package icu.fur93.esp32_car.ui.component
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send 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.Home
import androidx.compose.material.icons.filled.Settings 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.Icon
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
@ -29,19 +32,19 @@ fun BottomNavigationBar(
val navItems = listOf( val navItems = listOf(
BottomNavigationItem( BottomNavigationItem(
title = "主页", title = "主页",
icon = Icons.Default.Home, icon = Icons.Outlined.Home,
selectedIcon = Icons.Default.Home, selectedIcon = Icons.Default.Home,
route = Route.Home route = Route.Home
), ),
BottomNavigationItem( BottomNavigationItem(
title = "控制", title = "控制",
icon = Icons.AutoMirrored.Filled.Send, icon = Icons.AutoMirrored.Outlined.Send,
selectedIcon = Icons.AutoMirrored.Filled.Send, selectedIcon = Icons.AutoMirrored.Filled.Send,
route = Route.Control route = Route.Control
), ),
BottomNavigationItem( BottomNavigationItem(
title = "设置", title = "设置",
icon = Icons.Default.Settings, icon = Icons.Outlined.Settings,
selectedIcon = Icons.Default.Settings, selectedIcon = Icons.Default.Settings,
route = Route.Settings route = Route.Settings
) )

View File

@ -4,22 +4,22 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.ui.theme.CardButtonTypography
data class CardButtonItem( data class CardButtonItem(
val text: String, val text: String,
@ -37,7 +37,7 @@ fun CardButtonGroup(
) { ) {
Text( Text(
text = title, text = title,
style = CardButtonTypography.CardButtonGroupTitleTextStyle style = MaterialTheme.typography.titleLarge
) )
Spacer(Modifier.height(CardButtonGroupGap)) Spacer(Modifier.height(CardButtonGroupGap))
LazyVerticalGrid( LazyVerticalGrid(
@ -45,14 +45,47 @@ fun CardButtonGroup(
horizontalArrangement = Arrangement.spacedBy(CardButtonGroupGap), horizontalArrangement = Arrangement.spacedBy(CardButtonGroupGap),
verticalArrangement = Arrangement.spacedBy(CardButtonGroupGap) verticalArrangement = Arrangement.spacedBy(CardButtonGroupGap)
) { ) {
buttons.forEach { buttonItem -> items(buttons.size) { index ->
item { M3CardButton(buttons[index])
CardButton(buttonItem) }
}
}
@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 @Composable
fun CardButton( fun CardButton(
item: CardButtonItem item: CardButtonItem
@ -79,4 +112,5 @@ fun CardButton(
} }
} }
} }
} }
*/

View File

@ -4,8 +4,11 @@ import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import icu.fur93.esp32_car.const.CarCommands
import icu.fur93.esp32_car.entity.BleDevice 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.BluetoothRepository
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
import icu.fur93.esp32_car.repository.ConnectionState import icu.fur93.esp32_car.repository.ConnectionState
@ -16,71 +19,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch 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( class CarControlUseCase(
@ -140,9 +78,21 @@ class CarViewModel(
fun startScan() = (repository as BluetoothRepositoryImpl).startScan() fun startScan() = (repository as BluetoothRepositoryImpl).startScan()
fun stopScan() = (repository as BluetoothRepositoryImpl).stopScan() fun stopScan() = (repository as BluetoothRepositoryImpl).stopScan()
fun connectToDevice(device: BluetoothDevice) { fun connectToDevice(device: BluetoothDevice) {
updateDeviceInfo(device) (repository as BluetoothRepositoryImpl).connectToDevice(device) { isConnected ->
(repository as BluetoothRepositoryImpl).connectToDevice(device) 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() val carState: StateFlow<CarState> = repository.observeCarState()
@ -152,8 +102,8 @@ class CarViewModel(
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
// 设备信息状态 // 设备信息状态
private val _deviceInfo = MutableStateFlow(ConnectedDeviceInfo()) private val _connectionInfoState = MutableStateFlow(ConnectionInfoState())
val deviceInfo: StateFlow<ConnectedDeviceInfo> = _deviceInfo.asStateFlow() val connectionInfoState: StateFlow<ConnectionInfoState> = _connectionInfoState.asStateFlow()
val connectionState: StateFlow<ConnectionState> = val connectionState: StateFlow<ConnectionState> =
(repository as BluetoothRepositoryImpl).connectionState (repository as BluetoothRepositoryImpl).connectionState
@ -169,7 +119,7 @@ class CarViewModel(
connectionState.collect { state -> connectionState.collect { state ->
if (state == ConnectionState.DISCONNECTED) { if (state == ConnectionState.DISCONNECTED) {
// 断开连接时清空设备信息 // 断开连接时清空设备信息
_deviceInfo.value = ConnectedDeviceInfo() _connectionInfoState.value = ConnectionInfoState()
} }
} }
} }
@ -178,7 +128,7 @@ class CarViewModel(
// 更新设备信息的方法 // 更新设备信息的方法
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun updateDeviceInfo(device: BluetoothDevice) { fun updateDeviceInfo(device: BluetoothDevice) {
_deviceInfo.value = ConnectedDeviceInfo( _connectionInfoState.value = ConnectionInfoState(
name = device.name ?: "未知设备", name = device.name ?: "未知设备",
address = device.address, address = device.address,
version = "001" version = "001"

View File

@ -10,6 +10,10 @@ activityCompose = "1.9.3"
composeBom = "2024.04.01" composeBom = "2024.04.01"
roomKtx = "2.6.1" roomKtx = "2.6.1"
navigationCompose = "2.8.5" navigationCompose = "2.8.5"
datastoreCoreAndroid = "1.1.1"
datastorePreferencesCoreJvm = "1.1.1"
datastorePreferences = "1.1.1"
lifecycleViewmodelCompose = "2.9.0-alpha08"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } 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-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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }