Skip to content

Commit 91d3c40

Browse files
author
Kame
committed
Fix remote reorder + correct Kaseikyo protocol import; add Remote View rotation option
1 parent 58d9190 commit 91d3c40

32 files changed

Lines changed: 1849 additions & 455 deletions

.flutter-plugins-dependencies

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: release
22

3-
flutter-version: '8b872868494e429d94fa06dca855c306438b22c0'
3+
flutter-version: '6f3039bf7c3cb5306513c75092822d4d94716003'
44

55
on:
66
workflow_dispatch:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ The Signal Tester is designed to help discover unknown working IR commands.
147147
| SONY15 | 4 hex → 15 bits (from 16 padded) | 40,000 | Same timings as SONY12; remove last duration; pad to 45,000µs; duplicate frame | Strict 4 hex |
148148
| SONY20 | 5 hex → 20 bits | 40,000 | Same timings as SONY12; remove last duration; pad to 45,000µs; duplicate frame | Strict 5 hex |
149149
| Thomson7 | 3 hex (int) | 33,000 | Mask 0xF7F; 12 bits = last4 + toggle + first7; 0=[460,2000]; 1=[460,4600]; append 460; pad to 80,000µs; duplicate frame | Toggle maintained; hex int input with min/max |
150+
| Kaseikyo (Panasonic) | 6 hex → 24 bits (Address12 + Command8 + VendorParity/ignored) | 37,000 | Header 3456/1728; bit mark 432, space 432 (0) or 1296 (1); this finder uses a 24-bit payload (address:12 + command:8 + low vendor nibble) to search; production encoder computes full 48-bit frame with vendor parity and 8-bit XOR parity | Use vendor defaults or provide vendor/address/command when creating a button in remotes |
150151

