Skip to content

Commit 606005b

Browse files
committed
Add jvm target with Hot Reload (beta06)
1 parent 8e6ba36 commit 606005b

File tree

21 files changed

+254
-27
lines changed

21 files changed

+254
-27
lines changed

app-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
pacemaker {
77
ios()
88
android()
9+
jvm()
910

1011
features {
1112
useSqlDelight {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.sellmair.pacemaker
2+
3+
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
4+
import com.russhwolf.settings.PreferencesSettings
5+
import com.russhwolf.settings.Settings
6+
import io.sellmair.evas.Events
7+
import io.sellmair.evas.States
8+
import io.sellmair.pacemaker.bluetooth.HeartRateSensor
9+
import io.sellmair.pacemaker.bluetooth.HeartRateSensorBluetoothService
10+
import io.sellmair.pacemaker.bluetooth.PacemakerBluetoothService
11+
import io.sellmair.pacemaker.sql.PacemakerDatabase
12+
import kotlinx.coroutines.CompletableDeferred
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Deferred
15+
import kotlinx.coroutines.flow.MutableSharedFlow
16+
import kotlinx.coroutines.flow.MutableStateFlow
17+
import kotlinx.coroutines.flow.update
18+
import java.util.prefs.Preferences
19+
20+
actual fun ApplicationBackend.launchPlatform(scope: CoroutineScope) {
21+
scope.launchHeartRateSensorEmulation()
22+
}
23+
24+
object JvmApplicationBackend : ApplicationBackend {
25+
override val pacemakerBluetoothService: Deferred<PacemakerBluetoothService>
26+
get() = CompletableDeferred()
27+
28+
override val heartRateSensorBluetoothService: Deferred<HeartRateSensorBluetoothService>
29+
get() = CompletableDeferred(JvmHeartRateSensorBluetoothService)
30+
31+
private val database by lazy { createInMemoryDatabase() }
32+
33+
override val sessionService: SessionService by lazy {
34+
SqlSessionService(database)
35+
}
36+
37+
private val meId by lazy {
38+
settings.meId
39+
}
40+
41+
override val userService: UserService by lazy {
42+
SqlUserService(database, meId)
43+
}
44+
45+
override val states: States = States()
46+
override val events: Events = Events()
47+
override val settings: Settings = PreferencesSettings(Preferences.userRoot())
48+
}
49+
50+
51+
internal fun createInMemoryDatabase(): SafePacemakerDatabase = SafePacemakerDatabase {
52+
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
53+
PacemakerDatabase.Schema.create(driver)
54+
PacemakerDatabase(driver)
55+
}
56+
57+
object JvmHeartRateSensorBluetoothService : HeartRateSensorBluetoothService {
58+
fun addSensor(sensor: HeartRateSensor) {
59+
allSensorsNearby.update { sensors -> sensors + sensor }
60+
newSensorsNearby.tryEmit(sensor)
61+
}
62+
63+
override val newSensorsNearby = MutableSharedFlow<HeartRateSensor>(replay = 1)
64+
65+
66+
override val allSensorsNearby = MutableStateFlow<List<HeartRateSensor>>(emptyList())
67+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.sellmair.pacemaker
2+
3+
internal actual suspend fun isBluetoothEnabled(): Boolean {
4+
return true
5+
}
6+
7+
internal actual suspend fun isBluetoothPermissionGranted(): Boolean {
8+
return true
9+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.sellmair.pacemaker
2+
3+
import io.sellmair.pacemaker.ble.BleConnectable
4+
import io.sellmair.pacemaker.ble.BleConnection
5+
import io.sellmair.pacemaker.ble.BleDeviceId
6+
import io.sellmair.pacemaker.ble.BleServiceDescriptor
7+
import io.sellmair.pacemaker.bluetooth.HeartRateSensor
8+
import io.sellmair.pacemaker.bluetooth.HeartRateSensorMeasurement
9+
import io.sellmair.pacemaker.bluetooth.HeartRateSensorServiceDescriptors
10+
import io.sellmair.pacemaker.model.HeartRate
11+
import io.sellmair.pacemaker.model.HeartRateSensorId
12+
import io.sellmair.pacemaker.model.HeartRateSensorInfo
13+
import kotlinx.coroutines.*
14+
import kotlinx.coroutines.flow.*
15+
import kotlin.random.Random
16+
import kotlin.time.Clock
17+
import kotlin.time.Duration.Companion.milliseconds
18+
import kotlin.time.Duration.Companion.seconds
19+
20+
fun CoroutineScope.launchHeartRateSensorEmulation(): Job = launch {
21+
val mySensor = object : HeartRateSensor {
22+
override val heartRate = MutableSharedFlow<HeartRateSensorMeasurement>()
23+
24+
init {
25+
launch {
26+
var currentHeartRate = HeartRate(Random.nextInt(50, 100))
27+
var drift = 0f
28+
29+
launch {
30+
while (isActive) {
31+
delay(2.seconds)
32+
drift = (Random.nextFloat() - 0.5f) * 0.5f
33+
}
34+
}
35+
36+
while (isActive) {
37+
delay(128.milliseconds)
38+
val delta = (Random.nextFloat() - 0.5f) + drift
39+
currentHeartRate = HeartRate(Math.clamp(currentHeartRate.value + delta, 40f, 160f))
40+
41+
heartRate.emit(
42+
HeartRateSensorMeasurement(
43+
heartRate = currentHeartRate,
44+
sensorInfo = HeartRateSensorInfo(HeartRateSensorId(deviceId.value)),
45+
receivedTime = Clock.System.now()
46+
)
47+
)
48+
}
49+
}
50+
}
51+
52+
override val deviceName: String = "Emulated"
53+
54+
override val deviceId: BleDeviceId
55+
get() = BleDeviceId("Emulated")
56+
57+
override val service: BleServiceDescriptor
58+
get() = HeartRateSensorServiceDescriptors.service
59+
60+
private val _connection = MutableStateFlow<BleConnection?>(null)
61+
62+
override val connection: SharedFlow<BleConnection> = _connection
63+
.filterNotNull()
64+
.shareIn(this@launch, SharingStarted.Eagerly)
65+
66+
override val connectionState = MutableStateFlow(BleConnectable.ConnectionState.Disconnected)
67+
68+
override val connectIfPossible = MutableStateFlow(false)
69+
70+
override fun connectIfPossible(connect: Boolean) {
71+
connectIfPossible.value = connect
72+
73+
connectionState.value = if (connect) BleConnectable.ConnectionState.Connected
74+
else BleConnectable.ConnectionState.Disconnected
75+
}
76+
77+
override val rssi = MutableStateFlow<Int?>(24)
78+
79+
}
80+
81+
JvmHeartRateSensorBluetoothService.addSensor(mySensor)
82+
}

app/build.gradle.kts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
77
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
88

99
plugins {
10-
id("pacemaker-application")
11-
id("org.jetbrains.compose")
12-
kotlin("plugin.compose")
10+
`pacemaker-application`
11+
org.jetbrains.compose
12+
org.jetbrains.kotlin.plugin.compose
13+
id("org.jetbrains.compose.hot-reload")
1314
}
1415

1516
pacemaker {
1617
ios()
1718
android()
19+
jvm()
1820
}
1921

2022
extensions.configure(ApplicationExtension::class) {
@@ -48,6 +50,11 @@ kotlin {
4850

4951
}
5052

53+
sourceSets.jvmMain.dependencies {
54+
implementation(compose.desktop.currentOs)
55+
implementation(deps.coroutines.swing)
56+
}
57+
5158
sourceSets.androidMain.get().dependencies {
5259
/* androidx */
5360
implementation("androidx.activity:activity-compose:1.9.3")

app/src/androidMain/kotlin/launchPlatformSpecificFrontendServices.kt

Lines changed: 0 additions & 5 deletions
This file was deleted.

app/src/appleMain/kotlin/launchPlatformSpecificFrontendServices.kt

Lines changed: 0 additions & 5 deletions
This file was deleted.

app/src/commonMain/kotlin/io/sellmair/pacemaker/launchFrontendServices.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,4 @@ import kotlinx.coroutines.CoroutineScope
55

66
fun CoroutineScope.launchFrontendServices() {
77
launchHeartRateUtteranceActor()
8-
launchPlatformSpecificFrontendServices()
98
}
10-
11-
expect fun CoroutineScope.launchPlatformSpecificFrontendServices()
12-
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.sellmair.pacemaker.ui
2+
3+
@androidx.compose.runtime.Composable
4+
internal actual fun BackHandlerIfAny(enabled: Boolean, onBack: () -> Unit) {
5+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.sellmair.pacemaker.ui
2+
3+
import androidx.compose.material.Text
4+
import androidx.compose.ui.Alignment
5+
import androidx.compose.ui.unit.DpSize
6+
import androidx.compose.ui.unit.dp
7+
import androidx.compose.ui.window.WindowPosition
8+
import androidx.compose.ui.window.WindowState
9+
import androidx.compose.ui.window.singleWindowApplication
10+
import io.sellmair.evas.compose.installEvas
11+
import io.sellmair.evas.eventsOrThrow
12+
import io.sellmair.evas.statesOrThrow
13+
import io.sellmair.pacemaker.JvmApplicationBackend
14+
import io.sellmair.pacemaker.JvmHeartRateSensorBluetoothService
15+
import io.sellmair.pacemaker.launchApplicationBackend
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.SupervisorJob
19+
20+
val appScope = CoroutineScope(
21+
SupervisorJob() + Dispatchers.Main +
22+
JvmApplicationBackend.events + JvmApplicationBackend.states
23+
)
24+
25+
fun main() {
26+
27+
JvmApplicationBackend.launchApplicationBackend(appScope)
28+
29+
30+
singleWindowApplication(
31+
title = "Pacemaker",
32+
alwaysOnTop = true,
33+
state = WindowState(position = WindowPosition.Aligned(Alignment.TopEnd), size = DpSize(400.dp, 800.dp)),
34+
) {
35+
installEvas(appScope.coroutineContext.eventsOrThrow, appScope.coroutineContext.statesOrThrow) {
36+
ApplicationWindow()
37+
}
38+
}
39+
}
40+

0 commit comments

Comments
 (0)