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