diff --git a/.codex_tmp/LilyGoLib b/.codex_tmp/LilyGoLib deleted file mode 160000 index a64fc6ca9..000000000 --- a/.codex_tmp/LilyGoLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a64fc6ca94757baa5401ad71b39fb7f92cd1a7e9 diff --git a/.codex_tmp/NFC-RFAL-fork b/.codex_tmp/NFC-RFAL-fork deleted file mode 160000 index 7bde4587e..000000000 --- a/.codex_tmp/NFC-RFAL-fork +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7bde4587ea44c36c178a43d083d7a7345c1e4cfe diff --git a/.codex_tmp/ST25R3916-fork b/.codex_tmp/ST25R3916-fork deleted file mode 160000 index 0c8e00f49..000000000 --- a/.codex_tmp/ST25R3916-fork +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0c8e00f49d12881d1cff6d6f0879b90ce7ed4033 diff --git a/.github/workflows/buil_parallel.yml b/.github/workflows/buil_parallel.yml index 53ef14685..1e1df085f 100644 --- a/.github/workflows/buil_parallel.yml +++ b/.github/workflows/buil_parallel.yml @@ -77,6 +77,7 @@ jobs: - { env: "lilygo-t-lora-pager", family: "ESP32-S3",} - { env: "elecrow-24B", family: "ESP32",} - { env: "elecrow-28B", family: "ESP32",} + - { env: "elecrow-advance-35-s3", family: "ESP32-S3",} - { env: "smoochiee-board", family: "ESP32-S3",} - { env: "reaper", family: "ESP32-S3",} - { env: "Phantom_S024R", family: "ESP32",} diff --git a/.gitignore b/.gitignore index 243b5b105..a5ed19720 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .vscode/launch.json .vscode/extensions.json .vscode/ipch +.codex_tmp +.claude Bruce3_*.bin bruce.conf _*.sh @@ -20,4 +22,4 @@ lib/mquickjs_headers/mqjs_stdlib_generator lib/mquickjs_headers/mqjs_stdlib_generator.exe .github/agents/Bruce-AI.agent.md .idea -.ai \ No newline at end of file +.ai diff --git a/2.0_road_path.md b/2.0_road_path.md index 589171969..731358e07 100644 --- a/2.0_road_path.md +++ b/2.0_road_path.md @@ -30,11 +30,11 @@ the objective of 2.0 is changing the Framework to Arduino-ESP32 v3.+, to be able ### RF - [] Rolljam - - Decode/Encode Protocols + - [x] Decode/Encode Protocols ### NRF24 - - [] mouse jacking - - [] keyboard jacking + - [x] mouse jacking + - [x] keyboard jacking ### Interpreter - Possibility to access I2C, SPI, UART buses diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 000000000..475a85aba --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,100 @@ +# Third-party code and attribution + +Bruce is licensed under the **GNU Affero General Public License v3.0 or later** +(AGPL-3.0-or-later); see [`LICENSE`](LICENSE). + +Parts of Bruce's RF / sub-GHz module (`src/modules/rf/`) are **derived from** +other free-software projects. Those portions were modified to fit Bruce's native +RMT engine, data model and UI, but remain the work of their original authors and +are used under their respective licenses. Each affected source file carries a +header pointing back to this document. + +> This file documents attribution for copyleft compliance. It is not legal +> advice; if you redistribute Bruce, review these obligations yourself. + +## Sub-GHz protocols / KeeLoq + +### Flipper Zero firmware — GPL-3.0-or-later +- Upstream: +- License: GNU General Public License v3.0 +- Copyright (C) Flipper Devices Inc. and the flipperzero-firmware contributors. +- Used by: the KeeLoq block cipher, the manufacturer "learning" key-derivation + schemes, the KeeLoq frame decoder/encoder framing, the `.sub` keystore format + and protocol identities. + Files: `src/modules/rf/protocols/rf_keeloq.{h,cpp}`, + `src/modules/rf/protocols/rf_decoder.{h,cpp}`, + `src/modules/rf/protocols/rf_encoder.{h,cpp}`. + +### Momentum firmware — GPL-3.0-or-later +- Upstream: +- License: GNU General Public License v3.0 +- Copyright (C) the Momentum Firmware contributors. +- Used by: the extended KeeLoq manufacturer list and the model for the + encrypted built-in keystore (`keeloq_mfcodes`), which Bruce mirrors by + shipping an AES-encrypted keystore decrypted at runtime as a fallback. + Files: `src/modules/rf/protocols/rf_keeloq.cpp`, + `src/modules/rf/protocols/rf_keeloq_mfcodes_data.h` (generated), + `tools/gen_mfcodes.py`. + +> Note: the KeeLoq manufacturer keys themselves originate from the respective +> device manufacturers. They are proprietary key material; their redistribution +> is a separate concern from the GPL attribution above and is the redistributor's +> responsibility to evaluate. + +### rc-switch (RCSwitch) — LGPL-2.1-or-later +- Upstream: +- License: GNU Lesser General Public License v2.1 +- Copyright (C) 2011 Suat Özgür and the rc-switch contributors. +- Used by: the classic OOK protocol timing table (the numbered `RcSwitch_N` + protocols and the factor-based `{high,low}×te` model) and the + capture/decode and send state machines that Bruce re-implemented over its + native RMT engine. + Files: `src/modules/rf/protocols/rf_registry.{h,cpp}`, + `src/modules/rf/protocols/rf_decoder.{h,cpp}`, + `src/modules/rf/protocols/rf_encoder.{h,cpp}`. + +## NFC / RFID (ST25R3916) + +The ST25R3916 NFC front-end driver (`src/modules/rfid/ST25R3916.{h,cpp}`) is a +native re-implementation over a fork of ST's RFAL stack, but several parts — +particularly the card-emulation (listen / passive-target) path — were **derived +from** the Momentum firmware, itself a fork of the Flipper Zero firmware. Those +portions were rewritten to drive the ST25R3916 register interface directly +(the RFAL fork's high-level `rfalListenStart` is a stub), but the register +sequences and protocol state machines follow the upstream NFC HAL/stack. + +### Momentum firmware (Flipper Zero NFC stack) — GPL-3.0-or-later +- Upstream: +- Based on: +- License: GNU General Public License v3.0 +- Copyright (C) the Momentum Firmware and flipperzero-firmware contributors. +- Used by: + - The NFC-A listen / passive-target register configuration (MODE.targ + + `om_targ_nfca`, PT memory load of UID/ATQA/SAK, OP_CONTROL / PASSIVE_TARGET + / MASK_RX_TIMER setup and the target-mode IRQ mask), which follows the + Flipper `furi_hal_nfc` sequence on the same ST25R chip family. + - The MIFARE Classic listener-side Crypto1 emulation: authentication + handshake, manual-parity TX/RX handling and the encrypted READ/WRITE + service from the loaded dump. + - The MIFARE Ultralight / NTAG21x listener command handling — `GET_VERSION`, + `READ_SIG` (ECC signature), `READ_CNT`, `PWD_AUTH`/`PACK` and the amiibo + (NTAG215, UID-derived password, `80 80` PACK) behaviour. + - The FeliCa (NFC-F) and Type 4 Tag (ISO-DEP / NDEF) emulation responders. + Files: `src/modules/rfid/ST25R3916.cpp`. + +### Crypto1 cipher (Crapto1 / Proxmark lineage) — public reimplementation +- References: "Dismantling MIFARE Classic" (Garcia et al., 2008) and the public + Crapto1 reference; the same algorithm is used by the Flipper / Momentum and + Proxmark NFC stacks. +- Used by: the compact Crypto1 stream cipher that backs both reader-side + MIFARE Classic auth/read/write and the listener-side emulation, since the + RFAL fork has no native Crypto1 support. + Files: `src/modules/rfid/crypto1.{h,cpp}`. + +## Bundled libraries (linked dependencies) + +Fetched via PlatformIO `lib_deps` (see `platformio.ini`); each keeps its own +license. Notably the CC1101 radio driver: + +- **SmartRC-CC1101-Driver-Lib** (ELECHOUSE driver fork) — + diff --git a/boards/ESP32-C5-tft/interface.cpp b/boards/ESP32-C5-tft/interface.cpp index 593ba8b85..9e4635feb 100644 --- a/boards/ESP32-C5-tft/interface.cpp +++ b/boards/ESP32-C5-tft/interface.cpp @@ -167,8 +167,12 @@ void InputHandler(void) { if (anyPressed && wakeUpScreen()) return; AnyKeyPress = anyPressed; + if (upPressed && dwPressed) { + EscPress = true; + return; + } PrevPress = upPressed; - EscPress = upPressed && dwPressed; + NextPress = dwPressed; SelPress = selPressed; #endif diff --git a/boards/_boards_json/esp-wroom32-ili9341.json b/boards/_boards_json/esp-wroom32-ili9341.json new file mode 100644 index 000000000..922cde402 --- /dev/null +++ b/boards/_boards_json/esp-wroom32-ili9341.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DESP_WROOM32_ILI9341", + "-DARDUINO_ESP32_DEV", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "pinouts" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "ESP-WROOM-32 with ILI9341 2.4 TFT", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://en.wikipedia.org/wiki/ESP32", + "vendor": "Espressif" +} diff --git a/boards/_boards_json/esp-wroom32-st7789.json b/boards/_boards_json/esp-wroom32-st7789.json new file mode 100644 index 000000000..1f25be190 --- /dev/null +++ b/boards/_boards_json/esp-wroom32-st7789.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DESP_WROOM32_ST7789", + "-DARDUINO_ESP32_DEV", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "pinouts" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "ESP-WROOM-32 with ST7789 2.8 TFT Touch", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://en.wikipedia.org/wiki/ESP32", + "vendor": "Espressif" +} diff --git a/boards/_boards_json/esp32s3-ili9341.json b/boards/_boards_json/esp32s3-ili9341.json new file mode 100644 index 000000000..0da764519 --- /dev/null +++ b/boards/_boards_json/esp32s3-ili9341.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DESP32S3_ILI9341", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_CDC_ON_BOOT" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "pinouts" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "ESP32-S3 ILI9341 Touchscreen (8MB Flash)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html", + "vendor": "Espressif" + } diff --git a/boards/_boards_json/esp32s3-st7789.json b/boards/_boards_json/esp32s3-st7789.json new file mode 100644 index 000000000..d5e6d5095 --- /dev/null +++ b/boards/_boards_json/esp32s3-st7789.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DESP32S3_ST7789", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_CDC_ON_BOOT" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "pinouts" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "ESP32-S3 ST7789 Touchscreen (8MB Flash)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html", + "vendor": "Espressif" + } diff --git a/boards/elecrow_advance_s3/elecrow_advance_s3.ini b/boards/elecrow_advance_s3/elecrow_advance_s3.ini new file mode 100644 index 000000000..47f6b82e7 --- /dev/null +++ b/boards/elecrow_advance_s3/elecrow_advance_s3.ini @@ -0,0 +1,133 @@ +#################################### CrowPanel Advance 3.5" ESP32-S3 ############################## +; Elecrow CrowPanel Advance 3.5" HMI (https://www.elecrow.com/crowpanel-advance-3-5-hmi-esp32-ai-display) +; Hardware: ESP32-S3-WROOM-1-N16R8 (16MB Flash, 8MB OPI PSRAM) +; ILI9488 480x320 IPS over SPI, GT911 capacitive touch over I2C +; TFT SPI: MOSI=39, MISO=-1, SCLK=42, CS=40, DC=41, BL=38 (PWM) +; Touch GT911: I2C SDA=15, SCL=16, INT=47, addr 0x5D +; SD Card SPI: MOSI=6, MISO=4, SCK=5, CS=7 +; Speaker I2S: BCLK=13, WCLK=11, DOUT=12 +; Microphone I2S: CLK=9, WS=3, DATA=10 +; Note: Serial routed to UART0 (CH341 bridge) via ARDUINO_USB_CDC_ON_BOOT=0. + +[env:elecrow-advance-35-s3] +board = esp32-s3-devkitc1-n16r8 +board_build.arduino.memory_type = qio_opi +board_build.f_flash = 80000000L +board_build.flash_mode = qio +board_build.partitions = custom_16Mb.csv +build_src_filter = ${env.build_src_filter} +<../boards/elecrow_advance_s3> +build_unflags = + -DARDUINO_USB_CDC_ON_BOOT=1 +build_flags = + ${env.build_flags} + -Iboards/elecrow_advance_s3 + -Os + -DARDUINO_USB_CDC_ON_BOOT=0 + -DBOARD_HAS_PSRAM + -DDISABLE_ALL_LIBRARY_WARNINGS + + -DELECROW + + ; Speaker I2S + -DBCLK=13 + -DWCLK=11 + -DDOUT=12 + + ; Microphone I2S + -DPIN_CLK=9 + -DI2S_SCLK_PIN=9 + -DPIN_DATA=10 + -DI2S_DATA_PIN=10 + + ; No RGB LED + -DRGB_LED=-1 + + ; BadUSB on UART pins + -DBAD_TX=17 + -DBAD_RX=18 + + ; Serial/GPS + -DSERIAL_TX=43 + -DSERIAL_RX=44 + -DGPS_SERIAL_TX=SERIAL_TX + -DGPS_SERIAL_RX=SERIAL_RX + + ; Buttons - touch only + -DHAS_BTN=0 + -DBTN_ALIAS='"Ok"' + -DBTN_PIN=0 + + ; IR/RF free pins + -DIR_TX_PINS='{{"Pin 17", 17}, {"Pin 18", 18}}' + -DIR_RX_PINS='{{"Pin 17", 17}, {"Pin 18", 18}}' + -DTXLED=-1 + -DLED_ON=HIGH + -DLED_OFF=LOW + + -DRF_TX_PINS='{{"Pin 17", 17}, {"Pin 18", 18}}' + -DRF_RX_PINS='{{"Pin 17", 17}, {"Pin 18", 18}}' + + ; Font sizes + -DFP=1 + -DFM=2 + -DFG=3 + + ; Screen + -DHAS_SCREEN=1 + -DROTATION=1 + -DBACKLIGHT=38 + -DMINBRIGHT=10 + + ; TFT_eSPI - SPI display (ILI9488, 480x320) + ; USE_HSPI_PORT is required on ESP32-S3 or the SPI register pointers stay + ; null and the first write in begin_tft_write() panics (StoreProhibited). + -DUSER_SETUP_LOADED=1 + -DUSE_HSPI_PORT=1 + -DILI9488_DRIVER=1 + -DTFT_WIDTH=320 + -DTFT_HEIGHT=480 + -DTFT_MOSI=39 + -DTFT_MISO=-1 + -DTFT_SCLK=42 + -DTFT_CS=40 + -DTFT_DC=41 + -DTFT_RST=-1 + -DTFT_BL=38 + -DTFT_BACKLIGHT_ON=HIGH + -DSMOOTH_FONT=1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=16000000 + + ; Touch GT911 capacitive I2C + -DHAS_TOUCH=1 + -DHAS_CAPACITIVE_TOUCH=1 + -DTOUCH_GT911_I2C=1 + -DGT911_SLAVE_ADDRESS_L=0x5D + -DGT911_I2C_CONFIG_SDA_IO_NUM=15 + -DGT911_I2C_CONFIG_SCL_IO_NUM=16 + -DGT911_TOUCH_CONFIG_INT_GPIO_NUM=47 + -DBOARD_TOUCH_INT=47 + + ; SD Card SPI + -DSDCARD_CS=7 + -DSDCARD_SCK=5 + -DSDCARD_MISO=4 + -DSDCARD_MOSI=6 + + ; SPI bus pin aliases (required by utils.cpp) + -DSPI_SCK_PIN=5 + -DSPI_MOSI_PIN=6 + -DSPI_MISO_PIN=4 + -DSPI_SS_PIN=7 + + ; Backlight PWM + -DTFT_BRIGHT_CHANNEL=0 + -DTFT_BRIGHT_Bits=8 + -DTFT_BRIGHT_FREQ=5000 + + -DDEVICE_NAME='"Elecrow Advance 3.5 S3"' + +lib_deps = + ${env.lib_deps} + lewisxhe/SensorLib @ 0.3.4 +################################## END CrowPanel Advance 3.5" ##################################### diff --git a/boards/elecrow_advance_s3/interface.cpp b/boards/elecrow_advance_s3/interface.cpp new file mode 100644 index 000000000..c6c5525d7 --- /dev/null +++ b/boards/elecrow_advance_s3/interface.cpp @@ -0,0 +1,162 @@ +#include "core/powerSave.h" +#include "core/utils.h" +#include +#include +#include + +// ============================================================================= +// CrowPanel Advance 3.5" (ESP32-S3) interface +// - Display: ILI9488 over SPI (handled by TFT_eSPI) +// - Touch: GT911 capacitive, directly on I2C (SDA=15, SCL=16, INT=47), +// no IO expander, no dedicated RST line. +// - Backlight: direct PWM on GPIO38. +// ============================================================================= + +#if defined(HAS_CAPACITIVE_TOUCH) && defined(TOUCH_GT911_I2C) +#include "TouchDrvGT911.hpp" +TouchDrvGT911 touch; +struct TouchPointPro { + int16_t x = 0; + int16_t y = 0; +}; +#endif + +/*************************************************************************************** +** Function name: _setup_gpio() +***************************************************************************************/ +void _setup_gpio() { + bruceConfig.colorInverted = 0; + +#if defined(HAS_CAPACITIVE_TOUCH) && defined(TOUCH_GT911_I2C) + // Bring up the I2C bus the GT911 lives on + Wire.begin(GT911_I2C_CONFIG_SDA_IO_NUM, GT911_I2C_CONFIG_SCL_IO_NUM); + + // GT911 power-on reset sequence. No RST line on this board, so hold INT low + // briefly to keep address 0x5D, then release it as an input. + pinMode(BOARD_TOUCH_INT, OUTPUT); + digitalWrite(BOARD_TOUCH_INT, LOW); + delay(10); + pinMode(BOARD_TOUCH_INT, INPUT); + delay(50); + + // No reset line available -> pass -1 for RST. + touch.setPins(-1, BOARD_TOUCH_INT); + if (!touch.begin( + Wire, GT911_SLAVE_ADDRESS_L, GT911_I2C_CONFIG_SDA_IO_NUM, GT911_I2C_CONFIG_SCL_IO_NUM + )) { + Serial.println("Failed to find GT911 touch - check wiring!"); + } else { + Serial.println("GT911 touch started"); + } +#endif +} + +/*************************************************************************************** +** Function name: _post_setup_gpio() +***************************************************************************************/ +void _post_setup_gpio() { + pinMode(TFT_BL, OUTPUT); + ledcAttach(TFT_BL, TFT_BRIGHT_FREQ, TFT_BRIGHT_Bits); + ledcWrite(TFT_BL, 255); +} + +/*************************************************************************************** +** Function name: getBattery() +***************************************************************************************/ +int getBattery() { return 100; } + +/********************************************************************* +** Function: _setBrightness +**********************************************************************/ +void _setBrightness(uint8_t brightval) { + int dutyCycle; + if (brightval == 100) dutyCycle = 255; + else if (brightval == 75) dutyCycle = 130; + else if (brightval == 50) dutyCycle = 70; + else if (brightval == 25) dutyCycle = 20; + else if (brightval == 0) dutyCycle = 0; + else dutyCycle = ((brightval * 255) / 100); + ledcWrite(TFT_BL, dutyCycle); +} + +/********************************************************************* +** Function: InputHandler (GT911 capacitive) +**********************************************************************/ +void InputHandler(void) { +#if defined(HAS_CAPACITIVE_TOUCH) && defined(TOUCH_GT911_I2C) + static long d_tmp = 0; + if (millis() - d_tmp > 200 || LongPress) { + static unsigned long tm = millis(); + TouchPointPro t; + uint8_t touched = 0; + static uint8_t rot = 5; + + if (rot != bruceConfigPins.rotation) { + if (bruceConfigPins.rotation == 1) { + touch.setMaxCoordinates(TFT_HEIGHT, TFT_WIDTH); + touch.setSwapXY(true); + touch.setMirrorXY(false, true); + } + if (bruceConfigPins.rotation == 3) { + touch.setMaxCoordinates(TFT_HEIGHT, TFT_WIDTH); + touch.setSwapXY(true); + touch.setMirrorXY(true, false); + } + if (bruceConfigPins.rotation == 0) { + touch.setMaxCoordinates(TFT_WIDTH, TFT_HEIGHT); + touch.setSwapXY(false); + touch.setMirrorXY(false, false); + } + if (bruceConfigPins.rotation == 2) { + touch.setMaxCoordinates(TFT_WIDTH, TFT_HEIGHT); + touch.setSwapXY(false); + touch.setMirrorXY(true, true); + } + rot = bruceConfigPins.rotation; + } + + static bool lastTouchState = false; + static unsigned long lastTouchTime = 0; + + touched = touch.getPoint(&t.x, &t.y); + bool currentTouchState = touched > 0; + + if (currentTouchState && !lastTouchState && (millis() - lastTouchTime) > 100) { + lastTouchTime = millis(); + } else if (!currentTouchState || lastTouchState) { + touched = 0; + } + lastTouchState = currentTouchState; + + if (((millis() - tm) > 190 || LongPress) && touched) { + tm = millis(); + if (!wakeUpScreen()) AnyKeyPress = true; + else goto END; + + touchPoint.x = t.x; + touchPoint.y = t.y; + touchPoint.pressed = true; + touchHeatMap(touchPoint); + END: + d_tmp = millis(); + } + } +#else + checkPowerSaveTime(); + PrevPress = false; + NextPress = false; + SelPress = false; + AnyKeyPress = false; + EscPress = false; +#endif +} + +/********************************************************************* +** Function: powerOff +**********************************************************************/ +void powerOff() {} + +/********************************************************************* +** Function: checkReboot +**********************************************************************/ +void checkReboot() {} diff --git a/boards/elecrow_advance_s3/pins_arduino.h b/boards/elecrow_advance_s3/pins_arduino.h new file mode 100644 index 000000000..bb6e7462a --- /dev/null +++ b/boards/elecrow_advance_s3/pins_arduino.h @@ -0,0 +1,32 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#ifndef DEVICE_NAME +#define DEVICE_NAME "Elecrow Advance 3.5 S3" +#endif + +// ============================================================================= +// Elecrow CrowPanel Advance 3.5" HMI +// ESP32-S3-WROOM-1-N16R8, ILI9488 480x320 IPS over SPI (TFT_eSPI backend), +// GT911 capacitive touch over I2C. Concrete pin assignments live in the build +// flags (boards/elecrow_advance_s3/elecrow_advance_s3.ini); this header only +// provides the standard Arduino aliases the core/libraries expect. +// ============================================================================= + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// GT911 capacitive touch I2C bus +static const uint8_t SDA = 15; +static const uint8_t SCL = 16; + +// Display / SD share SPI signals; chip-selects differ (see board .ini) +static const uint8_t SS = 40; // TFT_CS +static const uint8_t MOSI = 39; // TFT_MOSI +static const uint8_t MISO = 4; // SD_MISO (TFT MISO is unused, -1) +static const uint8_t SCK = 42; // TFT_SCLK + +#endif /* Pins_Arduino_h */ diff --git a/boards/esp-wroom32-ili9341/connections.md b/boards/esp-wroom32-ili9341/connections.md new file mode 100644 index 000000000..cfec32fa6 --- /dev/null +++ b/boards/esp-wroom32-ili9341/connections.md @@ -0,0 +1,90 @@ +# ESP-WROOM32 with ILI9341 Display Wiring Guide + +This board features an ESP32 microcontroller paired with a 2.4" ILI9341 display and supports 5-way button navigation. + +## Components + +- **MCU**: ESP-WROOM32 (ESP32) +- **Display**: ILI9341 2.4" SPI TFT LCD (240x320) +- **buttons**: 5-way tactile button pad (up/down/left/right/select) +- **LED**: Single status LED +- **Communication**: SPI for display/radio modules, I2C for sensors + +## Display Pinout (ILI9341) + +| Display Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| CS | 17 | Chip Select | +| RESET | 5 | Reset | +| DC | 16 | Data/Command | +| MOSI | 23 | Serial Data In | +| MISO | 19 | Serial Data Out | +| SCK | 18 | Clock | +| BL/LED | 32 | Backlight (PWM) | + +## Button Pinout (5-Way Tactile) + +| Button | ESP32 Pin | Function | +|---|---|---| +| Select/OK | 35 | Center button | +| Up | 34 | Navigation up | +| Down | 26 | Navigation down | +| Right | 27 | Navigation right | +| Left | 33 | Navigation left | + +All buttons are **active-LOW** (GND when pressed). + +## Radio Modules (CC1101/NRF24) - Shared SPI Bus + +| Radio Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 23 | Serial Data In (shared with display) | +| MISO | 19 | Serial Data Out (shared with display) | +| SCK | 18 | Clock (shared with display) | +| CS | 15 | Chip Select (software selectable) | +| *Specific to RF module* | | See module documentation | + +**Note**: Radio modules share the SPI bus with the display but have separate chip select (CS) lines. The firmware manages CS timing to prevent conflicts. + +## I2C Sensors (Grove/Standard) + +| Sensor Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| SCL | 22 | I2C Clock | +| SDA | 21 | I2C Data | + +## Status LED + +| LED Pin | ESP32 Pin | Purpose | +|---|---|---| +| Anode | 2 | Active HIGH output | +| Cathode | GND | Ground | + +## Serial Port + +| Pin | ESP32 Pin | Type | +|---|---|---| +| RX | 3 | UART0 Input (programming/debug) | +| TX | 1 | UART0 Output (programming/debug) | + +## Power Supply + +- **Logic voltage**: 3.3V +- **Supply current**: ~500mA typical (display + MCU) +- **Peak current**: ~800mA (during radio transmission) + +Use a quality USB power supply or regulated 3.3V source rated for at least 1A. + +## Wiring Notes + +1. **SPI Bus Sharing**: Display, radio, and SD card (if used) all share SCK/MOSI/MISO lines. Each device has its own CS line to prevent conflicts. +2. **Button Ground**: All 5 buttons should be pulled to GND when pressed. Use 10kΩ pull-up resistors on pins if needed. +3. **Backlight**: The BL pin (GPIO32) supports PWM for brightness control (0-255). +4. **Reset**: The display RESET (GPIO5) is active LOW. Normally tied HIGH through 10kΩ resistor. +5. **Debouncing**: The firmware handles software debouncing for buttons (typical bounce time ~20ms). diff --git a/boards/esp-wroom32-ili9341/diagrams/espwroom32_ili9341_wiring.png b/boards/esp-wroom32-ili9341/diagrams/espwroom32_ili9341_wiring.png new file mode 100644 index 000000000..30b50a554 Binary files /dev/null and b/boards/esp-wroom32-ili9341/diagrams/espwroom32_ili9341_wiring.png differ diff --git a/boards/esp-wroom32-ili9341/esp-wroom32-ili9341.ini b/boards/esp-wroom32-ili9341/esp-wroom32-ili9341.ini new file mode 100644 index 000000000..bf4f52f8f --- /dev/null +++ b/boards/esp-wroom32-ili9341/esp-wroom32-ili9341.ini @@ -0,0 +1,58 @@ +; PlatformIO Project Configuration File +; ESP-WROOM-32 + ILI9341 2.4" TFT (no touch) + 5-way tactile switch +; +; TFT Pins: MISO=19, MOSI=23, SCLK=18, CS=17, DC=16, RST=5, BL=32 +; 5-Way Nav: Up=34, Down=26, Left=33, Right=27, Center=35 + +[env:esp-wroom32-ili9341] +board = esp-wroom32-ili9341 +monitor_speed = 115200 +upload_port = COM3 +upload_speed = 115200 +upload_flags = --no-stub +board_build.partitions = custom_4Mb_full.csv +build_src_filter = ${env.build_src_filter} +<../boards/esp-wroom32-ili9341> +build_flags = + ${env.build_flags} + -Iboards/esp-wroom32-ili9341 + -Os + -DCORE_DEBUG_LEVEL=0 + + -DDEVICE_NAME='"ESP32-ILI9341"' + + ; Microphone (disabled) + -DPIN_CLK=-1 + -DI2S_SCLK_PIN=-1 + -DI2S_DATA_PIN=-1 + -DPIN_DATA=-1 + + ; Speaker (disabled) + -DBCLK=-1 + -DWCLK=-1 + -DDOUT=-1 + + ; RGB LED (disabled) + -DRGB_LED=-1 + + ; CC1101 SPI + -DUSE_CC1101_VIA_SPI + -DCC1101_GDO0_PIN=2 + -DCC1101_SS_PIN=15 + -DCC1101_MOSI_PIN=SPI_MOSI_PIN + -DCC1101_SCK_PIN=SPI_SCK_PIN + -DCC1101_MISO_PIN=SPI_MISO_PIN + + ; NRF24 SPI + -DUSE_NRF24_VIA_SPI + -DNRF24_CE_PIN=4 + -DNRF24_SS_PIN=15 + -DNRF24_MOSI_PIN=SPI_MOSI_PIN + -DNRF24_SCK_PIN=SPI_SCK_PIN + -DNRF24_MISO_PIN=SPI_MISO_PIN + +lib_deps = + ${env.lib_deps} +lib_ignore = + FastLED + ESP8266Audio + ESP8266SAM diff --git a/boards/esp-wroom32-ili9341/interface.cpp b/boards/esp-wroom32-ili9341/interface.cpp new file mode 100644 index 000000000..beb5d9812 --- /dev/null +++ b/boards/esp-wroom32-ili9341/interface.cpp @@ -0,0 +1,174 @@ +#include "core/powerSave.h" +#include +#include + +// Deselect NRF24/CC1101 before any Arduino/TFT init runs (runs before setup()) +static void __attribute__((constructor)) _early_spi_deselect() { + gpio_reset_pin(GPIO_NUM_15); // CC1101_SS / NRF24_SS + gpio_set_direction(GPIO_NUM_15, GPIO_MODE_OUTPUT); + gpio_set_level(GPIO_NUM_15, 1); // CS HIGH = deselected + + gpio_reset_pin(GPIO_NUM_4); // NRF24_CE + gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT); + gpio_set_level(GPIO_NUM_4, 0); // CE LOW = standby +} + +/*************************************************************************************** +** Function name: _setup_gpio() +** Location: main.cpp +** Description: initial setup for the device +***************************************************************************************/ +void _setup_gpio() { + // 5-way tactile switch — GPIO 34/35 are input-only (no pull-up on chip) + // Ensure external pull-up resistors are present on these pins + pinMode(UP_BTN, INPUT); + pinMode(DW_BTN, INPUT_PULLUP); + pinMode(L_BTN, INPUT_PULLUP); + pinMode(R_BTN, INPUT_PULLUP); + pinMode(SEL_BTN, INPUT); + + // Deselect CC1101 and NRF24 on shared SPI bus so they don't interfere with TFT + pinMode(CC1101_SS_PIN, OUTPUT); + digitalWrite(CC1101_SS_PIN, HIGH); + pinMode(NRF24_SS_PIN, OUTPUT); + digitalWrite(NRF24_SS_PIN, HIGH); + pinMode(NRF24_CE_PIN, OUTPUT); + digitalWrite(NRF24_CE_PIN, LOW); +} + +/*************************************************************************************** +** Function name: _post_setup_gpio() +** Location: main.cpp +** Description: second stage gpio setup +***************************************************************************************/ +void _post_setup_gpio() { + // Backlight PWM + pinMode(TFT_BL, OUTPUT); + analogWrite(TFT_BL, 255); +} + +/********************************************************************* +** Function: setBrightness +** location: settings.cpp +** set brightness value +**********************************************************************/ +void _setBrightness(uint8_t brightval) { + if (brightval == 0) { + analogWrite(TFT_BL, 0); + } else { + int bl = MINBRIGHT + round(((255 - MINBRIGHT) * brightval / 100)); + analogWrite(TFT_BL, bl); + } +} + +/********************************************************************* +** Function: InputHandler +** Handles the variables PrevPress, NextPress, SelPress, AnyKeyPress and EscPress +** On 5-way joystick: hold UP for ~400ms = EscPress (back/exit) +**********************************************************************/ +void InputHandler(void) { + static unsigned long tm = 0; + static unsigned long upHoldStart = 0; + static bool upEscFired = false; + + // --- Hold-UP-for-Escape: runs every call, bypasses debounce --- + // Read UP_BTN on every task cycle (~10ms) so the hold timer + // accumulates correctly regardless of the 200ms debounce gate. + // Keep setting EscPress while held so the task can't clear it + // before the application loop reads it via check(). + bool _up_raw = (digitalRead(UP_BTN) == BTN_ACT); + if (_up_raw) { + if (upHoldStart == 0) upHoldStart = millis(); + if (millis() - upHoldStart >= 400) { + EscPress = true; + AnyKeyPress = true; + upEscFired = true; + } + } else { + upHoldStart = 0; + upEscFired = false; + } + + if (millis() - tm < 200 && !LongPress) return; + + bool _u = _up_raw; + bool _d = (digitalRead(DW_BTN) == BTN_ACT); + bool _l = (digitalRead(L_BTN) == BTN_ACT); + bool _r = (digitalRead(R_BTN) == BTN_ACT); + bool _s = (digitalRead(SEL_BTN) == BTN_ACT); + + if (_u || _d || _l || _r || _s) { + tm = millis(); + if (!wakeUpScreen()) AnyKeyPress = true; + else return; + } + + if (_l) { PrevPress = true; } + if (_r) { NextPress = true; } + if (_u && !upEscFired) { + UpPress = true; + PrevPagePress = true; + } + if (_d) { + DownPress = true; + NextPagePress = true; + } + if (_s) { SelPress = true; } + if (_l && _r) { + EscPress = true; + NextPress = false; + PrevPress = false; + } +} + +/********************************************************************* +** Function: powerOff +** location: mykeyboard.cpp +** Turns off the device (or try to) +**********************************************************************/ +void powerOff() { + esp_sleep_enable_ext0_wakeup((gpio_num_t)SEL_BTN, BTN_ACT); + esp_deep_sleep_start(); +} + +/********************************************************************* +** Function: checkReboot +** location: mykeyboard.cpp +** Btn logic to turn off the device +**********************************************************************/ +void checkReboot() { + int countDown = 0; + /* Long press Left+Right to power off */ + if (digitalRead(L_BTN) == BTN_ACT && digitalRead(R_BTN) == BTN_ACT) { + uint32_t time_count = millis(); + while (digitalRead(L_BTN) == BTN_ACT && digitalRead(R_BTN) == BTN_ACT) { + if (millis() - time_count > 500) { + if (countDown == 0) { + int textWidth = tft.textWidth("PWR OFF IN 3/3", 1); + tft.fillRect(tftWidth / 2 - textWidth / 2, 7, textWidth, 18, bruceConfig.bgColor); + } + tft.setTextSize(1); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + countDown = (millis() - time_count) / 1000 + 1; + if (countDown < 4) + tft.drawCentreString("PWR OFF IN " + String(countDown) + "/3", tftWidth / 2, 12, 1); + else { + tft.fillScreen(bruceConfig.bgColor); + while (digitalRead(L_BTN) == BTN_ACT || digitalRead(R_BTN) == BTN_ACT); + delay(200); + powerOff(); + } + delay(10); + } + } + delay(30); + if (millis() - time_count > 500) { + tft.fillRect(60, 12, tftWidth - 60, tft.fontHeight(1), bruceConfig.bgColor); + drawStatusBar(); + } + } +} + +int getBattery() { return 0; } + +bool isCharging() { return false; } diff --git a/boards/esp-wroom32-ili9341/pins_arduino.h b/boards/esp-wroom32-ili9341/pins_arduino.h new file mode 100644 index 000000000..503c2a417 --- /dev/null +++ b/boards/esp-wroom32-ili9341/pins_arduino.h @@ -0,0 +1,81 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 22; + +static const uint8_t SS = 5; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 19; +static const uint8_t SCK = 18; + +// 5-way tactile switch +#define HAS_5_BUTTONS +#define SEL_BTN 35 +#define UP_BTN 34 +#define DW_BTN 26 +#define R_BTN 27 +#define L_BTN 33 +#define BTN_ACT LOW + +#define BTN_ALIAS "\"OK\"" + +#define SERIAL_TX 1 +#define SERIAL_RX 3 +#define BAD_TX SERIAL_TX +#define BAD_RX SERIAL_RX +#define GPS_SERIAL_TX SERIAL_TX +#define GPS_SERIAL_RX SERIAL_RX + +#define TXLED 2 +#define LED_ON HIGH +#define LED_OFF LOW + +#define FP 1 +#define FM 2 +#define FG 3 + +#define HAS_SCREEN 1 +#define ROTATION 1 +#define MINBRIGHT 160 + +// TFT_eSPI Setup for ILI9341 2.4" display +#define USER_SETUP_LOADED 1 +#define ILI9341_2_DRIVER 1 +#define TFT_WIDTH 240 +#define TFT_HEIGHT 320 +#define TFT_MISO 19 +#define TFT_MOSI 23 +#define TFT_SCLK 18 +#define TFT_CS 17 +#define TFT_DC 16 +#define TFT_RST 5 +#define TFT_BL 32 +#define TFT_BACKLIGHT_ON HIGH +#define TOUCH_CS -1 +#define SMOOTH_FONT 1 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 20000000 + +// SD Card (directly on main SPI bus) +#define SDCARD_CS -1 +#define SDCARD_SCK -1 +#define SDCARD_MISO -1 +#define SDCARD_MOSI -1 + +// Default I2C +#define GROVE_SDA 21 +#define GROVE_SCL 22 + +// Shared SPI bus for CC1101/NRF24 (main VSPI) +#define SPI_SCK_PIN 18 +#define SPI_MOSI_PIN 23 +#define SPI_MISO_PIN 19 +#define SPI_SS_PIN 15 + +#endif /* Pins_Arduino_h */ diff --git a/boards/esp-wroom32-st7789/connections.md b/boards/esp-wroom32-st7789/connections.md new file mode 100644 index 000000000..afe6fb6a6 --- /dev/null +++ b/boards/esp-wroom32-st7789/connections.md @@ -0,0 +1,125 @@ +# ESP-WROOM32 with ST7789 Display & Touchscreen Wiring Guide + +This board features an ESP32 microcontroller paired with a 2.8" ST7789 display with integrated XPT2046 resistive touchscreen. + +## Components + +- **MCU**: ESP-WROOM32 (ESP32) +- **Display**: ST7789 2.8" SPI TFT LCD with RGB-BGR color order (240x320) +- **Touchscreen**: XPT2046 resistive touch controller (shares SPI bus) +- **SD Card**: Slot on main SPI bus +- **Button**: Single boot button (GPIO0; active-LOW) +- **LED**: Single status LED +- **Communication**: SPI for display/touch/SD card, I2C for sensors + +## Display Pinout (ST7789) + +| Display Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| CS | 17 | Chip Select | +| RESET | 5 | Reset | +| DC | 16 | Data/Command | +| MOSI | 23 | Serial Data In | +| MISO | 19 | Serial Data Out (not typically used) | +| SCK | 18 | Clock | +| BL/LED | 32 | Backlight (PWM) | + +## Touchscreen Pinout (XPT2046) + +The touchscreen shares the SPI bus (MOSI/MISO/SCK) with the display but has separate CS. + +| Touch Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 23 | Serial Data In (shared with display) | +| MISO | 19 | Serial Data Out (shared with display) | +| SCK | 18 | Clock (shared with display) | +| CS | 21 | Chip Select (touch only) | +| IRQ | Not used | Interrupt (optional) | + +## SD Card Slot Pinout + +| SD Card Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 23 | Serial Data In (shared with display/touch) | +| MISO | 19 | Serial Data Out (shared) | +| SCK | 18 | Clock (shared) | +| CS | 12 | Chip Select (SD card only) | + +## Radio Modules (CC1101/NRF24) - Shared SPI Bus + +| Radio Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 23 | Serial Data In (shared) | +| MISO | 19 | Serial Data Out (shared) | +| SCK | 18 | Clock (shared) | +| CS | 15 | Chip Select (radio only) | +| *Specific to RF module* | | See module documentation | + +**Note**: Display, touchscreen, SD card, and radio modules all share MOSI/MISO/SCK but each has its own CS line for isolation. + +## User Input + +| Input | ESP32 Pin | Type | +|---|---|---| +| Boot Button | 0 | Tactile switch, active-LOW (can be remapped as OK button) | + +## I2C Sensors (Grove/Standard) + +| Sensor Pin | ESP32 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| SCL | 22 | I2C Clock | +| SDA | 21 | I2C Data (warning: shared with touch CS—check firmware) | + +## Status LED + +| LED Pin | ESP32 Pin | Purpose | +|---|---|---| +| Anode | 2 | Active HIGH output | +| Cathode | GND | Ground | + +## Serial Port + +| Pin | ESP32 Pin | Type | +|---|---|---| +| RX | 3 | UART0 Input (programming/debug) | +| TX | 1 | UART0 Output (programming/debug) | + +## Power Supply + +- **Logic voltage**: 3.3V +- **Supply current**: ~600mA typical (display + touch + MCU) +- **Peak current**: ~1000mA (during radio transmission + backlight at full brightness) + +Use a quality USB power supply or regulated 3.3V source rated for at least 1.5A. + +## SPI Bus Architecture + +``` +ESP32 (VSPI Bus) +├─ Display (CS=17) — ST7789 + BL +├─ Touchscreen (CS=21) — XPT2046 +├─ SD Card (CS=12) — microSD +└─ Radio (CS=15) — NRF24 or CC1101 + +Shared lines: MOSI=23, MISO=19, SCK=18 +``` + +## Wiring Notes + +1. **Multi-Device SPI**: All four devices (display, touch, SD, radio) share the same MOSI/MISO/SCK lines. Each device gets its own CS line. The firmware manages CS timing automatically. +2. **Touch CS Conflict**: GPIO21 is used for both touch CS and Grove SDA (I2C). If using Grove I2C sensors, disable touch or use alternate GPIO. +3. **Backlight**: The BL pin (GPIO32) supports PWM for brightness control (0-255). +4. **Reset**: Display RESET (GPIO5) is active LOW. Normally tied HIGH through 10kΩ resistor or managed by firmware. +5. **SPI Frequency**: Write at 40MHz, read at 20MHz. Touch operations at 2.5MHz. +6. **Display Rotation**: Set to 90° rotation (landscape). +7. **Color Order**: ST7789 configured for RGB→BGR conversion. diff --git a/boards/esp-wroom32-st7789/diagrams/espwroom32_st7789_wiring.png b/boards/esp-wroom32-st7789/diagrams/espwroom32_st7789_wiring.png new file mode 100644 index 000000000..de2598769 Binary files /dev/null and b/boards/esp-wroom32-st7789/diagrams/espwroom32_st7789_wiring.png differ diff --git a/boards/esp-wroom32-st7789/esp-wroom32-st7789.ini b/boards/esp-wroom32-st7789/esp-wroom32-st7789.ini new file mode 100644 index 000000000..055b94e50 --- /dev/null +++ b/boards/esp-wroom32-st7789/esp-wroom32-st7789.ini @@ -0,0 +1,136 @@ +; PlatformIO Project Configuration File +; ESP-WROOM-32 + ST7789 2.8" TFT with XPT2046 Touch +; +; TFT Pins (VSPI): MISO=19, MOSI=23, SCLK=18, CS=17, DC=16, RST=5, BL=32 +; Touch (XPT2046): CS=21, shares VSPI bus (MISO=19, MOSI=23, SCLK=18) +; SD Card: CS=12, shares VSPI bus + +[env:esp-wroom32-st7789] +board = esp-wroom32-st7789 +board_build.partitions = custom_4Mb_full.csv +build_src_filter = ${env.build_src_filter} +<../boards/esp-wroom32-st7789> +build_flags = + ${env.build_flags} + -Iboards/esp-wroom32-st7789 + -Os + -DCORE_DEBUG_LEVEL=0 + -DCONFIG_ESP32_JTAG_SUPPORT_DISABLE=1 + + -DDEVICE_NAME='"ESP32-ST7789 Touch"' + + ; Boot button as OK + -DHAS_BTN=1 + -DBTN_ALIAS='"OK"' + -DBTN_PIN=0 + -DBTN_ACT=LOW + + ; Infrared Led + -DIR_TX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + -DIR_RX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + -DTXLED=2 + -DLED_ON=HIGH + -DLED_OFF=LOW + + ; Radio Frequency (one pin modules) pin setting + -DRF_TX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + -DRF_RX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + + ; CC1101 SPI connection pins + -DUSE_CC1101_VIA_SPI + -DCC1101_GDO0_PIN=2 + -DCC1101_SS_PIN=15 + -DCC1101_MOSI_PIN=SPI_MOSI_PIN + -DCC1101_SCK_PIN=SPI_SCK_PIN + -DCC1101_MISO_PIN=SPI_MISO_PIN + + ; NRF24 SPI connection pins + -DUSE_NRF24_VIA_SPI + -DNRF24_CE_PIN=4 + -DNRF24_SS_PIN=15 + -DNRF24_MOSI_PIN=SPI_MOSI_PIN + -DNRF24_SCK_PIN=SPI_SCK_PIN + -DNRF24_MISO_PIN=SPI_MISO_PIN + + ; W5500 Ethernet (disabled by default) + ;-DUSE_W5500_VIA_SPI + -DW5500_SS_PIN=-1 + -DW5500_MOSI_PIN=SPI_MOSI_PIN + -DW5500_SCK_PIN=SPI_SCK_PIN + -DW5500_MISO_PIN=SPI_MISO_PIN + -DW5500_INT_PIN=-1 + + ; Lora setup pins (not connected by default) + -DLORA_SCK=SPI_SCK_PIN + -DLORA_MISO=SPI_MISO_PIN + -DLORA_MOSI=SPI_MOSI_PIN + -DLORA_CS=-1 + -DLORA_RST=-1 + -DLORA_DIO0=-1 + + ; FM Radio + -DFM_RSTPIN=-1 + + ; Font sizes + -DFP=1 + -DFM=2 + -DFG=3 + + ; Screen Setup + -DHAS_SCREEN=1 + -DROTATION=1 + -DMINBRIGHT=1 + + ; TFT_eSPI display — ST7789 240x320 SPI (VSPI) + -DUSER_SETUP_LOADED=1 + -DST7789_DRIVER=1 + -DTFT_RGB_ORDER=TFT_BGR + -DTFT_WIDTH=240 + -DTFT_HEIGHT=320 + -DTFT_INVERSION_ON + -DTFT_CS=17 + -DTFT_DC=16 + -DTFT_RST=5 + -DTFT_BL=32 + -DTFT_MOSI=23 + -DTFT_SCLK=18 + -DTFT_MISO=19 + -DTOUCH_CS=-1 + -DSMOOTH_FONT=1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=20000000 + -DSPI_TOUCH_FREQUENCY=2500000 + + ; Touchscreen — XPT2046 resistive (shares VSPI bus with display) + -DHAS_TOUCH=1 + -DTOUCH_XPT2046_SPI=1 + -DXPT2046_SPI_BUS_MOSI_IO_NUM=23 + -DXPT2046_SPI_BUS_MISO_IO_NUM=19 + -DXPT2046_SPI_BUS_SCLK_IO_NUM=18 + -DXPT2046_SPI_CONFIG_CS_GPIO_NUM=21 + -DXPT2046_TOUCH_CONFIG_INT_GPIO_NUM=-1 + -DINPUT_HANDLER_TASK_STACK_SIZE=8192 + + ; SD Card pins (on VSPI bus) + -DSDCARD_CS=12 + -DSDCARD_SCK=18 + -DSDCARD_MISO=19 + -DSDCARD_MOSI=23 + + ; Default I2C port + -DGROVE_SDA=21 + -DGROVE_SCL=22 + + ; SERIAL (GPS) dedicated pins + -DSERIAL_TX=1 + -DSERIAL_RX=3 + -DGPS_SERIAL_TX=SERIAL_TX + -DGPS_SERIAL_RX=SERIAL_RX + + ; Main SPI bus pins (for CC1101/NRF24/etc) + -DSPI_SCK_PIN=18 + -DSPI_MOSI_PIN=23 + -DSPI_MISO_PIN=19 + -DSPI_SS_PIN=15 + +lib_deps = + ${env.lib_deps} diff --git a/boards/esp-wroom32-st7789/interface.cpp b/boards/esp-wroom32-st7789/interface.cpp new file mode 100644 index 000000000..97493b216 --- /dev/null +++ b/boards/esp-wroom32-st7789/interface.cpp @@ -0,0 +1,136 @@ +#include "CYD28_TouchscreenR.h" +#include "core/powerSave.h" +#include "core/utils.h" +#include + +#define CYD28_DISPLAY_HOR_RES_MAX 320 +#define CYD28_DISPLAY_VER_RES_MAX 240 +CYD28_TouchR touch(CYD28_DISPLAY_HOR_RES_MAX, CYD28_DISPLAY_VER_RES_MAX); + +/*************************************************************************************** +** Function name: _setup_gpio() +** Location: main.cpp +** Description: initial setup for the device +***************************************************************************************/ +void _setup_gpio() { + // Keep XPT2046 CS high until needed + pinMode(XPT2046_SPI_CONFIG_CS_GPIO_NUM, OUTPUT); + digitalWrite(XPT2046_SPI_CONFIG_CS_GPIO_NUM, HIGH); + + bruceConfig.colorInverted = 1; +} + +/*************************************************************************************** +** Function name: _post_setup_gpio() +** Location: main.cpp +** Description: second stage gpio setup — runs AFTER tft.init() +***************************************************************************************/ +void _post_setup_gpio() { + // Initialize XPT2046 touch using hardware SPI (shares bus with display) + if (!touch.begin(&tft.getSPIinstance())) { + Serial.println("[TOUCH] XPT2046 not started"); + } else { + Serial.println("[TOUCH] XPT2046 started OK"); + } + touch.setRotation(ROTATION); + + // Backlight on + pinMode(TFT_BL, OUTPUT); + analogWrite(TFT_BL, 255); +} + +/*************************************************************************************** +** Function name: getBattery() +** location: display.cpp +** Description: Delivers the battery value from 1-100 +***************************************************************************************/ +int getBattery() { return 0; } + +/*************************************************************************************** +** Function name: isCharging() +** Description: Determines if the device is charging +***************************************************************************************/ +bool isCharging() { return false; } + +/********************************************************************* +** Function: setBrightness +** location: settings.cpp +** set brightness value +**********************************************************************/ +void _setBrightness(uint8_t brightval) { + if (brightval == 0) { + analogWrite(TFT_BL, brightval); + } else { + analogWrite(TFT_BL, brightval); + } +} + +/********************************************************************* +** Function: InputHandler +** Handles the variables PrevPress, NextPress, SelPress, AnyKeyPress and EscPress +**********************************************************************/ +void InputHandler(void) { + static long d_tmp = 0; + if (millis() - d_tmp > 200 || LongPress) { + if (touch.touched()) { + auto t = touch.getPointScaled(); + + // Coordinate transform based on rotation + if (bruceConfigPins.rotation == 3) { + t.y = (tftHeight + 20) - t.y; + t.x = tftWidth - t.x; + } + if (bruceConfigPins.rotation == 0) { + int tmp = t.x; + t.x = tftWidth - t.y; + t.y = tmp; + } + if (bruceConfigPins.rotation == 2) { + int tmp = t.x; + t.x = t.y; + t.y = (tftHeight + 20) - tmp; + } + + if (!wakeUpScreen()) AnyKeyPress = true; + else goto END; + + touchPoint.x = t.x; + touchPoint.y = t.y; + touchPoint.pressed = true; + touchHeatMap(touchPoint); + END: + d_tmp = millis(); + } + } + +#ifdef HAS_BTN + checkPowerSaveTime(); + if (digitalRead(BTN_PIN) == BTN_ACT) { + if (!wakeUpScreen()) AnyKeyPress = true; + SelPress = true; + long tmp = millis(); + while ((millis() - tmp) < 200 && digitalRead(BTN_PIN) == BTN_ACT); + } +#endif +} + +/********************************************************************* +** Function: powerOff +** location: mykeyboard.cpp +** Turns off the device (or try to) +**********************************************************************/ +void powerOff() { esp_deep_sleep_start(); } + +/********************************************************************* +** Function: goToDeepSleep +** location: mykeyboard.cpp +** Puts the device into DeepSleep +**********************************************************************/ +void goToDeepSleep() { esp_deep_sleep_start(); } + +/********************************************************************* +** Function: checkReboot +** location: mykeyboard.cpp +** Btn logic to turn off the device +**********************************************************************/ +void checkReboot() {} diff --git a/boards/esp-wroom32-st7789/pins_arduino.h b/boards/esp-wroom32-st7789/pins_arduino.h new file mode 100644 index 000000000..43046d197 --- /dev/null +++ b/boards/esp-wroom32-st7789/pins_arduino.h @@ -0,0 +1,128 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 22; + +static const uint8_t SS = 5; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 19; +static const uint8_t SCK = 18; + +// Boot button as OK (active-low) +#ifndef HAS_BTN +#define HAS_BTN 1 +#endif +#ifndef BTN_ALIAS +#define BTN_ALIAS "\"OK\"" +#endif +#ifndef BTN_PIN +#define BTN_PIN 0 +#endif +#ifndef BTN_ACT +#define BTN_ACT LOW +#endif + +#define SERIAL_TX 1 +#define SERIAL_RX 3 +#define BAD_TX SERIAL_TX +#define BAD_RX SERIAL_RX +#define GPS_SERIAL_TX SERIAL_TX +#define GPS_SERIAL_RX SERIAL_RX + +#define TXLED 2 +#define LED_ON HIGH +#define LED_OFF LOW + +#define FP 1 +#define FM 2 +#define FG 3 + +#define HAS_SCREEN 1 +#define ROTATION 1 +#define MINBRIGHT 1 + +// TFT_eSPI Setup for ST7789 2.8" display (VSPI bus) +#ifndef USER_SETUP_LOADED +#define USER_SETUP_LOADED 1 +#endif +#ifndef ST7789_DRIVER +#define ST7789_DRIVER 1 +#endif +#ifndef TFT_RGB_ORDER +#define TFT_RGB_ORDER TFT_BGR +#endif +#ifndef TFT_WIDTH +#define TFT_WIDTH 240 +#endif +#ifndef TFT_HEIGHT +#define TFT_HEIGHT 320 +#endif +#ifndef TFT_INVERSION_ON +#define TFT_INVERSION_ON +#endif +#ifndef TFT_MISO +#define TFT_MISO 19 +#endif +#ifndef TFT_MOSI +#define TFT_MOSI 23 +#endif +#ifndef TFT_SCLK +#define TFT_SCLK 18 +#endif +#ifndef TFT_CS +#define TFT_CS 17 +#endif +#ifndef TFT_DC +#define TFT_DC 16 +#endif +#ifndef TFT_RST +#define TFT_RST 5 +#endif +#ifndef TFT_BL +#define TFT_BL 32 +#endif +#ifndef TFT_BACKLIGHT_ON +#define TFT_BACKLIGHT_ON HIGH +#endif +#ifndef TOUCH_CS +#define TOUCH_CS -1 +#endif +#ifndef SMOOTH_FONT +#define SMOOTH_FONT 1 +#endif +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 20000000 +#define SPI_TOUCH_FREQUENCY 2500000 + +// Touchscreen — XPT2046 resistive (shares VSPI bus with display) +#define HAS_TOUCH 1 +#define TOUCH_XPT2046_SPI 1 +#define XPT2046_SPI_BUS_MOSI_IO_NUM 23 +#define XPT2046_SPI_BUS_MISO_IO_NUM 19 +#define XPT2046_SPI_BUS_SCLK_IO_NUM 18 +#define XPT2046_SPI_CONFIG_CS_GPIO_NUM 21 +#define XPT2046_TOUCH_CONFIG_INT_GPIO_NUM -1 + +// SD Card (on VSPI bus) +#define SDCARD_CS 12 +#define SDCARD_SCK 18 +#define SDCARD_MISO 19 +#define SDCARD_MOSI 23 + +// Default I2C +#define GROVE_SDA 21 +#define GROVE_SCL 22 + +// Shared SPI bus for CC1101/NRF24 (main VSPI) +#define SPI_SCK_PIN 18 +#define SPI_MOSI_PIN 23 +#define SPI_MISO_PIN 19 +#define SPI_SS_PIN 15 + +#endif /* Pins_Arduino_h */ diff --git a/boards/esp32s3-ili9341/connections.md b/boards/esp32s3-ili9341/connections.md new file mode 100644 index 000000000..5c60050c3 --- /dev/null +++ b/boards/esp32s3-ili9341/connections.md @@ -0,0 +1,167 @@ +# ESP32-S3 with ILI9341 Display & Touchscreen Wiring Guide + +This is the most advanced board, featuring the ESP32-S3 microcontroller with dual-core processing, native USB support, and multiple wireless capabilities. + +## Components + +- **MCU**: ESP32-S3 (dual-core, USB native support) +- **Display**: ILI9341 2.8" SPI TFT LCD with RGB-BGR color order (240x320) +- **Touchscreen**: XPT2046 resistive touch controller (hardware SPI) +- **Radio Options**: CC1101, NRF24, or LoRa (SPI bus) +- **I/O**: IR transmit/receive, RF transmit/receive (IR-based and 433/868MHz RF supported) +- **Button**: Single boot button (GPIO0; active-LOW) +- **LED**: Single status LED (GPIO40) +- **USB**: Native USB support (CDC + custom HID) +- **Communication**: Dedicated SPI for display/touch, separate SPI for radio modules + +## Display Pinout (ILI9341 on HSPI) + +| Display Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| CS | 10 | Chip Select | +| RESET | 5 | Reset (active-LOW) | +| DC | 7 | Data/Command | +| MOSI | 11 | Serial Data In (HSPI) | +| MISM | 13 | Serial Data Out (HSPI) | +| SCK | 12 | Clock (HSPI) | +| BL/LED | 38 | Backlight (PWM) | + +## Touchscreen Pinout (XPT2046 on HSPI) + +The touchscreen shares HSPI bus (MOSI/MISO/SCK) with the display but has separate CS. + +| Touch Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In (shared HSPI) | +| MISO | 13 | Serial Data Out (shared HSPI) | +| SCK | 12 | Clock (shared HSPI) | +| CS | 3 | Chip Select (touch only) | +| IRQ | Not used | Interrupt (optional) | + +## Radio Modules (CC1101/NRF24/W5500) - SPI Bus Pins + +All radio modules use the **main SPI bus** with separate chip select lines: + +### CC1101 (FSK radio module) +| CC1101 Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In | +| MISO | 13 | Serial Data Out | +| SCK | 12 | Clock | +| CS | 10 | Chip Select | +| GDO0 | 9 | Data Output (optional) | + +### NRF24 (2.4GHz wireless module) +| NRF24 Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power (needs 3A supply!) | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In | +| MISO | 13 | Serial Data Out | +| SCK | 12 | Clock | +| CS | 14 | Chip Select | +| CE | 16 | Chip Enable | + +### W5500 (Ethernet module - optional) +| W5500 Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In | +| MISO | 13 | Serial Data Out | +| SCK | 12 | Clock | +| CS | (varies) | Chip Select (externally configurable) | +| INT | (varies) | Interrupt (externally configurable) | + +**Note**: CC1101 and NRF24 can be used simultaneously (different CS). W5500 requires custom pin assignment. + +## IR & RF Pins (Single-Pin Modules) + +For IR LED and single-pin RF receivers/transmitters: + +| Function | GPIO Options | +|---|---| +| IR TX | GPIO1, GPIO2 (also Grove Y via GPIO8) | +| IR RX | GPIO1, GPIO2 (also Grove W via GPIO9) | +| RF TX (433/868MHz) | GPIO1, GPIO2 (also Grove Y via GPIO8) | +| RF RX (433/868MHz) | GPIO1, GPIO2 (also Grove W via GPIO9) | + +## User Input & LED + +| Input/Output | ESP32-S3 Pin | Type | +|---|---|---| +| Boot Button | 0 | Tactile switch, active-LOW (remapped as OK button) | +| Status LED | 40 | Output, active-HIGH | + +## I2C Interface (Grove/Standard Sensors) + +| Pin Name | ESP32-S3 Pin | Purpose | +|---|---|---| +| SDA | 8 | I2C Data (also used for IR RX) | +| SCL | 9 | I2C Clock (also used for RF RX) | + +**Note**: Grove pins share GPIO with RF/IR inputs. Prioritize based on your application. + +## Serial Interfaces + +| Interface | TX Pin | RX Pin | Purpose | +|---|---|---| +| UART0 (USB CDC) | — | — | Native USB serial (programming & debug) | +| UART1 (GPS/Ext) | 43 | 44 | External serial (e.g., GPS module) | + +USB is native on the ESP32-S3 via GPIO19/GPIO20 (internal). + +## Power Supply + +- **Logic voltage**: 3.3V +- **Supply current**: ~400mA typical (display + touch + MCU) +- **Peak current**: + - ~800mA with radio + backlight + - **~2500mA with NRF24** (provide dedicated 3A supply if using NRF24!) + +Use a high-quality USB power supply (2A minimum, 3A recommended if using NRF24). + +## SPI Architecture + +``` +ESP32-S3 Dual SPI +├─ HSPI Bus (Display/Touch): +│ ├─ Display (CS=10) — ILI9341 +│ └─ Touchscreen (CS=3) — XPT2046 +│ Shared: MOSI=11, MISO=13, SCK=12 +│ +└─ Main SPI Bus (Radio/Peripherals): + ├─ CC1101 (CS=10, GDO0=9) — FSK transceiver + ├─ NRF24 (CS=14, CE=16) — 2.4GHz wireless + └─ W5500 (CS=?, INT=?) — Optional Ethernet + Shared: MOSI=11, MISO=13, SCK=12 +``` + +**Note**: Display CS and CC1101 CS both use GPIO10 (hardware selectable). Use firmware configuration to choose which module is available. + +## Wiring Notes + +1. **Dual SPI Buses**: HSPI for display/touch, main SPI for radio modules. Both use GPIO11/12/13, but separate CS lines prevent conflicts. +2. **NRF24 Power**: The NRF24 module requires a dedicated 3A power supply. Do **NOT** power it from the USB port alone—use a separate 3.3V regulator. +3. **Backlight**: GPIO38 supports PWM for brightness control (0-255). +4. **Reset**: Display RESET (GPIO5) is active LOW. Normally tied HIGH through resistor or managed by firmware. +5. **Boot Button**: GPIO0 serves dual purpose: boot selection during power-up, OK button during normal operation. +6. **USB Serial**: Unlike older ESP32, this board has native USB. Use USB for uploads and debugging (no FTDI chip needed). +7. **Color Order**: ILI9341 configured for RGB→BGR conversion. +8. **SPI Frequencies**: 40MHz write, 20MHz read, 2.5MHz touch operations. +9. **Grove I2C Conflict**: GPIO8 (SDA) overlaps with IR RX, GPIO9 (SCL) overlaps with RF RX. Configure based on priority. + +## Features + +- **Dual-core 240MHz processor** — More performance-intensive tasks +- **USB Native Support** — Faster uploads, serial over USB +- **More GPIO** — Supports more peripheral devices simultaneously +- **Larger Flash** — 8MB partition for bigger firmware +- **Integrated IR LED driver** — GPIO40 for IR output +- **Advanced Radio Options** — CC1101, NRF24, and optional W5500 Ethernet diff --git a/boards/esp32s3-ili9341/diagrams/esp32s3_ili9341_wiring.png b/boards/esp32s3-ili9341/diagrams/esp32s3_ili9341_wiring.png new file mode 100644 index 000000000..735aa04f0 Binary files /dev/null and b/boards/esp32s3-ili9341/diagrams/esp32s3_ili9341_wiring.png differ diff --git a/boards/esp32s3-ili9341/esp32s3-ili9341.ini b/boards/esp32s3-ili9341/esp32s3-ili9341.ini new file mode 100644 index 000000000..cb58fb61c --- /dev/null +++ b/boards/esp32s3-ili9341/esp32s3-ili9341.ini @@ -0,0 +1,146 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32s3-ili9341] +board = esp32s3-st7789 +board_build.partitions = custom_8Mb.csv +build_src_filter =${env.build_src_filter} +<../boards/esp32s3-ili9341> +build_flags = + ${env.build_flags} + -Iboards/esp32s3-ili9341 + -Os + -DCORE_DEBUG_LEVEL=5 + + ;Can run USB as HID + -DUSB_as_HID=1 + -DBAD_TX=GROVE_SDA + -DBAD_RX=GROVE_SCL + + ;Buttons configuration (active-low boot button on GPIO 0) + -DHAS_BTN=1 + -DBTN_ALIAS='"OK"' + -DBTN_PIN=0 + -DBTN_ACT=LOW + + ;-DALLOW_ALL_GPIO_FOR_IR_RF=1 ; Set this option to make use of all GPIOs, from 1 to 44 to be chosen, except TFT and SD pins + + ;Infrared Led + -DIR_TX_PINS='{{"Grove Y", GROVE_SDA}, {"G1", 1}, {"G2", 2}}' + -DIR_RX_PINS='{{"Grove W", GROVE_SCL}, {"G1", 1}, {"G2", 2}}' + -DTXLED=40 + -DLED_ON=HIGH + -DLED_OFF=LOW + + ;Radio Frequency (one pin modules) pin setting + -DRF_TX_PINS='{{"Grove Y", GROVE_SDA}, {"G1", 1}, {"G2", 2}}' + -DRF_RX_PINS='{{"Grove W", GROVE_SCL}, {"G1", 1}, {"G2", 2}}' + + ;CC1101 SPI connection pins + -DUSE_CC1101_VIA_SPI + -DCC1101_GDO0_PIN=9 + -DCC1101_SS_PIN=10 + -DCC1101_MOSI_PIN=SPI_MOSI_PIN + -DCC1101_SCK_PIN=SPI_SCK_PIN + -DCC1101_MISO_PIN=SPI_MISO_PIN + + ;NRF24 SPI connection pins + -DUSE_NRF24_VIA_SPI + -DNRF24_CE_PIN=16 + -DNRF24_SS_PIN=14 + -DNRF24_MOSI_PIN=SPI_MOSI_PIN + -DNRF24_SCK_PIN=SPI_SCK_PIN + -DNRF24_MISO_PIN=SPI_MISO_PIN + + ;W5500 Ethernet (disabled by default) + ;-DUSE_W5500_VIA_SPI + -DW5500_SS_PIN=-1 + -DW5500_MOSI_PIN=SPI_MOSI_PIN + -DW5500_SCK_PIN=SPI_SCK_PIN + -DW5500_MISO_PIN=SPI_MISO_PIN + -DW5500_INT_PIN=-1 + + ;Lora setup pins (not connected by default) + -DLORA_SCK=SPI_SCK_PIN + -DLORA_MISO=SPI_MISO_PIN + -DLORA_MOSI=SPI_MOSI_PIN + -DLORA_CS=-1 + -DLORA_RST=-1 + -DLORA_DIO0=-1 + + ;FM Radio + -DFM_RSTPIN=-1 + + ;Font sizes + -DFP=1 + -DFM=2 + -DFG=3 + + ;Screen Setup + -DHAS_SCREEN=1 + -DROTATION=1 + -DMINBRIGHT=1 + + ;TFT_eSPI display — ILI9341 240x320 SPI + -DUSER_SETUP_LOADED=1 + -DUSE_HSPI_PORT=1 + -DILI9341_DRIVER=1 + -DTFT_RGB_ORDER=TFT_BGR + -DTFT_WIDTH=240 + -DTFT_HEIGHT=320 + -DTFT_INVERSION_ON + -DTFT_CS=10 + -DTFT_DC=7 + -DTFT_RST=5 + -DTFT_BL=38 + -DTFT_MOSI=11 + -DTFT_SCLK=12 + -DTFT_MISO=13 + -DTOUCH_CS=-1 + -DSMOOTH_FONT=1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=20000000 + -DSPI_TOUCH_FREQUENCY=2500000 + + ;Touchscreen — XPT2046 resistive (hardware SPI via CYD28_TouchscreenR) + -DHAS_TOUCH=1 + -DTOUCH_XPT2046_SPI=1 + -DXPT2046_SPI_BUS_MOSI_IO_NUM=11 + -DXPT2046_SPI_BUS_MISO_IO_NUM=13 + -DXPT2046_SPI_BUS_SCLK_IO_NUM=12 + -DXPT2046_SPI_CONFIG_CS_GPIO_NUM=3 + -DXPT2046_TOUCH_CONFIG_INT_GPIO_NUM=-1 + -DINPUT_HANDLER_TASK_STACK_SIZE=8192 + + ;SD Card pins (no SD card by default, set your pins if connected) + -DSDCARD_CS=4 + -DSDCARD_SCK=12 + -DSDCARD_MISO=13 + -DSDCARD_MOSI=11 + + ;Default I2C port (touch + grove) + -DGROVE_SDA=8 + -DGROVE_SCL=9 + + ; SERIAL (GPS) dedicated pins + -DSERIAL_TX=43 + -DSERIAL_RX=44 + -DGPS_SERIAL_TX=SERIAL_TX + -DGPS_SERIAL_RX=SERIAL_RX + + ;Main SPI bus pins (for CC1101/NRF24/etc) + -DSPI_SCK_PIN=12 + -DSPI_MOSI_PIN=11 + -DSPI_MISO_PIN=13 + -DSPI_SS_PIN=10 + + -DDEVICE_NAME='"ESP32-S3 ILI9341"' + +lib_deps = + ${env.lib_deps} diff --git a/boards/esp32s3-ili9341/interface.cpp b/boards/esp32s3-ili9341/interface.cpp new file mode 100644 index 000000000..97493b216 --- /dev/null +++ b/boards/esp32s3-ili9341/interface.cpp @@ -0,0 +1,136 @@ +#include "CYD28_TouchscreenR.h" +#include "core/powerSave.h" +#include "core/utils.h" +#include + +#define CYD28_DISPLAY_HOR_RES_MAX 320 +#define CYD28_DISPLAY_VER_RES_MAX 240 +CYD28_TouchR touch(CYD28_DISPLAY_HOR_RES_MAX, CYD28_DISPLAY_VER_RES_MAX); + +/*************************************************************************************** +** Function name: _setup_gpio() +** Location: main.cpp +** Description: initial setup for the device +***************************************************************************************/ +void _setup_gpio() { + // Keep XPT2046 CS high until needed + pinMode(XPT2046_SPI_CONFIG_CS_GPIO_NUM, OUTPUT); + digitalWrite(XPT2046_SPI_CONFIG_CS_GPIO_NUM, HIGH); + + bruceConfig.colorInverted = 1; +} + +/*************************************************************************************** +** Function name: _post_setup_gpio() +** Location: main.cpp +** Description: second stage gpio setup — runs AFTER tft.init() +***************************************************************************************/ +void _post_setup_gpio() { + // Initialize XPT2046 touch using hardware SPI (shares bus with display) + if (!touch.begin(&tft.getSPIinstance())) { + Serial.println("[TOUCH] XPT2046 not started"); + } else { + Serial.println("[TOUCH] XPT2046 started OK"); + } + touch.setRotation(ROTATION); + + // Backlight on + pinMode(TFT_BL, OUTPUT); + analogWrite(TFT_BL, 255); +} + +/*************************************************************************************** +** Function name: getBattery() +** location: display.cpp +** Description: Delivers the battery value from 1-100 +***************************************************************************************/ +int getBattery() { return 0; } + +/*************************************************************************************** +** Function name: isCharging() +** Description: Determines if the device is charging +***************************************************************************************/ +bool isCharging() { return false; } + +/********************************************************************* +** Function: setBrightness +** location: settings.cpp +** set brightness value +**********************************************************************/ +void _setBrightness(uint8_t brightval) { + if (brightval == 0) { + analogWrite(TFT_BL, brightval); + } else { + analogWrite(TFT_BL, brightval); + } +} + +/********************************************************************* +** Function: InputHandler +** Handles the variables PrevPress, NextPress, SelPress, AnyKeyPress and EscPress +**********************************************************************/ +void InputHandler(void) { + static long d_tmp = 0; + if (millis() - d_tmp > 200 || LongPress) { + if (touch.touched()) { + auto t = touch.getPointScaled(); + + // Coordinate transform based on rotation + if (bruceConfigPins.rotation == 3) { + t.y = (tftHeight + 20) - t.y; + t.x = tftWidth - t.x; + } + if (bruceConfigPins.rotation == 0) { + int tmp = t.x; + t.x = tftWidth - t.y; + t.y = tmp; + } + if (bruceConfigPins.rotation == 2) { + int tmp = t.x; + t.x = t.y; + t.y = (tftHeight + 20) - tmp; + } + + if (!wakeUpScreen()) AnyKeyPress = true; + else goto END; + + touchPoint.x = t.x; + touchPoint.y = t.y; + touchPoint.pressed = true; + touchHeatMap(touchPoint); + END: + d_tmp = millis(); + } + } + +#ifdef HAS_BTN + checkPowerSaveTime(); + if (digitalRead(BTN_PIN) == BTN_ACT) { + if (!wakeUpScreen()) AnyKeyPress = true; + SelPress = true; + long tmp = millis(); + while ((millis() - tmp) < 200 && digitalRead(BTN_PIN) == BTN_ACT); + } +#endif +} + +/********************************************************************* +** Function: powerOff +** location: mykeyboard.cpp +** Turns off the device (or try to) +**********************************************************************/ +void powerOff() { esp_deep_sleep_start(); } + +/********************************************************************* +** Function: goToDeepSleep +** location: mykeyboard.cpp +** Puts the device into DeepSleep +**********************************************************************/ +void goToDeepSleep() { esp_deep_sleep_start(); } + +/********************************************************************* +** Function: checkReboot +** location: mykeyboard.cpp +** Btn logic to turn off the device +**********************************************************************/ +void checkReboot() {} diff --git a/boards/esp32s3-ili9341/pins_arduino.h b/boards/esp32s3-ili9341/pins_arduino.h new file mode 100644 index 000000000..256e2a325 --- /dev/null +++ b/boards/esp32s3-ili9341/pins_arduino.h @@ -0,0 +1,146 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +// Boot button as OK (active-low) +#ifndef HAS_BTN +#define HAS_BTN 1 +#endif +#ifndef BTN_ALIAS +#define BTN_ALIAS "\"OK\"" +#endif +#ifndef BTN_PIN +#define BTN_PIN 0 +#endif +#ifndef BTN_ACT +#define BTN_ACT LOW +#endif + +#define HAS_SCREEN 1 +#define ROTATION 1 +#define MINBRIGHT 1 + +// TFT_eSPI Setup for ILI9341 2.8" display (HSPI bus) +#ifndef USER_SETUP_LOADED +#define USER_SETUP_LOADED 1 +#endif +#ifndef ILI9341_DRIVER +#define ILI9341_DRIVER 1 +#endif +#ifndef TFT_RGB_ORDER +#define TFT_RGB_ORDER TFT_BGR +#endif +#ifndef TFT_WIDTH +#define TFT_WIDTH 240 +#endif +#ifndef TFT_HEIGHT +#define TFT_HEIGHT 320 +#endif +#ifndef TFT_MISO +#define TFT_MISO 13 +#endif +#ifndef TFT_MOSI +#define TFT_MOSI 11 +#endif +#ifndef TFT_SCLK +#define TFT_SCLK 12 +#endif +#ifndef TFT_CS +#define TFT_CS 10 +#endif +#ifndef TFT_DC +#define TFT_DC 7 +#endif +#ifndef TFT_RST +#define TFT_RST 5 +#endif +#ifndef TFT_BL +#define TFT_BL 38 +#endif +#ifndef TFT_BACKLIGHT_ON +#define TFT_BACKLIGHT_ON HIGH +#endif +#ifndef SMOOTH_FONT +#define SMOOTH_FONT 1 +#endif +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 20000000 +#define SPI_TOUCH_FREQUENCY 2500000 + +// Touchscreen — XPT2046 resistive (shares HSPI bus with display) +#define HAS_TOUCH 1 +#define TOUCH_XPT2046_SPI 1 +#define XPT2046_SPI_BUS_MOSI_IO_NUM 11 +#define XPT2046_SPI_BUS_MISO_IO_NUM 13 +#define XPT2046_SPI_BUS_SCLK_IO_NUM 12 +#define XPT2046_SPI_CONFIG_CS_GPIO_NUM 3 +#define XPT2046_TOUCH_CONFIG_INT_GPIO_NUM -1 + +// Status LED +#define TXLED 40 +#define LED_ON HIGH +#define LED_OFF LOW + +// Default I2C +#define GROVE_SDA 8 +#define GROVE_SCL 9 + +// Shared SPI bus for CC1101/NRF24 (main SPI) +#define SPI_SCK_PIN 12 +#define SPI_MOSI_PIN 11 +#define SPI_MISO_PIN 13 +#define SPI_SS_PIN 10 + +#endif /* Pins_Arduino_h */ diff --git a/boards/esp32s3-st7789/connections.md b/boards/esp32s3-st7789/connections.md new file mode 100644 index 000000000..af2808519 --- /dev/null +++ b/boards/esp32s3-st7789/connections.md @@ -0,0 +1,167 @@ +# ESP32-S3 with ST7789 Display & Touchscreen Wiring Guide + +This is the most advanced board, featuring the ESP32-S3 microcontroller with dual-core processing, native USB support, and multiple wireless capabilities. + +## Components + +- **MCU**: ESP32-S3 (dual-core, USB native support) +- **Display**: ST7789 2.8" SPI TFT LCD with RGB-BGR color order (240x320) +- **Touchscreen**: XPT2046 resistive touch controller (hardware SPI) +- **Radio Options**: CC1101, NRF24, or LoRa (SPI bus) +- **I/O**: IR transmit/receive, RF transmit/receive (IR-based and 433/868MHz RF supported) +- **Button**: Single boot button (GPIO0; active-LOW) +- **LED**: Single status LED (GPIO40) +- **USB**: Native USB support (CDC + custom HID) +- **Communication**: Dedicated SPI for display/touch, separate SPI for radio modules + +## Display Pinout (ST7789 on HSPI) + +| Display Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| CS | 10 | Chip Select | +| RESET | 5 | Reset (active-LOW) | +| DC | 7 | Data/Command | +| MOSI | 11 | Serial Data In (HSPI) | +| MISM | 13 | Serial Data Out (HSPI) | +| SCK | 12 | Clock (HSPI) | +| BL/LED | 38 | Backlight (PWM) | + +## Touchscreen Pinout (XPT2046 on HSPI) + +The touchscreen shares HSPI bus (MOSI/MISO/SCK) with the display but has separate CS. + +| Touch Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In (shared HSPI) | +| MISO | 13 | Serial Data Out (shared HSPI) | +| SCK | 12 | Clock (shared HSPI) | +| CS | 3 | Chip Select (touch only) | +| IRQ | Not used | Interrupt (optional) | + +## Radio Modules (CC1101/NRF24/W5500) - SPI Bus Pins + +All radio modules use the **main SPI bus** with separate chip select lines: + +### CC1101 (FSK radio module) +| CC1101 Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In | +| MISO | 13 | Serial Data Out | +| SCK | 12 | Clock | +| CS | 10 | Chip Select | +| GDO0 | 9 | Data Output (optional) | + +### NRF24 (2.4GHz wireless module) +| NRF24 Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power (needs 3A supply!) | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In | +| MISO | 13 | Serial Data Out | +| SCK | 12 | Clock | +| CS | 14 | Chip Select | +| CE | 16 | Chip Enable | + +### W5500 (Ethernet module - optional) +| W5500 Pin | ESP32-S3 Pin | Purpose | +|---|---|---| +| VCC | 3.3V | Power | +| GND | GND | Ground | +| MOSI | 11 | Serial Data In | +| MISO | 13 | Serial Data Out | +| SCK | 12 | Clock | +| CS | (varies) | Chip Select (externally configurable) | +| INT | (varies) | Interrupt (externally configurable) | + +**Note**: CC1101 and NRF24 can be used simultaneously (different CS). W5500 requires custom pin assignment. + +## IR & RF Pins (Single-Pin Modules) + +For IR LED and single-pin RF receivers/transmitters: + +| Function | GPIO Options | +|---|---| +| IR TX | GPIO1, GPIO2 (also Grove Y via GPIO8) | +| IR RX | GPIO1, GPIO2 (also Grove W via GPIO9) | +| RF TX (433/868MHz) | GPIO1, GPIO2 (also Grove Y via GPIO8) | +| RF RX (433/868MHz) | GPIO1, GPIO2 (also Grove W via GPIO9) | + +## User Input & LED + +| Input/Output | ESP32-S3 Pin | Type | +|---|---|---| +| Boot Button | 0 | Tactile switch, active-LOW (remapped as OK button) | +| Status LED | 40 | Output, active-HIGH | + +## I2C Interface (Grove/Standard Sensors) + +| Pin Name | ESP32-S3 Pin | Purpose | +|---|---|---| +| SDA | 8 | I2C Data (also used for IR RX) | +| SCL | 9 | I2C Clock (also used for RF RX) | + +**Note**: Grove pins share GPIO with RF/IR inputs. Prioritize based on your application. + +## Serial Interfaces + +| Interface | TX Pin | RX Pin | Purpose | +|---|---|---| +| UART0 (USB CDC) | — | — | Native USB serial (programming & debug) | +| UART1 (GPS/Ext) | 43 | 44 | External serial (e.g., GPS module) | + +USB is native on the ESP32-S3 via GPIO19/GPIO20 (internal). + +## Power Supply + +- **Logic voltage**: 3.3V +- **Supply current**: ~400mA typical (display + touch + MCU) +- **Peak current**: + - ~800mA with radio + backlight + - **~2500mA with NRF24** (provide dedicated 3A supply if using NRF24!) + +Use a high-quality USB power supply (2A minimum, 3A recommended if using NRF24). + +## SPI Architecture + +``` +ESP32-S3 Dual SPI +├─ HSPI Bus (Display/Touch): +│ ├─ Display (CS=10) — ST7789 +│ └─ Touchscreen (CS=3) — XPT2046 +│ Shared: MOSI=11, MISO=13, SCK=12 +│ +└─ Main SPI Bus (Radio/Peripherals): + ├─ CC1101 (CS=10, GDO0=9) — FSK transceiver + ├─ NRF24 (CS=14, CE=16) — 2.4GHz wireless + └─ W5500 (CS=?, INT=?) — Optional Ethernet + Shared: MOSI=11, MISO=13, SCK=12 +``` + +**Note**: Display CS and CC1101 CS both use GPIO10 (hardware selectable). Use firmware configuration to choose which module is available. + +## Wiring Notes + +1. **Dual SPI Buses**: HSPI for display/touch, main SPI for radio modules. Both use GPIO11/12/13, but separate CS lines prevent conflicts. +2. **NRF24 Power**: The NRF24 module requires a dedicated 3A power supply. Do **NOT** power it from the USB port alone—use a separate 3.3V regulator. +3. **Backlight**: GPIO38 supports PWM for brightness control (0-255). +4. **Reset**: Display RESET (GPIO5) is active LOW. Normally tied HIGH through resistor or managed by firmware. +5. **Boot Button**: GPIO0 serves dual purpose: boot selection during power-up, OK button during normal operation. +6. **USB Serial**: Unlike older ESP32, this board has native USB. Use USB for uploads and debugging (no FTDI chip needed). +7. **Color Order**: ST7789 configured for RGB→BGR conversion. +8. **SPI Frequencies**: 40MHz write, 20MHz read, 2.5MHz touch operations. +9. **Grove I2C Conflict**: GPIO8 (SDA) overlaps with IR RX, GPIO9 (SCL) overlaps with RF RX. Configure based on priority. + +## Features + +- **Dual-core 240MHz processor** — More performance-intensive tasks +- **USB Native Support** — Faster uploads, serial over USB +- **More GPIO** — Supports more peripheral devices simultaneously +- **Larger Flash** — 8MB partition for bigger firmware +- **Integrated IR LED driver** — GPIO40 for IR output +- **Advanced Radio Options** — CC1101, NRF24, and optional W5500 Ethernet diff --git a/boards/esp32s3-st7789/diagrams/esp32s3_st7789_wiring.png b/boards/esp32s3-st7789/diagrams/esp32s3_st7789_wiring.png new file mode 100644 index 000000000..f5baaee7b Binary files /dev/null and b/boards/esp32s3-st7789/diagrams/esp32s3_st7789_wiring.png differ diff --git a/boards/esp32s3-st7789/esp32s3-st7789.ini b/boards/esp32s3-st7789/esp32s3-st7789.ini new file mode 100644 index 000000000..07372f886 --- /dev/null +++ b/boards/esp32s3-st7789/esp32s3-st7789.ini @@ -0,0 +1,146 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32s3-st7789] +board = esp32s3-st7789 +board_build.partitions = custom_8Mb.csv +build_src_filter =${env.build_src_filter} +<../boards/esp32s3-st7789> +build_flags = + ${env.build_flags} + -Iboards/esp32s3-st7789 + -Os + -DCORE_DEBUG_LEVEL=5 + + ;Can run USB as HID + -DUSB_as_HID=1 + -DBAD_TX=GROVE_SDA + -DBAD_RX=GROVE_SCL + + ;Buttons configuration (active-low boot button on GPIO 0) + -DHAS_BTN=1 + -DBTN_ALIAS='"OK"' + -DBTN_PIN=0 + -DBTN_ACT=LOW + + ;-DALLOW_ALL_GPIO_FOR_IR_RF=1 ; Set this option to make use of all GPIOs, from 1 to 44 to be chosen, except TFT and SD pins + + ;Infrared Led + -DIR_TX_PINS='{{"Grove Y", GROVE_SDA}, {"G1", 1}, {"G2", 2}}' + -DIR_RX_PINS='{{"Grove W", GROVE_SCL}, {"G1", 1}, {"G2", 2}}' + -DTXLED=40 + -DLED_ON=HIGH + -DLED_OFF=LOW + + ;Radio Frequency (one pin modules) pin setting + -DRF_TX_PINS='{{"Grove Y", GROVE_SDA}, {"G1", 1}, {"G2", 2}}' + -DRF_RX_PINS='{{"Grove W", GROVE_SCL}, {"G1", 1}, {"G2", 2}}' + + ;CC1101 SPI connection pins + -DUSE_CC1101_VIA_SPI + -DCC1101_GDO0_PIN=9 + -DCC1101_SS_PIN=10 + -DCC1101_MOSI_PIN=SPI_MOSI_PIN + -DCC1101_SCK_PIN=SPI_SCK_PIN + -DCC1101_MISO_PIN=SPI_MISO_PIN + + ;NRF24 SPI connection pins + -DUSE_NRF24_VIA_SPI + -DNRF24_CE_PIN=16 + -DNRF24_SS_PIN=14 + -DNRF24_MOSI_PIN=SPI_MOSI_PIN + -DNRF24_SCK_PIN=SPI_SCK_PIN + -DNRF24_MISO_PIN=SPI_MISO_PIN + + ;W5500 Ethernet (disabled by default) + ;-DUSE_W5500_VIA_SPI + -DW5500_SS_PIN=-1 + -DW5500_MOSI_PIN=SPI_MOSI_PIN + -DW5500_SCK_PIN=SPI_SCK_PIN + -DW5500_MISO_PIN=SPI_MISO_PIN + -DW5500_INT_PIN=-1 + + ;Lora setup pins (not connected by default) + -DLORA_SCK=SPI_SCK_PIN + -DLORA_MISO=SPI_MISO_PIN + -DLORA_MOSI=SPI_MOSI_PIN + -DLORA_CS=-1 + -DLORA_RST=-1 + -DLORA_DIO0=-1 + + ;FM Radio + -DFM_RSTPIN=-1 + + ;Font sizes + -DFP=1 + -DFM=2 + -DFG=3 + + ;Screen Setup + -DHAS_SCREEN=1 + -DROTATION=1 + -DMINBRIGHT=1 + + ;TFT_eSPI display — ST7789 240x320 SPI + -DUSER_SETUP_LOADED=1 + -DUSE_HSPI_PORT=1 + -DST7789_DRIVER=1 + -DTFT_RGB_ORDER=TFT_BGR + -DTFT_WIDTH=240 + -DTFT_HEIGHT=320 + -DTFT_INVERSION_ON + -DTFT_CS=10 + -DTFT_DC=7 + -DTFT_RST=5 + -DTFT_BL=38 + -DTFT_MOSI=11 + -DTFT_SCLK=12 + -DTFT_MISO=13 + -DTOUCH_CS=-1 + -DSMOOTH_FONT=1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=20000000 + -DSPI_TOUCH_FREQUENCY=2500000 + + ;Touchscreen — XPT2046 resistive (hardware SPI via CYD28_TouchscreenR) + -DHAS_TOUCH=1 + -DTOUCH_XPT2046_SPI=1 + -DXPT2046_SPI_BUS_MOSI_IO_NUM=11 + -DXPT2046_SPI_BUS_MISO_IO_NUM=13 + -DXPT2046_SPI_BUS_SCLK_IO_NUM=12 + -DXPT2046_SPI_CONFIG_CS_GPIO_NUM=3 + -DXPT2046_TOUCH_CONFIG_INT_GPIO_NUM=-1 + -DINPUT_HANDLER_TASK_STACK_SIZE=8192 + + ;SD Card pins (no SD card by default, set your pins if connected) + -DSDCARD_CS=-1 + -DSDCARD_SCK=-1 + -DSDCARD_MISO=-1 + -DSDCARD_MOSI=-1 + + ;Default I2C port (touch + grove) + -DGROVE_SDA=8 + -DGROVE_SCL=9 + + ; SERIAL (GPS) dedicated pins + -DSERIAL_TX=43 + -DSERIAL_RX=44 + -DGPS_SERIAL_TX=SERIAL_TX + -DGPS_SERIAL_RX=SERIAL_RX + + ;Main SPI bus pins (for CC1101/NRF24/etc) + -DSPI_SCK_PIN=12 + -DSPI_MOSI_PIN=11 + -DSPI_MISO_PIN=13 + -DSPI_SS_PIN=10 + + -DDEVICE_NAME='"ESP32-S3 ST7789"' + +lib_deps = + ${env.lib_deps} diff --git a/boards/esp32s3-st7789/interface.cpp b/boards/esp32s3-st7789/interface.cpp new file mode 100644 index 000000000..97493b216 --- /dev/null +++ b/boards/esp32s3-st7789/interface.cpp @@ -0,0 +1,136 @@ +#include "CYD28_TouchscreenR.h" +#include "core/powerSave.h" +#include "core/utils.h" +#include + +#define CYD28_DISPLAY_HOR_RES_MAX 320 +#define CYD28_DISPLAY_VER_RES_MAX 240 +CYD28_TouchR touch(CYD28_DISPLAY_HOR_RES_MAX, CYD28_DISPLAY_VER_RES_MAX); + +/*************************************************************************************** +** Function name: _setup_gpio() +** Location: main.cpp +** Description: initial setup for the device +***************************************************************************************/ +void _setup_gpio() { + // Keep XPT2046 CS high until needed + pinMode(XPT2046_SPI_CONFIG_CS_GPIO_NUM, OUTPUT); + digitalWrite(XPT2046_SPI_CONFIG_CS_GPIO_NUM, HIGH); + + bruceConfig.colorInverted = 1; +} + +/*************************************************************************************** +** Function name: _post_setup_gpio() +** Location: main.cpp +** Description: second stage gpio setup — runs AFTER tft.init() +***************************************************************************************/ +void _post_setup_gpio() { + // Initialize XPT2046 touch using hardware SPI (shares bus with display) + if (!touch.begin(&tft.getSPIinstance())) { + Serial.println("[TOUCH] XPT2046 not started"); + } else { + Serial.println("[TOUCH] XPT2046 started OK"); + } + touch.setRotation(ROTATION); + + // Backlight on + pinMode(TFT_BL, OUTPUT); + analogWrite(TFT_BL, 255); +} + +/*************************************************************************************** +** Function name: getBattery() +** location: display.cpp +** Description: Delivers the battery value from 1-100 +***************************************************************************************/ +int getBattery() { return 0; } + +/*************************************************************************************** +** Function name: isCharging() +** Description: Determines if the device is charging +***************************************************************************************/ +bool isCharging() { return false; } + +/********************************************************************* +** Function: setBrightness +** location: settings.cpp +** set brightness value +**********************************************************************/ +void _setBrightness(uint8_t brightval) { + if (brightval == 0) { + analogWrite(TFT_BL, brightval); + } else { + analogWrite(TFT_BL, brightval); + } +} + +/********************************************************************* +** Function: InputHandler +** Handles the variables PrevPress, NextPress, SelPress, AnyKeyPress and EscPress +**********************************************************************/ +void InputHandler(void) { + static long d_tmp = 0; + if (millis() - d_tmp > 200 || LongPress) { + if (touch.touched()) { + auto t = touch.getPointScaled(); + + // Coordinate transform based on rotation + if (bruceConfigPins.rotation == 3) { + t.y = (tftHeight + 20) - t.y; + t.x = tftWidth - t.x; + } + if (bruceConfigPins.rotation == 0) { + int tmp = t.x; + t.x = tftWidth - t.y; + t.y = tmp; + } + if (bruceConfigPins.rotation == 2) { + int tmp = t.x; + t.x = t.y; + t.y = (tftHeight + 20) - tmp; + } + + if (!wakeUpScreen()) AnyKeyPress = true; + else goto END; + + touchPoint.x = t.x; + touchPoint.y = t.y; + touchPoint.pressed = true; + touchHeatMap(touchPoint); + END: + d_tmp = millis(); + } + } + +#ifdef HAS_BTN + checkPowerSaveTime(); + if (digitalRead(BTN_PIN) == BTN_ACT) { + if (!wakeUpScreen()) AnyKeyPress = true; + SelPress = true; + long tmp = millis(); + while ((millis() - tmp) < 200 && digitalRead(BTN_PIN) == BTN_ACT); + } +#endif +} + +/********************************************************************* +** Function: powerOff +** location: mykeyboard.cpp +** Turns off the device (or try to) +**********************************************************************/ +void powerOff() { esp_deep_sleep_start(); } + +/********************************************************************* +** Function: goToDeepSleep +** location: mykeyboard.cpp +** Puts the device into DeepSleep +**********************************************************************/ +void goToDeepSleep() { esp_deep_sleep_start(); } + +/********************************************************************* +** Function: checkReboot +** location: mykeyboard.cpp +** Btn logic to turn off the device +**********************************************************************/ +void checkReboot() {} diff --git a/boards/esp32s3-st7789/pins_arduino.h b/boards/esp32s3-st7789/pins_arduino.h new file mode 100644 index 000000000..c9cb5113c --- /dev/null +++ b/boards/esp32s3-st7789/pins_arduino.h @@ -0,0 +1,155 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +// Boot button as OK (active-low) +#ifndef HAS_BTN +#define HAS_BTN 1 +#endif +#ifndef BTN_ALIAS +#define BTN_ALIAS "\"OK\"" +#endif +#ifndef BTN_PIN +#define BTN_PIN 0 +#endif +#ifndef BTN_ACT +#define BTN_ACT LOW +#endif + +#define HAS_SCREEN 1 +#define ROTATION 1 +#define MINBRIGHT 1 + +// TFT_eSPI Setup for ST7789 2.8" display (HSPI bus) +#ifndef USER_SETUP_LOADED +#define USER_SETUP_LOADED 1 +#endif +#ifndef ST7789_DRIVER +#define ST7789_DRIVER 1 +#endif +#ifndef TFT_RGB_ORDER +#define TFT_RGB_ORDER TFT_BGR +#endif +#ifndef TFT_WIDTH +#define TFT_WIDTH 240 +#endif +#ifndef TFT_HEIGHT +#define TFT_HEIGHT 320 +#endif +#ifndef TFT_INVERSION_ON +#define TFT_INVERSION_ON +#endif +#ifndef TFT_MISO +#define TFT_MISO 13 +#endif +#ifndef TFT_MOSI +#define TFT_MOSI 11 +#endif +#ifndef TFT_SCLK +#define TFT_SCLK 12 +#endif +#ifndef TFT_CS +#define TFT_CS 10 +#endif +#ifndef TFT_DC +#define TFT_DC 7 +#endif +#ifndef TFT_RST +#define TFT_RST 5 +#endif +#ifndef TFT_BL +#define TFT_BL 38 +#endif +#ifndef TFT_BACKLIGHT_ON +#define TFT_BACKLIGHT_ON HIGH +#endif +#ifndef SMOOTH_FONT +#define SMOOTH_FONT 1 +#endif +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 20000000 +#define SPI_TOUCH_FREQUENCY 2500000 + +// Touchscreen — XPT2046 resistive (shares HSPI bus with display) +#define HAS_TOUCH 1 +#define TOUCH_XPT2046_SPI 1 +#define XPT2046_SPI_BUS_MOSI_IO_NUM 11 +#define XPT2046_SPI_BUS_MISO_IO_NUM 13 +#define XPT2046_SPI_BUS_SCLK_IO_NUM 12 +#define XPT2046_SPI_CONFIG_CS_GPIO_NUM 3 +#define XPT2046_TOUCH_CONFIG_INT_GPIO_NUM -1 + +// Status LED +#define TXLED 40 +#define LED_ON HIGH +#define LED_OFF LOW + +// SD Card (on HSPI bus with display/touch) +#define SDCARD_CS 4 +#define SDCARD_SCK 12 +#define SDCARD_MISO 13 +#define SDCARD_MOSI 11 + +// Default I2C +#define GROVE_SDA 8 +#define GROVE_SCL 9 + +// Shared SPI bus for CC1101/NRF24 (main SPI) +#define SPI_SCK_PIN 12 +#define SPI_MOSI_PIN 11 +#define SPI_MISO_PIN 13 +#define SPI_SS_PIN 10 + +#endif /* Pins_Arduino_h */ diff --git a/boards/lilygo-t-deck/lilygo-t-deck.ini b/boards/lilygo-t-deck/lilygo-t-deck.ini index 13a9aa9f7..4c5fa2a92 100644 --- a/boards/lilygo-t-deck/lilygo-t-deck.ini +++ b/boards/lilygo-t-deck/lilygo-t-deck.ini @@ -50,9 +50,9 @@ build_flags = ;-DHAS_RTC=1 ;Speaker to run music, compatible with NS4168 - ;-DHAS_NS4168_SPKR=1 ;uncomment to enable - -DBCLK=5 - -DWCLK=7 + -DHAS_NS4168_SPKR=1 + -DBCLK=7 + -DWCLK=5 -DDOUT=6 -DMCLK=48 diff --git a/boards/lilygo-t-lora-pager/interface.cpp b/boards/lilygo-t-lora-pager/interface.cpp index fcbe41171..081a9f831 100644 --- a/boards/lilygo-t-lora-pager/interface.cpp +++ b/boards/lilygo-t-lora-pager/interface.cpp @@ -11,10 +11,6 @@ extern RotaryEncoder *encoder; RotaryEncoder *encoder = nullptr; IRAM_ATTR void checkPosition() { encoder->tick(); } -// GPIO expander -#include -ExtensionIOXL9555 io; - // Charger chip #define XPOWERS_CHIP_BQ25896 #include @@ -123,6 +119,33 @@ int handleSpecialKeys(uint8_t k, bool pressed) { return 0; } +void initPeripherals() { + if (ioExpander.init()) { + const uint8_t expands[] = { + EXPANDS_DRV_EN, + EXPANDS_AMP_EN, + EXPANDS_KB_RST, + EXPANDS_LORA_EN, + EXPANDS_GPS_EN, + EXPANDS_NFC_EN, + EXPANDS_GPS_RST, + EXPANDS_KB_PWR, + EXPANDS_KB_EN, + EXPANDS_GPIO_EN, + EXPANDS_SD_EN + }; + for (auto pin : expands) { + ioExpander.pinMode(pin, OUTPUT); + ioExpander.digitalWrite(pin, HIGH); + delay(1); + } + ioExpander.pinMode(EXPANDS_SD_DET, INPUT); + Serial.println("Initializing expander OK"); + } else { + Serial.println("Initializing expander failed"); + } + delay(50); +} /*************************************************************************************** ** Function name: _setup_gpio() ** Description: initial setup for the device @@ -131,6 +154,7 @@ void _setup_gpio() { pinMode(SEL_BTN, INPUT); pinMode(BK_BTN, INPUT); + pinMode(ST25R_IRQ, INPUT); pinMode(TFT_CS, OUTPUT); digitalWrite(TFT_CS, HIGH); @@ -146,6 +170,7 @@ void _setup_gpio() { pinMode(LORA_RST, OUTPUT); digitalWrite(LORA_RST, HIGH); + Wire.begin(GROVE_SDA, GROVE_SCL); // Power management bool pmu_ret = false; @@ -161,48 +186,7 @@ void _setup_gpio() { // Battery gauge if (bq.getDesignCap() != BATTERY_DESIGN_CAPACITY) { bq.setDesignCap(BATTERY_DESIGN_CAPACITY); } - - // IO Expander - // TODO: Needs updating to use the same interface as the other IO Expanders (io_expander ioExpander) - // if (ioExpander.init(IO_EXPANDER_ADDRESS, &Wire)) { - // const uint8_t expands[] = { - // EXPANDS_KB_RST, - // EXPANDS_KB_EN, - // EXPANDS_SD_EN, - // EXPANDS_DRV_EN, - // EXPANDS_AMP_EN, // Audio - // }; - // for (auto pin : expands) { - // ioExpander.pinMode(pin, OUTPUT); - // ioExpander.digitalWrite(pin, HIGH); - // delay(1); - // } - // ioExpander.pinMode(EXPANDS_SD_PULLEN, INPUT); - // ioExpander.digitalWrite(EXPANDS_DRV_EN, LOW); - // } else { - // Serial.println("Initializing expander failed"); - // } - if (io.begin(Wire, IO_EXPANDER_ADDRESS)) { - const uint8_t expands[] = { - EXPANDS_KB_RST, - EXPANDS_KB_EN, - EXPANDS_SD_EN, - EXPANDS_DRV_EN, - EXPANDS_AMP_EN, // Audio - EXPANDS_LORA_EN, - EXPANDS_GPS_EN, - EXPANDS_GPS_RST, - EXPANDS_NFC_EN, - }; - for (auto pin : expands) { - io.pinMode(pin, OUTPUT); - io.digitalWrite(pin, HIGH); - delay(1); - } - io.pinMode(EXPANDS_SD_PULLEN, INPUT); - } else { - Serial.println("Initializing expander failed"); - } + initPeripherals(); // Initialise keyboard keyboard = new Adafruit_TCA8418(); @@ -261,6 +245,8 @@ void _setup_gpio() { board.begin(cfg); } +void _post_setup_gpio() { initPeripherals(); } + /*************************************************************************************** ** Function name: getBattery() ** Description: Delivers the battery value from 1-100 diff --git a/boards/lilygo-t-lora-pager/pins_arduino.h b/boards/lilygo-t-lora-pager/pins_arduino.h index 6442eee96..00ad270bd 100644 --- a/boards/lilygo-t-lora-pager/pins_arduino.h +++ b/boards/lilygo-t-lora-pager/pins_arduino.h @@ -23,6 +23,30 @@ // Battery charger #define BQ25896_I2C_ADDRESS 0x6B +/* +Expand(XL9555) GPIO0 Haptic Driver Enable ❌ +Expand(XL9555) GPIO1 Audio Power Amplifier Enable ❌ +Expand(XL9555) GPIO2 Keyboard RESET ❌ +Expand(XL9555) GPIO3 LoRa Power supply Enable ❌ +Expand(XL9555) GPIO4 GNSS Power supply Enable ❌ +Expand(XL9555) GPIO5 NFC Power supply Enable ❌ +Expand(XL9555) GPIO6 Display RESET (No connect) ❌ +Expand(XL9555) GPIO7 GNSS RESET ❌ +Expand(XL9555) GPIO10 Keyboard Power supply Enable ❌ +Expand(XL9555) GPIO11 External 12-Pin socket ✅️ +Expand(XL9555) GPIO12 SD Insert Detect ❌ +Expand(XL9555) GPIO14 SD Power supply Enable + + PowerManage Channel +Channel Peripherals +Expand(XL9555) GPIO0 DRV2605 Enable +Expand(XL9555) GPIO1 Speaker +Expand(XL9555) GPIO3 LoRa +Expand(XL9555) GPIO4 GNSS +Expand(XL9555) GPIO5 NFC +Expand(XL9555) GPIO8 Keyboard +Expand(XL9555) GPIO14 SD Card +*/ // IO Expander #define EXPANDS_DRV_EN 0 #define EXPANDS_AMP_EN 1 @@ -31,17 +55,18 @@ #define EXPANDS_GPS_EN 4 #define EXPANDS_NFC_EN 5 #define EXPANDS_GPS_RST 7 -#define EXPANDS_KB_EN 8 -#define EXPANDS_GPIO_EN 9 -#define EXPANDS_SD_DET 10 -#define EXPANDS_SD_PULLEN 11 -#define EXPANDS_SD_EN 12 +#define EXPANDS_KB_PWR 8 +#define EXPANDS_KB_EN 10 +#define EXPANDS_GPIO_EN 11 +#define EXPANDS_SD_DET 12 +#define EXPANDS_SD_EN 14 #define IO_EXPANDER_PCA9555 // to make GPS stays turned on all the time // making Wardrive start way faster // comment the next line #define IO_EXP_GPS EXPANDS_GPS_EN +#define IO_EXP_NFC EXPANDS_NFC_EN // Main SPI Bus #define SPI_SS_PIN 21 #define SPI_MOSI_PIN 34 @@ -49,8 +74,8 @@ #define SPI_SCK_PIN 35 static const uint8_t SS = SPI_SS_PIN; static const uint8_t MOSI = SPI_MOSI_PIN; -static const uint8_t SCK = SPI_MISO_PIN; -static const uint8_t MISO = SPI_SCK_PIN; +static const uint8_t SCK = SPI_SCK_PIN; +static const uint8_t MISO = SPI_MISO_PIN; // Set Main I2C Bus #define GROVE_SDA 3 @@ -72,6 +97,7 @@ static const uint8_t SCL = GROVE_SCL; #define TFT_SCLK SPI_SCK_PIN #define TFT_CS 38 #define TFT_DC 37 +#define TOUCH_CS -1 #define SPI_FREQUENCY 80000000 #define SPI_READ_FREQUENCY 20000000 @@ -162,7 +188,6 @@ static const uint8_t RX = SERIAL_RX; #define FM_SI4713 // RFID -#define HAS_ST25R3916 #define RFID125_RX_PIN SERIAL_RX #define RFID125_TX_PIN SERIAL_TX @@ -198,6 +223,14 @@ static const uint8_t RX = SERIAL_RX; #define SENSOR_INT 8 #define NFC_CS 39 +// NFC ST25R3916 +#define HAS_ST25R3916 +#define ST25R_MISO SPI_MISO_PIN +#define ST25R_MOSI SPI_MOSI_PIN +#define ST25R_SCLK SPI_SCK_PIN +#define ST25R_CS 39 +#define ST25R_IRQ 5 + // Audio ES8311 #define AUDIO_I2S_WS 18 #define AUDIO_I2S_SCK 11 diff --git a/boards/m5stack-cardputer/interface.cpp b/boards/m5stack-cardputer/interface.cpp index 07c734aaf..b9af35fab 100644 --- a/boards/m5stack-cardputer/interface.cpp +++ b/boards/m5stack-cardputer/interface.cpp @@ -130,6 +130,18 @@ void _post_setup_gpio() { bruceConfigPins.gps_bus.tx = (gpio_num_t)13; bruceConfigPins.gpsBaudrate = 115200; + bruceConfigPins.CC1101_bus.sck = (gpio_num_t)40; + bruceConfigPins.CC1101_bus.miso = (gpio_num_t)39; + bruceConfigPins.CC1101_bus.mosi = (gpio_num_t)14; + bruceConfigPins.CC1101_bus.cs = (gpio_num_t)13; + bruceConfigPins.CC1101_bus.io0 = (gpio_num_t)5; + + bruceConfigPins.NRF24_bus.sck = (gpio_num_t)40; + bruceConfigPins.NRF24_bus.miso = (gpio_num_t)39; + bruceConfigPins.NRF24_bus.mosi = (gpio_num_t)14; + bruceConfigPins.NRF24_bus.cs = (gpio_num_t)6; + bruceConfigPins.NRF24_bus.io0 = (gpio_num_t)4; + tca.matrix(7, 8); tca.flush(); pinMode(11, INPUT); diff --git a/boards/m5stack-core/interface.cpp b/boards/m5stack-core/interface.cpp index cb75af22f..ccd131c5f 100644 --- a/boards/m5stack-core/interface.cpp +++ b/boards/m5stack-core/interface.cpp @@ -50,8 +50,11 @@ void InputHandler(void) { if (anyPressed && wakeUpScreen()) return; AnyKeyPress = anyPressed; + if (aPressed && cPressed) { + EscPress = true; + return; + } PrevPress = aPressed; - EscPress = aPressed; NextPress = cPressed; SelPress = bPressed; } diff --git a/boards/m5stack-core/m5stack-core.ini b/boards/m5stack-core/m5stack-core.ini index 1a546ee76..2d3ed3219 100644 --- a/boards/m5stack-core/m5stack-core.ini +++ b/boards/m5stack-core/m5stack-core.ini @@ -156,6 +156,8 @@ extends=m5stack-core_base lib_ignore = ${env_4mb.lib_ignore} + ST25R3916-fork + NFC-RFAL-fork build_flags = ${m5stack-core_base.build_flags} diff --git a/boards/m5stack-cplus1_1/interface.cpp b/boards/m5stack-cplus1_1/interface.cpp index 4d5ae3c6e..92d211f26 100644 --- a/boards/m5stack-cplus1_1/interface.cpp +++ b/boards/m5stack-cplus1_1/interface.cpp @@ -53,8 +53,11 @@ void InputHandler(void) { if (anyPressed && wakeUpScreen()) return; AnyKeyPress = anyPressed; + if (upPressed && dwPressed) { + EscPress = true; + return; + } PrevPress = upPressed; - EscPress = upPressed; NextPress = dwPressed; SelPress = selPressed; } diff --git a/boards/m5stack-cplus2/interface.cpp b/boards/m5stack-cplus2/interface.cpp index 6a5c4097f..af6e22f45 100644 --- a/boards/m5stack-cplus2/interface.cpp +++ b/boards/m5stack-cplus2/interface.cpp @@ -68,8 +68,11 @@ void InputHandler(void) { if (anyPressed && wakeUpScreen()) return; AnyKeyPress = anyPressed; + if (upPressed && dwPressed) { + EscPress = true; + return; + } PrevPress = upPressed; - EscPress = upPressed; NextPress = dwPressed; SelPress = selPressed; } diff --git a/boards/m5stack-dinmeter/interface.cpp b/boards/m5stack-dinmeter/interface.cpp index aefeda289..6d75dad39 100644 --- a/boards/m5stack-dinmeter/interface.cpp +++ b/boards/m5stack-dinmeter/interface.cpp @@ -80,6 +80,12 @@ void InputHandler(void) { SelPress = true; tm = millis(); } + + if (PrevPress && SelPress) { + EscPress = true; + SelPress = false; + PrevPress = false; + } } /********************************************************************* diff --git a/boards/pinouts/pins_arduino.h b/boards/pinouts/pins_arduino.h index def219d4b..02bd4f5bd 100644 --- a/boards/pinouts/pins_arduino.h +++ b/boards/pinouts/pins_arduino.h @@ -52,6 +52,14 @@ #include "../ESP32-C5-tft/pins_arduino.h" #elif ESP32C5_DEVKITC_1 #include "../ESP32-C5/pins_arduino.h" +#elif ESP32S3_ILI9341 +#include "../esp32s3-ili9341/pins_arduino.h" +#elif ESP32S3_ST7789 +#include "../esp32s3-st7789/pins_arduino.h" +#elif ESP_WROOM32_ILI9341 +#include "../esp-wroom32-ili9341/pins_arduino.h" +#elif ESP_WROOM32_ST7789 +#include "../esp-wroom32-st7789/pins_arduino.h" #elif NM_CYD_ESP32C5 #include "../nm-cyd-c5/pins_arduino.h" #endif diff --git a/boards/reaper/interface.cpp b/boards/reaper/interface.cpp index 94255524f..ef60b04f2 100644 --- a/boards/reaper/interface.cpp +++ b/boards/reaper/interface.cpp @@ -21,8 +21,11 @@ XPowersPPM PPM; #define BATTERY_DESIGN_CAPACITY 1000 #include BQ27220 bq; +bool gaugeOn = false; void _setup_gpio() { + Wire.setPins(GROVE_SDA, GROVE_SCL); + Wire.begin(GROVE_SDA, GROVE_SCL); pinMode(UP_BTN, INPUT); // Sets the power btn as an INPUT pinMode(SEL_BTN, INPUT); @@ -45,13 +48,10 @@ void _setup_gpio() { pinMode(SDCARD_CS, OUTPUT); digitalWrite(SDCARD_CS, HIGH); - Wire.setPins(GROVE_SDA, GROVE_SCL); - // Wire.begin(); - // bruceConfig.rfModule = CC1101_SPI_MODULE; - // bruceConfig.irRx = RXLED; - // bruceConfig.irTx = LED; - Wire.begin(GROVE_SDA, GROVE_SCL); - + bruceConfigPins.rfModule = CC1101_SPI_MODULE; + bruceConfigPins.irRx = RXLED; + bruceConfigPins.irTx = TXLED; + bruceConfigPins.rfidModule = ST25R3916_SPI_MODULE; bool pmu_ret = false; pmu_ret = PPM.init(Wire, GROVE_SDA, GROVE_SCL, BQ25896_SLAVE_ADDRESS); @@ -72,7 +72,11 @@ void _setup_gpio() { // PPM.enableInputDetection(); PPM.enableCharge(); } - if (bq.getDesignCap() != BATTERY_DESIGN_CAPACITY) { bq.setDesignCap(BATTERY_DESIGN_CAPACITY); } + Wire.beginTransmission(BQ27220_I2C_ADDRESS); + if (Wire.endTransmission() == 0) { + if (bq.getDesignCap() != BATTERY_DESIGN_CAPACITY) { bq.setDesignCap(BATTERY_DESIGN_CAPACITY); } + gaugeOn = true; + } } /*************************************************************************************** @@ -84,7 +88,7 @@ void _setup_gpio() { int getBattery() { int percent = 0; #if defined(USE_BQ27220_VIA_I2C) - percent = bq.getChargePcnt(); + if (gaugeOn) percent = bq.getChargePcnt(); #endif return (percent < 0) ? 0 : (percent >= 100) ? 100 : percent; @@ -92,7 +96,7 @@ int getBattery() { #ifdef USE_BQ27220_VIA_I2C bool isCharging() { - return bq.getIsCharging(); // Return the charging status from BQ27220 + return gaugeOn ? bq.getIsCharging() : false; // Return the charging status from BQ27220 } #else bool isCharging() { return false; } diff --git a/boards/reaper/pins_arduino.h b/boards/reaper/pins_arduino.h index 02eb2d488..64394d384 100644 --- a/boards/reaper/pins_arduino.h +++ b/boards/reaper/pins_arduino.h @@ -60,11 +60,11 @@ static const uint8_t SCK = SPI_SCK_PIN; // NRF24 #define USE_NRF24_VIA_SPI -#define NRF24_CE_PIN 14 -#define NRF24_SS_PIN 13 -#define NRF24_MOSI_PIN SPI_MOSI_PIN -#define NRF24_SCK_PIN SPI_SCK_PIN -#define NRF24_MISO_PIN SPI_MISO_PIN +#define NRF24_CE_PIN -1 +#define NRF24_SS_PIN -1 +#define NRF24_MOSI_PIN -1 +#define NRF24_SCK_PIN -1 +#define NRF24_MISO_PIN -1 // FONT SIZE #define FP 1 @@ -119,8 +119,6 @@ static const uint8_t SCK = SPI_SCK_PIN; #define XPOWERS_CHIP_BQ25896 - - // Fuel Gauge #define USE_BQ27220_VIA_I2C #define BQ27220_I2C_ADDRESS 0x55 @@ -153,11 +151,16 @@ static const uint8_t SCK = SPI_SCK_PIN; #define LORA_MISO SPI_MISO_PIN #define LORA_MOSI SPI_MOSI_PIN #define LORA_CS 4 -#define LORA_RST 43///OR 44 +#define LORA_RST 43 /// OR 44 #define LORA_BUSY 5 - #define LORA_IRQ 42 - +// NFC ST25R3916 +#define HAS_ST25R3916 +#define ST25R_MISO SPI_MISO_PIN +#define ST25R_MOSI SPI_MOSI_PIN +#define ST25R_SCLK SPI_SCK_PIN +#define ST25R_CS 11 +#define ST25R_IRQ 12 #endif /* Pins_Arduino_h */ diff --git a/boards/reaper/reaper.ini b/boards/reaper/reaper.ini index 5e8ba0e09..ff62eb66a 100644 --- a/boards/reaper/reaper.ini +++ b/boards/reaper/reaper.ini @@ -7,16 +7,15 @@ build_flags = -Iboards/reaper -Os -DARDUINO_USB_MODE=1 - -DCORE_DEBUG_LEVEL=5 -DBUZZ_PIN=10 -D USE_HSPI_PORT=1 -DDEVICE_NAME='"reaper board"' ;Infrared Led default pin and state - -DIR_TX_PINS='{{"M5 IR Mod", GROVE_SDA}, {"Grove W", GROVE_SCL}, {"Grove Y", GROVE_SDA}, {"GPIO 5", 5}, {"GPIO 2", 2}}' - -DIR_RX_PINS='{{"M5 IR Mod", GROVE_SCL}, {"Grove W", GROVE_SCL}, {"Grove Y", GROVE_SDA}, {"GPIO 21", 21}, {"GPIO 1", 1}}' - -DRF_TX_PINS='{{"M5 RF433T", GROVE_SDA}, {"Grove W", GROVE_SCL}, {"Grove Y", GROVE_SDA}, {"GPIO 1", 1}, {"GPIO 2", 2}}' - -DRF_RX_PINS='{{"M5 RF433R", GROVE_SCL}, {"Grove W", GROVE_SCL}, {"Grove Y", GROVE_SDA}, {"GPIO 1", 1}, {"GPIO 2", 2}}' + -DIR_TX_PINS='{{"GPIO 4", 4}, {"GPIO 5", 5}, {"GPIO 42", 42}}' + -DIR_RX_PINS='{{"GPIO 4", 4}, {"GPIO 5", 5}, {"GPIO 42", 42}}' + -DRF_TX_PINS='{{"GPIO 4", 4}, {"GPIO 5", 5}, {"GPIO 42", 42}}' + -DRF_RX_PINS='{{"GPIO 4", 4}, {"GPIO 5", 5}, {"GPIO 42", 42}}' lib_deps = diff --git a/docker/run_all_envs.sh b/docker/run_all_envs.sh index 27f6a573d..adf3641aa 100644 --- a/docker/run_all_envs.sh +++ b/docker/run_all_envs.sh @@ -46,6 +46,7 @@ DEFAULT_ENVS=( elecrow-28B elecrow-35B elecrow-35Bv2_2 + elecrow-advance-35-s3 LAUNCHER_elecrow-24B LAUNCHER_elecrow-28B LAUNCHER_elecrow-35B diff --git a/embedded_resources/web_interface/index.css b/embedded_resources/web_interface/index.css index 853014a5d..bda7b42a4 100644 --- a/embedded_resources/web_interface/index.css +++ b/embedded_resources/web_interface/index.css @@ -527,3 +527,58 @@ svg > path { transform: rotate(360deg); } } + +@media (max-width: 600px) { + body { + padding: 6px; + } + .container .header { + flex-wrap: wrap; + } + .container .header .title { + flex: 1 1 100%; + border-bottom: 1px solid var(--color); + } + .left-part, + .right-part { + text-align: center; + } + .container .header button { + width: auto; + min-width: 90px; + } + .container .free-space { + flex-direction: column; + } + .container .free-space .block-space { + width: 100%; + border-right: 0; + border-bottom: 1px solid var(--color); + } + .container .free-space .block-space:last-child { + border-bottom: 0; + } + .dialog .dialog-body input { + min-width: 0; + } + .dialog.upload .upload-loading { + width: 100%; + } + .upload-area::before { + width: auto; + height: auto; + max-width: 90vw; + font-size: 18px; + padding: 16px; + } + .loading-area .text { + min-width: 0; + max-width: 90vw; + } + .container .content .table .col-action { + width: 96px; + } + .container .content .table .col-size { + width: 64px; + } +} diff --git a/lib/HAL/io_expander/PCA9555.cpp b/lib/HAL/io_expander/PCA9555.cpp index 1b0f508a3..96a27b123 100644 --- a/lib/HAL/io_expander/PCA9555.cpp +++ b/lib/HAL/io_expander/PCA9555.cpp @@ -11,14 +11,15 @@ bool PCA9555::begin(uint8_t address, TwoWire *wire) { _i2caddr = address; _wire = wire; - output_state[0] = 0; - output_state[1] = 0; - direction_state[0] = 0xFF; // all as inputs - direction_state[1] = 0xFF; - - // test communication - uint8_t dummy; - return readRegister(PCA9555_INPUT_PORT0, dummy); + uint8_t dummy = 0; + if (!readRegister(PCA9555_INPUT_PORT0, dummy)) return false; + + if (!readRegister(PCA9555_OUTPUT_PORT0, output_state[0])) output_state[0] = 0; + if (!readRegister(PCA9555_OUTPUT_PORT0 + 1, output_state[1])) output_state[1] = 0; + if (!readRegister(PCA9555_CONFIG_PORT0, direction_state[0])) direction_state[0] = 0xFF; + if (!readRegister(PCA9555_CONFIG_PORT0 + 1, direction_state[1])) direction_state[1] = 0xFF; + + return true; } bool PCA9555::outputGPIO(uint16_t pins) { diff --git a/lib/HAL/io_expander/io_expander.h b/lib/HAL/io_expander/io_expander.h index 5e15a53c1..341adadb3 100644 --- a/lib/HAL/io_expander/io_expander.h +++ b/lib/HAL/io_expander/io_expander.h @@ -35,6 +35,9 @@ #ifndef IO_EXP_NRF // Used in C5 #define IO_EXP_NRF -1 #endif +#ifndef IO_EXP_NFC // Used in T-LoraPager +#define IO_EXP_NFC -1 +#endif // Button pins (likely inputs on the expander) #ifndef IO_EXP_UP @@ -62,6 +65,12 @@ class io_expander : public IO_EXP_CLASS { private: bool _started = false; + void output(int8_t pin, bool val) { + if (pin < 0 || pin > 15) return; + IO_EXP_CLASS::pinMode(static_cast(pin), OUTPUT); + IO_EXP_CLASS::digitalWrite(static_cast(pin), val); + } + static void clearInterruptBit(uint16_t &mask, int8_t pin) { if (pin >= 0 && pin <= 15) { mask = static_cast(mask & ~(static_cast(1u) << static_cast(pin))); @@ -84,15 +93,14 @@ class io_expander : public IO_EXP_CLASS { _started = begin(a, _w); if (!_started) return false; - configureDirection(0xFFFF); // All outputs initially - - turnPinOnOff(IO_EXP_GPS, LOW); // SMOOOCHIE||REAPER - turnPinOnOff(IO_EXP_MIC, LOW); // SMOOOCHIE - turnPinOnOff(IO_EXP_VIBRO, LOW); // SMOOOCHIE||REAPER - turnPinOnOff(IO_EXP_CC_RX, LOW); // SMOOOCHIE||REAPER - turnPinOnOff(IO_EXP_CC_TX, LOW); // SMOOOCHIE||REAPER - turnPinOnOff(IO_EXP_LOGO, HIGH); // BRUCE LOGO LEAD ON REAPER - turnPinOnOff(IO_EXP_NRF, HIGH); // NRF ON BY DEFAULT FOR C5 + output(IO_EXP_GPS, LOW); // SMOOOCHIE||REAPER + output(IO_EXP_MIC, LOW); // SMOOCHIE + output(IO_EXP_VIBRO, LOW); // SMOOCHIE||REAPER + output(IO_EXP_CC_RX, LOW); // SMOOCHIE||REAPER + output(IO_EXP_CC_TX, LOW); // SMOOCHIE||REAPER + output(IO_EXP_LOGO, HIGH); // BRUCE LOGO LED ON REAPER + output(IO_EXP_NRF, HIGH); // NRF ON BY DEFAULT FOR C5 + output(IO_EXP_NFC, HIGH); // ST25R NFC chip power on Lilygo T-LoraPager // Set button pins as inputs button(IO_EXP_UP); @@ -110,7 +118,7 @@ class io_expander : public IO_EXP_CLASS { void turnPinOnOff(int8_t pin, bool val) { if (!_started) return; - return pin >= 0 ? IO_EXP_CLASS::digitalWrite(pin, val) : delay(0); + output(pin, val); } void setPinDirection(uint8_t pin, uint8_t mode) { diff --git a/patch_library_conflicts.py b/patch_library_conflicts.py index 8a4e15c47..34a12b7f9 100644 --- a/patch_library_conflicts.py +++ b/patch_library_conflicts.py @@ -41,6 +41,46 @@ r'#include ', '#include \n#include ' ), + # ST25R3916-fork: support IRQ-less operation (I2C units have no interrupt + # line). When int_pin < 0 we skip attaching the GPIO interrupt and poll the + # chip's IRQ register over the bus instead of gating on digitalRead(int_pin). + # The fork is pinned to a fixed commit in platformio.ini so these patterns + # stay valid. + ( + ".pio/libdeps/*/ST25R3916-fork/src/rfal_rfst25r3916.cpp", + r'(?s)pinMode\(int_pin, INPUT\);.*?attachInterrupt\(int_pin, irq_handler, RISING\);', + 'if (int_pin >= 0) {\n' + ' pinMode(int_pin, INPUT);\n' + ' Callback::func = std::bind(&RfalRfST25R3916Class::setISRPending, this);\n' + ' irq_handler = static_cast(Callback::callback);\n' + ' attachInterrupt(int_pin, irq_handler, RISING);\n' + ' }' + ), + ( + ".pio/libdeps/*/ST25R3916-fork/src/rfal_rfst25r3916.cpp", + r'detachInterrupt\(int_pin\);', + 'if (int_pin >= 0) { detachInterrupt(int_pin); }' + ), + ( + ".pio/libdeps/*/ST25R3916-fork/src/rfal_rfst25r3916.cpp", + r'return \(isr_pending \|\| \(digitalRead\(int_pin\) == HIGH\)\);', + 'return (int_pin < 0) ? true : (isr_pending || (digitalRead(int_pin) == HIGH));' + ), + # In polling mode read the IRQ registers exactly once per call (the read + # auto-clears them on the chip); in IRQ mode keep draining while the pin is + # high. Converts the edge-drain while-loop into a do/while. + ( + ".pio/libdeps/*/ST25R3916-fork/src/st25r3916_interrupt.cpp", + r'(?s)while \(digitalRead\(int_pin\) == HIGH\) \{.*?iregs\[3\] << 24;\s*\}', + 'do {\n' + ' st25r3916ReadMultipleRegisters(ST25R3916_REG_IRQ_MAIN, iregs, ST25R3916_INT_REGS_LEN);\n' + '\n' + ' irqStatus |= (uint32_t)iregs[0];\n' + ' irqStatus |= (uint32_t)iregs[1] << 8;\n' + ' irqStatus |= (uint32_t)iregs[2] << 16;\n' + ' irqStatus |= (uint32_t)iregs[3] << 24;\n' + ' } while (int_pin >= 0 && digitalRead(int_pin) == HIGH);' + ), ] for file_pattern, search, replace in conflicts: diff --git a/platformio.ini b/platformio.ini index fa639618e..8c11c4d14 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = ;m5stack-dinmeter ;arduino-nesso-n1 ;esp32-s3-devkitc-1 - ;CYD-2432S028 + ;CYD-2432S ;CYD-2USB ;CYD-2432W328C ;CYD-2432W328C_2 @@ -66,6 +66,11 @@ default_envs = ;LAUNCHER_WaveSentry-R1 ;WaveSentry-R1 ;esp32-c5-tft + ;esp32s3-st7789 + ;esp-wroom32-st7789 + ;esp-wroom32-ili9341 + ;esp32-c5 + ;ES3C28P ;esp32-c5 ;ES3C28P ;nm-cyd-c5 @@ -73,6 +78,7 @@ default_envs = ;elecrow-28B ;elecrow-35B ;elecrow-35Bv2_2 + ;elecrow-advance-35-s3 ;uncomment to not use global dirs to avoid possible conflicts @@ -82,9 +88,9 @@ build_cache_dir = .pio/buildcache cache_dir = .pio/cache boards_dir = boards/_boards_json -extra_configs = - boards/*.ini - boards/*/*.ini +;extra_configs = +; boards/*.ini +; boards/*/*.ini [env] platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip ; Arduino 3.3.6 @@ -146,17 +152,6 @@ build_flags = -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 ; -DCONFIG_ASYNC_TCP_USE_WDT=0 ; Disables the Watchdog timer on Async TCP, to avoid restartings during file uploads -DGEN_MQJS_HEADERS ; Enable generation of mquickjs headers - ; mbedtls settings for crypto operations (WhisperPair/audio exploits) - -DMBEDTLS_CONFIG_FILE=\"mbedtls/esp_config.h\" - -DCONFIG_MBEDTLS_ECDH_C=1 - -DCONFIG_MBEDTLS_AES_C=1 - -DCONFIG_MBEDTLS_CTR_DRBG_C=1 - ; Additional mbedtls flags that could be useful: - ;-DCONFIG_MBEDTLS_ECP_C=1 - ;-DCONFIG_MBEDTLS_SHA256_C=1 - ;-DCONFIG_MBEDTLS_SHA512_C=1 - ;-DCONFIG_MBEDTLS_HKDF_C=1 - ;-DCONFIG_MBEDTLS_GCM_C=1 extra_scripts = pre:patch.py @@ -171,7 +166,7 @@ lib_deps = ;WireGuard-ESP32 https://github.com/bmorcelli/IRremoteESP8266 ;IRremoteESP8266 - paulstoffregen/Time@1.6.1 + Time LibSSH-ESP32 Adafruit BusIO=https://github.com/emericklaw/Adafruit-BusIO_Bruce@1.17.2-bruce.1 Adafruit PN532=https://github.com/emericklaw/Adafruit-PN532_Bruce@1.3.3-bruce.1 @@ -183,11 +178,10 @@ lib_deps = https://github.com/whywilson/ESP-PN532BLE https://github.com/whywilson/ESP-PN532-UART https://github.com/whywilson/ESP-PN532Killer - NTPClient@3.2.1 - ESP32Time@2.0.6 + NTPClient + ESP32Time FFat bblanchon/ArduinoJson - https://github.com/bmorcelli/rc-switch ESP8266Audio@^2.4.1 earlephilhower/ESP8266SAM@^1.1.0 mikalhart/TinyGPSPlus @@ -197,7 +191,7 @@ lib_deps = Adafruit Si4713 Library@1.2.3 Bodmer/JPEGDecoder https://github.com/bmorcelli/SmartRC-CC1101-Driver-Lib/ - mquickjs=https://github.com/BruceDevices/mquickjs#0.0.6 + mquickjs=https://github.com/BruceDevices/mquickjs#0.0.4 ;paulstoffregen/OneWire@^2.3.8 https://github.com/bmorcelli/OneWire#patch-1 ;jackjansen/esp32_idf5_https_server_compat @@ -208,6 +202,10 @@ lib_deps = ESP32Async/ESPAsyncWebServer fastled/FastLED @^3.10.3 jgromes/RadioLib @ ^7.4.0 + ; Pinned to a fixed commit so upstream changes never break our IRQ-less I2C + ; polling patch (applied by patch_library_conflicts.py). + https://github.com/lewisxhe/ST25R3916-fork#0c8e00f49d12881d1cff6d6f0879b90ce7ed4033 + https://github.com/lewisxhe/NFC-RFAL-fork [env_light] extends = env @@ -238,7 +236,7 @@ lib_deps = ;WireGuard-ESP32 https://github.com/BorisKofman/IRremoteESP8266#Espressif-version-3 ;IRremoteESP8266 - paulstoffregen/Time@1.6.1 + Time ;LibSSH-ESP32 Adafruit BusIO=https://github.com/emericklaw/Adafruit-BusIO_Bruce@1.17.2-bruce.1 Adafruit PN532=https://github.com/emericklaw/Adafruit-PN532_Bruce@1.3.3-bruce.1 @@ -249,11 +247,12 @@ lib_deps = ;https://github.com/bmorcelli/ESP-PN532BLE ;https://github.com/whywilson/ESP-PN532-UART@^0.0.2 ;https://github.com/whywilson/ESP-PN532Killer#079790e - NTPClient@3.2.1 - ESP32Time@2.0.6 + NTPClient + ESP32Time FFat - bblanchon/ArduinoJson@7.4.2 + bblanchon/ArduinoJson https://github.com/bmorcelli/rc-switch + bblanchon/ArduinoJson@7.4.2 ;ESP8266Audio@^2.4.1 ;earlephilhower/ESP8266SAM@^1.1.0 mikalhart/TinyGPSPlus@1.1.0 @@ -263,19 +262,28 @@ lib_deps = ;Adafruit Si4713 Library@1.2.3 Bodmer/JPEGDecoder https://github.com/bmorcelli/SmartRC-CC1101-Driver-Lib/ - ;mquickjs=https://github.com/BruceDevices/mquickjs#0.0.6 + ;mquickjs=https://github.com/BruceDevices/mquickjs#0.0.4 ;paulstoffregen/OneWire@^2.3.8 ;https://github.com/bmorcelli/OneWire#patch-1 ;https://github.com/bmorcelli/ESP32-PsRamFS#patch-1 ;bitbank2/AnimatedGIF ;bitbank2/PNGdec @ ^1.1.2 - ESP32Async/AsyncTCP@3.4.10 - ESP32Async/ESPAsyncWebServer@3.9.6 + ESP32Async/ESPAsyncWebServer ;https://github.com/bmorcelli/FastLED +; PlatformIO Project Configuration File +; ESP-WROOM-32 + ST7789 2.8" TFT with XPT2046 Touch +; +; TFT Pins (VSPI): MISO=19, MOSI=23, SCLK=18, CS=17, DC=16, RST=5, BL=32 +; Touch (XPT2046): CS=21, shares VSPI bus (MISO=19, MOSI=23, SCLK=18) +; SD Card: CS=12, shares VSPI bus -[env_4mb] -extends = env +[env:esp-wroom32-st7789] +board = esp-wroom32-st7789 +board_build.partitions = custom_4Mb_full.csv +build_src_filter = ${env.build_src_filter} +<../boards/esp-wroom32-st7789> build_flags = + ${env.build_flags} + -Iboards/esp-wroom32-st7789 -Os -DCORE_DEBUG_LEVEL=0 -DCONFIG_ESP32_JTAG_SUPPORT_DISABLE=1 @@ -297,7 +305,122 @@ build_flags = -DDECODE_PANASONIC=true -DSEND_RAW=true -lib_ignore = - FastLED - ESP8266Audio - ESP8266SAM + -DDEVICE_NAME='"ESP32-ST7789 Touch"' + + ; Boot button as OK + -DHAS_BTN=1 + -DBTN_ALIAS='"OK"' + -DBTN_PIN=0 + -DBTN_ACT=LOW + + ; Infrared Led + -DIR_TX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + -DIR_RX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + -DTXLED=2 + -DLED_ON=HIGH + -DLED_OFF=LOW + + ; Radio Frequency (one pin modules) pin setting + -DRF_TX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + -DRF_RX_PINS='{ {"G4", 4}, {"G2", 2}, {"G25", 25} }' + + ; CC1101 SPI connection pins + -DUSE_CC1101_VIA_SPI + -DCC1101_GDO0_PIN=2 + -DCC1101_SS_PIN=15 + -DCC1101_MOSI_PIN=SPI_MOSI_PIN + -DCC1101_SCK_PIN=SPI_SCK_PIN + -DCC1101_MISO_PIN=SPI_MISO_PIN + + ; NRF24 SPI connection pins + -DUSE_NRF24_VIA_SPI + -DNRF24_CE_PIN=4 + -DNRF24_SS_PIN=15 + -DNRF24_MOSI_PIN=SPI_MOSI_PIN + -DNRF24_SCK_PIN=SPI_SCK_PIN + -DNRF24_MISO_PIN=SPI_MISO_PIN + + ; W5500 Ethernet (disabled by default) + ;-DUSE_W5500_VIA_SPI + -DW5500_SS_PIN=-1 + -DW5500_MOSI_PIN=SPI_MOSI_PIN + -DW5500_SCK_PIN=SPI_SCK_PIN + -DW5500_MISO_PIN=SPI_MISO_PIN + -DW5500_INT_PIN=-1 + + ; Lora setup pins (not connected by default) + -DLORA_SCK=SPI_SCK_PIN + -DLORA_MISO=SPI_MISO_PIN + -DLORA_MOSI=SPI_MOSI_PIN + -DLORA_CS=-1 + -DLORA_RST=-1 + -DLORA_DIO0=-1 + + ; FM Radio + -DFM_RSTPIN=-1 + + ; Font sizes + -DFP=1 + -DFM=2 + -DFG=3 + + ; Screen Setup + -DHAS_SCREEN=1 + -DROTATION=1 + -DMINBRIGHT=1 + + ; TFT_eSPI display — ST7789 240x320 SPI (VSPI) + -DUSER_SETUP_LOADED=1 + -DST7789_DRIVER=1 + -DTFT_RGB_ORDER=TFT_BGR + -DTFT_WIDTH=240 + -DTFT_HEIGHT=320 + -DTFT_INVERSION_ON + -DTFT_CS=17 + -DTFT_DC=16 + -DTFT_RST=5 + -DTFT_BL=32 + -DTFT_MOSI=23 + -DTFT_SCLK=18 + -DTFT_MISO=19 + -DTOUCH_CS=-1 + -DSMOOTH_FONT=1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=20000000 + -DSPI_TOUCH_FREQUENCY=2500000 + + ; Touchscreen — XPT2046 resistive (shares VSPI bus with display) + -DHAS_TOUCH=1 + -DTOUCH_XPT2046_SPI=1 + -DXPT2046_SPI_BUS_MOSI_IO_NUM=23 + -DXPT2046_SPI_BUS_MISO_IO_NUM=19 + -DXPT2046_SPI_BUS_SCLK_IO_NUM=18 + -DXPT2046_SPI_CONFIG_CS_GPIO_NUM=21 + -DXPT2046_TOUCH_CONFIG_INT_GPIO_NUM=-1 + -DINPUT_HANDLER_TASK_STACK_SIZE=8192 + + ; SD Card pins (on VSPI bus) + -DSDCARD_CS=12 + -DSDCARD_SCK=18 + -DSDCARD_MISO=19 + -DSDCARD_MOSI=23 + + ; Default I2C port + -DGROVE_SDA=21 + -DGROVE_SCL=22 + + ; SERIAL (GPS) dedicated pins + -DSERIAL_TX=1 + -DSERIAL_RX=3 + -DGPS_SERIAL_TX=SERIAL_TX + -DGPS_SERIAL_RX=SERIAL_RX + + ; Main SPI bus pins (for CC1101/NRF24/etc) + -DSPI_SCK_PIN=18 + -DSPI_MOSI_PIN=23 + -DSPI_MISO_PIN=19 + -DSPI_SS_PIN=15 + +lib_deps = + ${env.lib_deps} + diff --git a/src/core/configPins.cpp b/src/core/configPins.cpp index d390821cb..097b1101b 100644 --- a/src/core/configPins.cpp +++ b/src/core/configPins.cpp @@ -188,6 +188,18 @@ void BruceConfigPins::fromJson(JsonObject obj) { count++; log_e("Fail"); } + + if (!root["ST25R_Pins"].isNull()) { + SPIPins def = ST25R_bus; + ST25R_bus.fromJson(root["ST25R_Pins"].as()); + if (ST25R_bus.sck == GPIO_NUM_NC && def.sck != GPIO_NUM_NC) { + ST25R_bus = def; + count++; + } + } else { + count++; + log_e("Fail"); + } #endif // if (!root["sys_i2c"].isNull()) { // sys_i2c.fromJson(root["sys_i2c"].as()); @@ -253,6 +265,9 @@ void BruceConfigPins::toJson(JsonObject obj) const { JsonObject _LoRa = root["LoRa_Pins"].to(); LoRa_bus.toJson(_LoRa); + + JsonObject _ST25R = root["ST25R_Pins"].to(); + ST25R_bus.toJson(_ST25R); #endif // JsonObject _si2c = root["sys_i2c"].as(); // sys_i2c.toJson(_si2c); @@ -365,6 +380,7 @@ void BruceConfigPins::validateConfig() { validateRfidModuleValue(); validateGpsBaudrateValue(); #if !defined(LITE_VERSION) + validateSpiPins(ST25R_bus); validateSpiPins(LoRa_bus); validateSpiPins(W5500_bus); #endif @@ -387,6 +403,11 @@ void BruceConfigPins::setW5500Pins(SPIPins value) { validateSpiPins(W5500_bus); saveFile(); } +void BruceConfigPins::setSR25RPins(SPIPins value) { + ST25R_bus = value; + validateSpiPins(ST25R_bus); + saveFile(); +} #endif void BruceConfigPins::setCC1101Pins(SPIPins value) { CC1101_bus = value; @@ -525,7 +546,12 @@ void BruceConfigPins::setRfidModule(RFIDModules value) { void BruceConfigPins::validateRfidModuleValue() { if (rfidModule != M5_RFID2_MODULE && rfidModule != PN532_I2C_MODULE && rfidModule != PN532_SPI_MODULE && - rfidModule != RC522_SPI_MODULE && rfidModule != PN532_I2C_SPI_MODULE) { + rfidModule != RC522_SPI_MODULE && rfidModule != PN532_I2C_SPI_MODULE +#if !defined(LITE_VERSION) + && rfidModule != ST25R3916_SPI_MODULE + && rfidModule != ST25R3916_I2C_MODULE +#endif + ) { rfidModule = M5_RFID2_MODULE; } } diff --git a/src/core/configPins.h b/src/core/configPins.h index d7ebd4b1e..4ab38d79b 100644 --- a/src/core/configPins.h +++ b/src/core/configPins.h @@ -15,7 +15,8 @@ enum RFIDModules { PN532_SPI_MODULE = 2, RC522_SPI_MODULE = 3, ST25R3916_SPI_MODULE = 4, - PN532_I2C_SPI_MODULE = 5 + PN532_I2C_SPI_MODULE = 5, + ST25R3916_I2C_MODULE = 6, }; enum RFModules { @@ -42,6 +43,12 @@ class BruceConfigPins { obj["rx"] = rx; obj["tx"] = tx; } + + bool checkConflict(int8_t p) { + gpio_num_t pin = (gpio_num_t)p; + if (rx == pin || tx == pin) return true; + return false; + } }; struct I2CPins { @@ -61,6 +68,12 @@ class BruceConfigPins { obj["sda"] = sda; obj["scl"] = scl; } + + bool checkConflict(int8_t p) { + gpio_num_t pin = (gpio_num_t)p; + if (sda == pin || scl == pin) return true; + return false; + } }; struct SPIPins { @@ -99,7 +112,7 @@ class BruceConfigPins { obj["io2"] = io2; } - bool checkConflict(uint8_t p) { + bool checkConflict(int8_t p) { gpio_num_t pin = (gpio_num_t)p; if (sck == pin || miso == pin || mosi == pin || cs == pin) return true; return false; @@ -147,6 +160,19 @@ class BruceConfigPins { SPIPins PN532_bus; #endif +#ifdef ST25R_SCLK + SPIPins ST25R_bus = { + (gpio_num_t)ST25R_SCLK, + (gpio_num_t)ST25R_MISO, + (gpio_num_t)ST25R_MOSI, + (gpio_num_t)ST25R_CS, + (gpio_num_t)ST25R_IRQ, + GPIO_NUM_NC + }; +#else + SPIPins ST25R_bus; +#endif + #ifdef SDCARD_SCK SPIPins SDCARD_bus = { (gpio_num_t)SDCARD_SCK, (gpio_num_t)SDCARD_MISO, (gpio_num_t)SDCARD_MOSI, (gpio_num_t)SDCARD_CS @@ -237,6 +263,7 @@ class BruceConfigPins { void setPn532Pins(SPIPins value); void setSDCardPins(SPIPins value); #if !defined(LITE_VERSION) + void setSR25RPins(SPIPins value); void setLoRaPins(SPIPins value); void setW5500Pins(SPIPins value); #endif diff --git a/src/core/display.cpp b/src/core/display.cpp index aebbf901a..6b41ceae5 100644 --- a/src/core/display.cpp +++ b/src/core/display.cpp @@ -568,10 +568,12 @@ int loopOptions( displayScrollingText(txt, coord); } - // Checks ESC Press first, to not exit after PrevPress is processed - // PrevPress condition is a StickCPlus workaround, as it uses the same button for Prev and Esc - // Same happens to Core and some other boards +// Checks ESC Press first, to not exit after PrevPress is processed +// PrevPress condition is a StickCPlus workaround, as it uses the same button for Prev and Esc +// Same happens to Core and some other boards +#ifdef HAS_3_BUTTONS if (EscPress && PrevPress) EscPress = false; +#endif if (menuType != MENU_TYPE_MAIN && check(EscPress)) { index = -1; break; @@ -826,22 +828,22 @@ void drawStatusBar() { } int iconCount = 0; - bool showSD = sdcardMounted; - bool showGPS = gpsConnected; + bool showSD = sdcardMounted; + bool showGPS = gpsConnected; bool showWifi = (WiFi.getMode() != 0); - bool showWeb = isWebUIActive; - bool showBLE = BLEConnected; - bool showWG = isConnectedWireguard; - if (showSD) iconCount++; - if (showGPS) iconCount++; + bool showWeb = isWebUIActive; + bool showBLE = BLEConnected; + bool showWG = isConnectedWireguard; + if (showSD) iconCount++; + if (showGPS) iconCount++; if (showWifi) iconCount++; - if (showWeb) iconCount++; - if (showBLE) iconCount++; - if (showWG) iconCount++; + if (showWeb) iconCount++; + if (showBLE) iconCount++; + if (showWG) iconCount++; if (iconCount > 0) { - const int IW = 16; - const int IH = 16; + const int IW = 16; + const int IH = 16; const int GAP = 6; int totalW = iconCount * IW + (iconCount - 1) * GAP; int sx = (tftWidth - totalW) / 2; diff --git a/src/core/menu_items/BleMenu.cpp b/src/core/menu_items/BleMenu.cpp index da9e7856f..f13169e38 100644 --- a/src/core/menu_items/BleMenu.cpp +++ b/src/core/menu_items/BleMenu.cpp @@ -7,6 +7,8 @@ #include "modules/ble/ble_spam.h" #if !defined(LITE_VERSION) #include "modules/ble/BLE_Suite.h" +#else +#include "modules/ble/ble_sniffer.h" #endif #include @@ -41,6 +43,8 @@ void BleMenu::optionsMenu() { #if !defined(LITE_VERSION) options.push_back({"BLE Suite", [=]() { BleSuiteMenu(); }}); options.push_back({"Ninebot", [=]() { BLENinebot(); }}); +#else + options.push_back({"BLE Sniffer", [=]() { BLE_SnifferMenu(); }}); #endif addOptionToMainMenu(); diff --git a/src/core/menu_items/ConfigMenu.cpp b/src/core/menu_items/ConfigMenu.cpp index 49d321096..5ed77a35e 100644 --- a/src/core/menu_items/ConfigMenu.cpp +++ b/src/core/menu_items/ConfigMenu.cpp @@ -266,6 +266,7 @@ void ConfigMenu::devMenu() { {"NRF24 Pins", [this]() { setSPIPinsMenu(bruceConfigPins.NRF24_bus); } }, #if !defined(LITE_VERSION) {"LoRa Pins", [this]() { setSPIPinsMenu(bruceConfigPins.LoRa_bus); } }, + {"ST25R3916 Pins", [this]() { setSPIPinsMenu(bruceConfigPins.ST25R_bus); } }, {"W5500 Pins", [this]() { setSPIPinsMenu(bruceConfigPins.W5500_bus); } }, #endif {"SDCard Pins", [this]() { setSPIPinsMenu(bruceConfigPins.SDCARD_bus); }}, diff --git a/src/core/menu_items/RFIDMenu.cpp b/src/core/menu_items/RFIDMenu.cpp index 1c2872ece..bc43e9cb8 100644 --- a/src/core/menu_items/RFIDMenu.cpp +++ b/src/core/menu_items/RFIDMenu.cpp @@ -62,6 +62,10 @@ void RFIDMenu::optionsMenu() { #endif else if (bruceConfigPins.rfidModule == PN532_SPI_MODULE) txt += " (PN532-SPI)"; else if (bruceConfigPins.rfidModule == RC522_SPI_MODULE) txt += " (RC522-SPI)"; +#if !defined(LITE_VERSION) + else if (bruceConfigPins.rfidModule == ST25R3916_SPI_MODULE) txt += " (ST25R-SPI)"; + else if (bruceConfigPins.rfidModule == ST25R3916_I2C_MODULE) txt += " (ST25R-I2C)"; +#endif loopOptions(options, MENU_TYPE_SUBMENU, txt.c_str()); } diff --git a/src/core/menu_items/WifiMenu.cpp b/src/core/menu_items/WifiMenu.cpp index 7e7f9a8a8..ddc0528e5 100644 --- a/src/core/menu_items/WifiMenu.cpp +++ b/src/core/menu_items/WifiMenu.cpp @@ -19,6 +19,8 @@ #ifndef LITE_VERSION #include "modules/pwnagotchi/pwnagotchi.h" +#include "modules/wifi/channel_analyzer.h" +#include "modules/wifi/jam_detect.h" #include "modules/wifi/wifi_recover.h" #endif @@ -71,6 +73,8 @@ void WifiMenu::optionsMenu() { options.push_back({"TelNET", telnet_setup}); options.push_back({"SSH", lambdaHelper(ssh_setup, String(""))}); options.push_back({"Sniffer", sniffer_setup}); + options.push_back({"Channel Analyzer", channel_analyzer_setup}); + options.push_back({"Jam Detect", jam_detect_setup}); options.push_back({"Scan Hosts", [=]() { bool doScan = true; if (!wifiConnected) doScan = wifiConnectMenu(); diff --git a/src/core/serial_commands/cli.cpp b/src/core/serial_commands/cli.cpp index a4d447867..83bc1e8a6 100644 --- a/src/core/serial_commands/cli.cpp +++ b/src/core/serial_commands/cli.cpp @@ -7,6 +7,7 @@ #include "ir_commands.h" #include "power_commands.h" #include "rf_commands.h" +#include "rfid_commands.h" #include "screen_commands.h" #include "settings_commands.h" #include "sound_commands.h" @@ -38,6 +39,7 @@ void SerialCli::setup() { createIrCommands(&_cli); createPowerCommands(&_cli); createRfCommands(&_cli); + createRfidCommands(&_cli); createSettingsCommands(&_cli); createStorageCommands(&_cli); createUtilCommands(&_cli); diff --git a/src/core/serial_commands/rf_commands.cpp b/src/core/serial_commands/rf_commands.cpp index 90f3bd703..6a2e28543 100644 --- a/src/core/serial_commands/rf_commands.cpp +++ b/src/core/serial_commands/rf_commands.cpp @@ -1,11 +1,17 @@ #include "rf_commands.h" #include "cJSON.h" #include "core/sd_functions.h" +#include "core/type_convertion.h" // decimalToHexString #include "helpers.h" +#include "modules/rf/protocols/rf_config.h" // RF_DEBUG +#include "modules/rf/protocols/rf_encoder.h" // rf_tx_protocol, rf_encoder_selftest +#include "modules/rf/protocols/rf_keeloq.h" // rf_keeloq_selftest +#include "modules/rf/protocols/rf_registry.h" // rf_find_protocol #include "modules/rf/rf_scan.h" #include "modules/rf/rf_send.h" #include "modules/rf/rf_utils.h" #include +#include #include uint32_t rfRxCallback(cmd *c) { @@ -24,9 +30,9 @@ uint32_t rfRxCallback(cmd *c) { String r = ""; if (raw) { - r = RCSwitch_Read(frequency, 10, true); // true -> raw mode + r = rfReceiveSignal(frequency, 10, true, true); // raw mode, headless (Serial only) } else { - r = RCSwitch_Read(frequency, 10, false); // false -> decoded mode + r = rfReceiveSignal(frequency, 10, false, true); // decoded mode, headless (Serial only) } if (r.length() == 0) return false; @@ -63,11 +69,158 @@ uint32_t rfTxCallback(cmd *c) { // check valid frequency and init the rf module if (!initRfModule("tx", float(frequency / 1000000.0))) return false; - RCSwitch_send(key, bits, te, 1, count); + rfTransmitCode(key, bits, te, 1, count); deinitRfModule(); return true; } +uint32_t rfTxByNameCallback(cmd *c) { + // Transmit by protocol NAME (registry identity), e.g. + // subghz txp CAME 433920000 12 0xA5A 0 10 + // subghz txp Linear 433920000 10 0x2A9 + // Single-line counterpart to a `.sub` replay: resolves `rf_find_protocol` + // and drives the RMT encoder directly, so it round-trips with the decoder + // (`Protocol: `). Accepts a full 64-bit hex key. + Command cmd(c); + + String name = cmd.getArgument("protocol").getValue(); + String strFrequency = cmd.getArgument("frequency").getValue(); + String strBits = cmd.getArgument("bits").getValue(); + String strKey = cmd.getArgument("key").getValue(); + String strTe = cmd.getArgument("te").getValue(); + String strRepeat = cmd.getArgument("repeat").getValue(); + + const RfProtocolDef *def = rf_find_protocol(name); + if (def == nullptr) { + serialDevice->println("unknown protocol: " + name); + return false; + } + + uint64_t key = std::stoull(strKey.c_str(), nullptr, 16); + unsigned long frequency = std::stoul(strFrequency.c_str()); + unsigned int bits = std::stoul(strBits.c_str()); + int te = strTe.length() ? (int)std::stoul(strTe.c_str()) : 0; + int repeat = strRepeat.length() ? (int)std::stoul(strRepeat.c_str()) : 10; + + if (!initRfModule("tx", float(frequency / 1000000.0))) return false; + rf_tx_protocol(key, bits, te, def, repeat); + deinitRfModule(); + return true; +} + +#if RF_DEBUG +uint32_t rfSelftestCallback(cmd *c) { + // Golden encoder self-test (spec-fidelity of the registry defs). Diagnostic + // only; compiled under RF_DEBUG. + return rf_encoder_selftest() ? true : false; +} + +uint32_t rfKeeloqTestCallback(cmd *c) { + // KeeLoq route self-test (per-manufacturer hop framing + cipher round-trip). + // Diagnostic only; compiled under RF_DEBUG. + return rf_keeloq_selftest() ? true : false; +} + +uint32_t rfKeeloqFileTestCallback(cmd *c) { + // KeeLoq round-trip driven by the real /mfcodes keystore (every manufacturer + // with its actual key + learning type). Diagnostic only; under RF_DEBUG. + return rf_keeloq_filetest() ? true : false; +} +#endif + +uint32_t rfKeeloqTxCallback(cmd *c) { + // Emit a KeeLoq rolling-code frame for a manufacturer in the keystore: + // subghz keeloqtx - -
- +

🔴 BruceShell Offline

+

IP: 192.168.4.1 | Port: 23

+
+ + + +
+
~ BruceShell\n~ Connected: Waiting for shell...
+ + )rawliteral"; - webServer.send(200, "text/html", html); + request->send(200, "text/html", html); }); - webServer.on("/execute", [&webServer, &tcpClient, &lastCommand, &shellConnected]() { - if (webServer.hasArg("command")) { - lastCommand = webServer.arg("command"); - - if (shellConnected && tcpClient) { - tcpClient.println(lastCommand); - webServer.send(200, "text/plain", "Command executed: " + lastCommand); - } else { - webServer.send(503, "text/plain", "Error: No active shell connection."); - } - } else { - webServer.send(400, "text/plain", "Error: Command parameter is missing!"); - } - }); - - webServer.on("/status", [&webServer, &shellConnected]() { - String status = shellConnected ? "Connected" : "Disconnected"; - webServer.send(200, "text/plain", "Server is online. Shell status: " + status); - }); + webServer.begin(); + tft.println("Web server started on port 80!"); + tft.println("WebSocket server started on /ws"); dnsServer.start(53, "*", apGateway); - webServer.begin(); - tft.println("Web server started!"); + // ── Main Loop ────────────────────────────────────────────── while (true) { dnsServer.processNextRequest(); - webServer.handleClient(); + ws.cleanupClients(); if (!shellConnected) { tcpClient = tcpServer.accept(); if (tcpClient) { tft.println("Client connected."); tcpClient.println("~Welcome to BruceShell."); - tcpClient.println("~Developed by Fourier (github.com/9dl)"); + tcpClient.println("~Developed by Fourier & Ninja-jr"); + tcpClient.println("~Type 'help' for available commands"); shellConnected = true; } } @@ -124,10 +192,12 @@ void ReverseShell() { if (check(EscPress)) { tft.println("Exiting reverse shell server..."); tcpServer.stop(); - webServer.stop(); + ws.closeAll(); + webServer.end(); dnsServer.stop(); break; } + delay(10); } } #endif diff --git a/src/modules/reverseShell/reverseShell.h b/src/modules/reverseShell/reverseShell.h index cada005b5..576e57952 100644 --- a/src/modules/reverseShell/reverseShell.h +++ b/src/modules/reverseShell/reverseShell.h @@ -1,5 +1,10 @@ #if !defined(LITE_VERSION) +#ifndef REVERSE_SHELL_H +#define REVERSE_SHELL_H + #include "core/display.h" void ReverseShell(); -#endif + +#endif // REVERSE_SHELL_H +#endif // LITE_VERSION diff --git a/src/modules/rf/protocols/README.md b/src/modules/rf/protocols/README.md new file mode 100644 index 000000000..498454a03 --- /dev/null +++ b/src/modules/rf/protocols/README.md @@ -0,0 +1,158 @@ +# `rf/protocols/` — RF Protocol Definitions (sub-GHz) + +This directory is the **only place** where RF protocol definitions and +sub-GHz radio presets used by Bruce's RF module should live. + +## Purpose + +Centralize, in a single versioned and documented location: + +- The **timing parameters** of each static OOK protocol + (pulse length/`TE`, sync factors, bit 0/1 encoding, + inverted signal, typical bit count). +- The **radio presets** (modulation, bandwidth, deviation, data + rate) associated with each mode of operation. +- The mapping table between preset names in `.sub` file format and + the concrete parameters applied to the transceiver. + +Before this directory existed, this data was scattered and +duplicated across `rf_send.cpp`, `rf_scan.cpp`, and utilities. The +refactor goal is that any protocol addition/adjustment happens +**here**, and consumers (send, scan, replay) read from these definitions. + +## Rules + +1. No protocol parameters should be redefined outside this + directory. Consumers import, they don't recreate. +2. Each protocol is documented with the source/note that justifies + its timing values. +3. The external contract (CLI commands, JS names, `.sub` format, Menu + entries) does not change based on internal changes here — see + `.claude/rf_contract.md`. +4. The UI palette remains restricted to `priColor` / `bgColor` / + `getComplementaryColor2(priColor)`. + +## Files (Milestone 1) + +- `rf_config.h` — RF module compilation configuration. + `RF_SUB_LEGACY_MIGRATION` (default 1) enables `.sub` backward compatibility. +- `rf_protocol.h` — `RfPreset` and `RfProtocolDef` structs (data model, + no tables). `RfProtocolFlags` (`RF_PF_HAS_SYNC`, `RF_PF_FIXED_LEN`). +- `rf_presets.h/.cpp` — radio preset table + `rf_find_preset()`. +- `rf_registry.h/.cpp` — static OOK protocol table + + `rf_find_protocol()` / `rf_protocol_at()` / `rf_protocol_count()`. +- `rf_legacy_migrate.h/.cpp` — **removable backward compatibility module** + (guarded by `RF_SUB_LEGACY_MIGRATION`): `n→name` table, + `rf_sub_is_legacy()`, `rf_sub_migrate()`. Wiring in `readSubFile` + is deferred to M3. + +## Preset Alias Table (`.sub`) + +Old Furi names remain valid via alias → neutral canonical name: + +| `.sub` old (alias) | Canonical | +|------------------------------------------|------------------| +| `FuriHalSubGhzPresetOok270Async` | `Ook270Async` | +| `FuriHalSubGhzPresetOok650Async` | `Ook650Async` | +| `FuriHalSubGhzPreset2FSKDev238Async` | `2FSKDev238Async`| +| `FuriHalSubGhzPreset2FSKDev476Async` | `2FSKDev476Async`| +| `FuriHalSubGhzPresetMSK99_97KbAsync` | `MSK99_97KbAsync`| +| `FuriHalSubGhzPresetGFSK9_99KbAsync` | `GFSK9_99KbAsync`| + +## Protocol coverage + +Coverage is delivered in three layers: + +1. **Named/parameterized (registry).** Static OOK families whose pulses are + integer multiples of a base `TE` are described declaratively in + `rf_registry.cpp` and run through the shared decode/encode engines. These + carry a stable `name` written to `Protocol:` in the `.sub`, so a captured + code identifies and replays as the same protocol (round-trip). + The table distinguishes two structural shapes handled by one model + (`RfProtocolDef`): *high-first* (`inv=false`) and *space-coded* (`inv=true`, + `sync` = `{gap, preamble}`). Fixed-length protocols (`FIXED`) only match a + frame of exactly their bit length, which disambiguates codes that share + timings. + + | Protocol | bits | shape | decode | encode | round-trip tested | + |---------------|-----:|-------------|:------:|:------:|:-----------------------| + | Princeton | 24 | high-first | ✓ | ✓ | ✓ loopback + golden | + | NICE_FLO | 12 | high-first | ✓ | ✓ | ✓ loopback | + | Linear | 10 | high-first | ✓ | ✓ | ✓ loopback + golden | + | Clemsa | 18 | high-first | ✓ | ✓ | ✓ loopback + golden | + | Mastercode | 36 | high-first | ✓ | ✓ | ✓ loopback + golden | + | CAME | 12 | space-coded | ✓ | ✓ | ✓ loopback + golden | + | Ansonic | 12 | space-coded | ✓ | ✓ | ✓ TX; RX aliases CAME¹ | + | GateTX | 24 | space-coded | ✓ | ✓ | ✓ loopback + golden | + | Holtek | 40 | space-coded | ✓ | ✓ | ✓ loopback + golden | + | Holtek_HT12 | 12 | space-coded | ✓ | ✓ | ✓ loopback | + | PhoenixV2 | 52 | space-coded | ✓ | ✓ | ✓ loopback + golden | + | RcSwitch_1..12| var | both | ✓ | ✓ | ✓ loopback | + + - "loopback" = transmitted by one CC1101 (`subghz txp ...`) and decoded + by another (`subghz rx`); key/bits/te round-trip. + - "golden" = encoder output checked against absolute reference timings by + `subghz selftest` (`rf_encoder_selftest`). + - ¹ Ansonic and CAME are both 12-bit space-coded with a 1:2 ratio, so they are + timing-ambiguous (the classic OOK alias class): a captured Ansonic + frame decodes as `CAME` (key still correct). TX by name keeps Ansonic's own + `te`. Distinguishing them would need exact-`te` matching, which the + tolerance-based decoder intentionally does not do. + +2. **KeeLoq rolling code.** Manufacturer keystore (`/mfcodes`) + block cipher + live in `rf_keeloq.{h,cpp}`, together with `keeloq_build_hop()` — the pure + per-manufacturer hop framing (button/serial mask/counter), shared by the + counter step. The remaining per-frame state (counter step, key assembly) + stays on `RfCodes::keeloq_step` in `rf_utils`, since it operates on a captured + code; emulation transmits through the OOK engine. Supported manufacturers are + those handled by `keeloq_build_hop`. + The cipher is byte-identical to the reference algorithm (NLF, 528 rounds). + Learning schemes are centralized in one dispatch, `keeloq_derive_man(type, + fix, seed, key)`, shared by encode (`keeloq_step`) and decode + (`keeloq_identify`): simple(1), normal(2), secure(3), magic_xor_type1(4), + magic_serial_type1/2/3(6/7/8), erreka(12), pujol(13), aerf(14), + simple_jcm(15). FAAC SLH(5), KingGates(10) and Jarolift(11) are separate + protocols and are not handled here. `type` ids match the Flipper keystore, so + a `/mfcodes` exported from there works verbatim. The keystore is read via + `keeloq_mfcodes_fs()` (SD first, LittleFS fallback) and can be managed over + the CLI with `subghz mfcodes add/list/clear`. + `subghz keeloqtest` (`rf_keeloq_selftest`) round-trips every manufacturer + family — including Pecinin and Rossi — and every supported learning type; + `subghz keeloqfiletest` does the same driven by the real `/mfcodes`. Both are + under `RF_DEBUG`. + + KeeLoq's over-the-air framing (11-pulse header, sync gap, 64 PWM bits at + te 400/800) does not fit the factor-based registry model, so it has a dedicated + encoder (`rf_keeloq_durations`/`rf_tx_keeloq`) and decoder (`rf_decode_keeloq`, + wired into the RX paths via `rf_try_keeloq`). `sendRfCommand` routes + `Protocol: KeeLoq` to the encoder; `subghz rx` decodes it and `subghz keeloqtx` + emits it. Verified OTA between two CC1101 boards (simple/normal/secure types, + manufacturer + button + counter recovered). + +3. **RAW / BinRAW replay.** Any captured signal replays bit-exactly through the + RAW path, independent of the registry. Families that require a bespoke + per-protocol decoder — rolling-code derivatives and weather / TPMS sensors — + are **not** named-decoded today; they are captured and replayed via RAW. + Adding a named decoder for one of these means registering a decode/encode + callback on `RfProtocolDef` (planned extension point), not a timing row. + +## References / credits + +The sub-GHz protocol set, the KeeLoq cipher and learning schemes, and the +manufacturer keystore format are ports of / validated against the Flipper Zero +ecosystem. The encrypted built-in keystore (decrypted at runtime as a fallback +when no `/mfcodes` file is present — see `rf_keeloq.cpp`) mirrors how Momentum +ships its `keeloq_mfcodes` asset. + +- **Flipper Zero firmware** — SubGhz protocols, `keeloq.c` / `keeloq_common`, + and the SubGhz keystore format: + + (`lib/subghz/protocols/*`, `lib/subghz/blocks/*`) +- **Momentum firmware** — Flipper fork with an extended protocol set and + manufacturer list, and the encrypted `keeloq_mfcodes` asset this module's + built-in keystore is modeled on: + + (`applications/main/subghz/.../assets/keeloq_mfcodes`) + +The KeeLoq `type` ids (learning schemes) match that keystore's `type` column, so +a `/mfcodes` exported from either firmware works in Bruce verbatim. diff --git a/src/modules/rf/protocols/rf_config.h b/src/modules/rf/protocols/rf_config.h new file mode 100644 index 000000000..a04371d12 --- /dev/null +++ b/src/modules/rf/protocols/rf_config.h @@ -0,0 +1,30 @@ +#pragma once + +// Central compile-time configuration for the RF module refactor. +// +// RF_SUB_LEGACY_MIGRATION: when enabled, the one-shot migration of old +// `.sub` files (Protocol: RcSwitch + numeric Preset) into the new +// registry-based format is compiled in. All legacy code lives in a single +// removable module (rf_legacy_migrate.{h,cpp}) guarded by this macro, so a +// future release can drop legacy support by deleting that file and the +// single call site, and flipping this flag to 0. +#ifndef RF_SUB_LEGACY_MIGRATION +#define RF_SUB_LEGACY_MIGRATION 1 +#endif + +// RF_DEBUG: when set to 1, the RF decode/capture path prints diagnostic +// traces to Serial (capture sizes, separation gaps, decode matches, RAW +// classification). Intended for bring-up / hardware validation only — leave +// at 0 for normal builds. Toggle here (or with -DRF_DEBUG=1) to enable. +#ifndef RF_DEBUG +#define RF_DEBUG 1 +#endif + +#if RF_DEBUG +#include +#define RF_DBG(fmt, ...) Serial.printf("[RFDBG] " fmt "\n", ##__VA_ARGS__) +#else +#define RF_DBG(fmt, ...) \ + do { \ + } while (0) +#endif diff --git a/src/modules/rf/protocols/rf_decoder.cpp b/src/modules/rf/protocols/rf_decoder.cpp new file mode 100644 index 000000000..4deba4838 --- /dev/null +++ b/src/modules/rf/protocols/rf_decoder.cpp @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). This file contains code DERIVED FROM and +// modified after: +// - rc-switch (LGPL-2.1-or-later), (C) 2011 Suat Ozgur and contributors — +// the classic OOK protocol capture/decode state machine; +// - Flipper Zero firmware (GPL-3.0-or-later), (C) Flipper Devices Inc. and +// contributors — the KeeLoq frame decoder framing. +// See THIRD_PARTY.md for full attribution. +#include "rf_decoder.h" +#include "../rf_utils.h" // setup_rf_rx, find_pulse_index, crc64_ecma, RMT defines +#include "rf_config.h" // RF_DBG +#include "rf_registry.h" + +// --- Decode tuning (mirrors the classic OOK receiver) ---------------------- +#define RF_SEPARATION_LIMIT 4300 // µs: a longer low is treated as an inter-frame gap +#define RF_RECEIVE_TOLERANCE 60 // % tolerance on pulse-length matching +#define RF_MAX_CHANGES 131 // max transitions kept per frame + +// --- RX noise rejection ---------------------------------------------------- +// In RX the CC1101 OOK slicer outputs random hash when there is no real signal. +// Without these filters every noise burst becomes a "phantom" capture, flooding +// Scan/Copy. The classic OOK receiver rejects this via its own ISR noise +// pre-filter; we reproduce it here. +#define RF_RX_MIN_TRANSITIONS 16 // discard captures with fewer edges (noise bursts) +// NOTE: signal_range_min_ns is kept at the framework-proven 3µs; larger values +// (e.g. 100µs) were observed to stop the RMT receive from completing at all. + +static inline unsigned int rf_udiff(int a, int b) { return (unsigned int)abs(a - b); } + +// --------------------------------------------------------------------------- +// RMT capture session +// --------------------------------------------------------------------------- +static bool rf_rx_done_cb( + rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data +) { + BaseType_t high_task_wakeup = pdFALSE; + QueueHandle_t queue = (QueueHandle_t)user_data; + xQueueSendFromISR(queue, edata, &high_task_wakeup); + return high_task_wakeup == pdTRUE; +} + +void RfRxSession::arm() { + rmt_receive_config_t cfg = {}; + cfg.signal_range_min_ns = 3000; // 3µs minimum (framework-proven); noise is + // rejected by the transition-count floor below + // 30ms idle ends the capture. Must exceed the largest inter-frame gap so that + // several repeats stay in one capture (the decoder needs two gaps to lock on, + // exactly like the continuous OOK receiver). NICE's gap is ~25ms; the RMT + // hardware idle threshold maxes out near 32ms. + cfg.signal_range_max_ns = 30000000; + esp_err_t err = rmt_receive(_ch, _buf, _bufSymbols * sizeof(rmt_symbol_word_t), &cfg); + if (err != ESP_OK) RF_DBG("rmt_receive failed: %d", (int)err); +} + +bool RfRxSession::begin() { + if (_buf == nullptr) { + _buf = (rmt_symbol_word_t *)malloc(_bufSymbols * sizeof(rmt_symbol_word_t)); + if (_buf == nullptr) return false; + } + _ch = setup_rf_rx(); + if (_ch == nullptr) return false; + _queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t)); + if (_queue == nullptr) { + rmt_del_channel(_ch); + _ch = nullptr; + return false; + } + rmt_rx_event_callbacks_t cbs = {}; + cbs.on_recv_done = rf_rx_done_cb; + if (rmt_rx_register_event_callbacks(_ch, &cbs, _queue) != ESP_OK) { + end(); + return false; + } + rmt_enable(_ch); + arm(); + return true; +} + +bool RfRxSession::poll(std::vector &durations) { + if (_ch == nullptr) return false; + rmt_rx_done_event_data_t rx; + if (xQueueReceive(_queue, &rx, 0) == pdPASS) { + rf_symbols_to_durations(rx.received_symbols, rx.num_symbols, durations); + arm(); // re-arm for the next signal + + // Reject noise bursts: too few edges to be a real frame. + if ((int)durations.size() < RF_RX_MIN_TRANSITIONS) { + RF_DBG("capture ignored (noise): %u durations", (unsigned)durations.size()); + durations.clear(); + return false; + } +#if RF_DEBUG + RF_DBG("capture: %u symbols -> %u durations", (unsigned)rx.num_symbols, (unsigned)durations.size()); + String head; + for (size_t i = 0; i < durations.size() && i < 24; i++) head += String(durations[i]) + " "; + RF_DBG("durations[0..23]: %s", head.c_str()); +#endif + return !durations.empty(); + } + return false; +} + +void RfRxSession::end() { + if (_ch != nullptr) { + rmt_disable(_ch); + rmt_del_channel(_ch); + _ch = nullptr; + } + if (_queue != nullptr) { + vQueueDelete(_queue); + _queue = nullptr; + } + if (_buf != nullptr) { + free(_buf); + _buf = nullptr; + } +} + +void rf_symbols_to_durations(const rmt_symbol_word_t *symbols, size_t count, std::vector &out) { + out.clear(); + // RMT RX is configured at 1 MHz (1 tick = 1 µs), so durations are already µs. + for (size_t i = 0; i < count; i++) { + int d0 = symbols[i].duration0; + if (d0 == 0) break; + out.push_back(symbols[i].level0 ? d0 : -d0); + int d1 = symbols[i].duration1; + if (d1 == 0) break; + out.push_back(symbols[i].level1 ? d1 : -d1); + } +} + +// --------------------------------------------------------------------------- +// OOK decode (faithful port of the classic receiveProtocol state machine) +// --------------------------------------------------------------------------- +static bool rf_match_protocol( + const RfProtocolDef *pro, unsigned int changeCount, const unsigned int *timings, RfCodes &out +) { + if (pro == nullptr) return false; + uint64_t code = 0; + // The longer sync factor maps to the captured inter-frame gap in timings[0]. + unsigned int syncLen = (pro->sync.low > pro->sync.high) ? pro->sync.low : pro->sync.high; + if (syncLen == 0) return false; + unsigned int delay = timings[0] / syncLen; + if (delay == 0) return false; + unsigned int tol = delay * RF_RECEIVE_TOLERANCE / 100; + // Protocols that start high have their first data timing filtered out. + unsigned int first = pro->inverted ? 2 : 1; + + for (unsigned int i = first; i + 1 < changeCount; i += 2) { + code <<= 1; + if (rf_udiff(timings[i], delay * pro->zero.high) < tol && + rf_udiff(timings[i + 1], delay * pro->zero.low) < tol) { + // zero bit + } else if (rf_udiff(timings[i], delay * pro->one.high) < tol && + rf_udiff(timings[i + 1], delay * pro->one.low) < tol) { + code |= 1; // one bit + } else { + return false; + } + } + + if (changeCount > 7) { // ignore very short bursts: that would be noise + int nbits = (changeCount - 1) / 2; + // Fixed-length protocols only match a frame of exactly their length. This + // keeps a 12-bit CAME frame from being claimed by a longer/shorter code + // with otherwise-compatible timings (the M3 cross-match ambiguity). + if ((pro->flags & RF_PF_FIXED_LEN) && nbits != pro->bits) return false; + out.key = code; + out.Bit = nbits; + out.te = delay; + out.protocol = pro->name; + return true; + } + return false; +} + +bool rf_decode_ook(const std::vector &durations, RfCodes &out) { + unsigned int timings[RF_MAX_CHANGES]; + unsigned int changeCount = 0; + unsigned int repeatCount = 0; + const int protoCount = rf_protocol_count(); + + for (int d : durations) { + unsigned int dur = (d < 0) ? (unsigned int)(-d) : (unsigned int)d; + + if (dur > RF_SEPARATION_LIMIT) { + // A long stretch without a level change: likely the gap between two + // repeated transmissions. Two similar gaps bracket a full frame. + if (repeatCount == 0 || rf_udiff(dur, timings[0]) < 200) { + repeatCount++; + if (repeatCount == 2) { + RF_DBG("decode attempt: changeCount=%u gap=%u", changeCount, dur); + for (int p = 0; p < protoCount; p++) { + if (rf_match_protocol(rf_protocol_at(p), changeCount, timings, out)) { + out.preset = "Ook270Async"; + RF_DBG( + "decode MATCH proto=%s key=%llX bits=%d te=%d", + out.protocol.c_str(), + (unsigned long long)out.key, + out.Bit, + out.te + ); + return true; + } + } + RF_DBG("decode: no protocol matched (changeCount=%u)", changeCount); + repeatCount = 0; + } + } + changeCount = 0; + } + + if (changeCount >= RF_MAX_CHANGES) { + changeCount = 0; + repeatCount = 0; + } + timings[changeCount++] = dur; + } + + return false; +} + +// --------------------------------------------------------------------------- +// RAW builder (port of RFScan::read_raw inner loop) +// --------------------------------------------------------------------------- +int rf_build_raw( + const std::vector &durations, String &dataOut, bool &hasCrc, uint64_t &crcOut, + std::vector &indexedOut, int &bitsOut, int &teOut +) { + dataOut = ""; + hasCrc = false; + crcOut = 0; + indexedOut.clear(); + bitsOut = 0; + teOut = 0; + + std::vector pulseIndexes; // sequence of distinct-pulse indexes, for CRC + uint8_t repetition = 0; + int transitions = 0; + + for (int duration : durations) { + if (duration == 0) break; + if (transitions > 0) dataOut += " "; + + if (duration < -5000 && repetition < 2) repetition += 1; + dataOut += String(duration); + if (teOut == 0 && duration > 0) teOut = duration; + + if (repetition == 1 && duration >= -5000) { + int index = find_pulse_index(indexedOut, duration); + if (index == -1) { + indexedOut.push_back(abs(duration)); + index = indexedOut.size() - 1; + } + pulseIndexes.push_back(index); + } + transitions++; + } + + if (repetition >= 2 && !pulseIndexes.empty()) { + crcOut = crc64_ecma(pulseIndexes); + bitsOut = pulseIndexes.size(); + hasCrc = true; + } else { + indexedOut.clear(); // only meaningful alongside a CRC + } + + RF_DBG( + "raw: transitions=%d repetition=%u te=%d hasCrc=%d crc=%llX", + transitions, + repetition, + teOut, + (int)hasCrc, + (unsigned long long)crcOut + ); + return transitions; +} + +// KeeLoq dedicated decoder — faithful port of the reference feed() state machine +// (te_short=400, te_long=800, te_delta=180). Walks the signed durations, +// resyncing on the 11-pulse header + long sync gap, then reads 64 PWM bits +// MSB-first (short HIGH + long LOW = 1; long HIGH + short LOW = 0). +#define RF_KL_TE_SHORT 400 +#define RF_KL_TE_LONG 800 +#define RF_KL_TE_DELTA 180 +#define RF_KL_MIN_BITS 64 + +static inline uint32_t rf_kl_diff(uint32_t a, uint32_t b) { return (a > b) ? (a - b) : (b - a); } + +bool rf_decode_keeloq(const std::vector &durations, RfCodes &out) { + enum { ST_RESET, ST_PREAMBLE, ST_SAVE, ST_CHECK } step = ST_RESET; + int header = 0; + uint64_t data = 0; + int bits = 0; + uint32_t te_last = 0; + + for (int raw : durations) { + bool level = raw > 0; + uint32_t dur = (uint32_t)(raw > 0 ? raw : -raw); + + switch (step) { + case ST_RESET: + if (level && rf_kl_diff(dur, RF_KL_TE_SHORT) < RF_KL_TE_DELTA) { + step = ST_PREAMBLE; + header++; + } + break; + case ST_PREAMBLE: + if (!level && rf_kl_diff(dur, RF_KL_TE_SHORT) < RF_KL_TE_DELTA) { + step = ST_RESET; // a header LOW: keep counting via next HIGH + break; + } + if (header > 2 && rf_kl_diff(dur, RF_KL_TE_SHORT * 10) < RF_KL_TE_DELTA * 10) { + step = ST_SAVE; // sync gap found + data = 0; + bits = 0; + } else { + step = ST_RESET; + header = 0; + } + break; + case ST_SAVE: + if (level) { + te_last = dur; + step = ST_CHECK; + } + break; + case ST_CHECK: + if (!level) { + if (dur >= (uint32_t)(RF_KL_TE_SHORT * 2 + RF_KL_TE_DELTA)) { + // End of transmission. + if (bits >= RF_KL_MIN_BITS && bits <= RF_KL_MIN_BITS + 2) { + out.key = data; + out.Bit = RF_KL_MIN_BITS; + out.te = RF_KL_TE_SHORT; + out.protocol = "KeeLoq"; + out.preset = "Ook650Async"; + RF_DBG("decode keeloq: key=%llX bits=%d", (unsigned long long)data, bits); + return true; + } + step = ST_RESET; + header = 0; + } else if (rf_kl_diff(te_last, RF_KL_TE_SHORT) < RF_KL_TE_DELTA && rf_kl_diff(dur, RF_KL_TE_LONG) < RF_KL_TE_DELTA * 2) { + if (bits < RF_KL_MIN_BITS) data = (data << 1) | 1ULL; + bits++; + step = ST_SAVE; + } else if (rf_kl_diff(te_last, RF_KL_TE_LONG) < RF_KL_TE_DELTA * 2 && rf_kl_diff(dur, RF_KL_TE_SHORT) < RF_KL_TE_DELTA) { + if (bits < RF_KL_MIN_BITS) data = (data << 1); + bits++; + step = ST_SAVE; + } else { + step = ST_RESET; + header = 0; + } + } else { + step = ST_RESET; + header = 0; + } + break; + } + } + + // Capture may end before the trailing gap; accept a full 64-bit payload. + if (bits >= RF_KL_MIN_BITS && bits <= RF_KL_MIN_BITS + 2) { + out.key = data; + out.Bit = RF_KL_MIN_BITS; + out.te = RF_KL_TE_SHORT; + out.protocol = "KeeLoq"; + out.preset = "Ook650Async"; + RF_DBG("decode keeloq(eof): key=%llX bits=%d", (unsigned long long)data, bits); + return true; + } + return false; +} diff --git a/src/modules/rf/protocols/rf_decoder.h b/src/modules/rf/protocols/rf_decoder.h new file mode 100644 index 000000000..139f8e396 --- /dev/null +++ b/src/modules/rf/protocols/rf_decoder.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). This file contains code DERIVED FROM and +// modified after: +// - rc-switch (LGPL-2.1-or-later), (C) 2011 Suat Ozgur and contributors — +// the classic OOK protocol capture/decode state machine; +// - Flipper Zero firmware (GPL-3.0-or-later), (C) Flipper Devices Inc. and +// contributors — the KeeLoq frame decoder framing. +// See THIRD_PARTY.md for full attribution. +#pragma once + +#include "../structs.h" +#include +#include +#include + +// =========================================================================== +// Native RMT RX engine + generic OOK decoder. +// +// Replaces the former interrupt-driven receiver on the decode side. The decode +// logic is a faithful port of the classic OOK capture/decode state +// machine (separation-gap detection + per-protocol pulse matching), driven +// from durations captured with the framework's native RMT RX peripheral. +// Protocol definitions come from the central registry (rf_registry). +// +// References / credits — the SubGhz protocol set and decode behavior mirror +// the Flipper Zero ecosystem; timings and protocol identities were validated +// against these sources: +// - Flipper Zero firmware (SubGhz): +// https://github.com/flipperdevices/flipperzero-firmware +// (lib/subghz/protocols/*) +// - Momentum firmware (Flipper fork with an extended protocol set): +// https://github.com/Next-Flip/Momentum-Firmware +// =========================================================================== + +// One-shot RMT RX capture session. Wraps channel + queue lifecycle so callers +// can poll for captured signals without touching the RMT driver directly. +class RfRxSession { +public: + // Create + enable the RMT RX channel (via setup_rf_rx) and arm a receive. + // Returns false if the RF module / channel could not be initialised. + bool begin(); + // Non-blocking: when a signal has been captured, fills `durations` with the + // signed pulse lengths (HIGH > 0, LOW < 0, µs), re-arms the receiver and + // returns true. Returns false when nothing is ready yet. + bool poll(std::vector &durations); + // Disable + delete the channel and free the queue/buffer. + void end(); + bool active() const { return _ch != nullptr; } + ~RfRxSession() { end(); } + +private: + rmt_channel_handle_t _ch = nullptr; + QueueHandle_t _queue = nullptr; + // Heap-allocated capture buffer: keeping ~1KB off the (8KB) serialcmds task + // stack, where rfReceiveSignal runs, avoids stack overflow / corruption. + rmt_symbol_word_t *_buf = nullptr; + static const size_t _bufSymbols = 256; + void arm(); +}; + +// Convert a buffer of RMT symbols into signed durations (HIGH > 0, LOW < 0). +void rf_symbols_to_durations(const rmt_symbol_word_t *symbols, size_t count, std::vector &out); + +// Try to decode an OOK frame from `durations` using the protocol registry. +// On success fills out.key, out.Bit, out.te, out.protocol (registry name) and +// out.preset (radio preset name) and returns true. Other fields are untouched. +bool rf_decode_ook(const std::vector &durations, RfCodes &out); + +// Try to decode a KeeLoq frame (dedicated PWM state machine: header + sync gap + +// 64 PWM bits). On success sets out.key (raw 64-bit), out.Bit=64, out.te, +// out.protocol="KeeLoq", out.preset and returns true. The caller still splits +// out.key into fix/encrypted and runs keeloq_identify. +bool rf_decode_keeloq(const std::vector &durations, RfCodes &out); + +// Build the RAW representation from `durations`: +// - `dataOut` : "+a -b +c ..." string of signed durations. +// - `teOut` : first positive duration (base pulse estimate). +// - when a repeated pattern is detected, sets `hasCrc=true` and fills +// `crcOut` (CRC-64 of the pulse-index sequence), `indexedOut` (distinct +// pulse lengths) and `bitsOut` (number of indexed transitions). +// Returns the number of transitions written to `dataOut`. +int rf_build_raw( + const std::vector &durations, String &dataOut, bool &hasCrc, uint64_t &crcOut, + std::vector &indexedOut, int &bitsOut, int &teOut +); diff --git a/src/modules/rf/protocols/rf_encoder.cpp b/src/modules/rf/protocols/rf_encoder.cpp new file mode 100644 index 000000000..c4f237edf --- /dev/null +++ b/src/modules/rf/protocols/rf_encoder.cpp @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). This file contains code DERIVED FROM and +// modified after: +// - rc-switch (LGPL-2.1-or-later), (C) 2011 Suat Ozgur and contributors — +// the classic OOK protocol send state machine; +// - Flipper Zero firmware (GPL-3.0-or-later), (C) Flipper Devices Inc. and +// contributors — the KeeLoq OOK framing. +// See THIRD_PARTY.md for full attribution. +#include "rf_encoder.h" +#include "../rf_utils.h" // bruceConfigPins, RMT defines +#include "rf_config.h" // RF_DBG +#include "rf_registry.h" // rf_find_protocol (self-test) +#include // abs +#include + +// RMT duration fields are 15-bit: a single half-pulse maxes out at 32767 ticks. +// At 1 MHz resolution (1 tick = 1 µs) that is 32767 µs; longer stretches are +// split into several same-level entries (electrically continuous). +#define RF_RMT_MAX_DUR 32767 + +// Resolve the GPIO that actually drives the RF output for the current board. +static gpio_num_t rf_tx_gpio() { + if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) + return gpio_num_t(bruceConfigPins.CC1101_bus.io0); + return gpio_num_t(bruceConfigPins.rfTx); +} + +// Append a half-pulse (one logic level held for `dur` µs), splitting it when it +// exceeds the 15-bit RMT field. +static void rf_push_half(std::vector &syms, bool &pendingLow, uint8_t level, long dur) { + while (dur > 0) { + uint16_t chunk = (dur > RF_RMT_MAX_DUR) ? RF_RMT_MAX_DUR : (uint16_t)dur; + dur -= chunk; + if (!pendingLow) { + rmt_symbol_word_t s = {}; + s.level0 = level; + s.duration0 = chunk; + syms.push_back(s); + pendingLow = true; + } else { + rmt_symbol_word_t &s = syms.back(); + s.level1 = level; + s.duration1 = chunk; + pendingLow = false; + } + } +} + +bool rf_tx_durations(const std::vector &durations) { + if (durations.empty()) return false; + + // Pack signed µs timings into RMT symbols (two half-pulses per symbol). + std::vector syms; + syms.reserve(durations.size() / 2 + 1); + bool pendingLow = false; + for (int d : durations) { + if (d == 0) continue; + uint8_t level = (d > 0) ? 1 : 0; + long dur = (d > 0) ? d : -d; + rf_push_half(syms, pendingLow, level, dur); + } + if (syms.empty()) return false; + + RF_DBG("tx: %u durations -> %u symbols", (unsigned)durations.size(), (unsigned)syms.size()); + + // Create the RMT TX channel on the RF output pin (1 MHz / 1 tick = 1 µs, + // matching the RX side). The radio itself is already configured by the + // caller (CC1101 SetTx / single-pin OUTPUT). + rmt_tx_channel_config_t tx_cfg = {}; + tx_cfg.gpio_num = rf_tx_gpio(); + tx_cfg.clk_src = RMT_CLK_SRC_DEFAULT; + tx_cfg.resolution_hz = 1 * 1000 * 1000; + tx_cfg.mem_block_symbols = 64; + tx_cfg.trans_queue_depth = 4; + tx_cfg.flags.invert_out = false; + tx_cfg.flags.with_dma = false; + + rmt_channel_handle_t ch = nullptr; + esp_err_t err = rmt_new_tx_channel(&tx_cfg, &ch); + if (err != ESP_OK) { + RF_DBG("rmt_new_tx_channel failed: %d", (int)err); + return false; + } + + rmt_encoder_handle_t encoder = nullptr; + rmt_copy_encoder_config_t copy_cfg = {}; + err = rmt_new_copy_encoder(©_cfg, &encoder); + if (err != ESP_OK) { + RF_DBG("rmt_new_copy_encoder failed: %d", (int)err); + rmt_del_channel(ch); + return false; + } + + bool ok = (rmt_enable(ch) == ESP_OK); + if (ok) { + rmt_transmit_config_t txc = {}; + txc.loop_count = 0; // repetitions are already baked into `syms` + txc.flags.eot_level = 0; // leave the line LOW when done + err = rmt_transmit(ch, encoder, syms.data(), syms.size() * sizeof(rmt_symbol_word_t), &txc); + if (err != ESP_OK) { + RF_DBG("rmt_transmit failed: %d", (int)err); + ok = false; + } else { + err = rmt_tx_wait_all_done(ch, 2000); // up to 2s for the frame to flush + if (err != ESP_OK) { + RF_DBG("rmt_tx_wait_all_done failed: %d", (int)err); + ok = false; + } + } + rmt_disable(ch); + } + + rmt_del_encoder(encoder); + rmt_del_channel(ch); + return ok; +} + +bool rf_encode_protocol( + uint64_t data, unsigned int bits, int te, const RfProtocolDef *def, int repeat, + std::vector &out +) { + out.clear(); + if (def == nullptr || bits == 0) return false; + int base = (te > 0) ? te : def->te; + if (base <= 0) return false; + if (repeat < 1) repeat = 1; + + // Faithful port of the classic OOK send: for each repetition emit the data bits + // MSB first, then the sync/pilot pulse. Each pulse is firstLevel for + // high*te µs then secondLevel for low*te µs; inverted protocols swap levels. + const bool inv = def->inverted; + const bool hasSync = (def->sync.high != 0 || def->sync.low != 0); + + out.reserve((size_t)repeat * (bits + 1) * 2); + + auto emitPulse = [&](const HighLow &p) { + long first = (long)p.high * base; + long second = (long)p.low * base; + // sign: + HIGH, - LOW. Non-inverted -> first half HIGH, second LOW. + out.push_back(inv ? -(int)first : (int)first); + out.push_back(inv ? (int)second : -(int)second); + }; + + for (int r = 0; r < repeat; r++) { + for (int i = (int)bits - 1; i >= 0; i--) { + const HighLow &p = ((data >> i) & 1ULL) ? def->one : def->zero; + emitPulse(p); + } + if (hasSync) emitPulse(def->sync); + } + return true; +} + +bool rf_tx_protocol(uint64_t data, unsigned int bits, int te, const RfProtocolDef *def, int repeat) { + std::vector durs; + if (!rf_encode_protocol(data, bits, te, def, repeat, durs)) return false; + + RF_DBG( + "tx_protocol: proto=%s data=%llX bits=%u te=%d repeat=%d", + def->name, + (unsigned long long)data, + bits, + (te > 0) ? te : def->te, + (repeat < 1) ? 1 : repeat + ); + return rf_tx_durations(durs); +} + +// KeeLoq OOK framing (te_short=400, te_long=800), ported from the reference +// encoder: 11x{short,short} header, sync {short HIGH, 10*short LOW}, then 64 +// data bits MSB-first (bit 1 = short HIGH + long LOW; bit 0 = long HIGH + short +// LOW), a trailing status bit and a large inter-frame gap (40*short). +#define RF_KL_SHORT 400 +#define RF_KL_LONG 800 + +bool rf_keeloq_durations(uint64_t key, std::vector &out) { + out.clear(); + out.reserve(11 * 2 + 2 + 64 * 2 + 4); + + // Header: 11 short HIGH/LOW pairs. + for (int i = 0; i < 11; i++) { + out.push_back(RF_KL_SHORT); + out.push_back(-RF_KL_SHORT); + } + // Sync: short HIGH then 10*short LOW. + out.push_back(RF_KL_SHORT); + out.push_back(-RF_KL_SHORT * 10); + + // 64 data bits, MSB first. + for (int i = 63; i >= 0; i--) { + if ((key >> i) & 1ULL) { // bit 1: short HIGH, long LOW + out.push_back(RF_KL_SHORT); + out.push_back(-RF_KL_LONG); + } else { // bit 0: long HIGH, short LOW + out.push_back(RF_KL_LONG); + out.push_back(-RF_KL_SHORT); + } + } + // Trailing status bit + end pulse + large inter-frame gap. + out.push_back(RF_KL_SHORT); + out.push_back(-RF_KL_LONG); + out.push_back(RF_KL_SHORT); + out.push_back(-RF_KL_SHORT * 40); + return true; +} + +bool rf_tx_keeloq(uint64_t key, int repeat) { + if (repeat < 1) repeat = 1; + std::vector frame; + if (!rf_keeloq_durations(key, frame)) return false; + + std::vector durs; + durs.reserve(frame.size() * repeat); + for (int r = 0; r < repeat; r++) durs.insert(durs.end(), frame.begin(), frame.end()); + + RF_DBG("tx_keeloq: key=%llX repeat=%d", (unsigned long long)key, repeat); + return rf_tx_durations(durs); +} + +bool rf_tx_raw_timings(const int *timings) { + if (!timings) return false; + std::vector durs; + for (size_t i = 0; timings[i] != 0; i++) durs.push_back(timings[i]); + return rf_tx_durations(durs); +} + +bool rf_tx_raw_bits(const String &bits, int te) { + if (bits.length() == 0 || te <= 0) return false; + // Each bit is one half-pulse of `te` µs at the matching level. Matches the + // legacy bit sender, which walked the string from the end toward the start. + std::vector durs; + durs.reserve(bits.length()); + for (int i = bits.length() - 1; i >= 0; i--) { + char c = bits[i]; + if (c == '1') durs.push_back(te); + else if (c == '0') durs.push_back(-te); + // any other char is skipped (as before) + } + return rf_tx_durations(durs); +} + +#if RF_DEBUG +// --------------------------------------------------------------------------- +// Golden encoder self-test (`subghz selftest`). +// +// For each reference-derived protocol, encode key=0xA / 4 bits / 1 repetition +// and compare the produced durations against the EXPECTED absolute timings, +// written here straight from the protocol spec (te_short/te_long, sync gap and +// preamble) rather than re-derived from the registry factors. A mismatch beyond +// RF_GOLDEN_TOL therefore catches a wrong factor, te or pulse order in a def — +// the spec-fidelity gap that loopback (self-consistent by construction) hides. +// --------------------------------------------------------------------------- +#define RF_GOLDEN_TOL 16 // µs (covers the few protocols whose te_long != k*te_short) + +// key=0xA -> bits 1,0,1,0 (MSB first) -> one,zero,one,zero,sync +static const int g_linear[] = {1500,-500, 500,-1500, 1500,-500, 500,-1500, 1500,-21000}; +static const int g_clemsa[] = {2695,-385, 385,-2695, 2695,-385, 385,-2695, 2695,-19250}; +static const int g_mastercode[] = {2145,-1072, 1072,-2145, 2145,-1072, 1072,-2145, 2145,-15008}; +static const int g_came[] = {-320,640, -640,320, -320,640, -640,320, -11520,320}; +static const int g_ansonic[] = {-1111,555, -555,1111, -1111,555, -555,1111, -19425,555}; +static const int g_gatetx[] = {-700,350, -350,700, -700,350, -350,700, -17150,700}; +static const int g_holtek[] = {-870,430, -430,870, -870,430, -430,870, -15480,430}; +static const int g_phoenixv2[] = {-853,427, -427,853, -853,427, -427,853, -25620,2562}; + +bool rf_encoder_selftest() { + struct Case { + const char *name; + uint64_t key; + unsigned bits; + const int *exp; + size_t n; + }; + static const Case cases[] = { + {"Linear", 0xA, 4, g_linear, sizeof(g_linear) / sizeof(int)}, + {"Clemsa", 0xA, 4, g_clemsa, sizeof(g_clemsa) / sizeof(int)}, + {"Mastercode", 0xA, 4, g_mastercode, sizeof(g_mastercode) / sizeof(int)}, + {"CAME", 0xA, 4, g_came, sizeof(g_came) / sizeof(int)}, + {"Ansonic", 0xA, 4, g_ansonic, sizeof(g_ansonic) / sizeof(int)}, + {"GateTX", 0xA, 4, g_gatetx, sizeof(g_gatetx) / sizeof(int)}, + {"Holtek", 0xA, 4, g_holtek, sizeof(g_holtek) / sizeof(int)}, + {"PhoenixV2", 0xA, 4, g_phoenixv2, sizeof(g_phoenixv2) / sizeof(int)}, + }; + + bool allok = true; + for (const auto &c : cases) { + const RfProtocolDef *def = rf_find_protocol(c.name); + std::vector durs; + bool ok = (def != nullptr) && rf_encode_protocol(c.key, c.bits, 0, def, 1, durs); + int bad = -1; + if (ok) { + if (durs.size() != c.n) { + ok = false; + bad = -2; // length mismatch + } else { + for (size_t i = 0; i < c.n; i++) { + if (abs(durs[i] - c.exp[i]) > RF_GOLDEN_TOL) { + ok = false; + bad = (int)i; + break; + } + } + } + } + if (ok) { + RF_DBG("selftest %-11s PASS", c.name); + } else if (bad == -2) { + RF_DBG("selftest %-11s FAIL (len got=%u exp=%u)", c.name, (unsigned)durs.size(), (unsigned)c.n); + } else if (bad >= 0) { + RF_DBG( + "selftest %-11s FAIL @%d got=%d exp=%d", c.name, bad, durs[bad], c.exp[bad] + ); + } else { + RF_DBG("selftest %-11s FAIL (no def/encode)", c.name); + } + allok = allok && ok; + } + RF_DBG("selftest result: %s", allok ? "ALL PASS" : "FAILURES"); + return allok; +} +#endif // RF_DEBUG diff --git a/src/modules/rf/protocols/rf_encoder.h b/src/modules/rf/protocols/rf_encoder.h new file mode 100644 index 000000000..181823e2b --- /dev/null +++ b/src/modules/rf/protocols/rf_encoder.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). This file contains code DERIVED FROM and +// modified after: +// - rc-switch (LGPL-2.1-or-later), (C) 2011 Suat Ozgur and contributors — +// the classic OOK protocol send state machine; +// - Flipper Zero firmware (GPL-3.0-or-later), (C) Flipper Devices Inc. and +// contributors — the KeeLoq OOK framing. +// See THIRD_PARTY.md for full attribution. +#pragma once + +#include "../structs.h" +#include "rf_protocol.h" + +// --------------------------------------------------------------------------- +// Native RMT TX motor. +// +// Replaces the former library / bit-bang transmit paths. Every send goes +// through rf_tx_durations(): a list of signed microsecond timings (sign = logic +// level, + HIGH / - LOW) is packed into rmt_symbol_word_t[] and streamed out on +// the configured RF TX pin (CC1101 GDO0/io0, or rfTx on single-pinned modules). +// The radio (CC1101 modulation / PA / SetTx) is configured by the caller via +// initRfModule()/sendRfCommand(); this module only generates the pulse train. +// --------------------------------------------------------------------------- + +// Transmit a sequence of signed µs durations once. Blocks until the RMT +// hardware reports the whole frame done; leaves the line idle LOW. +bool rf_tx_durations(const std::vector &durations); + +// Build (without transmitting) the signed-µs duration list for `data` +// (`bits` long, MSB first) under protocol `def`, repeated `repeat` times. +// `te` overrides def->te when > 0. Pure / hardware-free: used both by +// rf_tx_protocol and by the encoder self-test. Returns false on bad input. +bool rf_encode_protocol( + uint64_t data, unsigned int bits, int te, const RfProtocolDef *def, int repeat, + std::vector &out +); + +// Encode `data` (`bits` long, MSB first) using protocol `def`, repeated +// `repeat` times, and transmit via RMT. `te` overrides def->te when > 0. +bool rf_tx_protocol(uint64_t data, unsigned int bits, int te, const RfProtocolDef *def, int repeat); + +// Golden self-test: encode known inputs for the reference-derived protocols and +// compare the produced durations against expected absolute timings. Prints a +// PASS/FAIL line per protocol. Returns true iff all pass. Compiled only under +// RF_DEBUG (diagnostic tool, see `subghz selftest`). +bool rf_encoder_selftest(); + +// KeeLoq has its own framing (12-pulse header, sync gap, 64 PWM bits, trailing +// gap) that does not fit the registry's factor-based OOK model, so it gets a dedicated +// encoder. `rf_keeloq_durations` builds the signed-µs train for the 64-bit +// `key` (MSB first); `rf_tx_keeloq` repeats and transmits it. Pure builder is +// exposed for testing. +bool rf_keeloq_durations(uint64_t key, std::vector &out); +bool rf_tx_keeloq(uint64_t key, int repeat); + +// Transmit a 0-terminated RAW timings array (signed µs) via RMT (1 repetition, +// matching the legacy RAW behaviour). +bool rf_tx_raw_timings(const int *timings); + +// Transmit a bit string: each '1'/'0' char is held at the matching logic level +// for `te` µs (other chars are skipped, as in the legacy bit sender). +bool rf_tx_raw_bits(const String &bits, int te); diff --git a/src/modules/rf/protocols/rf_keeloq.cpp b/src/modules/rf/protocols/rf_keeloq.cpp new file mode 100644 index 000000000..f037a3a28 --- /dev/null +++ b/src/modules/rf/protocols/rf_keeloq.cpp @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). This file is DERIVED FROM and modified +// after the Flipper Zero firmware (KeeLoq cipher + keeloq_common learning +// schemes), Copyright (C) Flipper Devices Inc. and the flipperzero-firmware +// contributors, licensed GPL-3.0-or-later. The encrypted built-in keystore +// mirrors Momentum firmware's keeloq_mfcodes (GPL-3.0-or-later). +// See THIRD_PARTY.md for full attribution. +#include "rf_keeloq.h" +#include "core/sd_functions.h" // getFsStorage +#include "rf_config.h" // RF_DEBUG, RF_DBG +#include "rf_keeloq_mfcodes_data.h" +#include +#include +#include +#include +#include +#include + +uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key) { + uint32_t x = data, r; + + for (r = 0; r < 528; r++) + x = (x >> 1) ^ ((bitAt(x, 0) ^ bitAt(x, 16) ^ (uint32_t)bitAt(key, r & 63) ^ + bitAt(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31))) + << 31); + + return x; +} + +uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key) { + uint32_t x = data, r; + + for (r = 0; r < 528; r++) + x = (x << 1) ^ bitAt(x, 31) ^ bitAt(x, 15) ^ (uint32_t)bitAt(key, (15 - r) & 63) ^ + bitAt(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); + + return x; +} + +uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key) { + uint32_t k1, k2; + + data &= 0x0FFFFFFF; + data |= 0x20000000; + k1 = keeloq_decrypt(data, key); + + data &= 0x0FFFFFFF; + data |= 0x60000000; + k2 = keeloq_decrypt(data, key); + + return ((uint64_t)k2 << 32) | k1; +} + +// --- Manufacturer-specific learning schemes (ported from keeloq_common) ------ + +uint64_t keeloq_secure_learning(uint32_t data, uint32_t seed, const uint64_t key) { + data &= 0x0FFFFFFF; + uint32_t k1 = keeloq_decrypt(data, key); + uint32_t k2 = keeloq_decrypt(seed, key); + return ((uint64_t)k1 << 32) | k2; +} + +uint64_t keeloq_magic_xor_type1_learning(uint32_t data, uint64_t xorv) { + data &= 0x0FFFFFFF; + return (((uint64_t)data << 32) | data) ^ xorv; +} + +uint64_t keeloq_magic_serial_type1_learning(uint32_t data, uint64_t man) { + return (man & 0xFFFFFFFF) | ((uint64_t)data << 40) | + ((uint64_t)(((data & 0xff) + ((data >> 8) & 0xFF)) & 0xFF) << 32); +} + +uint64_t keeloq_magic_serial_type2_learning(uint32_t data, uint64_t man) { + uint8_t *p = (uint8_t *)&data; + uint8_t *m = (uint8_t *)&man; + m[7] = p[0]; + m[6] = p[1]; + m[5] = p[2]; + m[4] = p[3]; + return man; +} + +uint64_t keeloq_magic_serial_type3_learning(uint32_t data, uint64_t man) { + return (man & 0xFFFFFFFFFF000000) | (data & 0xFFFFFF); +} + +uint64_t keeloq_learning_aerf(uint32_t data, const uint64_t key) { + uint32_t d = data & 0x0FFFFFFFu; + uint32_t k1 = keeloq_decrypt(d | 0x20000000u, key); + uint32_t k2 = keeloq_decrypt(d | 0x60000000u, key); + return ((uint64_t)k2 << 32) | k1; +} + +uint64_t keeloq_learning_erreka(uint32_t data, uint32_t mix, const uint64_t key) { + uint32_t d = data & 0x0FFFFFFFu; + uint32_t k1 = keeloq_decrypt(d | 0x20000000u, key); + uint32_t r4 = mix >> 4; + uint32_t r1 = (mix << 4) & 0xF000F000u; + r4 = (r4 & 0x0F000F00u) | r1; + uint32_t r5 = mix & 0x00FF00FFu; + uint32_t x = r4 | r5; + x |= 0x60000000u; + uint32_t k2 = keeloq_decrypt(x, key); + return ((uint64_t)k2 << 32) | k1; +} + +uint64_t keeloq_learning_pujol(uint32_t data, const uint64_t key) { + uint32_t d = data & 0x0FFFFFFFu; + uint32_t w1 = keeloq_decrypt(d | 0x20000000u, key); + uint32_t w2 = keeloq_decrypt(d | 0x60000000u, key); + uint32_t k1 = (w1 >> 16) | (w1 << 16); + uint32_t k2 = (w2 >> 16) | (w2 << 16); + return ((uint64_t)k2 << 32) | k1; +} + +uint64_t keeloq_derive_man(uint32_t type, uint32_t fix, uint32_t seed, uint64_t key) { + switch (type) { + case KEELOQ_NORMAL_LEARNING: return keeloq_normal_learning(fix, key); + case KEELOQ_SECURE_LEARNING: return keeloq_secure_learning(fix, seed, key); + case KEELOQ_MAGIC_XOR_TYPE1_LEARNING: return keeloq_magic_xor_type1_learning(fix, key); + case KEELOQ_MAGIC_SERIAL_TYPE1_LEARNING: return keeloq_magic_serial_type1_learning(fix, key); + case KEELOQ_MAGIC_SERIAL_TYPE2_LEARNING: return keeloq_magic_serial_type2_learning(fix, key); + case KEELOQ_MAGIC_SERIAL_TYPE3_LEARNING: return keeloq_magic_serial_type3_learning(fix, key); + case KEELOQ_AERF_LEARNING: return keeloq_learning_aerf(fix, key); + case KEELOQ_ERREKA_LEARNING: return keeloq_learning_erreka(fix, seed, key); + case KEELOQ_PUJOL_LEARNING: return keeloq_learning_pujol(fix, key); + // SIMPLE, SIMPLE_JCM, UNKNOWN and any unhandled type encrypt with the + // manufacturer key directly. + default: return key; + } +} + +static std::vector split_string(String str, char c) { + std::vector cols{}; + size_t start = 0; + + while (start < str.length()) { + auto it = str.indexOf(c, start); + + if (it == -1) break; + + cols.emplace_back(&str[start], it - start); + start = it + 1; + } + + if (start <= str.length() && !str.isEmpty()) cols.emplace_back(&str[start], str.length() - start); + + return cols; +} + +static void parse_keystore(const String &content, std::vector &keys) { + int start = 0; + const int len = content.length(); + + while (start < len) { + int nl = content.indexOf('\n', start); + String line = (nl < 0) ? content.substring(start) : content.substring(start, nl); + start = (nl < 0) ? len : nl + 1; + + line.trim(); // also drops a trailing '\r' + if (line.isEmpty()) continue; + + auto cols = split_string(line, ';'); + if (cols.size() != 3) continue; // skip malformed lines, keep the rest + + keys.push_back({cols[0], std::strtoull(cols[1].c_str(), NULL, 16), (uint8_t)cols[2].toInt()}); + } +} + +static String keeloq_embedded_plaintext() { + if (KEELOQ_MFCODES_ENC_LEN == 0 || (KEELOQ_MFCODES_ENC_LEN % 16) != 0) return ""; + + std::vector out(KEELOQ_MFCODES_ENC_LEN + 1, 0); + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + if (mbedtls_aes_setkey_dec(&ctx, KEELOQ_MFCODES_KEY, 256) != 0) { + mbedtls_aes_free(&ctx); + return ""; + } + + uint8_t iv[16]; + memcpy(iv, KEELOQ_MFCODES_IV, sizeof(iv)); // CBC mutates the IV in place + int rc = mbedtls_aes_crypt_cbc( + &ctx, MBEDTLS_AES_DECRYPT, KEELOQ_MFCODES_ENC_LEN, iv, KEELOQ_MFCODES_ENC, out.data() + ); + mbedtls_aes_free(&ctx); + if (rc != 0) return ""; + + size_t plen = KEELOQ_MFCODES_ENC_LEN; + uint8_t pad = out[plen - 1]; + if (pad >= 1 && pad <= 16 && pad <= plen) plen -= pad; + out[plen] = 0; + + return String((const char *)out.data()); +} + +KeeloqKeystore::KeeloqKeystore(FS *fs) { + if (fs) { + File keystore = fs->open("/mfcodes"); + if (keystore) { + parse_keystore(keystore.readString(), keys); + keystore.close(); + } + } + if (keys.empty()) parse_keystore(keeloq_embedded_plaintext(), keys); +} + +const std::vector &KeeloqKeystore::get_keys() { return keys; } + +FS *keeloq_mfcodes_fs() { + FS *fs = nullptr; + // Active storage first (SD when mounted) — but only if it actually has the + // keystore; otherwise fall back to a LittleFS copy. + if (getFsStorage(fs) && fs && fs->exists("/mfcodes")) return fs; + if (LittleFS.exists("/mfcodes")) return &LittleFS; + return fs; // may be nullptr +} + +uint32_t keeloq_build_hop(const String &mf_name, uint8_t btn, uint32_t serial, uint16_t cnt) { + const uint32_t b = (uint32_t)btn << 28; + + if (mf_name == "Aprimatic") { + uint32_t apri_serial = serial; + uint8_t apr1 = 0; + for (uint16_t i = 1; i != 0b10000000000; i <<= 1) { + if (apri_serial & i) apr1++; + } + apri_serial &= 0b00001111111111; + if (apr1 % 2 == 0) { apri_serial |= 0b110000000000; } + return b | (apri_serial & 0xFFF) << 16 | cnt; + } else if ( + mf_name == "DTM_Neo" || mf_name == "FAAC_RC,XT" || mf_name == "Mutanco_Mutancode" || + mf_name == "Came_Space" || mf_name == "Genius_Bravo" || mf_name == "GSN" || mf_name == "Rosh" || + mf_name == "Rossi" || mf_name == "Pecinin" || mf_name == "Peccinin" || mf_name == "Steelmate" || + mf_name == "Cardin_S449" + ) { + return b | (serial & 0xFFF) << 16 | cnt; + } else if (mf_name == "NICE_Smilo" || mf_name == "NICE_MHOUSE" || mf_name == "JCM_Tech") { + return b | (serial & 0xFF) << 16 | cnt; + } else if (mf_name == "Merlin") { + return b | (0x000) << 16 | cnt; + } else if (mf_name == "Centurion") { + return b | (0x1CE) << 16 | cnt; + } else if (mf_name == "Monarch") { + return b | (0x100) << 16 | cnt; + } else if (mf_name == "Dea_Mio") { + uint8_t first_disc_num = (serial >> 8) & 0xF; + uint8_t result_disc = (0xC + (first_disc_num % 4)); + uint32_t dea_serial = (serial & 0xFF) | (((uint32_t)result_disc) << 8); + return b | (dea_serial & 0xFFF) << 16 | cnt; + } + + // Default route (Unknown and the plain manufacturers): 10-bit serial. + return b | (serial & 0x3FF) << 16 | cnt; +} + +#if RF_DEBUG +// --------------------------------------------------------------------------- +// KeeLoq golden self-test (`subghz keeloqtest`). +// +// Cipher fidelity is already pinned by code-equivalence to the reference +// (identical NLF 0x3A5C742E, 528 rounds, taps), so this validates the *routes*: +// for every manufacturer family, build the hop, encrypt it (simple AND normal +// learning) exactly as the TX side does, then decrypt it back through the real +// `keeloq_check_decrypt[_centurion]` exactly as `keeloq_identify` does, and +// confirm the button/serial/counter survive the round-trip. +// +// Normal learning derives the manufacturer key from `fix` (button|serial) on +// BOTH sides — matching the reference encoder (keeloq.c). The test also probes +// the legacy `man = normal_learning(hop)` derivation to show it does NOT survive +// the round-trip (the bug this milestone fixes in keeloq_step). +// --------------------------------------------------------------------------- + +// An arbitrary manufacturer key (value is irrelevant to a round-trip; we only +// require encrypt/decrypt to use the same derived key). +#define RF_KL_TESTKEY 0x5cec6701b79fd949ULL + +static bool kl_check(const String &mf, uint32_t dec, uint8_t btn, uint32_t serial, uint16_t cnt) { + RfCodes rf; + rf.mf_name = mf; + rf.btn = btn; + rf.serial = serial; + rf.cnt = 0; + bool ok = (mf == "Centurion") ? rf.keeloq_check_decrypt_centurion(dec) : rf.keeloq_check_decrypt(dec); + return ok && (rf.cnt == cnt); +} + +bool rf_keeloq_selftest() { + struct KlCase { + const char *mf; + uint32_t serial; + }; // one per distinct hop route + the user-named families + static const KlCase cases[] = { + {"Unknown", 0x4D5E6}, // default 10-bit serial + {"Rossi", 0x4D5E6}, // 12-bit serial + {"Pecinin", 0x4D5E6}, // 12-bit serial + {"Peccinin", 0x4D5E6}, // 12-bit serial + {"FAAC_RC,XT", 0x4D5E6}, // 12-bit serial + {"Aprimatic", 0x4D5E6}, // parity-derived serial + {"NICE_Smilo", 0x4D5E6}, // 8-bit serial + {"Merlin", 0x4D5E6}, // fixed 0x000 + {"Monarch", 0x4D5E6}, // fixed 0x100 + {"Dea_Mio", 0x4D5E6}, // derived serial + {"Centurion", 0x4D5E6}, // fixed 0x1CE + dedicated check + }; + + const uint8_t btn = 0x5; + const uint16_t cnt = 0x1234; + const uint64_t key = RF_KL_TESTKEY; + bool allok = true; + + for (const auto &c : cases) { + String mf = c.mf; + uint32_t fix = ((uint32_t)btn << 28) | c.serial; + uint32_t hop = keeloq_build_hop(mf, btn, c.serial, cnt); + + // Simple learning: key used directly on both sides. + uint32_t enc_s = keeloq_encrypt(hop, key); + bool simple_ok = kl_check(mf, keeloq_decrypt(enc_s, key), btn, c.serial, cnt); + + // Normal learning, reference-correct: man derived from fix on both sides. + uint64_t man = keeloq_normal_learning(fix, key); + uint32_t enc_n = keeloq_encrypt(hop, man); + bool normal_ok = kl_check(mf, keeloq_decrypt(enc_n, man), btn, c.serial, cnt); + + // Legacy buggy derivation (man from hop on TX): proves why it failed. + uint64_t man_bug = keeloq_normal_learning(hop, key); + uint32_t enc_b = keeloq_encrypt(hop, man_bug); + bool legacy_ok = kl_check(mf, keeloq_decrypt(enc_b, man), btn, c.serial, cnt); + + bool ok = simple_ok && normal_ok; + allok = allok && ok; + RF_DBG( + "keeloq %-12s simple=%s normal=%s (legacy_hop=%s)", + c.mf, + simple_ok ? "PASS" : "FAIL", + normal_ok ? "PASS" : "FAIL", + legacy_ok ? "PASS" : "FAIL" + ); + } + // --- Learning-type round-trip -------------------------------------------- + // For every supported learning type, encrypt the hop with the derived + // manufacturer key (encode side) and decrypt it back with the same + // derivation (decode side), exactly as keeloq_step / keeloq_identify do. + struct LtCase { + const char *name; + uint32_t type; + }; + static const LtCase lts[] = { + {"Simple", KEELOQ_SIMPLE_LEARNING }, + {"Normal", KEELOQ_NORMAL_LEARNING }, + {"Secure", KEELOQ_SECURE_LEARNING }, + {"MagicXor1", KEELOQ_MAGIC_XOR_TYPE1_LEARNING }, + {"MagicSer1", KEELOQ_MAGIC_SERIAL_TYPE1_LEARNING}, + {"MagicSer2", KEELOQ_MAGIC_SERIAL_TYPE2_LEARNING}, + {"MagicSer3", KEELOQ_MAGIC_SERIAL_TYPE3_LEARNING}, + {"AERF", KEELOQ_AERF_LEARNING }, + {"Erreka", KEELOQ_ERREKA_LEARNING }, + {"Pujol", KEELOQ_PUJOL_LEARNING }, + {"SimpleJCM", KEELOQ_SIMPLE_JCM_LEARNING }, + }; + + const uint32_t serial = 0x4D5E6; + const uint32_t fix = ((uint32_t)btn << 28) | serial; + const uint32_t seed = fix & 0x0FFFFFFF; // serial-derived fallback + const uint32_t hop = keeloq_build_hop("Unknown", btn, serial, cnt); + + for (const auto < : lts) { + uint64_t man = keeloq_derive_man(lt.type, fix, seed, key); + uint32_t enc = keeloq_encrypt(hop, man); + uint32_t dec = keeloq_decrypt(enc, man); + bool ok = kl_check("Unknown", dec, btn, serial, cnt); + allok = allok && ok; + RF_DBG("keeloq learning %-10s round-trip=%s", lt.name, ok ? "PASS" : "FAIL"); + } + + RF_DBG("keeloq selftest result: %s", allok ? "ALL PASS" : "FAILURES"); + return allok; +} + +// A learning type whose algorithm we actually implement (round-trip is +// meaningful). FAAC/KingGates/Jarolift live in separate protocols and would +// only trivially "pass" as plain simple learning, so we flag them instead. +static bool kl_type_supported(uint32_t type) { + switch (type) { + case KEELOQ_UNKNOWN_LEARNING: + case KEELOQ_SIMPLE_LEARNING: + case KEELOQ_NORMAL_LEARNING: + case KEELOQ_SECURE_LEARNING: + case KEELOQ_MAGIC_XOR_TYPE1_LEARNING: + case KEELOQ_MAGIC_SERIAL_TYPE1_LEARNING: + case KEELOQ_MAGIC_SERIAL_TYPE2_LEARNING: + case KEELOQ_MAGIC_SERIAL_TYPE3_LEARNING: + case KEELOQ_ERREKA_LEARNING: + case KEELOQ_PUJOL_LEARNING: + case KEELOQ_AERF_LEARNING: + case KEELOQ_SIMPLE_JCM_LEARNING: return true; + default: return false; // 5 FAAC, 10 KingGates, 11 Jarolift, ... + } +} + +bool rf_keeloq_filetest() { + FS *fs = keeloq_mfcodes_fs(); + if (!fs) { + RF_DBG("keeloq filetest: no storage"); + return false; + } + KeeloqKeystore ks{fs}; + const std::vector &keys = ks.get_keys(); + if (keys.empty()) { + RF_DBG("keeloq filetest: /mfcodes missing or empty"); + return false; + } + + const uint8_t btn = 0x5; + const uint16_t cnt = 0x1234; + const uint32_t serial = 0x4D5E6; + const uint32_t fix = ((uint32_t)btn << 28) | serial; + const uint32_t seed = fix & 0x0FFFFFFF; + + int pass = 0, fail = 0, unsup = 0; + for (const auto &k : keys) { + if (!kl_type_supported(k.type)) { + unsup++; + RF_DBG("keeloq file %-18s type=%2u UNSUPPORTED", k.mf_name.c_str(), (unsigned)k.type); + continue; + } + uint32_t hop = keeloq_build_hop(k.mf_name, btn, serial, cnt); + uint64_t man = keeloq_derive_man(k.type, fix, seed, k.key); + uint32_t enc = keeloq_encrypt(hop, man); + uint32_t dec = keeloq_decrypt(enc, man); + bool ok = kl_check(k.mf_name, dec, btn, serial, cnt); + if (ok) pass++; + else fail++; + RF_DBG("keeloq file %-18s type=%2u %s", k.mf_name.c_str(), (unsigned)k.type, ok ? "PASS" : "FAIL"); + } + RF_DBG( + "keeloq filetest: %d pass, %d fail, %d unsupported (of %u)", pass, fail, unsup, (unsigned)keys.size() + ); + return fail == 0; +} +#endif // RF_DEBUG diff --git a/src/modules/rf/protocols/rf_keeloq.h b/src/modules/rf/protocols/rf_keeloq.h new file mode 100644 index 000000000..3ed86d4cd --- /dev/null +++ b/src/modules/rf/protocols/rf_keeloq.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). This file is DERIVED FROM and modified +// after the Flipper Zero firmware (KeeLoq cipher + keeloq_common learning +// schemes + SubGhz keystore format), Copyright (C) Flipper Devices Inc. and +// the flipperzero-firmware contributors, licensed GPL-3.0-or-later. The +// encrypted built-in keystore mirrors Momentum firmware's keeloq_mfcodes +// (GPL-3.0-or-later). See THIRD_PARTY.md for full attribution. +#pragma once + +#include "../structs.h" // KeeloqKey +#include +#include +#include + +// KeeLoq block cipher + manufacturer keystore. Centralized here so the +// rolling-code primitives live under protocols/ alongside the static protocol +// tables. The per-frame framing (button/serial/counter -> hop, manufacturer +// quirks) stays on the RfCodes struct in rf_utils, since it operates on a +// captured code; only the cipher core and the `/mfcodes` keystore reader moved. +// +// References / credits — the cipher core, the manufacturer "learning" schemes +// and the keystore format are ports of the Flipper Zero ecosystem. The +// encrypted built-in keystore (decrypted at runtime as a fallback) mirrors how +// Momentum ships its `keeloq_mfcodes` asset: +// - Flipper Zero firmware (KeeLoq + keeloq_common, SubGhz keystore): +// https://github.com/flipperdevices/flipperzero-firmware +// (lib/subghz/protocols/keeloq.c, lib/subghz/blocks/*) +// - Momentum firmware (extended manufacturer list / encrypted keystore): +// https://github.com/Next-Flip/Momentum-Firmware +// (applications/main/subghz/.../assets/keeloq_mfcodes) +// The learning-type ids below match that keystore's `type` column so a +// `/mfcodes` exported from there works verbatim. + +#define bitAt(x, n) (((x) >> (n)) & 1) +#define g5(x, a, b, c, d, e) \ + (bitAt(x, a) + bitAt(x, b) * 2 + bitAt(x, c) * 4 + bitAt(x, d) * 8 + bitAt(x, e) * 16) + +#define KEELOQ_NLF 0x3A5C742E + +// Learning-type ids. Values match the Flipper/SubGhz keystore `type` column so a +// `/mfcodes` exported from there can be used verbatim. SIMPLE/NORMAL are the +// historical ones; the rest were ported from the reference keeloq_common. +#define KEELOQ_UNKNOWN_LEARNING 0 +#define KEELOQ_SIMPLE_LEARNING 1 +#define KEELOQ_NORMAL_LEARNING 2 +#define KEELOQ_SECURE_LEARNING 3 +#define KEELOQ_MAGIC_XOR_TYPE1_LEARNING 4 +#define KEELOQ_FAAC_LEARNING 5 // separate protocol (FAAC SLH) — not handled here +#define KEELOQ_MAGIC_SERIAL_TYPE1_LEARNING 6 +#define KEELOQ_MAGIC_SERIAL_TYPE2_LEARNING 7 +#define KEELOQ_MAGIC_SERIAL_TYPE3_LEARNING 8 +#define KEELOQ_SIMPLE_KINGGATES_LEARNING 10 // separate protocol — not handled here +#define KEELOQ_NORMAL_JAROLIFT_LEARNING 11 // separate protocol — not handled here +#define KEELOQ_ERREKA_LEARNING 12 +#define KEELOQ_PUJOL_LEARNING 13 +#define KEELOQ_AERF_LEARNING 14 +#define KEELOQ_SIMPLE_JCM_LEARNING 15 + +class KeeloqKeystore { +public: + KeeloqKeystore(FS *fs); + + const std::vector &get_keys(); + +private: + std::vector keys{}; +}; + +// Pick the filesystem that holds `/mfcodes`: the active storage (SD when +// mounted) if it has the file, otherwise LittleFS (so a keystore written to +// LittleFS works even with an SD card inserted). Returns nullptr if neither has +// it and no storage is available. +FS *keeloq_mfcodes_fs(); + +uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key); +uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key); +uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key); + +// Manufacturer-specific key-derivation schemes ("learning"), ported from the +// reference keeloq_common. Each turns the fixed part (and sometimes a seed) into +// the 64-bit manufacturer key used to encrypt/decrypt the hopping code. +uint64_t keeloq_secure_learning(uint32_t data, uint32_t seed, const uint64_t key); +uint64_t keeloq_magic_xor_type1_learning(uint32_t data, uint64_t xorv); +uint64_t keeloq_magic_serial_type1_learning(uint32_t data, uint64_t man); +uint64_t keeloq_magic_serial_type2_learning(uint32_t data, uint64_t man); +uint64_t keeloq_magic_serial_type3_learning(uint32_t data, uint64_t man); +uint64_t keeloq_learning_aerf(uint32_t data, const uint64_t key); +uint64_t keeloq_learning_erreka(uint32_t data, uint32_t mix, const uint64_t key); +uint64_t keeloq_learning_pujol(uint32_t data, const uint64_t key); + +// Single dispatch used by both the encoder (keeloq_step) and the decoder +// (keeloq_identify): derive the manufacturer key for `type`. `fix` is +// button|serial; `seed` is only consulted by secure/erreka. Unknown/simple +// learning returns `key` unchanged. +uint64_t keeloq_derive_man(uint32_t type, uint32_t fix, uint32_t seed, uint64_t key); + +// Build the KeeLoq "hop" plaintext (button | manufacturer-masked serial | +// counter) for a manufacturer. Pure function so every framing route is testable +// without the SD keystore. Centurion/Merlin/Monarch use a fixed serial field; +// Aprimatic/Dea_Mio derive it from the serial; the rest mask it. Mirrors the +// branch table consumed by RfCodes::keeloq_step. +uint32_t keeloq_build_hop(const String &mf_name, uint8_t btn, uint32_t serial, uint16_t cnt); + +// Golden self-test of the KeeLoq routes: for each manufacturer family encrypt a +// frame and decrypt it back through the real check, for both learning types, and +// round-trip every supported learning type. Diagnostic; defined under RF_DEBUG. +bool rf_keeloq_selftest(); + +// Same round-trip, but driven by the real `/mfcodes` keystore: every entry is +// encoded and decoded with its actual key + learning type. Diagnostic; defined +// under RF_DEBUG. +bool rf_keeloq_filetest(); diff --git a/src/modules/rf/protocols/rf_keeloq_mfcodes_data.h b/src/modules/rf/protocols/rf_keeloq_mfcodes_data.h new file mode 100644 index 000000000..5a3e3f8e7 --- /dev/null +++ b/src/modules/rf/protocols/rf_keeloq_mfcodes_data.h @@ -0,0 +1,190 @@ +#pragma once +#include + +// Auto-generated by tools/gen_mfcodes.py - DO NOT EDIT. +// Encrypted KeeLoq manufacturer keystore (AES-256-CBC, PKCS7). +// Plaintext source is kept out of the repo; re-run the generator to +// rotate the key/IV and refresh this blob after changing the keystore. + +static const uint8_t KEELOQ_MFCODES_KEY[] = { + 0x18, 0xED, 0xD8, 0x9D, 0xD1, 0xD0, 0xCD, 0x4A, 0x35, 0x06, 0x84, 0xCD, + 0x3A, 0x0E, 0xA6, 0xEC, 0xB8, 0x58, 0xE2, 0x51, 0xD3, 0xBC, 0x7E, 0xDC, + 0x55, 0x05, 0x10, 0x8E, 0x12, 0x98, 0x04, 0xE4, +}; + +static const uint8_t KEELOQ_MFCODES_IV[] = { + 0xA9, 0xAD, 0x2A, 0x53, 0x91, 0x32, 0x78, 0xF2, 0xD7, 0x39, 0xA6, 0xC4, + 0x4D, 0x91, 0xCF, 0x96, +}; + +static const uint8_t KEELOQ_MFCODES_ENC[] = { + 0x11, 0xEB, 0x00, 0x77, 0x3F, 0x6E, 0x76, 0xDB, 0x46, 0x00, 0xF0, 0x09, + 0x78, 0xFE, 0xAC, 0xE3, 0x2C, 0x63, 0xFC, 0xAF, 0xE9, 0x72, 0x25, 0xE0, + 0x10, 0x3B, 0x58, 0xEE, 0x9B, 0x15, 0x10, 0xE0, 0x91, 0x89, 0xDE, 0xF1, + 0x4B, 0xE0, 0x86, 0x84, 0x76, 0x82, 0x80, 0x94, 0xB0, 0x8E, 0x10, 0x00, + 0x10, 0x78, 0x96, 0x85, 0xEA, 0x17, 0xB9, 0x47, 0x34, 0x32, 0xA9, 0x47, + 0x1B, 0x9B, 0x69, 0xFA, 0xA4, 0x64, 0x5C, 0x67, 0x21, 0xF1, 0x5B, 0xF2, + 0xD7, 0xE2, 0x8B, 0x74, 0xBF, 0xB4, 0x5F, 0x97, 0xFB, 0x18, 0x9F, 0x75, + 0xF6, 0x76, 0xF3, 0x26, 0xE9, 0x88, 0xC3, 0x85, 0xF7, 0x34, 0xD2, 0xA1, + 0x80, 0xF7, 0xBC, 0x2D, 0x46, 0x01, 0x86, 0xA5, 0x95, 0x32, 0xE3, 0xFB, + 0x70, 0x4A, 0xF7, 0x9A, 0x65, 0xDD, 0x9A, 0x8D, 0x6B, 0x9A, 0x11, 0xC0, + 0x3E, 0x07, 0x13, 0x28, 0xB8, 0xA6, 0x3E, 0x2D, 0x24, 0x45, 0x62, 0x33, + 0x3E, 0xB2, 0x08, 0x5D, 0xA1, 0x4F, 0xF3, 0xFB, 0x14, 0xBE, 0x3F, 0x37, + 0x0E, 0x25, 0xF8, 0xE6, 0xD9, 0x89, 0xC1, 0x17, 0xBB, 0x7D, 0x89, 0x85, + 0x02, 0xEE, 0xC8, 0x77, 0x27, 0x1E, 0x2B, 0xFC, 0x1E, 0x24, 0x4D, 0x1E, + 0x9E, 0xB7, 0x0C, 0x02, 0x1B, 0x61, 0xB1, 0x2E, 0x04, 0x17, 0x1D, 0x23, + 0xB9, 0xD5, 0x38, 0xF9, 0x80, 0x99, 0x84, 0x4B, 0xA2, 0x45, 0x72, 0x14, + 0x2E, 0x14, 0xFE, 0x85, 0xD5, 0x73, 0x60, 0xD3, 0x30, 0x14, 0xBC, 0x94, + 0x96, 0xDA, 0xF8, 0x8C, 0x5E, 0x41, 0xCD, 0xBD, 0x33, 0x70, 0x9E, 0x02, + 0xA6, 0x6C, 0x50, 0x2B, 0x4C, 0x70, 0xE3, 0x71, 0x7B, 0x01, 0xAF, 0x5E, + 0x46, 0x80, 0x68, 0x3A, 0x95, 0x38, 0xC3, 0xDD, 0x2C, 0xE8, 0x61, 0x8C, + 0xEA, 0x14, 0x59, 0x9B, 0x69, 0x07, 0xD6, 0x0C, 0xAF, 0xF6, 0x0E, 0x0A, + 0xC8, 0xEB, 0xF8, 0x2D, 0xE9, 0xB9, 0x86, 0x81, 0xF0, 0xAF, 0xF8, 0xD6, + 0xBB, 0x32, 0x00, 0xD0, 0x0D, 0x40, 0xFF, 0x85, 0xB4, 0x15, 0xBB, 0xF7, + 0x0D, 0x8B, 0xE4, 0x06, 0xC5, 0x2C, 0x23, 0xA3, 0xE7, 0xF6, 0x0F, 0x78, + 0x8C, 0xB1, 0x5F, 0x5C, 0x1F, 0xD5, 0x80, 0xF1, 0x3D, 0x53, 0x5F, 0xAF, + 0xCB, 0xDC, 0x2B, 0xBD, 0x8B, 0x4C, 0xCA, 0x9B, 0xFF, 0x1C, 0xCE, 0x80, + 0x1F, 0x1D, 0x10, 0x70, 0xFE, 0x46, 0x75, 0x65, 0xA5, 0xB4, 0x84, 0xCE, + 0xF4, 0xFB, 0x54, 0x68, 0x86, 0x40, 0xCF, 0x30, 0xCC, 0x28, 0xF6, 0xD5, + 0xCF, 0x1F, 0xB0, 0x15, 0xB1, 0x9F, 0xD9, 0x4B, 0x53, 0x5A, 0xED, 0x32, + 0x74, 0x99, 0xBA, 0x22, 0x2E, 0xFE, 0x77, 0x9C, 0x50, 0x50, 0x8A, 0xD7, + 0x89, 0x88, 0x37, 0x01, 0xE2, 0x66, 0xFA, 0x5C, 0xFE, 0xF4, 0x3C, 0x4C, + 0xB6, 0x27, 0x9B, 0xF9, 0x97, 0xE8, 0xD8, 0x25, 0x6E, 0x4B, 0x68, 0x6F, + 0xDC, 0xCA, 0xAB, 0x5B, 0x07, 0xEE, 0xAC, 0x68, 0x0E, 0x5B, 0xC7, 0x13, + 0x4D, 0x2D, 0x66, 0xC5, 0x6E, 0x79, 0x5A, 0x5E, 0xD5, 0x56, 0x77, 0x46, + 0xBA, 0x9A, 0xBB, 0x16, 0xF7, 0xC7, 0xEA, 0x7A, 0x8B, 0xD4, 0x0F, 0xE8, + 0xEB, 0x78, 0xDA, 0xDE, 0x0D, 0x4D, 0x3F, 0x7F, 0x31, 0xA1, 0xA0, 0x1A, + 0x20, 0xA0, 0x36, 0x65, 0x31, 0xDA, 0x1B, 0x97, 0x95, 0x36, 0xCC, 0xF2, + 0x57, 0x38, 0xC7, 0xA0, 0x43, 0x68, 0xA6, 0xEB, 0xD7, 0x49, 0xC8, 0xBF, + 0x4E, 0x58, 0x7C, 0x94, 0x36, 0x64, 0xCD, 0x44, 0x99, 0x57, 0x6F, 0x09, + 0xEC, 0x7E, 0xAE, 0x8F, 0x48, 0x8D, 0x1D, 0x4D, 0x46, 0x0C, 0xD1, 0xD1, + 0x11, 0x1B, 0xEB, 0x71, 0xF8, 0xF9, 0x16, 0x77, 0x2B, 0x49, 0xBD, 0xD0, + 0x55, 0xBD, 0x58, 0xCD, 0xB2, 0xFF, 0xB8, 0x2C, 0x1F, 0xE3, 0x29, 0x16, + 0x2E, 0x47, 0x1D, 0xA3, 0x48, 0xDB, 0xC2, 0x2E, 0x93, 0x18, 0xF4, 0x97, + 0xCD, 0xF2, 0xD7, 0x71, 0xC5, 0x7A, 0xC0, 0x1F, 0x87, 0xFE, 0x3A, 0x4C, + 0xF9, 0x24, 0x2F, 0x67, 0xEE, 0x80, 0xA3, 0x8B, 0x6E, 0xA0, 0x92, 0x3A, + 0xBB, 0x61, 0x79, 0xB5, 0x3C, 0x9F, 0x26, 0xB7, 0x80, 0x5A, 0x2C, 0xD8, + 0xB8, 0x1E, 0xF9, 0xCD, 0xF0, 0x09, 0x3A, 0xF3, 0xAE, 0x00, 0x51, 0xB5, + 0xEE, 0x6C, 0xF3, 0xEB, 0xB1, 0x25, 0x34, 0xD5, 0x9D, 0x19, 0xFA, 0x40, + 0xE1, 0x0F, 0x0D, 0x2C, 0xF8, 0xD8, 0x0E, 0x14, 0xF3, 0x14, 0xA3, 0xC0, + 0x32, 0x8C, 0x47, 0xC2, 0x11, 0xED, 0x26, 0x93, 0xED, 0x28, 0x28, 0x25, + 0x9A, 0x55, 0xB5, 0x3A, 0x51, 0xF4, 0x8B, 0x9B, 0xDC, 0x4D, 0xAF, 0x65, + 0x86, 0xF7, 0x48, 0x47, 0x8E, 0xC7, 0x19, 0xC0, 0x90, 0xF1, 0xE9, 0xAB, + 0x97, 0xF7, 0x69, 0xB0, 0xC7, 0xB9, 0xDC, 0x4B, 0x74, 0x3D, 0x9E, 0x7F, + 0x15, 0x54, 0x66, 0xBC, 0x30, 0xC0, 0x77, 0xED, 0x42, 0x2B, 0xA9, 0xC3, + 0x10, 0x1A, 0xB5, 0x05, 0xEA, 0x21, 0xE6, 0x13, 0x61, 0x4C, 0xE0, 0x5C, + 0x05, 0xCC, 0xB7, 0x24, 0x59, 0x74, 0xEF, 0xCC, 0x84, 0xF2, 0x7D, 0xDE, + 0x3D, 0xB3, 0xAE, 0x6C, 0x0B, 0xC7, 0xD7, 0xD3, 0x71, 0x2E, 0x53, 0x4D, + 0x38, 0xEC, 0x01, 0x7E, 0x4D, 0x12, 0x4D, 0x72, 0xB2, 0x4B, 0xBA, 0xFE, + 0xA3, 0x02, 0x53, 0x76, 0xD7, 0xBE, 0x77, 0x9B, 0xB5, 0x9E, 0x52, 0xB6, + 0x34, 0x24, 0xFC, 0xB6, 0xEA, 0x5A, 0xD7, 0x63, 0x80, 0xB9, 0x50, 0x5A, + 0xC1, 0x25, 0x0F, 0xAA, 0x36, 0x6C, 0x90, 0x26, 0xC8, 0xCA, 0xAF, 0x43, + 0x30, 0x7B, 0x82, 0xE2, 0xAA, 0xD8, 0x6A, 0x70, 0x45, 0xFF, 0x86, 0x0A, + 0xD6, 0x13, 0x9F, 0xC4, 0x23, 0xF4, 0x56, 0x46, 0x29, 0x4F, 0xEE, 0x00, + 0x8F, 0xAB, 0xCF, 0x89, 0x40, 0x4C, 0xF7, 0xE9, 0xA3, 0x07, 0xE4, 0x09, + 0x15, 0xF9, 0x02, 0x22, 0x52, 0x76, 0xCB, 0x5B, 0xB8, 0x39, 0x0A, 0xAF, + 0xA8, 0x02, 0x15, 0xCF, 0x0E, 0xF4, 0xAD, 0x3C, 0x81, 0xDE, 0xA7, 0xE4, + 0xC8, 0xAF, 0x58, 0x3D, 0xAD, 0xFE, 0xD2, 0x4B, 0xF7, 0xD7, 0x6C, 0xDF, + 0xA8, 0x14, 0x38, 0x70, 0xF4, 0xF8, 0x2F, 0x79, 0x40, 0x8C, 0xD6, 0xBC, + 0x7E, 0x8D, 0xE6, 0x0F, 0x3F, 0x51, 0x93, 0x2F, 0x6C, 0x3A, 0xDA, 0x31, + 0xAB, 0x35, 0x42, 0x23, 0x0F, 0x47, 0x0D, 0xB9, 0xE8, 0x56, 0x1D, 0x30, + 0x23, 0x22, 0x76, 0x28, 0xBC, 0x29, 0x08, 0x9D, 0x96, 0xD5, 0x1C, 0x19, + 0x8C, 0xFF, 0x16, 0x07, 0x46, 0x5C, 0xB6, 0x3D, 0xE1, 0xBA, 0x2C, 0x65, + 0xF5, 0x22, 0xBF, 0x96, 0x59, 0x06, 0x60, 0x0B, 0x3F, 0xF1, 0xAD, 0x22, + 0xC3, 0xCA, 0xAC, 0x5D, 0x50, 0x6F, 0x93, 0xA2, 0x64, 0x90, 0x66, 0x17, + 0x12, 0x69, 0x72, 0xB2, 0xC8, 0x5B, 0x71, 0xC9, 0x77, 0xCC, 0x3C, 0x24, + 0xC5, 0x8E, 0xFA, 0x09, 0xE7, 0x94, 0x11, 0xB9, 0x2F, 0xB7, 0xE6, 0xE3, + 0x7F, 0x0B, 0x21, 0x23, 0x1F, 0x12, 0x6D, 0xEE, 0x50, 0xD6, 0x0D, 0xF4, + 0xD4, 0x6E, 0xF9, 0xFC, 0x9C, 0xF3, 0xE2, 0x11, 0x79, 0xED, 0xBD, 0x15, + 0xE2, 0xE1, 0x20, 0xBE, 0xA4, 0x55, 0x84, 0x3B, 0xE8, 0x3D, 0x33, 0x6C, + 0x2B, 0xD6, 0x16, 0x5C, 0x9E, 0x18, 0xC5, 0xB8, 0x54, 0x67, 0x2F, 0x35, + 0x6D, 0xD3, 0x51, 0xDA, 0xD8, 0xDF, 0x69, 0xAE, 0x41, 0x46, 0x34, 0x8A, + 0x11, 0xA9, 0x22, 0xFA, 0x80, 0xA0, 0xD4, 0xF8, 0x4B, 0xDD, 0x02, 0x3D, + 0xBD, 0xFC, 0x56, 0xB1, 0x5A, 0x9C, 0x71, 0x81, 0x25, 0x20, 0x39, 0x4A, + 0x07, 0x2C, 0x70, 0x99, 0x97, 0x71, 0x80, 0x7F, 0xB6, 0xBA, 0xFB, 0xAF, + 0x09, 0xFC, 0x7E, 0x23, 0x02, 0x9B, 0x94, 0x4B, 0xE5, 0xE7, 0x3F, 0x84, + 0xCE, 0x01, 0x38, 0x44, 0x25, 0x25, 0xC7, 0xA4, 0x6D, 0xB2, 0x56, 0xB0, + 0x4C, 0x80, 0x6E, 0x54, 0xE9, 0x4B, 0x93, 0xE8, 0x42, 0xF4, 0x01, 0xDC, + 0x10, 0x96, 0xEB, 0x63, 0x63, 0xD2, 0x3A, 0x97, 0x66, 0xAC, 0xB8, 0x9B, + 0xE2, 0x28, 0x5F, 0xE9, 0x36, 0xD7, 0x63, 0xF9, 0x2B, 0xD9, 0xE5, 0x5F, + 0x70, 0x82, 0x81, 0xC6, 0xAD, 0x55, 0xAA, 0x7A, 0xBD, 0xD5, 0x18, 0x7C, + 0xAB, 0x52, 0x38, 0x7D, 0x12, 0x60, 0x4F, 0x2B, 0x9E, 0x94, 0x2B, 0x19, + 0xF7, 0x9D, 0xD9, 0xB0, 0xA1, 0xE5, 0x9F, 0xD5, 0x10, 0x57, 0x92, 0x2E, + 0x83, 0x86, 0x2B, 0x43, 0x9D, 0x2B, 0x7C, 0xBD, 0x23, 0x40, 0xFC, 0x35, + 0xE4, 0x00, 0x58, 0x0E, 0xD8, 0x52, 0x49, 0x2E, 0xAB, 0x37, 0xB7, 0x64, + 0x10, 0xCA, 0xF3, 0x2F, 0xD7, 0x00, 0xF7, 0xA6, 0x94, 0xF3, 0xAC, 0x8F, + 0x79, 0x46, 0xA0, 0xF1, 0xB2, 0x42, 0x97, 0x2D, 0x34, 0xE7, 0x45, 0xA7, + 0xF3, 0xCA, 0x38, 0x1A, 0xEC, 0x4F, 0x7A, 0x8F, 0x77, 0x94, 0x5D, 0xE3, + 0x16, 0x9B, 0xBC, 0xDB, 0x4A, 0x8E, 0x36, 0x8E, 0x25, 0xBE, 0xF3, 0x20, + 0x65, 0x29, 0x68, 0xB0, 0x5B, 0x4E, 0x4F, 0x3F, 0xCB, 0xAA, 0xA1, 0x04, + 0x5A, 0x5B, 0x01, 0xE7, 0x53, 0xC7, 0xEA, 0x18, 0x08, 0x02, 0xB7, 0x04, + 0x20, 0x6A, 0x39, 0xD7, 0x04, 0x50, 0x78, 0x42, 0x5B, 0x72, 0x27, 0x8C, + 0x12, 0x39, 0xE6, 0x56, 0x8F, 0xEE, 0x94, 0x87, 0x0E, 0x48, 0x6C, 0x87, + 0xF6, 0xE3, 0xBE, 0x32, 0x8F, 0x06, 0x75, 0x51, 0x99, 0x8C, 0xEE, 0x96, + 0x86, 0x4E, 0x55, 0xB5, 0x0B, 0x7C, 0xF1, 0x9F, 0x51, 0x41, 0x2A, 0x29, + 0x9C, 0x36, 0xEE, 0xEB, 0x2F, 0x3D, 0xD9, 0x40, 0x5B, 0xC8, 0x40, 0xAB, + 0x51, 0x8F, 0xFF, 0x3E, 0xAC, 0x53, 0xF3, 0x3A, 0x53, 0xAD, 0xD8, 0xF2, + 0x51, 0x64, 0xCB, 0x8E, 0x8D, 0x83, 0x99, 0xB6, 0xEF, 0x3D, 0x2E, 0x44, + 0x68, 0x2B, 0x4B, 0x26, 0x16, 0xA8, 0xCD, 0xF6, 0xEF, 0x46, 0xF2, 0x7D, + 0x0A, 0x33, 0x1D, 0x2F, 0xC5, 0x5B, 0xE8, 0x19, 0xB3, 0x2E, 0xEF, 0x48, + 0x82, 0x1E, 0x21, 0x1E, 0xFA, 0xBB, 0x9C, 0xD6, 0xDD, 0x8D, 0x32, 0xAF, + 0xA3, 0x74, 0x4C, 0x9B, 0x5B, 0x2C, 0xF8, 0x95, 0xEB, 0x82, 0xF9, 0x0C, + 0x72, 0xCE, 0x03, 0x36, 0xC6, 0xAC, 0xDD, 0xF3, 0xD8, 0x85, 0x3E, 0xF1, + 0x77, 0xF6, 0x13, 0xB4, 0x05, 0x67, 0xB8, 0xC8, 0xB7, 0x65, 0xD0, 0xD7, + 0x68, 0xD3, 0xA3, 0x0A, 0x98, 0x60, 0xB4, 0xC7, 0x0F, 0x3F, 0xAD, 0x3B, + 0x5B, 0xC4, 0xAE, 0x16, 0x63, 0x4B, 0x63, 0xC7, 0x10, 0x9C, 0x48, 0x40, + 0xC3, 0x8E, 0xBC, 0xD4, 0x9C, 0x5B, 0xD3, 0xA9, 0x76, 0x68, 0xD6, 0x55, + 0x9B, 0xDB, 0xA1, 0x52, 0xDC, 0x3C, 0xD3, 0x35, 0x42, 0x45, 0x1F, 0xEC, + 0x0F, 0x76, 0x5B, 0x46, 0xD6, 0x9C, 0xAD, 0x97, 0xD9, 0x39, 0x88, 0x96, + 0x27, 0x4E, 0x5B, 0x3A, 0x0B, 0x80, 0x9C, 0x8E, 0xFE, 0xF0, 0xB3, 0x07, + 0xA9, 0xF8, 0xDE, 0x6F, 0x4E, 0x66, 0xB2, 0x1B, 0xA9, 0x60, 0x28, 0xE1, + 0xDE, 0xB6, 0x17, 0x25, 0x9E, 0x42, 0xBD, 0xC2, 0x5D, 0xAC, 0x50, 0xD3, + 0x52, 0xAC, 0xAF, 0x24, 0x95, 0x8A, 0xD8, 0x4E, 0xE1, 0xE9, 0x54, 0x3D, + 0x81, 0x4A, 0x16, 0x2C, 0xA1, 0x83, 0x77, 0x92, 0x79, 0x8B, 0x37, 0x4D, + 0xAB, 0x56, 0xA0, 0xA4, 0x85, 0xB8, 0x92, 0x09, 0xE7, 0xDC, 0x1E, 0x16, + 0xE1, 0x71, 0xAF, 0xAF, 0x22, 0x32, 0xB1, 0x4E, 0x60, 0xD2, 0x02, 0x38, + 0x3A, 0x1D, 0xB3, 0xC2, 0xCF, 0xB2, 0x5E, 0x5F, 0x7D, 0x9B, 0xC2, 0x1A, + 0x2B, 0xC7, 0x26, 0xDE, 0xF4, 0x54, 0x0B, 0x99, 0x0F, 0x6C, 0x5E, 0xB4, + 0x79, 0x56, 0x34, 0xF4, 0xF4, 0x97, 0xB2, 0x1B, 0xA6, 0xF3, 0x9F, 0xE6, + 0x78, 0x7E, 0xBD, 0x19, 0x54, 0x57, 0x0F, 0x9C, 0x84, 0x25, 0xFA, 0x85, + 0x38, 0x43, 0x97, 0x23, 0x16, 0x0F, 0x70, 0xD7, 0x70, 0xF8, 0xF2, 0xB4, + 0x26, 0xD3, 0x0F, 0xB3, 0x8A, 0x5C, 0x69, 0xD5, 0x73, 0x5E, 0xC9, 0xF9, + 0xCF, 0xB7, 0xA5, 0xF6, 0x9B, 0x9F, 0x53, 0x45, 0x8D, 0x3E, 0x1E, 0xCB, + 0x85, 0x8F, 0x56, 0xE9, 0x10, 0xD3, 0xE1, 0xCB, 0x99, 0x50, 0x40, 0xC1, + 0x75, 0x1C, 0xE0, 0x93, 0x29, 0x19, 0x38, 0x70, 0xED, 0x51, 0x4D, 0x09, + 0x76, 0xA4, 0x1E, 0x91, 0x9E, 0x61, 0x80, 0x91, 0xE5, 0x42, 0xF0, 0x38, + 0x93, 0xAA, 0xFE, 0x8A, 0x47, 0x87, 0x29, 0x90, 0x13, 0xA8, 0x69, 0xB6, + 0xCB, 0x83, 0xD2, 0xF0, 0x41, 0xE7, 0x94, 0xA9, 0x40, 0xC8, 0x61, 0x17, + 0xDD, 0x3E, 0x84, 0x52, 0x75, 0x36, 0x01, 0x73, 0xE7, 0x81, 0x9D, 0x14, + 0x87, 0xCB, 0x9D, 0x0F, 0xB4, 0x0C, 0x5B, 0xDC, 0x2D, 0x28, 0x62, 0x24, + 0x38, 0xC7, 0x41, 0xF9, 0x08, 0x4E, 0xF3, 0x59, 0xC1, 0xBC, 0x69, 0x10, + 0x29, 0x91, 0xF2, 0x2E, 0x4F, 0x90, 0xA6, 0xF6, 0xDE, 0x80, 0xFE, 0xB5, + 0x47, 0xC9, 0x03, 0x37, 0xDB, 0xE7, 0x50, 0x7B, 0xD3, 0xE3, 0x1C, 0x78, + 0x0D, 0x8A, 0x8D, 0x2B, 0x06, 0xCE, 0x70, 0x8B, 0x16, 0x10, 0x49, 0xA2, + 0x6D, 0xAF, 0x43, 0xC1, 0x9E, 0x50, 0x60, 0x3F, 0x45, 0x48, 0x68, 0x3A, + 0x8C, 0x47, 0x11, 0x2C, 0x35, 0x6B, 0x2B, 0xF9, 0x30, 0xC7, 0xDC, 0xE7, + 0x2B, 0xF1, 0x7C, 0x1A, 0xCA, 0xE0, 0xB1, 0x47, 0x41, 0xD4, 0x07, 0xDE, + 0xB6, 0x79, 0xC5, 0xA3, 0x4B, 0x7D, 0xC3, 0x2B, 0x68, 0x31, 0x37, 0xF9, + 0x64, 0xED, 0x2B, 0xEE, 0x79, 0x1C, 0x6E, 0x06, 0x05, 0x64, 0xCF, 0x0B, + 0xD2, 0xA1, 0xD3, 0x3A, 0x48, 0x82, 0x73, 0xA4, 0x83, 0x77, 0xF5, 0xF9, + 0xD2, 0x9F, 0x75, 0xC8, 0x97, 0x3B, 0xD7, 0x43, 0xF1, 0xFB, 0x04, 0xDB, + 0x32, 0xA0, 0x0A, 0xD4, 0x90, 0x2A, 0x60, 0x13, 0x80, 0x50, 0x18, 0x0F, + 0x3B, 0xE4, 0xC5, 0x89, 0xC8, 0x2B, 0xC7, 0x2A, 0x47, 0x6E, 0xBB, 0x6E, + 0x8B, 0xA1, 0xAC, 0xE0, 0x79, 0x08, 0x8A, 0xF9, 0x0F, 0x09, 0x29, 0xD7, + 0x10, 0xEA, 0x80, 0xCC, 0x09, 0x91, 0x33, 0x11, 0x2A, 0x71, 0xD9, 0xEE, + 0x0B, 0x6D, 0xDC, 0x4F, 0x3D, 0x22, 0x53, 0xD6, 0x99, 0x5D, 0x48, 0x4E, + 0x63, 0x14, 0xBC, 0x8F, 0x4E, 0x3B, 0xED, 0xB5, 0x10, 0xDF, 0x60, 0xE4, + 0xD8, 0xC5, 0x4B, 0x82, 0x36, 0x7B, 0x06, 0xEF, 0xF3, 0x74, 0xC4, 0x29, + 0x30, 0xD4, 0x8C, 0x54, 0x13, 0x39, 0xC7, 0xB1, 0x23, 0xE0, 0xCA, 0xE1, + 0x14, 0x4E, 0x23, 0x3F, 0x1D, 0x04, 0xB4, 0xF5, 0x4B, 0xCD, 0x0F, 0xA4, + 0x91, 0x99, 0x22, 0xD7, 0xB1, 0x88, 0xFF, 0x3D, 0x38, 0x05, 0x28, 0x6C, + 0xCB, 0xF4, 0xE8, 0x86, 0x8D, 0x48, 0xEF, 0x91, 0x44, 0xF2, 0x97, 0x6D, + 0x33, 0x9F, 0xB3, 0x87, 0x58, 0x73, 0xCE, 0x4E, 0xBB, 0xC1, 0x54, 0x86, + 0xD4, 0xEC, 0x6B, 0xB3, 0xE1, 0x41, 0x3F, 0x4E, 0x26, 0xBC, 0x81, 0x92, + 0x56, 0x75, 0xDF, 0xBF, 0xF3, 0xD7, 0x87, 0x81, 0x6D, 0x57, 0xDF, 0xA5, + 0xE1, 0x85, 0x8F, 0xD1, 0x87, 0x35, 0xC1, 0xB2, 0xDF, 0x52, 0xC8, 0x6E, + 0x77, 0xD7, 0xD1, 0xA7, 0xF4, 0xA6, 0x49, 0x1C, 0x2E, 0x22, 0xF0, 0xB9, + 0x23, 0xB3, 0xC1, 0x77, 0x25, 0x27, 0xC6, 0x9A, +}; + +static const uint32_t KEELOQ_MFCODES_ENC_LEN = 2000; diff --git a/src/modules/rf/protocols/rf_legacy_migrate.cpp b/src/modules/rf/protocols/rf_legacy_migrate.cpp new file mode 100644 index 000000000..c8130635a --- /dev/null +++ b/src/modules/rf/protocols/rf_legacy_migrate.cpp @@ -0,0 +1,114 @@ +#include "rf_legacy_migrate.h" + +#if RF_SUB_LEGACY_MIGRATION + +#include "rf_registry.h" + +// Old legacy protocol number -> registry protocol name. Only the numbers +// the firmware actually emitted are mapped; everything else falls through to +// a generic "RcSwitch_N" entry when one exists. +struct LegacyMap { + int proto_no; + const char *name; +}; +static const LegacyMap legacy_map[] = { + {1, "Princeton" }, + {6, "Holtek_HT12"}, + {11, "RcSwitch_11"}, + {20, "CAME" }, + {22, "NICE_FLO" }, +}; + +const RfProtocolDef *rf_find_legacy(int preset_no) { + for (const auto &m : legacy_map) { + if (m.proto_no == preset_no) return rf_find_protocol(m.name); + } + // generic numbered fallback (RcSwitch_1..12) + const RfProtocolDef *p = rf_find_protocol(String("RcSwitch_") + String(preset_no)); + return p; +} + +bool rf_sub_is_legacy(const RfCodes &code) { + if (code.protocol == "RcSwitch") return true; + // A bare numeric preset (e.g. "11") is the old way of carrying a protocol. + if (code.preset.length() > 0) { + bool allDigits = true; + for (size_t i = 0; i < code.preset.length(); i++) { + if (!isDigit(code.preset[i])) { + allDigits = false; + break; + } + } + if (allDigits) return true; + } + return false; +} + +// Paths we must never try to rewrite (volatile / RAM-backed filesystems). +static bool is_volatile_path(const String &path) { + return path.startsWith("/tmpramfile") || path.indexOf("tmpramfile") >= 0; +} + +bool rf_sub_migrate(FS *fs, const String &path, RfCodes &code) { + if (!rf_sub_is_legacy(code)) return false; + + // Resolve the legacy protocol number -> registry name and migrate the + // in-memory representation first (this is what replay uses). + int proto_no = 0; + if (code.protocol == "RcSwitch") { + proto_no = code.preset.toInt(); // old: Preset carried the number + } else { + proto_no = code.preset.toInt(); + } + const RfProtocolDef *def = rf_find_legacy(proto_no); + if (!def) return false; + + code.protocol = def->name; + if (code.te == 0) code.te = def->te; + + // Best-effort on-disk rewrite. Skip volatile / unavailable filesystems; + // the in-memory migration above is enough for replay to work. + if (!fs || is_volatile_path(path)) return true; + + String bakPath = path + ".bak"; + if (fs->exists(bakPath)) return true; // already migrated once (idempotent) + + File in = fs->open(path, FILE_READ); + if (!in) return true; // read-only / unavailable: keep in-memory migration + + String original; + while (in.available()) original += (char)in.read(); + in.close(); + + // Back up the original verbatim, then rewrite Protocol:/Preset: lines. + File bak = fs->open(bakPath, FILE_WRITE); + if (!bak) return true; // can't back up -> don't risk rewriting + bak.print(original); + bak.close(); + + String out; + int start = 0; + while (start < (int)original.length()) { + int nl = original.indexOf('\n', start); + String line = (nl < 0) ? original.substring(start) : original.substring(start, nl + 1); + if (line.startsWith("Protocol:")) { + out += "Protocol: " + code.protocol + "\n"; + } else if (line.startsWith("Preset:")) { + // keep a sane OOK preset name so the new path resolves it + out += "Preset: Ook270Async\n"; + } else { + out += line; + } + if (nl < 0) break; + start = nl + 1; + } + + File rewrite = fs->open(path, FILE_WRITE); + if (rewrite) { + rewrite.print(out); + rewrite.close(); + } + return true; +} + +#endif // RF_SUB_LEGACY_MIGRATION diff --git a/src/modules/rf/protocols/rf_legacy_migrate.h b/src/modules/rf/protocols/rf_legacy_migrate.h new file mode 100644 index 000000000..a98c012fe --- /dev/null +++ b/src/modules/rf/protocols/rf_legacy_migrate.h @@ -0,0 +1,38 @@ +#pragma once + +#include "rf_config.h" + +#if RF_SUB_LEGACY_MIGRATION + +#include "../structs.h" +#include "rf_protocol.h" +#include + +// =========================================================================== +// REMOVABLE legacy-compatibility module. +// +// Everything related to the OLD `.sub` format (Protocol: RcSwitch + numeric +// Preset) lives here and only here, guarded by RF_SUB_LEGACY_MIGRATION. +// To drop legacy support in a future release: delete this file + its .cpp, +// remove the single call site in readSubFile(), and set the macro to 0. +// +// Do NOT spread RcSwitch / legacy handling into the registry files. +// =========================================================================== + +// Map an old legacy protocol number to a registry protocol definition. +// Returns nullptr when the number has no known mapping. +const RfProtocolDef *rf_find_legacy(int preset_no); + +// True if `code` is in the old format (Protocol == "RcSwitch", or a bare +// numeric Preset) and is therefore a migration candidate. +bool rf_sub_is_legacy(const RfCodes &code); + +// One-shot migration: rewrite `path` from the old format to the new +// registry-based format, after backing the original up to `.bak` +// (idempotent — never overwrites an existing `.bak`). Read-only filesystems +// and temporary paths (/tmpramfile, PSRamFS) are skipped. Updates `code` to +// the migrated values regardless of whether the file could be rewritten. +// Returns true when the in-memory `code` was migrated. +bool rf_sub_migrate(FS *fs, const String &path, RfCodes &code); + +#endif // RF_SUB_LEGACY_MIGRATION diff --git a/src/modules/rf/protocols/rf_presets.cpp b/src/modules/rf/protocols/rf_presets.cpp new file mode 100644 index 000000000..1dde711da --- /dev/null +++ b/src/modules/rf/protocols/rf_presets.cpp @@ -0,0 +1,46 @@ +#include "rf_presets.h" + +// Canonical preset table. Values reproduce exactly the parameters that used +// to be inlined in sendRfCommand(): a field at 0 means "keep module default". +// default OOK radio params (module defaults): mod=2, dev=1.58, BW=270.83, +// dataRate=10. So OOK presets only override what differs. +static const RfPreset rf_presets[] = { + // name mod dev rxBW dataRate legacyProto + {"Ook270Async", 2, 0.0f, 270.f, 0.0f, 1}, + {"Ook650Async", 2, 0.0f, 650.f, 0.0f, 2}, + {"2FSKDev238Async", 0, 2.380371f, 238.f, 0.0f, 1}, + {"2FSKDev476Async", 0, 47.60742f, 476.f, 0.0f, 1}, + {"MSK99_97KbAsync", 4, 47.60742f, 0.0f, 99.97f, 1}, + {"GFSK9_99KbAsync", 1, 19.042969f, 0.0f, 9.996f, 1}, +}; + +// Alias map: legacy Furi preset names found in existing `.sub` files → +// canonical preset name above. Keeps old files working without touching the +// preset table. Documented in protocols/README.md. +struct PresetAlias { + const char *alias; + const char *canonical; +}; +static const PresetAlias rf_preset_aliases[] = { + {"FuriHalSubGhzPresetOok270Async", "Ook270Async" }, + {"FuriHalSubGhzPresetOok650Async", "Ook650Async" }, + {"FuriHalSubGhzPreset2FSKDev238Async", "2FSKDev238Async"}, + {"FuriHalSubGhzPreset2FSKDev476Async", "2FSKDev476Async"}, + {"FuriHalSubGhzPresetMSK99_97KbAsync", "MSK99_97KbAsync"}, + {"FuriHalSubGhzPresetGFSK9_99KbAsync", "GFSK9_99KbAsync"}, +}; + +const RfPreset *rf_find_preset(const String &name) { + // resolve a legacy alias to its canonical name first + String wanted = name; + for (const auto &a : rf_preset_aliases) { + if (name == a.alias) { + wanted = a.canonical; + break; + } + } + for (const auto &p : rf_presets) { + if (wanted == p.name) return &p; + } + return nullptr; +} diff --git a/src/modules/rf/protocols/rf_presets.h b/src/modules/rf/protocols/rf_presets.h new file mode 100644 index 000000000..5d9e45bff --- /dev/null +++ b/src/modules/rf/protocols/rf_presets.h @@ -0,0 +1,10 @@ +#pragma once + +#include "rf_protocol.h" + +// Radio preset registry. Single place where the `.sub` "Preset:" names map +// to concrete transceiver parameters. + +// Resolve a preset by its canonical/alias name. Returns nullptr if unknown +// (caller then tries the numeric legacy-protocol path). +const RfPreset *rf_find_preset(const String &name); diff --git a/src/modules/rf/protocols/rf_protocol.h b/src/modules/rf/protocols/rf_protocol.h new file mode 100644 index 000000000..4747f7c82 --- /dev/null +++ b/src/modules/rf/protocols/rf_protocol.h @@ -0,0 +1,47 @@ +#pragma once + +#include "../structs.h" // HighLow +#include + +// Single source of truth for static OOK protocol definitions and radio +// presets used by the RF module. Consumers (send / scan / replay) read +// from here; no protocol parameters should be redefined elsewhere. + +// --------------------------------------------------------------------------- +// Radio preset: configures the transceiver (modulation, bandwidth, deviation, +// data rate). Replaces the string if/else previously inlined in sendRfCommand. +// A field left at 0 means "keep the module default" (do not override). +// --------------------------------------------------------------------------- +struct RfPreset { + const char *name; // canonical preset name written/read in `.sub` + uint8_t modulation; // CC1101: 0=2-FSK, 1=GFSK, 2=ASK/OOK, 4=MSK + float deviation; // kHz (0 = keep default) + float rxBW; // kHz (0 = keep default) + float dataRate; // kbps (0 = keep default) + uint8_t legacyProto; // default legacy protocol no. for OOK presets + // (kept so the legacy TX path stays bit-identical; + // irrelevant for FSK/MSK/GFSK presets) +}; + +// --------------------------------------------------------------------------- +// Static OOK protocol definition. Timings follow the classic factor model: +// every pulse is a multiple of `te` µs, expressed as {high, low} counts. +// `name` is the protocol identity written to `Protocol:` in the `.sub` file +// and used for replay dispatch — choose neutral, stable names. +// --------------------------------------------------------------------------- +struct RfProtocolDef { + const char *name; + uint16_t te; // base pulse length in µs + HighLow sync; // sync / pilot factor ({0,0} = none) + HighLow zero; // bit 0 encoding + HighLow one; // bit 1 encoding + uint8_t bits; // typical payload length in bits (0 = variable) + bool inverted; // inverted signal level + uint8_t flags; // bitmask, see RF_PF_* below +}; + +// Protocol flags bitmask. +enum RfProtocolFlags : uint8_t { + RF_PF_HAS_SYNC = 0x01, // protocol uses a sync/pilot pulse + RF_PF_FIXED_LEN = 0x02, // payload length is fixed (== bits) +}; diff --git a/src/modules/rf/protocols/rf_registry.cpp b/src/modules/rf/protocols/rf_registry.cpp new file mode 100644 index 000000000..70af4a8c1 --- /dev/null +++ b/src/modules/rf/protocols/rf_registry.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). The OOK protocol timing table is DERIVED +// FROM and modified after rc-switch (LGPL-2.1-or-later), (C) 2011 Suat Ozgur +// and contributors; the Flipper-name mapping mirrors the Flipper Zero firmware +// (GPL-3.0-or-later). See THIRD_PARTY.md for full attribution. +#include "rf_registry.h" + +// Canonical static OOK protocol table. Timings use the classic factor model +// ({high,low} multiples of `te` µs). The numbered "RcSwitch_N" entries mirror +// the classic numeric protocol table (so legacy numeric presets still +// resolve); the named entries are the human-facing protocol identities +// written to `Protocol:` in `.sub` files. +// +// Sources: classic numeric protocol table (proto 1..22) and the brute-force +// timing table previously kept in rf_bruteforce.h. +#define SYNC RF_PF_HAS_SYNC +#define FIXED RF_PF_FIXED_LEN + +static const RfProtocolDef rf_protocols[] = { + // ---- Named protocols (canonical identities) -------------------------- + // Two structural families share this table (same {high,low}-factor model): + // * high-first (inv=false): pulse is HIGH for high*te then LOW for low*te; + // the sync is a short HIGH followed by the long inter-frame LOW gap. + // * space-coded (inv=true): pulse is LOW for high*te then HIGH for low*te; + // the sync is the long LOW gap followed by a short HIGH preamble. For + // these `sync` is stored as {gap_factor, preamble_factor}. + // `bits` + FIXED is the per-protocol payload length; the decoder rejects a + // frame whose decoded length differs, which disambiguates same-timing codes. + // name te sync zero one bits inv flags + {"Princeton", 350, {1, 31}, {1, 3}, {3, 1}, 24, false, SYNC}, // legacy proto 1 + {"NICE_FLO", 700, {1, 36}, {2, 1}, {1, 2}, 12, false, SYNC | FIXED}, // legacy proto 22 + {"Linear", 500, {3, 42}, {1, 3}, {3, 1}, 10, false, SYNC | FIXED}, // 10-bit DIP + {"Clemsa", 385, {7, 50}, {1, 7}, {7, 1}, 18, false, SYNC | FIXED}, + {"Mastercode", 1072, {2, 14}, {1, 2}, {2, 1}, 36, false, SYNC | FIXED}, + {"CAME", 320, {36, 1}, {2, 1}, {1, 2}, 12, true, SYNC | FIXED}, // legacy proto 20 (space-coded) + {"Ansonic", 555, {35, 1}, {1, 2}, {2, 1}, 12, true, SYNC | FIXED}, + {"GateTX", 350, {49, 2}, {1, 2}, {2, 1}, 24, true, SYNC | FIXED}, + {"Holtek", 430, {36, 1}, {1, 2}, {2, 1}, 40, true, SYNC | FIXED}, // HT6Pxx 40-bit + {"Holtek_HT12", 450, {23, 1}, {1, 2}, {2, 1}, 12, true, SYNC | FIXED}, // legacy proto 6 (HT6P20B) + {"PhoenixV2", 427, {60, 6}, {1, 2}, {2, 1}, 52, true, SYNC | FIXED}, + + // ---- Generic numbered protocols (classic legacy proto 1..12) ------- + {"RcSwitch_1", 350, {1, 31}, {1, 3}, {3, 1}, 0, false, SYNC}, + {"RcSwitch_2", 650, {1, 10}, {1, 2}, {2, 1}, 0, false, SYNC}, + {"RcSwitch_3", 100, {30, 71}, {4, 11}, {9, 6}, 0, false, SYNC}, + {"RcSwitch_4", 380, {1, 6}, {1, 3}, {3, 1}, 0, false, SYNC}, + {"RcSwitch_5", 500, {6, 14}, {1, 2}, {2, 1}, 0, false, SYNC}, + {"RcSwitch_6", 450, {23, 1}, {1, 2}, {2, 1}, 0, true, SYNC}, + {"RcSwitch_7", 150, {2, 62}, {1, 6}, {6, 1}, 0, false, SYNC}, + {"RcSwitch_8", 200, {3, 130}, {7, 16}, {3, 16}, 0, false, SYNC}, + {"RcSwitch_9", 200, {130, 7}, {16, 7}, {16, 3}, 0, true, SYNC}, + {"RcSwitch_10", 365, {18, 1}, {3, 1}, {1, 3}, 0, true, SYNC}, + {"RcSwitch_11", 270, {36, 1}, {1, 2}, {2, 1}, 0, true, SYNC}, + {"RcSwitch_12", 320, {36, 1}, {1, 2}, {2, 1}, 0, true, SYNC}, +}; + +#undef SYNC +#undef FIXED + +static const int rf_protocols_count = sizeof(rf_protocols) / sizeof(rf_protocols[0]); + +// Flipper Zero protocol name <-> Bruce canonical registry name. Only the entries +// that differ in spelling are listed; names that already match (Princeton, CAME, +// Linear, Clemsa, Mastercode, Ansonic, GateTX, Holtek, KeeLoq...) need no alias. +struct RfProtoAlias { + const char *flipper; + const char *canonical; +}; +static const RfProtoAlias rf_proto_aliases[] = { + {"Nice FLO", "NICE_FLO"}, + {"Holtek_HT12X", "Holtek_HT12"}, + {"Phoenix_V2", "PhoenixV2"}, +}; + +const RfProtocolDef *rf_find_protocol(const String &name) { + // Resolve a Flipper protocol name to the canonical one first. + String wanted = name; + for (const auto &a : rf_proto_aliases) { + if (name == a.flipper) { + wanted = a.canonical; + break; + } + } + for (const auto &p : rf_protocols) { + if (wanted == p.name) return &p; + } + return nullptr; +} + +String rf_flipper_protocol_name(const String &canonical) { + for (const auto &a : rf_proto_aliases) { + if (canonical == a.canonical) return a.flipper; + } + return canonical; +} + +const RfProtocolDef *rf_protocol_for_number(int proto_no) { + // Classic legacy numbers that map to a NAMED registry identity. + switch (proto_no) { + case 20: return rf_find_protocol("CAME"); + case 22: return rf_find_protocol("NICE_FLO"); + default: break; + } + // Numbers 1..12 mirror the generic RcSwitch_N entries. + const RfProtocolDef *p = rf_find_protocol(String("RcSwitch_") + String(proto_no)); + if (p) return p; + return rf_find_protocol("RcSwitch_1"); // safe default (never null) +} + +const RfProtocolDef *rf_protocol_at(int index) { + if (index < 0 || index >= rf_protocols_count) return nullptr; + return &rf_protocols[index]; +} + +int rf_protocol_count() { return rf_protocols_count; } diff --git a/src/modules/rf/protocols/rf_registry.h b/src/modules/rf/protocols/rf_registry.h new file mode 100644 index 000000000..aaf8c20a2 --- /dev/null +++ b/src/modules/rf/protocols/rf_registry.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// Part of Bruce (AGPL-3.0-or-later). The OOK protocol timing table is DERIVED +// FROM and modified after rc-switch (LGPL-2.1-or-later), (C) 2011 Suat Ozgur +// and contributors; the Flipper-name mapping mirrors the Flipper Zero firmware +// (GPL-3.0-or-later). See THIRD_PARTY.md for full attribution. +#pragma once + +#include "rf_protocol.h" + +// Static OOK protocol registry. Lookup + iteration used by the decoder (M2) +// and the replay dispatch (M3). + +// Find a protocol definition by name. Flipper Zero protocol names (as written in +// standard `.sub` files, e.g. "Nice FLO", "Holtek_HT12X", "Phoenix_V2") are +// accepted and resolved to the canonical registry entry. Returns nullptr if none. +const RfProtocolDef *rf_find_protocol(const String &name); + +// Map a canonical registry name back to the Flipper protocol name (for writing +// Flipper-standard `.sub` files). Returns the name unchanged when it is already +// Flipper-compatible. +String rf_flipper_protocol_name(const String &canonical); + +// Resolve a classic OOK protocol NUMBER (as still carried by the Serial +// CLI `subghz tx` and the `RfSend` JSON API) to a registry definition. This is +// part of the permanent TX motor (not legacy `.sub` migration): the numeric +// transmit contract is external and must survive removal of the legacy module. +// Falls back to RcSwitch_1 for unknown numbers; never returns nullptr. +const RfProtocolDef *rf_protocol_for_number(int proto_no); + +// Iteration helpers (e.g. for the generic decoder to try every protocol). +const RfProtocolDef *rf_protocol_at(int index); +int rf_protocol_count(); diff --git a/src/modules/rf/rf_bruteforce.cpp b/src/modules/rf/rf_bruteforce.cpp index a1ef9cf08..d9001c555 100644 --- a/src/modules/rf/rf_bruteforce.cpp +++ b/src/modules/rf/rf_bruteforce.cpp @@ -103,10 +103,10 @@ void rf_bruteforce() { const BruteProtocol &proto = brute_protocols[brute_protocol_idx]; int option = 0; options = { + {"Start", [&]() { option = 4; }}, {"Frequency: " + String(brute_frequency, 2), [&]() { option = 1; }}, {String("Protocol: ") + proto.name, [&]() { option = 2; }}, {"Repeats: " + String(brute_repeats), [&]() { option = 3; }}, - {"Start", [&]() { option = 4; }}, {"Main Menu", [&]() { option = 5; }}, }; loopOptions(options); diff --git a/src/modules/rf/rf_jammer.cpp b/src/modules/rf/rf_jammer.cpp index 7554d7a8c..c0c0c5edc 100644 --- a/src/modules/rf/rf_jammer.cpp +++ b/src/modules/rf/rf_jammer.cpp @@ -5,20 +5,19 @@ #include #include +#define MAX_JAMMER_RUNTIME 30000 // 30 seconds max runtime for jammer (safety cutoff) + static const uint32_t MAX_SEQUENCE = 50; static const uint32_t DURATION_CYCLES = 3; static const char *JAM_MODE_NAMES[] = {"FULL POWER", "INTERMITTENT", "NOISE STORM", "FREQ SWEEP"}; -static const uint16_t JAM_MODE_COLORS[] = {TFT_RED, TFT_ORANGE, TFT_MAGENTA, TFT_CYAN}; RFJammer::RFJammer(bool full) { jamMode = full ? RF_JAM_FULL : RF_JAM_ITMT; setup(); } -RFJammer::~RFJammer() { - deinitRfModule(); -} +RFJammer::~RFJammer() { deinitRfModule(); } // ── Mode selection menu ───────────────────────────────────────── RFJamMode RFJammer::showModeMenu(bool defaultFull) { @@ -27,6 +26,7 @@ RFJamMode RFJammer::showModeMenu(bool defaultFull) { int menuIdx = defaultFull ? 0 : 1; bool redraw = true; int modeCount = isCC1101 ? RF_JAM_MODE_COUNT : 2; // Only Full+Itmt for raw GPIO + uint16_t accent = getComplementaryColor2(bruceConfig.priColor); while (true) { if (check(EscPress)) return (RFJamMode)255; // Cancel @@ -41,7 +41,7 @@ RFJamMode RFJammer::showModeMenu(bool defaultFull) { for (int i = 0; i < modeCount; i++) { int itemY = y + i * lineH; uint16_t fg = (i == menuIdx) ? bruceConfig.bgColor : bruceConfig.priColor; - uint16_t bg = (i == menuIdx) ? JAM_MODE_COLORS[i] : bruceConfig.bgColor; + uint16_t bg = (i == menuIdx) ? accent : bruceConfig.bgColor; tft.fillRect(7, itemY, tftWidth - 14, lineH - 2, bg); tft.setTextColor(fg, bg); @@ -51,7 +51,7 @@ RFJamMode RFJammer::showModeMenu(bool defaultFull) { // Description of selected mode int descY = y + modeCount * lineH + 4; tft.fillRect(7, descY, tftWidth - 14, lineH * 2, bruceConfig.bgColor); - tft.setTextColor(TFT_DARKGREY, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); const char *descs[] = { "Max duty cycle continuous TX", "Varied pulse patterns + bursts", @@ -63,14 +63,20 @@ RFJamMode RFJammer::showModeMenu(bool defaultFull) { // Footer int footerY = tftHeight - BORDER_PAD_X - FP * LH - 2; tft.fillRect(7, footerY, tftWidth - 14, FP * LH, bruceConfig.bgColor); - tft.setTextColor(TFT_DARKGREY, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); tft.drawCentreString("[OK]Select [ESC]Back", tftWidth / 2, footerY, 1); redraw = false; } - if (check(NextPress)) { menuIdx = (menuIdx + 1) % modeCount; redraw = true; } - if (check(PrevPress)) { menuIdx = (menuIdx + modeCount - 1) % modeCount; redraw = true; } + if (check(NextPress)) { + menuIdx = (menuIdx + 1) % modeCount; + redraw = true; + } + if (check(PrevPress)) { + menuIdx = (menuIdx + modeCount - 1) % modeCount; + redraw = true; + } if (check(SelPress)) return (RFJamMode)menuIdx; delay(50); @@ -80,16 +86,20 @@ RFJamMode RFJammer::showModeMenu(bool defaultFull) { void RFJammer::setup() { // Show mode selection first RFJamMode selected = showModeMenu(jamMode == RF_JAM_FULL); - if ((uint8_t)selected == 255) { sendRF = false; return; } + if ((uint8_t)selected == 255) { + sendRF = false; + return; + } jamMode = selected; nTransmitterPin = bruceConfigPins.rfTx; - if (!initRfModule("tx")) { sendRF = false; return; } + if (!initRfModule("tx")) { + sendRF = false; + return; + } isCC1101 = (bruceConfigPins.rfModule == CC1101_SPI_MODULE); - if (isCC1101) { - nTransmitterPin = bruceConfigPins.CC1101_bus.io0; - } + if (isCC1101) { nTransmitterPin = bruceConfigPins.CC1101_bus.io0; } sendRF = true; pulseCount = 0; @@ -97,8 +107,8 @@ void RFJammer::setup() { display_banner(); switch (jamMode) { - case RF_JAM_FULL: run_full_jammer(); break; - case RF_JAM_ITMT: run_itmt_jammer(); break; + case RF_JAM_FULL: run_full_jammer(); break; + case RF_JAM_ITMT: run_itmt_jammer(); break; case RF_JAM_NOISE: run_noise_jammer(); break; case RF_JAM_SWEEP: run_sweep_jammer(); break; default: break; @@ -112,34 +122,34 @@ void RFJammer::display_banner() { int lineH = max(14, tftHeight / 10); tft.setTextSize(FP); char buf[40]; + uint16_t accent = getComplementaryColor2(bruceConfig.priColor); - // Line 1: Mode badge (centered, colored rounded rect) + // Line 1: Mode badge (centered, accent rounded rect) const char *modeName = JAM_MODE_NAMES[jamMode]; - uint16_t modeClr = JAM_MODE_COLORS[jamMode]; int mbW = strlen(modeName) * 6 + 10; int mbX = (tftWidth - mbW) / 2; - tft.fillRoundRect(mbX, y, mbW, lineH - 2, 3, modeClr); - tft.setTextColor(TFT_BLACK, modeClr); + tft.fillRoundRect(mbX, y, mbW, lineH - 2, 3, accent); + tft.setTextColor(bruceConfig.bgColor, accent); tft.drawCentreString(modeName, tftWidth / 2, y + 2, 1); y += lineH; // Line 2: Frequency + Module badge tft.fillRect(7, y, tftWidth - 14, lineH, bruceConfig.bgColor); - tft.setTextColor(TFT_CYAN, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); snprintf(buf, sizeof(buf), "%.2f MHz", bruceConfigPins.rfFreq); tft.drawString(buf, 12, y + 2, 1); const char *modStr = isCC1101 ? "CC1101" : "RAW TX"; int mdW = strlen(modStr) * 6 + 8; int mdX = tftWidth - 12 - mdW; - tft.fillRoundRect(mdX, y + 1, mdW, lineH - 3, 3, TFT_DARKGREY); - tft.setTextColor(TFT_WHITE, TFT_DARKGREY); + tft.fillRoundRect(mdX, y + 1, mdW, lineH - 3, 3, bruceConfig.priColor); + tft.setTextColor(bruceConfig.bgColor, bruceConfig.priColor); tft.drawCentreString(modStr, mdX + mdW / 2, y + 2, 1); y += lineH; // Line 3: Timer + Stats - tft.setTextColor(TFT_YELLOW, bruceConfig.bgColor); + tft.setTextColor(accent, bruceConfig.bgColor); tft.drawString("00:00", 12, y + 2, 1); - tft.setTextColor(TFT_DARKGREY, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); if (jamMode == RF_JAM_SWEEP) { tft.drawRightString("Sweeps: 0", tftWidth - 12, y + 2, 1); } else { @@ -147,30 +157,16 @@ void RFJammer::display_banner() { } y += lineH; - // Line 4: TX Power bar (gradient fill) - { - int bX = 12, bW = tftWidth - 24; - int bH = max(6, lineH - 6); - int bY = y + (lineH - bH) / 2; - tft.drawRect(bX, bY, bW, bH, bruceConfig.priColor); - int segW = (bW - 2) / 4; - tft.fillRect(bX + 1, bY + 1, segW, bH - 2, TFT_GREEN); - tft.fillRect(bX + 1 + segW, bY + 1, segW, bH - 2, TFT_YELLOW); - tft.fillRect(bX + 1 + segW * 2, bY + 1, segW, bH - 2, TFT_ORANGE); - tft.fillRect(bX + 1 + segW * 3, bY + 1, bW - 2 - segW * 3, bH - 2, TFT_RED); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.drawCentreString("TX POWER", tftWidth / 2, bY + 1, 1); - } y += lineH; // Line 5: ACTIVE indicator with dot - tft.fillCircle(tftWidth / 2 - 50, y + lineH / 2, 4, modeClr); - tft.setTextColor(modeClr, bruceConfig.bgColor); + tft.fillCircle(tftWidth / 2 - 50, y + lineH / 2, 4, accent); + tft.setTextColor(accent, bruceConfig.bgColor); tft.drawString("JAMMING ACTIVE", tftWidth / 2 - 38, y + 2, 1); // Footer int footerY = tftHeight - BORDER_PAD_X - FP * LH - 2; - tft.setTextColor(TFT_DARKGREY, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); tft.drawCentreString("[ESC] Stop", tftWidth / 2, footerY, 1); } @@ -183,14 +179,16 @@ void RFJammer::update_display(uint32_t elapsedMs) { int lineH = max(14, tftHeight / 10); y += lineH * 2; // Skip to timer line + uint16_t accent = getComplementaryColor2(bruceConfig.priColor); + // Update timer + stats tft.fillRect(7, y, tftWidth - 14, lineH, bruceConfig.bgColor); tft.setTextSize(FP); - tft.setTextColor(TFT_YELLOW, bruceConfig.bgColor); + tft.setTextColor(accent, bruceConfig.bgColor); char buf[30]; snprintf(buf, sizeof(buf), "%02lu:%02lu", mins, secs); tft.drawString(buf, 12, y + 2, 1); - tft.setTextColor(TFT_CYAN, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); if (jamMode == RF_JAM_SWEEP) { snprintf(buf, sizeof(buf), "Sweeps: %lu", (unsigned long)sweepCount); } else { @@ -200,9 +198,8 @@ void RFJammer::update_display(uint32_t elapsedMs) { // Blink activity dot y += lineH * 2; // Skip to ACTIVE line - uint16_t modeClr = JAM_MODE_COLORS[jamMode]; bool dotOn = (secs % 2 == 0); - tft.fillCircle(tftWidth / 2 - 50, y + lineH / 2, 4, dotOn ? modeClr : bruceConfig.bgColor); + tft.fillCircle(tftWidth / 2 - 50, y + lineH / 2, 4, dotOn ? accent : bruceConfig.bgColor); } // ── FULL POWER: Maximum duty cycle continuous TX ──────────────── @@ -224,37 +221,37 @@ void RFJammer::run_full_jammer() { phase = (elapsed / 100) % 3; switch (phase) { - case 0: - // Phase A: Ultra-rapid micro-glitches (1µs LOW every 4µs) - for (int i = 0; i < 100 && sendRF; i++) { + case 0: + // Phase A: Ultra-rapid micro-glitches (1µs LOW every 4µs) + for (int i = 0; i < 100 && sendRF; i++) { + digitalWrite(nTransmitterPin, HIGH); + delayMicroseconds(3); + digitalWrite(nTransmitterPin, LOW); + delayMicroseconds(1); + pulseCount++; + } + break; + case 1: + // Phase B: Variable-width burst disruption (2-20µs pulses) + for (int i = 0; i < 50 && sendRF; i++) { + uint32_t w = 2 + (micros() % 18); + digitalWrite(nTransmitterPin, HIGH); + delayMicroseconds(w); + digitalWrite(nTransmitterPin, LOW); + delayMicroseconds(1); + pulseCount++; + } + break; + case 2: + // Phase C: Sustained carrier with periodic hard cuts digitalWrite(nTransmitterPin, HIGH); - delayMicroseconds(3); + delayMicroseconds(80); digitalWrite(nTransmitterPin, LOW); - delayMicroseconds(1); - pulseCount++; - } - break; - case 1: - // Phase B: Variable-width burst disruption (2-20µs pulses) - for (int i = 0; i < 50 && sendRF; i++) { - uint32_t w = 2 + (micros() % 18); + delayMicroseconds(2); digitalWrite(nTransmitterPin, HIGH); - delayMicroseconds(w); - digitalWrite(nTransmitterPin, LOW); - delayMicroseconds(1); - pulseCount++; - } - break; - case 2: - // Phase C: Sustained carrier with periodic hard cuts - digitalWrite(nTransmitterPin, HIGH); - delayMicroseconds(80); - digitalWrite(nTransmitterPin, LOW); - delayMicroseconds(2); - digitalWrite(nTransmitterPin, HIGH); - delayMicroseconds(80); - pulseCount += 2; - break; + delayMicroseconds(80); + pulseCount += 2; + break; } if (currentTime - lastCheckTime > 100) { @@ -270,6 +267,8 @@ void RFJammer::run_full_jammer() { lastDisplayTime = currentTime; update_display(currentTime - startTime); } + + if (currentTime - startTime > MAX_JAMMER_RUNTIME) break; } digitalWrite(nTransmitterPin, LOW); } @@ -281,9 +280,7 @@ void RFJammer::run_itmt_jammer() { uint32_t lastDisplayTime = startTime; uint32_t sequenceValues[MAX_SEQUENCE]; - for (uint32_t i = 0; i < MAX_SEQUENCE; i++) { - sequenceValues[i] = 10 * (i + 1); - } + for (uint32_t i = 0; i < MAX_SEQUENCE; i++) { sequenceValues[i] = 10 * (i + 1); } while (sendRF) { // Forward sweep: 10µs → 500µs @@ -319,7 +316,11 @@ void RFJammer::run_itmt_jammer() { uint32_t currentTime = millis(); if (currentTime - lastCheckTime > 50) { lastCheckTime = currentTime; - if (check(EscPress)) { sendRF = false; returnToMenu = true; break; } + if (check(EscPress)) { + sendRF = false; + returnToMenu = true; + break; + } } if (currentTime - lastDisplayTime >= 1000) { lastDisplayTime = currentTime; @@ -332,6 +333,8 @@ void RFJammer::run_itmt_jammer() { send_random_pattern(200); pulseCount += 200; } + + if (millis() - startTime > MAX_JAMMER_RUNTIME) break; } digitalWrite(nTransmitterPin, LOW); } @@ -345,12 +348,12 @@ void RFJammer::run_noise_jammer() { // Switch CC1101 to PN9 random TX mode — maximum aggression ELECHOUSE_cc1101.setSidle(); - ELECHOUSE_cc1101.setPktFormat(2); // PN9 random TX mode - ELECHOUSE_cc1101.setDRate(800); // Push data rate higher for wider occupied BW - ELECHOUSE_cc1101.setModulation(2); // Start with ASK/OOK - ELECHOUSE_cc1101.setDeviation(47.6);// Max deviation for FSK modes - ELECHOUSE_cc1101.setRxBW(812); // Widest RX BW setting (not critical for TX but sets filter) - ELECHOUSE_cc1101.setPA(12); // Maximum TX power + ELECHOUSE_cc1101.setPktFormat(2); // PN9 random TX mode + ELECHOUSE_cc1101.setDRate(800); // Push data rate higher for wider occupied BW + ELECHOUSE_cc1101.setModulation(2); // Start with ASK/OOK + ELECHOUSE_cc1101.setDeviation(47.6); // Max deviation for FSK modes + ELECHOUSE_cc1101.setRxBW(812); // Widest RX BW setting (not critical for TX but sets filter) + ELECHOUSE_cc1101.setPA(12); // Maximum TX power ELECHOUSE_cc1101.SetTx(); uint32_t startTime = millis(); @@ -360,9 +363,10 @@ void RFJammer::run_noise_jammer() { int y = BORDER_PAD_Y + FM * LH + 4; int lineH = max(14, tftHeight / 10); y += lineH * 4; // ACTIVE line + uint16_t accent = getComplementaryColor2(bruceConfig.priColor); tft.fillRect(7, y, tftWidth - 14, lineH, bruceConfig.bgColor); - tft.fillCircle(tftWidth / 2 - 50, y + lineH / 2, 4, TFT_MAGENTA); - tft.setTextColor(TFT_MAGENTA, bruceConfig.bgColor); + tft.fillCircle(tftWidth / 2 - 50, y + lineH / 2, 4, accent); + tft.setTextColor(accent, bruceConfig.bgColor); tft.setTextSize(FP); tft.drawString("PN9 HW NOISE TX", tftWidth / 2 - 38, y + 2, 1); @@ -384,8 +388,8 @@ void RFJammer::run_noise_jammer() { // Show current modulation on display int modY = BORDER_PAD_Y + FM * LH + 4 + max(14, tftHeight / 10) * 4; tft.fillRect(7, modY, tftWidth - 14, max(14, tftHeight / 10), bruceConfig.bgColor); - tft.fillCircle(tftWidth / 2 - 50, modY + max(14, tftHeight / 10) / 2, 4, TFT_MAGENTA); - tft.setTextColor(TFT_MAGENTA, bruceConfig.bgColor); + tft.fillCircle(tftWidth / 2 - 50, modY + max(14, tftHeight / 10) / 2, 4, accent); + tft.setTextColor(accent, bruceConfig.bgColor); tft.setTextSize(FP); char modBuf[30]; snprintf(modBuf, sizeof(modBuf), "PN9 %s TX", modNames[modCycle]); @@ -404,6 +408,8 @@ void RFJammer::run_noise_jammer() { break; } + if (currentTime - startTime > MAX_JAMMER_RUNTIME) break; + delay(50); // Low CPU usage since hardware handles TX } @@ -486,13 +492,15 @@ void RFJammer::run_sweep_jammer() { y += lineH; // Freq line tft.fillRect(7, y, tftWidth - 14, lineH, bruceConfig.bgColor); tft.setTextSize(FP); - tft.setTextColor(TFT_CYAN, bruceConfig.bgColor); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); char buf[30]; snprintf(buf, sizeof(buf), "%.2f MHz", currentFreq); tft.drawString(buf, 12, y + 2, 1); update_display(currentTime - startTime); } + + if (currentTime - startTime > MAX_JAMMER_RUNTIME) break; } digitalWrite(nTransmitterPin, LOW); diff --git a/src/modules/rf/rf_scan.cpp b/src/modules/rf/rf_scan.cpp index 969c9aea3..44defc560 100644 --- a/src/modules/rf/rf_scan.cpp +++ b/src/modules/rf/rf_scan.cpp @@ -2,18 +2,23 @@ #include "core/led_control.h" #include "core/sd_functions.h" #include "core/type_convertion.h" +#include "protocols/rf_config.h" // RF_DBG +#include "protocols/rf_registry.h" // rf_flipper_protocol_name #include "rf_send.h" #include #include RFScan::RFScan() { setup(); } -RFScan::~RFScan() { deinitRfModule(); } +RFScan::~RFScan() { + _rx.end(); + deinitRfModule(); +} void RFScan::setup() { if (!initRfModule("rx", bruceConfigPins.rfFreq)) { return; } - RCSwitch_Enable_Receive(rcswitch); + enable_receive(); if (bruceConfigPins.rfScanRange < 0 || bruceConfigPins.rfScanRange > 3) { bruceConfigPins.setRfScanRange(3); @@ -24,8 +29,6 @@ void RFScan::setup() { if (bruceConfigPins.rfFxdFreq) frequency = bruceConfigPins.rfFreq; - // Clear cache for RAW signal - rcswitch.resetAvailable(); returnToMenu = false; restartScan = false; @@ -56,23 +59,23 @@ void RFScan::loop() { if (fast_scan()) return setup(); // frequency found, reset } - if (rcswitch.available() && !ReadRAW) { - read_rcswitch(); - if (autoSave && (lastSavedKey != received.key || received.key == 0)) save_signal(); - } - if (rcswitch.RAWavailable() && ReadRAW) { - read_raw(); - if (autoSave && (lastSavedKey != received.key || received.key == 0)) save_signal(); + std::vector durations; + if (_rx.poll(durations)) { + if (!ReadRAW) { + decode_signal(durations); + if (autoSave && (lastSavedKey != received.key || received.key == 0)) save_signal(); + } else { + read_raw(durations); + if (autoSave && (lastSavedKey != received.key || received.key == 0)) save_signal(); + } } } } -void RFScan::RCSwitch_Enable_Receive(RCSwitch rcswitch) { - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) { - rcswitch.enableReceive(bruceConfigPins.CC1101_bus.io0); - } else { - rcswitch.enableReceive(bruceConfigPins.rfRx); - } +void RFScan::enable_receive() { + // (Re)start the native RMT RX session used to capture signals. + _rx.end(); + _rx.begin(); } void RFScan::init_freqs() { @@ -108,7 +111,6 @@ bool RFScan::fast_scan() { frequency = _freqs[max_index].freq; setMHZ(frequency); Serial.println("Frequency Found: " + String(frequency)); - rcswitch.resetAvailable(); // When changing to fixed frequency, need to restart the module to reset the registers // so we get good signal reception at this frequency deinitRfModule(); @@ -121,53 +123,60 @@ bool RFScan::fast_scan() { } void keeloq_identify(RfCodes &instance) { - FS *fs = NULL; + // A null fs is fine: the keystore falls back to the encrypted built-in keys. + KeeloqKeystore keystore{keeloq_mfcodes_fs()}; - if (!getFsStorage(fs)) { return; } - - KeeloqKeystore keystore{fs}; + // Secure/Erreka need a seed; mirror the reference fallback to the + // serial-derived seed when none was captured from the frame. + uint32_t seed_eff = instance.seed ? instance.seed : (instance.fix & 0x0FFFFFFF); for (const auto &key : keystore.get_keys()) { - switch (key.type) { - case KEELOQ_SIMPLE_LEARNING: { - uint64_t decrypt = keeloq_decrypt(instance.encrypted, key.key); - - if (instance.keeloq_check_decrypt(decrypt)) { - instance.mf_name = key.mf_name; - instance.hop = decrypt; - - return; - } - - break; + // Unified derivation: every learning type turns (fix, seed, key) into the + // manufacturer key, then we decrypt the captured `encrypted` with it. + uint64_t man = keeloq_derive_man(key.type, instance.fix, seed_eff, key.key); + uint64_t decrypt = keeloq_decrypt(instance.encrypted, man); + + if (key.mf_name == "Centurion") { + if (instance.keeloq_check_decrypt_centurion(decrypt)) { + instance.mf_name = key.mf_name; + instance.hop = decrypt; + return; } + } - case KEELOQ_NORMAL_LEARNING: { - uint64_t man = keeloq_normal_learning(instance.fix, key.key); - uint64_t decrypt = keeloq_decrypt(instance.encrypted, man); - - if (instance.mf_name == "Centurion") { - if (instance.keeloq_check_decrypt_centurion(decrypt)) { - instance.hop = decrypt; - - return; - } - } - - if (instance.keeloq_check_decrypt(decrypt)) { - instance.mf_name = key.mf_name; - instance.hop = decrypt; - - return; - } - - break; - } + if (instance.keeloq_check_decrypt(decrypt)) { + instance.mf_name = key.mf_name; + instance.hop = decrypt; + return; } } } -void RFScan::read_rcswitch() { +// Try to decode a KeeLoq frame and resolve its manufacturer. On success fills +// the keeloq fields (key/fix/encrypted/serial/btn + mf_name/cnt via the +// keystore) and returns true. Shared by the scan and CLI receive paths. +bool rf_try_keeloq(const std::vector &durations, RfCodes &received) { + if (!rf_decode_keeloq(durations, received)) return false; + + uint64_t yek = reverse_bits(received.key, 64); + received.fix = yek >> 32; + received.btn = received.fix >> 28; + received.encrypted = yek & 0xFFFFFFFF; + received.serial = (yek >> 32) & 0xFFFFFFF; + received.seed = 0; + + keeloq_identify(received); // sets mf_name + cnt when a keystore entry matches + RF_DBG( + "decode keeloq MATCH mf=%s btn=%u cnt=%04X key=%llX", + received.mf_name.c_str(), + (unsigned)received.btn, + (unsigned)received.cnt, + (unsigned long long)received.key + ); + return true; +} + +void RFScan::decode_signal(const std::vector &durations) { received.fix = 0; received.hop = 0; received.btn = 0; @@ -175,55 +184,24 @@ void RFScan::read_rcswitch() { received.mf_name = "Unknown"; received.encrypted = 0; - // Add decoded data only (if any) to the RCCode - uint64_t decoded = rcswitch.getReceivedValue(); - - if (decoded) { // if there is a value decoded by RCSwitch, show it - Serial.println("RcSwitch signal captured"); + // Decode-only mode: show the signal only if a registry protocol (or KeeLoq) + // matched. + if (rf_try_keeloq(durations, received) || rf_decode_ook(durations, received)) { + Serial.println("Decoded signal captured: " + received.protocol); blinkLed(); ++signals; found_freq = frequency; received.frequency = long(frequency * 1000000); - received.key = decoded; - received.preset = String(rcswitch.getReceivedProtocol()); - received.protocol = "RcSwitch"; - received.te = rcswitch.getReceivedDelay(); - received.Bit = rcswitch.getReceivedBitlength(); received.filepath = "signal_" + String(signals); received.data = ""; - if (rcswitch.getReceivedProtocol() == 23) { - uint64_t yek = reverse_bits(decoded, 64); - - received.fix = yek >> 32; - received.btn = received.fix >> 28; - received.encrypted = yek & 0xFFFFFFFF; - received.serial = (yek >> 32) & 0xFFFFFFF; - - keeloq_identify(received); - } - frequency = 0; display_info(received, signals, ReadRAW, codesOnly, autoSave, title); } - - rcswitch.resetAvailable(); } -void RFScan::read_raw() { - // Add RAW data (& decoded data if any) to the RCCode - vTaskDelay(400 / portTICK_PERIOD_MS); // wait for all the signal to be read +void RFScan::read_raw(const std::vector &durations) { found_freq = frequency; - unsigned int *raw = rcswitch.getRAWReceivedRawdata(); - uint64_t decoded = rcswitch.getReceivedValue(); - int transitions = 0; - String _data = ""; - std::vector durations; - std::vector indexed_durations; - uint64_t result = 0; - uint8_t repetition = 0; - - received.te = 0; received.fix = 0; received.hop = 0; @@ -232,75 +210,49 @@ void RFScan::read_raw() { received.mf_name = "Unknown"; received.encrypted = 0; - for (transitions = 0; transitions < RCSWITCH_RAW_MAX_CHANGES; transitions++) { - if (raw[transitions] == 0) break; - if (transitions > 0) _data += " "; - signed int sign = (transitions % 2 == 0) ? 1 : -1; - - int duration = sign * (int)raw[transitions]; - if (duration < -5000 && repetition < 2) { repetition += 1; } - _data += String(duration); - if (received.te == 0 && duration > 0) received.te = duration; - - if (!decoded && repetition == 1 && duration >= -5000) { - int index = find_pulse_index(indexed_durations, duration); - if (index == -1) { - indexed_durations.push_back(abs(duration)); - index = indexed_durations.size() - 1; - } - durations.push_back(index); // Store indexes for CRC calculation - } - } + // Build the RAW representation (durations string + optional CRC). + String _data; + bool hasCrc = false; + uint64_t crc = 0; + std::vector indexed_durations; + int rawBits = 0; + int rawTe = 0; + rf_build_raw(durations, _data, hasCrc, crc, indexed_durations, rawBits, rawTe); received.data = _data; + received.te = rawTe; received.filepath = "signal_" + String(signals); received.frequency = long(frequency * 1000000); - // if there is a value decoded by RCSwitch, show it - if (decoded) { - Serial.println("RcSwitch signal captured"); + // if a registry protocol (or KeeLoq) decoded the signal, show it + if (rf_try_keeloq(durations, received) || rf_decode_ook(durations, received)) { + Serial.println("Decoded signal captured: " + received.protocol); blinkLed(); ++signals; - received.key = decoded; - received.preset = String(rcswitch.getReceivedProtocol()); - received.protocol = "RcSwitch"; received.indexed_durations = {}; - received.te = rcswitch.getReceivedDelay(); - received.Bit = rcswitch.getReceivedBitlength(); - - if (rcswitch.getReceivedProtocol() == 23) { - uint64_t yek = reverse_bits(decoded, 64); - - received.fix = yek >> 32; - received.btn = received.fix >> 28; - received.encrypted = yek & 0xFFFFFFFF; - received.serial = (yek >> 32) & 0xFFFFFFF; - - keeloq_identify(received); - } frequency = 0; display_info(received, signals, ReadRAW, codesOnly, autoSave, title); } - // if there is no value decoded by RCSwitch, but we calculated a CRC, show it - else if (repetition >= 2 && !durations.empty()) { + // no decode, but a repeated pattern gave us a CRC + else if (hasCrc) { Serial.println("Raw signal captured"); blinkLed(); ++signals; - received.preset = "0"; + received.preset = "Ook270Async"; received.protocol = "RAW"; - received.key = crc64_ecma(durations); // Calculate CRC-64 + received.key = crc; received.indexed_durations = indexed_durations; - received.Bit = durations.size(); + received.Bit = rawBits; frequency = 0; display_info(received, signals, ReadRAW, codesOnly, autoSave, title); } - // If there is no decoded value and no CRC calculated, only show the data when specified + // no decode and no CRC: only show the raw data when not filtering for codes else if (!codesOnly) { Serial.println("Raw data captured"); blinkLed(); ++signals; - received.preset = "0"; + received.preset = "Ook270Async"; received.protocol = "RAW"; received.key = 0; received.indexed_durations = {}; @@ -308,14 +260,10 @@ void RFScan::read_raw() { frequency = 0; display_info(received, signals, ReadRAW, codesOnly, autoSave, title); } - - rcswitch.resetAvailable(); } void RFScan::select_menu_option() { -#if !defined(T_EMBED_1101) && !defined(CONFIG_IDF_TARGET_ESP32C5) - rcswitch.disableReceive(); // it is causing T-Embed to restart -#endif + _rx.end(); // stop the RMT receiver while the menu is open options = {}; @@ -408,9 +356,9 @@ void RFScan::replay_signal(bool asRaw) { void RFScan::save_signal(bool asRaw) { asRaw = asRaw || received.protocol == "RAW"; - Serial.println(asRaw ? "RCSwitch_SaveSignal RAW true" : "RCSwitch_SaveSignal RAW false"); + Serial.println(asRaw ? "rfSaveSignal RAW true" : "rfSaveSignal RAW false"); decimalToHexString(received.key, hexString); - RCSwitch_SaveSignal(found_freq, received, asRaw, hexString, autoSave); + rfSaveSignal(found_freq, received, asRaw, hexString, autoSave); lastSavedKey = received.key; } @@ -476,32 +424,45 @@ bruceConfigPins.setRfScanRange(2); }}, {subghz_frequency_ranges[3], else displayTextLine("Range set to " + String(subghz_frequency_ranges[bruceConfigPins.rfScanRange])); } */ -void display_info(RfCodes received, int signals, bool ReadRAW, bool codesOnly, bool autoSave, String title) { - if (title != "") drawMainBorderWithTitle(title); - else drawMainBorder(); +// Routes one info line to the right sink: Serial when running headless (CLI), +// otherwise the on-screen padded print used by the interactive scanner. +static void rf_info_line(bool headless, const String &s) { + if (headless) Serial.println(s); + else padprintln(s); +} - if (received.protocol != "") display_signal_data(received); +void display_info( + RfCodes received, int signals, bool ReadRAW, bool codesOnly, bool autoSave, String title, bool headless +) { + if (!headless) { + if (title != "") drawMainBorderWithTitle(title); + else drawMainBorder(); + } - tft.setTextColor(getColorVariation(bruceConfig.priColor), bruceConfig.bgColor); + if (received.protocol != "") display_signal_data(received, headless); - if (!ReadRAW) padprintln("Recording: Only RCSwitch codes."); - else if (codesOnly) padprintln("Recording: RAW with CRC or RCSwitch."); - else padprintln("Recording: Any RAW signal."); + if (!headless) tft.setTextColor(getColorVariation(bruceConfig.priColor), bruceConfig.bgColor); - if (autoSave) padprintln("Auto save: Enabled"); + if (!ReadRAW) rf_info_line(headless, "Recording: Only decoded codes."); + else if (codesOnly) rf_info_line(headless, "Recording: RAW with CRC or decoded codes."); + else rf_info_line(headless, "Recording: Any RAW signal."); - if (bruceConfigPins.rfFxdFreq) padprintln("Scanning: " + String(bruceConfigPins.rfFreq) + " MHz"); - else padprintln("Scanning: " + String(subghz_frequency_ranges[bruceConfigPins.rfScanRange])); + if (autoSave) rf_info_line(headless, "Auto save: Enabled"); - padprintln("Total signals found: " + String(signals)); + if (bruceConfigPins.rfFxdFreq) + rf_info_line(headless, "Scanning: " + String(bruceConfigPins.rfFreq) + " MHz"); + else rf_info_line(headless, "Scanning: " + String(subghz_frequency_ranges[bruceConfigPins.rfScanRange])); - tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + rf_info_line(headless, "Total signals found: " + String(signals)); - padprintln(""); - padprintln("Press [NEXT] for options."); + if (!headless) { + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + padprintln(""); + padprintln("Press [NEXT] for options."); + } } -void display_signal_data(RfCodes received) { +void display_signal_data(RfCodes received, bool headless) { std::string txt = received.data.c_str(); std::stringstream ss(txt); std::string palavra; @@ -512,54 +473,53 @@ void display_signal_data(RfCodes received) { if (received.preset != "") { if (received.fix != 0) { - padprintln("Protocol: KeeLoq"); - } else padprintln("Protocol: " + String(received.protocol) + "(" + received.preset + ")"); - } else padprintln("Protocol: " + String(received.protocol)); + rf_info_line(headless, "Protocol: KeeLoq"); + } else + rf_info_line(headless, "Protocol: " + String(received.protocol) + "(" + received.preset + ")"); + } else rf_info_line(headless, "Protocol: " + String(received.protocol)); if (received.key > 0) { decimalToHexString(received.key, hexString); if (received.protocol == "RAW") { - padprintln("Length: " + String(received.Bit) + " transitions"); - // tft.setCursor(tft.getCursorX(), tft.getCursorY() + 2); - padprintln("Record length: " + String(transitions) + " transitions"); + rf_info_line(headless, "Length: " + String(received.Bit) + " transitions"); + rf_info_line(headless, "Record length: " + String(transitions) + " transitions"); } else { if (received.fix == 0) { - padprintln("Length: " + String(received.Bit) + " bits"); + rf_info_line(headless, "Length: " + String(received.Bit) + " bits"); const char *b = dec2binWzerofill(received.key, min(received.Bit, 40)); - // tft.setCursor(tft.getCursorX(), tft.getCursorY() + 2); - padprintln("Binary: " + String(b)); + rf_info_line(headless, "Binary: " + String(b)); } } } else { strlcpy(hexString, "No code identified", sizeof(hexString)); - padprintln("Length: No code identified"); - padprintln("Record length: " + String(transitions) + " transitions"); + rf_info_line(headless, "Length: No code identified"); + rf_info_line(headless, "Record length: " + String(transitions) + " transitions"); } - if (received.protocol == "RAW") padprintln("CRC: " + String(hexString)); + if (received.protocol == "RAW") rf_info_line(headless, "CRC: " + String(hexString)); else { if (received.fix != 0) { - padprintln("Manufacturer: " + received.mf_name); + rf_info_line(headless, "Manufacturer: " + received.mf_name); decimalToHexString(received.serial, hexString); - padprintln("Serial: " + String(hexString)); + rf_info_line(headless, "Serial: " + String(hexString)); - padprintln("Btn: " + String(received.btn)); + rf_info_line(headless, "Btn: " + String(received.btn)); decimalToHexString(received.fix, hexString); - padprintln("Fix: " + String(hexString)); + rf_info_line(headless, "Fix: " + String(hexString)); if (received.mf_name != "Unknown") { decimalToHexString(received.hop, hexString); - padprintln("Hop: " + String(hexString)); + rf_info_line(headless, "Hop: " + String(hexString)); - padprintln("Counter: " + String(received.cnt)); + rf_info_line(headless, "Counter: " + String(received.cnt)); } else { decimalToHexString(received.encrypted, hexString); - padprintln("Encrypted: " + String(hexString)); + rf_info_line(headless, "Encrypted: " + String(hexString)); } } else { - padprintln("Key: " + String(hexString)); + rf_info_line(headless, "Key: " + String(hexString)); } } @@ -580,10 +540,10 @@ void display_signal_data(RfCodes received) { // else padprintln("PulseLenght: unknown"); // padprintln("Frequency: " + String(received.frequency) + " Hz"); - padprintln(""); + rf_info_line(headless, ""); } -bool RCSwitch_SaveSignal(float frequency, RfCodes codes, bool raw, char *key, bool autoSave) { +bool rfSaveSignal(float frequency, RfCodes codes, bool raw, char *key, bool autoSave) { FS *fs; String filename = ""; @@ -597,23 +557,30 @@ bool RCSwitch_SaveSignal(float frequency, RfCodes codes, bool raw, char *key, bo return false; } - String subfile_out = "Filetype: Bruce SubGhz File\nVersion 1\n"; - subfile_out += "Frequency: " + String(int(frequency * 1000000)) + "\n"; + String subfile_out = rf_subghz_header(frequency); if (!raw) { subfile_out += "Preset: " + String(codes.preset) + "\n"; - subfile_out += "Protocol: RcSwitch\n"; + // Write the identified protocol under its Flipper-standard name (so the + // `.sub` is portable), falling back to RcSwitch only if unidentified. + subfile_out += + "Protocol: " + + (codes.protocol == "" ? String("RcSwitch") : rf_flipper_protocol_name(codes.protocol)) + "\n"; subfile_out += "Bit: " + String(codes.Bit) + "\n"; + // The Key (64-bit) is always written so the signal can be replayed; for + // KeeLoq the rolling-code fields are added as informative extras. + subfile_out += "Key: " + String(key) + "\n"; if (codes.hop != 0) { - subfile_out += "Manufacturer: " + String(codes.mf_name) + "\n"; char hexString[64] = {0}; - decimalToHexString(codes.serial, hexString); - + if (codes.seed != 0) { + char seedHex[32] = {0}; + decimalToHexString(codes.seed, seedHex); + subfile_out += "Seed: " + String(seedHex) + "\n"; + } + subfile_out += "Manufacture: " + String(codes.mf_name) + "\n"; subfile_out += "Serial: " + String(hexString) + "\n"; subfile_out += "Button: " + String(codes.btn) + "\n"; subfile_out += "Counter: " + String(codes.cnt) + "\n"; - } else { - subfile_out += "Key: " + String(key) + "\n"; } subfile_out += "TE: " + String(codes.te) + "\n"; filename = "rcs.sub"; @@ -712,15 +679,13 @@ String rf_scan(float start_freq, float stop_freq, int max_loops) { return out; } -String RCSwitch_Read(float frequency, int max_loops, bool raw, bool headless) { - RCSwitch rcswitch = RCSwitch(); +String rfReceiveSignal(float frequency, int max_loops, bool raw, bool headless) { RfCodes received; if (!frequency) frequency = bruceConfigPins.rfFreq; // default from config - char hexString[64]; + char hexString[64] = {0}; -RestartRec: if (!headless) { drawMainBorder(); tft.setCursor(10, 28); @@ -728,105 +693,80 @@ String RCSwitch_Read(float frequency, int max_loops, bool raw, bool headless) { tft.println("Waiting for a " + String(frequency) + " MHz " + "signal."); } - // init receive + // init native RMT receive if (!initRfModule("rx", frequency)) return ""; - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) { // CC1101 in use - rcswitch.enableReceive(bruceConfigPins.CC1101_bus.io0); - Serial.println("CC1101 enableReceive()"); - - } else { - rcswitch.enableReceive(bruceConfigPins.rfRx); + RfRxSession rx; + if (!rx.begin()) { + deinitRfModule(); + return ""; } + while (!check(EscPress)) { - if (rcswitch.available()) { - // Serial.println("Available"); - long value = rcswitch.getReceivedValue(); - // Serial.println("getReceivedValue()"); - if (value) { - // Serial.println("has value"); - unsigned int *_raw = rcswitch.getReceivedRawdata(); + std::vector durations; + if (rx.poll(durations)) { + // In decode mode try KeeLoq, then the registry; raw mode skips decoding. + bool decoded = + (!raw) && (rf_try_keeloq(durations, received) || rf_decode_ook(durations, received)); + + // Build the RAW representation (also the data string when decoded). + String _data; + bool hasCrc = false; + uint64_t crc = 0; + std::vector indexed; + int rawBits = 0, rawTe = 0; + int transitions = rf_build_raw(durations, _data, hasCrc, crc, indexed, rawBits, rawTe); + + if (decoded) { received.frequency = long(frequency * 1000000); - received.key = rcswitch.getReceivedValue(); - received.protocol = "RcSwitch"; - received.preset = rcswitch.getReceivedProtocol(); - received.te = rcswitch.getReceivedDelay(); - received.Bit = rcswitch.getReceivedBitlength(); received.filepath = "unsaved"; - // Serial.println(received.te*2); - // derived from https://github.com/sui77/rc-switch/tree/master/examples/ReceiveDemo_Advanced - received.data = ""; - int sign = +1; - // if(received.preset.invertedSignal) sign = -1; - for (int i = 0; i < received.Bit * 2; i++) { - if (i > 0) received.data += " "; - if (i % 2 == 0) sign = +1; - else sign = -1; - received.data += String(sign * (int)_raw[i]); - } - // Serial.println(received.protocol); - // Serial.println(received.data); + received.data = _data; decimalToHexString(received.key, hexString); - - if (!headless) display_info(received, 1, raw); - } - rcswitch.resetAvailable(); - } - if (raw && rcswitch.RAWavailable()) { - // if no value were decoded, show raw data to be saved - vTaskDelay(100 / portTICK_PERIOD_MS); // give it time to process and store all signal - - unsigned int *_raw = rcswitch.getRAWReceivedRawdata(); - int transitions = 0; - signed int sign = 1; - received.data = ""; // initialize BEFORE building (was wrongly placed after, wiping data) - for (transitions = 0; transitions < RCSWITCH_RAW_MAX_CHANGES; transitions++) { - if (_raw[transitions] == 0) break; - if (transitions > 0) received.data += " "; - if (transitions % 2 == 0) sign = +1; - else sign = -1; - received.data += String(sign * (int)_raw[transitions]); - } - if (transitions > 20) { + // Interactive: draw on screen. Headless (CLI): emit the same + // formatted info on Serial instead of the display. + display_info(received, 1, raw, false, false, "", headless); + } else if (transitions > 20) { + // decoding failed (or raw requested): keep it as RAW received.frequency = long(frequency * 1000000); received.protocol = "RAW"; - received.preset = "0"; + received.preset = "Ook270Async"; + received.te = rawTe; + received.data = _data; received.filepath = "unsaved"; - // NOTE: do NOT clear received.data here - it was just built above - if (!headless) display_info(received, 1, raw); + display_info(received, 1, raw, false, false, "", headless); } else { received.data = ""; // too few transitions - discard } - // ResetSignal: - rcswitch.resetAvailable(); } if (received.key > 0 || received.data.length() > 20) { // RAW data does not have "key", 20 is more than 5 transitions - // switch to raw mode if decoding failed - if (received.preset == 0) { - Serial.println("signal decoding failed, switching to RAW mode"); - // displayWarning("signal decoding failed, switching to RAW mode", true); - raw = true; - // TODO: show a dialog/warning? - // raw = yesNoDialog("decoding failed, save as RAW?"); - } - String subfile_out = "Filetype: Bruce SubGhz File\nVersion 1\n"; - subfile_out += "Frequency: " + String(int(frequency * 1000000)) + "\n"; - if (!raw) { + bool outRaw = raw || received.protocol == "RAW"; + + String subfile_out = rf_subghz_header(frequency); + if (!outRaw) { subfile_out += "Preset: " + String(received.preset) + "\n"; - subfile_out += "Protocol: RcSwitch\n"; + subfile_out += + "Protocol: " + String(received.protocol == "" ? "RcSwitch" : received.protocol) + "\n"; subfile_out += "Bit: " + String(received.Bit) + "\n"; subfile_out += "Key: " + String(hexString) + "\n"; + if (received.fix != 0) { // KeeLoq: include the resolved rolling-code fields + char tmp[32] = {0}; + subfile_out += "Manufacture: " + received.mf_name + "\n"; + decimalToHexString(received.serial, tmp); + subfile_out += "Serial: " + String(tmp) + "\n"; + subfile_out += "Button: " + String(received.btn) + "\n"; + subfile_out += "Counter: " + String(received.cnt) + "\n"; + } subfile_out += "TE: " + String(received.te) + "\n"; } else { - // save as raw - if (received.preset == "1") received.preset = "FuriHalSubGhzPresetOok270Async"; - else if (received.preset == "2") received.preset = "FuriHalSubGhzPresetOok650Async"; - subfile_out += "Preset: " + String(received.preset) + "\n"; + subfile_out += + "Preset: " + String(received.preset == "" ? String("Ook270Async") : received.preset) + + "\n"; subfile_out += "Protocol: RAW\n"; subfile_out += "RAW_Data: " + received.data; } - // headless mode + rx.end(); + deinitRfModule(); return subfile_out; } if (max_loops > 0) { @@ -841,12 +781,13 @@ String RCSwitch_Read(float frequency, int max_loops, bool raw, bool headless) { } else if (max_loops == -1) { // Final check already done in this iteration - truly timed out Serial.println("timeout"); + rx.end(); + deinitRfModule(); return ""; } } -Exit: - vTaskDelay(1 / portTICK_PERIOD_MS); + rx.end(); deinitRfModule(); return ""; diff --git a/src/modules/rf/rf_scan.h b/src/modules/rf/rf_scan.h index 2000989bf..1248bf329 100644 --- a/src/modules/rf/rf_scan.h +++ b/src/modules/rf/rf_scan.h @@ -1,14 +1,12 @@ #ifndef __RF_SCAN_H__ #define __RF_SCAN_H__ +#include "protocols/rf_decoder.h" #include "rf_utils.h" #include "structs.h" -#include #define _MAX_TRIES 5 -#define PRESET_KEELOQ 23 - class RFScan { public: enum RFMenuOption { @@ -36,7 +34,7 @@ class RFScan { void loop(); private: - RCSwitch rcswitch = RCSwitch(); + RfRxSession _rx; RfCodes received; String title = "RF Scan Copy"; bool restartScan = false; @@ -63,8 +61,8 @@ class RFScan { ///////////////////////////////////////////////////////////////////////////////////// // Operations ///////////////////////////////////////////////////////////////////////////////////// - void read_rcswitch(); - void read_raw(); + void decode_signal(const std::vector &durations); + void read_raw(const std::vector &durations); void replay_signal(bool asRaw = false); void save_signal(bool asRaw = false); void reset_signals(); @@ -74,20 +72,20 @@ class RFScan { ///////////////////////////////////////////////////////////////////////////////////// // Utils ///////////////////////////////////////////////////////////////////////////////////// - void RCSwitch_Enable_Receive(RCSwitch rcswitch); + void enable_receive(); void init_freqs(); bool fast_scan(); }; void display_info( RfCodes received, int signals, bool ReadRAW = false, bool codesOnly = false, bool autoSave = false, - String title = "" + String title = "", bool headless = false ); -void display_signal_data(RfCodes received); +void display_signal_data(RfCodes received, bool headless = false); -bool RCSwitch_SaveSignal(float frequency, RfCodes codes, bool raw, char *key, bool autoSave = false); +bool rfSaveSignal(float frequency, RfCodes codes, bool raw, char *key, bool autoSave = false); String rf_scan(float start_freq, float stop_freq, int max_loops = -1); -String RCSwitch_Read(float frequency = 0, int max_loops = -1, bool raw = false, bool headless = false); +String rfReceiveSignal(float frequency = 0, int max_loops = -1, bool raw = false, bool headless = false); #endif diff --git a/src/modules/rf/rf_send.cpp b/src/modules/rf/rf_send.cpp index 480536863..6f49a3da1 100644 --- a/src/modules/rf/rf_send.cpp +++ b/src/modules/rf/rf_send.cpp @@ -1,8 +1,12 @@ #include "rf_send.h" #include "core/led_control.h" #include "core/type_convertion.h" +#include "protocols/rf_config.h" +#include "protocols/rf_encoder.h" +#include "protocols/rf_legacy_migrate.h" +#include "protocols/rf_presets.h" +#include "protocols/rf_registry.h" #include "rf_utils.h" -#include #define CLOSE_MENU 3 #define MAIN_MENU 4 @@ -112,15 +116,24 @@ void select_menu_option(bool keeloq) { void keeloq_save(RfCodes data) { String subfile_out = "Filetype: Bruce SubGhz File\nVersion 1\n"; subfile_out += "Frequency: " + String(data.frequency) + "\n"; - subfile_out += "Preset: " + String(data.preset) + "\n"; - subfile_out += "Protocol: RcSwitch\n"; + subfile_out += "Preset: " + String(data.preset == "" ? String("Ook650Async") : data.preset) + "\n"; + subfile_out += "Protocol: KeeLoq\n"; subfile_out += "Bit: " + String(data.Bit) + "\n"; - subfile_out += "Manufacturer: " + String(data.mf_name) + "\n"; char hexString[64] = {0}; + // The 64-bit Key is what gets replayed (sendRfCommand routes KeeLoq to the + // dedicated encoder); the rolling-code fields below are informative. + decimalToHexString(data.key, hexString); + subfile_out += "Key: " + String(hexString) + "\n"; + + if (data.seed != 0) { + char seedHex[32] = {0}; + decimalToHexString(data.seed, seedHex); + subfile_out += "Seed: " + String(seedHex) + "\n"; + } + subfile_out += "Manufacture: " + String(data.mf_name) + "\n"; decimalToHexString(data.serial, hexString); - subfile_out += "Serial: " + String(hexString) + "\n"; subfile_out += "Button: " + String(data.btn) + "\n"; subfile_out += "Counter: " + String(data.cnt) + "\n"; @@ -192,7 +205,7 @@ void display_info(RfCodes &data) { if (data.serial != 0) { padprintln("Protocol: KeeLoq"); - padprintln("Manufacturer: " + data.mf_name); + padprintln("Manufacture: " + data.mf_name); decimalToHexString(data.serial, hexString); padprintln("Serial: " + String(hexString)); @@ -249,17 +262,19 @@ bool readSubFile(FS *fs, String filepath, RfCodes &data) { if (line.startsWith("TE:")) selected_code.te = txt.toInt(); if (line.startsWith("Bit:")) bitList.push_back(txt.toInt()); // selected_code.Bit = txt.toInt(); - if (line.startsWith("Manufacturer:")) selected_code.mf_name = txt; + // KeeLoq rolling-code fields. Flipper writes "Manufacture" (no trailing + // r); accept both spellings. "Seed" is the secure-learning seed. + if (line.startsWith("Manufacturer:") || line.startsWith("Manufacture:")) selected_code.mf_name = txt; if (line.startsWith("Serial:")) selected_code.serial = hexStringToDecimal(txt.c_str()); if (line.startsWith("Button:")) selected_code.btn = txt.toInt(); if (line.startsWith("Counter:")) selected_code.cnt = txt.toInt(); + if (line.startsWith("Seed:")) selected_code.seed = (uint32_t)hexStringToU64(txt.c_str()); if (line.startsWith("Bit_RAW:")) bitRawList.push_back(txt.toInt()); // selected_code.BitRAW = txt.toInt(); - if (line.startsWith("Key:")) - keyList.push_back( - hexStringToDecimal(txt.c_str()) - ); // selected_code.key = hexStringToDecimal(txt.c_str()); + // Keys can exceed 32 bits (Holtek 40, Mastercode 36, PhoenixV2 52, + // KeeLoq 64), so parse the full 64-bit value, not a truncated uint32. + if (line.startsWith("Key:")) keyList.push_back(hexStringToU64(txt.c_str())); if (line.startsWith("RAW_Data:") || line.startsWith("Data_RAW:")) rawDataList.push_back(txt); // selected_code.data = txt; @@ -270,37 +285,32 @@ bool readSubFile(FS *fs, String filepath, RfCodes &data) { data = selected_code; +#if RF_SUB_LEGACY_MIGRATION + // One-shot migration of old-format `.sub` files (Protocol: RcSwitch + + // numeric Preset) into the registry-based format: backs the original up to + // `.bak` and rewrites the `.sub` once. `data` is migrated in memory + // regardless, so replay always uses the resolved protocol name. + if (rf_sub_is_legacy(data)) rf_sub_migrate(fs, filepath, data); +#endif + return true; } bool txSubFile(RfCodes &selected_code, bool hideDefaultUI) { int sent = 0; - int total = bitList.size() + bitRawList.size() + keyList.size() + rawDataList.size() > 0 ? 1 : 0; + int total = keyList.size() + rawDataList.size(); Serial.printf("Total signals found: %d\n", total); // If the signal is complete, send all of the code(s) that were found in it. // TODO: try to minimize the overhead between codes. if (selected_code.protocol != "" && selected_code.preset != "" && selected_code.frequency > 0) { - for (int bit : bitList) { - selected_code.Bit = bit; - sendRfCommand(selected_code, hideDefaultUI); - sent++; - if (!hideDefaultUI) { - if (check(EscPress)) break; - displayTextLine("Sent " + String(sent) + "/" + String(total)); - } - } - for (int bitRaw : bitRawList) { - selected_code.Bit = bitRaw; - sendRfCommand(selected_code, hideDefaultUI); - sent++; - if (!hideDefaultUI) { - if (check(EscPress)) break; - displayTextLine("Sent " + String(sent) + "/" + String(total)); - } - } - for (uint64_t key : keyList) { - selected_code.key = key; + // A `.sub` carries a code as a Key (paired with its Bit length) or as a + // RAW/BinRAW data stream (paired with Bit_RAW). Each code is sent ONCE: + // earlier this looped Bit and Key separately, transmitting every code + // twice — the first time with key=0 (a bogus frame). Pair them instead. + for (size_t i = 0; i < keyList.size(); i++) { + selected_code.key = keyList[i]; + if (i < bitList.size()) selected_code.Bit = bitList[i]; sendRfCommand(selected_code, hideDefaultUI); sent++; if (!hideDefaultUI) { @@ -309,14 +319,14 @@ bool txSubFile(RfCodes &selected_code, bool hideDefaultUI) { } } - // RAS_Data is considered one long signal, doesn't matter the number of lines it has - if (rawDataList.size() > 0) sent++; - for (String rawData : rawDataList) { - selected_code.data = rawData; + // RAW_Data / Data_RAW: one (long) signal per data line, BinRAW pairs it + // with Bit_RAW. RAW protocol ignores Bit. + for (size_t i = 0; i < rawDataList.size(); i++) { + selected_code.data = rawDataList[i]; + if (i < bitRawList.size()) selected_code.Bit = bitRawList[i]; sendRfCommand(selected_code, hideDefaultUI); - // sent++; + sent++; if (check(EscPress)) break; - // displayTextLine("Sent " + String(sent) + "/" + String(total)); } addToRecentCodes(selected_code); } @@ -364,40 +374,21 @@ void sendRfCommand(struct RfCodes rfcode, bool hideDefaultUI) { FuriHalSubGhzPresetGFSK9_99KbAsync, //< GFSK, deviation 19.042969 kHz, 9.996Kb/s, asynchronous FuriHalSubGhzPresetCustom, //Custom Preset */ - // struct Protocol rcswitch_protocol; - int rcswitch_protocol_no = 1; - if (preset == "FuriHalSubGhzPresetOok270Async") { - rcswitch_protocol_no = 1; - // pulseLength , syncFactor , zero , one, invertedSignal - // rcswitch_protocol = { 350, { 1, 31 }, { 1, 3 }, { 3, 1 }, false }; - modulation = 2; - rxBW = 270; - } else if (preset == "FuriHalSubGhzPresetOok650Async") { - rcswitch_protocol_no = 2; - // rcswitch_protocol = { 650, { 1, 10 }, { 1, 2 }, { 2, 1 }, false }; - modulation = 2; - rxBW = 650; - } else if (preset == "FuriHalSubGhzPreset2FSKDev238Async") { - modulation = 0; - deviation = 2.380371; - rxBW = 238; - } else if (preset == "FuriHalSubGhzPreset2FSKDev476Async") { - modulation = 0; - deviation = 47.60742; - rxBW = 476; - } else if (preset == "FuriHalSubGhzPresetMSK99_97KbAsync") { - modulation = 4; - deviation = 47.60742; - dataRate = 99.97; - } else if (preset == "FuriHalSubGhzPresetGFSK9_99KbAsync") { - modulation = 1; - deviation = 19.042969; - dataRate = 9.996; + // Radio preset parameters come from the central registry (protocols/). + // A preset field at 0 means "keep the module default" (set above). + int legacy_protocol_no = 1; + const RfPreset *rp = rf_find_preset(preset); + if (rp) { + modulation = rp->modulation; + if (rp->deviation) deviation = rp->deviation; + if (rp->rxBW) rxBW = rp->rxBW; + if (rp->dataRate) dataRate = rp->dataRate; + legacy_protocol_no = rp->legacyProto; } else { bool found = false; for (int p = 0; p < 30; p++) { if (preset == String(p)) { - rcswitch_protocol_no = preset.toInt(); + legacy_protocol_no = preset.toInt(); found = true; } } @@ -467,147 +458,67 @@ void sendRfCommand(struct RfCodes rfcode, bool hideDefaultUI) { // send rf command if (!hideDefaultUI) { displayTextLine("Sending.."); } - RCSwitch_RAW_send(transmittimings); + rfTransmitRawTimings(transmittimings); free(transmittimings); } else if (protocol == "BinRAW") { // transform from "00 01 02 ... FF" into "00000000 00000001 00000010 .... 11111111" rfcode.data = hexStrToBinStr(rfcode.data); // Serial.println(rfcode.data); rfcode.data.trim(); - RCSwitch_RAW_Bit_send(rfcode); + rfTransmitRawBits(rfcode); } - else if (protocol == "RcSwitch") { - data.replace(" ", ""); // remove spaces - // uint64_t data_val = strtoul(data.c_str(), nullptr, 16); - uint64_t data_val = rfcode.key; - int bits = rfcode.Bit; - int pulse = rfcode.te; // not sure about this... - int repeat = num_signal_repeat; - /* - Serial.print("RcSwitch: "); - Serial.println(data_val,16); - Serial.println(bits); - Serial.println(pulse); - Serial.println(rcswitch_protocol_no); - */ - // if (!hideDefaultUI) { displayTextLine("Sending.."); } - RCSwitch_send(data_val, bits, pulse, rcswitch_protocol_no, repeat); - } else if (protocol.startsWith("Princeton")) { - RCSwitch_send(rfcode.key, rfcode.Bit, 350, 1, 10); - } else { - Serial.print("unsupported protocol: "); - Serial.println(protocol); - Serial.println("Sending RcSwitch 11 protocol"); - // if(protocol.startsWith("CAME") || protocol.startsWith("HOLTEC" || NICE)) { - RCSwitch_send(rfcode.key, rfcode.Bit, 270, 11, 10); - //} - - return; + else if (protocol == "KeeLoq") { + // KeeLoq has dedicated framing (see rf_keeloq_durations). `rfcode.key` is + // the 64-bit rolling code already assembled by keeloq_step. + if (!hideDefaultUI) { displayTextLine("Sending.."); } + rf_tx_keeloq(rfcode.key, num_signal_repeat); } - // digitalWrite(bruceConfigPins.rfTx, LED_OFF); - deinitRfModule(); -} - -void RCSwitch_send(uint64_t data, unsigned int bits, int pulse, int protocol, int repeat) { - // derived from - // https://github.com/LSatan/SmartRC-CC1101-Driver-Lib/blob/master/examples/Rc-Switch%20examples%20cc1101/SendDemo_cc1101/SendDemo_cc1101.ino + else { + // Dispatch by protocol NAME (the decoder + migration now write + // `Protocol: `). The named definition comes straight from the + // central registry. + const RfProtocolDef *def = rf_find_protocol(protocol); - RCSwitch mySwitch = RCSwitch(); +#if RF_SUB_LEGACY_MIGRATION + // Safety net for non-migrated legacy sources (PSRamFS / read-only FS): + // a `.sub` still carrying `Protocol: RcSwitch` + numeric `Preset`. + if (def == nullptr && protocol == "RcSwitch") def = rf_find_legacy(legacy_protocol_no); +#endif - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) { - mySwitch.enableTransmit(bruceConfigPins.CC1101_bus.io0); - } else { - mySwitch.enableTransmit(bruceConfigPins.rfTx); + if (def != nullptr) { + rf_tx_protocol(rfcode.key, rfcode.Bit, rfcode.te, def, num_signal_repeat); + } else { + Serial.print("unsupported protocol: "); + Serial.println(protocol); + Serial.println("Falling back to generic RcSwitch_11"); + rfTransmitCode(rfcode.key, rfcode.Bit, 270, 11, 10); + } } - mySwitch.setProtocol(protocol); // override - if (pulse) { mySwitch.setPulseLength(pulse); } - mySwitch.setRepeatTransmit(repeat); - mySwitch.send(data, bits); - - /* - Serial.println(data,HEX); - Serial.println(bits); - Serial.println(pulse); - Serial.println(protocol); - Serial.println(repeat); - */ - - mySwitch.disableTransmit(); - + // digitalWrite(bruceConfigPins.rfTx, LED_OFF); deinitRfModule(); } -// ported from https://github.com/sui77/rc-switch/blob/3a536a172ab752f3c7a58d831c5075ca24fd920b/RCSwitch.cpp -void RCSwitch_RAW_Bit_send(RfCodes data) { - int nTransmitterPin = bruceConfigPins.rfTx; - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) { nTransmitterPin = bruceConfigPins.CC1101_bus.io0; } +// Transmit `data` (`bits` long) using a classic OOK protocol NUMBER. Thin +// wrapper over the RMT encoder; the number is resolved to a registry definition +// (keeps the external CLI/JSON numeric contract). Signature unchanged. +void rfTransmitCode(uint64_t data, unsigned int bits, int pulse, int protocol, int repeat) { + const RfProtocolDef *def = rf_protocol_for_number(protocol); + rf_tx_protocol(data, bits, pulse, def, repeat); + deinitRfModule(); +} +// Transmit a bit string (each bit held for `te` µs) via RMT. Signature unchanged. +void rfTransmitRawBits(RfCodes data) { if (data.data == "") return; - bool currentlogiclevel = false; - int nRepeatTransmit = 1; - for (int nRepeat = 0; nRepeat < nRepeatTransmit; nRepeat++) { - int currentBit = data.data.length(); - while (currentBit >= 0) { // Starts from the end of the string until the max number of bits to send - char c = data.data[currentBit]; - if (c == '1') { - currentlogiclevel = true; - } else if (c == '0') { - currentlogiclevel = false; - } else { - Serial.println("Invalid data"); - currentBit--; - continue; - // return; - } - - digitalWrite(nTransmitterPin, currentlogiclevel ? HIGH : LOW); - delayMicroseconds(data.te); - - // Serial.print(currentBit); - // Serial.print("="); - // Serial.println(currentlogiclevel); - - currentBit--; - } - digitalWrite(nTransmitterPin, LOW); - } + rf_tx_raw_bits(data.data, data.te); } -void RCSwitch_RAW_send(int *ptrtransmittimings) { - int nTransmitterPin = bruceConfigPins.rfTx; - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) { nTransmitterPin = bruceConfigPins.CC1101_bus.io0; } - +// Transmit a 0-terminated RAW timings array (signed µs) via RMT. Signature +// unchanged (1 repetition, as before). +void rfTransmitRawTimings(int *ptrtransmittimings) { if (!ptrtransmittimings) return; - - bool currentlogiclevel = true; - int nRepeatTransmit = 1; // repeats RAW signal twice! - // HighLow pulses ; - - for (int nRepeat = 0; nRepeat < nRepeatTransmit; nRepeat++) { - unsigned int currenttiming = 0; - while (ptrtransmittimings[currenttiming]) { // && currenttiming < RCSWITCH_MAX_CHANGES - if (ptrtransmittimings[currenttiming] >= 0) { - currentlogiclevel = true; - } else { - // negative value - currentlogiclevel = false; - ptrtransmittimings[currenttiming] = (-1) * ptrtransmittimings[currenttiming]; // invert sign - } - - digitalWrite(nTransmitterPin, currentlogiclevel ? HIGH : LOW); - delayMicroseconds(ptrtransmittimings[currenttiming]); - - /* - Serial.print(ptrtransmittimings[currenttiming]); - Serial.print("="); - Serial.println(currentlogiclevel); - */ - - currenttiming++; - } - digitalWrite(nTransmitterPin, LOW); - } // end for + rf_tx_raw_timings(ptrtransmittimings); } diff --git a/src/modules/rf/rf_send.h b/src/modules/rf/rf_send.h index 83b60ad14..28e3934e4 100644 --- a/src/modules/rf/rf_send.h +++ b/src/modules/rf/rf_send.h @@ -11,10 +11,12 @@ bool txSubFile(RfCodes &selected_code, bool hideDefaultUI = false); bool readSubFile(FS *fs, String filepath, RfCodes &data); void sendRfCommand(struct RfCodes rfcode, bool hideDefaultUI = false); -void RCSwitch_send(uint64_t data, unsigned int bits, int pulse = 0, int protocol = 1, int repeat = 10); -void RCSwitch_RAW_Bit_send(RfCodes data); -void RCSwitch_RAW_send(int *ptrtransmittimings); +// Native RMT transmit wrappers (formerly the library send / _RAW_send / +// _RAW_Bit_send paths). Signatures unchanged; they delegate to the RMT encoder. +void rfTransmitCode(uint64_t data, unsigned int bits, int pulse = 0, int protocol = 1, int repeat = 10); +void rfTransmitRawBits(RfCodes data); +void rfTransmitRawTimings(int *ptrtransmittimings); void display_info(RfCodes &data); void loopEmulate(RfCodes &data); diff --git a/src/modules/rf/rf_spectrum.cpp b/src/modules/rf/rf_spectrum.cpp index 05b1bed28..95e9f46e9 100644 --- a/src/modules/rf/rf_spectrum.cpp +++ b/src/modules/rf/rf_spectrum.cpp @@ -1,7 +1,7 @@ #include "rf_spectrum.h" +#include "protocols/rf_decoder.h" #include "rf_utils.h" #include "structs.h" -#include static bool spectrum_rmt_rx_done_callback( rmt_channel_t *channel, const rmt_rx_done_event_data_t *edata, void *user_data @@ -89,14 +89,16 @@ void rf_spectrum() { #define TIME_DIVIDER (tftWidth / 10) //@Pirata void rf_SquareWave() { - RCSwitch rcswitch; if (!initRfModule("rx", bruceConfigPins.rfFreq)) return; - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) rcswitch.enableReceive(bruceConfigPins.CC1101_bus.io0); - else rcswitch.enableReceive(bruceConfigPins.rfRx); + RfRxSession rx; + if (!rx.begin()) { + deinitRfModule(); + return; + } int line_w = 0; int line_h = 15; - unsigned int *raw; + std::vector durations; PRINT: tft.drawPixel(0, 0, 0); tft.fillScreen(bruceConfig.bgColor); @@ -105,17 +107,16 @@ void rf_SquareWave() { tft.printf(" RF - SquareWave (%.2f Mhz)", bruceConfigPins.rfFreq); while (1) { - if (rcswitch.RAWavailable()) { - raw = rcswitch.getRAWReceivedRawdata(); - // Clear the display area - // tft.fillRect(0, 0, tftWidth, tftHeight, bruceConfig.bgColor); - // Draw waveform based on signal strength - for (int i = 0; i < RCSWITCH_RAW_MAX_CHANGES - 1; i += 2) { - if (raw[i] == 0) break; + if (rx.poll(durations)) { + // Draw the captured square wave (HIGH width then LOW width per pair). + for (size_t i = 0; i + 1 < durations.size(); i += 2) { + int high = abs(durations[i]); + int low = abs(durations[i + 1]); + if (high == 0) break; - if (raw[i] > 20000) raw[i] = 20000; - if (raw[i + 1] > 20000) raw[i + 1] = 20000; - if (line_w + (raw[i] + raw[i + 1]) / TIME_DIVIDER > tftWidth) { + if (high > 20000) high = 20000; + if (low > 20000) low = 20000; + if (line_w + (high + low) / TIME_DIVIDER > tftWidth) { line_w = 10; line_h += 10; } @@ -124,24 +125,21 @@ void rf_SquareWave() { tft.fillRect(0, 12, tftWidth, tftHeight, bruceConfig.bgColor); } tft.drawFastVLine(line_w, line_h, 6, bruceConfig.priColor); - tft.drawFastHLine(line_w, line_h, raw[i] / TIME_DIVIDER, bruceConfig.priColor); + tft.drawFastHLine(line_w, line_h, high / TIME_DIVIDER, bruceConfig.priColor); - tft.drawFastVLine(line_w + raw[i] / TIME_DIVIDER, line_h, 6, bruceConfig.priColor); + tft.drawFastVLine(line_w + high / TIME_DIVIDER, line_h, 6, bruceConfig.priColor); tft.drawFastHLine( - line_w + raw[i] / TIME_DIVIDER, - line_h + 6, - raw[i + 1] / TIME_DIVIDER, - bruceConfig.priColor + line_w + high / TIME_DIVIDER, line_h + 6, low / TIME_DIVIDER, bruceConfig.priColor ); - line_w += (raw[i] + raw[i + 1]) / TIME_DIVIDER; + line_w += (high + low) / TIME_DIVIDER; } - rcswitch.resetAvailable(); } // Checks to leave while if (check(EscPress)) { break; } if (setMHZMenu()) goto PRINT; vTaskDelay(pdMS_TO_TICKS(10)); } + rx.end(); returnToMenu = true; } diff --git a/src/modules/rf/rf_spectrum.cpp.bak b/src/modules/rf/rf_spectrum.cpp.bak deleted file mode 100644 index a277006f1..000000000 --- a/src/modules/rf/rf_spectrum.cpp.bak +++ /dev/null @@ -1,303 +0,0 @@ -#include "rf_spectrum.h" -#include "rf_utils.h" -#include "structs.h" -#include - -static bool spectrum_rmt_rx_done_callback( - rmt_channel_t *channel, const rmt_rx_done_event_data_t *edata, void *user_data -) { - BaseType_t high_task_wakeup = pdFALSE; - QueueHandle_t receive_queue = (QueueHandle_t)user_data; - // send the received RMT symbols to the parser task - xQueueSendFromISR(receive_queue, edata, &high_task_wakeup); - return high_task_wakeup == pdTRUE; -} - -void draw_tf_spectrum_grid() { - tft.setTextSize(1); - tft.setCursor(3, 2); - tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); - tft.printf(" RF - Spectrum (%.2f Mhz)", bruceConfigPins.rfFreq); - tft.fillRect(0, 20, tftWidth, tftHeight - 20, bruceConfig.bgColor); - tft.drawFastHLine(0, 20 + tftHeight / 2, tftWidth, TFT_DARKGREY); - tft.drawFastVLine((1 * tftWidth) / 4, 20, tftHeight - 20, TFT_DARKGREY); - tft.drawFastVLine((2 * tftWidth) / 4, 20, tftHeight - 20, TFT_DARKGREY); - tft.drawFastVLine((3 * tftWidth) / 4, 20, tftHeight - 20, TFT_DARKGREY); -} - -void rf_spectrum() { - tft.fillScreen(bruceConfig.bgColor); - draw_tf_spectrum_grid(); - rmt_channel_handle_t rx_ch = NULL; - rx_ch = setup_rf_rx(); - if (rx_ch == NULL) return; - ESP_LOGI("RMT_SPECTRUM", "register RX done callback"); - QueueHandle_t receive_queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t)); - assert(receive_queue); - rmt_rx_event_callbacks_t cbs = { - .on_recv_done = spectrum_rmt_rx_done_callback, - }; - ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_ch, &cbs, receive_queue)); - ESP_ERROR_CHECK(rmt_enable(rx_ch)); - rmt_receive_config_t receive_config = { - .signal_range_min_ns = 3000, // 6us minimum signal duration - .signal_range_max_ns = 12000000, // 24ms maximum signal duration - }; - rmt_symbol_word_t item[64]; - rmt_rx_done_event_data_t rx_data; - ESP_ERROR_CHECK(rmt_receive(rx_ch, item, sizeof(item), &receive_config)); - - size_t rx_size = 0; - while (1) { - rmt_symbol_word_t *rx_items = NULL; - if (xQueueReceive(receive_queue, &rx_data, 0) == pdPASS) { - rx_size = rx_data.num_symbols; - rx_items = rx_data.received_symbols; - } - if (rx_size != 0) { - // Draw grid and info - draw_tf_spectrum_grid(); - // Draw waveform based on signal strength - for (size_t i = 0; i < rx_size; i++) { - int lineHeight = - map(rx_items[i].duration0 + rx_items[i].duration1, - 0, - SIGNAL_STRENGTH_THRESHOLD, - 0, - tftHeight / 2); - int lineX = map(i, 0, rx_size - 1, 0, tftWidth - 1); // Map i to within the display width - int startY = constrain(20 + tftHeight / 2 - lineHeight / 2, 20, 20 + tftHeight); - int endY = constrain(20 + tftHeight / 2 + lineHeight / 2, 20, 20 + tftHeight); - tft.drawLine(lineX, startY, lineX, endY, bruceConfig.priColor); - } - - ESP_ERROR_CHECK(rmt_receive(rx_ch, item, sizeof(item), &receive_config)); - rx_size = 0; - } - // Checks to leave while - if (check(EscPress)) { break; } - if (setMHZMenu()) yield(); - vTaskDelay(pdMS_TO_TICKS(10)); - } - returnToMenu = true; - rmt_disable(rx_ch); - rmt_del_channel(rx_ch); - vQueueDelete(receive_queue); - deinitRfModule(); -} - -#define TIME_DIVIDER (tftWidth / 10) -//@Pirata -void rf_SquareWave() { - RCSwitch rcswitch; - if (!initRfModule("rx", bruceConfigPins.rfFreq)) return; - - if (bruceConfigPins.rfModule == CC1101_SPI_MODULE) rcswitch.enableReceive(bruceConfigPins.CC1101_bus.io0); - else rcswitch.enableReceive(bruceConfigPins.rfRx); - int line_w = 0; - int line_h = 15; - unsigned int *raw; -PRINT: - tft.drawPixel(0, 0, 0); - tft.fillScreen(bruceConfig.bgColor); - tft.setTextSize(1); - tft.setCursor(3, 2); - tft.printf(" RF - SquareWave (%.2f Mhz)", bruceConfigPins.rfFreq); - - while (1) { - if (rcswitch.RAWavailable()) { - raw = rcswitch.getRAWReceivedRawdata(); - // Clear the display area - // tft.fillRect(0, 0, tftWidth, tftHeight, bruceConfig.bgColor); - // Draw waveform based on signal strength - for (int i = 0; i < RCSWITCH_RAW_MAX_CHANGES - 1; i += 2) { - if (raw[i] == 0) break; - - if (raw[i] > 20000) raw[i] = 20000; - if (raw[i + 1] > 20000) raw[i + 1] = 20000; - if (line_w + (raw[i] + raw[i + 1]) / TIME_DIVIDER > tftWidth) { - line_w = 10; - line_h += 10; - } - if (line_h > tftHeight) { - line_h = 15; - tft.fillRect(0, 12, tftWidth, tftHeight, bruceConfig.bgColor); - } - tft.drawFastVLine(line_w, line_h, 6, bruceConfig.priColor); - tft.drawFastHLine(line_w, line_h, raw[i] / TIME_DIVIDER, bruceConfig.priColor); - - tft.drawFastVLine(line_w + raw[i] / TIME_DIVIDER, line_h, 6, bruceConfig.priColor); - tft.drawFastHLine( - line_w + raw[i] / TIME_DIVIDER, - line_h + 6, - raw[i + 1] / TIME_DIVIDER, - bruceConfig.priColor - ); - line_w += (raw[i] + raw[i + 1]) / TIME_DIVIDER; - } - rcswitch.resetAvailable(); - } - // Checks to leave while - if (check(EscPress)) { break; } - if (setMHZMenu()) goto PRINT; - vTaskDelay(pdMS_TO_TICKS(10)); - } - returnToMenu = true; -} - -void rf_CC1101_rssi() { -#if !defined(LITE_VERSION) - if (bruceConfigPins.rfModule != CC1101_SPI_MODULE) { - displayError("only for CC1101 module", true); - return; - } - int graph_size = tftWidth - 20; - std::vector signal(graph_size, -95); - const size_t freq_count = sizeof(subghz_frequency_list) / sizeof(float); - std::vector bar_size(freq_count, 0); - std::vector peak_size(freq_count, 0); - int max_bar_size = tftHeight - 20 /*bottom margin*/ - 20 /*top margin*/; - bool redraw = true; - const int min_value = map(-70, -95, -20, 0, max_bar_size); - while (1) { - if (redraw) { - redraw = false; - tft.drawPixel(0, 0, 0); - tft.fillScreen(bruceConfig.bgColor); - tft.setTextSize(1); - tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); - tft.setCursor(3, 2); - // Fixed frequency sees a dot running grafic, showing RSSI over time - if (bruceConfigPins.rfFxdFreq) { - if (!initRfModule("rx", bruceConfigPins.rfFreq)) - displayError("Error setting frequency", true); - tft.printf(" RF - RSSI spectrum (%.2f Mhz)", bruceConfigPins.rfFreq); - tft.drawFastVLine(20, 20, tftHeight, bruceConfig.priColor); - tft.drawString("-95", 0, (tftHeight - 120) + 95); - tft.drawString("-80", 0, (tftHeight - 120) + 80); - tft.drawString("-65", 0, (tftHeight - 120) + 65); - tft.drawString("-50", 0, (tftHeight - 120) + 50); - tft.drawString("-35", 0, (tftHeight - 120) + 35); - tft.drawString("-20", 0, (tftHeight - 120) + 20); - // resets signal array - std::fill(signal.begin(), signal.end(), -95); - } - // Range Scan Sees a bargraph simillar to NRF24 grafic, using RSSI across frequencies - else { - if (!initRfModule("rx", bruceConfigPins.rfFreq)) displayError("Error starting module", true); - tft.printf(" RF - RSSI spectrum (%s)", subghz_frequency_ranges[bruceConfigPins.rfScanRange]); - tft.drawFastHLine(0, tftHeight - 20, tftWidth, bruceConfig.priColor); - char buf[7]; - float var = subghz_frequency_list[range_limits[bruceConfigPins.rfScanRange][0]]; - snprintf(buf, sizeof(buf), "%.3f", var); - tft.drawString(buf, 2, tftHeight - 10); - var = subghz_frequency_list[range_limits[bruceConfigPins.rfScanRange][1]]; - snprintf(buf, sizeof(buf), "%.3f", var); - tft.drawRightString(buf, tftWidth - 2, tftHeight - 10); - int range = range_limits[bruceConfigPins.rfScanRange][1] - - range_limits[bruceConfigPins.rfScanRange][0] + 1; - int space = tftWidth / range; - for (int i = 0; i < range; i++) { - tft.drawFastVLine(space * i, tftHeight - 20, 5, bruceConfig.priColor); - } - std::fill(bar_size.begin(), bar_size.end(), 0); - std::fill(peak_size.begin(), peak_size.end(), 0); - } - } - - // draw dot graph for fixed frequency - if (bruceConfigPins.rfFxdFreq) { - int rssi = ELECHOUSE_cc1101.getRssi(); - tft.drawPixel(0, 0, 0); // To make sure CC1101 shared with TFT works properly - const int base_y = tftHeight - 120; - int prev = signal[0]; - for (int i = 1; i < graph_size; i++) { - if (EscPress || SelPress) break; - const int x0 = 20 + (i - 1); - const int x1 = 20 + i; - const int curr = signal[i]; - // erase old segment between previous and current points - tft.drawLine(x0, base_y - prev, x1, base_y - curr, bruceConfig.bgColor); - const int next_val = (i == graph_size - 1) ? rssi : signal[i + 1]; - // shift buffer left by one - signal[i - 1] = curr; - if (i == graph_size - 1) signal[i] = rssi; - // draw updated segment using new values - tft.drawLine(x0, base_y - curr, x1, base_y - next_val, bruceConfig.priColor); - prev = curr; - } - tft.drawFastVLine(20, 20, tftHeight, bruceConfig.priColor); - vTaskDelay(pdMS_TO_TICKS(75)); - } - // draw a bargraph similar to nrf24 across the range - else { - int range = range_limits[bruceConfigPins.rfScanRange][1] - - range_limits[bruceConfigPins.rfScanRange][0] + 1; - - int space = tftWidth / range; - int max_idx = 0; - for (int i = 0; i < range; i++) { - if (EscPress || SelPress) break; - setMHZ(subghz_frequency_list[range_limits[bruceConfigPins.rfScanRange][0] + i]); - vTaskDelay(pdMS_TO_TICKS(5)); - int rssi = ELECHOUSE_cc1101.getRssi(); - tft.drawPixel(0, 0, 0); // To make sure CC1101 shared with TFT works properly - int size = map(rssi, -95, -20, 0, max_bar_size); - if (size > bar_size[i]) bar_size[i] = size; - else bar_size[i] = bar_size[i] - (bar_size[i] - size) / 2; // slow down decrease - // Peak hold tracking - if (bar_size[i] > peak_size[i]) peak_size[i] = bar_size[i]; - else if (peak_size[i] > 0) peak_size[i]--; - // Color gradient based on strength - uint16_t barClr = TFT_DARKGREEN; - if (bar_size[i] > max_bar_size * 85 / 100) barClr = TFT_RED; - else if (bar_size[i] > max_bar_size * 65 / 100) barClr = TFT_ORANGE; - else if (bar_size[i] > max_bar_size * 45 / 100) barClr = TFT_YELLOW; - else if (bar_size[i] > max_bar_size * 25 / 100) barClr = TFT_GREEN; - tft.fillRect( - i * space, tftHeight - 20 - bar_size[i], space - 2, bar_size[i], barClr - ); - tft.fillRect(i * space, 20, space, max_bar_size - bar_size[i], bruceConfig.bgColor); - // Peak hold marker (white line) - if (peak_size[i] > bar_size[i] && peak_size[i] > 0) { - int peakY = tftHeight - 20 - peak_size[i]; - tft.drawFastHLine(i * space, peakY, space - 2, TFT_WHITE); - } - if (bar_size[i] > bar_size[max_idx] && bar_size[i] > min_value) max_idx = i; - } - // dBm reference labels (overlaid on bars) - { - int refs[] = {-70, -45}; - const char *rlbl[] = {"-70", "-45"}; - tft.setTextSize(FP); - for (int r = 0; r < 2; r++) { - int refH = map(refs[r], -95, -20, 0, max_bar_size); - int refY = tftHeight - 20 - refH; - tft.fillRect(0, refY - 4, 20, 9, bruceConfig.bgColor); - tft.setTextColor(TFT_DARKGREY, bruceConfig.bgColor); - tft.drawString(rlbl[r], 1, refY - 4, 1); - tft.drawFastHLine(0, refY, 4, TFT_DARKGREY); - } - } - if (bar_size[max_idx] > min_value) { - char buf[24]; - float var = subghz_frequency_list[range_limits[bruceConfigPins.rfScanRange][0] + max_idx]; - int peakDbm = map(bar_size[max_idx], 0, max_bar_size, -95, -20); - snprintf(buf, sizeof(buf), "PK:%.2f %ddBm", var, peakDbm); - tft.fillRect(tftWidth / 2 - 70, tftHeight - 10, 140, 10, bruceConfig.bgColor); - tft.setTextColor(TFT_YELLOW, bruceConfig.bgColor); - tft.drawCentreString(buf, tftWidth / 2, tftHeight - 10); - } - } - if (check(EscPress)) { break; } - if (check(SelPress)) { - deinitRfModule(); - rf_range_selection(bruceConfigPins.rfFreq); - redraw = true; - } - } - deinitRfModule(); -#else - displayError("Not available on Launcher version"); -#endif -} diff --git a/src/modules/rf/rf_utils.cpp b/src/modules/rf/rf_utils.cpp index 2f1dc1e3d..f647b8ee4 100644 --- a/src/modules/rf/rf_utils.cpp +++ b/src/modules/rf/rf_utils.cpp @@ -13,6 +13,11 @@ const int range_limits[4][2] = { {0, 56} // All ranges }; const char *subghz_frequency_ranges[] = {"300-348 MHz", "387-464 MHz", "779-928 MHz", "All ranges"}; + +String rf_subghz_header(float frequencyMHz) { + return "Filetype: Bruce SubGhz File\nVersion 1\n" + String("Frequency: ") + + String(int(frequencyMHz * 1000000)) + "\n"; +} const float subghz_frequency_list[] = { /* 300 - 348 MHz Frequency Range */ 300.000f, @@ -78,10 +83,93 @@ const float subghz_frequency_list[] = { 928.000f }; +uint8_t +cc1101InterpolateFsctrl0(float frequency, float minFreq, float maxFreq, uint8_t minValue, uint8_t maxValue) { + if (frequency <= minFreq) return minValue; + if (frequency >= maxFreq) return maxValue; + + const float ratio = (frequency - minFreq) / (maxFreq - minFreq); + return uint8_t(minValue + (ratio * float(maxValue - minValue)) + 0.5f); +} + +void cc1101WaitForIdle() { + const uint32_t start = millis(); + while ((ELECHOUSE_cc1101.SpiReadStatus(CC1101_MARCSTATE) & 0x1F) != 0x01) { + if (millis() - start > 20) break; + delay(1); + } +} + +void cc1101ApplyPreciseCalibration(float frequency, bool isTx) { + uint8_t fsctrl0 = 0x00; + uint8_t test0 = 0x09; + bool highVco = true; + + if (frequency >= 280.0f && frequency <= 348.0f) { + fsctrl0 = cc1101InterpolateFsctrl0(frequency, 280.0f, 348.0f, 24, 28); + highVco = frequency >= 322.88f; + } else if (frequency >= 387.0f && frequency <= 464.0f) { + fsctrl0 = cc1101InterpolateFsctrl0(frequency, 387.0f, 464.0f, 31, 38); + highVco = frequency >= 430.50f; + } else if (frequency >= 779.0f && frequency <= 899.99f) { + fsctrl0 = cc1101InterpolateFsctrl0(frequency, 779.0f, 899.99f, 65, 76); + highVco = frequency >= 861.0f; + } else if (frequency >= 900.0f && frequency <= 928.0f) { + fsctrl0 = cc1101InterpolateFsctrl0(frequency, 900.0f, 928.0f, 77, 79); + highVco = true; + } else { + return; + } + + test0 = highVco ? 0x09 : 0x0B; + + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FSCTRL0, fsctrl0); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_TEST0, test0); + if (isTx) { + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FREND0, 0x11); + ELECHOUSE_cc1101.setPA(12); + } else { + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FREND1, 0xB6); + } + ELECHOUSE_cc1101.SpiStrobe(CC1101_SCAL); + cc1101WaitForIdle(); + + if (highVco) { + const uint8_t fscal2 = ELECHOUSE_cc1101.SpiReadReg(CC1101_FSCAL2); + if (fscal2 < 0x20) { + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FSCAL2, fscal2 + 0x20); + ELECHOUSE_cc1101.SpiStrobe(CC1101_SCAL); + cc1101WaitForIdle(); + } + } +} + +void cc1101ApplyFixedFreqOokPreset(bool isTx) { + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FIFOTHR, 0x47); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FSCTRL1, 0x06); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_MDMCFG0, 0x00); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_MDMCFG1, 0x00); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_MDMCFG2, 0x30); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_MDMCFG3, 0x32); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_MDMCFG4, 0x67); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_MCSM0, 0x18); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FOCCFG, 0x18); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FREND0, 0x11); + if (isTx) { + ELECHOUSE_cc1101.setPA(12); + } else { + ELECHOUSE_cc1101.SpiWriteReg(CC1101_AGCCTRL0, 0x40); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_AGCCTRL1, 0x00); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_AGCCTRL2, 0x03); + ELECHOUSE_cc1101.SpiWriteReg(CC1101_FREND1, 0xB6); + } +} + RfCodes recent_rfcodes[16]; // TODO: save/load in EEPROM int recent_rfcodes_last_used = 0; // TODO: save/load in EEPROM bool rmtInstalled = true; static bool cc1101_spi_ready = false; +static uint8_t cc1101_mode_hint = 0; bool RfCodes::keeloq_check_decrypt(uint32_t decrypt) { uint16_t end_serial = serial & 0xFF; @@ -109,48 +197,12 @@ bool RfCodes::keeloq_check_decrypt_centurion(uint32_t decrypt) { void RfCodes::keeloq_step(uint16_t step) { cnt += step; - hop = btn << 28 | (serial & 0x3FF) << 16 | cnt; - - if (mf_name == "Aprimatic") { - uint32_t apri_serial = serial; - uint8_t apr1 = 0; - - for (uint16_t i = 1; i != 0b10000000000; i <<= 1) { - if (apri_serial & i) apr1++; - } - - apri_serial &= 0b00001111111111; - - if (apr1 % 2 == 0) { apri_serial |= 0b110000000000; } - - hop = btn << 28 | (apri_serial & 0xFFF) << 16 | cnt; - } else if (mf_name == "DTM_Neo" || mf_name == "FAAC_RC,XT" || mf_name == "Mutanco_Mutancode" || - mf_name == "Came_Space" || mf_name == "Genius_Bravo" || mf_name == "GSN" || - mf_name == "Rosh" || mf_name == "Rossi" || mf_name == "Peccinin" || mf_name == "Steelmate" || - mf_name == "Cardin_S449") { - hop = btn << 28 | (serial & 0xFFF) << 16 | cnt; - } else if (mf_name == "NICE_Smilo" || mf_name == "NICE_MHOUSE" || mf_name == "JCM_Tech") { - hop = btn << 28 | (serial & 0xFF) << 16 | cnt; - } else if (mf_name == "Merlin") { - hop = btn << 28 | (0x000) << 16 | cnt; - } else if (mf_name == "Centurion") { - hop = btn << 28 | (0x1CE) << 16 | cnt; - } else if (mf_name == "Monarch") { - hop = btn << 28 | (0x100) << 16 | cnt; - } else if (mf_name == "Dea_Mio") { - uint8_t first_disc_num = (serial >> 8) & 0xF; - uint8_t result_disc = (0xC + (first_disc_num % 4)); - - uint32_t dea_serial = (serial & 0xFF) | (((uint32_t)result_disc) << 8); - - hop = btn << 28 | (dea_serial & 0xFFF) << 16 | cnt; - } + // Hop framing (per-manufacturer serial masking) lives in protocols/rf_keeloq + // so it can be unit-tested without the SD keystore (see rf_keeloq_selftest). + hop = keeloq_build_hop(mf_name, btn, serial, cnt); - FS *fs = NULL; - - if (!getFsStorage(fs)) { return; } - - KeeloqKeystore keystore{fs}; + // A null fs is fine: the keystore falls back to the encrypted built-in keys. + KeeloqKeystore keystore{keeloq_mfcodes_fs()}; KeeloqKey current_key; @@ -158,61 +210,21 @@ void RfCodes::keeloq_step(uint16_t step) { if (key.mf_name == mf_name) { current_key = key; } } - switch (current_key.type) { - case KEELOQ_SIMPLE_LEARNING: { - encrypted = keeloq_encrypt(hop, current_key.key); - - break; - } - case KEELOQ_NORMAL_LEARNING: { - uint64_t man = keeloq_normal_learning(hop, current_key.key); - - encrypted = keeloq_encrypt(hop, man); + // Derive the manufacturer key for this learning type and encrypt the hop. + // `man` comes from `fix` (button|serial) — matching the decode path in + // keeloq_identify and the reference encoder; deriving it from `hop` (the + // previous behavior) broke the round-trip because hop carries the counter. + // Secure/Erreka need a seed; fall back to the serial-derived seed as the + // reference does when none was captured. + uint32_t seed_eff = seed ? seed : (fix & 0x0FFFFFFF); + uint64_t man = keeloq_derive_man(current_key.type, fix, seed_eff, current_key.key); - break; - } - } + encrypted = keeloq_encrypt(hop, man); key = reverse_bits(encrypted, 32) << 32 | reverse_bits(fix, 32); } -std::vector split_string(String str, char c) { - std::vector cols{}; - size_t start = 0; - - while (start < str.length()) { - auto it = str.indexOf(c, start); - - if (it == -1) break; - - cols.emplace_back(&str[start], it - start); - start = it + 1; - } - - if (start <= str.length() && !str.isEmpty()) cols.emplace_back(&str[start], str.length() - start); - - return cols; -} - -KeeloqKeystore::KeeloqKeystore(FS *fs) { - File keystore = fs->open("/mfcodes"); - - if (!keystore) { return; } - - String line = keystore.readStringUntil('\n'); - - for (; line != ""; line = keystore.readStringUntil('\n')) { - auto cols = split_string(line, ';'); - - if (cols.size() != 3) { return; } - - KeeloqKey key{cols[0], std::strtoull(cols[1].c_str(), NULL, 16), (uint8_t)cols[2].toInt()}; - - keys.push_back(key); - } -} - -const std::vector &KeeloqKeystore::get_keys() { return keys; } +// split_string + KeeloqKeystore moved to protocols/rf_keeloq.cpp. bool initRfModule(String mode, float frequency) { // use default frequency if no one is passed @@ -226,15 +238,17 @@ bool initRfModule(String mode, float frequency) { #else yield(); #endif - } else if (bruceConfigPins.CC1101_bus.mosi == - bruceConfigPins.SDCARD_bus.mosi) { // (CARDPUTER) and (ESP32S3DEVKITC1) and devices that - // share CC1101 pin with only SDCard + } else if (bruceConfigPins.CC1101_bus.mosi == bruceConfigPins.SDCARD_bus.mosi) { // (CARDPUTER) and + // (ESP32S3DEVKITC1) + // and devices that + // share CC1101 pin + // with only SDCard initCC1101once(&sdcardSPI); - } else if (bruceConfigPins.NRF24_bus.mosi == bruceConfigPins.CC1101_bus.mosi && - bruceConfigPins.CC1101_bus.mosi != - bruceConfigPins.SDCARD_bus - .mosi) { // This board uses the same Bus for NRF and CC1101, but with - // different CS pins, different from Stick_Cs down below.. + } else if ( + bruceConfigPins.NRF24_bus.mosi == bruceConfigPins.CC1101_bus.mosi && + bruceConfigPins.CC1101_bus.mosi != bruceConfigPins.SDCARD_bus.mosi + ) { // This board uses the same Bus for NRF and CC1101, but with + // different CS pins, different from Stick_Cs down below.. CC_NRF_SPI.end(); // Closes in case it was already in use, it will overwrite the attempt // of SD start over to save configurations @@ -279,13 +293,19 @@ bool initRfModule(String mode, float frequency) { } // else // ELECHOUSE_cc1101.setRxBW(812.50); // reset to default - ELECHOUSE_cc1101.setRxBW(256); // narrow band for better accuracy - ELECHOUSE_cc1101.setClb(1, 13, 15); // Calibration Offset - ELECHOUSE_cc1101.setClb(2, 16, 19); // Calibration Offset + if (bruceConfigPins.rfFxdFreq == 1) { + cc1101_mode_hint = (mode == "tx") ? 1 : ((mode == "rx") ? 2 : 0); + cc1101ApplyFixedFreqOokPreset(cc1101_mode_hint == 1); + } else { + ELECHOUSE_cc1101.setRxBW(256); // generic profile for scan/hopping + ELECHOUSE_cc1101.setDRate(50); + } + ELECHOUSE_cc1101.setClb(1, 24, 28); // Keep upstream 315 MHz FSCTRL0 range + ELECHOUSE_cc1101.setClb(2, 31, 38); // Keep upstream 433 MHz FSCTRL0 range + ELECHOUSE_cc1101.setClb(3, 65, 76); // Keep upstream 868 MHz FSCTRL0 range + ELECHOUSE_cc1101.setClb(4, 77, 79); // Keep upstream 915 MHz FSCTRL0 range // set modulation mode. 0 = 2-FSK, 1 = GFSK, 2 = ASK/OOK, 3 = 4-FSK, 4 = MSK. ELECHOUSE_cc1101.setModulation(2); - // Set the Data Rate in kBaud. Value from 0.02 to 1621.83. Default is 99.97 kBaud! - ELECHOUSE_cc1101.setDRate(50); // Format of RX and TX data. // 0 = Normal mode, use FIFOs for RX and TX. // 1 = Synchronous serial mode, Data in on GDO0 and data out on either of the GDOx pins. @@ -294,6 +314,7 @@ bool initRfModule(String mode, float frequency) { //. 3 = Asynchronous serial mode, Data in on GDO0 and data out on either of the GDOx pins. ELECHOUSE_cc1101.setPktFormat(3); setMHZ(frequency); + cc1101_mode_hint = 0; Serial.println("cc1101 setMHZ(frequency);"); /* MEMO: cannot change other params after this is executed */ @@ -308,7 +329,7 @@ bool initRfModule(String mode, float frequency) { } else if (mode == "rx") { ioExpander.turnPinOnOff(IO_EXP_CC_RX, HIGH); ioExpander.turnPinOnOff(IO_EXP_CC_TX, LOW); - pinMode(bruceConfigPins.CC1101_bus.io0, INPUT); + pinMode(bruceConfigPins.CC1101_bus.io0, INPUT_PULLUP); ELECHOUSE_cc1101.SetRx(); Serial.println("cc1101 SetRx();"); } @@ -333,7 +354,7 @@ bool initRfModule(String mode, float frequency) { gsetRfRxPin(false); if (bruceConfigPins.SDCARD_bus.checkConflict(bruceConfigPins.rfRx)) sdcardSPI.end(); gpio_reset_pin((gpio_num_t)bruceConfigPins.rfRx); - pinMode(bruceConfigPins.rfRx, INPUT); + pinMode(bruceConfigPins.rfRx, INPUT_PULLUP); } } // no error @@ -409,7 +430,22 @@ void setMHZ(float frequency) { vTaskDelay(10 / portTICK_PERIOD_MS); // time to settle the antenna signal } #endif + const bool preciseCalibration = (bruceConfigPins.rfFxdFreq == 1); + const uint8_t previousMode = preciseCalibration ? ELECHOUSE_cc1101.getMode() : 0; + const uint8_t targetMode = + preciseCalibration ? (previousMode != 0 ? previousMode : cc1101_mode_hint) : 0; + const bool isTxProfile = (targetMode == 1); + + if (preciseCalibration && previousMode != 0) ELECHOUSE_cc1101.setSidle(); + ELECHOUSE_cc1101.setMHZ(frequency); + + if (preciseCalibration) { + cc1101ApplyPreciseCalibration(frequency, isTxProfile); + + if (previousMode == 1) ELECHOUSE_cc1101.SetTx(); + else if (previousMode == 2) ELECHOUSE_cc1101.SetRx(); + } } } @@ -564,37 +600,6 @@ void rf_range_selection(float currentFrequency) { else displayTextLine("Range set to " + String(subghz_frequency_ranges[bruceConfigPins.rfScanRange])); } -uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key) { - uint32_t x = data, r; - - for (r = 0; r < 528; r++) - x = (x >> 1) ^ ((bitAt(x, 0) ^ bitAt(x, 16) ^ (uint32_t)bitAt(key, r & 63) ^ - bitAt(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31))) - << 31); - - return x; -} - -uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key) { - uint32_t x = data, r; - - for (r = 0; r < 528; r++) - x = (x << 1) ^ bitAt(x, 31) ^ bitAt(x, 15) ^ (uint32_t)bitAt(key, (15 - r) & 63) ^ - bitAt(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); - - return x; -} - -uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key) { - uint32_t k1, k2; - - data &= 0x0FFFFFFF; - data |= 0x20000000; - k1 = keeloq_decrypt(data, key); - - data &= 0x0FFFFFFF; - data |= 0x60000000; - k2 = keeloq_decrypt(data, key); - - return ((uint64_t)k2 << 32) | k1; -} +// keeloq_encrypt / keeloq_decrypt / keeloq_normal_learning moved to +// protocols/rf_keeloq.cpp (declared via protocols/rf_keeloq.h, included by +// rf_utils.h). reverse_bits stays here since it is a general bit helper. diff --git a/src/modules/rf/rf_utils.h b/src/modules/rf/rf_utils.h index 75f5d8160..94e71e819 100644 --- a/src/modules/rf/rf_utils.h +++ b/src/modules/rf/rf_utils.h @@ -1,6 +1,7 @@ #ifndef __RF_UTILS_H__ #define __RF_UTILS_H__ +#include "protocols/rf_keeloq.h" // KeeLoq cipher/keystore + bitAt/g5/KEELOQ_* macros #include "structs.h" #include // ESP-IDF 5.5 based framework determines the channels autommatically @@ -13,25 +14,6 @@ rmt_channel_handle_t setup_rf_rx(); #define RMT_1MS_TICKS (RMT_1US_TICKS * 1000) #define SIGNAL_STRENGTH_THRESHOLD 1500 // Adjust this threshold as needed -#define bitAt(x, n) (((x) >> (n)) & 1) -#define g5(x, a, b, c, d, e) \ - (bitAt(x, a) + bitAt(x, b) * 2 + bitAt(x, c) * 4 + bitAt(x, d) * 8 + bitAt(x, e) * 16) - -#define KEELOQ_NLF 0x3A5C742E - -#define KEELOQ_SIMPLE_LEARNING 1 -#define KEELOQ_NORMAL_LEARNING 2 - -class KeeloqKeystore { -public: - KeeloqKeystore(FS *fs); - - const std::vector &get_keys(); - -private: - std::vector keys{}; -}; - extern const float subghz_frequency_list[57]; extern const char *subghz_frequency_ranges[]; extern const int range_limits[4][2]; @@ -50,10 +32,8 @@ struct RfCodes selectRecentRfMenu(); bool setMHZMenu(); void rf_range_selection(float currentFrequency = 0.0); -uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key); -uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key); -uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key); - uint64_t reverse_bits(uint64_t num, uint8_t bits); +String rf_subghz_header(float frequencyMHz); + #endif diff --git a/src/modules/rf/structs.h b/src/modules/rf/structs.h index 0e0ed5e62..429a95978 100644 --- a/src/modules/rf/structs.h +++ b/src/modules/rf/structs.h @@ -30,6 +30,7 @@ struct RfCodes { uint32_t fix = 0; uint32_t hop = 0; uint32_t encrypted = 0; + uint32_t seed = 0; // secure/erreka learning; 0 = derive from serial uint8_t btn = 0; String mf_name = "Unknown"; String protocol = ""; diff --git a/src/modules/rfid/RFIDInterface.cpp b/src/modules/rfid/RFIDInterface.cpp new file mode 100644 index 000000000..53a344c4c --- /dev/null +++ b/src/modules/rfid/RFIDInterface.cpp @@ -0,0 +1,106 @@ +/** + * @file RFIDInterface.cpp + * @brief Shared, module-agnostic helpers for RFID modules. + * + * These live in the base class so the serial CLI and the GUI flow through the + * exact same code regardless of which driver is active (PN532, RFID2, ...). + * Drivers may override loadFromFile() for richer, hardware-specific parsing. + */ + +#include "RFIDInterface.h" +#include "core/sd_functions.h" +#include + +// Generic Bruce .rfid parser. Drivers with extra fields (MIFARE Classic +// blocks/keys, NTAG version/signature/counters) override this. +int RFIDInterface::loadFromFile(const String &filepath) { + FS *fs; + if (!getFsStorage(fs)) return FAILURE; + + File file = fs->open(filepath, FILE_READ); + if (!file) return FAILURE; + + String line; + String strData; + strAllPages = ""; + totalPages = 0; + dataPages = 0; + pageReadSuccess = true; + pageReadStatus = SUCCESS; + + while (file.available()) { + line = file.readStringUntil('\n'); + line.trim(); + strData = line.substring(line.indexOf(":") + 1); + strData.trim(); + + if (line.startsWith("Device type:")) printableUID.picc_type = strData; + else if (line.startsWith("UID:")) printableUID.uid = strData; + else if (line.startsWith("SAK:")) printableUID.sak = strData; + else if (line.startsWith("ATQA:")) printableUID.atqa = strData; + else if (line.startsWith("Pages total:")) totalPages = strData.toInt(); + else if (line.startsWith("Pages read:")) pageReadSuccess = false; + else if (line.startsWith("Page ")) { + strAllPages += line + "\n"; + dataPages++; + } + // MIFARE Classic dumps (.rfid/.nfc): keep Block and Key lines so a + // driver can rebuild the dump for clone/emulate. + else if (line.startsWith("Block ")) { + strAllPages += line + "\n"; + dataPages++; + } else if (line.startsWith("Key A sector ") || line.startsWith("Key B sector ")) { + strAllPages += line + "\n"; + } + } + file.close(); + + String uidStr = printableUID.uid; + uidStr.trim(); + uidStr.replace(" ", ""); + uid.size = uidStr.length() / 2; + if (uid.size > sizeof(uid.uidByte)) uid.size = sizeof(uid.uidByte); + for (int i = 0; i < uid.size; i++) { + uid.uidByte[i] = strtoul(uidStr.substring(i * 2, i * 2 + 2).c_str(), NULL, 16); + } + uid.sak = strtoul(printableUID.sak.c_str(), NULL, 16); + + String atqaStr = printableUID.atqa; + atqaStr.replace(" ", ""); + if (atqaStr.length() >= 4) { + // ATQA is stored in transmission order (atqaByte[0] first). + uid.atqaByte[0] = strtoul(atqaStr.substring(0, 2).c_str(), NULL, 16); + uid.atqaByte[1] = strtoul(atqaStr.substring(2, 4).c_str(), NULL, 16); + } + if (totalPages == 0) totalPages = dataPages; + + return SUCCESS; +} + +void RFIDInterface::buildNdefMessage(const String &type, const String &value) { + String t = type; + t.toLowerCase(); + + memset(&ndefMessage, 0, sizeof(ndefMessage)); + ndefMessage.begin = 0x03; + ndefMessage.header = 0xD1; + ndefMessage.tnf = 0x01; + ndefMessage.end = 0xFE; + + if (t == "text") { + ndefMessage.payloadType = NDEF_TEXT; + ndefMessage.payload[0] = 0x02; + ndefMessage.payload[1] = 'e'; + ndefMessage.payload[2] = 'n'; + uint8_t len = min((int)value.length(), 96); + for (uint8_t i = 0; i < len; i++) ndefMessage.payload[i + 3] = value.charAt(i); + ndefMessage.payloadSize = len + 3; + } else { + ndefMessage.payloadType = NDEF_URI; + ndefMessage.payload[0] = 0x00; // no prefix abbreviation + uint8_t len = min((int)value.length(), 99); + for (uint8_t i = 0; i < len; i++) ndefMessage.payload[i + 1] = value.charAt(i); + ndefMessage.payloadSize = len + 1; + } + ndefMessage.messageSize = ndefMessage.payloadSize + 4; +} diff --git a/src/modules/rfid/RFIDInterface.h b/src/modules/rfid/RFIDInterface.h index 7c3af6bb8..0245ad440 100644 --- a/src/modules/rfid/RFIDInterface.h +++ b/src/modules/rfid/RFIDInterface.h @@ -72,6 +72,9 @@ class RFIDInterface { PrintableUID printableUID; NdefMessage ndefMessage; String strAllPages = ""; + // Optional forced emulation mode set by the CLI (e.g. "t4t", "felica"). + // Empty = auto-detect from the loaded tag. Honored by drivers that support it. + String emuMode = ""; int totalPages = 0; int dataPages = 0; bool pageReadSuccess = false; @@ -94,7 +97,18 @@ class RFIDInterface { virtual int write_ndef() = 0; virtual int emulate() { return NOT_IMPLEMENTED; } virtual int load() = 0; + // Load + parse a dump file by path (shared by the GUI and the serial CLI so + // their behavior never diverges). The base class provides a generic Bruce + // .rfid parser; drivers may override it for richer handling (e.g. MIFARE + // Classic blocks/keys, NTAG version/signature/counters in ST25R3916). + virtual int loadFromFile(const String &filepath); virtual int save(String filename) = 0; + virtual int saveFlipper(String filename) { return NOT_IMPLEMENTED; } + + // Build `ndefMessage` from a type ("url"/"text") and value. Shared by the + // serial `rfid ndef` and `rfid emulate t4t` paths so the encoding is + // identical regardless of entry point. + void buildNdefMessage(const String &type, const String &value); String statusMessage(int status) const { switch (status) { diff --git a/src/modules/rfid/ST25R3916.cpp b/src/modules/rfid/ST25R3916.cpp new file mode 100644 index 000000000..eb0644e00 --- /dev/null +++ b/src/modules/rfid/ST25R3916.cpp @@ -0,0 +1,3277 @@ +#include "ST25R3916.h" +#if !defined(LITE_VERSION) + +// ST25R3916 via RFAL fork (lewisxhe/ST25R3916-fork + NFC-RFAL-fork). +// Supported: SPI mode on boards with ST25R wiring such as lilygo-t-lora-pager and reaper. +// I2C mode uses the fork constructor RfalRfST25R3916Class(&Wire, irqPin). +// Card emulation/listen mode is not wired in this driver yet. +// ISO15693 writes are not implemented. + +#include "core/sd_functions.h" +#include "modules/rfid/apdu.h" +#include +#include + +#define ST25R_DEBUG 1 +#if ST25R_DEBUG +#define ST25R_LOG(fmt, ...) Serial.printf("[ST25R] " fmt "\n", ##__VA_ARGS__) +#else +#define ST25R_LOG(fmt, ...) \ + do { \ + } while (0) +#endif + +static const char *_stateStr(rfalNfcState st) { + switch (st) { + case RFAL_NFC_STATE_NOTINIT: return "NOTINIT"; + case RFAL_NFC_STATE_IDLE: return "IDLE"; + case RFAL_NFC_STATE_START_DISCOVERY: return "START_DISCOVERY"; + case RFAL_NFC_STATE_WAKEUP_MODE: return "WAKEUP_MODE"; + case RFAL_NFC_STATE_POLL_TECHDETECT: return "POLL_TECHDETECT"; + case RFAL_NFC_STATE_POLL_COLAVOIDANCE: return "POLL_COLAVOIDANCE"; + case RFAL_NFC_STATE_POLL_SELECT: return "POLL_SELECT"; + case RFAL_NFC_STATE_POLL_ACTIVATION: return "POLL_ACTIVATION"; + case RFAL_NFC_STATE_LISTEN_TECHDETECT: return "LISTEN_TECHDETECT"; + case RFAL_NFC_STATE_ACTIVATED: return "ACTIVATED"; + case RFAL_NFC_STATE_DATAEXCHANGE: return "DATAEXCHANGE"; + case RFAL_NFC_STATE_DEACTIVATION: return "DEACTIVATION"; + default: return "UNKNOWN"; + } +} + +static void _deselectSharedSpiDevices() { + /* + #if defined(TFT_CS) && TFT_CS >= 0 + digitalWrite(TFT_CS, HIGH); + #endif + #if defined(SDCARD_CS) && SDCARD_CS >= 0 + digitalWrite(SDCARD_CS, HIGH); + #endif + #if defined(LORA_CS) && LORA_CS >= 0 + digitalWrite(LORA_CS, HIGH); + #endif + #if defined(NFC_CS) && NFC_CS >= 0 + digitalWrite(NFC_CS, HIGH); + #endif + */ +} + +static void _setNfcPower(bool enabled) { +#if defined(IO_EXP_NFC) && IO_EXP_NFC >= 0 + ioExpander.setPinDirection(IO_EXP_NFC, OUTPUT); + ioExpander.turnPinOnOff(IO_EXP_NFC, enabled ? HIGH : LOW); +#endif +} + +namespace { +int st25HexNibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +bool st25ParseHexBytesAfterColon(const String &line, std::vector &bytes) { + bytes.clear(); + int colon = line.indexOf(':'); + if (colon < 0) return false; + + int hi = -1; + for (int i = colon + 1; i < line.length(); i++) { + int v = st25HexNibble(line.charAt(i)); + if (v < 0) continue; + if (hi < 0) { + hi = v; + } else { + bytes.push_back(static_cast((hi << 4) | v)); + hi = -1; + } + } + return !bytes.empty() && hi < 0; +} + +bool st25ExtractNdefMessageFromPageDump(const String &dump, std::vector &ndefOut) { + ndefOut.clear(); + if (dump.length() == 0) return false; + + std::vector userData; + std::vector lineBytes; + int pos = 0; + + while (pos < dump.length()) { + int nl = dump.indexOf('\n', pos); + if (nl < 0) nl = dump.length(); + + String line = dump.substring(pos, nl); + line.trim(); + pos = nl + 1; + + if (!line.startsWith("Page ")) continue; + + int colon = line.indexOf(':'); + if (colon < 0) continue; + + int page = line.substring(5, colon).toInt(); + if (page < 4) continue; + + if (!st25ParseHexBytesAfterColon(line, lineBytes)) continue; + if (lineBytes.size() < 4) continue; + userData.insert(userData.end(), lineBytes.begin(), lineBytes.begin() + 4); + } + + size_t i = 0; + while (i < userData.size()) { + uint8_t tlv = userData[i++]; + + if (tlv == 0x00) continue; + if (tlv == 0xFE) break; + if (i >= userData.size()) return false; + + uint32_t len = userData[i++]; + if (len == 0xFF) { + if (i + 1 >= userData.size()) return false; + len = (static_cast(userData[i]) << 8) | userData[i + 1]; + i += 2; + } + + if (i + len > userData.size()) return false; + + if (tlv == 0x03) { + ndefOut.assign(userData.begin() + i, userData.begin() + i + len); + return !ndefOut.empty(); + } + + i += len; + } + + return false; +} + +bool st25BuildNdefMessageFromStruct(const RFIDInterface::NdefMessage &src, std::vector &ndefOut) { + ndefOut.clear(); + if (src.messageSize == 0 || src.payloadSize == 0) return false; + + ndefOut.reserve(4 + src.payloadSize); + ndefOut.push_back(src.header); + ndefOut.push_back(src.tnf); + ndefOut.push_back(src.payloadSize); + ndefOut.push_back(src.payloadType); + ndefOut.insert(ndefOut.end(), src.payload, src.payload + src.payloadSize); + + return ndefOut.size() == src.messageSize; +} +} // namespace + +void ST25R3916::_logOpControl(const char *where) { + if (!_hw) return; + + uint8_t op = 0; + uint8_t id = 0; + uint8_t io1 = 0; + uint8_t io2 = 0; + _hw->st25r3916ReadRegister(ST25R3916_REG_OP_CONTROL, &op); + _hw->st25r3916ReadRegister(ST25R3916_REG_IC_IDENTITY, &id); + _hw->st25r3916ReadRegister(ST25R3916_REG_IO_CONF1, &io1); + _hw->st25r3916ReadRegister(ST25R3916_REG_IO_CONF2, &io2); + ST25R_LOG( + "%s OP_CONTROL=0x%02X en=%d rx=%d tx=%d IC_ID=0x%02X IO1=0x%02X IO2=0x%02X", + where, + op, + (op & ST25R3916_REG_OP_CONTROL_en) ? 1 : 0, + (op & ST25R3916_REG_OP_CONTROL_rx_en) ? 1 : 0, + (op & ST25R3916_REG_OP_CONTROL_tx_en) ? 1 : 0, + id, + io1, + io2 + ); +} + +void ST25R3916::_probeField(const char *where) { + if (!_hw) return; + + auto err = _hw->rfalFieldOnAndStartGT(); + ST25R_LOG("%s rfalFieldOnAndStartGT -> %d", where, err); + _logOpControl("after manual field on"); + _hw->rfalFieldOff(); + _logOpControl("after manual field off"); +} + +ST25R3916::ST25R3916(CONNECTION_TYPE connection_type) : _connection_type(connection_type) {} + +ST25R3916::~ST25R3916() { + stopDiscovery(); + _setNfcPower(false); + delete _nfc; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" + // RFAL class is deleted through its concrete type, but the fork lacks a virtual destructor. + delete _hw; +#pragma GCC diagnostic pop +} + +bool ST25R3916::_initSPI() { + _deselectSharedSpiDevices(); + + int cs = (int)bruceConfigPins.ST25R_bus.cs; + int irq = (int)bruceConfigPins.ST25R_bus.io0; + ST25R_LOG( + "_initSPI: CS=%d IRQ=%d MOSI=%d MISO=%d SCK=%d", + cs, + irq, + (int)bruceConfigPins.ST25R_bus.mosi, + (int)bruceConfigPins.ST25R_bus.miso, + (int)bruceConfigPins.ST25R_bus.sck + ); + + if (bruceConfigPins.ST25R_bus.mosi == (gpio_num_t)TFT_MOSI && + bruceConfigPins.ST25R_bus.mosi != GPIO_NUM_NC) { +#if TFT_MOSI >= 0 + ST25R_LOG("_initSPI: sharing TFT SPI"); + _spi = &tft.getSPIinstance(); +#endif + } else if (bruceConfigPins.ST25R_bus.mosi == bruceConfigPins.SDCARD_bus.mosi) { + ST25R_LOG("_initSPI: sharing SDCARD SPI"); + _spi = &sdcardSPI; + } else { + ST25R_LOG("_initSPI: creating dedicated HSPI"); + static SPIClass st25rSPI(HSPI); + static bool spiStarted = false; + if (!spiStarted) { + st25rSPI.begin( + (int)bruceConfigPins.ST25R_bus.sck, + (int)bruceConfigPins.ST25R_bus.miso, + (int)bruceConfigPins.ST25R_bus.mosi + ); + spiStarted = true; + } + _spi = &st25rSPI; + } + + _hw = new RfalRfST25R3916Class(_spi, cs, irq); + ST25R_LOG("_initSPI: _hw=%s", _hw ? "ok" : "NULL"); + return _hw != nullptr; +} + +bool ST25R3916::_initI2C() { + Wire.begin(bruceConfigPins.i2c_bus.sda, bruceConfigPins.i2c_bus.scl); + _hw = new RfalRfST25R3916Class(&Wire, GPIO_NUM_NC); + return _hw != nullptr; +} + +bool ST25R3916::begin() { + _setNfcPower(true); + delay(5); + _deselectSharedSpiDevices(); + ST25R_LOG("begin: mode=%s", _connection_type == SPI_MODE ? "SPI" : "I2C"); + bool ok = (_connection_type == SPI_MODE) ? _initSPI() : _initI2C(); + if (!ok) { + ST25R_LOG("begin: hw init FAILED"); + return false; + } + + _nfc = new RfalNfcClass(_hw); + uint16_t err = _nfc->rfalNfcInitialize(); + ST25R_LOG("begin: rfalNfcInitialize -> %d (%s)", err, err == ST_ERR_NONE ? "OK" : "FAIL"); + _logOpControl("after init"); + _hw->st25r3916OscOn(); + _logOpControl("after forced osc on"); + return (err == ST_ERR_NONE); +} + +static void _notifyCb(rfalNfcState st) { /* ST25R_LOG("notifyCb: %s(%d)", _stateStr(st), (int)st); */ } + +bool ST25R3916::_startDiscovery() { + _deselectSharedSpiDevices(); + + rfalNfcDiscoverParam params; + memset(¶ms, 0, sizeof(params)); + params.compMode = RFAL_COMPLIANCE_MODE_NFC; + params.devLimit = 1; + params.nfcfBR = RFAL_BR_212; + params.ap2pBR = RFAL_BR_424; + params.techs2Find = + RFAL_NFC_POLL_TECH_A | RFAL_NFC_POLL_TECH_B | RFAL_NFC_POLL_TECH_V | RFAL_NFC_POLL_TECH_F; + params.GBLen = RFAL_NFCDEP_GB_MAX_LEN; + params.notifyCb = _notifyCb; + params.totalDuration = 1000U; + params.wakeupEnabled = false; + params.wakeupConfigDefault = true; + + uint16_t err = _nfc->rfalNfcDiscover(¶ms); + ST25R_LOG("_startDiscovery: rfalNfcDiscover -> %d (%s)", err, err == ST_ERR_NONE ? "OK" : "FAIL"); + _hw->st25r3916OscOn(); + _logOpControl("after discover"); + return (err == ST_ERR_NONE); +} + +void ST25R3916::stopDiscovery() { + if (_discoveryStarted && _nfc) { + _nfc->rfalNfcDeactivate(false); + _discoveryStarted = false; + } +} + +String ST25R3916::_getNfcaTypeName(uint8_t sak) { + if (sak == 0x08 || sak == 0x88) return "MIFARE Classic 1K"; + if (sak == 0x18) return "MIFARE Classic 4K"; + if (sak == 0x09) return "MIFARE Mini"; + if (sak == 0x00) return "MIFARE Ultralight"; + if (sak == 0x20) return "ISO 14443-4"; + return "ISO 14443A"; +} + +void ST25R3916::_parseDevice(rfalNfcDevice *dev) { + uid.size = dev->nfcidLen; + memcpy(uid.uidByte, dev->nfcid, uid.size); + + printableUID.uid = ""; + for (int i = 0; i < uid.size; i++) { + char buf[3]; + sprintf(buf, "%02X", uid.uidByte[i]); + printableUID.uid += buf; + if (i < uid.size - 1) printableUID.uid += " "; + } + + uint8_t bcc = 0; + for (int i = 0; i < uid.size; i++) bcc ^= uid.uidByte[i]; + char bccBuf[3]; + sprintf(bccBuf, "%02X", bcc); + printableUID.bcc = String(bccBuf); + + switch (dev->type) { + case RFAL_NFC_LISTEN_TYPE_NFCA: { + rfalNfcaListenDevice *nfca = &dev->dev.nfca; + uid.sak = nfca->selRes.sak; + char sakBuf[3]; + sprintf(sakBuf, "%02X", uid.sak); + printableUID.sak = String(sakBuf); + + uid.atqaByte[0] = nfca->sensRes.anticollisionInfo; + uid.atqaByte[1] = nfca->sensRes.platformInfo; + // ATQA em ordem de transmissão (byte baixo primeiro), igual ao PN532/Flipper + // (ex.: NTAG => "44 00"). + char atqaBuf[6]; + sprintf(atqaBuf, "%02X %02X", uid.atqaByte[0], uid.atqaByte[1]); + printableUID.atqa = String(atqaBuf); + + printableUID.picc_type = _getNfcaTypeName(uid.sak); + break; + } + case RFAL_NFC_LISTEN_TYPE_NFCB: + printableUID.picc_type = "ISO14443B"; + printableUID.sak = "--"; + printableUID.atqa = "--"; + break; + case RFAL_NFC_LISTEN_TYPE_NFCV: + printableUID.picc_type = "ISO15693"; + printableUID.sak = "--"; + printableUID.atqa = "--"; + break; + default: + printableUID.picc_type = "Unknown"; + printableUID.sak = "--"; + printableUID.atqa = "--"; + break; + } +} + +int ST25R3916::_readDataBlocks(rfalNfcDevice *dev) { + strAllPages = ""; + dataPages = 0; + totalPages = 0; + pageReadSuccess = false; + + // ISO-DEP (ISO14443-4) — T4T / DESFire / EMV (NFC-A SAK=0x20 ou NFC-B). + if (dev->rfInterface == RFAL_NFC_INTERFACE_ISODEP) { return _readIsoDep(dev); } + + switch (dev->type) { + case RFAL_NFC_LISTEN_TYPE_NFCB: + _parseNfcB(dev); + pageReadStatus = SUCCESS; + pageReadSuccess = true; + return SUCCESS; + case RFAL_NFC_LISTEN_TYPE_NFCV: return _readNfcV(dev); + case RFAL_NFC_LISTEN_TYPE_NFCF: return _readFeliCa(dev); + default: break; + } + + // MIFARE Classic (SAK 0x08/0x88 = 1K, 0x18 = 4K, 0x09 = Mini) + if (dev->type == RFAL_NFC_LISTEN_TYPE_NFCA && isMifareClassicSak(uid.sak)) { + return _readMifareClassic(dev); + } + + if (dev->type != RFAL_NFC_LISTEN_TYPE_NFCA || uid.sak != 0x00) { + pageReadStatus = FAILURE; + return FAILURE; + } + + // Detect NTAG size from CC page (page 3) + uint8_t ccBuf[16] = {0}; + uint16_t rcvLen = 0; + auto ccErr = _nfc->rfalT2TPollerRead(3, ccBuf, sizeof(ccBuf), &rcvLen); + if (ccErr != ST_ERR_NONE || rcvLen < 4) { + pageReadStatus = FAILURE; + return FAILURE; + } + if (_ntagPagesHint > 0) { + // GET_VERSION is authoritative (also covers blank/unformatted tags) + totalPages = _ntagPagesHint; + } else { + switch (ccBuf[2]) { + case 0x12: totalPages = 45; break; // NTAG213 + case 0x3E: totalPages = 135; break; // NTAG215 + case 0x6D: totalPages = 231; break; // NTAG216 + default: totalPages = 64; break; // MF Ultralight + } + } + + for (int page = 0; page < totalPages; page++) { + uint8_t pageData[16] = {0}; + uint16_t len = 0; + auto err = _nfc->rfalT2TPollerRead(page, pageData, sizeof(pageData), &len); + if (err != ST_ERR_NONE || len < 4) { + pageReadStatus = FAILURE; + return FAILURE; + } + char line[32]; + sprintf( + line, "Page %d: %02X %02X %02X %02X", page, pageData[0], pageData[1], pageData[2], pageData[3] + ); + strAllPages += String(line) + "\n"; + dataPages++; + } + + pageReadStatus = SUCCESS; + pageReadSuccess = true; + return SUCCESS; +} + +void ST25R3916::_parseLoadedData() { + String strUID = printableUID.uid; + strUID.trim(); + strUID.replace(" ", ""); + uid.size = strUID.length() / 2; + if (uid.size > sizeof(uid.uidByte)) uid.size = sizeof(uid.uidByte); + for (size_t i = 0; i < uid.size; i++) { + uid.uidByte[i] = strtoul(strUID.substring(i * 2, i * 2 + 2).c_str(), NULL, 16); + } + + uint8_t bcc = 0; + for (int i = 0; i < uid.size; i++) bcc ^= uid.uidByte[i]; + char bccBuf[3]; + sprintf(bccBuf, "%02X", bcc); + printableUID.bcc = String(bccBuf); + + printableUID.sak.trim(); + uid.sak = strtoul(printableUID.sak.c_str(), NULL, 16); + + String strAtqa = printableUID.atqa; + strAtqa.trim(); + strAtqa.replace(" ", ""); + uid.atqaByte[0] = 0; + uid.atqaByte[1] = 0; + if (strAtqa.length() >= 4) { + // ATQA em ordem de transmissão (byte baixo primeiro), igual ao PN532/Flipper. + uid.atqaByte[0] = strtoul(strAtqa.substring(0, 2).c_str(), NULL, 16); + uid.atqaByte[1] = strtoul(strAtqa.substring(2, 4).c_str(), NULL, 16); + } +} + +bool ST25R3916::_isUltralightUserPage(int page) const { + if (page < 4) return false; + if (totalPages <= 0) return true; + return page < (totalPages - 5); +} + +bool ST25R3916::_buildLoadedNdefMessage(std::vector &ndefOut) { + if (st25ExtractNdefMessageFromPageDump(strAllPages, ndefOut)) return true; + if (st25BuildNdefMessageFromStruct(ndefMessage, ndefOut)) return true; + + std::vector uriPayload = Ndef::urlNdefAbbrv("https://bruce.computer"); + ndefOut = Ndef::newMessage(uriPayload); + return !ndefOut.empty(); +} + +int ST25R3916::read(int cardBaudRate) { + pageReadStatus = FAILURE; + pageReadSuccess = false; + if (!_nfc) return FAILURE; + _deselectSharedSpiDevices(); + + if (!_discoveryStarted) { + ST25R_LOG("starting discovery"); + if (!_startDiscovery()) { + ST25R_LOG("_startDiscovery FAILED"); + return FAILURE; + } + _discoveryStarted = true; + } + + _deselectSharedSpiDevices(); + _hw->st25r3916OscOn(); + _nfc->rfalNfcWorker(); + + rfalNfcState state = _nfc->rfalNfcGetState(); + ST25R_LOG("-> %s(%d)", _stateStr(state), (int)state); + + if (state != RFAL_NFC_STATE_ACTIVATED) { return TAG_NOT_PRESENT; } + + rfalNfcDevice *dev; + _nfc->rfalNfcGetActiveDevice(&dev); + ST25R_LOG("TAG! type=%d uid_len=%d", (int)dev->type, dev->nfcidLen); + _parseDevice(dev); + ST25R_LOG( + "uid=%s type=%s sak=%s atqa=%s", + printableUID.uid.c_str(), + printableUID.picc_type.c_str(), + printableUID.sak.c_str(), + printableUID.atqa.c_str() + ); + ntagVariant = ""; + ntagHasVersion = false; + ntagHasSignature = false; + ntagHasCounters = false; + _ntagPagesHint = 0; + bool isNfcaT2T = (dev->type == RFAL_NFC_LISTEN_TYPE_NFCA && uid.sak == 0x00); + + // Read data blocks FIRST. GET_VERSION (0x60) is unsupported by non-EV1 + // MIFARE Ultralight and puts the PICC into HALT, which would make every + // subsequent READ time out. _readDataBlocks detects the size from the CC + // page when _ntagPagesHint is 0, so the GET_VERSION hint is not required. + int blkResult = _readDataBlocks(dev); + ST25R_LOG("_readDataBlocks -> %d pages=%d", blkResult, dataPages); + + if (isNfcaT2T) { + // Variant/signature/counter are NTAG21x extras; running them after the + // data read means a HALT from an unsupported command no longer hurts. + String variant = _getNtagVariant(); + if (variant.length() > 0) { + ntagVariant = variant; + printableUID.picc_type = variant; + } + _readNtagSignature(); + _readNtagCounters(); + } + + _nfc->rfalNfcDeactivate(false); + _discoveryStarted = false; + return SUCCESS; +} + +bool ST25R3916::_pollForTag(rfalNfcDevice **dev, uint32_t timeoutMs) { + if (_discoveryStarted) { + _nfc->rfalNfcDeactivate(false); + _discoveryStarted = false; + } + _deselectSharedSpiDevices(); + + rfalNfcDiscoverParam params; + memset(¶ms, 0, sizeof(params)); + params.compMode = RFAL_COMPLIANCE_MODE_NFC; + params.devLimit = 1; + params.nfcfBR = RFAL_BR_212; + params.ap2pBR = RFAL_BR_424; + params.techs2Find = RFAL_NFC_POLL_TECH_A; + params.GBLen = RFAL_NFCDEP_GB_MAX_LEN; + params.notifyCb = _notifyCb; + params.totalDuration = timeoutMs; + params.wakeupEnabled = false; + params.wakeupConfigDefault = true; + + if (_nfc->rfalNfcDiscover(¶ms) != ST_ERR_NONE) return false; + _hw->st25r3916OscOn(); + + uint32_t deadline = millis() + timeoutMs; + while (millis() < deadline) { + _nfc->rfalNfcWorker(); + if (_nfc->rfalNfcGetState() == RFAL_NFC_STATE_ACTIVATED) { + _nfc->rfalNfcGetActiveDevice(dev); + return true; + } + vTaskDelay(pdMS_TO_TICKS(5)); + } + return false; +} + +bool ST25R3916::_writeT2TPage(uint8_t page, const uint8_t data[4], bool verify) { + auto err = _nfc->rfalT2TPollerWrite(page, const_cast(data)); + if (err != ST_ERR_NONE) { + ST25R_LOG("_writeT2TPage: page %u write failed err=%d", page, (int)err); + return false; + } + + delay(12); + + if (!verify) return true; + + uint8_t rx[16] = {0}; + uint16_t rxLen = 0; + err = _nfc->rfalT2TPollerRead(page, rx, sizeof(rx), &rxLen); + if (err != ST_ERR_NONE || rxLen < 4) { + ST25R_LOG("_writeT2TPage: page %u verify read failed err=%d len=%u", page, (int)err, rxLen); + return false; + } + + if (memcmp(rx, data, 4) != 0) { + ST25R_LOG( + "_writeT2TPage: page %u verify mismatch got=%02X %02X %02X %02X expected=%02X %02X %02X %02X", + page, + rx[0], + rx[1], + rx[2], + rx[3], + data[0], + data[1], + data[2], + data[3] + ); + return false; + } + + return true; +} + +int ST25R3916::_writeUltralight(rfalNfcDevice *dev) { + String pages = strAllPages; + int written = 0; + int pos = 0; + + while (pos < (int)pages.length()) { + int nl = pages.indexOf('\n', pos); + if (nl < 0) nl = pages.length(); + String line = pages.substring(pos, nl); + pos = nl + 1; + if (line.length() == 0) continue; + + int pageNum = -1; + unsigned int b0 = 0, b1 = 0, b2 = 0, b3 = 0; + if (sscanf(line.c_str(), "Page %d: %02X %02X %02X %02X", &pageNum, &b0, &b1, &b2, &b3) == 5) { + if (!_isUltralightUserPage(pageNum)) continue; + uint8_t data[4] = {(uint8_t)b0, (uint8_t)b1, (uint8_t)b2, (uint8_t)b3}; + if (!_writeT2TPage((uint8_t)pageNum, data)) { + _nfc->rfalNfcDeactivate(false); + return FAILURE; + } + written++; + } + } + + ST25R_LOG("_writeUltralight: wrote %d pages", written); + _nfc->rfalNfcDeactivate(false); + return SUCCESS; +} + +int ST25R3916::_eraseUltralight(rfalNfcDevice *dev) { + uint8_t zeros[4] = {0x00, 0x00, 0x00, 0x00}; + for (int page = 4; page < totalPages; page++) { + if (!_isUltralightUserPage(page)) continue; + if (!_writeT2TPage((uint8_t)page, zeros)) { + _nfc->rfalNfcDeactivate(false); + return FAILURE; + } + } + ST25R_LOG("_eraseUltralight: erased pages 4..%d", totalPages - 1); + _nfc->rfalNfcDeactivate(false); + return SUCCESS; +} + +int ST25R3916::_writeNdefBlocks(rfalNfcDevice *dev) { + uint8_t ndefPayload[180] = {0}; + size_t idx = 0; + + if (ndefMessage.messageSize == 0 || ndefMessage.payloadSize == 0) return FAILURE; + if ((size_t)ndefMessage.messageSize + 3 > sizeof(ndefPayload)) return FAILURE; + + ndefPayload[idx++] = ndefMessage.begin; + ndefPayload[idx++] = ndefMessage.messageSize; + ndefPayload[idx++] = ndefMessage.header; + ndefPayload[idx++] = ndefMessage.tnf; + ndefPayload[idx++] = ndefMessage.payloadSize; + ndefPayload[idx++] = ndefMessage.payloadType; + + for (uint8_t i = 0; i < ndefMessage.payloadSize && idx < sizeof(ndefPayload); i++) { + ndefPayload[idx++] = ndefMessage.payload[i]; + } + + if (idx >= sizeof(ndefPayload)) return FAILURE; + ndefPayload[idx++] = ndefMessage.end; + + int maxBytes = (totalPages > 0 ? (totalPages - 5 - 4) : 36) * 4; + if (maxBytes <= 0 || (int)idx > maxBytes) { + ST25R_LOG("_writeNdefBlocks: payload too large len=%d max=%d", (int)idx, maxBytes); + return FAILURE; + } + + int pagesWritten = 0; + for (int page = 4; _isUltralightUserPage(page); page++) { + int base = (page - 4) * 4; + if (base >= (int)idx) break; + + uint8_t data[4] = {0x00, 0x00, 0x00, 0x00}; + for (int b = 0; b < 4 && base + b < (int)idx; b++) data[b] = ndefPayload[base + b]; + + if (!_writeT2TPage((uint8_t)page, data)) { return FAILURE; } + pagesWritten++; + } + + uint8_t zeros[4] = {0x00, 0x00, 0x00, 0x00}; + for (int page = 4 + pagesWritten; _isUltralightUserPage(page); page++) { + if (!_writeT2TPage((uint8_t)page, zeros)) { return FAILURE; } + } + + ST25R_LOG("_writeNdefBlocks: wrote %d bytes", (int)idx); + return SUCCESS; +} + +bool ST25R3916::_writeMagicGen2UID(rfalNfcDevice *dev) { + if (uid.size == 7) { + uint8_t bcc0 = 0x88 ^ uid.uidByte[0] ^ uid.uidByte[1] ^ uid.uidByte[2]; + uint8_t bcc1 = uid.uidByte[3] ^ uid.uidByte[4] ^ uid.uidByte[5] ^ uid.uidByte[6]; + uint8_t page0[4] = {uid.uidByte[0], uid.uidByte[1], uid.uidByte[2], bcc0}; + uint8_t page1[4] = {uid.uidByte[3], uid.uidByte[4], uid.uidByte[5], uid.uidByte[6]}; + uint8_t page2[4] = {bcc1, 0x00, 0x00, 0x00}; + if (_writeT2TPage(0, page0) && _writeT2TPage(1, page1) && _writeT2TPage(2, page2)) { + ST25R_LOG("_writeMagicGen2UID: 7-byte UID written to pages 0-2"); + return true; + } + } else if (uid.size == 4) { + uint8_t bcc = uid.uidByte[0] ^ uid.uidByte[1] ^ uid.uidByte[2] ^ uid.uidByte[3]; + uint8_t page0[4] = {uid.uidByte[0], uid.uidByte[1], uid.uidByte[2], uid.uidByte[3]}; + uint8_t page1[4] = {bcc, uid.sak, uid.atqaByte[1], uid.atqaByte[0]}; + if (_writeT2TPage(0, page0) && _writeT2TPage(1, page1)) { + ST25R_LOG("_writeMagicGen2UID: 4-byte UID written to pages 0-1"); + return true; + } + } + return false; +} + +bool ST25R3916::_writeMagicGen1UID(rfalNfcDevice *dev) { + // Magic Gen1 backdoor: send 0x40 (7 bits, no CRC, no parity), then 0x43 + uint8_t cmd40 = 0x40; + uint8_t rxBuf[4] = {0}; + uint16_t actLen = 0; + + rfalTransceiveContext ctx; + ctx.txBuf = &cmd40; + ctx.txBufLen = 7; + ctx.rxBuf = rxBuf; + ctx.rxBufLen = 32; + ctx.rxRcvdLen = &actLen; + ctx.flags = (uint32_t)RFAL_TXRX_FLAGS_CRC_TX_MANUAL | (uint32_t)RFAL_TXRX_FLAGS_PAR_TX_NONE | + (uint32_t)RFAL_TXRX_FLAGS_CRC_RX_KEEP | (uint32_t)RFAL_TXRX_FLAGS_PAR_RX_KEEP; + ctx.fwt = rfalConv64fcTo1fc(5000); + + if (_hw->rfalStartTransceive(&ctx) != ST_ERR_NONE) return false; + uint32_t t0 = millis(); + while (millis() - t0 < 50) { + _hw->rfalWorker(); + if (_hw->rfalGetTransceiveState() == RFAL_TXRX_STATE_IDLE) break; + } + if (_hw->rfalGetTransceiveStatus() != ST_ERR_NONE || rxBuf[0] != 0x0A) { + ST25R_LOG("_writeMagicGen1UID: 0x40 no ACK (got 0x%02X)", rxBuf[0]); + return false; + } + + uint8_t cmd43 = 0x43; + memset(rxBuf, 0, sizeof(rxBuf)); + actLen = 0; + ctx.txBuf = &cmd43; + ctx.txBufLen = 8; + ctx.rxBuf = rxBuf; + ctx.rxBufLen = 32; + ctx.rxRcvdLen = &actLen; + ctx.flags = (uint32_t)RFAL_TXRX_FLAGS_CRC_TX_MANUAL | (uint32_t)RFAL_TXRX_FLAGS_PAR_TX_NONE | + (uint32_t)RFAL_TXRX_FLAGS_CRC_RX_KEEP | (uint32_t)RFAL_TXRX_FLAGS_PAR_RX_KEEP; + ctx.fwt = rfalConv64fcTo1fc(5000); + + if (_hw->rfalStartTransceive(&ctx) != ST_ERR_NONE) return false; + t0 = millis(); + while (millis() - t0 < 50) { + _hw->rfalWorker(); + if (_hw->rfalGetTransceiveState() == RFAL_TXRX_STATE_IDLE) break; + } + if (_hw->rfalGetTransceiveStatus() != ST_ERR_NONE || rxBuf[0] != 0x0A) { + ST25R_LOG("_writeMagicGen1UID: 0x43 no ACK (got 0x%02X)", rxBuf[0]); + return false; + } + + // Build block 0: UID (4 bytes) + BCC + SAK + ATQA (2) + filler (8) + uint8_t block0[16] = {0}; + uint8_t bcc = 0; + int uidLen = uid.size < 4 ? uid.size : 4; + for (int i = 0; i < uidLen; i++) { + block0[i] = uid.uidByte[i]; + bcc ^= uid.uidByte[i]; + } + block0[4] = bcc; + block0[5] = uid.sak; + block0[6] = uid.atqaByte[1]; + block0[7] = uid.atqaByte[0]; + + // Send MIFARE WRITE block 0 command (with CRC) + uint8_t writeCmd[2] = {0xA0, 0x00}; + memset(rxBuf, 0, sizeof(rxBuf)); + actLen = 0; + auto err = _hw->rfalTransceiveBlockingTxRx( + writeCmd, sizeof(writeCmd), rxBuf, sizeof(rxBuf), &actLen, RFAL_TXRX_FLAGS_DEFAULT, RFAL_FWT_NONE + ); + if (err != ST_ERR_NONE || rxBuf[0] != 0x0A) { + ST25R_LOG("_writeMagicGen1UID: WRITE cmd no ACK err=%d", (int)err); + return false; + } + + // Send block data (with CRC) + memset(rxBuf, 0, sizeof(rxBuf)); + actLen = 0; + err = _hw->rfalTransceiveBlockingTxRx( + block0, sizeof(block0), rxBuf, sizeof(rxBuf), &actLen, RFAL_TXRX_FLAGS_DEFAULT, RFAL_FWT_NONE + ); + if (err != ST_ERR_NONE) { + ST25R_LOG("_writeMagicGen1UID: data send failed err=%d", (int)err); + return false; + } + + ST25R_LOG("_writeMagicGen1UID: block 0 written successfully"); + return true; +} + +int ST25R3916::write(int cardBaudRate) { + if (!_nfc) return FAILURE; + _deselectSharedSpiDevices(); + + rfalNfcDevice *dev; + if (!_pollForTag(&dev)) return TAG_NOT_PRESENT; + + if (dev->type != RFAL_NFC_LISTEN_TYPE_NFCA) { + _nfc->rfalNfcDeactivate(false); + return TAG_NOT_MATCH; + } + + // MIFARE Classic: write the loaded dump back into a genuine MIFARE Classic card. + if (mfcLoaded && isMifareClassicSak(dev->dev.nfca.selRes.sak)) { + _parseDevice(dev); + return _writeMifareClassic(dev); + } + + bool storedIsUltralight = printableUID.picc_type.indexOf("Ultralight") >= 0; + bool presentIsUltralight = (dev->dev.nfca.selRes.sak == 0x00); + + if (!storedIsUltralight || !presentIsUltralight) { + _nfc->rfalNfcDeactivate(false); + return TAG_NOT_MATCH; + } + + return _writeUltralight(dev); +} + +int ST25R3916::clone() { + if (!_nfc) return FAILURE; + _deselectSharedSpiDevices(); + + rfalNfcDevice *dev = nullptr; + if (!_pollForTag(&dev)) return TAG_NOT_PRESENT; + + if (dev->type != RFAL_NFC_LISTEN_TYPE_NFCA) { + _nfc->rfalNfcDeactivate(false); + return TAG_NOT_MATCH; + } + + // MIFARE Classic dump → clone full content into a Magic Gen1 card. + if (mfcLoaded) { + int r = _writeMifareClassicMagic(dev); + _nfc->rfalNfcDeactivate(false); + return r; + } + + if (_writeMagicGen2UID(dev)) { + _nfc->rfalNfcDeactivate(false); + return SUCCESS; + } + + _nfc->rfalNfcDeactivate(false); + delay(50); + + dev = nullptr; + if (!_pollForTag(&dev)) return TAG_NOT_PRESENT; + + if (dev->type != RFAL_NFC_LISTEN_TYPE_NFCA) { + _nfc->rfalNfcDeactivate(false); + return TAG_NOT_MATCH; + } + + if (_writeMagicGen1UID(dev)) { + _nfc->rfalNfcDeactivate(false); + return SUCCESS; + } + + _nfc->rfalNfcDeactivate(false); + return FAILURE; +} + +int ST25R3916::erase() { + if (!_nfc) return FAILURE; + _deselectSharedSpiDevices(); + + rfalNfcDevice *dev; + if (!_pollForTag(&dev)) return TAG_NOT_PRESENT; + + if (dev->type == RFAL_NFC_LISTEN_TYPE_NFCA && dev->dev.nfca.selRes.sak == 0x00) { + return _eraseUltralight(dev); + } + + _nfc->rfalNfcDeactivate(false); + return NOT_IMPLEMENTED; +} + +int ST25R3916::write_ndef() { + if (!_nfc) return FAILURE; + _deselectSharedSpiDevices(); + + rfalNfcDevice *dev; + if (!_pollForTag(&dev)) return TAG_NOT_PRESENT; + + if (dev->type != RFAL_NFC_LISTEN_TYPE_NFCA || dev->dev.nfca.selRes.sak != 0x00) { + _nfc->rfalNfcDeactivate(false); + return TAG_NOT_MATCH; + } + + if (totalPages <= 0) { + _parseDevice(dev); + if (_readDataBlocks(dev) != SUCCESS) { + _nfc->rfalNfcDeactivate(false); + return FAILURE; + } + } + + int result = _writeNdefBlocks(dev); + _nfc->rfalNfcDeactivate(false); + return result; +} + +// --------------------------------------------------------------------------- +// Emulação de tag NFC-A (modo passive target / listen) +// +// O wrapper RFAL de alto nível (rfalListenStart) é stub (ST_ERR_NOTSUPP) neste +// fork. Porém o ST25R3916 faz a anti-colisão NFC-A em hardware no modo +// "Passive Target" (MODE.targ + om_targ_nfca): basta carregar UID/ATQA/SAK na +// PT Memory e o chip responde sozinho SENS_RES/anticolisão/SELECT. A sequência +// de registradores abaixo segue o furi_hal_nfc do Flipper (mesmo CI). +// --------------------------------------------------------------------------- + +// Converte strAllPages ("Page N: XX XX XX XX") em _emuPages. Retorna nº de páginas. +int ST25R3916::_buildEmuPages() { + _emuPageCount = 0; + memset(_emuPages, 0, sizeof(_emuPages)); + int pos = 0; + while (pos < strAllPages.length() && _emuPageCount < 256) { + int nl = strAllPages.indexOf('\n', pos); + if (nl < 0) nl = strAllPages.length(); + String line = strAllPages.substring(pos, nl); + line.trim(); + pos = nl + 1; + if (!line.startsWith("Page ")) continue; + int colon = line.indexOf(':'); + if (colon < 0) continue; + int page = line.substring(5, colon).toInt(); + if (page < 0 || page >= 256) continue; + std::vector bytes; + if (!st25ParseHexBytesAfterColon(line, bytes) || bytes.size() < 4) continue; + memcpy(_emuPages[page], bytes.data(), 4); + if (page + 1 > _emuPageCount) _emuPageCount = page + 1; + } + return _emuPageCount; +} + +bool ST25R3916::_setupListenMode(const uint8_t *uidBuf, uint8_t uidLen, const uint8_t *atqa, uint8_t sak) { + _deselectSharedSpiDevices(); + _hw->st25r3916OscOn(); + + // Configura modo + front-end analógico para listen NFC-A (caminho testado do fork). + auto mres = _hw->rfalSetMode(RFAL_MODE_LISTEN_NFCA, RFAL_BR_106, RFAL_BR_106); + if (mres != ST_ERR_NONE) { + ST25R_LOG("emulate: rfalSetMode(LISTEN_NFCA) -> %d", (int)mres); + return false; + } + + // Passive target: receptor ligado, detector de campo externo automático (sem TX próprio). + _hw->st25r3916WriteRegister( + ST25R3916_REG_OP_CONTROL, + ST25R3916_REG_OP_CONTROL_en | ST25R3916_REG_OP_CONTROL_rx_en | ST25R3916_REG_OP_CONTROL_en_fd_auto_efd + ); + _hw->st25r3916WriteRegister(ST25R3916_REG_MODE, ST25R3916_REG_MODE_targ_targ | ST25R3916_REG_MODE_om0); + _hw->st25r3916WriteRegister( + ST25R3916_REG_PASSIVE_TARGET, + ST25R3916_REG_PASSIVE_TARGET_fdel_2 | ST25R3916_REG_PASSIVE_TARGET_fdel_0 | + ST25R3916_REG_PASSIVE_TARGET_d_ac_ap2p | ST25R3916_REG_PASSIVE_TARGET_d_212_424_1r + ); + _hw->st25r3916WriteRegister(ST25R3916_REG_MASK_RX_TIMER, 0x02); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_STOP); + + // Habilita as interrupções relevantes ao modo target. + uint32_t interrupts = ST25R3916_IRQ_MASK_FWL | ST25R3916_IRQ_MASK_TXE | ST25R3916_IRQ_MASK_RXS | + ST25R3916_IRQ_MASK_RXE | ST25R3916_IRQ_MASK_PAR | ST25R3916_IRQ_MASK_CRC | + ST25R3916_IRQ_MASK_ERR1 | ST25R3916_IRQ_MASK_ERR2 | ST25R3916_IRQ_MASK_NRE | + ST25R3916_IRQ_MASK_EON | ST25R3916_IRQ_MASK_EOF | ST25R3916_IRQ_MASK_WU_A_X | + ST25R3916_IRQ_MASK_WU_A; + _hw->st25r3916ClearInterrupts(); + _hw->st25r3916DisableInterrupts(ST25R3916_IRQ_MASK_ALL); + _hw->st25r3916EnableInterrupts(interrupts); + + // UID de 4 ou 7 bytes. + _hw->st25r3916ChangeRegisterBits( + ST25R3916_REG_AUX, + ST25R3916_REG_AUX_nfc_id_mask, + (uidLen == 4) ? ST25R3916_REG_AUX_nfc_id_4bytes : ST25R3916_REG_AUX_nfc_id_7bytes + ); + + // PT Memory A (15 bytes): UID[0..len], ATQA em [10..11], SAK em [12..14]. + uint8_t pt[ST25R3916_PTM_A_LEN] = {0}; + memcpy(pt, uidBuf, uidLen); + pt[10] = atqa[0]; + pt[11] = atqa[1]; + pt[12] = (uidLen == 4) ? (uint8_t)(sak & ~0x04) : 0x04; // cascade level 1 + pt[13] = (uint8_t)(sak & ~0x04); + pt[14] = (uint8_t)(sak & ~0x04); + _hw->st25r3916WritePTMem(pt, sizeof(pt)); + + // Habilita anti-colisão automática (bit limpo) e entra no estado Sense. + _hw->st25r3916ClrRegisterBits(ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_GOTO_SENSE); + return true; +} + +void ST25R3916::_listenStop() { + if (!_hw) return; + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_STOP); + _hw->st25r3916DisableInterrupts(ST25R3916_IRQ_MASK_ALL); + _hw->st25r3916ClearInterrupts(); + // Volta ao modo poller padrão para que rfid read funcione em seguida. + _hw->rfalSetMode(RFAL_MODE_POLL_NFCA, RFAL_BR_106, RFAL_BR_106); + _hw->rfalFieldOff(); + _discoveryStarted = false; +} + +// Transmite uma resposta (com CRC apêndice automático) via FIFO no modo target. +bool ST25R3916::_listenRespond(const uint8_t *resp, uint16_t len) { + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_CLEAR_FIFO); + _hw->st25r3916WriteFifo(resp, len); + _hw->st25r3916SetNumTxBits((uint16_t)(len * 8U)); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_TRANSMIT_WITH_CRC); + uint32_t irqs = _hw->st25r3916WaitForInterruptsTimed(ST25R3916_IRQ_MASK_TXE, 20); + return (irqs & ST25R3916_IRQ_MASK_TXE) != 0U; +} + +// Loop principal: o hardware responde anti-colisão/SELECT sozinho; aqui apenas +// detectamos seleções (WU_A) e respondemos comandos T2T (READ/FAST_READ/GET_VERSION). +int ST25R3916::_handleListenLoop(uint32_t timeoutMs) { + uint32_t deadline = millis() + timeoutMs; + int readers = 0; + uint32_t nRead = 0; + bool active = false; + uint8_t fifo[64]; + // Trace diferido: registra a sequência de comandos e imprime só quando o leitor + // sai do campo, p/ não atrasar o caminho RX->resposta (timing do Switch é estrito). + uint8_t traceCmd[96]; + uint8_t traceArg[96]; + uint16_t traceN = 0; + LongPress = true; + while ((int32_t)(deadline - millis()) > 0) { + // WaitForInterruptsTimed faz busy-wait sem ceder CPU; este yield dá tempo + // ao task de input (xHandle) para setar EscPress, senão não dá p/ sair. + vTaskDelay(pdMS_TO_TICKS(1)); // time to pull EscPress from other tasks + if (check(EscPress)) { + ST25R_LOG("emulate: Esc — encerrando"); + break; + } + + uint32_t irqs = _hw->st25r3916WaitForInterruptsTimed( + ST25R3916_IRQ_MASK_WU_A | ST25R3916_IRQ_MASK_WU_A_X | ST25R3916_IRQ_MASK_RXE | + ST25R3916_IRQ_MASK_EOF, + 50 + ); + if (irqs == 0U) continue; + + if ((irqs & (ST25R3916_IRQ_MASK_WU_A | ST25R3916_IRQ_MASK_WU_A_X)) != 0U) { + if (!active) { + readers++; + traceN = 0; + ST25R_LOG("emulate: reader selecionou a tag (UID enviado) #%d", readers); + } + active = true; + // Passa a tratar dados manualmente (desabilita auto-AC). + _hw->st25r3916SetRegisterBits( + ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a + ); + } + + if ((irqs & ST25R3916_IRQ_MASK_RXE) != 0U) { + uint16_t n = _hw->st25r3916GetNumFIFOBytes(); + if (_emuIsMfc || _emuIsT4T) { + // MIFARE Classic (Crypto1) or Type 4 Tag (ISO-DEP/NDEF). + if (n > 0U && n <= sizeof(fifo)) { + _hw->st25r3916ReadFifo(fifo, n); + if (traceN < sizeof(traceCmd)) { + traceCmd[traceN] = fifo[0]; + traceArg[traceN] = (n > 1) ? fifo[1] : 0; + traceN++; + } + if (fifo[0] == 0x30) nRead++; + if (_emuIsMfc) _emuMfcHandle(fifo, n); + else _emuT4THandle(fifo, n); + } + continue; + } + if (n > 0U && n <= sizeof(fifo)) { + _hw->st25r3916ReadFifo(fifo, n); + uint8_t cmd = fifo[0]; + // Registra no trace (impressão diferida p/ não atrasar a resposta). + if (cmd == 0x30 || cmd == 0x3A) nRead++; + if (traceN < sizeof(traceCmd)) { + traceCmd[traceN] = cmd; + traceArg[traceN] = (n > 1) ? fifo[1] : 0; + traceN++; + } + if (cmd == 0x30 && n >= 2) { // READ: 16 bytes (4 páginas a partir de addr) + uint8_t addr = fifo[1]; + uint8_t resp[16]; + for (int i = 0; i < 4; i++) { + int p = (_emuPageCount > 0) ? ((addr + i) % _emuPageCount) : (addr + i); + memcpy(&resp[i * 4], _emuPages[p & 0xFF], 4); + } + _listenRespond(resp, sizeof(resp)); + } else if (cmd == 0x3A && n >= 3) { // FAST_READ start..end (até 64 páginas/256 B) + uint8_t start = fifo[1], end = fifo[2]; + if (end >= start && (uint16_t)((end - start + 1) * 4) <= 256) { + uint8_t resp[256]; + uint16_t rl = 0; + for (int p = start; p <= end; p++) { + int pp = (_emuPageCount > 0) ? (p % _emuPageCount) : p; + memcpy(&resp[rl], _emuPages[pp & 0xFF], 4); + rl += 4; + } + _listenRespond(resp, rl); + } + } else if (cmd == 0x60) { // GET_VERSION (necessário p/ NTAG21x e amiibo) + uint8_t ver[8]; + if (ntagHasVersion) { + memcpy(ver, ntagVersion, 8); + } else { + // Default NTAG21x conforme nº de páginas (0F=213, 11=215, 13=216). + uint8_t storage = 0x11; // NTAG215 (caso típico amiibo) + if (_emuPageCount > 0 && _emuPageCount <= 45) storage = 0x0F; + else if (_emuPageCount > 135) storage = 0x13; + uint8_t def[8] = {0x00, 0x04, 0x04, 0x02, 0x01, 0x00, storage, 0x03}; + memcpy(ver, def, 8); + } + _listenRespond(ver, 8); + } else if (cmd == 0x3C && n >= 2) { // READ_SIG: assinatura ECC (amiibo/Switch) + uint8_t sig[32]; + if (ntagHasSignature) memcpy(sig, ntagSignature, 32); + else memset(sig, 0, 32); + _listenRespond(sig, 32); + } else if (cmd == 0x39 && n >= 2) { // READ_CNT: contador de 24 bits + uint8_t idx = fifo[1]; + uint8_t cnt[3] = {0, 0, 0}; + if (idx < 3 && ntagHasCounters) { + cnt[0] = (uint8_t)(ntagCounters[idx] & 0xFF); + cnt[1] = (uint8_t)((ntagCounters[idx] >> 8) & 0xFF); + cnt[2] = (uint8_t)((ntagCounters[idx] >> 16) & 0xFF); + } + _listenRespond(cnt, 3); + } else if (cmd == 0x1B && n >= 5) { // PWD_AUTH: responde PACK + uint8_t pack[2] = {0, 0}; + if (_emuPageCount > 0) { + pack[0] = _emuPages[_emuPageCount - 1][0]; + pack[1] = _emuPages[_emuPageCount - 1][1]; + } + // Amiibo (NTAG215): a senha deriva do UID e o PACK correto é 80 80. + // Na tag genuína a página PACK é lida como 00 00 (protegida), então + // sem isto o Switch recebe PACK errado e recusa antes de ler os dados. + if (uid.size == 7) { + uint8_t apwd[4] = { + (uint8_t)(0xAA ^ uid.uidByte[1] ^ uid.uidByte[3]), + (uint8_t)(0x55 ^ uid.uidByte[2] ^ uid.uidByte[4]), + (uint8_t)(0xAA ^ uid.uidByte[3] ^ uid.uidByte[5]), + (uint8_t)(0x55 ^ uid.uidByte[4] ^ uid.uidByte[6]), + }; + if (fifo[1] == apwd[0] && fifo[2] == apwd[1] && fifo[3] == apwd[2] && + fifo[4] == apwd[3]) { + pack[0] = 0x80; + pack[1] = 0x80; + } + } + _listenRespond(pack, 2); + } else if (cmd == 0x50) { // HALT + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_GOTO_SLEEP); + } else if (cmd == 0xA2 && n >= 6) { // WRITE: aceita e guarda, ACK + uint8_t addr = fifo[1]; + if (addr < 256) { + memcpy(_emuPages[addr], &fifo[2], 4); + if (addr + 1 > _emuPageCount) _emuPageCount = addr + 1; + } + uint8_t ack = 0x0A; // ACK NTAG (4 bits, sem CRC) + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_CLEAR_FIFO); + _hw->st25r3916WriteFifo(&ack, 1); + _hw->st25r3916SetNumTxBits(4); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_TRANSMIT_WITHOUT_CRC); + } + } + } + + if ((irqs & ST25R3916_IRQ_MASK_EOF) != 0U) { + // Campo do reader desligou: imprime o trace acumulado e rearma. + if (active) { + String tr; + for (uint16_t i = 0; i < traceN; i++) { + char b[8]; + if (traceCmd[i] == 0x30 || traceCmd[i] == 0x3A) + sprintf(b, "%02X:%02X ", traceCmd[i], traceArg[i]); + else sprintf(b, "%02X ", traceCmd[i]); + tr += b; + } + ST25R_LOG( + "emulate: reader saiu (cmds=%u reads=%lu): %s", traceN, (unsigned long)nRead, tr.c_str() + ); + if (_emuIsMfc) { + ST25R_LOG( + "emu mfc: authReq=%u authOk=%u badAr=%u noNr=%u reads=%u lastNrBits=%u", + _emuMfcAuthReq, + _emuMfcAuthOk, + _emuMfcBadAr, + _emuMfcNoNr, + _emuMfcReads, + _emuMfcLastNrBits + ); + ST25R_LOG( + "emu mfc dbg: blk=%u nt=%08lX enc=%02X%02X%02X%02X|%02X%02X%02X%02X arCalc=%08lX " + "arExp=%08lX", + _emuDbgBlock, + (unsigned long)_emuDbgNt, + _emuDbgEnc[0], + _emuDbgEnc[1], + _emuDbgEnc[2], + _emuDbgEnc[3], + _emuDbgEnc[4], + _emuDbgEnc[5], + _emuDbgEnc[6], + _emuDbgEnc[7], + (unsigned long)_emuDbgArCalc, + (unsigned long)_emuDbgArExp + ); + } + } + active = false; + _hw->st25r3916ClrRegisterBits( + ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a + ); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_GOTO_SENSE); + } + } + LongPress = false; + return readers; +} + +int ST25R3916::emulate() { + if (!_hw || !_nfc) return FAILURE; + + // Milestone 6 — choose the emulation technology (forced via CLI or detected). + bool wantFelica = (emuMode == "felica") || printableUID.picc_type.startsWith("FeliCa"); + if (wantFelica) return _emulateFelica(); + _emuIsT4T = (emuMode == "t4t") || printableUID.picc_type == "ISO14443-4" || + printableUID.picc_type.indexOf("ISO14443-4") >= 0; + + // Obtém UID: do struct (após read) ou do printableUID (após load). + uint8_t uidBuf[7] = {0}; + uint8_t uidLen = 0; + if (uid.size == 4 || uid.size == 7) { + uidLen = uid.size; + memcpy(uidBuf, uid.uidByte, uidLen); + } else if (printableUID.uid.length() > 0) { + std::vector ub; + int hi = -1; + for (unsigned i = 0; i < printableUID.uid.length(); i++) { + int v = st25HexNibble(printableUID.uid.charAt(i)); + if (v < 0) continue; + if (hi < 0) hi = v; + else { + ub.push_back((uint8_t)((hi << 4) | v)); + hi = -1; + } + } + if (ub.size() == 4 || ub.size() == 7) { + uidLen = ub.size(); + memcpy(uidBuf, ub.data(), uidLen); + } + } + if (uidLen != 4 && uidLen != 7) { + if (_emuIsT4T) { + // T4T NDEF without a captured tag: synthesize a random 4-byte UID. + uidLen = 4; + uidBuf[0] = 0x08; // RID (random UID) marker + uint32_t r = esp_random(); + uidBuf[1] = (uint8_t)r; + uidBuf[2] = (uint8_t)(r >> 8); + uidBuf[3] = (uint8_t)(r >> 16); + } else { + ST25R_LOG("emulate: UID inválido/ausente — faça 'rfid read' ou 'rfid loadfile' antes"); + return FAILURE; + } + } + + uint8_t atqa[2]; + atqa[0] = uid.atqaByte[0] ? uid.atqaByte[0] : (uidLen == 7 ? 0x44 : 0x04); + atqa[1] = uid.atqaByte[1]; + uint8_t sak = uid.sak; // 0x00 = NTAG/Ultralight (T2T) + + // MIFARE Classic? (SAK known or stored type string). Build the block/key dump. + _emuIsMfc = + !_emuIsT4T && (isMifareClassicSak(sak) || printableUID.picc_type.indexOf("MIFARE Classic") >= 0 || + printableUID.picc_type.indexOf("Mifare Classic") >= 0); + if (_emuIsT4T) { + _buildT4TFiles(); + sak = 0x20; // ISO14443-4 (ISO-DEP / Type 4 Tag) + if (atqa[0] == 0 && atqa[1] == 0) atqa[0] = 0x04; + _t4tSelected = 0; + ST25R_LOG("emulate: Type 4 Tag (NDEF) ndefLen=%u", _t4tNdefLen); + } else if (_emuIsMfc) { + if (!_buildEmuMfc()) { + ST25R_LOG("emulate: MFC dump vazio — faça 'rfid read'/'loadfile' antes"); + return FAILURE; + } + if (sak == 0x00) sak = (mfcDump.totalBlocks == 256) ? 0x18 : 0x08; + if (atqa[0] == 0 && atqa[1] == 0) atqa[0] = 0x04; + _emuMfcAuthed = false; + _emuMfcAuthReq = _emuMfcAuthOk = _emuMfcBadAr = _emuMfcNoNr = _emuMfcReads = _emuMfcLastNrBits = 0; + } else { + _buildEmuPages(); + } + + // Garante que nenhum discovery/poller esteja ativo antes de virar target. + stopDiscovery(); + _nfc->rfalNfcDeactivate(false); + _discoveryStarted = false; + + if (!_setupListenMode(uidBuf, uidLen, atqa, sak)) { + _listenStop(); + return FAILURE; + } + + ST25R_LOG( + "emulate: emulando UID=%s ATQA=%02X%02X SAK=%02X pages=%d ver=%d sig=%d (timeout 30s, Esc p/ sair)", + printableUID.uid.c_str(), + atqa[1], + atqa[0], + sak, + _emuPageCount, + ntagHasVersion ? 1 : 0, + ntagHasSignature ? 1 : 0 + ); + + int readers = _handleListenLoop(30000); + + _listenStop(); + ST25R_LOG("emulate: encerrado — readers detectados=%d", readers); + return SUCCESS; +} + +int ST25R3916::load() { + // GUI path: pick a file, then delegate to the shared parser so the serial + // command (rfid loadfile) and the GUI behave identically. + FS *fs; + if (!getFsStorage(fs)) return FAILURE; + + String filepath = loopSD(*fs, true, "RFID|NFC", "/BruceRFID"); + if (filepath.length() == 0) return FAILURE; + + return loadFromFile(filepath); +} + +int ST25R3916::loadFromFile(const String &filepath) { + FS *fs; + if (!getFsStorage(fs)) return FAILURE; + + File file = fs->open(filepath, FILE_READ); + if (!file) return FAILURE; + + String line; + String strData; + strAllPages = ""; + totalPages = 0; + dataPages = 0; + pageReadSuccess = true; + pageReadStatus = SUCCESS; + ntagHasVersion = false; + ntagHasSignature = false; + ntagHasCounters = false; + + while (file.available()) { + line = file.readStringUntil('\n'); + line.trim(); + strData = line.substring(line.indexOf(":") + 1); + strData.trim(); + + if (line.startsWith("Device type:")) printableUID.picc_type = strData; + else if (line.startsWith("UID:")) printableUID.uid = strData; + else if (line.startsWith("SAK:")) printableUID.sak = strData; + else if (line.startsWith("ATQA:")) printableUID.atqa = strData; + else if (line.startsWith("Pages total:")) totalPages = strData.toInt(); + else if (line.startsWith("Pages read:")) pageReadSuccess = false; + // NTAG/Ultralight: restaura versão/assinatura/contadores p/ emulação fiel. + else if (line.startsWith("Mifare version:")) { + std::vector b; + if (st25ParseHexBytesAfterColon(line, b) && b.size() >= 8) { + memcpy(ntagVersion, b.data(), 8); + ntagHasVersion = true; + } + } else if (line.startsWith("Signature:")) { + std::vector b; + if (st25ParseHexBytesAfterColon(line, b) && b.size() >= 32) { + memcpy(ntagSignature, b.data(), 32); + ntagHasSignature = true; + } + } else if (line.startsWith("Counter ")) { + int idx = line.substring(8, line.indexOf(":")).toInt(); + if (idx >= 0 && idx < 3) { + ntagCounters[idx] = (uint32_t)strData.toInt(); + ntagHasCounters = true; + } + } else if (line.startsWith("Page ")) { + strAllPages += line + "\n"; + dataPages++; + } + // MIFARE Classic dumps (.rfid/.nfc): keep Block and Key lines so the + // driver can rebuild the dump for clone/emulate (mesma lógica do loadfile serial). + else if (line.startsWith("Block ")) { + strAllPages += line + "\n"; + dataPages++; + } else if (line.startsWith("Key A sector ") || line.startsWith("Key B sector ")) { + strAllPages += line + "\n"; + } + } + + file.close(); + + if (totalPages == 0) totalPages = dataPages; + if (printableUID.uid.length() == 0) return FAILURE; + + _parseLoadedData(); + return SUCCESS; +} + +String ST25R3916::_getNtagVariant() { + ntagHasVersion = false; + memset(ntagVersion, 0, sizeof(ntagVersion)); + + uint8_t cmd = 0x60; + uint8_t rx[10] = {0}; + uint16_t rxLen = 0; + auto err = _hw->rfalTransceiveBlockingTxRx( + &cmd, 1, rx, sizeof(rx), &rxLen, RFAL_TXRX_FLAGS_DEFAULT, rfalConvMsTo1fc(20) + ); + if (err != ST_ERR_NONE || rxLen < 8) { + ST25R_LOG("_getNtagVariant: GET_VERSION failed err=%d len=%u", (int)err, rxLen); + return ""; + } + memcpy(ntagVersion, rx, 8); + ntagHasVersion = true; + ST25R_LOG( + "version=%02X %02X %02X %02X %02X %02X %02X %02X", + rx[0], + rx[1], + rx[2], + rx[3], + rx[4], + rx[5], + rx[6], + rx[7] + ); + + uint8_t productType = rx[2]; // 0x03 = Ultralight, 0x04 = NTAG + uint8_t storage = rx[6]; // storage size code + if (productType == 0x04) { + switch (storage) { + case 0x0F: _ntagPagesHint = 45; return "NTAG213"; + case 0x11: _ntagPagesHint = 135; return "NTAG215"; + case 0x13: _ntagPagesHint = 231; return "NTAG216"; + default: return "NTAG21x"; + } + } + if (productType == 0x03) { + switch (storage) { + case 0x0B: _ntagPagesHint = 20; return "MF Ultralight EV1 (UL11)"; + case 0x0E: _ntagPagesHint = 41; return "MF Ultralight EV1 (UL21)"; + default: return "MF Ultralight EV1"; + } + } + return ""; +} + +bool ST25R3916::_readNtagSignature() { + ntagHasSignature = false; + memset(ntagSignature, 0, sizeof(ntagSignature)); + + uint8_t cmd[2] = {0x3C, 0x00}; + uint8_t rx[40] = {0}; + uint16_t rxLen = 0; + auto err = _hw->rfalTransceiveBlockingTxRx( + cmd, sizeof(cmd), rx, sizeof(rx), &rxLen, RFAL_TXRX_FLAGS_DEFAULT, rfalConvMsTo1fc(20) + ); + if (err != ST_ERR_NONE || rxLen < 32) { + ST25R_LOG("_readNtagSignature: READ_SIG failed err=%d len=%u", (int)err, rxLen); + return false; + } + memcpy(ntagSignature, rx, 32); + ntagHasSignature = true; + ST25R_LOG("signature=%02X %02X %02X %02X...%02X %02X", rx[0], rx[1], rx[2], rx[3], rx[30], rx[31]); + return true; +} + +bool ST25R3916::_readNtagCounters() { + ntagHasCounters = false; + bool any = false; + for (uint8_t idx = 0; idx < 3; idx++) { + ntagCounters[idx] = 0; + ntagTearing[idx] = 0xBD; + + uint8_t cmd[2] = {0x39, idx}; + uint8_t rx[4] = {0}; + uint16_t rxLen = 0; + auto err = _hw->rfalTransceiveBlockingTxRx( + cmd, sizeof(cmd), rx, sizeof(rx), &rxLen, RFAL_TXRX_FLAGS_DEFAULT, rfalConvMsTo1fc(20) + ); + if (err == ST_ERR_NONE && rxLen >= 3) { + ntagCounters[idx] = (uint32_t)rx[0] | ((uint32_t)rx[1] << 8) | ((uint32_t)rx[2] << 16); + any = true; + ST25R_LOG("counter[%u]=%lu", idx, (unsigned long)ntagCounters[idx]); + } + } + ntagHasCounters = any; + return any; +} + +static String _bytesToHex(const uint8_t *data, size_t len); + +int ST25R3916::_readNfcV(rfalNfcDevice *dev) { + strAllPages = ""; + dataPages = 0; + totalPages = 0; + + const uint8_t *nfcvUid = dev->nfcid; + uint8_t flags = RFAL_NFCV_REQ_FLAG_DEFAULT; + + uint8_t blockCount = 0; + uint8_t blockSize = 4; + { + uint8_t sysInfo[32] = {0}; + uint16_t rcvLen = 0; + auto err = + _nfc->rfalNfcvPollerGetSystemInformation(flags, nfcvUid, sysInfo, sizeof(sysInfo), &rcvLen); + if (err == ST_ERR_NONE && rcvLen >= 10) { + // Layout ISO15693: RES_FLAG(1) InfoFlags(1) UID(8) [DSFID] [AFI] [MemSize(2)] [ICref] + uint8_t infoFlags = sysInfo[1]; + size_t idx = 10; + if (infoFlags & 0x01) idx += 1; // DSFID presente + if (infoFlags & 0x02) idx += 1; // AFI presente + if ((infoFlags & 0x04) && (idx + 1 < rcvLen)) { + blockCount = sysInfo[idx] + 1; // nº de blocos (valor+1) + blockSize = (sysInfo[idx + 1] & 0x1F) + 1; // tamanho do bloco (valor+1) + } + ST25R_LOG("NFC-V SysInfo flags=0x%02X blocks=%u size=%u", infoFlags, blockCount, blockSize); + } else { + ST25R_LOG("NFC-V GetSystemInformation err=%d len=%u (lendo até falhar)", (int)err, rcvLen); + } + } + + if (blockSize == 0 || blockSize > 8) blockSize = 4; + uint8_t maxBlocks = (blockCount > 0) ? blockCount : 255; + + for (uint16_t blk = 0; blk < maxBlocks; blk++) { + uint8_t rxBuf[16] = {0}; + uint16_t rxLen = 0; + auto err = + _nfc->rfalNfcvPollerReadSingleBlock(flags, nfcvUid, (uint8_t)blk, rxBuf, sizeof(rxBuf), &rxLen); + if (err != ST_ERR_NONE || rxLen < (uint16_t)(blockSize + 1)) { + if (blockCount == 0) break; // tamanho desconhecido: parar no 1º erro + // tamanho conhecido: bloco ilegível, registra como zeros e segue + char line[64]; + sprintf(line, "Block %02u: (unreadable err=%d)", blk, (int)err); + strAllPages += String(line) + "\n"; + continue; + } + // rxBuf[0] = RES_FLAG; dados a partir de rxBuf[1] + String line = "Block "; + char idxBuf[8]; + sprintf(idxBuf, "%02u: ", blk); + line += idxBuf; + for (uint8_t b = 0; b < blockSize; b++) { + char hb[4]; + sprintf(hb, "%02X", rxBuf[1 + b]); + line += hb; + if (b < blockSize - 1) line += " "; + } + strAllPages += line + "\n"; + dataPages++; + } + + totalPages = (blockCount > 0) ? blockCount : dataPages; + ST25R_LOG("NFC-V read: %d/%d blocos lidos", dataPages, totalPages); + + if (dataPages == 0) { + pageReadStatus = FAILURE; + return FAILURE; + } + pageReadStatus = SUCCESS; + pageReadSuccess = true; + return SUCCESS; +} + +void ST25R3916::_parseNfcB(rfalNfcDevice *dev) { + rfalNfcbListenDevice *nfcb = &dev->dev.nfcb; + const uint8_t *pupi = nfcb->sensbRes.nfcid0; // 4 bytes (PUPI / pseudo-UID) + + printableUID.picc_type = "ISO14443B"; + printableUID.sak = "--"; + printableUID.atqa = "--"; + + printableUID.uid = ""; + for (int i = 0; i < RFAL_NFCB_NFCID0_LEN; i++) { + char buf[3]; + sprintf(buf, "%02X", pupi[i]); + printableUID.uid += buf; + if (i < RFAL_NFCB_NFCID0_LEN - 1) printableUID.uid += " "; + } + + const uint8_t *appData = (const uint8_t *)&nfcb->sensbRes.appData; + const uint8_t *protInfo = (const uint8_t *)&nfcb->sensbRes.protInfo; + + strAllPages = ""; + strAllPages += "PUPI: " + printableUID.uid + "\n"; + strAllPages += "Application data: " + _bytesToHex(appData, 4) + "\n"; + strAllPages += "Protocol info: " + _bytesToHex(protInfo, 4) + "\n"; + + ST25R_LOG( + "NFC-B PUPI=%s appData=%02X%02X%02X%02X protInfo=%02X%02X%02X%02X", + printableUID.uid.c_str(), + appData[0], + appData[1], + appData[2], + appData[3], + protInfo[0], + protInfo[1], + protInfo[2], + protInfo[3] + ); +} + +int ST25R3916::_readFeliCa(rfalNfcDevice *dev) { + rfalNfcfListenDevice *nfcf = &dev->dev.nfcf; + const uint8_t *idm = nfcf->sensfRes.NFCID2; // 8 bytes (IDm) + const uint8_t *pmm = (const uint8_t *)nfcf->sensfRes.PAD0; // 8 bytes contíguos (PMm) + + printableUID.picc_type = "FeliCa"; + printableUID.sak = "--"; + printableUID.atqa = "--"; + + printableUID.uid = ""; + for (int i = 0; i < RFAL_NFCF_NFCID2_LEN; i++) { + char buf[3]; + sprintf(buf, "%02X", idm[i]); + printableUID.uid += buf; + if (i < RFAL_NFCF_NFCID2_LEN - 1) printableUID.uid += " "; + } + + strAllPages = ""; + strAllPages += "IDm: " + _bytesToHex(idm, 8) + "\n"; + strAllPages += "PMm: " + _bytesToHex(pmm, 8) + "\n"; + + ST25R_LOG("FeliCa IDm=%s PMm=%s", _bytesToHex(idm, 8).c_str(), _bytesToHex(pmm, 8).c_str()); + + rfalNfcfServ service = 0x000B; + rfalNfcfBlockListElem block; + block.conf = 0x80; // 2-byte block list element, acesso normal + block.blockNum = 0; + rfalNfcfServBlockListParam servBlock; + servBlock.numServ = 1; + servBlock.servList = &service; + servBlock.numBlock = 1; + servBlock.blockList = █ + + uint8_t rxBuf[32] = {0}; + uint16_t rcvdLen = 0; + auto err = _nfc->rfalNfcfPollerCheck(idm, &servBlock, rxBuf, sizeof(rxBuf), &rcvdLen); + if (err == ST_ERR_NONE && rcvdLen >= 16) { + // resposta T3T Check: status flags + dados do bloco (16 bytes) + strAllPages += "Block 00: " + _bytesToHex(rxBuf + (rcvdLen - 16), 16) + "\n"; + ST25R_LOG("FeliCa block0 lido (%u bytes)", rcvdLen); + dataPages = 1; + } else { + ST25R_LOG("FeliCa Check err=%d len=%u (serviço pode exigir chave)", (int)err, rcvdLen); + dataPages = 0; + } + + totalPages = dataPages; + pageReadStatus = SUCCESS; + pageReadSuccess = true; + return SUCCESS; +} + +// ============================================================================ +// ISO-DEP / Type 4 Tag (T4T): DESFire, NDEF T4T, EMV +// ============================================================================ + +bool ST25R3916::_isoDepApdu(const uint8_t *tx, uint16_t txLen, uint8_t *rx, uint16_t rxCap, uint16_t *rxLen) { + if (rxLen) *rxLen = 0; + uint8_t *rxData = nullptr; + uint16_t *rcvLen = nullptr; + auto err = + _nfc->rfalNfcDataExchangeStart(const_cast(tx), txLen, &rxData, &rcvLen, RFAL_FWT_NONE); + if (err != ST_ERR_NONE) { + ST25R_LOG("isoDep APDU start err=%d", (int)err); + return false; + } + + uint16_t total = 0; + uint32_t t0 = millis(); + for (;;) { + _nfc->rfalNfcWorker(); + err = _nfc->rfalNfcDataExchangeGetStatus(); + if (err == ST_ERR_BUSY) { + if (millis() - t0 > 2000) { + ST25R_LOG("isoDep APDU timeout"); + return false; + } + continue; + } + // Copia o bloco recebido (NONE = último; AGAIN = chaining, virão mais). + uint16_t n = (rcvLen != nullptr) ? *rcvLen : 0; + if (rxData != nullptr && rx != nullptr && n > 0 && total < rxCap) { + uint16_t cp = (total + n > rxCap) ? (uint16_t)(rxCap - total) : n; + memcpy(rx + total, rxData, cp); + total += cp; + } + if (err == ST_ERR_AGAIN) { + t0 = millis(); + continue; + } + if (err != ST_ERR_NONE) { + ST25R_LOG("isoDep APDU status err=%d", (int)err); + return false; + } + break; + } + if (rxLen) *rxLen = total; + return true; +} + +// Verifica se a resposta termina com Status Word 0x9000. +static bool _swOk(const uint8_t *rx, uint16_t len) { + return (len >= 2 && rx[len - 2] == 0x90 && rx[len - 1] == 0x00); +} + +// NDEF Type 4 Tag: SELECT NDEF App -> SELECT CC -> READ CC -> SELECT NDEF file -> READ. +bool ST25R3916::_readNdefT4T() { + uint8_t rx[256]; + uint16_t rxLen = 0; + + // 1. SELECT NDEF Tag Application (AID D2 76 00 00 85 01 01) + static const uint8_t selApp[] = { + 0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00 + }; + if (!_isoDepApdu(selApp, sizeof(selApp), rx, sizeof(rx), &rxLen) || !_swOk(rx, rxLen)) { + ST25R_LOG("T4T: SELECT NDEF App falhou"); + return false; + } + + // 2. SELECT Capability Container (file ID E1 03) + static const uint8_t selCC[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; + if (!_isoDepApdu(selCC, sizeof(selCC), rx, sizeof(rx), &rxLen) || !_swOk(rx, rxLen)) return false; + + // 3. READ BINARY do CC (15 bytes) + static const uint8_t readCC[] = {0x00, 0xB0, 0x00, 0x00, 0x0F}; + if (!_isoDepApdu(readCC, sizeof(readCC), rx, sizeof(rx), &rxLen) || !_swOk(rx, rxLen) || rxLen < 15 + 2) { + return false; + } + // CC: [7]=NDEF FileCtrl TLV tag(0x04) [8]=len(0x06) [9-10]=NDEF file ID [11-12]=max NDEF size + uint16_t ndefFileId = ((uint16_t)rx[9] << 8) | rx[10]; + uint16_t maxNdef = ((uint16_t)rx[11] << 8) | rx[12]; + + // 4. SELECT NDEF file + uint8_t selNdef[] = { + 0x00, 0xA4, 0x00, 0x0C, 0x02, (uint8_t)(ndefFileId >> 8), (uint8_t)(ndefFileId & 0xFF) + }; + if (!_isoDepApdu(selNdef, sizeof(selNdef), rx, sizeof(rx), &rxLen) || !_swOk(rx, rxLen)) return false; + + // 5. READ NLEN (2 bytes no offset 0) + static const uint8_t readNlen[] = {0x00, 0xB0, 0x00, 0x00, 0x02}; + if (!_isoDepApdu(readNlen, sizeof(readNlen), rx, sizeof(rx), &rxLen) || rxLen < 4) return false; + uint16_t ndefLen = ((uint16_t)rx[0] << 8) | rx[1]; + + printableUID.picc_type = "NDEF T4T"; + strAllPages = ""; + strAllPages += "NDEF T4T\n"; + char fidLine[40]; + sprintf(fidLine, "NDEF file: %04X (max %u)\n", ndefFileId, maxNdef); + strAllPages += fidLine; + strAllPages += "NDEF len: " + String(ndefLen) + "\n"; + + // 6. READ BINARY da mensagem NDEF (offset 2). Limita a um único READ. + if (ndefLen > 0) { + uint16_t toRead = (ndefLen > 240) ? 240 : ndefLen; + uint8_t readMsg[] = {0x00, 0xB0, 0x00, 0x02, (uint8_t)toRead}; + if (_isoDepApdu(readMsg, sizeof(readMsg), rx, sizeof(rx), &rxLen) && rxLen >= toRead) { + strAllPages += "NDEF: " + _bytesToHex(rx, toRead) + "\n"; + } + } + dataPages = 1; + pageReadSuccess = true; + pageReadStatus = SUCCESS; + return true; +} + +// MIFARE DESFire: GetVersion (0x60) e lista de aplicações (0x6A), via APDU wrapped. +bool ST25R3916::_readDESFireInfo() { + uint8_t rx[64]; + uint16_t rxLen = 0; + + static const uint8_t getVer[] = {0x90, 0x60, 0x00, 0x00, 0x00}; + if (!_isoDepApdu(getVer, sizeof(getVer), rx, sizeof(rx), &rxLen) || rxLen < 2) return false; + // DESFire responde SW1=0x91 com SW2 0xAF (additional frame) ou 0x00. + if (rx[rxLen - 2] != 0x91) { + ST25R_LOG("DESFire GetVersion SW=%02X%02X (não-DESFire)", rx[rxLen - 2], rx[rxLen - 1]); + return false; + } + + printableUID.picc_type = "MIFARE DESFire"; + strAllPages = ""; + strAllPages += "MIFARE DESFire\n"; + if (rxLen >= 7 + 2) { + // HW info: vendor type subtype major minor storage proto + strAllPages += "HW: " + _bytesToHex(rx, 7) + "\n"; + } + + // Coleta frames adicionais (SW 91 AF) — limite de segurança de 3 frames. + int guard = 0; + while (rxLen >= 2 && rx[rxLen - 2] == 0x91 && rx[rxLen - 1] == 0xAF && guard++ < 3) { + static const uint8_t more[] = {0x90, 0xAF, 0x00, 0x00, 0x00}; + if (!_isoDepApdu(more, sizeof(more), rx, sizeof(rx), &rxLen) || rxLen < 2) break; + strAllPages += "Frame: " + _bytesToHex(rx, (uint16_t)(rxLen - 2)) + "\n"; + } + + // GetApplicationIDs (0x6A) — lista AIDs de 3 bytes. + static const uint8_t getApps[] = {0x90, 0x6A, 0x00, 0x00, 0x00}; + if (_isoDepApdu(getApps, sizeof(getApps), rx, sizeof(rx), &rxLen) && rxLen >= 2 && + rx[rxLen - 2] == 0x91) { + uint16_t n = (uint16_t)(rxLen - 2); + strAllPages += "AIDs: " + (n > 0 ? _bytesToHex(rx, n) : String("none")) + "\n"; + } + + dataPages = 1; + pageReadSuccess = true; + pageReadStatus = SUCCESS; + return true; +} + +// Cartão EMV (pagamento): SELECT PPSE e lista os AIDs públicos (tag 4F). Sem dados sensíveis. +bool ST25R3916::_probeEmv() { + uint8_t rx[256]; + uint16_t rxLen = 0; + + // SELECT PPSE "2PAY.SYS.DDF01" + static const uint8_t selPpse[] = {0x00, 0xA4, 0x04, 0x00, 0x0E, 0x32, 0x50, 0x41, 0x59, 0x2E, + 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0x00}; + if (!_isoDepApdu(selPpse, sizeof(selPpse), rx, sizeof(rx), &rxLen) || !_swOk(rx, rxLen)) { + ST25R_LOG("EMV: SELECT PPSE falhou"); + return false; + } + + printableUID.picc_type = "EMV"; + strAllPages = ""; + strAllPages += "EMV (payment card)\n"; + uint16_t fciLen = (uint16_t)(rxLen - 2); + strAllPages += "FCI: " + _bytesToHex(rx, fciLen) + "\n"; + + // Procura tags 4F (Application Identifier) na resposta FCI. + bool foundAid = false; + for (uint16_t i = 0; i + 1 < fciLen; i++) { + if (rx[i] == 0x4F) { + uint8_t len = rx[i + 1]; + if (len >= 5 && len <= 16 && (uint16_t)(i + 2 + len) <= fciLen) { + strAllPages += "AID: " + _bytesToHex(rx + i + 2, len) + "\n"; + foundAid = true; + } + } + } + if (!foundAid) strAllPages += "AID: (não encontrado no PPSE)\n"; + + dataPages = 1; + pageReadSuccess = true; + pageReadStatus = SUCCESS; + return true; +} + +// Orquestra a leitura ISO-DEP: ATS + NDEF T4T -> DESFire -> EMV. +int ST25R3916::_readIsoDep(rfalNfcDevice *dev) { + strAllPages = ""; + dataPages = 0; + totalPages = 0; + + // Extrai ATS (NFC-A) — útil para identificação. RFAL grava o ATS bruto na struct. + String atsHex; + if (dev->type == RFAL_NFC_LISTEN_TYPE_NFCA) { + uint8_t atsLen = dev->proto.isoDep.activation.A.Listener.ATSLen; + const uint8_t *ats = (const uint8_t *)&dev->proto.isoDep.activation.A.Listener.ATS; + if (atsLen > 0 && atsLen <= 20) atsHex = _bytesToHex(ats, atsLen); + ST25R_LOG("ISO-DEP NFC-A ATS(%u)=%s", atsLen, atsHex.c_str()); + } else { + atsHex = "(NFC-B ISO-DEP)"; + } + + // Tenta NDEF T4T (mais genérico), depois DESFire, depois EMV. + bool ok = _readNdefT4T(); + if (!ok) ok = _readDESFireInfo(); + if (!ok) ok = _probeEmv(); + + if (!ok) { + // ISO-DEP ativo mas sem aplicação reconhecida — reporta o que se tem. + printableUID.picc_type = "ISO14443-4"; + strAllPages = "ISO-DEP activated (no known application)\n"; + ST25R_LOG("ISO-DEP: nenhuma aplicação T4T/DESFire/EMV reconhecida"); + } + + // Prefixa a linha de ATS ao dump. + if (atsHex.length()) strAllPages = "ATS: " + atsHex + "\n" + strAllPages; + + totalPages = dataPages; + pageReadStatus = SUCCESS; + pageReadSuccess = true; + return SUCCESS; +} + +static String _bytesToHex(const uint8_t *data, size_t len) { + String out; + char buf[4]; + for (size_t i = 0; i < len; i++) { + sprintf(buf, "%02X", data[i]); + out += buf; + if (i < len - 1) out += " "; + } + return out; +} + +// dump no formato .nfc do Flipper Zero. +int ST25R3916::saveFlipper(String filename) { + if (mfcLoaded || isMifareClassicSak(uid.sak)) return _saveMifareClassicFlipper(filename); + + FS *fs; + if (!getFsStorage(fs)) return FAILURE; + + File file = createNewFile(fs, "/BruceRFID", filename + ".nfc"); + if (!file) return FAILURE; + + String devType = ntagVariant.length() ? ntagVariant : printableUID.picc_type; + + file.println("Filetype: Flipper NFC device"); + file.println("Version: 4"); + file.println( + "# Device type can be ISO14443-3A, ISO14443-3B, ISO14443-4A, ISO14443-4B, " + "ISO15693-3, FeliCa, NTAG/Ultralight, Mifare Classic, Mifare DESFire, SLIX, " + "ST25TB, EMV" + ); + file.println("Device type: " + devType); + file.println("# UID is common for all formats"); + file.println("UID: " + printableUID.uid); + // Flipper grava ATQA em little-endian (ex.: NTAG => "44 00") + char atqaBuf[6]; + sprintf(atqaBuf, "%02X %02X", uid.atqaByte[0], uid.atqaByte[1]); + file.println("ATQA: " + String(atqaBuf)); + file.println("SAK: " + printableUID.sak); + file.println("# Mifare Ultralight specific data"); + file.println("Data format version: 2"); + file.println("Signature: " + _bytesToHex(ntagSignature, 32)); + file.println("Mifare version: " + _bytesToHex(ntagVersion, 8)); + for (int i = 0; i < 3; i++) { + file.println("Counter " + String(i) + ": " + String(ntagCounters[i])); + char tBuf[3]; + sprintf(tBuf, "%02X", ntagTearing[i]); + file.println("Tearing " + String(i) + ": " + String(tBuf)); + } + file.println("Pages total: " + String(totalPages > 0 ? totalPages : dataPages)); + file.println("Pages read: " + String(dataPages)); + file.print(strAllPages); + file.println("Failed authentication attempts: 0"); + + file.close(); + delay(100); + return SUCCESS; +} + +int ST25R3916::save(String filename) { + FS *fs; + if (!getFsStorage(fs)) return FAILURE; + + File file = createNewFile(fs, "/BruceRFID", filename + ".rfid"); + if (!file) return FAILURE; + + file.println("Filetype: Bruce RFID File"); + file.println("Version 1"); + file.println("Device type: " + printableUID.picc_type); + file.println("# UID, ATQA and SAK are common for all formats"); + file.println("UID: " + printableUID.uid); + file.println("SAK: " + printableUID.sak); + file.println("ATQA: " + printableUID.atqa); + // NTAG/Ultralight: persiste versão e assinatura p/ emulação fiel (ex.: amiibo). + if (ntagHasVersion) file.println("Mifare version: " + _bytesToHex(ntagVersion, 8)); + if (ntagHasSignature) file.println("Signature: " + _bytesToHex(ntagSignature, 32)); + if (ntagHasCounters) { + for (int i = 0; i < 3; i++) file.println("Counter " + String(i) + ": " + String(ntagCounters[i])); + } + file.println("# Memory dump"); + file.println("Pages total: " + String(totalPages > 0 ? totalPages : dataPages)); + if (!pageReadSuccess) file.println("Pages read: " + String(dataPages)); + file.print(strAllPages); + + file.close(); + delay(100); + return SUCCESS; +} + +// =========================================================================== +// MIFARE Classic — leitura, escrita, clone (Crypto1) — Milestone 5 +// =========================================================================== + +bool ST25R3916::isMifareClassicSak(uint8_t sak) const { + return (sak == 0x08 || sak == 0x88 || sak == 0x18 || sak == 0x09 || sak == 0x28 || sak == 0x38); +} + +uint32_t ST25R3916::_mfcUid32() const { + // Crypto1 uses the (last) 4 UID bytes. For 7-byte UIDs that is bytes [3..6]. + int off = (uid.size > 4) ? (uid.size - 4) : 0; + return ((uint32_t)uid.uidByte[off] << 24) | ((uint32_t)uid.uidByte[off + 1] << 16) | + ((uint32_t)uid.uidByte[off + 2] << 8) | (uint32_t)uid.uidByte[off + 3]; +} + +uint8_t ST25R3916::_mfcSectorFirstBlock(uint8_t sector) const { + if (sector < 32) return (uint8_t)(sector * 4); + return (uint8_t)(128 + (sector - 32) * 16); +} + +uint8_t ST25R3916::_mfcSectorBlockCount(uint8_t sector) const { return (sector < 32) ? 4 : 16; } + +uint8_t ST25R3916::_mfcBlockToSector(uint16_t block) const { + if (block < 128) return (uint8_t)(block / 4); + return (uint8_t)(32 + (block - 128) / 16); +} + +// Pack data+parity into an ISO14443-A bitstream (LSB first, 9 bits per byte). +static uint16_t _mfcPackBits(const uint8_t *data, const uint8_t *par, uint8_t nbytes, uint8_t *out) { + uint16_t bit = 0; + for (uint8_t i = 0; i < nbytes; i++) { + for (uint8_t b = 0; b < 8; b++) { + if (data[i] & (1u << b)) out[bit >> 3] |= (uint8_t)(1u << (bit & 7)); + else out[bit >> 3] &= (uint8_t)~(1u << (bit & 7)); + bit++; + } + if (par[i] & 1u) out[bit >> 3] |= (uint8_t)(1u << (bit & 7)); + else out[bit >> 3] &= (uint8_t)~(1u << (bit & 7)); + bit++; + } + return bit; +} + +// Reception of MIFARE frames with PAR_RX_KEEP ends on an "incomplete byte" +// (the trailing parity bit), which RFAL reports as ST_ERR_INCOMPLETE_BYTE[_0x]. +// Those are valid receptions for us — only hard errors (timeout/etc) are failures. +static inline bool _mfcRxOk(::ReturnCode e) { + return (e == ST_ERR_NONE) || (e >= ST_ERR_INCOMPLETE_BYTE && e <= ST_ERR_INCOMPLETE_BYTE_07); +} + +// Unpack an ISO14443-A bitstream (8 data bits + parity bit per byte). Drops parity. +static uint8_t _mfcUnpackBits(const uint8_t *in, uint16_t nbits, uint8_t *outData, uint8_t maxBytes) { + uint8_t nbytes = 0; + uint16_t bit = 0; + while ((bit + 8) <= nbits && nbytes < maxBytes) { + uint8_t v = 0; + for (uint8_t b = 0; b < 8; b++) { + if (in[bit >> 3] & (1u << (bit & 7))) v |= (uint8_t)(1u << b); + bit++; + } + outData[nbytes++] = v; + if (bit < nbits) bit++; // skip parity bit + } + return nbytes; +} + +// Encrypted MIFARE frame: software supplies parity (PAR_TX_NONE) and CRC +// (CRC_TX_MANUAL); reception keeps parity + CRC bits and skips HW checks +// (PAR_RX_KEEP disables both parity AND CRC validation on the ST25R3916). +#define MFC_FLAGS_ENC \ + ((uint32_t)RFAL_TXRX_FLAGS_CRC_TX_MANUAL | (uint32_t)RFAL_TXRX_FLAGS_CRC_RX_KEEP | \ + (uint32_t)RFAL_TXRX_FLAGS_NFCIP1_OFF | (uint32_t)RFAL_TXRX_FLAGS_AGC_ON | \ + (uint32_t)RFAL_TXRX_FLAGS_PAR_RX_KEEP | (uint32_t)RFAL_TXRX_FLAGS_PAR_TX_NONE) + +// First-auth command travels in the clear: software supplies the CRC +// (CRC_TX_MANUAL, already in the buffer) but HW adds parity (PAR_TX_AUTO). The +// nonce response carries no CRC, so reception must skip parity/CRC checks. +#define MFC_FLAGS_FIRSTAUTH \ + ((uint32_t)RFAL_TXRX_FLAGS_CRC_TX_MANUAL | (uint32_t)RFAL_TXRX_FLAGS_CRC_RX_KEEP | \ + (uint32_t)RFAL_TXRX_FLAGS_NFCIP1_OFF | (uint32_t)RFAL_TXRX_FLAGS_AGC_ON | \ + (uint32_t)RFAL_TXRX_FLAGS_PAR_RX_KEEP | (uint32_t)RFAL_TXRX_FLAGS_PAR_TX_AUTO) + +::ReturnCode ST25R3916::_mifareTransceiveRaw( + uint8_t *txBuf, uint16_t txBits, uint8_t *rxBuf, uint16_t rxCapBytes, uint16_t *rxBits, uint32_t fwt, + uint32_t flags +) { + rfalTransceiveContext ctx; + ctx.txBuf = txBuf; + ctx.txBufLen = txBits; + ctx.rxBuf = rxBuf; + ctx.rxBufLen = (uint16_t)rfalConvBytesToBits(rxCapBytes); + ctx.rxRcvdLen = rxBits; + ctx.flags = flags; + ctx.fwt = fwt; + + if (rxBits) *rxBits = 0; + auto err = _hw->rfalStartTransceive(&ctx); + if (err != ST_ERR_NONE) return err; + + uint32_t t0 = millis(); + while ((millis() - t0) < 60) { + _hw->rfalWorker(); + err = _hw->rfalGetTransceiveStatus(); + if (err != ST_ERR_BUSY) break; + } + return err; +} + +bool ST25R3916::_mifareAuth(uint8_t block, const uint8_t key[6], bool useKeyB) { + uint64_t k = 0; + for (int i = 0; i < 6; i++) k = (k << 8) | key[i]; + + uint8_t cmd[2] = {(uint8_t)(useKeyB ? 0x61 : 0x60), block}; + uint32_t nt = 0; + + if (!_mfcAuthed) { + // First authentication: command in the clear with manual CRC + HW parity. + // The nonce response has no CRC, so RX skips parity/CRC checks. + uint16_t crc = _hw->rfalCrcCalculateCcitt(0x6363, cmd, 2); + uint8_t tx[4] = {cmd[0], cmd[1], (uint8_t)(crc & 0xFF), (uint8_t)(crc >> 8)}; + uint8_t rx[8] = {0}; + uint16_t rxBits = 0; + auto err = _mifareTransceiveRaw( + tx, + (uint16_t)rfalConvBytesToBits(4), + rx, + sizeof(rx), + &rxBits, + rfalConvMsTo1fc(20), + MFC_FLAGS_FIRSTAUTH + ); + if (!_mfcRxOk(err) || rxBits < 32) { + ST25R_LOG("mfc auth1 blk=%u no nonce err=%d bits=%u", block, (int)err, rxBits); + return false; + } + uint8_t ntBuf[4] = {0}; + _mfcUnpackBits(rx, rxBits, ntBuf, 4); + nt = ((uint32_t)ntBuf[0] << 24) | ((uint32_t)ntBuf[1] << 16) | ((uint32_t)ntBuf[2] << 8) | ntBuf[3]; + crypto1_init(&_mfcCipher, k); + crypto1_word(&_mfcCipher, nt ^ _mfcUid32(), 0); + } else { + // Nested authentication: command + nonce travel over the encrypted channel. + uint16_t crc = _hw->rfalCrcCalculateCcitt(0x6363, cmd, 2); + uint8_t plain[4] = {cmd[0], cmd[1], (uint8_t)(crc & 0xFF), (uint8_t)(crc >> 8)}; + uint8_t enc[4], par[4]; + for (int i = 0; i < 4; i++) { + enc[i] = (uint8_t)(crypto1_byte(&_mfcCipher, 0, 0) ^ plain[i]); + par[i] = (uint8_t)(crypto1_filter_bit(&_mfcCipher) ^ nfc_oddparity(plain[i])); + } + uint8_t tx[8] = {0}; + uint16_t txBits = _mfcPackBits(enc, par, 4, tx); + uint8_t rx[8] = {0}; + uint16_t rxBits = 0; + auto err = + _mifareTransceiveRaw(tx, txBits, rx, sizeof(rx), &rxBits, rfalConvMsTo1fc(20), MFC_FLAGS_ENC); + if (!_mfcRxOk(err) || rxBits < 32) { + ST25R_LOG("mfc nested auth blk=%u no nonce err=%d bits=%u", block, (int)err, rxBits); + return false; + } + uint8_t encNt[4] = {0}; + _mfcUnpackBits(rx, rxBits, encNt, 4); + for (int i = 0; i < 4; i++) { + uint8_t ksb = crypto1_byte(&_mfcCipher, 0, 0); + nt = (nt << 8) | (uint8_t)(encNt[i] ^ ksb); + } + crypto1_init(&_mfcCipher, k); + crypto1_word(&_mfcCipher, nt ^ _mfcUid32(), 0); + } + + // Build encrypted reader answer: nr (fed in) + ar (= suc64(nt)). + uint8_t nrPlain[4]; + uint32_t nr = esp_random(); + nrPlain[0] = (uint8_t)(nr >> 24); + nrPlain[1] = (uint8_t)(nr >> 16); + nrPlain[2] = (uint8_t)(nr >> 8); + nrPlain[3] = (uint8_t)(nr); + + uint8_t arData[8], arPar[8]; + for (int i = 0; i < 4; i++) { + arData[i] = (uint8_t)(crypto1_byte(&_mfcCipher, nrPlain[i], 0) ^ nrPlain[i]); + arPar[i] = (uint8_t)(crypto1_filter_bit(&_mfcCipher) ^ nfc_oddparity(nrPlain[i])); + } + uint32_t ar = prng_successor(nt, 64); + for (int i = 0; i < 4; i++) { + uint8_t arb = (uint8_t)(ar >> (24 - 8 * i)); + arData[4 + i] = (uint8_t)(crypto1_byte(&_mfcCipher, 0, 0) ^ arb); + arPar[4 + i] = (uint8_t)(crypto1_filter_bit(&_mfcCipher) ^ nfc_oddparity(arb)); + } + + uint8_t tx[16] = {0}; + uint16_t txBits = _mfcPackBits(arData, arPar, 8, tx); + uint8_t rx[8] = {0}; + uint16_t rxBits = 0; + auto err = _mifareTransceiveRaw(tx, txBits, rx, sizeof(rx), &rxBits, rfalConvMsTo1fc(20), MFC_FLAGS_ENC); + if (!_mfcRxOk(err) || rxBits < 32) { + ST25R_LOG("mfc auth blk=%u no AT err=%d bits=%u", block, (int)err, rxBits); + _mfcAuthed = false; + return false; + } + uint8_t atEnc[4] = {0}; + _mfcUnpackBits(rx, rxBits, atEnc, 4); + uint32_t at = 0; + for (int i = 0; i < 4; i++) { + uint8_t ksb = crypto1_byte(&_mfcCipher, 0, 0); + at = (at << 8) | (uint8_t)(atEnc[i] ^ ksb); + } + uint32_t atExpected = prng_successor(nt, 96); + if (at != atExpected) { + ST25R_LOG( + "mfc auth blk=%u AT mismatch (%08lX != %08lX)", + block, + (unsigned long)at, + (unsigned long)atExpected + ); + _mfcAuthed = false; + return false; + } + _mfcAuthed = true; + return true; +} + +bool ST25R3916::_mifareReadBlock(uint8_t block, uint8_t data[16]) { + if (!_mfcAuthed) return false; + uint8_t plain[4] = {0x30, block, 0, 0}; + uint16_t crc = _hw->rfalCrcCalculateCcitt(0x6363, plain, 2); + plain[2] = (uint8_t)(crc & 0xFF); + plain[3] = (uint8_t)(crc >> 8); + + uint8_t enc[4], par[4]; + for (int i = 0; i < 4; i++) { + enc[i] = (uint8_t)(crypto1_byte(&_mfcCipher, 0, 0) ^ plain[i]); + par[i] = (uint8_t)(crypto1_filter_bit(&_mfcCipher) ^ nfc_oddparity(plain[i])); + } + uint8_t tx[8] = {0}; + uint16_t txBits = _mfcPackBits(enc, par, 4, tx); + uint8_t rx[32] = {0}; + uint16_t rxBits = 0; + auto err = _mifareTransceiveRaw(tx, txBits, rx, sizeof(rx), &rxBits, rfalConvMsTo1fc(20), MFC_FLAGS_ENC); + if (!_mfcRxOk(err) || rxBits < (18 * 8)) { + ST25R_LOG("mfc read blk=%u err=%d bits=%u", block, (int)err, rxBits); + return false; + } + uint8_t recv[18] = {0}; + uint8_t n = _mfcUnpackBits(rx, rxBits, recv, 18); + if (n < 16) return false; + // Decrypt all 18 bytes (16 data + 2 CRC) to keep the cipher in sync. + for (int i = 0; i < 18; i++) recv[i] ^= crypto1_byte(&_mfcCipher, 0, 0); + memcpy(data, recv, 16); + return true; +} + +bool ST25R3916::_mifareWriteBlock(uint8_t block, const uint8_t data[16]) { + if (!_mfcAuthed) return false; + + // Phase 1: WRITE command (0xA0 + block). + uint8_t plain[4] = {0xA0, block, 0, 0}; + uint16_t crc = _hw->rfalCrcCalculateCcitt(0x6363, plain, 2); + plain[2] = (uint8_t)(crc & 0xFF); + plain[3] = (uint8_t)(crc >> 8); + + uint8_t enc[4], par[4]; + for (int i = 0; i < 4; i++) { + enc[i] = (uint8_t)(crypto1_byte(&_mfcCipher, 0, 0) ^ plain[i]); + par[i] = (uint8_t)(crypto1_filter_bit(&_mfcCipher) ^ nfc_oddparity(plain[i])); + } + uint8_t tx[24] = {0}; + uint16_t txBits = _mfcPackBits(enc, par, 4, tx); + uint8_t rx[8] = {0}; + uint16_t rxBits = 0; + auto err = _mifareTransceiveRaw(tx, txBits, rx, sizeof(rx), &rxBits, rfalConvMsTo1fc(20), MFC_FLAGS_ENC); + if (!_mfcRxOk(err) || rxBits < 4) { + ST25R_LOG("mfc write1 blk=%u err=%d bits=%u", block, (int)err, rxBits); + return false; + } + // ACK nibble (4 bits) encrypted with the next keystream bits; expect 0xA. + uint8_t ack = 0; + for (int i = 0; i < 4; i++) { + uint8_t ks = crypto1_bit(&_mfcCipher, 0, 0); + uint8_t b = (uint8_t)((rx[0] >> i) & 1); + ack |= (uint8_t)((b ^ ks) << i); + } + if ((ack & 0x0F) != 0x0A) { + ST25R_LOG("mfc write1 blk=%u NAK 0x%X", block, ack & 0x0F); + return false; + } + + // Phase 2: 16 data bytes + CRC. + uint8_t dplain[18]; + memcpy(dplain, data, 16); + uint16_t dcrc = _hw->rfalCrcCalculateCcitt(0x6363, dplain, 16); + dplain[16] = (uint8_t)(dcrc & 0xFF); + dplain[17] = (uint8_t)(dcrc >> 8); + + uint8_t denc[18], dpar[18]; + for (int i = 0; i < 18; i++) { + denc[i] = (uint8_t)(crypto1_byte(&_mfcCipher, 0, 0) ^ dplain[i]); + dpar[i] = (uint8_t)(crypto1_filter_bit(&_mfcCipher) ^ nfc_oddparity(dplain[i])); + } + uint8_t tx2[24] = {0}; + uint16_t txBits2 = _mfcPackBits(denc, dpar, 18, tx2); + uint8_t rx2[8] = {0}; + uint16_t rxBits2 = 0; + err = _mifareTransceiveRaw(tx2, txBits2, rx2, sizeof(rx2), &rxBits2, rfalConvMsTo1fc(20), MFC_FLAGS_ENC); + if (!_mfcRxOk(err) || rxBits2 < 4) { + ST25R_LOG("mfc write2 blk=%u err=%d bits=%u", block, (int)err, rxBits2); + return false; + } + uint8_t ack2 = 0; + for (int i = 0; i < 4; i++) { + uint8_t ks = crypto1_bit(&_mfcCipher, 0, 0); + uint8_t b = (uint8_t)((rx2[0] >> i) & 1); + ack2 |= (uint8_t)((b ^ ks) << i); + } + if ((ack2 & 0x0F) != 0x0A) { + ST25R_LOG("mfc write2 blk=%u NAK 0x%X", block, ack2 & 0x0F); + return false; + } + return true; +} + +void ST25R3916::_mfcHalt() { + // HLTA (0x50 0x00) + CRC, then drop the cipher so the next auth restarts clean. + uint8_t halt[4] = {0x50, 0x00, 0, 0}; + uint16_t crc = _hw->rfalCrcCalculateCcitt(0x6363, halt, 2); + halt[2] = (uint8_t)(crc & 0xFF); + halt[3] = (uint8_t)(crc >> 8); + uint8_t rx[4] = {0}; + uint16_t rxLen = 0; + _hw->rfalTransceiveBlockingTxRx( + halt, sizeof(halt), rx, sizeof(rx), &rxLen, RFAL_TXRX_FLAGS_CRC_TX_MANUAL, rfalConvMsTo1fc(5) + ); + _mfcAuthed = false; +} + +void ST25R3916::_mfcRebuildStrAllPages() { + strAllPages = ""; + char line[80]; + for (uint8_t s = 0; s < mfcDump.sectors; s++) { + if (mfcDump.keyAFound[s]) { + const uint8_t *k = mfcDump.keyA[s]; + sprintf( + line, "Key A sector %u: %02X %02X %02X %02X %02X %02X", s, k[0], k[1], k[2], k[3], k[4], k[5] + ); + strAllPages += String(line) + "\n"; + } + if (mfcDump.keyBFound[s]) { + const uint8_t *k = mfcDump.keyB[s]; + sprintf( + line, "Key B sector %u: %02X %02X %02X %02X %02X %02X", s, k[0], k[1], k[2], k[3], k[4], k[5] + ); + strAllPages += String(line) + "\n"; + } + } + for (uint16_t b = 0; b < mfcDump.totalBlocks; b++) { + if (mfcDump.blockRead[b]) { + const uint8_t *d = mfcDump.blocks[b]; + sprintf( + line, + "Block %u: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + b, + d[0], + d[1], + d[2], + d[3], + d[4], + d[5], + d[6], + d[7], + d[8], + d[9], + d[10], + d[11], + d[12], + d[13], + d[14], + d[15] + ); + } else { + sprintf(line, "Block %u: [AUTH FAIL]", b); + } + strAllPages += String(line) + "\n"; + } + dataPages = mfcDump.totalBlocks; + totalPages = mfcDump.totalBlocks; +} + +int ST25R3916::_readMifareClassic(rfalNfcDevice *dev) { + memset(&mfcDump, 0, sizeof(mfcDump)); + _mfcAuthed = false; + + if (uid.sak == 0x18 || uid.sak == 0x38) { + mfcDump.sectors = 40; + mfcDump.totalBlocks = 256; + mfcType = "4K"; + } else if (uid.sak == 0x09) { + mfcDump.sectors = 5; + mfcDump.totalBlocks = 20; + mfcType = "Mini"; + } else { + mfcDump.sectors = 16; + mfcDump.totalBlocks = 64; + mfcType = "1K"; + } + printableUID.picc_type = "MIFARE Classic " + mfcType; + + const int nKeys = (int)(sizeof(keys) / sizeof(keys[0])); + int blocksOk = 0; + + for (uint8_t s = 0; s < mfcDump.sectors; s++) { + uint8_t firstBlock = _mfcSectorFirstBlock(s); + uint8_t nBlocks = _mfcSectorBlockCount(s); + uint8_t trailer = (uint8_t)(firstBlock + nBlocks - 1); + + bool authed = false; + bool usedKeyB = false; + int usedKeyIdx = -1; + + // Try Key A then Key B from the dictionary against the sector trailer. + for (int useB = 0; useB <= 1 && !authed; useB++) { + for (int kidx = 0; kidx < nKeys; kidx++) { + if (_mifareAuth(trailer, keys[kidx], useB != 0)) { + authed = true; + usedKeyB = (useB != 0); + usedKeyIdx = kidx; + break; + } + // A failed auth leaves the field/cipher dirty: halt + reselect. + _mfcHalt(); + _nfc->rfalNfcDeactivate(false); + delay(5); + rfalNfcDevice *d2 = nullptr; + if (!_pollForTag(&d2, 500)) { + pageReadStatus = FAILURE; + return FAILURE; + } + _parseDevice(d2); + _mfcAuthed = false; + } + } + + if (!authed) { + ST25R_LOG("mfc sector %u: no key found", s); + continue; + } + + if (usedKeyB) { + memcpy(mfcDump.keyB[s], keys[usedKeyIdx], 6); + mfcDump.keyBFound[s] = true; + } else { + memcpy(mfcDump.keyA[s], keys[usedKeyIdx], 6); + mfcDump.keyAFound[s] = true; + } + + for (uint8_t bo = 0; bo < nBlocks; bo++) { + uint8_t blk = (uint8_t)(firstBlock + bo); + uint8_t data[16] = {0}; + if (_mifareReadBlock(blk, data)) { + memcpy(mfcDump.blocks[blk], data, 16); + mfcDump.blockRead[blk] = true; + blocksOk++; + } else { + ST25R_LOG("mfc block %u read fail", blk); + } + } + + // Re-select before moving to the next sector (fresh first-auth is more robust). + _mfcHalt(); + _nfc->rfalNfcDeactivate(false); + delay(5); + rfalNfcDevice *d2 = nullptr; + if (s + 1 < mfcDump.sectors) { + if (!_pollForTag(&d2, 500)) break; + _parseDevice(d2); + _mfcAuthed = false; + } + } + + mfcLoaded = (blocksOk > 0); + _mfcRebuildStrAllPages(); + ST25R_LOG("mfc read done: %d/%u blocks", blocksOk, mfcDump.totalBlocks); + + pageReadStatus = mfcLoaded ? SUCCESS : FAILURE; + pageReadSuccess = mfcLoaded; + return mfcLoaded ? SUCCESS : FAILURE; +} + +int ST25R3916::_writeMifareClassic(rfalNfcDevice *dev) { + if (!mfcLoaded) return FAILURE; + _mfcAuthed = false; + + int written = 0; + for (uint8_t s = 0; s < mfcDump.sectors; s++) { + uint8_t firstBlock = _mfcSectorFirstBlock(s); + uint8_t nBlocks = _mfcSectorBlockCount(s); + uint8_t trailer = (uint8_t)(firstBlock + nBlocks - 1); + + const uint8_t *key = + mfcDump.keyAFound[s] ? mfcDump.keyA[s] : (mfcDump.keyBFound[s] ? mfcDump.keyB[s] : nullptr); + bool useB = !mfcDump.keyAFound[s] && mfcDump.keyBFound[s]; + if (!key) continue; + + if (!_mifareAuth(trailer, key, useB)) { + _mfcHalt(); + _nfc->rfalNfcDeactivate(false); + delay(5); + rfalNfcDevice *d2 = nullptr; + if (!_pollForTag(&d2, 500)) break; + _parseDevice(d2); + _mfcAuthed = false; + continue; + } + + for (uint8_t bo = 0; bo < nBlocks; bo++) { + uint8_t blk = (uint8_t)(firstBlock + bo); + if (blk == 0) continue; // block 0 is read-only on genuine cards + if (!mfcDump.blockRead[blk]) continue; // skip blocks we never recovered + if (_mifareWriteBlock(blk, mfcDump.blocks[blk])) written++; + } + + _mfcHalt(); + _nfc->rfalNfcDeactivate(false); + delay(5); + rfalNfcDevice *d2 = nullptr; + if (s + 1 < mfcDump.sectors) { + if (!_pollForTag(&d2, 500)) break; + _parseDevice(d2); + _mfcAuthed = false; + } + } + + ST25R_LOG("mfc write done: %d blocks", written); + _nfc->rfalNfcDeactivate(false); + return written > 0 ? SUCCESS : FAILURE; +} + +int ST25R3916::_writeMifareClassicMagic(rfalNfcDevice *dev) { + if (!mfcLoaded) return FAILURE; + + // Magic Gen1 backdoor: 0x40 (7 bits) then 0x43; afterwards any block accepts + // a plain MIFARE WRITE (0xA0) without authentication/crypto. + auto gen1Cmd = [&](uint8_t cmd, uint8_t bits) -> bool { + uint8_t rxBuf[4] = {0}; + uint16_t actLen = 0; + rfalTransceiveContext ctx; + ctx.txBuf = &cmd; + ctx.txBufLen = bits; + ctx.rxBuf = rxBuf; + ctx.rxBufLen = 32; + ctx.rxRcvdLen = &actLen; + ctx.flags = (uint32_t)RFAL_TXRX_FLAGS_CRC_TX_MANUAL | (uint32_t)RFAL_TXRX_FLAGS_PAR_TX_NONE | + (uint32_t)RFAL_TXRX_FLAGS_CRC_RX_KEEP | (uint32_t)RFAL_TXRX_FLAGS_PAR_RX_KEEP; + ctx.fwt = rfalConv64fcTo1fc(5000); + if (_hw->rfalStartTransceive(&ctx) != ST_ERR_NONE) return false; + uint32_t t0 = millis(); + while (millis() - t0 < 50) { + _hw->rfalWorker(); + if (_hw->rfalGetTransceiveState() == RFAL_TXRX_STATE_IDLE) break; + } + return (_hw->rfalGetTransceiveStatus() == ST_ERR_NONE && rxBuf[0] == 0x0A); + }; + + if (!gen1Cmd(0x40, 7) || !gen1Cmd(0x43, 8)) { + ST25R_LOG("mfc magic: not a Gen1 card (backdoor failed)"); + return TAG_NOT_MATCH; + } + + int written = 0; + for (uint16_t b = 0; b < mfcDump.totalBlocks; b++) { + if (!mfcDump.blockRead[b]) continue; + uint8_t writeCmd[2] = {0xA0, (uint8_t)b}; + uint8_t rxBuf[4] = {0}; + uint16_t actLen = 0; + auto err = _hw->rfalTransceiveBlockingTxRx( + writeCmd, + sizeof(writeCmd), + rxBuf, + sizeof(rxBuf), + &actLen, + RFAL_TXRX_FLAGS_DEFAULT, + rfalConvMsTo1fc(20) + ); + if (err != ST_ERR_NONE || rxBuf[0] != 0x0A) { + ST25R_LOG("mfc magic: WRITE blk=%u no ACK err=%d", b, (int)err); + continue; + } + uint8_t blockData[16]; + memcpy(blockData, mfcDump.blocks[b], 16); + memset(rxBuf, 0, sizeof(rxBuf)); + actLen = 0; + err = _hw->rfalTransceiveBlockingTxRx( + blockData, + sizeof(blockData), + rxBuf, + sizeof(rxBuf), + &actLen, + RFAL_TXRX_FLAGS_DEFAULT, + rfalConvMsTo1fc(20) + ); + if (err == ST_ERR_NONE) written++; + } + + ST25R_LOG("mfc magic clone done: %d blocks", written); + return written > 0 ? SUCCESS : FAILURE; +} + +int ST25R3916::_saveMifareClassicFlipper(const String &filename) { + FS *fs; + if (!getFsStorage(fs)) return FAILURE; + + File file = createNewFile(fs, "/BruceRFID", filename + ".nfc"); + if (!file) return FAILURE; + + String type = mfcType.length() ? mfcType : "1K"; + + file.println("Filetype: Flipper NFC device"); + file.println("Version: 4"); + file.println("Device type: Mifare Classic"); + file.println("# UID is common for all formats"); + file.println("UID: " + printableUID.uid); + char atqaBuf[6]; + sprintf(atqaBuf, "%02X %02X", uid.atqaByte[0], uid.atqaByte[1]); + file.println("ATQA: " + String(atqaBuf)); + file.println("SAK: " + printableUID.sak); + file.println("# Mifare Classic specific data"); + file.println("Mifare Classic type: " + type); + file.println("Data format version: 2"); + + char line[80]; + for (uint8_t s = 0; s < mfcDump.sectors; s++) { + const uint8_t *ka = + mfcDump.keyAFound[s] ? mfcDump.keyA[s] : (const uint8_t *)"\xFF\xFF\xFF\xFF\xFF\xFF"; + const uint8_t *kb = + mfcDump.keyBFound[s] ? mfcDump.keyB[s] : (const uint8_t *)"\xFF\xFF\xFF\xFF\xFF\xFF"; + sprintf( + line, + "Key A sector %u: %02X %02X %02X %02X %02X %02X", + s, + ka[0], + ka[1], + ka[2], + ka[3], + ka[4], + ka[5] + ); + file.println(line); + sprintf( + line, + "Key B sector %u: %02X %02X %02X %02X %02X %02X", + s, + kb[0], + kb[1], + kb[2], + kb[3], + kb[4], + kb[5] + ); + file.println(line); + } + for (uint16_t b = 0; b < mfcDump.totalBlocks; b++) { + const uint8_t *d = mfcDump.blocks[b]; + if (mfcDump.blockRead[b]) { + sprintf( + line, + "Block %u: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + b, + d[0], + d[1], + d[2], + d[3], + d[4], + d[5], + d[6], + d[7], + d[8], + d[9], + d[10], + d[11], + d[12], + d[13], + d[14], + d[15] + ); + } else { + sprintf(line, "Block %u: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??", b); + } + file.println(line); + } + + file.close(); + delay(100); + return SUCCESS; +} + +// --------------------------------------------------------------------------- +// MIFARE Classic emulation (listener-side Crypto1) — Milestone 5 +// +// The ST25R3916 handles ATQA/anticollision/SELECT in hardware (PT memory), so a +// reader always sees the right UID/SAK/ATQA/type. On top of that, this code +// answers the Crypto1 authentication and serves encrypted READ/WRITE from the +// loaded dump. Timing is tight at 106 kb/s — validated UID/type always; the +// full crypto path needs on-hardware tuning. +// --------------------------------------------------------------------------- + +bool ST25R3916::_buildEmuMfc() { + memset(&mfcDump, 0, sizeof(mfcDump)); + uint16_t maxBlock = 0; + int pos = 0; + while (pos < (int)strAllPages.length()) { + int nl = strAllPages.indexOf('\n', pos); + if (nl < 0) nl = strAllPages.length(); + String line = strAllPages.substring(pos, nl); + pos = nl + 1; + line.trim(); + std::vector bytes; + if (line.startsWith("Block ")) { + int colon = line.indexOf(':'); + int idx = line.substring(6, colon).toInt(); + if (idx >= 0 && idx < 256 && st25ParseHexBytesAfterColon(line, bytes) && bytes.size() >= 16) { + memcpy(mfcDump.blocks[idx], bytes.data(), 16); + mfcDump.blockRead[idx] = true; + if (idx + 1 > maxBlock) maxBlock = (uint16_t)(idx + 1); + } + } else if (line.startsWith("Key A sector ")) { + int colon = line.indexOf(':'); + int s = line.substring(13, colon).toInt(); + if (s >= 0 && s < 40 && st25ParseHexBytesAfterColon(line, bytes) && bytes.size() >= 6) { + memcpy(mfcDump.keyA[s], bytes.data(), 6); + mfcDump.keyAFound[s] = true; + } + } else if (line.startsWith("Key B sector ")) { + int colon = line.indexOf(':'); + int s = line.substring(13, colon).toInt(); + if (s >= 0 && s < 40 && st25ParseHexBytesAfterColon(line, bytes) && bytes.size() >= 6) { + memcpy(mfcDump.keyB[s], bytes.data(), 6); + mfcDump.keyBFound[s] = true; + } + } + } + if (maxBlock == 0) return false; + + mfcDump.totalBlocks = (maxBlock > 64) ? 256 : 64; + mfcDump.sectors = (mfcDump.totalBlocks == 256) ? 40 : 16; + + // Inject the recovered keys back into each sector trailer (Key A reads as 0 + // on a genuine card, so the dump's trailer must be patched for auth to work). + for (uint8_t s = 0; s < mfcDump.sectors; s++) { + uint8_t trailer = (uint8_t)(_mfcSectorFirstBlock(s) + _mfcSectorBlockCount(s) - 1); + if (mfcDump.keyAFound[s]) memcpy(&mfcDump.blocks[trailer][0], mfcDump.keyA[s], 6); + if (mfcDump.keyBFound[s]) memcpy(&mfcDump.blocks[trailer][10], mfcDump.keyB[s], 6); + } + ST25R_LOG("emu mfc: blocks=%u sectors=%u", mfcDump.totalBlocks, mfcDump.sectors); + return true; +} + +void ST25R3916::_emuTxClear(const uint8_t *data, uint8_t n, bool withCrc) { + _hw->st25r3916ChangeRegisterBits( + ST25R3916_REG_ISO14443A_NFC, + ST25R3916_REG_ISO14443A_NFC_no_tx_par, + ST25R3916_REG_ISO14443A_NFC_no_tx_par_off + ); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_CLEAR_FIFO); + _hw->st25r3916WriteFifo(data, n); + _hw->st25r3916SetNumTxBits((uint16_t)(n * 8U)); + _hw->st25r3916ExecuteCommand( + withCrc ? ST25R3916_CMD_TRANSMIT_WITH_CRC : ST25R3916_CMD_TRANSMIT_WITHOUT_CRC + ); + _hw->st25r3916WaitForInterruptsTimed(ST25R3916_IRQ_MASK_TXE, 20); +} + +void ST25R3916::_emuTxBits(const uint8_t *bitstream, uint16_t nbits) { + _hw->st25r3916ChangeRegisterBits( + ST25R3916_REG_ISO14443A_NFC, + ST25R3916_REG_ISO14443A_NFC_no_tx_par, + ST25R3916_REG_ISO14443A_NFC_no_tx_par + ); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_CLEAR_FIFO); + _hw->st25r3916WriteFifo(bitstream, (uint16_t)((nbits + 7) / 8)); + _hw->st25r3916SetNumTxBits(nbits); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_TRANSMIT_WITHOUT_CRC); + _hw->st25r3916WaitForInterruptsTimed(ST25R3916_IRQ_MASK_TXE, 20); +} + +uint16_t ST25R3916::_emuRxRaw(uint8_t *out, uint8_t maxBytes, uint32_t toMs) { + _hw->st25r3916ChangeRegisterBits( + ST25R3916_REG_ISO14443A_NFC, + ST25R3916_REG_ISO14443A_NFC_no_rx_par, + ST25R3916_REG_ISO14443A_NFC_no_rx_par + ); + uint32_t irqs = + _hw->st25r3916WaitForInterruptsTimed(ST25R3916_IRQ_MASK_RXE | ST25R3916_IRQ_MASK_EOF, toMs); + if ((irqs & ST25R3916_IRQ_MASK_RXE) == 0U) return 0; + uint16_t nb = _hw->st25r3916GetNumFIFOBytes(); + uint8_t st2 = 0; + _hw->st25r3916ReadRegister(ST25R3916_REG_FIFO_STATUS2, &st2); + uint8_t inc = (uint8_t)((st2 & ST25R3916_REG_FIFO_STATUS2_fifo_lb_mask) >> + ST25R3916_REG_FIFO_STATUS2_fifo_lb_shift); + if (nb == 0U || nb > maxBytes) return 0; + _hw->st25r3916ReadFifo(out, nb); + return (uint16_t)(inc ? ((nb - 1) * 8 + inc) : (nb * 8)); +} + +void ST25R3916::_emuParityOff() { + _hw->st25r3916ChangeRegisterBits( + ST25R3916_REG_ISO14443A_NFC, + ST25R3916_REG_ISO14443A_NFC_no_tx_par | ST25R3916_REG_ISO14443A_NFC_no_rx_par, + ST25R3916_REG_ISO14443A_NFC_no_tx_par_off | ST25R3916_REG_ISO14443A_NFC_no_rx_par_off + ); +} + +bool ST25R3916::_emuMfcHandle(uint8_t *fifo, uint16_t n) { + uint8_t cmd = fifo[0]; + if ((cmd != 0x60 && cmd != 0x61) || n < 2) return false; // only AUTH bootstraps a session + + uint8_t block = fifo[1]; + uint8_t sector = _mfcBlockToSector(block); + if (sector >= mfcDump.sectors) return false; + _emuMfcAuthReq++; + + static const uint8_t ffKey[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const uint8_t *key = (cmd == 0x60) ? mfcDump.keyA[sector] : mfcDump.keyB[sector]; + bool haveKey = (cmd == 0x60) ? mfcDump.keyAFound[sector] : mfcDump.keyBFound[sector]; + if (!haveKey) key = ffKey; + + uint64_t k = 0; + for (int i = 0; i < 6; i++) k = (k << 8) | key[i]; + + _emuNt = esp_random(); + uint8_t ntB[4] = { + (uint8_t)(_emuNt >> 24), (uint8_t)(_emuNt >> 16), (uint8_t)(_emuNt >> 8), (uint8_t)_emuNt + }; + _emuTxClear(ntB, 4, false); // nonce: parity, no CRC + + crypto1_init(&_emuCipher, k); + crypto1_word(&_emuCipher, _mfcUid32() ^ _emuNt, 0); + + // Receive reader answer: {nr}{ar} = 8 encrypted bytes + parity (72 bits). + uint8_t raw[16] = {0}; + uint16_t bits = _emuRxRaw(raw, sizeof(raw), 30); + _emuMfcLastNrBits = bits; + if (bits < 64) { + _emuMfcNoNr++; + _emuMfcAuthed = false; + _emuParityOff(); + return false; + } + uint8_t enc[8] = {0}; + _mfcUnpackBits(raw, bits, enc, 8); + for (int i = 0; i < 4; i++) crypto1_byte(&_emuCipher, enc[i], 1); // decrypt+feed nr + uint32_t ar = 0; + for (int i = 0; i < 4; i++) { + uint8_t ks = crypto1_byte(&_emuCipher, 0, 0); + ar = (ar << 8) | (uint8_t)(ks ^ enc[4 + i]); + } + _emuDbgNt = _emuNt; + _emuDbgBlock = block; + memcpy(_emuDbgEnc, enc, 8); + _emuDbgArCalc = ar; + _emuDbgArExp = prng_successor(_emuNt, 64); + if (ar != _emuDbgArExp) { + _emuMfcBadAr++; + _emuMfcAuthed = false; + _emuParityOff(); + return false; + } + + // Answer AT = suc3(nt), encrypted with encrypted parity. + uint32_t at = prng_successor(_emuNt, 96); + uint8_t atEnc[4], atPar[4]; + for (int i = 0; i < 4; i++) { + uint8_t b = (uint8_t)(at >> (24 - 8 * i)); + atEnc[i] = (uint8_t)(crypto1_byte(&_emuCipher, 0, 0) ^ b); + atPar[i] = (uint8_t)(crypto1_filter_bit(&_emuCipher) ^ nfc_oddparity(b)); + } + uint8_t bs[8] = {0}; + uint16_t nbits = _mfcPackBits(atEnc, atPar, 4, bs); + _emuTxBits(bs, nbits); + _emuMfcAuthed = true; + _emuMfcAuthOk++; + + // Encrypted session: serve READ/WRITE until HALT, field off or Esc. + while (true) { + // Yield 1ms so the input task can set EscPress (RXE IRQ stays latched, + // so the next reader command is not lost). Lets the user abort mid-session. + vTaskDelay(pdMS_TO_TICKS(1)); + if (EscPress) break; + uint8_t rbuf[40] = {0}; + uint16_t rbits = _emuRxRaw(rbuf, sizeof(rbuf), 40); + if (rbits < 8) break; + uint8_t dec[34] = {0}; + uint8_t cnt = _mfcUnpackBits(rbuf, rbits, dec, sizeof(dec)); + for (uint8_t i = 0; i < cnt; i++) dec[i] ^= crypto1_byte(&_emuCipher, 0, 0); + uint8_t c = dec[0]; + + if (c == 0x30 && cnt >= 2) { // READ block + uint8_t blk = dec[1]; + uint8_t plain[18]; + memcpy(plain, (blk < mfcDump.totalBlocks) ? mfcDump.blocks[blk] : ffKey, 16); + if (blk >= mfcDump.totalBlocks) memset(plain, 0, 16); + uint16_t crc = _hw->rfalCrcCalculateCcitt(0x6363, plain, 16); + plain[16] = (uint8_t)(crc & 0xFF); + plain[17] = (uint8_t)(crc >> 8); + uint8_t e[18], p[18]; + for (int i = 0; i < 18; i++) { + e[i] = (uint8_t)(crypto1_byte(&_emuCipher, 0, 0) ^ plain[i]); + p[i] = (uint8_t)(crypto1_filter_bit(&_emuCipher) ^ nfc_oddparity(plain[i])); + } + uint8_t ob[24] = {0}; + uint16_t nb = _mfcPackBits(e, p, 18, ob); + _emuTxBits(ob, nb); + _emuMfcReads++; + } else if (c == 0xA0 && cnt >= 2) { // WRITE block (2 phases) + uint8_t blk = dec[1]; + // ACK phase 1 (4-bit 0x0A, encrypted) + uint8_t ackbs = 0; + for (int i = 0; i < 4; i++) + ackbs |= (uint8_t)((crypto1_bit(&_emuCipher, 0, 0) ^ ((0x0A >> i) & 1)) << i); + _emuTxBits(&ackbs, 4); + // data phase + uint8_t dbuf[40] = {0}; + uint16_t dbits = _emuRxRaw(dbuf, sizeof(dbuf), 40); + if (dbits < 8) break; + uint8_t dd[20] = {0}; + uint8_t dn = _mfcUnpackBits(dbuf, dbits, dd, sizeof(dd)); + for (uint8_t i = 0; i < dn; i++) dd[i] ^= crypto1_byte(&_emuCipher, 0, 0); + if (blk < mfcDump.totalBlocks && dn >= 16) memcpy(mfcDump.blocks[blk], dd, 16); + uint8_t ack2 = 0; + for (int i = 0; i < 4; i++) + ack2 |= (uint8_t)((crypto1_bit(&_emuCipher, 0, 0) ^ ((0x0A >> i) & 1)) << i); + _emuTxBits(&ack2, 4); + } else if (c == 0x50) { // HALT + break; + } else { + break; + } + } + + _emuMfcAuthed = false; + _emuParityOff(); // restore HW parity so the next plain AUTH is received intact + return true; +} + +// =========================================================================== +// Type 4 Tag (NDEF) emulation over the NFC-A target — Milestone 6 +// =========================================================================== + +void ST25R3916::_buildT4TFiles() { + // Capability Container: NDEF file E1 04, max 256 bytes, read-only. + static const uint8_t cc[15] = { + 0x00, 0x0F, 0x20, 0x00, 0x3B, 0x00, 0x34, 0x04, 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00 + }; + memcpy(_t4tCC, cc, sizeof(cc)); + + uint8_t rec[400]; + uint16_t rl = 0; + if (ndefMessage.payloadSize > 0 && + (ndefMessage.payloadType == NDEF_URI || ndefMessage.payloadType == NDEF_TEXT)) { + // Single short NDEF record from the ndefMessage struct (set via 'rfid ndef'). + rec[0] = 0xD1; // MB|ME|SR, TNF=well-known + rec[1] = 0x01; // type length + rec[2] = ndefMessage.payloadSize; + rec[3] = ndefMessage.payloadType; + memcpy(&rec[4], ndefMessage.payload, ndefMessage.payloadSize); + rl = (uint16_t)(4 + ndefMessage.payloadSize); + } else { + // Default: URI "https://bruce.computer". + const char *url = "bruce.computer"; + uint8_t ul = (uint8_t)strlen(url); + rec[0] = 0xD1; + rec[1] = 0x01; + rec[2] = (uint8_t)(ul + 1); + rec[3] = 0x55; // URI record + rec[4] = 0x04; // prefix "https://" + memcpy(&rec[5], url, ul); + rl = (uint16_t)(5 + ul); + } + + _t4tNdef[0] = (uint8_t)(rl >> 8); + _t4tNdef[1] = (uint8_t)(rl & 0xFF); + memcpy(&_t4tNdef[2], rec, rl); + _t4tNdefLen = (uint16_t)(rl + 2); +} + +uint16_t ST25R3916::_t4tProcessApdu(const uint8_t *c, uint16_t clen, uint8_t *r) { + if (clen < 4) { + r[0] = 0x6A; + r[1] = 0x00; + return 2; + } + uint8_t ins = c[1]; + + if (ins == 0xA4) { // SELECT + if (c[2] == 0x04) { // select by AID (NDEF application) + _t4tSelected = 0; + r[0] = 0x90; + r[1] = 0x00; + return 2; + } + if (c[2] == 0x00 && clen >= 7) { // select EF by file id + uint8_t f0 = c[5], f1 = c[6]; + if (f0 == 0xE1 && f1 == 0x03) { + _t4tSelected = 1; + r[0] = 0x90; + r[1] = 0x00; + return 2; + } + if (f0 == 0xE1 && f1 == 0x04) { + _t4tSelected = 2; + r[0] = 0x90; + r[1] = 0x00; + return 2; + } + } + r[0] = 0x6A; + r[1] = 0x82; + return 2; + } + + if (ins == 0xB0) { // READ BINARY + uint16_t off = (uint16_t)((c[2] << 8) | c[3]); + uint8_t le = (clen >= 5) ? c[4] : 0; + const uint8_t *src; + uint16_t srclen; + if (_t4tSelected == 1) { + src = _t4tCC; + srclen = sizeof(_t4tCC); + } else if (_t4tSelected == 2) { + src = _t4tNdef; + srclen = _t4tNdefLen; + } else { + r[0] = 0x6A; + r[1] = 0x82; + return 2; + } + if (off > srclen) { + r[0] = 0x6A; + r[1] = 0x86; + return 2; + } + uint16_t nn = le; + if (off + nn > srclen) nn = (uint16_t)(srclen - off); + memcpy(r, &src[off], nn); + r[nn] = 0x90; + r[nn + 1] = 0x00; + return (uint16_t)(nn + 2); + } + + r[0] = 0x6D; // INS not supported + r[1] = 0x00; + return 2; +} + +bool ST25R3916::_emuT4THandle(uint8_t *fifo, uint16_t n) { + uint8_t c0 = fifo[0]; + + if (c0 == 0xE0) { // RATS -> ATS + uint8_t ats[5] = {0x05, 0x78, 0x80, 0x70, 0x02}; + _listenRespond(ats, sizeof(ats)); + return true; + } + if ((c0 & 0xF0) == 0xD0) { // PPS -> echo PPS response + uint8_t resp = 0xD0; + _listenRespond(&resp, 1); + return true; + } + if (c0 == 0x02 || c0 == 0x03) { // ISO-DEP I-block carrying an APDU + if (n < 3) return false; + uint16_t apduLen = (uint16_t)(n - 1 - 2); // strip PCB + CRC(2) + uint8_t rapdu[300]; + uint16_t rl = _t4tProcessApdu(&fifo[1], apduLen, rapdu); + uint8_t out[302]; + out[0] = c0; // preserve PCB toggle bit + memcpy(&out[1], rapdu, rl); + _listenRespond(out, (uint16_t)(rl + 1)); + return true; + } + if (c0 == 0xC2) { // S(DESELECT) + uint8_t resp = 0xC2; + _listenRespond(&resp, 1); + return true; + } + if (c0 == 0x50) { // HALT + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_GOTO_SLEEP); + return true; + } + return false; +} + +// =========================================================================== +// FeliCa (NFC-F) emulation — Milestone 6 +// =========================================================================== + +bool ST25R3916::_setupListenModeF() { + _deselectSharedSpiDevices(); + _hw->st25r3916OscOn(); + + auto mres = _hw->rfalSetMode(RFAL_MODE_LISTEN_NFCF, RFAL_BR_212, RFAL_BR_212); + if (mres != ST_ERR_NONE) { + ST25R_LOG("emulate felica: rfalSetMode(LISTEN_NFCF) -> %d", (int)mres); + return false; + } + + _hw->st25r3916WriteRegister( + ST25R3916_REG_OP_CONTROL, + ST25R3916_REG_OP_CONTROL_en | ST25R3916_REG_OP_CONTROL_rx_en | ST25R3916_REG_OP_CONTROL_en_fd_auto_efd + ); + _hw->st25r3916WriteRegister( + ST25R3916_REG_MODE, ST25R3916_REG_MODE_targ_targ | ST25R3916_REG_MODE_om_targ_nfcf + ); + + uint32_t interrupts = ST25R3916_IRQ_MASK_FWL | ST25R3916_IRQ_MASK_TXE | ST25R3916_IRQ_MASK_RXS | + ST25R3916_IRQ_MASK_RXE | ST25R3916_IRQ_MASK_EOF | ST25R3916_IRQ_MASK_WU_F; + _hw->st25r3916ClearInterrupts(); + _hw->st25r3916DisableInterrupts(ST25R3916_IRQ_MASK_ALL); + _hw->st25r3916EnableInterrupts(interrupts); + + // PT Memory F: IDm(8) + PMm(8) + System Code(2). HW auto-answers SENSF_REQ. + uint8_t ptF[18] = {0}; + memcpy(&ptF[0], _felicaIDm, 8); + memcpy(&ptF[8], _felicaPMm, 8); + ptF[16] = _felicaSys[0]; + ptF[17] = _felicaSys[1]; + _hw->st25r3916WritePTMemF(ptF, sizeof(ptF)); + + uint8_t tsn[ST25R3916_PTM_TSN_LEN]; + for (uint8_t i = 0; i < sizeof(tsn); i++) tsn[i] = (uint8_t)esp_random(); + _hw->st25r3916WritePTMemTSN(tsn, sizeof(tsn)); + + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_GOTO_SENSE); + return true; +} + +int ST25R3916::_handleFelicaListen(uint32_t timeoutMs) { + uint32_t deadline = millis() + timeoutMs; + int polls = 0; + uint8_t fifo[64]; + LongPress = true; + while ((int32_t)(deadline - millis()) > 0) { + vTaskDelay(pdMS_TO_TICKS(1)); + if (EscPress) { + ST25R_LOG("emulate felica: Esc — encerrando"); + break; + } + uint32_t irqs = _hw->st25r3916WaitForInterruptsTimed( + ST25R3916_IRQ_MASK_WU_F | ST25R3916_IRQ_MASK_RXE | ST25R3916_IRQ_MASK_EOF, 50 + ); + if (irqs == 0U) continue; + + if (irqs & ST25R3916_IRQ_MASK_WU_F) { + polls++; + ST25R_LOG("emulate felica: reader fez Polling (#%d)", polls); + } + + if (irqs & ST25R3916_IRQ_MASK_RXE) { + uint16_t nb = _hw->st25r3916GetNumFIFOBytes(); + if (nb == 0U || nb > sizeof(fifo)) continue; + _hw->st25r3916ReadFifo(fifo, nb); + // fifo: LEN, CMD, IDm[8], ... — answer Read Without Encryption from dump. + if (nb >= 2 && fifo[1] == 0x06) { // Read Without Encryption + // Minimal RD: respond with status flags 00 00 and zeroed blocks. + // (Real block data depends on a parsed FeliCa dump — out of scope.) + uint8_t resp[13 + 16] = {0}; + uint8_t blocks = (nb > 14) ? fifo[14] : 1; + if (blocks == 0 || blocks > 1) blocks = 1; + uint8_t len = (uint8_t)(13 + 16 * blocks); + resp[0] = len; + resp[1] = 0x07; // response code for 0x06 + memcpy(&resp[2], _felicaIDm, 8); + resp[10] = 0x00; // status flag 1 + resp[11] = 0x00; // status flag 2 + resp[12] = blocks; + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_CLEAR_FIFO); + _hw->st25r3916WriteFifo(resp, len); + _hw->st25r3916SetNumTxBits((uint16_t)(len * 8U)); + _hw->st25r3916ExecuteCommand(ST25R3916_CMD_TRANSMIT_WITH_CRC); + _hw->st25r3916WaitForInterruptsTimed(ST25R3916_IRQ_MASK_TXE, 20); + } + } + + if (irqs & ST25R3916_IRQ_MASK_EOF) { _hw->st25r3916ExecuteCommand(ST25R3916_CMD_GOTO_SENSE); } + } + LongPress = false; + return polls; +} + +int ST25R3916::_emulateFelica() { + // IDm from the loaded tag (printableUID.uid) or a synthetic one. + memset(_felicaIDm, 0, 8); + memset(_felicaPMm, 0xFF, 8); + _felicaSys[0] = 0x88; + _felicaSys[1] = 0xB4; // NDEF system code (common); IDm detection is what matters + + std::vector idm; + int hi = -1; + for (unsigned i = 0; i < printableUID.uid.length(); i++) { + int v = st25HexNibble(printableUID.uid.charAt(i)); + if (v < 0) continue; + if (hi < 0) hi = v; + else { + idm.push_back((uint8_t)((hi << 4) | v)); + hi = -1; + } + } + if (idm.size() >= 8) memcpy(_felicaIDm, idm.data(), 8); + else { + _felicaIDm[0] = 0x01; + _felicaIDm[1] = 0xFE; + uint32_t r = esp_random(); + memcpy(&_felicaIDm[2], &r, 4); + } + // PMm: try to recover from the saved dump ("PMm: .."). + int p = strAllPages.indexOf("PMm:"); + if (p >= 0) { + std::vector pmm; + if (st25ParseHexBytesAfterColon(strAllPages.substring(p, strAllPages.indexOf('\n', p)), pmm) && + pmm.size() >= 8) + memcpy(_felicaPMm, pmm.data(), 8); + } + + stopDiscovery(); + _nfc->rfalNfcDeactivate(false); + _discoveryStarted = false; + + if (!_setupListenModeF()) { + _listenStop(); + return FAILURE; + } + ST25R_LOG( + "emulate felica: IDm=%s sys=%02X%02X (timeout 30s, Esc p/ sair)", + _bytesToHex(_felicaIDm, 8).c_str(), + _felicaSys[0], + _felicaSys[1] + ); + int polls = _handleFelicaListen(30000); + _listenStop(); + ST25R_LOG("emulate felica: encerrado — polls=%d", polls); + return SUCCESS; +} + +#endif // !LITE_VERSION diff --git a/src/modules/rfid/ST25R3916.h b/src/modules/rfid/ST25R3916.h new file mode 100644 index 000000000..384c3c2ac --- /dev/null +++ b/src/modules/rfid/ST25R3916.h @@ -0,0 +1,180 @@ +#pragma once +#if !defined(LITE_VERSION) + +#include "RFIDInterface.h" +#include "crypto1.h" +#include +#include +#include +#include +#include + +class ST25R3916 : public RFIDInterface { +public: + enum CONNECTION_TYPE { SPI_MODE = 0, I2C_MODE = 1 }; + + static constexpr uint8_t I2C_ADDRESS = 0x50; + + ST25R3916(CONNECTION_TYPE connection_type = SPI_MODE); + ~ST25R3916(); + + bool begin() override; + int read(int cardBaudRate = 0) override; + int clone() override; + int erase() override; + int write(int cardBaudRate = 0) override; + int write_ndef() override; + int emulate() override; + int load() override; + int loadFromFile(const String &filepath) override; + int save(String filename) override; + int saveFlipper(String filename) override; + + void stopDiscovery(); + + // Milestone 1 — advanced NFC-A info (NTAG / Ultralight) + String ntagVariant; // "NTAG213", "NTAG215", "NTAG216", "MF Ultralight", ... + uint8_t ntagVersion[8]; // raw GET_VERSION response + uint8_t ntagSignature[32]; // raw READ_SIG response (ECC-P256) + uint32_t ntagCounters[3]; // monotonic counters 0/1/2 + uint8_t ntagTearing[3]; // tearing flags per counter + bool ntagHasVersion = false; + bool ntagHasSignature = false; + bool ntagHasCounters = false; + int _ntagPagesHint = 0; // page count derived from GET_VERSION (0 = unknown) + + // Milestone 5 — MIFARE Classic dump (1K / 4K / Mini) + struct MifareClassicDump { + uint8_t blocks[256][16]; // up to 256 blocks (4K) + uint8_t keyA[40][6]; // recovered Key A per sector + uint8_t keyB[40][6]; // recovered Key B per sector + bool blockRead[256]; // which blocks were read successfully + bool keyAFound[40]; // which Key A were found + bool keyBFound[40]; // which Key B were found + uint8_t sectors; // 5 (Mini), 16 (1K), 40 (4K) + uint16_t totalBlocks; // 20, 64 or 256 + }; + MifareClassicDump mfcDump; + bool mfcLoaded = false; // true when mfcDump holds a valid MIFARE Classic dump + String mfcType; // "Mini", "1K", "4K" + + bool isMifareClassicSak(uint8_t sak) const; + +private: + CONNECTION_TYPE _connection_type; + RfalRfST25R3916Class *_hw = nullptr; + RfalNfcClass *_nfc = nullptr; + SPIClass *_spi = nullptr; + bool _discoveryStarted = false; + + bool _initSPI(); + bool _initI2C(); + bool _startDiscovery(); + bool _pollForTag(rfalNfcDevice **dev, uint32_t timeoutMs = 5000); + void _logOpControl(const char *where); + void _probeField(const char *where); + void _parseDevice(rfalNfcDevice *dev); + String _getNfcaTypeName(uint8_t sak); + void _parseLoadedData(); + int _readDataBlocks(rfalNfcDevice *dev); + int _writeUltralight(rfalNfcDevice *dev); + int _eraseUltralight(rfalNfcDevice *dev); + int _writeNdefBlocks(rfalNfcDevice *dev); + bool _buildLoadedNdefMessage(std::vector &ndefOut); + bool _writeT2TPage(uint8_t page, const uint8_t data[4], bool verify = true); + bool _isUltralightUserPage(int page) const; + bool _writeMagicGen1UID(rfalNfcDevice *dev); + bool _writeMagicGen2UID(rfalNfcDevice *dev); + + // MIFARE Classic (Crypto1) — Milestone 5 + Crypto1State _mfcCipher; + bool _mfcAuthed = false; + int _readMifareClassic(rfalNfcDevice *dev); + uint32_t _mfcUid32() const; + uint8_t _mfcSectorFirstBlock(uint8_t sector) const; + uint8_t _mfcSectorBlockCount(uint8_t sector) const; + uint8_t _mfcBlockToSector(uint16_t block) const; + ::ReturnCode _mifareTransceiveRaw( + uint8_t *txBuf, uint16_t txBits, uint8_t *rxBuf, uint16_t rxCapBytes, uint16_t *rxBits, uint32_t fwt, + uint32_t flags + ); + bool _mifareAuth(uint8_t block, const uint8_t key[6], bool useKeyB); + bool _mifareReadBlock(uint8_t block, uint8_t data[16]); + bool _mifareWriteBlock(uint8_t block, const uint8_t data[16]); + void _mfcHalt(); + int _writeMifareClassic(rfalNfcDevice *dev); // authenticated write of loaded dump + int _writeMifareClassicMagic(rfalNfcDevice *dev); // clone to Magic Gen1 + void _mfcRebuildStrAllPages(); + int _saveMifareClassicFlipper(const String &filename); + + // helpers + String _getNtagVariant(); + bool _readNtagSignature(); + bool _readNtagCounters(); + + // ISO15693 (NFC-V), NFC-B e NFC-F (FeliCa) + int _readNfcV(rfalNfcDevice *dev); + void _parseNfcB(rfalNfcDevice *dev); + int _readFeliCa(rfalNfcDevice *dev); + + // ISO-DEP / Type 4 Tag (T4T): DESFire, NDEF T4T, EMV + int _readIsoDep(rfalNfcDevice *dev); + bool _isoDepApdu(const uint8_t *tx, uint16_t txLen, uint8_t *rx, uint16_t rxCap, uint16_t *rxLen); + bool _readNdefT4T(); + bool _readDESFireInfo(); + bool _probeEmv(); + + // Emulation NFC-A + bool _setupListenMode(const uint8_t *uidBuf, uint8_t uidLen, const uint8_t *atqa, uint8_t sak); + void _listenStop(); + int _handleListenLoop(uint32_t timeoutMs); + bool _listenRespond(const uint8_t *resp, uint16_t len); + int _buildEmuPages(); + + uint8_t _emuPages[256][4]; + int _emuPageCount = 0; + + // MIFARE Classic emulation (listener-side Crypto1) — Milestone 5 + bool _emuIsMfc = false; + Crypto1State _emuCipher; + bool _emuMfcAuthed = false; + uint32_t _emuNt = 0; + void _emuParityOff(); // restore HW parity (TX+RX) after an encrypted session + // Deferred diagnostics (printed on field-off so logging doesn't break timing). + uint16_t _emuMfcAuthReq = 0; + uint16_t _emuMfcAuthOk = 0; + uint16_t _emuMfcBadAr = 0; + uint16_t _emuMfcNoNr = 0; + uint16_t _emuMfcReads = 0; + uint16_t _emuMfcLastNrBits = 0; + uint32_t _emuDbgNt = 0; + uint8_t _emuDbgEnc[8] = {0}; + uint32_t _emuDbgArCalc = 0; + uint32_t _emuDbgArExp = 0; + uint8_t _emuDbgBlock = 0; + bool _buildEmuMfc(); // parse strAllPages -> mfcDump (keys injected into trailers) + bool _emuMfcHandle(uint8_t *fifo, uint16_t n); // handle one MFC command in listen loop + void _emuTxClear(const uint8_t *data, uint8_t n, bool withCrc); // plain TX (HW parity) + void _emuTxBits(const uint8_t *bitstream, uint16_t nbits); // raw TX, no HW parity + uint16_t _emuRxRaw(uint8_t *out, uint8_t maxBytes, uint32_t toMs); // RX keeping parity, returns bits + + // Type 4 Tag (NDEF) emulation over the NFC-A target — Milestone 6 + bool _emuIsT4T = false; + uint8_t _t4tCC[15]; + uint8_t _t4tNdef[512]; // NLEN(2) + NDEF message + uint16_t _t4tNdefLen = 0; + uint8_t _t4tSelected = 0; // 0 = none, 1 = CC, 2 = NDEF + void _buildT4TFiles(); + bool _emuT4THandle(uint8_t *fifo, uint16_t n); + uint16_t _t4tProcessApdu(const uint8_t *c, uint16_t clen, uint8_t *r); + + // FeliCa (NFC-F) emulation — Milestone 6 + uint8_t _felicaIDm[8]; + uint8_t _felicaPMm[8]; + uint8_t _felicaSys[2]; + bool _setupListenModeF(); + int _handleFelicaListen(uint32_t timeoutMs); + int _emulateFelica(); +}; + +#endif // !LITE_VERSION diff --git a/src/modules/rfid/crypto1.cpp b/src/modules/rfid/crypto1.cpp new file mode 100644 index 000000000..45686b661 --- /dev/null +++ b/src/modules/rfid/crypto1.cpp @@ -0,0 +1,86 @@ +#include "crypto1.h" + +#define LF_POLY_ODD (0x29CE5Cu) +#define LF_POLY_EVEN (0x870804u) + +#define C1_BIT(x, n) ((uint32_t)(((uint64_t)(x)) >> (n)) & 1u) +#define C1_BEBIT(x, n) C1_BIT((x), (n) ^ 24) + +// Crypto1 non-linear filter function f(x). +static inline uint32_t crypto1_filter(uint32_t x) { + uint32_t f; + f = 0xf22c0u >> (x & 0xfu) & 16u; + f |= 0x6c9c0u >> ((x >> 4) & 0xfu) & 8u; + f |= 0x3c8b0u >> ((x >> 8) & 0xfu) & 4u; + f |= 0x1e458u >> ((x >> 12) & 0xfu) & 2u; + f |= 0x0d938u >> ((x >> 16) & 0xfu) & 1u; + return C1_BIT(0xEC57E80Au, f); +} + +// Parity of the masked feedback value (LFSR feedback bit). +static inline uint32_t crypto1_parity(uint32_t x) { + x ^= x >> 16; + x ^= x >> 8; + x ^= x >> 4; + return C1_BIT(0x6996u, x & 0xfu); +} + +void crypto1_init(Crypto1State *s, uint64_t key) { + s->odd = 0; + s->even = 0; + for (int i = 47; i > 0; i -= 2) { + s->odd = (s->odd << 1) | C1_BIT(key, (i - 1) ^ 7); + s->even = (s->even << 1) | C1_BIT(key, i ^ 7); + } +} + +uint8_t crypto1_bit(Crypto1State *s, uint8_t in, int is_encrypted) { + uint8_t ret = (uint8_t)crypto1_filter(s->odd); + + uint32_t feedin = (uint32_t)(ret & (is_encrypted ? 1u : 0u)); + feedin ^= (uint32_t)(in ? 1u : 0u); + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= LF_POLY_EVEN & s->even; + s->even = (s->even << 1) | crypto1_parity(feedin); + + uint32_t t = s->odd; + s->odd = s->even; + s->even = t; + + return ret; +} + +uint8_t crypto1_byte(Crypto1State *s, uint8_t in, int is_encrypted) { + uint8_t ret = 0; + for (int i = 0; i < 8; i++) { ret |= (uint8_t)(crypto1_bit(s, (uint8_t)C1_BIT(in, i), is_encrypted) << i); } + return ret; +} + +uint32_t crypto1_word(Crypto1State *s, uint32_t in, int is_encrypted) { + uint32_t ret = 0; + for (int i = 0; i < 32; i++) { + ret |= (uint32_t)crypto1_bit(s, (uint8_t)C1_BEBIT(in, i), is_encrypted) << (24 ^ i); + } + return ret; +} + +uint8_t crypto1_filter_bit(const Crypto1State *s) { return (uint8_t)crypto1_filter(s->odd); } + +static inline uint32_t crypto1_swapendian(uint32_t x) { + x = (x >> 8 & 0xff00ffu) | ((x & 0xff00ffu) << 8); + x = (x >> 16) | (x << 16); + return x; +} + +uint32_t prng_successor(uint32_t x, uint32_t n) { + x = crypto1_swapendian(x); + while (n--) { x = (x >> 1) | ((((x >> 16) ^ (x >> 18) ^ (x >> 19) ^ (x >> 21)) & 1u) << 31); } + return crypto1_swapendian(x); +} + +uint8_t nfc_oddparity(uint8_t b) { + b ^= b >> 4; + b ^= b >> 2; + b ^= b >> 1; + return (uint8_t)((b & 1u) ^ 1u); +} diff --git a/src/modules/rfid/crypto1.h b/src/modules/rfid/crypto1.h new file mode 100644 index 000000000..602f7458d --- /dev/null +++ b/src/modules/rfid/crypto1.h @@ -0,0 +1,40 @@ +/** + * @file crypto1.h + * @brief Compact Crypto1 cipher for MIFARE Classic (reader side). + * + * Re-implementation of the public Crypto1 stream cipher (as described in + * "Dismantling MIFARE Classic", Garcia et al. 2008, and the public Crapto1 + * reference). Used by the ST25R3916 driver to authenticate, read and write + * MIFARE Classic sectors, since the RFAL fork has no native Crypto1 support. + * + * The ST25R3916 must run with manual parity (PAR_TX_NONE / PAR_RX_KEEP) so the + * encrypted ISO14443-A parity bits can be supplied/consumed by software. + */ +#pragma once +#include + +struct Crypto1State { + uint32_t odd; + uint32_t even; +}; + +/** Initialise the cipher state from a 48-bit key (top 16 bits of the uint64 ignored). */ +void crypto1_init(Crypto1State *state, uint64_t key); + +/** Clock the cipher one bit. Returns the keystream bit. */ +uint8_t crypto1_bit(Crypto1State *state, uint8_t in, int is_encrypted); + +/** Clock the cipher one byte (LSB first). Returns the keystream byte. */ +uint8_t crypto1_byte(Crypto1State *state, uint8_t in, int is_encrypted); + +/** Clock the cipher one 32-bit word (big-endian bit order). Returns keystream word. */ +uint32_t crypto1_word(Crypto1State *state, uint32_t in, int is_encrypted); + +/** Current filter output (next keystream bit) — used to compute encrypted parity. */ +uint8_t crypto1_filter_bit(const Crypto1State *state); + +/** MIFARE LCG nonce successor: advances PRNG state x by n cycles. */ +uint32_t prng_successor(uint32_t x, uint32_t n); + +/** ISO14443-A odd parity bit for a data byte (1 when byte has even number of set bits). */ +uint8_t nfc_oddparity(uint8_t b); diff --git a/src/modules/rfid/tag_o_matic.cpp b/src/modules/rfid/tag_o_matic.cpp index 421c4f5bc..7309715c7 100644 --- a/src/modules/rfid/tag_o_matic.cpp +++ b/src/modules/rfid/tag_o_matic.cpp @@ -11,6 +11,9 @@ #include "core/mykeyboard.h" #include "esp_task_wdt.h" //Include for Headless mode (long write trigger watchdog in JS) +#if !defined(LITE_VERSION) +#include "ST25R3916.h" +#endif #include "PN532.h" #include "RFID2.h" @@ -47,6 +50,10 @@ void TagOMatic::set_rfid_module() { #endif case PN532_SPI_MODULE: _rfid = new PN532(PN532::CONNECTION_TYPE::SPI); break; case RC522_SPI_MODULE: _rfid = new RFID2(false); break; +#if !defined(LITE_VERSION) + case ST25R3916_SPI_MODULE: _rfid = new ST25R3916(ST25R3916::SPI_MODE); break; + case ST25R3916_I2C_MODULE: _rfid = new ST25R3916(ST25R3916::I2C_MODE); break; +#endif case M5_RFID2_MODULE: default: _rfid = new RFID2(); break; } @@ -190,6 +197,9 @@ void TagOMatic::dump_card_details() { padprintln("UID: " + _rfid->printableUID.uid); padprintln("ATQA: " + _rfid->printableUID.atqa); padprintln("SAK: " + _rfid->printableUID.sak); + if (_rfid->dataPages > 0 || _rfid->totalPages > 0) { + padprintln("Pages read: " + String(_rfid->dataPages) + "/" + String(_rfid->totalPages)); + } } else { padprintln("IDm: " + _rfid->printableUID.uid); padprintln("PMm: " + _rfid->printableUID.sak); @@ -309,7 +319,7 @@ void TagOMatic::emulate_card() { set_state(EMULATE_MODE); break; case RFIDInterface::NOT_IMPLEMENTED: - displayError("Not implemented for this module.", true); + displayError("Card emulation not supported.", true); set_state(READ_MODE); break; case RFIDInterface::FAILURE: @@ -354,7 +364,7 @@ void TagOMatic::erase_card() { switch (result) { case RFIDInterface::TAG_NOT_PRESENT: return; break; - case RFIDInterface::SUCCESS: displaySuccess("Tag erased successfully."); break; + case RFIDInterface::SUCCESS: displaySuccess("Tag erased successfully.", true); break; default: displayError("Error erasing data from tag."); break; } diff --git a/src/modules/wifi/channel_analyzer.cpp b/src/modules/wifi/channel_analyzer.cpp new file mode 100644 index 000000000..330e110b7 --- /dev/null +++ b/src/modules/wifi/channel_analyzer.cpp @@ -0,0 +1,190 @@ +#if !defined(LITE_VERSION) +#include "channel_analyzer.h" + +#include "esp_err.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nvs_flash.h" + +#include "core/display.h" +#include "core/mykeyboard.h" +#include "core/wifi/wifi_common.h" +#include +#include + +// 2.4GHz channels to sweep. +static const uint8_t CA_CHANNELS[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; +static const int CA_NCH = sizeof(CA_CHANNELS) / sizeof(CA_CHANNELS[0]); + +// Load% at/above which a bar is drawn solid (busy) instead of dimmed. +static const uint8_t CA_BUSY_THRESHOLD = 50; + +// Counters updated from the promiscuous RX callback for the *current* channel. +static volatile uint32_t ca_bytes = 0; +static volatile uint32_t ca_pkts = 0; +static volatile int8_t ca_rssi_peak = -128; + +// Keep the callback minimal: just accumulate. Airtime is estimated in the loop. +static void IRAM_ATTR ca_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) { + const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf; + if (!pkt) return; + ca_pkts++; + ca_bytes += pkt->rx_ctrl.sig_len; + if (pkt->rx_ctrl.rssi > ca_rssi_peak) ca_rssi_peak = pkt->rx_ctrl.rssi; +} + +// Tolerant WiFi bring-up. Unlike the sniffer (ESP_ERROR_CHECK), we never abort: +// if WiFi is already initialised/started we get ESP_ERR_WIFI_INIT_STATE (or +// similar) and just carry on — promiscuous mode works regardless. +static void ca_start_wifi() { + ensureWifiPlatform(); + nvs_flash_init(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t e = esp_wifi_init(&cfg); + if (e != ESP_OK && e != ESP_ERR_WIFI_INIT_STATE) + Serial.printf("[ChAnalyzer] wifi_init: %s\n", esp_err_to_name(e)); + esp_wifi_set_storage(WIFI_STORAGE_RAM); + esp_wifi_set_mode(WIFI_MODE_STA); + e = esp_wifi_start(); + if (e != ESP_OK && e != ESP_ERR_WIFI_INIT_STATE) + Serial.printf("[ChAnalyzer] wifi_start: %s\n", esp_err_to_name(e)); + esp_wifi_set_promiscuous(true); + // Capture every frame type so the airtime estimate reflects real load. + wifi_promiscuous_filter_t filt = {}; + filt.filter_mask = WIFI_PROMIS_FILTER_MASK_ALL; + esp_wifi_set_promiscuous_filter(&filt); + esp_wifi_set_promiscuous_rx_cb(ca_rx_cb); +} + +static void ca_stop_wifi() { + esp_wifi_set_promiscuous(false); + esp_wifi_set_promiscuous_rx_cb(NULL); + esp_wifi_stop(); + wifiDisconnect(); + vTaskDelay(1 / portTICK_RATE_MS); +} + +static void ca_draw( + const uint8_t *load, const uint8_t *peak, const int8_t *rssi, uint8_t curCh, uint16_t dwell +) { + drawMainBorderWithTitle("Channel Analyzer"); + + const int x0 = 8; // left of bars + const int top = 26; // below title + const int bottom = tftHeight - 16; // leave room for footer + const int avail = bottom - top; + const int rowH = avail / CA_NCH; + const int labelW = 30; // "Ch11" + const int valW = 30; // " 100%" + const int barX = x0 + labelW; + const int barW = tftWidth - barX - valW - 6; + + tft.setTextSize(FP); + for (int i = 0; i < CA_NCH; i++) { + uint8_t ch = CA_CHANNELS[i]; + int y = top + i * rowH; + bool isCur = (ch == curCh); + + // label + tft.setTextColor(isCur ? bruceConfig.bgColor : bruceConfig.priColor, isCur ? bruceConfig.priColor : bruceConfig.bgColor); + tft.drawString("Ch" + String(ch), x0, y, 1); + + // bar frame + int bh = rowH - 3; + if (bh < 4) bh = 4; + tft.drawRect(barX, y, barW, bh, bruceConfig.priColor); + // clear interior + tft.fillRect(barX + 1, y + 1, barW - 2, bh - 2, bruceConfig.bgColor); + + // filled portion ~ load% + int fillW = (barW - 2) * load[ch] / 100; + if (fillW > 0) { + // solid above threshold, dimmed below + uint16_t c = (load[ch] >= CA_BUSY_THRESHOLD) ? bruceConfig.priColor : TFT_DARKGREY; + tft.fillRect(barX + 1, y + 1, fillW, bh - 2, c); + } + // peak-hold marker + int peakX = barX + 1 + (barW - 2) * peak[ch] / 100; + if (peakX > barX + 1) tft.drawFastVLine(peakX, y + 1, bh - 2, TFT_RED); + + // value + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + tft.drawString(String(load[ch]) + "%", barX + barW + 4, y, 1); + } + + // footer: current channel detail + signal meter + dwell + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + String foot = "Ch" + String(curCh) + " " + String(load[curCh]) + "% pk" + String(peak[curCh]) + + "% " + String(rssi[curCh]) + "dBm dwell " + String(dwell) + "ms"; + tft.drawString(foot, x0, tftHeight - 13, 1); +} + +void channel_analyzer_setup() { + returnToMenu = false; + + uint8_t load[12] = {0}; + uint8_t peak[12] = {0}; + int8_t rssi[12]; + for (int i = 0; i < 12; i++) rssi[i] = -128; + + uint16_t dwell = 350; // ms per channel, adjustable with Up/Down + int idx = 0; + + ca_start_wifi(); + + tft.fillScreen(bruceConfig.bgColor); + drawMainBorderWithTitle("Channel Analyzer"); + tft.setTextSize(FP); + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + padprintln(""); + padprintln(" sweeping 1-11 ..."); + delay(300); + + for (;;) { + if (returnToMenu) break; + if (check(EscPress)) { + returnToMenu = true; + break; + } + if (check(UpPress) && dwell < 1000) dwell += 100; // longer dwell = more accurate + if (check(DownPress) && dwell > 150) dwell -= 100; // shorter dwell = faster sweep + + uint8_t ch = CA_CHANNELS[idx]; + esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); + + // reset counters for this dwell window + ca_bytes = 0; + ca_pkts = 0; + ca_rssi_peak = -128; + + uint32_t t0 = millis(); + while (millis() - t0 < dwell) { + if (check(EscPress)) { + returnToMenu = true; + break; + } + vTaskDelay(20 / portTICK_PERIOD_MS); + } + if (returnToMenu) break; + + // Estimate airtime utilisation: bytes at a conservative ~6Mbps baseline + // plus per-frame preamble/IFS overhead. Clamp to 0-100%. + uint32_t airtime_us = (ca_bytes * 8UL) / 6UL + ca_pkts * 60UL; + uint32_t dwell_us = (uint32_t)dwell * 1000UL; + uint32_t l = dwell_us ? (airtime_us * 100UL / dwell_us) : 0; + if (l > 100) l = 100; + + load[ch] = (uint8_t)l; + if (load[ch] > peak[ch]) peak[ch] = load[ch]; + rssi[ch] = (ca_rssi_peak == -128) ? 0 : ca_rssi_peak; + + ca_draw(load, peak, rssi, ch, dwell); + + idx = (idx + 1) % CA_NCH; + } + + ca_stop_wifi(); +} + +#endif diff --git a/src/modules/wifi/channel_analyzer.h b/src/modules/wifi/channel_analyzer.h new file mode 100644 index 000000000..720ed2bc8 --- /dev/null +++ b/src/modules/wifi/channel_analyzer.h @@ -0,0 +1,10 @@ +#pragma once + +#if !defined(LITE_VERSION) + +// Real-time 2.4GHz channel utilization analyzer. +// Hops channels 1-11 in promiscuous mode, estimates per-channel airtime load, +// and draws solid-threshold bars with load %, peak hold and a signal meter. +void channel_analyzer_setup(); + +#endif diff --git a/src/modules/wifi/jam_detect.cpp b/src/modules/wifi/jam_detect.cpp new file mode 100644 index 000000000..ec94f47ff --- /dev/null +++ b/src/modules/wifi/jam_detect.cpp @@ -0,0 +1,209 @@ +#if !defined(LITE_VERSION) +#include "jam_detect.h" + +#include "esp_err.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nvs_flash.h" + +#include "core/display.h" +#include "core/mykeyboard.h" +#include "core/wifi/wifi_common.h" +#include +#include + +static const uint8_t JD_CHANNELS[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; +static const int JD_NCH = sizeof(JD_CHANNELS) / sizeof(JD_CHANNELS[0]); + +// HARDWARE CONSTRAINT: the ESP32 has a single WiFi radio, so it can only be +// tuned to one channel at a time — watching "all channels at once" is physically +// impossible. We approximate full-band coverage by hopping rapidly with a very +// short dwell, so a full sweep of channels 1-11 completes in ~1s. We also alert +// the instant a deauth is seen on the current channel rather than waiting for the +// dwell window to expire (see the sampling loop below). +// +// dwell per channel while hopping (ms). ~100ms * 11 channels ≈ 1.1s per sweep. +static const uint16_t JD_DWELL = 100; + +// Counters for the current channel's dwell window. +static volatile uint32_t jd_deauth = 0; +static volatile uint32_t jd_total = 0; + +static void IRAM_ATTR jd_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) { + const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf; + if (!pkt) return; + jd_total++; + if (pkt->rx_ctrl.sig_len < 2) return; + const uint8_t *f = pkt->payload; + uint16_t fc = (uint16_t)f[0] | ((uint16_t)f[1] << 8); + uint8_t ftype = (fc & 0x0C) >> 2; // 0 = management + uint8_t fsub = (fc & 0xF0) >> 4; // 0x0C deauth, 0x0A disassoc + if (ftype == 0x00 && (fsub == 0x0C || fsub == 0x0A)) jd_deauth++; +} + +static void jd_start_wifi() { + ensureWifiPlatform(); + nvs_flash_init(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t e = esp_wifi_init(&cfg); + if (e != ESP_OK && e != ESP_ERR_WIFI_INIT_STATE) + Serial.printf("[JamDetect] wifi_init: %s\n", esp_err_to_name(e)); + esp_wifi_set_storage(WIFI_STORAGE_RAM); + esp_wifi_set_mode(WIFI_MODE_STA); + e = esp_wifi_start(); + if (e != ESP_OK && e != ESP_ERR_WIFI_INIT_STATE) + Serial.printf("[JamDetect] wifi_start: %s\n", esp_err_to_name(e)); + esp_wifi_disconnect(); // drop any STA association so channel hopping isn't locked to one channel + esp_wifi_set_promiscuous(true); + // CRITICAL: deauth/disassoc are management frames — capture MGMT (+DATA for + // an activity reference). Without this the default filter may drop them. + wifi_promiscuous_filter_t filt = {}; + filt.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA; + esp_wifi_set_promiscuous_filter(&filt); + esp_wifi_set_promiscuous_rx_cb(jd_rx_cb); +} + +static void jd_stop_wifi() { + esp_wifi_set_promiscuous(false); + esp_wifi_set_promiscuous_rx_cb(NULL); + esp_wifi_stop(); + wifiDisconnect(); + vTaskDelay(1 / portTICK_RATE_MS); +} + +static void +jd_draw(const uint16_t *dps, const uint16_t *peak, uint32_t thr, uint8_t curCh, int attackCh) { + drawMainBorderWithTitle("Jam Detect"); + tft.setTextSize(FP); + + const int x0 = 8; + int y = 26; + + // status banner + bool attack = (attackCh >= 0); + uint16_t sc = attack ? TFT_RED : TFT_GREEN; + tft.fillRect(x0, y, tftWidth - 2 * x0, 18, sc); + tft.setTextColor(TFT_BLACK, sc); + String banner = attack ? ("ATTACK ch" + String(attackCh) + " " + String(dps[attackCh]) + "/s") + : "scanning... no jamming"; + tft.drawCentreString(banner, tftWidth / 2, y + 3, 1); + y += 24; + + // per-channel deauth bars + const int labelW = 28; + const int valW = 26; + const int barX = x0 + labelW; + const int bottom = tftHeight - 14; + const int rowH = (bottom - y) / JD_NCH; + const int barW = tftWidth - barX - valW - 6; + uint32_t scale = thr * 2; + if (scale < 4) scale = 4; + + for (int i = 0; i < JD_NCH; i++) { + uint8_t ch = JD_CHANNELS[i]; + int ry = y + i * rowH; + bool isCur = (ch == curCh); + bool over = (dps[ch] >= thr); + + tft.setTextColor( + isCur ? bruceConfig.bgColor : bruceConfig.priColor, + isCur ? bruceConfig.priColor : bruceConfig.bgColor + ); + tft.drawString("Ch" + String(ch), x0, ry, 1); + + int bh = rowH - 3; + if (bh < 4) bh = 4; + tft.drawRect(barX, ry, barW, bh, bruceConfig.priColor); + tft.fillRect(barX + 1, ry + 1, barW - 2, bh - 2, bruceConfig.bgColor); + uint32_t fillW = (uint32_t)(barW - 2) * dps[ch] / scale; + if (fillW > (uint32_t)(barW - 2)) fillW = barW - 2; + if (fillW > 0) tft.fillRect(barX + 1, ry + 1, (int)fillW, bh - 2, over ? TFT_RED : bruceConfig.priColor); + // peak-hold marker + uint32_t pkX = (uint32_t)(barX + 1) + (uint32_t)(barW - 2) * peak[ch] / scale; + if (peak[ch] > 0 && pkX > (uint32_t)(barX + 1)) tft.drawFastVLine((int)pkX, ry + 1, bh - 2, TFT_YELLOW); + + tft.setTextColor(over ? TFT_RED : bruceConfig.priColor, bruceConfig.bgColor); + tft.drawString(String(dps[ch]), barX + barW + 4, ry, 1); + } + + tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); + tft.drawString("scan ch" + String(curCh) + " thr" + String(thr) + "/s UP/DN ESC", x0, tftHeight - 12, 1); +} + +void jam_detect_setup() { + returnToMenu = false; + + uint16_t dps[12] = {0}; // last measured deauth/s per channel (latched) + uint16_t peak[12] = {0}; // peak-hold + uint32_t threshold = 10; // deauth/s to flag (adjustable) + int idx = 0; + + jd_start_wifi(); + tft.fillScreen(bruceConfig.bgColor); + + for (;;) { + if (returnToMenu) break; + + uint8_t ch = JD_CHANNELS[idx]; + esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); + vTaskDelay(5 / portTICK_PERIOD_MS); // brief radio settle (kept short vs the dwell) + + jd_deauth = 0; + jd_total = 0; + + // Sample the current channel for one short dwell. The deauth/sec rate is + // extrapolated from however many frames we caught in this window: + // rate = deauths * (1000 / dwell_ms) + // so even a single deauth in a ~100ms window registers strongly. + // IMMEDIATE ALERT: we poll the live counter inside the dwell and, the + // moment the extrapolated rate reaches the threshold, we latch the alert + // and redraw RIGHT AWAY instead of waiting for the dwell to finish. + bool tripped = false; + uint32_t t0 = millis(); + while (millis() - t0 < JD_DWELL) { + if (check(EscPress)) { + returnToMenu = true; + break; + } + if (check(UpPress) && threshold < 250) threshold += 5; + if (check(DownPress) && threshold > 5) threshold -= 5; + + // live extrapolated rate for the deauths seen so far this window + uint32_t live = (uint32_t)jd_deauth * 1000UL / JD_DWELL; + if (live >= threshold) { + tripped = true; + break; // detected — stop dwelling, draw immediately, then hop on + } + vTaskDelay(5 / portTICK_PERIOD_MS); + } + if (returnToMenu) break; + + uint32_t d = (uint32_t)jd_deauth * 1000UL / JD_DWELL; + if (d > 65535) d = 65535; + dps[ch] = (uint16_t)d; + if (dps[ch] > peak[ch]) peak[ch] = dps[ch]; + + // worst channel currently at/over threshold (latched values persist + // across the sweep so an attack stays flagged after we hop away). + // `tripped` guarantees the channel we just left is considered even if a + // later channel happens to read higher this redraw. + int attackCh = -1; + uint16_t worst = 0; + for (int i = 0; i < JD_NCH; i++) { + uint8_t c = JD_CHANNELS[i]; + if (dps[c] >= threshold && dps[c] >= worst) { + worst = dps[c]; + attackCh = c; + } + } + if (tripped && attackCh < 0) attackCh = ch; // ensure the live trip is shown + + jd_draw(dps, peak, threshold, ch, attackCh); + idx = (idx + 1) % JD_NCH; + } + + jd_stop_wifi(); +} + +#endif diff --git a/src/modules/wifi/jam_detect.h b/src/modules/wifi/jam_detect.h new file mode 100644 index 000000000..5dad170e9 --- /dev/null +++ b/src/modules/wifi/jam_detect.h @@ -0,0 +1,10 @@ +#pragma once + +#if !defined(LITE_VERSION) + +// Deauth/disassoc flood ("jamming") detector. +// Watches management-frame deauth rate on the selected channel and raises a +// visual alert when it crosses a user-adjustable threshold. +void jam_detect_setup(); + +#endif diff --git a/tools/gen_mfcodes.py b/tools/gen_mfcodes.py new file mode 100644 index 000000000..4922455fd --- /dev/null +++ b/tools/gen_mfcodes.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Generate the encrypted, built-in KeeLoq manufacturer keystore header. + +The plaintext keystore (one `name;key_hex;type` entry per line) is the source +of truth and is kept OUT of the repository (default: .idea/mfcodes, which is +gitignored). This script encrypts it with AES-256-CBC and emits a C header with +the key, IV and ciphertext, which IS committed. At runtime the firmware decrypts +this blob as a fallback when no external /mfcodes file is present. + +Encryption is obfuscation, not real security: the AES key ships in the binary. +The goal is simply to keep the plaintext manufacturer keys out of the git repo +and out of `strings firmware.bin`, mirroring how Flipper-like firmwares ship +their keystore. Re-run this script (rotating the key/IV) whenever the plaintext +keystore changes. + +Requirements: `openssl` in PATH (Git for Windows ships it). Pure stdlib otherwise. + +Usage: + python tools/gen_mfcodes.py [plaintext_path] [header_path] +""" +import os +import subprocess +import sys +import tempfile + +DEFAULT_PLAIN = ".idea/mfcodes" +DEFAULT_HEADER = "src/modules/rf/protocols/rf_keeloq_mfcodes_data.h" + + +def normalize(plaintext_path): + """Read the plaintext keystore and keep only valid `name;key;type` lines.""" + lines = [] + with open(plaintext_path, "r", encoding="utf-8") as f: + for raw in f: + line = raw.strip() + if not line: + continue + cols = line.split(";") + if len(cols) != 3: + print(f" skipping malformed line: {line!r}", file=sys.stderr) + continue + lines.append(";".join(c.strip() for c in cols)) + if not lines: + sys.exit("error: no valid entries in plaintext keystore") + # Trailing newline so the C parser sees the last entry cleanly. + return ("\n".join(lines) + "\n").encode("utf-8") + + +def openssl_cbc(data, key, iv, decrypt=False): + """Run AES-256-CBC (PKCS7) through the openssl CLI, return the result bytes.""" + with tempfile.TemporaryDirectory() as d: + src = os.path.join(d, "in.bin") + dst = os.path.join(d, "out.bin") + with open(src, "wb") as f: + f.write(data) + cmd = [ + "openssl", "enc", "-aes-256-cbc", + "-K", key.hex(), "-iv", iv.hex(), + "-in", src, "-out", dst, + ] + if decrypt: + cmd.append("-d") + subprocess.run(cmd, check=True) + with open(dst, "rb") as f: + return f.read() + + +def c_array(name, data, ctype="uint8_t"): + body = [] + for i in range(0, len(data), 12): + chunk = ", ".join(f"0x{b:02X}" for b in data[i:i + 12]) + body.append(" " + chunk + ",") + return f"static const {ctype} {name}[] = {{\n" + "\n".join(body) + "\n};\n" + + +def main(): + plain_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PLAIN + header_path = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_HEADER + + if not os.path.exists(plain_path): + sys.exit( + f"error: plaintext keystore not found: {plain_path}\n" + "Provide it as the first argument or place it at .idea/mfcodes." + ) + + plaintext = normalize(plain_path) + key = os.urandom(32) + iv = os.urandom(16) + + ciphertext = openssl_cbc(plaintext, key, iv) + + # Self-check: decrypt back and compare, so a committed header is never wrong. + roundtrip = openssl_cbc(ciphertext, key, iv, decrypt=True) + if roundtrip != plaintext: + sys.exit("error: AES round-trip self-check failed") + + header = ( + "#pragma once\n" + "#include \n\n" + "// Auto-generated by tools/gen_mfcodes.py - DO NOT EDIT.\n" + "// Encrypted KeeLoq manufacturer keystore (AES-256-CBC, PKCS7).\n" + "// Plaintext source is kept out of the repo; re-run the generator to\n" + "// rotate the key/IV and refresh this blob after changing the keystore.\n\n" + + c_array("KEELOQ_MFCODES_KEY", key) + + "\n" + + c_array("KEELOQ_MFCODES_IV", iv) + + "\n" + + c_array("KEELOQ_MFCODES_ENC", ciphertext) + + f"\nstatic const uint32_t KEELOQ_MFCODES_ENC_LEN = {len(ciphertext)};\n" + ) + + os.makedirs(os.path.dirname(header_path), exist_ok=True) + with open(header_path, "w", encoding="utf-8", newline="\n") as f: + f.write(header) + + entries = plaintext.decode("utf-8").count("\n") + print(f"wrote {header_path}: {entries} entries, " + f"{len(plaintext)} B plaintext -> {len(ciphertext)} B ciphertext") + + +if __name__ == "__main__": + main()