diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c3aa6b..c2715f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt index 7cc799b..437695f 100644 --- a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt +++ b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt b/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt new file mode 100644 index 0000000..eda3ac7 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/const/CarCommands.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/data/PreferencesDataStore.kt b/app/src/main/java/icu/fur93/esp32_car/data/PreferencesDataStore.kt new file mode 100644 index 0000000..cd17cf6 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/data/PreferencesDataStore.kt @@ -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 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 { + return context.dataStore.data.map { preferences -> + preferences[lastConnectedDevice] + } + } +} \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/LogEntry.kt b/app/src/main/java/icu/fur93/esp32_car/entity/LogEntry.kt new file mode 100644 index 0000000..8c2cec1 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/LogEntry.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/CarControlState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/CarControlState.kt new file mode 100644 index 0000000..94133f5 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/CarControlState.kt @@ -0,0 +1,6 @@ +package icu.fur93.esp32_car.entity.car + +data class CarControlState( + val speed: UInt = 0u, + val direction: UInt = 0u +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt new file mode 100644 index 0000000..afbfa21 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/CarState.kt @@ -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() +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/ConnectedDeviceInfo.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/ConnectionInfoState.kt similarity index 55% rename from app/src/main/java/icu/fur93/esp32_car/entity/ConnectedDeviceInfo.kt rename to app/src/main/java/icu/fur93/esp32_car/entity/car/ConnectionInfoState.kt index fe36e51..7babec6 100644 --- a/app/src/main/java/icu/fur93/esp32_car/entity/ConnectedDeviceInfo.kt +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/ConnectionInfoState.kt @@ -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 = "" diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/InfraredState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/InfraredState.kt new file mode 100644 index 0000000..ff9666d --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/InfraredState.kt @@ -0,0 +1,6 @@ +package icu.fur93.esp32_car.entity.car + +data class InfraredState( + val enable: Boolean = false, + val statusList: List = listOf() +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/MotorState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/MotorState.kt new file mode 100644 index 0000000..3d1e184 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/MotorState.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt new file mode 100644 index 0000000..35779ba --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/car/UltrasoundState.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/page/HomePage.kt b/app/src/main/java/icu/fur93/esp32_car/page/HomePage.kt index d6fbc8e..51fc290 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/HomePage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/HomePage.kt @@ -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( diff --git a/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt b/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt index 78b13fb..9fa5edb 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt @@ -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 = { diff --git a/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt b/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt index 896ef27..643dfca 100644 --- a/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt +++ b/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt @@ -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 fun observeLogs(): Flow> fun clearLogs() + val connectionState: StateFlow } // 添加连接状态枚举 @@ -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.asStateFlow() + override val connectionState: StateFlow = _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) diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfo.kt b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfo.kt index 6ac5ebb..693db14 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfo.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfo.kt @@ -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 = { diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfraredStatus.kt b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfraredStatus.kt index 037e496..afdcb5f 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfraredStatus.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardInfraredStatus.kt @@ -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 diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardUltrasoundStatus.kt b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardUltrasoundStatus.kt index 32542fa..e2766b6 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardUltrasoundStatus.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardUltrasoundStatus.kt @@ -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 diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StautsCardMotorStatus.kt b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StautsCardMotorStatus.kt index 1601122..c78a179 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StautsCardMotorStatus.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StautsCardMotorStatus.kt @@ -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 diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt b/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt index 1566240..7a1f923 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt @@ -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 ) diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/component/CardButtonGroup.kt b/app/src/main/java/icu/fur93/esp32_car/ui/component/CardButtonGroup.kt index 9826ee9..65857c9 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/component/CardButtonGroup.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/CardButtonGroup.kt @@ -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( } } } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt b/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt index d2f0180..2db6a55 100644 --- a/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt +++ b/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt @@ -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 = 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 = repository.observeCarState() @@ -152,8 +102,8 @@ class CarViewModel( .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) // 设备信息状态 - private val _deviceInfo = MutableStateFlow(ConnectedDeviceInfo()) - val deviceInfo: StateFlow = _deviceInfo.asStateFlow() + private val _connectionInfoState = MutableStateFlow(ConnectionInfoState()) + val connectionInfoState: StateFlow = _connectionInfoState.asStateFlow() val connectionState: StateFlow = (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" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8408da1..3190180 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }