Skip to content

Commit 2751f09

Browse files
eriedclaude
andcommitted
Watch: dashes for offline metrics, accent speed, gauge band follows phone
Header on the second page reads just "Disconnected" instead of "Wheel disconnected". Every metric except speed renders as an em-dash when the wheel isn't connected — voltage, current, power, PWM, temp, torque, trip, and the wheel battery chip on page one. Speed stays at 0 (and is now tinted with the accent color while disconnected so it doesn't read as a green-tier 'idle' value). PWM display option renamed Numbers to Text in the picker. When the indicator is set to Text-only the watch shows "Load (PWM): N%" instead of a bare percentage so the rider has the label without the bar. Speed gauge color band now follows the phone Display setting (showGaugeColorBand + orange/red threshold percentages) so toggling it on the phone reflects on the watch within a publish cycle. Horn and light buttons stay on the dashboard while disconnected, greyed rather than hidden. The hasHorn / hasLight defaults flip to true so the buttons render before the first push from the phone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8f16952 commit 2751f09

6 files changed

Lines changed: 76 additions & 25 deletions

File tree

app/src/main/java/com/eried/eucplanet/wear/WearBridge.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ class WearBridge @Inject constructor(
7676
private const val K_OPT_SHOW_WATCH_BATT = "wwb"
7777
private const val K_OPT_PWM_DISPLAY = "wpd"
7878
private const val K_OPT_SHOW_SPEED_UNIT = "wsu"
79+
private const val K_OPT_GAUGE_BAND = "wgb"
80+
private const val K_OPT_GAUGE_ORANGE = "wgo"
81+
private const val K_OPT_GAUGE_RED = "wgr"
7982

8083
private const val PATH_WAKE = "/euc/wake"
8184
}
@@ -169,6 +172,9 @@ class WearBridge @Inject constructor(
169172
dataMap.putBoolean(K_OPT_SHOW_WATCH_BATT, settings.watchShowWatchBattery)
170173
dataMap.putString(K_OPT_PWM_DISPLAY, settings.watchPwmDisplay)
171174
dataMap.putBoolean(K_OPT_SHOW_SPEED_UNIT, settings.watchShowSpeedUnit)
175+
dataMap.putBoolean(K_OPT_GAUGE_BAND, settings.showGaugeColorBand)
176+
dataMap.putInt(K_OPT_GAUGE_ORANGE, settings.gaugeOrangeThresholdPct)
177+
dataMap.putInt(K_OPT_GAUGE_RED, settings.gaugeRedThresholdPct)
172178
// DataItems dedupe by content. Bumping a timestamp guarantees
173179
// the watch sees every snapshot when the values stop changing
174180
// (e.g. wheel idle, but we want the connection-state heartbeat).

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
<string name="watch_show_watch_battery">Show watch battery</string>
9999
<string name="watch_pwm_display">Load (PWM) indicator</string>
100100
<string name="watch_pwm_bar">Bar</string>
101-
<string name="watch_pwm_numbers">Numbers</string>
101+
<string name="watch_pwm_numbers">Text</string>
102102
<string name="watch_pwm_both">Both</string>
103103
<string name="watch_show_speed_unit">Speed unit label</string>
104104
<string name="watch_show_speed_unit_desc">Show the unit label next to the big speed number</string>

wear/src/main/java/com/eried/eucplanet/wear/bridge/WatchBridgeService.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ class WatchBridgeService : WearableListenerService() {
5454
showPhoneBattery = map.getBoolean(WatchKeys.OPT_SHOW_PHONE_BATT, true),
5555
showWatchBattery = map.getBoolean(WatchKeys.OPT_SHOW_WATCH_BATT, true),
5656
pwmDisplay = map.getString(WatchKeys.OPT_PWM_DISPLAY, "BOTH") ?: "BOTH",
57-
showSpeedUnit = map.getBoolean(WatchKeys.OPT_SHOW_SPEED_UNIT, true)
57+
showSpeedUnit = map.getBoolean(WatchKeys.OPT_SHOW_SPEED_UNIT, true),
58+
showGaugeBand = map.getBoolean(WatchKeys.OPT_GAUGE_BAND, false),
59+
gaugeOrangeThresholdPct = map.getInt(WatchKeys.OPT_GAUGE_ORANGE, 65),
60+
gaugeRedThresholdPct = map.getInt(WatchKeys.OPT_GAUGE_RED, 85)
5861
)
5962
)
6063
}

