Skip to content

Commit 6dfff85

Browse files
eriedclaude
andcommitted
Watch: full-bleed dial, three batteries, accent and imperial follow phone
The dial now wraps the whole watch face. Inside it sits a compact battery row (wheel / phone / watch), the live speed number with units below, and the horn / light buttons. Same gauge geometry as the phone dashboard so fixes flow either direction; full-bleed mode skips the outer scale labels because they collide with the buttons at the bottom. Wire format additions on `/euc/state`: - PHONE_BATT — phone-side BatteryManager percent - IMPERIAL — AppSettings.imperialUnits - ACCENT — AppSettings.accentColor key WatchState gains phoneBatteryPercent / imperialUnits / accentKey to match. Watch resolves the accent through a small mirror palette (no shared module); units flip mph / mi / °F when imperial is on. The Wear bridge now also depends on SettingsRepository so the publisher sees those fields. Page 2 (details) gets a wheel-name header in the user's accent, a live speed line so you don't have to swipe back, and a Power row computed as V x A. Rows render in a fixed-width table (label right, value left) so the eye scans cleanly down each column on round and rectangular faces. Disconnected screen swaps the misleading red dot for a phone glyph and a two-line message ("Open EUC Planet / on your phone") so it reads as "do this", not "error". Layout sizes derive from BoxWithConstraints.maxWidth so the same code looks right on small round (~390 dp) and Watch Ultra (~454 dp). Debug builds expose an ADB demo broadcast on the watch app so the connected dashboard can be exercised without a paired phone: adb shell am broadcast -p com.eried.eucplanet \ -a com.eried.eucplanet.wear.DEMO --ef speed 32 \ --ei battery 78 --ei phone 64 --es accent teal Gated by ApplicationInfo.FLAG_DEBUGGABLE so it never reaches release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a5d0482 commit 6dfff85

8 files changed

Lines changed: 654 additions & 93 deletions

File tree

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

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.eried.eucplanet.wear
22

