Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ infrafi/
daemon/ # Linux daemon (infrafid)
main.c # Entry point, CLI args, main loop
wfr_lirc.h/c # LIRC scancode reader (/dev/lirc0, LIRC_MODE_SCANCODE)
wfr_decode.h/c # RC-6 scancode reassembler
wfr_evdev.h/c # evdev input reader (/dev/input/eventN, NEC via MSC_RAW)
wfr_decode.h/c # IR scancode reassembler
wfr_network.h/c # NetworkManager/systemd-networkd/ifupdown WiFi connector with rollback
wfr_ack.h/c # IR ACK transmitter (LIRC TX, RC-6 scancodes)
Makefile # Build with `make` (requires gcc, linux headers)
Expand Down
66 changes: 52 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ flowchart LR

subgraph Linux Server
B[infrafid daemon]
B1["IR RX (/dev/lirc0)"]
B1["IR RX (LIRC or evdev)"]
B2[WiFi Connect]
B3["IR TX (ACK)"]
B1 --> B --> B2
B -.-> B3
end

A -- "IR (RC-6, 36kHz)" --> B1
A -- "IR (RC-6 or NEC)" --> B1
B3 -. "ACK (OK/FAIL)" .-> A
```

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.
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.

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

Expand All @@ -41,7 +41,8 @@ With **ACK enabled**, the daemon transmits a response back via IR — the Flippe
- **Manual entry** — On-screen keyboard for SSID and password, security type selector (Open/WPA/WEP/SAE)
- **NFC WiFi tags** — Scan an NTAG213/215/216 tag with WiFi credentials (standard NDEF WiFi Simple Configuration format) and transmit instantly
- **Saved networks** — Credentials auto-save to SD card after successful transmit. Browse, resend, or delete saved networks
- **Fast transmission** — Full credentials sent in under a second via RC-6 protocol
- **IR protocol selection** — Choose between RC-6 (36kHz, for CIR receivers like ITE8708) or NEC (38kHz, for devices like the Squeezebox Touch) in Settings
- **Fast transmission** — Full credentials sent in under a second via RC-6, or a few seconds via NEC
- **Hidden network support** — Toggle hidden SSID flag
- **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

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

## Getting Started

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

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

Expand Down Expand Up @@ -97,7 +101,7 @@ sudo systemctl enable --now infrafid
```

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

Expand Down Expand Up @@ -148,6 +152,13 @@ ir-keytable -t -s rc0
2. Open **InfraFi** → **Saved** to browse them
3. Select a network to resend

### IR Protocol
1. Open **InfraFi** → **Settings** → set **IR Protocol** to **RC-6** (default) or **NEC**
2. Use **RC-6** for CIR hardware designed for media center remotes (Intel NUCs)
3. Use **NEC** for devices with NEC-based IR receivers (Squeezebox Touch, many consumer devices)

> **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).

### ACK (Bidirectional Feedback)
1. Open **InfraFi** → **Settings** → set **Wait for ACK** to **On**
2. Send credentials as usual
Expand All @@ -165,21 +176,47 @@ ir-keytable -t -s rc0
# Run in foreground with verbose logging (useful for testing)
sudo infrafid -f -v

# Use an evdev device for NEC reception (e.g., Squeezebox Touch)
sudo infrafid -e /dev/input/event1 -f -v

# Use a separate IR device for ACK transmission
sudo infrafid -a /dev/lirc1

# LIRC RX on lirc0, ACK TX on lirc1
sudo infrafid -d /dev/lirc0 -a /dev/lirc1

# Check service status
sudo systemctl status infrafid

