diff --git a/CLAUDE.md b/CLAUDE.md index 2cc858c..d1a7cdd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/README.md b/README.md index c39275e..8a1086d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -165,9 +176,15 @@ 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 @@ -175,11 +192,31 @@ sudo systemctl status infrafid 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 | |-------|------|-------------| @@ -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/ @@ -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 diff --git a/daemon/99-infrafid-ir.rules b/daemon/99-infrafid-ir.rules new file mode 100644 index 0000000..ad92d24 --- /dev/null +++ b/daemon/99-infrafid-ir.rules @@ -0,0 +1 @@ +ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6 nec" diff --git a/daemon/99-infrafid-rc6.rules b/daemon/99-infrafid-rc6.rules deleted file mode 100644 index 6db1e49..0000000 --- a/daemon/99-infrafid-rc6.rules +++ /dev/null @@ -1 +0,0 @@ -ACTION=="add", SUBSYSTEM=="rc", ATTR{protocols}="rc-6" diff --git a/daemon/Makefile b/daemon/Makefile index 84c3242..a5bac0b 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -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 diff --git a/daemon/infrafid.service b/daemon/infrafid.service index fef4938..1ea28d8 100644 --- a/daemon/infrafid.service +++ b/daemon/infrafid.service @@ -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 diff --git a/daemon/install.sh b/daemon/install.sh index 0ac382a..30fbd7a 100644 --- a/daemon/install.sh +++ b/daemon/install.sh @@ -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 diff --git a/daemon/main.c b/daemon/main.c index b90b3a8..4f9a2a2 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -1,4 +1,5 @@ #include "wfr_lirc.h" +#include "wfr_evdev.h" #include "wfr_decode.h" #include "wfr_network.h" #include "wfr_ack.h" @@ -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"); @@ -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'}, @@ -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; @@ -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; } @@ -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; } @@ -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; } diff --git a/daemon/wfr_ack.c b/daemon/wfr_ack.c index 0f66713..fa1b34e 100644 --- a/daemon/wfr_ack.c +++ b/daemon/wfr_ack.c @@ -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; } diff --git a/daemon/wfr_decode.c b/daemon/wfr_decode.c index 64e1f1d..320187d 100644 --- a/daemon/wfr_decode.c +++ b/daemon/wfr_decode.c @@ -39,13 +39,13 @@ int wfr_decode_feed_scancode( if(wfr_decode_check_timeout(dec)) return -1; /* Filter: must have InfraFi magic in high nibble */ - if((rc6_address & WFR_RC6_MAGIC_MASK) != WFR_RC6_MAGIC) return 0; + if((rc6_address & WFR_FRAME_MAGIC_MASK) != WFR_FRAME_MAGIC) return 0; - uint8_t frame_type = rc6_address & WFR_RC6_TYPE_MASK; - uint8_t pass = rc6_address & WFR_RC6_PASS_MASK; + uint8_t frame_type = rc6_address & WFR_FRAME_TYPE_MASK; + uint8_t pass = rc6_address & WFR_FRAME_PASS_MASK; switch(frame_type) { - case WFR_RC6_TYPE_START: { + case WFR_FRAME_TYPE_START: { uint8_t total_len = rc6_command; if(total_len == 0) { syslog(LOG_WARNING, "infrafid: START with zero length"); @@ -75,7 +75,7 @@ int wfr_decode_feed_scancode( return 0; } - case WFR_RC6_TYPE_DATA: { + case WFR_FRAME_TYPE_DATA: { if(!dec->in_transmission) return 0; if(dec->write_cursor >= dec->expected_len) return 0; @@ -91,7 +91,7 @@ int wfr_decode_feed_scancode( return 0; } - case WFR_RC6_TYPE_END: { + case WFR_FRAME_TYPE_END: { if(!dec->in_transmission) return 0; if(dec->write_cursor != dec->expected_len) { diff --git a/daemon/wfr_evdev.c b/daemon/wfr_evdev.c new file mode 100644 index 0000000..fb5996b --- /dev/null +++ b/daemon/wfr_evdev.c @@ -0,0 +1,111 @@ +#include "wfr_evdev.h" + +#include +#include +#include +#include +#include +#include +#include + +/* Reverse bit order within a byte. + * The FAB4 IR receiver delivers NEC scancodes with bit-reversed bytes + * relative to the standard NEC encoding. */ +static uint8_t reverse_bits(uint8_t b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +int wfr_evdev_open(const char* device) { + int fd = open(device, O_RDONLY); + if(fd < 0) { + syslog(LOG_ERR, "infrafid: failed to open %s: %s", device, strerror(errno)); + return -1; + } + + /* Verify the device supports EV_MSC */ + enum { + EVBITS_WORDS = (EV_MAX / (8 * sizeof(unsigned long))) + 1 + }; + unsigned long evbits[EVBITS_WORDS]; + memset(evbits, 0, sizeof(evbits)); + if(ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), evbits) < 0) { + syslog(LOG_ERR, "infrafid: EVIOCGBIT failed on %s: %s", device, strerror(errno)); + close(fd); + return -1; + } + + if(!(evbits[EV_MSC / (8 * sizeof(unsigned long))] & + (1UL << (EV_MSC % (8 * sizeof(unsigned long)))))) { + syslog(LOG_ERR, "infrafid: %s does not support EV_MSC events", device); + close(fd); + return -1; + } + + syslog(LOG_INFO, "infrafid: opened evdev %s (fd=%d)", device, fd); + return fd; +} + +void wfr_evdev_close(int fd) { + if(fd >= 0) { + close(fd); + } +} + +int wfr_evdev_read_scancode(int fd, uint8_t* address, uint8_t* command) { + for(;;) { + struct input_event ev; + ssize_t n = read(fd, &ev, sizeof(ev)); + if(n != (ssize_t)sizeof(ev)) { + if(n < 0 && errno != EINTR) { + syslog(LOG_ERR, "infrafid: evdev read error: %s", strerror(errno)); + } + return -1; + } + + /* Only process MSC_RAW (code 3) events */ + if(ev.type != EV_MSC || ev.code != MSC_RAW) { + continue; + } + + uint32_t raw = (uint32_t)ev.value; + + syslog(LOG_DEBUG, + "infrafid: evdev raw=0x%08x", + raw); + + /* Bit-reverse each byte to recover the NEC frame. + * NOTE: This assumes FAB4-style MSC_RAW bit ordering. + * Other receivers may already report bytes in standard NEC order. + * + * byte 0 = address, byte 1 = ~address, byte 2 = command, byte 3 = ~command */ + uint8_t addr = reverse_bits((raw >> 24) & 0xFF); + uint8_t addr_inv = reverse_bits((raw >> 16) & 0xFF); + uint8_t cmd = reverse_bits((raw >> 8) & 0xFF); + uint8_t cmd_inv = reverse_bits( raw & 0xFF); + + /* Validate NEC inverse bytes */ + if((addr ^ addr_inv) != 0xFF) { + syslog(LOG_DEBUG, + "infrafid: NEC address validation failed (0x%02x ^ 0x%02x != 0xFF)", + addr, addr_inv); + continue; + } + if((cmd ^ cmd_inv) != 0xFF) { + syslog(LOG_DEBUG, + "infrafid: NEC command validation failed (0x%02x ^ 0x%02x != 0xFF)", + cmd, cmd_inv); + continue; + } + + syslog(LOG_DEBUG, + "infrafid: NEC decoded addr=0x%02x cmd=0x%02x", + addr, cmd); + + *address = addr; + *command = cmd; + return 0; + } +} diff --git a/daemon/wfr_evdev.h b/daemon/wfr_evdev.h new file mode 100644 index 0000000..963735f --- /dev/null +++ b/daemon/wfr_evdev.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +/* Open an evdev input device for reading IR scancodes. Returns fd or -1 on error. */ +int wfr_evdev_open(const char* device); + +/* Close an evdev device. */ +void wfr_evdev_close(int fd); + +/* Read one NEC IR scancode from an evdev device (MSC_RAW events). + * Blocks until a valid NEC scancode is available. + * Performs FAB4-style bit-reversal and NEC inverse validation. + * + * address: the decoded 8-bit address field + * command: the decoded 8-bit command field + * + * Returns 0 on success, -1 on error. */ +int wfr_evdev_read_scancode(int fd, uint8_t* address, uint8_t* command); diff --git a/daemon/wfr_lirc.c b/daemon/wfr_lirc.c index c0525e2..b9f6f2a 100644 --- a/daemon/wfr_lirc.c +++ b/daemon/wfr_lirc.c @@ -34,7 +34,7 @@ void wfr_lirc_close(int fd) { } } -int wfr_lirc_read_scancode(int fd, uint8_t* rc6_address, uint8_t* rc6_command) { +int wfr_lirc_read_scancode(int fd, uint8_t* address, uint8_t* command) { for(;;) { struct lirc_scancode sc; ssize_t n = read(fd, &sc, sizeof(sc)); @@ -51,14 +51,14 @@ int wfr_lirc_read_scancode(int fd, uint8_t* rc6_address, uint8_t* rc6_command) { (unsigned)sc.flags, (unsigned long long)sc.scancode); - /* Only accept RC-6 Mode 0 */ - if(sc.rc_proto != RC_PROTO_RC6_0) { + /* Accept RC-6 Mode 0 or standard NEC — both encode as (address << 8 | command) */ + if(sc.rc_proto != RC_PROTO_RC6_0 && sc.rc_proto != RC_PROTO_NEC) { continue; } - /* RC-6 Mode 0 scancode: bits [15:8] = address, bits [7:0] = command */ - *rc6_address = (uint8_t)((sc.scancode >> 8) & 0xFF); - *rc6_command = (uint8_t)(sc.scancode & 0xFF); + /* Scancode layout: bits [15:8] = address, bits [7:0] = command */ + *address = (uint8_t)((sc.scancode >> 8) & 0xFF); + *command = (uint8_t)(sc.scancode & 0xFF); return 0; } } diff --git a/daemon/wfr_lirc.h b/daemon/wfr_lirc.h index 624f726..ca76a4e 100644 --- a/daemon/wfr_lirc.h +++ b/daemon/wfr_lirc.h @@ -9,11 +9,11 @@ int wfr_lirc_open(const char* device); /* Close a LIRC device. */ void wfr_lirc_close(int fd); -/* Read one decoded RC-6 scancode from the LIRC device. - * Blocks until an RC-6 scancode is available. +/* Read one decoded RC-6 or NEC scancode from the LIRC device. + * Blocks until a supported scancode is available. * - * rc6_address: the 8-bit address field - * rc6_command: the 8-bit command field + * address: the 8-bit address field + * command: the 8-bit command field * * Returns 0 on success, -1 on error. */ -int wfr_lirc_read_scancode(int fd, uint8_t* rc6_address, uint8_t* rc6_command); +int wfr_lirc_read_scancode(int fd, uint8_t* address, uint8_t* command); diff --git a/debian/infrafid.install b/debian/infrafid.install index 9aa0ad1..e5ba2a7 100644 --- a/debian/infrafid.install +++ b/debian/infrafid.install @@ -1,2 +1,2 @@ daemon/infrafid usr/bin -daemon/99-infrafid-rc6.rules etc/udev/rules.d +daemon/99-infrafid-ir.rules etc/udev/rules.d diff --git a/flipper/protocol/version.h b/flipper/protocol/version.h index 32033e7..ecc0ef9 100644 --- a/flipper/protocol/version.h +++ b/flipper/protocol/version.h @@ -1,6 +1,6 @@ #pragma once -#define INFRAFI_VERSION "1.0.0" +#define INFRAFI_VERSION "1.2.0" #define INFRAFI_VERSION_MAJOR 1 -#define INFRAFI_VERSION_MINOR 0 +#define INFRAFI_VERSION_MINOR 2 #define INFRAFI_VERSION_PATCH 0 diff --git a/flipper/protocol/wfr_protocol.h b/flipper/protocol/wfr_protocol.h index fec819a..b1c4518 100644 --- a/flipper/protocol/wfr_protocol.h +++ b/flipper/protocol/wfr_protocol.h @@ -8,11 +8,17 @@ extern "C" { #endif +/* IR transport protocol selection */ +typedef enum { + WfrIrProtocolRC6 = 0, + WfrIrProtocolNEC = 1, +} WfrIrProtocol; + /* - * InfraFi Protocol — RC-6 Scancode Encoding + * InfraFi Protocol — IR Frame Encoding (RC-6 and NEC) * - * Uses standard RC-6 IR messages (decoded by the kernel) instead of raw timings. - * Each RC-6 message carries 1 byte of payload via: address (framing) + command (data). + * Uses kernel-decoded RC-6/NEC scancodes instead of raw timings. + * Each IR message carries 1 byte of payload via: address (framing) + command (data). * Payload is a WiFi QR string: WIFI:T:;S:;P:;H:;; * * Address byte layout: @@ -27,24 +33,38 @@ extern "C" { * Same framing. Payload is "OK:" on success or "FAIL" on failure. */ -/* RC-6 address byte encoding */ -#define WFR_RC6_MAGIC 0xA0 -#define WFR_RC6_MAGIC_MASK 0xF0 -#define WFR_RC6_TYPE_MASK 0x0C -#define WFR_RC6_PASS_MASK 0x03 +/* Protocol framing encoded in the address byte */ +#define WFR_FRAME_MAGIC 0xA0 +#define WFR_FRAME_MAGIC_MASK 0xF0 +#define WFR_FRAME_TYPE_MASK 0x0C +#define WFR_FRAME_PASS_MASK 0x03 -#define WFR_RC6_TYPE_START 0x00 -#define WFR_RC6_TYPE_DATA 0x04 -#define WFR_RC6_TYPE_END 0x08 +#define WFR_FRAME_TYPE_START 0x00 +#define WFR_FRAME_TYPE_DATA 0x04 +#define WFR_FRAME_TYPE_END 0x08 /* Frame type for ACK responses (daemon → Flipper) */ -#define WFR_RC6_TYPE_ACK 0x0C +#define WFR_FRAME_TYPE_ACK 0x0C + +/* Backward-compatible aliases; prefer WFR_FRAME_* for new code. */ +#define WFR_RC6_MAGIC WFR_FRAME_MAGIC +#define WFR_RC6_MAGIC_MASK WFR_FRAME_MAGIC_MASK +#define WFR_RC6_TYPE_MASK WFR_FRAME_TYPE_MASK +#define WFR_RC6_PASS_MASK WFR_FRAME_PASS_MASK +#define WFR_RC6_TYPE_START WFR_FRAME_TYPE_START +#define WFR_RC6_TYPE_DATA WFR_FRAME_TYPE_DATA +#define WFR_RC6_TYPE_END WFR_FRAME_TYPE_END +#define WFR_RC6_TYPE_ACK WFR_FRAME_TYPE_ACK /* Timing */ #define WFR_RC6_INTER_MSG_MS 20 /* Delay between RC-6 messages (ms) */ #define WFR_RC6_RETRANSMIT_GAP_MS 200 /* Gap between retransmission passes */ #define WFR_RETRANSMIT_COUNT 1 +/* NEC timing — longer frames (~67ms each) need wider gaps to avoid repeat codes */ +#define WFR_NEC_INTER_MSG_MS 50 +#define WFR_NEC_RETRANSMIT_GAP_MS 200 + /* ACK timeout — how long Flipper waits for a response (seconds) */ #define WFR_ACK_TIMEOUT_SEC 30 diff --git a/flipper/scenes/wi_fir_scene_settings.c b/flipper/scenes/wi_fir_scene_settings.c index a5dc1d9..2ef0ae1 100644 --- a/flipper/scenes/wi_fir_scene_settings.c +++ b/flipper/scenes/wi_fir_scene_settings.c @@ -2,13 +2,26 @@ #include "../wfr_storage.h" static const char* ack_names[] = {"Off", "On"}; +static const char* protocol_names[] = {"RC-6", "NEC"}; + +static void wi_fir_scene_settings_save(WiFirApp* app) { + wfr_storage_save_settings(app->storage, app->ack_enabled, app->ir_protocol); +} + +static void wi_fir_scene_settings_protocol_change(VariableItem* item) { + WiFirApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + app->ir_protocol = (WfrIrProtocol)index; + variable_item_set_current_value_text(item, protocol_names[index]); + wi_fir_scene_settings_save(app); +} static void wi_fir_scene_settings_ack_change(VariableItem* item) { WiFirApp* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); app->ack_enabled = (index == 1); variable_item_set_current_value_text(item, ack_names[index]); - wfr_storage_save_settings(app->storage, app->ack_enabled); + wi_fir_scene_settings_save(app); } void wi_fir_scene_settings_on_enter(void* context) { @@ -16,6 +29,17 @@ void wi_fir_scene_settings_on_enter(void* context) { variable_item_list_reset(app->variable_item_list); + /* IR Protocol toggle */ + VariableItem* proto_item = variable_item_list_add( + app->variable_item_list, + "IR Protocol", + 2, + wi_fir_scene_settings_protocol_change, + app); + variable_item_set_current_value_index(proto_item, (uint8_t)app->ir_protocol); + variable_item_set_current_value_text(proto_item, protocol_names[app->ir_protocol]); + + /* Wait for ACK toggle */ VariableItem* item = variable_item_list_add( app->variable_item_list, "Wait for ACK", diff --git a/flipper/scenes/wi_fir_scene_transmit.c b/flipper/scenes/wi_fir_scene_transmit.c index 17326ee..315a773 100644 --- a/flipper/scenes/wi_fir_scene_transmit.c +++ b/flipper/scenes/wi_fir_scene_transmit.c @@ -14,7 +14,8 @@ static void wi_fir_ir_rx_callback(void* context, InfraredWorkerSignal* signal) { if(!infrared_worker_signal_is_decoded(signal)) return; const InfraredMessage* message = infrared_worker_get_decoded_signal(signal); - if(message->protocol != InfraredProtocolRC6) return; + if(message->protocol != InfraredProtocolRC6 && + message->protocol != InfraredProtocolNEC) return; uint8_t address = (uint8_t)(message->address & 0xFF); uint8_t command = (uint8_t)(message->command & 0xFF); @@ -83,7 +84,7 @@ void wi_fir_scene_transmit_on_enter(void* context) { notification_message(app->notifications, &sequence_blink_start_magenta); /* Transmit via IR */ - bool success = wfr_transmit_credentials(&creds); + bool success = wfr_transmit_credentials(&creds, app->ir_protocol); notification_message(app->notifications, &sequence_blink_stop); diff --git a/flipper/wfr_decode.c b/flipper/wfr_decode.c index 48e2c0c..b36dfea 100644 --- a/flipper/wfr_decode.c +++ b/flipper/wfr_decode.c @@ -34,12 +34,12 @@ int wfr_ack_decode_feed( if(ack_check_timeout(dec)) return -1; /* Must have InfraFi magic in high nibble */ - if((rc6_address & WFR_RC6_MAGIC_MASK) != WFR_RC6_MAGIC) return 0; + if((rc6_address & WFR_FRAME_MAGIC_MASK) != WFR_FRAME_MAGIC) return 0; - uint8_t frame_type = rc6_address & WFR_RC6_TYPE_MASK; + uint8_t frame_type = rc6_address & WFR_FRAME_TYPE_MASK; switch(frame_type) { - case WFR_RC6_TYPE_START: { + case WFR_FRAME_TYPE_START: { uint8_t total_len = rc6_command; if(total_len == 0) { FURI_LOG_W(TAG, "START with zero length"); @@ -63,7 +63,7 @@ int wfr_ack_decode_feed( return 0; } - case WFR_RC6_TYPE_DATA: { + case WFR_FRAME_TYPE_DATA: { if(!dec->in_transmission) return 0; if(dec->write_cursor >= dec->expected_len) return 0; @@ -72,7 +72,7 @@ int wfr_ack_decode_feed( return 0; } - case WFR_RC6_TYPE_END: { + case WFR_FRAME_TYPE_END: { if(!dec->in_transmission) return 0; if(dec->write_cursor != dec->expected_len) { diff --git a/flipper/wfr_encode.c b/flipper/wfr_encode.c index a4d4839..67a2275 100644 --- a/flipper/wfr_encode.c +++ b/flipper/wfr_encode.c @@ -2,10 +2,10 @@ #include #include -/* Send one RC-6 message */ -static void wfr_send_rc6(uint8_t address, uint8_t command) { +/* Send one IR message using the selected protocol */ +static void wfr_send_ir(WfrIrProtocol protocol, uint8_t address, uint8_t command) { InfraredMessage message = { - .protocol = InfraredProtocolRC6, + .protocol = (protocol == WfrIrProtocolNEC) ? InfraredProtocolNEC : InfraredProtocolRC6, .address = address, .command = command, .repeat = false, @@ -13,7 +13,7 @@ static void wfr_send_rc6(uint8_t address, uint8_t command) { infrared_send(&message, 1); } -bool wfr_transmit_credentials(const WfrWifiCreds* creds) { +bool wfr_transmit_credentials(const WfrWifiCreds* creds, WfrIrProtocol protocol) { if(!creds || creds->ssid[0] == '\0') return false; /* Build WiFi QR string */ @@ -24,26 +24,32 @@ bool wfr_transmit_credentials(const WfrWifiCreds* creds) { const uint8_t* payload = (const uint8_t*)wifi_str; uint8_t payload_crc = wfr_crc8(payload, wifi_len); + /* Select timing based on protocol */ + uint32_t inter_msg_ms = (protocol == WfrIrProtocolNEC) ? + WFR_NEC_INTER_MSG_MS : WFR_RC6_INTER_MSG_MS; + uint32_t retransmit_gap_ms = (protocol == WfrIrProtocolNEC) ? + WFR_NEC_RETRANSMIT_GAP_MS : WFR_RC6_RETRANSMIT_GAP_MS; + /* Retransmit the full sequence multiple times */ for(uint8_t attempt = 0; attempt < WFR_RETRANSMIT_COUNT; attempt++) { - uint8_t pass = attempt & WFR_RC6_PASS_MASK; + uint8_t pass = attempt & WFR_FRAME_PASS_MASK; /* --- START: address = magic | TYPE_START | pass, command = length --- */ - wfr_send_rc6(WFR_RC6_MAGIC | WFR_RC6_TYPE_START | pass, (uint8_t)wifi_len); - furi_delay_ms(WFR_RC6_INTER_MSG_MS); + wfr_send_ir(protocol, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_START | pass, (uint8_t)wifi_len); + furi_delay_ms(inter_msg_ms); - /* --- DATA: one RC-6 message per payload byte --- */ + /* --- DATA: one IR message per payload byte --- */ for(size_t i = 0; i < wifi_len; i++) { - wfr_send_rc6(WFR_RC6_MAGIC | WFR_RC6_TYPE_DATA | pass, payload[i]); - furi_delay_ms(WFR_RC6_INTER_MSG_MS); + wfr_send_ir(protocol, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_DATA | pass, payload[i]); + furi_delay_ms(inter_msg_ms); } /* --- END: address = magic | TYPE_END | pass, command = CRC-8 --- */ - wfr_send_rc6(WFR_RC6_MAGIC | WFR_RC6_TYPE_END | pass, payload_crc); + wfr_send_ir(protocol, WFR_FRAME_MAGIC | WFR_FRAME_TYPE_END | pass, payload_crc); /* Gap between retransmission passes */ if(attempt + 1 < WFR_RETRANSMIT_COUNT) { - furi_delay_ms(WFR_RC6_RETRANSMIT_GAP_MS); + furi_delay_ms(retransmit_gap_ms); } } diff --git a/flipper/wfr_encode.h b/flipper/wfr_encode.h index f6455a1..c9f2329 100644 --- a/flipper/wfr_encode.h +++ b/flipper/wfr_encode.h @@ -8,14 +8,15 @@ extern "C" { #endif /* - * Transmit WiFi credentials over IR using RC-6 protocol messages. + * Transmit WiFi credentials over IR. * - * Builds the WiFi QR string, sends it as a sequence of RC-6 messages + * Builds the WiFi QR string, sends it as a sequence of IR messages * (START + DATA bytes + END), repeated WFR_RETRANSMIT_COUNT times. + * Uses RC-6 or NEC encoding based on the protocol parameter. * * Returns true if transmission completed, false on error. */ -bool wfr_transmit_credentials(const WfrWifiCreds* creds); +bool wfr_transmit_credentials(const WfrWifiCreds* creds, WfrIrProtocol protocol); #ifdef __cplusplus } diff --git a/flipper/wfr_storage.c b/flipper/wfr_storage.c index 5cb6c15..18fd9c1 100644 --- a/flipper/wfr_storage.c +++ b/flipper/wfr_storage.c @@ -152,7 +152,7 @@ bool wfr_storage_delete(Storage* storage, const char* filename) { #define WFR_SETTINGS_TYPE "InfraFi Settings" #define WFR_SETTINGS_VERSION 1 -bool wfr_storage_load_settings(Storage* storage, bool* ack_enabled) { +bool wfr_storage_load_settings(Storage* storage, bool* ack_enabled, WfrIrProtocol* ir_protocol) { FlipperFormat* ff = flipper_format_file_alloc(storage); bool ok = false; FuriString* tmp = furi_string_alloc(); @@ -169,6 +169,10 @@ bool wfr_storage_load_settings(Storage* storage, bool* ack_enabled) { if(flipper_format_read_uint32(ff, "WaitForACK", &ack, 1)) { *ack_enabled = (ack != 0); } + uint32_t proto = 0; + if(flipper_format_read_uint32(ff, "IrProtocol", &proto, 1)) { + *ir_protocol = (proto <= WfrIrProtocolNEC) ? (WfrIrProtocol)proto : WfrIrProtocolRC6; + } ok = true; } while(false); @@ -177,7 +181,7 @@ bool wfr_storage_load_settings(Storage* storage, bool* ack_enabled) { return ok; } -bool wfr_storage_save_settings(Storage* storage, bool ack_enabled) { +bool wfr_storage_save_settings(Storage* storage, bool ack_enabled, WfrIrProtocol ir_protocol) { storage_simply_mkdir(storage, WFR_SAVE_DIR); FlipperFormat* ff = flipper_format_file_alloc(storage); @@ -188,6 +192,8 @@ bool wfr_storage_save_settings(Storage* storage, bool ack_enabled) { if(!flipper_format_write_header_cstr(ff, WFR_SETTINGS_TYPE, WFR_SETTINGS_VERSION)) break; uint32_t ack = ack_enabled ? 1 : 0; if(!flipper_format_write_uint32(ff, "WaitForACK", &ack, 1)) break; + uint32_t proto = (uint32_t)ir_protocol; + if(!flipper_format_write_uint32(ff, "IrProtocol", &proto, 1)) break; ok = true; } while(false); diff --git a/flipper/wfr_storage.h b/flipper/wfr_storage.h index 302d265..cb35b64 100644 --- a/flipper/wfr_storage.h +++ b/flipper/wfr_storage.h @@ -28,7 +28,7 @@ uint8_t wfr_storage_list( bool wfr_storage_delete(Storage* storage, const char* filename); /* Load app settings from SD card. Returns false if file doesn't exist. */ -bool wfr_storage_load_settings(Storage* storage, bool* ack_enabled); +bool wfr_storage_load_settings(Storage* storage, bool* ack_enabled, WfrIrProtocol* ir_protocol); /* Save app settings to SD card. */ -bool wfr_storage_save_settings(Storage* storage, bool ack_enabled); +bool wfr_storage_save_settings(Storage* storage, bool ack_enabled, WfrIrProtocol ir_protocol); diff --git a/flipper/wi_fir.c b/flipper/wi_fir.c index 4110049..85adb94 100644 --- a/flipper/wi_fir.c +++ b/flipper/wi_fir.c @@ -60,7 +60,7 @@ static WiFirApp* wi_fir_alloc(void) { app->view_dispatcher, WiFirViewLoading, loading_get_view(app->loading)); /* Load settings from SD card */ - wfr_storage_load_settings(app->storage, &app->ack_enabled); + wfr_storage_load_settings(app->storage, &app->ack_enabled, &app->ir_protocol); return app; } diff --git a/flipper/wi_fir.h b/flipper/wi_fir.h index 7b51351..333a855 100644 --- a/flipper/wi_fir.h +++ b/flipper/wi_fir.h @@ -81,6 +81,7 @@ typedef struct { char selected_saved_file[WFR_FILENAME_MAX]; /* Settings */ + WfrIrProtocol ir_protocol; bool ack_enabled; /* IR ACK receive state */ diff --git a/rpm/infrafid.spec b/rpm/infrafid.spec index 4e0bc74..ad18f81 100644 --- a/rpm/infrafid.spec +++ b/rpm/infrafid.spec @@ -15,7 +15,7 @@ BuildRequires: systemd-rpm-macros %global debug_package %{nil} %description -Receives WiFi credentials transmitted via infrared (RC-6 protocol) from a +Receives WiFi credentials transmitted via infrared (RC-6 or NEC protocols) from a Flipper Zero running the InfraFi app. Automatically connects to the transmitted WiFi network using NetworkManager, systemd-networkd, or ifupdown. @@ -32,14 +32,14 @@ make %{?_smp_mflags} %install install -D -m 0755 daemon/infrafid %{buildroot}%{_bindir}/infrafid install -D -m 0644 daemon/infrafid.service %{buildroot}%{_unitdir}/infrafid.service -install -D -m 0644 daemon/99-infrafid-rc6.rules %{buildroot}%{_udevrulesdir}/99-infrafid-rc6.rules +install -D -m 0644 daemon/99-infrafid-ir.rules %{buildroot}%{_udevrulesdir}/99-infrafid-ir.rules %files %license LICENSE.md %doc README.md %{_bindir}/infrafid %{_unitdir}/infrafid.service -%{_udevrulesdir}/99-infrafid-rc6.rules +%{_udevrulesdir}/99-infrafid-ir.rules %post %systemd_post infrafid.service diff --git a/screenshots/ss7.png b/screenshots/ss7.png index d83ee92..2f13e7a 100644 Binary files a/screenshots/ss7.png and b/screenshots/ss7.png differ