From 3563bccdf8dd0c082875f79aa96b797bddae4c18 Mon Sep 17 00:00:00 2001 From: noproto Date: Tue, 1 Oct 2024 23:49:12 -0400 Subject: [PATCH 01/74] Xero initial commit --- .github/assets/xero.svg | 56 +++++++++++++++++++++++++ ReadMe.md | 93 +++++++++++++++-------------------------- 2 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 .github/assets/xero.svg diff --git a/.github/assets/xero.svg b/.github/assets/xero.svg new file mode 100644 index 000000000..21d84837c --- /dev/null +++ b/.github/assets/xero.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + XERO + FIRMWARE + + + + XERO + FIRMWARE + + + + + + + + + + + + + + + + + + + + + + + + + + + + XERO + FIRMWARE + + + diff --git a/ReadMe.md b/ReadMe.md index f4c0dffed..57f0e9ce9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,45 +1,47 @@ - - - - A pixel art of a Dophin with text: Flipper Zero Official Repo - +

+ +

-# Flipper Zero Firmware +Xero is a fork of the [official Flipper Zero firmware](https://github.com/flipperdevices/flipperzero-firmware) that focuses on providing the latest NFC/RFID features, improved memory utilization, a curated selection of applications, and above all: stability. + +Flipper Zero resources: - [Flipper Zero Official Website](https://flipperzero.one). A simple way to explain to your friends what Flipper Zero can do. -- [Flipper Zero Firmware Update](https://update.flipperzero.one). Improvements for your dolphin: latest firmware releases, upgrade tools for PC and mobile devices. -- [User Documentation](https://docs.flipperzero.one). Learn more about your dolphin: specs, usage guides, and anything you want to ask. -- [Developer Documentation](https://developer.flipper.net/flipperzero/doxygen). Dive into the Flipper Zero Firmware source code: build system, firmware structure, and more. +- [Official Documentation](https://docs.flipperzero.one). Learn more about your dolphin: specs, usage guides, and anything you want to ask. +- [Community Wiki](https://flipper.wiki/). Contribute to the community wiki covering what is possible on the Flipper Zero. + +# Why Xero + +Coming from the **Official Firmware** (OFW), you'll get: + +* Newly supported cards and protocols +* Fixes and performance improvements for existing cards and protocols +* Reduced memory usage of core applications +* A curated list of community applications with a focus on utility + +Coming from other custom firmware, you'll get: + +* All of the above +* General stability improvements +* Reduced memory usage +* Minimal theming + +# Xero features + +* Ultralight C attacks (coming soon!) +* Minimal theme (coming soon!) # Contributing Our main goal is to build a healthy and sustainable community around Flipper, so we're open to any new ideas and contributions. We also have some rules and taboos here, so please read this page and our [Code of Conduct](/CODE_OF_CONDUCT.md) carefully. -## I need help - -The best place to search for answers is our [User Documentation](https://docs.flipperzero.one). If you can't find the answer there, check our [Discord Server](https://flipp.dev/discord) or our [Forum](https://forum.flipperzero.one/). If you want to contribute to the firmware development, or modify it for your own needs, you can also check our [Developer Documentation](https://developer.flipper.net/flipperzero/doxygen). - ## I want to report an issue -If you've found an issue and want to report it, please check our [Issues](https://github.com/flipperdevices/flipperzero-firmware/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue. - -## I want to contribute code - -Before opening a PR, please confirm that your changes must be contained in the firmware. Many ideas can easily be implemented as external applications and published in the [Flipper Application Catalog](https://github.com/flipperdevices/flipper-application-catalog). If you are unsure, reach out to us on the [Discord Server](https://flipp.dev/discord) or the [Issues](https://github.com/flipperdevices/flipperzero-firmware/issues) page, and we'll help you find the right place for your code. - -Also, please read our [Contribution Guide](/CONTRIBUTING.md) and our [Coding Style](/CODING_STYLE.md), and make sure your code is compatible with our [Project License](/LICENSE). - -Finally, open a [Pull Request](https://github.com/flipperdevices/flipperzero-firmware/pulls) and make sure that CI/CD statuses are all green. - -# Development - -Flipper Zero Firmware is written in C, with some bits and pieces written in C++ and armv7m assembly languages. An intermediate level of C knowledge is recommended for comfortable programming. C, C++, and armv7m assembly languages are supported for Flipper applications. +If you've found an issue and want to report it, please check our [Issues](https://github.com/noproto/xero-firmware/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue. # Firmware RoadMap -[Firmware RoadMap Miro Board](https://miro.com/app/board/uXjVO_3D6xU=/) +[Firmware RoadMap](https://github.com/noproto/xero-firmware/projects?query=is%3Aopen) ## Requirements @@ -63,7 +65,7 @@ Flipper Build System will take care of all the other dependencies. Make sure you have enough space and clone the source code: ```shell -git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git +git clone --recursive https://github.com/noproto/xero-firmware.git ``` ## Building @@ -87,36 +89,9 @@ Connect your in-circuit debugger to your Flipper and flash firmware using Flippe Make sure your Flipper is on, and your firmware is functioning. Connect your Flipper with a USB cable and flash firmware using Flipper Build Tool: ```shell -./fbt flash_usb +./fbt flash_usb_full ``` -## Documentation - -- [Flipper Build Tool](/documentation/fbt.md) - building, flashing, and debugging Flipper software -- [Applications](/documentation/AppsOnSDCard.md), [Application Manifest](/documentation/AppManifests.md) - developing, building, deploying, and debugging Flipper applications -- [Hardware combos and Un-bricking](/documentation/KeyCombo.md) - recovering your Flipper from the most nasty situations -- [Flipper File Formats](/documentation/file_formats) - everything about how Flipper stores your data and how you can work with it -- [Universal Remotes](/documentation/UniversalRemotes.md) - contributing your infrared remote to the universal remote database -- [Firmware Roadmap](https://miro.com/app/board/uXjVO_3D6xU=/) -- And much more in the [Developer Documentation](https://developer.flipper.net/flipperzero/doxygen) - -# Project structure - -- `applications` - Applications and services used in firmware -- `applications_users` - Place for your additional applications and services -- `assets` - Assets used by applications and services -- `documentation` - Documentation generation system configs and input files -- `furi` - Furi Core: OS-level primitives and helpers -- `lib` - Our and 3rd party libraries, drivers, tools and etc... -- `site_scons` - Build system configuration and modules -- `scripts` - Supplementary scripts and various python libraries -- `targets` - Firmware targets: platform specific code - -Also, see `ReadMe.md` files inside those directories for further details. - # Links -- Discord: [flipp.dev/discord](https://flipp.dev/discord) -- Website: [flipperzero.one](https://flipperzero.one) -- Forum: [forum.flipperzero.one](https://forum.flipperzero.one/) -- Kickstarter: [kickstarter.com](https://www.kickstarter.com/projects/flipper-devices/flipper-zero-tamagochi-for-hackers) +- Official Discord: [flipp.dev/discord](https://flipp.dev/discord) From 399326c1a4145e53a542df41c592ad646eb3acd7 Mon Sep 17 00:00:00 2001 From: noproto Date: Tue, 1 Oct 2024 23:49:12 -0400 Subject: [PATCH 02/74] Xero initial commit --- .github/assets/xero.svg | 56 +++++++++++++++++++++++++ ReadMe.md | 93 +++++++++++++++-------------------------- 2 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 .github/assets/xero.svg diff --git a/.github/assets/xero.svg b/.github/assets/xero.svg new file mode 100644 index 000000000..21d84837c --- /dev/null +++ b/.github/assets/xero.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + XERO + FIRMWARE + + + + XERO + FIRMWARE + + + + + + + + + + + + + + + + + + + + + + + + + + + + XERO + FIRMWARE + + + diff --git a/ReadMe.md b/ReadMe.md index f4c0dffed..57f0e9ce9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,45 +1,47 @@ - - - - A pixel art of a Dophin with text: Flipper Zero Official Repo - +

+ +

-# Flipper Zero Firmware +Xero is a fork of the [official Flipper Zero firmware](https://github.com/flipperdevices/flipperzero-firmware) that focuses on providing the latest NFC/RFID features, improved memory utilization, a curated selection of applications, and above all: stability. + +Flipper Zero resources: - [Flipper Zero Official Website](https://flipperzero.one). A simple way to explain to your friends what Flipper Zero can do. -- [Flipper Zero Firmware Update](https://update.flipperzero.one). Improvements for your dolphin: latest firmware releases, upgrade tools for PC and mobile devices. -- [User Documentation](https://docs.flipperzero.one). Learn more about your dolphin: specs, usage guides, and anything you want to ask. -- [Developer Documentation](https://developer.flipper.net/flipperzero/doxygen). Dive into the Flipper Zero Firmware source code: build system, firmware structure, and more. +- [Official Documentation](https://docs.flipperzero.one). Learn more about your dolphin: specs, usage guides, and anything you want to ask. +- [Community Wiki](https://flipper.wiki/). Contribute to the community wiki covering what is possible on the Flipper Zero. + +# Why Xero + +Coming from the **Official Firmware** (OFW), you'll get: + +* Newly supported cards and protocols +* Fixes and performance improvements for existing cards and protocols +* Reduced memory usage of core applications +* A curated list of community applications with a focus on utility + +Coming from other custom firmware, you'll get: + +* All of the above +* General stability improvements +* Reduced memory usage +* Minimal theming + +# Xero features + +* Ultralight C attacks (coming soon!) +* Minimal theme (coming soon!) # Contributing Our main goal is to build a healthy and sustainable community around Flipper, so we're open to any new ideas and contributions. We also have some rules and taboos here, so please read this page and our [Code of Conduct](/CODE_OF_CONDUCT.md) carefully. -## I need help - -The best place to search for answers is our [User Documentation](https://docs.flipperzero.one). If you can't find the answer there, check our [Discord Server](https://flipp.dev/discord) or our [Forum](https://forum.flipperzero.one/). If you want to contribute to the firmware development, or modify it for your own needs, you can also check our [Developer Documentation](https://developer.flipper.net/flipperzero/doxygen). - ## I want to report an issue -If you've found an issue and want to report it, please check our [Issues](https://github.com/flipperdevices/flipperzero-firmware/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue. - -## I want to contribute code - -Before opening a PR, please confirm that your changes must be contained in the firmware. Many ideas can easily be implemented as external applications and published in the [Flipper Application Catalog](https://github.com/flipperdevices/flipper-application-catalog). If you are unsure, reach out to us on the [Discord Server](https://flipp.dev/discord) or the [Issues](https://github.com/flipperdevices/flipperzero-firmware/issues) page, and we'll help you find the right place for your code. - -Also, please read our [Contribution Guide](/CONTRIBUTING.md) and our [Coding Style](/CODING_STYLE.md), and make sure your code is compatible with our [Project License](/LICENSE). - -Finally, open a [Pull Request](https://github.com/flipperdevices/flipperzero-firmware/pulls) and make sure that CI/CD statuses are all green. - -# Development - -Flipper Zero Firmware is written in C, with some bits and pieces written in C++ and armv7m assembly languages. An intermediate level of C knowledge is recommended for comfortable programming. C, C++, and armv7m assembly languages are supported for Flipper applications. +If you've found an issue and want to report it, please check our [Issues](https://github.com/noproto/xero-firmware/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue. # Firmware RoadMap -[Firmware RoadMap Miro Board](https://miro.com/app/board/uXjVO_3D6xU=/) +[Firmware RoadMap](https://github.com/noproto/xero-firmware/projects?query=is%3Aopen) ## Requirements @@ -63,7 +65,7 @@ Flipper Build System will take care of all the other dependencies. Make sure you have enough space and clone the source code: ```shell -git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git +git clone --recursive https://github.com/noproto/xero-firmware.git ``` ## Building @@ -87,36 +89,9 @@ Connect your in-circuit debugger to your Flipper and flash firmware using Flippe Make sure your Flipper is on, and your firmware is functioning. Connect your Flipper with a USB cable and flash firmware using Flipper Build Tool: ```shell -./fbt flash_usb +./fbt flash_usb_full ``` -## Documentation - -- [Flipper Build Tool](/documentation/fbt.md) - building, flashing, and debugging Flipper software -- [Applications](/documentation/AppsOnSDCard.md), [Application Manifest](/documentation/AppManifests.md) - developing, building, deploying, and debugging Flipper applications -- [Hardware combos and Un-bricking](/documentation/KeyCombo.md) - recovering your Flipper from the most nasty situations -- [Flipper File Formats](/documentation/file_formats) - everything about how Flipper stores your data and how you can work with it -- [Universal Remotes](/documentation/UniversalRemotes.md) - contributing your infrared remote to the universal remote database -- [Firmware Roadmap](https://miro.com/app/board/uXjVO_3D6xU=/) -- And much more in the [Developer Documentation](https://developer.flipper.net/flipperzero/doxygen) - -# Project structure - -- `applications` - Applications and services used in firmware -- `applications_users` - Place for your additional applications and services -- `assets` - Assets used by applications and services -- `documentation` - Documentation generation system configs and input files -- `furi` - Furi Core: OS-level primitives and helpers -- `lib` - Our and 3rd party libraries, drivers, tools and etc... -- `site_scons` - Build system configuration and modules -- `scripts` - Supplementary scripts and various python libraries -- `targets` - Firmware targets: platform specific code - -Also, see `ReadMe.md` files inside those directories for further details. - # Links -- Discord: [flipp.dev/discord](https://flipp.dev/discord) -- Website: [flipperzero.one](https://flipperzero.one) -- Forum: [forum.flipperzero.one](https://forum.flipperzero.one/) -- Kickstarter: [kickstarter.com](https://www.kickstarter.com/projects/flipper-devices/flipper-zero-tamagochi-for-hackers) +- Official Discord: [flipp.dev/discord](https://flipp.dev/discord) From 68a71eebb7db0a2030235f2d8db67d17844349eb Mon Sep 17 00:00:00 2001 From: noproto Date: Wed, 2 Oct 2024 00:33:58 -0400 Subject: [PATCH 03/74] Update README.md with feature goals --- ReadMe.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 57f0e9ce9..e1e4a6a39 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -28,8 +28,15 @@ Coming from other custom firmware, you'll get: # Xero features -* Ultralight C attacks (coming soon!) -* Minimal theme (coming soon!) +* **MIFARE Classic Key recovery improvements** + * *MIFARE Classic Accelerated dictionary attack*: dictionary attacks reduced to several seconds - checks ~3500 keys per second + * *MIFARE Classic Nested attack support*: collects nested nonces to be cracked by MFKey + * *MIFARE Classic Static encrypted backdoor support*: collects static encrypted nonces to be cracked by MFKey using NXP/Fudan backdoor +* **MFKey 3.0**: Mfkey32, Static Nested, and Static Encrypted attacks all on your Flipper Zero (coming soon!) +* **MIFARE Ultralight C Dictionary attack** (coming soon!) +* **MIFARE Ultralight C Emulation** (coming soon!) +* **NFC app memory improvements** (coming soon!) +* **Minimal theme** (coming soon!) # Contributing From 619445da4b30afa004db0dab289e1bbe74cfaa48 Mon Sep 17 00:00:00 2001 From: noproto Date: Tue, 8 Oct 2024 22:42:30 -0400 Subject: [PATCH 04/74] Add MFKey to firmware --- ReadMe.md | 34 +- applications/ReadMe.md | 1 + applications/system/application.fam | 1 + applications/system/mfkey/.catalog/README.md | 12 + .../system/mfkey/.catalog/changelog.md | 18 + .../system/mfkey/.catalog/screenshots/1.png | Bin 0 -> 4277 bytes .../system/mfkey/.catalog/screenshots/2.png | Bin 0 -> 6980 bytes .../system/mfkey/.catalog/screenshots/3.png | Bin 0 -> 5937 bytes applications/system/mfkey/application.fam | 28 + applications/system/mfkey/crypto1.c | 22 + applications/system/mfkey/crypto1.h | 256 +++++ applications/system/mfkey/images/mfkey.png | Bin 0 -> 107 bytes applications/system/mfkey/init_plugin.c | 356 +++++++ applications/system/mfkey/mfkey.c | 915 ++++++++++++++++++ applications/system/mfkey/mfkey.h | 108 +++ applications/system/mfkey/mfkey.png | Bin 0 -> 107 bytes applications/system/mfkey/plugin_interface.h | 13 + 17 files changed, 1747 insertions(+), 17 deletions(-) create mode 100644 applications/system/mfkey/.catalog/README.md create mode 100644 applications/system/mfkey/.catalog/changelog.md create mode 100644 applications/system/mfkey/.catalog/screenshots/1.png create mode 100644 applications/system/mfkey/.catalog/screenshots/2.png create mode 100644 applications/system/mfkey/.catalog/screenshots/3.png create mode 100644 applications/system/mfkey/application.fam create mode 100644 applications/system/mfkey/crypto1.c create mode 100644 applications/system/mfkey/crypto1.h create mode 100644 applications/system/mfkey/images/mfkey.png create mode 100644 applications/system/mfkey/init_plugin.c create mode 100644 applications/system/mfkey/mfkey.c create mode 100644 applications/system/mfkey/mfkey.h create mode 100644 applications/system/mfkey/mfkey.png create mode 100644 applications/system/mfkey/plugin_interface.h diff --git a/ReadMe.md b/ReadMe.md index e1e4a6a39..0420d969d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -14,29 +14,29 @@ Flipper Zero resources: Coming from the **Official Firmware** (OFW), you'll get: -* Newly supported cards and protocols -* Fixes and performance improvements for existing cards and protocols -* Reduced memory usage of core applications -* A curated list of community applications with a focus on utility +- Newly supported cards and protocols +- Fixes and performance improvements for existing cards and protocols +- Reduced memory usage of core applications +- A curated list of community applications with a focus on utility Coming from other custom firmware, you'll get: -* All of the above -* General stability improvements -* Reduced memory usage -* Minimal theming +- All of the above +- General stability improvements +- Reduced memory usage +- Minimal theming # Xero features -* **MIFARE Classic Key recovery improvements** - * *MIFARE Classic Accelerated dictionary attack*: dictionary attacks reduced to several seconds - checks ~3500 keys per second - * *MIFARE Classic Nested attack support*: collects nested nonces to be cracked by MFKey - * *MIFARE Classic Static encrypted backdoor support*: collects static encrypted nonces to be cracked by MFKey using NXP/Fudan backdoor -* **MFKey 3.0**: Mfkey32, Static Nested, and Static Encrypted attacks all on your Flipper Zero (coming soon!) -* **MIFARE Ultralight C Dictionary attack** (coming soon!) -* **MIFARE Ultralight C Emulation** (coming soon!) -* **NFC app memory improvements** (coming soon!) -* **Minimal theme** (coming soon!) +- **MIFARE Classic Key recovery improvements** + - *MIFARE Classic Accelerated dictionary attack*: dictionary attacks reduced to several seconds - checks ~3500 keys per second + - *MIFARE Classic Nested attack support*: collects nested nonces to be cracked by MFKey + - *MIFARE Classic Static encrypted backdoor support*: collects static encrypted nonces to be cracked by MFKey using NXP/Fudan backdoor +- **MFKey 3.0**: Mfkey32, Static Nested, and Static Encrypted attacks all on your Flipper Zero +- **MIFARE Ultralight C Dictionary attack** (coming soon!) +- **MIFARE Ultralight C Emulation** (coming soon!) +- **NFC app memory improvements** (coming soon!) +- **Minimal theme** (coming soon!) # Contributing diff --git a/applications/ReadMe.md b/applications/ReadMe.md index 44bd8c5d7..dd664f37b 100644 --- a/applications/ReadMe.md +++ b/applications/ReadMe.md @@ -79,3 +79,4 @@ Utility apps not visible in other menus, plus few external apps pre-packaged wit - `snake_game` - Snake game - `storage_move_to_sd` - Data migration tool for internal storage - `updater` - Update service & application +- `mfkey` - MIFARE Classic key recovery tool diff --git a/applications/system/application.fam b/applications/system/application.fam index c5f81defa..42e273c40 100644 --- a/applications/system/application.fam +++ b/applications/system/application.fam @@ -6,6 +6,7 @@ App( "updater_app", "js_app", "js_app_start", + "mfkey", # "archive", ], ) diff --git a/applications/system/mfkey/.catalog/README.md b/applications/system/mfkey/.catalog/README.md new file mode 100644 index 000000000..7f130b41f --- /dev/null +++ b/applications/system/mfkey/.catalog/README.md @@ -0,0 +1,12 @@ +# Flipper Zero MFKey + +This application allows you to calculate the keys of MIFARE Classic cards using the Mfkey32 and Nested algorithms directly on your Flipper Zero. After collecting the nonces using the Extract MF Keys feature of the NFC app, they can be used to calculate the keys to the card in the MFKey app. + +## Usage + +After collecting nonces using the Extract MF Keys option, press the Start button in the MFKey app and wait for it to finish. The calculation can take more than 10 minutes, so you'll have to be patient. After the calculation is complete, the keys will be saved to the user key dictionary. + +## Credits + +Developers: noproto, AG, Flipper Devices, WillyJL +Thanks: AloneLiberty, Foxushka, bettse, Equip diff --git a/applications/system/mfkey/.catalog/changelog.md b/applications/system/mfkey/.catalog/changelog.md new file mode 100644 index 000000000..2b05351e0 --- /dev/null +++ b/applications/system/mfkey/.catalog/changelog.md @@ -0,0 +1,18 @@ +## 3.0 + - Added Static Encrypted Nested key recovery, added NFC app support, dropped FlipperNested support +## 2.7 + - Mfkey32 recovery is 30% faster, fix UI and slowdown bugs +## 2.6 + - Version bump for catalog build system +## 2.5 + - Plugin path fixed +## 2.4 + - Update API for app rename +## 2.3 + - Update API v65.0 +## 2.0 + - Added Nested key recovery, use new KeysDict API, fix crashes, more efficient RAM utilization, faster +## 1.1 + - Rework application with new NFC API +## 1.0 + - Initial release diff --git a/applications/system/mfkey/.catalog/screenshots/1.png b/applications/system/mfkey/.catalog/screenshots/1.png new file mode 100644 index 0000000000000000000000000000000000000000..c3cc6886773d50587294fc05a8afa563e0458fec GIT binary patch literal 4277 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 literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/.catalog/screenshots/2.png b/applications/system/mfkey/.catalog/screenshots/2.png new file mode 100644 index 0000000000000000000000000000000000000000..9fc4407e59b9325ddd61586244ac152706ec1917 GIT binary patch literal 6980 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ǯ?neT@~YA$GQ|E+iSE{IhH6FCgTN$@4PJ znyux-9IK-Od&6$qM`ZQ;XGpg^|khDai3>OI-B<3Y6F&B_+n*kT$}u<-=88`fS^} z!}~RlNSc>My0U#cKaeGWX;a4iaCKev2gW)kYr@uWF9>SK7@FCQ zp?RcuCMuMyX4cJ8H41bvH0ERms6aMuH%V&Azl&klq+Aye$Y`SlW%nG)MS_W>5s2q) z)*!E}>tizzU|Lg|%)GeKWeJGcGJA(}M|Uo>sZF>HOuSX@R*%W#(6B(IiiCOnaGkWD zU~*;K{98wNTrXwWB6(DWTiI2R-{4!m?30okZ{BLMb~$#bGb`5Yv8-ejpd4QW>4$C; z+V8EtD;)^_L%CPCr~*^o8SZ@H^>e|4EdH=dil8OY+4vVK8f0Ns0PheqdlSDs*}#mO zmh2bl{y_;CK93mjOMV`L82sSxuG;5@0yxH9a14&D%nxyC?Z^ttIq+S@SD?vCyoO=NnMz{s2#c^NY%|L+m#y5ZjG9JnOUn()Ug8 zEF%q}DgeUr22VMSzkaDz{pt>FwD|Dt0zncC`vGmr!2DJRN5(BM(oFn=yEu)XV`)#; z_7cd(k0DK4a04nJAgg=OHgn5-X(d>tJySVBu}=g?q!rHi6dcJGF(*=9+`F)>3B>Am zqHoZRTzMYVRvw`*2F0q_4SZ%b>y@`}{kRYGa>}0h*KvZhd0mR=NZ(xD&x0PH_%Ee^ zkdH<2vK6nq8G<8c&&jz#wXFl$G>%~S`FFTrxzAZB7Tq?c3_B^r@LKg2<;A_USzQ{s zc9ITPu~2M|^FvT?fPWy>>%z+#Em3&82XR} z{%7p1r{-@;Jr)>Cn9kwkmSO3Y5^mt`K5a?x<|Vh*C;k-pwv5I(*AStM%(>pPM@18r zBmg0sC_xe0 zlshBy0wwTAu0l!pwXtseg|Ft~v0Uryaec%h>Mw$y`VCF(hymow-2$KC{p*6pnqRC`t+L z@mofGO>UE$U-0M;nf39t){0v=Q%jlWcA|yn*Y;cr7p1SkmEIub&63haW-^bilStRg z?1HxDr>dBz+tH2;%k8F!Q;4#|N$8fI*WD zkPwrQ5)qRWkq|c)mscQ*MG{g%VqywnVlJ?`n*T*`^SJBc82FzAIRz={e+bE8l5jFX p;D0LkxVWLceeSq<{s%`|OhHoiA5LsS;tZJsbTm-vW!LQ<{}(gqvjhMD literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/.catalog/screenshots/3.png b/applications/system/mfkey/.catalog/screenshots/3.png new file mode 100644 index 0000000000000000000000000000000000000000..7eebc6377c82fd002f60f96077578c0f4307f94c GIT binary patch literal 5937 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 X{6C#c`qnxSI6zy&K)p)UKIT6F&zg|h literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/application.fam b/applications/system/mfkey/application.fam new file mode 100644 index 000000000..dfd513847 --- /dev/null +++ b/applications/system/mfkey/application.fam @@ -0,0 +1,28 @@ +App( + appid="mfkey", + name="MFKey", + apptype=FlipperAppType.EXTERNAL, + targets=["f7"], + entry_point="mfkey_main", + requires=[ + "gui", + "storage", + ], + stack_size=1 * 1024, + fap_icon="mfkey.png", + fap_category="NFC", + fap_author="@noproto", + fap_icon_assets="images", + fap_weburl="https://github.com/noproto/FlipperMfkey", + fap_description="MIFARE Classic key recovery tool", + fap_version="3.0", +) + +App( + appid="mfkey_init_plugin", + apptype=FlipperAppType.PLUGIN, + entry_point="init_plugin_ep", + requires=["mfkey"], + sources=["init_plugin.c"], + fal_embedded=True, +) diff --git a/applications/system/mfkey/crypto1.c b/applications/system/mfkey/crypto1.c new file mode 100644 index 000000000..e862b14d1 --- /dev/null +++ b/applications/system/mfkey/crypto1.c @@ -0,0 +1,22 @@ +#pragma GCC optimize("O3") +#pragma GCC optimize("-funroll-all-loops") + +#include +#include "crypto1.h" +#include "mfkey.h" + +#define BIT(x, n) ((x) >> (n) & 1) + +void crypto1_get_lfsr(struct Crypto1State* state, MfClassicKey* lfsr) { + int i; + uint64_t lfsr_value = 0; + for(i = 23; i >= 0; --i) { + lfsr_value = lfsr_value << 1 | BIT(state->odd, i ^ 3); + lfsr_value = lfsr_value << 1 | BIT(state->even, i ^ 3); + } + + // Assign the key value to the MfClassicKey struct + for(i = 0; i < 6; ++i) { + lfsr->data[i] = (lfsr_value >> ((5 - i) * 8)) & 0xFF; + } +} diff --git a/applications/system/mfkey/crypto1.h b/applications/system/mfkey/crypto1.h new file mode 100644 index 000000000..9caf5b35e --- /dev/null +++ b/applications/system/mfkey/crypto1.h @@ -0,0 +1,256 @@ +#ifndef CRYPTO1_H +#define CRYPTO1_H + +#include +#include "mfkey.h" +#include +#include + +#define LF_POLY_ODD (0x29CE5C) +#define LF_POLY_EVEN (0x870804) +#define BIT(x, n) ((x) >> (n) & 1) +#define BEBIT(x, n) BIT(x, (n) ^ 24) +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16) + +static inline uint32_t prng_successor(uint32_t x, uint32_t n); +static inline int filter(uint32_t const x); +static inline uint8_t evenparity32(uint32_t x); +static inline void update_contribution(unsigned int data[], int item, int mask1, int mask2); +void crypto1_get_lfsr(struct Crypto1State* state, MfClassicKey* lfsr); +static inline uint32_t crypt_word(struct Crypto1State* s); +static inline void crypt_word_noret(struct Crypto1State* s, uint32_t in, int x); +static inline uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x); +static uint32_t crypt_word_par( + struct Crypto1State* s, + uint32_t in, + int is_encrypted, + uint32_t nt_plain, + uint8_t* parity_keystream_bits); +static inline void rollback_word_noret(struct Crypto1State* s, uint32_t in, int x); +static inline uint8_t napi_lfsr_rollback_bit(struct Crypto1State* s, uint32_t in, int fb); +static inline uint32_t napi_lfsr_rollback_word(struct Crypto1State* s, uint32_t in, int fb); + +static const uint8_t lookup1[256] = { + 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, + 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, 8, 8, 24, 24, 8, 24, 8, 8, + 8, 24, 8, 8, 24, 24, 24, 24, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24}; +static const uint8_t lookup2[256] = { + 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, + 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, + 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, + 2, 2, 6, 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, + 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, + 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, + 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, + 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, + 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6}; + +static inline int filter(uint32_t const x) { + uint32_t f; + f = lookup1[x & 0xff] | lookup2[(x >> 8) & 0xff]; + f |= 0x0d938 >> (x >> 16 & 0xf) & 1; + return BIT(0xEC57E80A, f); +} + +#ifndef __ARM_ARCH_7EM__ +static inline uint8_t evenparity32(uint32_t x) { + return __builtin_parity(x); +} +#endif + +#ifdef __ARM_ARCH_7EM__ +static inline uint8_t evenparity32(uint32_t x) { + uint32_t result; + __asm__ volatile("eor r1, %[x], %[x], lsr #16 \n\t" // r1 = x ^ (x >> 16) + "eor r1, r1, r1, lsr #8 \n\t" // r1 = r1 ^ (r1 >> 8) + "eor r1, r1, r1, lsr #4 \n\t" // r1 = r1 ^ (r1 >> 4) + "eor r1, r1, r1, lsr #2 \n\t" // r1 = r1 ^ (r1 >> 2) + "eor r1, r1, r1, lsr #1 \n\t" // r1 = r1 ^ (r1 >> 1) + "and %[result], r1, #1 \n\t" // result = r1 & 1 + : [result] "=r"(result) + : [x] "r"(x) + : "r1"); + return result; +} +#endif + +static inline void update_contribution(unsigned int data[], int item, int mask1, int mask2) { + int p = data[item] >> 25; + p = p << 1 | evenparity32(data[item] & mask1); + p = p << 1 | evenparity32(data[item] & mask2); + data[item] = p << 24 | (data[item] & 0xffffff); +} + +static inline uint32_t crypt_word(struct Crypto1State* s) { + // "in" and "x" are always 0 (last iteration) + uint32_t res_ret = 0; + uint32_t feedin, t; + for(int i = 0; i <= 31; i++) { + res_ret |= (filter(s->odd) << (24 ^ i)); //-V629 + feedin = LF_POLY_EVEN & s->even; + feedin ^= LF_POLY_ODD & s->odd; + s->even = s->even << 1 | (evenparity32(feedin)); + t = s->odd, s->odd = s->even, s->even = t; + } + return res_ret; +} + +static inline void crypt_word_noret(struct Crypto1State* s, uint32_t in, int x) { + uint8_t ret; + uint32_t feedin, t, next_in; + for(int i = 0; i <= 31; i++) { + next_in = BEBIT(in, i); + ret = filter(s->odd); + feedin = ret & (!!x); + feedin ^= LF_POLY_EVEN & s->even; + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even = s->even << 1 | (evenparity32(feedin)); + t = s->odd, s->odd = s->even, s->even = t; + } + return; +} + +static inline uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x) { + uint32_t ret = 0; + uint32_t feedin, t, next_in; + uint8_t next_ret; + for(int i = 0; i <= 31; i++) { + next_in = BEBIT(in, i); + next_ret = filter(s->odd); + feedin = next_ret & (!!x); + feedin ^= LF_POLY_EVEN & s->even; + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even = s->even << 1 | (evenparity32(feedin)); + t = s->odd, s->odd = s->even, s->even = t; + ret |= next_ret << (24 ^ i); + } + return ret; +} + +static uint8_t get_nth_byte(uint32_t value, int n) { + if(n < 0 || n > 3) { + // Handle invalid input + return 0; + } + return (value >> (8 * (3 - n))) & 0xFF; +} + +static uint8_t crypt_bit(struct Crypto1State* s, uint8_t in, int is_encrypted) { + uint32_t feedin, t; + uint8_t ret = filter(s->odd); + feedin = ret & !!is_encrypted; + feedin ^= !!in; + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= LF_POLY_EVEN & s->even; + s->even = s->even << 1 | evenparity32(feedin); + t = s->odd, s->odd = s->even, s->even = t; + return ret; +} + +static inline uint32_t crypt_word_par( + struct Crypto1State* s, + uint32_t in, + int is_encrypted, + uint32_t nt_plain, + uint8_t* parity_keystream_bits) { + uint32_t ret = 0; + *parity_keystream_bits = 0; // Reset parity keystream bits + + for(int i = 0; i < 32; i++) { + uint8_t bit = crypt_bit(s, BEBIT(in, i), is_encrypted); + ret |= bit << (24 ^ i); + // Save keystream parity bit + if((i + 1) % 8 == 0) { + *parity_keystream_bits |= + (filter(s->odd) ^ nfc_util_even_parity8(get_nth_byte(nt_plain, i / 8))) + << (3 - (i / 8)); + } + } + return ret; +} + +static inline void rollback_word_noret(struct Crypto1State* s, uint32_t in, int x) { + uint8_t ret; + uint32_t feedin, t, next_in; + for(int i = 31; i >= 0; i--) { + next_in = BEBIT(in, i); + s->odd &= 0xffffff; + t = s->odd, s->odd = s->even, s->even = t; + ret = filter(s->odd); + feedin = ret & (!!x); + feedin ^= s->even & 1; + feedin ^= LF_POLY_EVEN & (s->even >>= 1); + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even |= (evenparity32(feedin)) << 23; + } + return; +} + +// TODO: +/* +uint32_t rollback_word(struct Crypto1State *s, uint32_t in, int x) { + uint32_t res_ret = 0; + uint8_t ret; + uint32_t feedin, t, next_in; + for (int i = 31; i >= 0; i--) { + next_in = BEBIT(in, i); + s->odd &= 0xffffff; + t = s->odd, s->odd = s->even, s->even = t; + ret = filter(s->odd); + feedin = ret & (!!x); + feedin ^= s->even & 1; + feedin ^= LF_POLY_EVEN & (s->even >>= 1); + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even |= (evenparity32(feedin)) << 23; + res_ret |= (ret << (24 ^ i)); + } + return res_ret; +} +*/ + +uint8_t napi_lfsr_rollback_bit(struct Crypto1State* s, uint32_t in, int fb) { + int out; + uint8_t ret; + uint32_t t; + s->odd &= 0xffffff; + t = s->odd, s->odd = s->even, s->even = t; + + out = s->even & 1; + out ^= LF_POLY_EVEN & (s->even >>= 1); + out ^= LF_POLY_ODD & s->odd; + out ^= !!in; + out ^= (ret = filter(s->odd)) & !!fb; + + s->even |= evenparity32(out) << 23; + return ret; +} + +uint32_t napi_lfsr_rollback_word(struct Crypto1State* s, uint32_t in, int fb) { + int i; + uint32_t ret = 0; + for(i = 31; i >= 0; --i) + ret |= napi_lfsr_rollback_bit(s, BEBIT(in, i), fb) << (i ^ 24); + return ret; +} + +static inline uint32_t prng_successor(uint32_t x, uint32_t n) { + SWAPENDIAN(x); + while(n--) + x = x >> 1 | (x >> 16 ^ x >> 18 ^ x >> 19 ^ x >> 21) << 31; + return SWAPENDIAN(x); +} + +#endif // CRYPTO1_H diff --git a/applications/system/mfkey/images/mfkey.png b/applications/system/mfkey/images/mfkey.png new file mode 100644 index 0000000000000000000000000000000000000000..f1694bb335a8619e53cd3b98651ba995cc7a1caf GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVhivIasB`QKad%E=yDy9Qt)(f z4B@z*{KD=)L3iUrapuFX*xK~i1d{R-5*QT~m>F7Tu$Q|1zuOE{%i!ti=d#Wzp$P!I C{~rec literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/init_plugin.c b/applications/system/mfkey/init_plugin.c new file mode 100644 index 000000000..8540a8f2d --- /dev/null +++ b/applications/system/mfkey/init_plugin.c @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include +#include +#include "mfkey.h" +#include "crypto1.h" +#include "plugin_interface.h" +#include + +#define TAG "MFKey" + +// TODO: Remove defines that are not needed +#define MF_CLASSIC_NONCE_PATH EXT_PATH("nfc/.mfkey32.log") +#define MF_CLASSIC_NESTED_NONCE_PATH EXT_PATH("nfc/.nested.log") +#define MAX_NAME_LEN 32 +#define MAX_PATH_LEN 64 + +#define LF_POLY_ODD (0x29CE5C) +#define LF_POLY_EVEN (0x870804) +#define CONST_M1_1 (LF_POLY_EVEN << 1 | 1) +#define CONST_M2_1 (LF_POLY_ODD << 1) +#define CONST_M1_2 (LF_POLY_ODD) +#define CONST_M2_2 (LF_POLY_EVEN << 1 | 1) +#define BIT(x, n) ((x) >> (n) & 1) +#define BEBIT(x, n) BIT(x, (n) ^ 24) +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16) + +bool key_already_found_for_nonce_in_dict(KeysDict* dict, MfClassicNonce* nonce) { + // This function must not be passed the CUID dictionary + bool found = false; + uint8_t key_bytes[sizeof(MfClassicKey)]; + keys_dict_rewind(dict); + while(keys_dict_get_next_key(dict, key_bytes, sizeof(MfClassicKey))) { + uint64_t k = bit_lib_bytes_to_num_be(key_bytes, sizeof(MfClassicKey)); + struct Crypto1State temp = {0, 0}; + for(int i = 0; i < 24; i++) { + (&temp)->odd |= (BIT(k, 2 * i + 1) << (i ^ 3)); + (&temp)->even |= (BIT(k, 2 * i) << (i ^ 3)); + } + if(nonce->attack == mfkey32) { + crypt_word_noret(&temp, nonce->uid_xor_nt1, 0); + crypt_word_noret(&temp, nonce->nr1_enc, 1); + if(nonce->ar1_enc == (crypt_word(&temp) ^ nonce->p64b)) { + found = true; + break; + } + } else if(nonce->attack == static_nested || nonce->attack == static_encrypted) { + uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0); + if(nonce->ks1_1_enc == expected_ks1) { + found = true; + break; + } + } + } + return found; +} + +bool napi_mf_classic_mfkey32_nonces_check_presence() { + Storage* storage = furi_record_open(RECORD_STORAGE); + + bool nonces_present = storage_common_stat(storage, MF_CLASSIC_NONCE_PATH, NULL) == FSE_OK; + + furi_record_close(RECORD_STORAGE); + + return nonces_present; +} + +bool napi_mf_classic_nested_nonces_check_presence() { + Storage* storage = furi_record_open(RECORD_STORAGE); + Stream* stream = buffered_file_stream_alloc(storage); + bool nonces_present = false; + FuriString* line = furi_string_alloc(); + + do { + if(!buffered_file_stream_open( + stream, MF_CLASSIC_NESTED_NONCE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + break; + } + + while(stream_read_line(stream, line)) { + if(furi_string_search_str(line, "dist 0") != FURI_STRING_FAILURE) { + nonces_present = true; + break; + } + } + + } while(false); + + furi_string_free(line); + buffered_file_stream_close(stream); + stream_free(stream); + furi_record_close(RECORD_STORAGE); + + return nonces_present; +} + +int binaryStringToInt(const char* binStr) { + int result = 0; + while(*binStr) { + result <<= 1; + if(*binStr == '1') { + result |= 1; + } + binStr++; + } + return result; +} + +bool load_mfkey32_nonces( + MfClassicNonceArray* nonce_array, + ProgramState* program_state, + KeysDict* system_dict, + bool system_dict_exists, + KeysDict* user_dict) { + bool array_loaded = false; + + do { + // https://github.com/flipperdevices/flipperzero-firmware/blob/5134f44c09d39344a8747655c0d59864bb574b96/applications/services/storage/filesystem_api_defines.h#L8-L22 + if(!buffered_file_stream_open( + nonce_array->stream, MF_CLASSIC_NONCE_PATH, FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) { + buffered_file_stream_close(nonce_array->stream); + break; + } + + // Check for newline ending + if(!stream_eof(nonce_array->stream)) { + if(!stream_seek(nonce_array->stream, -1, StreamOffsetFromEnd)) break; + uint8_t last_char = 0; + if(stream_read(nonce_array->stream, &last_char, 1) != 1) break; + if(last_char != '\n') { + //FURI_LOG_D(TAG, "Adding new line ending"); + if(stream_write_char(nonce_array->stream, '\n') != 1) break; + } + if(!stream_rewind(nonce_array->stream)) break; + } + + // Read total amount of nonces + FuriString* next_line; + next_line = furi_string_alloc(); + while(!(program_state->close_thread_please)) { + if(!stream_read_line(nonce_array->stream, next_line)) { + //FURI_LOG_T(TAG, "No nonces left"); + break; + } + /* + FURI_LOG_T( + TAG, + "Read line: %s, len: %zu", + furi_string_get_cstr(next_line), + furi_string_size(next_line)); + */ + if(!furi_string_start_with_str(next_line, "Sec")) continue; + const char* next_line_cstr = furi_string_get_cstr(next_line); + MfClassicNonce res = {0}; + res.attack = mfkey32; + int i = 0; + char* endptr; + for(i = 0; i <= 17; i++) { + if(i != 0) { + next_line_cstr = strchr(next_line_cstr, ' '); + if(next_line_cstr) { + next_line_cstr++; + } else { + break; + } + } + unsigned long value = strtoul(next_line_cstr, &endptr, 16); + switch(i) { + case 5: + res.uid = value; + break; + case 7: + res.nt0 = value; + break; + case 9: + res.nr0_enc = value; + break; + case 11: + res.ar0_enc = value; + break; + case 13: + res.nt1 = value; + break; + case 15: + res.nr1_enc = value; + break; + case 17: + res.ar1_enc = value; + break; + default: + break; // Do nothing + } + next_line_cstr = endptr; + } + res.p64 = prng_successor(res.nt0, 64); + res.p64b = prng_successor(res.nt1, 64); + res.uid_xor_nt0 = res.uid ^ res.nt0; + res.uid_xor_nt1 = res.uid ^ res.nt1; + + (program_state->total)++; + if((system_dict_exists && key_already_found_for_nonce_in_dict(system_dict, &res)) || + (key_already_found_for_nonce_in_dict(user_dict, &res))) { + (program_state->cracked)++; + (program_state->num_completed)++; + continue; + } + //FURI_LOG_I(TAG, "No key found for %8lx %8lx", res.uid, res.ar1_enc); + // TODO: Refactor + nonce_array->remaining_nonce_array = realloc( //-V701 + nonce_array->remaining_nonce_array, + sizeof(MfClassicNonce) * ((nonce_array->remaining_nonces) + 1)); + nonce_array->remaining_nonces++; + nonce_array->remaining_nonce_array[(nonce_array->remaining_nonces) - 1] = res; + nonce_array->total_nonces++; + } + furi_string_free(next_line); + buffered_file_stream_close(nonce_array->stream); + + array_loaded = true; + //FURI_LOG_I(TAG, "Loaded %lu Mfkey32 nonces", nonce_array->total_nonces); + } while(false); + + return array_loaded; +} + +bool load_nested_nonces( + MfClassicNonceArray* nonce_array, + ProgramState* program_state, + KeysDict* system_dict, + bool system_dict_exists, + KeysDict* user_dict) { + if(!buffered_file_stream_open( + nonce_array->stream, MF_CLASSIC_NESTED_NONCE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + return false; + } + + FuriString* next_line = furi_string_alloc(); + bool array_loaded = false; + + while(stream_read_line(nonce_array->stream, next_line)) { + const char* line = furi_string_get_cstr(next_line); + + // Only process lines ending with "dist 0" + if(!strstr(line, "dist 0")) { + continue; + } + + MfClassicNonce res = {0}; + res.attack = static_encrypted; + + int parsed = sscanf( + line, + "Sec %*d key %*c cuid %" PRIx32 " nt0 %" PRIx32 " ks0 %" PRIx32 + " par0 %4[01] nt1 %" PRIx32 " ks1 %" PRIx32 " par1 %4[01]", + &res.uid, + &res.nt0, + &res.ks1_1_enc, + res.par_1_str, + &res.nt1, + &res.ks1_2_enc, + res.par_2_str); + + if(parsed >= 4) { // At least one nonce is present + res.par_1 = binaryStringToInt(res.par_1_str); + res.uid_xor_nt0 = res.uid ^ res.nt0; + + if(parsed == 7) { // Both nonces are present + res.attack = static_nested; + res.par_2 = binaryStringToInt(res.par_2_str); + res.uid_xor_nt1 = res.uid ^ res.nt1; + } + + (program_state->total)++; + if((system_dict_exists && key_already_found_for_nonce_in_dict(system_dict, &res)) || + (key_already_found_for_nonce_in_dict(user_dict, &res))) { + (program_state->cracked)++; + (program_state->num_completed)++; + continue; + } + + nonce_array->remaining_nonce_array = realloc( + nonce_array->remaining_nonce_array, + sizeof(MfClassicNonce) * (nonce_array->remaining_nonces + 1)); + nonce_array->remaining_nonce_array[nonce_array->remaining_nonces] = res; + nonce_array->remaining_nonces++; + nonce_array->total_nonces++; + array_loaded = true; + } + } + + furi_string_free(next_line); + buffered_file_stream_close(nonce_array->stream); + + //FURI_LOG_I(TAG, "Loaded %lu Static Nested nonces", nonce_array->total_nonces); + return array_loaded; +} + +MfClassicNonceArray* napi_mf_classic_nonce_array_alloc( + KeysDict* system_dict, + bool system_dict_exists, + KeysDict* user_dict, + ProgramState* program_state) { + MfClassicNonceArray* nonce_array = malloc(sizeof(MfClassicNonceArray)); + MfClassicNonce* remaining_nonce_array_init = malloc(sizeof(MfClassicNonce) * 1); + nonce_array->remaining_nonce_array = remaining_nonce_array_init; + Storage* storage = furi_record_open(RECORD_STORAGE); + nonce_array->stream = buffered_file_stream_alloc(storage); + furi_record_close(RECORD_STORAGE); + + if(program_state->mfkey32_present) { + load_mfkey32_nonces( + nonce_array, program_state, system_dict, system_dict_exists, user_dict); + } + + if(program_state->nested_present) { + load_nested_nonces(nonce_array, program_state, system_dict, system_dict_exists, user_dict); + } + + return nonce_array; +} + +void napi_mf_classic_nonce_array_free(MfClassicNonceArray* nonce_array) { + // TODO: Track free state at the time this is called to ensure double free does not happen + furi_assert(nonce_array); + furi_assert(nonce_array->stream); + + // TODO: Already closed? + buffered_file_stream_close(nonce_array->stream); + stream_free(nonce_array->stream); + free(nonce_array); +} + +/* Actual implementation of app<>plugin interface */ +static const MfkeyPlugin init_plugin = { + .name = "Initialization Plugin", + .napi_mf_classic_mfkey32_nonces_check_presence = + &napi_mf_classic_mfkey32_nonces_check_presence, + .napi_mf_classic_nested_nonces_check_presence = &napi_mf_classic_nested_nonces_check_presence, + .napi_mf_classic_nonce_array_alloc = &napi_mf_classic_nonce_array_alloc, + .napi_mf_classic_nonce_array_free = &napi_mf_classic_nonce_array_free, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor init_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &init_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* init_plugin_ep() { + return &init_plugin_descriptor; +} diff --git a/applications/system/mfkey/mfkey.c b/applications/system/mfkey/mfkey.c new file mode 100644 index 000000000..ae5cc90f2 --- /dev/null +++ b/applications/system/mfkey/mfkey.c @@ -0,0 +1,915 @@ +#pragma GCC optimize("O3") +#pragma GCC optimize("-funroll-all-loops") + +// TODO: More efficient dictionary bruteforce by scanning through hardcoded very common keys and previously found dictionary keys first? +// (a cache for key_already_found_for_nonce_in_dict) +// TODO: Selectively unroll loops to reduce binary size +// TODO: Collect parity during Mfkey32 attacks to further optimize the attack +// TODO: Why different sscanf between Mfkey32 and Nested? +// TODO: "Read tag again with NFC app" message upon completion, "Complete. Keys added: " +// TODO: Separate Mfkey32 and Nested functions where possible to reduce branch statements +// TODO: Find ~1 KB memory leak +// TODO: Use seednt16 to reduce static encrypted key candidates: https://gist.github.com/noproto/8102f8f32546564cd674256e62ff76ea +// https://eprint.iacr.org/2024/1275.pdf section X +// TODO: Static Encrypted: Minimum RAM for adding to keys dict (avoid crashes) +// TODO: Static Encrypted: Optimize KeysDict or buffer keys to write in chunks + +#include +#include +#include +#include +#include "mfkey_icons.h" +#include +#include +#include +#include +#include +#include +#include +#include "mfkey.h" +#include "crypto1.h" +#include "plugin_interface.h" +#include +#include +#include + +#define TAG "MFKey" + +// TODO: Remove defines that are not needed +#define KEYS_DICT_SYSTEM_PATH EXT_PATH("nfc/assets/mf_classic_dict.nfc") +#define KEYS_DICT_USER_PATH EXT_PATH("nfc/assets/mf_classic_dict_user.nfc") +#define MAX_NAME_LEN 32 +#define MAX_PATH_LEN 64 + +#define LF_POLY_ODD (0x29CE5C) +#define LF_POLY_EVEN (0x870804) +#define CONST_M1_1 (LF_POLY_EVEN << 1 | 1) +#define CONST_M2_1 (LF_POLY_ODD << 1) +#define CONST_M1_2 (LF_POLY_ODD) +#define CONST_M2_2 (LF_POLY_EVEN << 1 | 1) +#define BIT(x, n) ((x) >> (n) & 1) +#define BEBIT(x, n) BIT(x, (n) ^ 24) +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16) +//#define SIZEOF(arr) sizeof(arr) / sizeof(*arr) + +static int eta_round_time = 44; +static int eta_total_time = 705; +// MSB_LIMIT: Chunk size (out of 256) +static int MSB_LIMIT = 16; + +static inline int + check_state(struct Crypto1State* t, MfClassicNonce* n, ProgramState* program_state) { + if(!(t->odd | t->even)) return 0; + if(n->attack == mfkey32) { + uint32_t rb = (napi_lfsr_rollback_word(t, 0, 0) ^ n->p64); + if(rb != n->ar0_enc) { + return 0; + } + rollback_word_noret(t, n->nr0_enc, 1); + rollback_word_noret(t, n->uid_xor_nt0, 0); + struct Crypto1State temp = {t->odd, t->even}; + crypt_word_noret(t, n->uid_xor_nt1, 0); + crypt_word_noret(t, n->nr1_enc, 1); + if(n->ar1_enc == (crypt_word(t) ^ n->p64b)) { + crypto1_get_lfsr(&temp, &(n->key)); + return 1; + } + } else if(n->attack == static_nested) { + struct Crypto1State temp = {t->odd, t->even}; + rollback_word_noret(t, n->uid_xor_nt1, 0); + if(n->ks1_1_enc == crypt_word_ret(t, n->uid_xor_nt0, 0)) { + rollback_word_noret(&temp, n->uid_xor_nt1, 0); + crypto1_get_lfsr(&temp, &(n->key)); + return 1; + } + } else if(n->attack == static_encrypted) { + // TODO: Parity bits from rollback_word? + if(n->ks1_1_enc == napi_lfsr_rollback_word(t, n->uid_xor_nt0, 0)) { + // Reduce with parity + uint8_t local_parity_keystream_bits; + struct Crypto1State temp = {t->odd, t->even}; + if((crypt_word_par(&temp, n->uid_xor_nt0, 0, n->nt0, &local_parity_keystream_bits) == + n->ks1_1_enc) && + (local_parity_keystream_bits == n->par_1)) { + // Found key candidate + crypto1_get_lfsr(t, &(n->key)); + program_state->num_candidates++; + keys_dict_add_key(program_state->cuid_dict, n->key.data, sizeof(MfClassicKey)); + } + } + } + return 0; +} + +static inline int state_loop( + unsigned int* states_buffer, + int xks, + int m1, + int m2, + unsigned int in, + uint8_t and_val) { + int states_tail = 0; + int round = 0, s = 0, xks_bit = 0, round_in = 0; + + for(round = 1; round <= 12; round++) { + xks_bit = BIT(xks, round); + if(round > 4) { + round_in = ((in >> (2 * (round - 4))) & and_val) << 24; + } + + for(s = 0; s <= states_tail; s++) { + states_buffer[s] <<= 1; + + if((filter(states_buffer[s]) ^ filter(states_buffer[s] | 1)) != 0) { + states_buffer[s] |= filter(states_buffer[s]) ^ xks_bit; + if(round > 4) { + update_contribution(states_buffer, s, m1, m2); + states_buffer[s] ^= round_in; + } + } else if(filter(states_buffer[s]) == xks_bit) { + // TODO: Refactor + if(round > 4) { + states_buffer[++states_tail] = states_buffer[s + 1]; + states_buffer[s + 1] = states_buffer[s] | 1; + update_contribution(states_buffer, s, m1, m2); + states_buffer[s++] ^= round_in; + update_contribution(states_buffer, s, m1, m2); + states_buffer[s] ^= round_in; + } else { + states_buffer[++states_tail] = states_buffer[++s]; + states_buffer[s] = states_buffer[s - 1] | 1; + } + } else { + states_buffer[s--] = states_buffer[states_tail--]; + } + } + } + + return states_tail; +} + +int binsearch(unsigned int data[], int start, int stop) { + int mid, val = data[stop] & 0xff000000; + while(start != stop) { + mid = (stop - start) >> 1; + if((data[start + mid] ^ 0x80000000) > (val ^ 0x80000000)) + stop = start + mid; + else + start += mid + 1; + } + return start; +} +void quicksort(unsigned int array[], int low, int high) { + //if (SIZEOF(array) == 0) + // return; + if(low >= high) return; + int middle = low + (high - low) / 2; + unsigned int pivot = array[middle]; + int i = low, j = high; + while(i <= j) { + while(array[i] < pivot) { + i++; + } + while(array[j] > pivot) { + j--; + } + if(i <= j) { // swap + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + i++; + j--; + } + } + if(low < j) { + quicksort(array, low, j); + } + if(high > i) { + quicksort(array, i, high); + } +} +int extend_table(unsigned int data[], int tbl, int end, int bit, int m1, int m2, unsigned int in) { + in <<= 24; + for(data[tbl] <<= 1; tbl <= end; data[++tbl] <<= 1) { + if((filter(data[tbl]) ^ filter(data[tbl] | 1)) != 0) { + data[tbl] |= filter(data[tbl]) ^ bit; + update_contribution(data, tbl, m1, m2); + data[tbl] ^= in; + } else if(filter(data[tbl]) == bit) { + data[++end] = data[tbl + 1]; + data[tbl + 1] = data[tbl] | 1; + update_contribution(data, tbl, m1, m2); + data[tbl++] ^= in; + update_contribution(data, tbl, m1, m2); + data[tbl] ^= in; + } else { + data[tbl--] = data[end--]; + } + } + return end; +} + +int old_recover( + unsigned int odd[], + int o_head, + int o_tail, + int oks, + unsigned int even[], + int e_head, + int e_tail, + int eks, + int rem, + int s, + MfClassicNonce* n, + unsigned int in, + int first_run, + ProgramState* program_state) { + int o, e, i; + if(rem == -1) { + for(e = e_head; e <= e_tail; ++e) { + even[e] = (even[e] << 1) ^ evenparity32(even[e] & LF_POLY_EVEN) ^ (!!(in & 4)); + for(o = o_head; o <= o_tail; ++o, ++s) { + struct Crypto1State temp = {0, 0}; + temp.even = odd[o]; + temp.odd = even[e] ^ evenparity32(odd[o] & LF_POLY_ODD); + if(check_state(&temp, n, program_state)) { + return -1; + } + } + } + return s; + } + if(first_run == 0) { + for(i = 0; (i < 4) && (rem-- != 0); i++) { + oks >>= 1; + eks >>= 1; + in >>= 2; + o_tail = extend_table( + odd, o_head, o_tail, oks & 1, LF_POLY_EVEN << 1 | 1, LF_POLY_ODD << 1, 0); + if(o_head > o_tail) return s; + e_tail = extend_table( + even, e_head, e_tail, eks & 1, LF_POLY_ODD, LF_POLY_EVEN << 1 | 1, in & 3); + if(e_head > e_tail) return s; + } + } + first_run = 0; + quicksort(odd, o_head, o_tail); + quicksort(even, e_head, e_tail); + while(o_tail >= o_head && e_tail >= e_head) { + if(((odd[o_tail] ^ even[e_tail]) >> 24) == 0) { + o_tail = binsearch(odd, o_head, o = o_tail); + e_tail = binsearch(even, e_head, e = e_tail); + s = old_recover( + odd, + o_tail--, + o, + oks, + even, + e_tail--, + e, + eks, + rem, + s, + n, + in, + first_run, + program_state); + if(s == -1) { + break; + } + } else if((odd[o_tail] ^ 0x80000000) > (even[e_tail] ^ 0x80000000)) { + o_tail = binsearch(odd, o_head, o_tail) - 1; + } else { + e_tail = binsearch(even, e_head, e_tail) - 1; + } + } + return s; +} + +static inline int sync_state(ProgramState* program_state) { + int ts = furi_hal_rtc_get_timestamp(); + int elapsed_time = ts - program_state->eta_timestamp; + if(elapsed_time < program_state->eta_round) { + program_state->eta_round -= elapsed_time; + } else { + program_state->eta_round = 0; + } + if(elapsed_time < program_state->eta_total) { + program_state->eta_total -= elapsed_time; + } else { + program_state->eta_total = 0; + } + program_state->eta_timestamp = ts; + if(program_state->close_thread_please) { + return 1; + } + return 0; +} + +int calculate_msb_tables( + int oks, + int eks, + int msb_round, + MfClassicNonce* n, + unsigned int* states_buffer, + struct Msb* odd_msbs, + struct Msb* even_msbs, + unsigned int* temp_states_odd, + unsigned int* temp_states_even, + unsigned int in, + ProgramState* program_state) { + //FURI_LOG_I(TAG, "MSB GO %i", msb_iter); // DEBUG + unsigned int msb_head = (MSB_LIMIT * msb_round); // msb_iter ranges from 0 to (256/MSB_LIMIT)-1 + unsigned int msb_tail = (MSB_LIMIT * (msb_round + 1)); + int states_tail = 0, tail = 0; + int i = 0, j = 0, semi_state = 0, found = 0; + unsigned int msb = 0; + in = ((in >> 16 & 0xff) | (in << 16) | (in & 0xff00)) << 1; + // TODO: Why is this necessary? + memset(odd_msbs, 0, MSB_LIMIT * sizeof(struct Msb)); + memset(even_msbs, 0, MSB_LIMIT * sizeof(struct Msb)); + + for(semi_state = 1 << 20; semi_state >= 0; semi_state--) { + if(semi_state % 32768 == 0) { + if(sync_state(program_state) == 1) { + return 0; + } + } + + if(filter(semi_state) == (oks & 1)) { //-V547 + states_buffer[0] = semi_state; + states_tail = state_loop(states_buffer, oks, CONST_M1_1, CONST_M2_1, 0, 0); + + for(i = states_tail; i >= 0; i--) { + msb = states_buffer[i] >> 24; + if((msb >= msb_head) && (msb < msb_tail)) { + found = 0; + for(j = 0; j < odd_msbs[msb - msb_head].tail - 1; j++) { + if(odd_msbs[msb - msb_head].states[j] == states_buffer[i]) { + found = 1; + break; + } + } + + if(!found) { + tail = odd_msbs[msb - msb_head].tail++; + odd_msbs[msb - msb_head].states[tail] = states_buffer[i]; + } + } + } + } + + if(filter(semi_state) == (eks & 1)) { //-V547 + states_buffer[0] = semi_state; + states_tail = state_loop(states_buffer, eks, CONST_M1_2, CONST_M2_2, in, 3); + + for(i = 0; i <= states_tail; i++) { + msb = states_buffer[i] >> 24; + if((msb >= msb_head) && (msb < msb_tail)) { + found = 0; + + for(j = 0; j < even_msbs[msb - msb_head].tail; j++) { + if(even_msbs[msb - msb_head].states[j] == states_buffer[i]) { + found = 1; + break; + } + } + + if(!found) { + tail = even_msbs[msb - msb_head].tail++; + even_msbs[msb - msb_head].states[tail] = states_buffer[i]; + } + } + } + } + } + + oks >>= 12; + eks >>= 12; + + for(i = 0; i < MSB_LIMIT; i++) { + if(sync_state(program_state) == 1) { + return 0; + } + // TODO: Why is this necessary? + memset(temp_states_even, 0, sizeof(unsigned int) * (1280)); + memset(temp_states_odd, 0, sizeof(unsigned int) * (1280)); + memcpy(temp_states_odd, odd_msbs[i].states, odd_msbs[i].tail * sizeof(unsigned int)); + memcpy(temp_states_even, even_msbs[i].states, even_msbs[i].tail * sizeof(unsigned int)); + int res = old_recover( + temp_states_odd, + 0, + odd_msbs[i].tail, + oks, + temp_states_even, + 0, + even_msbs[i].tail, + eks, + 3, + 0, + n, + in >> 16, + 1, + program_state); + if(res == -1) { + return 1; + } + //odd_msbs[i].tail = 0; + //even_msbs[i].tail = 0; + } + + return 0; +} + +void** allocate_blocks(const size_t* block_sizes, int num_blocks) { + void** block_pointers = malloc(num_blocks * sizeof(void*)); + + for(int i = 0; i < num_blocks; i++) { + if(memmgr_heap_get_max_free_block() < block_sizes[i]) { + // Not enough memory, free previously allocated blocks + for(int j = 0; j < i; j++) { + free(block_pointers[j]); + } + free(block_pointers); + return NULL; + } + + block_pointers[i] = malloc(block_sizes[i]); + } + + return block_pointers; +} + +bool is_full_speed() { + return MSB_LIMIT == 16; +} + +bool recover(MfClassicNonce* n, int ks2, unsigned int in, ProgramState* program_state) { + bool found = false; + const size_t block_sizes[] = {49216, 49216, 5120, 5120, 4096}; + const size_t reduced_block_sizes[] = {24608, 24608, 5120, 5120, 4096}; + const int num_blocks = sizeof(block_sizes) / sizeof(block_sizes[0]); + void** block_pointers = allocate_blocks(block_sizes, num_blocks); + if(block_pointers == NULL) { + // System has less than the guaranteed amount of RAM (140 KB) - adjust some parameters to run anyway at half speed + if(is_full_speed()) { + //eta_round_time *= 2; + eta_total_time *= 2; + MSB_LIMIT /= 2; + } + block_pointers = allocate_blocks(reduced_block_sizes, num_blocks); + if(block_pointers == NULL) { + // System has less than 70 KB of RAM - should never happen so we don't reduce speed further + program_state->err = InsufficientRAM; + program_state->mfkey_state = Error; + return false; + } + } + // Adjust estimates for static encrypted attacks + if(n->attack == static_encrypted) { + eta_round_time *= 4; + eta_total_time *= 4; + if(is_full_speed()) { + eta_round_time *= 4; + eta_total_time *= 4; + } + } + struct Msb* odd_msbs = block_pointers[0]; + struct Msb* even_msbs = block_pointers[1]; + unsigned int* temp_states_odd = block_pointers[2]; + unsigned int* temp_states_even = block_pointers[3]; + unsigned int* states_buffer = block_pointers[4]; + int oks = 0, eks = 0; + int i = 0, msb = 0; + for(i = 31; i >= 0; i -= 2) { + oks = oks << 1 | BEBIT(ks2, i); + } + for(i = 30; i >= 0; i -= 2) { + eks = eks << 1 | BEBIT(ks2, i); + } + int bench_start = furi_hal_rtc_get_timestamp(); + program_state->eta_total = eta_total_time; + program_state->eta_timestamp = bench_start; + for(msb = 0; msb <= ((256 / MSB_LIMIT) - 1); msb++) { + program_state->search = msb; + program_state->eta_round = eta_round_time; + program_state->eta_total = eta_total_time - (eta_round_time * msb); + if(calculate_msb_tables( + oks, + eks, + msb, + n, + states_buffer, + odd_msbs, + even_msbs, + temp_states_odd, + temp_states_even, + in, + program_state)) { + //int bench_stop = furi_hal_rtc_get_timestamp(); + //FURI_LOG_I(TAG, "Cracked in %i seconds", bench_stop - bench_start); + found = true; + break; + } + if(program_state->close_thread_please) { + break; + } + } + // Free the allocated blocks + for(int i = 0; i < num_blocks; i++) { + free(block_pointers[i]); + } + free(block_pointers); + return found; +} + +bool key_already_found_for_nonce_in_solved( + MfClassicKey* keyarray, + int keyarray_size, + MfClassicNonce* nonce) { + for(int k = 0; k < keyarray_size; k++) { + uint64_t key_as_int = bit_lib_bytes_to_num_be(keyarray[k].data, sizeof(MfClassicKey)); + struct Crypto1State temp = {0, 0}; + for(int i = 0; i < 24; i++) { + (&temp)->odd |= (BIT(key_as_int, 2 * i + 1) << (i ^ 3)); + (&temp)->even |= (BIT(key_as_int, 2 * i) << (i ^ 3)); + } + if(nonce->attack == mfkey32) { + crypt_word_noret(&temp, nonce->uid_xor_nt1, 0); + crypt_word_noret(&temp, nonce->nr1_enc, 1); + if(nonce->ar1_enc == (crypt_word(&temp) ^ nonce->p64b)) { + return true; + } + } else if(nonce->attack == static_nested) { + uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0); + if(nonce->ks1_1_enc == expected_ks1) { + return true; + } + } + } + return false; +} + +#pragma GCC push_options +#pragma GCC optimize("Os") +static void finished_beep() { + // Beep to indicate completion + NotificationApp* notification = furi_record_open("notification"); + notification_message(notification, &sequence_audiovisual_alert); + notification_message(notification, &sequence_display_backlight_on); + furi_record_close("notification"); +} + +void mfkey(ProgramState* program_state) { + uint32_t ks_enc = 0, nt_xor_uid = 0; + MfClassicKey found_key; // Recovered key + size_t keyarray_size = 0; + MfClassicKey* keyarray = malloc(sizeof(MfClassicKey) * 1); + uint32_t i = 0, j = 0; + //FURI_LOG_I(TAG, "Free heap before alloc(): %zub", memmgr_get_free_heap()); + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperApplication* app = flipper_application_alloc(storage, firmware_api_interface); + flipper_application_preload(app, APP_ASSETS_PATH("plugins/mfkey_init_plugin.fal")); + flipper_application_map_to_memory(app); + const FlipperAppPluginDescriptor* app_descriptor = + flipper_application_plugin_get_descriptor(app); + const MfkeyPlugin* init_plugin = app_descriptor->entry_point; + // Check for nonces + program_state->mfkey32_present = init_plugin->napi_mf_classic_mfkey32_nonces_check_presence(); + program_state->nested_present = init_plugin->napi_mf_classic_nested_nonces_check_presence(); + if(!(program_state->mfkey32_present) && !(program_state->nested_present)) { + program_state->err = MissingNonces; + program_state->mfkey_state = Error; + flipper_application_free(app); + furi_record_close(RECORD_STORAGE); + free(keyarray); + return; + } + // Read dictionaries (optional) + KeysDict* system_dict = {0}; + bool system_dict_exists = keys_dict_check_presence(KEYS_DICT_SYSTEM_PATH); + KeysDict* user_dict = {0}; + bool user_dict_exists = keys_dict_check_presence(KEYS_DICT_USER_PATH); + uint32_t total_dict_keys = 0; + if(system_dict_exists) { + system_dict = + keys_dict_alloc(KEYS_DICT_SYSTEM_PATH, KeysDictModeOpenExisting, sizeof(MfClassicKey)); + total_dict_keys += keys_dict_get_total_keys(system_dict); + } + user_dict = keys_dict_alloc(KEYS_DICT_USER_PATH, KeysDictModeOpenAlways, sizeof(MfClassicKey)); + if(user_dict_exists) { + total_dict_keys += keys_dict_get_total_keys(user_dict); + } + user_dict_exists = true; + program_state->dict_count = total_dict_keys; + program_state->mfkey_state = DictionaryAttack; + // Read nonces + MfClassicNonceArray* nonce_arr; + nonce_arr = init_plugin->napi_mf_classic_nonce_array_alloc( + system_dict, system_dict_exists, user_dict, program_state); + if(system_dict_exists) { + keys_dict_free(system_dict); + } + if(nonce_arr->total_nonces == 0) { + // Nothing to crack + program_state->err = ZeroNonces; + program_state->mfkey_state = Error; + init_plugin->napi_mf_classic_nonce_array_free(nonce_arr); + flipper_application_free(app); + furi_record_close(RECORD_STORAGE); + keys_dict_free(user_dict); + free(keyarray); + return; + } + flipper_application_free(app); + furi_record_close(RECORD_STORAGE); + // TODO: Track free state at the time this is called to ensure double free does not happen + furi_assert(nonce_arr); + furi_assert(nonce_arr->stream); + // TODO: Already closed? + buffered_file_stream_close(nonce_arr->stream); + stream_free(nonce_arr->stream); + //FURI_LOG_I(TAG, "Free heap after free(): %zub", memmgr_get_free_heap()); + program_state->mfkey_state = MFKeyAttack; + // TODO: Work backwards on this array and free memory + for(i = 0; i < nonce_arr->total_nonces; i++) { + MfClassicNonce next_nonce = nonce_arr->remaining_nonce_array[i]; + if(key_already_found_for_nonce_in_solved(keyarray, keyarray_size, &next_nonce)) { + nonce_arr->remaining_nonces--; + (program_state->cracked)++; + (program_state->num_completed)++; + continue; + } + //FURI_LOG_I(TAG, "Beginning recovery for %8lx", next_nonce.uid); + FuriString* cuid_dict_path; + switch(next_nonce.attack) { + case mfkey32: + ks_enc = next_nonce.ar0_enc ^ next_nonce.p64; + nt_xor_uid = 0; + break; + case static_nested: + ks_enc = next_nonce.ks1_2_enc; + nt_xor_uid = next_nonce.uid_xor_nt1; + break; + case static_encrypted: + ks_enc = next_nonce.ks1_1_enc; + nt_xor_uid = next_nonce.uid_xor_nt0; + cuid_dict_path = furi_string_alloc_printf( + "%s/mf_classic_dict_%08lx.nfc", EXT_PATH("nfc/assets"), next_nonce.uid); + // May need RECORD_STORAGE? + program_state->cuid_dict = keys_dict_alloc( + furi_string_get_cstr(cuid_dict_path), + KeysDictModeOpenAlways, + sizeof(MfClassicKey)); + break; + } + + if(!recover(&next_nonce, ks_enc, nt_xor_uid, program_state)) { + if((next_nonce.attack == static_encrypted) && (program_state->cuid_dict)) { + keys_dict_free(program_state->cuid_dict); + } + if(program_state->close_thread_please) { + break; + } + // No key found in recover() or static encrypted + (program_state->num_completed)++; + continue; + } + (program_state->cracked)++; + (program_state->num_completed)++; + found_key = next_nonce.key; + bool already_found = false; + for(j = 0; j < keyarray_size; j++) { + if(memcmp(keyarray[j].data, found_key.data, MF_CLASSIC_KEY_SIZE) == 0) { + already_found = true; + break; + } + } + if(already_found == false) { + // New key + keyarray = realloc(keyarray, sizeof(MfClassicKey) * (keyarray_size + 1)); //-V701 + keyarray_size += 1; + keyarray[keyarray_size - 1] = found_key; + (program_state->unique_cracked)++; + } + } + // TODO: Update display to show all keys were found + // TODO: Prepend found key(s) to user dictionary file + //FURI_LOG_I(TAG, "Unique keys found:"); + for(i = 0; i < keyarray_size; i++) { + //FURI_LOG_I(TAG, "%012" PRIx64, keyarray[i]); + keys_dict_add_key(user_dict, keyarray[i].data, sizeof(MfClassicKey)); + } + if(keyarray_size > 0) { + dolphin_deed(DolphinDeedNfcMfcAdd); + } + free(nonce_arr); + keys_dict_free(user_dict); + free(keyarray); + if(program_state->mfkey_state == Error) { + return; + } + //FURI_LOG_I(TAG, "mfkey function completed normally"); // DEBUG + program_state->mfkey_state = Complete; + // No need to alert the user if they asked it to stop + if(!(program_state->close_thread_please)) { + finished_beep(); + } + return; +} + +// Screen is 128x64 px +static void render_callback(Canvas* const canvas, void* ctx) { + furi_assert(ctx); + ProgramState* program_state = ctx; + furi_mutex_acquire(program_state->mutex, FuriWaitForever); + char draw_str[44] = {}; + + canvas_draw_frame(canvas, 0, 0, 128, 64); + canvas_draw_frame(canvas, 0, 15, 128, 64); + + // FontSecondary by default, title is drawn at the end + snprintf(draw_str, sizeof(draw_str), "RAM: %zub", memmgr_get_free_heap()); + canvas_draw_str_aligned(canvas, 48, 5, AlignLeft, AlignTop, draw_str); + canvas_draw_icon(canvas, 114, 4, &I_mfkey); + if(program_state->mfkey_state == MFKeyAttack) { + float eta_round = (float)1 - ((float)program_state->eta_round / (float)eta_round_time); + float eta_total = (float)1 - ((float)program_state->eta_total / (float)eta_total_time); + float progress = (float)program_state->num_completed / (float)program_state->total; + if(eta_round < 0 || eta_round > 1) { + // Round ETA miscalculated + eta_round = 1; + program_state->eta_round = 0; + } + if(eta_total < 0 || eta_round > 1) { + // Total ETA miscalculated + eta_total = 1; + program_state->eta_total = 0; + } + snprintf( + draw_str, + sizeof(draw_str), + "Cracking: %d/%d - in prog.", + program_state->num_completed, + program_state->total); + elements_progress_bar_with_text(canvas, 5, 18, 118, progress, draw_str); + snprintf( + draw_str, + sizeof(draw_str), + "Round: %d/%d - ETA %02d Sec", + (program_state->search) + 1, // Zero indexed + 256 / MSB_LIMIT, + program_state->eta_round); + elements_progress_bar_with_text(canvas, 5, 31, 118, eta_round, draw_str); + snprintf(draw_str, sizeof(draw_str), "Total ETA %03d Sec", program_state->eta_total); + elements_progress_bar_with_text(canvas, 5, 44, 118, eta_total, draw_str); + } else if(program_state->mfkey_state == DictionaryAttack) { + snprintf( + draw_str, sizeof(draw_str), "Dict solves: %d (in progress)", program_state->cracked); + canvas_draw_str_aligned(canvas, 10, 18, AlignLeft, AlignTop, draw_str); + snprintf(draw_str, sizeof(draw_str), "Keys in dict: %d", program_state->dict_count); + canvas_draw_str_aligned(canvas, 26, 28, AlignLeft, AlignTop, draw_str); + } else if(program_state->mfkey_state == Complete) { + // TODO: Scrollable list view to see cracked keys if user presses down + elements_progress_bar(canvas, 5, 18, 118, 1); + canvas_draw_str_aligned(canvas, 64, 31, AlignCenter, AlignTop, "Complete"); + snprintf( + draw_str, + sizeof(draw_str), + "Keys added to user dict: %d", + program_state->unique_cracked); + canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignTop, draw_str); + if(program_state->num_candidates > 0) { + snprintf( + draw_str, + sizeof(draw_str), + "SEN key candidates: %d", + program_state->num_candidates); + canvas_draw_str_aligned(canvas, 64, 51, AlignCenter, AlignTop, draw_str); + } + } else if(program_state->mfkey_state == Ready) { + canvas_draw_str_aligned(canvas, 50, 30, AlignLeft, AlignTop, "Ready"); + elements_button_center(canvas, "Start"); + elements_button_right(canvas, "Help"); + } else if(program_state->mfkey_state == Help) { + canvas_draw_str_aligned(canvas, 7, 20, AlignLeft, AlignTop, "Collect nonces by reading"); + canvas_draw_str_aligned(canvas, 7, 30, AlignLeft, AlignTop, "tag or reader in NFC app:"); + canvas_draw_str_aligned(canvas, 7, 40, AlignLeft, AlignTop, "https://docs.flipper.net/"); + canvas_draw_str_aligned(canvas, 7, 50, AlignLeft, AlignTop, "nfc/mfkey32"); + } else if(program_state->mfkey_state == Error) { + canvas_draw_str_aligned(canvas, 50, 25, AlignLeft, AlignTop, "Error"); + if(program_state->err == MissingNonces) { + canvas_draw_str_aligned(canvas, 25, 36, AlignLeft, AlignTop, "No nonces found"); + } else if(program_state->err == ZeroNonces) { + canvas_draw_str_aligned(canvas, 15, 36, AlignLeft, AlignTop, "Nonces already cracked"); + } else if(program_state->err == InsufficientRAM) { + canvas_draw_str_aligned(canvas, 35, 36, AlignLeft, AlignTop, "No free RAM"); + } else { + // Unhandled error + } + } else { + // Unhandled program state + } + // Title + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 5, 4, AlignLeft, AlignTop, "MFKey"); + furi_mutex_release(program_state->mutex); +} + +static void input_callback(InputEvent* input_event, void* event_queue) { + furi_assert(event_queue); + furi_message_queue_put((FuriMessageQueue*)event_queue, input_event, FuriWaitForever); +} + +static void mfkey_state_init(ProgramState* program_state) { + program_state->mfkey_state = Ready; + program_state->cracked = 0; + program_state->unique_cracked = 0; + program_state->num_completed = 0; + program_state->num_candidates = 0; + program_state->total = 0; + program_state->dict_count = 0; +} + +// Entrypoint for worker thread +static int32_t mfkey_worker_thread(void* ctx) { + ProgramState* program_state = ctx; + program_state->mfkey_state = Initializing; + mfkey(program_state); + return 0; +} + +int32_t mfkey_main() { + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + ProgramState* program_state = malloc(sizeof(ProgramState)); + + mfkey_state_init(program_state); + + program_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + // Set system callbacks + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, render_callback, program_state); + view_port_input_callback_set(view_port, input_callback, event_queue); + + // Open GUI and register view_port + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + program_state->mfkeythread = + furi_thread_alloc_ex("MFKeyWorker", 2048, mfkey_worker_thread, program_state); + + InputEvent input_event; + for(bool main_loop = true; main_loop;) { + FuriStatus event_status = furi_message_queue_get(event_queue, &input_event, 100); + + furi_mutex_acquire(program_state->mutex, FuriWaitForever); + + if(event_status == FuriStatusOk) { + if(input_event.type == InputTypePress) { + switch(input_event.key) { + case InputKeyRight: + if(program_state->mfkey_state == Ready) { + program_state->mfkey_state = Help; + } + break; + case InputKeyOk: + if(program_state->mfkey_state == Ready) { + furi_thread_start(program_state->mfkeythread); + } + break; + case InputKeyBack: + if(program_state->mfkey_state == Help) { + program_state->mfkey_state = Ready; + } else { + program_state->close_thread_please = true; + // Wait until thread is finished + furi_thread_join(program_state->mfkeythread); + main_loop = false; + } + break; + default: + break; + } + } + } + + furi_mutex_release(program_state->mutex); + view_port_update(view_port); + } + + // Thread joined in back event handler + furi_thread_free(program_state->mfkeythread); + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + furi_record_close(RECORD_GUI); + view_port_free(view_port); + furi_message_queue_free(event_queue); + furi_mutex_free(program_state->mutex); + free(program_state); + + return 0; +} +#pragma GCC pop_options diff --git a/applications/system/mfkey/mfkey.h b/applications/system/mfkey/mfkey.h new file mode 100644 index 000000000..4a7ab3423 --- /dev/null +++ b/applications/system/mfkey/mfkey.h @@ -0,0 +1,108 @@ +#ifndef MFKEY_H +#define MFKEY_H + +#include +#include +#include +#include +#include +#include +#include + +struct Crypto1State { + uint32_t odd, even; +}; +struct Msb { + int tail; + uint32_t states[768]; +}; + +typedef enum { + MissingNonces, + ZeroNonces, + InsufficientRAM, +} MFKeyError; + +typedef enum { + Ready, + Initializing, + DictionaryAttack, + MFKeyAttack, + Complete, + Error, + Help, +} MFKeyState; + +// TODO: Can we eliminate any of the members of this struct? +typedef struct { + FuriMutex* mutex; + MFKeyError err; + MFKeyState mfkey_state; + int cracked; + int unique_cracked; + int num_completed; + int num_candidates; + int total; + int dict_count; + int search; + int eta_timestamp; + int eta_total; + int eta_round; + bool mfkey32_present; + bool nested_present; + bool close_thread_please; + FuriThread* mfkeythread; + KeysDict* cuid_dict; +} ProgramState; + +typedef enum { + mfkey32, + static_nested, + static_encrypted +} AttackType; + +typedef struct { + AttackType attack; + MfClassicKey key; // key + uint32_t uid; // serial number + uint32_t nt0; // tag challenge first + uint32_t nt1; // tag challenge second + uint32_t uid_xor_nt0; // uid ^ nt0 + uint32_t uid_xor_nt1; // uid ^ nt1 + union { + // Mfkey32 + struct { + uint32_t p64; // 64th successor of nt0 + uint32_t p64b; // 64th successor of nt1 + uint32_t nr0_enc; // first encrypted reader challenge + uint32_t ar0_enc; // first encrypted reader response + uint32_t nr1_enc; // second encrypted reader challenge + uint32_t ar1_enc; // second encrypted reader response + }; + // Nested + struct { + uint32_t ks1_1_enc; // first encrypted keystream + uint32_t ks1_2_enc; // second encrypted keystream + char par_1_str[5]; // first parity bits (string representation) + char par_2_str[5]; // second parity bits (string representation) + uint8_t par_1; // first parity bits + uint8_t par_2; // second parity bits + }; + }; +} MfClassicNonce; + +typedef struct { + Stream* stream; + uint32_t total_nonces; + MfClassicNonce* remaining_nonce_array; + size_t remaining_nonces; +} MfClassicNonceArray; + +struct KeysDict { + Stream* stream; + size_t key_size; + size_t key_size_symbols; + size_t total_keys; +}; + +#endif // MFKEY_H diff --git a/applications/system/mfkey/mfkey.png b/applications/system/mfkey/mfkey.png new file mode 100644 index 0000000000000000000000000000000000000000..f1694bb335a8619e53cd3b98651ba995cc7a1caf GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVhivIasB`QKad%E=yDy9Qt)(f z4B@z*{KD=)L3iUrapuFX*xK~i1d{R-5*QT~m>F7Tu$Q|1zuOE{%i!ti=d#Wzp$P!I C{~rec literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/plugin_interface.h b/applications/system/mfkey/plugin_interface.h new file mode 100644 index 000000000..e7ca438b8 --- /dev/null +++ b/applications/system/mfkey/plugin_interface.h @@ -0,0 +1,13 @@ +#pragma once + +#define PLUGIN_APP_ID "mfkey" +#define PLUGIN_API_VERSION 1 + +typedef struct { + const char* name; + bool (*napi_mf_classic_mfkey32_nonces_check_presence)(); + bool (*napi_mf_classic_nested_nonces_check_presence)(); + MfClassicNonceArray* ( + *napi_mf_classic_nonce_array_alloc)(KeysDict*, bool, KeysDict*, ProgramState*); + void (*napi_mf_classic_nonce_array_free)(MfClassicNonceArray*); +} MfkeyPlugin; From c655b8b5c2c3c3b77dbb4c50bff33e2a2bb76cc1 Mon Sep 17 00:00:00 2001 From: Nathan N Date: Thu, 10 Oct 2024 15:42:35 -0400 Subject: [PATCH 05/74] Update ReadMe.md --- ReadMe.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index 0420d969d..074c73150 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -37,6 +37,7 @@ Coming from other custom firmware, you'll get: - **MIFARE Ultralight C Emulation** (coming soon!) - **NFC app memory improvements** (coming soon!) - **Minimal theme** (coming soon!) +- *Future*: MIFARE DESFire Dictionary attack, Tesla credential enrollment # Contributing @@ -101,4 +102,5 @@ Make sure your Flipper is on, and your firmware is functioning. Connect your Fli # Links +- Xero Discord: [Discord](https://discord.gg/qQNgCrQkkj) - Official Discord: [flipp.dev/discord](https://flipp.dev/discord) From 749aa1d121a42fc0535bb084f57941dd2f551e69 Mon Sep 17 00:00:00 2001 From: noproto Date: Tue, 15 Oct 2024 16:07:26 -0400 Subject: [PATCH 06/74] Remove out of date screenshot --- .../system/mfkey/.catalog/screenshots/2.png | Bin 6980 -> 5937 bytes .../system/mfkey/.catalog/screenshots/3.png | Bin 5937 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 applications/system/mfkey/.catalog/screenshots/3.png diff --git a/applications/system/mfkey/.catalog/screenshots/2.png b/applications/system/mfkey/.catalog/screenshots/2.png index 9fc4407e59b9325ddd61586244ac152706ec1917..7eebc6377c82fd002f60f96077578c0f4307f94c 100644 GIT binary patch literal 5937 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 X{6C#c`qnxSI6zy&K)p)UKIT6F&zg|h literal 6980 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ǯ?neT@~YA$GQ|E+iSE{IhH6FCgTN$@4PJ znyux-9IK-Od&6$qM`ZQ;XGpg^|khDai3>OI-B<3Y6F&B_+n*kT$}u<-=88`fS^} z!}~RlNSc>My0U#cKaeGWX;a4iaCKev2gW)kYr@uWF9>SK7@FCQ zp?RcuCMuMyX4cJ8H41bvH0ERms6aMuH%V&Azl&klq+Aye$Y`SlW%nG)MS_W>5s2q) z)*!E}>tizzU|Lg|%)GeKWeJGcGJA(}M|Uo>sZF>HOuSX@R*%W#(6B(IiiCOnaGkWD zU~*;K{98wNTrXwWB6(DWTiI2R-{4!m?30okZ{BLMb~$#bGb`5Yv8-ejpd4QW>4$C; z+V8EtD;)^_L%CPCr~*^o8SZ@H^>e|4EdH=dil8OY+4vVK8f0Ns0PheqdlSDs*}#mO zmh2bl{y_;CK93mjOMV`L82sSxuG;5@0yxH9a14&D%nxyC?Z^ttIq+S@SD?vCyoO=NnMz{s2#c^NY%|L+m#y5ZjG9JnOUn()Ug8 zEF%q}DgeUr22VMSzkaDz{pt>FwD|Dt0zncC`vGmr!2DJRN5(BM(oFn=yEu)XV`)#; z_7cd(k0DK4a04nJAgg=OHgn5-X(d>tJySVBu}=g?q!rHi6dcJGF(*=9+`F)>3B>Am zqHoZRTzMYVRvw`*2F0q_4SZ%b>y@`}{kRYGa>}0h*KvZhd0mR=NZ(xD&x0PH_%Ee^ zkdH<2vK6nq8G<8c&&jz#wXFl$G>%~S`FFTrxzAZB7Tq?c3_B^r@LKg2<;A_USzQ{s zc9ITPu~2M|^FvT?fPWy>>%z+#Em3&82XR} z{%7p1r{-@;Jr)>Cn9kwkmSO3Y5^mt`K5a?x<|Vh*C;k-pwv5I(*AStM%(>pPM@18r zBmg0sC_xe0 zlshBy0wwTAu0l!pwXtseg|Ft~v0Uryaec%h>Mw$y`VCF(hymow-2$KC{p*6pnqRC`t+L z@mofGO>UE$U-0M;nf39t){0v=Q%jlWcA|yn*Y;cr7p1SkmEIub&63haW-^bilStRg z?1HxDr>dBz+tH2;%k8F!Q;4#|N$8fI*WD zkPwrQ5)qRWkq|c)mscQ*MG{g%VqywnVlJ?`n*T*`^SJBc82FzAIRz={e+bE8l5jFX p;D0LkxVWLceeSq<{s%`|OhHoiA5LsS;tZJsbTm-vW!LQ<{}(gqvjhMD diff --git a/applications/system/mfkey/.catalog/screenshots/3.png b/applications/system/mfkey/.catalog/screenshots/3.png deleted file mode 100644 index 7eebc6377c82fd002f60f96077578c0f4307f94c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5937 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 X{6C#c`qnxSI6zy&K)p)UKIT6F&zg|h From c39edce7c6fa4e21cfe73024d78f8a30d0e2e94c Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Thu, 17 Oct 2024 01:41:27 +0300 Subject: [PATCH 07/74] Fixed bug with UL reading --- lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c index 5cdf07f33..83875bb85 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c @@ -508,7 +508,7 @@ static NfcCommand mf_ultralight_poller_handler_read_pages(MfUltralightPoller* in if(instance->error == MfUltralightErrorNone) { if (start_page < instance->pages_total) { FURI_LOG_D(TAG, "Read page %d success", start_page); - instance->data->page[start_page] = data.page[start_page]; + instance->data->page[start_page] = data.page[0]; instance->pages_read++; instance->data->pages_read = instance->pages_read; } From f305f6efa9ab8e0315286c17a2d19ad4a55b2cf8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:26:20 +0300 Subject: [PATCH 08/74] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65721b6cc..7d901006b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Misc: - Add extra settings to disable GPIO pins control used for external modules amplifiers and/or LEDs (in radio settings menu with debug ON) - NFC: - - Read Ultralight block by block (by @mishamyte | PR #825) + - Read Ultralight block by block (by @mishamyte | PR #825 #826) - OFW PR 3822: MIFARE Classic Key Recovery Improvements (by @noproto) - OFW PR 3930: NFC Emulation freeze (by @RebornedBrain) - OFW PR 3885: Add API to enforce ISO15693 mode (by @aaronjamt) From e2d0a86d3ed9f86b1eb82b7af69d0e2a5bc4d7b0 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:06:51 +0300 Subject: [PATCH 09/74] merge js stuff by Willy-JL --- applications/system/js_app/application.fam | 8 ++++ applications/system/js_app/js_thread.c | 2 +- .../js_app/modules/js_gui/file_picker.c | 47 +++++++++++++++++++ .../system/js_app/types/gui/file_picker.d.ts | 6 +++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 applications/system/js_app/modules/js_gui/file_picker.c create mode 100644 applications/system/js_app/types/gui/file_picker.d.ts diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 3dabee4b0..fd9f0fe09 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -90,6 +90,14 @@ App( sources=["modules/js_gui/dialog.c"], ) +App( + appid="js_gui__file_picker", + apptype=FlipperAppType.PLUGIN, + entry_point="js_gui_file_picker_ep", + requires=["js_app"], + sources=["modules/js_gui/file_picker.c"], +) + App( appid="js_notification", apptype=FlipperAppType.PLUGIN, diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index c448945de..d26fe1fb0 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,7 +198,7 @@ static void js_require(struct mjs* mjs) { static void js_global_to_string(struct mjs* mjs) { int base = 10; if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); - double num = mjs_get_int(mjs, mjs_arg(mjs, 0)); + double num = mjs_get_double(mjs, mjs_arg(mjs, 0)); char tmp_str[] = "-2147483648"; itoa(num, tmp_str, base); mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); diff --git a/applications/system/js_app/modules/js_gui/file_picker.c b/applications/system/js_app/modules/js_gui/file_picker.c new file mode 100644 index 000000000..49cf5e89d --- /dev/null +++ b/applications/system/js_app/modules/js_gui/file_picker.c @@ -0,0 +1,47 @@ +#include "../../js_modules.h" +#include +#include + +static void js_gui_file_picker_pick_file(struct mjs* mjs) { + const char *base_path, *extension; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&base_path), JS_ARG_STR(&extension)); + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + const DialogsFileBrowserOptions browser_options = { + .extension = extension, + .icon = &I_file_10px, + .base_path = base_path, + }; + FuriString* path = furi_string_alloc_set(base_path); + if(dialog_file_browser_show(dialogs, path, path, &browser_options)) { + mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true)); + } else { + mjs_return(mjs, MJS_UNDEFINED); + } + furi_string_free(path); + furi_record_close(RECORD_DIALOGS); +} + +static void* js_gui_file_picker_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + *object = mjs_mk_object(mjs); + mjs_set(mjs, *object, "pickFile", ~0, MJS_MK_FN(js_gui_file_picker_pick_file)); + return NULL; +} + +static const JsModuleDescriptor js_gui_file_picker_desc = { + "gui__file_picker", + js_gui_file_picker_create, + NULL, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_gui_file_picker_desc, +}; + +const FlipperAppPluginDescriptor* js_gui_file_picker_ep(void) { + return &plugin_descriptor; +} diff --git a/applications/system/js_app/types/gui/file_picker.d.ts b/applications/system/js_app/types/gui/file_picker.d.ts new file mode 100644 index 000000000..2051e1432 --- /dev/null +++ b/applications/system/js_app/types/gui/file_picker.d.ts @@ -0,0 +1,6 @@ +/** + * @brief Displays a file picker and returns the selected file, or undefined if cancelled + * @param basePath The path to start at + * @param extension The file extension(s) to show (like `.sub`, `.iso|.img`, `*`) + */ +export declare function pickFile(basePath: string, extension: string): string | undefined; From ca68c953c0a1688c8b9374e9b15b34a9c282014e Mon Sep 17 00:00:00 2001 From: Ivan Barsukov Date: Thu, 17 Oct 2024 22:57:10 +0700 Subject: [PATCH 10/74] Add warning about stealth mode in vibro CLI (#3957) --- applications/services/cli/cli_commands.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index cb813840a..f2c6e4049 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -477,13 +478,24 @@ void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { void cli_command_vibro(Cli* cli, FuriString* args, void* context) { UNUSED(cli); UNUSED(context); + if(!furi_string_cmp(args, "0")) { NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); notification_message_block(notification, &sequence_reset_vibro); furi_record_close(RECORD_NOTIFICATION); } else if(!furi_string_cmp(args, "1")) { + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { + printf("Flipper is in stealth mode. Unmute the device to control vibration."); + return; + } + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); - notification_message_block(notification, &sequence_set_vibro_on); + if(notification->settings.vibro_on) { + notification_message_block(notification, &sequence_set_vibro_on); + } else { + printf("Vibro is disabled in settings. Enable it to control vibration."); + } + furi_record_close(RECORD_NOTIFICATION); } else { cli_print_usage("vibro", "<1|0>", furi_string_get_cstr(args)); From b723d463afccf628712475e11a3d4579f0331f5c Mon Sep 17 00:00:00 2001 From: porta Date: Thu, 17 Oct 2024 19:12:27 +0300 Subject: [PATCH 11/74] Revert "[FL-3909] CLI improvements, part I (#3928)" (#3955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0f831412fa617ae51afc5375b801c3b5c7543834. Co-authored-by: あく --- applications/main/subghz/subghz_cli.c | 9 +- applications/main/subghz/subghz_cli.h | 1 - applications/services/cli/cli.c | 194 +++++------------ applications/services/cli/cli.h | 16 +- applications/services/cli/cli_ansi.c | 76 ------- applications/services/cli/cli_ansi.h | 94 -------- applications/services/cli/cli_commands.c | 227 +++----------------- applications/services/crypto/crypto_cli.c | 9 +- applications/services/storage/storage_cli.c | 3 +- 9 files changed, 101 insertions(+), 528 deletions(-) delete mode 100644 applications/services/cli/cli_ansi.c delete mode 100644 applications/services/cli/cli_ansi.h diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index bce88b7a3..6375f2eee 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -999,12 +999,13 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { chat_event = subghz_chat_worker_get_event_chat(subghz_chat); switch(chat_event.event) { case SubGhzChatEventInputData: - if(chat_event.c == CliKeyETX) { + if(chat_event.c == CliSymbolAsciiETX) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); break; - } else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) { + } else if( + (chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) { size_t len = furi_string_utf8_length(input); if(len > furi_string_utf8_length(name)) { printf("%s", "\e[D\e[1P"); @@ -1026,7 +1027,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { } furi_string_set(input, sysmsg); } - } else if(chat_event.c == CliKeyCR) { + } else if(chat_event.c == CliSymbolAsciiCR) { printf("\r\n"); furi_string_push_back(input, '\r'); furi_string_push_back(input, '\n'); @@ -1040,7 +1041,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { furi_string_printf(input, "%s", furi_string_get_cstr(name)); printf("%s", furi_string_get_cstr(input)); fflush(stdout); - } else if(chat_event.c == CliKeyLF) { + } else if(chat_event.c == CliSymbolAsciiLF) { //cut out the symbol \n } else { putc(chat_event.c, stdout); diff --git a/applications/main/subghz/subghz_cli.h b/applications/main/subghz/subghz_cli.h index 18c84c3e0..f6388218f 100644 --- a/applications/main/subghz/subghz_cli.h +++ b/applications/main/subghz/subghz_cli.h @@ -1,6 +1,5 @@ #pragma once #include -#include void subghz_on_system_start(void); diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 6f18ee973..0d8f52c04 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -1,15 +1,12 @@ #include "cli_i.h" #include "cli_commands.h" #include "cli_vcp.h" -#include "cli_ansi.h" #include #include #define TAG "CliSrv" #define CLI_INPUT_LEN_LIMIT 256 -#define CLI_PROMPT ">: " // qFlipper does not recognize us if we use escape sequences :( -#define CLI_PROMPT_LENGTH 3 // printable characters Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); @@ -88,7 +85,7 @@ bool cli_cmd_interrupt_received(Cli* cli) { char c = '\0'; if(cli_is_connected(cli)) { if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) { - return c == CliKeyETX; + return c == CliSymbolAsciiETX; } } else { return true; @@ -105,8 +102,7 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { } void cli_motd(void) { - printf(ANSI_FLIPPER_BRAND_ORANGE - "\r\n" + printf("\r\n" " _.-------.._ -,\r\n" " .-\"```\"--..,,_/ /`-, -, \\ \r\n" " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" @@ -120,11 +116,12 @@ void cli_motd(void) { " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" ANSI_RESET - "\r\n" ANSI_FG_BR_WHITE "Welcome to " ANSI_FLIPPER_BRAND_ORANGE - "Flipper Zero" ANSI_FG_BR_WHITE " Command Line Interface!\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" + "\r\n" + "Welcome to Flipper Zero Command Line Interface!\r\n" "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" ANSI_RESET "\r\n"); + "Run `help` or `?` to list available commands\r\n" + "\r\n"); const Version* firmware_version = furi_hal_version_get_firmware_version(); if(firmware_version) { @@ -145,7 +142,7 @@ void cli_nl(Cli* cli) { void cli_prompt(Cli* cli) { UNUSED(cli); - printf("\r\n" CLI_PROMPT "%s", furi_string_get_cstr(cli->line)); + printf("\r\n>: %s", furi_string_get_cstr(cli->line)); fflush(stdout); } @@ -168,7 +165,7 @@ static void cli_handle_backspace(Cli* cli) { cli->cursor_position--; } else { - cli_putc(cli, CliKeyBell); + cli_putc(cli, CliSymbolAsciiBell); } } @@ -244,7 +241,7 @@ static void cli_handle_enter(Cli* cli) { printf( "`%s` command not found, use `help` or `?` to list all available commands", furi_string_get_cstr(command)); - cli_putc(cli, CliKeyBell); + cli_putc(cli, CliSymbolAsciiBell); } cli_reset(cli); @@ -308,85 +305,8 @@ static void cli_handle_autocomplete(Cli* cli) { cli_prompt(cli); } -typedef enum { - CliCharClassWord, - CliCharClassSpace, - CliCharClassOther, -} CliCharClass; - -/** - * @brief Determines the class that a character belongs to - * - * The return value of this function should not be used on its own; it should - * only be used for comparing it with other values returned by this function. - * This function is used internally in `cli_skip_run`. - */ -static CliCharClass cli_char_class(char c) { - if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { - return CliCharClassWord; - } else if(c == ' ') { - return CliCharClassSpace; - } else { - return CliCharClassOther; - } -} - -typedef enum { - CliSkipDirectionLeft, - CliSkipDirectionRight, -} CliSkipDirection; - -/** - * @brief Skips a run of a class of characters - * - * @param string Input string - * @param original_pos Position to start the search at - * @param direction Direction in which to perform the search - * @returns The position at which the run ends - */ -static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { - if(furi_string_size(string) == 0) return original_pos; - if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos; - if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string)) - return original_pos; - - int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0; - int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1; - int32_t position = original_pos; - CliCharClass start_class = - cli_char_class(furi_string_get_char(string, position + look_offset)); - - while(true) { - position += increment; - if(position < 0) break; - if(position >= (int32_t)furi_string_size(string)) break; - if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class) - break; - } - - return MAX(0, position); -} - -void cli_process_input(Cli* cli) { - CliKeyCombo combo = cli_read_ansi_key_combo(cli); - FURI_LOG_T(TAG, "code=0x%02x, mod=0x%x\r\n", combo.key, combo.modifiers); - - if(combo.key == CliKeyTab) { - cli_handle_autocomplete(cli); - - } else if(combo.key == CliKeySOH) { - furi_delay_ms(33); // We are too fast, Minicom is not ready yet - cli_motd(); - cli_prompt(cli); - - } else if(combo.key == CliKeyETX) { - cli_reset(cli); - cli_prompt(cli); - - } else if(combo.key == CliKeyEOT) { - cli_reset(cli); - - } else if(combo.key == CliKeyUp && combo.modifiers == CliModKeyNo) { +static void cli_handle_escape(Cli* cli, char c) { + if(c == 'A') { // Use previous command if line buffer is empty if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) { // Set line buffer and cursor position @@ -395,85 +315,67 @@ void cli_process_input(Cli* cli) { // Show new line to user printf("%s", furi_string_get_cstr(cli->line)); } - - } else if(combo.key == CliKeyDown && combo.modifiers == CliModKeyNo) { - // Clear input buffer - furi_string_reset(cli->line); - cli->cursor_position = 0; - printf("\r" CLI_PROMPT "\e[0K"); - - } else if(combo.key == CliKeyRight && combo.modifiers == CliModKeyNo) { - // Move right + } else if(c == 'B') { + } else if(c == 'C') { if(cli->cursor_position < furi_string_size(cli->line)) { cli->cursor_position++; printf("\e[C"); } - - } else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) { - // Move left + } else if(c == 'D') { if(cli->cursor_position > 0) { cli->cursor_position--; printf("\e[D"); } + } + fflush(stdout); +} - } else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) { - // Move to beginning of line - cli->cursor_position = 0; - printf("\e[%uG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/ +void cli_process_input(Cli* cli) { + char in_chr = cli_getc(cli); + size_t rx_len; - } else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) { - // Move to end of line - cli->cursor_position = furi_string_size(cli->line); - printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); - - } else if( - combo.modifiers == CliModKeyCtrl && - (combo.key == CliKeyLeft || combo.key == CliKeyRight)) { - // Skip run of similar chars to the left or right - CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft : - CliSkipDirectionRight; - cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction); - printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); - - } else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) { + if(in_chr == CliSymbolAsciiTab) { + cli_handle_autocomplete(cli); + } else if(in_chr == CliSymbolAsciiSOH) { + furi_delay_ms(33); // We are too fast, Minicom is not ready yet + cli_motd(); + cli_prompt(cli); + } else if(in_chr == CliSymbolAsciiETX) { + cli_reset(cli); + cli_prompt(cli); + } else if(in_chr == CliSymbolAsciiEOT) { + cli_reset(cli); + } else if(in_chr == CliSymbolAsciiEsc) { + rx_len = cli_read(cli, (uint8_t*)&in_chr, 1); + if((rx_len > 0) && (in_chr == '[')) { + cli_read(cli, (uint8_t*)&in_chr, 1); + cli_handle_escape(cli, in_chr); + } else { + cli_putc(cli, CliSymbolAsciiBell); + } + } else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) { cli_handle_backspace(cli); - - } else if(combo.key == CliKeyETB) { // Ctrl + Backspace - // Delete run of similar chars to the left - size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipDirectionLeft); - furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, ""); - cli->cursor_position = run_start; - printf( - "\e[%zuG%s\e[0K\e[%zuG", // move cursor, print second half of line, erase remains, move cursor again - CLI_PROMPT_LENGTH + cli->cursor_position + 1, - furi_string_get_cstr(cli->line) + run_start, - CLI_PROMPT_LENGTH + run_start + 1); - - } else if(combo.key == CliKeyCR) { + } else if(in_chr == CliSymbolAsciiCR) { cli_handle_enter(cli); - } else if( - (combo.key >= 0x20 && combo.key < 0x7F) && //-V560 + (in_chr >= 0x20 && in_chr < 0x7F) && //-V560 (furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) { if(cli->cursor_position == furi_string_size(cli->line)) { - furi_string_push_back(cli->line, combo.key); - cli_putc(cli, combo.key); + furi_string_push_back(cli->line, in_chr); + cli_putc(cli, in_chr); } else { // Insert character to line buffer - const char in_str[2] = {combo.key, 0}; + const char in_str[2] = {in_chr, 0}; furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str); // Print character in replace mode - printf("\e[4h%c\e[4l", combo.key); + printf("\e[4h%c\e[4l", in_chr); fflush(stdout); } cli->cursor_position++; - } else { - cli_putc(cli, CliKeyBell); + cli_putc(cli, CliSymbolAsciiBell); } - - fflush(stdout); } void cli_add_command( diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index c91f71c44..bb84670a7 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -10,12 +10,26 @@ extern "C" { #endif +typedef enum { + CliSymbolAsciiSOH = 0x01, + CliSymbolAsciiETX = 0x03, + CliSymbolAsciiEOT = 0x04, + CliSymbolAsciiBell = 0x07, + CliSymbolAsciiBackspace = 0x08, + CliSymbolAsciiTab = 0x09, + CliSymbolAsciiLF = 0x0A, + CliSymbolAsciiCR = 0x0D, + CliSymbolAsciiEsc = 0x1B, + CliSymbolAsciiUS = 0x1F, + CliSymbolAsciiSpace = 0x20, + CliSymbolAsciiDel = 0x7F, +} CliSymbols; + typedef enum { CliCommandFlagDefault = 0, /**< Default, loader lock is used */ CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */ CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ - CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */ } CliCommandFlag; #define RECORD_CLI "cli" diff --git a/applications/services/cli/cli_ansi.c b/applications/services/cli/cli_ansi.c deleted file mode 100644 index d27c20bad..000000000 --- a/applications/services/cli/cli_ansi.c +++ /dev/null @@ -1,76 +0,0 @@ -#include "cli_ansi.h" - -/** - * @brief Converts a single character representing a special key into the enum - * representation - */ -static CliKey cli_ansi_key_from_mnemonic(char c) { - switch(c) { - case 'A': - return CliKeyUp; - case 'B': - return CliKeyDown; - case 'C': - return CliKeyRight; - case 'D': - return CliKeyLeft; - case 'F': - return CliKeyEnd; - case 'H': - return CliKeyHome; - default: - return CliKeyUnrecognized; - } -} - -CliKeyCombo cli_read_ansi_key_combo(Cli* cli) { - char ch = cli_getc(cli); - - if(ch != CliKeyEsc) - return (CliKeyCombo){ - .modifiers = CliModKeyNo, - .key = ch, - }; - - ch = cli_getc(cli); - - // ESC ESC -> ESC - if(ch == '\e') - return (CliKeyCombo){ - .modifiers = CliModKeyNo, - .key = '\e', - }; - - // ESC -> Alt + - if(ch != '[') - return (CliKeyCombo){ - .modifiers = CliModKeyAlt, - .key = cli_getc(cli), - }; - - ch = cli_getc(cli); - - // ESC [ 1 - if(ch == '1') { - // ESC [ 1 ; - if(cli_getc(cli) == ';') { - CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number - modifiers &= ~1; - return (CliKeyCombo){ - .modifiers = modifiers, - .key = cli_ansi_key_from_mnemonic(cli_getc(cli)), - }; - } - - return (CliKeyCombo){ - .modifiers = CliModKeyNo, - .key = CliKeyUnrecognized, - }; - } - - // ESC [ - return (CliKeyCombo){ - .modifiers = CliModKeyNo, - .key = cli_ansi_key_from_mnemonic(ch), - }; -} diff --git a/applications/services/cli/cli_ansi.h b/applications/services/cli/cli_ansi.h deleted file mode 100644 index 110d8a5fc..000000000 --- a/applications/services/cli/cli_ansi.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include "cli.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define ANSI_RESET "\e[0m" -#define ANSI_BOLD "\e[1m" -#define ANSI_FAINT "\e[2m" - -#define ANSI_FG_BLACK "\e[30m" -#define ANSI_FG_RED "\e[31m" -#define ANSI_FG_GREEN "\e[32m" -#define ANSI_FG_YELLOW "\e[33m" -#define ANSI_FG_BLUE "\e[34m" -#define ANSI_FG_MAGENTA "\e[35m" -#define ANSI_FG_CYAN "\e[36m" -#define ANSI_FG_WHITE "\e[37m" -#define ANSI_FG_BR_BLACK "\e[90m" -#define ANSI_FG_BR_RED "\e[91m" -#define ANSI_FG_BR_GREEN "\e[92m" -#define ANSI_FG_BR_YELLOW "\e[93m" -#define ANSI_FG_BR_BLUE "\e[94m" -#define ANSI_FG_BR_MAGENTA "\e[95m" -#define ANSI_FG_BR_CYAN "\e[96m" -#define ANSI_FG_BR_WHITE "\e[97m" - -#define ANSI_BG_BLACK "\e[40m" -#define ANSI_BG_RED "\e[41m" -#define ANSI_BG_GREEN "\e[42m" -#define ANSI_BG_YELLOW "\e[43m" -#define ANSI_BG_BLUE "\e[44m" -#define ANSI_BG_MAGENTA "\e[45m" -#define ANSI_BG_CYAN "\e[46m" -#define ANSI_BG_WHITE "\e[47m" -#define ANSI_BG_BR_BLACK "\e[100m" -#define ANSI_BG_BR_RED "\e[101m" -#define ANSI_BG_BR_GREEN "\e[102m" -#define ANSI_BG_BR_YELLOW "\e[103m" -#define ANSI_BG_BR_BLUE "\e[104m" -#define ANSI_BG_BR_MAGENTA "\e[105m" -#define ANSI_BG_BR_CYAN "\e[106m" -#define ANSI_BG_BR_WHITE "\e[107m" - -#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m" - -typedef enum { - CliKeyUnrecognized = 0, - - CliKeySOH = 0x01, - CliKeyETX = 0x03, - CliKeyEOT = 0x04, - CliKeyBell = 0x07, - CliKeyBackspace = 0x08, - CliKeyTab = 0x09, - CliKeyLF = 0x0A, - CliKeyCR = 0x0D, - CliKeyETB = 0x17, - CliKeyEsc = 0x1B, - CliKeyUS = 0x1F, - CliKeySpace = 0x20, - CliKeyDEL = 0x7F, - - CliKeySpecial = 0x80, - CliKeyLeft, - CliKeyRight, - CliKeyUp, - CliKeyDown, - CliKeyHome, - CliKeyEnd, -} CliKey; - -typedef enum { - CliModKeyNo = 0, - CliModKeyAlt = 2, - CliModKeyCtrl = 4, - CliModKeyMeta = 8, -} CliModKey; - -typedef struct { - CliModKey modifiers; - CliKey key; -} CliKeyCombo; - -/** - * @brief Reads a key or key combination - */ -CliKeyCombo cli_read_ansi_key_combo(Cli* cli); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index f2c6e4049..e4503b274 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -1,6 +1,5 @@ #include "cli_commands.h" #include "cli_command_gpio.h" -#include "cli_ansi.h" #include #include @@ -12,7 +11,6 @@ #include #include #include -#include // Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'` #define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d" @@ -55,196 +53,37 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) { } } -// Lil Easter egg :> -void cli_command_neofetch(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); - UNUSED(args); - UNUSED(context); - - static const char* const neofetch_logo[] = { - " _.-------.._ -,", - " .-\"```\"--..,,_/ /`-, -, \\ ", - " .:\" /:/ /'\\ \\ ,_..., `. | |", - " / ,----/:/ /`\\ _\\~`_-\"` _;", - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ", - " | | | 0 | | .-' ,/` /", - " | ,..\\ \\ ,.-\"` ,/` /", - "; : `/`\"\"\\` ,/--==,/-----,", - "| `-...| -.___-Z:_______J...---;", - ": ` _-'", - }; -#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE - - // Determine logo parameters - size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0; - for(size_t i = 0; i < logo_height; i++) - logo_width = MAX(logo_width, strlen(neofetch_logo[i])); - logo_width += 4; // space between logo and info - - // Format hostname delimiter - const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr()); - char delimiter[64]; - memset(delimiter, '-', size_of_hostname); - delimiter[size_of_hostname] = '\0'; - - // Get heap info - size_t heap_total = memmgr_get_total_heap(); - size_t heap_used = heap_total - memmgr_get_free_heap(); - uint16_t heap_percent = (100 * heap_used) / heap_total; - - // Get storage info - Storage* storage = furi_record_open(RECORD_STORAGE); - uint64_t ext_total, ext_free, ext_used, ext_percent; - storage_common_fs_info(storage, "/ext", &ext_total, &ext_free); - ext_used = ext_total - ext_free; - ext_percent = (100 * ext_used) / ext_total; - ext_used /= 1024 * 1024; - ext_total /= 1024 * 1024; - furi_record_close(RECORD_STORAGE); - - // Get battery info - uint16_t charge_percent = furi_hal_power_get_pct(); - const char* charge_state; - if(furi_hal_power_is_charging()) { - if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) { - charge_state = "charging"; - } else { - charge_state = "charged"; - } - } else { - charge_state = "discharging"; - } - - // Get misc info - uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); - const Version* version = version_get(); - uint16_t major, minor; - furi_hal_info_get_api_version(&major, &minor); - - // Print ASCII art with info - const size_t info_height = 16; - for(size_t i = 0; i < MAX(logo_height, info_height); i++) { - printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : ""); - switch(i) { - case 0: // you@ - printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr()); - break; - case 1: // delimiter - printf(ANSI_RESET "%s", delimiter); - break; - case 2: // OS: FURI (SDK .) - printf( - "OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)", - version_get_version(version), - version_get_gitbranch(version), - version_get_version(version), - version_get_githash(version), - major, - minor); - break; - case 3: // Host: - printf( - "Host" ANSI_RESET ": %s %s", - furi_hal_version_get_model_code(), - furi_hal_version_get_device_name_ptr()); - break; - case 4: // Kernel: FreeRTOS .. - printf( - "Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d", - tskKERNEL_VERSION_MAJOR, - tskKERNEL_VERSION_MINOR, - tskKERNEL_VERSION_BUILD); - break; - case 5: // Uptime: ?h?m?s - printf( - "Uptime" ANSI_RESET ": %luh%lum%lus", - uptime / 60 / 60, - uptime / 60 % 60, - uptime % 60); - break; - case 6: // ST7567 128x64 @ 1 bpp in 1.4" - printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\""); - break; - case 7: // DE: GuiSrv - printf("DE" ANSI_RESET ": GuiSrv"); - break; - case 8: // Shell: CliSrv - printf("Shell" ANSI_RESET ": CliSrv"); - break; - case 9: // CPU: STM32WB55RG @ 64 MHz - printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz"); - break; - case 10: // Memory: / B (??%) - printf( - "Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent); - break; - case 11: // Disk (/ext): / MiB (??%) - printf( - "Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)", - ext_used, - ext_total, - ext_percent); - break; - case 12: // Battery: ??% () - printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state); - break; - case 13: // empty space - break; - case 14: // Colors (line 1) - for(size_t j = 30; j <= 37; j++) - printf("\e[%dm███", j); - break; - case 15: // Colors (line 2) - for(size_t j = 90; j <= 97; j++) - printf("\e[%dm███", j); - break; - default: - break; - } - printf("\r\n"); - } - printf(ANSI_RESET); -#undef NEOFETCH_COLOR -} - void cli_command_help(Cli* cli, FuriString* args, void* context) { + UNUSED(args); UNUSED(context); printf("Commands available:"); - // Count non-hidden commands - CliCommandTree_it_t it_count; - CliCommandTree_it(it_count, cli->commands); - size_t commands_count = 0; - while(!CliCommandTree_end_p(it_count)) { - if(!(CliCommandTree_cref(it_count)->value_ptr->flags & CliCommandFlagHidden)) - commands_count++; - CliCommandTree_next(it_count); - } + // Command count + const size_t commands_count = CliCommandTree_size(cli->commands); + const size_t commands_count_mid = commands_count / 2 + commands_count % 2; - // Create iterators starting at different positions - const size_t columns = 3; - const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); - CliCommandTree_it_t iterators[columns]; - for(size_t c = 0; c < columns; c++) { - CliCommandTree_it(iterators[c], cli->commands); - for(size_t i = 0; i < c * commands_per_column; i++) - CliCommandTree_next(iterators[c]); - } + // Use 2 iterators from start and middle to show 2 columns + CliCommandTree_it_t it_left; + CliCommandTree_it(it_left, cli->commands); + CliCommandTree_it_t it_right; + CliCommandTree_it(it_right, cli->commands); + for(size_t i = 0; i < commands_count_mid; i++) + CliCommandTree_next(it_right); - // Print commands - for(size_t r = 0; r < commands_per_column; r++) { + // Iterate throw tree + for(size_t i = 0; i < commands_count_mid; i++) { printf("\r\n"); - - for(size_t c = 0; c < columns; c++) { - if(!CliCommandTree_end_p(iterators[c])) { - const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); - if(!(item->value_ptr->flags & CliCommandFlagHidden)) { - printf("%-30s", furi_string_get_cstr(*item->key_ptr)); - } - CliCommandTree_next(iterators[c]); - } + // Left Column + if(!CliCommandTree_end_p(it_left)) { + printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr)); + CliCommandTree_next(it_left); } - } + // Right Column + if(!CliCommandTree_end_p(it_right)) { + printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr)); + CliCommandTree_next(it_right); + } + }; if(furi_string_size(args) > 0) { cli_nl(cli); @@ -564,18 +403,16 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { int interval = 1000; args_read_int_and_trim(args, &interval); - if(interval) printf("\e[2J\e[?25l"); // Clear display, hide cursor - FuriThreadList* thread_list = furi_thread_list_alloc(); while(!cli_cmd_interrupt_received(cli)) { uint32_t tick = furi_get_tick(); furi_thread_enumerate(thread_list); - if(interval) printf("\e[0;0f"); // Return to 0,0 + if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0 uint32_t uptime = tick / furi_kernel_get_tick_frequency(); printf( - "\rThreads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\e[0K\r\n", + "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n", furi_thread_list_size(thread_list), (double)furi_thread_list_get_isr_time(thread_list), uptime / 60 / 60, @@ -583,14 +420,14 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { uptime % 60); printf( - "\rHeap: total %zu, free %zu, minimum %zu, max block %zu\e[0K\r\n\r\n", + "Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n", memmgr_get_total_heap(), memmgr_get_free_heap(), memmgr_get_minimum_free_heap(), memmgr_heap_get_max_free_block()); printf( - "\r%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\e[0K\r\n", + "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n", "AppID", "Name", "State", @@ -604,7 +441,7 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) { const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i); printf( - "\r%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\e[0K\r\n", + "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n", item->app_id, item->name, item->state, @@ -623,8 +460,6 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { } } furi_thread_list_free(thread_list); - - if(interval) printf("\e[?25h"); // Show cursor } void cli_command_free(Cli* cli, FuriString* args, void* context) { @@ -676,12 +511,6 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command( - cli, - "neofetch", - CliCommandFlagParallelSafe | CliCommandFlagHidden, - cli_command_neofetch, - NULL); cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index 90746801a..744fa7151 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -3,7 +3,6 @@ #include #include -#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -46,14 +45,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { input = furi_string_alloc(); char c; while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliKeyETX) { + if(c == CliSymbolAsciiETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(input, c); - } else if(c == CliKeyCR) { + } else if(c == CliSymbolAsciiCR) { printf("\r\n"); furi_string_cat(input, "\r\n"); } @@ -121,14 +120,14 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { hex_input = furi_string_alloc(); char c; while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliKeyETX) { + if(c == CliSymbolAsciiETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(hex_input, c); - } else if(c == CliKeyCR) { + } else if(c == CliSymbolAsciiCR) { printf("\r\n"); } } diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 17bbc02a5..441b58da6 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -225,7 +224,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { while(true) { uint8_t symbol = cli_getc(cli); - if(symbol == CliKeyETX) { + if(symbol == CliSymbolAsciiETX) { size_t write_size = read_index % buffer_size; if(write_size > 0) { From 4be9e793421828516579b152bac716b961dd55f7 Mon Sep 17 00:00:00 2001 From: noproto Date: Thu, 17 Oct 2024 19:16:30 -0400 Subject: [PATCH 12/74] Fix multiple crashes and state machine logic --- .../protocols/mf_classic/mf_classic_poller.c | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 7417322e9..963bdeec5 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -8,7 +8,6 @@ // TODO: Buffer writes for Hardnested, set state to Log when finished and sum property matches // TODO: Store target key in CUID dictionary -// TODO: Fix rare nested_target_key 64 bug // TODO: Dead code for malloc returning NULL? // TODO: Auth1 static encrypted exists (rare) @@ -1663,7 +1662,7 @@ NfcCommand mf_classic_poller_handler_nested_dict_attack(MfClassicPoller* instanc "Found key candidate %06llx", bit_lib_bytes_to_num_be(key_candidate->data, sizeof(MfClassicKey))); dict_attack_ctx->current_key = *key_candidate; - dict_attack_ctx->reuse_key_sector = (target_block / 4); + dict_attack_ctx->reuse_key_sector = target_sector; dict_attack_ctx->current_key_type = target_key_type; free(key_candidate); break; @@ -2004,9 +2003,7 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance } uint16_t nonce_collect_key_max; if(dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) { - nonce_collect_key_max = dict_attack_ctx->static_encrypted ? - ((instance->sectors_total * 4) - 2) : - (instance->sectors_total * 4); + nonce_collect_key_max = instance->sectors_total * 4; } else { nonce_collect_key_max = instance->sectors_total * 2; } @@ -2047,30 +2044,6 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance dict_attack_ctx->attempt_count = 0; } dict_attack_ctx->auth_passed = true; - if(!(dict_attack_ctx->current_key_checked)) { - dict_attack_ctx->current_key_checked = true; - - // Check if the nested target key is a known key - if(mf_classic_nested_is_target_key_found(instance, false)) { - // Continue to next key - if(!(dict_attack_ctx->static_encrypted)) { - dict_attack_ctx->nested_target_key++; - dict_attack_ctx->current_key_checked = false; - } - instance->state = MfClassicPollerStateNestedController; - return command; - } - - // If it is not a known key, we'll need to calibrate for static encrypted backdoored tags - if((dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) && - (dict_attack_ctx->nested_target_key < nonce_collect_key_max) && - !(recalibrated)) { - dict_attack_ctx->calibrated = false; - dict_attack_ctx->nested_phase = MfClassicNestedPhaseRecalibrate; - instance->state = MfClassicPollerStateNestedController; - return command; - } - } // If we have tried to collect this nonce too many times, skip if((is_weak && (dict_attack_ctx->attempt_count >= MF_CLASSIC_NESTED_RETRY_MAXIMUM)) || @@ -2096,12 +2069,52 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance dict_attack_ctx->attempt_count = 0; } + FURI_LOG_D( + TAG, + "Nested target key: %u (max: %u)", + dict_attack_ctx->nested_target_key, + nonce_collect_key_max); + + if(!(dict_attack_ctx->current_key_checked)) { + if(dict_attack_ctx->nested_target_key == nonce_collect_key_max) { + // All nonces have been collected + FURI_LOG_D(TAG, "All nonces collected"); + instance->state = MfClassicPollerStateNestedController; + return command; + } + + dict_attack_ctx->current_key_checked = true; + + // Check if the nested target key is a known key + if(mf_classic_nested_is_target_key_found(instance, false)) { + // Continue to next key + if(!(dict_attack_ctx->static_encrypted)) { + dict_attack_ctx->nested_target_key++; + dict_attack_ctx->current_key_checked = false; + } + instance->state = MfClassicPollerStateNestedController; + return command; + } + + // If it is not a known key, we'll need to calibrate for static encrypted backdoored tags + if((dict_attack_ctx->backdoor == MfClassicBackdoorAuth3) && + (dict_attack_ctx->nested_target_key < nonce_collect_key_max) && + !(recalibrated)) { + dict_attack_ctx->calibrated = false; + dict_attack_ctx->nested_phase = MfClassicNestedPhaseRecalibrate; + instance->state = MfClassicPollerStateNestedController; + return command; + } + } + FURI_LOG_T(TAG, "Collecting a nonce"); + // Collect a nonce dict_attack_ctx->auth_passed = false; instance->state = MfClassicPollerStateNestedCollectNtEnc; return command; } } + dict_attack_ctx->nested_target_key = 0; dict_attack_ctx->nested_phase = MfClassicNestedPhaseFinished; instance->state = MfClassicPollerStateSuccess; return command; From 4a0dfa8de9b0279bffd5d54c4b210c8c0722d1d4 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 18 Oct 2024 04:35:27 +0100 Subject: [PATCH 13/74] JS: First batch of OFW PR review changes --- CHANGELOG.md | 15 ++- .../examples/apps/Scripts/Examples/load.js | 2 +- .../examples/apps/Scripts/Examples/path.js | 6 +- .../apps/Scripts/Examples/stringutils.js | 4 +- applications/system/js_app/js_thread.c | 92 +++---------------- applications/system/js_app/types/global.d.ts | 29 +++--- lib/mjs/mjs_exec.c | 6 ++ lib/mjs/mjs_string.c | 35 +++++++ lib/mjs/mjs_string.h | 2 + 9 files changed, 86 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b043ed45..68660155c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,32 +8,41 @@ - Non-exhaustive list of changes to help you fix your scripts: - `badusb`: - `setup()`: `mfr_name`, `prod_name`, `layout_path` parameters renamed to `mfrName`, `prodName`, `layoutPath` - - existing scripts using badusb will need to simply rename these + - effort required to update old scripts using badusb: very minimal - `dialog`: - removed, now replaced by `gui/dialog` and `gui/file_picker` (see below) - `event_loop`: - new module, allows timer functionality, callbacks and event-driven programming, used heavily alongside gpio and gui modules - `gpio`: - fully overhauled, now you `get()` pin instances and perform actions on them like `.init()` - - existing scripts using gpio will need some reworking + - now supports interrupts, callbacks and more cool things + - effort required to update old scripts using gpio: moderate - `gui`: - new module, fully overhauled, replaces dialog, keyboard, submenu, textbox modules - higher barrier to entry than older modules (requires usage of `event_loop` and `gui.viewDispatcher`), but much more flexible, powerful and easier to extend - includes all previously available js gui functionality (except `widget`), and also adds `gui/loading` and `gui/empty_screen` views - existing scripts using gui in any way will need a huge amount of reworking + - effort required to update old scripts using gui: extensive - `keyboard`: - removed, now replaced by `gui/text_input` and `gui/byte_input` (see above) - `storage`: - fully overhauled, now you `openFile()`s and perform actions on them like `.read()` - now supports many more operations including different open modes, directories and much more - `virtualInit()`, `virtualMount()`, `virtualQuit()` still work the same - - existing scripts using storage will need some reworking + - effort required to update old scripts using storage: moderate - `submenu`: - removed, now replaced by `gui/submenu` (see above) - `textbox`: - removed, now replace by `gui/text_box` (see above) - `widget`: - only gui functionality not ported to new gui module, remains unchanged for now but likely to be ported later on + - globals: + - `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs + - `to_string()` renamed to `toString()`, now supports optional base parameter + - `to_hex_string()` removed, now use `toString(num, 16)` + - `parse_int()` renamed to `parseInt()`, now supports optional base parameter + - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `"".toUpperCase()` and `"".toLowerCase()` + - effort required to update old scripts using these: minimal - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript, and you code in javascript) - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct - Type definitions for extra modules we have that OFW doesn't will come later diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/load.js b/applications/system/js_app/examples/apps/Scripts/Examples/load.js index 3b8f2d8fd..82b2d2046 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/load.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/load.js @@ -1,3 +1,3 @@ -let math = load(__dirpath + "/load_api.js"); +let math = load(__dirname + "/load_api.js"); let result = math.add(5, 10); print(result); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/path.js b/applications/system/js_app/examples/apps/Scripts/Examples/path.js index 1f50717a7..0be31b81d 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/path.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/path.js @@ -1,8 +1,8 @@ let storage = require("storage"); -print("script has __dirpath of" + __dirpath); -print("script has __filepath of" + __filepath); -if (storage.fileExists(__dirpath + "/math.js")) { +print("script has __dirname of" + __dirname); +print("script has __filename of" + __filename); +if (storage.fileExists(__dirname + "/math.js")) { print("math.js exist here."); } else { print("math.js does not exist here."); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js b/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js index c1f4d447a..7c57f9bc5 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js @@ -12,8 +12,8 @@ let searchStr = "World"; let result2 = toString(sampleText.indexOf(searchStr)); print(result2); -let upperCaseText = "Text in upper case: " + toUpperCase(sampleText); +let upperCaseText = "Text in upper case: " + sampleText.toUpperCase(); print(upperCaseText); -let lowerCaseText = "Text in lower case: " + toLowerCase(sampleText); +let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase(); print(lowerCaseText); diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index d26fe1fb0..31e9f721e 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -206,87 +207,20 @@ static void js_global_to_string(struct mjs* mjs) { } static void js_parse_int(struct mjs* mjs) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_string(arg)) { - mjs_return(mjs, mjs_mk_number(mjs, 0)); - return; + const char* str; + int32_t base = 10; + if(mjs_nargs(mjs) == 1) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str)); + } else { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base)); } - size_t str_len = 0; - const char* str = mjs_get_string(mjs, &arg, &str_len); - if((str_len == 0) || (str == NULL)) { - mjs_return(mjs, mjs_mk_number(mjs, 0)); - return; + int32_t num; + if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { + num = 0; } - - int32_t num = 0; - int32_t sign = 1; - size_t i = 0; - - if(str[0] == '-') { - sign = -1; - i = 1; - } else if(str[0] == '+') { - i = 1; - } - - for(; i < str_len; i++) { - if(str[i] >= '0' && str[i] <= '9') { - num = num * 10 + (str[i] - '0'); - } else { - break; - } - } - num *= sign; - mjs_return(mjs, mjs_mk_number(mjs, num)); } -static void js_to_upper_case(struct mjs* mjs) { - mjs_val_t arg0 = mjs_arg(mjs, 0); - - size_t str_len; - const char* str = NULL; - if(mjs_is_string(arg0)) { - str = mjs_get_string(mjs, &arg0, &str_len); - } - if(!str) { - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - char* upperStr = strdup(str); - for(size_t i = 0; i < str_len; i++) { - upperStr[i] = toupper(upperStr[i]); - } - - mjs_val_t resultStr = mjs_mk_string(mjs, upperStr, ~0, true); - free(upperStr); - mjs_return(mjs, resultStr); -} - -static void js_to_lower_case(struct mjs* mjs) { - mjs_val_t arg0 = mjs_arg(mjs, 0); - - size_t str_len; - const char* str = NULL; - if(mjs_is_string(arg0)) { - str = mjs_get_string(mjs, &arg0, &str_len); - } - if(!str) { - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - char* lowerStr = strdup(str); - for(size_t i = 0; i < str_len; i++) { - lowerStr[i] = tolower(lowerStr[i]); - } - - mjs_val_t resultStr = mjs_mk_string(mjs, lowerStr, ~0, true); - free(lowerStr); - mjs_return(mjs, resultStr); -} - #ifdef JS_DEBUG static void js_dump_write_callback(void* ctx, const char* format, ...) { File* file = ctx; @@ -320,14 +254,14 @@ static int32_t js_thread(void* arg) { mjs_set( mjs, global, - "__filepath", + "__filename", ~0, mjs_mk_string( mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true)); mjs_set( mjs, global, - "__dirpath", + "__dirname", ~0, mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true)); furi_string_free(dirpath); @@ -338,8 +272,6 @@ static int32_t js_thread(void* arg) { mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int)); - mjs_set(mjs, global, "toUpperCase", ~0, MJS_MK_FN(js_to_upper_case)); - mjs_set(mjs, global, "toLowerCase", ~0, MJS_MK_FN(js_to_lower_case)); mjs_val_t console_obj = mjs_mk_object(mjs); mjs_set(mjs, console_obj, "log", ~0, MJS_MK_FN(js_console_log)); diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index 14e045a1e..052a2bb6a 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -14,37 +14,26 @@ declare function print(...args: any[]): void; /** * @brief Converts a number to a string * @param value The number to convert to a string - * @param base Integer base (`2`...`16`), default: 16 + * @param base Integer base (`2`...`16`), default: 10 */ declare function toString(value: number, base?: number): string; /** * @brief Converts a string to a number * @param text The string to convert to a number + * @param base Integer base (`2`...`16`), default: 10 */ -declare function parseInt(text: string): number; - -/** - * @brief Transforms a string to upper case - * @param text The string to transforms to upper case - */ -declare function toUpperCase(text: string): string; - -/** - * @brief Transforms a string to lower case - * @param text The string to transforms to lower case - */ -declare function toLowerCase(text: string): string; +declare function parseInt(text: string, base?: number): number; /** * @brief Path to the directory containing the current script */ -declare const __dirpath: string; +declare const __dirname: string; /** * @brief Path to the current script file */ -declare const __filepath: string; +declare const __filename: string; /** * @brief Reads a JS value from a file @@ -211,6 +200,14 @@ declare class String { * @param end The index to end substring at */ slice(start: number, end?: number): string; + /** + * @brief Return this string transformed to upper case + */ + toUpperCase(): string; + /** + * @brief Return this string transformed to lower case + */ + toLowerCase(): string; } declare class Boolean { } diff --git a/lib/mjs/mjs_exec.c b/lib/mjs/mjs_exec.c index 265e7d5c3..0213a871e 100644 --- a/lib/mjs/mjs_exec.c +++ b/lib/mjs/mjs_exec.c @@ -452,6 +452,12 @@ static int getprop_builtin_string( } else if(strcmp(name, "slice") == 0) { *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_slice); return 1; + } else if(strcmp(name, "toUpperCase") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_upper_case); + return 1; + } else if(strcmp(name, "toLowerCase") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_lower_case); + return 1; } else if(isnum) { /* * string subscript: return a new one-byte string if the index diff --git a/lib/mjs/mjs_string.c b/lib/mjs/mjs_string.c index f74bf1074..f771ff4ea 100644 --- a/lib/mjs/mjs_string.c +++ b/lib/mjs/mjs_string.c @@ -286,6 +286,41 @@ MJS_PRIVATE mjs_val_t s_concat(struct mjs* mjs, mjs_val_t a, mjs_val_t b) { return res; } +MJS_PRIVATE void mjs_string_to_case(struct mjs* mjs, bool upper) { + mjs_val_t ret = MJS_UNDEFINED; + size_t size; + const char* s = NULL; + + /* get string from `this` */ + if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_STRING, NULL)) { + goto clean; + } + s = mjs_get_string(mjs, &mjs->vals.this_obj, &size); + + if(size == 0) { + ret = mjs_mk_string(mjs, "", 0, 1); + goto clean; + } + + char* tmp = malloc(size); + for(size_t i = 0; i < size; i++) { + tmp[i] = upper ? toupper(s[i]) : tolower(s[i]); + } + ret = mjs_mk_string(mjs, tmp, size, 1); + free(tmp); + +clean: + mjs_return(mjs, ret); +} + +MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs) { + mjs_string_to_case(mjs, false); +} + +MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs) { + mjs_string_to_case(mjs, true); +} + MJS_PRIVATE void mjs_string_slice(struct mjs* mjs) { int nargs = mjs_nargs(mjs); mjs_val_t ret = mjs_mk_number(mjs, 0); diff --git a/lib/mjs/mjs_string.h b/lib/mjs/mjs_string.h index ba6869b62..f0801f1a4 100644 --- a/lib/mjs/mjs_string.h +++ b/lib/mjs/mjs_string.h @@ -33,6 +33,8 @@ MJS_PRIVATE void embed_string( MJS_PRIVATE void mjs_mkstr(struct mjs* mjs); +MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs); +MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs); MJS_PRIVATE void mjs_string_slice(struct mjs* mjs); MJS_PRIVATE void mjs_string_index_of(struct mjs* mjs); MJS_PRIVATE void mjs_string_char_code_at(struct mjs* mjs); From ec5bafbfaf22a15fa9d88900cf37b6a2f7106e0e Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 18 Oct 2024 04:56:56 +0100 Subject: [PATCH 14/74] Update apps - Seader: Show error for timeout, fix wrong LRC logging (by bettse) --- CHANGELOG.md | 2 +- applications/external | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a9078e0..20fc3c5f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,7 +97,7 @@ - DTMF Dolphin: Add EAS tone support (by @JendrBendr) - NFC Playlist: Error screens for playlist already exists and item already in playlist, general improvements (by @acegoal07), refactor rename/new scene without thread (by @Willy-JL) - CLI-GUI Bridge: Fixes and improvements (by @ranchordo) - - Seader: Enable T=1 (by @bettse) + - Seader: Enable T=1, show error for timeout, fix wrong LRC logging (by @bettse) - BLE Spam: Fix menu index callback (by @Willy-JL) - Solitaire: App rewrite, Added quick solve, New effects and sounds, Removed hacky canvas manipulation (by @doofy-dev) - CLI-GUI Bridge: Add more symbols to keyboard (#222 by @Willy-JL) diff --git a/applications/external b/applications/external index 6e157c28b..2d045b733 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 6e157c28b3bea31122f0fcb4282a81cd4fcafa04 +Subproject commit 2d045b733614b1ead29adcbbab421bdd09c2fdb4 From 897817a829583f705d0ece5734c2a3fdd235304a Mon Sep 17 00:00:00 2001 From: noproto Date: Fri, 18 Oct 2024 00:33:49 -0400 Subject: [PATCH 15/74] Fix inconsistent assignment of known key and known key type/sector --- lib/nfc/protocols/mf_classic/mf_classic.c | 16 ++++++++++++++++ lib/nfc/protocols/mf_classic/mf_classic.h | 3 +++ lib/nfc/protocols/mf_classic/mf_classic_poller.c | 4 +++- targets/f7/api_symbols.csv | 3 ++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic.c b/lib/nfc/protocols/mf_classic/mf_classic.c index 4f92201e3..b1c5c20c9 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic.c +++ b/lib/nfc/protocols/mf_classic/mf_classic.c @@ -543,6 +543,22 @@ void mf_classic_set_key_not_found( } } +MfClassicKey + mf_classic_get_key(const MfClassicData* data, uint8_t sector_num, MfClassicKeyType key_type) { + furi_check(data); + furi_check(sector_num < mf_classic_get_total_sectors_num(data->type)); + furi_check(key_type == MfClassicKeyTypeA || key_type == MfClassicKeyTypeB); + + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector_num); + + if(key_type == MfClassicKeyTypeA) { + return sector_trailer->key_a; + } else { + return sector_trailer->key_b; + } +} + bool mf_classic_is_block_read(const MfClassicData* data, uint8_t block_num) { furi_check(data); diff --git a/lib/nfc/protocols/mf_classic/mf_classic.h b/lib/nfc/protocols/mf_classic/mf_classic.h index 38c3e9ba7..6ae7a623e 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic.h +++ b/lib/nfc/protocols/mf_classic/mf_classic.h @@ -213,6 +213,9 @@ void mf_classic_set_key_not_found( uint8_t sector_num, MfClassicKeyType key_type); +MfClassicKey + mf_classic_get_key(const MfClassicData* data, uint8_t sector_num, MfClassicKeyType key_type); + bool mf_classic_is_block_read(const MfClassicData* data, uint8_t block_num); void mf_classic_set_block_read(MfClassicData* data, uint8_t block_num, MfClassicBlock* block_data); diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 963bdeec5..8d3a1ee74 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -10,6 +10,7 @@ // TODO: Store target key in CUID dictionary // TODO: Dead code for malloc returning NULL? // TODO: Auth1 static encrypted exists (rare) +// TODO: Use keys found by NFC plugins, cached keys #define MF_CLASSIC_MAX_BUFF_SIZE (64) @@ -1817,12 +1818,13 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance bool initial_dict_attack_iter = false; if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseNone) { dict_attack_ctx->auth_passed = true; - dict_attack_ctx->nested_known_key = dict_attack_ctx->current_key; bool backdoor_present = (dict_attack_ctx->backdoor != MfClassicBackdoorNone); if(!(backdoor_present)) { for(uint8_t sector = 0; sector < instance->sectors_total; sector++) { for(uint8_t key_type = 0; key_type < 2; key_type++) { if(mf_classic_is_key_found(instance->data, sector, key_type)) { + dict_attack_ctx->nested_known_key = + mf_classic_get_key(instance->data, sector, key_type); dict_attack_ctx->nested_known_key_sector = sector; dict_attack_ctx->nested_known_key_type = key_type; break; diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index a91c2b91b..5ad8c3be3 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.2,, +Version,+,77.3,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -2512,6 +2512,7 @@ Function,+,mf_classic_get_base_data,Iso14443_3aData*,const MfClassicData* Function,+,mf_classic_get_blocks_num_in_sector,uint8_t,uint8_t Function,+,mf_classic_get_device_name,const char*,"const MfClassicData*, NfcDeviceNameType" Function,+,mf_classic_get_first_block_num_of_sector,uint8_t,uint8_t +Function,+,mf_classic_get_key,MfClassicKey,"const MfClassicData*, uint8_t, MfClassicKeyType" Function,+,mf_classic_get_read_sectors_and_keys,void,"const MfClassicData*, uint8_t*, uint8_t*" Function,+,mf_classic_get_sector_by_block,uint8_t,uint8_t Function,+,mf_classic_get_sector_trailer_by_sector,MfClassicSectorTrailer*,"const MfClassicData*, uint8_t" From fd331b1d3c8eda9033fdc12468ac794472c6681c Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 18 Oct 2024 05:37:35 +0100 Subject: [PATCH 16/74] Revert api bump --- targets/f7/api_symbols.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index a5d370d7b..afaf30427 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.3,, +Version,+,77.2,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/main/archive/helpers/archive_helpers_ext.h,, Header,+,applications/main/subghz/subghz_fap.h,, From db26c85b231cee324a6e3520e5eb199b52991190 Mon Sep 17 00:00:00 2001 From: noproto Date: Fri, 18 Oct 2024 00:46:24 -0400 Subject: [PATCH 17/74] Backdoor known key logic still needs the current key --- lib/nfc/protocols/mf_classic/mf_classic_poller.c | 1 + targets/f18/api_symbols.csv | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 8d3a1ee74..bc502534f 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -1833,6 +1833,7 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance } dict_attack_ctx->nested_phase = MfClassicNestedPhaseAnalyzePRNG; } else { + dict_attack_ctx->nested_known_key = dict_attack_ctx->current_key; dict_attack_ctx->nested_known_key_sector = 0; dict_attack_ctx->nested_known_key_type = MfClassicKeyTypeA; dict_attack_ctx->prng_type = MfClassicPrngTypeWeak; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 7943c4cfc..be60c6a1c 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.2,, +Version,+,77.3,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, From bc73b13b1b9192b6cc2c9ed191f2b3d1afc41869 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:21:09 +0300 Subject: [PATCH 18/74] merge js stuff https://github.com/Next-Flip/Momentum-Firmware/commits/js-backport-of-backport/ --- applications/system/js_app/js_thread.c | 92 +++---------------- .../system/js_app/modules/js_gui/js_gui.c | 21 +++++ .../system/js_app/modules/js_gui/text_input.c | 1 - .../system/js_app/types/badusb/index.d.ts | 5 +- applications/system/js_app/types/global.d.ts | 34 ++++--- .../system/js_app/types/gui/index.d.ts | 17 ++++ 6 files changed, 70 insertions(+), 100 deletions(-) diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index d26fe1fb0..31e9f721e 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -206,87 +207,20 @@ static void js_global_to_string(struct mjs* mjs) { } static void js_parse_int(struct mjs* mjs) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_string(arg)) { - mjs_return(mjs, mjs_mk_number(mjs, 0)); - return; + const char* str; + int32_t base = 10; + if(mjs_nargs(mjs) == 1) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str)); + } else { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base)); } - size_t str_len = 0; - const char* str = mjs_get_string(mjs, &arg, &str_len); - if((str_len == 0) || (str == NULL)) { - mjs_return(mjs, mjs_mk_number(mjs, 0)); - return; + int32_t num; + if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { + num = 0; } - - int32_t num = 0; - int32_t sign = 1; - size_t i = 0; - - if(str[0] == '-') { - sign = -1; - i = 1; - } else if(str[0] == '+') { - i = 1; - } - - for(; i < str_len; i++) { - if(str[i] >= '0' && str[i] <= '9') { - num = num * 10 + (str[i] - '0'); - } else { - break; - } - } - num *= sign; - mjs_return(mjs, mjs_mk_number(mjs, num)); } -static void js_to_upper_case(struct mjs* mjs) { - mjs_val_t arg0 = mjs_arg(mjs, 0); - - size_t str_len; - const char* str = NULL; - if(mjs_is_string(arg0)) { - str = mjs_get_string(mjs, &arg0, &str_len); - } - if(!str) { - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - char* upperStr = strdup(str); - for(size_t i = 0; i < str_len; i++) { - upperStr[i] = toupper(upperStr[i]); - } - - mjs_val_t resultStr = mjs_mk_string(mjs, upperStr, ~0, true); - free(upperStr); - mjs_return(mjs, resultStr); -} - -static void js_to_lower_case(struct mjs* mjs) { - mjs_val_t arg0 = mjs_arg(mjs, 0); - - size_t str_len; - const char* str = NULL; - if(mjs_is_string(arg0)) { - str = mjs_get_string(mjs, &arg0, &str_len); - } - if(!str) { - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - char* lowerStr = strdup(str); - for(size_t i = 0; i < str_len; i++) { - lowerStr[i] = tolower(lowerStr[i]); - } - - mjs_val_t resultStr = mjs_mk_string(mjs, lowerStr, ~0, true); - free(lowerStr); - mjs_return(mjs, resultStr); -} - #ifdef JS_DEBUG static void js_dump_write_callback(void* ctx, const char* format, ...) { File* file = ctx; @@ -320,14 +254,14 @@ static int32_t js_thread(void* arg) { mjs_set( mjs, global, - "__filepath", + "__filename", ~0, mjs_mk_string( mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true)); mjs_set( mjs, global, - "__dirpath", + "__dirname", ~0, mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true)); furi_string_free(dirpath); @@ -338,8 +272,6 @@ static int32_t js_thread(void* arg) { mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int)); - mjs_set(mjs, global, "toUpperCase", ~0, MJS_MK_FN(js_to_upper_case)); - mjs_set(mjs, global, "toLowerCase", ~0, MJS_MK_FN(js_to_lower_case)); mjs_val_t console_obj = mjs_mk_object(mjs); mjs_set(mjs, console_obj, "log", ~0, MJS_MK_FN(js_console_log)); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index 4bd4ccc31..cd87e5c4b 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -260,6 +260,26 @@ static void js_gui_view_set(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } +/** + * @brief `View.hasProperty` + */ +static void js_gui_view_has_property(struct mjs* mjs) { + const char* name; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name)); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); + const JsViewDescriptor* descriptor = data->descriptor; + + for(size_t i = 0; i < descriptor->prop_cnt; i++) { + JsViewPropDescriptor prop = descriptor->props[i]; + if(strcmp(prop.name, name) != 0) continue; + + mjs_return(mjs, mjs_mk_boolean(mjs, true)); + return; + } + + mjs_return(mjs, mjs_mk_boolean(mjs, false)); +} + /** * @brief `View` destructor */ @@ -284,6 +304,7 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr // generic view API mjs_val_t view_obj = mjs_mk_object(mjs); mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set)); + mjs_set(mjs, view_obj, "hasProperty", ~0, MJS_MK_FN(js_gui_view_has_property)); // object data JsGuiViewData* data = malloc(sizeof(JsGuiViewData)); diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index f68978e2d..d2bf4a8f9 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -49,7 +49,6 @@ static bool max_len_assign( JsViewPropValue value, JsKbdContext* context) { UNUSED(mjs); - UNUSED(input); context->buffer_size = (size_t)(value.number + 1); context->buffer = realloc(context->buffer, context->buffer_size); //-V701 text_input_set_result_callback( diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/types/badusb/index.d.ts index 872b8fe04..4fbda5ef8 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/types/badusb/index.d.ts @@ -14,6 +14,9 @@ export type MainKey = "F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" | "F20" | "F21" | "F22" | "F23" | "F24" | + "NUM0" | "NUM1" | "NUM2" | "NUM3" | "NUM4" | "NUM5" | "NUM6" | "NUM7" | + "NUM8" | "NUM9" | + "\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" | "]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" | @@ -99,6 +102,6 @@ export declare function altPrint(string: string, delay?: number): void; export declare function altPrintln(string: string, delay?: number): void; /** - * @brief Releases usb, optional, but allows to interchange with usbdisk + * @brief Releases usb, optional, but allows to switch usb profile */ export declare function quit(): void; diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index 01a8fbcef..052a2bb6a 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -14,47 +14,37 @@ declare function print(...args: any[]): void; /** * @brief Converts a number to a string * @param value The number to convert to a string - * @param base Integer base (`2`...`16`), default: 16 + * @param base Integer base (`2`...`16`), default: 10 */ declare function toString(value: number, base?: number): string; /** * @brief Converts a string to a number * @param text The string to convert to a number + * @param base Integer base (`2`...`16`), default: 10 */ -declare function parseInt(text: string): number; - -/** - * @brief Transforms a string to upper case - * @param text The string to transforms to upper case - */ -declare function toUpperCase(text: string): string; - -/** - * @brief Transforms a string to lower case - * @param text The string to transforms to lower case - */ -declare function toLowerCase(text: string): string; +declare function parseInt(text: string, base?: number): number; /** * @brief Path to the directory containing the current script */ -declare const __dirpath: string; +declare const __dirname: string; /** * @brief Path to the current script file */ -declare const __filepath: string; +declare const __filename: string; /** * @brief Reads a JS value from a file * * Reads a file at the specified path, interprets it as a JS value and returns - * said value. + * the last value pushed on the stack. * * @param path The path to the file + * @param scope An object to use as global scope while running this file */ -declare function load(path: string): any; +declare function load(path: string, scope?: object): any; /** * @brief Return 1-byte string whose ASCII code is the integer `n` @@ -210,6 +200,14 @@ declare class String { * @param end The index to end substring at */ slice(start: number, end?: number): string; + /** + * @brief Return this string transformed to upper case + */ + toUpperCase(): string; + /** + * @brief Return this string transformed to lower case + */ + toLowerCase(): string; } declare class Boolean { } diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts index 85f191059..5efcfdc03 100644 --- a/applications/system/js_app/types/gui/index.d.ts +++ b/applications/system/js_app/types/gui/index.d.ts @@ -3,11 +3,28 @@ import type { Contract } from "../event_loop"; type Properties = { [K: string]: any }; export declare class View { + /** + * Assign value to property by name + * @param property Name of the property + * @param value Value to assign + */ set

(property: P, value: Props[P]): void; + /** + * Check if property is available + * @param name Name of the property + */ + hasProperty(name: string): boolean; } export declare class ViewFactory> { + /** + * Create view instance with default values, can be changed later with set() + */ make(): V; + /** + * Create view instance with custom values, can be changed later with set() + * @param initial Dictionary of property names to values + */ makeWith(initial: Partial): V; } From fe3228b63ec83cb8046d4f4a4116c424c86e4af4 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:22:21 +0300 Subject: [PATCH 19/74] merge p2 --- .../apps/Scripts/js_examples/badusb_demo.js | 2 +- .../examples/apps/Scripts/js_examples/gui.js | 12 ++++++ .../apps/Scripts/js_examples/interactive.js | 37 ++++++++++++++----- .../examples/apps/Scripts/js_examples/load.js | 2 +- .../examples/apps/Scripts/js_examples/path.js | 8 ++-- .../apps/Scripts/js_examples/storage.js | 4 +- .../apps/Scripts/js_examples/stringutils.js | 4 +- 7 files changed, 49 insertions(+), 20 deletions(-) diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js index 971c16c9e..758cffd13 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js @@ -59,7 +59,7 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) notify.error(); } - // Optional, but allows to interchange with usbdisk + // Optional, but allows to unlock usb interface to switch profile badusb.quit(); eventLoop.stop(); diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js index 7e61e04ed..10e88b649 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js @@ -8,6 +8,7 @@ let textInputView = require("gui/text_input"); let byteInputView = require("gui/byte_input"); let textBoxView = require("gui/text_box"); let dialogView = require("gui/dialog"); +let filePicker = require("gui/file_picker"); let flipper = require("flipper"); // declare view instances @@ -40,6 +41,7 @@ let views = { "Text input & Dialog", "Byte input", "Text box", + "File picker", "Exit app", ], }), @@ -63,12 +65,22 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v } else if (index === 4) { gui.viewDispatcher.switchTo(views.longText); } else if (index === 5) { + let path = filePicker.pickFile("/ext", "*"); + if (path) { + views.helloDialog.set("text", "You selected:\n" + path); + } else { + views.helloDialog.set("text", "You didn't select a file"); + } + views.helloDialog.set("center", "Nice!"); + gui.viewDispatcher.switchTo(views.helloDialog); + } else if (index === 6) { eventLoop.stop(); } }, gui, eventLoop, views); // say hi after keyboard input eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) { + views.keyboard.set("defaultText", name); // Remember for next usage views.helloDialog.set("text", "Hi " + name + "! :)"); views.helloDialog.set("center", "Hi Flipper! :)"); gui.viewDispatcher.switchTo(views.helloDialog); diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js index 63db6c5e0..29f3bb253 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js @@ -11,8 +11,9 @@ storage.makeDirectory("/ext/.tmp/js"); storage.rmrf("/ext/.tmp/js/repl") storage.makeDirectory("/ext/.tmp/js/repl") let ctx = { - tmp_template: "/ext/.tmp/js/repl/", - tmp_number: 0, + tmpTemplate: "/ext/.tmp/js/repl/", + tmpNumber: 0, + persistentScope: {}, }; let views = { @@ -41,28 +42,40 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) { eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) { gui.viewDispatcher.switchTo(views.loading); - let path = ctx.tmp_template + toString(ctx.tmp_number++); + let path = ctx.tmpTemplate + toString(ctx.tmpNumber++); let file = storage.openFile(path, "w", "create_always"); - file.write("({run:function(){return " + text + ";},})"); + file.write(text); file.close(); // Hide GUI before running, we want to see console and avoid deadlock if code fails gui.viewDispatcher.sendTo("back"); - let result = load(path).run(); + let result = load(path, ctx.persistentScope); // Load runs JS and returns last value on stack storage.remove(path); - gui.viewDispatcher.sendTo("front"); // Must convert to string explicitly - if (typeof result === "number") { + if (result === null) { // mJS: typeof null === "null", ECMAScript: typeof null === "object", IDE complains when checking "null" type + result = "null"; + } else if (typeof result === "string") { + result = "'" + result + "'"; + } else if (typeof result === "number") { result = toString(result); - } else if (typeof result === "undefined") { - result = "undefined"; + } else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check + result = "bigint"; } else if (typeof result === "boolean") { result = result ? "true" : "false"; + } else if (typeof result === "symbol") { // mJS doesn't support Symbol() but might aswell check + result = "symbol"; + } else if (typeof result === "undefined") { + result = "undefined"; } else if (typeof result === "object") { - result = JSON.stringify(result); + result = "object"; // JSON.stringify() is not implemented + } else if (typeof result === "function") { + result = "function"; + } else { + result = "unknown type: " + typeof result; } + gui.viewDispatcher.sendTo("front"); views.dialog.set("header", "JS Returned:"); views.dialog.set("text", result); gui.viewDispatcher.switchTo(views.dialog); @@ -74,4 +87,8 @@ eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) }, eventLoop); gui.viewDispatcher.switchTo(views.dialog); + +// Message behind GUI if something breaks +print("If you're stuck here, something went wrong, re-run the script") eventLoop.run(); +print("\n\nFinished correctly :)") diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/load.js b/applications/system/js_app/examples/apps/Scripts/js_examples/load.js index 3b8f2d8fd..82b2d2046 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/load.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/load.js @@ -1,3 +1,3 @@ -let math = load(__dirpath + "/load_api.js"); +let math = load(__dirname + "/load_api.js"); let result = math.add(5, 10); print(result); diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/path.js b/applications/system/js_app/examples/apps/Scripts/js_examples/path.js index 9d3159d9f..0be31b81d 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/path.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/path.js @@ -1,9 +1,9 @@ let storage = require("storage"); -print("script has __dirpath of" + __dirpath); -print("script has __filepath of" + __filepath); -if (storage.fileExists(__dirpath + "/math.js")) { +print("script has __dirname of" + __dirname); +print("script has __filename of" + __filename); +if (storage.fileExists(__dirname + "/math.js")) { print("math.js exist here."); } else { print("math.js does not exist here."); -} \ No newline at end of file +} diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/storage.js b/applications/system/js_app/examples/apps/Scripts/js_examples/storage.js index 5f273c628..c0ec8bfa4 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/storage.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/storage.js @@ -25,5 +25,5 @@ storage.remove(path); print("Done") -// You don't need to close the file after each operation, this is just to show some different ways to use -// There's also many more functions and options, check types docs in firmware repo \ No newline at end of file +// You don't need to close the file after each operation, this is just to show some different ways to use the API +// There's also many more functions and options, check type definitions in firmware repo \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js b/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js index c1f4d447a..7c57f9bc5 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js @@ -12,8 +12,8 @@ let searchStr = "World"; let result2 = toString(sampleText.indexOf(searchStr)); print(result2); -let upperCaseText = "Text in upper case: " + toUpperCase(sampleText); +let upperCaseText = "Text in upper case: " + sampleText.toUpperCase(); print(upperCaseText); -let lowerCaseText = "Text in lower case: " + toLowerCase(sampleText); +let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase(); print(lowerCaseText); From 8ed1dc2289fd4cd8a229b79042dbbca693abc7ec Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:23:05 +0300 Subject: [PATCH 20/74] merge p3 --- lib/mjs/mjs_exec.c | 6 ++++++ lib/mjs/mjs_string.c | 35 +++++++++++++++++++++++++++++++++++ lib/mjs/mjs_string.h | 2 ++ 3 files changed, 43 insertions(+) diff --git a/lib/mjs/mjs_exec.c b/lib/mjs/mjs_exec.c index 265e7d5c3..0213a871e 100644 --- a/lib/mjs/mjs_exec.c +++ b/lib/mjs/mjs_exec.c @@ -452,6 +452,12 @@ static int getprop_builtin_string( } else if(strcmp(name, "slice") == 0) { *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_slice); return 1; + } else if(strcmp(name, "toUpperCase") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_upper_case); + return 1; + } else if(strcmp(name, "toLowerCase") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_string_to_lower_case); + return 1; } else if(isnum) { /* * string subscript: return a new one-byte string if the index diff --git a/lib/mjs/mjs_string.c b/lib/mjs/mjs_string.c index f74bf1074..f771ff4ea 100644 --- a/lib/mjs/mjs_string.c +++ b/lib/mjs/mjs_string.c @@ -286,6 +286,41 @@ MJS_PRIVATE mjs_val_t s_concat(struct mjs* mjs, mjs_val_t a, mjs_val_t b) { return res; } +MJS_PRIVATE void mjs_string_to_case(struct mjs* mjs, bool upper) { + mjs_val_t ret = MJS_UNDEFINED; + size_t size; + const char* s = NULL; + + /* get string from `this` */ + if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_STRING, NULL)) { + goto clean; + } + s = mjs_get_string(mjs, &mjs->vals.this_obj, &size); + + if(size == 0) { + ret = mjs_mk_string(mjs, "", 0, 1); + goto clean; + } + + char* tmp = malloc(size); + for(size_t i = 0; i < size; i++) { + tmp[i] = upper ? toupper(s[i]) : tolower(s[i]); + } + ret = mjs_mk_string(mjs, tmp, size, 1); + free(tmp); + +clean: + mjs_return(mjs, ret); +} + +MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs) { + mjs_string_to_case(mjs, false); +} + +MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs) { + mjs_string_to_case(mjs, true); +} + MJS_PRIVATE void mjs_string_slice(struct mjs* mjs) { int nargs = mjs_nargs(mjs); mjs_val_t ret = mjs_mk_number(mjs, 0); diff --git a/lib/mjs/mjs_string.h b/lib/mjs/mjs_string.h index ba6869b62..f0801f1a4 100644 --- a/lib/mjs/mjs_string.h +++ b/lib/mjs/mjs_string.h @@ -33,6 +33,8 @@ MJS_PRIVATE void embed_string( MJS_PRIVATE void mjs_mkstr(struct mjs* mjs); +MJS_PRIVATE void mjs_string_to_lower_case(struct mjs* mjs); +MJS_PRIVATE void mjs_string_to_upper_case(struct mjs* mjs); MJS_PRIVATE void mjs_string_slice(struct mjs* mjs); MJS_PRIVATE void mjs_string_index_of(struct mjs* mjs); MJS_PRIVATE void mjs_string_char_code_at(struct mjs* mjs); From bb6c015235680da5dc7ea48bd847c68034d9da4c Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:17:22 +0100 Subject: [PATCH 21/74] JS: Second batch of OFW PR changes --- CHANGELOG.md | 6 ++-- applications/system/js_app/application.fam | 14 ++++----- .../apps/Scripts/Examples/badusb_demo.js | 2 +- .../apps/Scripts/Examples/blebeacon.js | 2 +- .../examples/apps/Scripts/Examples/gui.js | 2 +- .../apps/Scripts/Examples/stringutils.js | 4 +-- .../apps/Scripts/Examples/uart_echo.js | 2 +- .../examples/apps/Scripts/interactive.js | 4 +-- applications/system/js_app/js_thread.c | 11 ------- .../system/js_app/modules/js_gui/js_gui.c | 21 ------------- applications/system/js_app/types/global.d.ts | 15 +++++----- .../system/js_app/types/gui/index.d.ts | 5 ---- lib/mjs/mjs_exec.c | 18 +++++++++++ lib/mjs/mjs_primitive.c | 30 +++++++++++++++++++ lib/mjs/mjs_primitive.h | 5 ++++ 15 files changed, 78 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fc3c5f2..d8e756d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ - new module, fully overhauled, replaces dialog, keyboard, submenu, textbox modules - higher barrier to entry than older modules (requires usage of `event_loop` and `gui.viewDispatcher`), but much more flexible, powerful and easier to extend - includes all previously available js gui functionality (except `widget`), and also adds `gui/loading` and `gui/empty_screen` views - - existing scripts using gui in any way will need a huge amount of reworking + - currently `gui/file_picker` works different than other new view objects, it is a simple `.pickFile()` synchronous function, but this [may change later](https://github.com/flipperdevices/flipperzero-firmware/pull/3961#discussion_r1805579153) - effort required to update old scripts using gui: extensive - `keyboard`: - removed, now replaced by `gui/text_input` and `gui/byte_input` (see above) @@ -38,10 +38,10 @@ - only gui functionality not ported to new gui module, remains unchanged for now but likely to be ported later on - globals: - `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs - - `to_string()` renamed to `toString()`, now supports optional base parameter + - `to_string()` renamed and moved to number class as `n.toString()`, now supports optional base parameter - `to_hex_string()` removed, now use `toString(num, 16)` - `parse_int()` renamed to `parseInt()`, now supports optional base parameter - - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `"".toUpperCase()` and `"".toLowerCase()` + - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `s.toUpperCase()` and `s.toLowerCase()` - effort required to update old scripts using these: minimal - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript, and you code in javascript) - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index a29251f0e..65fafed76 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -53,7 +53,7 @@ App( appid="js_gui", apptype=FlipperAppType.PLUGIN, entry_point="js_gui_ep", - requires=["js_app", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"], ) @@ -61,7 +61,7 @@ App( appid="js_gui__loading", apptype=FlipperAppType.PLUGIN, entry_point="js_view_loading_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/loading.c"], ) @@ -69,7 +69,7 @@ App( appid="js_gui__empty_screen", apptype=FlipperAppType.PLUGIN, entry_point="js_view_empty_screen_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/empty_screen.c"], ) @@ -77,7 +77,7 @@ App( appid="js_gui__submenu", apptype=FlipperAppType.PLUGIN, entry_point="js_view_submenu_ep", - requires=["js_app", "js_gui"], + requires=["js_app"], sources=["modules/js_gui/submenu.c"], ) @@ -85,7 +85,7 @@ App( appid="js_gui__text_input", apptype=FlipperAppType.PLUGIN, entry_point="js_view_text_input_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/text_input.c"], ) @@ -93,7 +93,7 @@ App( appid="js_gui__byte_input", apptype=FlipperAppType.PLUGIN, entry_point="js_view_byte_input_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/byte_input.c"], ) @@ -149,7 +149,7 @@ App( appid="js_gpio", apptype=FlipperAppType.PLUGIN, entry_point="js_gpio_ep", - requires=["js_app", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gpio.c"], ) diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js index 758cffd13..d1ace3845 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js @@ -45,7 +45,7 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) badusb.println("Flipper Model: " + flipper.getModel()); badusb.println("Flipper Name: " + flipper.getName()); - badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%"); + badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%"); // Alt+Numpad method works only on Windows!!! badusb.altPrintln("This was printed with Alt+Numpad method!"); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js b/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js index 9a30ad151..1187f1c20 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js @@ -45,7 +45,7 @@ function sendRandomModelAdvertisement() { blebeacon.start(); - print("Sent data for model ID " + toString(model)); + print("Sent data for model ID " + model.toString()); currentIndex = (currentIndex + 1) % watchValues.length; diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js index 10e88b649..5faeb6d32 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js @@ -97,7 +97,7 @@ eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) { let data_view = Uint8Array(data); let text = "0x"; for (let i = 0; i < data_view.length; i++) { - text += toString(data_view[i], 16); + text += data_view[i].toString(16); } views.helloDialog.set("text", "You typed:\n" + text); views.helloDialog.set("center", "Cool!"); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js b/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js index 7c57f9bc5..b2facb237 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js @@ -1,6 +1,6 @@ let sampleText = "Hello, World!"; -let lengthOfText = "Length of text: " + toString(sampleText.length); +let lengthOfText = "Length of text: " + sampleText.length.toString(); print(lengthOfText); let start = 7; @@ -9,7 +9,7 @@ let substringResult = sampleText.slice(start, end); print(substringResult); let searchStr = "World"; -let result2 = toString(sampleText.indexOf(searchStr)); +let result2 = sampleText.indexOf(searchStr).toString(); print(result2); let upperCaseText = "Text in upper case: " + sampleText.toUpperCase(); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js b/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js index 72b4617a6..d43c0baa3 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js @@ -6,7 +6,7 @@ while (1) { if (rx_data !== undefined) { serial.write(rx_data); let data_view = Uint8Array(rx_data); - print("0x" + toString(data_view[0], 16)); + print("0x" + data_view[0].toString(16)); } } diff --git a/applications/system/js_app/examples/apps/Scripts/interactive.js b/applications/system/js_app/examples/apps/Scripts/interactive.js index 29f3bb253..34639cdac 100644 --- a/applications/system/js_app/examples/apps/Scripts/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/interactive.js @@ -42,7 +42,7 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) { eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) { gui.viewDispatcher.switchTo(views.loading); - let path = ctx.tmpTemplate + toString(ctx.tmpNumber++); + let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString(); let file = storage.openFile(path, "w", "create_always"); file.write(text); file.close(); @@ -58,7 +58,7 @@ eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx } else if (typeof result === "string") { result = "'" + result + "'"; } else if (typeof result === "number") { - result = toString(result); + result = result.toString(); } else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check result = "bigint"; } else if (typeof result === "boolean") { diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 31e9f721e..7a774d324 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -196,16 +196,6 @@ static void js_require(struct mjs* mjs) { mjs_return(mjs, req_object); } -static void js_global_to_string(struct mjs* mjs) { - int base = 10; - if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); - double num = mjs_get_double(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-2147483648"; - itoa(num, tmp_str, base); - mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); - mjs_return(mjs, ret); -} - static void js_parse_int(struct mjs* mjs) { const char* str; int32_t base = 10; @@ -268,7 +258,6 @@ static int32_t js_thread(void* arg) { } mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); - mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string)); mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int)); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index cd87e5c4b..4bd4ccc31 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -260,26 +260,6 @@ static void js_gui_view_set(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } -/** - * @brief `View.hasProperty` - */ -static void js_gui_view_has_property(struct mjs* mjs) { - const char* name; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name)); - JsGuiViewData* data = JS_GET_CONTEXT(mjs); - const JsViewDescriptor* descriptor = data->descriptor; - - for(size_t i = 0; i < descriptor->prop_cnt; i++) { - JsViewPropDescriptor prop = descriptor->props[i]; - if(strcmp(prop.name, name) != 0) continue; - - mjs_return(mjs, mjs_mk_boolean(mjs, true)); - return; - } - - mjs_return(mjs, mjs_mk_boolean(mjs, false)); -} - /** * @brief `View` destructor */ @@ -304,7 +284,6 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr // generic view API mjs_val_t view_obj = mjs_mk_object(mjs); mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set)); - mjs_set(mjs, view_obj, "hasProperty", ~0, MJS_MK_FN(js_gui_view_has_property)); // object data JsGuiViewData* data = malloc(sizeof(JsGuiViewData)); diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index 052a2bb6a..d132f89f5 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -11,13 +11,6 @@ declare function delay(ms: number): void; */ declare function print(...args: any[]): void; -/** - * @brief Converts a number to a string - * @param value The number to convert to a string - * @param base Integer base (`2`...`16`), default: 10 - */ -declare function toString(value: number, base?: number): string; - /** * @brief Converts a string to a number * @param text The string to convert to a number @@ -214,7 +207,13 @@ declare class Boolean { } declare class Function { } -declare class Number { } +declare class Number { + /** + * @brief Converts this number to a string + * @param base Integer base (`2`...`16`), default: 10 + */ + toString(base?: number): string; +} declare class Object { } diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts index 5efcfdc03..d4cdc6bc9 100644 --- a/applications/system/js_app/types/gui/index.d.ts +++ b/applications/system/js_app/types/gui/index.d.ts @@ -9,11 +9,6 @@ export declare class View { * @param value Value to assign */ set

(property: P, value: Props[P]): void; - /** - * Check if property is available - * @param name Name of the property - */ - hasProperty(name: string): boolean; } export declare class ViewFactory> { diff --git a/lib/mjs/mjs_exec.c b/lib/mjs/mjs_exec.c index 0213a871e..8fdb2d7e5 100644 --- a/lib/mjs/mjs_exec.c +++ b/lib/mjs/mjs_exec.c @@ -475,6 +475,22 @@ static int getprop_builtin_string( return 0; } +static int getprop_builtin_number( + struct mjs* mjs, + mjs_val_t val, + const char* name, + size_t name_len, + mjs_val_t* res) { + if(strcmp(name, "toString") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_number_to_string); + return 1; + } + + (void)val; + (void)name_len; + return 0; +} + static int getprop_builtin_array( struct mjs* mjs, mjs_val_t val, @@ -589,6 +605,8 @@ static int getprop_builtin(struct mjs* mjs, mjs_val_t val, mjs_val_t name, mjs_v } else if(s != NULL && n == 5 && strncmp(s, "apply", n) == 0) { *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_apply_); handled = 1; + } else if(mjs_is_number(val)) { + handled = getprop_builtin_number(mjs, val, s, n, res); } else if(mjs_is_array(val)) { handled = getprop_builtin_array(mjs, val, s, n, res); } else if(mjs_is_foreign(val)) { diff --git a/lib/mjs/mjs_primitive.c b/lib/mjs/mjs_primitive.c index b63a268e5..e73ae892d 100644 --- a/lib/mjs/mjs_primitive.c +++ b/lib/mjs/mjs_primitive.c @@ -6,6 +6,8 @@ #include "mjs_core.h" #include "mjs_internal.h" #include "mjs_primitive.h" +#include "mjs_string_public.h" +#include "mjs_util.h" mjs_val_t mjs_mk_null(void) { return MJS_NULL; @@ -158,3 +160,31 @@ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs) { mjs_return(mjs, ret); } + +MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs) { + mjs_val_t ret = MJS_UNDEFINED; + mjs_val_t base_v = MJS_UNDEFINED; + int32_t base = 10; + int32_t num; + + /* get number from `this` */ + if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_NUMBER, NULL)) { + goto clean; + } + num = mjs_get_int32(mjs, mjs->vals.this_obj); + + if(mjs_nargs(mjs) >= 1) { + /* get base from arg 0 */ + if(!mjs_check_arg(mjs, 0, "base", MJS_TYPE_NUMBER, &base_v)) { + goto clean; + } + base = mjs_get_int(mjs, base_v); + } + + char tmp_str[] = "-2147483648"; + itoa(num, tmp_str, base); + ret = mjs_mk_string(mjs, tmp_str, ~0, true); + +clean: + mjs_return(mjs, ret); +} diff --git a/lib/mjs/mjs_primitive.h b/lib/mjs/mjs_primitive.h index 6d6f91785..f1c3c8220 100644 --- a/lib/mjs/mjs_primitive.h +++ b/lib/mjs/mjs_primitive.h @@ -34,6 +34,11 @@ MJS_PRIVATE void* get_ptr(mjs_val_t v); */ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs); +/* + * Implementation for JS Number.toString() + */ +MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs); + #if defined(__cplusplus) } #endif /* __cplusplus */ From 9865a9367fac1ac4311004bca3b894c8aa9c7530 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:31:00 +0100 Subject: [PATCH 22/74] IR: Basic script to remove dupes had this lying around from last week PRs and could come useful again, code is meh but works --- .../resources/infrared/assets/digital_sign.ir | 24 ----------- scripts/fix_ir_universals.py | 40 +++++++++++++++++++ scripts/fix_mfc_dict.py | 1 + 3 files changed, 41 insertions(+), 24 deletions(-) create mode 100755 scripts/fix_ir_universals.py mode change 100644 => 100755 scripts/fix_mfc_dict.py diff --git a/applications/main/infrared/resources/infrared/assets/digital_sign.ir b/applications/main/infrared/resources/infrared/assets/digital_sign.ir index bfe058c54..7c7c81fef 100644 --- a/applications/main/infrared/resources/infrared/assets/digital_sign.ir +++ b/applications/main/infrared/resources/infrared/assets/digital_sign.ir @@ -76,30 +76,6 @@ address: 80 02 20 00 command: 70 00 00 00 # name: POWER -type: parsed -protocol: RC6 -address: 00 00 00 00 -command: 0C 00 00 00 -# -name: SOURCE -type: parsed -protocol: RC6 -address: 00 00 00 00 -command: 38 00 00 00 -# -name: PLAY -type: parsed -protocol: RC6 -address: 00 00 00 00 -command: 2C 00 00 00 -# -name: STOP -type: parsed -protocol: RC6 -address: 00 00 00 00 -command: 31 00 00 00 -# -name: POWER type: raw frequency: 38000 duty_cycle: 0.330000 diff --git a/scripts/fix_ir_universals.py b/scripts/fix_ir_universals.py new file mode 100755 index 000000000..e51687593 --- /dev/null +++ b/scripts/fix_ir_universals.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import pathlib + +universals = ( + pathlib.Path(__file__).parent + / "../applications/main/infrared/resources/infrared/assets" +) + +for universal in universals.glob("*.ir"): + text = universal.read_text() + lines = text.splitlines() + signal = [] + comment = [] + signals = [] + for line in lines: + if line.startswith("#"): + comment.append(line) + continue + signal.append(line) + if line.startswith(("data: ", "command: ")): # Last line of this signal + signals.append(("\n".join(signal), "\n".join(comment))) + signal.clear() + comment.clear() + found = dict() + for signal, comment in signals: + if signal in found: + if ( + universal.stem == "projectors" + and found[signal] == 1 + and signal.startswith("name: Power") + ): + # Projectors need double press of power to confirm shutdown, so 1 dupe is fine + found[signal] += 1 + continue + replace = f"\n{comment}\n{signal}" + pos = text.rfind(replace) + text = text[:pos] + text[pos + len(replace) :] + continue + found[signal] = 1 + universal.write_text(text) diff --git a/scripts/fix_mfc_dict.py b/scripts/fix_mfc_dict.py old mode 100644 new mode 100755 index 3bdaf5c69..f89973d3c --- a/scripts/fix_mfc_dict.py +++ b/scripts/fix_mfc_dict.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 import pathlib import string From b17147053671855e1c1e6c714ecad8fcaad3f37f Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:42:29 +0100 Subject: [PATCH 23/74] Fix changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e756d12..617951345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,11 +39,11 @@ - globals: - `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs - `to_string()` renamed and moved to number class as `n.toString()`, now supports optional base parameter - - `to_hex_string()` removed, now use `toString(num, 16)` + - `to_hex_string()` removed, now use `n.toString(16)` - `parse_int()` renamed to `parseInt()`, now supports optional base parameter - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `s.toUpperCase()` and `s.toLowerCase()` - effort required to update old scripts using these: minimal - - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript, and you code in javascript) + - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript) - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct - Type definitions for extra modules we have that OFW doesn't will come later From 2e3c6d32cf48f3c364e46acb7bd4375816d0994e Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:09:24 +0100 Subject: [PATCH 24/74] Sync apps and hopefully wake github up PR 270 merged, but no github actions ran? --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index 2d045b733..14cdbeee8 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 2d045b733614b1ead29adcbbab421bdd09c2fdb4 +Subproject commit 14cdbeee81347b9719da143a42ca8b042d9de59c From 0b7f174162d9276a18128fa5763bb13a97d342c8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 19 Oct 2024 12:33:57 +0300 Subject: [PATCH 25/74] fix icon --- .../system/js_app/modules/js_gui/file_picker.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/applications/system/js_app/modules/js_gui/file_picker.c b/applications/system/js_app/modules/js_gui/file_picker.c index 49cf5e89d..b9736ba35 100644 --- a/applications/system/js_app/modules/js_gui/file_picker.c +++ b/applications/system/js_app/modules/js_gui/file_picker.c @@ -1,6 +1,17 @@ #include "../../js_modules.h" #include -#include + +// File icon +#include +static const uint8_t _I_file_10px_0[] = { + 0x00, 0x7f, 0x00, 0xa1, 0x00, 0x2d, 0x01, 0xe1, 0x01, 0x0d, 0x01, + 0x01, 0x01, 0x7d, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0x01, +}; +static const uint8_t* const _I_file_10px[] = {_I_file_10px_0}; + +static const Icon I_file_10px = + {.width = 10, .height = 10, .frame_count = 1, .frame_rate = 0, .frames = _I_file_10px}; +// File icon end static void js_gui_file_picker_pick_file(struct mjs* mjs) { const char *base_path, *extension; From 8cdb5619f5664d799e4ca4dce360140365dbea91 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:19:57 +0300 Subject: [PATCH 26/74] keeloq add key --- .../resources/subghz/assets/keeloq_mfcodes | 115 +++++++++--------- lib/subghz/protocols/keeloq.c | 10 +- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes index 0db72444e..23b7a031a 100644 --- a/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes +++ b/applications/main/subghz/resources/subghz/assets/keeloq_mfcodes @@ -1,60 +1,61 @@ Filetype: Flipper SubGhz Keystore File Version: 0 Encryption: 1 -IV: 41 72 65 20 79 6F 75 20 61 20 46 75 72 72 79 3F -8B441920A9F30EA40001F37E0F3C0D72161467678511928D219A7DBC42F8D9B7 -1ADE5B8541491C46674EECC05A1C4297CB28259954FD7C6DA0EAADE8D57773FC -0497A50135FA4468B49694974DAADEF2A9CC2B7BAFC8465E0AA58E940C085850 -4BC77520AE09B4F6A5A5365B6B9C16CA53566E096159A98EF0AB6E7FE5C2DD1C -B674AC1674312E74471E44917F6FC049DAB48BBB65B84F14A4CD64223A49E93DC0C507B8E17BFFDCBB67B55F1B4091C6 -FBBE4E4F703A9C1F17268F73EC00A3F76517A4470783363102D2B7647F0F2E7F -21BE8A51F32CE62DA6B5E207E627D224639877860C71933C4F54AC7BD742B265 -74E3EA89CC7AA99855C1BC87734506413B882CF36D68C3329022415EB29D1360 -567937E84052F230610B3A2FC26CA1BE32B9617F7554EF5E6E7CB35A76718ADA -4C34F7FE5D6EAE40B0EB6BAB7DE4F0D059C32E4F312F9C62F2C50C54C5486276 -74C8656F8EA033C60F3183FA0923C9AF1EB6689FAACC39E6548ECE16E3312D39DCF45AC6EC70A70421FBFFA43D91E19E -275903678A81B6E62CD2BB4C50E93CB3E4B092E2894EBAA04AC4E0244E4D5BA9FCDFC6BCF845EA41DF0691BF2AEDF300 -BFE52324D666ADEB3D33D94331376B33A3D31566331DE0F372548FE7255F8085 -8E1B2E82B4C78360B3174741FB8031EE0DFEB479859BE8708F8AF40C5228BFDF -6014A4A9D9703DAC11DA6B93FB7A0E0E0856E9379148F3CC125D77BA4C3D4931 -B8C22EA72B35E60A7A3DD02956D2375EE7C07E243F17F413CA4AC3A1445AA250 -B545037B8798370A8638B7ADCBE9EEF748F73C89EB5E04B460057BAFA52B1D7DF29B009FBB3FA07CE87247D185BE4926 -09AEA3223E4FFB06333425BA977403E7C2F9B5F121FDA8313E73FE98D0041425 -E7A97F65883E4D3CDA02483738219E219EED0681C2BB140EF3E5D02230433D42 -4AE67B9EA288183047A9498F58978E7C41BB8905533F42FF5B94E41AFEFF0EB4907A8C472B9E32F6D659CFA6C391179B -1F9752FBA7A7A00D518D91BA8A8C1ADEF2EDF6282A5AFB2E4332B4351CF67F18317D2BCFF2DA6B5216EA138A4CCBDB81 -EDC508FE0D0E894B529C5E4AB82A7188072592BE9D4D9A3D2485822A9F4C5A67 -780582645A71D3A9841B0E8F5E84A2285F783872102AA1A98A956B98AEA19421 -37E2858B90868BCAA6B3198C3B205DC1413A9D378AF1671BEA3B8F47FCE1C5CE -229719C5AFE1E567B8BD5F07EFF89C30B11CB79C9C841FFE7B53404CA5422EDB -CDA16E59B8C24DBE95ABF978CAD4D04A4F124DED1965124F92AACA4C5AEB8721 -5C61D5C655DD9EB78C4F8E965101F98D7899155D2379EF6553FA9DEA0302A232 -7D3DCFDE942398244624C7ABFE08363404BA688EDA39D45A05BB6FB982BB9042 -ECCB10BCB2A8CF2093C61634E9AFEE9B69EB4A7C8B7139061730B5FA869F65BF -7AE6C19ED3C97F45D1167FC9EF52CCE321EA002D31D8AEA94106A2F711626551 -793A984F6EBD35253C319FAB671348A4C6843AEA7CCCD5A52ACA9BBE96484B5E -BB3ADBB5FD60F0FB01E5A8B1610D30A24975DC67ED1B70472E509218E213393E -BB971C6304723B9928F50FA2E5C30A48ED5527556EE1BA5A7EA75AB7A4ED4137 -489579673025F7A351072EB8D5C7FC1DBF651AA6233FB01D99DAC4BDC0A60F2F -ACA000B78B091A9B2621A9FA1A50FA6A3B63DDDF7D966224837028F93D570C08 -C1EB4EA179EC0437D5B0E72C47AAD06803F7619C53D3D2FEF38DD3C068118766B73202B75C534C6249312245218C9285 -639C6F53600C0F01410361164FEF715DB8E08C68D68B8AE76CF2D09A0C6C4CA1 -F5F2D77DD3AF60489312055EBA3B511F7D9E312F02465639FFF3E71A862BB9EB -3EC1F7B2C43646249C4D14B4B1B85B3416B8D3B0B8DA9E87BD6199DC27BD45C2 -AA24DB7345317FE980C917909F364EAA13591A664E0893271E9DBCC250782EA5 -32C18DBA479AD57BFF9FE7B7BD1022D4969D8E2EC99BC0799148809796E227D5 -494B44697DACDFDBBFEB0FDBEF6E9B5D7291B6CB43B4C93088F3ED8632AF23B5 -B79CC1A90C7AC36DA2F318BF69E6BB1FAC32C178453663069C0CF2C4AE77B211 -A059D006EF98446BDF9E1F5E09641A641CA88348E79499D79BCFDE6646C716A0BE2253421858D327E796EB5237B6E2C4 -9E90AE6138A3676E3E5058FF277331032CD6BBF114768104A33D684B0825C5A5 -7477B55332666FC6B1279F5FDF84DCA01176498984809B74E17AB726F888000F -6A0300B90BC11127A031E62537468A1CA7A0775BE85ED12069074E26181FBA50 -4DAF986F9A850EB18F0FE3B88B9796D80EEC3984C92F4F8B32D513A176CCAA9A -EB84517DCAA7D10A6E7C4D0A7995AE278AAF3FDE52DAE30C7FD3FB853C799369 -0941FB8965604ABC852B94F29396783783632E4C80AEE665E42C233B6E7AA7D0 -71FEF69734589BE3C0893A1584FC1C0A75A5D4BF88CFF323C8C68E0EEC06993C -790A26B77912EE07BABCDF478D8E492422B73465DC209E5DBC8E31F43AA2DA7448EAB6312AC1DC4FE3F20D7A2EC93968 -CE0AA966F33B7AC9E9982D2241524F2FC7613DAAC6C5C768AAAE29A630B414CE -93FA632D945E18DE6358D4F443CB6F023C640407B7FAD6E8AF6EF15139B044E3 -059ABA7A866A865159D4E0B3E30FA1900BAB2BAB82A1D1F9D74CAEC11FA95393 -9C5A4CF1D49E0F741577BAB868575739AEBBDC15D7178C87A46EA3B3E7E856EFD815AD5F998778E8FBAA2832F387B967 +IV: 46 75 72 72 79 20 63 68 65 63 6B 20 3F 3F 21 21 +3FF98B52C38558ECDB26C3E86D118FAC9AF22EDDDE7649CB53726855CEBCCF9E +06B901C6E36E381B4A83554ED972288977F999C232106D337C3BE4A9A44608F8 +8153E6CAF4B272BA0EA168B9F0F29CF02CEB33E4ABBD4D5C858D1ADBFF474A25 +F61216CE3A7E500E0C9B2173F91C2E7B1BB7D7AA65D4150EAC28169116647DDD +A3ABC262415035A190447EC9C15CFD1AB1720560AB7C82D7447215342305299732776A9A6DEA4D359C52A23BFCE6B015 +5784D0E77A55E1361F47A1F6CFFBFAF715CFCAD2BD502ED266AA86DC47F98541 +E082BF64C75023F23FB333C53F2590F408FD932EB71DF4ED4D0E0EAF2AEBD488 +EFF328889D57D9F4B9111918F9C9BC641645104223009842FFF7B6F73E24E5B7 +BDCD1DBCBCA789C5A5C3623C0A287D791F1CACCBBD7D144E2EF1F92DA5513D90 +6310362BE6ED7CB5E3F4CA84D3093DF620BABD2C9D419A2BA5AA05F241EE7592 +4C6F97C553276D5403103A6BA6C4DBBE017A9E4159E6E5AC4C28ACD645DB1E8D4551CDC228A0457BEEF49179A081E1FD +861ECA071AEEF3854005FEC9BF22C7DB76D07B7930968314FFAC70995E28680E473F8A1A7E56E8776EDD1C6E2DD6DD26 +55ADDFBE97BCB0FB43EDEB9CAFF9F32DD788A6306D3702BC2923DA5C69F5F0A5 +AB838DA2B25547AA43DEB7FB40B15E289B9209057BE564E7CB78F0D5DCFC65FD +21BACE924E522AD0E97F0AAF64A9DB6A5F7BA09B3A1759AAFD4016F3ADD4353D +4B37D449E44BAE7F377EE5CE52E94882E42617417F77ECE9803D9435892167F2 +E63BA93D98BB3EEEAD34B38C5F271FB777AEABD9E6A05597AE09815E578AA811900AE9F144FBEC7DBFDDEC87D1ED368B +7DF24DCB7D73F1638FFD22325463B23C101653EE770F086D323BE90868A7E267 +18E533990C408061309323675116429652B3F4D228F00D704310E7AB26F6BAF4 +26042EC21D73490D9E7F968EDCC4CECF3989E6E982427DEB7012478E214CED8797A9BEBC481C81E9646214809A409B4A +AC4169C26E402720F1E2A0AD690AE2CF708CE203898BC7102178A738C70F361718E794F0D0CB1A2F1938EF35CCA11887 +6962CADB9BD1B86B779DF7F4D7E06E92EB42474C7FD9EA23879F9982D7127357 +23AEC0F6F9FF529DDDCD1CDAA77F7B136CE2CCD3AC8B949954D5D3B2ECFA8CBC +C6A3A849BC4A8DB438065255BD387DCD77AD7E7FEB3B0E11B6D3A43279AB9087 +CCB71F8BDC8B31F36D6141B3B57BF31B7EDF72B87593B7497871F5738A0B7E00 +345FE0FAD3F60C017D9793406981878EA2226072D624208AC33682793B415C41 +78AF1FCFDB780744708DBB547F7C3F095BA5FAFB34FA83BF4323B32829836ED1 +C2F19F077E71E710D39F5F11C1FE55C2A3CD6F33384CD8603288765F266F1BDF +084B1A9565AFC88900D0FF25413E659E17AAB649DB2B7A9F0381CB0DF6D2B8E5 +568CECD994676CF3D6B225DEF21D40787DCB41C101F52B0C5AA43ED66709D158 +0742E0AAB6504AC63A9C59FBDD980BD4C4760EFF3E556C8A6241442CDDD23A74 +37BB60EE11BEC7D25A607DE9B0B6D3EAD23321AFEB94995FAB42184A95D1CADA +42FD71B98A3BEE9CF372B03E158D51181BFAA9CF54F7300A93FE7665402B3D1F +9346B7D12346E264E84F91145EBF86C53DD0061E0FA6556FAE5F6FBB8CEB799E +430B0D7FC09C09051AEB667A370E6D9D9BDF0C0C553AD2791682D43CE5DC48B7 +5BBB5129797BC07CDC1D25A8A919A637B9FFF2F920BD42D1721018ABF8D34959AF877AFF450AD91548184E1A0D991CC8 +0BC8D3E1C2D9A8FB445FC55564471007A28C09C9CE602203176F1BFF02AC6328 +1C4CA912791BCD9CB231C64AB479AF240D02EC9F431D7C479B9A172E5B97F5CD +15A9CDF17E72255DE0942C09E67CD251C3D465246845F9C0B97A7EFEE4AF9110 +F95543395BFF39B0A093AC80D0EF1BEFA218A3769D074250414D1357105A4D34 +138D433824E691C6B67A08CF1DDE55FBCA2D65AC0B7D9320EA1FDCB1742B11BF +3BE3A385F1C0B8922C4E23EFF6912748DF715A4546CECB5E8972C1D1E47D0D3A +9ECA2554ED36700326F2E5140C434ADA8DFF55A53382F19541E9AAE45DA5CBF2 +5C75D528678AF199E191C49F310913F401014F97EEA5FA507C7310B48A98FDE3297EA398B08959951FF99EDDB64C5E0D +22A9C66645B3944925A496D9F2312429CC787B6314948B482EAD9360124F59DA +DA3A8571664DBBFC1DE97B53E7C141554A2FBCEBA980696D32409CC5ADB7FC41 +20A52AABC518FCC2FC75AE3F5CC7C4838AA4973111DA696B890D884A18098D91 +EBB7163F580A1A5D26F12FBE650A227791193BA9AFCE277584B171F2FE1C77CF +86369EE5277CB81B9417B6232F8D994FAEAB34D0D5363B143257C62B10CACAFD +2E2EAB32891E172A3C31D434703480E69793435BB198E6AA06AA066EE8234D85 +745FC576D77C41BAEFF15A822E6B4058A485A2CAA0A3B283928D17AC02299AD9 +1FF8D49F2F7D785D64B6FE365CD9C2BD958E9527F66BB8A85C9AEBC73ECFE064 +3CFB77F3E274C1EB2772CDFA7B5B17255C2554198BE60C058A3405AEC644FABC5AAECC8F9C7F4A4E5B2D5252E8C62628 +AE514C44B55A1A4744E1106FD226C587D1B71CA7B5DCF010265D769E22012866 +2B2D787A4B0F30CAB9CD3DBC7686165637F091B31745CAB53B369A804F76F9EE +EA4279C80F0B4AC0B32AB9E8B8CFB9C25FF81840BEE65B2160F85E56FA576C48 +E41D853750D68643E929F94F46BCDC050040935883E9A0C45BB238CDE06340DD745BDA7D6C16AE2B028E073EDDC0FC49 diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index cee45cfd6..60ff2f0f1 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -253,6 +253,9 @@ static bool subghz_protocol_keeloq_gen_data( } else if(strcmp(instance->manufacture_name, "Centurion") == 0) { decrypt = btn << 28 | (0x1CE) << 16 | instance->generic.cnt; // Centurion -> no serial in hop, uses fixed value 0x1CE - normal learning + } else if(strcmp(instance->manufacture_name, "Monarch") == 0) { + decrypt = btn << 28 | (0x100) << 16 | instance->generic.cnt; + // Monarch -> no serial in hop, uses fixed value 0x100 - normal learning } else if(strcmp(instance->manufacture_name, "Dea_Mio") == 0) { uint8_t first_disc_num = (instance->generic.serial >> 8) & 0xF; uint8_t result_disc = (0xC + (first_disc_num % 4)); @@ -428,12 +431,13 @@ static bool (strcmp(instance->manufacture_name, "Dea_Mio") == 0) || (strcmp(instance->manufacture_name, "NICE_MHOUSE") == 0)) { klq_last_custom_btn = 0xF; - } else if((strcmp(instance->manufacture_name, "FAAC_RC,XT") == 0)) { + } else if( + (strcmp(instance->manufacture_name, "FAAC_RC,XT") == 0) || + (strcmp(instance->manufacture_name, "Monarch") == 0) || + (strcmp(instance->manufacture_name, "NICE_Smilo") == 0)) { klq_last_custom_btn = 0xB; } else if((strcmp(instance->manufacture_name, "Novoferm") == 0)) { klq_last_custom_btn = 0x9; - } else if((strcmp(instance->manufacture_name, "NICE_Smilo") == 0)) { - klq_last_custom_btn = 0xB; } else if((strcmp(instance->manufacture_name, "EcoStar") == 0)) { klq_last_custom_btn = 0x6; } From 2441643ac6ab3984625c42c6cf69c9f35a01762d Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:40:35 +0300 Subject: [PATCH 27/74] subghz monarch add manually --- .../main/subghz/helpers/subghz_custom_event.h | 1 + .../main/subghz/scenes/subghz_scene_set_type.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/applications/main/subghz/helpers/subghz_custom_event.h b/applications/main/subghz/helpers/subghz_custom_event.h index a3cb4a72a..e28c018d8 100644 --- a/applications/main/subghz/helpers/subghz_custom_event.h +++ b/applications/main/subghz/helpers/subghz_custom_event.h @@ -81,6 +81,7 @@ typedef enum { SetTypeAllmatic433, SetTypeAllmatic868, SetTypeCenturion433, + SetTypeMonarch433, SetTypeSommer_FM_434, SetTypeSommer_FM_868, SetTypeSommer_FM238_434, diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 43bf05044..a03c194f8 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -28,6 +28,7 @@ static const char* submenu_names[SetTypeMAX] = { [SetTypeAllmatic433] = "KL: Allmatic 433MHz", [SetTypeAllmatic868] = "KL: Allmatic 868MHz", [SetTypeCenturion433] = "KL: Centurion 433MHz", + [SetTypeMonarch433] = "KL: Monarch 433MHz", [SetTypeSommer_FM_434] = "KL: Sommer 434MHz", [SetTypeSommer_FM_868] = "KL: Sommer 868MHz", [SetTypeSommer_FM238_434] = "KL: Sommer fm2 434Mhz", @@ -417,6 +418,16 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .keeloq.cnt = 0x03, .keeloq.manuf = "Centurion"}; break; + case SetTypeMonarch433: + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF), + .keeloq.btn = 0x0A, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Monarch"}; + break; case SetTypeElmesElectronic: gen_info = (GenInfo){ .type = GenKeeloq, From abd3add64e878d8f45b09787f7465d1c7315803a Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 20 Oct 2024 05:42:44 +0100 Subject: [PATCH 28/74] Suppress plugin warn for NFC plugin symbols --nobuild --- scripts/fbt_tools/fbt_extapps.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index 9c3c28124..da5e272a8 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -314,8 +314,9 @@ def _validate_app_imports(target, source, env): "totp_", "token_info_", "memset_s", - # troika + # social_moscow, troika "mosgortrans_parse_transport_block", + "render_section_header", ) ) and any( @@ -324,6 +325,7 @@ def _validate_app_imports(target, source, env): "advanced_plugin", "gallagher", "js_", # js_app and all js_ modules + "social_moscow", "test_js", "totp_", "troika", From e682b6b19e1c1f4a6c373f72fc526d527f9375d7 Mon Sep 17 00:00:00 2001 From: Derek Jamison Date: Sat, 19 Oct 2024 23:24:44 -0600 Subject: [PATCH 29/74] JavaScript: Add I2C support (#259) * JavaScript: Add I2C support * Format * Check input bytes, allow arraybuf write input * Add comment * Return arraybuf, remove unnecessary malloc and memset * More meaningful var names for code readability, fix some bugs * Remove unnecessary state * Fix build * Update changelog * Add typedefs * toString() updates in example script --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> --- CHANGELOG.md | 2 + applications/system/js_app/application.fam | 8 + .../examples/apps/Scripts/Examples/i2c.js | 48 +++ applications/system/js_app/modules/js_i2c.c | 280 ++++++++++++++++++ .../system/js_app/types/i2c/index.d.ts | 31 ++ 5 files changed, 369 insertions(+) create mode 100644 applications/system/js_app/examples/apps/Scripts/Examples/i2c.js create mode 100644 applications/system/js_app/modules/js_i2c.c create mode 100644 applications/system/js_app/types/i2c/index.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 617951345..bdeb6e0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ - Bluray/DVD Universal Remote (#250 by @jaylikesbunda) - Option to "Load from Library File" for Universal Remotes (#255 by @zxkmm) - Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap) +- JS: + - New `i2c` module (#259 by @jamisonderek) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) - Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme) diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 65fafed76..8c7cdf9ad 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -208,3 +208,11 @@ App( requires=["js_app"], sources=["modules/js_usbdisk/*.c"], ) + +App( + appid="js_i2c", + apptype=FlipperAppType.PLUGIN, + entry_point="js_i2c_ep", + requires=["js_app"], + sources=["modules/js_i2c.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js b/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js new file mode 100644 index 000000000..456b44ccd --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js @@ -0,0 +1,48 @@ +// Connect an 24C32N EEPROM to the I2C bus of the board. SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8. +let i2c = require("i2c"); + +function i2c_find_first_device() { + let addr = -1; + for (let try_addr = 0; try_addr !== 0xff; try_addr++) { + if (i2c.isDeviceReady(try_addr, 5)) { + addr = try_addr; + break; + } + } + return addr; +} + +let addr = i2c_find_first_device(); +if (addr === -1) { + print("I2C device not found"); + print("Please connect a 24C32N EEPROM I2C device to the Flipper Zero."); + print("SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8."); +} else { + print("I2C device found at address: " + addr.toString(16)); + delay(1000); + + // first two bytes are the start address (0x0000) + // the remaining bytes are the data to store. + // can also use Uint8Array([0x00, 0x00, ...]) as write parameter + i2c.write(addr, [0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47]); + while (i2c.isDeviceReady(addr, 9) === false) { + print("Waiting for device to be ready..."); + } + + // write the address to read from (we start at address 0x0001) + // read 3 bytes - 0x42, 0x43, 0x44 + let data_buf = i2c.writeRead(addr, [0x00, 0x01], 3, 100); + let data = Uint8Array(data_buf); + print("Read bytes: " + data.length.toString()); + for (let i = 0; i < data.length; i++) { + print("data[" + i.toString() + "] = " + data[i].toString(16)); + } + + // read two more bytes (0x45, 0x46) from current address + data_buf = i2c.read(addr, 2); + data = Uint8Array(data_buf); + print("Read bytes: " + data.length.toString()); + for (let i = 0; i < data.length; i++) { + print("data[" + i.toString() + "] = " + data[i].toString(16)); + } +} diff --git a/applications/system/js_app/modules/js_i2c.c b/applications/system/js_app/modules/js_i2c.c new file mode 100644 index 000000000..6f105142c --- /dev/null +++ b/applications/system/js_app/modules/js_i2c.c @@ -0,0 +1,280 @@ +#include "../js_modules.h" +#include + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count_range(struct mjs* mjs, size_t min_count, size_t max_count) { + size_t num_args = mjs_nargs(mjs); + if(num_args < min_count || num_args > max_count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_i2c_is_device_ready(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool ready = furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external, addr, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_return(mjs, mjs_mk_boolean(mjs, ready)); +} + +static void js_i2c_write(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 2, 3)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 1); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 2) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_tx(&furi_hal_i2c_handle_external, addr, tx_buf, tx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + if(tx_buf_was_allocated) free(tx_buf); + mjs_return(mjs, mjs_mk_boolean(mjs, result)); +} + +static void js_i2c_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 2, 3)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t rx_len_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + return; + } + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 2) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_rx(&furi_hal_i2c_handle_external, addr, rx_buf, rx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + free(rx_buf); + mjs_return(mjs, ret); +} + +static void js_i2c_write_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 3, 4)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 1); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + mjs_val_t rx_len_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 3) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 3); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_trx( + &furi_hal_i2c_handle_external, addr, tx_buf, tx_len, rx_buf, rx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + mjs_return(mjs, ret); +} + +static void* js_i2c_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + mjs_val_t i2c_obj = mjs_mk_object(mjs); + mjs_set(mjs, i2c_obj, "isDeviceReady", ~0, MJS_MK_FN(js_i2c_is_device_ready)); + mjs_set(mjs, i2c_obj, "write", ~0, MJS_MK_FN(js_i2c_write)); + mjs_set(mjs, i2c_obj, "read", ~0, MJS_MK_FN(js_i2c_read)); + mjs_set(mjs, i2c_obj, "writeRead", ~0, MJS_MK_FN(js_i2c_write_read)); + *object = i2c_obj; + + return (void*)1; +} + +static const JsModuleDescriptor js_i2c_desc = { + "i2c", + js_i2c_create, + NULL, + NULL, +}; + +static const FlipperAppPluginDescriptor i2c_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_i2c_desc, +}; + +const FlipperAppPluginDescriptor* js_i2c_ep(void) { + return &i2c_plugin_descriptor; +} diff --git a/applications/system/js_app/types/i2c/index.d.ts b/applications/system/js_app/types/i2c/index.d.ts new file mode 100644 index 000000000..ea0a640cb --- /dev/null +++ b/applications/system/js_app/types/i2c/index.d.ts @@ -0,0 +1,31 @@ +/** + * @brief Check if there is an I2C device ready on the bus + * @param address The device address to check + * @param timeout Timeout in milliseconds + */ +export declare function isDeviceReady(address: number, timeout?: number): boolean; + +/** + * @brief Write data to I2C device and return success status + * @param address The device address to write to + * @param data The data to write to the device + * @param timeout Timeout in milliseconds + */ +export declare function write(address: number, data: number[] | ArrayBuffer, timeout?: number): boolean; + +/** + * @brief Read data from I2C device or return undefined on failure + * @param address The device address to read from + * @param length How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function read(address: number, length: number, timeout?: number): ArrayBuffer | undefined; + +/** + * @brief Write data then read from I2C device or return undefined on failure + * @param address The device address to talk to + * @param writeData The data to write to the device + * @param readLength How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function writeRead(address: number, writeData: number[] | ArrayBuffer, readLength: number, timeout?: number): ArrayBuffer | undefined; From 7e09f4b774cf6fa1bce3b3a70f4f4e5dbfaed749 Mon Sep 17 00:00:00 2001 From: m7i-org <64315540+m7i-org@users.noreply.github.com> Date: Sun, 20 Oct 2024 01:27:03 -0400 Subject: [PATCH 30/74] Sub-GHz: Added Vauno-EN8822 weather station (#262) * feat(subghz): Added Vauno-EN8822 weather station * Update changelog --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> --- CHANGELOG.md | 1 + .../unit_tests/subghz/vauno_en8822c.sub | 16 ++ .../unit_tests/subghz/vauno_en8822c_raw.sub | 6 + .../unit_tests/tests/subghz/subghz_test.c | 8 + lib/subghz/protocols/protocol_items.c | 1 + lib/subghz/protocols/protocol_items.h | 1 + lib/subghz/protocols/vauno_en8822c.c | 258 ++++++++++++++++++ lib/subghz/protocols/vauno_en8822c.h | 80 ++++++ 8 files changed, 371 insertions(+) create mode 100644 applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c.sub create mode 100644 applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c_raw.sub create mode 100644 lib/subghz/protocols/vauno_en8822c.c create mode 100644 lib/subghz/protocols/vauno_en8822c.h diff --git a/CHANGELOG.md b/CHANGELOG.md index bdeb6e0c7..23fca96d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ - Sub-GHz: - Show satellites count with an icon (#215 by @m7i-org) - Add Bresser 3CH weather station protocol (#217 #245 by @m7i-org) + - Add Vauno-EN8822 weather station protocol (#262 by @m7i-org) - UL: Add Marantec24 protocol (static 24 bit) with add manually (by @xMasterX) - UL: Add GangQi protocol (static 34 bit) with button parsing and add manually (by @xMasterX & @Skorpionm) - UL: Add Hollarm protocol (static 42 bit) with button parsing and add manually (by @xMasterX & @Skorpionm) diff --git a/applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c.sub b/applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c.sub new file mode 100644 index 000000000..28751fe2f --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c.sub @@ -0,0 +1,16 @@ +Filetype: Flipper SubGhz Key File +Version: 1 +Frequency: 433920000 +Preset: FuriHalSubGhzPresetOok270Async +Lat: 0.000000 +Lon: 0.000000 +Protocol: Vauno-EN8822C +Id: 64 +Bit: 42 +Data: 00 00 01 00 02 16 88 1D +Batt: 0 +Hum: 81 +Ts: 1728836876 +Ch: 0 +Btn: 255 +Temp: 13.300000 diff --git a/applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c_raw.sub b/applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c_raw.sub new file mode 100644 index 000000000..86e51ebe7 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/subghz/vauno_en8822c_raw.sub @@ -0,0 +1,6 @@ +Filetype: Flipper SubGhz RAW File +Version: 1 +Frequency: 433920000 +Preset: FuriHalSubGhzPresetOok270Async +Protocol: RAW +RAW_Data: 588 -4136 546 -4150 578 -4158 546 -2096 578 -2092 592 -2068 592 -4126 578 -4144 570 -2064 580 -2092 594 -2102 564 -2168 582 -2078 582 -2112 544 -2102 572 -2096 572 -4130 574 -2114 552 -2090 594 -2064 590 -2104 560 -2094 566 -2090 564 -2092 580 -2098 566 -2096 570 -2164 578 -2090 546 -4140 578 -2104 568 -4138 578 -2078 550 -2086 594 -9092 592 -61848 200 -17582 66 -5072 130 -11038 100 -60 164 -8288 166 -2374 302 -94 166 -300 66 -18706 96 -25730 192 -66 158 -29578 64 -99962 128 -99858 96 -99908 130 -9892 164 -96 98 -89580 162 -106 76 -11000 196 -88450 506 -3900 522 -1900 522 -3878 500 -1936 482 -1958 488 -1938 500 -3906 462 -1966 482 -1950 500 -1924 502 -1958 474 -1940 480 -1960 520 -3858 512 -3878 524 -3888 464 -1968 482 -3910 478 -7792 518 -1912 502 -7802 478 -1962 476 -3902 510 -1938 500 -1938 480 -1954 504 -1936 488 -1954 476 -1960 476 -1948 478 -1964 466 -1994 446 -1970 464 -1972 480 -1954 474 -1954 482 -1970 474 -3934 446 -1958 466 -1978 480 -1956 468 -1960 472 -3900 516 -1948 478 -3908 494 -3878 508 -1938 502 -3896 482 -1950 480 -1938 514 -1924 516 -3884 484 -1940 502 -1942 480 -1958 502 -1930 506 -1938 480 -1934 516 -3860 510 -3912 488 -3888 504 -1940 486 -3876 506 -7800 526 -1928 476 -7790 512 -1942 508 -3876 486 -1918 518 -1962 482 -1954 482 -1930 498 -1958 470 -1936 490 -1962 484 -1960 476 -1932 520 -1938 490 -1928 508 -1952 478 -1940 480 -1958 500 -3892 498 -1930 484 -1960 474 -1936 496 -1934 514 -3890 508 -1906 522 -3882 478 -3916 484 -1932 506 -3888 512 -1928 502 -1928 502 -1938 492 -3920 490 -1924 510 -1928 498 -1934 480 -1954 488 -1972 462 -1964 478 -3896 484 -3902 476 -3912 484 -1944 476 -3916 498 -7784 510 -1930 504 -7796 498 -1936 518 -3864 492 -1964 482 -1950 474 -1962 470 -1954 490 -1960 478 -1952 482 -1934 520 -1934 482 -1948 482 -1944 486 -1970 448 -1976 500 -1936 480 -1964 474 -3902 484 -1960 480 -1952 482 -1962 474 -1950 486 -3914 448 -1984 450 -3916 520 -3872 490 -1942 480 -3938 448 -1970 464 -1972 482 -1964 484 -3910 484 -1938 476 -1950 516 -1926 470 -1970 514 -1906 500 -1950 506 -3876 510 -3850 524 -3896 486 -1934 518 -3870 506 -7798 512 -1912 512 -7798 516 -1918 478 -3924 458 -1956 472 -1954 506 -1936 512 -1930 474 -1968 482 -1930 504 -1940 486 -1958 482 -1960 486 -1936 486 -1936 494 -1934 516 -1912 510 -1962 474 -3882 504 -1956 478 -1938 510 -1926 500 -1924 514 -3920 450 -1970 478 -3904 488 -3896 502 -1942 486 -3912 476 -1926 508 -1940 486 -1962 474 -3920 484 -1930 512 -1944 470 -1970 470 -1948 502 -1936 478 -1962 482 -3914 484 -3878 502 -3906 472 -1968 488 -3894 498 -7774 496 -1948 diff --git a/applications/debug/unit_tests/tests/subghz/subghz_test.c b/applications/debug/unit_tests/tests/subghz/subghz_test.c index 8f1471089..f2e48e079 100644 --- a/applications/debug/unit_tests/tests/subghz/subghz_test.c +++ b/applications/debug/unit_tests/tests/subghz/subghz_test.c @@ -691,6 +691,13 @@ MU_TEST(subghz_decoder_bresser_3ch_v0_test) { "Test decoder " WS_PROTOCOL_BRESSER_3CH_NAME " v0 error\r\n"); } +MU_TEST(subghz_decoder_vauno_en8822c_test) { + mu_assert( + subghz_decoder_test( + EXT_PATH("unit_tests/subghz/vauno_en8822c.sub"), WS_PROTOCOL_VAUNO_EN8822C_NAME), + "Test decoder " WS_PROTOCOL_VAUNO_EN8822C_NAME " error\r\n"); +} + //test encoders MU_TEST(subghz_encoder_princeton_test) { mu_assert( @@ -916,6 +923,7 @@ MU_TEST_SUITE(subghz) { MU_RUN_TEST(subghz_decoder_solight_te44_test); MU_RUN_TEST(subghz_decoder_bresser_3ch_v1_test); MU_RUN_TEST(subghz_decoder_bresser_3ch_v0_test); + MU_RUN_TEST(subghz_decoder_vauno_en8822c_test); MU_RUN_TEST(subghz_encoder_princeton_test); MU_RUN_TEST(subghz_encoder_came_test); diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index c6a1c08f5..3ebb8b4f7 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -67,6 +67,7 @@ const SubGhzProtocol* subghz_protocol_registry_items[] = { &ws_protocol_kedsum_th, &ws_protocol_emose601x, &ws_protocol_acurite_5n1, + &ws_protocol_vauno_en8822c, &subghz_protocol_pocsag, &tpms_protocol_schrader_gg4, &subghz_protocol_bin_raw, diff --git a/lib/subghz/protocols/protocol_items.h b/lib/subghz/protocols/protocol_items.h index 48e72cc44..8fbbc9a82 100644 --- a/lib/subghz/protocols/protocol_items.h +++ b/lib/subghz/protocols/protocol_items.h @@ -68,6 +68,7 @@ #include "acurite_5n1.h" #include "solight_te44.h" #include "bresser_3ch.h" +#include "vauno_en8822c.h" #include "pocsag.h" #include "schrader_gg4.h" #include "bin_raw.h" diff --git a/lib/subghz/protocols/vauno_en8822c.c b/lib/subghz/protocols/vauno_en8822c.c new file mode 100644 index 000000000..e80ec2f84 --- /dev/null +++ b/lib/subghz/protocols/vauno_en8822c.c @@ -0,0 +1,258 @@ +#include "vauno_en8822c.h" +#include "furi/core/log.h" +#define TAG "WSProtocolVaunoEN8822C" + +/* + * Help + * https://github.com/merbanan/rtl_433/blob/master/src/devices/vauno_en8822c.c + * + * Vauno EN8822C sensor on 433.92MHz. + * + * Largely the same as Esperanza EWS, s3318p. + * @sa esperanza_ews.c s3318p.c + + * List of known supported devices: + * - Vauno EN8822C-1 + * - FUZHOU ESUN ELECTRONIC outdoor T21 sensor + * + * Frame structure (42 bits): + * + * Byte: 0 1 2 3 4 + * Nibble: 1 2 3 4 5 6 7 8 9 10 11 + * Type: IIIIIIII B?CCTTTT TTTTTTTT HHHHHHHF FFFBXXXX XX + * + * - I: Random device ID + * - C: Channel (1-3) + * - T: Temperature (Little-endian) + * - H: Humidity (Little-endian) + * - F: Flags (unknown) + * - B: Battery (1=low voltage ~<2.5V) + * - X: Checksum (6 bit nibble sum) + * + * Sample Data: + * + * [00] {42} af 0f a2 7c 01 c0 : 10101111 00001111 10100010 01111100 00000001 11 + * + * - Sensor ID = 175 = 0xaf + * - Channel = 0 + * - temp = -93 = 0x111110100010 + * - TemperatureC = -9.3 + * - hum = 62% = 0x0111110 + * + * Copyright (C) 2022 Jamie Barron + * + * @m7i-org - because the ether is wavy + * + */ + +static const SubGhzBlockConst ws_protocol_vauno_en8822c_const = { + .te_short = 500, + .te_long = 1940, + .te_delta = 150, + .min_count_bit_for_found = 42, +}; + +struct WSProtocolDecoderVaunoEN8822C { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + WSBlockGeneric generic; +}; + +struct WSProtocolEncoderVaunoEN8822C { + SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; + WSBlockGeneric generic; +}; + +const SubGhzProtocolDecoder ws_protocol_vauno_en8822c_decoder = { + .alloc = ws_protocol_decoder_vauno_en8822c_alloc, + .free = ws_protocol_decoder_vauno_en8822c_free, + + .feed = ws_protocol_decoder_vauno_en8822c_feed, + .reset = ws_protocol_decoder_vauno_en8822c_reset, + + .get_hash_data = NULL, + .get_hash_data_long = ws_protocol_decoder_vauno_en8822c_get_hash_data, + .serialize = ws_protocol_decoder_vauno_en8822c_serialize, + .deserialize = ws_protocol_decoder_vauno_en8822c_deserialize, + .get_string = ws_protocol_decoder_vauno_en8822c_get_string, + .get_string_brief = NULL, +}; + +const SubGhzProtocolEncoder ws_protocol_vauno_en8822c_encoder = { + .alloc = NULL, + .free = NULL, + + .deserialize = NULL, + .stop = NULL, + .yield = NULL, +}; + +const SubGhzProtocol ws_protocol_vauno_en8822c = { + .name = WS_PROTOCOL_VAUNO_EN8822C_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 | + SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | SubGhzProtocolFlag_Load | + SubGhzProtocolFlag_Save, + .filter = SubGhzProtocolFilter_Weather, + .decoder = &ws_protocol_vauno_en8822c_decoder, + .encoder = &ws_protocol_vauno_en8822c_encoder, +}; + +typedef enum { + VaunoEN8822CDecoderStepReset = 0, + VaunoEN8822CDecoderStepSaveDuration, + VaunoEN8822CDecoderStepCheckDuration, +} VaunoEN8822CDecoderStep; + +void* ws_protocol_decoder_vauno_en8822c_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + WSProtocolDecoderVaunoEN8822C* instance = malloc(sizeof(WSProtocolDecoderVaunoEN8822C)); + instance->base.protocol = &ws_protocol_vauno_en8822c; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void ws_protocol_decoder_vauno_en8822c_free(void* context) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + free(instance); +} + +void ws_protocol_decoder_vauno_en8822c_reset(void* context) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + instance->decoder.parser_step = VaunoEN8822CDecoderStepReset; +} + +static bool ws_protocol_vauno_en8822c_check(WSProtocolDecoderVaunoEN8822C* instance) { + if(!instance->decoder.decode_data) return false; + + // The sum of all nibbles should match the last 6 bits + uint8_t sum = 0; + for(uint8_t i = 6; i <= 38; i += 4) { + sum += ((instance->decoder.decode_data >> i) & 0x0f); + } + + return sum != 0 && (sum & 0x3f) == (instance->decoder.decode_data & 0x3f); +} + +/** + * Analysis of received data + * @param instance Pointer to a WSBlockGeneric* instance + */ +static void ws_protocol_vauno_en8822c_extract_data(WSBlockGeneric* instance) { + instance->id = (instance->data >> 34) & 0xff; + instance->battery_low = (instance->data >> 33) & 0x01; + instance->channel = ((instance->data >> 30) & 0x03); + + int16_t temp = (instance->data >> 18) & 0x0fff; + /* Handle signed data */ + if(temp & 0x0800) { + temp |= 0xf000; + } + instance->temp = (float)temp / 10.0; + + instance->humidity = (instance->data >> 11) & 0x7f; + instance->btn = WS_NO_BTN; +} + +void ws_protocol_decoder_vauno_en8822c_feed(void* context, bool level, uint32_t duration) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + + switch(instance->decoder.parser_step) { + case VaunoEN8822CDecoderStepReset: + if((!level) && DURATION_DIFF(duration, ws_protocol_vauno_en8822c_const.te_long * 4) < + ws_protocol_vauno_en8822c_const.te_delta) { + instance->decoder.parser_step = VaunoEN8822CDecoderStepSaveDuration; + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + } + break; + + case VaunoEN8822CDecoderStepSaveDuration: + if(level) { + instance->decoder.te_last = duration; + instance->decoder.parser_step = VaunoEN8822CDecoderStepCheckDuration; + } else { + instance->decoder.parser_step = VaunoEN8822CDecoderStepReset; + } + break; + + case VaunoEN8822CDecoderStepCheckDuration: + if(!level) { + if(DURATION_DIFF(instance->decoder.te_last, ws_protocol_vauno_en8822c_const.te_short) < + ws_protocol_vauno_en8822c_const.te_delta) { + if(DURATION_DIFF(duration, ws_protocol_vauno_en8822c_const.te_long * 2) < + ws_protocol_vauno_en8822c_const.te_delta) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + instance->decoder.parser_step = VaunoEN8822CDecoderStepSaveDuration; + } else if( + DURATION_DIFF(duration, ws_protocol_vauno_en8822c_const.te_long) < + ws_protocol_vauno_en8822c_const.te_delta) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + instance->decoder.parser_step = VaunoEN8822CDecoderStepSaveDuration; + } else if( + DURATION_DIFF(duration, ws_protocol_vauno_en8822c_const.te_long * 4) < + ws_protocol_vauno_en8822c_const.te_delta) { + instance->decoder.parser_step = VaunoEN8822CDecoderStepReset; + if(instance->decoder.decode_count_bit == + ws_protocol_vauno_en8822c_const.min_count_bit_for_found && + ws_protocol_vauno_en8822c_check(instance)) { + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + ws_protocol_vauno_en8822c_extract_data(&instance->generic); + + if(instance->base.callback) { + instance->base.callback(&instance->base, instance->base.context); + } + } else if(instance->decoder.decode_count_bit == 1) { + instance->decoder.parser_step = VaunoEN8822CDecoderStepSaveDuration; + } + + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + } else + instance->decoder.parser_step = VaunoEN8822CDecoderStepReset; + } else + instance->decoder.parser_step = VaunoEN8822CDecoderStepReset; + } else + instance->decoder.parser_step = VaunoEN8822CDecoderStepReset; + break; + } +} + +uint32_t ws_protocol_decoder_vauno_en8822c_get_hash_data(void* context) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + return subghz_protocol_blocks_get_hash_data_long( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +SubGhzProtocolStatus ws_protocol_decoder_vauno_en8822c_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + return ws_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +SubGhzProtocolStatus + ws_protocol_decoder_vauno_en8822c_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + return ws_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + ws_protocol_vauno_en8822c_const.min_count_bit_for_found); +} + +void ws_protocol_decoder_vauno_en8822c_get_string(void* context, FuriString* output) { + furi_assert(context); + WSProtocolDecoderVaunoEN8822C* instance = context; + ws_block_generic_get_string(&instance->generic, output); +} diff --git a/lib/subghz/protocols/vauno_en8822c.h b/lib/subghz/protocols/vauno_en8822c.h new file mode 100644 index 000000000..5c61d32d6 --- /dev/null +++ b/lib/subghz/protocols/vauno_en8822c.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include +#include +#include +#include "ws_generic.h" +#include + +#define WS_PROTOCOL_VAUNO_EN8822C_NAME "Vauno-EN8822C" + +typedef struct WSProtocolDecoderVaunoEN8822C WSProtocolDecoderVaunoEN8822C; +typedef struct WSProtocolEncoderVaunoEN8822C WSProtocolEncoderVaunoEN8822C; + +extern const SubGhzProtocolDecoder ws_protocol_vauno_en8822c_decoder; +extern const SubGhzProtocolEncoder ws_protocol_vauno_en8822c_encoder; +extern const SubGhzProtocol ws_protocol_vauno_en8822c; + +/** + * Allocate WSProtocolDecoderVaunoEN8822C. + * @param environment Pointer to a SubGhzEnvironment instance + * @return WSProtocolDecoderVaunoEN8822C* pointer to a WSProtocolDecoderVaunoEN8822C instance + */ +void* ws_protocol_decoder_vauno_en8822c_alloc(SubGhzEnvironment* environment); + +/** + * Free WSProtocolDecoderVaunoEN8822C. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + */ +void ws_protocol_decoder_vauno_en8822c_free(void* context); + +/** + * Reset decoder WSProtocolDecoderVaunoEN8822C. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + */ +void ws_protocol_decoder_vauno_en8822c_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void ws_protocol_decoder_vauno_en8822c_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + * @return hash Hash sum + */ +uint32_t ws_protocol_decoder_vauno_en8822c_get_hash_data(void* context); + +/** + * Serialize data WSProtocolDecoderVaunoEN8822C. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return status + */ +SubGhzProtocolStatus ws_protocol_decoder_vauno_en8822c_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data WSProtocolDecoderVaunoEN8822C. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + ws_protocol_decoder_vauno_en8822c_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a WSProtocolDecoderVaunoEN8822C instance + * @param output Resulting text + */ +void ws_protocol_decoder_vauno_en8822c_get_string(void* context, FuriString* output); From 815211fc54466afe103ee7f18688f4081bcca9df Mon Sep 17 00:00:00 2001 From: Aholicknight Date: Sat, 19 Oct 2024 23:27:26 -0600 Subject: [PATCH 31/74] Update ReadMe.md (#263) * Update ReadMe.md Add additional protocol information * Fix typo --------- Co-authored-by: WillyJL <49810075+Willy-JL@users.noreply.github.com> --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 6c05f5bef..5552bdb86 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -34,7 +34,7 @@ We offer a powerful and easy-to-use application tailor-made for our firmware, th - Interface: Tweak every part of your Flipper, from the desktop animations, to the main menu, lockscreen behavior, file browser, etc. -- Protocols: Configure SubGhz settings, add/remove custom frequencies, and setup which GPIO pins are used by different external modules. +- Protocols: Configure SubGhz settings, add/remove custom frequencies, extend SubGhz frequencies to 281-361, 378-481, 749-962 MHz and setup which GPIO pins are used by different external modules. - Misc: Everything else that doesn't fit the previous categories. Change your Flipper's name, XP level, screen options, and configure the RGB backlight. From 3e17d6588ac83966e39b9d63df9704ab433b5a5c Mon Sep 17 00:00:00 2001 From: jay candel Date: Sun, 20 Oct 2024 13:27:48 +0800 Subject: [PATCH 32/74] IR: Update universal bluray remote (#264) * update universal bluray remote * Add files via upload * 2 new of each button * Update changelog --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> --- CHANGELOG.md | 2 +- .../resources/infrared/assets/bluray_dvd.ir | 1155 ++++++++++++----- 2 files changed, 833 insertions(+), 324 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23fca96d9..14bc9ad31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,7 +71,7 @@ - OFW: H World Hotel Chain Room Key Parser and MFC keys (by @zinongli) - OFW: Parser for Tianjin Railway Transit (by @zinongli) - Infrared: - - Bluray/DVD Universal Remote (#250 by @jaylikesbunda) + - Bluray/DVD Universal Remote (#250 #264 by @jaylikesbunda) - Option to "Load from Library File" for Universal Remotes (#255 by @zxkmm) - Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap) - JS: diff --git a/applications/main/infrared/resources/infrared/assets/bluray_dvd.ir b/applications/main/infrared/resources/infrared/assets/bluray_dvd.ir index 18aecdaee..aff93ef1a 100644 --- a/applications/main/infrared/resources/infrared/assets/bluray_dvd.ir +++ b/applications/main/infrared/resources/infrared/assets/bluray_dvd.ir @@ -1,7 +1,7 @@ Filetype: IR library file Version: 1 # -# Last Updated: 2nd October 2024 +# Last Updated: 15th October 2024 # name: Power type: parsed @@ -9,12 +9,6 @@ protocol: Samsung32 address: 2D 00 00 00 command: 30 00 00 00 # -name: Ok -type: parsed -protocol: Samsung32 -address: 2D 00 00 00 -command: 58 00 00 00 -# name: Play type: parsed protocol: Samsung32 @@ -41,12 +35,6 @@ protocol: Samsung32 address: 2C 00 00 00 command: 1E 00 00 00 # -name: Ok -type: parsed -protocol: Samsung32 -address: 2C 00 00 00 -command: AA 00 00 00 -# name: Play type: parsed protocol: Samsung32 @@ -59,6 +47,18 @@ protocol: Samsung32 address: 2C 00 00 00 command: 4F 00 00 00 # +name: Eject +type: parsed +protocol: Samsung32 +address: 2C 00 00 00 +command: 9A 00 00 00 +# +name: Ok +type: parsed +protocol: Samsung32 +address: 2C 00 00 00 +command: AA 00 00 00 +# name: Fast_fo type: parsed protocol: Samsung32 @@ -71,12 +71,6 @@ protocol: Samsung32 address: 2C 00 00 00 command: 53 00 00 00 # -name: Eject -type: parsed -protocol: Samsung32 -address: 2C 00 00 00 -command: 9A 00 00 00 -# name: Subtitle type: parsed protocol: Samsung32 @@ -89,7 +83,7 @@ protocol: Samsung32 address: 2D 00 00 00 command: 36 00 00 00 # -# Model: OPPO BDP93 +# Model: OPPO BDP103 # name: Power type: parsed @@ -97,17 +91,29 @@ protocol: NEC address: 49 00 00 00 command: 1A 00 00 00 # +name: Eject +type: parsed +protocol: NEC +address: 49 00 00 00 +command: 1B 00 00 00 +# name: Ok type: parsed protocol: NEC address: 49 00 00 00 command: 53 00 00 00 # -name: Play +name: Fast_fo type: parsed protocol: NEC address: 49 00 00 00 -command: 56 00 00 00 +command: 51 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 49 00 00 00 +command: 55 00 00 00 # name: Pause type: parsed @@ -115,13 +121,19 @@ protocol: NEC address: 49 00 00 00 command: 14 00 00 00 # +name: Play +type: parsed +protocol: NEC +address: 49 00 00 00 +command: 56 00 00 00 +# name: Subtitle type: parsed protocol: NEC address: 49 00 00 00 command: 4B 00 00 00 # -# Model: Panasonic DMP_BD75 +# Model: Panasonic DMPBDT167 # name: Power type: parsed @@ -129,42 +141,54 @@ protocol: Kaseikyo address: B0 02 20 00 command: D0 03 00 00 # +name: Eject +type: parsed +protocol: Kaseikyo +address: B0 02 20 00 +command: 10 00 00 00 +# name: Ok type: parsed protocol: Kaseikyo address: B0 02 20 02 command: 20 00 00 00 # -name: Play -type: parsed -protocol: Kaseikyo -address: B0 02 20 00 -command: A0 00 00 00 -# -name: Pause -type: parsed -protocol: Kaseikyo -address: B0 02 20 00 -command: 60 00 00 00 -# -name: Fast_ba -type: parsed -protocol: Kaseikyo -address: B0 02 20 00 -command: 40 00 00 00 -# name: Fast_fo type: parsed protocol: Kaseikyo address: B0 02 20 00 command: 50 00 00 00 # +name: Fast_ba +type: parsed +protocol: Kaseikyo +address: B0 02 20 00 +command: 40 00 00 00 +# +name: Pause +type: parsed +protocol: Kaseikyo +address: B0 02 20 00 +command: 60 00 00 00 +# +name: Play +type: parsed +protocol: Kaseikyo +address: B0 02 20 00 +command: A0 00 00 00 +# name: Subtitle type: parsed protocol: Kaseikyo address: B1 02 20 01 command: 10 03 00 00 # +name: Subtitle +type: parsed +protocol: Kaseikyo +address: B0 02 20 02 +command: 30 01 00 00 +# # Model: Philips BDP2501/F7 # name: Power @@ -343,12 +367,6 @@ protocol: Samsung32 address: 07 00 00 00 command: 02 00 00 00 # -name: Ok -type: parsed -protocol: Samsung32 -address: 07 00 00 00 -command: 68 00 00 00 -# name: Play type: parsed protocol: Samsung32 @@ -361,12 +379,6 @@ protocol: Samsung32 address: 07 00 00 00 command: 4A 00 00 00 # -name: Fast_fo -type: parsed -protocol: Samsung32 -address: 07 00 00 00 -command: 48 00 00 00 -# # Model: Samsung BD-D5300 # name: Power @@ -387,12 +399,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 4512 4510 505 498 507 499 505 498 507 499 506 498 507 1503 507 498 506 498 507 498 507 499 506 498 506 499 506 499 505 499 506 499 505 499 506 4505 505 1504 505 1505 505 1504 483 520 507 500 482 1527 504 499 507 500 482 1527 482 1527 482 520 507 500 483 1527 482 521 484 1526 484 1526 505 499 484 521 483 1527 483 1526 483 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 4512 4509 537 470 535 468 537 470 535 468 537 470 535 1475 535 468 537 470 535 469 536 470 535 468 537 469 536 469 536 469 536 469 536 469 536 4477 534 1476 534 1476 534 1475 535 468 537 470 535 1476 534 468 537 470 535 1475 535 468 537 470 535 468 537 1472 538 471 534 1476 534 1475 535 468 537 1473 536 1473 537 1473 536 -# name: Play type: raw frequency: 38000 @@ -405,6 +411,106 @@ frequency: 38000 duty_cycle: 0.330000 data: 4510 4508 537 470 535 467 538 470 535 468 537 471 534 1475 534 467 538 470 535 467 538 469 536 467 538 470 535 467 538 470 535 468 537 469 536 4472 537 1472 537 1472 537 1472 537 470 535 467 538 470 535 1475 534 1475 534 1474 535 467 538 470 535 468 537 1472 537 1473 536 469 536 468 537 469 536 1474 535 1473 536 1473 536 # +# Model: Samsung BDD7500 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4488 4496 566 432 567 432 567 431 568 432 567 432 514 1480 567 432 567 433 566 432 567 433 566 432 567 432 567 431 568 432 567 432 567 433 566 4427 567 1428 566 1427 567 1428 566 432 567 433 566 432 567 433 566 432 567 432 567 433 566 432 567 433 566 1346 648 1428 566 1427 567 1428 566 1428 566 1428 566 1428 566 1428 566 +# +name: Eject +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4488 4498 460 537 462 536 463 536 463 537 462 536 463 1532 462 536 463 536 463 536 463 536 463 537 462 536 463 536 463 537 462 536 463 537 462 4532 462 1532 462 1532 462 1532 462 537 462 1531 463 536 463 536 463 536 463 536 463 536 463 537 462 537 462 536 463 1531 463 1531 463 1532 462 1532 462 1532 462 1531 544 1450 463 +# +name: Ok +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4487 4497 462 538 461 538 461 537 462 538 461 537 462 1532 462 538 461 538 461 538 461 505 494 537 462 537 462 538 461 537 462 537 462 538 461 4533 461 1532 462 1534 460 1532 462 479 520 538 461 537 462 1532 462 1533 461 1532 462 538 461 538 461 538 461 1533 461 1533 461 537 462 538 461 537 462 1532 462 1533 461 1533 461 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4488 4496 463 536 463 536 463 536 463 536 463 537 462 1532 462 536 463 538 461 537 462 537 462 536 463 538 461 537 462 537 462 536 463 537 462 4532 462 1532 462 1532 462 1532 462 536 463 1530 464 537 462 1532 462 536 463 1531 463 537 462 537 462 537 462 536 463 1531 463 504 495 1531 463 537 462 1532 462 1531 463 1532 462 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4489 4496 514 485 514 484 515 485 514 485 514 484 515 1479 515 485 514 484 515 485 514 485 514 485 514 478 521 485 514 484 515 484 515 484 515 4482 512 1480 514 1479 515 1480 514 485 514 486 513 1480 514 485 514 485 514 1479 515 485 514 485 514 485 514 1481 513 485 514 1480 514 1480 514 486 513 1479 515 1481 513 1481 513 +# +name: Pause +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4489 4496 487 513 486 513 486 513 486 513 486 513 486 1507 487 512 487 484 515 513 486 513 486 513 486 513 486 514 485 513 486 513 486 513 486 4510 486 1506 486 1507 487 1509 485 513 486 513 486 1508 486 512 487 513 486 1509 485 1508 486 514 485 512 487 1508 486 513 486 1509 485 1509 485 515 484 513 486 1509 485 1509 485 +# +name: Play +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4486 4498 460 539 460 538 461 538 461 539 460 538 461 1534 460 539 460 538 461 538 461 542 457 538 461 538 461 539 460 539 460 538 461 538 461 4534 483 1510 461 1533 461 1533 461 538 461 537 462 537 462 1533 461 538 461 1533 461 537 462 537 462 538 461 1532 462 1532 462 538 461 1532 462 537 462 1533 461 1533 461 1532 462 +# +name: Subtitle +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4489 4495 514 428 571 485 514 486 513 486 513 486 513 1482 512 487 512 488 511 486 488 510 489 512 487 511 488 511 488 512 487 506 493 512 487 4507 487 1507 487 1507 487 1508 486 510 489 513 486 1506 488 1507 487 513 486 458 541 1507 487 512 487 512 487 1507 487 512 487 512 487 1507 487 1506 488 512 487 1426 568 1506 488 +# +# Model: Samsung BDE5300 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4490 4479 514 482 516 483 515 482 516 482 516 482 516 1456 515 483 515 483 515 484 514 483 515 483 515 513 485 484 514 483 515 484 514 481 517 4451 515 1457 514 1486 485 1457 514 483 515 484 514 482 516 482 516 484 514 482 516 482 516 483 515 482 516 1456 515 1458 513 1455 516 1456 515 1456 515 1456 515 1456 515 1456 515 +# +name: Eject +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4464 4505 461 537 461 536 462 536 462 537 461 537 461 1510 461 537 461 537 461 537 461 536 462 537 461 536 462 537 461 537 461 536 462 537 461 4505 462 1509 462 1510 461 1508 463 537 461 1509 462 537 461 537 461 538 460 537 461 536 462 536 462 537 461 536 462 1510 461 1510 461 1509 462 1509 462 1509 462 1509 462 1510 461 57969 4462 4502 461 537 461 536 462 537 461 537 461 536 462 1510 461 536 462 537 461 536 462 464 534 536 462 537 461 536 462 536 462 535 463 536 462 4505 461 1510 461 1509 462 1510 461 537 461 1509 462 536 462 536 462 536 462 535 463 537 461 536 462 536 462 536 462 1509 462 1509 462 1509 462 1510 461 1509 462 1509 462 1510 461 +# +name: Ok +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4464 4505 461 508 490 536 462 537 461 537 461 537 461 1508 463 537 461 537 461 540 458 536 462 537 461 536 462 536 462 537 461 538 460 536 462 4506 460 1511 460 1509 462 1510 461 537 461 537 461 536 462 1510 461 1508 463 1512 459 537 461 536 462 535 463 1509 462 1510 461 535 463 537 461 537 461 1510 461 1511 460 1510 461 57969 4462 4502 461 537 461 537 461 539 459 536 462 537 461 1510 461 536 462 537 461 537 461 536 462 537 461 537 461 537 461 538 460 536 462 538 460 4505 461 1510 461 1510 461 1511 460 537 461 537 461 521 477 1510 461 1511 460 1510 461 537 461 537 461 537 461 1510 461 1510 461 537 461 538 460 537 461 1509 462 1509 462 1510 461 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4460 4506 460 537 461 537 461 537 461 538 460 538 460 1511 460 538 460 539 459 539 459 538 460 538 460 537 461 540 458 538 460 538 460 538 460 4506 460 1512 459 1511 460 1510 461 538 460 1510 461 537 461 1511 460 537 461 1509 462 537 461 536 462 537 461 538 460 1509 462 537 461 1511 460 536 462 1511 460 1423 548 1510 461 57968 4463 4503 460 537 461 537 461 537 461 537 461 535 463 1509 462 536 462 537 461 537 461 536 462 538 460 537 461 464 534 536 462 536 462 537 461 4504 462 1510 461 1510 461 1510 461 536 462 1510 461 535 463 1509 462 537 461 1509 462 535 463 536 462 535 463 537 461 1510 461 536 462 1510 461 537 461 1509 462 1509 462 1509 462 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4461 4504 462 537 461 537 461 537 461 537 461 537 461 1510 461 537 461 537 461 537 461 537 461 538 460 536 462 537 461 536 462 536 462 537 461 4505 461 1510 461 1510 461 1511 460 536 462 537 461 1511 460 536 462 536 462 1510 461 537 461 537 461 537 461 1509 462 538 460 1510 461 1510 461 536 462 1510 461 1510 461 1509 462 57968 4464 4501 462 536 462 536 462 537 461 536 462 536 462 1510 461 537 461 536 462 536 462 536 462 537 461 535 463 536 462 535 463 537 461 537 461 4503 463 1510 461 1511 460 1509 462 537 461 538 460 1510 461 535 463 537 461 1509 462 536 462 537 461 536 462 1511 460 537 461 1509 462 1510 461 537 461 1510 461 1509 462 1510 461 +# +name: Pause +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4463 4505 461 536 462 537 461 537 461 536 462 536 462 1511 460 536 462 536 462 536 462 537 461 536 462 537 461 537 461 537 461 537 461 537 461 4505 461 1512 462 1508 461 1508 463 537 461 536 462 1510 461 537 461 537 461 1510 461 1509 462 537 461 536 462 1509 462 535 560 1353 521 1510 461 538 460 536 462 1509 462 1511 460 57968 4463 4502 461 536 462 537 461 536 462 538 460 538 460 1509 462 516 482 536 462 536 462 536 462 537 461 537 461 536 462 536 462 537 461 537 461 4507 461 1508 461 1513 458 1508 463 537 461 536 462 1510 461 536 462 536 462 1510 461 1509 462 536 462 537 461 1509 462 537 461 1510 461 1510 461 537 461 538 460 1510 461 1509 462 +# +name: Play +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4462 4508 481 514 484 518 480 516 482 515 483 514 484 1488 461 538 460 539 459 537 461 538 460 538 460 537 461 538 460 539 459 539 459 538 460 4507 459 1511 460 1512 459 1511 460 537 461 537 461 536 462 1511 460 538 460 1509 462 537 461 538 460 538 461 1509 461 1508 463 537 461 1510 461 538 460 1510 461 1510 461 1510 461 57968 4464 4501 462 536 462 536 462 537 461 535 463 475 523 1508 463 536 462 536 462 537 461 537 461 537 461 537 461 536 462 537 461 536 462 538 460 4505 461 1509 462 1509 462 1509 462 537 461 536 462 537 461 1509 462 536 462 1509 462 537 461 536 462 537 461 1509 462 1509 462 536 462 1510 461 536 462 1510 461 1509 462 1510 461 +# +name: Subtitle +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4489 4478 516 481 517 483 515 483 515 482 516 484 514 1456 515 481 517 484 514 482 516 484 514 483 515 483 515 481 517 482 516 367 632 482 516 4451 515 1457 514 1457 514 1455 516 483 515 483 515 1456 515 1412 559 483 515 484 514 1456 515 483 515 486 512 1458 513 483 515 482 516 1456 515 1455 516 484 514 1457 514 1457 514 57918 4486 4476 514 484 514 483 515 484 514 484 514 484 514 1458 513 483 515 483 515 484 514 484 514 483 515 484 514 483 515 483 515 484 514 483 515 4452 514 1457 514 1457 514 1456 515 482 516 484 514 1457 514 1457 514 484 514 486 512 1456 515 483 515 484 514 1456 515 485 513 484 514 1457 514 1457 514 483 515 1457 514 1456 515 +# # Model: Sanyo Bluray_NC088 # name: Power @@ -425,12 +531,6 @@ protocol: NECext address: 87 22 00 00 command: 72 8D 00 00 # -name: Fast_ba -type: parsed -protocol: NECext -address: 87 22 00 00 -command: 52 AD 00 00 -# name: Play type: parsed protocol: NECext @@ -507,12 +607,6 @@ protocol: SIRC20 address: 5A 1C 00 00 command: 15 00 00 00 # -name: Ok -type: parsed -protocol: SIRC20 -address: 5A 1C 00 00 -command: 3D 00 00 00 -# name: Play type: parsed protocol: SIRC20 @@ -525,6 +619,24 @@ protocol: SIRC20 address: 5A 1C 00 00 command: 19 00 00 00 # +name: Subtitle +type: parsed +protocol: SIRC20 +address: 5A 1C 00 00 +command: 63 00 00 00 +# +name: Ok +type: parsed +protocol: SIRC20 +address: 5A 1C 00 00 +command: 3D 00 00 00 +# +name: Eject +type: parsed +protocol: SIRC20 +address: 5A 1C 00 00 +command: 16 00 00 00 +# name: Fast_fo type: parsed protocol: SIRC20 @@ -537,18 +649,6 @@ protocol: SIRC20 address: 5A 1C 00 00 command: 1B 00 00 00 # -name: Eject -type: parsed -protocol: SIRC20 -address: 5A 1C 00 00 -command: 16 00 00 00 -# -name: Subtitle -type: parsed -protocol: SIRC20 -address: 5A 1C 00 00 -command: 63 00 00 00 -# # Model: Toshiba SE-R0398 # name: Power @@ -575,18 +675,6 @@ protocol: NECext address: 45 B5 00 00 command: C2 3D 00 00 # -name: Fast_ba -type: parsed -protocol: NECext -address: 45 B5 00 00 -command: 19 E6 00 00 -# -name: Fast_fo -type: parsed -protocol: NECext -address: 45 B5 00 00 -command: 13 EC 00 00 -# name: Subtitle type: parsed protocol: NECext @@ -643,6 +731,12 @@ protocol: NEC address: 00 00 00 00 command: 5C 00 00 00 # +name: Play +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 33 00 00 00 +# # Model: Bose 3-2-1_Series_1 # name: Power @@ -651,12 +745,6 @@ protocol: NECext address: BA 4B 00 00 command: 4C B3 00 00 # -name: Ok -type: parsed -protocol: NECext -address: BA 4B 00 00 -command: E0 1F 00 00 -# name: Pause type: parsed protocol: NECext @@ -709,12 +797,6 @@ protocol: NEC address: 00 00 00 00 command: 03 00 00 00 # -name: Fast_ba -type: parsed -protocol: Samsung32 -address: 2D 00 00 00 -command: 32 00 00 00 -# # Model: LG DKS-6100Q # name: Power @@ -723,12 +805,6 @@ protocol: Samsung32 address: 10 00 00 00 command: 1E 00 00 00 # -name: Fast_ba -type: parsed -protocol: Samsung32 -address: 10 00 00 00 -command: B1 00 00 00 -# name: Play type: parsed protocol: Samsung32 @@ -741,30 +817,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 3487 3438 903 833 877 859 878 860 877 2596 878 861 876 2599 875 860 877 2598 876 860 877 2596 878 860 877 861 876 2597 877 2597 905 2597 878 858 879 2597 878 859 878 2597 877 860 877 2598 877 858 879 2599 876 2597 904 33497 3487 3463 878 860 877 859 878 832 878 2624 878 832 905 2598 876 860 877 2598 876 860 877 2597 877 859 878 834 903 2597 877 2597 877 2598 904 834 876 2623 879 832 878 2627 875 832 878 2626 876 832 905 2599 875 2598 877 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 3482 3466 878 860 877 860 877 833 877 2625 877 834 903 2598 876 860 877 833 877 2626 876 2597 877 860 877 834 903 2598 877 2597 877 2598 904 832 878 2626 876 833 877 2625 877 2598 876 859 878 832 905 2598 877 2598 876 33546 3483 3440 904 834 876 859 878 861 876 2596 878 861 876 2598 876 859 878 858 879 2597 877 2597 877 860 877 859 878 2597 877 2597 905 2596 879 858 879 2597 877 860 877 2596 878 2597 878 859 878 859 878 2597 877 2597 904 -# -name: Fast_fo -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 3484 3464 907 829 908 830 907 804 906 2596 906 804 933 2569 878 858 879 832 878 2622 880 831 879 858 879 860 877 2596 878 2623 879 2597 877 858 879 2597 877 857 880 2595 879 2594 880 857 880 2595 907 2595 879 2595 880 33544 3485 3437 907 831 906 831 906 830 907 2568 906 830 907 2567 907 831 906 830 907 2567 907 830 907 805 932 802 908 2594 908 2568 907 2567 907 832 905 2568 906 831 906 2568 879 2622 879 831 906 2596 878 2596 879 2596 906 -# -name: Eject -type: parsed -protocol: NEC -address: 49 00 00 00 -command: 1B 00 00 00 -# -name: Eject -type: parsed -protocol: Kaseikyo -address: B0 02 20 00 -command: 10 00 00 00 -# name: Subtitle type: parsed protocol: Kaseikyo @@ -819,44 +871,20 @@ command: 9E 00 00 00 # # Model: Prinz DVD_Player_T182 # -name: Eject -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8345 4517 571 597 543 571 568 572 547 593 546 567 573 568 540 599 540 574 576 1678 570 1684 575 1679 600 1681 567 1686 573 1682 597 1683 576 1679 569 1685 594 1686 573 1681 567 600 550 564 576 564 545 1683 596 1685 574 566 574 567 541 598 542 1686 573 1682 597 1683 576 564 576 565 543 40175 8355 2264 575 -# name: Power type: raw frequency: 38000 duty_cycle: 0.330000 data: 8348 4516 572 570 569 544 595 545 574 566 574 540 599 541 567 572 567 547 603 1677 571 1684 575 1679 600 1681 567 1687 572 1682 597 1684 575 1679 569 1685 594 546 573 1681 598 542 577 563 576 538 601 1679 569 1685 574 566 573 1681 567 573 566 1688 571 1683 597 1685 574 540 599 540 568 40177 8374 2266 573 # -name: Ok -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8368 4519 569 598 541 572 567 573 546 595 544 569 570 569 550 591 548 565 574 1679 569 1686 573 1681 598 1683 576 1678 570 1684 595 1686 573 1682 576 1677 602 1679 569 571 568 572 547 1681 598 568 540 600 550 1678 570 596 543 544 595 1685 573 1681 567 599 551 1677 571 1683 596 571 548 40170 8376 2268 571 -# name: Play type: raw frequency: 38000 duty_cycle: 0.330000 data: 8362 4520 567 599 540 547 603 564 544 595 544 543 596 544 575 592 547 566 573 1680 568 1685 574 1681 598 1709 550 1704 544 1710 569 1712 546 1708 540 1714 576 1705 543 544 595 1711 547 540 599 541 567 573 566 1687 571 569 570 544 595 1711 547 540 599 1681 567 1686 573 1682 597 570 549 40168 8377 2261 567 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8328 4524 563 592 527 569 550 545 553 596 533 565 554 568 530 595 534 566 553 1683 565 1684 564 1684 647 1624 562 1685 563 1685 646 1625 561 1687 561 598 531 567 552 571 527 1687 592 567 531 596 533 567 552 1687 572 1678 570 1680 589 1685 563 572 557 1681 619 1630 566 1684 595 565 533 40169 8337 2267 572 -# # Model: Sony DVD_RMT-D197A # -name: Eject -type: parsed -protocol: SIRC20 -address: 3A 09 00 00 -command: 16 00 00 00 -# name: Power type: parsed protocol: SIRC20 @@ -869,24 +897,6 @@ protocol: SIRC20 address: 3A 09 00 00 command: 63 00 00 00 # -name: Ok -type: parsed -protocol: SIRC20 -address: 3A 09 00 00 -command: 0B 00 00 00 -# -name: Fast_fo -type: parsed -protocol: SIRC20 -address: 3A 09 00 00 -command: 23 00 00 00 -# -name: Fast_ba -type: parsed -protocol: SIRC20 -address: 3A 09 00 00 -command: 22 00 00 00 -# name: Play type: parsed protocol: SIRC20 @@ -913,18 +923,6 @@ protocol: NEC address: 00 00 00 00 command: 1E 00 00 00 # -name: Play -type: parsed -protocol: NEC -address: 00 00 00 00 -command: 1D 00 00 00 -# -name: Ok -type: parsed -protocol: NEC -address: 00 00 00 00 -command: 0E 00 00 00 -# # Model: Toshiba SE_R0108 # name: Power @@ -951,18 +949,6 @@ protocol: NEC address: 45 00 00 00 command: 15 00 00 00 # -name: Fast_ba -type: parsed -protocol: NEC -address: 45 00 00 00 -command: 19 00 00 00 -# -name: Ok -type: parsed -protocol: NEC -address: 45 00 00 00 -command: 21 00 00 00 -# # Model: TOSHIBA SE_R0420 # name: Power @@ -999,18 +985,6 @@ protocol: NEC address: 00 00 00 00 command: 53 00 00 00 # -name: Fast_ba -type: parsed -protocol: NEC -address: 00 00 00 00 -command: 18 00 00 00 -# -name: Fast_fo -type: parsed -protocol: NEC -address: 00 00 00 00 -command: 5F 00 00 00 -# # Model: APEX RM_3800 # name: Power @@ -1025,18 +999,6 @@ protocol: NEC address: 01 00 00 00 command: 19 00 00 00 # -name: Ok -type: parsed -protocol: NEC -address: 01 00 00 00 -command: 13 00 00 00 -# -name: Fast_ba -type: parsed -protocol: NEC -address: 01 00 00 00 -command: 07 00 00 00 -# # Model: RVR-4000 # name: Power @@ -1045,12 +1007,6 @@ protocol: NEC address: 35 00 00 00 command: 1D 00 00 00 # -name: Fast_ba -type: parsed -protocol: NEC -address: 35 00 00 00 -command: 19 00 00 00 -# name: Play type: parsed protocol: NEC @@ -1063,12 +1019,6 @@ protocol: NEC address: 35 00 00 00 command: 1B 00 00 00 # -name: Fast_fo -type: parsed -protocol: NEC -address: 35 00 00 00 -command: 18 00 00 00 -# # Model: JVC HR-A591U # name: Power @@ -1089,24 +1039,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 8414 4228 507 1601 506 1600 507 547 506 548 505 546 507 547 506 1601 506 547 506 547 506 546 507 1600 507 1599 508 547 506 547 506 546 507 546 507 23151 505 1600 507 1601 506 547 506 547 506 547 506 547 506 1600 507 546 507 547 506 547 506 1600 507 1600 507 546 507 548 505 547 506 546 507 23150 506 1600 507 1601 506 545 508 547 506 547 506 545 508 1601 506 548 505 547 506 546 507 1599 508 1600 507 547 506 547 506 546 507 547 506 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8416 4228 507 1601 506 1601 506 549 504 546 507 547 506 547 506 1602 505 548 505 1602 505 1601 506 1599 508 547 506 546 507 547 506 547 506 546 507 22096 507 1600 507 1601 506 548 505 546 507 548 505 548 505 1603 504 547 506 1599 508 1600 507 1601 506 547 506 548 505 547 506 546 507 546 507 22096 506 1600 507 1600 507 546 507 547 506 547 506 546 507 1600 507 546 507 1601 506 1600 507 1601 506 546 507 547 506 546 507 548 505 547 506 -# -name: Fast_fo -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8414 4229 506 1601 506 1601 506 547 506 547 506 546 507 546 507 1600 507 546 507 546 507 1601 506 1602 505 546 507 546 507 547 506 547 506 546 507 23151 505 1600 507 1601 506 546 507 546 507 546 507 547 506 1601 506 547 506 547 506 1601 506 1600 507 546 507 545 508 547 506 547 506 547 506 23149 507 1601 506 1601 506 546 507 546 507 546 507 547 506 1599 508 547 506 547 506 1602 505 1600 507 547 506 547 506 547 506 547 506 546 507 -# -name: Ok -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8414 4230 506 1600 507 1601 506 547 506 545 508 547 506 546 507 1600 507 547 506 546 507 546 507 1600 507 1601 506 1599 508 1599 508 547 506 546 507 21043 506 1599 508 1599 508 547 506 546 507 547 506 546 507 1600 507 547 506 547 506 546 507 1600 507 1600 507 1601 506 1600 507 546 507 547 506 21043 506 1599 508 1601 506 546 507 546 507 546 507 546 507 1601 506 547 506 546 507 547 506 1601 506 1601 506 1601 506 1600 507 546 507 547 506 -# # Model: JVC HR-J700E # name: Power @@ -1127,12 +1059,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 8511 4145 626 1488 624 1486 626 430 626 433 623 430 626 429 627 1487 625 430 626 1485 627 429 627 1484 627 1485 627 429 626 429 627 429 627 429 626 21132 626 1486 626 1485 626 430 626 430 626 429 626 429 627 1488 623 429 627 1487 624 429 627 1485 627 1487 624 431 625 430 625 429 627 429 627 22224 626 1487 625 1484 628 428 627 430 626 430 625 429 627 1487 625 431 624 1486 626 430 625 1487 625 1485 627 430 625 429 626 429 627 429 626 22224 625 1485 627 1486 625 431 625 430 626 429 627 429 626 1487 625 431 624 1485 626 429 627 1487 624 1485 627 430 625 431 625 430 625 429 626 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8509 4148 547 1566 545 1565 547 508 547 509 547 506 550 508 548 1564 548 508 548 1565 547 1565 547 1565 547 507 549 507 549 507 549 508 548 508 548 21187 546 1565 547 1566 546 508 548 507 549 508 547 508 548 1564 548 509 546 1563 549 1562 549 1565 547 508 548 507 549 507 549 508 548 507 549 22302 547 1565 547 1565 547 509 546 507 548 507 549 507 549 1564 548 508 547 1565 547 1564 548 1563 549 509 547 507 548 508 548 506 549 508 548 22303 546 1565 547 1565 546 508 548 509 547 507 549 509 547 1564 547 510 546 1564 548 1566 546 1566 546 507 548 509 547 508 548 509 547 507 549 22302 547 1565 546 1564 548 508 548 508 547 507 549 508 548 1564 548 509 547 1563 549 1564 547 1564 548 508 547 508 548 507 549 507 549 508 548 22302 547 1564 548 1563 549 508 548 507 548 508 548 511 545 1567 545 510 546 1564 548 1565 547 1564 548 507 626 429 549 508 548 509 624 431 547 -# name: Ok type: raw frequency: 38000 @@ -1147,18 +1073,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 8371 4179 504 1565 505 1591 504 542 506 568 506 569 505 544 504 1592 504 570 504 1565 505 1591 504 544 503 1591 505 571 503 543 505 569 505 568 506 22944 504 1565 504 1590 505 544 504 569 505 568 506 542 505 1591 505 569 505 1565 504 1592 504 544 503 1591 505 570 504 543 505 569 505 569 505 22917 505 1590 505 1564 505 569 505 572 502 543 505 570 504 1565 504 570 504 1592 504 1565 504 571 503 1591 505 544 504 568 506 542 506 570 504 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8376 4179 506 1564 506 1593 503 541 507 570 504 568 506 542 506 1591 505 569 505 1566 504 1592 504 1566 504 570 504 570 504 542 506 570 504 568 506 22873 505 1591 505 1566 504 570 504 569 505 543 505 570 504 1565 505 569 505 1590 506 1565 505 1590 506 568 506 543 505 568 506 543 505 569 505 22873 505 1591 505 1564 506 570 504 569 505 543 505 569 505 1565 505 568 506 1592 504 1566 504 1591 505 569 505 542 506 568 506 543 505 569 505 -# -name: Fast_fo -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 8373 4181 503 1567 503 1591 506 543 505 569 505 569 505 542 506 1592 505 569 505 542 506 1591 505 1565 505 569 505 569 505 543 505 569 505 570 504 23897 503 1592 504 1566 504 569 505 569 505 543 505 569 505 1565 505 569 505 569 505 1566 504 1592 505 569 505 542 506 568 506 542 506 568 506 -# name: Play type: raw frequency: 38000 @@ -1217,12 +1131,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 3518 3478 902 859 909 860 908 839 902 2601 908 859 909 2581 901 862 906 2601 908 839 901 2602 907 861 907 839 901 2600 909 2601 908 2580 902 859 909 2601 908 839 901 2601 908 860 907 2581 901 860 908 2603 906 2580 902 34025 3518 3473 907 860 908 840 900 860 908 2602 907 840 900 2601 908 859 909 2582 900 861 907 2602 907 839 902 859 909 2602 907 2581 901 2601 908 860 908 2581 901 858 910 2601 908 840 901 2602 907 860 908 2582 900 2602 907 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 3511 3500 932 815 925 833 935 833 935 2556 926 835 933 2575 934 813 927 835 933 2577 932 2555 927 834 934 835 933 2554 928 2575 934 2576 933 814 926 2577 932 833 935 2556 926 2575 934 835 933 814 926 2575 934 2575 934 33973 3519 3479 899 860 934 834 908 842 898 2602 907 860 908 2583 899 861 907 860 908 2584 898 2602 907 862 906 841 899 2602 933 2577 932 2556 926 836 932 2575 934 814 927 2576 933 2576 933 814 926 834 934 2575 934 2555 927 -# name: Pause type: raw frequency: 38000 @@ -1237,12 +1145,6 @@ protocol: Kaseikyo address: 90 02 20 00 command: A0 00 00 00 # -name: Fast_ba -type: parsed -protocol: Kaseikyo -address: 90 02 20 00 -command: 20 00 00 00 -# name: Power type: parsed protocol: Kaseikyo @@ -1269,12 +1171,6 @@ frequency: 38000 duty_cycle: 0.330000 data: 3538 1702 463 407 461 1273 463 406 462 405 463 405 463 405 463 405 487 382 487 407 460 408 459 410 457 411 457 411 457 1280 457 411 457 411 457 411 457 411 457 411 457 411 457 1280 457 411 457 412 456 1280 457 412 457 412 456 411 457 411 457 412 456 412 456 412 456 412 456 1280 457 412 456 1281 456 1281 456 1281 456 1281 456 412 456 412 456 1281 456 413 455 1281 456 1282 455 413 455 1282 455 413 455 1282 455 74229 3532 1709 457 411 457 1280 457 411 457 412 456 412 456 412 457 411 457 412 456 412 456 412 456 412 456 412 456 412 456 1281 456 412 457 412 456 412 456 412 456 412 456 412 456 1281 456 412 456 412 456 1281 456 413 455 413 455 413 455 413 455 413 455 413 455 413 455 413 455 1282 455 413 455 1282 455 1282 455 1282 455 1282 455 414 454 414 454 1283 454 414 454 1283 454 1283 454 414 454 1283 454 414 454 1283 454 # -name: Ok -type: parsed -protocol: Kaseikyo -address: 91 02 20 01 -command: 80 01 00 00 -# # Model: Panasonic VCR_PV9662 # name: Power @@ -1295,30 +1191,12 @@ frequency: 38000 duty_cycle: 0.330000 data: 3518 1687 446 421 449 1284 445 422 448 419 451 416 454 413 446 421 449 418 452 415 444 422 448 419 451 416 454 413 446 1287 453 414 445 422 448 419 451 416 454 413 446 420 450 1284 445 421 449 418 452 1282 447 420 450 417 453 414 445 421 449 418 452 415 444 423 447 420 450 417 453 1281 449 418 452 1281 449 419 451 416 454 413 446 421 449 418 452 1282 448 419 451 1283 446 1287 453 414 445 422 448 1286 454 74506 3516 1689 444 423 447 1287 453 414 445 422 448 418 452 415 444 423 447 420 450 417 453 414 445 421 449 418 452 415 444 1290 450 416 454 413 446 421 449 418 452 415 444 422 448 1286 454 413 446 420 450 1284 446 422 448 419 451 416 443 423 447 420 450 417 453 414 445 421 449 418 452 1282 448 419 451 1283 447 420 450 417 453 415 444 422 448 419 451 1283 447 420 450 1284 446 1288 452 415 444 423 447 1286 454 # -name: Fast_ba -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 3514 1691 453 414 445 1287 453 415 444 422 448 419 451 417 453 413 446 421 449 418 452 415 444 422 448 419 451 416 454 1279 451 417 453 414 445 421 449 418 452 415 444 423 447 1286 454 413 446 421 449 1284 445 422 448 419 451 416 454 413 446 421 449 417 453 414 445 422 448 419 451 1282 448 419 451 416 454 414 445 421 449 418 452 415 444 422 448 1285 455 412 447 420 450 1283 447 421 449 418 452 1281 449 74543 3516 1689 445 423 447 1286 454 413 446 421 449 418 452 415 444 422 448 419 451 416 443 423 447 420 450 417 453 414 445 1288 452 415 444 422 448 419 451 416 454 413 446 420 450 1284 446 421 449 418 452 1282 448 419 451 416 454 413 446 421 449 418 452 414 445 422 448 419 451 416 443 1289 451 416 454 413 446 421 449 418 452 415 444 422 448 419 451 1282 448 420 450 417 453 1280 450 418 452 415 444 1288 452 74540 3519 1686 448 419 451 1282 448 420 450 417 453 414 445 422 448 419 451 415 444 423 447 420 450 417 453 414 445 421 449 1284 446 422 448 419 451 416 454 413 446 420 450 417 453 1281 449 418 452 415 444 1289 451 416 454 413 446 421 449 418 452 415 444 422 448 419 451 416 443 423 447 1286 454 413 446 421 449 418 452 415 444 422 448 419 451 416 454 1279 451 417 453 414 445 1287 453 415 444 422 448 1286 454 -# -name: Fast_fo -type: raw -frequency: 38000 -duty_cycle: 0.330000 -data: 3513 1687 447 421 449 1311 419 421 449 419 451 416 454 413 446 421 449 418 452 414 445 422 448 419 451 416 454 413 446 1286 454 413 446 421 449 418 452 415 455 412 447 420 450 1282 448 420 450 417 453 1281 449 418 452 416 454 413 446 420 450 417 453 414 445 422 448 419 451 1281 449 1286 454 413 446 421 449 418 452 415 444 422 448 419 451 1282 448 1286 454 413 446 421 449 1284 446 422 448 419 451 1282 448 74517 3509 1691 453 414 445 1316 424 416 454 413 446 420 450 417 453 414 445 422 448 419 451 416 454 413 446 420 450 417 453 1280 450 417 453 415 444 422 448 419 451 416 454 413 446 1286 454 414 445 421 449 1285 445 422 448 420 450 417 453 414 445 421 449 418 452 415 444 423 447 1285 455 1279 451 417 453 414 445 422 448 419 451 416 454 413 446 1286 454 1280 450 418 452 415 455 1278 452 416 454 413 446 1287 453 74511 3515 1685 449 419 451 1282 448 420 450 417 453 414 445 422 448 418 452 415 455 412 447 420 450 417 453 414 445 421 449 1284 446 421 449 419 451 416 454 413 446 420 450 417 453 1280 450 418 452 415 444 1288 452 416 454 413 446 421 449 418 452 415 444 422 448 419 451 416 454 1279 451 1283 447 420 450 418 452 415 444 422 448 419 451 416 454 1279 451 1284 446 421 449 419 451 1309 421 419 451 417 453 1280 450 74543 3515 1690 454 413 446 1314 426 414 445 422 448 419 451 416 454 413 446 420 450 417 453 414 445 421 449 418 452 415 444 1288 452 416 454 413 446 420 450 417 453 414 445 422 448 1285 445 422 448 420 450 1283 447 420 450 418 452 415 444 422 448 419 451 416 454 413 446 420 450 1283 447 1287 453 415 455 412 447 420 450 417 453 414 445 421 449 1284 446 1288 452 416 454 413 446 1314 426 414 445 422 448 1312 428 74533 3513 1687 447 421 449 1312 418 422 448 419 451 416 454 414 445 421 449 418 452 415 444 422 448 419 451 416 454 413 446 1286 454 413 446 421 449 418 452 415 455 412 447 420 450 1283 447 420 450 418 452 1308 422 418 452 416 454 413 446 420 450 417 453 414 445 422 448 419 451 1281 449 1313 427 413 446 421 449 418 452 415 444 422 448 419 451 1282 448 1286 454 414 445 422 448 1312 418 422 448 420 450 1310 420 -# name: Pause type: raw frequency: 38000 duty_cycle: 0.330000 data: 3510 1694 450 416 454 1280 450 417 453 414 445 422 448 418 452 415 455 412 447 420 450 416 454 413 446 420 450 417 453 1282 448 419 451 416 454 412 447 420 450 417 453 414 445 1288 452 415 444 422 448 1286 454 413 446 420 450 417 453 414 445 422 448 418 452 415 455 412 447 419 451 1284 446 1288 452 414 445 422 448 419 451 416 454 413 446 420 450 1284 446 1288 452 415 444 1290 450 416 454 413 446 1288 452 74520 3511 1688 446 422 448 1286 454 412 447 420 450 416 454 413 446 421 449 418 452 415 444 422 448 419 451 415 455 412 447 1287 453 414 445 421 449 418 452 415 444 422 448 419 451 1283 447 420 450 417 453 1281 449 418 452 415 444 422 448 419 451 416 454 413 446 420 450 417 453 414 445 1289 451 1283 447 420 450 417 453 414 445 421 449 418 452 415 455 1279 451 1284 446 421 449 1285 445 422 448 419 451 1283 447 # -name: Fast_fo -type: parsed -protocol: Kaseikyo -address: 90 02 20 00 -command: 30 00 00 00 -# # Model: Philips-RC2K16 # name: Power @@ -1347,24 +1225,12 @@ protocol: SIRC address: 0B 00 00 00 command: 16 00 00 00 # -name: Ok -type: parsed -protocol: SIRC -address: 01 00 00 00 -command: 0B 00 00 00 -# name: Pause type: parsed protocol: SIRC address: 0B 00 00 00 command: 19 00 00 00 # -name: Fast_ba -type: parsed -protocol: SIRC -address: 0B 00 00 00 -command: 1B 00 00 00 -# name: Play type: parsed protocol: SIRC @@ -1386,3 +1252,646 @@ type: parsed protocol: NEC address: 7F 00 00 00 command: 00 00 00 00 +# +name: Ok +type: parsed +protocol: Samsung32 +address: 2D 00 00 00 +command: 58 00 00 00 +# +name: Ok +type: parsed +protocol: Samsung32 +address: 07 00 00 00 +command: 68 00 00 00 +# +name: Fast_fo +type: parsed +protocol: Samsung32 +address: 07 00 00 00 +command: 48 00 00 00 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4512 4509 537 470 535 468 537 470 535 468 537 470 535 1475 535 468 537 470 535 469 536 470 535 468 537 469 536 469 536 469 536 469 536 469 536 4477 534 1476 534 1476 534 1475 535 468 537 470 535 1476 534 468 537 470 535 1475 535 468 537 470 535 468 537 1472 538 471 534 1476 534 1475 535 468 537 1473 536 1473 537 1473 536 +# +name: Fast_ba +type: parsed +protocol: NECext +address: 87 22 00 00 +command: 52 AD 00 00 +# +name: Fast_fo +type: parsed +protocol: NECext +address: 87 22 00 00 +command: 53 AC 00 00 +# +name: Fast_ba +type: parsed +protocol: NECext +address: 45 B5 00 00 +command: 19 E6 00 00 +# +name: Fast_fo +type: parsed +protocol: NECext +address: 45 B5 00 00 +command: 13 EC 00 00 +# +name: Ok +type: parsed +protocol: NECext +address: BA 4B 00 00 +command: E0 1F 00 00 +# +# Model: Brandt DVDP-7R +# +name: Subtitle +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 50 00 00 00 +# +name: Fast_fo +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 02 00 00 00 +# +name: Fast_ba +type: parsed +protocol: Samsung32 +address: 2D 00 00 00 +command: 32 00 00 00 +# +name: Fast_ba +type: parsed +protocol: Samsung32 +address: 10 00 00 00 +command: B1 00 00 00 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3482 3466 878 860 877 860 877 833 877 2625 877 834 903 2598 876 860 877 833 877 2626 876 2597 877 860 877 834 903 2598 877 2597 877 2598 904 832 878 2626 876 833 877 2625 877 2598 876 859 878 832 905 2598 877 2598 876 33546 3483 3440 904 834 876 859 878 861 876 2596 878 861 876 2598 876 859 878 858 879 2597 877 2597 877 860 877 859 878 2597 877 2597 905 2596 879 858 879 2597 877 860 877 2596 878 2597 878 859 878 859 878 2597 877 2597 904 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3484 3464 907 829 908 830 907 804 906 2596 906 804 933 2569 878 858 879 832 878 2622 880 831 879 858 879 860 877 2596 878 2623 879 2597 877 858 879 2597 877 857 880 2595 879 2594 880 857 880 2595 907 2595 879 2595 880 33544 3485 3437 907 831 906 831 906 830 907 2568 906 830 907 2567 907 831 906 830 907 2567 907 830 907 805 932 802 908 2594 908 2568 907 2567 907 832 905 2568 906 831 906 2568 879 2622 879 831 906 2596 878 2596 879 2596 906 +# +# Model: Prinz DVD_Player_T182 +# +name: Eject +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8345 4517 571 597 543 571 568 572 547 593 546 567 573 568 540 599 540 574 576 1678 570 1684 575 1679 600 1681 567 1686 573 1682 597 1683 576 1679 569 1685 594 1686 573 1681 567 600 550 564 576 564 545 1683 596 1685 574 566 574 567 541 598 542 1686 573 1682 597 1683 576 564 576 565 543 40175 8355 2264 575 +# +name: Ok +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8368 4519 569 598 541 572 567 573 546 595 544 569 570 569 550 591 548 565 574 1679 569 1686 573 1681 598 1683 576 1678 570 1684 595 1686 573 1682 576 1677 602 1679 569 571 568 572 547 1681 598 568 540 600 550 1678 570 596 543 544 595 1685 573 1681 567 599 551 1677 571 1683 596 571 548 40170 8376 2268 571 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8328 4524 563 592 527 569 550 545 553 596 533 565 554 568 530 595 534 566 553 1683 565 1684 564 1684 647 1624 562 1685 563 1685 646 1625 561 1687 561 598 531 567 552 571 527 1687 592 567 531 596 533 567 552 1687 572 1678 570 1680 589 1685 563 572 557 1681 619 1630 566 1684 595 565 533 40169 8337 2267 572 +# +# Model: Sony DVD_RMT-D197A +# +name: Eject +type: parsed +protocol: SIRC20 +address: 3A 09 00 00 +command: 16 00 00 00 +# +name: Ok +type: parsed +protocol: SIRC20 +address: 3A 09 00 00 +command: 0B 00 00 00 +# +name: Fast_fo +type: parsed +protocol: SIRC20 +address: 3A 09 00 00 +command: 23 00 00 00 +# +name: Fast_ba +type: parsed +protocol: SIRC20 +address: 3A 09 00 00 +command: 22 00 00 00 +# +name: Play +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 1D 00 00 00 +# +name: Ok +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 0E 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 45 00 00 00 +command: 19 00 00 00 +# +name: Fast_fo +type: parsed +protocol: NEC +address: 45 00 00 00 +command: 13 00 00 00 +# +name: Ok +type: parsed +protocol: NEC +address: 45 00 00 00 +command: 21 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 18 00 00 00 +# +name: Fast_fo +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 5F 00 00 00 +# +name: Ok +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 13 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 07 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 35 00 00 00 +command: 19 00 00 00 +# +name: Fast_fo +type: parsed +protocol: NEC +address: 35 00 00 00 +command: 18 00 00 00 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8416 4228 507 1601 506 1601 506 549 504 546 507 547 506 547 506 1602 505 548 505 1602 505 1601 506 1599 508 547 506 546 507 547 506 547 506 546 507 22096 507 1600 507 1601 506 548 505 546 507 548 505 548 505 1603 504 547 506 1599 508 1600 507 1601 506 547 506 548 505 547 506 546 507 546 507 22096 506 1600 507 1600 507 546 507 547 506 547 506 546 507 1600 507 546 507 1601 506 1600 507 1601 506 546 507 547 506 546 507 548 505 547 506 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8414 4229 506 1601 506 1601 506 547 506 547 506 546 507 546 507 1600 507 546 507 546 507 1601 506 1602 505 546 507 546 507 547 506 547 506 546 507 23151 505 1600 507 1601 506 546 507 546 507 546 507 547 506 1601 506 547 506 547 506 1601 506 1600 507 546 507 545 508 547 506 547 506 547 506 23149 507 1601 506 1601 506 546 507 546 507 546 507 547 506 1599 508 547 506 547 506 1602 505 1600 507 547 506 547 506 547 506 547 506 546 507 +# +name: Ok +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8414 4230 506 1600 507 1601 506 547 506 545 508 547 506 546 507 1600 507 547 506 546 507 546 507 1600 507 1601 506 1599 508 1599 508 547 506 546 507 21043 506 1599 508 1599 508 547 506 546 507 547 506 546 507 1600 507 547 506 547 506 546 507 1600 507 1600 507 1601 506 1600 507 546 507 547 506 21043 506 1599 508 1601 506 546 507 546 507 546 507 546 507 1601 506 547 506 546 507 547 506 1601 506 1601 506 1601 506 1600 507 546 507 547 506 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8509 4148 547 1566 545 1565 547 508 547 509 547 506 550 508 548 1564 548 508 548 1565 547 1565 547 1565 547 507 549 507 549 507 549 508 548 508 548 21187 546 1565 547 1566 546 508 548 507 549 508 547 508 548 1564 548 509 546 1563 549 1562 549 1565 547 508 548 507 549 507 549 508 548 507 549 22302 547 1565 547 1565 547 509 546 507 548 507 549 507 549 1564 548 508 547 1565 547 1564 548 1563 549 509 547 507 548 508 548 506 549 508 548 22303 546 1565 547 1565 546 508 548 509 547 507 549 509 547 1564 547 510 546 1564 548 1566 546 1566 546 507 548 509 547 508 548 509 547 507 549 22302 547 1565 546 1564 548 508 548 508 547 507 549 508 548 1564 548 509 547 1563 549 1564 547 1564 548 508 547 508 548 507 549 507 549 508 548 22302 547 1564 548 1563 549 508 548 507 548 508 548 511 545 1567 545 510 546 1564 548 1565 547 1564 548 507 626 429 549 508 548 509 624 431 547 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8419 4234 549 1564 548 1564 548 508 548 508 548 507 548 508 548 1564 548 507 549 507 548 1564 548 1564 548 508 548 509 547 509 547 508 547 508 548 22258 547 1563 549 1565 547 508 548 508 547 508 548 508 548 1562 550 509 547 507 549 1564 548 1563 549 507 549 507 549 507 548 507 548 508 548 23360 545 1563 549 1564 548 508 548 510 545 509 547 508 548 1564 548 508 547 508 548 1565 547 1563 549 507 549 506 549 506 550 508 548 509 547 23358 547 1564 548 1566 546 508 547 508 548 508 548 506 550 1564 548 507 548 508 548 1565 547 1564 548 510 546 508 548 509 546 508 548 513 542 23358 548 1564 548 1565 547 510 546 508 548 509 547 507 549 1564 548 509 546 507 549 1563 549 1563 549 507 549 506 549 507 549 509 547 509 547 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8376 4179 506 1564 506 1593 503 541 507 570 504 568 506 542 506 1591 505 569 505 1566 504 1592 504 1566 504 570 504 570 504 542 506 570 504 568 506 22873 505 1591 505 1566 504 570 504 569 505 543 505 570 504 1565 505 569 505 1590 506 1565 505 1590 506 568 506 543 505 568 506 543 505 569 505 22873 505 1591 505 1564 506 570 504 569 505 543 505 569 505 1565 505 568 506 1592 504 1566 504 1591 505 569 505 542 506 568 506 543 505 569 505 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8373 4181 503 1567 503 1591 506 543 505 569 505 569 505 542 506 1592 505 569 505 542 506 1591 505 1565 505 569 505 569 505 543 505 569 505 570 504 23897 503 1592 504 1566 504 569 505 569 505 543 505 569 505 1565 505 569 505 569 505 1566 504 1592 505 569 505 542 506 568 506 542 506 568 506 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3511 3500 932 815 925 833 935 833 935 2556 926 835 933 2575 934 813 927 835 933 2577 932 2555 927 834 934 835 933 2554 928 2575 934 2576 933 814 926 2577 932 833 935 2556 926 2575 934 835 933 814 926 2575 934 2575 934 33973 3519 3479 899 860 934 834 908 842 898 2602 907 860 908 2583 899 861 907 860 908 2584 898 2602 907 862 906 841 899 2602 933 2577 932 2556 926 836 932 2575 934 814 927 2576 933 2576 933 814 926 834 934 2575 934 2555 927 +# +name: Fast_ba +type: parsed +protocol: Kaseikyo +address: 90 02 20 00 +command: 20 00 00 00 +# +name: Ok +type: parsed +protocol: Kaseikyo +address: 91 02 20 01 +command: 80 01 00 00 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3514 1691 453 414 445 1287 453 415 444 422 448 419 451 417 453 413 446 421 449 418 452 415 444 422 448 419 451 416 454 1279 451 417 453 414 445 421 449 418 452 415 444 423 447 1286 454 413 446 421 449 1284 445 422 448 419 451 416 454 413 446 421 449 417 453 414 445 422 448 419 451 1282 448 419 451 416 454 414 445 421 449 418 452 415 444 422 448 1285 455 412 447 420 450 1283 447 421 449 418 452 1281 449 74543 3516 1689 445 423 447 1286 454 413 446 421 449 418 452 415 444 422 448 419 451 416 443 423 447 420 450 417 453 414 445 1288 452 415 444 422 448 419 451 416 454 413 446 420 450 1284 446 421 449 418 452 1282 448 419 451 416 454 413 446 421 449 418 452 414 445 422 448 419 451 416 443 1289 451 416 454 413 446 421 449 418 452 415 444 422 448 419 451 1282 448 420 450 417 453 1280 450 418 452 415 444 1288 452 74540 3519 1686 448 419 451 1282 448 420 450 417 453 414 445 422 448 419 451 415 444 423 447 420 450 417 453 414 445 421 449 1284 446 422 448 419 451 416 454 413 446 420 450 417 453 1281 449 418 452 415 444 1289 451 416 454 413 446 421 449 418 452 415 444 422 448 419 451 416 443 423 447 1286 454 413 446 421 449 418 452 415 444 422 448 419 451 416 454 1279 451 417 453 414 445 1287 453 415 444 422 448 1286 454 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3513 1687 447 421 449 1311 419 421 449 419 451 416 454 413 446 421 449 418 452 414 445 422 448 419 451 416 454 413 446 1286 454 413 446 421 449 418 452 415 455 412 447 420 450 1282 448 420 450 417 453 1281 449 418 452 416 454 413 446 420 450 417 453 414 445 422 448 419 451 1281 449 1286 454 413 446 421 449 418 452 415 444 422 448 419 451 1282 448 1286 454 413 446 421 449 1284 446 422 448 419 451 1282 448 74517 3509 1691 453 414 445 1316 424 416 454 413 446 420 450 417 453 414 445 422 448 419 451 416 454 413 446 420 450 417 453 1280 450 417 453 415 444 422 448 419 451 416 454 413 446 1286 454 414 445 421 449 1285 445 422 448 420 450 417 453 414 445 421 449 418 452 415 444 423 447 1285 455 1279 451 417 453 414 445 422 448 419 451 416 454 413 446 1286 454 1280 450 418 452 415 455 1278 452 416 454 413 446 1287 453 74511 3515 1685 449 419 451 1282 448 420 450 417 453 414 445 422 448 418 452 415 455 412 447 420 450 417 453 414 445 421 449 1284 446 421 449 419 451 416 454 413 446 420 450 417 453 1280 450 418 452 415 444 1288 452 416 454 413 446 421 449 418 452 415 444 422 448 419 451 416 454 1279 451 1283 447 420 450 418 452 415 444 422 448 419 451 416 454 1279 451 1284 446 421 449 419 451 1309 421 419 451 417 453 1280 450 74543 3515 1690 454 413 446 1314 426 414 445 422 448 419 451 416 454 413 446 420 450 417 453 414 445 421 449 418 452 415 444 1288 452 416 454 413 446 420 450 417 453 414 445 422 448 1285 445 422 448 420 450 1283 447 420 450 418 452 415 444 422 448 419 451 416 454 413 446 420 450 1283 447 1287 453 415 455 412 447 420 450 417 453 414 445 421 449 1284 446 1288 452 416 454 413 446 1314 426 414 445 422 448 1312 428 74533 3513 1687 447 421 449 1312 418 422 448 419 451 416 454 414 445 421 449 418 452 415 444 422 448 419 451 416 454 413 446 1286 454 413 446 421 449 418 452 415 455 412 447 420 450 1283 447 420 450 418 452 1308 422 418 452 416 454 413 446 420 450 417 453 414 445 422 448 419 451 1281 449 1313 427 413 446 421 449 418 452 415 444 422 448 419 451 1282 448 1286 454 414 445 422 448 1312 418 422 448 420 450 1310 420 +# +name: Fast_fo +type: parsed +protocol: Kaseikyo +address: 90 02 20 00 +command: 30 00 00 00 +# +name: Ok +type: parsed +protocol: SIRC +address: 01 00 00 00 +command: 0B 00 00 00 +# +name: Fast_ba +type: parsed +protocol: SIRC +address: 0B 00 00 00 +command: 1B 00 00 00 +# +name: Fast_fo +type: parsed +protocol: Samsung32 +address: 2D 00 00 00 +command: 33 00 00 00 +# +# Model: NAD T557 +# +name: Power +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: CA 35 00 00 +# +name: Eject +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: D0 2F 00 00 +# +name: Ok +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: DE 21 00 00 +# +name: Fast_fo +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: D6 29 00 00 +# +name: Fast_ba +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: D7 28 00 00 +# +name: Pause +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: D2 2D 00 00 +# +name: Play +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: D3 2C 00 00 +# +name: Subtitle +type: parsed +protocol: NECext +address: 86 0F 00 00 +command: CD 32 00 00 +# +# Model: Samsung UBDK8500 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4486 4500 459 538 483 517 482 516 484 516 483 515 484 1510 484 503 496 515 484 515 460 538 485 515 484 515 484 515 484 515 484 515 484 514 485 4510 484 1509 485 1508 486 1510 484 515 484 515 484 514 485 514 485 515 484 514 485 515 484 515 484 514 485 1509 485 1509 485 1510 484 1510 484 1539 455 1510 484 1510 484 1510 484 +# +name: Eject +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4484 4501 458 540 459 540 459 540 459 540 459 541 458 1536 458 541 458 540 459 540 459 540 459 542 457 540 459 540 459 541 458 541 458 541 458 4536 458 1536 458 1536 458 1535 459 540 459 1536 458 541 458 541 458 540 459 541 458 541 458 541 458 541 458 541 458 1535 459 1536 458 1535 459 1536 458 1536 458 1536 458 1536 458 +# +name: Ok +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4487 4499 460 538 461 538 461 538 461 537 462 538 461 1533 461 539 460 540 459 539 460 538 461 484 515 538 461 539 460 540 459 540 459 539 460 4533 461 1535 459 1535 459 1534 460 540 459 489 510 538 461 1534 460 1534 460 1534 460 540 459 539 460 539 460 1534 460 1535 459 538 461 539 460 539 460 1533 461 1533 461 1534 460 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4512 4473 486 512 487 512 487 512 487 512 487 512 487 1507 487 512 487 512 487 513 486 513 486 513 486 512 487 513 486 512 487 512 487 512 487 4507 487 1507 487 1508 486 1508 486 512 487 1507 487 512 487 1507 487 513 486 1507 487 512 487 512 487 512 514 485 487 1507 487 512 487 1507 487 512 487 1507 487 1507 487 1507 487 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4485 4498 460 538 461 539 460 539 460 539 460 539 460 1534 460 539 460 539 460 539 460 539 460 539 460 539 460 471 528 538 461 539 460 539 460 4535 459 1534 460 1534 460 1534 460 539 460 539 460 1472 522 539 460 539 460 1534 460 539 460 539 460 538 461 1534 460 539 460 1535 459 1535 459 539 460 1534 460 1537 460 1532 459 +# +name: Pause +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4484 4500 459 540 459 540 459 540 459 540 459 540 459 1535 459 540 459 540 459 540 459 527 472 540 459 540 459 539 460 540 459 540 459 540 459 4535 459 1534 460 1534 460 1534 460 540 459 540 459 539 460 1535 459 539 460 1535 459 540 459 540 459 540 459 1534 460 1534 460 540 459 1534 460 539 460 1535 459 1534 460 1535 459 +# +name: Play +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4486 4499 483 516 483 515 484 515 484 514 485 515 484 1510 484 514 485 516 483 516 483 515 484 515 484 516 483 516 483 515 484 516 483 516 483 4450 544 1511 461 1533 461 1534 482 516 461 539 460 539 460 1534 460 539 460 1534 460 539 460 538 461 539 460 1533 461 1534 460 538 461 1534 460 538 461 1534 460 1534 460 1533 461 +# +# Model: Sylvania SDVD1111 +# +name: Power +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 5F 00 00 00 +# +name: Eject +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 4E 00 00 00 +# +name: Ok +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 55 00 00 00 +# +name: Fast_fo +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 40 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 5D 00 00 00 +# +name: Pause +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8983 4501 564 559 565 559 565 558 566 559 565 558 566 442 682 566 558 560 564 1669 565 1669 565 1669 565 1670 564 1669 565 1668 566 1599 635 1670 564 560 564 560 564 560 564 1669 565 1669 565 559 565 1670 564 559 565 1668 566 1669 565 1669 565 559 565 558 566 1670 564 560 564 1669 565 40232 8985 2239 566 +# +name: Play +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 5B 00 00 00 +# +name: Subtitle +type: parsed +protocol: NEC +address: 00 00 00 00 +command: 02 00 00 00 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4541 4492 516 514 491 514 491 513 492 486 519 485 520 1515 486 519 486 518 487 517 488 517 488 516 489 515 490 515 490 514 491 513 492 499 486 4496 512 1497 514 1520 491 1518 493 511 494 484 511 1525 486 518 487 517 488 1521 490 515 490 514 491 513 492 1490 511 520 485 1524 487 1521 490 488 517 1492 519 1516 485 1523 488 57487 4537 4521 487 518 487 490 515 516 489 489 516 515 490 1519 492 512 493 512 493 511 494 510 485 520 485 519 486 519 486 518 487 517 488 503 492 4489 519 1516 485 1524 487 1522 489 515 490 515 490 1518 493 512 493 511 494 1515 486 492 513 518 487 517 488 1494 517 488 517 1491 520 1515 486 519 486 1522 489 1520 491 1491 520 +# +name: Fast_ba +type: parsed +protocol: Samsung32 +address: 07 00 00 00 +command: 45 00 00 00 +# +# Model: Marantz BD8002 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 873 931 873 931 1763 943 873 1833 873 931 873 897 907 3546 873 931 873 1834 872 931 1763 943 873 931 873 931 873 931 873 931 873 1834 1762 75052 873 932 872 932 1762 944 872 1834 872 932 872 932 872 3547 872 932 872 1834 872 932 1762 944 872 932 872 932 872 932 872 932 872 1835 1761 75054 871 932 872 934 1760 944 872 1834 872 932 872 932 872 3547 872 932 872 1836 870 932 1762 945 871 932 872 932 872 932 872 932 872 1834 1762 +# +name: Eject +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 872 931 873 932 1762 944 872 1833 873 931 873 931 873 4448 1763 1846 873 931 1763 1845 1763 943 873 931 873 930 874 931 873 931 873 75041 872 932 872 931 1763 885 932 1834 872 932 872 932 872 4449 1762 1875 844 932 1762 1847 1762 944 872 932 872 932 872 932 872 932 872 75041 872 932 872 932 1762 888 928 1834 872 932 872 931 873 4449 1763 1846 872 931 1763 1846 1763 944 872 932 872 932 872 932 872 932 872 +# +name: Ok +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 1764 943 873 931 897 906 874 1833 898 907 897 906 898 3520 900 1809 1763 1846 897 906 898 906 1763 944 897 907 897 907 897 907 897 907 897 75016 1762 943 898 906 898 906 898 1809 897 907 897 907 897 3522 897 1808 1764 1844 899 907 897 907 1763 943 898 907 897 907 897 907 897 906 898 75015 1763 944 897 907 872 931 898 1809 897 907 897 907 897 3522 897 1809 1763 1846 897 907 897 907 1762 944 897 907 896 908 897 907 897 907 897 +# +name: Fast_fo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 873 931 873 931 1763 943 873 1832 874 931 873 931 873 4448 873 931 1763 1846 1763 943 873 931 873 931 873 931 873 931 873 931 873 931 873 75038 899 907 897 907 1787 919 897 1809 897 907 897 907 897 4424 897 907 1787 1822 1786 919 898 907 897 907 873 931 897 939 865 907 897 907 897 74960 953 907 897 907 1787 919 897 1809 897 907 897 907 897 4423 898 907 1787 1822 1786 919 897 907 897 907 897 907 897 893 911 907 897 907 897 +# +name: Fast_ba +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 873 931 1763 944 872 931 873 1834 872 932 872 931 873 4449 873 931 1763 944 872 1834 1762 944 872 931 873 931 873 933 871 931 873 932 872 75040 873 931 1763 944 872 932 872 1835 871 932 872 932 872 4448 873 932 1762 944 872 1834 1762 944 872 931 873 931 873 931 873 931 873 931 873 75039 898 907 1787 919 897 907 897 1809 897 907 897 907 897 4424 897 906 1788 919 897 1809 1787 919 897 907 897 907 897 907 897 907 897 907 897 +# +name: Pause +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 873 931 873 930 1764 943 873 1833 873 931 873 925 879 4448 873 931 1763 943 873 931 873 931 873 931 873 930 874 931 873 931 873 931 873 931 873 75039 874 931 873 930 1787 920 873 1833 873 931 873 931 873 4448 873 931 1763 943 873 931 873 931 873 931 873 931 873 931 873 930 874 931 873 932 872 75040 896 907 897 835 1860 919 897 1809 897 907 897 908 873 4448 873 931 1786 920 873 931 873 931 873 931 873 931 873 931 873 931 873 931 873 901 903 +# +name: Play +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 874 930 1764 942 874 931 873 1833 873 931 873 931 873 4448 873 930 1764 1846 1762 1846 1763 943 873 931 873 931 873 931 873 931 873 75040 873 931 1763 944 872 931 873 1833 873 931 873 931 873 4449 872 931 1763 1846 1762 1845 1764 943 873 931 873 931 873 931 873 931 873 75040 873 931 1763 944 872 931 873 1834 872 931 873 931 873 4449 872 931 1763 1846 1763 1846 1763 943 873 931 873 931 873 931 873 931 873 +# +# Model: Yamaha BDS667 +# +name: Power +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: 80 00 00 00 +# +name: Eject +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: 81 00 00 00 +# +name: Ok +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: B8 00 00 00 +# +name: Fast_fo +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: 87 00 00 00 +# +name: Fast_ba +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: 86 00 00 00 +# +name: Pause +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: 83 00 00 00 +# +name: Play +type: parsed +protocol: NEC +address: 7C 00 00 00 +command: 82 00 00 00 +# Model: JVC XVBP1 +# +name: Power +type: parsed +protocol: Kaseikyo +address: 2A 03 01 00 +command: 01 01 00 00 +# +name: Eject +type: parsed +protocol: Kaseikyo +address: 2A 03 01 00 +command: 41 01 00 00 +# +name: Ok +type: parsed +protocol: Kaseikyo +address: 2A 03 01 02 +command: 41 02 00 00 +# +name: Fast_fo +type: parsed +protocol: Kaseikyo +address: 2A 03 01 01 +command: 61 00 00 00 +# +name: Fast_ba +type: parsed +protocol: Kaseikyo +address: 2A 03 01 01 +command: 01 00 00 00 +# +name: Pause +type: parsed +protocol: Kaseikyo +address: 2A 03 01 00 +command: D1 01 00 00 +# +name: Play +type: parsed +protocol: Kaseikyo +address: 2A 03 01 00 +command: C1 01 00 00 +# +name: Subtitle +type: parsed +protocol: Kaseikyo +address: 2A 03 01 02 +command: E1 01 00 00 +# +# Model: Soniq B100 +# +name: Power +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 1C E3 00 00 +# +name: Eject +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 08 F7 00 00 +# +name: Ok +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 06 F9 00 00 +# +name: Fast_fo +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 09 F6 00 00 +# +name: Fast_ba +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 05 FA 00 00 +# +name: Pause +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 03 FC 00 00 +# +name: Play +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 1E E1 00 00 +# +name: Subtitle +type: parsed +protocol: NECext +address: 00 DF 00 00 +command: 5A A5 00 00 From 7ebbadfbf86459f7aba816b70a5bb45d85f30775 Mon Sep 17 00:00:00 2001 From: jay candel Date: Sun, 20 Oct 2024 13:28:10 +0800 Subject: [PATCH 33/74] NFC: Improve URI decoding in NDEF parser (#267) * add decode_char * Skip double malloc(), can just tell printf() how long the string is * Decode URI within the same buffer * Only decode if next 2 chars are valid hex * Update changelog * Clearer code for hex decode Co-authored-by: jaylikesbunda --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Co-authored-by: jaylikesbunda --- CHANGELOG.md | 1 + .../main/nfc/plugins/supported_cards/ndef.c | 55 ++++++++++++++----- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14bc9ad31..e2b8571ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ - NFC: - Added 6 new Mifare Classic keys from Bulgaria Hotel (#216 by @z3r0l1nk) - NDEF parser supports NTAG I2C Plus 1k and 2k chips too (by @RocketGod-git) + - NDEF parser decodes URL-encoded URI characters (#267 by @jaylikesbunda) - UL: Add iq aparts hotel key (by @xMasterX) - OFW/UL: Rename 'Detect Reader' to 'Extract MFC Keys' (by @bettse & @xMasterX) - OFW: Plantain parser improvements (by @assasinfil) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index 05b31f32b..45809c847 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -27,11 +27,7 @@ static void print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) { if(prefix) furi_string_cat_printf(str, "%s: ", prefix); if(!force_hex && is_text(buf, len)) { - char* tmp = malloc(len + 1); - memcpy(tmp, buf, len); - tmp[len] = '\0'; - furi_string_cat_printf(str, "%s", tmp); - free(tmp); + furi_string_cat_printf(str, "%.*s", len, buf); } else { for(uint8_t i = 0; i < len; i++) { furi_string_cat_printf(str, "%02X ", buf[i]); @@ -40,6 +36,17 @@ static void furi_string_cat(str, "\n"); } +static inline uint8_t hex_to_int(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + return 0; +} + +static char decode_char(const char* str) { + return (hex_to_int(str[1]) << 4) | hex_to_int(str[2]); +} + static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) { // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 const char* prepends[] = { @@ -88,26 +95,44 @@ static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t pay size_t prepend_len = strlen(prepend); size_t uri_len = prepend_len + (payload_len - 1); - char* const uri_buf = malloc(uri_len); + char* const uri_buf = malloc(uri_len); // const to keep the original pointer to free later memcpy(uri_buf, prepend, prepend_len); memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1); - char* uri = uri_buf; + char* uri = uri_buf; // cursor we can iterate and shift freely + + // Encoded chars take 3 bytes (%AB), decoded chars take 1 byte + // We can decode by iterating and overwriting the same buffer + size_t decoded_len = 0; + for(size_t encoded_idx = 0; encoded_idx < uri_len; encoded_idx++) { + if(uri[encoded_idx] == '%' && encoded_idx + 2 < uri_len) { + char hi = toupper(uri[encoded_idx + 1]); + char lo = toupper(uri[encoded_idx + 2]); + if(((hi >= 'A' && hi <= 'F') || (hi >= '0' && hi <= '9')) && + ((lo >= 'A' && lo <= 'F') || (lo >= '0' && lo <= '9'))) { + uri[decoded_len++] = decode_char(&uri[encoded_idx]); + encoded_idx += 2; + continue; + } + } + uri[decoded_len++] = uri[encoded_idx]; + } const char* type = "URI"; - if(strncmp(uri, "http", strlen("http")) == 0) { + if(strncmp(uri, "http", 4) == 0) { type = "URL"; - } else if(strncmp(uri, "tel:", strlen("tel:")) == 0) { + } else if(strncmp(uri, "tel:", 4) == 0) { type = "Phone"; - uri += strlen("tel:"); - uri_len -= strlen("tel:"); - } else if(strncmp(uri, "mailto:", strlen("mailto:")) == 0) { + uri += 4; + decoded_len -= 4; + } else if(strncmp(uri, "mailto:", 7) == 0) { type = "Mail"; - uri += strlen("mailto:"); - uri_len -= strlen("mailto:"); + uri += 7; + decoded_len -= 7; } furi_string_cat_printf(str, "%s\n", type); - print_data(str, NULL, (uint8_t*)uri, uri_len, false); + print_data(str, NULL, (uint8_t*)uri, decoded_len, false); + free(uri_buf); } From 8c960a990cabed32141fe5c8ee5c6441d1d356b2 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:00:10 +0300 Subject: [PATCH 34/74] merge js upds by jamisonderek & Willy-JL --- applications/system/js_app/application.fam | 22 +- applications/system/js_app/js_thread.c | 11 - .../system/js_app/modules/js_gui/js_gui.c | 21 -- applications/system/js_app/modules/js_i2c.c | 280 ++++++++++++++++++ applications/system/js_app/types/global.d.ts | 15 +- .../system/js_app/types/gui/index.d.ts | 5 - .../system/js_app/types/i2c/index.d.ts | 31 ++ 7 files changed, 333 insertions(+), 52 deletions(-) create mode 100644 applications/system/js_app/modules/js_i2c.c create mode 100644 applications/system/js_app/types/i2c/index.d.ts diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index fd9f0fe09..f8f1be13f 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -30,7 +30,7 @@ App( appid="js_gui", apptype=FlipperAppType.PLUGIN, entry_point="js_gui_ep", - requires=["js_app", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"], ) @@ -38,7 +38,7 @@ App( appid="js_gui__loading", apptype=FlipperAppType.PLUGIN, entry_point="js_view_loading_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/loading.c"], ) @@ -46,7 +46,7 @@ App( appid="js_gui__empty_screen", apptype=FlipperAppType.PLUGIN, entry_point="js_view_empty_screen_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/empty_screen.c"], ) @@ -54,7 +54,7 @@ App( appid="js_gui__submenu", apptype=FlipperAppType.PLUGIN, entry_point="js_view_submenu_ep", - requires=["js_app", "js_gui"], + requires=["js_app"], sources=["modules/js_gui/submenu.c"], ) @@ -62,7 +62,7 @@ App( appid="js_gui__text_input", apptype=FlipperAppType.PLUGIN, entry_point="js_view_text_input_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/text_input.c"], ) @@ -70,7 +70,7 @@ App( appid="js_gui__byte_input", apptype=FlipperAppType.PLUGIN, entry_point="js_view_byte_input_ep", - requires=["js_app", "js_gui", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gui/byte_input.c"], ) @@ -126,7 +126,7 @@ App( appid="js_gpio", apptype=FlipperAppType.PLUGIN, entry_point="js_gpio_ep", - requires=["js_app", "js_event_loop"], + requires=["js_app"], sources=["modules/js_gpio.c"], ) @@ -185,3 +185,11 @@ App( requires=["js_app"], sources=["modules/js_usbdisk/*.c"], ) + +App( + appid="js_i2c", + apptype=FlipperAppType.PLUGIN, + entry_point="js_i2c_ep", + requires=["js_app"], + sources=["modules/js_i2c.c"], +) diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 31e9f721e..7a774d324 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -196,16 +196,6 @@ static void js_require(struct mjs* mjs) { mjs_return(mjs, req_object); } -static void js_global_to_string(struct mjs* mjs) { - int base = 10; - if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); - double num = mjs_get_double(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-2147483648"; - itoa(num, tmp_str, base); - mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); - mjs_return(mjs, ret); -} - static void js_parse_int(struct mjs* mjs) { const char* str; int32_t base = 10; @@ -268,7 +258,6 @@ static int32_t js_thread(void* arg) { } mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); - mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string)); mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int)); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index cd87e5c4b..4bd4ccc31 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -260,26 +260,6 @@ static void js_gui_view_set(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } -/** - * @brief `View.hasProperty` - */ -static void js_gui_view_has_property(struct mjs* mjs) { - const char* name; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name)); - JsGuiViewData* data = JS_GET_CONTEXT(mjs); - const JsViewDescriptor* descriptor = data->descriptor; - - for(size_t i = 0; i < descriptor->prop_cnt; i++) { - JsViewPropDescriptor prop = descriptor->props[i]; - if(strcmp(prop.name, name) != 0) continue; - - mjs_return(mjs, mjs_mk_boolean(mjs, true)); - return; - } - - mjs_return(mjs, mjs_mk_boolean(mjs, false)); -} - /** * @brief `View` destructor */ @@ -304,7 +284,6 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr // generic view API mjs_val_t view_obj = mjs_mk_object(mjs); mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set)); - mjs_set(mjs, view_obj, "hasProperty", ~0, MJS_MK_FN(js_gui_view_has_property)); // object data JsGuiViewData* data = malloc(sizeof(JsGuiViewData)); diff --git a/applications/system/js_app/modules/js_i2c.c b/applications/system/js_app/modules/js_i2c.c new file mode 100644 index 000000000..6f105142c --- /dev/null +++ b/applications/system/js_app/modules/js_i2c.c @@ -0,0 +1,280 @@ +#include "../js_modules.h" +#include + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count_range(struct mjs* mjs, size_t min_count, size_t max_count) { + size_t num_args = mjs_nargs(mjs); + if(num_args < min_count || num_args > max_count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_i2c_is_device_ready(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool ready = furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external, addr, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_return(mjs, mjs_mk_boolean(mjs, ready)); +} + +static void js_i2c_write(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 2, 3)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 1); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 2) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_tx(&furi_hal_i2c_handle_external, addr, tx_buf, tx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + if(tx_buf_was_allocated) free(tx_buf); + mjs_return(mjs, mjs_mk_boolean(mjs, result)); +} + +static void js_i2c_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 2, 3)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t rx_len_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + return; + } + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 2) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_rx(&furi_hal_i2c_handle_external, addr, rx_buf, rx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + free(rx_buf); + mjs_return(mjs, ret); +} + +static void js_i2c_write_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 3, 4)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 1); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + mjs_val_t rx_len_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 3) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 3); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_trx( + &furi_hal_i2c_handle_external, addr, tx_buf, tx_len, rx_buf, rx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + mjs_return(mjs, ret); +} + +static void* js_i2c_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + mjs_val_t i2c_obj = mjs_mk_object(mjs); + mjs_set(mjs, i2c_obj, "isDeviceReady", ~0, MJS_MK_FN(js_i2c_is_device_ready)); + mjs_set(mjs, i2c_obj, "write", ~0, MJS_MK_FN(js_i2c_write)); + mjs_set(mjs, i2c_obj, "read", ~0, MJS_MK_FN(js_i2c_read)); + mjs_set(mjs, i2c_obj, "writeRead", ~0, MJS_MK_FN(js_i2c_write_read)); + *object = i2c_obj; + + return (void*)1; +} + +static const JsModuleDescriptor js_i2c_desc = { + "i2c", + js_i2c_create, + NULL, + NULL, +}; + +static const FlipperAppPluginDescriptor i2c_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_i2c_desc, +}; + +const FlipperAppPluginDescriptor* js_i2c_ep(void) { + return &i2c_plugin_descriptor; +} diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index 052a2bb6a..d132f89f5 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -11,13 +11,6 @@ declare function delay(ms: number): void; */ declare function print(...args: any[]): void; -/** - * @brief Converts a number to a string - * @param value The number to convert to a string - * @param base Integer base (`2`...`16`), default: 10 - */ -declare function toString(value: number, base?: number): string; - /** * @brief Converts a string to a number * @param text The string to convert to a number @@ -214,7 +207,13 @@ declare class Boolean { } declare class Function { } -declare class Number { } +declare class Number { + /** + * @brief Converts this number to a string + * @param base Integer base (`2`...`16`), default: 10 + */ + toString(base?: number): string; +} declare class Object { } diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts index 5efcfdc03..d4cdc6bc9 100644 --- a/applications/system/js_app/types/gui/index.d.ts +++ b/applications/system/js_app/types/gui/index.d.ts @@ -9,11 +9,6 @@ export declare class View { * @param value Value to assign */ set

(property: P, value: Props[P]): void; - /** - * Check if property is available - * @param name Name of the property - */ - hasProperty(name: string): boolean; } export declare class ViewFactory> { diff --git a/applications/system/js_app/types/i2c/index.d.ts b/applications/system/js_app/types/i2c/index.d.ts new file mode 100644 index 000000000..ea0a640cb --- /dev/null +++ b/applications/system/js_app/types/i2c/index.d.ts @@ -0,0 +1,31 @@ +/** + * @brief Check if there is an I2C device ready on the bus + * @param address The device address to check + * @param timeout Timeout in milliseconds + */ +export declare function isDeviceReady(address: number, timeout?: number): boolean; + +/** + * @brief Write data to I2C device and return success status + * @param address The device address to write to + * @param data The data to write to the device + * @param timeout Timeout in milliseconds + */ +export declare function write(address: number, data: number[] | ArrayBuffer, timeout?: number): boolean; + +/** + * @brief Read data from I2C device or return undefined on failure + * @param address The device address to read from + * @param length How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function read(address: number, length: number, timeout?: number): ArrayBuffer | undefined; + +/** + * @brief Write data then read from I2C device or return undefined on failure + * @param address The device address to talk to + * @param writeData The data to write to the device + * @param readLength How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function writeRead(address: number, writeData: number[] | ArrayBuffer, readLength: number, timeout?: number): ArrayBuffer | undefined; From 48acd9f0759422cf713e290acd0e17381cd8fd59 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:01:21 +0300 Subject: [PATCH 35/74] merge examples --- .../apps/Scripts/js_examples/badusb_demo.js | 2 +- .../apps/Scripts/js_examples/blebeacon.js | 2 +- .../examples/apps/Scripts/js_examples/gui.js | 2 +- .../examples/apps/Scripts/js_examples/i2c.js | 48 +++++++++++++++++++ .../apps/Scripts/js_examples/interactive.js | 4 +- .../apps/Scripts/js_examples/stringutils.js | 4 +- .../apps/Scripts/js_examples/uart_echo.js | 2 +- 7 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 applications/system/js_app/examples/apps/Scripts/js_examples/i2c.js diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js index 758cffd13..d1ace3845 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/badusb_demo.js @@ -45,7 +45,7 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) badusb.println("Flipper Model: " + flipper.getModel()); badusb.println("Flipper Name: " + flipper.getName()); - badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%"); + badusb.println("Battery level: " + flipper.getBatteryCharge().toString() + "%"); // Alt+Numpad method works only on Windows!!! badusb.altPrintln("This was printed with Alt+Numpad method!"); diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/blebeacon.js b/applications/system/js_app/examples/apps/Scripts/js_examples/blebeacon.js index 9a30ad151..1187f1c20 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/blebeacon.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/blebeacon.js @@ -45,7 +45,7 @@ function sendRandomModelAdvertisement() { blebeacon.start(); - print("Sent data for model ID " + toString(model)); + print("Sent data for model ID " + model.toString()); currentIndex = (currentIndex + 1) % watchValues.length; diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js index 10e88b649..5faeb6d32 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js @@ -97,7 +97,7 @@ eventLoop.subscribe(views.bytekb.input, function (_sub, data, gui, views) { let data_view = Uint8Array(data); let text = "0x"; for (let i = 0; i < data_view.length; i++) { - text += toString(data_view[i], 16); + text += data_view[i].toString(16); } views.helloDialog.set("text", "You typed:\n" + text); views.helloDialog.set("center", "Cool!"); diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/i2c.js b/applications/system/js_app/examples/apps/Scripts/js_examples/i2c.js new file mode 100644 index 000000000..456b44ccd --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/i2c.js @@ -0,0 +1,48 @@ +// Connect an 24C32N EEPROM to the I2C bus of the board. SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8. +let i2c = require("i2c"); + +function i2c_find_first_device() { + let addr = -1; + for (let try_addr = 0; try_addr !== 0xff; try_addr++) { + if (i2c.isDeviceReady(try_addr, 5)) { + addr = try_addr; + break; + } + } + return addr; +} + +let addr = i2c_find_first_device(); +if (addr === -1) { + print("I2C device not found"); + print("Please connect a 24C32N EEPROM I2C device to the Flipper Zero."); + print("SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8."); +} else { + print("I2C device found at address: " + addr.toString(16)); + delay(1000); + + // first two bytes are the start address (0x0000) + // the remaining bytes are the data to store. + // can also use Uint8Array([0x00, 0x00, ...]) as write parameter + i2c.write(addr, [0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47]); + while (i2c.isDeviceReady(addr, 9) === false) { + print("Waiting for device to be ready..."); + } + + // write the address to read from (we start at address 0x0001) + // read 3 bytes - 0x42, 0x43, 0x44 + let data_buf = i2c.writeRead(addr, [0x00, 0x01], 3, 100); + let data = Uint8Array(data_buf); + print("Read bytes: " + data.length.toString()); + for (let i = 0; i < data.length; i++) { + print("data[" + i.toString() + "] = " + data[i].toString(16)); + } + + // read two more bytes (0x45, 0x46) from current address + data_buf = i2c.read(addr, 2); + data = Uint8Array(data_buf); + print("Read bytes: " + data.length.toString()); + for (let i = 0; i < data.length; i++) { + print("data[" + i.toString() + "] = " + data[i].toString(16)); + } +} diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js index 29f3bb253..34639cdac 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js @@ -42,7 +42,7 @@ eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) { eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) { gui.viewDispatcher.switchTo(views.loading); - let path = ctx.tmpTemplate + toString(ctx.tmpNumber++); + let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString(); let file = storage.openFile(path, "w", "create_always"); file.write(text); file.close(); @@ -58,7 +58,7 @@ eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx } else if (typeof result === "string") { result = "'" + result + "'"; } else if (typeof result === "number") { - result = toString(result); + result = result.toString(); } else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check result = "bigint"; } else if (typeof result === "boolean") { diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js b/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js index 7c57f9bc5..b2facb237 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/stringutils.js @@ -1,6 +1,6 @@ let sampleText = "Hello, World!"; -let lengthOfText = "Length of text: " + toString(sampleText.length); +let lengthOfText = "Length of text: " + sampleText.length.toString(); print(lengthOfText); let start = 7; @@ -9,7 +9,7 @@ let substringResult = sampleText.slice(start, end); print(substringResult); let searchStr = "World"; -let result2 = toString(sampleText.indexOf(searchStr)); +let result2 = sampleText.indexOf(searchStr).toString(); print(result2); let upperCaseText = "Text in upper case: " + sampleText.toUpperCase(); diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/uart_echo.js b/applications/system/js_app/examples/apps/Scripts/js_examples/uart_echo.js index 72b4617a6..d43c0baa3 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/uart_echo.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/uart_echo.js @@ -6,7 +6,7 @@ while (1) { if (rx_data !== undefined) { serial.write(rx_data); let data_view = Uint8Array(rx_data); - print("0x" + toString(data_view[0], 16)); + print("0x" + data_view[0].toString(16)); } } From 75625ffd5e8bce3d495c093a1e34dd32096fdffc Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:02:15 +0300 Subject: [PATCH 36/74] merge mjs --- lib/mjs/mjs_exec.c | 18 ++++++++++++++++++ lib/mjs/mjs_primitive.c | 30 ++++++++++++++++++++++++++++++ lib/mjs/mjs_primitive.h | 5 +++++ 3 files changed, 53 insertions(+) diff --git a/lib/mjs/mjs_exec.c b/lib/mjs/mjs_exec.c index 0213a871e..8fdb2d7e5 100644 --- a/lib/mjs/mjs_exec.c +++ b/lib/mjs/mjs_exec.c @@ -475,6 +475,22 @@ static int getprop_builtin_string( return 0; } +static int getprop_builtin_number( + struct mjs* mjs, + mjs_val_t val, + const char* name, + size_t name_len, + mjs_val_t* res) { + if(strcmp(name, "toString") == 0) { + *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_number_to_string); + return 1; + } + + (void)val; + (void)name_len; + return 0; +} + static int getprop_builtin_array( struct mjs* mjs, mjs_val_t val, @@ -589,6 +605,8 @@ static int getprop_builtin(struct mjs* mjs, mjs_val_t val, mjs_val_t name, mjs_v } else if(s != NULL && n == 5 && strncmp(s, "apply", n) == 0) { *res = mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_apply_); handled = 1; + } else if(mjs_is_number(val)) { + handled = getprop_builtin_number(mjs, val, s, n, res); } else if(mjs_is_array(val)) { handled = getprop_builtin_array(mjs, val, s, n, res); } else if(mjs_is_foreign(val)) { diff --git a/lib/mjs/mjs_primitive.c b/lib/mjs/mjs_primitive.c index b63a268e5..e73ae892d 100644 --- a/lib/mjs/mjs_primitive.c +++ b/lib/mjs/mjs_primitive.c @@ -6,6 +6,8 @@ #include "mjs_core.h" #include "mjs_internal.h" #include "mjs_primitive.h" +#include "mjs_string_public.h" +#include "mjs_util.h" mjs_val_t mjs_mk_null(void) { return MJS_NULL; @@ -158,3 +160,31 @@ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs) { mjs_return(mjs, ret); } + +MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs) { + mjs_val_t ret = MJS_UNDEFINED; + mjs_val_t base_v = MJS_UNDEFINED; + int32_t base = 10; + int32_t num; + + /* get number from `this` */ + if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_NUMBER, NULL)) { + goto clean; + } + num = mjs_get_int32(mjs, mjs->vals.this_obj); + + if(mjs_nargs(mjs) >= 1) { + /* get base from arg 0 */ + if(!mjs_check_arg(mjs, 0, "base", MJS_TYPE_NUMBER, &base_v)) { + goto clean; + } + base = mjs_get_int(mjs, base_v); + } + + char tmp_str[] = "-2147483648"; + itoa(num, tmp_str, base); + ret = mjs_mk_string(mjs, tmp_str, ~0, true); + +clean: + mjs_return(mjs, ret); +} diff --git a/lib/mjs/mjs_primitive.h b/lib/mjs/mjs_primitive.h index 6d6f91785..f1c3c8220 100644 --- a/lib/mjs/mjs_primitive.h +++ b/lib/mjs/mjs_primitive.h @@ -34,6 +34,11 @@ MJS_PRIVATE void* get_ptr(mjs_val_t v); */ MJS_PRIVATE void mjs_op_isnan(struct mjs* mjs); +/* + * Implementation for JS Number.toString() + */ +MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs); + #if defined(__cplusplus) } #endif /* __cplusplus */ From ea2757908b68948b2217af90419f6e3deaa7f19d Mon Sep 17 00:00:00 2001 From: Kris Bahnsen Date: Sun, 20 Oct 2024 08:31:40 -0700 Subject: [PATCH 37/74] lib: digital_signal: digital_sequence: add furi_hal.h wrapped in ifdefs (#3964) Per the comment at the top of the file, defining DIGITAL_SIGNAL_DEBUG_OUTPUT_PIN to be a GpioPin variable name should allow additional debug output on that pin. However, this would not work without modifying the file as well to add the furi_hal.h header. Wrap including that header in the same macro define to automatically include it when used. Fixes: d92b0a82cc4c ("NFC refactoring (#3050)") Signed-off-by: Kris Bahnsen Co-authored-by: hedger --- lib/digital_signal/digital_sequence.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/digital_signal/digital_sequence.c b/lib/digital_signal/digital_sequence.c index f93ce6d95..14cb3aab2 100644 --- a/lib/digital_signal/digital_sequence.c +++ b/lib/digital_signal/digital_sequence.c @@ -15,6 +15,9 @@ * Example: * ./fbt --extra-define=DIGITAL_SIGNAL_DEBUG_OUTPUT_PIN=gpio_ext_pb3 */ +#ifdef DIGITAL_SIGNAL_DEBUG_OUTPUT_PIN +#include +#endif #define TAG "DigitalSequence" From 1822221c69fe1c95a2c3b4d91f2c3b352eb8002f Mon Sep 17 00:00:00 2001 From: Ruslan Nadyrshin <110516632+rnadyrshin@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:40:20 +0400 Subject: [PATCH 38/74] Minor change in the dev docs (#3962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The main page title of the Dev docs has been changed Co-authored-by: あく --- documentation/doxygen/index.dox | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/documentation/doxygen/index.dox b/documentation/doxygen/index.dox index 7bd9024a1..9587afd8b 100644 --- a/documentation/doxygen/index.dox +++ b/documentation/doxygen/index.dox @@ -1,7 +1,8 @@ /** -@mainpage Overview +@mainpage notitle +## Welcome to Flipper Developer Documentation! -Welcome to the Flipper Developer Documentation! +--- This documentation is intended for developers interested in modifying the Flipper Zero firmware, creating Apps and JavaScript programs, or working on external hardware modules for the device. From f8fa71c575b2bf14d26c0ab6f5a4b0e4d6fb99d9 Mon Sep 17 00:00:00 2001 From: Zinong Li <131403964+zinongli@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:58:33 +0800 Subject: [PATCH 39/74] NFC TRT Parser: Additional checks to prevent false positives (#3966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * additional checks to prevent false positives * Update trt.c Co-authored-by: あく --- applications/main/nfc/plugins/supported_cards/trt.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/applications/main/nfc/plugins/supported_cards/trt.c b/applications/main/nfc/plugins/supported_cards/trt.c index 5407ab1f7..793ad7f2d 100644 --- a/applications/main/nfc/plugins/supported_cards/trt.c +++ b/applications/main/nfc/plugins/supported_cards/trt.c @@ -40,7 +40,10 @@ static bool trt_parse(const NfcDevice* device, FuriString* parsed_data) { const uint8_t* full_record_pointer = &data->page[FULL_SALE_TIME_STAMP_PAGE].data[0]; uint32_t latest_sale_record = bit_lib_get_bits_32(partial_record_pointer, 3, 20); uint32_t latest_sale_full_record = bit_lib_get_bits_32(full_record_pointer, 0, 27); - if(latest_sale_record != (latest_sale_full_record & 0xFFFFF)) break; + if(latest_sale_record != (latest_sale_full_record & 0xFFFFF)) + break; // check if the copy matches + if((latest_sale_record == 0) || (latest_sale_full_record == 0)) + break; // prevent false positive // Parse date // yyy yyyymmmm dddddhhh hhnnnnnn From b0e591601ce19bcef8429bb451e9c2f286f753b1 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:37:04 +0300 Subject: [PATCH 40/74] upd ndef parser by jaylikesbunda and Willy-JL --- .../main/nfc/plugins/supported_cards/ndef.c | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index d22a0e48a..fe71915eb 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -27,11 +27,7 @@ static void print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) { if(prefix) furi_string_cat_printf(str, "%s: ", prefix); if(!force_hex && is_text(buf, len)) { - char* tmp = malloc(len + 1); - memcpy(tmp, buf, len); - tmp[len] = '\0'; - furi_string_cat_printf(str, "%s", tmp); - free(tmp); + furi_string_cat_printf(str, "%.*s", len, buf); } else { for(uint8_t i = 0; i < len; i++) { furi_string_cat_printf(str, "%02X ", buf[i]); @@ -40,6 +36,17 @@ static void furi_string_cat(str, "\n"); } +static inline uint8_t hex_to_int(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + return 0; +} + +static char decode_char(const char* str) { + return (hex_to_int(str[1]) << 4) | hex_to_int(str[2]); +} + static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) { // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 const char* prepends[] = { @@ -88,26 +95,44 @@ static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t pay size_t prepend_len = strlen(prepend); size_t uri_len = prepend_len + (payload_len - 1); - char* const uri_buf = malloc(uri_len); + char* const uri_buf = malloc(uri_len); // const to keep the original pointer to free later memcpy(uri_buf, prepend, prepend_len); memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1); - char* uri = uri_buf; + char* uri = uri_buf; // cursor we can iterate and shift freely + + // Encoded chars take 3 bytes (%AB), decoded chars take 1 byte + // We can decode by iterating and overwriting the same buffer + size_t decoded_len = 0; + for(size_t encoded_idx = 0; encoded_idx < uri_len; encoded_idx++) { + if(uri[encoded_idx] == '%' && encoded_idx + 2 < uri_len) { + char hi = toupper(uri[encoded_idx + 1]); + char lo = toupper(uri[encoded_idx + 2]); + if(((hi >= 'A' && hi <= 'F') || (hi >= '0' && hi <= '9')) && + ((lo >= 'A' && lo <= 'F') || (lo >= '0' && lo <= '9'))) { + uri[decoded_len++] = decode_char(&uri[encoded_idx]); + encoded_idx += 2; + continue; + } + } + uri[decoded_len++] = uri[encoded_idx]; + } const char* type = "URI"; - if(strncmp(uri, "http", strlen("http")) == 0) { + if(strncmp(uri, "http", 4) == 0) { type = "URL"; - } else if(strncmp(uri, "tel:", strlen("tel:")) == 0) { + } else if(strncmp(uri, "tel:", 4) == 0) { type = "Phone"; - uri += strlen("tel:"); - uri_len -= strlen("tel:"); - } else if(strncmp(uri, "mailto:", strlen("mailto:")) == 0) { + uri += 4; + decoded_len -= 4; + } else if(strncmp(uri, "mailto:", 7) == 0) { type = "Mail"; - uri += strlen("mailto:"); - uri_len -= strlen("mailto:"); + uri += 7; + decoded_len -= 7; } furi_string_cat_printf(str, "%s\n", type); - print_data(str, NULL, (uint8_t*)uri, uri_len, false); + print_data(str, NULL, (uint8_t*)uri, decoded_len, false); + free(uri_buf); } @@ -477,6 +502,6 @@ static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { }; /* Plugin entry point - must return a pointer to const descriptor */ -const FlipperAppPluginDescriptor* ndef_plugin_ep() { +const FlipperAppPluginDescriptor* ndef_plugin_ep(void) { return &ndef_plugin_descriptor; } From 3374ed1dad1c4f7b30b615ae67f8ec64728bce57 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 22 Oct 2024 03:49:34 +0300 Subject: [PATCH 41/74] upd changelog --- CHANGELOG.md | 12 +++++++++--- ReadMe.md | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d901006b..8d4cd0014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fix duplicated frequency lists and use user config for nearest frequency selector too - Fix buttons logic, fix crash - Protocol improvements: + - Keeloq: Monarch full support, with add manually option (thanks to anonymous contributor (TBA)) - Princeton support for second button encoding type (8bit) - GangQi fix serial check and remove broken check from UI - Hollarm add more button codes (thanks to @mishamyte for captures) @@ -13,6 +14,7 @@ - Add extra settings to disable GPIO pins control used for external modules amplifiers and/or LEDs (in radio settings menu with debug ON) - NFC: - Read Ultralight block by block (by @mishamyte | PR #825 #826) + - Update NDEF parser (by @jaylikesbunda and @Willy-JL) - OFW PR 3822: MIFARE Classic Key Recovery Improvements (by @noproto) - OFW PR 3930: NFC Emulation freeze (by @RebornedBrain) - OFW PR 3885: Add API to enforce ISO15693 mode (by @aaronjamt) @@ -21,19 +23,23 @@ - OFW: Moscow social card parser (by @assasinfil) - OFW: NFC: H World Hotel Chain Room Key Parser - OFW: NFC Parser for Tianjin Railway Transit + - OFW: NFC TRT Parser: Additional checks to prevent false positives - New keys in system dict - Infrared: - Add LEDs universal remote (DB by @amec0e) - Update universal remote assets (by @amec0e | PR #813 #816) -* JS: Backporting custom features - WIP (by @xMasterX and @Willy-JL) +- JS: + - OFW: JS modules -> **Breaking API change** + - Backporting custom features - WIP (by @xMasterX and @Willy-JL) + - Add i2c module (by @jamisonderek) * OFW: FuriHal, drivers: rework gauge initialization routine -> **Downgrade to older releases will break battery UI percent indicator, upgrade to this or newer version to restore** -* OFW: JS modules -> **Breaking API change** * OFW: heap: increased size -> **More free RAM!!** -* OFW: CLI improvements, part I -> **Breaking API change** -> + fix web flipper lab * OFW: New layout for BadUSB (es-LA) * OFW: Require PIN on boot * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* OFW: lib: digital_signal: digital_sequence: add furi_hal.h wrapped in ifdefs +* OFW: Add warning about stealth mode in vibro CLI * OFW: Small fixes in the wifi devboard docs * OFW: BadUSB - Improve ChromeOS and GNOME demo scripts * OFW: Small JS fixes diff --git a/ReadMe.md b/ReadMe.md index fb88dc8d2..ac4c442f8 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -117,7 +117,7 @@ Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX: - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) -- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer +- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks anonymous contributor (TBA)) Protocols support made by Skorp (original implementation) and @xMasterX (current version): - CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) From bbb85a4df944dc2be6a149cbc3be133cb4bf22ea Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:38:21 +0100 Subject: [PATCH 42/74] JS: Third batch of OFW PR changes --- applications/system/js_app/modules/js_math.c | 2 +- applications/system/js_app/types/badusb/index.d.ts | 2 +- applications/system/js_app/types/math/index.d.ts | 3 +++ documentation/js/js_math.md | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index 7d54cf9b9..cf66b6a44 100644 --- a/applications/system/js_app/modules/js_math.c +++ b/applications/system/js_app/modules/js_math.c @@ -308,7 +308,7 @@ void js_math_trunc(struct mjs* mjs) { static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); mjs_val_t math_obj = mjs_mk_object(mjs); - mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal)); + mjs_set(mjs, math_obj, "isEqual", ~0, MJS_MK_FN(js_math_is_equal)); mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs)); mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos)); mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh)); diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/types/badusb/index.d.ts index 4fbda5ef8..57c2662cd 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/types/badusb/index.d.ts @@ -40,7 +40,7 @@ export type KeyCode = MainKey | ModifierKey | number; * * @param settings USB device settings. Omit to select default parameters */ -export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath: string }): void; +export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath?: string }): void; /** * @brief Tells whether the virtual USB HID device has successfully connected diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/types/math/index.d.ts index 25abca4af..4924eea7e 100644 --- a/applications/system/js_app/types/math/index.d.ts +++ b/applications/system/js_app/types/math/index.d.ts @@ -1,3 +1,4 @@ +export function isEqual(a: number, b: number, tolerance: number): boolean; export function abs(n: number): number; export function acos(n: number): number; export function acosh(n: number): number; @@ -12,6 +13,7 @@ export function clz32(n: number): number; export function cos(n: number): number; export function exp(n: number): number; export function floor(n: number): number; +export function log(n: number): number; export function max(n: number, m: number): number; export function min(n: number, m: number): number; export function pow(n: number, m: number): number; @@ -21,4 +23,5 @@ export function sin(n: number): number; export function sqrt(n: number): number; export function trunc(n: number): number; declare const PI: number; +declare const E: number; declare const EPSILON: number; diff --git a/documentation/js/js_math.md b/documentation/js/js_math.md index 12dae8fb3..ca16a9111 100644 --- a/documentation/js/js_math.md +++ b/documentation/js/js_math.md @@ -223,7 +223,7 @@ math.floor(45.05); // 45 math.floor(45.95); // 45 ``` -## is_equal +## isEqual Return true if the difference between numbers `a` and `b` is less than the specified parameter `e`. ### Parameters @@ -236,8 +236,8 @@ True if the difference between numbers `a` and `b` is less than the specified pa ### Example ```js -math.is_equal(1.4, 1.6, 0.2); // false -math.is_equal(3.556, 3.555, 0.01); // true +math.isEqual(1.4, 1.6, 0.2); // false +math.isEqual(3.556, 3.555, 0.01); // true ``` ## max From ccc4c682e876053d6bebeaf81ed23382fcfdec68 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 22 Oct 2024 03:39:06 +0100 Subject: [PATCH 43/74] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee6344f1c..d3c605163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - effort required to update old scripts using gui: extensive - `keyboard`: - removed, now replaced by `gui/text_input` and `gui/byte_input` (see above) + - `math`: + - `is_equal()` renamed to `isEqual()` - `storage`: - fully overhauled, now you `openFile()`s and perform actions on them like `.read()` - now supports many more operations including different open modes, directories and much more From b60a420d32755753b963f1d7a0278049abb9a9ea Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 22 Oct 2024 04:21:01 +0100 Subject: [PATCH 44/74] Update apps - Seader: Fix crash scanning NTAG215 with MFC option (by bettse) - Solitaire: Fix memory leak (by doofy-dev) --- CHANGELOG.md | 2 +- applications/external | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c605163..6ad7da5ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,7 +103,7 @@ - DTMF Dolphin: Add EAS tone support (by @JendrBendr) - NFC Playlist: Error screens for playlist already exists and item already in playlist, general improvements (by @acegoal07), refactor rename/new scene without thread (by @Willy-JL) - CLI-GUI Bridge: Fixes and improvements (by @ranchordo) - - Seader: Enable T=1, show error for timeout, fix wrong LRC logging (by @bettse) + - Seader: Enable T=1, show error for timeout, fix wrong LRC logging, fix crash scanning NTAG215 with MFC option (by @bettse) - BLE Spam: Fix menu index callback (by @Willy-JL) - Solitaire: App rewrite, Added quick solve, New effects and sounds, Removed hacky canvas manipulation (by @doofy-dev) - CLI-GUI Bridge: Add more symbols to keyboard (#222 by @Willy-JL) diff --git a/applications/external b/applications/external index 14cdbeee8..3700ee70a 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 14cdbeee81347b9719da143a42ca8b042d9de59c +Subproject commit 3700ee70a6b4a924bfb9bd3cb66f6985d1de40a7 From 7644e39614d9f21f2a82cacd9ab94e77e77d9acb Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 22 Oct 2024 04:27:05 +0100 Subject: [PATCH 45/74] Update changelog --nobuild --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad7da5ad..9fff09747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ - CLI-GUI Bridge: Add more symbols to keyboard (#222 by @Willy-JL) - UL: Sub-GHz Bruteforcer: Add new protocols for existing dump option (by @xMasterX), use FW functions for top buttons (by @DerSkythe) - UL: NRF24 Apps: Use string library compatible with OFW SDK (by @xMasterX) + - UL: W5500 Ethernet: Various fixes and improvements (by @xMasterX) - OFW: SPI Mem Manager: Fixed UI rendering bug related to line breaks (by @portasynthinca3) - OFW: USB/BT Remote: Mouse clicker option to click as fast as possible (by @sumukhj1219) - CLI: Print plugin name on load fail (by @Willy-JL) From 92aa70ba9d719cf2593b5fdd92a1caa8d0791917 Mon Sep 17 00:00:00 2001 From: noproto Date: Tue, 22 Oct 2024 09:54:46 -0400 Subject: [PATCH 46/74] Larger data type for 4K support --- lib/nfc/protocols/mf_classic/mf_classic_poller.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index bc502534f..8d5865865 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -1780,7 +1780,7 @@ NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { bool mf_classic_nested_is_target_key_found(MfClassicPoller* instance, bool is_dict_attack) { MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; bool is_weak = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; - uint8_t nested_target_key = dict_attack_ctx->nested_target_key; + uint16_t nested_target_key = dict_attack_ctx->nested_target_key; MfClassicKeyType target_key_type; uint8_t target_sector; From 6b9ae090f8122f90a0d82af8d4b653dccf167393 Mon Sep 17 00:00:00 2001 From: Derek Jamison Date: Tue, 22 Oct 2024 16:33:34 -0600 Subject: [PATCH 47/74] JavaScript: SPI implementation (#272) * JavaScript: SPI implementation * Fix build * Add typedefs * Port example script to newJS * Push new manifest to bottom * Update changelog --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> --- CHANGELOG.md | 1 + applications/system/js_app/application.fam | 8 + .../examples/apps/Scripts/Examples/spi.js | 88 ++++++ applications/system/js_app/modules/js_spi.c | 283 ++++++++++++++++++ .../system/js_app/types/spi/index.d.ts | 30 ++ 5 files changed, 410 insertions(+) create mode 100644 applications/system/js_app/examples/apps/Scripts/Examples/spi.js create mode 100644 applications/system/js_app/modules/js_spi.c create mode 100644 applications/system/js_app/types/spi/index.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fff09747..9a268c3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ - Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap) - JS: - New `i2c` module (#259 by @jamisonderek) + - New `spi` module (#272 by @jamisonderek) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) - Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme) diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 8c7cdf9ad..6a00f9c22 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -216,3 +216,11 @@ App( requires=["js_app"], sources=["modules/js_i2c.c"], ) + +App( + appid="js_spi", + apptype=FlipperAppType.PLUGIN, + entry_point="js_spi_ep", + requires=["js_app"], + sources=["modules/js_spi.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/spi.js b/applications/system/js_app/examples/apps/Scripts/Examples/spi.js new file mode 100644 index 000000000..810637b3b --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/Examples/spi.js @@ -0,0 +1,88 @@ +// Connect a w25q32 SPI device to the Flipper Zero. +// D1=pin 2 (MOSI), SLK=pin 5 (SCK), GND=pin 8 (GND), D0=pin 3 (MISO), CS=pin 4 (CS), VCC=pin 9 (3V3) +let spi = require("spi"); + +// Display textbox so user can scroll to see all output. +let eventLoop = require("event_loop"); +let gui = require("gui"); +let text = "SPI demo\n"; +let textBox = require("gui/text_box").makeWith({ + focus: "end", + font: "text", + text: text, +}); + +function addText(add) { + text += add; + textBox.set("text", text); +} + +gui.viewDispatcher.switchTo(textBox); + +// writeRead returns a buffer the same length as the input buffer. +// We send 6 bytes of data, starting with 0x90, which is the command to read the manufacturer ID. +// Can also use Uint8Array([0x90, 0x00, ...]) as write parameter +// Optional timeout parameter in ms. We set to 100ms. +let data_buf = spi.writeRead([0x90, 0x0, 0x0, 0x0, 0x0, 0x0], 100); +let data = Uint8Array(data_buf); +if (data.length === 6) { + if (data[4] === 0xEF) { + addText("Found Winbond device\n"); + if (data[5] === 0x15) { + addText("Device ID: W25Q32\n"); + } else { + addText("Unknown device ID: " + data[5].toString(16) + "\n"); + } + } else if (data[4] === 0x0) { + addText("Be sure Winbond W25Q32 is connected to Flipper Zero SPI pins.\n"); + } else { + addText("Unknown device. Manufacturer ID: " + data[4].toString(16) + "\n"); + } +} + +addText("\nReading JEDEC ID\n"); + +// Acquire the SPI bus. Multiple calls will happen with Chip Select (CS) held low. +spi.acquire(); + +// Send command (0x9F) to read JEDEC ID. +// Can also use Uint8Array([0x9F]) as write parameter +// Note: you can pass an optional timeout parameter in milliseconds. +spi.write([0x9F]); + +// Request 3 bytes of data. +// Note: you can pass an optional timeout parameter in milliseconds. +data_buf = spi.read(3); + +// Release the SPI bus as soon as we are done with the set of SPI commands. +spi.release(); + +data = Uint8Array(data_buf); +addText("JEDEC MF ID: " + data[0].toString(16) + "\n"); +addText("JEDEC Memory Type: " + data[1].toString(16) + "\n"); +addText("JEDEC Capacity ID: " + data[2].toString(16) + "\n"); + +if (data[0] === 0xEF) { + addText("Found Winbond device\n"); +} +let capacity = data[1] << 8 | data[2]; +if (capacity === 0x4016) { + addText("Device: W25Q32\n"); +} else if (capacity === 0x4015) { + addText("Device: W25Q16\n"); +} else if (capacity === 0x4014) { + addText("Device: W25Q80\n"); +} else { + addText("Unknown device\n"); +} + +// Wait for user to close the app +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) { + eventLoop.stop(); +}, eventLoop); + +// This script has no interaction, only textbox, so event loop doesn't need to be running all the time +// We run it at the end to accept input for the back button press to quit +// But before that, user sees a textbox and pressing back has no effect +// This is fine because it allows simpler logic and the code above takes no time at all to run +eventLoop.run(); diff --git a/applications/system/js_app/modules/js_spi.c b/applications/system/js_app/modules/js_spi.c new file mode 100644 index 000000000..c0f4d684d --- /dev/null +++ b/applications/system/js_app/modules/js_spi.c @@ -0,0 +1,283 @@ +#include "../js_modules.h" +#include + +typedef struct { + bool acquired_bus; +} JsSpiInst; + +static JsSpiInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpiInst* spi = mjs_get_ptr(mjs, obj_inst); + furi_assert(spi); + return spi; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count_range(struct mjs* mjs, size_t min_count, size_t max_count) { + size_t num_args = mjs_nargs(mjs); + if(num_args < min_count || num_args > max_count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_spi_acquire(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 0, 0)) return; + JsSpiInst* spi = get_this_ctx(mjs); + if(!spi->acquired_bus) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + spi->acquired_bus = true; + } + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_spi_release(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 0, 0)) return; + JsSpiInst* spi = get_this_ctx(mjs); + if(spi->acquired_bus) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + spi->acquired_bus = false; + } + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool js_spi_is_acquired(struct mjs* mjs) { + JsSpiInst* spi = get_this_ctx(mjs); + return spi->acquired_bus; +} + +static void js_spi_write(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 0); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = furi_hal_spi_bus_tx(&furi_hal_spi_bus_handle_external, tx_buf, tx_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + if(tx_buf_was_allocated) free(tx_buf); + mjs_return(mjs, mjs_mk_boolean(mjs, result)); +} + +static void js_spi_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t rx_len_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + return; + } + + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = furi_hal_spi_bus_rx(&furi_hal_spi_bus_handle_external, rx_buf, rx_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + free(rx_buf); + mjs_return(mjs, ret); +} + +static void js_spi_write_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 0); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t data_len = 0; + if(mjs_is_array(tx_buf_arg)) { + data_len = mjs_array_length(mjs, tx_buf_arg); + if(data_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(data_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < data_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &data_len); + if(data_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint8_t* rx_buf = malloc(data_len); // RX and TX are same length for SPI writeRead. + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = + furi_hal_spi_bus_trx(&furi_hal_spi_bus_handle_external, tx_buf, rx_buf, data_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, data_len); + } + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + mjs_return(mjs, ret); +} + +static void* js_spi_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + JsSpiInst* spi = (JsSpiInst*)malloc(sizeof(JsSpiInst)); + spi->acquired_bus = false; + mjs_val_t spi_obj = mjs_mk_object(mjs); + mjs_set(mjs, spi_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, spi)); + mjs_set(mjs, spi_obj, "acquire", ~0, MJS_MK_FN(js_spi_acquire)); + mjs_set(mjs, spi_obj, "release", ~0, MJS_MK_FN(js_spi_release)); + mjs_set(mjs, spi_obj, "write", ~0, MJS_MK_FN(js_spi_write)); + mjs_set(mjs, spi_obj, "read", ~0, MJS_MK_FN(js_spi_read)); + mjs_set(mjs, spi_obj, "writeRead", ~0, MJS_MK_FN(js_spi_write_read)); + *object = spi_obj; + + furi_hal_spi_bus_handle_init(&furi_hal_spi_bus_handle_external); + return (void*)spi; +} + +static void js_spi_destroy(void* inst) { + JsSpiInst* spi = (JsSpiInst*)inst; + if(spi->acquired_bus) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + free(spi); + furi_hal_spi_bus_handle_deinit(&furi_hal_spi_bus_handle_external); +} + +static const JsModuleDescriptor js_spi_desc = { + "spi", + js_spi_create, + js_spi_destroy, + NULL, +}; + +static const FlipperAppPluginDescriptor spi_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_spi_desc, +}; + +const FlipperAppPluginDescriptor* js_spi_ep(void) { + return &spi_plugin_descriptor; +} diff --git a/applications/system/js_app/types/spi/index.d.ts b/applications/system/js_app/types/spi/index.d.ts new file mode 100644 index 000000000..8d72bc29c --- /dev/null +++ b/applications/system/js_app/types/spi/index.d.ts @@ -0,0 +1,30 @@ +/** + * @brief Acquire SPI bus + */ +export declare function acquire(): void; + +/** + * @brief Release SPI bus + */ +export declare function release(): void; + +/** + * @brief Write data to SPI bus and return success status + * @param data The data to write + * @param timeout Timeout in milliseconds + */ +export declare function write(data: number[] | ArrayBuffer, timeout?: number): boolean; + +/** + * @brief Read data from SPI bus or return undefined on failure + * @param length How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function read(length: number, timeout?: number): ArrayBuffer | undefined; + +/** + * @brief Write and read data on SPI bus or return undefined on failure + * @param data The data to write, its length also indicates how many bytes will be read + * @param timeout Timeout in milliseconds + */ +export declare function writeRead(data: number[] | ArrayBuffer, timeout?: number): ArrayBuffer | undefined; From bf7b91f45f684c58f573cf87483ebafa4a5df3be Mon Sep 17 00:00:00 2001 From: noproto Date: Wed, 23 Oct 2024 05:21:43 -0400 Subject: [PATCH 48/74] Fix typo --- lib/nfc/protocols/mf_classic/mf_classic_poller_i.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c index aac05748f..deccdbcda 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.c @@ -5,7 +5,7 @@ #include -#define TAG "MfCLassicPoller" +#define TAG "MfClassicPoller" MfClassicError mf_classic_process_error(Iso14443_3aError error) { MfClassicError ret = MfClassicErrorNone; From d0214c7b272d277fc5f3bded5e0d65adf1a63be1 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 23 Oct 2024 20:07:03 +0300 Subject: [PATCH 49/74] fix: incorrect usage of mjs_own --- applications/system/js_app/modules/js_gpio.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 70021968f..d2d65da4d 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -269,7 +269,6 @@ static void js_gpio_get(struct mjs* mjs) { manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); manager_data->adc_handle = module->adc_handle; manager_data->adc_channel = pin_record->channel; - mjs_own(mjs, &manager); mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); From 8174ffa5b3e5799c9ee73bc0e8215d87ce74b8f7 Mon Sep 17 00:00:00 2001 From: Luu <112649910+luu176@users.noreply.github.com> Date: Thu, 24 Oct 2024 07:55:47 +0200 Subject: [PATCH 50/74] NFC: NDEF parser Mifare Classic support, protocol-agnostic refactor and more improvements (#265) * Update ndef.c included mifare classic support * Update application.fam * Update ndef.c * Update ndef.c * Update ndef.c * Update ndef.c * Update ndef.c for better parsing Because mifare classic 1k has a sector trailer, the ndef parser might conflict with it if the payload is long enough or if there's too many payloads. This code removes all sector trailer in a new buffer and uses that for parsing without any problems. * Rewrite NDEF as protocol agnostic * Move type name buffer to stack when possible * Parse URI without heap buffer * Reduce binary sizes by using preprocessor defines * Parse MFC without clone buffer and support MAD2 * Keep message count between different TLVs * Simplify macro check * Simplify length conversion * Improve output of unparsed types * Add some more logging * Fix WiFi parsing * Check MAD key before reading * Rename ndef_read() to ndef_get() * Update changelog * Wording --------- Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com> --- CHANGELOG.md | 3 +- applications/main/nfc/application.fam | 13 +- .../main/nfc/plugins/supported_cards/ndef.c | 1004 ++++++++++++----- targets/f7/api_symbols.csv | 2 +- 4 files changed, 712 insertions(+), 310 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a268c3cd..48f70030e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,9 +115,10 @@ - OFW: USB/BT Remote: Mouse clicker option to click as fast as possible (by @sumukhj1219) - CLI: Print plugin name on load fail (by @Willy-JL) - NFC: - - Added 6 new Mifare Classic keys from Bulgaria Hotel (#216 by @z3r0l1nk) + - NDEF parser supports Mifare Classic (#265 by @luu176), rewritten to be protocol-agnostic and more improvements (#265 by @Willy-JL) - NDEF parser supports NTAG I2C Plus 1k and 2k chips too (by @RocketGod-git) - NDEF parser decodes URL-encoded URI characters (#267 by @jaylikesbunda) + - Added 6 new Mifare Classic keys from Bulgaria Hotel (#216 by @z3r0l1nk) - UL: Add iq aparts hotel key (by @xMasterX) - OFW/UL: Rename 'Detect Reader' to 'Extract MFC Keys' (by @bettse & @xMasterX) - OFW: Plantain parser improvements (by @assasinfil) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 0104c078c..f49ba84a2 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -255,8 +255,19 @@ App( ) App( - appid="ndef_parser", + appid="ndef_ul_parser", apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_UL")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + +App( + appid="ndef_mfc_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_MFC")], entry_point="ndef_plugin_ep", targets=["f7"], requires=["nfc"], diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index 45809c847..f2ef765a6 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -3,39 +3,260 @@ // Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty // Documentation and sources indicated where relevant // Made by @Willy-JL +// Mifare Classic support added by @luu176 + +// We use an arbitrary position system here, in order to support more protocols. +// Each protocol parses basic structure of the card, then starts ndef_parse_tlv() +// using an arbitrary position value that it can understand. When accessing data +// to parse NDEF content, ndef_get() will then map this arbitrary value to the +// card using state in Ndef struct, skip blocks or sectors as needed. This way, +// NDEF parsing code does not need to know details of card layout. #include "nfc_supported_card_plugin.h" #include #include +#include #include #define TAG "NDEF" +#define NDEF_PROTO_NONE (0) +#define NDEF_PROTO_UL (1) +#define NDEF_PROTO_MFC (2) + +#if !defined(NDEF_PROTO) || (NDEF_PROTO != NDEF_PROTO_UL && NDEF_PROTO != NDEF_PROTO_MFC) +#error Must specify what protocol to use with NDEF_PROTO define! +#endif + +// ---=== structures ===--- + +// TLV structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data +typedef enum FURI_PACKED { + NdefTlvPadding = 0x00, + NdefTlvLockControl = 0x01, + NdefTlvMemoryControl = 0x02, + NdefTlvNdefMessage = 0x03, + NdefTlvProprietary = 0xFD, + NdefTlvTerminator = 0xFE, +} NdefTlv; + +// Type Name Format values: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf +typedef enum FURI_PACKED { + NdefTnfEmpty = 0x00, + NdefTnfWellKnownType = 0x01, + NdefTnfMediaType = 0x02, + NdefTnfAbsoluteUri = 0x03, + NdefTnfExternalType = 0x04, + NdefTnfUnknown = 0x05, + NdefTnfUnchanged = 0x06, + NdefTnfReserved = 0x07, +} NdefTnf; + +// Flags and TNF structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf +typedef struct FURI_PACKED { + // Reversed due to endianness + NdefTnf type_name_format : 3; + bool id_length_present : 1; + bool short_record : 1; + bool chunk_flag : 1; + bool message_end : 1; + bool message_begin : 1; +} NdefFlagsTnf; +_Static_assert(sizeof(NdefFlagsTnf) == 1); + +// URI payload format: +// https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 +static const char* ndef_uri_prepends[] = { + // clang-format off + [0x00] = NULL, + [0x01] = "http://www.", + [0x02] = "https://www.", + [0x03] = "http://", + [0x04] = "https://", + [0x05] = "tel:", + [0x06] = "mailto:", + [0x07] = "ftp://anonymous:anonymous@", + [0x08] = "ftp://ftp.", + [0x09] = "ftps://", + [0x0A] = "sftp://", + [0x0B] = "smb://", + [0x0C] = "nfs://", + [0x0D] = "ftp://", + [0x0E] = "dav://", + [0x0F] = "news:", + [0x10] = "telnet://", + [0x11] = "imap:", + [0x12] = "rtsp://", + [0x13] = "urn:", + [0x14] = "pop:", + [0x15] = "sip:", + [0x16] = "sips:", + [0x17] = "tftp:", + [0x18] = "btspp://", + [0x19] = "btl2cap://", + [0x1A] = "btgoep://", + [0x1B] = "tcpobex://", + [0x1C] = "irdaobex://", + [0x1D] = "file://", + [0x1E] = "urn:epc:id:", + [0x1F] = "urn:epc:tag:", + [0x20] = "urn:epc:pat:", + [0x21] = "urn:epc:raw:", + [0x22] = "urn:epc:", + [0x23] = "urn:nfc:", + // clang-format on +}; + +// ---=== card memory layout abstraction ===--- + +// Shared context and state, read above +typedef struct { + FuriString* output; +#if NDEF_PROTO == NDEF_PROTO_UL + struct { + const uint8_t* start; + size_t size; + } ul; +#elif NDEF_PROTO == NDEF_PROTO_MFC + struct { + const MfClassicBlock* blocks; + size_t size; + } mfc; +#endif +} Ndef; + +static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { +#if NDEF_PROTO == NDEF_PROTO_UL + + // Memory space is contiguous, simply need to remap to data pointer + if(pos + len > ndef->ul.size) return false; + memcpy(buf, ndef->ul.start + pos, len); + return true; + +#elif NDEF_PROTO == NDEF_PROTO_MFC + + // We need to skip sector trailers and MAD2, NDEF parsing just uses + // a position offset in data space, as if it were contiguous. + + // Start with a simple data space size check + if(pos + len > ndef->mfc.size) return false; + + // First 128 blocks are 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 93 (31*3) data blocks correspond to 128 real blocks. + // Last 128 blocks are 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 120 (8*15) data blocks correspond to 128 real blocks. + div_t small_sector_data_blocks = div(pos, MF_CLASSIC_BLOCK_SIZE); + size_t large_sector_data_blocks = 0; + if(small_sector_data_blocks.quot > 93) { + large_sector_data_blocks = small_sector_data_blocks.quot - 93; + small_sector_data_blocks.quot = 93; + } + + div_t small_sectors = div(small_sector_data_blocks.quot, 3); + size_t real_block = small_sectors.quot * 4 + small_sectors.rem; + if(small_sectors.quot >= 16) { + real_block += 4; // Skip MAD2 + } + if(large_sector_data_blocks) { + div_t large_sectors = div(large_sector_data_blocks, 15); + real_block += large_sectors.quot * 16 + large_sectors.rem; + } + + const uint8_t* cur = &ndef->mfc.blocks[real_block].data[small_sector_data_blocks.rem]; + while(len) { + size_t sector_trailer = mf_classic_get_sector_trailer_num_by_block(real_block); + const uint8_t* end = &ndef->mfc.blocks[sector_trailer].data[0]; + + size_t chunk_len = MIN((size_t)(end - cur), len); + memcpy(buf, cur, chunk_len); + len -= chunk_len; + + if(len) { + real_block = sector_trailer + 1; + if(real_block == 64) { + real_block += 4; // Skip MAD2 + } + cur = &ndef->mfc.blocks[real_block].data[0]; + } + } + + return true; + +#else + + UNUSED(ndef); + UNUSED(pos); + UNUSED(len); + UNUSED(buf); + return false; + +#endif +} + +// ---=== output helpers ===--- + +static inline bool is_printable(char c) { + return (c >= ' ' && c <= '~') || c == '\r' || c == '\n'; +} + static bool is_text(const uint8_t* buf, size_t len) { for(size_t i = 0; i < len; i++) { - const char c = buf[i]; - if((c < ' ' || c > '~') && c != '\r' && c != '\n') { - return false; - } + if(!is_printable(buf[i])) return false; } return true; } -static void - print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) { - if(prefix) furi_string_cat_printf(str, "%s: ", prefix); - if(!force_hex && is_text(buf, len)) { - furi_string_cat_printf(str, "%.*s", len, buf); - } else { - for(uint8_t i = 0; i < len; i++) { - furi_string_cat_printf(str, "%02X ", buf[i]); +static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + // We don't have direct access to memory chunks due to different card layouts + // Making a temporary buffer is wasteful of RAM and we can't afford this + // So while iterating like this is inefficient, it saves RAM and works between multiple card types + if(!force_hex) { + // If we find a non-printable character along the way, reset string to prev state and re-do as hex + size_t string_prev = furi_string_size(ndef->output); + for(size_t i = 0; i < len; i++) { + char c; + if(!ndef_get(ndef, pos + i, 1, &c)) return false; + if(!is_printable(c)) { + furi_string_left(ndef->output, string_prev); + force_hex = true; + break; + } + furi_string_push_back(ndef->output, c); } } - furi_string_cat(str, "\n"); + if(force_hex) { + for(size_t i = 0; i < len; i++) { + uint8_t b; + if(!ndef_get(ndef, pos + i, 1, &b)) return false; + furi_string_cat_printf(ndef->output, "%02X ", b); + } + } + furi_string_cat(ndef->output, "\n"); + return true; } +static void + ndef_print(Ndef* ndef, const char* prefix, const void* buf, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + if(!force_hex && is_text(buf, len)) { + furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); + } else { + for(size_t i = 0; i < len; i++) { + furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); + } + } + furi_string_cat(ndef->output, "\n"); +} + +// ---=== payload parsing ===--- + static inline uint8_t hex_to_int(char c) { if(c >= '0' && c <= '9') return c - '0'; if(c >= 'A' && c <= 'F') return c - 'A' + 10; @@ -43,115 +264,107 @@ static inline uint8_t hex_to_int(char c) { return 0; } -static char decode_char(const char* str) { - return (hex_to_int(str[1]) << 4) | hex_to_int(str[2]); +static char url_decode_char(const char* str) { + return (hex_to_int(str[0]) << 4) | hex_to_int(str[1]); } -static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 - const char* prepends[] = { - [0x00] = "", - [0x01] = "http://www.", - [0x02] = "https://www.", - [0x03] = "http://", - [0x04] = "https://", - [0x05] = "tel:", - [0x06] = "mailto:", - [0x07] = "ftp://anonymous:anonymous@", - [0x08] = "ftp://ftp.", - [0x09] = "ftps://", - [0x0A] = "sftp://", - [0x0B] = "smb://", - [0x0C] = "nfs://", - [0x0D] = "ftp://", - [0x0E] = "dav://", - [0x0F] = "news:", - [0x10] = "telnet://", - [0x11] = "imap:", - [0x12] = "rtsp://", - [0x13] = "urn:", - [0x14] = "pop:", - [0x15] = "sip:", - [0x16] = "sips:", - [0x17] = "tftp:", - [0x18] = "btspp://", - [0x19] = "btl2cap://", - [0x1A] = "btgoep://", - [0x1B] = "tcpobex://", - [0x1C] = "irdaobex://", - [0x1D] = "file://", - [0x1E] = "urn:epc:id:", - [0x1F] = "urn:epc:tag:", - [0x20] = "urn:epc:pat:", - [0x21] = "urn:epc:raw:", - [0x22] = "urn:epc:", - [0x23] = "urn:nfc:", - }; - const char* prepend = ""; - uint8_t prepend_type = payload[0]; - if(prepend_type < COUNT_OF(prepends)) { - prepend = prepends[prepend_type]; - } - size_t prepend_len = strlen(prepend); - - size_t uri_len = prepend_len + (payload_len - 1); - char* const uri_buf = malloc(uri_len); // const to keep the original pointer to free later - memcpy(uri_buf, prepend, prepend_len); - memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1); - char* uri = uri_buf; // cursor we can iterate and shift freely - - // Encoded chars take 3 bytes (%AB), decoded chars take 1 byte - // We can decode by iterating and overwriting the same buffer - size_t decoded_len = 0; - for(size_t encoded_idx = 0; encoded_idx < uri_len; encoded_idx++) { - if(uri[encoded_idx] == '%' && encoded_idx + 2 < uri_len) { - char hi = toupper(uri[encoded_idx + 1]); - char lo = toupper(uri[encoded_idx + 2]); - if(((hi >= 'A' && hi <= 'F') || (hi >= '0' && hi <= '9')) && - ((lo >= 'A' && lo <= 'F') || (lo >= '0' && lo <= '9'))) { - uri[decoded_len++] = decode_char(&uri[encoded_idx]); - encoded_idx += 2; - continue; - } - } - uri[decoded_len++] = uri[encoded_idx]; - } - +static bool ndef_parse_uri(Ndef* ndef, size_t pos, size_t len) { const char* type = "URI"; - if(strncmp(uri, "http", 4) == 0) { - type = "URL"; - } else if(strncmp(uri, "tel:", 4) == 0) { - type = "Phone"; - uri += 4; - decoded_len -= 4; - } else if(strncmp(uri, "mailto:", 7) == 0) { - type = "Mail"; - uri += 7; - decoded_len -= 7; + + // Parse URI prepend type + const char* prepend = NULL; + uint8_t prepend_type; + if(!ndef_get(ndef, pos++, 1, &prepend_type)) return false; + len--; + if(prepend_type < COUNT_OF(ndef_uri_prepends)) { + prepend = ndef_uri_prepends[prepend_type]; + } + if(prepend) { + if(strncmp(prepend, "http", 4) == 0) { + type = "URL"; + } else if(strncmp(prepend, "tel:", 4) == 0) { + type = "Phone"; + prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output + } else if(strncmp(prepend, "mailto:", 7) == 0) { + type = "Mail"; + prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output + } } - furi_string_cat_printf(str, "%s\n", type); - print_data(str, NULL, (uint8_t*)uri, decoded_len, false); + // Parse and optionally skip schema, if no prepend was specified + if(!prepend) { + char schema[7] = {0}; // Longest schema we check is 7 char long without terminator + if(!ndef_get(ndef, pos, MIN(sizeof(schema), len), schema)) return false; + if(strncmp(schema, "http", 4) == 0) { + type = "URL"; + } else if(strncmp(schema, "tel:", 4) == 0) { + type = "Phone"; + pos += 4; + len -= 4; + } else if(strncmp(schema, "mailto:", 7) == 0) { + type = "Mail"; + pos += 7; + len -= 7; + } + } - free(uri_buf); + // Print static data as-is + furi_string_cat_printf(ndef->output, "%s\n", type); + if(prepend) { + furi_string_cat(ndef->output, prepend); + } + + // Print URI one char at a time and perform URL decode + while(len) { + char c; + if(!ndef_get(ndef, pos++, 1, &c)) return false; + len--; + if(c != '%' || len < 2) { // Not encoded, or not enough remaining text for encoded char + furi_string_push_back(ndef->output, c); + continue; + } + char enc[2]; + if(!ndef_get(ndef, pos, 2, enc)) return false; + enc[0] = toupper(enc[0]); + enc[1] = toupper(enc[1]); + // Only consume and print these 2 characters if they're valid URL encoded + // Otherwise they're processed in next iterations and we output the % char + if(((enc[0] >= 'A' && enc[0] <= 'F') || (enc[0] >= '0' && enc[0] <= '9')) && + ((enc[1] >= 'A' && enc[1] <= 'F') || (enc[1] >= '0' && enc[1] <= '9'))) { + pos += 2; + len -= 2; + c = url_decode_char(enc); + } + furi_string_push_back(ndef->output, c); + } + + return true; } -static void parse_ndef_text(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - furi_string_cat(str, "Text\n"); - print_data(str, NULL, payload + 3, payload_len - 3, false); +static bool ndef_parse_text(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "Text\n"); + if(!ndef_dump(ndef, NULL, pos + 3, len - 3, false)) return false; + return true; } -static void parse_ndef_bt(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - furi_string_cat(str, "BT MAC\n"); - print_data(str, NULL, payload + 2, payload_len - 2, true); +static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "BT MAC\n"); + if(len != 8) return false; + if(!ndef_dump(ndef, NULL, pos + 2, len - 2, true)) return false; + return true; } -static void parse_ndef_vcard(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - char* tmp = malloc(payload_len + 1); - memcpy(tmp, payload, payload_len); - tmp[payload_len] = '\0'; - FuriString* fmt = furi_string_alloc_set(tmp); - free(tmp); +static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { + size_t end = pos + len; + + // Same concept as ndef_dump(), inefficient but has least drawbacks + FuriString* fmt = furi_string_alloc(); + furi_string_reserve(fmt, len + 1); + while(pos < end) { + char c; + if(!ndef_get(ndef, pos++, 1, &c)) return false; + furi_string_push_back(fmt, c); + } furi_string_trim(fmt); if(furi_string_start_with(fmt, "BEGIN:VCARD")) { @@ -166,13 +379,15 @@ static void parse_ndef_vcard(FuriString* str, const uint8_t* payload, uint32_t p } } - furi_string_cat(str, "Contact\n"); - print_data(str, NULL, (uint8_t*)furi_string_get_cstr(fmt), furi_string_size(fmt), false); + furi_string_cat(ndef->output, "Contact\n"); + ndef_print(ndef, NULL, furi_string_get_cstr(fmt), furi_string_size(fmt), false); furi_string_free(fmt); + return true; } -static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t payload_len) { -// https://android.googlesource.com/platform/packages/apps/Nfc/+/refs/heads/main/src/com/android/nfc/NfcWifiProtectedSetup.java +// Loosely based on Android WiFi NDEF implementation: +// https://android.googlesource.com/platform/packages/apps/Nfc/+/025560080737b43876c9d81feff3151f497947e8/src/com/android/nfc/NfcWifiProtectedSetup.java +static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { #define CREDENTIAL_FIELD_ID (0x100E) #define SSID_FIELD_ID (0x1045) #define NETWORK_KEY_FIELD_ID (0x1027) @@ -186,44 +401,57 @@ static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t pa #define AUTH_TYPE_WPA_AND_WPA2_PSK (0x0022) #define MAX_NETWORK_KEY_SIZE_BYTES (64) - size_t i = 0; - while(i < payload_len) { - uint16_t field_id = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - uint16_t field_len = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + furi_string_cat(ndef->output, "WiFi\n"); + size_t end = pos + len; + + uint8_t tmp_buf[2]; + while(pos < end) { + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t field_id = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t field_len = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + FURI_LOG_D(TAG, "wifi field: %04X len: %d", field_id, field_len); + + if(pos + field_len > end) { + return false; + } if(field_id == CREDENTIAL_FIELD_ID) { - furi_string_cat(str, "WiFi\n"); - size_t start_position = i; - while(i < start_position + field_len) { - uint16_t cfg_id = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - uint16_t cfg_len = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + size_t field_end = pos + field_len; + while(pos < field_end) { + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t cfg_id = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t cfg_len = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + FURI_LOG_D(TAG, "wifi cfg: %04X len: %d", cfg_id, cfg_len); - if(i + cfg_len > start_position + field_len) { - return; + if(pos + cfg_len > field_end) { + return false; } switch(cfg_id) { case SSID_FIELD_ID: - print_data(str, "SSID", payload + i, cfg_len, false); - i += cfg_len; + if(!ndef_dump(ndef, "SSID", pos, cfg_len, false)) return false; + pos += cfg_len; break; case NETWORK_KEY_FIELD_ID: if(cfg_len > MAX_NETWORK_KEY_SIZE_BYTES) { - return; + return false; } - print_data(str, "PWD", payload + i, cfg_len, false); - i += cfg_len; + if(!ndef_dump(ndef, "PWD", pos, cfg_len, false)) return false; + pos += cfg_len; break; case AUTH_TYPE_FIELD_ID: if(cfg_len != AUTH_TYPE_EXPECTED_SIZE) { - return; + return false; } - short auth_type = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t auth_type = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; const char* auth; switch(auth_type) { case AUTH_TYPE_OPEN: @@ -248,252 +476,414 @@ static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t pa auth = "Unknown"; break; } - print_data(str, "AUTH", (uint8_t*)auth, strlen(auth), false); + ndef_print(ndef, "AUTH", auth, strlen(auth), false); break; default: - i += cfg_len; + pos += cfg_len; break; } } - return; + return true; } - i += field_len; + pos += field_len; } + + furi_string_cat(ndef->output, "No data parsed\n"); + return true; } -static void parse_ndef_payload( - FuriString* str, - uint8_t tnf, +static bool ndef_parse_payload( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, const char* type, - uint8_t type_len, - const uint8_t* payload, - uint32_t payload_len) { - if(!payload_len) { - furi_string_cat(str, "Empty\n"); - return; + uint8_t type_len) { + FURI_LOG_D(TAG, "payload type: %.*s len: %d", type_len, type, len); + if(!len) { + furi_string_cat(ndef->output, "Empty\n"); + return true; } + switch(tnf) { - case 0x01: // NFC Forum well-known type [NFC RTD] + case NdefTnfWellKnownType: if(strncmp("U", type, type_len) == 0) { - parse_ndef_uri(str, payload, payload_len); + return ndef_parse_uri(ndef, pos, len); } else if(strncmp("T", type, type_len) == 0) { - parse_ndef_text(str, payload, payload_len); - } else { - print_data(str, "Well-known type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); + return ndef_parse_text(ndef, pos, len); } - break; - case 0x02: // Media-type [RFC 2046] - if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { - parse_ndef_bt(str, payload, payload_len); - } else if(strncmp("text/vcard", type, type_len) == 0) { - parse_ndef_vcard(str, payload, payload_len); - } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { - parse_ndef_wifi(str, payload, payload_len); - } else { - print_data(str, "Media Type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); - } - break; - case 0x00: // Empty - case 0x03: // Absolute URI [RFC 3986] - case 0x04: // NFC Forum external type [NFC RTD] - case 0x05: // Unknown - case 0x06: // Unchanged - case 0x07: // Reserved - default: // Unknown // Dump data without parsing - print_data(str, "Type name format", &tnf, 1, true); - print_data(str, "Type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); - break; + furi_string_cat(ndef->output, "Unsupported\n"); + ndef_print(ndef, "Well-known Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfMediaType: + if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { + return ndef_parse_bt(ndef, pos, len); + } else if(strncmp("text/vcard", type, type_len) == 0) { + return ndef_parse_vcard(ndef, pos, len); + } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { + return ndef_parse_wifi(ndef, pos, len); + } + // Dump data without parsing + furi_string_cat(ndef->output, "Unsupported\n"); + ndef_print(ndef, "Media Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfEmpty: + case NdefTnfAbsoluteUri: + case NdefTnfExternalType: + case NdefTnfUnknown: + case NdefTnfUnchanged: + case NdefTnfReserved: + default: + // Dump data without parsing + furi_string_cat(ndef->output, "Unsupported\n"); + ndef_print(ndef, "Type name format", &tnf, 1, true); + ndef_print(ndef, "Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; } } -static const uint8_t* parse_ndef_message( - FuriString* str, - size_t message_num, - const uint8_t* cur, - const uint8_t* message_end) { - // NDEF message and record documentation: - // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/protocols/nfc/index.html#ndef-message-and-record-format +// ---=== tlv and message parsing ===--- + +// NDEF message structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format +static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num) { + size_t end = pos + len; + size_t record_num = 0; bool last_record = false; - while(cur < message_end) { + while(pos < end) { // Flags and TNF - uint8_t flags_tnf = *cur++; + NdefFlagsTnf flags_tnf; + if(!ndef_get(ndef, pos++, 1, &flags_tnf)) return false; + FURI_LOG_D(TAG, "flags_tnf: %02X", *(uint8_t*)&flags_tnf); + FURI_LOG_D(TAG, "flags_tnf.message_begin: %d", flags_tnf.message_begin); + FURI_LOG_D(TAG, "flags_tnf.message_end: %d", flags_tnf.message_end); + FURI_LOG_D(TAG, "flags_tnf.chunk_flag: %d", flags_tnf.chunk_flag); + FURI_LOG_D(TAG, "flags_tnf.short_record: %d", flags_tnf.short_record); + FURI_LOG_D(TAG, "flags_tnf.id_length_present: %d", flags_tnf.id_length_present); + FURI_LOG_D(TAG, "flags_tnf.type_name_format: %02X", flags_tnf.type_name_format); // Message Begin should only be set on first record - if(record_num++ && flags_tnf & (1 << 7)) break; + if(record_num++ && flags_tnf.message_begin) return false; // Message End should only be set on last record - if(last_record) break; - if(flags_tnf & (1 << 6)) last_record = true; - // Chunked Flag not supported - if(flags_tnf & (1 << 5)) break; - // Payload Length field of 1 vs 4 bytes - bool short_record = flags_tnf & (1 << 4); - // Is payload ID length and value present - bool id_present = flags_tnf & (1 << 3); - // Type Name Format 3 bit value - uint8_t tnf = flags_tnf & 0b00000111; + if(last_record) return false; + if(flags_tnf.message_end) last_record = true; + // Chunk Flag not supported + if(flags_tnf.chunk_flag) return false; // Type Length - uint8_t type_len = *cur++; + uint8_t type_len; + if(!ndef_get(ndef, pos++, 1, &type_len)) return false; - // Payload Length + // Payload Length field of 1 or 4 bytes uint32_t payload_len; - if(short_record) { - payload_len = *cur++; + if(flags_tnf.short_record) { + uint8_t payload_len_short; + if(!ndef_get(ndef, pos++, 1, &payload_len_short)) return false; + payload_len = payload_len_short; } else { - payload_len = bit_lib_bytes_to_num_be(cur, 4); - cur += 4; + if(!ndef_get(ndef, pos, sizeof(payload_len), &payload_len)) return false; + payload_len = bit_lib_bytes_to_num_be((void*)&payload_len, sizeof(payload_len)); + pos += sizeof(payload_len); } // ID Length uint8_t id_len = 0; - if(id_present) { - id_len = *cur++; + if(flags_tnf.id_length_present) { + if(!ndef_get(ndef, pos++, 1, &id_len)) return false; } // Payload Type - char* type = NULL; + char type_buf[32]; // Longest type supported in ndef_parse_payload() is 32 chars excl terminator + char* type = type_buf; + bool type_was_allocated = false; if(type_len) { - type = malloc(type_len); - memcpy(type, cur, type_len); - cur += type_len; + if(type_len > sizeof(type_buf)) { + type = malloc(type_len); + type_was_allocated = true; + } + if(!ndef_get(ndef, pos, type_len, type)) { + if(type_was_allocated) free(type); + return false; + } + pos += type_len; } // Payload ID - cur += id_len; + pos += id_len; - furi_string_cat_printf(str, "\e*> M:%d R:%d - ", message_num, record_num); - parse_ndef_payload(str, tnf, type, type_len, cur, payload_len); - cur += payload_len; + furi_string_cat_printf(ndef->output, "\e*> M%dR%d: ", message_num, record_num); + if(!ndef_parse_payload( + ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { + if(type_was_allocated) free(type); + return false; + } + pos += payload_len; - free(type); - furi_string_trim(str, "\n"); - furi_string_cat(str, "\n\n"); + if(type_was_allocated) free(type); + furi_string_trim(ndef->output, "\n"); + furi_string_cat(ndef->output, "\n\n"); } - return cur; + + return pos == end && last_record; } -static bool ndef_parse(const NfcDevice* device, FuriString* parsed_data) { +// TLV structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data +static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { + size_t message_num = 0; + + while(true) { + NdefTlv tlv; + if(!ndef_get(ndef, pos++, 1, &tlv)) return 0; + FURI_LOG_D(TAG, "tlv: %02X", tlv); + + switch(tlv) { + default: + // Unknown, bail to avoid problems + return 0; + + case NdefTlvPadding: + // Has no length, skip to next byte + break; + + case NdefTlvTerminator: + // NDEF message finished, return whether we parsed something + return message_num; + + case NdefTlvLockControl: + case NdefTlvMemoryControl: + case NdefTlvProprietary: + case NdefTlvNdefMessage: { + // Calculate length + uint16_t len; + uint8_t len_type; + if(!ndef_get(ndef, pos++, 1, &len_type)) return 0; + if(len_type < 0xFF) { // 1 byte length + len = len_type; + } else { // 3 byte length (0xFF marker + 2 byte integer) + if(!ndef_get(ndef, pos, sizeof(len), &len)) return 0; + len = bit_lib_bytes_to_num_be((void*)&len, sizeof(len)); + pos += sizeof(len); + } + + if(tlv != NdefTlvNdefMessage) { + // We don't care, skip this TLV block to next one + pos += len; + break; + } + + if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed)) return 0; + pos += len; + + break; + } + } + } +} + +// ---=== protocol entry-points ===--- + +#if NDEF_PROTO == NDEF_PROTO_UL + +// MF UL memory layout: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#memory_layout +static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); furi_assert(parsed_data); const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight); - bool parsed = false; + // Check card type can contain NDEF + if(data->type != MfUltralightTypeNTAG203 && data->type != MfUltralightTypeNTAG213 && + data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && + data->type != MfUltralightTypeNTAGI2C1K && data->type != MfUltralightTypeNTAGI2C2K && + data->type != MfUltralightTypeNTAGI2CPlus1K && + data->type != MfUltralightTypeNTAGI2CPlus2K) { + return false; + } - do { - // Memory layout documentation: - // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrfxlib/nfc/doc/type_2_tag.html#id2 + // Double check Capability Container (CC) values + struct { + uint8_t nfc_magic_number; + uint8_t document_version_number; + uint8_t data_area_size; + uint8_t read_write_access; + }* cc = (void*)&data->page[3].data[0]; + if(cc->nfc_magic_number != 0xE1) return false; + if(cc->document_version_number != 0x10) return false; - // Check card type can contain NDEF - if(data->type != MfUltralightTypeNTAG203 && data->type != MfUltralightTypeNTAG213 && - data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && - data->type != MfUltralightTypeNTAGI2C1K && data->type != MfUltralightTypeNTAGI2C2K && - data->type != MfUltralightTypeNTAGI2CPlus1K && - data->type != MfUltralightTypeNTAGI2CPlus2K) { - break; - } + // Calculate usable data area + const uint8_t* start = &data->page[4].data[0]; + const uint8_t* end = start + (cc->data_area_size * 2 * MF_ULTRALIGHT_PAGE_SIZE); + size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; + end = MIN(end, &data->page[0].data[0] + max_size); - // Double check Capability Container (CC) and find data area bounds - struct { - uint8_t nfc_magic_number; - uint8_t document_version_number; - uint8_t data_area_size; - uint8_t read_write_access; - }* cc = (void*)&data->page[3].data[0]; - if(cc->nfc_magic_number != 0xE1) break; - if(cc->document_version_number != 0x10) break; - const uint8_t* cur = &data->page[4].data[0]; - const uint8_t* end = cur + (cc->data_area_size * 2 * MF_ULTRALIGHT_PAGE_SIZE); - size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; - end = MIN(end, &data->page[0].data[0] + max_size); - size_t message_num = 0; + furi_string_printf( + parsed_data, + "\e#NDEF Format Data\nCard type: %s\n", + mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); - // Parse as TLV (see docs above) - while(cur < end) { - switch(*cur++) { - case 0x03: { // NDEF message - if(cur >= end) break; - uint16_t len; - if(*cur < 0xFF) { // 1 byte length - len = *cur++; - } else { // 3 byte length (0xFF marker + 2 byte integer) - if(cur + 2 >= end) { - cur = end; - break; - } - len = bit_lib_bytes_to_num_be(++cur, 2); - cur += 2; - } - if(cur + len >= end) { - cur = end; - break; - } + Ndef ndef = { + .output = parsed_data, + .ul = + { + .start = start, + .size = end - start, + }, + }; + size_t parsed = ndef_parse_tlv(&ndef, 0, 0); - if(message_num++ == 0) { - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); - } + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } - const uint8_t* message_end = cur + len; - cur = parse_ndef_message(parsed_data, message_num, cur, message_end); - if(cur != message_end) cur = end; + return parsed > 0; +} - break; - } +#elif NDEF_PROTO == NDEF_PROTO_MFC - case 0xFE: // TLV end - cur = end; - if(message_num != 0) parsed = true; - break; +// MFC MAD datasheet: +// https://www.nxp.com/docs/en/application-note/AN10787.pdf +#define AID_SIZE (2) +static const uint64_t mad_key = 0xA0A1A2A3A4A5; - case 0x00: // Padding, has no length, skip - break; +// NDEF on MFC breakdown: +// https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#storing-ndef-messages-in-mifare-sectors-607778 +static const uint8_t ndef_aid[AID_SIZE] = {0x03, 0xE1}; - case 0x01: // Lock control - case 0x02: // Memory control - case 0xFD: // Proprietary - // We don't care, skip this TLV block - if(cur >= end) break; - if(*cur < 0xFF) { // 1 byte length - cur += *cur + 1; // Shift by TLV length - } else { // 3 byte length (0xFF marker + 2 byte integer) - if(cur + 2 >= end) { - cur = end; - break; - } - cur += bit_lib_bytes_to_num_be(cur + 1, 2) + 3; // Shift by TLV length - } - break; +static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); - default: // Unknown, bail to avoid problems - cur = end; - break; + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + // Check card type can contain NDEF + if(data->type != MfClassicType1k && data->type != MfClassicType4k && + data->type != MfClassicTypeMini) { + return false; + } + + // Check MADs for what sectors contain NDEF data AIDs + bool sectors_with_ndef[MF_CLASSIC_TOTAL_SECTORS_MAX] = {0}; + const size_t sector_count = mf_classic_get_total_sectors_num(data->type); + const struct { + size_t block; + uint8_t aid_count; + } mads[2] = { + {1, 15}, + {64, 23}, + }; + for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { + if(sector_count <= 16 && mad > 0) break; // Skip MAD2 if not present + for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { + const size_t block = mads[mad].block; + const size_t sector = mf_classic_get_sector_by_block(block); + // Check MAD key + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector); + const uint64_t sector_key_a = bit_lib_bytes_to_num_be( + sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); + if(sector_key_a != mad_key) return false; + // Find NDEF AIDs + const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; + if(!memcmp(aid, ndef_aid, AID_SIZE)) { + sectors_with_ndef[aid_index + 1] = true; } } + } + + furi_string_printf( + parsed_data, + "\e#NDEF Format Data\nCard type: %s\n", + mf_classic_get_device_name(data, NfcDeviceNameTypeFull)); + + // Calculate how large the data space is, so excluding sector trailers and MAD2. + // Makes sure we stay within this card's actual content when parsing. + // First 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 32 sectors correspond to 93 (31*3) data blocks. + // Last 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 8 sectors correspond to 120 (8*15) data blocks. + size_t data_size; + if(sector_count > 32) { + data_size = 93 + (sector_count - 32) * 15; + } else { + data_size = sector_count * 3; + if(sector_count >= 16) { + data_size -= 3; // Skip MAD2 + } + } + data_size *= MF_CLASSIC_BLOCK_SIZE; + + Ndef ndef = { + .output = parsed_data, + .mfc = + { + .blocks = data->block, + .size = data_size, + }, + }; + size_t total_parsed = 0; + + for(size_t sector = 0; sector < sector_count; sector++) { + if(!sectors_with_ndef[sector]) continue; + FURI_LOG_D(TAG, "sector: %d", sector); + size_t string_prev = furi_string_size(parsed_data); + + // Convert real sector number to data block number + // to skip sector trailers and MAD2 + size_t data_block; + if(sector < 32) { + data_block = sector * 3; + if(sector >= 16) { + data_block -= 3; // Skip MAD2 + } + } else { + data_block = 93 + (sector - 32) * 15; + } + FURI_LOG_D(TAG, "data_block: %d", data_block); + size_t parsed = ndef_parse_tlv(&ndef, data_block * MF_CLASSIC_BLOCK_SIZE, total_parsed); if(parsed) { + total_parsed += parsed; furi_string_trim(parsed_data, "\n"); furi_string_cat(parsed_data, "\n"); } else { - furi_string_reset(parsed_data); + furi_string_left(parsed_data, string_prev); } - } while(false); + } - return parsed; + if(!total_parsed) { + furi_string_reset(parsed_data); + } + + return total_parsed > 0; } +#endif + +// ---=== boilerplate ===--- + /* Actual implementation of app<>plugin interface */ static const NfcSupportedCardsPlugin ndef_plugin = { - .protocol = NfcProtocolMfUltralight, .verify = NULL, .read = NULL, - .parse = ndef_parse, +#if NDEF_PROTO == NDEF_PROTO_UL + .parse = ndef_ul_parse, + .protocol = NfcProtocolMfUltralight, +#elif NDEF_PROTO == NDEF_PROTO_MFC + .parse = ndef_mfc_parse, + .protocol = NfcProtocolMfClassic, +#endif }; /* Plugin descriptor to comply with basic plugin specification */ diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index afaf30427..70011042f 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -975,7 +975,7 @@ Function,+,dir_walk_read,DirWalkResult,"DirWalk*, FuriString*, FileInfo*" Function,+,dir_walk_set_filter_cb,void,"DirWalk*, DirWalkFilterCb, void*" Function,+,dir_walk_set_recurse_filter,void,"DirWalk*, const char**, size_t" Function,+,dir_walk_set_recursive,void,"DirWalk*, _Bool" -Function,-,div,div_t,"int, int" +Function,+,div,div_t,"int, int" Function,+,dolphin_deed,void,DolphinDeed Function,+,dolphin_deed_get_app,DolphinApp,DolphinDeed Function,+,dolphin_deed_get_app_limit,uint8_t,DolphinApp From e1fdc5e98492d4a3d1bafd9b7fb2ee2adc82ed3c Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 03:20:53 +0100 Subject: [PATCH 51/74] NFC: NDEF SmartPoster support (#275) * NFC: NDEF SmartPoster support * Consistent naming * Nicer result output * Update changelog --- CHANGELOG.md | 8 ++-- .../main/nfc/plugins/supported_cards/ndef.c | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f70030e..2a78b2923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,9 +115,11 @@ - OFW: USB/BT Remote: Mouse clicker option to click as fast as possible (by @sumukhj1219) - CLI: Print plugin name on load fail (by @Willy-JL) - NFC: - - NDEF parser supports Mifare Classic (#265 by @luu176), rewritten to be protocol-agnostic and more improvements (#265 by @Willy-JL) - - NDEF parser supports NTAG I2C Plus 1k and 2k chips too (by @RocketGod-git) - - NDEF parser decodes URL-encoded URI characters (#267 by @jaylikesbunda) + - NDEF Parser: + - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) + - Decoding of URL-encoded URI characters (#267 by @jaylikesbunda) + - SmartPoster record support (#275 by @Willy-JL) + - Enable parsing NTAG I2C Plus 1k and 2k chips too (#237 by @RocketGod-git) - Added 6 new Mifare Classic keys from Bulgaria Hotel (#216 by @z3r0l1nk) - UL: Add iq aparts hotel key (by @xMasterX) - OFW/UL: Rename 'Detect Reader' to 'Extract MFC Keys' (by @bettse & @xMasterX) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index f2ef765a6..b4c56d64f 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -492,7 +492,20 @@ static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { return true; } -static bool ndef_parse_payload( +// ---=== ndef layout parsing ===--- + +static bool + ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); +static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed); +static bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, + const char* type, + uint8_t type_len); + +static bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, @@ -507,13 +520,16 @@ static bool ndef_parse_payload( switch(tnf) { case NdefTnfWellKnownType: - if(strncmp("U", type, type_len) == 0) { + if(strncmp("Sp", type, type_len) == 0) { + furi_string_cat(ndef->output, "SmartPoster\nContained records below\n\n"); + return ndef_parse_message(ndef, pos, len, 0, true); + } else if(strncmp("U", type, type_len) == 0) { return ndef_parse_uri(ndef, pos, len); } else if(strncmp("T", type, type_len) == 0) { return ndef_parse_text(ndef, pos, len); } // Dump data without parsing - furi_string_cat(ndef->output, "Unsupported\n"); + furi_string_cat(ndef->output, "Unknown\n"); ndef_print(ndef, "Well-known Type", type, type_len, false); if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; return true; @@ -527,7 +543,7 @@ static bool ndef_parse_payload( return ndef_parse_wifi(ndef, pos, len); } // Dump data without parsing - furi_string_cat(ndef->output, "Unsupported\n"); + furi_string_cat(ndef->output, "Unknown\n"); ndef_print(ndef, "Media Type", type, type_len, false); if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; return true; @@ -548,11 +564,10 @@ static bool ndef_parse_payload( } } -// ---=== tlv and message parsing ===--- - // NDEF message structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format -static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num) { +static bool + ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { size_t end = pos + len; size_t record_num = 0; @@ -599,7 +614,7 @@ static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t messag } // Payload Type - char type_buf[32]; // Longest type supported in ndef_parse_payload() is 32 chars excl terminator + char type_buf[32]; // Longest type supported in ndef_parse_record() is 32 chars excl terminator char* type = type_buf; bool type_was_allocated = false; if(type_len) { @@ -617,9 +632,12 @@ static bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t messag // Payload ID pos += id_len; - furi_string_cat_printf(ndef->output, "\e*> M%dR%d: ", message_num, record_num); - if(!ndef_parse_payload( - ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { + if(smart_poster) { + furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", message_num, record_num); + } + if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { if(type_was_allocated) free(type); return false; } @@ -678,7 +696,8 @@ static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { break; } - if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed)) return 0; + if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed, false)) + return 0; pos += len; break; From 907a8403aedfbb52a0b3c4e8109c9f8c03d8ea61 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 03:28:14 +0100 Subject: [PATCH 52/74] Update apps - TOTP: Added polyfill for cli/cli_ansi.h (by akopachov) --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index 3700ee70a..bb84d4d2f 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 3700ee70a6b4a924bfb9bd3cb66f6985d1de40a7 +Subproject commit bb84d4d2f0d4eb57f5162d43518f0627cad9c0c0 From 56332919de9c352f0bc87064c08e127592bebe4e Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 03:32:34 +0100 Subject: [PATCH 53/74] Format --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index bb84d4d2f..067aa52f5 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit bb84d4d2f0d4eb57f5162d43518f0627cad9c0c0 +Subproject commit 067aa52f521e3f05e2181f753bffab733c424602 From 96e5b24a71082a465d3abacf3084b08f2675356c Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:27:41 +0100 Subject: [PATCH 54/74] JS: Fourth batch of OFW PR changes --- .../examples/apps/Scripts/Examples/gui.js | 8 ++--- .../examples/apps/Scripts/interactive.js | 5 ++- applications/system/js_app/js_thread.c | 14 +++++--- .../system/js_app/modules/js_gui/byte_input.c | 32 ++++++++++++++----- .../system/js_app/modules/js_gui/js_gui.c | 4 +-- .../system/js_app/modules/js_gui/js_gui.h | 2 +- .../system/js_app/modules/js_gui/submenu.c | 4 +-- .../system/js_app/modules/js_gui/text_input.c | 17 +++++++++- applications/system/js_app/types/global.d.ts | 6 ++-- 9 files changed, 64 insertions(+), 28 deletions(-) diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js index 5faeb6d32..a1e023853 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/gui.js @@ -17,18 +17,16 @@ let views = { empty: emptyView.make(), keyboard: textInputView.makeWith({ header: "Enter your name", - defaultText: flipper.getName(), - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 32, + defaultText: flipper.getName(), + defaultTextClear: true, }), helloDialog: dialogView.make(), bytekb: byteInputView.makeWith({ header: "Look ma, I'm a header text!", - defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), - // Props for makeWith() are passed in reverse order, so length must be after defaultData length: 8, + defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), }), longText: textBoxView.makeWith({ text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.", diff --git a/applications/system/js_app/examples/apps/Scripts/interactive.js b/applications/system/js_app/examples/apps/Scripts/interactive.js index 34639cdac..40ca98c30 100644 --- a/applications/system/js_app/examples/apps/Scripts/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/interactive.js @@ -24,11 +24,10 @@ let views = { }), textInput: textInput.makeWith({ header: "Type JavaScript Code:", - defaultText: "2+2", - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 256, + defaultText: "2+2", + defaultTextClear: true, }), loading: loading.make(), }; diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 7a774d324..83f9e604c 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,12 +198,18 @@ static void js_require(struct mjs* mjs) { static void js_parse_int(struct mjs* mjs) { const char* str; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str)); + int32_t base = 10; - if(mjs_nargs(mjs) == 1) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str)); - } else { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base)); + if(mjs_nargs(mjs) >= 2) { + mjs_val_t base_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(base_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number"); + mjs_return(mjs, MJS_UNDEFINED); + } + base = mjs_get_int(mjs, base_arg); } + int32_t num; if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { num = 0; diff --git a/applications/system/js_app/modules/js_gui/byte_input.c b/applications/system/js_app/modules/js_gui/byte_input.c index 5c8844d22..2d6dae475 100644 --- a/applications/system/js_app/modules/js_gui/byte_input.c +++ b/applications/system/js_app/modules/js_gui/byte_input.c @@ -8,6 +8,7 @@ typedef struct { uint8_t* buffer; size_t buffer_size; + size_t default_data_size; FuriString* header; FuriSemaphore* input_semaphore; JsEventLoopContract contract; @@ -38,7 +39,14 @@ static bool len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) { UNUSED(mjs); UNUSED(input); - context->buffer_size = (size_t)(value.number); + size_t new_buffer_size = value.number; + if(new_buffer_size < context->default_data_size) { + // Avoid confusing parameters from user + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "length must be larger than defaultData length"); + return false; + } + context->buffer_size = new_buffer_size; context->buffer = realloc(context->buffer, context->buffer_size); //-V701 byte_input_set_result_callback( input, @@ -57,16 +65,24 @@ static bool default_data_assign( JsByteKbContext* context) { UNUSED(mjs); - mjs_val_t array_buf = value.array; + mjs_val_t array_buf = value.term; if(mjs_is_data_view(array_buf)) { array_buf = mjs_dataview_get_buf(mjs, array_buf); } - size_t default_data_len = 0; - char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &default_data_len); - memcpy( - context->buffer, - (uint8_t*)default_data, - MIN((size_t)context->buffer_size, default_data_len)); + char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &context->default_data_size); + if(context->buffer_size < context->default_data_size) { + // Ensure buffer is large enough for defaultData + context->buffer_size = context->default_data_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + } + memcpy(context->buffer, (uint8_t*)default_data, context->default_data_size); + if(context->buffer_size > context->default_data_size) { + // Reset previous data after defaultData + memset( + context->buffer + context->default_data_size, + 0x00, + context->buffer_size - context->default_data_size); + } byte_input_set_result_callback( input, diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index 4bd4ccc31..22d04855d 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -216,14 +216,14 @@ static bool expected_type = "array"; break; } - c_value = (JsViewPropValue){.array = value}; + c_value = (JsViewPropValue){.term = value}; } break; case JsViewPropTypeTypedArr: { if(!mjs_is_typed_array(value)) { expected_type = "typed_array"; break; } - c_value = (JsViewPropValue){.array = value}; + c_value = (JsViewPropValue){.term = value}; } break; case JsViewPropTypeBool: { if(!mjs_is_boolean(value)) { diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h index 67266b1fc..d400d0a33 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -16,8 +16,8 @@ typedef enum { typedef union { const char* string; int32_t number; - mjs_val_t array; bool boolean; + mjs_val_t term; } JsViewPropValue; /** diff --git a/applications/system/js_app/modules/js_gui/submenu.c b/applications/system/js_app/modules/js_gui/submenu.c index aecd413be..c142bcddb 100644 --- a/applications/system/js_app/modules/js_gui/submenu.c +++ b/applications/system/js_app/modules/js_gui/submenu.c @@ -33,9 +33,9 @@ static bool static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { UNUSED(mjs); submenu_reset(submenu); - size_t len = mjs_array_length(mjs, value.array); + size_t len = mjs_array_length(mjs, value.term); for(size_t i = 0; i < len; i++) { - mjs_val_t item = mjs_array_get(mjs, value.array, i); + mjs_val_t item = mjs_array_get(mjs, value.term, i); if(!mjs_is_string(item)) return false; submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context); } diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index d2bf4a8f9..e93bbfad0 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -8,6 +8,7 @@ typedef struct { char* buffer; size_t buffer_size; + size_t default_text_size; FuriString* header; bool default_text_clear; FuriSemaphore* input_semaphore; @@ -49,7 +50,14 @@ static bool max_len_assign( JsViewPropValue value, JsKbdContext* context) { UNUSED(mjs); - context->buffer_size = (size_t)(value.number + 1); + size_t new_buffer_size = value.number + 1; + if(new_buffer_size < context->default_text_size) { + // Avoid confusing parameters from user + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "maxLength must be larger than defaultText length"); + return false; + } + context->buffer_size = new_buffer_size; context->buffer = realloc(context->buffer, context->buffer_size); //-V701 text_input_set_result_callback( input, @@ -70,6 +78,13 @@ static bool default_text_assign( UNUSED(input); if(value.string) { + context->default_text_size = strlen(value.string) + 1; + if(context->buffer_size < context->default_text_size) { + // Ensure buffer is large enough for defaultData + context->buffer_size = context->default_text_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + } + // Also trim excess previous data with strlcpy() strlcpy(context->buffer, value.string, context->buffer_size); text_input_set_result_callback( input, diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index d132f89f5..a55dae7d9 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -31,8 +31,10 @@ declare const __filename: string; /** * @brief Reads a JS value from a file * - * Reads a file at the specified path, interprets it as a JS value and returns - * the last value pushed on the stack. + * Reads a file at the specified path and runs it as JS, returning the last evaluated value. + * + * The result is cached and this filepath will not re-evaluated on future + * load() calls for this session. * * @param path The path to the file * @param scope An object to use as global scope while running this file From 19a65a85ba6bb544579b4ebc0dfbe3c2179910e3 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:28:26 +0100 Subject: [PATCH 55/74] JS: Temp enable illegal symbols in textinput until prop is available --- applications/system/js_app/modules/js_gui/text_input.c | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index e93bbfad0..79a138388 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -135,6 +135,7 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_ .transformer_context = context, }, }; + text_input_add_illegal_symbols(input); text_input_set_result_callback( input, (TextInputCallback)input_callback, From 6dbb46a81d1de7aa0bc55775461ef747a3ab6964 Mon Sep 17 00:00:00 2001 From: noproto Date: Fri, 25 Oct 2024 18:38:11 -0400 Subject: [PATCH 56/74] Fix issue with resume logic --- applications/main/nfc/views/dict_attack.c | 2 ++ .../protocols/mf_classic/mf_classic_poller.c | 13 +++++++++---- .../protocols/mf_classic/mf_classic_poller.h | 17 +++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/applications/main/nfc/views/dict_attack.c b/applications/main/nfc/views/dict_attack.c index 4efbe6d60..726076972 100644 --- a/applications/main/nfc/views/dict_attack.c +++ b/applications/main/nfc/views/dict_attack.c @@ -45,6 +45,7 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { furi_string_set(m->header, "PRNG Analysis"); break; case MfClassicNestedPhaseDictAttack: + case MfClassicNestedPhaseDictAttackVerify: case MfClassicNestedPhaseDictAttackResume: furi_string_set(m->header, "Nested Dictionary"); break; @@ -91,6 +92,7 @@ static void dict_attack_draw_callback(Canvas* canvas, void* model) { float dict_progress = 0; if(m->nested_phase == MfClassicNestedPhaseAnalyzePRNG || m->nested_phase == MfClassicNestedPhaseDictAttack || + m->nested_phase == MfClassicNestedPhaseDictAttackVerify || m->nested_phase == MfClassicNestedPhaseDictAttackResume) { // Phase: Nested dictionary attack uint8_t target_sector = diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 8d5865865..edfdb2a95 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -1881,9 +1881,10 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance uint16_t dict_target_key_max = (dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) ? (instance->sectors_total * 2) : (instance->sectors_total * 16); - if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackResume) { + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackVerify) { if(!(mf_classic_nested_is_target_key_found(instance, true)) && (dict_attack_ctx->nested_nonce.count > 0)) { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackResume; instance->state = MfClassicPollerStateNestedDictAttack; return command; } else { @@ -1898,7 +1899,8 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; } } - if((dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttack) && + if((dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttack || + dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackResume) && (dict_attack_ctx->nested_target_key < dict_target_key_max)) { bool is_last_iter_for_hard_key = ((!is_weak) && ((dict_attack_ctx->nested_target_key % 8) == 7)); @@ -1922,11 +1924,14 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance NULL; } if((is_weak || is_last_iter_for_hard_key) && dict_attack_ctx->nested_nonce.count > 0) { - // Key reuse - dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackResume; + // Key verify and reuse + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackVerify; dict_attack_ctx->auth_passed = false; instance->state = MfClassicPollerStateKeyReuseStartNoOffset; return command; + } else if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttackResume) { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; + dict_attack_ctx->auth_passed = true; } if(!(dict_attack_ctx->auth_passed)) { dict_attack_ctx->attempt_count++; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.h b/lib/nfc/protocols/mf_classic/mf_classic_poller.h index 7dfd3b6ab..8efb931aa 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.h @@ -52,14 +52,15 @@ typedef enum { * @brief MfClassic poller nested attack phase. */ typedef enum { - MfClassicNestedPhaseNone, - MfClassicNestedPhaseAnalyzePRNG, - MfClassicNestedPhaseDictAttack, - MfClassicNestedPhaseDictAttackResume, - MfClassicNestedPhaseCalibrate, - MfClassicNestedPhaseRecalibrate, - MfClassicNestedPhaseCollectNtEnc, - MfClassicNestedPhaseFinished, + MfClassicNestedPhaseNone, /**< No nested attack has taken place yet. */ + MfClassicNestedPhaseAnalyzePRNG, /**< Analyze nonces produced by the PRNG to determine if they fit a weak PRNG */ + MfClassicNestedPhaseDictAttack, /**< Search keys which match the expected PRNG properties and parity for collected nonces */ + MfClassicNestedPhaseDictAttackVerify, /**< Verify candidate keys by authenticating to the sector with the key */ + MfClassicNestedPhaseDictAttackResume, /**< Resume nested dictionary attack from the last tested (invalid) key */ + MfClassicNestedPhaseCalibrate, /**< Perform necessary calculations to recover the plaintext nonce during later collection phase (weak PRNG tags only) */ + MfClassicNestedPhaseRecalibrate, /**< Collect the next plaintext static encrypted nonce for backdoor static encrypted nonce nested attack */ + MfClassicNestedPhaseCollectNtEnc, /**< Log nonces collected during nested authentication for key recovery */ + MfClassicNestedPhaseFinished, /**< Nested attack has finished */ } MfClassicNestedPhase; /** From a50ae0a6041e2915043b976876c68bf721b87727 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:48:33 +0100 Subject: [PATCH 57/74] NFC: NDEF Parser SLIX support (#278) * Simpler protocol check * Simplify header code * Minor code fixes * NFC: NDEF Parser SLIX support * Parse success even without TLV terminator * Also parse empty NDEF TLVs * Add NDEF_PROTO_RAW to feed data manually * Update changelog * Ordering * Comment changes * Fix inefficiency in MAD checking --- CHANGELOG.md | 1 + applications/main/nfc/application.fam | 10 + .../main/nfc/plugins/supported_cards/ndef.c | 207 ++++++++++++++---- 3 files changed, 171 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a78b2923..fdb4830ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ - NFC: - NDEF Parser: - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) + - SLIX support, parse even with TLV terminator omitted, parse empty NDEF TLVs (#277 by @Willy-JL) - Decoding of URL-encoded URI characters (#267 by @jaylikesbunda) - SmartPoster record support (#275 by @Willy-JL) - Enable parsing NTAG I2C Plus 1k and 2k chips too (#237 by @RocketGod-git) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index f49ba84a2..53c04f609 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -274,6 +274,16 @@ App( sources=["plugins/supported_cards/ndef.c"], ) +App( + appid="ndef_slix_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_SLIX")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + App( appid="itso_parser", apptype=FlipperAppType.PLUGIN, diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index b4c56d64f..97e67e9ad 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -1,34 +1,49 @@ // Parser for NDEF format data // Supports multiple NDEF messages and records in same tag -// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty +// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty, SmartPoster // Documentation and sources indicated where relevant // Made by @Willy-JL -// Mifare Classic support added by @luu176 +// Mifare Ultralight support by @Willy-JL +// Mifare Classic support by @luu176 & @Willy-JL +// SLIX support by @Willy-JL // We use an arbitrary position system here, in order to support more protocols. // Each protocol parses basic structure of the card, then starts ndef_parse_tlv() // using an arbitrary position value that it can understand. When accessing data // to parse NDEF content, ndef_get() will then map this arbitrary value to the -// card using state in Ndef struct, skip blocks or sectors as needed. This way, -// NDEF parsing code does not need to know details of card layout. +// card using state in Ndef struct, skipping blocks or sectors as needed. This +// way, NDEF parsing code does not need to know details of card layout. #include "nfc_supported_card_plugin.h" #include #include #include +#include #include #define TAG "NDEF" -#define NDEF_PROTO_NONE (0) -#define NDEF_PROTO_UL (1) -#define NDEF_PROTO_MFC (2) +#define NDEF_PROTO_INVALID (-1) +#define NDEF_PROTO_RAW (0) // For parsing data fed manually +#define NDEF_PROTO_UL (1) +#define NDEF_PROTO_MFC (2) +#define NDEF_PROTO_SLIX (3) +#define NDEF_PROTO_TOTAL (4) -#if !defined(NDEF_PROTO) || (NDEF_PROTO != NDEF_PROTO_UL && NDEF_PROTO != NDEF_PROTO_MFC) +#ifndef NDEF_PROTO #error Must specify what protocol to use with NDEF_PROTO define! #endif +#if NDEF_PROTO <= NDEF_PROTO_INVALID || NDEF_PROTO >= NDEF_PROTO_TOTAL +#error Invalid NDEF_PROTO specified! +#endif + +#define NDEF_TITLE(device, parsed_data) \ + furi_string_printf( \ + parsed_data, \ + "\e#NDEF Format Data\nCard type: %s\n", \ + nfc_device_get_name(device, NfcDeviceNameTypeFull)) // ---=== structures ===--- @@ -72,8 +87,7 @@ _Static_assert(sizeof(NdefFlagsTnf) == 1); // URI payload format: // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 static const char* ndef_uri_prepends[] = { - // clang-format off - [0x00] = NULL, + [0x00] = NULL, // Allows detecting no prepend and checking schema for type [0x01] = "http://www.", [0x02] = "https://www.", [0x03] = "http://", @@ -109,7 +123,6 @@ static const char* ndef_uri_prepends[] = { [0x21] = "urn:epc:raw:", [0x22] = "urn:epc:", [0x23] = "urn:nfc:", - // clang-format on }; // ---=== card memory layout abstraction ===--- @@ -117,7 +130,12 @@ static const char* ndef_uri_prepends[] = { // Shared context and state, read above typedef struct { FuriString* output; -#if NDEF_PROTO == NDEF_PROTO_UL +#if NDEF_PROTO == NDEF_PROTO_RAW + struct { + const uint8_t* data; + size_t size; + } raw; +#elif NDEF_PROTO == NDEF_PROTO_UL struct { const uint8_t* start; size_t size; @@ -127,11 +145,23 @@ typedef struct { const MfClassicBlock* blocks; size_t size; } mfc; +#elif NDEF_PROTO == NDEF_PROTO_SLIX + struct { + const uint8_t* start; + size_t size; + } slix; #endif } Ndef; static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { -#if NDEF_PROTO == NDEF_PROTO_UL +#if NDEF_PROTO == NDEF_PROTO_RAW + + // Using user-provided pointer, simply need to remap to it + if(pos + len > ndef->raw.size) return false; + memcpy(buf, ndef->raw.data + pos, len); + return true; + +#elif NDEF_PROTO == NDEF_PROTO_UL // Memory space is contiguous, simply need to remap to data pointer if(pos + len > ndef->ul.size) return false; @@ -188,6 +218,13 @@ static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { return true; +#elif NDEF_PROTO == NDEF_PROTO_SLIX + + // Memory space is contiguous, simply need to remap to data pointer + if(pos + len > ndef->slix.size) return false; + memcpy(buf, ndef->slix.start + pos, len); + return true; + #else UNUSED(ndef); @@ -494,18 +531,17 @@ static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { // ---=== ndef layout parsing ===--- -static bool - ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); -static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed); -static bool ndef_parse_record( +bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, NdefTnf tnf, const char* type, uint8_t type_len); +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed); -static bool ndef_parse_record( +bool ndef_parse_record( Ndef* ndef, size_t pos, size_t len, @@ -566,8 +602,7 @@ static bool ndef_parse_record( // NDEF message structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format -static bool - ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { size_t end = pos + len; size_t record_num = 0; @@ -648,15 +683,24 @@ static bool furi_string_cat(ndef->output, "\n\n"); } - return pos == end && last_record; + if(record_num == 0) { + if(smart_poster) { + furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%d: Empty\n\n", message_num); + } + } + + return pos == end && (last_record || record_num == 0); } // TLV structure: // https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data -static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed) { + size_t end = pos + len; size_t message_num = 0; - while(true) { + while(pos < end) { NdefTlv tlv; if(!ndef_get(ndef, pos++, 1, &tlv)) return 0; FURI_LOG_D(TAG, "tlv: %02X", tlv); @@ -704,8 +748,14 @@ static size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t already_parsed) { } } } + + // Reached data end with no TLV terminator, + // but also no errors, treat this as a success + return message_num; } +#if NDEF_PROTO != NDEF_PROTO_RAW + // ---=== protocol entry-points ===--- #if NDEF_PROTO == NDEF_PROTO_UL @@ -727,11 +777,11 @@ static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { return false; } - // Double check Capability Container (CC) values + // Check Capability Container (CC) values struct { uint8_t nfc_magic_number; uint8_t document_version_number; - uint8_t data_area_size; + uint8_t data_area_size; // Usable byte size / 8, only includes user memory uint8_t read_write_access; }* cc = (void*)&data->page[3].data[0]; if(cc->nfc_magic_number != 0xE1) return false; @@ -739,24 +789,23 @@ static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { // Calculate usable data area const uint8_t* start = &data->page[4].data[0]; - const uint8_t* end = start + (cc->data_area_size * 2 * MF_ULTRALIGHT_PAGE_SIZE); + const uint8_t* end = start + (cc->data_area_size * 8); size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; end = MIN(end, &data->page[0].data[0] + max_size); + size_t data_start = 0; + size_t data_size = end - start; - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); + NDEF_TITLE(device, parsed_data); Ndef ndef = { .output = parsed_data, .ul = { .start = start, - .size = end - start, + .size = data_size, }, }; - size_t parsed = ndef_parse_tlv(&ndef, 0, 0); + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); if(parsed) { furi_string_trim(parsed_data, "\n"); @@ -802,17 +851,17 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { {64, 23}, }; for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { - if(sector_count <= 16 && mad > 0) break; // Skip MAD2 if not present + const size_t block = mads[mad].block; + const size_t sector = mf_classic_get_sector_by_block(block); + if(sector_count <= sector) break; // Skip this MAD if not present + // Check MAD key + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector); + const uint64_t sector_key_a = bit_lib_bytes_to_num_be( + sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); + if(sector_key_a != mad_key) return false; + // Find NDEF AIDs for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { - const size_t block = mads[mad].block; - const size_t sector = mf_classic_get_sector_by_block(block); - // Check MAD key - const MfClassicSectorTrailer* sector_trailer = - mf_classic_get_sector_trailer_by_sector(data, sector); - const uint64_t sector_key_a = bit_lib_bytes_to_num_be( - sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); - if(sector_key_a != mad_key) return false; - // Find NDEF AIDs const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; if(!memcmp(aid, ndef_aid, AID_SIZE)) { sectors_with_ndef[aid_index + 1] = true; @@ -820,10 +869,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { } } - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_classic_get_device_name(data, NfcDeviceNameTypeFull)); + NDEF_TITLE(device, parsed_data); // Calculate how large the data space is, so excluding sector trailers and MAD2. // Makes sure we stay within this card's actual content when parsing. @@ -870,7 +916,8 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { data_block = 93 + (sector - 32) * 15; } FURI_LOG_D(TAG, "data_block: %d", data_block); - size_t parsed = ndef_parse_tlv(&ndef, data_block * MF_CLASSIC_BLOCK_SIZE, total_parsed); + size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed); if(parsed) { total_parsed += parsed; @@ -888,6 +935,67 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { return total_parsed > 0; } +#elif NDEF_PROTO == NDEF_PROTO_SLIX + +// SLIX NDEF memory layout: +// https://community.nxp.com/pwmxy87654/attachments/pwmxy87654/nfc/7583/1/EEOL_2011FEB16_EMS_RFD_AN_01.pdf +static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + const Iso15693_3Data* data = nfc_device_get_data(device, NfcProtocolIso15693_3); + const uint8_t block_size = iso15693_3_get_block_size(data); + const uint16_t block_count = iso15693_3_get_block_count(data); + const uint8_t* blocks = simple_array_cget_data(data->block_data); + + // TODO: Find some way to check for other iso15693 NDEF cards and + // split this to also support non-slix iso15693 NDEF tags + // Rest of the code works on iso15693 too, but uses SLIX layout assumptions + if(block_size != SLIX_BLOCK_SIZE) { + return false; + } + + // Check Capability Container (CC) values + struct { + uint8_t nfc_magic_number; + uint8_t read_write_access : 4; // Reversed due to endianness + uint8_t document_version_number : 4; + uint8_t data_area_size; // Total byte size / 8, includes block 0 + uint8_t mbread_ipread; + }* cc = (void*)&blocks[0 * block_size]; + if(cc->nfc_magic_number != 0xE1) return false; + if(cc->document_version_number != 0x4) return false; + + // Calculate usable data area + const uint8_t* start = &blocks[1 * block_size]; + const uint8_t* end = blocks + (cc->data_area_size * 8); + size_t max_size = block_count * block_size; + end = MIN(end, blocks + max_size); + size_t data_start = 0; + size_t data_size = end - start; + + NDEF_TITLE(device, parsed_data); + + Ndef ndef = { + .output = parsed_data, + .slix = + { + .start = start, + .size = data_size, + }, + }; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); + + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } + + return parsed > 0; +} + #endif // ---=== boilerplate ===--- @@ -902,6 +1010,9 @@ static const NfcSupportedCardsPlugin ndef_plugin = { #elif NDEF_PROTO == NDEF_PROTO_MFC .parse = ndef_mfc_parse, .protocol = NfcProtocolMfClassic, +#elif NDEF_PROTO == NDEF_PROTO_SLIX + .parse = ndef_slix_parse, + .protocol = NfcProtocolSlix, #endif }; @@ -916,3 +1027,5 @@ static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { const FlipperAppPluginDescriptor* ndef_plugin_ep(void) { return &ndef_plugin_descriptor; } + +#endif From 170db80b6deb9bf749235ee9640726bf0d3db672 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:49:13 +0100 Subject: [PATCH 58/74] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb4830ac..173720bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,7 +117,7 @@ - NFC: - NDEF Parser: - Mifare Classic support (#265 by @luu176), protocol-agnostic rewrite and more improvements (#265 by @Willy-JL) - - SLIX support, parse even with TLV terminator omitted, parse empty NDEF TLVs (#277 by @Willy-JL) + - SLIX support, parse even with TLV terminator omitted, parse empty NDEF TLVs (#278 by @Willy-JL) - Decoding of URL-encoded URI characters (#267 by @jaylikesbunda) - SmartPoster record support (#275 by @Willy-JL) - Enable parsing NTAG I2C Plus 1k and 2k chips too (#237 by @RocketGod-git) From ef2dd08b713e782218b05a34775161a892cafd9c Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:57:19 +0000 Subject: [PATCH 59/74] NFC: NDEF parser less RAM waste for contact vcards --- .../main/nfc/plugins/supported_cards/ndef.c | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index 97e67e9ad..f65e9f548 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -392,33 +392,67 @@ static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { } static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { - size_t end = pos + len; + // We hide redundant tags the user is probably not interested in. + // Would be easier with FuriString checks for start_with() and end_with() + // but to do that would waste lots of RAM on a cloned string buffer + // so instead we just look for these markers at start/end and shift + // pos and len then use ndef_dump() to output one char at a time. + // Results in miniml stack and no heap usage at all. + static const char* const begin_tag = "BEGIN:VCARD"; + static const uint8_t begin_len = strlen(begin_tag); + static const char* const version_tag = "VERSION:"; + static const uint8_t version_len = strlen(version_tag); + static const char* const end_tag = "END:VCARD"; + static const uint8_t end_len = strlen(end_tag); + char tmp[13] = {0}; // Enough for BEGIN:VCARD\r\n + uint8_t skip = 0; - // Same concept as ndef_dump(), inefficient but has least drawbacks - FuriString* fmt = furi_string_alloc(); - furi_string_reserve(fmt, len + 1); - while(pos < end) { - char c; - if(!ndef_get(ndef, pos++, 1, &c)) return false; - furi_string_push_back(fmt, c); + // Skip BEGIN tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(begin_tag, tmp, begin_len) == 0) { + skip = begin_len; + if(tmp[skip] == '\r') skip++; + if(tmp[skip] == '\n') skip++; + pos += skip; + len -= skip; + } } - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "BEGIN:VCARD")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - if(furi_string_end_with(fmt, "END:VCARD")) { - furi_string_left(fmt, furi_string_search_rchar(fmt, '\n')); + // Skip VERSION tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(version_tag, tmp, version_len) == 0) { + skip = version_len; + while(skip < len) { + if(!ndef_get(ndef, pos + skip, 1, &tmp[0])) return false; + skip++; + if(tmp[0] == '\n') break; + } + pos += skip; + len -= skip; } - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "VERSION:")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - furi_string_trim(fmt); + } + + // Skip END tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos + len - sizeof(tmp), sizeof(tmp), tmp)) return false; + // Read more than length of END tag and check multiple offsets, might have some padding after + // Worst case: there is END:VCARD\r\n\r\n which is same length as tmp buffer (13) + // Not sure if this is in spec but might aswell check + static const uint8_t offsets = sizeof(tmp) - end_len + 1; + for(uint8_t offset = 0; offset < offsets; offset++) { + if(strncmp(end_tag, tmp + offset, end_len) == 0) { + skip = sizeof(tmp) - offset; + len -= skip; + break; + } } } furi_string_cat(ndef->output, "Contact\n"); - ndef_print(ndef, NULL, furi_string_get_cstr(fmt), furi_string_size(fmt), false); - furi_string_free(fmt); + ndef_dump(ndef, NULL, pos, len, false); + return true; } @@ -863,7 +897,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { // Find NDEF AIDs for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; - if(!memcmp(aid, ndef_aid, AID_SIZE)) { + if(memcmp(aid, ndef_aid, AID_SIZE) == 0) { sectors_with_ndef[aid_index + 1] = true; } } From 2788ae14a6c43a907922417549232931fd536cb6 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 03:59:39 +0000 Subject: [PATCH 60/74] Fix typo --- applications/main/nfc/plugins/supported_cards/ndef.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index f65e9f548..820324421 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -397,7 +397,7 @@ static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { // but to do that would waste lots of RAM on a cloned string buffer // so instead we just look for these markers at start/end and shift // pos and len then use ndef_dump() to output one char at a time. - // Results in miniml stack and no heap usage at all. + // Results in minimal stack and no heap usage at all. static const char* const begin_tag = "BEGIN:VCARD"; static const uint8_t begin_len = strlen(begin_tag); static const char* const version_tag = "VERSION:"; From 3f1b181c5326dd154f67df0c027f610190e9c3c5 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:21:54 +0000 Subject: [PATCH 61/74] Asset Packer: Enforce LF when packing --nobuild --- scripts/asset_packer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/asset_packer.py b/scripts/asset_packer.py index dcfa68f8e..095b89970 100755 --- a/scripts/asset_packer.py +++ b/scripts/asset_packer.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# https://github.com/Next-Flip/Momentum-Firmware/blob/dev/scripts/asset_packer.py from PIL import Image, ImageOps import heatshrink2 import pathlib @@ -45,6 +45,10 @@ def convert_bmx(img: "Image.Image | pathlib.Path") -> bytes: return data +def copy_file_as_lf(src: "pathlib.Path", dst: "pathlib.Path"): + dst.write_bytes(src.read_bytes().replace(b"\r\n", b"\n")) + + def pack_anim(src: pathlib.Path, dst: pathlib.Path): if not (src / "meta.txt").is_file(): return @@ -53,7 +57,7 @@ def pack_anim(src: pathlib.Path, dst: pathlib.Path): if not frame.is_file(): continue if frame.name == "meta.txt": - shutil.copyfile(frame, dst / frame.name) + copy_file_as_lf(frame, dst / frame.name) elif frame.name.startswith("frame_"): if frame.suffix == ".png": (dst / frame.with_suffix(".bm").name).write_bytes(convert_bm(frame)) @@ -145,7 +149,7 @@ def pack( if (source / "Anims/manifest.txt").exists(): (packed / "Anims").mkdir(parents=True, exist_ok=True) - shutil.copyfile( + copy_file_as_lf( source / "Anims/manifest.txt", packed / "Anims/manifest.txt" ) manifest = (source / "Anims/manifest.txt").read_bytes() From e00d1aee8f4b0e3f746b39338c07c106d8b57222 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:22:54 +0000 Subject: [PATCH 62/74] Oops --nobuild --- scripts/asset_packer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/asset_packer.py b/scripts/asset_packer.py index 095b89970..dda625fc3 100755 --- a/scripts/asset_packer.py +++ b/scripts/asset_packer.py @@ -1,4 +1,4 @@ -# https://github.com/Next-Flip/Momentum-Firmware/blob/dev/scripts/asset_packer.py +#!/usr/bin/env python from PIL import Image, ImageOps import heatshrink2 import pathlib From 6b6d98da2af9fda8051c631659264bd75fa49c8f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:21:42 +0300 Subject: [PATCH 63/74] merge js changes changes by Willy-JL spi module by jamisonderek --- applications/system/js_app/application.fam | 8 + applications/system/js_app/js_thread.c | 14 +- .../system/js_app/modules/js_gui/byte_input.c | 32 +- .../system/js_app/modules/js_gui/js_gui.c | 4 +- .../system/js_app/modules/js_gui/js_gui.h | 2 +- .../system/js_app/modules/js_gui/submenu.c | 4 +- .../system/js_app/modules/js_gui/text_input.c | 17 +- applications/system/js_app/modules/js_math.c | 2 +- applications/system/js_app/modules/js_spi.c | 283 ++++++++++++++++++ .../system/js_app/types/badusb/index.d.ts | 2 +- applications/system/js_app/types/global.d.ts | 6 +- .../system/js_app/types/math/index.d.ts | 3 + .../system/js_app/types/spi/index.d.ts | 30 ++ 13 files changed, 385 insertions(+), 22 deletions(-) create mode 100644 applications/system/js_app/modules/js_spi.c create mode 100644 applications/system/js_app/types/spi/index.d.ts diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index f8f1be13f..5402bada7 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -193,3 +193,11 @@ App( requires=["js_app"], sources=["modules/js_i2c.c"], ) + +App( + appid="js_spi", + apptype=FlipperAppType.PLUGIN, + entry_point="js_spi_ep", + requires=["js_app"], + sources=["modules/js_spi.c"], +) diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 7a774d324..83f9e604c 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,12 +198,18 @@ static void js_require(struct mjs* mjs) { static void js_parse_int(struct mjs* mjs) { const char* str; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str)); + int32_t base = 10; - if(mjs_nargs(mjs) == 1) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str)); - } else { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&str), JS_ARG_INT32(&base)); + if(mjs_nargs(mjs) >= 2) { + mjs_val_t base_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(base_arg)) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number"); + mjs_return(mjs, MJS_UNDEFINED); + } + base = mjs_get_int(mjs, base_arg); } + int32_t num; if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { num = 0; diff --git a/applications/system/js_app/modules/js_gui/byte_input.c b/applications/system/js_app/modules/js_gui/byte_input.c index 5c8844d22..2d6dae475 100644 --- a/applications/system/js_app/modules/js_gui/byte_input.c +++ b/applications/system/js_app/modules/js_gui/byte_input.c @@ -8,6 +8,7 @@ typedef struct { uint8_t* buffer; size_t buffer_size; + size_t default_data_size; FuriString* header; FuriSemaphore* input_semaphore; JsEventLoopContract contract; @@ -38,7 +39,14 @@ static bool len_assign(struct mjs* mjs, ByteInput* input, JsViewPropValue value, JsByteKbContext* context) { UNUSED(mjs); UNUSED(input); - context->buffer_size = (size_t)(value.number); + size_t new_buffer_size = value.number; + if(new_buffer_size < context->default_data_size) { + // Avoid confusing parameters from user + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "length must be larger than defaultData length"); + return false; + } + context->buffer_size = new_buffer_size; context->buffer = realloc(context->buffer, context->buffer_size); //-V701 byte_input_set_result_callback( input, @@ -57,16 +65,24 @@ static bool default_data_assign( JsByteKbContext* context) { UNUSED(mjs); - mjs_val_t array_buf = value.array; + mjs_val_t array_buf = value.term; if(mjs_is_data_view(array_buf)) { array_buf = mjs_dataview_get_buf(mjs, array_buf); } - size_t default_data_len = 0; - char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &default_data_len); - memcpy( - context->buffer, - (uint8_t*)default_data, - MIN((size_t)context->buffer_size, default_data_len)); + char* default_data = mjs_array_buf_get_ptr(mjs, array_buf, &context->default_data_size); + if(context->buffer_size < context->default_data_size) { + // Ensure buffer is large enough for defaultData + context->buffer_size = context->default_data_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + } + memcpy(context->buffer, (uint8_t*)default_data, context->default_data_size); + if(context->buffer_size > context->default_data_size) { + // Reset previous data after defaultData + memset( + context->buffer + context->default_data_size, + 0x00, + context->buffer_size - context->default_data_size); + } byte_input_set_result_callback( input, diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index 4bd4ccc31..22d04855d 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -216,14 +216,14 @@ static bool expected_type = "array"; break; } - c_value = (JsViewPropValue){.array = value}; + c_value = (JsViewPropValue){.term = value}; } break; case JsViewPropTypeTypedArr: { if(!mjs_is_typed_array(value)) { expected_type = "typed_array"; break; } - c_value = (JsViewPropValue){.array = value}; + c_value = (JsViewPropValue){.term = value}; } break; case JsViewPropTypeBool: { if(!mjs_is_boolean(value)) { diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h index 67266b1fc..d400d0a33 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -16,8 +16,8 @@ typedef enum { typedef union { const char* string; int32_t number; - mjs_val_t array; bool boolean; + mjs_val_t term; } JsViewPropValue; /** diff --git a/applications/system/js_app/modules/js_gui/submenu.c b/applications/system/js_app/modules/js_gui/submenu.c index aecd413be..c142bcddb 100644 --- a/applications/system/js_app/modules/js_gui/submenu.c +++ b/applications/system/js_app/modules/js_gui/submenu.c @@ -33,9 +33,9 @@ static bool static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { UNUSED(mjs); submenu_reset(submenu); - size_t len = mjs_array_length(mjs, value.array); + size_t len = mjs_array_length(mjs, value.term); for(size_t i = 0; i < len; i++) { - mjs_val_t item = mjs_array_get(mjs, value.array, i); + mjs_val_t item = mjs_array_get(mjs, value.term, i); if(!mjs_is_string(item)) return false; submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context); } diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index d2bf4a8f9..e93bbfad0 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -8,6 +8,7 @@ typedef struct { char* buffer; size_t buffer_size; + size_t default_text_size; FuriString* header; bool default_text_clear; FuriSemaphore* input_semaphore; @@ -49,7 +50,14 @@ static bool max_len_assign( JsViewPropValue value, JsKbdContext* context) { UNUSED(mjs); - context->buffer_size = (size_t)(value.number + 1); + size_t new_buffer_size = value.number + 1; + if(new_buffer_size < context->default_text_size) { + // Avoid confusing parameters from user + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "maxLength must be larger than defaultText length"); + return false; + } + context->buffer_size = new_buffer_size; context->buffer = realloc(context->buffer, context->buffer_size); //-V701 text_input_set_result_callback( input, @@ -70,6 +78,13 @@ static bool default_text_assign( UNUSED(input); if(value.string) { + context->default_text_size = strlen(value.string) + 1; + if(context->buffer_size < context->default_text_size) { + // Ensure buffer is large enough for defaultData + context->buffer_size = context->default_text_size; + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + } + // Also trim excess previous data with strlcpy() strlcpy(context->buffer, value.string, context->buffer_size); text_input_set_result_callback( input, diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index 7d54cf9b9..cf66b6a44 100644 --- a/applications/system/js_app/modules/js_math.c +++ b/applications/system/js_app/modules/js_math.c @@ -308,7 +308,7 @@ void js_math_trunc(struct mjs* mjs) { static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); mjs_val_t math_obj = mjs_mk_object(mjs); - mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal)); + mjs_set(mjs, math_obj, "isEqual", ~0, MJS_MK_FN(js_math_is_equal)); mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs)); mjs_set(mjs, math_obj, "acos", ~0, MJS_MK_FN(js_math_acos)); mjs_set(mjs, math_obj, "acosh", ~0, MJS_MK_FN(js_math_acosh)); diff --git a/applications/system/js_app/modules/js_spi.c b/applications/system/js_app/modules/js_spi.c new file mode 100644 index 000000000..c0f4d684d --- /dev/null +++ b/applications/system/js_app/modules/js_spi.c @@ -0,0 +1,283 @@ +#include "../js_modules.h" +#include + +typedef struct { + bool acquired_bus; +} JsSpiInst; + +static JsSpiInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsSpiInst* spi = mjs_get_ptr(mjs, obj_inst); + furi_assert(spi); + return spi; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count_range(struct mjs* mjs, size_t min_count, size_t max_count) { + size_t num_args = mjs_nargs(mjs); + if(num_args < min_count || num_args > max_count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_spi_acquire(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 0, 0)) return; + JsSpiInst* spi = get_this_ctx(mjs); + if(!spi->acquired_bus) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + spi->acquired_bus = true; + } + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_spi_release(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 0, 0)) return; + JsSpiInst* spi = get_this_ctx(mjs); + if(spi->acquired_bus) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + spi->acquired_bus = false; + } + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool js_spi_is_acquired(struct mjs* mjs) { + JsSpiInst* spi = get_this_ctx(mjs); + return spi->acquired_bus; +} + +static void js_spi_write(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 0); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = furi_hal_spi_bus_tx(&furi_hal_spi_bus_handle_external, tx_buf, tx_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + if(tx_buf_was_allocated) free(tx_buf); + mjs_return(mjs, mjs_mk_boolean(mjs, result)); +} + +static void js_spi_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t rx_len_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + return; + } + + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = furi_hal_spi_bus_rx(&furi_hal_spi_bus_handle_external, rx_buf, rx_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + free(rx_buf); + mjs_return(mjs, ret); +} + +static void js_spi_write_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 0); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t data_len = 0; + if(mjs_is_array(tx_buf_arg)) { + data_len = mjs_array_length(mjs, tx_buf_arg); + if(data_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(data_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < data_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &data_len); + if(data_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint8_t* rx_buf = malloc(data_len); // RX and TX are same length for SPI writeRead. + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); + } + bool result = + furi_hal_spi_bus_trx(&furi_hal_spi_bus_handle_external, tx_buf, rx_buf, data_len, timeout); + if(!js_spi_is_acquired(mjs)) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, data_len); + } + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + mjs_return(mjs, ret); +} + +static void* js_spi_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + JsSpiInst* spi = (JsSpiInst*)malloc(sizeof(JsSpiInst)); + spi->acquired_bus = false; + mjs_val_t spi_obj = mjs_mk_object(mjs); + mjs_set(mjs, spi_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, spi)); + mjs_set(mjs, spi_obj, "acquire", ~0, MJS_MK_FN(js_spi_acquire)); + mjs_set(mjs, spi_obj, "release", ~0, MJS_MK_FN(js_spi_release)); + mjs_set(mjs, spi_obj, "write", ~0, MJS_MK_FN(js_spi_write)); + mjs_set(mjs, spi_obj, "read", ~0, MJS_MK_FN(js_spi_read)); + mjs_set(mjs, spi_obj, "writeRead", ~0, MJS_MK_FN(js_spi_write_read)); + *object = spi_obj; + + furi_hal_spi_bus_handle_init(&furi_hal_spi_bus_handle_external); + return (void*)spi; +} + +static void js_spi_destroy(void* inst) { + JsSpiInst* spi = (JsSpiInst*)inst; + if(spi->acquired_bus) { + furi_hal_spi_release(&furi_hal_spi_bus_handle_external); + } + free(spi); + furi_hal_spi_bus_handle_deinit(&furi_hal_spi_bus_handle_external); +} + +static const JsModuleDescriptor js_spi_desc = { + "spi", + js_spi_create, + js_spi_destroy, + NULL, +}; + +static const FlipperAppPluginDescriptor spi_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_spi_desc, +}; + +const FlipperAppPluginDescriptor* js_spi_ep(void) { + return &spi_plugin_descriptor; +} diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/types/badusb/index.d.ts index 4fbda5ef8..57c2662cd 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/types/badusb/index.d.ts @@ -40,7 +40,7 @@ export type KeyCode = MainKey | ModifierKey | number; * * @param settings USB device settings. Omit to select default parameters */ -export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath: string }): void; +export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath?: string }): void; /** * @brief Tells whether the virtual USB HID device has successfully connected diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts index d132f89f5..a55dae7d9 100644 --- a/applications/system/js_app/types/global.d.ts +++ b/applications/system/js_app/types/global.d.ts @@ -31,8 +31,10 @@ declare const __filename: string; /** * @brief Reads a JS value from a file * - * Reads a file at the specified path, interprets it as a JS value and returns - * the last value pushed on the stack. + * Reads a file at the specified path and runs it as JS, returning the last evaluated value. + * + * The result is cached and this filepath will not re-evaluated on future + * load() calls for this session. * * @param path The path to the file * @param scope An object to use as global scope while running this file diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/types/math/index.d.ts index 25abca4af..4924eea7e 100644 --- a/applications/system/js_app/types/math/index.d.ts +++ b/applications/system/js_app/types/math/index.d.ts @@ -1,3 +1,4 @@ +export function isEqual(a: number, b: number, tolerance: number): boolean; export function abs(n: number): number; export function acos(n: number): number; export function acosh(n: number): number; @@ -12,6 +13,7 @@ export function clz32(n: number): number; export function cos(n: number): number; export function exp(n: number): number; export function floor(n: number): number; +export function log(n: number): number; export function max(n: number, m: number): number; export function min(n: number, m: number): number; export function pow(n: number, m: number): number; @@ -21,4 +23,5 @@ export function sin(n: number): number; export function sqrt(n: number): number; export function trunc(n: number): number; declare const PI: number; +declare const E: number; declare const EPSILON: number; diff --git a/applications/system/js_app/types/spi/index.d.ts b/applications/system/js_app/types/spi/index.d.ts new file mode 100644 index 000000000..8d72bc29c --- /dev/null +++ b/applications/system/js_app/types/spi/index.d.ts @@ -0,0 +1,30 @@ +/** + * @brief Acquire SPI bus + */ +export declare function acquire(): void; + +/** + * @brief Release SPI bus + */ +export declare function release(): void; + +/** + * @brief Write data to SPI bus and return success status + * @param data The data to write + * @param timeout Timeout in milliseconds + */ +export declare function write(data: number[] | ArrayBuffer, timeout?: number): boolean; + +/** + * @brief Read data from SPI bus or return undefined on failure + * @param length How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function read(length: number, timeout?: number): ArrayBuffer | undefined; + +/** + * @brief Write and read data on SPI bus or return undefined on failure + * @param data The data to write, its length also indicates how many bytes will be read + * @param timeout Timeout in milliseconds + */ +export declare function writeRead(data: number[] | ArrayBuffer, timeout?: number): ArrayBuffer | undefined; From e630f44afde3f3fd57d68d1cf1421abe496aecb0 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:22:21 +0300 Subject: [PATCH 64/74] merge p2 --- .../examples/apps/Scripts/js_examples/gui.js | 8 +- .../apps/Scripts/js_examples/interactive.js | 5 +- .../examples/apps/Scripts/js_examples/spi.js | 88 +++++++++++++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 applications/system/js_app/examples/apps/Scripts/js_examples/spi.js diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js index 5faeb6d32..a1e023853 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/gui.js @@ -17,18 +17,16 @@ let views = { empty: emptyView.make(), keyboard: textInputView.makeWith({ header: "Enter your name", - defaultText: flipper.getName(), - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 32, + defaultText: flipper.getName(), + defaultTextClear: true, }), helloDialog: dialogView.make(), bytekb: byteInputView.makeWith({ header: "Look ma, I'm a header text!", - defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), - // Props for makeWith() are passed in reverse order, so length must be after defaultData length: 8, + defaultData: Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), }), longText: textBoxView.makeWith({ text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.", diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js index 34639cdac..40ca98c30 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/interactive.js @@ -24,11 +24,10 @@ let views = { }), textInput: textInput.makeWith({ header: "Type JavaScript Code:", - defaultText: "2+2", - defaultTextClear: true, - // Props for makeWith() are passed in reverse order, so maxLength must be after defaultText minLength: 0, maxLength: 256, + defaultText: "2+2", + defaultTextClear: true, }), loading: loading.make(), }; diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/spi.js b/applications/system/js_app/examples/apps/Scripts/js_examples/spi.js new file mode 100644 index 000000000..810637b3b --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/spi.js @@ -0,0 +1,88 @@ +// Connect a w25q32 SPI device to the Flipper Zero. +// D1=pin 2 (MOSI), SLK=pin 5 (SCK), GND=pin 8 (GND), D0=pin 3 (MISO), CS=pin 4 (CS), VCC=pin 9 (3V3) +let spi = require("spi"); + +// Display textbox so user can scroll to see all output. +let eventLoop = require("event_loop"); +let gui = require("gui"); +let text = "SPI demo\n"; +let textBox = require("gui/text_box").makeWith({ + focus: "end", + font: "text", + text: text, +}); + +function addText(add) { + text += add; + textBox.set("text", text); +} + +gui.viewDispatcher.switchTo(textBox); + +// writeRead returns a buffer the same length as the input buffer. +// We send 6 bytes of data, starting with 0x90, which is the command to read the manufacturer ID. +// Can also use Uint8Array([0x90, 0x00, ...]) as write parameter +// Optional timeout parameter in ms. We set to 100ms. +let data_buf = spi.writeRead([0x90, 0x0, 0x0, 0x0, 0x0, 0x0], 100); +let data = Uint8Array(data_buf); +if (data.length === 6) { + if (data[4] === 0xEF) { + addText("Found Winbond device\n"); + if (data[5] === 0x15) { + addText("Device ID: W25Q32\n"); + } else { + addText("Unknown device ID: " + data[5].toString(16) + "\n"); + } + } else if (data[4] === 0x0) { + addText("Be sure Winbond W25Q32 is connected to Flipper Zero SPI pins.\n"); + } else { + addText("Unknown device. Manufacturer ID: " + data[4].toString(16) + "\n"); + } +} + +addText("\nReading JEDEC ID\n"); + +// Acquire the SPI bus. Multiple calls will happen with Chip Select (CS) held low. +spi.acquire(); + +// Send command (0x9F) to read JEDEC ID. +// Can also use Uint8Array([0x9F]) as write parameter +// Note: you can pass an optional timeout parameter in milliseconds. +spi.write([0x9F]); + +// Request 3 bytes of data. +// Note: you can pass an optional timeout parameter in milliseconds. +data_buf = spi.read(3); + +// Release the SPI bus as soon as we are done with the set of SPI commands. +spi.release(); + +data = Uint8Array(data_buf); +addText("JEDEC MF ID: " + data[0].toString(16) + "\n"); +addText("JEDEC Memory Type: " + data[1].toString(16) + "\n"); +addText("JEDEC Capacity ID: " + data[2].toString(16) + "\n"); + +if (data[0] === 0xEF) { + addText("Found Winbond device\n"); +} +let capacity = data[1] << 8 | data[2]; +if (capacity === 0x4016) { + addText("Device: W25Q32\n"); +} else if (capacity === 0x4015) { + addText("Device: W25Q16\n"); +} else if (capacity === 0x4014) { + addText("Device: W25Q80\n"); +} else { + addText("Unknown device\n"); +} + +// Wait for user to close the app +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) { + eventLoop.stop(); +}, eventLoop); + +// This script has no interaction, only textbox, so event loop doesn't need to be running all the time +// We run it at the end to accept input for the back button press to quit +// But before that, user sees a textbox and pressing back has no effect +// This is fine because it allows simpler logic and the code above takes no time at all to run +eventLoop.run(); From 3d52187aa9b9c24bd7b6ee2a617deac5ead0493c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:45:02 +0300 Subject: [PATCH 65/74] ndef parser updates by luu176 and Willy-JL --- .../main/nfc/plugins/supported_cards/ndef.c | 1206 ++++++++++++----- 1 file changed, 882 insertions(+), 324 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index fe71915eb..820324421 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -1,41 +1,299 @@ // Parser for NDEF format data // Supports multiple NDEF messages and records in same tag -// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty +// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty, SmartPoster // Documentation and sources indicated where relevant // Made by @Willy-JL +// Mifare Ultralight support by @Willy-JL +// Mifare Classic support by @luu176 & @Willy-JL +// SLIX support by @Willy-JL + +// We use an arbitrary position system here, in order to support more protocols. +// Each protocol parses basic structure of the card, then starts ndef_parse_tlv() +// using an arbitrary position value that it can understand. When accessing data +// to parse NDEF content, ndef_get() will then map this arbitrary value to the +// card using state in Ndef struct, skipping blocks or sectors as needed. This +// way, NDEF parsing code does not need to know details of card layout. #include "nfc_supported_card_plugin.h" #include #include +#include +#include #include #define TAG "NDEF" +#define NDEF_PROTO_INVALID (-1) +#define NDEF_PROTO_RAW (0) // For parsing data fed manually +#define NDEF_PROTO_UL (1) +#define NDEF_PROTO_MFC (2) +#define NDEF_PROTO_SLIX (3) +#define NDEF_PROTO_TOTAL (4) + +#ifndef NDEF_PROTO +#error Must specify what protocol to use with NDEF_PROTO define! +#endif +#if NDEF_PROTO <= NDEF_PROTO_INVALID || NDEF_PROTO >= NDEF_PROTO_TOTAL +#error Invalid NDEF_PROTO specified! +#endif + +#define NDEF_TITLE(device, parsed_data) \ + furi_string_printf( \ + parsed_data, \ + "\e#NDEF Format Data\nCard type: %s\n", \ + nfc_device_get_name(device, NfcDeviceNameTypeFull)) + +// ---=== structures ===--- + +// TLV structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data +typedef enum FURI_PACKED { + NdefTlvPadding = 0x00, + NdefTlvLockControl = 0x01, + NdefTlvMemoryControl = 0x02, + NdefTlvNdefMessage = 0x03, + NdefTlvProprietary = 0xFD, + NdefTlvTerminator = 0xFE, +} NdefTlv; + +// Type Name Format values: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf +typedef enum FURI_PACKED { + NdefTnfEmpty = 0x00, + NdefTnfWellKnownType = 0x01, + NdefTnfMediaType = 0x02, + NdefTnfAbsoluteUri = 0x03, + NdefTnfExternalType = 0x04, + NdefTnfUnknown = 0x05, + NdefTnfUnchanged = 0x06, + NdefTnfReserved = 0x07, +} NdefTnf; + +// Flags and TNF structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#flags-and-tnf +typedef struct FURI_PACKED { + // Reversed due to endianness + NdefTnf type_name_format : 3; + bool id_length_present : 1; + bool short_record : 1; + bool chunk_flag : 1; + bool message_end : 1; + bool message_begin : 1; +} NdefFlagsTnf; +_Static_assert(sizeof(NdefFlagsTnf) == 1); + +// URI payload format: +// https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 +static const char* ndef_uri_prepends[] = { + [0x00] = NULL, // Allows detecting no prepend and checking schema for type + [0x01] = "http://www.", + [0x02] = "https://www.", + [0x03] = "http://", + [0x04] = "https://", + [0x05] = "tel:", + [0x06] = "mailto:", + [0x07] = "ftp://anonymous:anonymous@", + [0x08] = "ftp://ftp.", + [0x09] = "ftps://", + [0x0A] = "sftp://", + [0x0B] = "smb://", + [0x0C] = "nfs://", + [0x0D] = "ftp://", + [0x0E] = "dav://", + [0x0F] = "news:", + [0x10] = "telnet://", + [0x11] = "imap:", + [0x12] = "rtsp://", + [0x13] = "urn:", + [0x14] = "pop:", + [0x15] = "sip:", + [0x16] = "sips:", + [0x17] = "tftp:", + [0x18] = "btspp://", + [0x19] = "btl2cap://", + [0x1A] = "btgoep://", + [0x1B] = "tcpobex://", + [0x1C] = "irdaobex://", + [0x1D] = "file://", + [0x1E] = "urn:epc:id:", + [0x1F] = "urn:epc:tag:", + [0x20] = "urn:epc:pat:", + [0x21] = "urn:epc:raw:", + [0x22] = "urn:epc:", + [0x23] = "urn:nfc:", +}; + +// ---=== card memory layout abstraction ===--- + +// Shared context and state, read above +typedef struct { + FuriString* output; +#if NDEF_PROTO == NDEF_PROTO_RAW + struct { + const uint8_t* data; + size_t size; + } raw; +#elif NDEF_PROTO == NDEF_PROTO_UL + struct { + const uint8_t* start; + size_t size; + } ul; +#elif NDEF_PROTO == NDEF_PROTO_MFC + struct { + const MfClassicBlock* blocks; + size_t size; + } mfc; +#elif NDEF_PROTO == NDEF_PROTO_SLIX + struct { + const uint8_t* start; + size_t size; + } slix; +#endif +} Ndef; + +static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { +#if NDEF_PROTO == NDEF_PROTO_RAW + + // Using user-provided pointer, simply need to remap to it + if(pos + len > ndef->raw.size) return false; + memcpy(buf, ndef->raw.data + pos, len); + return true; + +#elif NDEF_PROTO == NDEF_PROTO_UL + + // Memory space is contiguous, simply need to remap to data pointer + if(pos + len > ndef->ul.size) return false; + memcpy(buf, ndef->ul.start + pos, len); + return true; + +#elif NDEF_PROTO == NDEF_PROTO_MFC + + // We need to skip sector trailers and MAD2, NDEF parsing just uses + // a position offset in data space, as if it were contiguous. + + // Start with a simple data space size check + if(pos + len > ndef->mfc.size) return false; + + // First 128 blocks are 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 93 (31*3) data blocks correspond to 128 real blocks. + // Last 128 blocks are 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 120 (8*15) data blocks correspond to 128 real blocks. + div_t small_sector_data_blocks = div(pos, MF_CLASSIC_BLOCK_SIZE); + size_t large_sector_data_blocks = 0; + if(small_sector_data_blocks.quot > 93) { + large_sector_data_blocks = small_sector_data_blocks.quot - 93; + small_sector_data_blocks.quot = 93; + } + + div_t small_sectors = div(small_sector_data_blocks.quot, 3); + size_t real_block = small_sectors.quot * 4 + small_sectors.rem; + if(small_sectors.quot >= 16) { + real_block += 4; // Skip MAD2 + } + if(large_sector_data_blocks) { + div_t large_sectors = div(large_sector_data_blocks, 15); + real_block += large_sectors.quot * 16 + large_sectors.rem; + } + + const uint8_t* cur = &ndef->mfc.blocks[real_block].data[small_sector_data_blocks.rem]; + while(len) { + size_t sector_trailer = mf_classic_get_sector_trailer_num_by_block(real_block); + const uint8_t* end = &ndef->mfc.blocks[sector_trailer].data[0]; + + size_t chunk_len = MIN((size_t)(end - cur), len); + memcpy(buf, cur, chunk_len); + len -= chunk_len; + + if(len) { + real_block = sector_trailer + 1; + if(real_block == 64) { + real_block += 4; // Skip MAD2 + } + cur = &ndef->mfc.blocks[real_block].data[0]; + } + } + + return true; + +#elif NDEF_PROTO == NDEF_PROTO_SLIX + + // Memory space is contiguous, simply need to remap to data pointer + if(pos + len > ndef->slix.size) return false; + memcpy(buf, ndef->slix.start + pos, len); + return true; + +#else + + UNUSED(ndef); + UNUSED(pos); + UNUSED(len); + UNUSED(buf); + return false; + +#endif +} + +// ---=== output helpers ===--- + +static inline bool is_printable(char c) { + return (c >= ' ' && c <= '~') || c == '\r' || c == '\n'; +} + static bool is_text(const uint8_t* buf, size_t len) { for(size_t i = 0; i < len; i++) { - const char c = buf[i]; - if((c < ' ' || c > '~') && c != '\r' && c != '\n') { - return false; - } + if(!is_printable(buf[i])) return false; } return true; } -static void - print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) { - if(prefix) furi_string_cat_printf(str, "%s: ", prefix); - if(!force_hex && is_text(buf, len)) { - furi_string_cat_printf(str, "%.*s", len, buf); - } else { - for(uint8_t i = 0; i < len; i++) { - furi_string_cat_printf(str, "%02X ", buf[i]); +static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + // We don't have direct access to memory chunks due to different card layouts + // Making a temporary buffer is wasteful of RAM and we can't afford this + // So while iterating like this is inefficient, it saves RAM and works between multiple card types + if(!force_hex) { + // If we find a non-printable character along the way, reset string to prev state and re-do as hex + size_t string_prev = furi_string_size(ndef->output); + for(size_t i = 0; i < len; i++) { + char c; + if(!ndef_get(ndef, pos + i, 1, &c)) return false; + if(!is_printable(c)) { + furi_string_left(ndef->output, string_prev); + force_hex = true; + break; + } + furi_string_push_back(ndef->output, c); } } - furi_string_cat(str, "\n"); + if(force_hex) { + for(size_t i = 0; i < len; i++) { + uint8_t b; + if(!ndef_get(ndef, pos + i, 1, &b)) return false; + furi_string_cat_printf(ndef->output, "%02X ", b); + } + } + furi_string_cat(ndef->output, "\n"); + return true; } +static void + ndef_print(Ndef* ndef, const char* prefix, const void* buf, size_t len, bool force_hex) { + if(prefix) furi_string_cat_printf(ndef->output, "%s: ", prefix); + if(!force_hex && is_text(buf, len)) { + furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); + } else { + for(size_t i = 0; i < len; i++) { + furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); + } + } + furi_string_cat(ndef->output, "\n"); +} + +// ---=== payload parsing ===--- + static inline uint8_t hex_to_int(char c) { if(c >= '0' && c <= '9') return c - '0'; if(c >= 'A' && c <= 'F') return c - 'A' + 10; @@ -43,136 +301,164 @@ static inline uint8_t hex_to_int(char c) { return 0; } -static char decode_char(const char* str) { - return (hex_to_int(str[1]) << 4) | hex_to_int(str[2]); +static char url_decode_char(const char* str) { + return (hex_to_int(str[0]) << 4) | hex_to_int(str[1]); } -static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763 - const char* prepends[] = { - [0x00] = "", - [0x01] = "http://www.", - [0x02] = "https://www.", - [0x03] = "http://", - [0x04] = "https://", - [0x05] = "tel:", - [0x06] = "mailto:", - [0x07] = "ftp://anonymous:anonymous@", - [0x08] = "ftp://ftp.", - [0x09] = "ftps://", - [0x0A] = "sftp://", - [0x0B] = "smb://", - [0x0C] = "nfs://", - [0x0D] = "ftp://", - [0x0E] = "dav://", - [0x0F] = "news:", - [0x10] = "telnet://", - [0x11] = "imap:", - [0x12] = "rtsp://", - [0x13] = "urn:", - [0x14] = "pop:", - [0x15] = "sip:", - [0x16] = "sips:", - [0x17] = "tftp:", - [0x18] = "btspp://", - [0x19] = "btl2cap://", - [0x1A] = "btgoep://", - [0x1B] = "tcpobex://", - [0x1C] = "irdaobex://", - [0x1D] = "file://", - [0x1E] = "urn:epc:id:", - [0x1F] = "urn:epc:tag:", - [0x20] = "urn:epc:pat:", - [0x21] = "urn:epc:raw:", - [0x22] = "urn:epc:", - [0x23] = "urn:nfc:", - }; - const char* prepend = ""; - uint8_t prepend_type = payload[0]; - if(prepend_type < COUNT_OF(prepends)) { - prepend = prepends[prepend_type]; +static bool ndef_parse_uri(Ndef* ndef, size_t pos, size_t len) { + const char* type = "URI"; + + // Parse URI prepend type + const char* prepend = NULL; + uint8_t prepend_type; + if(!ndef_get(ndef, pos++, 1, &prepend_type)) return false; + len--; + if(prepend_type < COUNT_OF(ndef_uri_prepends)) { + prepend = ndef_uri_prepends[prepend_type]; + } + if(prepend) { + if(strncmp(prepend, "http", 4) == 0) { + type = "URL"; + } else if(strncmp(prepend, "tel:", 4) == 0) { + type = "Phone"; + prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output + } else if(strncmp(prepend, "mailto:", 7) == 0) { + type = "Mail"; + prepend = ""; // Not NULL to avoid schema check below, only want to hide it from output + } } - size_t prepend_len = strlen(prepend); - size_t uri_len = prepend_len + (payload_len - 1); - char* const uri_buf = malloc(uri_len); // const to keep the original pointer to free later - memcpy(uri_buf, prepend, prepend_len); - memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1); - char* uri = uri_buf; // cursor we can iterate and shift freely + // Parse and optionally skip schema, if no prepend was specified + if(!prepend) { + char schema[7] = {0}; // Longest schema we check is 7 char long without terminator + if(!ndef_get(ndef, pos, MIN(sizeof(schema), len), schema)) return false; + if(strncmp(schema, "http", 4) == 0) { + type = "URL"; + } else if(strncmp(schema, "tel:", 4) == 0) { + type = "Phone"; + pos += 4; + len -= 4; + } else if(strncmp(schema, "mailto:", 7) == 0) { + type = "Mail"; + pos += 7; + len -= 7; + } + } - // Encoded chars take 3 bytes (%AB), decoded chars take 1 byte - // We can decode by iterating and overwriting the same buffer - size_t decoded_len = 0; - for(size_t encoded_idx = 0; encoded_idx < uri_len; encoded_idx++) { - if(uri[encoded_idx] == '%' && encoded_idx + 2 < uri_len) { - char hi = toupper(uri[encoded_idx + 1]); - char lo = toupper(uri[encoded_idx + 2]); - if(((hi >= 'A' && hi <= 'F') || (hi >= '0' && hi <= '9')) && - ((lo >= 'A' && lo <= 'F') || (lo >= '0' && lo <= '9'))) { - uri[decoded_len++] = decode_char(&uri[encoded_idx]); - encoded_idx += 2; - continue; + // Print static data as-is + furi_string_cat_printf(ndef->output, "%s\n", type); + if(prepend) { + furi_string_cat(ndef->output, prepend); + } + + // Print URI one char at a time and perform URL decode + while(len) { + char c; + if(!ndef_get(ndef, pos++, 1, &c)) return false; + len--; + if(c != '%' || len < 2) { // Not encoded, or not enough remaining text for encoded char + furi_string_push_back(ndef->output, c); + continue; + } + char enc[2]; + if(!ndef_get(ndef, pos, 2, enc)) return false; + enc[0] = toupper(enc[0]); + enc[1] = toupper(enc[1]); + // Only consume and print these 2 characters if they're valid URL encoded + // Otherwise they're processed in next iterations and we output the % char + if(((enc[0] >= 'A' && enc[0] <= 'F') || (enc[0] >= '0' && enc[0] <= '9')) && + ((enc[1] >= 'A' && enc[1] <= 'F') || (enc[1] >= '0' && enc[1] <= '9'))) { + pos += 2; + len -= 2; + c = url_decode_char(enc); + } + furi_string_push_back(ndef->output, c); + } + + return true; +} + +static bool ndef_parse_text(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "Text\n"); + if(!ndef_dump(ndef, NULL, pos + 3, len - 3, false)) return false; + return true; +} + +static bool ndef_parse_bt(Ndef* ndef, size_t pos, size_t len) { + furi_string_cat(ndef->output, "BT MAC\n"); + if(len != 8) return false; + if(!ndef_dump(ndef, NULL, pos + 2, len - 2, true)) return false; + return true; +} + +static bool ndef_parse_vcard(Ndef* ndef, size_t pos, size_t len) { + // We hide redundant tags the user is probably not interested in. + // Would be easier with FuriString checks for start_with() and end_with() + // but to do that would waste lots of RAM on a cloned string buffer + // so instead we just look for these markers at start/end and shift + // pos and len then use ndef_dump() to output one char at a time. + // Results in minimal stack and no heap usage at all. + static const char* const begin_tag = "BEGIN:VCARD"; + static const uint8_t begin_len = strlen(begin_tag); + static const char* const version_tag = "VERSION:"; + static const uint8_t version_len = strlen(version_tag); + static const char* const end_tag = "END:VCARD"; + static const uint8_t end_len = strlen(end_tag); + char tmp[13] = {0}; // Enough for BEGIN:VCARD\r\n + uint8_t skip = 0; + + // Skip BEGIN tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(begin_tag, tmp, begin_len) == 0) { + skip = begin_len; + if(tmp[skip] == '\r') skip++; + if(tmp[skip] == '\n') skip++; + pos += skip; + len -= skip; + } + } + + // Skip VERSION tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos, sizeof(tmp), tmp)) return false; + if(strncmp(version_tag, tmp, version_len) == 0) { + skip = version_len; + while(skip < len) { + if(!ndef_get(ndef, pos + skip, 1, &tmp[0])) return false; + skip++; + if(tmp[0] == '\n') break; + } + pos += skip; + len -= skip; + } + } + + // Skip END tag + if(len >= sizeof(tmp)) { + if(!ndef_get(ndef, pos + len - sizeof(tmp), sizeof(tmp), tmp)) return false; + // Read more than length of END tag and check multiple offsets, might have some padding after + // Worst case: there is END:VCARD\r\n\r\n which is same length as tmp buffer (13) + // Not sure if this is in spec but might aswell check + static const uint8_t offsets = sizeof(tmp) - end_len + 1; + for(uint8_t offset = 0; offset < offsets; offset++) { + if(strncmp(end_tag, tmp + offset, end_len) == 0) { + skip = sizeof(tmp) - offset; + len -= skip; + break; } } - uri[decoded_len++] = uri[encoded_idx]; } - const char* type = "URI"; - if(strncmp(uri, "http", 4) == 0) { - type = "URL"; - } else if(strncmp(uri, "tel:", 4) == 0) { - type = "Phone"; - uri += 4; - decoded_len -= 4; - } else if(strncmp(uri, "mailto:", 7) == 0) { - type = "Mail"; - uri += 7; - decoded_len -= 7; - } + furi_string_cat(ndef->output, "Contact\n"); + ndef_dump(ndef, NULL, pos, len, false); - furi_string_cat_printf(str, "%s\n", type); - print_data(str, NULL, (uint8_t*)uri, decoded_len, false); - - free(uri_buf); + return true; } -static void parse_ndef_text(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - furi_string_cat(str, "Text\n"); - print_data(str, NULL, payload + 3, payload_len - 3, false); -} - -static void parse_ndef_bt(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - furi_string_cat(str, "BT MAC\n"); - print_data(str, NULL, payload + 2, payload_len - 2, true); -} - -static void parse_ndef_vcard(FuriString* str, const uint8_t* payload, uint32_t payload_len) { - char* tmp = malloc(payload_len + 1); - memcpy(tmp, payload, payload_len); - tmp[payload_len] = '\0'; - FuriString* fmt = furi_string_alloc_set(tmp); - free(tmp); - - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "BEGIN:VCARD")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - if(furi_string_end_with(fmt, "END:VCARD")) { - furi_string_left(fmt, furi_string_search_rchar(fmt, '\n')); - } - furi_string_trim(fmt); - if(furi_string_start_with(fmt, "VERSION:")) { - furi_string_right(fmt, furi_string_search_char(fmt, '\n')); - furi_string_trim(fmt); - } - } - - furi_string_cat(str, "Contact\n"); - print_data(str, NULL, (uint8_t*)furi_string_get_cstr(fmt), furi_string_size(fmt), false); - furi_string_free(fmt); -} - -static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t payload_len) { -// https://android.googlesource.com/platform/packages/apps/Nfc/+/refs/heads/main/src/com/android/nfc/NfcWifiProtectedSetup.java +// Loosely based on Android WiFi NDEF implementation: +// https://android.googlesource.com/platform/packages/apps/Nfc/+/025560080737b43876c9d81feff3151f497947e8/src/com/android/nfc/NfcWifiProtectedSetup.java +static bool ndef_parse_wifi(Ndef* ndef, size_t pos, size_t len) { #define CREDENTIAL_FIELD_ID (0x100E) #define SSID_FIELD_ID (0x1045) #define NETWORK_KEY_FIELD_ID (0x1027) @@ -186,44 +472,57 @@ static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t pa #define AUTH_TYPE_WPA_AND_WPA2_PSK (0x0022) #define MAX_NETWORK_KEY_SIZE_BYTES (64) - size_t i = 0; - while(i < payload_len) { - uint16_t field_id = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - uint16_t field_len = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + furi_string_cat(ndef->output, "WiFi\n"); + size_t end = pos + len; + + uint8_t tmp_buf[2]; + while(pos < end) { + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t field_id = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t field_len = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + FURI_LOG_D(TAG, "wifi field: %04X len: %d", field_id, field_len); + + if(pos + field_len > end) { + return false; + } if(field_id == CREDENTIAL_FIELD_ID) { - furi_string_cat(str, "WiFi\n"); - size_t start_position = i; - while(i < start_position + field_len) { - uint16_t cfg_id = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; - uint16_t cfg_len = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + size_t field_end = pos + field_len; + while(pos < field_end) { + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t cfg_id = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t cfg_len = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; + FURI_LOG_D(TAG, "wifi cfg: %04X len: %d", cfg_id, cfg_len); - if(i + cfg_len > start_position + field_len) { - return; + if(pos + cfg_len > field_end) { + return false; } switch(cfg_id) { case SSID_FIELD_ID: - print_data(str, "SSID", payload + i, cfg_len, false); - i += cfg_len; + if(!ndef_dump(ndef, "SSID", pos, cfg_len, false)) return false; + pos += cfg_len; break; case NETWORK_KEY_FIELD_ID: if(cfg_len > MAX_NETWORK_KEY_SIZE_BYTES) { - return; + return false; } - print_data(str, "PWD", payload + i, cfg_len, false); - i += cfg_len; + if(!ndef_dump(ndef, "PWD", pos, cfg_len, false)) return false; + pos += cfg_len; break; case AUTH_TYPE_FIELD_ID: if(cfg_len != AUTH_TYPE_EXPECTED_SIZE) { - return; + return false; } - short auth_type = bit_lib_bytes_to_num_be(payload + i, 2); - i += 2; + if(!ndef_get(ndef, pos, 2, &tmp_buf)) return false; + uint16_t auth_type = bit_lib_bytes_to_num_be(tmp_buf, 2); + pos += 2; const char* auth; switch(auth_type) { case AUTH_TYPE_OPEN: @@ -248,250 +547,507 @@ static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t pa auth = "Unknown"; break; } - print_data(str, "AUTH", (uint8_t*)auth, strlen(auth), false); + ndef_print(ndef, "AUTH", auth, strlen(auth), false); break; default: - i += cfg_len; + pos += cfg_len; break; } } - return; + return true; } - i += field_len; + pos += field_len; } + + furi_string_cat(ndef->output, "No data parsed\n"); + return true; } -static void parse_ndef_payload( - FuriString* str, - uint8_t tnf, +// ---=== ndef layout parsing ===--- + +bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, const char* type, - uint8_t type_len, - const uint8_t* payload, - uint32_t payload_len) { - if(!payload_len) { - furi_string_cat(str, "Empty\n"); - return; + uint8_t type_len); +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster); +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed); + +bool ndef_parse_record( + Ndef* ndef, + size_t pos, + size_t len, + NdefTnf tnf, + const char* type, + uint8_t type_len) { + FURI_LOG_D(TAG, "payload type: %.*s len: %d", type_len, type, len); + if(!len) { + furi_string_cat(ndef->output, "Empty\n"); + return true; } + switch(tnf) { - case 0x01: // NFC Forum well-known type [NFC RTD] - if(strncmp("U", type, type_len) == 0) { - parse_ndef_uri(str, payload, payload_len); + case NdefTnfWellKnownType: + if(strncmp("Sp", type, type_len) == 0) { + furi_string_cat(ndef->output, "SmartPoster\nContained records below\n\n"); + return ndef_parse_message(ndef, pos, len, 0, true); + } else if(strncmp("U", type, type_len) == 0) { + return ndef_parse_uri(ndef, pos, len); } else if(strncmp("T", type, type_len) == 0) { - parse_ndef_text(str, payload, payload_len); - } else { - print_data(str, "Well-known type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); + return ndef_parse_text(ndef, pos, len); } - break; - case 0x02: // Media-type [RFC 2046] - if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { - parse_ndef_bt(str, payload, payload_len); - } else if(strncmp("text/vcard", type, type_len) == 0) { - parse_ndef_vcard(str, payload, payload_len); - } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { - parse_ndef_wifi(str, payload, payload_len); - } else { - print_data(str, "Media Type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); - } - break; - case 0x00: // Empty - case 0x03: // Absolute URI [RFC 3986] - case 0x04: // NFC Forum external type [NFC RTD] - case 0x05: // Unknown - case 0x06: // Unchanged - case 0x07: // Reserved - default: // Unknown // Dump data without parsing - print_data(str, "Type name format", &tnf, 1, true); - print_data(str, "Type", (uint8_t*)type, type_len, false); - print_data(str, "Payload", payload, payload_len, false); - break; + furi_string_cat(ndef->output, "Unknown\n"); + ndef_print(ndef, "Well-known Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfMediaType: + if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) { + return ndef_parse_bt(ndef, pos, len); + } else if(strncmp("text/vcard", type, type_len) == 0) { + return ndef_parse_vcard(ndef, pos, len); + } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) { + return ndef_parse_wifi(ndef, pos, len); + } + // Dump data without parsing + furi_string_cat(ndef->output, "Unknown\n"); + ndef_print(ndef, "Media Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; + + case NdefTnfEmpty: + case NdefTnfAbsoluteUri: + case NdefTnfExternalType: + case NdefTnfUnknown: + case NdefTnfUnchanged: + case NdefTnfReserved: + default: + // Dump data without parsing + furi_string_cat(ndef->output, "Unsupported\n"); + ndef_print(ndef, "Type name format", &tnf, 1, true); + ndef_print(ndef, "Type", type, type_len, false); + if(!ndef_dump(ndef, "Payload", pos, len, false)) return false; + return true; } } -static const uint8_t* parse_ndef_message( - FuriString* str, - size_t message_num, - const uint8_t* cur, - const uint8_t* message_end) { - // NDEF message and record documentation: - // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/protocols/nfc/index.html#ndef-message-and-record-format +// NDEF message structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/protocols/nfc/index.html#ndef_message_and_record_format +bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, bool smart_poster) { + size_t end = pos + len; + size_t record_num = 0; bool last_record = false; - while(cur < message_end) { + while(pos < end) { // Flags and TNF - uint8_t flags_tnf = *cur++; + NdefFlagsTnf flags_tnf; + if(!ndef_get(ndef, pos++, 1, &flags_tnf)) return false; + FURI_LOG_D(TAG, "flags_tnf: %02X", *(uint8_t*)&flags_tnf); + FURI_LOG_D(TAG, "flags_tnf.message_begin: %d", flags_tnf.message_begin); + FURI_LOG_D(TAG, "flags_tnf.message_end: %d", flags_tnf.message_end); + FURI_LOG_D(TAG, "flags_tnf.chunk_flag: %d", flags_tnf.chunk_flag); + FURI_LOG_D(TAG, "flags_tnf.short_record: %d", flags_tnf.short_record); + FURI_LOG_D(TAG, "flags_tnf.id_length_present: %d", flags_tnf.id_length_present); + FURI_LOG_D(TAG, "flags_tnf.type_name_format: %02X", flags_tnf.type_name_format); // Message Begin should only be set on first record - if(record_num++ && flags_tnf & (1 << 7)) break; + if(record_num++ && flags_tnf.message_begin) return false; // Message End should only be set on last record - if(last_record) break; - if(flags_tnf & (1 << 6)) last_record = true; - // Chunked Flag not supported - if(flags_tnf & (1 << 5)) break; - // Payload Length field of 1 vs 4 bytes - bool short_record = flags_tnf & (1 << 4); - // Is payload ID length and value present - bool id_present = flags_tnf & (1 << 3); - // Type Name Format 3 bit value - uint8_t tnf = flags_tnf & 0b00000111; + if(last_record) return false; + if(flags_tnf.message_end) last_record = true; + // Chunk Flag not supported + if(flags_tnf.chunk_flag) return false; // Type Length - uint8_t type_len = *cur++; + uint8_t type_len; + if(!ndef_get(ndef, pos++, 1, &type_len)) return false; - // Payload Length + // Payload Length field of 1 or 4 bytes uint32_t payload_len; - if(short_record) { - payload_len = *cur++; + if(flags_tnf.short_record) { + uint8_t payload_len_short; + if(!ndef_get(ndef, pos++, 1, &payload_len_short)) return false; + payload_len = payload_len_short; } else { - payload_len = bit_lib_bytes_to_num_be(cur, 4); - cur += 4; + if(!ndef_get(ndef, pos, sizeof(payload_len), &payload_len)) return false; + payload_len = bit_lib_bytes_to_num_be((void*)&payload_len, sizeof(payload_len)); + pos += sizeof(payload_len); } // ID Length uint8_t id_len = 0; - if(id_present) { - id_len = *cur++; + if(flags_tnf.id_length_present) { + if(!ndef_get(ndef, pos++, 1, &id_len)) return false; } // Payload Type - char* type = NULL; + char type_buf[32]; // Longest type supported in ndef_parse_record() is 32 chars excl terminator + char* type = type_buf; + bool type_was_allocated = false; if(type_len) { - type = malloc(type_len); - memcpy(type, cur, type_len); - cur += type_len; + if(type_len > sizeof(type_buf)) { + type = malloc(type_len); + type_was_allocated = true; + } + if(!ndef_get(ndef, pos, type_len, type)) { + if(type_was_allocated) free(type); + return false; + } + pos += type_len; } // Payload ID - cur += id_len; + pos += id_len; - furi_string_cat_printf(str, "\e*> M:%d R:%d - ", message_num, record_num); - parse_ndef_payload(str, tnf, type, type_len, cur, payload_len); - cur += payload_len; + if(smart_poster) { + furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", message_num, record_num); + } + if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { + if(type_was_allocated) free(type); + return false; + } + pos += payload_len; - free(type); - furi_string_trim(str, "\n"); - furi_string_cat(str, "\n\n"); + if(type_was_allocated) free(type); + furi_string_trim(ndef->output, "\n"); + furi_string_cat(ndef->output, "\n\n"); } - return cur; + + if(record_num == 0) { + if(smart_poster) { + furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); + } else { + furi_string_cat_printf(ndef->output, "\e*> M%d: Empty\n\n", message_num); + } + } + + return pos == end && (last_record || record_num == 0); } -static bool ndef_parse(const NfcDevice* device, FuriString* parsed_data) { +// TLV structure: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#data +size_t ndef_parse_tlv(Ndef* ndef, size_t pos, size_t len, size_t already_parsed) { + size_t end = pos + len; + size_t message_num = 0; + + while(pos < end) { + NdefTlv tlv; + if(!ndef_get(ndef, pos++, 1, &tlv)) return 0; + FURI_LOG_D(TAG, "tlv: %02X", tlv); + + switch(tlv) { + default: + // Unknown, bail to avoid problems + return 0; + + case NdefTlvPadding: + // Has no length, skip to next byte + break; + + case NdefTlvTerminator: + // NDEF message finished, return whether we parsed something + return message_num; + + case NdefTlvLockControl: + case NdefTlvMemoryControl: + case NdefTlvProprietary: + case NdefTlvNdefMessage: { + // Calculate length + uint16_t len; + uint8_t len_type; + if(!ndef_get(ndef, pos++, 1, &len_type)) return 0; + if(len_type < 0xFF) { // 1 byte length + len = len_type; + } else { // 3 byte length (0xFF marker + 2 byte integer) + if(!ndef_get(ndef, pos, sizeof(len), &len)) return 0; + len = bit_lib_bytes_to_num_be((void*)&len, sizeof(len)); + pos += sizeof(len); + } + + if(tlv != NdefTlvNdefMessage) { + // We don't care, skip this TLV block to next one + pos += len; + break; + } + + if(!ndef_parse_message(ndef, pos, len, ++message_num + already_parsed, false)) + return 0; + pos += len; + + break; + } + } + } + + // Reached data end with no TLV terminator, + // but also no errors, treat this as a success + return message_num; +} + +#if NDEF_PROTO != NDEF_PROTO_RAW + +// ---=== protocol entry-points ===--- + +#if NDEF_PROTO == NDEF_PROTO_UL + +// MF UL memory layout: +// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_2_tag.html#memory_layout +static bool ndef_ul_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); furi_assert(parsed_data); const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight); - bool parsed = false; + // Check card type can contain NDEF + if(data->type != MfUltralightTypeNTAG203 && data->type != MfUltralightTypeNTAG213 && + data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && + data->type != MfUltralightTypeNTAGI2C1K && data->type != MfUltralightTypeNTAGI2C2K && + data->type != MfUltralightTypeNTAGI2CPlus1K && + data->type != MfUltralightTypeNTAGI2CPlus2K) { + return false; + } - do { - // Memory layout documentation: - // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrfxlib/nfc/doc/type_2_tag.html#id2 + // Check Capability Container (CC) values + struct { + uint8_t nfc_magic_number; + uint8_t document_version_number; + uint8_t data_area_size; // Usable byte size / 8, only includes user memory + uint8_t read_write_access; + }* cc = (void*)&data->page[3].data[0]; + if(cc->nfc_magic_number != 0xE1) return false; + if(cc->document_version_number != 0x10) return false; - // Check card type can contain NDEF - if(data->type != MfUltralightTypeNTAG203 && data->type != MfUltralightTypeNTAG213 && - data->type != MfUltralightTypeNTAG215 && data->type != MfUltralightTypeNTAG216 && - data->type != MfUltralightTypeNTAGI2C1K && data->type != MfUltralightTypeNTAGI2C2K) { - break; - } + // Calculate usable data area + const uint8_t* start = &data->page[4].data[0]; + const uint8_t* end = start + (cc->data_area_size * 8); + size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; + end = MIN(end, &data->page[0].data[0] + max_size); + size_t data_start = 0; + size_t data_size = end - start; - // Double check Capability Container (CC) and find data area bounds - struct { - uint8_t nfc_magic_number; - uint8_t document_version_number; - uint8_t data_area_size; - uint8_t read_write_access; - }* cc = (void*)&data->page[3].data[0]; - if(cc->nfc_magic_number != 0xE1) break; - if(cc->document_version_number != 0x10) break; - const uint8_t* cur = &data->page[4].data[0]; - const uint8_t* end = cur + (cc->data_area_size * 2 * MF_ULTRALIGHT_PAGE_SIZE); - size_t max_size = mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE; - end = MIN(end, &data->page[0].data[0] + max_size); - size_t message_num = 0; + NDEF_TITLE(device, parsed_data); - // Parse as TLV (see docs above) - while(cur < end) { - switch(*cur++) { - case 0x03: { // NDEF message - if(cur >= end) break; - uint16_t len; - if(*cur < 0xFF) { // 1 byte length - len = *cur++; - } else { // 3 byte length (0xFF marker + 2 byte integer) - if(cur + 2 >= end) { - cur = end; - break; - } - len = bit_lib_bytes_to_num_be(++cur, 2); - cur += 2; - } - if(cur + len >= end) { - cur = end; - break; - } + Ndef ndef = { + .output = parsed_data, + .ul = + { + .start = start, + .size = data_size, + }, + }; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); - if(message_num++ == 0) { - furi_string_printf( - parsed_data, - "\e#NDEF Format Data\nCard type: %s\n", - mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull)); - } + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } - const uint8_t* message_end = cur + len; - cur = parse_ndef_message(parsed_data, message_num, cur, message_end); - if(cur != message_end) cur = end; + return parsed > 0; +} - break; - } +#elif NDEF_PROTO == NDEF_PROTO_MFC - case 0xFE: // TLV end - cur = end; - if(message_num != 0) parsed = true; - break; +// MFC MAD datasheet: +// https://www.nxp.com/docs/en/application-note/AN10787.pdf +#define AID_SIZE (2) +static const uint64_t mad_key = 0xA0A1A2A3A4A5; - case 0x00: // Padding, has no length, skip - break; +// NDEF on MFC breakdown: +// https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#storing-ndef-messages-in-mifare-sectors-607778 +static const uint8_t ndef_aid[AID_SIZE] = {0x03, 0xE1}; - case 0x01: // Lock control - case 0x02: // Memory control - case 0xFD: // Proprietary - // We don't care, skip this TLV block - if(cur >= end) break; - if(*cur < 0xFF) { // 1 byte length - cur += *cur + 1; // Shift by TLV length - } else { // 3 byte length (0xFF marker + 2 byte integer) - if(cur + 2 >= end) { - cur = end; - break; - } - cur += bit_lib_bytes_to_num_be(cur + 1, 2) + 3; // Shift by TLV length - } - break; +static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); - default: // Unknown, bail to avoid problems - cur = end; - break; + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + // Check card type can contain NDEF + if(data->type != MfClassicType1k && data->type != MfClassicType4k && + data->type != MfClassicTypeMini) { + return false; + } + + // Check MADs for what sectors contain NDEF data AIDs + bool sectors_with_ndef[MF_CLASSIC_TOTAL_SECTORS_MAX] = {0}; + const size_t sector_count = mf_classic_get_total_sectors_num(data->type); + const struct { + size_t block; + uint8_t aid_count; + } mads[2] = { + {1, 15}, + {64, 23}, + }; + for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { + const size_t block = mads[mad].block; + const size_t sector = mf_classic_get_sector_by_block(block); + if(sector_count <= sector) break; // Skip this MAD if not present + // Check MAD key + const MfClassicSectorTrailer* sector_trailer = + mf_classic_get_sector_trailer_by_sector(data, sector); + const uint64_t sector_key_a = bit_lib_bytes_to_num_be( + sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); + if(sector_key_a != mad_key) return false; + // Find NDEF AIDs + for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { + const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; + if(memcmp(aid, ndef_aid, AID_SIZE) == 0) { + sectors_with_ndef[aid_index + 1] = true; } } + } + + NDEF_TITLE(device, parsed_data); + + // Calculate how large the data space is, so excluding sector trailers and MAD2. + // Makes sure we stay within this card's actual content when parsing. + // First 32 sectors: 3 data blocks, 1 sector trailer. + // Sector 16 contains MAD2 and we need to skip this. + // So the first 32 sectors correspond to 93 (31*3) data blocks. + // Last 8 sectors: 15 data blocks, 1 sector trailer. + // So the last 8 sectors correspond to 120 (8*15) data blocks. + size_t data_size; + if(sector_count > 32) { + data_size = 93 + (sector_count - 32) * 15; + } else { + data_size = sector_count * 3; + if(sector_count >= 16) { + data_size -= 3; // Skip MAD2 + } + } + data_size *= MF_CLASSIC_BLOCK_SIZE; + + Ndef ndef = { + .output = parsed_data, + .mfc = + { + .blocks = data->block, + .size = data_size, + }, + }; + size_t total_parsed = 0; + + for(size_t sector = 0; sector < sector_count; sector++) { + if(!sectors_with_ndef[sector]) continue; + FURI_LOG_D(TAG, "sector: %d", sector); + size_t string_prev = furi_string_size(parsed_data); + + // Convert real sector number to data block number + // to skip sector trailers and MAD2 + size_t data_block; + if(sector < 32) { + data_block = sector * 3; + if(sector >= 16) { + data_block -= 3; // Skip MAD2 + } + } else { + data_block = 93 + (sector - 32) * 15; + } + FURI_LOG_D(TAG, "data_block: %d", data_block); + size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed); if(parsed) { + total_parsed += parsed; furi_string_trim(parsed_data, "\n"); furi_string_cat(parsed_data, "\n"); } else { - furi_string_reset(parsed_data); + furi_string_left(parsed_data, string_prev); } - } while(false); + } - return parsed; + if(!total_parsed) { + furi_string_reset(parsed_data); + } + + return total_parsed > 0; } +#elif NDEF_PROTO == NDEF_PROTO_SLIX + +// SLIX NDEF memory layout: +// https://community.nxp.com/pwmxy87654/attachments/pwmxy87654/nfc/7583/1/EEOL_2011FEB16_EMS_RFD_AN_01.pdf +static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + furi_assert(parsed_data); + + const Iso15693_3Data* data = nfc_device_get_data(device, NfcProtocolIso15693_3); + const uint8_t block_size = iso15693_3_get_block_size(data); + const uint16_t block_count = iso15693_3_get_block_count(data); + const uint8_t* blocks = simple_array_cget_data(data->block_data); + + // TODO: Find some way to check for other iso15693 NDEF cards and + // split this to also support non-slix iso15693 NDEF tags + // Rest of the code works on iso15693 too, but uses SLIX layout assumptions + if(block_size != SLIX_BLOCK_SIZE) { + return false; + } + + // Check Capability Container (CC) values + struct { + uint8_t nfc_magic_number; + uint8_t read_write_access : 4; // Reversed due to endianness + uint8_t document_version_number : 4; + uint8_t data_area_size; // Total byte size / 8, includes block 0 + uint8_t mbread_ipread; + }* cc = (void*)&blocks[0 * block_size]; + if(cc->nfc_magic_number != 0xE1) return false; + if(cc->document_version_number != 0x4) return false; + + // Calculate usable data area + const uint8_t* start = &blocks[1 * block_size]; + const uint8_t* end = blocks + (cc->data_area_size * 8); + size_t max_size = block_count * block_size; + end = MIN(end, blocks + max_size); + size_t data_start = 0; + size_t data_size = end - start; + + NDEF_TITLE(device, parsed_data); + + Ndef ndef = { + .output = parsed_data, + .slix = + { + .start = start, + .size = data_size, + }, + }; + size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, 0); + + if(parsed) { + furi_string_trim(parsed_data, "\n"); + furi_string_cat(parsed_data, "\n"); + } else { + furi_string_reset(parsed_data); + } + + return parsed > 0; +} + +#endif + +// ---=== boilerplate ===--- + /* Actual implementation of app<>plugin interface */ static const NfcSupportedCardsPlugin ndef_plugin = { - .protocol = NfcProtocolMfUltralight, .verify = NULL, .read = NULL, - .parse = ndef_parse, +#if NDEF_PROTO == NDEF_PROTO_UL + .parse = ndef_ul_parse, + .protocol = NfcProtocolMfUltralight, +#elif NDEF_PROTO == NDEF_PROTO_MFC + .parse = ndef_mfc_parse, + .protocol = NfcProtocolMfClassic, +#elif NDEF_PROTO == NDEF_PROTO_SLIX + .parse = ndef_slix_parse, + .protocol = NfcProtocolSlix, +#endif }; /* Plugin descriptor to comply with basic plugin specification */ @@ -505,3 +1061,5 @@ static const FlipperAppPluginDescriptor ndef_plugin_descriptor = { const FlipperAppPluginDescriptor* ndef_plugin_ep(void) { return &ndef_plugin_descriptor; } + +#endif From e7b64c843c2408c3c8aac780ecb12c4727dfb16e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:50:07 +0300 Subject: [PATCH 66/74] upd manifest --- applications/main/nfc/application.fam | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index a78a35950..3eb24096b 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -246,8 +246,28 @@ App( ) App( - appid="ndef_parser", + appid="ndef_ul_parser", apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_UL")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) +App( + appid="ndef_mfc_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_MFC")], + entry_point="ndef_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/ndef.c"], +) + +App( + appid="ndef_slix_parser", + apptype=FlipperAppType.PLUGIN, + cdefines=[("NDEF_PROTO", "NDEF_PROTO_SLIX")], entry_point="ndef_plugin_ep", targets=["f7"], requires=["nfc"], From ed57ddb14be48be3134bbca57726cff247e1d6e1 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:28:33 +0300 Subject: [PATCH 67/74] NFC: Plaintain parser fix by mxcdoam https://github.com/flipperdevices/flipperzero-firmware/pull/3975/files --- .../nfc/plugins/supported_cards/plantain.c | 84 +++++++++++++++++-- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/plantain.c b/applications/main/nfc/plugins/supported_cards/plantain.c index c38140de2..9f2491691 100644 --- a/applications/main/nfc/plugins/supported_cards/plantain.c +++ b/applications/main/nfc/plugins/supported_cards/plantain.c @@ -201,8 +201,9 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) { static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { furi_assert(device); - + size_t uid_len = 0; const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); bool parsed = false; @@ -220,12 +221,30 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { if(key != cfg.keys[cfg.data_sector].a) break; furi_string_printf(parsed_data, "\e#Plantain card\n"); + + const uint8_t* temp_ptr = &uid[0]; + + // UID is read from last to first byte + uint8_t card_number_tmp[uid_len]; + + if(uid_len == 4) { + for(size_t i = 0; i < 4; i++) { + card_number_tmp[i] = temp_ptr[3 - i]; + } + } else if(uid_len == 7) { + for(size_t i = 0; i < 7; i++) { + card_number_tmp[i] = temp_ptr[6 - i]; + } + } else { + break; + } + //UID is converted to a card number uint64_t card_number = 0; - for(size_t i = 0; i < 7; i++) { - card_number = (card_number << 8) | data->block[0].data[6 - i]; + for(size_t i = 0; i < uid_len; i++) { + card_number = (card_number << 8) | card_number_tmp[i]; } - // Print card number with 4-digit groups + // Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards. furi_string_cat_printf(parsed_data, "Number: "); FuriString* card_number_s = furi_string_alloc(); furi_string_cat_printf(card_number_s, "%llu", card_number); @@ -237,6 +256,7 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { furi_string_push_back(tmp_s, ' '); } furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s)); + // this works for 2K Plantain if(data->type == MfClassicType1k) { //balance uint32_t balance = 0; @@ -290,20 +310,70 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) { last_payment_date.year, last_payment_date.hour, last_payment_date.minute); - //payment summ + //payment amount. This needs to be investigated more, currently it shows incorrect amount on some cards. uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8]; furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100); furi_string_free(card_number_s); furi_string_free(tmp_s); + //This is for 4K Plantains. } else if(data->type == MfClassicType4k) { + //balance + uint32_t balance = 0; + for(uint8_t i = 0; i < 4; i++) { + balance = (balance << 8) | data->block[16].data[3 - i]; + } + furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100); + //trips - uint8_t trips_metro = data->block[36].data[0]; - uint8_t trips_ground = data->block[36].data[1]; + uint8_t trips_metro = data->block[21].data[0]; + uint8_t trips_ground = data->block[21].data[1]; furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground); + //trip time + uint32_t last_trip_timestamp = 0; + for(uint8_t i = 0; i < 3; i++) { + last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i]; + } + DateTime last_trip = {0}; + from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010); + furi_string_cat_printf( + parsed_data, + "Trip start: %02d.%02d.%04d %02d:%02d\n", + last_trip.day, + last_trip.month, + last_trip.year, + last_trip.hour, + last_trip.minute); + //validator + uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4]; + furi_string_cat_printf(parsed_data, "Validator: %d\n", validator); + //tariff + uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6]; + furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100); //trips in metro furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro); //trips on ground furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground); + //last payment + uint32_t last_payment_timestamp = 0; + for(uint8_t i = 0; i < 3; i++) { + last_payment_timestamp = (last_payment_timestamp << 8) | + data->block[18].data[4 - i]; + } + DateTime last_payment_date = {0}; + from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010); + furi_string_cat_printf( + parsed_data, + "Last pay: %02d.%02d.%04d %02d:%02d\n", + last_payment_date.day, + last_payment_date.month, + last_payment_date.year, + last_payment_date.hour, + last_payment_date.minute); + //payment amount + uint16_t last_payment = (data->block[18].data[9] << 8) | data->block[18].data[8]; + furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment / 100); + furi_string_free(card_number_s); + furi_string_free(tmp_s); } parsed = true; } while(false); From bb922de569a2c09159a1365f2d1e875a9895d669 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:36:44 +0300 Subject: [PATCH 68/74] upd changelog --- CHANGELOG.md | 67 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4cd0014..4665f45a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,43 +1,45 @@ ## Main changes - SubGHz: - Frequency analyzer fixes and improvements: - - Enforce int module (like in OFW) usage due to lack of required hardware on external boards (PathIsolate (+rf switch for multiple paths)) and incorrect usage and/or understanding the purpose of frequency analyzer app by users, it should be used only to get frequency of the remote placed around 1-10cm around flipper's left corner - - Fix possible GSM mobile towers signal interference by limiting upper frequency to 920mhz max + - **Enforce int module** (like in OFW) usage due to lack of required hardware on external boards (PathIsolate (+rf switch for multiple paths)) and incorrect usage and/or understanding the purpose of frequency analyzer app by users, it should be used only to get frequency of the remote placed around 1-10cm around flipper's left corner + - **Fix possible GSM mobile towers signal interference** by limiting upper frequency to 920mhz max - Fix duplicated frequency lists and use user config for nearest frequency selector too - - Fix buttons logic, fix crash + - Fix buttons logic, **fix crash** - Protocol improvements: - - Keeloq: Monarch full support, with add manually option (thanks to anonymous contributor (TBA)) - - Princeton support for second button encoding type (8bit) + - **Keeloq: Monarch full support, with add manually option** (thanks to anonymous contributor (TBA)) + - **Princeton support for second button encoding type** (8bit) - GangQi fix serial check and remove broken check from UI - Hollarm add more button codes (thanks to @mishamyte for captures) - Misc: - Add extra settings to disable GPIO pins control used for external modules amplifiers and/or LEDs (in radio settings menu with debug ON) - NFC: - - Read Ultralight block by block (by @mishamyte | PR #825 #826) - - Update NDEF parser (by @jaylikesbunda and @Willy-JL) - - OFW PR 3822: MIFARE Classic Key Recovery Improvements (by @noproto) + - Read Ultralight block by block (**fix password protected MFUL reading issue**) (by @mishamyte | PR #825 #826) + - **Update NDEF parser** (SLIX and MFC support) (by @luu176 and @jaylikesbunda and @Willy-JL) + - OFW PR 3822: **MIFARE Classic Key Recovery Improvements** (by @noproto) - OFW PR 3930: NFC Emulation freeze (by @RebornedBrain) - OFW PR 3885: Add API to enforce ISO15693 mode (by @aaronjamt) - OFW: iso14443_4a improvements (by @RebornedBrain) - - OFW: Plantain parser improvements (by @assasinfil) + - OFW: Plantain parser improvements (by @assasinfil) & fixes (by @mxcdoam) - OFW: Moscow social card parser (by @assasinfil) - OFW: NFC: H World Hotel Chain Room Key Parser - OFW: NFC Parser for Tianjin Railway Transit - OFW: NFC TRT Parser: Additional checks to prevent false positives - New keys in system dict - Infrared: - - Add LEDs universal remote (DB by @amec0e) + - **Add LEDs universal remote** (DB by @amec0e) - Update universal remote assets (by @amec0e | PR #813 #816) - JS: - OFW: JS modules -> **Breaking API change** - - Backporting custom features - WIP (by @xMasterX and @Willy-JL) + - **Backporting custom features** (read about most of the changes after other changes section) (by @xMasterX and @Willy-JL) - Add i2c module (by @jamisonderek) + - Add SPI module (by @jamisonderek) * OFW: FuriHal, drivers: rework gauge initialization routine -> **Downgrade to older releases will break battery UI percent indicator, upgrade to this or newer version to restore** * OFW: heap: increased size -> **More free RAM!!** * OFW: New layout for BadUSB (es-LA) * OFW: Require PIN on boot * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* OFW PR 3971: Fix JS memory corruption (in gpio module) (by @portasynthinca3) * OFW: lib: digital_signal: digital_sequence: add furi_hal.h wrapped in ifdefs * OFW: Add warning about stealth mode in vibro CLI * OFW: Small fixes in the wifi devboard docs @@ -59,6 +61,49 @@ * OFW: Folder rename fails * OFW: Put errno into TCB * OFW: Fix USB-UART bridge exit screen stopping the bridge prematurely +**More details on JS changes** (js changelog written by @Willy-JL , thanks!): +- Non-exhaustive list of changes to help you fix your scripts: + - `badusb`: + - `setup()`: `mfr_name`, `prod_name`, `layout_path` parameters renamed to `mfrName`, `prodName`, `layoutPath` + - effort required to update old scripts using badusb: very minimal + - `dialog`: + - removed, now replaced by `gui/dialog` and `gui/file_picker` (see below) + - `event_loop`: + - new module, allows timer functionality, callbacks and event-driven programming, used heavily alongside gpio and gui modules + - `gpio`: + - fully overhauled, now you `get()` pin instances and perform actions on them like `.init()` + - now supports interrupts, callbacks and more cool things + - effort required to update old scripts using gpio: moderate + - `gui`: + - new module, fully overhauled, replaces dialog, keyboard, submenu, textbox modules + - higher barrier to entry than older modules (requires usage of `event_loop` and `gui.viewDispatcher`), but much more flexible, powerful and easier to extend + - includes all previously available js gui functionality (except `widget`), and also adds `gui/loading` and `gui/empty_screen` views + - currently `gui/file_picker` works different than other new view objects, it is a simple `.pickFile()` synchronous function, but this [may change later](https://github.com/flipperdevices/flipperzero-firmware/pull/3961#discussion_r1805579153) + - effort required to update old scripts using gui: extensive + - `keyboard`: + - removed, now replaced by `gui/text_input` and `gui/byte_input` (see above) + - `math`: + - `is_equal()` renamed to `isEqual()` + - `storage`: + - fully overhauled, now you `openFile()`s and perform actions on them like `.read()` + - now supports many more operations including different open modes, directories and much more + - effort required to update old scripts using storage: moderate + - `submenu`: + - removed, now replaced by `gui/submenu` (see above) + - `textbox`: + - removed, now replace by `gui/text_box` (see above) + - `widget`: + - only gui functionality not ported to new gui module, remains unchanged for now but likely to be ported later on + - globals: + - `__filepath` and `__dirpath` renamed to `__filename` and `__dirname` like in nodejs + - `to_string()` renamed and moved to number class as `n.toString()`, now supports optional base parameter + - `to_hex_string()` removed, now use `n.toString(16)` + - `parse_int()` renamed to `parseInt()`, now supports optional base parameter + - `to_upper_case()` and `to_lower_case()` renamed and moved to string class as `s.toUpperCase()` and `s.toLowerCase()` + - effort required to update old scripts using these: minimal + - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript) + - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct + - Type definitions for extra modules we have that OFW doesn't will come later

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From b462329dd52c191446075ad4b4426df98a1f9f36 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:51:49 +0300 Subject: [PATCH 69/74] upd changelog [ci skip] --- CHANGELOG.md | 2 +- ReadMe.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4665f45a6..ff636a393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Fix duplicated frequency lists and use user config for nearest frequency selector too - Fix buttons logic, **fix crash** - Protocol improvements: - - **Keeloq: Monarch full support, with add manually option** (thanks to anonymous contributor (TBA)) + - **Keeloq: Monarch full support, with add manually option** (thanks @ashphx !) - **Princeton support for second button encoding type** (8bit) - GangQi fix serial check and remove broken check from UI - Hollarm add more button codes (thanks to @mishamyte for captures) diff --git a/ReadMe.md b/ReadMe.md index ac4c442f8..61fe9ca7b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -117,7 +117,7 @@ Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX: - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) -- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks anonymous contributor (TBA)) +- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks @ashphx !) Protocols support made by Skorp (original implementation) and @xMasterX (current version): - CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) From 73241a6e70a1678bc4391fbb022a525666080a56 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:07:19 +0000 Subject: [PATCH 70/74] Sync apps No actual app updates Mr Christopher smartass Fails if you see this, you can shut up thank you There's been no app updates in 4 days Last time I pulled was 25th Oct I pulled now on 29th Oct and there were no updates So you have no place saying "Uh oh ... Looks like rogue Masters official release has more up-to-date apps than momentums dev build currently.." If anything, RM updated later than I did --- applications/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/external b/applications/external index 067aa52f5..da86546e7 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 067aa52f521e3f05e2181f753bffab733c424602 +Subproject commit da86546e7cc0351f5f6a7cdf6e8c0ab63953d6eb From 38471fdd75311c2cd1f6bb5c948dfa841c305b4d Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:08:52 +0000 Subject: [PATCH 71/74] GUI: Refactor TextInput illegal symbols API --- CHANGELOG.md | 2 + applications/external | 2 +- .../services/gui/modules/text_input.c | 144 +++++++----------- .../services/gui/modules/text_input.h | 11 +- .../system/js_app/modules/js_gui/text_input.c | 2 +- targets/f7/api_symbols.csv | 3 +- 6 files changed, 68 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a50ef468..a8c8a5286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ - Added type definitions (typescript files for type checking in IDE, Flipper does not run typescript) - Documentation is incomplete and deprecated, from now on you should refer to type definitions (`applications/system/js_app/types`), those will always be correct - Type definitions for extra modules we have that OFW doesn't will come later +- GUI: Refactored TextInput illegal symbols (by @Willy-JL) + - If your app used `text_input_add_illegal_symbols(text_input)` it should change to `text_input_show_illegal_symbols(text_input, true)` ### Added: - Apps: diff --git a/applications/external b/applications/external index da86546e7..d0300c7a5 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit da86546e7cc0351f5f6a7cdf6e8c0ab63953d6eb +Subproject commit d0300c7a5a353ba0c73030c81afc75ec440797e6 diff --git a/applications/services/gui/modules/text_input.c b/applications/services/gui/modules/text_input.c index 58c53653a..b4a9ce46e 100644 --- a/applications/services/gui/modules/text_input.c +++ b/applications/services/gui/modules/text_input.c @@ -2,7 +2,6 @@ #include #include #include -#include struct TextInput { View* view; @@ -38,10 +37,10 @@ typedef struct { FuriString* validator_text; bool validator_message_visible; - char extra_symbols[9]; + bool illegal_symbols; bool cursor_select; - size_t cursor_pos; uint8_t selected_keyboard; + size_t cursor_pos; } TextInputModel; static const uint8_t keyboard_origin_x = 1; @@ -102,7 +101,7 @@ static const TextInputKey keyboard_keys_row_3[] = { {'9', 120, 32}, }; -static TextInputKey symbol_keyboard_keys_row_1[] = { +static const TextInputKey symbol_keyboard_keys_row_1[] = { {'!', 2, 8}, {'@', 12, 8}, {'#', 22, 8}, @@ -118,7 +117,7 @@ static TextInputKey symbol_keyboard_keys_row_1[] = { {'3', 120, 8}, }; -static TextInputKey symbol_keyboard_keys_row_2[] = { +static const TextInputKey symbol_keyboard_keys_row_2[] = { {'~', 2, 20}, {'+', 12, 20}, {'-', 22, 20}, @@ -133,7 +132,7 @@ static TextInputKey symbol_keyboard_keys_row_2[] = { {'6', 120, 20}, }; -static TextInputKey symbol_keyboard_keys_row_3[] = { +static const TextInputKey symbol_keyboard_keys_row_3[] = { {SWITCH_KEYBOARD_KEY, 0, 23}, {'.', 15, 32}, {',', 29, 32}, @@ -241,6 +240,33 @@ static char char_to_uppercase(const char letter) { } } +static char char_to_illegal_symbol(char original) { + switch(original) { + default: + return original; + case '0': + return '_'; + case '1': + return '<'; + case '2': + return '>'; + case '3': + return ':'; + case '4': + return '"'; + case '5': + return '/'; + case '6': + return '\\'; + case '7': + return '|'; + case '8': + return '?'; + case '9': + return '*'; + } +} + static void text_input_backspace_cb(TextInputModel* model) { if(model->clear_default_text) { model->text_buffer[0] = 0; @@ -356,7 +382,8 @@ static void text_input_view_draw_callback(Canvas* canvas, void* _model) { keyboard_origin_x + keys[column].x, keyboard_origin_y + keys[column].y - (glyph == '_' || char_is_lowercase(glyph)), - glyph); + (symbols && model->illegal_symbols) ? char_to_illegal_symbol(glyph) : + glyph); } } } @@ -479,6 +506,10 @@ static void text_input_handle_ok(TextInput* text_input, TextInputModel* model, I model->selected_keyboard != symbol_keyboard.keyboard_index) { selected = char_to_uppercase(selected); } + if(model->selected_keyboard == symbol_keyboard.keyboard_index && + model->illegal_symbols) { + selected = char_to_illegal_symbol(selected); + } if(model->clear_default_text) { model->text_buffer[0] = selected; model->text_buffer[1] = '\0'; @@ -622,35 +653,34 @@ static bool text_input_view_ascii_callback(AsciiEvent* event, void* context) { true); return true; default: // Look in keyboards + TextInputModel* model = view_get_model(text_input->view); + uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0; + bool uppercase = model->clear_default_text || text_length == 0; for(size_t k = 0; k < keyboard_count; k++) { + bool symbols = k == symbol_keyboard.keyboard_index; const Keyboard* keyboard = keyboards[k]; for(size_t r = 0; r < keyboard_row_count; r++) { const TextInputKey* row = get_row(keyboard, r); uint8_t size = get_row_size(keyboard, r); for(size_t key = 0; key < size; key++) { char lower = row[key].text; - char upper = char_to_uppercase(lower); + if(symbols && model->illegal_symbols) lower = char_to_illegal_symbol(lower); + char upper = symbols ? lower : char_to_uppercase(lower); if(event->value == lower || event->value == upper) { - with_view_model( - text_input->view, - TextInputModel * model, - { - model->cursor_select = false; - model->selected_keyboard = k; - model->selected_row = r; - model->selected_column = key; - bool shift = - (event->value == upper) != - (model->clear_default_text || strlen(model->text_buffer) == 0); - text_input_handle_ok( - text_input, model, shift ? InputTypeLong : InputTypeShort); - }, - true); + model->cursor_select = false; + model->selected_keyboard = k; + model->selected_row = r; + model->selected_column = key; + bool shift = (event->value == upper) != uppercase && !symbols; + text_input_handle_ok( + text_input, model, shift ? InputTypeLong : InputTypeShort); + view_commit_model(text_input->view, true); return true; } } } } + view_commit_model(text_input->view, false); break; } @@ -668,33 +698,6 @@ void text_input_timer_callback(void* context) { true); } -static void reset_extra_symbols(TextInputModel* model) { - memset(model->extra_symbols, 0, sizeof(model->extra_symbols)); - size_t symbol = 0; - for(size_t row = 0; row < 3; row++) { - size_t size = get_row_size(&symbol_keyboard, row) - 3; - for(size_t i = 0; i < 3; i++) { - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, row)[size++].text, '1' + (symbol++)); - } - } - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, 0)[get_row_size(&symbol_keyboard, 0) - 4].text, '0'); -} - -static void apply_extra_symbols(TextInputModel* model) { - size_t symbol = 0; - for(size_t row = 0; row < 3; row++) { - size_t size = get_row_size(&symbol_keyboard, row) - 3; - for(size_t i = 0; i < 3; i++) { - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, row)[size++].text, model->extra_symbols[symbol++]); - } - } - FURI_CONST_ASSIGN_( - char, get_row(&symbol_keyboard, 0)[get_row_size(&symbol_keyboard, 0) - 4].text, '_'); -} - TextInput* text_input_alloc(void) { TextInput* text_input = malloc(sizeof(TextInput)); text_input->view = view_alloc(); @@ -712,7 +715,7 @@ TextInput* text_input_alloc(void) { { model->validator_text = furi_string_alloc(); model->minimum_length = 1; - reset_extra_symbols(model); + model->illegal_symbols = false; model->cursor_pos = 0; model->cursor_select = false; }, @@ -752,7 +755,7 @@ void text_input_reset(TextInput* text_input) { model->selected_column = 0; model->selected_keyboard = 0; model->minimum_length = 1; - reset_extra_symbols(model); + model->illegal_symbols = false; model->clear_default_text = false; model->cursor_pos = 0; model->cursor_select = false; @@ -813,43 +816,10 @@ void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length) true); } -void text_input_add_extra_symbol(TextInput* text_input, char symbol) { - furi_check(text_input); - if(!symbol) return; - with_view_model( - text_input->view, - TextInputModel * model, - { - for(size_t i = 0; i < sizeof(model->extra_symbols); i++) { - if(!model->extra_symbols[i]) { - model->extra_symbols[i] = symbol; - apply_extra_symbols(model); - break; - } - } - }, - true); -} - -void text_input_add_illegal_symbols(TextInput* text_input) { +void text_input_show_illegal_symbols(TextInput* text_input, bool show) { furi_check(text_input); with_view_model( - text_input->view, - TextInputModel * model, - { - size_t i = 0; - model->extra_symbols[i++] = '<'; - model->extra_symbols[i++] = '>'; - model->extra_symbols[i++] = ':'; - model->extra_symbols[i++] = '"'; - model->extra_symbols[i++] = '/'; - model->extra_symbols[i++] = '\\'; - model->extra_symbols[i++] = '|'; - model->extra_symbols[i++] = '?'; - model->extra_symbols[i++] = '*'; - apply_extra_symbols(model); - }, - true); + text_input->view, TextInputModel * model, { model->illegal_symbols = show; }, true); } void text_input_set_validator( diff --git a/applications/services/gui/modules/text_input.h b/applications/services/gui/modules/text_input.h index 7656a21bf..6353d9732 100644 --- a/applications/services/gui/modules/text_input.h +++ b/applications/services/gui/modules/text_input.h @@ -77,11 +77,12 @@ void text_input_set_validator( TextInputValidatorCallback callback, void* callback_context); -// Add up to 9 extra characters for symbol keyboard -void text_input_add_extra_symbol(TextInput* text_input, char symbol); - -// Add the 9 predefined illegal (windows) symbols <>:"/\|?* -void text_input_add_illegal_symbols(TextInput* text_input); +/** + * @brief Show the 9 illegal (windows) symbols <>:"/\|?* in the symbols keyboard instead of the numbers + * @param [in] text_input TextInput + * @param [in] show Whether to show the illegal symbols or not + */ +void text_input_show_illegal_symbols(TextInput* text_input, bool show); TextInputValidatorCallback text_input_get_validator_callback(TextInput* text_input); diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index 79a138388..38dabb488 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -135,7 +135,7 @@ static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_ .transformer_context = context, }, }; - text_input_add_illegal_symbols(input); + text_input_show_illegal_symbols(input, true); // Temporary until prop is available text_input_set_result_callback( input, (TextInputCallback)input_callback, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 70011042f..55c66b02d 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3669,8 +3669,6 @@ Function,+,text_box_reset,void,TextBox* Function,+,text_box_set_focus,void,"TextBox*, TextBoxFocus" Function,+,text_box_set_font,void,"TextBox*, TextBoxFont" Function,+,text_box_set_text,void,"TextBox*, const char*" -Function,+,text_input_add_extra_symbol,void,"TextInput*, char" -Function,+,text_input_add_illegal_symbols,void,TextInput* Function,+,text_input_alloc,TextInput*, Function,+,text_input_free,void,TextInput* Function,+,text_input_get_validator_callback,TextInputValidatorCallback,TextInput* @@ -3681,6 +3679,7 @@ Function,+,text_input_set_header_text,void,"TextInput*, const char*" Function,+,text_input_set_minimum_length,void,"TextInput*, size_t" Function,+,text_input_set_result_callback,void,"TextInput*, TextInputCallback, void*, char*, size_t, _Bool" Function,+,text_input_set_validator,void,"TextInput*, TextInputValidatorCallback, void*" +Function,+,text_input_show_illegal_symbols,void,"TextInput*, _Bool" Function,-,tgamma,double,double Function,-,tgammaf,float,float Function,-,tgammal,long double,long double From 4ee5789168d581a1b5f7cb1555e8dc087a13b907 Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:19:15 +0000 Subject: [PATCH 72/74] GUI: Ascii input for ByteInput --- CHANGELOG.md | 1 + .../services/gui/modules/byte_input.c | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c8a5286..88f7a3a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ - OFW: Dolphin: Happy mode in Desktop settings (by @portasynthinca3) - OFW: CLI: Improvements part I, `neofetch` command (by @portasynthinca3), fix for lab.flipper.net (by @xMasterX) - GUI: + - ByteInput supports ASCII input (by @Willy-JL) - OFW: Add up and down button drawing functions to GUI elements (by @DerSkythe) - OFW: Extended icon draw function in Canvas (by @RebornedBrain) - OFW: RPC: Support 5V on GPIO control for ext. modules (by @gsurkov) diff --git a/applications/services/gui/modules/byte_input.c b/applications/services/gui/modules/byte_input.c index 3e23ee6c1..c9f689662 100644 --- a/applications/services/gui/modules/byte_input.c +++ b/applications/services/gui/modules/byte_input.c @@ -726,6 +726,51 @@ static bool byte_input_view_input_callback(InputEvent* event, void* context) { return consumed; } +static bool byte_input_view_ascii_callback(AsciiEvent* event, void* context) { + ByteInput* byte_input = context; + furi_assert(byte_input); + + switch(event->value) { + case AsciiValueDC3: // Right + case AsciiValueDC4: // Left + with_view_model( + byte_input->view, + ByteInputModel * model, + { + if(event->value == AsciiValueDC3) { + byte_input_inc_selected_byte_mini(model); + } else { + byte_input_dec_selected_byte_mini(model); + } + }, + true); + return true; + default: // Look in keyboard + for(size_t r = 0; r < keyboard_row_count; r++) { + const ByteInputKey* row = byte_input_get_row(r); + uint8_t size = byte_input_get_row_size(r); + for(size_t key = 0; key < size; key++) { + char value = row[key].value; + if(event->value == value) { + with_view_model( + byte_input->view, + ByteInputModel * model, + { + model->selected_row = r; + model->selected_column = key; + byte_input_handle_ok(model); + }, + true); + return true; + } + } + } + break; + } + + return false; +} + /** Reset all input-related data in model * * @param model The model @@ -747,6 +792,7 @@ ByteInput* byte_input_alloc(void) { view_allocate_model(byte_input->view, ViewModelTypeLocking, sizeof(ByteInputModel)); view_set_draw_callback(byte_input->view, byte_input_view_draw_callback); view_set_input_callback(byte_input->view, byte_input_view_input_callback); + view_set_ascii_callback(byte_input->view, byte_input_view_ascii_callback); with_view_model( byte_input->view, From 45f6e448c6b7e5bbb43963049b40943b50f7e66f Mon Sep 17 00:00:00 2001 From: Willy-JL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:52:58 +0000 Subject: [PATCH 73/74] RPC: Fix apps not updating and staying at 100% --nobuild --- CHANGELOG.md | 1 + applications/services/rpc/rpc_storage.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f7a3a68..f47d67abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,6 +177,7 @@ - Fallback SD format prompt when storage settings is unavailable (by @Willy-JL) - OFW: Fix folder rename fails (by @portasynthinca3) - About: Fix BLE stack version string (by @Willy-JL) +- RPC: Fixed apps not updating and staying at 100% (by @Willy-JL) - OFW: Loader: Warn about missing SD card for main apps (by @Willy-JL) - NFC: - UL: Read Ultralight block by block (by @mishamyte) diff --git a/applications/services/rpc/rpc_storage.c b/applications/services/rpc/rpc_storage.c index 79b91d947..10261461c 100644 --- a/applications/services/rpc/rpc_storage.c +++ b/applications/services/rpc/rpc_storage.c @@ -629,7 +629,7 @@ static void rpc_system_storage_rename_process(const PB_Main* request, void* cont rpc_system_storage_reset_state(rpc_storage, session, true); if(path_contains_only_ascii(request->content.storage_rename_request.new_path)) { - FS_Error error = storage_common_rename_safe( + FS_Error error = storage_common_rename( rpc_storage->api, request->content.storage_rename_request.old_path, request->content.storage_rename_request.new_path); From 39a3422b4517cd7138f600a91dfb551e0614a28e Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:16:27 +0000 Subject: [PATCH 74/74] RPC: Add ASCII event support (#284) * RPC: SendAsciiEventRequest support * GUI: Ascii input for ByteInput * Sync protobuf submodule * Update changelog --- CHANGELOG.md | 1 + applications/services/rpc/rpc_gui.c | 34 +++++++++++++++++++++++++++++ assets/protobuf | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f47d67abb..b9cba5555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ - JS: - New `i2c` module (#259 by @jamisonderek) - New `spi` module (#272 by @jamisonderek) +- RPC: Added ASCII event support (#284 by @Willy-JL) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) - Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme) diff --git a/applications/services/rpc/rpc_gui.c b/applications/services/rpc/rpc_gui.c index 447156bc9..70c5f1463 100644 --- a/applications/services/rpc/rpc_gui.c +++ b/applications/services/rpc/rpc_gui.c @@ -269,6 +269,37 @@ static void rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); } +static void + rpc_system_gui_send_ascii_event_request_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_gui_send_ascii_event_request_tag); + furi_assert(context); + + FURI_LOG_D(TAG, "SendAsciiEvent"); + + RpcGuiSystem* rpc_gui = context; + RpcSession* session = rpc_gui->session; + furi_assert(session); + + bool is_valid = (request->content.gui_send_ascii_event_request.value <= 0xFF); + + if(!is_valid) { + rpc_send_and_release_empty( + session, request->command_id, PB_CommandStatus_ERROR_INVALID_PARAMETERS); + return; + } + + AsciiEvent event = { + .value = request->content.gui_send_ascii_event_request.value, + }; + + // Submit event + FuriPubSub* ascii_events = furi_record_open(RECORD_ASCII_EVENTS); + furi_pubsub_publish(ascii_events, &event); + furi_record_close(RECORD_ASCII_EVENTS); + rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); +} + static void rpc_system_gui_virtual_display_render_callback(Canvas* canvas, void* context) { furi_assert(canvas); furi_assert(context); @@ -459,6 +490,9 @@ void* rpc_system_gui_alloc(RpcSession* session) { rpc_handler.message_handler = rpc_system_gui_send_input_event_request_process; rpc_add_handler(session, PB_Main_gui_send_input_event_request_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_gui_send_ascii_event_request_process; + rpc_add_handler(session, PB_Main_gui_send_ascii_event_request_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_gui_start_virtual_display_process; rpc_add_handler(session, PB_Main_gui_start_virtual_display_request_tag, &rpc_handler); diff --git a/assets/protobuf b/assets/protobuf index b7d588169..c255d71a9 160000 --- a/assets/protobuf +++ b/assets/protobuf @@ -1 +1 @@ -Subproject commit b7d5881690298de836df6305d4c71f96d5d34c61 +Subproject commit c255d71a90af202515deb7aaa51685d45a196152