# Watch logs
sudo journalctl -u infrafid -f
```

| Flag | Description |
|------|-------------|
| `-d`, `--device PATH` | LIRC device for RX (default: `/dev/lirc0`) |
| `-e`, `--evdev PATH` | evdev input device for RX (NEC via `MSC_RAW`, FAB4-style bit ordering) |
| `-a`, `--ack-device PATH` | LIRC device for ACK TX (default: same as `-d`) |
| `-f`, `--foreground` | Run in foreground (don't daemonize) |
| `-v`, `--verbose` | Verbose logging |

## Protocol

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.
InfraFi supports two IR transport protocols. The framing and payload format are identical — only the physical IR encoding differs:

| | RC-6 Mode 0 | NEC |
|---|---|---|
| **Carrier** | 36kHz | 38kHz |
| **Frame time** | ~25ms | ~67ms |
| **Daemon input** | LIRC | LIRC or evdev |
| **Typical hardware** | ITE8708 CIR (Intel NUCs) | Squeezebox Touch (FAB4 IR), any NEC receiver |
| **Flipper setting** | RC-6 (default) | NEC |

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.

> **evdev note:** The current evdev decoder assumes FAB4-style NEC byte bit-ordering and bit-reverses each byte before validation.

Each RC-6 message carries one byte of payload:
Each IR message carries one byte of payload:

| Field | Bits | Description |
|-------|------|-------------|
Expand All @@ -201,8 +238,8 @@ infrafi/
├── application.fam # Flipper app manifest
├── flipper/ # Flipper Zero app
│ ├── wi_fir.h/c # App entry, ViewDispatcher + SceneManager
│ ├── wfr_encode.h/c # RC-6 IR encoder + transmitter
│ ├── wfr_decode.h/c # RC-6 ACK decoder (IR receive)
│ ├── wfr_encode.h/c # IR encoder + transmitter (RC-6 / NEC)
│ ├── wfr_decode.h/c # ACK decoder (IR receive)
│ ├── wfr_nfc.h/c # NFC NDEF WiFi tag parser
│ ├── wfr_storage.h/c # SD card credential + settings storage
│ ├── protocol/
Expand All @@ -213,8 +250,9 @@ infrafi/
│ └── images/ # App icon
├── daemon/ # Linux daemon (infrafid)
│ ├── main.c # Entry point, CLI args, main loop
│ ├── wfr_lirc.h/c # LIRC scancode reader
│ ├── wfr_decode.h/c # RC-6 message reassembler
│ ├── wfr_lirc.h/c # LIRC scancode reader (RC-6 / NEC)
│ ├── wfr_evdev.h/c # evdev input reader (NEC via MSC_RAW)
│ ├── wfr_decode.h/c # IR message reassembler
│ ├── wfr_network.h/c # WiFi connector (NM/networkd/ifupdown) with rollback
│ ├── wfr_ack.h/c # IR ACK transmitter (LIRC TX)
│ ├── Makefile # Build
Expand Down
1 change: 1 addition & 0 deletions daemon/99-infrafid-ir.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6 nec"
1 change: 0 additions & 1 deletion daemon/99-infrafid-rc6.rules

This file was deleted.

2 changes: 1 addition & 1 deletion daemon/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ CFLAGS = -Wall -Wextra -Wpedantic -O2 -std=c11 -D_GNU_SOURCE
LDFLAGS =

# Source files
SRCS = main.c wfr_lirc.c wfr_decode.c wfr_network.c wfr_ack.c ../flipper/protocol/wfr_protocol.c
SRCS = main.c wfr_lirc.c wfr_evdev.c wfr_decode.c wfr_network.c wfr_ack.c ../flipper/protocol/wfr_protocol.c
OBJS = $(SRCS:.c=.o)
TARGET = infrafid

Expand Down
4 changes: 2 additions & 2 deletions daemon/infrafid.service
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ After=network.target
Wants=network.target
[Service]
Type=simple
ExecCondition=/bin/sh -c 'ls /dev/lirc* >/dev/null 2>&1'
ExecStart=/usr/local/bin/infrafid --foreground
EnvironmentFile=-/etc/default/infrafid
ExecStart=/usr/local/bin/infrafid --foreground $INFRAFID_ARGS
Restart=on-failure
RestartSec=5
StartLimitBurst=3
Expand Down
39 changes: 23 additions & 16 deletions daemon/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@ if [ "$(id -u)" -ne 0 ]; then
exit 1
fi

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

# Configure IR receiver for RC-6 only
RC_DIR="/sys/class/rc/rc0"
if [ -d "$RC_DIR" ]; then
echo "Configuring IR receiver for RC-6 protocol..."
echo rc-6 > "$RC_DIR/protocols"
echo " Active protocols: $(cat "$RC_DIR/protocols")"

# Persist via udev rule so it survives reboot
UDEV_RULE="/etc/udev/rules.d/99-infrafid-rc6.rules"
echo 'ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6"' > "$UDEV_RULE"
echo " Created udev rule: $UDEV_RULE"
if [ "$HAS_LIRC" = true ]; then
# Configure IR receiver for RC-6 + NEC (both supported via LIRC)
RC_DIR="/sys/class/rc/rc0"
if [ -d "$RC_DIR" ]; then
echo "Configuring IR receiver for RC-6 and NEC protocols..."
echo "rc-6 nec" > "$RC_DIR/protocols"
echo " Active protocols: $(cat "$RC_DIR/protocols")"

# Persist via udev rule so it survives reboot
UDEV_RULE="/etc/udev/rules.d/99-infrafid-ir.rules"
echo 'ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6 nec"' > "$UDEV_RULE"
echo " Created udev rule: $UDEV_RULE"
else
echo "Warning: $RC_DIR not found. You may need to configure protocols manually."
fi
else
echo "Warning: $RC_DIR not found. You may need to configure RC-6 manually."
echo "Warning: no /dev/lirc* device found."
echo " If your device uses evdev (e.g. Squeezebox Touch), configure it via:"
echo " echo 'INFRAFID_ARGS=\"--evdev /dev/input/event1\"' > /etc/default/infrafid"
echo " Then: systemctl restart infrafid"
fi

# Build
Expand Down
50 changes: 42 additions & 8 deletions daemon/main.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "wfr_lirc.h"
#include "wfr_evdev.h"
#include "wfr_decode.h"
#include "wfr_network.h"
#include "wfr_ack.h"
Expand All @@ -23,6 +24,7 @@ static void print_usage(const char* prog) {
fprintf(stderr, "Usage: %s [options]\n", prog);
fprintf(stderr, "Options:\n");
fprintf(stderr, " -d, --device PATH LIRC device for RX (default: /dev/lirc0)\n");
fprintf(stderr, " -e, --evdev PATH Use evdev input device for RX (NEC via MSC_RAW, FAB4-style bit order)\n");
fprintf(stderr, " -a, --ack-device PATH LIRC device for ACK TX (default: same as --device)\n");
fprintf(stderr, " -f, --foreground Run in foreground (don't daemonize)\n");
fprintf(stderr, " -v, --verbose Verbose logging\n");
Expand Down Expand Up @@ -90,12 +92,14 @@ static void handle_credentials(const char* payload) {

int main(int argc, char* argv[]) {
const char* device = "/dev/lirc0";
const char* evdev_device = NULL;
const char* ack_device = NULL;
bool foreground = false;
int log_level = LOG_INFO;

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

int opt;
while((opt = getopt_long(argc, argv, "d:a:fvVh", long_opts, NULL)) != -1) {
while((opt = getopt_long(argc, argv, "d:e:a:fvVh", long_opts, NULL)) != -1) {
switch(opt) {
case 'd':
device = optarg;
break;
case 'e':
evdev_device = optarg;
break;
case 'a':
ack_device = optarg;
break;
Expand All @@ -134,19 +141,35 @@ int main(int argc, char* argv[]) {
openlog("infrafid", LOG_PID | (foreground ? LOG_PERROR : 0), LOG_DAEMON);
setlogmask(LOG_UPTO(log_level));

/* Default ACK device to same as RX device */
bool use_evdev = (evdev_device != NULL);
const char* rx_device = use_evdev ? evdev_device : device;

/* Default ACK device to LIRC device (not evdev — evdev can't transmit) */
if(!ack_device) ack_device = device;

syslog(LOG_INFO, "infrafid %s starting (rx=%s, tx=%s)", INFRAFI_VERSION, device, ack_device);
if(use_evdev && strcmp(ack_device, "/dev/lirc0") == 0) {
syslog(LOG_WARNING,
"infrafid: --evdev is active and ACK TX defaults to %s; set --ack-device if this host has no LIRC TX device",
ack_device);
}

syslog(LOG_INFO, "infrafid %s starting (rx=%s [%s], tx=%s)",
INFRAFI_VERSION, rx_device, use_evdev ? "evdev" : "lirc", ack_device);

struct sigaction sa = {0};
sa.sa_handler = signal_handler;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);

int lirc_fd = wfr_lirc_open(device);
if(lirc_fd < 0) {
syslog(LOG_ERR, "failed to open LIRC device %s", device);
int rx_fd;
if(use_evdev) {
rx_fd = wfr_evdev_open(evdev_device);
} else {
rx_fd = wfr_lirc_open(device);
}
if(rx_fd < 0) {
syslog(LOG_ERR, "failed to open %s device %s",
use_evdev ? "evdev" : "LIRC", rx_device);
closelog();
return 1;
}
Expand All @@ -165,8 +188,15 @@ int main(int argc, char* argv[]) {

while(running) {
uint8_t address, command;
int rc;

if(use_evdev) {
rc = wfr_evdev_read_scancode(rx_fd, &address, &command);
} else {
rc = wfr_lirc_read_scancode(rx_fd, &address, &command);
}

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

syslog(LOG_INFO, "infrafid shutting down");
wfr_ack_close(ack_fd);
wfr_lirc_close(lirc_fd);
if(use_evdev) {
wfr_evdev_close(rx_fd);
} else {
wfr_lirc_close(rx_fd);
}
closelog();
return 0;
}
6 changes: 3 additions & 3 deletions daemon/wfr_ack.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,21 @@ bool wfr_ack_send(int fd, bool success, const char* ip_str) {
uint8_t pass = 0;

/* START */
if(!send_rc6(fd, WFR_RC6_MAGIC | WFR_RC6_TYPE_START | pass, (uint8_t)payload_len)) {
if(!send_rc6(fd, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_START | pass, (uint8_t)payload_len)) {
return false;
}
delay_ms(WFR_RC6_INTER_MSG_MS);

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

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

Expand Down
Loading
Loading