1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
package coffee.liz.rookery.stackmat
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
/**
* One status packet from a speed-stacks timer.
* [status]: ' ' running, 'S' stopped, 'I' reset, 'A' both hands down and ready, 'L'/'R' one hand, 'C' both hands.
*/
data class StackmatPacket(val status: Char, val ms: Long, val digits: Int) {
val running: Boolean get() = status == ' '
val handsDown: Boolean get() = status == 'L' || status == 'R' || status == 'C' || status == 'A'
val ready: Boolean get() = status == 'A'
}
/**
* Decodes the timer's data port from audio samples: 1200 baud serial (start bit, 8 data bits LSB
* first, stop bit), packets of status + 5 or 6 digits + checksum (64 + digit sum) + line ending.
*
* Sound cards are AC-coupled and may invert the signal, so levels are tracked by edges rather than
* by sign, and whichever level the line rests at between packets is taken as logical 1.
*/
class StackmatDecoder(sampleRate: Int, private val onPacket: (StackmatPacket) -> Unit) {
private val spb = sampleRate / 1200.0
private val w = max(1, (spb / 3).toInt())
private val ring = FloatArray(w)
private var ringAt = 0
private var peak = 0f
private var level = false
private var run = 0L
private var idle: Boolean? = null
private val bits = ArrayList<Boolean>(32)
private val tail = ArrayList<Int>(12)
/** Size of recent signal edges, 0..~2: for a level meter. */
val signal: Float get() = peak
fun feed(samples: FloatArray, count: Int = samples.size) {
for (i in 0 until count) {
val x = samples[i]
val d = x - ring[ringAt]
ring[ringAt] = x; ringAt = (ringAt + 1) % w
peak = max(peak * 0.99995f, abs(d))
run++
if (peak > 0.02f) {
val next = if (d > 0.4f * peak) true else if (d < -0.4f * peak) false else level
if (next != level) { runEnded(level, run); level = next; run = 0 }
}
// the last byte of a packet has no following edge to end it: time it out
if (run == (12 * spb).toLong()) runEnded(level, run, stillGoing = true)
}
}
private fun runEnded(lvl: Boolean, samples: Long, stillGoing: Boolean = false) {
val n = (samples / spb).roundToInt()
if (n >= 12) {
// a long rest: this is the idle level; finish any byte in flight with stop bits
if (idle == lvl && bits.isNotEmpty()) { repeat(10) { bits += lvl }; drain() }
idle = lvl; bits.clear()
return
}
if (stillGoing) return
val idleLevel = idle ?: return
if (n == 0 || (bits.isEmpty() && lvl == idleLevel)) return
repeat(n) { bits += lvl }
drain()
}
private fun drain() {
val idleLevel = idle ?: return
while (bits.size >= 10) {
if (bits[0] == idleLevel || bits[9] != idleLevel) { bits.clear(); idle = null; return } // framing lost: wait for the next rest
var byte = 0
for (k in 0 until 8) if (bits[1 + k] == idleLevel) byte = byte or (1 shl k)
repeat(10) { bits.removeAt(0) }
while (bits.isNotEmpty() && bits[0] == idleLevel) bits.removeAt(0)
byte(byte)
}
}
private fun byte(b: Int) {
tail += b
if (tail.size > 8) tail.removeAt(0)
// does the tail end in status, digits, checksum?
for (digits in intArrayOf(6, 5)) {
if (tail.size < digits + 2) continue
val at = tail.size - digits - 2
val status = tail[at].toChar()
if (status !in " SILRCA") continue
val ds = tail.subList(at + 1, at + 1 + digits)
if (ds.any { it !in '0'.code..'9'.code }) continue
if (64 + ds.sumOf { it - '0'.code } != tail.last()) continue
val v = ds.map { it - '0'.code }
val ms = if (digits == 6) v[0] * 60_000L + (v[1] * 10 + v[2]) * 1000L + v[3] * 100 + v[4] * 10 + v[5]
else v[0] * 60_000L + (v[1] * 10 + v[2]) * 1000L + (v[3] * 10 + v[4]) * 10L
tail.clear()
onPacket(StackmatPacket(status, ms, digits))
return
}
}
}
/**
* Turns the packet stream into solves: a result is taken when the timer shows a stopped, non-zero
* time after having been reset, so a packet lost at the moment of stopping doesn't lose the solve
* and a time left on the display isn't recorded twice.
*/
class StackmatSession(private val onSolve: (ms: Long) -> Unit) {
var last: StackmatPacket? = null
private set
private var armed = false
fun packet(p: StackmatPacket) {
last = p
if (p.ms == 0L || p.running) armed = true
else if (p.status == 'S' && armed) { armed = false; onSolve(p.ms) }
}
}
|