summaryrefslogtreecommitdiff
path: root/desktop/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/src/main')
-rw-r--r--desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt43
-rw-r--r--desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt22
2 files changed, 64 insertions, 1 deletions
diff --git a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt
index c2f65ca..085ad9a 100644
--- a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt
+++ b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt
@@ -9,7 +9,13 @@ import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import coffee.liz.rookery.audio.Chime
import coffee.liz.rookery.audio.ChimePlayer
+import coffee.liz.rookery.bt.BlueZTransport
+import coffee.liz.rookery.bt.CubeBackend
+import coffee.liz.rookery.bt.GanCubeConnector
+import coffee.liz.rookery.bt.JvmAes128
import coffee.liz.rookery.images.BraveImages
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.filter
import coffee.liz.rookery.images.ImageHit
import coffee.liz.rookery.model.Event
import coffee.liz.rookery.scramble.TnoodleScrambler
@@ -50,6 +56,9 @@ class LocalConfig {
runCatching { java.nio.file.Files.setPosixFilePermissions(file, java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")) }
}
+ fun get(key: String): String? = props.getProperty(key)
+ fun set(key: String, value: String?) { if (value == null) props.remove(key) else props.setProperty(key, value); save() }
+
/** Brave Search API key: env ROOKERY_BRAVE_KEY, else `braveApiKey` in this (never synced) file. */
val braveApiKey: String? get() = System.getenv("ROOKERY_BRAVE_KEY")?.takeIf { it.isNotBlank() } ?: props.getProperty("braveApiKey")?.takeIf { it.isNotBlank() }
@@ -142,6 +151,40 @@ class DesktopHost(private val config: LocalConfig) : Host {
data.bytes to "webp"
}.getOrNull()
+ /** Bluetooth goes through BlueZ, so Linux only for now. */
+ override val cubes: CubeBackend? = if (!System.getProperty("os.name").lowercase().contains("linux")) null else object : CubeBackend {
+ private val transport by lazy { BlueZTransport() }
+ override fun scan() = transport.scan().filter { GanCubeConnector.isGanCube(it) }
+ override suspend fun connect(scope: CoroutineScope, address: String, mac: String) = GanCubeConnector(transport, JvmAes128).connect(scope, address, mac)
+ }
+
+ override val audioSampleRate: Int get() = 44_100
+ private val audioFormat = javax.sound.sampled.AudioFormat(44_100f, 16, 1, true, false)
+ private val lineInfo = javax.sound.sampled.DataLine.Info(javax.sound.sampled.TargetDataLine::class.java, audioFormat)
+
+ override fun audioInputs(): List<String> = javax.sound.sampled.AudioSystem.getMixerInfo()
+ .filter { runCatching { javax.sound.sampled.AudioSystem.getMixer(it).isLineSupported(lineInfo) }.getOrDefault(false) }.map { it.name }.distinct()
+
+ override fun openAudio(input: String?, onSamples: (FloatArray, Int) -> Unit): AutoCloseable {
+ val mixer = javax.sound.sampled.AudioSystem.getMixerInfo().firstOrNull { it.name == input }?.let { javax.sound.sampled.AudioSystem.getMixer(it) }
+ val line = try {
+ ((mixer?.getLine(lineInfo) ?: javax.sound.sampled.AudioSystem.getLine(lineInfo)) as javax.sound.sampled.TargetDataLine).apply { open(audioFormat, 8192); start() }
+ } catch (e: Exception) { error("couldn't open ${input ?: "the default input"} (${e.message})") }
+ val thread = Thread {
+ val bytes = ByteArray(2048); val samples = FloatArray(1024)
+ while (line.isOpen) {
+ val n = line.read(bytes, 0, bytes.size)
+ if (n <= 0) break
+ for (i in 0 until n / 2) samples[i] = (((bytes[2 * i + 1].toInt() shl 8) or (bytes[2 * i].toInt() and 0xFF)).toShort()) / 32768f
+ onSamples(samples, n / 2)
+ }
+ }.apply { isDaemon = true; name = "stackmat-audio"; start() }
+ return AutoCloseable { line.stop(); line.close(); thread.interrupt() }
+ }
+
+ override fun localPref(key: String): String? = config.get("local.$key")
+ override fun setLocalPref(key: String, value: String?) = config.set("local.$key", value)
+
var onFullscreen: (Boolean) -> Unit = {}
override fun setFullscreen(on: Boolean) = onFullscreen(on)
diff --git a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt
index d7621eb..6a00c24 100644
--- a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt
+++ b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt
@@ -57,6 +57,24 @@ fun shots(args: List<String>) {
override suspend fun fetchBytes(url: String) = ByteArray(0)
override fun shrinkImage(bytes: ByteArray): Pair<ByteArray, String>? = null
override fun setFullscreen(on: Boolean) {}
+ override val cubes = object : coffee.liz.rookery.bt.CubeBackend {
+ override fun scan() = kotlinx.coroutines.flow.flowOf(coffee.liz.rookery.bt.BleAdvertisement("AA:BB:CC:00:11:22", "GANicE3_1122", -58))
+ override suspend fun connect(scope: CoroutineScope, address: String, mac: String) = object : coffee.liz.rookery.bt.SmartCube {
+ override val name = "GANicE3_1122"; override val address = address
+ override val events = kotlinx.coroutines.flow.flow {
+ emit(coffee.liz.rookery.bt.SmartCubeEvent.Battery(87))
+ emit(coffee.liz.rookery.bt.SmartCubeEvent.Facelets("UUUUUUFFFRRRRRRRRRFFDFFDFFDDDDDDDBBBLLLLLLLLLUBBUBBUBB"))
+ emit(coffee.liz.rookery.bt.SmartCubeEvent.Move("F", null, 0)); kotlinx.coroutines.awaitCancellation()
+ }
+ override suspend fun requestFacelets() {}; override suspend fun requestBattery() {}; override suspend fun requestHardware() {}
+ override suspend fun resetToSolved() {}; override suspend fun disconnect() {}
+ }
+ }
+ override fun audioInputs() = listOf("USB Audio Device")
+ override fun openAudio(input: String?, onSamples: (FloatArray, Int) -> Unit) = AutoCloseable { }
+ override val audioSampleRate = 44_100
+ override fun localPref(key: String): String? = null
+ override fun setLocalPref(key: String, value: String?) {}
}
val model = AppModel(repo, TnoodleScrambler(), host, CoroutineScope(SupervisorJob() + Dispatchers.Default))
model.selectSession("s1")
@@ -78,7 +96,9 @@ fun shots(args: List<String>) {
shoot("party", settle = 1800) { model.testParty() }
model.party = null
for (mode in BldMode.entries) shoot("bld-${mode.name.lowercase()}", settle = 4000) { model.tab = Tab.BLD; model.bldMode = mode }
- shoot("settings") { model.tab = Tab.SETTINGS }
+ shoot("settings") { model.tab = Tab.SETTINGS; model.cube.scan(1) }
+ shoot("settings-cube") { model.cube.connect("AA:BB:CC:00:11:22", "GANicE3_1122"); model.stackmat.change(true) }
+ model.stackmat.change(false)
shoot("import") { model.tab = Tab.IMPORT }
model.setTheme(ThemeMode.LIGHT)
shoot("timer-paper") { model.tab = Tab.TIMER }