diff --git a/AGENTS.md b/AGENTS.md index 8c1ded3..c3c53e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,12 +191,12 @@ val y = power / 100.0 * sin(angle) // 水平分量 - [x] 摇杆尺寸增大:左右摇杆从 140dp 增大到 180dp,提升操控精度 - [x] 视频缩放模式:`VideoResizeMode` 枚举(FIT/ZOOM/FILL),设置界面新增"视频缩放模式"下拉选项,持久化保存,实时生效 - [x] 状态机修复:趴下状态下允许切换站立状态(机器人站立后自动进入 RL 控制,无需 App 下发) +- [x] UI 布局:增加 bg1.png 作为底图,无视频流时显示(视频流播放时覆盖底图) +- [x] UI 布局:开关灯合并为前灯/后灯两个按键,通过颜色(琥珀色=开/暗色=关)表示置位状态,配合机器人回报状态同步 ### 待办 -- [ ] UI 布局:增加 logo 图作为底图,无视频流时显示底图 - [ ] UI 布局:header 部分虽已避开状态栏,但底色与屏幕顶端视觉不吸附,需优化视觉传达效果 - [ ] UI 布局:功能按钮均布在四周,充分利用屏幕空间,优化单手操作体验 -- [ ] UI 布局:开关灯合并为一个按键,通过图标/颜色表示置位状态,需配合机器人回报状态同步 - [ ] 状态栏:增加 ICMP 延迟测量显示,显示当前 WiFi 强度和延迟 ms - [ ] 视频进一步延迟优化:探索更激进的低延迟策略(如降低帧率、缩小分辨率、调整 ExoPlayer 缓冲策略等) diff --git a/app/src/main/java/com/example/m20_gamepad/data/AppSettings.kt b/app/src/main/java/com/example/m20_gamepad/data/AppSettings.kt index bd207ed..7db742c 100644 --- a/app/src/main/java/com/example/m20_gamepad/data/AppSettings.kt +++ b/app/src/main/java/com/example/m20_gamepad/data/AppSettings.kt @@ -28,9 +28,9 @@ data class AppSettings( val videoResizeMode: VideoResizeMode = VideoResizeMode.ZOOM ) { companion object { - const val DEFAULT_ROBOT_HOST = "10.21.31.103" + const val DEFAULT_ROBOT_HOST = "10.21.41.1" const val DEFAULT_ROBOT_PORT = 30000 - const val DEFAULT_RTSP_URL = "rtsp://10.21.31.103:554/stream" + const val DEFAULT_RTSP_URL = "rtsp://10.21.41.1:8554/video1" /** RTSP 端口范围校验 */ fun isValidPort(port: Int): Boolean = port in 1..65535 diff --git a/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt b/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt index 2c636d8..bd8b52c 100644 --- a/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt +++ b/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt @@ -1,5 +1,6 @@ package com.example.m20_gamepad.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -40,6 +41,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -49,9 +52,11 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.erz.joysticklibrary.Joystick import com.erz.joysticklibrary.JoystickType +import com.example.m20_gamepad.R import com.example.m20_gamepad.data.SettingsRepository import com.example.m20_gamepad.network.ProtocolClient import com.example.m20_gamepad.network.models.ControlCommands +import com.example.m20_gamepad.network.models.LedStatus import com.example.m20_gamepad.ui.components.StatusPanel import com.example.m20_gamepad.video.RtspVideoPlayer @@ -83,6 +88,7 @@ fun MainScreen( val motionStatus by viewModel.motionStatus.collectAsStateWithLifecycle() val deviceTemperature by viewModel.deviceTemperature.collectAsStateWithLifecycle() val batteryStatus by viewModel.batteryStatus.collectAsStateWithLifecycle() + val ledStatus by viewModel.ledStatus.collectAsStateWithLifecycle() val lastUpdate by viewModel.lastUpdate.collectAsStateWithLifecycle() val actionFeedback by viewModel.actionFeedback.collectAsStateWithLifecycle() @@ -99,6 +105,14 @@ fun MainScreen( } Box(modifier = Modifier.fillMaxSize()) { + // ---- 底图层(无视频流时显示) ---- + Image( + painter = painterResource(id = R.drawable.bg1), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = Modifier.fillMaxSize() + ) + // ---- 视频背景层 ---- RtspVideoPlayer( url = settings.rtspUrl, @@ -123,6 +137,7 @@ fun MainScreen( viewModel = viewModel, connectionState = connectionState, motionState = motionState, + ledStatus = ledStatus, modifier = Modifier .align(Alignment.BottomCenter) .windowInsetsPadding(WindowInsets.navigationBars) @@ -245,6 +260,7 @@ private fun BottomControls( viewModel: MainViewModel, connectionState: ProtocolClient.State, motionState: Int, + ledStatus: LedStatus?, modifier: Modifier = Modifier ) { Row( @@ -269,7 +285,8 @@ private fun BottomControls( ControlButtons( viewModel = viewModel, connectionState = connectionState, - motionState = motionState + motionState = motionState, + ledStatus = ledStatus ) // 右摇杆(Yaw 旋转) @@ -289,7 +306,8 @@ private fun BottomControls( private fun ControlButtons( viewModel: MainViewModel, connectionState: ProtocolClient.State, - motionState: Int + motionState: Int, + ledStatus: LedStatus? ) { val connected = connectionState == ProtocolClient.State.CONNECTED val isRlControl = motionState == ControlCommands.MOTION_RL_CONTROL @@ -383,13 +401,23 @@ private fun ControlButtons( // 照明 SectionLabel("照明") + val frontOn = ledStatus?.front == 1 + val backOn = ledStatus?.back == 1 Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MiniButton("前灯开") { viewModel.sendLightControl(1, 0) } - MiniButton("前灯关") { viewModel.sendLightControl(0, 0) } - } - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MiniButton("后灯开") { viewModel.sendLightControl(0, 1) } - MiniButton("后灯关") { viewModel.sendLightControl(0, 0) } + LightToggleButton( + text = "前灯", + isOn = frontOn, + enabled = connected + ) { + viewModel.sendLightControl(if (frontOn) 0 else 1, ledStatus?.back ?: 0) + } + LightToggleButton( + text = "后灯", + isOn = backOn, + enabled = connected + ) { + viewModel.sendLightControl(ledStatus?.front ?: 0, if (backOn) 0 else 1) + } } Spacer(Modifier.height(4.dp)) @@ -451,6 +479,30 @@ private fun MiniButton(text: String, enabled: Boolean = true, onClick: () -> Uni } } +@Composable +private fun LightToggleButton( + text: String, + isOn: Boolean, + enabled: Boolean = true, + onClick: () -> Unit +) { + Button( + onClick = onClick, + enabled = enabled, + colors = ButtonDefaults.buttonColors( + containerColor = if (isOn) Color(0xCCFFC107) else Color(0x66333333), + disabledContainerColor = Color(0x33222222) + ), + modifier = Modifier.width(110.dp) + ) { + Text( + "$text ${if (isOn) "开" else "关"}", + color = if (enabled) OverlayText else Color(0x88FFFFFF), + fontWeight = FontWeight.Medium + ) + } +} + @Composable private fun SectionLabel(text: String) { Text( diff --git a/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt b/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt index 01acee1..3b2b1bf 100644 --- a/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt +++ b/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt @@ -9,6 +9,7 @@ import com.example.m20_gamepad.network.models.BatteryStatus import com.example.m20_gamepad.network.models.ControlCommands import com.example.m20_gamepad.network.models.DeviceTemperature import com.example.m20_gamepad.network.models.ErrorInfo +import com.example.m20_gamepad.network.models.LedStatus import com.example.m20_gamepad.network.models.MotionStatus import com.example.m20_gamepad.network.models.StatusReport import com.example.m20_gamepad.service.JoystickController @@ -70,6 +71,9 @@ class MainViewModel( private val _batteryStatus = MutableStateFlow(null) val batteryStatus: StateFlow = _batteryStatus.asStateFlow() + private val _ledStatus = MutableStateFlow(null) + val ledStatus: StateFlow = _ledStatus.asStateFlow() + private val _lastUpdate = MutableStateFlow(null) val lastUpdate: StateFlow = _lastUpdate.asStateFlow() @@ -153,6 +157,7 @@ class MainViewModel( _motionStatus.value = null _deviceTemperature.value = null _batteryStatus.value = null + _ledStatus.value = null _lastUpdate.value = null } @@ -165,6 +170,7 @@ class MainViewModel( report.motionStatus?.let { _motionStatus.value = it } report.deviceTemperature?.let { _deviceTemperature.value = it } report.batteryStatus?.let { _batteryStatus.value = it } + report.led?.let { _ledStatus.value = it } // 通用响应错误提示 report.errorCode?.let { code -> if (code != 0) { diff --git a/app/src/main/java/com/example/m20_gamepad/video/RtspVideoPlayer.kt b/app/src/main/java/com/example/m20_gamepad/video/RtspVideoPlayer.kt index 53ebbfd..7596169 100644 --- a/app/src/main/java/com/example/m20_gamepad/video/RtspVideoPlayer.kt +++ b/app/src/main/java/com/example/m20_gamepad/video/RtspVideoPlayer.kt @@ -131,22 +131,24 @@ fun RtspVideoPlayer( } Box(modifier = modifier) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { ctx -> - PlayerView(ctx).apply { - useController = false - this.resizeMode = resizeMode.toMedia3ResizeMode() - this.player = player - setShutterBackgroundColor(android.graphics.Color.BLACK) + // 仅在视频播放中时渲染 PlayerView,否则透出底层背景 + if (state is RtspState.Playing) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + PlayerView(ctx).apply { + useController = false + this.resizeMode = resizeMode.toMedia3ResizeMode() + this.player = player + setShutterBackgroundColor(android.graphics.Color.TRANSPARENT) + } + }, + update = { view -> + view.player = player + view.resizeMode = resizeMode.toMedia3ResizeMode() } - }, - // player 引用变化时(codec 重建)更新 PlayerView - update = { view -> - view.player = player - view.resizeMode = resizeMode.toMedia3ResizeMode() - } - ) + ) + } // 连接中 / 错误时的提示覆盖层 when (val s = state) {