Skip to content

Commit 598540a

Browse files
authored
feat: ✨ add support for NEC protocol (#2)
* Adding support for devices with NEC either via lirc or via evdev * fix: properly detect receiver before service start * fix: address NEC/evdev review nits and rename IR udev rule * chore: 🔖 increase version
1 parent ffd57c3 commit 598540a

28 files changed

Lines changed: 379 additions & 109 deletions

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ infrafi/
2222
daemon/ # Linux daemon (infrafid)
2323
main.c # Entry point, CLI args, main loop
2424
wfr_lirc.h/c # LIRC scancode reader (/dev/lirc0, LIRC_MODE_SCANCODE)
25-
wfr_decode.h/c # RC-6 scancode reassembler
25+
wfr_evdev.h/c # evdev input reader (/dev/input/eventN, NEC via MSC_RAW)
26+
wfr_decode.h/c # IR scancode reassembler
2627
wfr_network.h/c # NetworkManager/systemd-networkd/ifupdown WiFi connector with rollback
2728
wfr_ack.h/c # IR ACK transmitter (LIRC TX, RC-6 scancodes)
2829
Makefile # Build with `make` (requires gcc, linux headers)

README.md

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@ flowchart LR
2020
2121
subgraph Linux Server
2222
B[infrafid daemon]
23-
B1["IR RX (/dev/lirc0)"]
23+
B1["IR RX (LIRC or evdev)"]
2424
B2[WiFi Connect]
2525
B3["IR TX (ACK)"]
2626
B1 --> B --> B2
2727
B -.-> B3
2828
end
2929
30-
A -- "IR (RC-6, 36kHz)" --> B1
30+
A -- "IR (RC-6 or NEC)" --> B1
3131
B3 -. "ACK (OK/FAIL)" .-> A
3232
```
3333

34-
The Flipper encodes WiFi credentials as a sequence of **RC-6 IR messages** and blasts them at the server's CIR (Consumer IR) receiver. The `infrafid` daemon decodes the transmission and connects to the network automatically. No pairing, no Bluetooth, no network required — just line-of-sight IR.
34+
The Flipper encodes WiFi credentials as a sequence of **IR messages** (RC-6 or NEC) and blasts them at the server's IR receiver. The `infrafid` daemon decodes the transmission and connects to the network automatically. No pairing, no Bluetooth, no network required — just line-of-sight IR.
3535

3636
With **ACK enabled**, the daemon transmits a response back via IR — the Flipper displays whether the connection succeeded (with IP address) or failed.
3737

@@ -41,7 +41,8 @@ With **ACK enabled**, the daemon transmits a response back via IR — the Flippe
4141
- **Manual entry** — On-screen keyboard for SSID and password, security type selector (Open/WPA/WEP/SAE)
4242
- **NFC WiFi tags** — Scan an NTAG213/215/216 tag with WiFi credentials (standard NDEF WiFi Simple Configuration format) and transmit instantly
4343
- **Saved networks** — Credentials auto-save to SD card after successful transmit. Browse, resend, or delete saved networks
44-
- **Fast transmission** — Full credentials sent in under a second via RC-6 protocol
44+
- **IR protocol selection** — Choose between RC-6 (36kHz, for CIR receivers like ITE8708) or NEC (38kHz, for devices like the Squeezebox Touch) in Settings
45+
- **Fast transmission** — Full credentials sent in under a second via RC-6, or a few seconds via NEC
4546
- **Hidden network support** — Toggle hidden SSID flag
4647
- **ACK feedback** — Optional. When enabled in Settings, the Flipper waits for a response from the server after sending credentials. Shows "Connected! IP: x.x.x.x" or "Failed" on screen
4748

@@ -52,7 +53,9 @@ With **ACK enabled**, the daemon transmits a response back via IR — the Flippe
5253
- **SSID verification** — After WPA handshake completes, verifies the connected SSID matches the target to avoid false positives
5354
- **IR ACK response** — Sends connection result back to the Flipper via IR (requires TX hardware or external IR blaster)
5455
- **Runs as a service** — systemd unit with auto-restart, logs to journald/syslog
55-
- **ITE8708 optimized** — Uses `LIRC_MODE_SCANCODE` for kernel-decoded RC-6, avoiding hardware FIFO overflow issues with the CIR receivers found in Intel NUCs
56+
- **Dual protocol** — LIRC input accepts both RC-6 and NEC scancodes automatically (just enable the protocol in `/sys/class/rc/rc*/protocols`)
57+
- **ITE8708 optimized** — Uses `LIRC_MODE_SCANCODE` for kernel-decoded scancodes, avoiding hardware FIFO overflow issues with the CIR receivers found in Intel NUCs
58+
- **evdev fallback** — For devices without LIRC (e.g., Squeezebox Touch), read NEC scancodes from `/dev/input/eventN` via `--evdev` (expects FAB4-style bit ordering)
5659

5760
## Getting Started
5861

@@ -63,8 +66,9 @@ With **ACK enabled**, the daemon transmits a response back via IR — the Flippe
6366
- [ufbt](https://github.com/flipperdevices/flipperzero-ufbt) (Flipper build tool)
6467

6568
**Linux Server:**
66-
- IR receiver (tested with ITE8708 CIR in Intel NUCs)
67-
- `/dev/lirc0` device available
69+
- IR receiver — tested with:
70+
- ITE8708 CIR in Intel NUCs (`/dev/lirc0`, RC-6 or NEC via LIRC)
71+
- Squeezebox Touch FAB4 IR (`/dev/input/event1`, NEC via evdev)
6872
- `gcc` and Linux headers for building
6973
- NetworkManager, systemd-networkd, or ifupdown + wpa_supplicant for WiFi management
7074

@@ -97,7 +101,7 @@ sudo systemctl enable --now infrafid
97101
```
98102

99103
The install script automatically:
100-
- Configures the IR receiver for RC-6 protocol only
104+
- Configures the IR receiver for RC-6 and NEC protocols
101105
- Creates a udev rule so the config persists across reboots
102106
- Installs and starts the systemd service
103107

@@ -148,6 +152,13 @@ ir-keytable -t -s rc0
148152
2. Open **InfraFi****Saved** to browse them
149153
3. Select a network to resend
150154

155+
### IR Protocol
156+
1. Open **InfraFi****Settings** → set **IR Protocol** to **RC-6** (default) or **NEC**
157+
2. Use **RC-6** for CIR hardware designed for media center remotes (Intel NUCs)
158+
3. Use **NEC** for devices with NEC-based IR receivers (Squeezebox Touch, many consumer devices)
159+
160+
> **Note:** On the daemon side, LIRC accepts both RC-6 and NEC automatically — no flag changes needed, just ensure the protocol is enabled in `/sys/class/rc/rc*/protocols`. The `--evdev` flag is only needed for devices that don't have LIRC (like the Squeezebox Touch).
161+
151162
### ACK (Bidirectional Feedback)
152163
1. Open **InfraFi****Settings** → set **Wait for ACK** to **On**
153164
2. Send credentials as usual
@@ -165,21 +176,47 @@ ir-keytable -t -s rc0
165176
# Run in foreground with verbose logging (useful for testing)
166177
sudo infrafid -f -v
167178

179+
# Use an evdev device for NEC reception (e.g., Squeezebox Touch)
180+
sudo infrafid -e /dev/input/event1 -f -v
181+
168182
# Use a separate IR device for ACK transmission
169183
sudo infrafid -a /dev/lirc1
170184

185+
# LIRC RX on lirc0, ACK TX on lirc1
186+
sudo infrafid -d /dev/lirc0 -a /dev/lirc1
187+
171188
# Check service status
172189
sudo systemctl status infrafid
173190

174191
# Watch logs
175192
sudo journalctl -u infrafid -f
176193
```
177194

195+
| Flag | Description |
196+
|------|-------------|
197+
| `-d`, `--device PATH` | LIRC device for RX (default: `/dev/lirc0`) |
198+
| `-e`, `--evdev PATH` | evdev input device for RX (NEC via `MSC_RAW`, FAB4-style bit ordering) |
199+
| `-a`, `--ack-device PATH` | LIRC device for ACK TX (default: same as `-d`) |
200+
| `-f`, `--foreground` | Run in foreground (don't daemonize) |
201+
| `-v`, `--verbose` | Verbose logging |
202+
178203
## Protocol
179204

180-
InfraFi uses **RC-6 Mode 0** IR protocol at 36kHz — the same protocol used by standard media center remotes. This is intentional: CIR receivers like the ITE8708 (found in Intel NUCs) have hardware decoders optimized for RC-6. Using the kernel's built-in RC-6 decoder (`LIRC_MODE_SCANCODE`) avoids the tiny hardware FIFO that overflows with custom raw protocols.
205+
InfraFi supports two IR transport protocols. The framing and payload format are identical — only the physical IR encoding differs:
206+
207+
| | RC-6 Mode 0 | NEC |
208+
|---|---|---|
209+
| **Carrier** | 36kHz | 38kHz |
210+
| **Frame time** | ~25ms | ~67ms |
211+
| **Daemon input** | LIRC | LIRC or evdev |
212+
| **Typical hardware** | ITE8708 CIR (Intel NUCs) | Squeezebox Touch (FAB4 IR), any NEC receiver |
213+
| **Flipper setting** | RC-6 (default) | NEC |
214+
215+
RC-6 is the default and recommended for CIR receivers — it's faster and uses the kernel's built-in decoder (`LIRC_MODE_SCANCODE`), avoiding the tiny hardware FIFO that overflows with custom raw protocols. NEC works through LIRC as well (enable `nec` in `/sys/class/rc/rc*/protocols`), or through the Linux input subsystem (`--evdev`) for devices without LIRC.
216+
217+
> **evdev note:** The current evdev decoder assumes FAB4-style NEC byte bit-ordering and bit-reverses each byte before validation.
181218
182-
Each RC-6 message carries one byte of payload:
219+
Each IR message carries one byte of payload:
183220

184221
| Field | Bits | Description |
185222
|-------|------|-------------|
@@ -201,8 +238,8 @@ infrafi/
201238
├── application.fam # Flipper app manifest
202239
├── flipper/ # Flipper Zero app
203240
│ ├── wi_fir.h/c # App entry, ViewDispatcher + SceneManager
204-
│ ├── wfr_encode.h/c # RC-6 IR encoder + transmitter
205-
│ ├── wfr_decode.h/c # RC-6 ACK decoder (IR receive)
241+
│ ├── wfr_encode.h/c # IR encoder + transmitter (RC-6 / NEC)
242+
│ ├── wfr_decode.h/c # ACK decoder (IR receive)
206243
│ ├── wfr_nfc.h/c # NFC NDEF WiFi tag parser
207244
│ ├── wfr_storage.h/c # SD card credential + settings storage
208245
│ ├── protocol/
@@ -213,8 +250,9 @@ infrafi/
213250
│ └── images/ # App icon
214251
├── daemon/ # Linux daemon (infrafid)
215252
│ ├── main.c # Entry point, CLI args, main loop
216-
│ ├── wfr_lirc.h/c # LIRC scancode reader
217-
│ ├── wfr_decode.h/c # RC-6 message reassembler
253+
│ ├── wfr_lirc.h/c # LIRC scancode reader (RC-6 / NEC)
254+
│ ├── wfr_evdev.h/c # evdev input reader (NEC via MSC_RAW)
255+
│ ├── wfr_decode.h/c # IR message reassembler
218256
│ ├── wfr_network.h/c # WiFi connector (NM/networkd/ifupdown) with rollback
219257
│ ├── wfr_ack.h/c # IR ACK transmitter (LIRC TX)
220258
│ ├── Makefile # Build

daemon/99-infrafid-ir.rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6 nec"

daemon/99-infrafid-rc6.rules

Lines changed: 0 additions & 1 deletion
This file was deleted.

daemon/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ CFLAGS = -Wall -Wextra -Wpedantic -O2 -std=c11 -D_GNU_SOURCE
33
LDFLAGS =
44

55
# Source files
6-
SRCS = main.c wfr_lirc.c wfr_decode.c wfr_network.c wfr_ack.c ../flipper/protocol/wfr_protocol.c
6+
SRCS = main.c wfr_lirc.c wfr_evdev.c wfr_decode.c wfr_network.c wfr_ack.c ../flipper/protocol/wfr_protocol.c
77
OBJS = $(SRCS:.c=.o)
88
TARGET = infrafid
99

daemon/infrafid.service

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ After=network.target
44
Wants=network.target
55
[Service]
66
Type=simple
7-
ExecCondition=/bin/sh -c 'ls /dev/lirc* >/dev/null 2>&1'
8-
ExecStart=/usr/local/bin/infrafid --foreground
7+
EnvironmentFile=-/etc/default/infrafid
8+
ExecStart=/usr/local/bin/infrafid --foreground $INFRAFID_ARGS
99
Restart=on-failure
1010
RestartSec=5
1111
StartLimitBurst=3

daemon/install.sh

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,32 @@ if [ "$(id -u)" -ne 0 ]; then
1111
exit 1
1212
fi
1313

14-
# Check for LIRC device
15-
if [ ! -e /dev/lirc0 ]; then
16-
echo "Warning: /dev/lirc0 not found. The daemon will fail to start without it."
17-
echo "Ensure your IR receiver is connected and the kernel module is loaded."
14+
# Detect input mode — LIRC vs evdev
15+
HAS_LIRC=false
16+
if ls /dev/lirc* >/dev/null 2>&1; then
17+
HAS_LIRC=true
1818
fi
1919

20-
# Configure IR receiver for RC-6 only
21-
RC_DIR="/sys/class/rc/rc0"
22-
if [ -d "$RC_DIR" ]; then
23-
echo "Configuring IR receiver for RC-6 protocol..."
24-
echo rc-6 > "$RC_DIR/protocols"
25-
echo " Active protocols: $(cat "$RC_DIR/protocols")"
26-
27-
# Persist via udev rule so it survives reboot
28-
UDEV_RULE="/etc/udev/rules.d/99-infrafid-rc6.rules"
29-
echo 'ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6"' > "$UDEV_RULE"
30-
echo " Created udev rule: $UDEV_RULE"
20+
if [ "$HAS_LIRC" = true ]; then
21+
# Configure IR receiver for RC-6 + NEC (both supported via LIRC)
22+
RC_DIR="/sys/class/rc/rc0"
23+
if [ -d "$RC_DIR" ]; then
24+
echo "Configuring IR receiver for RC-6 and NEC protocols..."
25+
echo "rc-6 nec" > "$RC_DIR/protocols"
26+
echo " Active protocols: $(cat "$RC_DIR/protocols")"
27+
28+
# Persist via udev rule so it survives reboot
29+
UDEV_RULE="/etc/udev/rules.d/99-infrafid-ir.rules"
30+
echo 'ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6 nec"' > "$UDEV_RULE"
31+
echo " Created udev rule: $UDEV_RULE"
32+
else
33+
echo "Warning: $RC_DIR not found. You may need to configure protocols manually."
34+
fi
3135
else
32-
echo "Warning: $RC_DIR not found. You may need to configure RC-6 manually."
36+
echo "Warning: no /dev/lirc* device found."
37+
echo " If your device uses evdev (e.g. Squeezebox Touch), configure it via:"
38+
echo " echo 'INFRAFID_ARGS=\"--evdev /dev/input/event1\"' > /etc/default/infrafid"
39+
echo " Then: systemctl restart infrafid"
3340
fi
3441

3542
# Build

daemon/main.c

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "wfr_lirc.h"
2+
#include "wfr_evdev.h"
23
#include "wfr_decode.h"
34
#include "wfr_network.h"
45
#include "wfr_ack.h"
@@ -23,6 +24,7 @@ static void print_usage(const char* prog) {
2324
fprintf(stderr, "Usage: %s [options]\n", prog);
2425
fprintf(stderr, "Options:\n");
2526
fprintf(stderr, " -d, --device PATH LIRC device for RX (default: /dev/lirc0)\n");
27+
fprintf(stderr, " -e, --evdev PATH Use evdev input device for RX (NEC via MSC_RAW, FAB4-style bit order)\n");
2628
fprintf(stderr, " -a, --ack-device PATH LIRC device for ACK TX (default: same as --device)\n");
2729
fprintf(stderr, " -f, --foreground Run in foreground (don't daemonize)\n");
2830
fprintf(stderr, " -v, --verbose Verbose logging\n");
@@ -90,12 +92,14 @@ static void handle_credentials(const char* payload) {
9092

9193
int main(int argc, char* argv[]) {
9294
const char* device = "/dev/lirc0";
95+
const char* evdev_device = NULL;
9396
const char* ack_device = NULL;
9497
bool foreground = false;
9598
int log_level = LOG_INFO;
9699

97100
static struct option long_opts[] = {
98101
{"device", required_argument, NULL, 'd'},
102+
{"evdev", required_argument, NULL, 'e'},
99103
{"ack-device", required_argument, NULL, 'a'},
100104
{"foreground", no_argument, NULL, 'f'},
101105
{"verbose", no_argument, NULL, 'v'},
@@ -105,11 +109,14 @@ int main(int argc, char* argv[]) {
105109
};
106110

107111
int opt;
108-
while((opt = getopt_long(argc, argv, "d:a:fvVh", long_opts, NULL)) != -1) {
112+
while((opt = getopt_long(argc, argv, "d:e:a:fvVh", long_opts, NULL)) != -1) {
109113
switch(opt) {
110114
case 'd':
111115
device = optarg;
112116
break;
117+
case 'e':
118+
evdev_device = optarg;
119+
break;
113120
case 'a':
114121
ack_device = optarg;
115122
break;
@@ -134,19 +141,35 @@ int main(int argc, char* argv[]) {
134141
openlog("infrafid", LOG_PID | (foreground ? LOG_PERROR : 0), LOG_DAEMON);
135142
setlogmask(LOG_UPTO(log_level));
136143

137-
/* Default ACK device to same as RX device */
144+
bool use_evdev = (evdev_device != NULL);
145+
const char* rx_device = use_evdev ? evdev_device : device;
146+
147+
/* Default ACK device to LIRC device (not evdev — evdev can't transmit) */
138148
if(!ack_device) ack_device = device;
139149

140-
syslog(LOG_INFO, "infrafid %s starting (rx=%s, tx=%s)", INFRAFI_VERSION, device, ack_device);
150+
if(use_evdev && strcmp(ack_device, "/dev/lirc0") == 0) {
151+
syslog(LOG_WARNING,
152+
"infrafid: --evdev is active and ACK TX defaults to %s; set --ack-device if this host has no LIRC TX device",
153+
ack_device);
154+
}
155+
156+
syslog(LOG_INFO, "infrafid %s starting (rx=%s [%s], tx=%s)",
157+
INFRAFI_VERSION, rx_device, use_evdev ? "evdev" : "lirc", ack_device);
141158

142159
struct sigaction sa = {0};
143160
sa.sa_handler = signal_handler;
144161
sigaction(SIGTERM, &sa, NULL);
145162
sigaction(SIGINT, &sa, NULL);
146163

147-
int lirc_fd = wfr_lirc_open(device);
148-
if(lirc_fd < 0) {
149-
syslog(LOG_ERR, "failed to open LIRC device %s", device);
164+
int rx_fd;
165+
if(use_evdev) {
166+
rx_fd = wfr_evdev_open(evdev_device);
167+
} else {
168+
rx_fd = wfr_lirc_open(device);
169+
}
170+
if(rx_fd < 0) {
171+
syslog(LOG_ERR, "failed to open %s device %s",
172+
use_evdev ? "evdev" : "LIRC", rx_device);
150173
closelog();
151174
return 1;
152175
}
@@ -165,8 +188,15 @@ int main(int argc, char* argv[]) {
165188

166189
while(running) {
167190
uint8_t address, command;
191+
int rc;
192+
193+
if(use_evdev) {
194+
rc = wfr_evdev_read_scancode(rx_fd, &address, &command);
195+
} else {
196+
rc = wfr_lirc_read_scancode(rx_fd, &address, &command);
197+
}
168198

169-
if(wfr_lirc_read_scancode(lirc_fd, &address, &command) < 0) {
199+
if(rc < 0) {
170200
if(!running) break;
171201
continue;
172202
}
@@ -180,7 +210,11 @@ int main(int argc, char* argv[]) {
180210

181211
syslog(LOG_INFO, "infrafid shutting down");
182212
wfr_ack_close(ack_fd);
183-
wfr_lirc_close(lirc_fd);
213+
if(use_evdev) {
214+
wfr_evdev_close(rx_fd);
215+
} else {
216+
wfr_lirc_close(rx_fd);
217+
}
184218
closelog();
185219
return 0;
186220
}

daemon/wfr_ack.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,21 @@ bool wfr_ack_send(int fd, bool success, const char* ip_str) {
8585
uint8_t pass = 0;
8686

8787
/* START */
88-
if(!send_rc6(fd, WFR_RC6_MAGIC | WFR_RC6_TYPE_START | pass, (uint8_t)payload_len)) {
88+
if(!send_rc6(fd, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_START | pass, (uint8_t)payload_len)) {
8989
return false;
9090
}
9191
delay_ms(WFR_RC6_INTER_MSG_MS);
9292

9393
/* DATA — one byte per message */
9494
for(size_t i = 0; i < payload_len; i++) {
95-
if(!send_rc6(fd, WFR_RC6_MAGIC | WFR_RC6_TYPE_DATA | pass, data[i])) {
95+
if(!send_rc6(fd, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_DATA | pass, data[i])) {
9696
return false;
9797
}
9898
delay_ms(WFR_RC6_INTER_MSG_MS);
9999
}
100100

101101
/* END — CRC-8 */
102-
if(!send_rc6(fd, WFR_RC6_MAGIC | WFR_RC6_TYPE_END | pass, crc)) {
102+
if(!send_rc6(fd, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_END | pass, crc)) {
103103
return false;
104104
}
105105

0 commit comments

Comments
 (0)