151152
Notes:
152153
- Protocol identifiers and display names are maintained in `lib/ir/ir_protocol_registry.dart`.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package org.nslabs.ir_blaster
2+
3+
import android.hardware.usb.UsbDeviceConnection
4+
import android.hardware.usb.UsbEndpoint
5+
import android.os.SystemClock
6+
import android.util.Log
7+
import kotlin.math.min
8+
9+
object ElkSmartUsbProtocolFormatter : UsbWireProtocol {
10+
override val name: String = "elksmart_bulk"
11+
override val strictHandshake: Boolean = true
12+
override val wantsBackgroundReader: Boolean = false
13+
override val interFrameDelayMs: Long = 2L
14+
15+
override fun openHandshake(
16+
connection: UsbDeviceConnection,
17+
inEndpoint: UsbEndpoint,
18+
outEndpoint: UsbEndpoint
19+
): Boolean {
20+
return try {
21+
val tmp = ByteArray(maxOf(inEndpoint.maxPacketSize, 64))
22+
while (true) {
23+
val r = connection.bulkTransfer(inEndpoint, tmp, tmp.size, 10)
24+
if (r < 0) break
25+
}
26+
27+
val identify = byteArrayOf(
28+
0xFC.toByte(),
29+
0xFC.toByte(),
30+
0xFC.toByte(),
31+
0xFC.toByte()
32+
)
33+
34+
val w = connection.bulkTransfer(outEndpoint, identify, identify.size, 150)
35+
if (w != identify.size) return false
36+
37+
val resp = ByteArray(64)
38+
var got = -1
39+
val deadline = SystemClock.uptimeMillis() + 400L
40+
while (SystemClock.uptimeMillis() < deadline) {
41+
val n = connection.bulkTransfer(inEndpoint, resp, resp.size, 120)
42+
if (n > 0) {
43+
got = n
44+
break
45+
}
46+
}
47+
if (got < 6) return false
48+
49+
resp[0] == 0xFC.toByte() &&
50+
resp[1] == 0xFC.toByte() &&
51+
resp[2] == 0xFC.toByte() &&
52+
resp[3] == 0xFC.toByte() &&
53+
(resp[4].toInt() and 0xFF) == 0x70 &&
54+
(resp[5].toInt() and 0xFF) == 0x01
55+
} catch (t: Throwable) {
56+
Log.w("ElkSmartUsbProtocol", "openHandshake failed: ${t.message}")
57+
false
58+
}
59+
}
60+
61+
override fun encode(frequencyHz: Int, patternUs: IntArray): List<ByteArray> {
62+
val pulses = toPulses(patternUs)
63+
val payload = compressPulses(pulses)
64+
65+
val f = (frequencyHz + 0x7FFFF)
66+
val len = payload.size
67+
68+
val msg = ByteArrayOutput()
69+
msg.write(0xFF)
70+
msg.write(0xFF)
71+
msg.write(0xFF)
72+
msg.write(0xFF)
73+
74+
msg.write(mangleByte(f ushr 8).toInt() and 0xFF)
75+
msg.write(mangleByte(f ushr 16).toInt() and 0xFF)
76+
msg.write(mangleByte(f).toInt() and 0xFF)
77+
78+
msg.write(mangleByte(len ushr 8).toInt() and 0xFF)
79+
msg.write(mangleByte(len).toInt() and 0xFF)
80+
81+
for (b in payload) msg.write(b.toInt() and 0xFF)
82+
83+
val message = msg.toByteArray()
84+
85+
val frames = ArrayList<ByteArray>()
86+
var offset = 0
87+
while (offset < message.size) {
88+
val chunk = min(62, message.size - offset)
89+
if (chunk == 62) {
90+
val buf = ByteArray(63)
91+
System.arraycopy(message, offset, buf, 0, 62)
92+
buf[62] = checksum62(buf)
93+
frames.add(buf)
94+
} else {
95+
val buf = ByteArray(chunk)
96+
System.arraycopy(message, offset, buf, 0, chunk)
97+
frames.add(buf)
98+
}
99+
offset += chunk
100+
}
101+
102+
return frames
103+
}
104+
105+
override fun postTransmitDelayMs(patternUs: IntArray): Long {
106+
return 2L
107+
}
108+
109+
private data class Pulse(val onUs: Int, val offUs: Int)
110+
111+
private fun toPulses(patternUs: IntArray): List<Pulse> {
112+
if (patternUs.isEmpty()) return emptyList()
113+
val out = ArrayList<Pulse>((patternUs.size + 1) / 2)
114+
var i = 0
115+
while (i < patternUs.size) {
116+
val on = patternUs[i].coerceAtLeast(0)
117+
val off = if (i + 1 < patternUs.size) patternUs[i + 1].coerceAtLeast(0) else 0
118+
out.add(Pulse(on, off))
119+
i += 2
120+
}
121+
return out
122+
}
123+
124+
private fun compressPulses(pulses: List<Pulse>): ByteArray {
125+
if (pulses.isEmpty()) return ByteArray(0)
126+
127+
val freq = HashMap<Pulse, Int>(pulses.size)
128+
for (p in pulses) {
129+
freq[p] = (freq[p] ?: 0) + 1
130+
}
131+
132+
val sorted = freq.entries.sortedByDescending { it.value }
133+
val p1 = sorted.getOrNull(0)?.key ?: pulses[0]
134+
val p2 = sorted.getOrNull(1)?.key ?: p1
135+
136+
val out = ByteArrayOutput()
137+
138+
compressValueUs(p2.onUs, out)
139+
compressValueUs(p2.offUs, out)
140+
compressValueUs(p1.onUs, out)
141+
compressValueUs(p1.offUs, out)
142+
143+
out.write(0xFF)
144+
out.write(0xFF)
145+
out.write(0xFF)
146+
147+
for (p in pulses) {
148+
when {
149+
p == p1 -> out.write(0x00)
150+
p == p2 -> out.write(0x01)
151+
else -> {
152+
compressValueUs(p.onUs, out)
153+
compressValueUs(p.offUs, out)
154+
}
155+
}
156+
}
157+
158+
return out.toByteArray()
159+
}
160+
161+
private fun compressValueUs(valueUs: Int, out: ByteArrayOutput) {
162+
if (valueUs <= 2032) {
163+
val q = ((valueUs + 8) / 16).coerceAtLeast(2)
164+
out.write(q and 0xFF)
165+
return
166+
}
167+
168+
var v = valueUs
169+
while (true) {
170+
var b = v and 0x7F
171+
v = v ushr 7
172+
if (v != 0) b = b or 0x80
173+
out.write(b and 0xFF)
174+
if (v == 0) break
175+
}
176+
}
177+
178+
private fun mangleByte(v: Int): Byte {
179+
var value = v and 0xFF
180+
var reversed = 0
181+
repeat(8) {
182+
reversed = (reversed shl 1) or (value and 1)
183+
value = value ushr 1
184+
}
185+
return (reversed.inv() and 0xFF).toByte()
186+
}
187+
188+
private fun checksum62(buf: ByteArray): Byte {
189+
var sum = 0
190+
for (i in 0 until 62) sum += (buf[i].toInt() and 0xFF)
191+
val x = (sum and 0xF0) or ((sum ushr 8) and 0x0F)
192+
return mangleByte(x)
193+
}
194+
195+
private class ByteArrayOutput {
196+
private var buf = ByteArray(256)
197+
private var size = 0
198+
199+
fun write(v: Int) {
200+
ensure(1)
201+
buf[size++] = v.toByte()
202+
}
203+
204+
fun toByteArray(): ByteArray = buf.copyOf(size)
205+
206+
private fun ensure(n: Int) {
207+
val need = size + n
208+
if (need <= buf.size) return
209+
var newCap = buf.size * 2
210+
while (newCap < need) newCap *= 2
211+
buf = buf.copyOf(newCap)
212+
}
213+
}
214+
}

android/app/src/main/kotlin/com/example/irblaster_controller/usb/UsbDeviceFilter.kt

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ package org.nslabs.ir_blaster
33
import android.hardware.usb.UsbDevice
44

55
object UsbDeviceFilter {
6+
fun hasKnownVidPid(device: UsbDevice): Boolean {
7+
val vid = device.vendorId
8+
val pid = device.productId
9+
return (vid == 0x10C4 && pid == 0x8468) ||
10+
(vid == 0x045E && pid == 0x8468) ||
11+
(vid == 0x045C && (pid == 0x0195 || pid == 0x0184 || pid == 0x014A || pid == 0x02AA))
12+
}
613

7-
fun hasKnownVidPid(device: UsbDevice): Boolean {
8-
val vid = device.vendorId
9-
val pid = device.productId
10-
return (vid == 0x10C4 && pid == 0x8468) ||
11-
(vid == 0x045E && pid == 0x8468) ||
12-
(vid == 0x045C && (pid == 0x0195 || pid == 0x014A || pid == 0x02AA))
13-
}
14+
fun isSupported(device: UsbDevice): Boolean {
15+
return hasKnownVidPid(device)
16+
}
1417

15-
fun isSupported(device: UsbDevice): Boolean {
16-
// Relax detection: rely on known VID/PID only.
17-
// Some dongle variants expose multiple interfaces or expose endpoints on a non-zero interface.
18-
return hasKnownVidPid(device)
19-
}
18+
fun isElkSmart(device: UsbDevice): Boolean {
19+
val vid = device.vendorId
20+
val pid = device.productId
21+
return vid == 0x045C && (pid == 0x0184 || pid == 0x0195)
22+
}
2023
}

0 commit comments

Comments
 (0)