|
| 1 | +package baaahs.controller |
| 2 | + |
| 3 | +import baaahs.device.PixelFormat |
| 4 | +import baaahs.io.ByteArrayWriter |
| 5 | +import baaahs.util.Clock |
| 6 | +import baaahs.util.Logger |
| 7 | +import io.github.rowak.nanoleafapi.Frame |
| 8 | +import io.github.rowak.nanoleafapi.NanoleafDevice |
| 9 | +import io.github.rowak.nanoleafapi.NanoleafSearchCallback |
| 10 | +import io.github.rowak.nanoleafapi.StaticEffect |
| 11 | +import io.github.rowak.nanoleafapi.util.NanoleafDeviceMeta |
| 12 | +import io.github.rowak.nanoleafapi.util.NanoleafSetup |
| 13 | +import kotlinx.datetime.Instant |
| 14 | +import kotlin.coroutines.CoroutineContext |
| 15 | +import kotlin.time.Duration.Companion.milliseconds |
| 16 | + |
| 17 | +actual class NanoleafAdapter actual constructor( |
| 18 | + coroutineContext: CoroutineContext, |
| 19 | + private val clock: Clock |
| 20 | +) { |
| 21 | + private val deviceMetas = mutableSetOf<NanoleafDeviceMeta>() |
| 22 | + |
| 23 | + actual fun start(callback: (NanoleafDeviceMetadata) -> Unit) { |
| 24 | + deviceMetas.clear() |
| 25 | + |
| 26 | + NanoleafSetup.findNanoleafDevicesAsync(object : NanoleafSearchCallback { |
| 27 | + override fun onDeviceFound(meta: NanoleafDeviceMeta) { |
| 28 | + println("Found nanoleaf device: ${meta.deviceId} ${meta.deviceName} at ${meta.hostName}:${meta.port}") |
| 29 | + val deviceMeta = NanoleafDeviceMetadataData(meta.hostName, meta.port, meta.deviceId, meta.deviceName) |
| 30 | + callback(deviceMeta) |
| 31 | + } |
| 32 | + |
| 33 | + override fun onTimeout() { |
| 34 | + println("timeout!") |
| 35 | + } |
| 36 | + }, 10_000) |
| 37 | + } |
| 38 | + |
| 39 | + actual fun stop() { |
| 40 | + } |
| 41 | + |
| 42 | + actual fun getAccessToken(deviceMetadata: NanoleafDeviceMetadata): String { |
| 43 | + return NanoleafSetup.createAccessToken(deviceMetadata.hostName, deviceMetadata.port) |
| 44 | + } |
| 45 | + |
| 46 | + actual fun openDevice( |
| 47 | + deviceMetadata: NanoleafDeviceMetadata, |
| 48 | + accessToken: String |
| 49 | + ): baaahs.controller.NanoleafDevice { |
| 50 | + return JvmNanoleafDevice( |
| 51 | + deviceMetadata.hostName, deviceMetadata.port, deviceMetadata.deviceId, deviceMetadata.deviceName, |
| 52 | + accessToken |
| 53 | + ) |
| 54 | + } |
| 55 | + |
| 56 | + inner class JvmNanoleafDevice( |
| 57 | + override val hostName: String, |
| 58 | + override val port: Int, |
| 59 | + override val deviceId: String, |
| 60 | + override val deviceName: String, |
| 61 | + val accessToken: String |
| 62 | + ) : baaahs.controller.NanoleafDevice { |
| 63 | + // val accessToken = "xI2UVHZzcbuWWMIbig1Zv9NhajekW0oC" |
| 64 | + val device: NanoleafDevice = NanoleafDevice.createDevice(hostName, port, accessToken) |
| 65 | + .also { it.enableExternalStreaming() } |
| 66 | + init { |
| 67 | + println("device.globalOrientation = ${device.globalOrientation}") |
| 68 | + } |
| 69 | + val devicePanels = device.panelsRotated |
| 70 | + override val panels = devicePanels.map { |
| 71 | + NanoleafPanel(it.id, it.x, it.y, it.orientation, it.shape.toString()) |
| 72 | + } |
| 73 | + private var lastFrameSentAtMillis: Instant? = null |
| 74 | + private var consecutiveFramesSent: Int = 0 |
| 75 | + |
| 76 | + override fun deliverComponents( |
| 77 | + componentCount: Int, |
| 78 | + bytesPerComponent: Int, |
| 79 | + pixelFormat: PixelFormat, |
| 80 | + fn: (componentIndex: Int, buf: ByteArrayWriter) -> Unit |
| 81 | + ) { |
| 82 | + if (bytesPerComponent != 3) error("Expected 3 bytes per component but got $bytesPerComponent") |
| 83 | + |
| 84 | + val nowMillis = clock.now() |
| 85 | + val elapsedMillis = lastFrameSentAtMillis?.let { nowMillis - it } ?: 0.milliseconds |
| 86 | + if (elapsedMillis < minMillisBetweenFrames) { |
| 87 | + logger.warn { "Dropping frame for $deviceId after $consecutiveFramesSent successfully sent," + |
| 88 | + " last frame was ${elapsedMillis}ms ago." } |
| 89 | + consecutiveFramesSent = 0 |
| 90 | + return |
| 91 | + } |
| 92 | + lastFrameSentAtMillis = nowMillis |
| 93 | + consecutiveFramesSent++ |
| 94 | + |
| 95 | + val componentRange = 0 until componentCount |
| 96 | + val out = ByteArrayWriter() |
| 97 | + for (i in componentRange) fn(i, out) |
| 98 | + |
| 99 | + val reader = out.reader() |
| 100 | + val builder = StaticEffect.Builder(devicePanels) |
| 101 | + for (i in componentRange) { |
| 102 | + reader.offset = i * 3 |
| 103 | + pixelFormat.readColorInts(reader) { r: Int, g: Int, b: Int -> |
| 104 | + builder.setPanel(panels[i].id, Frame(r, g, b, 1)) |
| 105 | + } |
| 106 | + } |
| 107 | + device.sendStaticEffectExternalStreaming(builder.build("sparklemotion!")) |
| 108 | + |
| 109 | +// val sendComponents = if (shuffleSend) componentRange.toList().shuffled() else componentRange |
| 110 | +// for (i in sendComponents) { |
| 111 | +// reader.offset = i * 3 |
| 112 | +// pixelFormat.readColorInts(reader) { r: Int, g: Int, b: Int -> |
| 113 | +// device.setPanelExternalStreaming(panels[i].id, r, g, b, 1) |
| 114 | +// } |
| 115 | +// } |
| 116 | + } |
| 117 | + |
| 118 | + init { |
| 119 | + println("token=${accessToken}") |
| 120 | + |
| 121 | + for (panel in panels) { |
| 122 | + println("panel = ${panel.id} ${panel.x} ${panel.y} ${panel.orientation} ${panel.shape}") |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + companion object { |
| 128 | + val minMillisBetweenFrames = 50.milliseconds |
| 129 | + const val shuffleSend = true |
| 130 | + private val logger = Logger<NanoleafAdapter>() |
| 131 | + } |
| 132 | +} |
0 commit comments