33
import android.content.Context
4+
import android.content.Intent
5+
import android.content.IntentFilter
6+
import android.os.BatteryManager
47
import android.util.Log
58
import com.eried.eucplanet.ble.ConnectionState
9+
import com.eried.eucplanet.data.model.AppSettings
10+
import com.eried.eucplanet.data.repository.SettingsRepository
611
import com.eried.eucplanet.data.repository.WheelRepository
712
import com.google.android.gms.wearable.PutDataMapRequest
813
import com.google.android.gms.wearable.Wearable
@@ -36,7 +41,8 @@ import javax.inject.Singleton
3641
@Singleton
3742
class WearBridge @Inject constructor(
3843
@ApplicationContext private val context: Context,
39-
private val wheelRepository: WheelRepository
44+
private val wheelRepository: WheelRepository,
45+
private val settingsRepository: SettingsRepository
4046
) {
4147
companion object {
4248
private const val TAG = "WearBridge"
@@ -48,6 +54,7 @@ class WearBridge @Inject constructor(
4854
private const val K_CONNECTED = "c"
4955
private const val K_SPEED = "s"
5056
private const val K_BATTERY = "b"
57+
private const val K_PHONE_BATT = "b2"
5158
private const val K_VOLTAGE = "v"
5259
private const val K_CURRENT = "i"
5360
private const val K_PWM = "p"
@@ -59,6 +66,8 @@ class WearBridge @Inject constructor(
5966
private const val K_WHEEL_NAME = "n"
6067
private const val K_HAS_HORN = "ch"
6168
private const val K_HAS_LIGHT = "cl"
69+
private const val K_IMPERIAL = "im"
70+
private const val K_ACCENT = "ac"
6271
private const val K_TIMESTAMP = "ts"
6372
}
6473

@@ -80,14 +89,15 @@ class WearBridge @Inject constructor(
8089
wheelRepository.wheelData,
8190
wheelRepository.connectionState,
8291
wheelRepository.modelName,
83-
wheelRepository.maxSpeedCap
84-
) { data, state, name, maxSpeed ->
85-
Quad(data, state, name, maxSpeed)
92+
wheelRepository.maxSpeedCap,
93+
settingsRepository.settings
94+
) { data, state, name, maxSpeed, settings ->
95+
Snapshot(data, state, name, maxSpeed, settings)
8696
}
8797
.sample(PUBLISH_INTERVAL_MS)
8898
.distinctUntilChanged()
89-
.collect { (data, state, name, maxSpeed) ->
90-
publish(data, state, name, maxSpeed)
99+
.collect { snap ->
100+
publish(snap.data, snap.state, snap.name, snap.maxSpeed, snap.settings)
91101
}
92102
}
93103
}
@@ -96,14 +106,16 @@ class WearBridge @Inject constructor(
96106
data: com.eried.eucplanet.data.model.WheelData,
97107
state: ConnectionState,
98108
name: String?,
99-
maxSpeed: Float
109+
maxSpeed: Float,
110+
settings: AppSettings
100111
) {
101112
try {
102113
val request = PutDataMapRequest.create(PATH_STATE).apply {
103114
dataMap.putBoolean(K_CONNECTED, state == ConnectionState.CONNECTED)
104115
dataMap.putString(K_WHEEL_NAME, name ?: "")
105116
dataMap.putFloat(K_SPEED, data.speed)
106117
dataMap.putInt(K_BATTERY, data.batteryPercent)
118+
dataMap.putInt(K_PHONE_BATT, readPhoneBatteryPercent())
107119
dataMap.putFloat(K_VOLTAGE, data.voltage)
108120
dataMap.putFloat(K_CURRENT, data.current)
109121
dataMap.putFloat(K_PWM, data.pwm)
@@ -116,6 +128,8 @@ class WearBridge @Inject constructor(
116128
// don't, gate these on WheelAdapter.capabilities. For now true.
117129
dataMap.putBoolean(K_HAS_HORN, true)
118130
dataMap.putBoolean(K_HAS_LIGHT, true)
131+
dataMap.putBoolean(K_IMPERIAL, settings.imperialUnits)
132+
dataMap.putString(K_ACCENT, settings.accentColor)
119133
// DataItems dedupe by content. Bumping a timestamp guarantees
120134
// the watch sees every snapshot when the values stop changing
121135
// (e.g. wheel idle, but we want the connection-state heartbeat).
@@ -128,5 +142,24 @@ class WearBridge @Inject constructor(
128142
}
129143
}
130144

131-
private data class Quad<A, B, C, D>(val a: A, val b: B, val c: C, val d: D)
145+
private fun readPhoneBatteryPercent(): Int {
146+
val bm = context.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
147+
bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)?.let { lvl ->
148+
if (lvl in 0..100) return lvl
149+
}
150+
// Fallback to the sticky ACTION_BATTERY_CHANGED broadcast on devices where
151+
// BATTERY_PROPERTY_CAPACITY isn't reliable.
152+
val intent: Intent? = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
153+
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
154+
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
155+
return if (level >= 0 && scale > 0) (level * 100 / scale).coerceIn(0, 100) else 0
156+
}
157+
158+
private data class Snapshot(
159+
val data: com.eried.eucplanet.data.model.WheelData,
160+
val state: ConnectionState,
161+
val name: String?,
162+
val maxSpeed: Float,
163+
val settings: AppSettings
164+
)
132165
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,75 @@
11
package com.eried.eucplanet.wear
22

3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.content.IntentFilter
7+
import android.content.pm.ApplicationInfo
38
import android.os.Bundle
49
import androidx.activity.ComponentActivity
510
import androidx.activity.compose.setContent
11+
import androidx.core.content.ContextCompat
12+
import com.eried.eucplanet.wear.bridge.WatchState
13+
import com.eried.eucplanet.wear.bridge.WatchStateRepository
614
import com.eried.eucplanet.wear.ui.WatchApp
715

816
class MainActivity : ComponentActivity() {
17+
18+
private val demoReceiver = object : BroadcastReceiver() {
19+
/**
20+
* Debug-only hook. `adb shell am broadcast -p com.eried.eucplanet \
21+
* -a com.eried.eucplanet.wear.DEMO --ef speed 35 --ei battery 78 \
22+
* --ei phone 64 --es accent teal`
23+
* fills the watch UI with synthetic state so the dashboard renders
24+
* without the phone/Data Layer pairing being live. Gated by the
25+
* debuggable flag so this never ships in a release build.
26+
*/
27+
override fun onReceive(context: Context, intent: Intent) {
28+
if (!context.isDebuggable()) return
29+
WatchStateRepository.update(
30+
WatchState(
31+
connected = true,
32+
wheelName = intent.getStringExtra("name") ?: "Demo wheel",
33+
speedKmh = intent.getFloatExtra("speed", 28f),
34+
batteryPercent = intent.getIntExtra("battery", 78),
35+
phoneBatteryPercent = intent.getIntExtra("phone", 64),
36+
voltage = intent.getFloatExtra("voltage", 95.4f),
37+
current = intent.getFloatExtra("current", 12.3f),
38+
pwmPercent = intent.getFloatExtra("pwm", 35f),
39+
temperatureC = intent.getFloatExtra("temp", 42f),
40+
tripKm = intent.getFloatExtra("trip", 12.34f),
41+
torque = intent.getFloatExtra("torque", 5.6f),
42+
lightOn = intent.getBooleanExtra("light", false),
43+
maxSpeedKmh = intent.getFloatExtra("maxSpeed", 70f),
44+
hasHorn = intent.getBooleanExtra("horn", true),
45+
hasLight = intent.getBooleanExtra("hasLight", true),
46+
imperialUnits = intent.getBooleanExtra("imperial", false),
47+
accentKey = intent.getStringExtra("accent") ?: "default"
48+
)
49+
)
50+
}
51+
}
52+
953
override fun onCreate(savedInstanceState: Bundle?) {
1054
super.onCreate(savedInstanceState)
55+
if (isDebuggable()) {
56+
ContextCompat.registerReceiver(
57+
this,
58+
demoReceiver,
59+
IntentFilter("com.eried.eucplanet.wear.DEMO"),
60+
ContextCompat.RECEIVER_EXPORTED
61+
)
62+
}
1163
setContent { WatchApp() }
1264
}
65+
66+
override fun onDestroy() {
67+
super.onDestroy()
68+
if (isDebuggable()) {
69+
runCatching { unregisterReceiver(demoReceiver) }
70+
}
71+
}
1372
}
73+
74+
private fun Context.isDebuggable(): Boolean =
75+
(applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0

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
@@ -27,6 +27,7 @@ class WatchBridgeService : WearableListenerService() {
2727
wheelName = map.getString(WatchKeys.WHEEL_NAME, "") ?: "",
2828
speedKmh = map.getFloat(WatchKeys.SPEED, 0f),
2929
batteryPercent = map.getInt(WatchKeys.BATTERY, 0),
30+
phoneBatteryPercent = map.getInt(WatchKeys.PHONE_BATT, 0),
3031
voltage = map.getFloat(WatchKeys.VOLTAGE, 0f),
3132
current = map.getFloat(WatchKeys.CURRENT, 0f),
3233
pwmPercent = map.getFloat(WatchKeys.PWM, 0f),
@@ -36,7 +37,9 @@ class WatchBridgeService : WearableListenerService() {
3637
lightOn = map.getBoolean(WatchKeys.LIGHT_ON, false),
3738
maxSpeedKmh = map.getFloat(WatchKeys.MAX_SPEED, 0f),
3839
hasHorn = map.getBoolean(WatchKeys.HAS_HORN, false),
39-
hasLight = map.getBoolean(WatchKeys.HAS_LIGHT, false)
40+
hasLight = map.getBoolean(WatchKeys.HAS_LIGHT, false),
41+
imperialUnits = map.getBoolean(WatchKeys.IMPERIAL, false),
42+
accentKey = map.getString(WatchKeys.ACCENT, "default") ?: "default"
4043
)
4144
)
4245
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ object WatchKeys {
2121
const val CONNECTED = "c"
2222
const val SPEED = "s"
2323
const val BATTERY = "b"
24+
const val PHONE_BATT = "b2"
2425
const val VOLTAGE = "v"
2526
const val CURRENT = "i"
2627
const val PWM = "p"
@@ -32,6 +33,8 @@ object WatchKeys {
3233
const val WHEEL_NAME = "n"
3334
const val HAS_HORN = "ch"
3435
const val HAS_LIGHT = "cl"
36+
const val IMPERIAL = "im"
37+
const val ACCENT = "ac"
3538
/** Bumped on every snapshot so the watch always sees a fresh DataItem. */
3639
const val TIMESTAMP = "ts"
3740
}
@@ -51,6 +54,7 @@ data class WatchState(
5154
val wheelName: String = "",
5255
val speedKmh: Float = 0f,
5356
val batteryPercent: Int = 0,
57+
val phoneBatteryPercent: Int = 0,
5458
val voltage: Float = 0f,
5559
val current: Float = 0f,
5660
val pwmPercent: Float = 0f,
@@ -60,5 +64,7 @@ data class WatchState(
6064
val lightOn: Boolean = false,
6165
val maxSpeedKmh: Float = 0f,
6266
val hasHorn: Boolean = false,
63-
val hasLight: Boolean = false
67+
val hasLight: Boolean = false,
68+
val imperialUnits: Boolean = false,
69+
val accentKey: String = "default"
6470
)

0 commit comments

Comments
 (0)