wear/src/main/java/com/eried/eucplanet/wear/bridge/WatchProtocol.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ object WatchKeys {
4949
/** "BAR" / "NUMBERS" / "BOTH". */
5050
const val OPT_PWM_DISPLAY = "wpd"
5151
const val OPT_SHOW_SPEED_UNIT = "wsu"
52+
/** Phone's `showGaugeColorBand` toggle. When false, the gauge stays
53+
* monochrome on the watch too. */
54+
const val OPT_GAUGE_BAND = "wgb"
55+
/** Phone's orange-zone threshold percentage of the speed range. */
56+
const val OPT_GAUGE_ORANGE = "wgo"
57+
/** Phone's red-zone threshold percentage. */
58+
const val OPT_GAUGE_RED = "wgr"
5259
}
5360

5461
object WatchControl {
@@ -90,8 +97,12 @@ data class WatchState(
9097
val torque: Float = 0f,
9198
val lightOn: Boolean = false,
9299
val maxSpeedKmh: Float = 0f,
93-
val hasHorn: Boolean = false,
94-
val hasLight: Boolean = false,
100+
// Default both to true so the disconnected dashboard still renders the
101+
// (greyed) horn + light buttons — V14 family always has these and the
102+
// watch never sees a wheel without them. Phone's WearBridge will overwrite
103+
// with the wheel's actual capabilities once telemetry starts.
104+
val hasHorn: Boolean = true,
105+
val hasLight: Boolean = true,
95106
val imperialUnits: Boolean = false,
96107
val accentKey: String = "default",
97108
// Watch UI options sourced from phone Settings.
@@ -100,5 +111,8 @@ data class WatchState(
100111
val showPhoneBattery: Boolean = true,
101112
val showWatchBattery: Boolean = true,
102113
val pwmDisplay: String = "BOTH",
103-
val showSpeedUnit: Boolean = true
114+
val showSpeedUnit: Boolean = true,
115+
val showGaugeBand: Boolean = false,
116+
val gaugeOrangeThresholdPct: Int = 65,
117+
val gaugeRedThresholdPct: Int = 85
104118
)

wear/src/main/java/com/eried/eucplanet/wear/ui/WatchApp.kt

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ import com.eried.eucplanet.wear.bridge.WatchControl
5858
import com.eried.eucplanet.wear.bridge.WatchState
5959
import com.eried.eucplanet.wear.bridge.WatchStateRepository
6060

61+
/** Em-dash placeholder used when a metric isn't live. Matches the phone dashboard. */
62+
private const val DASH = ""
63+
6164
/**
6265
* Two-page horizontal pager: page 0 is the dashboard (gauge + horn/light);
6366
* page 1 is the detail strip (voltage / current / PWM / temp / trip / torque).
@@ -108,13 +111,17 @@ private fun MainScreen(state: WatchState, accent: Color) {
108111
val centerOffsetY = -(sw * 0.06f).coerceIn(18f, 28f).dp
109112
val watchPercent = rememberWatchBatteryPercent()
110113

111-
// Full-bleed dial as the background frame.
114+
// Full-bleed dial as the background frame. Color-band visibility and
115+
// its orange/red thresholds follow the phone's Display settings so the
116+
// two surfaces always agree.
112117
SpeedGauge(
113118
speed = state.speedKmh,
114119
maxSpeed = maxSpeed,
115120
imperial = state.imperialUnits,
116121
accent = accent,
117-
showColorBand = true,
122+
showColorBand = state.showGaugeBand,
123+
orangeThresholdPct = state.gaugeOrangeThresholdPct,
124+
redThresholdPct = state.gaugeRedThresholdPct,
118125
fullBleed = true,
119126
drawSpeedText = false,
120127
modifier = Modifier.fillMaxSize()
@@ -150,7 +157,11 @@ private fun MainScreen(state: WatchState, accent: Color) {
150157
text = "%.0f".format(WatchUnits.speed(state.speedKmh, state.imperialUnits)),
151158
fontSize = speedFontSp,
152159
fontWeight = FontWeight.Bold,
153-
color = speedTierColor(state.speedKmh, maxSpeed)
160+
// Disconnected: use the accent color so the dashboard
161+
// matches the gauge's safe-band tint instead of the
162+
// green-tier "0" that reads as if the wheel were idle.
163+
color = if (state.connected) speedTierColor(state.speedKmh, maxSpeed)
164+
else accent
154165
)
155166
if (state.showSpeedUnit) {
156167
Spacer(Modifier.width(3.dp))
@@ -183,11 +194,20 @@ private fun MainScreen(state: WatchState, accent: Color) {
183194
}
184195
if (showBar && showPwmNumber) Spacer(Modifier.width(6.dp))
185196
if (showPwmNumber) {
197+
// When the bar is alongside, the "%" is enough — the
198+
// bar itself is the visual context. Text-only mode
199+
// gets a full "Load (PWM): N%" label since there's no
200+
// bar to clue the rider in.
201+
val pwmText = when {
202+
!state.connected -> if (showBar) DASH else "Load (PWM): $DASH"
203+
showBar -> "%.0f%%".format(state.pwmPercent)
204+
else -> "Load (PWM): %.0f%%".format(state.pwmPercent)
205+
}
186206
Text(
187-
text = "%.0f%%".format(state.pwmPercent),
207+
text = pwmText,
188208
fontSize = pwmNumberSp,
189209
fontWeight = FontWeight.Medium,
190-
color = pwmTierColor(state.pwmPercent)
210+
color = if (state.connected) pwmTierColor(state.pwmPercent) else Color(0xFF606060)
191211
)
192212
}
193213
}
@@ -204,9 +224,10 @@ private fun MainScreen(state: WatchState, accent: Color) {
204224
verticalArrangement = Arrangement.spacedBy((sw * 0.018f).coerceIn(5f, 9f).dp)
205225
) {
206226
BatteryRow(
207-
wheelPercent = state.batteryPercent.takeIf { state.showWheelBattery },
227+
wheelPercent = state.batteryPercent.takeIf { state.showWheelBattery && state.connected },
208228
phonePercent = state.phoneBatteryPercent.takeIf { state.showPhoneBattery },
209229
watchPercent = watchPercent.takeIf { state.showWatchBattery },
230+
showWheelChip = state.showWheelBattery,
210231
fontSize = batteryFontSp,
211232
iconSize = batteryIconDp,
212233
spacing = batterySpacingDp
@@ -259,6 +280,7 @@ private fun BatteryRow(
259280
wheelPercent: Int?,
260281
phonePercent: Int?,
261282
watchPercent: Int?,
283+
showWheelChip: Boolean,
262284
fontSize: TextUnit,
263285
iconSize: Dp,
264286
spacing: Dp
@@ -267,9 +289,14 @@ private fun BatteryRow(
267289
horizontalArrangement = Arrangement.spacedBy(spacing),
268290
verticalAlignment = Alignment.CenterVertically
269291
) {
270-
if (wheelPercent != null) BatteryChip(Icons.Filled.ElectricScooter, wheelPercent, fontSize, iconSize)
271-
if (phonePercent != null) BatteryChip(Icons.Filled.PhoneAndroid, phonePercent, fontSize, iconSize)
272-
if (watchPercent != null) BatteryChip(Icons.Filled.Watch, watchPercent, fontSize, iconSize)
292+
// Wheel chip is visible when the user enabled it; disconnected shows
293+
// an em-dash so the row layout stays put rather than collapsing the
294+
// moment a wheel drops out.
295+
if (showWheelChip) {
296+
BatteryChip(Icons.Filled.ElectricScooter, wheelPercent, fontSize, iconSize)
297+
}
298+
phonePercent?.let { BatteryChip(Icons.Filled.PhoneAndroid, it, fontSize, iconSize) }
299+
watchPercent?.let { BatteryChip(Icons.Filled.Watch, it, fontSize, iconSize) }
273300
}
274301
}
275302

@@ -281,8 +308,8 @@ private fun pwmTierColor(percent: Float): Color = when {
281308
}
282309

283310
@Composable
284-
private fun BatteryChip(icon: ImageVector, percent: Int, fontSize: TextUnit, iconSize: Dp) {
285-
val tint = batteryTint(percent)
311+
private fun BatteryChip(icon: ImageVector, percent: Int?, fontSize: TextUnit, iconSize: Dp) {
312+
val tint = if (percent != null) batteryTint(percent) else Color(0xFF606060)
286313
Row(verticalAlignment = Alignment.CenterVertically) {
287314
Icon(
288315
imageVector = icon,
@@ -292,7 +319,7 @@ private fun BatteryChip(icon: ImageVector, percent: Int, fontSize: TextUnit, ico
292319
)
293320
Spacer(Modifier.width(2.dp))
294321
Text(
295-
text = "$percent",
322+
text = percent?.toString() ?: DASH,
296323
fontSize = fontSize,
297324
color = tint,
298325
fontWeight = FontWeight.Medium
@@ -429,13 +456,14 @@ private fun DetailsScreen(state: WatchState, accent: Color) {
429456
color = Color.White
430457
)
431458
Spacer(Modifier.height(4.dp))
432-
DetailRow(R.string.watch_voltage_label, "%.1f V".format(state.voltage), labelSp, valueSp, labelWidth, valueWidth)
433-
DetailRow(R.string.watch_current_label, "%.1f A".format(state.current), labelSp, valueSp, labelWidth, valueWidth)
434-
DetailRow(R.string.watch_power_label, "%.0f W".format(powerW), labelSp, valueSp, labelWidth, valueWidth)
435-
DetailRow(R.string.watch_pwm_label, "%.0f %%".format(state.pwmPercent), labelSp, valueSp, labelWidth, valueWidth)
436-
DetailRow(R.string.watch_temp_label, "%.0f %s".format(tempDisplay, tempUnit), labelSp, valueSp, labelWidth, valueWidth)
437-
DetailRow(R.string.watch_torque_label, "%.1f".format(state.torque), labelSp, valueSp, labelWidth, valueWidth)
438-
DetailRow(R.string.watch_trip_label, "%.2f %s".format(tripDisplay, distUnit), labelSp, valueSp, labelWidth, valueWidth)
459+
val live = state.connected
460+
DetailRow(R.string.watch_voltage_label, if (live) "%.1f V".format(state.voltage) else DASH, labelSp, valueSp, labelWidth, valueWidth)
461+
DetailRow(R.string.watch_current_label, if (live) "%.1f A".format(state.current) else DASH, labelSp, valueSp, labelWidth, valueWidth)
462+
DetailRow(R.string.watch_power_label, if (live) "%.0f W".format(powerW) else DASH, labelSp, valueSp, labelWidth, valueWidth)
463+
DetailRow(R.string.watch_pwm_label, if (live) "%.0f %%".format(state.pwmPercent) else DASH, labelSp, valueSp, labelWidth, valueWidth)
464+
DetailRow(R.string.watch_temp_label, if (live) "%.0f %s".format(tempDisplay, tempUnit) else DASH, labelSp, valueSp, labelWidth, valueWidth)
465+
DetailRow(R.string.watch_torque_label, if (live) "%.1f".format(state.torque) else DASH, labelSp, valueSp, labelWidth, valueWidth)
466+
DetailRow(R.string.watch_trip_label, if (live) "%.2f %s".format(tripDisplay, distUnit) else DASH, labelSp, valueSp, labelWidth, valueWidth)
439467
}
440468
}
441469
}

wear/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<string name="app_name">EUC Planet</string>
44
<string name="watch_waiting_phone">Open EUC Planet\non your phone</string>
55
<string name="watch_waiting_wheel">Connect a wheel\nin EUC Planet</string>
6-
<string name="watch_disconnected">Wheel disconnected</string>
6+
<string name="watch_disconnected">Disconnected</string>
77
<string name="watch_horn">Horn</string>
88
<string name="watch_light">Light</string>
99
<string name="watch_speed_unit">km/h</string>

0 commit comments

Comments
 (0)