feat: add demo mode for testing without BLE device
Add "Demo Mode" button in device scan screen that bypasses BLE connection and calibration. NIRS monitoring generates simulated random data at 4Hz. Both monitoring views show DEMO badge in top bar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ class AppState(private val context: Context) {
|
||||
var sensorMode by mutableStateOf(SensorMode.PIEZO)
|
||||
var bladderLevel by mutableStateOf(BladderLevel())
|
||||
var isDeviceConnected by mutableStateOf(false)
|
||||
var isDemoMode by mutableStateOf(false)
|
||||
var piezoSettings by mutableStateOf(PiezoSettings())
|
||||
var catheterCount = mutableIntStateOf(0)
|
||||
|
||||
@@ -66,11 +67,22 @@ class AppState(private val context: Context) {
|
||||
fun goHome() {
|
||||
NirsManager.instance.stopStreaming()
|
||||
NirsManager.instance.reset()
|
||||
bleManager.disconnect()
|
||||
if (!isDemoMode) bleManager.disconnect()
|
||||
isDeviceConnected = false
|
||||
isDemoMode = false
|
||||
currentScreen = AppScreen.HOME
|
||||
}
|
||||
|
||||
fun enterDemoMode() {
|
||||
isDemoMode = true
|
||||
isDeviceConnected = true
|
||||
currentScreen = if (sensorMode == SensorMode.NIRS) {
|
||||
AppScreen.VIVAMYO_MONITORING
|
||||
} else {
|
||||
AppScreen.PIEZO_MONITORING
|
||||
}
|
||||
}
|
||||
|
||||
fun startFromHome() {
|
||||
currentScreen = AppScreen.SENSOR_SELECT
|
||||
}
|
||||
|
||||
+10
@@ -251,6 +251,16 @@ fun DeviceScanView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Demo mode button
|
||||
TextButton(
|
||||
onClick = { appState.enterDemoMode() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
Text("Demo Mode (No Device)", fontSize = 14.sp, color = MlSecondaryText)
|
||||
}
|
||||
|
||||
// Scan button
|
||||
Button(
|
||||
onClick = {
|
||||
|
||||
+8
-2
@@ -48,6 +48,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
|
||||
var isAutoMeasuring by remember { mutableStateOf(false) }
|
||||
|
||||
val isDemoMode = appState.isDemoMode
|
||||
val urgency = appState.bladderLevel.urgency
|
||||
val batteryLvl by bleManager.batteryLevel
|
||||
val isConnected by bleManager.isConnected
|
||||
@@ -179,13 +180,18 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
|
||||
// Battery
|
||||
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText)
|
||||
if (isDemoMode) {
|
||||
Text("DEMO", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White,
|
||||
modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 6.dp, vertical = 2.dp))
|
||||
} else {
|
||||
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (isConnected) Color.Green else Color.Red)
|
||||
.background(if (isDemoMode || isConnected) Color.Green else Color.Red)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+58
-7
@@ -31,10 +31,12 @@ import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.ble.BleManager
|
||||
import com.example.medilightv2android.managers.NirsManager
|
||||
import com.example.medilightv2android.models.NirsMcjPacket
|
||||
import com.example.medilightv2android.models.NirsOxyResult
|
||||
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
|
||||
import com.example.medilightv2android.ui.components.BleDisconnectionBanner
|
||||
import com.example.medilightv2android.ui.components.BleReconnectionState
|
||||
import com.example.medilightv2android.ui.theme.*
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun VivamyoMonitoringView(appState: AppState) {
|
||||
@@ -56,6 +58,11 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
val sampleCount by nirsManager.sampleCount
|
||||
val isFilterEnabled by nirsManager.isFilterEnabled
|
||||
|
||||
val isDemoMode = appState.isDemoMode
|
||||
var demoRunnable by remember { mutableStateOf<Runnable?>(null) }
|
||||
var demoBaseHbO2 by remember { mutableStateOf(12.0) }
|
||||
var demoBaseHb by remember { mutableStateOf(8.0) }
|
||||
|
||||
var mcjRetryCount = remember { mutableIntStateOf(0) }
|
||||
var mcjRetryRunnable by remember { mutableStateOf<Runnable?>(null) }
|
||||
|
||||
@@ -108,6 +115,39 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
handler.postDelayed(initialRetry, 2000)
|
||||
}
|
||||
|
||||
fun startDemoStream() {
|
||||
val tick = object : Runnable {
|
||||
override fun run() {
|
||||
if (!nirsManager.isStreaming.value) return
|
||||
demoBaseHbO2 += Random.nextDouble(-0.3, 0.3)
|
||||
demoBaseHb += Random.nextDouble(-0.2, 0.2)
|
||||
val hbO2 = demoBaseHbO2.coerceIn(5.0, 25.0)
|
||||
val hb = demoBaseHb.coerceIn(3.0, 15.0)
|
||||
demoBaseHbO2 = hbO2
|
||||
demoBaseHb = hb
|
||||
val tHb = hbO2 + hb
|
||||
val stO2 = if (tHb > 0) (hbO2 / tHb * 100.0) else 0.0
|
||||
|
||||
nirsManager.currentOxy.value = NirsOxyResult(hbO2, hb, tHb, stO2)
|
||||
nirsManager.sampleCount.intValue++
|
||||
nirsManager.oxyHbO2History.add(hbO2)
|
||||
nirsManager.oxyHbHistory.add(hb)
|
||||
if (nirsManager.oxyHbO2History.size > 240) nirsManager.oxyHbO2History.removeAt(0)
|
||||
if (nirsManager.oxyHbHistory.size > 240) nirsManager.oxyHbHistory.removeAt(0)
|
||||
|
||||
handler.postDelayed(this, 250)
|
||||
}
|
||||
}
|
||||
demoRunnable = tick
|
||||
handler.post(tick)
|
||||
}
|
||||
|
||||
fun stopDemoStream() {
|
||||
demoRunnable?.let { handler.removeCallbacks(it) }
|
||||
demoRunnable = null
|
||||
nirsManager.stopStreaming()
|
||||
}
|
||||
|
||||
fun stopNirsStream() {
|
||||
// 1. Stop polling chain & clear callback first
|
||||
nirsManager.stopStreaming()
|
||||
@@ -124,11 +164,16 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
if (isToggling) return
|
||||
isToggling = true
|
||||
if (isStreaming) {
|
||||
stopNirsStream()
|
||||
if (isDemoMode) stopDemoStream() else stopNirsStream()
|
||||
} else {
|
||||
bleManager.stopBatteryPolling() // Pause battery polling during streaming
|
||||
nirsManager.startStreaming()
|
||||
startNirsStream()
|
||||
if (isDemoMode) {
|
||||
nirsManager.startStreaming()
|
||||
startDemoStream()
|
||||
} else {
|
||||
bleManager.stopBatteryPolling()
|
||||
nirsManager.startStreaming()
|
||||
startNirsStream()
|
||||
}
|
||||
}
|
||||
handler.postDelayed({ isToggling = false }, 1000)
|
||||
}
|
||||
@@ -141,7 +186,8 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) { onDispose {
|
||||
if (nirsManager.isStreaming.value) stopNirsStream()
|
||||
if (isDemoMode) { stopDemoStream() }
|
||||
else if (nirsManager.isStreaming.value) stopNirsStream()
|
||||
else { cancelMcjRetry(); bleManager.onNirsMcjReceived = null }
|
||||
} }
|
||||
|
||||
@@ -196,9 +242,14 @@ fun VivamyoMonitoringView(appState: AppState) {
|
||||
Text("NIRS Monitoring", fontSize = 13.sp, color = MlSecondaryText)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText)
|
||||
if (isDemoMode) {
|
||||
Text("DEMO", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White,
|
||||
modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 6.dp, vertical = 2.dp))
|
||||
} else {
|
||||
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(if (isConnected) Color.Green else Color.Red))
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(if (isDemoMode || isConnected) Color.Green else Color.Red))
|
||||
}
|
||||
|
||||
if (showDisconnectionBanner) {
|
||||
|
||||
Reference in New Issue
Block a user