MX 2 лет назад
Родитель
Сommit
4c7d203aee
100 измененных файлов с 10451 добавлено и 0 удалено
  1. 674 0
      main_apps_sources/gps_nmea_uart/LICENSE
  2. 46 0
      main_apps_sources/gps_nmea_uart/README.md
  3. 14 0
      main_apps_sources/gps_nmea_uart/application.fam
  4. 190 0
      main_apps_sources/gps_nmea_uart/gps.c
  5. BIN
      main_apps_sources/gps_nmea_uart/gps_10px.png
  6. 217 0
      main_apps_sources/gps_nmea_uart/gps_uart.c
  7. 48 0
      main_apps_sources/gps_nmea_uart/gps_uart.h
  8. BIN
      main_apps_sources/gps_nmea_uart/img/1.png
  9. BIN
      main_apps_sources/gps_nmea_uart/img/2.png
  10. BIN
      main_apps_sources/gps_nmea_uart/img/3.png
  11. 640 0
      main_apps_sources/gps_nmea_uart/minmea.c
  12. 295 0
      main_apps_sources/gps_nmea_uart/minmea.h
  13. BIN
      main_apps_sources/gps_nmea_uart/ui.png
  14. BIN
      main_apps_sources/gps_nmea_uart/wiring.png
  15. 18 0
      main_apps_sources/pocsag_pager/README.md
  16. 15 0
      main_apps_sources/pocsag_pager/application.fam
  17. 14 0
      main_apps_sources/pocsag_pager/helpers/pocsag_pager_event.h
  18. 49 0
      main_apps_sources/pocsag_pager/helpers/pocsag_pager_types.h
  19. 66 0
      main_apps_sources/pocsag_pager/helpers/radio_device_loader.c
  20. 17 0
      main_apps_sources/pocsag_pager/helpers/radio_device_loader.h
  21. BIN
      main_apps_sources/pocsag_pager/images/Fishing_123x52.png
  22. BIN
      main_apps_sources/pocsag_pager/images/Lock_7x8.png
  23. BIN
      main_apps_sources/pocsag_pager/images/Message_8x7.png
  24. BIN
      main_apps_sources/pocsag_pager/images/Pin_back_arrow_10x8.png
  25. BIN
      main_apps_sources/pocsag_pager/images/Quest_7x8.png
  26. BIN
      main_apps_sources/pocsag_pager/images/Scanning_123x52.png
  27. BIN
      main_apps_sources/pocsag_pager/images/Unlock_7x8.png
  28. BIN
      main_apps_sources/pocsag_pager/images/WarningDolphin_45x42.png
  29. BIN
      main_apps_sources/pocsag_pager/img/1.png
  30. BIN
      main_apps_sources/pocsag_pager/img/2.png
  31. BIN
      main_apps_sources/pocsag_pager/img/3.png
  32. BIN
      main_apps_sources/pocsag_pager/pocsag_pager_10px.png
  33. 206 0
      main_apps_sources/pocsag_pager/pocsag_pager_app.c
  34. 146 0
      main_apps_sources/pocsag_pager/pocsag_pager_app_i.c
  35. 76 0
      main_apps_sources/pocsag_pager/pocsag_pager_app_i.h
  36. 223 0
      main_apps_sources/pocsag_pager/pocsag_pager_history.c
  37. 112 0
      main_apps_sources/pocsag_pager/pocsag_pager_history.h
  38. 124 0
      main_apps_sources/pocsag_pager/protocols/pcsg_generic.c
  39. 56 0
      main_apps_sources/pocsag_pager/protocols/pcsg_generic.h
  40. 374 0
      main_apps_sources/pocsag_pager/protocols/pocsag.c
  41. 13 0
      main_apps_sources/pocsag_pager/protocols/pocsag.h
  42. 9 0
      main_apps_sources/pocsag_pager/protocols/protocol_items.c
  43. 6 0
      main_apps_sources/pocsag_pager/protocols/protocol_items.h
  44. 214 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_receiver.c
  45. 30 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene.c
  46. 29 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene.h
  47. 74 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_about.c
  48. 5 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_config.h
  49. 221 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_receiver_config.c
  50. 50 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_receiver_info.c
  51. 58 0
      main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_start.c
  52. 478 0
      main_apps_sources/pocsag_pager/views/pocsag_pager_receiver.c
  53. 43 0
      main_apps_sources/pocsag_pager/views/pocsag_pager_receiver.h
  54. 137 0
      main_apps_sources/pocsag_pager/views/pocsag_pager_receiver_info.c
  55. 16 0
      main_apps_sources/pocsag_pager/views/pocsag_pager_receiver_info.h
  56. 24 0
      main_apps_sources/protoview/LICENSE
  57. 159 0
      main_apps_sources/protoview/README.md
  58. 400 0
      main_apps_sources/protoview/app.c
  59. 377 0
      main_apps_sources/protoview/app.h
  60. 206 0
      main_apps_sources/protoview/app_subghz.c
  61. BIN
      main_apps_sources/protoview/appicon.png
  62. 14 0
      main_apps_sources/protoview/application.fam
  63. 36 0
      main_apps_sources/protoview/crc.c
  64. 317 0
      main_apps_sources/protoview/custom_presets.h
  65. 369 0
      main_apps_sources/protoview/fields.c
  66. 64 0
      main_apps_sources/protoview/helpers/radio_device_loader.c
  67. 15 0
      main_apps_sources/protoview/helpers/radio_device_loader.h
  68. BIN
      main_apps_sources/protoview/images/ProtoViewSignal.jpg
  69. BIN
      main_apps_sources/protoview/images/protoview_1.jpg
  70. BIN
      main_apps_sources/protoview/images/protoview_2.jpg
  71. BIN
      main_apps_sources/protoview/img/1.png
  72. BIN
      main_apps_sources/protoview/img/2.png
  73. BIN
      main_apps_sources/protoview/img/3.png
  74. 93 0
      main_apps_sources/protoview/protocols/b4b1.c
  75. 121 0
      main_apps_sources/protoview/protocols/keeloq.c
  76. 84 0
      main_apps_sources/protoview/protocols/oregon2.c
  77. 205 0
      main_apps_sources/protoview/protocols/pvchat.c
  78. 58 0
      main_apps_sources/protoview/protocols/tpms/citroen.c
  79. 61 0
      main_apps_sources/protoview/protocols/tpms/ford.c
  80. 119 0
      main_apps_sources/protoview/protocols/tpms/renault.c
  81. 70 0
      main_apps_sources/protoview/protocols/tpms/schrader.c
  82. 62 0
      main_apps_sources/protoview/protocols/tpms/schrader_eg53ma4.c
  83. 81 0
      main_apps_sources/protoview/protocols/tpms/toyota.c
  84. 326 0
      main_apps_sources/protoview/protocols/unknown.c
  85. 93 0
      main_apps_sources/protoview/raw_samples.c
  86. 33 0
      main_apps_sources/protoview/raw_samples.h
  87. 692 0
      main_apps_sources/protoview/signal.c
  88. 138 0
      main_apps_sources/protoview/signal_file.c
  89. 145 0
      main_apps_sources/protoview/ui.c
  90. 248 0
      main_apps_sources/protoview/view_build.c
  91. 173 0
      main_apps_sources/protoview/view_direct_sampling.c
  92. 332 0
      main_apps_sources/protoview/view_info.c
  93. 114 0
      main_apps_sources/protoview/view_raw_signal.c
  94. 119 0
      main_apps_sources/protoview/view_settings.c
  95. 7 0
      main_apps_sources/spectrum_analyzer/README.md
  96. 14 0
      main_apps_sources/spectrum_analyzer/application.fam
  97. 64 0
      main_apps_sources/spectrum_analyzer/helpers/radio_device_loader.c
  98. 15 0
      main_apps_sources/spectrum_analyzer/helpers/radio_device_loader.h
  99. BIN
      main_apps_sources/spectrum_analyzer/img/1.png
  100. BIN
      main_apps_sources/spectrum_analyzer/img/2.png

+ 674 - 0
main_apps_sources/gps_nmea_uart/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>.

+ 46 - 0
main_apps_sources/gps_nmea_uart/README.md

@@ -0,0 +1,46 @@
+# GPS for Flipper Zero
+
+A simple Flipper Zero application for NMEA 0183 serial GPS modules, such as the
+- Adafruit Ultimate GPS Breakout.
+
+Heavy lifting (NMEA parsing) provided by minmea.
+
+## Usage
+
+This is a single-screen app, and a few interactions are provided via the
+hardware buttons:
+
+- Long press the up button to change the **baud rate**. The default baud rate
+  is 9600, but 19200, 38400, 57600, and 115200 baud are also supported.
+- Long press the right button to change **speed units** from knots to
+  kilometers per hour.
+- Press the OK button to set the **backlight** to always on mode. Press it
+  again to disable.
+- Long press the back button to **exit** the app.
+
+## Hardware Setup
+
+Connect the GPS module to power and the USART using GPIO pins 9 (3.3V), 11
+(GND), 13 (TX), and 14 (RX), as appropriate.
+
+
+See the tutorial video - https://www.youtube.com/watch?v=5vSGFzEBp-k from
+Lab401 by RocketGod - https://github.com/RocketGod-git for a visual guide to
+the hardware setup.
+
+### Confirmed Compatible Modules
+
+* Adafruit Ultimate GPS Breakout
+* Beitian BN-180
+* Royaltek RBT-2100LP
+* u-blox NEO-6M
+
+If you have verified this application working with a module not listed here,
+please submit a PR adding it to the list.
+
+## Links
+
+Original repo link - https://github.com/ezod/flipperzero-gps
+Adafruit Ultimate GPS Breakout: https://www.adafruit.com/product/746
+minmea: https://github.com/kosma/minmea
+u-blox NEO-6M: https://www.u-blox.com/en/product/neo-6-series

+ 14 - 0
main_apps_sources/gps_nmea_uart/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="gps_nmea",
+    name="[NMEA] GPS",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="gps_app",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    order=35,
+    fap_icon="gps_10px.png",
+    fap_category="GPIO",
+    fap_author="@ezod & @xMasterX",
+    fap_version="1.0",
+    fap_description="Works with GPS modules via UART, using NMEA protocol.",
+)

+ 190 - 0
main_apps_sources/gps_nmea_uart/gps.c

@@ -0,0 +1,190 @@
+#include "gps_uart.h"
+
+#include <furi.h>
+#include <gui/gui.h>
+#include <string.h>
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} PluginEvent;
+
+static void render_callback(Canvas* const canvas, void* context) {
+    furi_assert(context);
+    GpsUart* gps_uart = context;
+    furi_mutex_acquire(gps_uart->mutex, FuriWaitForever);
+
+    if(!gps_uart->changing_baudrate) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 32, 8, AlignCenter, AlignBottom, "Latitude");
+        canvas_draw_str_aligned(canvas, 96, 8, AlignCenter, AlignBottom, "Longitude");
+        canvas_draw_str_aligned(canvas, 21, 30, AlignCenter, AlignBottom, "Course");
+        canvas_draw_str_aligned(canvas, 64, 30, AlignCenter, AlignBottom, "Speed");
+        canvas_draw_str_aligned(canvas, 107, 30, AlignCenter, AlignBottom, "Altitude");
+        canvas_draw_str_aligned(canvas, 32, 52, AlignCenter, AlignBottom, "Satellites");
+        canvas_draw_str_aligned(canvas, 96, 52, AlignCenter, AlignBottom, "Last Fix");
+
+        canvas_set_font(canvas, FontSecondary);
+        char buffer[64];
+        snprintf(buffer, 64, "%f", (double)gps_uart->status.latitude);
+        canvas_draw_str_aligned(canvas, 32, 18, AlignCenter, AlignBottom, buffer);
+        snprintf(buffer, 64, "%f", (double)gps_uart->status.longitude);
+        canvas_draw_str_aligned(canvas, 96, 18, AlignCenter, AlignBottom, buffer);
+        snprintf(buffer, 64, "%.1f", (double)gps_uart->status.course);
+        canvas_draw_str_aligned(canvas, 21, 40, AlignCenter, AlignBottom, buffer);
+        if(!gps_uart->speed_in_kms) {
+            snprintf(buffer, 64, "%.2f kn", (double)gps_uart->status.speed);
+        } else {
+            snprintf(buffer, 64, "%.2f km", (double)(gps_uart->status.speed * 1.852));
+        }
+        canvas_draw_str_aligned(canvas, 64, 40, AlignCenter, AlignBottom, buffer);
+        snprintf(
+            buffer,
+            64,
+            "%.1f %c",
+            (double)gps_uart->status.altitude,
+            tolower(gps_uart->status.altitude_units));
+        canvas_draw_str_aligned(canvas, 107, 40, AlignCenter, AlignBottom, buffer);
+        snprintf(buffer, 64, "%d", gps_uart->status.satellites_tracked);
+        canvas_draw_str_aligned(canvas, 32, 62, AlignCenter, AlignBottom, buffer);
+        snprintf(
+            buffer,
+            64,
+            "%02d:%02d:%02d UTC",
+            gps_uart->status.time_hours,
+            gps_uart->status.time_minutes,
+            gps_uart->status.time_seconds);
+        canvas_draw_str_aligned(canvas, 96, 62, AlignCenter, AlignBottom, buffer);
+    } else {
+        char buffer[64];
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignBottom, "Baudrate set to:");
+
+        snprintf(buffer, 64, "%ld baud", gps_uart->baudrate);
+        canvas_draw_str_aligned(canvas, 64, 47, AlignCenter, AlignBottom, buffer);
+    }
+
+    furi_mutex_release(gps_uart->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);
+}
+
+int32_t gps_app(void* p) {
+    UNUSED(p);
+
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
+
+    GpsUart* gps_uart = gps_uart_enable();
+
+    gps_uart->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    if(!gps_uart->mutex) {
+        FURI_LOG_E("GPS", "cannot create mutex\r\n");
+        free(gps_uart);
+        return 255;
+    }
+
+    // set system callbacks
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, render_callback, gps_uart);
+    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);
+
+    PluginEvent event;
+    for(bool processing = true; processing;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
+
+        furi_mutex_acquire(gps_uart->mutex, FuriWaitForever);
+
+        if(event_status == FuriStatusOk) {
+            // press events
+            if(event.type == EventTypeKey) {
+                if(event.input.type == InputTypeShort) {
+                    switch(event.input.key) {
+                    case InputKeyUp:
+                    case InputKeyDown:
+                    case InputKeyRight:
+                    case InputKeyLeft:
+                    case InputKeyBack:
+                        break;
+                    case InputKeyOk:
+                        if(!gps_uart->backlight_on) {
+                            notification_message_block(
+                                gps_uart->notifications, &sequence_display_backlight_enforce_on);
+                            gps_uart->backlight_on = true;
+                        } else {
+                            notification_message_block(
+                                gps_uart->notifications, &sequence_display_backlight_enforce_auto);
+                            notification_message(
+                                gps_uart->notifications, &sequence_display_backlight_off);
+                            gps_uart->backlight_on = false;
+                        }
+                        break;
+                    default:
+                        break;
+                    }
+                } else if(event.input.type == InputTypeLong) {
+                    switch(event.input.key) {
+                    case InputKeyUp:
+                        gps_uart_deinit_thread(gps_uart);
+                        const int baudrate_length =
+                            sizeof(gps_baudrates) / sizeof(gps_baudrates[0]);
+                        current_gps_baudrate++;
+                        if(current_gps_baudrate >= baudrate_length) {
+                            current_gps_baudrate = 0;
+                        }
+                        gps_uart->baudrate = gps_baudrates[current_gps_baudrate];
+
+                        gps_uart_init_thread(gps_uart);
+                        gps_uart->changing_baudrate = true;
+                        view_port_update(view_port);
+                        furi_mutex_release(gps_uart->mutex);
+                        break;
+                    case InputKeyRight:
+                        if(gps_uart->speed_in_kms) {
+                            gps_uart->speed_in_kms = false;
+                        } else {
+                            gps_uart->speed_in_kms = true;
+                        }
+                        break;
+                    case InputKeyBack:
+                        processing = false;
+                        break;
+                    default:
+                        break;
+                    }
+                }
+            }
+        }
+        if(!gps_uart->changing_baudrate) {
+            view_port_update(view_port);
+            furi_mutex_release(gps_uart->mutex);
+        } else {
+            furi_delay_ms(1000);
+            gps_uart->changing_baudrate = false;
+        }
+    }
+
+    notification_message_block(gps_uart->notifications, &sequence_display_backlight_enforce_auto);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(gps_uart->mutex);
+    gps_uart_disable(gps_uart);
+
+    return 0;
+}

BIN
main_apps_sources/gps_nmea_uart/gps_10px.png


+ 217 - 0
main_apps_sources/gps_nmea_uart/gps_uart.c

@@ -0,0 +1,217 @@
+#include <string.h>
+
+#include "minmea.h"
+#include "gps_uart.h"
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone)
+
+static void gps_uart_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) {
+    GpsUart* gps_uart = (GpsUart*)context;
+
+    if(ev == UartIrqEventRXNE) {
+        furi_stream_buffer_send(gps_uart->rx_stream, &data, 1, 0);
+        furi_thread_flags_set(furi_thread_get_id(gps_uart->thread), WorkerEvtRxDone);
+    }
+}
+
+static void gps_uart_serial_init(GpsUart* gps_uart) {
+    furi_hal_console_disable();
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, gps_uart_on_irq_cb, gps_uart);
+    furi_hal_uart_set_br(FuriHalUartIdUSART1, gps_uart->baudrate);
+}
+
+static void gps_uart_serial_deinit(GpsUart* gps_uart) {
+    UNUSED(gps_uart);
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL);
+    furi_hal_console_enable();
+}
+
+static void gps_uart_parse_nmea(GpsUart* gps_uart, char* line) {
+    switch(minmea_sentence_id(line, false)) {
+    case MINMEA_SENTENCE_RMC: {
+        struct minmea_sentence_rmc frame;
+        if(minmea_parse_rmc(&frame, line)) {
+            gps_uart->status.valid = frame.valid;
+            gps_uart->status.latitude = minmea_tocoord(&frame.latitude);
+            gps_uart->status.longitude = minmea_tocoord(&frame.longitude);
+            gps_uart->status.speed = minmea_tofloat(&frame.speed);
+            gps_uart->status.course = minmea_tofloat(&frame.course);
+            gps_uart->status.time_hours = frame.time.hours;
+            gps_uart->status.time_minutes = frame.time.minutes;
+            gps_uart->status.time_seconds = frame.time.seconds;
+
+            notification_message_block(gps_uart->notifications, &sequence_blink_green_10);
+        }
+    } break;
+
+    case MINMEA_SENTENCE_GGA: {
+        struct minmea_sentence_gga frame;
+        if(minmea_parse_gga(&frame, line)) {
+            gps_uart->status.latitude = minmea_tocoord(&frame.latitude);
+            gps_uart->status.longitude = minmea_tocoord(&frame.longitude);
+            gps_uart->status.altitude = minmea_tofloat(&frame.altitude);
+            gps_uart->status.altitude_units = frame.altitude_units;
+            gps_uart->status.fix_quality = frame.fix_quality;
+            gps_uart->status.satellites_tracked = frame.satellites_tracked;
+            gps_uart->status.time_hours = frame.time.hours;
+            gps_uart->status.time_minutes = frame.time.minutes;
+            gps_uart->status.time_seconds = frame.time.seconds;
+
+            notification_message_block(gps_uart->notifications, &sequence_blink_magenta_10);
+        }
+    } break;
+
+    case MINMEA_SENTENCE_GLL: {
+        struct minmea_sentence_gll frame;
+        if(minmea_parse_gll(&frame, line)) {
+            gps_uart->status.latitude = minmea_tocoord(&frame.latitude);
+            gps_uart->status.longitude = minmea_tocoord(&frame.longitude);
+            gps_uart->status.time_hours = frame.time.hours;
+            gps_uart->status.time_minutes = frame.time.minutes;
+            gps_uart->status.time_seconds = frame.time.seconds;
+
+            notification_message_block(gps_uart->notifications, &sequence_blink_red_10);
+        }
+    } break;
+
+    default:
+        break;
+    }
+}
+
+static int32_t gps_uart_worker(void* context) {
+    GpsUart* gps_uart = (GpsUart*)context;
+
+    size_t rx_offset = 0;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check((events & FuriFlagError) == 0);
+
+        if(events & WorkerEvtStop) {
+            break;
+        }
+
+        if(events & WorkerEvtRxDone) {
+            size_t len = 0;
+            do {
+                // receive serial bytes into rx_buf, starting at rx_offset from the start of the buffer
+                // the maximum we can receive is RX_BUF_SIZE - 1 - rx_offset
+                len = furi_stream_buffer_receive(
+                    gps_uart->rx_stream,
+                    gps_uart->rx_buf + rx_offset,
+                    RX_BUF_SIZE - 1 - rx_offset,
+                    0);
+                if(len > 0) {
+                    // increase rx_offset by the number of bytes received, and null-terminate rx_buf
+                    rx_offset += len;
+                    gps_uart->rx_buf[rx_offset] = '\0';
+
+                    // look for strings ending in newlines, starting at the start of rx_buf
+                    char* line_current = (char*)gps_uart->rx_buf;
+                    while(1) {
+                        // skip null characters
+                        while(*line_current == '\0' &&
+                              line_current < (char*)gps_uart->rx_buf + rx_offset - 1) {
+                            line_current++;
+                        }
+
+                        // find the next newline
+                        char* newline = strchr(line_current, '\n');
+                        if(newline) // newline found
+                        {
+                            // put a null terminator in place of the newline, to delimit the line string
+                            *newline = '\0';
+
+                            // attempt to parse the line as a NMEA sentence
+                            gps_uart_parse_nmea(gps_uart, line_current);
+
+                            // move the cursor to the character after the newline
+                            line_current = newline + 1;
+                        } else // no more newlines found
+                        {
+                            if(line_current >
+                               (char*)gps_uart->rx_buf) // at least one line was found
+                            {
+                                // clear parsed lines, and move any leftover bytes to the start of rx_buf
+                                rx_offset = 0;
+                                while(
+                                    *line_current) // stop when the original rx_offset terminator is reached
+                                {
+                                    gps_uart->rx_buf[rx_offset++] = *(line_current++);
+                                }
+                            }
+                            break; // go back to receiving bytes from the serial stream
+                        }
+                    }
+                }
+            } while(len > 0);
+        }
+    }
+
+    gps_uart_serial_deinit(gps_uart);
+    furi_stream_buffer_free(gps_uart->rx_stream);
+
+    return 0;
+}
+
+void gps_uart_init_thread(GpsUart* gps_uart) {
+    furi_assert(gps_uart);
+    gps_uart->status.valid = false;
+    gps_uart->status.latitude = 0.0;
+    gps_uart->status.longitude = 0.0;
+    gps_uart->status.speed = 0.0;
+    gps_uart->status.course = 0.0;
+    gps_uart->status.altitude = 0.0;
+    gps_uart->status.altitude_units = ' ';
+    gps_uart->status.fix_quality = 0;
+    gps_uart->status.satellites_tracked = 0;
+    gps_uart->status.time_hours = 0;
+    gps_uart->status.time_minutes = 0;
+    gps_uart->status.time_seconds = 0;
+
+    gps_uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE * 5, 1);
+
+    gps_uart->thread = furi_thread_alloc();
+    furi_thread_set_name(gps_uart->thread, "GpsUartWorker");
+    furi_thread_set_stack_size(gps_uart->thread, 1024);
+    furi_thread_set_context(gps_uart->thread, gps_uart);
+    furi_thread_set_callback(gps_uart->thread, gps_uart_worker);
+
+    furi_thread_start(gps_uart->thread);
+
+    gps_uart_serial_init(gps_uart);
+}
+
+void gps_uart_deinit_thread(GpsUart* gps_uart) {
+    furi_assert(gps_uart);
+    furi_thread_flags_set(furi_thread_get_id(gps_uart->thread), WorkerEvtStop);
+    furi_thread_join(gps_uart->thread);
+    furi_thread_free(gps_uart->thread);
+}
+
+GpsUart* gps_uart_enable() {
+    GpsUart* gps_uart = malloc(sizeof(GpsUart));
+
+    gps_uart->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    gps_uart->baudrate = gps_baudrates[current_gps_baudrate];
+
+    gps_uart_init_thread(gps_uart);
+
+    return gps_uart;
+}
+
+void gps_uart_disable(GpsUart* gps_uart) {
+    furi_assert(gps_uart);
+    gps_uart_deinit_thread(gps_uart);
+    furi_record_close(RECORD_NOTIFICATION);
+
+    free(gps_uart);
+}

+ 48 - 0
main_apps_sources/gps_nmea_uart/gps_uart.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include <furi_hal.h>
+#include <notification/notification_messages.h>
+
+#define RX_BUF_SIZE 1024
+
+static const int gps_baudrates[5] = {9600, 19200, 38400, 57600, 115200};
+static int current_gps_baudrate = 0;
+
+typedef struct
+{
+    bool valid;
+    float latitude;
+    float longitude;
+    float speed;
+    float course;
+    float altitude;
+    char altitude_units;
+    int fix_quality;
+    int satellites_tracked;
+    int time_hours;
+    int time_minutes;
+    int time_seconds;
+} GpsStatus;
+
+typedef struct
+{
+    FuriMutex *mutex;
+    FuriThread *thread;
+    FuriStreamBuffer *rx_stream;
+    uint8_t rx_buf[RX_BUF_SIZE];
+
+    NotificationApp *notifications;
+    uint32_t baudrate;
+    bool changing_baudrate;
+    bool backlight_on;
+    bool speed_in_kms;
+
+    GpsStatus status;
+} GpsUart;
+
+void gps_uart_init_thread(GpsUart *gps_uart);
+void gps_uart_deinit_thread(GpsUart *gps_uart);
+
+GpsUart *gps_uart_enable();
+
+void gps_uart_disable(GpsUart *gps_uart);

BIN
main_apps_sources/gps_nmea_uart/img/1.png


BIN
main_apps_sources/gps_nmea_uart/img/2.png


BIN
main_apps_sources/gps_nmea_uart/img/3.png


+ 640 - 0
main_apps_sources/gps_nmea_uart/minmea.c

@@ -0,0 +1,640 @@
+/*
+ * Copyright © 2014 Kosma Moczek <kosma@cloudyourcar.com>
+ * This program is free software. It comes without any warranty, to the extent
+ * permitted by applicable law. You can redistribute it and/or modify it under
+ * the terms of the Do What The Fuck You Want To Public License, Version 2, as
+ * published by Sam Hocevar. See the COPYING file for more details.
+ */
+
+#include "minmea.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+#define boolstr(s) ((s) ? "true" : "false")
+
+static int hex2int(char c) {
+    if(c >= '0' && c <= '9') return c - '0';
+    if(c >= 'A' && c <= 'F') return c - 'A' + 10;
+    if(c >= 'a' && c <= 'f') return c - 'a' + 10;
+    return -1;
+}
+
+uint8_t minmea_checksum(const char* sentence) {
+    // Support senteces with or without the starting dollar sign.
+    if(*sentence == '$') sentence++;
+
+    uint8_t checksum = 0x00;
+
+    // The optional checksum is an XOR of all bytes between "$" and "*".
+    while(*sentence && *sentence != '*') checksum ^= *sentence++;
+
+    return checksum;
+}
+
+bool minmea_check(const char* sentence, bool strict) {
+    uint8_t checksum = 0x00;
+
+    // A valid sentence starts with "$".
+    if(*sentence++ != '$') return false;
+
+    // The optional checksum is an XOR of all bytes between "$" and "*".
+    while(*sentence && *sentence != '*' && isprint((unsigned char)*sentence))
+        checksum ^= *sentence++;
+
+    // If checksum is present...
+    if(*sentence == '*') {
+        // Extract checksum.
+        sentence++;
+        int upper = hex2int(*sentence++);
+        if(upper == -1) return false;
+        int lower = hex2int(*sentence++);
+        if(lower == -1) return false;
+        int expected = upper << 4 | lower;
+
+        // Check for checksum mismatch.
+        if(checksum != expected) return false;
+    } else if(strict) {
+        // Discard non-checksummed frames in strict mode.
+        return false;
+    }
+
+    // The only stuff allowed at this point is a newline.
+    while(*sentence == '\r' || *sentence == '\n') {
+        sentence++;
+    }
+
+    if(*sentence) {
+        return false;
+    }
+
+    return true;
+}
+
+bool minmea_scan(const char* sentence, const char* format, ...) {
+    bool result = false;
+    bool optional = false;
+
+    if(sentence == NULL) return false;
+
+    va_list ap;
+    va_start(ap, format);
+
+    const char* field = sentence;
+#define next_field()                                 \
+    do {                                             \
+        /* Progress to the next field. */            \
+        while(minmea_isfield(*sentence)) sentence++; \
+        /* Make sure there is a field there. */      \
+        if(*sentence == ',') {                       \
+            sentence++;                              \
+            field = sentence;                        \
+        } else {                                     \
+            field = NULL;                            \
+        }                                            \
+    } while(0)
+
+    while(*format) {
+        char type = *format++;
+
+        if(type == ';') {
+            // All further fields are optional.
+            optional = true;
+            continue;
+        }
+
+        if(!field && !optional) {
+            // Field requested but we ran out if input. Bail out.
+            goto parse_error;
+        }
+
+        switch(type) {
+        case 'c': { // Single character field (char).
+            char value = '\0';
+
+            if(field && minmea_isfield(*field)) value = *field;
+
+            *va_arg(ap, char*) = value;
+        } break;
+
+        case 'd': { // Single character direction field (int).
+            int value = 0;
+
+            if(field && minmea_isfield(*field)) {
+                switch(*field) {
+                case 'N':
+                case 'E':
+                    value = 1;
+                    break;
+                case 'S':
+                case 'W':
+                    value = -1;
+                    break;
+                default:
+                    goto parse_error;
+                }
+            }
+
+            *va_arg(ap, int*) = value;
+        } break;
+
+        case 'f': { // Fractional value with scale (struct minmea_float).
+            int sign = 0;
+            int_least32_t value = -1;
+            int_least32_t scale = 0;
+
+            if(field) {
+                while(minmea_isfield(*field)) {
+                    if(*field == '+' && !sign && value == -1) {
+                        sign = 1;
+                    } else if(*field == '-' && !sign && value == -1) {
+                        sign = -1;
+                    } else if(isdigit((unsigned char)*field)) {
+                        int digit = *field - '0';
+                        if(value == -1) value = 0;
+                        if(value > (INT_LEAST32_MAX - digit) / 10) {
+                            /* we ran out of bits, what do we do? */
+                            if(scale) {
+                                /* truncate extra precision */
+                                break;
+                            } else {
+                                /* integer overflow. bail out. */
+                                goto parse_error;
+                            }
+                        }
+                        value = (10 * value) + digit;
+                        if(scale) scale *= 10;
+                    } else if(*field == '.' && scale == 0) {
+                        scale = 1;
+                    } else if(*field == ' ') {
+                        /* Allow spaces at the start of the field. Not NMEA
+                             * conformant, but some modules do this. */
+                        if(sign != 0 || value != -1 || scale != 0) goto parse_error;
+                    } else {
+                        goto parse_error;
+                    }
+                    field++;
+                }
+            }
+
+            if((sign || scale) && value == -1) goto parse_error;
+
+            if(value == -1) {
+                /* No digits were scanned. */
+                value = 0;
+                scale = 0;
+            } else if(scale == 0) {
+                /* No decimal point. */
+                scale = 1;
+            }
+            if(sign) value *= sign;
+
+            *va_arg(ap, struct minmea_float*) = (struct minmea_float){value, scale};
+        } break;
+
+        case 'i': { // Integer value, default 0 (int).
+            int value = 0;
+
+            if(field) {
+                char* endptr;
+                value = strtol(field, &endptr, 10);
+                if(minmea_isfield(*endptr)) goto parse_error;
+            }
+
+            *va_arg(ap, int*) = value;
+        } break;
+
+        case 's': { // String value (char *).
+            char* buf = va_arg(ap, char*);
+
+            if(field) {
+                while(minmea_isfield(*field)) *buf++ = *field++;
+            }
+
+            *buf = '\0';
+        } break;
+
+        case 't': { // NMEA talker+sentence identifier (char *).
+            // This field is always mandatory.
+            if(!field) goto parse_error;
+
+            if(field[0] != '$') goto parse_error;
+            for(int f = 0; f < 5; f++)
+                if(!minmea_isfield(field[1 + f])) goto parse_error;
+
+            char* buf = va_arg(ap, char*);
+            memcpy(buf, field + 1, 5);
+            buf[5] = '\0';
+        } break;
+
+        case 'D': { // Date (int, int, int), -1 if empty.
+            struct minmea_date* date = va_arg(ap, struct minmea_date*);
+
+            int d = -1, m = -1, y = -1;
+
+            if(field && minmea_isfield(*field)) {
+                // Always six digits.
+                for(int f = 0; f < 6; f++)
+                    if(!isdigit((unsigned char)field[f])) goto parse_error;
+
+                char dArr[] = {field[0], field[1], '\0'};
+                char mArr[] = {field[2], field[3], '\0'};
+                char yArr[] = {field[4], field[5], '\0'};
+                d = strtol(dArr, NULL, 10);
+                m = strtol(mArr, NULL, 10);
+                y = strtol(yArr, NULL, 10);
+            }
+
+            date->day = d;
+            date->month = m;
+            date->year = y;
+        } break;
+
+        case 'T': { // Time (int, int, int, int), -1 if empty.
+            struct minmea_time* time_ = va_arg(ap, struct minmea_time*);
+
+            int h = -1, i = -1, s = -1, u = -1;
+
+            if(field && minmea_isfield(*field)) {
+                // Minimum required: integer time.
+                for(int f = 0; f < 6; f++)
+                    if(!isdigit((unsigned char)field[f])) goto parse_error;
+
+                char hArr[] = {field[0], field[1], '\0'};
+                char iArr[] = {field[2], field[3], '\0'};
+                char sArr[] = {field[4], field[5], '\0'};
+                h = strtol(hArr, NULL, 10);
+                i = strtol(iArr, NULL, 10);
+                s = strtol(sArr, NULL, 10);
+                field += 6;
+
+                // Extra: fractional time. Saved as microseconds.
+                if(*field++ == '.') {
+                    uint32_t value = 0;
+                    uint32_t scale = 1000000LU;
+                    while(isdigit((unsigned char)*field) && scale > 1) {
+                        value = (value * 10) + (*field++ - '0');
+                        scale /= 10;
+                    }
+                    u = value * scale;
+                } else {
+                    u = 0;
+                }
+            }
+
+            time_->hours = h;
+            time_->minutes = i;
+            time_->seconds = s;
+            time_->microseconds = u;
+        } break;
+
+        case '_': { // Ignore the field.
+        } break;
+
+        default: { // Unknown.
+            goto parse_error;
+        }
+        }
+
+        next_field();
+    }
+
+    result = true;
+
+parse_error:
+    va_end(ap);
+    return result;
+}
+
+bool minmea_talker_id(char talker[3], const char* sentence) {
+    char type[6];
+    if(!minmea_scan(sentence, "t", type)) return false;
+
+    talker[0] = type[0];
+    talker[1] = type[1];
+    talker[2] = '\0';
+
+    return true;
+}
+
+enum minmea_sentence_id minmea_sentence_id(const char* sentence, bool strict) {
+    if(!minmea_check(sentence, strict)) return MINMEA_INVALID;
+
+    char type[6];
+    if(!minmea_scan(sentence, "t", type)) return MINMEA_INVALID;
+
+    if(!strcmp(type + 2, "GBS")) return MINMEA_SENTENCE_GBS;
+    if(!strcmp(type + 2, "GGA")) return MINMEA_SENTENCE_GGA;
+    if(!strcmp(type + 2, "GLL")) return MINMEA_SENTENCE_GLL;
+    if(!strcmp(type + 2, "GSA")) return MINMEA_SENTENCE_GSA;
+    if(!strcmp(type + 2, "GST")) return MINMEA_SENTENCE_GST;
+    if(!strcmp(type + 2, "GSV")) return MINMEA_SENTENCE_GSV;
+    if(!strcmp(type + 2, "RMC")) return MINMEA_SENTENCE_RMC;
+    if(!strcmp(type + 2, "VTG")) return MINMEA_SENTENCE_VTG;
+    if(!strcmp(type + 2, "ZDA")) return MINMEA_SENTENCE_ZDA;
+
+    return MINMEA_UNKNOWN;
+}
+
+bool minmea_parse_gbs(struct minmea_sentence_gbs* frame, const char* sentence) {
+    // $GNGBS,170556.00,3.0,2.9,8.3,,,,*5C
+    char type[6];
+    if(!minmea_scan(
+           sentence,
+           "tTfffifff",
+           type,
+           &frame->time,
+           &frame->err_latitude,
+           &frame->err_longitude,
+           &frame->err_altitude,
+           &frame->svid,
+           &frame->prob,
+           &frame->bias,
+           &frame->stddev))
+        return false;
+    if(strcmp(type + 2, "GBS")) return false;
+
+    return true;
+}
+
+bool minmea_parse_rmc(struct minmea_sentence_rmc* frame, const char* sentence) {
+    // $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
+    char type[6];
+    char validity;
+    int latitude_direction;
+    int longitude_direction;
+    int variation_direction;
+    if(!minmea_scan(
+           sentence,
+           "tTcfdfdffDfd",
+           type,
+           &frame->time,
+           &validity,
+           &frame->latitude,
+           &latitude_direction,
+           &frame->longitude,
+           &longitude_direction,
+           &frame->speed,
+           &frame->course,
+           &frame->date,
+           &frame->variation,
+           &variation_direction))
+        return false;
+    if(strcmp(type + 2, "RMC")) return false;
+
+    frame->valid = (validity == 'A');
+    frame->latitude.value *= latitude_direction;
+    frame->longitude.value *= longitude_direction;
+    frame->variation.value *= variation_direction;
+
+    return true;
+}
+
+bool minmea_parse_gga(struct minmea_sentence_gga* frame, const char* sentence) {
+    // $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
+    char type[6];
+    int latitude_direction;
+    int longitude_direction;
+
+    if(!minmea_scan(
+           sentence,
+           "tTfdfdiiffcfcf_",
+           type,
+           &frame->time,
+           &frame->latitude,
+           &latitude_direction,
+           &frame->longitude,
+           &longitude_direction,
+           &frame->fix_quality,
+           &frame->satellites_tracked,
+           &frame->hdop,
+           &frame->altitude,
+           &frame->altitude_units,
+           &frame->height,
+           &frame->height_units,
+           &frame->dgps_age))
+        return false;
+    if(strcmp(type + 2, "GGA")) return false;
+
+    frame->latitude.value *= latitude_direction;
+    frame->longitude.value *= longitude_direction;
+
+    return true;
+}
+
+bool minmea_parse_gsa(struct minmea_sentence_gsa* frame, const char* sentence) {
+    // $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tciiiiiiiiiiiiifff",
+           type,
+           &frame->mode,
+           &frame->fix_type,
+           &frame->sats[0],
+           &frame->sats[1],
+           &frame->sats[2],
+           &frame->sats[3],
+           &frame->sats[4],
+           &frame->sats[5],
+           &frame->sats[6],
+           &frame->sats[7],
+           &frame->sats[8],
+           &frame->sats[9],
+           &frame->sats[10],
+           &frame->sats[11],
+           &frame->pdop,
+           &frame->hdop,
+           &frame->vdop))
+        return false;
+    if(strcmp(type + 2, "GSA")) return false;
+
+    return true;
+}
+
+bool minmea_parse_gll(struct minmea_sentence_gll* frame, const char* sentence) {
+    // $GPGLL,3723.2475,N,12158.3416,W,161229.487,A,A*41$;
+    char type[6];
+    int latitude_direction;
+    int longitude_direction;
+
+    if(!minmea_scan(
+           sentence,
+           "tfdfdTc;c",
+           type,
+           &frame->latitude,
+           &latitude_direction,
+           &frame->longitude,
+           &longitude_direction,
+           &frame->time,
+           &frame->status,
+           &frame->mode))
+        return false;
+    if(strcmp(type + 2, "GLL")) return false;
+
+    frame->latitude.value *= latitude_direction;
+    frame->longitude.value *= longitude_direction;
+
+    return true;
+}
+
+bool minmea_parse_gst(struct minmea_sentence_gst* frame, const char* sentence) {
+    // $GPGST,024603.00,3.2,6.6,4.7,47.3,5.8,5.6,22.0*58
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tTfffffff",
+           type,
+           &frame->time,
+           &frame->rms_deviation,
+           &frame->semi_major_deviation,
+           &frame->semi_minor_deviation,
+           &frame->semi_major_orientation,
+           &frame->latitude_error_deviation,
+           &frame->longitude_error_deviation,
+           &frame->altitude_error_deviation))
+        return false;
+    if(strcmp(type + 2, "GST")) return false;
+
+    return true;
+}
+
+bool minmea_parse_gsv(struct minmea_sentence_gsv* frame, const char* sentence) {
+    // $GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74
+    // $GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D
+    // $GPGSV,4,2,11,08,51,203,30,09,45,215,28*75
+    // $GPGSV,4,4,13,39,31,170,27*40
+    // $GPGSV,4,4,13*7B
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tiii;iiiiiiiiiiiiiiii",
+           type,
+           &frame->total_msgs,
+           &frame->msg_nr,
+           &frame->total_sats,
+           &frame->sats[0].nr,
+           &frame->sats[0].elevation,
+           &frame->sats[0].azimuth,
+           &frame->sats[0].snr,
+           &frame->sats[1].nr,
+           &frame->sats[1].elevation,
+           &frame->sats[1].azimuth,
+           &frame->sats[1].snr,
+           &frame->sats[2].nr,
+           &frame->sats[2].elevation,
+           &frame->sats[2].azimuth,
+           &frame->sats[2].snr,
+           &frame->sats[3].nr,
+           &frame->sats[3].elevation,
+           &frame->sats[3].azimuth,
+           &frame->sats[3].snr)) {
+        return false;
+    }
+    if(strcmp(type + 2, "GSV")) return false;
+
+    return true;
+}
+
+bool minmea_parse_vtg(struct minmea_sentence_vtg* frame, const char* sentence) {
+    // $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48
+    // $GPVTG,156.1,T,140.9,M,0.0,N,0.0,K*41
+    // $GPVTG,096.5,T,083.5,M,0.0,N,0.0,K,D*22
+    // $GPVTG,188.36,T,,M,0.820,N,1.519,K,A*3F
+    char type[6];
+    char c_true, c_magnetic, c_knots, c_kph, c_faa_mode;
+
+    if(!minmea_scan(
+           sentence,
+           "t;fcfcfcfcc",
+           type,
+           &frame->true_track_degrees,
+           &c_true,
+           &frame->magnetic_track_degrees,
+           &c_magnetic,
+           &frame->speed_knots,
+           &c_knots,
+           &frame->speed_kph,
+           &c_kph,
+           &c_faa_mode))
+        return false;
+    if(strcmp(type + 2, "VTG")) return false;
+    // values are only valid with the accompanying characters
+    if(c_true != 'T') frame->true_track_degrees.scale = 0;
+    if(c_magnetic != 'M') frame->magnetic_track_degrees.scale = 0;
+    if(c_knots != 'N') frame->speed_knots.scale = 0;
+    if(c_kph != 'K') frame->speed_kph.scale = 0;
+    frame->faa_mode = (enum minmea_faa_mode)c_faa_mode;
+
+    return true;
+}
+
+bool minmea_parse_zda(struct minmea_sentence_zda* frame, const char* sentence) {
+    // $GPZDA,201530.00,04,07,2002,00,00*60
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tTiiiii",
+           type,
+           &frame->time,
+           &frame->date.day,
+           &frame->date.month,
+           &frame->date.year,
+           &frame->hour_offset,
+           &frame->minute_offset))
+        return false;
+    if(strcmp(type + 2, "ZDA")) return false;
+
+    // check offsets
+    if(abs(frame->hour_offset) > 13 || frame->minute_offset > 59 || frame->minute_offset < 0)
+        return false;
+
+    return true;
+}
+
+int minmea_getdatetime(
+    struct tm* tm,
+    const struct minmea_date* date,
+    const struct minmea_time* time_) {
+    if(date->year == -1 || time_->hours == -1) return -1;
+
+    memset(tm, 0, sizeof(*tm));
+    if(date->year < 80) {
+        tm->tm_year = 2000 + date->year - 1900; // 2000-2079
+    } else if(date->year >= 1900) {
+        tm->tm_year = date->year - 1900; // 4 digit year, use directly
+    } else {
+        tm->tm_year = date->year; // 1980-1999
+    }
+    tm->tm_mon = date->month - 1;
+    tm->tm_mday = date->day;
+    tm->tm_hour = time_->hours;
+    tm->tm_min = time_->minutes;
+    tm->tm_sec = time_->seconds;
+
+    return 0;
+}
+
+int minmea_gettime(
+    struct timespec* ts,
+    const struct minmea_date* date,
+    const struct minmea_time* time_) {
+    struct tm tm;
+    if(minmea_getdatetime(&tm, date, time_)) return -1;
+
+    time_t timestamp = mktime(&tm); /* See README.md if your system lacks timegm(). */
+    if(timestamp != (time_t)-1) {
+        ts->tv_sec = timestamp;
+        ts->tv_nsec = time_->microseconds * 1000;
+        return 0;
+    } else {
+        return -1;
+    }
+}
+
+/* vim: set ts=4 sw=4 et: */

+ 295 - 0
main_apps_sources/gps_nmea_uart/minmea.h

@@ -0,0 +1,295 @@
+/*
+ * Copyright © 2014 Kosma Moczek <kosma@cloudyourcar.com>
+ * This program is free software. It comes without any warranty, to the extent
+ * permitted by applicable law. You can redistribute it and/or modify it under
+ * the terms of the Do What The Fuck You Want To Public License, Version 2, as
+ * published by Sam Hocevar. See the COPYING file for more details.
+ */
+
+#ifndef MINMEA_H
+#define MINMEA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <time.h>
+#include <math.h>
+#ifdef MINMEA_INCLUDE_COMPAT
+#include <minmea_compat.h>
+#endif
+
+#ifndef MINMEA_MAX_SENTENCE_LENGTH
+#define MINMEA_MAX_SENTENCE_LENGTH 80
+#endif
+
+enum minmea_sentence_id {
+    MINMEA_INVALID = -1,
+    MINMEA_UNKNOWN = 0,
+    MINMEA_SENTENCE_GBS,
+    MINMEA_SENTENCE_GGA,
+    MINMEA_SENTENCE_GLL,
+    MINMEA_SENTENCE_GSA,
+    MINMEA_SENTENCE_GST,
+    MINMEA_SENTENCE_GSV,
+    MINMEA_SENTENCE_RMC,
+    MINMEA_SENTENCE_VTG,
+    MINMEA_SENTENCE_ZDA,
+};
+
+struct minmea_float {
+    int_least32_t value;
+    int_least32_t scale;
+};
+
+struct minmea_date {
+    int day;
+    int month;
+    int year;
+};
+
+struct minmea_time {
+    int hours;
+    int minutes;
+    int seconds;
+    int microseconds;
+};
+
+struct minmea_sentence_gbs {
+    struct minmea_time time;
+    struct minmea_float err_latitude;
+    struct minmea_float err_longitude;
+    struct minmea_float err_altitude;
+    int svid;
+    struct minmea_float prob;
+    struct minmea_float bias;
+    struct minmea_float stddev;
+};
+
+struct minmea_sentence_rmc {
+    struct minmea_time time;
+    bool valid;
+    struct minmea_float latitude;
+    struct minmea_float longitude;
+    struct minmea_float speed;
+    struct minmea_float course;
+    struct minmea_date date;
+    struct minmea_float variation;
+};
+
+struct minmea_sentence_gga {
+    struct minmea_time time;
+    struct minmea_float latitude;
+    struct minmea_float longitude;
+    int fix_quality;
+    int satellites_tracked;
+    struct minmea_float hdop;
+    struct minmea_float altitude;
+    char altitude_units;
+    struct minmea_float height;
+    char height_units;
+    struct minmea_float dgps_age;
+};
+
+enum minmea_gll_status {
+    MINMEA_GLL_STATUS_DATA_VALID = 'A',
+    MINMEA_GLL_STATUS_DATA_NOT_VALID = 'V',
+};
+
+// FAA mode added to some fields in NMEA 2.3.
+enum minmea_faa_mode {
+    MINMEA_FAA_MODE_AUTONOMOUS = 'A',
+    MINMEA_FAA_MODE_DIFFERENTIAL = 'D',
+    MINMEA_FAA_MODE_ESTIMATED = 'E',
+    MINMEA_FAA_MODE_MANUAL = 'M',
+    MINMEA_FAA_MODE_SIMULATED = 'S',
+    MINMEA_FAA_MODE_NOT_VALID = 'N',
+    MINMEA_FAA_MODE_PRECISE = 'P',
+};
+
+struct minmea_sentence_gll {
+    struct minmea_float latitude;
+    struct minmea_float longitude;
+    struct minmea_time time;
+    char status;
+    char mode;
+};
+
+struct minmea_sentence_gst {
+    struct minmea_time time;
+    struct minmea_float rms_deviation;
+    struct minmea_float semi_major_deviation;
+    struct minmea_float semi_minor_deviation;
+    struct minmea_float semi_major_orientation;
+    struct minmea_float latitude_error_deviation;
+    struct minmea_float longitude_error_deviation;
+    struct minmea_float altitude_error_deviation;
+};
+
+enum minmea_gsa_mode {
+    MINMEA_GPGSA_MODE_AUTO = 'A',
+    MINMEA_GPGSA_MODE_FORCED = 'M',
+};
+
+enum minmea_gsa_fix_type {
+    MINMEA_GPGSA_FIX_NONE = 1,
+    MINMEA_GPGSA_FIX_2D = 2,
+    MINMEA_GPGSA_FIX_3D = 3,
+};
+
+struct minmea_sentence_gsa {
+    char mode;
+    int fix_type;
+    int sats[12];
+    struct minmea_float pdop;
+    struct minmea_float hdop;
+    struct minmea_float vdop;
+};
+
+struct minmea_sat_info {
+    int nr;
+    int elevation;
+    int azimuth;
+    int snr;
+};
+
+struct minmea_sentence_gsv {
+    int total_msgs;
+    int msg_nr;
+    int total_sats;
+    struct minmea_sat_info sats[4];
+};
+
+struct minmea_sentence_vtg {
+    struct minmea_float true_track_degrees;
+    struct minmea_float magnetic_track_degrees;
+    struct minmea_float speed_knots;
+    struct minmea_float speed_kph;
+    enum minmea_faa_mode faa_mode;
+};
+
+struct minmea_sentence_zda {
+    struct minmea_time time;
+    struct minmea_date date;
+    int hour_offset;
+    int minute_offset;
+};
+
+/**
+ * Calculate raw sentence checksum. Does not check sentence integrity.
+ */
+uint8_t minmea_checksum(const char* sentence);
+
+/**
+ * Check sentence validity and checksum. Returns true for valid sentences.
+ */
+bool minmea_check(const char* sentence, bool strict);
+
+/**
+ * Determine talker identifier.
+ */
+bool minmea_talker_id(char talker[3], const char* sentence);
+
+/**
+ * Determine sentence identifier.
+ */
+enum minmea_sentence_id minmea_sentence_id(const char* sentence, bool strict);
+
+/**
+ * Scanf-like processor for NMEA sentences. Supports the following formats:
+ * c - single character (char *)
+ * d - direction, returned as 1/-1, default 0 (int *)
+ * f - fractional, returned as value + scale (struct minmea_float *)
+ * i - decimal, default zero (int *)
+ * s - string (char *)
+ * t - talker identifier and type (char *)
+ * D - date (struct minmea_date *)
+ * T - time stamp (struct minmea_time *)
+ * _ - ignore this field
+ * ; - following fields are optional
+ * Returns true on success. See library source code for details.
+ */
+bool minmea_scan(const char* sentence, const char* format, ...);
+
+/*
+ * Parse a specific type of sentence. Return true on success.
+ */
+bool minmea_parse_gbs(struct minmea_sentence_gbs* frame, const char* sentence);
+bool minmea_parse_rmc(struct minmea_sentence_rmc* frame, const char* sentence);
+bool minmea_parse_gga(struct minmea_sentence_gga* frame, const char* sentence);
+bool minmea_parse_gsa(struct minmea_sentence_gsa* frame, const char* sentence);
+bool minmea_parse_gll(struct minmea_sentence_gll* frame, const char* sentence);
+bool minmea_parse_gst(struct minmea_sentence_gst* frame, const char* sentence);
+bool minmea_parse_gsv(struct minmea_sentence_gsv* frame, const char* sentence);
+bool minmea_parse_vtg(struct minmea_sentence_vtg* frame, const char* sentence);
+bool minmea_parse_zda(struct minmea_sentence_zda* frame, const char* sentence);
+
+/**
+ * Convert GPS UTC date/time representation to a UNIX calendar time.
+ */
+int minmea_getdatetime(
+    struct tm* tm,
+    const struct minmea_date* date,
+    const struct minmea_time* time_);
+
+/**
+ * Convert GPS UTC date/time representation to a UNIX timestamp.
+ */
+int minmea_gettime(
+    struct timespec* ts,
+    const struct minmea_date* date,
+    const struct minmea_time* time_);
+
+/**
+ * Rescale a fixed-point value to a different scale. Rounds towards zero.
+ */
+static inline int_least32_t minmea_rescale(const struct minmea_float* f, int_least32_t new_scale) {
+    if(f->scale == 0) return 0;
+    if(f->scale == new_scale) return f->value;
+    if(f->scale > new_scale)
+        return (f->value + ((f->value > 0) - (f->value < 0)) * f->scale / new_scale / 2) /
+               (f->scale / new_scale);
+    else
+        return f->value * (new_scale / f->scale);
+}
+
+/**
+ * Convert a fixed-point value to a floating-point value.
+ * Returns NaN for "unknown" values.
+ */
+static inline float minmea_tofloat(const struct minmea_float* f) {
+    if(f->scale == 0) return NAN;
+    return (float)f->value / (float)f->scale;
+}
+
+/**
+ * Convert a raw coordinate to a floating point DD.DDD... value.
+ * Returns NaN for "unknown" values.
+ */
+static inline float minmea_tocoord(const struct minmea_float* f) {
+    if(f->scale == 0) return NAN;
+    if(f->scale > (INT_LEAST32_MAX / 100)) return NAN;
+    if(f->scale < (INT_LEAST32_MIN / 100)) return NAN;
+    int_least32_t degrees = f->value / (f->scale * 100);
+    int_least32_t minutes = f->value % (f->scale * 100);
+    return (float)degrees + (float)minutes / (60 * f->scale);
+}
+
+/**
+ * Check whether a character belongs to the set of characters allowed in a
+ * sentence data field.
+ */
+static inline bool minmea_isfield(char c) {
+    return isprint((unsigned char)c) && c != ',' && c != '*';
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MINMEA_H */
+
+/* vim: set ts=4 sw=4 et: */

BIN
main_apps_sources/gps_nmea_uart/ui.png


BIN
main_apps_sources/gps_nmea_uart/wiring.png


+ 18 - 0
main_apps_sources/pocsag_pager/README.md

@@ -0,0 +1,18 @@
+# Flipper POCSAG Receiver plugin
+
+App can capture POCSAG 1200 messages using internal or external CC1101 module
+
+Plugin based on Weather Station - https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/plugins/weather_station
+
+Protocol implementation made by @Shmuma
+Icons by @Svaarich
+
+Includes new FM preset built into code - 2FSK with 9.5KHz freq deviation.
+
+Default frequency is set to DAPNET - 439987500
+
+To add new presets and frequencies create file - yourMicroSD/pocsag/settings.txt
+And put THIS - https://github.com/flipperdevices/flipperzero-firmware/blob/dev/assets/resources/subghz/assets/setting_user.example file contents into it, and edit this example for yourself, add needed frequencies
+
+
+All contributions are welcome!

+ 15 - 0
main_apps_sources/pocsag_pager/application.fam

@@ -0,0 +1,15 @@
+App(
+    appid="pocsag_pager",
+    name="POCSAG Pager",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="pocsag_pager_app",
+    requires=["gui"],
+    stack_size=4 * 1024,
+    order=50,
+    fap_icon="pocsag_pager_10px.png",
+    fap_category="Sub-GHz",
+    fap_icon_assets="images",
+    fap_author="@xMasterX & @Shmuma",
+    fap_version="1.0",
+    fap_description="App can capture POCSAG 1200 messages on CC1101 supported frequencies.",
+)

+ 14 - 0
main_apps_sources/pocsag_pager/helpers/pocsag_pager_event.h

@@ -0,0 +1,14 @@
+#pragma once
+
+typedef enum {
+    //PCSGCustomEvent
+    PCSGCustomEventStartId = 100,
+
+    PCSGCustomEventSceneSettingLock,
+
+    PCSGCustomEventViewReceiverOK,
+    PCSGCustomEventViewReceiverConfig,
+    PCSGCustomEventViewReceiverBack,
+    PCSGCustomEventViewReceiverOffDisplay,
+    PCSGCustomEventViewReceiverUnlock,
+} PCSGCustomEvent;

+ 49 - 0
main_apps_sources/pocsag_pager/helpers/pocsag_pager_types.h

@@ -0,0 +1,49 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#define PCSG_VERSION_APP "0.1"
+#define PCSG_DEVELOPED "@xMasterX & @Shmuma"
+#define PCSG_GITHUB "https://github.com/xMasterX/flipper-pager"
+
+#define PCSG_KEY_FILE_VERSION 1
+#define PCSG_KEY_FILE_TYPE "Flipper POCSAG Pager Key File"
+
+/** PCSGRxKeyState state */
+typedef enum {
+    PCSGRxKeyStateIDLE,
+    PCSGRxKeyStateBack,
+    PCSGRxKeyStateStart,
+    PCSGRxKeyStateAddKey,
+} PCSGRxKeyState;
+
+/** PCSGHopperState state */
+typedef enum {
+    PCSGHopperStateOFF,
+    PCSGHopperStateRunnig,
+    PCSGHopperStatePause,
+    PCSGHopperStateRSSITimeOut,
+} PCSGHopperState;
+
+/** PCSGLock */
+typedef enum {
+    PCSGLockOff,
+    PCSGLockOn,
+} PCSGLock;
+
+typedef enum {
+    POCSAGPagerViewVariableItemList,
+    POCSAGPagerViewSubmenu,
+    POCSAGPagerViewReceiver,
+    POCSAGPagerViewReceiverInfo,
+    POCSAGPagerViewWidget,
+} POCSAGPagerView;
+
+/** POCSAGPagerTxRx state */
+typedef enum {
+    PCSGTxRxStateIDLE,
+    PCSGTxRxStateRx,
+    PCSGTxRxStateTx,
+    PCSGTxRxStateSleep,
+} PCSGTxRxState;

+ 66 - 0
main_apps_sources/pocsag_pager/helpers/radio_device_loader.c

@@ -0,0 +1,66 @@
+#include "radio_device_loader.h"
+
+#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+
+static void radio_device_loader_power_on() {
+    uint8_t attempts = 0;
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        //CC1101 power-up time
+        furi_delay_ms(10);
+    }
+}
+
+static void radio_device_loader_power_off() {
+    if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+}
+
+bool radio_device_loader_is_connect_external(const char* name) {
+    bool is_connect = false;
+    bool is_otg_enabled = furi_hal_power_is_otg_enabled();
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_on();
+    }
+
+    is_connect = subghz_devices_is_connect(subghz_devices_get_by_name(name));
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_off();
+    }
+    return is_connect;
+}
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type) {
+    const SubGhzDevice* radio_device;
+
+    if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
+       radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
+        radio_device_loader_power_on();
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
+        subghz_devices_begin(radio_device);
+    } else if(current_radio_device == NULL) {
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    } else {
+        radio_device_loader_end(current_radio_device);
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    }
+
+    return radio_device;
+}
+
+bool radio_device_loader_is_external(const SubGhzDevice* radio_device) {
+    furi_assert(radio_device);
+    return (radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME));
+}
+
+void radio_device_loader_end(const SubGhzDevice* radio_device) {
+    furi_assert(radio_device);
+    radio_device_loader_power_off();
+    if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
+        subghz_devices_end(radio_device);
+    }
+}

+ 17 - 0
main_apps_sources/pocsag_pager/helpers/radio_device_loader.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <lib/subghz/devices/devices.h>
+
+/** SubGhzRadioDeviceType */
+typedef enum {
+    SubGhzRadioDeviceTypeInternal,
+    SubGhzRadioDeviceTypeExternalCC1101,
+} SubGhzRadioDeviceType;
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type);
+
+bool radio_device_loader_is_external(const SubGhzDevice* radio_device);
+
+void radio_device_loader_end(const SubGhzDevice* radio_device);

BIN
main_apps_sources/pocsag_pager/images/Fishing_123x52.png


BIN
main_apps_sources/pocsag_pager/images/Lock_7x8.png


BIN
main_apps_sources/pocsag_pager/images/Message_8x7.png


BIN
main_apps_sources/pocsag_pager/images/Pin_back_arrow_10x8.png


BIN
main_apps_sources/pocsag_pager/images/Quest_7x8.png


BIN
main_apps_sources/pocsag_pager/images/Scanning_123x52.png


BIN
main_apps_sources/pocsag_pager/images/Unlock_7x8.png


BIN
main_apps_sources/pocsag_pager/images/WarningDolphin_45x42.png


BIN
main_apps_sources/pocsag_pager/img/1.png


BIN
main_apps_sources/pocsag_pager/img/2.png


BIN
main_apps_sources/pocsag_pager/img/3.png


BIN
main_apps_sources/pocsag_pager/pocsag_pager_10px.png


+ 206 - 0
main_apps_sources/pocsag_pager/pocsag_pager_app.c

@@ -0,0 +1,206 @@
+#include "pocsag_pager_app_i.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <lib/flipper_format/flipper_format.h>
+#include "protocols/protocol_items.h"
+
+static bool pocsag_pager_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool pocsag_pager_app_back_event_callback(void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void pocsag_pager_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+POCSAGPagerApp* pocsag_pager_app_alloc() {
+    POCSAGPagerApp* app = malloc(sizeof(POCSAGPagerApp));
+
+    // GUI
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // View Dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->scene_manager = scene_manager_alloc(&pocsag_pager_scene_handlers, app);
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, pocsag_pager_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, pocsag_pager_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, pocsag_pager_app_tick_event_callback, 100);
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Variable Item List
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        POCSAGPagerViewVariableItemList,
+        variable_item_list_get_view(app->variable_item_list));
+
+    // SubMenu
+    app->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, POCSAGPagerViewSubmenu, submenu_get_view(app->submenu));
+
+    // Widget
+    app->widget = widget_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, POCSAGPagerViewWidget, widget_get_view(app->widget));
+
+    // Receiver
+    app->pcsg_receiver = pcsg_view_receiver_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        POCSAGPagerViewReceiver,
+        pcsg_view_receiver_get_view(app->pcsg_receiver));
+
+    // Receiver Info
+    app->pcsg_receiver_info = pcsg_view_receiver_info_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        POCSAGPagerViewReceiverInfo,
+        pcsg_view_receiver_info_get_view(app->pcsg_receiver_info));
+
+    //init setting
+    app->setting = subghz_setting_alloc();
+
+    //ToDo FIX  file name setting
+
+    subghz_setting_load(app->setting, EXT_PATH("pocsag/settings.txt"));
+
+    //init Worker & Protocol & History
+    app->lock = PCSGLockOff;
+    app->txrx = malloc(sizeof(POCSAGPagerTxRx));
+    app->txrx->preset = malloc(sizeof(SubGhzRadioPreset));
+    app->txrx->preset->name = furi_string_alloc();
+
+    furi_hal_power_suppress_charge_enter();
+
+    // Radio Devices init & load
+    subghz_devices_init();
+    app->txrx->radio_device =
+        radio_device_loader_set(app->txrx->radio_device, SubGhzRadioDeviceTypeExternalCC1101);
+
+    subghz_devices_reset(app->txrx->radio_device);
+    subghz_devices_idle(app->txrx->radio_device);
+
+    // Custom Presets load without using config file
+
+    FlipperFormat* temp_fm_preset = flipper_format_string_alloc();
+    flipper_format_write_string_cstr(
+        temp_fm_preset,
+        (const char*)"Custom_preset_data",
+        (const char*)"02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00");
+    flipper_format_rewind(temp_fm_preset);
+    subghz_setting_load_custom_preset(app->setting, (const char*)"FM95", temp_fm_preset);
+
+    flipper_format_free(temp_fm_preset);
+
+    // custom presets loading - end
+
+    pcsg_preset_init(app, "FM95", 439987500, NULL, 0);
+
+    app->txrx->hopper_state = PCSGHopperStateOFF;
+    app->txrx->history = pcsg_history_alloc();
+    app->txrx->worker = subghz_worker_alloc();
+    app->txrx->environment = subghz_environment_alloc();
+    subghz_environment_set_protocol_registry(
+        app->txrx->environment, (void*)&pocsag_pager_protocol_registry);
+    app->txrx->receiver = subghz_receiver_alloc_init(app->txrx->environment);
+
+    subghz_receiver_set_filter(app->txrx->receiver, SubGhzProtocolFlag_Decodable);
+    subghz_worker_set_overrun_callback(
+        app->txrx->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
+    subghz_worker_set_pair_callback(
+        app->txrx->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
+    subghz_worker_set_context(app->txrx->worker, app->txrx->receiver);
+
+    scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneStart);
+
+    return app;
+}
+
+void pocsag_pager_app_free(POCSAGPagerApp* app) {
+    furi_assert(app);
+
+    // Radio Devices sleep & off
+    pcsg_sleep(app);
+    radio_device_loader_end(app->txrx->radio_device);
+
+    subghz_devices_deinit();
+
+    // Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewSubmenu);
+    submenu_free(app->submenu);
+
+    // Variable Item List
+    view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewVariableItemList);
+    variable_item_list_free(app->variable_item_list);
+
+    //  Widget
+    view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewWidget);
+    widget_free(app->widget);
+
+    // Receiver
+    view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewReceiver);
+    pcsg_view_receiver_free(app->pcsg_receiver);
+
+    // Receiver Info
+    view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewReceiverInfo);
+    pcsg_view_receiver_info_free(app->pcsg_receiver_info);
+
+    //setting
+    subghz_setting_free(app->setting);
+
+    //Worker & Protocol & History
+    subghz_receiver_free(app->txrx->receiver);
+    subghz_environment_free(app->txrx->environment);
+    pcsg_history_free(app->txrx->history);
+    subghz_worker_free(app->txrx->worker);
+    furi_string_free(app->txrx->preset->name);
+    free(app->txrx->preset);
+    free(app->txrx);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    app->notifications = NULL;
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+
+    furi_hal_power_suppress_charge_exit();
+
+    free(app);
+}
+
+int32_t pocsag_pager_app(void* p) {
+    UNUSED(p);
+    POCSAGPagerApp* pocsag_pager_app = pocsag_pager_app_alloc();
+
+    view_dispatcher_run(pocsag_pager_app->view_dispatcher);
+
+    pocsag_pager_app_free(pocsag_pager_app);
+
+    return 0;
+}

+ 146 - 0
main_apps_sources/pocsag_pager/pocsag_pager_app_i.c

@@ -0,0 +1,146 @@
+#include "pocsag_pager_app_i.h"
+
+#define TAG "POCSAGPager"
+#include <flipper_format/flipper_format_i.h>
+
+void pcsg_preset_init(
+    void* context,
+    const char* preset_name,
+    uint32_t frequency,
+    uint8_t* preset_data,
+    size_t preset_data_size) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    furi_string_set(app->txrx->preset->name, preset_name);
+    app->txrx->preset->frequency = frequency;
+    app->txrx->preset->data = preset_data;
+    app->txrx->preset->data_size = preset_data_size;
+}
+
+void pcsg_get_frequency_modulation(
+    POCSAGPagerApp* app,
+    FuriString* frequency,
+    FuriString* modulation) {
+    furi_assert(app);
+    if(frequency != NULL) {
+        furi_string_printf(
+            frequency,
+            "%03ld.%02ld",
+            app->txrx->preset->frequency / 1000000 % 1000,
+            app->txrx->preset->frequency / 10000 % 100);
+    }
+    if(modulation != NULL) {
+        furi_string_printf(modulation, "%.2s", furi_string_get_cstr(app->txrx->preset->name));
+    }
+}
+
+void pcsg_begin(POCSAGPagerApp* app, uint8_t* preset_data) {
+    furi_assert(app);
+
+    subghz_devices_reset(app->txrx->radio_device);
+    subghz_devices_idle(app->txrx->radio_device);
+    subghz_devices_load_preset(app->txrx->radio_device, FuriHalSubGhzPresetCustom, preset_data);
+
+    // furi_hal_gpio_init(furi_hal_subghz.cc1101_g0_pin, GpioModeInput, GpioPullNo, GpioSpeedLow);
+    app->txrx->txrx_state = PCSGTxRxStateIDLE;
+}
+
+uint32_t pcsg_rx(POCSAGPagerApp* app, uint32_t frequency) {
+    furi_assert(app);
+    if(!subghz_devices_is_frequency_valid(app->txrx->radio_device, frequency)) {
+        furi_crash("POCSAGPager: Incorrect RX frequency.");
+    }
+    furi_assert(
+        app->txrx->txrx_state != PCSGTxRxStateRx && app->txrx->txrx_state != PCSGTxRxStateSleep);
+
+    subghz_devices_idle(app->txrx->radio_device);
+    uint32_t value = subghz_devices_set_frequency(app->txrx->radio_device, frequency);
+
+    // Not need. init in subghz_devices_start_async_tx
+    // furi_hal_gpio_init(furi_hal_subghz.cc1101_g0_pin, GpioModeInput, GpioPullNo, GpioSpeedLow);
+
+    subghz_devices_flush_rx(app->txrx->radio_device);
+    subghz_devices_set_rx(app->txrx->radio_device);
+
+    subghz_devices_start_async_rx(
+        app->txrx->radio_device, subghz_worker_rx_callback, app->txrx->worker);
+    subghz_worker_start(app->txrx->worker);
+    app->txrx->txrx_state = PCSGTxRxStateRx;
+    return value;
+}
+
+void pcsg_idle(POCSAGPagerApp* app) {
+    furi_assert(app);
+    furi_assert(app->txrx->txrx_state != PCSGTxRxStateSleep);
+    subghz_devices_idle(app->txrx->radio_device);
+    app->txrx->txrx_state = PCSGTxRxStateIDLE;
+}
+
+void pcsg_rx_end(POCSAGPagerApp* app) {
+    furi_assert(app);
+    furi_assert(app->txrx->txrx_state == PCSGTxRxStateRx);
+    if(subghz_worker_is_running(app->txrx->worker)) {
+        subghz_worker_stop(app->txrx->worker);
+        subghz_devices_stop_async_rx(app->txrx->radio_device);
+    }
+    subghz_devices_idle(app->txrx->radio_device);
+    app->txrx->txrx_state = PCSGTxRxStateIDLE;
+}
+
+void pcsg_sleep(POCSAGPagerApp* app) {
+    furi_assert(app);
+    subghz_devices_sleep(app->txrx->radio_device);
+    app->txrx->txrx_state = PCSGTxRxStateSleep;
+}
+
+void pcsg_hopper_update(POCSAGPagerApp* app) {
+    furi_assert(app);
+
+    switch(app->txrx->hopper_state) {
+    case PCSGHopperStateOFF:
+        return;
+        break;
+    case PCSGHopperStatePause:
+        return;
+        break;
+    case PCSGHopperStateRSSITimeOut:
+        if(app->txrx->hopper_timeout != 0) {
+            app->txrx->hopper_timeout--;
+            return;
+        }
+        break;
+    default:
+        break;
+    }
+    float rssi = -127.0f;
+    if(app->txrx->hopper_state != PCSGHopperStateRSSITimeOut) {
+        // See RSSI Calculation timings in CC1101 17.3 RSSI
+        rssi = subghz_devices_get_rssi(app->txrx->radio_device);
+
+        // Stay if RSSI is high enough
+        if(rssi > -90.0f) {
+            app->txrx->hopper_timeout = 10;
+            app->txrx->hopper_state = PCSGHopperStateRSSITimeOut;
+            return;
+        }
+    } else {
+        app->txrx->hopper_state = PCSGHopperStateRunnig;
+    }
+    // Select next frequency
+    if(app->txrx->hopper_idx_frequency <
+       subghz_setting_get_hopper_frequency_count(app->setting) - 1) {
+        app->txrx->hopper_idx_frequency++;
+    } else {
+        app->txrx->hopper_idx_frequency = 0;
+    }
+
+    if(app->txrx->txrx_state == PCSGTxRxStateRx) {
+        pcsg_rx_end(app);
+    };
+    if(app->txrx->txrx_state == PCSGTxRxStateIDLE) {
+        subghz_receiver_reset(app->txrx->receiver);
+        app->txrx->preset->frequency =
+            subghz_setting_get_hopper_frequency(app->setting, app->txrx->hopper_idx_frequency);
+        pcsg_rx(app, app->txrx->preset->frequency);
+    }
+}

+ 76 - 0
main_apps_sources/pocsag_pager/pocsag_pager_app_i.h

@@ -0,0 +1,76 @@
+#pragma once
+
+#include "helpers/pocsag_pager_types.h"
+#include "helpers/radio_device_loader.h"
+
+#include "scenes/pocsag_pager_scene.h"
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <notification/notification_messages.h>
+#include "views/pocsag_pager_receiver.h"
+#include "views/pocsag_pager_receiver_info.h"
+#include "pocsag_pager_history.h"
+
+#include <lib/subghz/subghz_setting.h>
+#include <lib/subghz/subghz_worker.h>
+#include <lib/subghz/receiver.h>
+#include <lib/subghz/transmitter.h>
+#include <lib/subghz/registry.h>
+#include <lib/subghz/devices/devices.h>
+
+typedef struct POCSAGPagerApp POCSAGPagerApp;
+
+struct POCSAGPagerTxRx {
+    SubGhzWorker* worker;
+
+    SubGhzEnvironment* environment;
+    SubGhzReceiver* receiver;
+    SubGhzRadioPreset* preset;
+    PCSGHistory* history;
+    uint16_t idx_menu_chosen;
+    PCSGTxRxState txrx_state;
+    PCSGHopperState hopper_state;
+    uint8_t hopper_timeout;
+    uint8_t hopper_idx_frequency;
+    PCSGRxKeyState rx_key_state;
+
+    const SubGhzDevice* radio_device;
+};
+
+typedef struct POCSAGPagerTxRx POCSAGPagerTxRx;
+
+struct POCSAGPagerApp {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    POCSAGPagerTxRx* txrx;
+    SceneManager* scene_manager;
+    NotificationApp* notifications;
+    VariableItemList* variable_item_list;
+    Submenu* submenu;
+    Widget* widget;
+    PCSGReceiver* pcsg_receiver;
+    PCSGReceiverInfo* pcsg_receiver_info;
+    PCSGLock lock;
+    SubGhzSetting* setting;
+};
+
+void pcsg_preset_init(
+    void* context,
+    const char* preset_name,
+    uint32_t frequency,
+    uint8_t* preset_data,
+    size_t preset_data_size);
+void pcsg_get_frequency_modulation(
+    POCSAGPagerApp* app,
+    FuriString* frequency,
+    FuriString* modulation);
+void pcsg_begin(POCSAGPagerApp* app, uint8_t* preset_data);
+uint32_t pcsg_rx(POCSAGPagerApp* app, uint32_t frequency);
+void pcsg_idle(POCSAGPagerApp* app);
+void pcsg_rx_end(POCSAGPagerApp* app);
+void pcsg_sleep(POCSAGPagerApp* app);
+void pcsg_hopper_update(POCSAGPagerApp* app);

+ 223 - 0
main_apps_sources/pocsag_pager/pocsag_pager_history.c

@@ -0,0 +1,223 @@
+#include "pocsag_pager_history.h"
+#include <flipper_format/flipper_format_i.h>
+#include <lib/toolbox/stream/stream.h>
+#include <lib/subghz/receiver.h>
+#include "protocols/pcsg_generic.h"
+
+#include <furi.h>
+
+#define PCSG_HISTORY_MAX 50
+#define TAG "PCSGHistory"
+
+typedef struct {
+    FuriString* item_str;
+    FlipperFormat* flipper_string;
+    uint8_t type;
+    SubGhzRadioPreset* preset;
+} PCSGHistoryItem;
+
+ARRAY_DEF(PCSGHistoryItemArray, PCSGHistoryItem, M_POD_OPLIST)
+
+#define M_OPL_PCSGHistoryItemArray_t() ARRAY_OPLIST(PCSGHistoryItemArray, M_POD_OPLIST)
+
+typedef struct {
+    PCSGHistoryItemArray_t data;
+} PCSGHistoryStruct;
+
+struct PCSGHistory {
+    uint32_t last_update_timestamp;
+    uint16_t last_index_write;
+    uint8_t code_last_hash_data;
+    FuriString* tmp_string;
+    PCSGHistoryStruct* history;
+};
+
+PCSGHistory* pcsg_history_alloc(void) {
+    PCSGHistory* instance = malloc(sizeof(PCSGHistory));
+    instance->tmp_string = furi_string_alloc();
+    instance->history = malloc(sizeof(PCSGHistoryStruct));
+    PCSGHistoryItemArray_init(instance->history->data);
+    return instance;
+}
+
+void pcsg_history_free(PCSGHistory* instance) {
+    furi_assert(instance);
+    furi_string_free(instance->tmp_string);
+    for
+        M_EACH(item, instance->history->data, PCSGHistoryItemArray_t) {
+            furi_string_free(item->item_str);
+            furi_string_free(item->preset->name);
+            free(item->preset);
+            flipper_format_free(item->flipper_string);
+            item->type = 0;
+        }
+    PCSGHistoryItemArray_clear(instance->history->data);
+    free(instance->history);
+    free(instance);
+}
+
+uint32_t pcsg_history_get_frequency(PCSGHistory* instance, uint16_t idx) {
+    furi_assert(instance);
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    return item->preset->frequency;
+}
+
+SubGhzRadioPreset* pcsg_history_get_radio_preset(PCSGHistory* instance, uint16_t idx) {
+    furi_assert(instance);
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    return item->preset;
+}
+
+const char* pcsg_history_get_preset(PCSGHistory* instance, uint16_t idx) {
+    furi_assert(instance);
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    return furi_string_get_cstr(item->preset->name);
+}
+
+void pcsg_history_reset(PCSGHistory* instance) {
+    furi_assert(instance);
+    furi_string_reset(instance->tmp_string);
+    for
+        M_EACH(item, instance->history->data, PCSGHistoryItemArray_t) {
+            furi_string_free(item->item_str);
+            furi_string_free(item->preset->name);
+            free(item->preset);
+            flipper_format_free(item->flipper_string);
+            item->type = 0;
+        }
+    PCSGHistoryItemArray_reset(instance->history->data);
+    instance->last_index_write = 0;
+    instance->code_last_hash_data = 0;
+}
+
+uint16_t pcsg_history_get_item(PCSGHistory* instance) {
+    furi_assert(instance);
+    return instance->last_index_write;
+}
+
+uint8_t pcsg_history_get_type_protocol(PCSGHistory* instance, uint16_t idx) {
+    furi_assert(instance);
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    return item->type;
+}
+
+const char* pcsg_history_get_protocol_name(PCSGHistory* instance, uint16_t idx) {
+    furi_assert(instance);
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    flipper_format_rewind(item->flipper_string);
+    if(!flipper_format_read_string(item->flipper_string, "Protocol", instance->tmp_string)) {
+        FURI_LOG_E(TAG, "Missing Protocol");
+        furi_string_reset(instance->tmp_string);
+    }
+    return furi_string_get_cstr(instance->tmp_string);
+}
+
+FlipperFormat* pcsg_history_get_raw_data(PCSGHistory* instance, uint16_t idx) {
+    furi_assert(instance);
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    if(item->flipper_string) {
+        return item->flipper_string;
+    } else {
+        return NULL;
+    }
+}
+bool pcsg_history_get_text_space_left(PCSGHistory* instance, FuriString* output) {
+    furi_assert(instance);
+    if(instance->last_index_write == PCSG_HISTORY_MAX) {
+        if(output != NULL) furi_string_printf(output, "Memory is FULL");
+        return true;
+    }
+    if(output != NULL)
+        furi_string_printf(output, "%02u/%02u", instance->last_index_write, PCSG_HISTORY_MAX);
+    return false;
+}
+
+void pcsg_history_get_text_item_menu(PCSGHistory* instance, FuriString* output, uint16_t idx) {
+    PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx);
+    furi_string_set(output, item->item_str);
+}
+
+PCSGHistoryStateAddKey
+    pcsg_history_add_to_history(PCSGHistory* instance, void* context, SubGhzRadioPreset* preset) {
+    furi_assert(instance);
+    furi_assert(context);
+
+    if(instance->last_index_write >= PCSG_HISTORY_MAX) return PCSGHistoryStateAddKeyOverflow;
+
+    SubGhzProtocolDecoderBase* decoder_base = context;
+    if((instance->code_last_hash_data ==
+        subghz_protocol_decoder_base_get_hash_data(decoder_base)) &&
+       ((furi_get_tick() - instance->last_update_timestamp) < 500)) {
+        instance->last_update_timestamp = furi_get_tick();
+        return PCSGHistoryStateAddKeyTimeOut;
+    }
+
+    instance->code_last_hash_data = subghz_protocol_decoder_base_get_hash_data(decoder_base);
+    instance->last_update_timestamp = furi_get_tick();
+
+    FlipperFormat* fff = flipper_format_string_alloc();
+    subghz_protocol_decoder_base_serialize(decoder_base, fff, preset);
+
+    do {
+        if(!flipper_format_rewind(fff)) {
+            FURI_LOG_E(TAG, "Rewind error");
+            break;
+        }
+
+    } while(false);
+    flipper_format_free(fff);
+
+    PCSGHistoryItem* item = PCSGHistoryItemArray_push_raw(instance->history->data);
+    item->preset = malloc(sizeof(SubGhzRadioPreset));
+    item->type = decoder_base->protocol->type;
+    item->preset->frequency = preset->frequency;
+    item->preset->name = furi_string_alloc();
+    furi_string_set(item->preset->name, preset->name);
+    item->preset->data = preset->data;
+    item->preset->data_size = preset->data_size;
+
+    item->item_str = furi_string_alloc();
+    item->flipper_string = flipper_format_string_alloc();
+    subghz_protocol_decoder_base_serialize(decoder_base, item->flipper_string, preset);
+
+    do {
+        if(!flipper_format_rewind(item->flipper_string)) {
+            FURI_LOG_E(TAG, "Rewind error");
+            break;
+        }
+        if(!flipper_format_read_string(item->flipper_string, "Protocol", instance->tmp_string)) {
+            FURI_LOG_E(TAG, "Missing Protocol");
+            break;
+        }
+
+        if(!flipper_format_rewind(item->flipper_string)) {
+            FURI_LOG_E(TAG, "Rewind error");
+            break;
+        }
+        FuriString* temp_ric = furi_string_alloc();
+        if(!flipper_format_read_string(item->flipper_string, "Ric", temp_ric)) {
+            FURI_LOG_E(TAG, "Missing Ric");
+            break;
+        }
+
+        FuriString* temp_message = furi_string_alloc();
+        if(!flipper_format_read_string(item->flipper_string, "Message", temp_message)) {
+            FURI_LOG_E(TAG, "Missing Message");
+            break;
+        }
+
+        furi_string_printf(
+            item->item_str,
+            "%s%s",
+            furi_string_get_cstr(temp_ric),
+            furi_string_get_cstr(temp_message));
+
+        furi_string_free(temp_message);
+        furi_string_free(temp_ric);
+
+    } while(false);
+    instance->last_index_write++;
+    return PCSGHistoryStateAddKeyNewDada;
+
+    return PCSGHistoryStateAddKeyUnknown;
+}

+ 112 - 0
main_apps_sources/pocsag_pager/pocsag_pager_history.h

@@ -0,0 +1,112 @@
+
+#pragma once
+
+#include <math.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <lib/flipper_format/flipper_format.h>
+#include <lib/subghz/types.h>
+
+typedef struct PCSGHistory PCSGHistory;
+
+/** History state add key */
+typedef enum {
+    PCSGHistoryStateAddKeyUnknown,
+    PCSGHistoryStateAddKeyTimeOut,
+    PCSGHistoryStateAddKeyNewDada,
+    PCSGHistoryStateAddKeyUpdateData,
+    PCSGHistoryStateAddKeyOverflow,
+} PCSGHistoryStateAddKey;
+
+/** Allocate PCSGHistory
+ * 
+ * @return PCSGHistory* 
+ */
+PCSGHistory* pcsg_history_alloc(void);
+
+/** Free PCSGHistory
+ * 
+ * @param instance - PCSGHistory instance
+ */
+void pcsg_history_free(PCSGHistory* instance);
+
+/** Clear history
+ * 
+ * @param instance - PCSGHistory instance
+ */
+void pcsg_history_reset(PCSGHistory* instance);
+
+/** Get frequency to history[idx]
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param idx       - record index  
+ * @return frequency - frequency Hz
+ */
+uint32_t pcsg_history_get_frequency(PCSGHistory* instance, uint16_t idx);
+
+SubGhzRadioPreset* pcsg_history_get_radio_preset(PCSGHistory* instance, uint16_t idx);
+
+/** Get preset to history[idx]
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param idx       - record index  
+ * @return preset   - preset name
+ */
+const char* pcsg_history_get_preset(PCSGHistory* instance, uint16_t idx);
+
+/** Get history index write 
+ * 
+ * @param instance  - PCSGHistory instance
+ * @return idx      - current record index  
+ */
+uint16_t pcsg_history_get_item(PCSGHistory* instance);
+
+/** Get type protocol to history[idx]
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param idx       - record index  
+ * @return type      - type protocol  
+ */
+uint8_t pcsg_history_get_type_protocol(PCSGHistory* instance, uint16_t idx);
+
+/** Get name protocol to history[idx]
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param idx       - record index  
+ * @return name      - const char* name protocol  
+ */
+const char* pcsg_history_get_protocol_name(PCSGHistory* instance, uint16_t idx);
+
+/** Get string item menu to history[idx]
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param output    - FuriString* output
+ * @param idx       - record index
+ */
+void pcsg_history_get_text_item_menu(PCSGHistory* instance, FuriString* output, uint16_t idx);
+
+/** Get string the remaining number of records to history
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param output    - FuriString* output
+ * @return bool - is FUUL
+ */
+bool pcsg_history_get_text_space_left(PCSGHistory* instance, FuriString* output);
+
+/** Add protocol to history
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param context    - SubGhzProtocolCommon context
+ * @param preset    - SubGhzRadioPreset preset
+ * @return PCSGHistoryStateAddKey;
+ */
+PCSGHistoryStateAddKey
+    pcsg_history_add_to_history(PCSGHistory* instance, void* context, SubGhzRadioPreset* preset);
+
+/** Get SubGhzProtocolCommonLoad to load into the protocol decoder bin data
+ * 
+ * @param instance  - PCSGHistory instance
+ * @param idx       - record index
+ * @return SubGhzProtocolCommonLoad*
+ */
+FlipperFormat* pcsg_history_get_raw_data(PCSGHistory* instance, uint16_t idx);

+ 124 - 0
main_apps_sources/pocsag_pager/protocols/pcsg_generic.c

@@ -0,0 +1,124 @@
+#include "pcsg_generic.h"
+#include <lib/toolbox/stream/stream.h>
+#include <lib/flipper_format/flipper_format_i.h>
+#include "../helpers/pocsag_pager_types.h"
+
+#define TAG "PCSGBlockGeneric"
+
+void pcsg_block_generic_get_preset_name(const char* preset_name, FuriString* preset_str) {
+    const char* preset_name_temp;
+    if(!strcmp(preset_name, "AM270")) {
+        preset_name_temp = "FuriHalSubGhzPresetOok270Async";
+    } else if(!strcmp(preset_name, "AM650")) {
+        preset_name_temp = "FuriHalSubGhzPresetOok650Async";
+    } else if(!strcmp(preset_name, "FM238")) {
+        preset_name_temp = "FuriHalSubGhzPreset2FSKDev238Async";
+    } else if(!strcmp(preset_name, "FM476")) {
+        preset_name_temp = "FuriHalSubGhzPreset2FSKDev476Async";
+    } else {
+        preset_name_temp = "FuriHalSubGhzPresetCustom";
+    }
+    furi_string_set(preset_str, preset_name_temp);
+}
+
+SubGhzProtocolStatus pcsg_block_generic_serialize(
+    PCSGBlockGeneric* instance,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset) {
+    furi_assert(instance);
+    SubGhzProtocolStatus res = SubGhzProtocolStatusError;
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    do {
+        stream_clean(flipper_format_get_raw_stream(flipper_format));
+        if(!flipper_format_write_header_cstr(
+               flipper_format, PCSG_KEY_FILE_TYPE, PCSG_KEY_FILE_VERSION)) {
+            FURI_LOG_E(TAG, "Unable to add header");
+            break;
+        }
+
+        if(!flipper_format_write_uint32(flipper_format, "Frequency", &preset->frequency, 1)) {
+            FURI_LOG_E(TAG, "Unable to add Frequency");
+            break;
+        }
+
+        pcsg_block_generic_get_preset_name(furi_string_get_cstr(preset->name), temp_str);
+        if(!flipper_format_write_string_cstr(
+               flipper_format, "Preset", furi_string_get_cstr(temp_str))) {
+            FURI_LOG_E(TAG, "Unable to add Preset");
+            break;
+        }
+        if(!strcmp(furi_string_get_cstr(temp_str), "FuriHalSubGhzPresetCustom")) {
+            if(!flipper_format_write_string_cstr(
+                   flipper_format, "Custom_preset_module", "CC1101")) {
+                FURI_LOG_E(TAG, "Unable to add Custom_preset_module");
+                break;
+            }
+            if(!flipper_format_write_hex(
+                   flipper_format, "Custom_preset_data", preset->data, preset->data_size)) {
+                FURI_LOG_E(TAG, "Unable to add Custom_preset_data");
+                break;
+            }
+        }
+        if(!flipper_format_write_string_cstr(flipper_format, "Protocol", instance->protocol_name)) {
+            FURI_LOG_E(TAG, "Unable to add Protocol");
+            break;
+        }
+
+        if(!flipper_format_write_string(flipper_format, "Ric", instance->result_ric)) {
+            FURI_LOG_E(TAG, "Unable to add Ric");
+            break;
+        }
+
+        if(!flipper_format_write_string(flipper_format, "Message", instance->result_msg)) {
+            FURI_LOG_E(TAG, "Unable to add Message");
+            break;
+        }
+
+        res = SubGhzProtocolStatusOk;
+    } while(false);
+    furi_string_free(temp_str);
+    return res;
+}
+
+SubGhzProtocolStatus
+    pcsg_block_generic_deserialize(PCSGBlockGeneric* instance, FlipperFormat* flipper_format) {
+    furi_assert(instance);
+    SubGhzProtocolStatus res = SubGhzProtocolStatusError;
+    FuriString* temp_data = furi_string_alloc();
+    FuriString* temp_data2 = furi_string_alloc();
+
+    do {
+        if(!flipper_format_rewind(flipper_format)) {
+            FURI_LOG_E(TAG, "Rewind error");
+            break;
+        }
+
+        if(!flipper_format_read_string(flipper_format, "Ric", temp_data2)) {
+            FURI_LOG_E(TAG, "Missing Ric");
+            break;
+        }
+        if(instance->result_ric != NULL) {
+            furi_string_set(instance->result_ric, temp_data2);
+        } else {
+            instance->result_ric = furi_string_alloc_set(temp_data2);
+        }
+
+        if(!flipper_format_read_string(flipper_format, "Message", temp_data)) {
+            FURI_LOG_E(TAG, "Missing Message");
+            break;
+        }
+        if(instance->result_msg != NULL) {
+            furi_string_set(instance->result_msg, temp_data);
+        } else {
+            instance->result_msg = furi_string_alloc_set(temp_data);
+        }
+
+        res = SubGhzProtocolStatusOk;
+    } while(0);
+
+    furi_string_free(temp_data);
+    furi_string_free(temp_data2);
+
+    return res;
+}

+ 56 - 0
main_apps_sources/pocsag_pager/protocols/pcsg_generic.h

@@ -0,0 +1,56 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stddef.h>
+
+#include <lib/flipper_format/flipper_format.h>
+#include "furi.h"
+#include "furi_hal.h"
+#include <lib/subghz/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct PCSGBlockGeneric PCSGBlockGeneric;
+
+struct PCSGBlockGeneric {
+    const char* protocol_name;
+    FuriString* result_ric;
+    FuriString* result_msg;
+};
+
+/**
+ * Get name preset.
+ * @param preset_name name preset
+ * @param preset_str Output name preset
+ */
+void pcsg_block_generic_get_preset_name(const char* preset_name, FuriString* preset_str);
+
+/**
+ * Serialize data PCSGBlockGeneric.
+ * @param instance Pointer to a PCSGBlockGeneric instance
+ * @param flipper_format Pointer to a FlipperFormat instance
+ * @param preset The modulation on which the signal was received, SubGhzRadioPreset
+ * @return true On success
+ */
+SubGhzProtocolStatus pcsg_block_generic_serialize(
+    PCSGBlockGeneric* instance,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset);
+
+/**
+ * Deserialize data PCSGBlockGeneric.
+ * @param instance Pointer to a PCSGBlockGeneric instance
+ * @param flipper_format Pointer to a FlipperFormat instance
+ * @return true On success
+ */
+SubGhzProtocolStatus
+    pcsg_block_generic_deserialize(PCSGBlockGeneric* instance, FlipperFormat* flipper_format);
+
+float pcsg_block_generic_fahrenheit_to_celsius(float fahrenheit);
+
+#ifdef __cplusplus
+}
+#endif

+ 374 - 0
main_apps_sources/pocsag_pager/protocols/pocsag.c

@@ -0,0 +1,374 @@
+#include "pocsag.h"
+
+#include <inttypes.h>
+#include <lib/flipper_format/flipper_format_i.h>
+#include <furi/core/string.h>
+
+#define TAG "POCSAG"
+
+static const SubGhzBlockConst pocsag_const = {
+    .te_short = 833,
+    .te_delta = 100,
+};
+
+// Minimal amount of sync bits (interleaving zeros and ones)
+#define POCSAG_MIN_SYNC_BITS 32
+#define POCSAG_CW_BITS 32
+#define POCSAG_CW_MASK 0xFFFFFFFF
+#define POCSAG_FRAME_SYNC_CODE 0x7CD215D8
+#define POCSAG_IDLE_CODE_WORD 0x7A89C197
+
+#define POCSAG_FUNC_NUM 0
+#define POCSAG_FUNC_ALERT1 1
+#define POCSAG_FUNC_ALERT2 2
+#define POCSAG_FUNC_ALPHANUM 3
+
+static const char* func_msg[] = {"\e#Num:\e# ", "\e#Alert\e#", "\e#Alert:\e# ", "\e#Msg:\e# "};
+static const char* bcd_chars = "*U -)(";
+
+struct SubGhzProtocolDecoderPocsag {
+    SubGhzProtocolDecoderBase base;
+
+    SubGhzBlockDecoder decoder;
+    PCSGBlockGeneric generic;
+
+    uint8_t codeword_idx;
+    uint32_t ric;
+    uint8_t func;
+
+    // partially decoded character
+    uint8_t char_bits;
+    uint8_t char_data;
+
+    // message being decoded
+    FuriString* msg;
+
+    // Done messages, ready to be serialized/deserialized
+    FuriString* done_msg;
+};
+
+typedef struct SubGhzProtocolDecoderPocsag SubGhzProtocolDecoderPocsag;
+
+typedef enum {
+    PocsagDecoderStepReset = 0,
+    PocsagDecoderStepFoundSync,
+    PocsagDecoderStepFoundPreamble,
+    PocsagDecoderStepMessage,
+} PocsagDecoderStep;
+
+void* subghz_protocol_decoder_pocsag_alloc(SubGhzEnvironment* environment) {
+    UNUSED(environment);
+
+    SubGhzProtocolDecoderPocsag* instance = malloc(sizeof(SubGhzProtocolDecoderPocsag));
+    instance->base.protocol = &subghz_protocol_pocsag;
+    instance->generic.protocol_name = instance->base.protocol->name;
+    instance->msg = furi_string_alloc();
+    instance->done_msg = furi_string_alloc();
+    if(instance->generic.result_msg == NULL) {
+        instance->generic.result_msg = furi_string_alloc();
+    }
+    if(instance->generic.result_ric == NULL) {
+        instance->generic.result_ric = furi_string_alloc();
+    }
+
+    return instance;
+}
+
+void subghz_protocol_decoder_pocsag_free(void* context) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+    furi_string_free(instance->msg);
+    furi_string_free(instance->done_msg);
+    free(instance);
+}
+
+void subghz_protocol_decoder_pocsag_reset(void* context) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+
+    instance->decoder.parser_step = PocsagDecoderStepReset;
+    instance->decoder.decode_data = 0UL;
+    instance->decoder.decode_count_bit = 0;
+    instance->codeword_idx = 0;
+    instance->char_bits = 0;
+    instance->char_data = 0;
+    furi_string_reset(instance->msg);
+    furi_string_reset(instance->done_msg);
+    furi_string_reset(instance->generic.result_msg);
+    furi_string_reset(instance->generic.result_ric);
+}
+
+static void pocsag_decode_address_word(SubGhzProtocolDecoderPocsag* instance, uint32_t data) {
+    instance->ric = (data >> 13);
+    instance->ric = (instance->ric << 3) | (instance->codeword_idx >> 1);
+    instance->func = (data >> 11) & 0b11;
+}
+
+static bool decode_message_alphanumeric(SubGhzProtocolDecoderPocsag* instance, uint32_t data) {
+    for(uint8_t i = 0; i < 20; i++) {
+        instance->char_data >>= 1;
+        if(data & (1 << 30)) {
+            instance->char_data |= 1 << 6;
+        }
+        instance->char_bits++;
+        if(instance->char_bits == 7) {
+            if(instance->char_data == 0) return false;
+            furi_string_push_back(instance->msg, instance->char_data);
+            instance->char_data = 0;
+            instance->char_bits = 0;
+        }
+        data <<= 1;
+    }
+    return true;
+}
+
+static void decode_message_numeric(SubGhzProtocolDecoderPocsag* instance, uint32_t data) {
+    // 5 groups with 4 bits each
+    uint8_t val;
+    for(uint8_t i = 0; i < 5; i++) {
+        val = (data >> (27 - i * 4)) & 0b1111;
+        // reverse the order of 4 bits
+        val = (val & 0x5) << 1 | (val & 0xA) >> 1;
+        val = (val & 0x3) << 2 | (val & 0xC) >> 2;
+
+        if(val <= 9)
+            val += '0';
+        else
+            val = bcd_chars[val - 10];
+        furi_string_push_back(instance->msg, val);
+    }
+}
+
+// decode message word, maintaining instance state for partial decoding. Return true if more data
+// might follow or false if end of message reached.
+static bool pocsag_decode_message_word(SubGhzProtocolDecoderPocsag* instance, uint32_t data) {
+    switch(instance->func) {
+    case POCSAG_FUNC_ALERT2:
+    case POCSAG_FUNC_ALPHANUM:
+        return decode_message_alphanumeric(instance, data);
+
+    case POCSAG_FUNC_NUM:
+        decode_message_numeric(instance, data);
+        return true;
+    }
+    return false;
+}
+
+// Function called when current message got decoded, but other messages might follow
+static void pocsag_message_done(SubGhzProtocolDecoderPocsag* instance) {
+    // append the message to the long-term storage string
+    furi_string_printf(instance->generic.result_ric, "\e#RIC: %" PRIu32 "\e# | ", instance->ric);
+    furi_string_cat_str(instance->generic.result_ric, func_msg[instance->func]);
+    if(instance->func != POCSAG_FUNC_ALERT1) {
+        furi_string_cat(instance->done_msg, instance->msg);
+    }
+    furi_string_cat_str(instance->done_msg, " ");
+
+    furi_string_cat(instance->generic.result_msg, instance->done_msg);
+
+    // reset the state
+    instance->char_bits = 0;
+    instance->char_data = 0;
+    furi_string_reset(instance->msg);
+}
+
+void subghz_protocol_decoder_pocsag_feed(void* context, bool level, uint32_t duration) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+
+    // reset state - waiting for 32 bits of interleaving 1s and 0s
+    if(instance->decoder.parser_step == PocsagDecoderStepReset) {
+        if(DURATION_DIFF(duration, pocsag_const.te_short) < pocsag_const.te_delta) {
+            // POCSAG signals are inverted
+            subghz_protocol_blocks_add_bit(&instance->decoder, !level);
+
+            if(instance->decoder.decode_count_bit == POCSAG_MIN_SYNC_BITS) {
+                instance->decoder.parser_step = PocsagDecoderStepFoundSync;
+            }
+        } else if(instance->decoder.decode_count_bit > 0) {
+            subghz_protocol_decoder_pocsag_reset(context);
+        }
+        return;
+    }
+
+    int bits_count = duration / pocsag_const.te_short;
+    uint32_t extra = duration - pocsag_const.te_short * bits_count;
+
+    if(DURATION_DIFF(extra, pocsag_const.te_short) < pocsag_const.te_delta)
+        bits_count++;
+    else if(extra > pocsag_const.te_delta) {
+        // in non-reset state we faced the error signal - we reached the end of the packet, flush data
+        if(furi_string_size(instance->done_msg) > 0) {
+            if(instance->base.callback)
+                instance->base.callback(&instance->base, instance->base.context);
+        }
+        subghz_protocol_decoder_pocsag_reset(context);
+        return;
+    }
+
+    uint32_t codeword;
+
+    // handle state machine for every incoming bit
+    while(bits_count-- > 0) {
+        subghz_protocol_blocks_add_bit(&instance->decoder, !level);
+
+        switch(instance->decoder.parser_step) {
+        case PocsagDecoderStepFoundSync:
+            if((instance->decoder.decode_data & POCSAG_CW_MASK) == POCSAG_FRAME_SYNC_CODE) {
+                instance->decoder.parser_step = PocsagDecoderStepFoundPreamble;
+                instance->decoder.decode_count_bit = 0;
+                instance->decoder.decode_data = 0UL;
+            }
+            break;
+        case PocsagDecoderStepFoundPreamble:
+            // handle codewords
+            if(instance->decoder.decode_count_bit == POCSAG_CW_BITS) {
+                codeword = (uint32_t)(instance->decoder.decode_data & POCSAG_CW_MASK);
+                switch(codeword) {
+                case POCSAG_IDLE_CODE_WORD:
+                    instance->codeword_idx++;
+                    break;
+                case POCSAG_FRAME_SYNC_CODE:
+                    instance->codeword_idx = 0;
+                    break;
+                default:
+                    // Here we expect only address messages
+                    if(codeword >> 31 == 0) {
+                        pocsag_decode_address_word(instance, codeword);
+                        instance->decoder.parser_step = PocsagDecoderStepMessage;
+                    }
+                    instance->codeword_idx++;
+                }
+                instance->decoder.decode_count_bit = 0;
+                instance->decoder.decode_data = 0UL;
+            }
+            break;
+
+        case PocsagDecoderStepMessage:
+            if(instance->decoder.decode_count_bit == POCSAG_CW_BITS) {
+                codeword = (uint32_t)(instance->decoder.decode_data & POCSAG_CW_MASK);
+                switch(codeword) {
+                case POCSAG_IDLE_CODE_WORD:
+                    // Idle during the message stops the message
+                    instance->codeword_idx++;
+                    instance->decoder.parser_step = PocsagDecoderStepFoundPreamble;
+                    pocsag_message_done(instance);
+                    break;
+                case POCSAG_FRAME_SYNC_CODE:
+                    instance->codeword_idx = 0;
+                    break;
+                default:
+                    // In this state, both address and message words can arrive
+                    if(codeword >> 31 == 0) {
+                        pocsag_message_done(instance);
+                        pocsag_decode_address_word(instance, codeword);
+                    } else {
+                        if(!pocsag_decode_message_word(instance, codeword)) {
+                            instance->decoder.parser_step = PocsagDecoderStepFoundPreamble;
+                            pocsag_message_done(instance);
+                        }
+                    }
+                    instance->codeword_idx++;
+                }
+                instance->decoder.decode_count_bit = 0;
+                instance->decoder.decode_data = 0UL;
+            }
+            break;
+        }
+    }
+}
+
+uint8_t subghz_protocol_decoder_pocsag_get_hash_data(void* context) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+    uint8_t hash = 0;
+    for(size_t i = 0; i < furi_string_size(instance->done_msg); i++)
+        hash ^= furi_string_get_char(instance->done_msg, i);
+    return hash;
+}
+
+SubGhzProtocolStatus subghz_protocol_decoder_pocsag_serialize(
+    void* context,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+    uint32_t msg_len;
+
+    if(SubGhzProtocolStatusOk !=
+       pcsg_block_generic_serialize(&instance->generic, flipper_format, preset))
+        return SubGhzProtocolStatusError;
+
+    msg_len = furi_string_size(instance->done_msg);
+    if(!flipper_format_write_uint32(flipper_format, "MsgLen", &msg_len, 1)) {
+        FURI_LOG_E(TAG, "Error adding MsgLen");
+        return SubGhzProtocolStatusError;
+    }
+
+    uint8_t* s = (uint8_t*)furi_string_get_cstr(instance->done_msg);
+    if(!flipper_format_write_hex(flipper_format, "Msg", s, msg_len)) {
+        FURI_LOG_E(TAG, "Error adding Msg");
+        return SubGhzProtocolStatusError;
+    }
+    return SubGhzProtocolStatusOk;
+}
+
+SubGhzProtocolStatus
+    subghz_protocol_decoder_pocsag_deserialize(void* context, FlipperFormat* flipper_format) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+    SubGhzProtocolStatus ret = SubGhzProtocolStatusError;
+    uint32_t msg_len;
+    uint8_t* buf;
+
+    do {
+        if(SubGhzProtocolStatusOk !=
+           pcsg_block_generic_deserialize(&instance->generic, flipper_format)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(flipper_format, "MsgLen", &msg_len, 1)) {
+            FURI_LOG_E(TAG, "Missing MsgLen");
+            break;
+        }
+
+        buf = malloc(msg_len);
+        if(!flipper_format_read_hex(flipper_format, "Msg", buf, msg_len)) {
+            FURI_LOG_E(TAG, "Missing Msg");
+            free(buf);
+            break;
+        }
+        furi_string_set_strn(instance->done_msg, (const char*)buf, msg_len);
+        free(buf);
+
+        ret = SubGhzProtocolStatusOk;
+    } while(false);
+    return ret;
+}
+
+void subhz_protocol_decoder_pocsag_get_string(void* context, FuriString* output) {
+    furi_assert(context);
+    SubGhzProtocolDecoderPocsag* instance = context;
+    furi_string_cat_printf(output, "%s\r\n", instance->generic.protocol_name);
+    furi_string_cat(output, instance->done_msg);
+}
+
+const SubGhzProtocolDecoder subghz_protocol_pocsag_decoder = {
+    .alloc = subghz_protocol_decoder_pocsag_alloc,
+    .free = subghz_protocol_decoder_pocsag_free,
+    .reset = subghz_protocol_decoder_pocsag_reset,
+    .feed = subghz_protocol_decoder_pocsag_feed,
+    .get_hash_data = subghz_protocol_decoder_pocsag_get_hash_data,
+    .serialize = subghz_protocol_decoder_pocsag_serialize,
+    .deserialize = subghz_protocol_decoder_pocsag_deserialize,
+    .get_string = subhz_protocol_decoder_pocsag_get_string,
+};
+
+const SubGhzProtocol subghz_protocol_pocsag = {
+    .name = SUBGHZ_PROTOCOL_POCSAG_NAME,
+    .type = SubGhzProtocolTypeStatic,
+    .flag = SubGhzProtocolFlag_FM | SubGhzProtocolFlag_Decodable | SubGhzProtocolFlag_Save |
+            SubGhzProtocolFlag_Load,
+
+    .decoder = &subghz_protocol_pocsag_decoder,
+};

+ 13 - 0
main_apps_sources/pocsag_pager/protocols/pocsag.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <lib/subghz/protocols/base.h>
+
+#include <lib/subghz/blocks/const.h>
+#include <lib/subghz/blocks/decoder.h>
+#include <lib/subghz/blocks/encoder.h>
+#include "pcsg_generic.h"
+#include <lib/subghz/blocks/math.h>
+
+#define SUBGHZ_PROTOCOL_POCSAG_NAME "POCSAG"
+
+extern const SubGhzProtocol subghz_protocol_pocsag;

+ 9 - 0
main_apps_sources/pocsag_pager/protocols/protocol_items.c

@@ -0,0 +1,9 @@
+#include "protocol_items.h"
+
+const SubGhzProtocol* pocsag_pager_protocol_registry_items[] = {
+    &subghz_protocol_pocsag,
+};
+
+const SubGhzProtocolRegistry pocsag_pager_protocol_registry = {
+    .items = pocsag_pager_protocol_registry_items,
+    .size = COUNT_OF(pocsag_pager_protocol_registry_items)};

+ 6 - 0
main_apps_sources/pocsag_pager/protocols/protocol_items.h

@@ -0,0 +1,6 @@
+#pragma once
+#include "../pocsag_pager_app_i.h"
+
+#include "pocsag.h"
+
+extern const SubGhzProtocolRegistry pocsag_pager_protocol_registry;

+ 214 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_receiver.c

@@ -0,0 +1,214 @@
+#include "../pocsag_pager_app_i.h"
+#include "../views/pocsag_pager_receiver.h"
+
+static const NotificationSequence subghs_sequence_rx = {
+    &message_green_255,
+
+    &message_vibro_on,
+    &message_note_c6,
+    &message_delay_50,
+    &message_sound_off,
+    &message_vibro_off,
+
+    &message_delay_50,
+    NULL,
+};
+
+static const NotificationSequence subghs_sequence_rx_locked = {
+    &message_green_255,
+
+    &message_display_backlight_on,
+
+    &message_vibro_on,
+    &message_note_c6,
+    &message_delay_50,
+    &message_sound_off,
+    &message_vibro_off,
+
+    &message_delay_500,
+
+    &message_display_backlight_off,
+    NULL,
+};
+
+static void pocsag_pager_scene_receiver_update_statusbar(void* context) {
+    POCSAGPagerApp* app = context;
+    FuriString* history_stat_str;
+    history_stat_str = furi_string_alloc();
+    if(!pcsg_history_get_text_space_left(app->txrx->history, history_stat_str)) {
+        FuriString* frequency_str;
+        FuriString* modulation_str;
+
+        frequency_str = furi_string_alloc();
+        modulation_str = furi_string_alloc();
+
+        pcsg_get_frequency_modulation(app, frequency_str, modulation_str);
+
+        pcsg_view_receiver_add_data_statusbar(
+            app->pcsg_receiver,
+            furi_string_get_cstr(frequency_str),
+            furi_string_get_cstr(modulation_str),
+            furi_string_get_cstr(history_stat_str));
+
+        furi_string_free(frequency_str);
+        furi_string_free(modulation_str);
+    } else {
+        pcsg_view_receiver_add_data_statusbar(
+            app->pcsg_receiver, furi_string_get_cstr(history_stat_str), "", "");
+    }
+    furi_string_free(history_stat_str);
+}
+
+void pocsag_pager_scene_receiver_callback(PCSGCustomEvent event, void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+static void pocsag_pager_scene_receiver_add_to_history_callback(
+    SubGhzReceiver* receiver,
+    SubGhzProtocolDecoderBase* decoder_base,
+    void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    FuriString* str_buff;
+    str_buff = furi_string_alloc();
+
+    if(pcsg_history_add_to_history(app->txrx->history, decoder_base, app->txrx->preset) ==
+       PCSGHistoryStateAddKeyNewDada) {
+        furi_string_reset(str_buff);
+
+        pcsg_history_get_text_item_menu(
+            app->txrx->history, str_buff, pcsg_history_get_item(app->txrx->history) - 1);
+        pcsg_view_receiver_add_item_to_menu(
+            app->pcsg_receiver,
+            furi_string_get_cstr(str_buff),
+            pcsg_history_get_type_protocol(
+                app->txrx->history, pcsg_history_get_item(app->txrx->history) - 1));
+
+        pocsag_pager_scene_receiver_update_statusbar(app);
+        notification_message(app->notifications, &sequence_blink_green_10);
+        if(app->lock != PCSGLockOn) {
+            notification_message(app->notifications, &subghs_sequence_rx);
+        } else {
+            notification_message(app->notifications, &subghs_sequence_rx_locked);
+        }
+    }
+    subghz_receiver_reset(receiver);
+    furi_string_free(str_buff);
+    app->txrx->rx_key_state = PCSGRxKeyStateAddKey;
+}
+
+void pocsag_pager_scene_receiver_on_enter(void* context) {
+    POCSAGPagerApp* app = context;
+
+    FuriString* str_buff;
+    str_buff = furi_string_alloc();
+
+    if(app->txrx->rx_key_state == PCSGRxKeyStateIDLE) {
+        pcsg_preset_init(app, "FM95", 439987500, NULL, 0);
+        pcsg_history_reset(app->txrx->history);
+        app->txrx->rx_key_state = PCSGRxKeyStateStart;
+    }
+
+    pcsg_view_receiver_set_lock(app->pcsg_receiver, app->lock);
+    pcsg_view_receiver_set_ext_module_state(
+        app->pcsg_receiver, radio_device_loader_is_external(app->txrx->radio_device));
+
+    //Load history to receiver
+    pcsg_view_receiver_exit(app->pcsg_receiver);
+    for(uint8_t i = 0; i < pcsg_history_get_item(app->txrx->history); i++) {
+        furi_string_reset(str_buff);
+        pcsg_history_get_text_item_menu(app->txrx->history, str_buff, i);
+        pcsg_view_receiver_add_item_to_menu(
+            app->pcsg_receiver,
+            furi_string_get_cstr(str_buff),
+            pcsg_history_get_type_protocol(app->txrx->history, i));
+        app->txrx->rx_key_state = PCSGRxKeyStateAddKey;
+    }
+    furi_string_free(str_buff);
+    pocsag_pager_scene_receiver_update_statusbar(app);
+
+    pcsg_view_receiver_set_callback(app->pcsg_receiver, pocsag_pager_scene_receiver_callback, app);
+    subghz_receiver_set_rx_callback(
+        app->txrx->receiver, pocsag_pager_scene_receiver_add_to_history_callback, app);
+
+    if(app->txrx->txrx_state == PCSGTxRxStateRx) {
+        pcsg_rx_end(app);
+    };
+    if((app->txrx->txrx_state == PCSGTxRxStateIDLE) ||
+       (app->txrx->txrx_state == PCSGTxRxStateSleep)) {
+        // Start RX
+        pcsg_begin(
+            app,
+            subghz_setting_get_preset_data_by_name(
+                app->setting, furi_string_get_cstr(app->txrx->preset->name)));
+
+        pcsg_rx(app, app->txrx->preset->frequency);
+    }
+
+    pcsg_view_receiver_set_idx_menu(app->pcsg_receiver, app->txrx->idx_menu_chosen);
+    view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewReceiver);
+}
+
+bool pocsag_pager_scene_receiver_on_event(void* context, SceneManagerEvent event) {
+    POCSAGPagerApp* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case PCSGCustomEventViewReceiverBack:
+            // Stop CC1101 Rx
+            if(app->txrx->txrx_state == PCSGTxRxStateRx) {
+                pcsg_rx_end(app);
+                pcsg_idle(app);
+            };
+            app->txrx->hopper_state = PCSGHopperStateOFF;
+            app->txrx->idx_menu_chosen = 0;
+            subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, app);
+
+            app->txrx->rx_key_state = PCSGRxKeyStateIDLE;
+            pcsg_preset_init(app, "FM95", 439987500, NULL, 0);
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, POCSAGPagerSceneStart);
+            consumed = true;
+            break;
+        case PCSGCustomEventViewReceiverOK:
+            app->txrx->idx_menu_chosen = pcsg_view_receiver_get_idx_menu(app->pcsg_receiver);
+            scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneReceiverInfo);
+            consumed = true;
+            break;
+        case PCSGCustomEventViewReceiverConfig:
+            app->txrx->idx_menu_chosen = pcsg_view_receiver_get_idx_menu(app->pcsg_receiver);
+            scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneReceiverConfig);
+            consumed = true;
+            break;
+        case PCSGCustomEventViewReceiverOffDisplay:
+            notification_message(app->notifications, &sequence_display_backlight_off);
+            consumed = true;
+            break;
+        case PCSGCustomEventViewReceiverUnlock:
+            app->lock = PCSGLockOff;
+            consumed = true;
+            break;
+        default:
+            break;
+        }
+    } else if(event.type == SceneManagerEventTypeTick) {
+        if(app->txrx->hopper_state != PCSGHopperStateOFF) {
+            pcsg_hopper_update(app);
+            pocsag_pager_scene_receiver_update_statusbar(app);
+        }
+        // Get current RSSI
+        float rssi = subghz_devices_get_rssi(app->txrx->radio_device);
+        pcsg_receiver_rssi(app->pcsg_receiver, rssi);
+
+        if(app->txrx->txrx_state == PCSGTxRxStateRx) {
+            notification_message(app->notifications, &sequence_blink_cyan_10);
+        }
+    }
+    return consumed;
+}
+
+void pocsag_pager_scene_receiver_on_exit(void* context) {
+    UNUSED(context);
+}

+ 30 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene.c

@@ -0,0 +1,30 @@
+#include "../pocsag_pager_app_i.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const pocsag_pager_scene_on_enter_handlers[])(void*) = {
+#include "pocsag_pager_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const pocsag_pager_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "pocsag_pager_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const pocsag_pager_scene_on_exit_handlers[])(void* context) = {
+#include "pocsag_pager_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers pocsag_pager_scene_handlers = {
+    .on_enter_handlers = pocsag_pager_scene_on_enter_handlers,
+    .on_event_handlers = pocsag_pager_scene_on_event_handlers,
+    .on_exit_handlers = pocsag_pager_scene_on_exit_handlers,
+    .scene_num = POCSAGPagerSceneNum,
+};

+ 29 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) POCSAGPagerScene##id,
+typedef enum {
+#include "pocsag_pager_scene_config.h"
+    POCSAGPagerSceneNum,
+} POCSAGPagerScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers pocsag_pager_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "pocsag_pager_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "pocsag_pager_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "pocsag_pager_scene_config.h"
+#undef ADD_SCENE

+ 74 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_about.c

@@ -0,0 +1,74 @@
+#include "../pocsag_pager_app_i.h"
+#include "../helpers/pocsag_pager_types.h"
+
+void pocsag_pager_scene_about_widget_callback(GuiButtonType result, InputType type, void* context) {
+    POCSAGPagerApp* app = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, result);
+    }
+}
+
+void pocsag_pager_scene_about_on_enter(void* context) {
+    POCSAGPagerApp* app = context;
+
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    furi_string_printf(temp_str, "\e#%s\n", "Information");
+
+    furi_string_cat_printf(temp_str, "Version: %s\n", PCSG_VERSION_APP);
+    furi_string_cat_printf(temp_str, "Developed by:\n%s\n\n", PCSG_DEVELOPED);
+    furi_string_cat_printf(temp_str, "Github: %s\n\n", PCSG_GITHUB);
+
+    furi_string_cat_printf(temp_str, "\e#%s\n", "Description");
+    furi_string_cat_printf(temp_str, "Receiving POCSAG Pager \nmessages\n\n");
+
+    furi_string_cat_printf(temp_str, "Supported protocols:\n");
+    size_t i = 0;
+    const char* protocol_name =
+        subghz_environment_get_protocol_name_registry(app->txrx->environment, i++);
+    do {
+        furi_string_cat_printf(temp_str, "%s\n", protocol_name);
+        protocol_name = subghz_environment_get_protocol_name_registry(app->txrx->environment, i++);
+    } while(protocol_name != NULL);
+
+    widget_add_text_box_element(
+        app->widget,
+        0,
+        0,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!                                                      \e!\n",
+        false);
+    widget_add_text_box_element(
+        app->widget,
+        0,
+        2,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!        POCSAG Pager       \e!\n",
+        false);
+    widget_add_text_scroll_element(app->widget, 0, 16, 128, 50, furi_string_get_cstr(temp_str));
+    furi_string_free(temp_str);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewWidget);
+}
+
+bool pocsag_pager_scene_about_on_event(void* context, SceneManagerEvent event) {
+    POCSAGPagerApp* app = context;
+    bool consumed = false;
+    UNUSED(app);
+    UNUSED(event);
+
+    return consumed;
+}
+
+void pocsag_pager_scene_about_on_exit(void* context) {
+    POCSAGPagerApp* app = context;
+
+    // Clear views
+    widget_reset(app->widget);
+}

+ 5 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_config.h

@@ -0,0 +1,5 @@
+ADD_SCENE(pocsag_pager, start, Start)
+ADD_SCENE(pocsag_pager, about, About)
+ADD_SCENE(pocsag_pager, receiver, Receiver)
+ADD_SCENE(pocsag_pager, receiver_config, ReceiverConfig)
+ADD_SCENE(pocsag_pager, receiver_info, ReceiverInfo)

+ 221 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_receiver_config.c

@@ -0,0 +1,221 @@
+#include "../pocsag_pager_app_i.h"
+
+enum PCSGSettingIndex {
+    PCSGSettingIndexFrequency,
+    PCSGSettingIndexHopping,
+    PCSGSettingIndexModulation,
+    PCSGSettingIndexLock,
+};
+
+#define HOPPING_COUNT 2
+const char* const hopping_text[HOPPING_COUNT] = {
+    "OFF",
+    "ON",
+};
+const uint32_t hopping_value[HOPPING_COUNT] = {
+    PCSGHopperStateOFF,
+    PCSGHopperStateRunnig,
+};
+
+uint8_t pocsag_pager_scene_receiver_config_next_frequency(const uint32_t value, void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    uint8_t index = 0;
+    for(uint8_t i = 0; i < subghz_setting_get_frequency_count(app->setting); i++) {
+        if(value == subghz_setting_get_frequency(app->setting, i)) {
+            index = i;
+            break;
+        } else {
+            index = subghz_setting_get_frequency_default_index(app->setting);
+        }
+    }
+    return index;
+}
+
+uint8_t pocsag_pager_scene_receiver_config_next_preset(const char* preset_name, void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    uint8_t index = 0;
+    for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) {
+        if(!strcmp(subghz_setting_get_preset_name(app->setting, i), preset_name)) {
+            index = i;
+            break;
+        } else {
+            //  index = subghz_setting_get_frequency_default_index(app ->setting);
+        }
+    }
+    return index;
+}
+
+uint8_t pocsag_pager_scene_receiver_config_hopper_value_index(
+    const uint32_t value,
+    const uint32_t values[],
+    uint8_t values_count,
+    void* context) {
+    furi_assert(context);
+    UNUSED(values_count);
+    POCSAGPagerApp* app = context;
+
+    if(value == values[0]) {
+        return 0;
+    } else {
+        variable_item_set_current_value_text(
+            (VariableItem*)scene_manager_get_scene_state(
+                app->scene_manager, POCSAGPagerSceneReceiverConfig),
+            " -----");
+        return 1;
+    }
+}
+
+static void pocsag_pager_scene_receiver_config_set_frequency(VariableItem* item) {
+    POCSAGPagerApp* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    if(app->txrx->hopper_state == PCSGHopperStateOFF) {
+        char text_buf[10] = {0};
+        snprintf(
+            text_buf,
+            sizeof(text_buf),
+            "%lu.%02lu",
+            subghz_setting_get_frequency(app->setting, index) / 1000000,
+            (subghz_setting_get_frequency(app->setting, index) % 1000000) / 10000);
+        variable_item_set_current_value_text(item, text_buf);
+        app->txrx->preset->frequency = subghz_setting_get_frequency(app->setting, index);
+    } else {
+        variable_item_set_current_value_index(
+            item, subghz_setting_get_frequency_default_index(app->setting));
+    }
+}
+
+static void pocsag_pager_scene_receiver_config_set_preset(VariableItem* item) {
+    POCSAGPagerApp* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(
+        item, subghz_setting_get_preset_name(app->setting, index));
+    pcsg_preset_init(
+        app,
+        subghz_setting_get_preset_name(app->setting, index),
+        app->txrx->preset->frequency,
+        subghz_setting_get_preset_data(app->setting, index),
+        subghz_setting_get_preset_data_size(app->setting, index));
+}
+
+static void pocsag_pager_scene_receiver_config_set_hopping_running(VariableItem* item) {
+    POCSAGPagerApp* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, hopping_text[index]);
+    if(hopping_value[index] == PCSGHopperStateOFF) {
+        char text_buf[10] = {0};
+        snprintf(
+            text_buf,
+            sizeof(text_buf),
+            "%lu.%02lu",
+            subghz_setting_get_default_frequency(app->setting) / 1000000,
+            (subghz_setting_get_default_frequency(app->setting) % 1000000) / 10000);
+        variable_item_set_current_value_text(
+            (VariableItem*)scene_manager_get_scene_state(
+                app->scene_manager, POCSAGPagerSceneReceiverConfig),
+            text_buf);
+        app->txrx->preset->frequency = subghz_setting_get_default_frequency(app->setting);
+        variable_item_set_current_value_index(
+            (VariableItem*)scene_manager_get_scene_state(
+                app->scene_manager, POCSAGPagerSceneReceiverConfig),
+            subghz_setting_get_frequency_default_index(app->setting));
+    } else {
+        variable_item_set_current_value_text(
+            (VariableItem*)scene_manager_get_scene_state(
+                app->scene_manager, POCSAGPagerSceneReceiverConfig),
+            " -----");
+        variable_item_set_current_value_index(
+            (VariableItem*)scene_manager_get_scene_state(
+                app->scene_manager, POCSAGPagerSceneReceiverConfig),
+            subghz_setting_get_frequency_default_index(app->setting));
+    }
+
+    app->txrx->hopper_state = hopping_value[index];
+}
+
+static void
+    pocsag_pager_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    if(index == PCSGSettingIndexLock) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, PCSGCustomEventSceneSettingLock);
+    }
+}
+
+void pocsag_pager_scene_receiver_config_on_enter(void* context) {
+    POCSAGPagerApp* app = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Frequency:",
+        subghz_setting_get_frequency_count(app->setting),
+        pocsag_pager_scene_receiver_config_set_frequency,
+        app);
+    value_index =
+        pocsag_pager_scene_receiver_config_next_frequency(app->txrx->preset->frequency, app);
+    scene_manager_set_scene_state(
+        app->scene_manager, POCSAGPagerSceneReceiverConfig, (uint32_t)item);
+    variable_item_set_current_value_index(item, value_index);
+    char text_buf[10] = {0};
+    snprintf(
+        text_buf,
+        sizeof(text_buf),
+        "%lu.%02lu",
+        subghz_setting_get_frequency(app->setting, value_index) / 1000000,
+        (subghz_setting_get_frequency(app->setting, value_index) % 1000000) / 10000);
+    variable_item_set_current_value_text(item, text_buf);
+
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Hopping:",
+        HOPPING_COUNT,
+        pocsag_pager_scene_receiver_config_set_hopping_running,
+        app);
+    value_index = pocsag_pager_scene_receiver_config_hopper_value_index(
+        app->txrx->hopper_state, hopping_value, HOPPING_COUNT, app);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, hopping_text[value_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Modulation:",
+        subghz_setting_get_preset_count(app->setting),
+        pocsag_pager_scene_receiver_config_set_preset,
+        app);
+    value_index = pocsag_pager_scene_receiver_config_next_preset(
+        furi_string_get_cstr(app->txrx->preset->name), app);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(
+        item, subghz_setting_get_preset_name(app->setting, value_index));
+
+    variable_item_list_add(app->variable_item_list, "Lock Keyboard", 1, NULL, NULL);
+    variable_item_list_set_enter_callback(
+        app->variable_item_list, pocsag_pager_scene_receiver_config_var_list_enter_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewVariableItemList);
+}
+
+bool pocsag_pager_scene_receiver_config_on_event(void* context, SceneManagerEvent event) {
+    POCSAGPagerApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PCSGCustomEventSceneSettingLock) {
+            app->lock = PCSGLockOn;
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void pocsag_pager_scene_receiver_config_on_exit(void* context) {
+    POCSAGPagerApp* app = context;
+    variable_item_list_set_selected_item(app->variable_item_list, 0);
+    variable_item_list_reset(app->variable_item_list);
+}

+ 50 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_receiver_info.c

@@ -0,0 +1,50 @@
+#include "../pocsag_pager_app_i.h"
+#include "../views/pocsag_pager_receiver.h"
+
+void pocsag_pager_scene_receiver_info_callback(PCSGCustomEvent event, void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+static void pocsag_pager_scene_receiver_info_add_to_history_callback(
+    SubGhzReceiver* receiver,
+    SubGhzProtocolDecoderBase* decoder_base,
+    void* context) {
+    furi_assert(context);
+    POCSAGPagerApp* app = context;
+
+    if(pcsg_history_add_to_history(app->txrx->history, decoder_base, app->txrx->preset) ==
+       PCSGHistoryStateAddKeyUpdateData) {
+        pcsg_view_receiver_info_update(
+            app->pcsg_receiver_info,
+            pcsg_history_get_raw_data(app->txrx->history, app->txrx->idx_menu_chosen));
+        subghz_receiver_reset(receiver);
+
+        notification_message(app->notifications, &sequence_blink_green_10);
+        app->txrx->rx_key_state = PCSGRxKeyStateAddKey;
+    }
+}
+
+void pocsag_pager_scene_receiver_info_on_enter(void* context) {
+    POCSAGPagerApp* app = context;
+
+    subghz_receiver_set_rx_callback(
+        app->txrx->receiver, pocsag_pager_scene_receiver_info_add_to_history_callback, app);
+    pcsg_view_receiver_info_update(
+        app->pcsg_receiver_info,
+        pcsg_history_get_raw_data(app->txrx->history, app->txrx->idx_menu_chosen));
+    view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewReceiverInfo);
+}
+
+bool pocsag_pager_scene_receiver_info_on_event(void* context, SceneManagerEvent event) {
+    POCSAGPagerApp* app = context;
+    bool consumed = false;
+    UNUSED(app);
+    UNUSED(event);
+    return consumed;
+}
+
+void pocsag_pager_scene_receiver_info_on_exit(void* context) {
+    UNUSED(context);
+}

+ 58 - 0
main_apps_sources/pocsag_pager/scenes/pocsag_pager_scene_start.c

@@ -0,0 +1,58 @@
+#include "../pocsag_pager_app_i.h"
+
+typedef enum {
+    SubmenuIndexPOCSAGPagerReceiver,
+    SubmenuIndexPOCSAGPagerAbout,
+} SubmenuIndex;
+
+void pocsag_pager_scene_start_submenu_callback(void* context, uint32_t index) {
+    POCSAGPagerApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void pocsag_pager_scene_start_on_enter(void* context) {
+    UNUSED(context);
+    POCSAGPagerApp* app = context;
+    Submenu* submenu = app->submenu;
+
+    submenu_add_item(
+        submenu,
+        "Receive messages",
+        SubmenuIndexPOCSAGPagerReceiver,
+        pocsag_pager_scene_start_submenu_callback,
+        app);
+    submenu_add_item(
+        submenu,
+        "About",
+        SubmenuIndexPOCSAGPagerAbout,
+        pocsag_pager_scene_start_submenu_callback,
+        app);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(app->scene_manager, POCSAGPagerSceneStart));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewSubmenu);
+}
+
+bool pocsag_pager_scene_start_on_event(void* context, SceneManagerEvent event) {
+    POCSAGPagerApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexPOCSAGPagerAbout) {
+            scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneAbout);
+            consumed = true;
+        } else if(event.event == SubmenuIndexPOCSAGPagerReceiver) {
+            scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneReceiver);
+            consumed = true;
+        }
+        scene_manager_set_scene_state(app->scene_manager, POCSAGPagerSceneStart, event.event);
+    }
+
+    return consumed;
+}
+
+void pocsag_pager_scene_start_on_exit(void* context) {
+    POCSAGPagerApp* app = context;
+    submenu_reset(app->submenu);
+}

+ 478 - 0
main_apps_sources/pocsag_pager/views/pocsag_pager_receiver.c

@@ -0,0 +1,478 @@
+#include "pocsag_pager_receiver.h"
+#include "../pocsag_pager_app_i.h"
+#include <pocsag_pager_icons.h>
+#include <math.h>
+
+#include <input/input.h>
+#include <gui/elements.h>
+#include <m-array.h>
+
+#define FRAME_HEIGHT 12
+#define MAX_LEN_PX 112
+#define MENU_ITEMS 4u
+#define UNLOCK_CNT 3
+
+#define SUBGHZ_RAW_THRESHOLD_MIN -90.0f
+
+typedef struct {
+    FuriString* item_str;
+    uint8_t type;
+} PCSGReceiverMenuItem;
+
+ARRAY_DEF(PCSGReceiverMenuItemArray, PCSGReceiverMenuItem, M_POD_OPLIST)
+
+#define M_OPL_PCSGReceiverMenuItemArray_t() ARRAY_OPLIST(PCSGReceiverMenuItemArray, M_POD_OPLIST)
+
+struct PCSGReceiverHistory {
+    PCSGReceiverMenuItemArray_t data;
+};
+
+typedef struct PCSGReceiverHistory PCSGReceiverHistory;
+
+static const Icon* ReceiverItemIcons[] = {
+    [SubGhzProtocolTypeUnknown] = &I_Quest_7x8,
+    [SubGhzProtocolTypeStatic] = &I_Message_8x7,
+    [SubGhzProtocolTypeDynamic] = &I_Lock_7x8,
+};
+
+typedef enum {
+    PCSGReceiverBarShowDefault,
+    PCSGReceiverBarShowLock,
+    PCSGReceiverBarShowToUnlockPress,
+    PCSGReceiverBarShowUnlock,
+} PCSGReceiverBarShow;
+
+struct PCSGReceiver {
+    PCSGLock lock;
+    uint8_t lock_count;
+    FuriTimer* timer;
+    View* view;
+    PCSGReceiverCallback callback;
+    void* context;
+};
+
+typedef struct {
+    FuriString* frequency_str;
+    FuriString* preset_str;
+    FuriString* history_stat_str;
+    PCSGReceiverHistory* history;
+    uint16_t idx;
+    uint16_t list_offset;
+    uint16_t history_item;
+    PCSGReceiverBarShow bar_show;
+    uint8_t u_rssi;
+    bool ext_module;
+} PCSGReceiverModel;
+
+void pcsg_receiver_rssi(PCSGReceiver* instance, float rssi) {
+    furi_assert(instance);
+    with_view_model(
+        instance->view,
+        PCSGReceiverModel * model,
+        {
+            if(rssi < SUBGHZ_RAW_THRESHOLD_MIN) {
+                model->u_rssi = 0;
+            } else {
+                model->u_rssi = (uint8_t)(rssi - SUBGHZ_RAW_THRESHOLD_MIN);
+            }
+        },
+        true);
+}
+
+void pcsg_view_receiver_set_lock(PCSGReceiver* pcsg_receiver, PCSGLock lock) {
+    furi_assert(pcsg_receiver);
+    pcsg_receiver->lock_count = 0;
+    if(lock == PCSGLockOn) {
+        pcsg_receiver->lock = lock;
+        with_view_model(
+            pcsg_receiver->view,
+            PCSGReceiverModel * model,
+            { model->bar_show = PCSGReceiverBarShowLock; },
+            true);
+        furi_timer_start(pcsg_receiver->timer, pdMS_TO_TICKS(1000));
+    } else {
+        with_view_model(
+            pcsg_receiver->view,
+            PCSGReceiverModel * model,
+            { model->bar_show = PCSGReceiverBarShowDefault; },
+            true);
+    }
+}
+
+void pcsg_view_receiver_set_ext_module_state(PCSGReceiver* pcsg_receiver, bool is_external) {
+    furi_assert(pcsg_receiver);
+    with_view_model(
+        pcsg_receiver->view, PCSGReceiverModel * model, { model->ext_module = is_external; }, true);
+}
+
+void pcsg_view_receiver_set_callback(
+    PCSGReceiver* pcsg_receiver,
+    PCSGReceiverCallback callback,
+    void* context) {
+    furi_assert(pcsg_receiver);
+    furi_assert(callback);
+    pcsg_receiver->callback = callback;
+    pcsg_receiver->context = context;
+}
+
+static void pcsg_view_receiver_update_offset(PCSGReceiver* pcsg_receiver) {
+    furi_assert(pcsg_receiver);
+
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            size_t history_item = model->history_item;
+            uint16_t bounds = history_item > 3 ? 2 : history_item;
+
+            if(history_item > 3 && model->idx >= (int16_t)(history_item - 1)) {
+                model->list_offset = model->idx - 3;
+            } else if(model->list_offset < model->idx - bounds) {
+                model->list_offset =
+                    CLAMP(model->list_offset + 1, (int16_t)(history_item - bounds), 0);
+            } else if(model->list_offset > model->idx - bounds) {
+                model->list_offset = CLAMP(model->idx - 1, (int16_t)(history_item - bounds), 0);
+            }
+        },
+        true);
+}
+
+void pcsg_view_receiver_add_item_to_menu(
+    PCSGReceiver* pcsg_receiver,
+    const char* name,
+    uint8_t type) {
+    furi_assert(pcsg_receiver);
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            PCSGReceiverMenuItem* item_menu =
+                PCSGReceiverMenuItemArray_push_raw(model->history->data);
+            item_menu->item_str = furi_string_alloc_set(name);
+            item_menu->type = type;
+            if((model->idx == model->history_item - 1)) {
+                model->history_item++;
+                model->idx++;
+            } else {
+                model->history_item++;
+            }
+        },
+        true);
+    pcsg_view_receiver_update_offset(pcsg_receiver);
+}
+
+void pcsg_view_receiver_add_data_statusbar(
+    PCSGReceiver* pcsg_receiver,
+    const char* frequency_str,
+    const char* preset_str,
+    const char* history_stat_str) {
+    furi_assert(pcsg_receiver);
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            furi_string_set_str(model->frequency_str, frequency_str);
+            furi_string_set_str(model->preset_str, preset_str);
+            furi_string_set_str(model->history_stat_str, history_stat_str);
+        },
+        true);
+}
+
+static void pcsg_view_receiver_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) {
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_box(canvas, 0, 0 + idx * FRAME_HEIGHT, scrollbar ? 122 : 127, FRAME_HEIGHT);
+
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_dot(canvas, 0, 0 + idx * FRAME_HEIGHT);
+    canvas_draw_dot(canvas, 1, 0 + idx * FRAME_HEIGHT);
+    canvas_draw_dot(canvas, 0, (0 + idx * FRAME_HEIGHT) + 1);
+
+    canvas_draw_dot(canvas, 0, (0 + idx * FRAME_HEIGHT) + 11);
+    canvas_draw_dot(canvas, scrollbar ? 121 : 126, 0 + idx * FRAME_HEIGHT);
+    canvas_draw_dot(canvas, scrollbar ? 121 : 126, (0 + idx * FRAME_HEIGHT) + 11);
+}
+
+static void pcsg_view_rssi_draw(Canvas* canvas, PCSGReceiverModel* model) {
+    for(uint8_t i = 1; i < model->u_rssi; i++) {
+        if(i % 5) {
+            canvas_draw_dot(canvas, 46 + i, 50);
+            canvas_draw_dot(canvas, 47 + i, 51);
+            canvas_draw_dot(canvas, 46 + i, 52);
+        }
+    }
+}
+
+void pcsg_view_receiver_draw(Canvas* canvas, PCSGReceiverModel* model) {
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontSecondary);
+
+    elements_button_left(canvas, "Config");
+    //canvas_draw_line(canvas, 46, 51, 125, 51);
+
+    bool scrollbar = model->history_item > 4;
+    FuriString* str_buff;
+    str_buff = furi_string_alloc();
+
+    PCSGReceiverMenuItem* item_menu;
+
+    for(size_t i = 0; i < MIN(model->history_item, MENU_ITEMS); ++i) {
+        size_t idx = CLAMP((uint16_t)(i + model->list_offset), model->history_item, 0);
+        item_menu = PCSGReceiverMenuItemArray_get(model->history->data, idx);
+        furi_string_set(str_buff, item_menu->item_str);
+        furi_string_replace_all(str_buff, "#", "");
+        elements_string_fit_width(canvas, str_buff, scrollbar ? MAX_LEN_PX - 7 : MAX_LEN_PX);
+        if(model->idx == idx) {
+            pcsg_view_receiver_draw_frame(canvas, i, scrollbar);
+        } else {
+            canvas_set_color(canvas, ColorBlack);
+        }
+        canvas_draw_icon(canvas, 4, 2 + i * FRAME_HEIGHT, ReceiverItemIcons[item_menu->type]);
+        canvas_draw_str(canvas, 15, 9 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buff));
+        furi_string_reset(str_buff);
+    }
+    if(scrollbar) {
+        elements_scrollbar_pos(canvas, 128, 0, 49, model->idx, model->history_item);
+    }
+    furi_string_free(str_buff);
+
+    canvas_set_color(canvas, ColorBlack);
+
+    if(model->history_item == 0) {
+        canvas_draw_icon(canvas, 0, 0, model->ext_module ? &I_Fishing_123x52 : &I_Scanning_123x52);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 63, 46, "Scanning...");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 44, 10, model->ext_module ? "Ext" : "Int");
+    }
+
+    // Draw RSSI
+    pcsg_view_rssi_draw(canvas, model);
+
+    switch(model->bar_show) {
+    case PCSGReceiverBarShowLock:
+        canvas_draw_icon(canvas, 64, 55, &I_Lock_7x8);
+        canvas_draw_str(canvas, 74, 62, "Locked");
+        break;
+    case PCSGReceiverBarShowToUnlockPress:
+        canvas_draw_str(canvas, 44, 62, furi_string_get_cstr(model->frequency_str));
+        canvas_draw_str(canvas, 79, 62, furi_string_get_cstr(model->preset_str));
+        canvas_draw_str(canvas, 96, 62, furi_string_get_cstr(model->history_stat_str));
+        canvas_set_font(canvas, FontSecondary);
+        elements_bold_rounded_frame(canvas, 14, 8, 99, 48);
+        elements_multiline_text(canvas, 65, 26, "To unlock\npress:");
+        canvas_draw_icon(canvas, 65, 42, &I_Pin_back_arrow_10x8);
+        canvas_draw_icon(canvas, 80, 42, &I_Pin_back_arrow_10x8);
+        canvas_draw_icon(canvas, 95, 42, &I_Pin_back_arrow_10x8);
+        canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42);
+        canvas_draw_dot(canvas, 17, 61);
+        break;
+    case PCSGReceiverBarShowUnlock:
+        canvas_draw_icon(canvas, 64, 55, &I_Unlock_7x8);
+        canvas_draw_str(canvas, 74, 62, "Unlocked");
+        break;
+    default:
+        canvas_draw_str(canvas, 44, 62, furi_string_get_cstr(model->frequency_str));
+        canvas_draw_str(canvas, 79, 62, furi_string_get_cstr(model->preset_str));
+        canvas_draw_str(canvas, 96, 62, furi_string_get_cstr(model->history_stat_str));
+        break;
+    }
+}
+
+static void pcsg_view_receiver_timer_callback(void* context) {
+    furi_assert(context);
+    PCSGReceiver* pcsg_receiver = context;
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        { model->bar_show = PCSGReceiverBarShowDefault; },
+        true);
+    if(pcsg_receiver->lock_count < UNLOCK_CNT) {
+        pcsg_receiver->callback(PCSGCustomEventViewReceiverOffDisplay, pcsg_receiver->context);
+    } else {
+        pcsg_receiver->lock = PCSGLockOff;
+        pcsg_receiver->callback(PCSGCustomEventViewReceiverUnlock, pcsg_receiver->context);
+    }
+    pcsg_receiver->lock_count = 0;
+}
+
+bool pcsg_view_receiver_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    PCSGReceiver* pcsg_receiver = context;
+
+    if(pcsg_receiver->lock == PCSGLockOn) {
+        with_view_model(
+            pcsg_receiver->view,
+            PCSGReceiverModel * model,
+            { model->bar_show = PCSGReceiverBarShowToUnlockPress; },
+            true);
+        if(pcsg_receiver->lock_count == 0) {
+            furi_timer_start(pcsg_receiver->timer, pdMS_TO_TICKS(1000));
+        }
+        if(event->key == InputKeyBack && event->type == InputTypeShort) {
+            pcsg_receiver->lock_count++;
+        }
+        if(pcsg_receiver->lock_count >= UNLOCK_CNT) {
+            pcsg_receiver->callback(PCSGCustomEventViewReceiverUnlock, pcsg_receiver->context);
+            with_view_model(
+                pcsg_receiver->view,
+                PCSGReceiverModel * model,
+                { model->bar_show = PCSGReceiverBarShowUnlock; },
+                true);
+            pcsg_receiver->lock = PCSGLockOff;
+            furi_timer_start(pcsg_receiver->timer, pdMS_TO_TICKS(650));
+        }
+
+        return true;
+    }
+
+    if(event->key == InputKeyBack && event->type == InputTypeShort) {
+        pcsg_receiver->callback(PCSGCustomEventViewReceiverBack, pcsg_receiver->context);
+    } else if(
+        event->key == InputKeyUp &&
+        (event->type == InputTypeShort || event->type == InputTypeRepeat)) {
+        with_view_model(
+            pcsg_receiver->view,
+            PCSGReceiverModel * model,
+            {
+                if(model->idx != 0) model->idx--;
+            },
+            true);
+    } else if(
+        event->key == InputKeyDown &&
+        (event->type == InputTypeShort || event->type == InputTypeRepeat)) {
+        with_view_model(
+            pcsg_receiver->view,
+            PCSGReceiverModel * model,
+            {
+                if(model->history_item && model->idx != model->history_item - 1) model->idx++;
+            },
+            true);
+    } else if(event->key == InputKeyLeft && event->type == InputTypeShort) {
+        pcsg_receiver->callback(PCSGCustomEventViewReceiverConfig, pcsg_receiver->context);
+    } else if(event->key == InputKeyOk && event->type == InputTypeShort) {
+        with_view_model(
+            pcsg_receiver->view,
+            PCSGReceiverModel * model,
+            {
+                if(model->history_item != 0) {
+                    pcsg_receiver->callback(PCSGCustomEventViewReceiverOK, pcsg_receiver->context);
+                }
+            },
+            false);
+    }
+
+    pcsg_view_receiver_update_offset(pcsg_receiver);
+
+    return true;
+}
+
+void pcsg_view_receiver_enter(void* context) {
+    furi_assert(context);
+}
+
+void pcsg_view_receiver_exit(void* context) {
+    furi_assert(context);
+    PCSGReceiver* pcsg_receiver = context;
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            furi_string_reset(model->frequency_str);
+            furi_string_reset(model->preset_str);
+            furi_string_reset(model->history_stat_str);
+                for
+                    M_EACH(item_menu, model->history->data, PCSGReceiverMenuItemArray_t) {
+                        furi_string_free(item_menu->item_str);
+                        item_menu->type = 0;
+                    }
+                PCSGReceiverMenuItemArray_reset(model->history->data);
+                model->idx = 0;
+                model->list_offset = 0;
+                model->history_item = 0;
+        },
+        false);
+    furi_timer_stop(pcsg_receiver->timer);
+}
+
+PCSGReceiver* pcsg_view_receiver_alloc() {
+    PCSGReceiver* pcsg_receiver = malloc(sizeof(PCSGReceiver));
+
+    // View allocation and configuration
+    pcsg_receiver->view = view_alloc();
+
+    pcsg_receiver->lock = PCSGLockOff;
+    pcsg_receiver->lock_count = 0;
+    view_allocate_model(pcsg_receiver->view, ViewModelTypeLocking, sizeof(PCSGReceiverModel));
+    view_set_context(pcsg_receiver->view, pcsg_receiver);
+    view_set_draw_callback(pcsg_receiver->view, (ViewDrawCallback)pcsg_view_receiver_draw);
+    view_set_input_callback(pcsg_receiver->view, pcsg_view_receiver_input);
+    view_set_enter_callback(pcsg_receiver->view, pcsg_view_receiver_enter);
+    view_set_exit_callback(pcsg_receiver->view, pcsg_view_receiver_exit);
+
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            model->frequency_str = furi_string_alloc();
+            model->preset_str = furi_string_alloc();
+            model->history_stat_str = furi_string_alloc();
+            model->bar_show = PCSGReceiverBarShowDefault;
+            model->history = malloc(sizeof(PCSGReceiverHistory));
+            PCSGReceiverMenuItemArray_init(model->history->data);
+        },
+        true);
+    pcsg_receiver->timer =
+        furi_timer_alloc(pcsg_view_receiver_timer_callback, FuriTimerTypeOnce, pcsg_receiver);
+    return pcsg_receiver;
+}
+
+void pcsg_view_receiver_free(PCSGReceiver* pcsg_receiver) {
+    furi_assert(pcsg_receiver);
+
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            furi_string_free(model->frequency_str);
+            furi_string_free(model->preset_str);
+            furi_string_free(model->history_stat_str);
+                for
+                    M_EACH(item_menu, model->history->data, PCSGReceiverMenuItemArray_t) {
+                        furi_string_free(item_menu->item_str);
+                        item_menu->type = 0;
+                    }
+                PCSGReceiverMenuItemArray_clear(model->history->data);
+                free(model->history);
+        },
+        false);
+    furi_timer_free(pcsg_receiver->timer);
+    view_free(pcsg_receiver->view);
+    free(pcsg_receiver);
+}
+
+View* pcsg_view_receiver_get_view(PCSGReceiver* pcsg_receiver) {
+    furi_assert(pcsg_receiver);
+    return pcsg_receiver->view;
+}
+
+uint16_t pcsg_view_receiver_get_idx_menu(PCSGReceiver* pcsg_receiver) {
+    furi_assert(pcsg_receiver);
+    uint32_t idx = 0;
+    with_view_model(
+        pcsg_receiver->view, PCSGReceiverModel * model, { idx = model->idx; }, false);
+    return idx;
+}
+
+void pcsg_view_receiver_set_idx_menu(PCSGReceiver* pcsg_receiver, uint16_t idx) {
+    furi_assert(pcsg_receiver);
+    with_view_model(
+        pcsg_receiver->view,
+        PCSGReceiverModel * model,
+        {
+            model->idx = idx;
+            if(model->idx > 2) model->list_offset = idx - 2;
+        },
+        true);
+    pcsg_view_receiver_update_offset(pcsg_receiver);
+}

+ 43 - 0
main_apps_sources/pocsag_pager/views/pocsag_pager_receiver.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/pocsag_pager_types.h"
+#include "../helpers/pocsag_pager_event.h"
+
+typedef struct PCSGReceiver PCSGReceiver;
+
+typedef void (*PCSGReceiverCallback)(PCSGCustomEvent event, void* context);
+
+void pcsg_receiver_rssi(PCSGReceiver* instance, float rssi);
+
+void pcsg_view_receiver_set_lock(PCSGReceiver* pcsg_receiver, PCSGLock keyboard);
+
+void pcsg_view_receiver_set_ext_module_state(PCSGReceiver* pcsg_receiver, bool is_external);
+
+void pcsg_view_receiver_set_callback(
+    PCSGReceiver* pcsg_receiver,
+    PCSGReceiverCallback callback,
+    void* context);
+
+PCSGReceiver* pcsg_view_receiver_alloc();
+
+void pcsg_view_receiver_free(PCSGReceiver* pcsg_receiver);
+
+View* pcsg_view_receiver_get_view(PCSGReceiver* pcsg_receiver);
+
+void pcsg_view_receiver_add_data_statusbar(
+    PCSGReceiver* pcsg_receiver,
+    const char* frequency_str,
+    const char* preset_str,
+    const char* history_stat_str);
+
+void pcsg_view_receiver_add_item_to_menu(
+    PCSGReceiver* pcsg_receiver,
+    const char* name,
+    uint8_t type);
+
+uint16_t pcsg_view_receiver_get_idx_menu(PCSGReceiver* pcsg_receiver);
+
+void pcsg_view_receiver_set_idx_menu(PCSGReceiver* pcsg_receiver, uint16_t idx);
+
+void pcsg_view_receiver_exit(void* context);

+ 137 - 0
main_apps_sources/pocsag_pager/views/pocsag_pager_receiver_info.c

@@ -0,0 +1,137 @@
+#include "pocsag_pager_receiver.h"
+#include "../pocsag_pager_app_i.h"
+#include "pocsag_pager_icons.h"
+#include "../protocols/pcsg_generic.h"
+#include <input/input.h>
+#include <gui/elements.h>
+
+#define abs(x) ((x) > 0 ? (x) : -(x))
+
+struct PCSGReceiverInfo {
+    View* view;
+};
+
+typedef struct {
+    FuriString* protocol_name;
+    PCSGBlockGeneric* generic;
+} PCSGReceiverInfoModel;
+
+void pcsg_view_receiver_info_update(PCSGReceiverInfo* pcsg_receiver_info, FlipperFormat* fff) {
+    furi_assert(pcsg_receiver_info);
+    furi_assert(fff);
+
+    with_view_model(
+        pcsg_receiver_info->view,
+        PCSGReceiverInfoModel * model,
+        {
+            flipper_format_rewind(fff);
+            flipper_format_read_string(fff, "Protocol", model->protocol_name);
+
+            pcsg_block_generic_deserialize(model->generic, fff);
+        },
+        true);
+}
+
+void pcsg_view_receiver_info_draw(Canvas* canvas, PCSGReceiverInfoModel* model) {
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontSecondary);
+    if(model->generic->result_ric != NULL) {
+        elements_text_box(
+            canvas,
+            0,
+            0,
+            128,
+            64,
+            AlignLeft,
+            AlignTop,
+            furi_string_get_cstr(model->generic->result_ric),
+            false);
+    }
+    if(model->generic->result_msg != NULL) {
+        elements_text_box(
+            canvas,
+            0,
+            12,
+            128,
+            64,
+            AlignLeft,
+            AlignTop,
+            furi_string_get_cstr(model->generic->result_msg),
+            false);
+    }
+}
+
+bool pcsg_view_receiver_info_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    //PCSGReceiverInfo* pcsg_receiver_info = context;
+
+    if(event->key == InputKeyBack) {
+        return false;
+    }
+
+    return true;
+}
+
+void pcsg_view_receiver_info_enter(void* context) {
+    furi_assert(context);
+}
+
+void pcsg_view_receiver_info_exit(void* context) {
+    furi_assert(context);
+    PCSGReceiverInfo* pcsg_receiver_info = context;
+
+    with_view_model(
+        pcsg_receiver_info->view,
+        PCSGReceiverInfoModel * model,
+        { furi_string_reset(model->protocol_name); },
+        false);
+}
+
+PCSGReceiverInfo* pcsg_view_receiver_info_alloc() {
+    PCSGReceiverInfo* pcsg_receiver_info = malloc(sizeof(PCSGReceiverInfo));
+
+    // View allocation and configuration
+    pcsg_receiver_info->view = view_alloc();
+
+    view_allocate_model(
+        pcsg_receiver_info->view, ViewModelTypeLocking, sizeof(PCSGReceiverInfoModel));
+    view_set_context(pcsg_receiver_info->view, pcsg_receiver_info);
+    view_set_draw_callback(
+        pcsg_receiver_info->view, (ViewDrawCallback)pcsg_view_receiver_info_draw);
+    view_set_input_callback(pcsg_receiver_info->view, pcsg_view_receiver_info_input);
+    view_set_enter_callback(pcsg_receiver_info->view, pcsg_view_receiver_info_enter);
+    view_set_exit_callback(pcsg_receiver_info->view, pcsg_view_receiver_info_exit);
+
+    with_view_model(
+        pcsg_receiver_info->view,
+        PCSGReceiverInfoModel * model,
+        {
+            model->generic = malloc(sizeof(PCSGBlockGeneric));
+            model->protocol_name = furi_string_alloc();
+        },
+        true);
+
+    return pcsg_receiver_info;
+}
+
+void pcsg_view_receiver_info_free(PCSGReceiverInfo* pcsg_receiver_info) {
+    furi_assert(pcsg_receiver_info);
+
+    with_view_model(
+        pcsg_receiver_info->view,
+        PCSGReceiverInfoModel * model,
+        {
+            furi_string_free(model->protocol_name);
+            free(model->generic);
+        },
+        false);
+
+    view_free(pcsg_receiver_info->view);
+    free(pcsg_receiver_info);
+}
+
+View* pcsg_view_receiver_info_get_view(PCSGReceiverInfo* pcsg_receiver_info) {
+    furi_assert(pcsg_receiver_info);
+    return pcsg_receiver_info->view;
+}

+ 16 - 0
main_apps_sources/pocsag_pager/views/pocsag_pager_receiver_info.h

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/pocsag_pager_types.h"
+#include "../helpers/pocsag_pager_event.h"
+#include <lib/flipper_format/flipper_format.h>
+
+typedef struct PCSGReceiverInfo PCSGReceiverInfo;
+
+void pcsg_view_receiver_info_update(PCSGReceiverInfo* pcsg_receiver_info, FlipperFormat* fff);
+
+PCSGReceiverInfo* pcsg_view_receiver_info_alloc();
+
+void pcsg_view_receiver_info_free(PCSGReceiverInfo* pcsg_receiver_info);
+
+View* pcsg_view_receiver_info_get_view(PCSGReceiverInfo* pcsg_receiver_info);

+ 24 - 0
main_apps_sources/protoview/LICENSE

@@ -0,0 +1,24 @@
+Copyright (c) 2022-2023 Salvatore Sanfilippo <antirez at gmail dot com>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 159 - 0
main_apps_sources/protoview/README.md

@@ -0,0 +1,159 @@
+ProtoView is a digital signal detection, visualization, editing and reply tool for the Flipper Zero. The Flipper default application, called Subghz, is able to identify certain RF protocols, but when the exact protocol is not implemented (and there are many undocumented and unimplemented ones, such as the ones in use in TPMS systems, car keys and many others), the curious person is left wondering what the device is sending at all. Using ProtoView she or he can visualize the high and low pulses  
+This is often enough to make an initial idea about the encoding used
+and if the selected modulation is correct. For example, in the signal above
+you can see a set of regular pulses and gaps used for synchronization, and then
+a sequence of bits encoded in Manchester - https://en.wikipedia.org/wiki/Manchester_code line code. If you study these things for five minutes, you'll find yourself able to decode the bits with naked eyes.
+
+## Decoding capabilities
+
+Other than showing the raw signal, ProtoView is able to decode a few interesting protocols:
+
+* TPMS sensors: Renault, Toyota, Schrader, Citroen, Ford.
+* Microchip HSC200/300/301 Keeloq protocol.
+* Oregon thermometer protocol 2.
+* PT2262, SC5262 based remotes.
+* ... more will be implemented soon, hopefully. Send PRs
+
+
+The app implements a framework that makes adding and experimenting with new
+protocols very simple. Check the (protocols) directory to see how the
+API works, or read the full documentation at the end of (README) file.
+The gist of it is that the decoder receives the signal already converted into
+a bitmap, where each bit represents a short pulse duration. Then there are
+functions to seek specific sync/preamble sequences inside the bitmap, to decode
+from different line codes, to compute checksums and so forth.
+
+## Signals transmission capabilities
+
+Once ProtoView decodes a given message, it is able to *resample* it
+in pulses and gaps of the theoretical duration, and send the signal again
+via the Flipper TX capabilities. The captured signal can be sent
+to different frequencies and modulations than the ones it was captured
+from.
+
+For selected protocols, that implement the message creation methods,
+ProtoView is also able to edit the message received, modify fields,
+and finally re-detect the new produced signal and resend it. Signals
+can also be produced from scratch, by setting all the fields to appropriate
+values.
+
+## A well-documented app for the Flipper
+
+The secondary goal of ProtoView is to provide a well-documented application for the Flipper (even if ProtoView is a pretty atypical application: it doesn't make use of the standard widgets and other abstractions provided by the framework).
+Most apps dealing with the *subghz subsystem* of the Flipper (the abstraction used to work with the CC1101 chip - https://www.ti.com/product/CC1101) tend to be complicated and completely undocumented.
+Unfortunately, this is also true for the firmware of the device.
+It's a shame, because especially in the case of code that talks with hardware peripherals there are tons of assumptions and hard-gained lessons that can only be captured by comments and are in the code only implicitly - http://antirez.com/news/124
+
+However, the Flipper firmware source code is well written even if it
+lacks comments and documentation (and sometimes makes use of abstractions more convoluted than needed), so it is possible to make some ideas of how things work just grepping inside. In order to develop this application, I ended reading most parts of the firmware of the device.
+
+## Detection algorithm
+
+In order to detect and show unknown signals, the application attempts to understand if the samples obtained by the Flipper API (a series of pulses that are high
+or low, and with different duration in microseconds) look like belonging to
+a legitimate signal, and aren't just noise.
+
+We can't make assumptions about
+the encoding and the data rate of the communication, so we use a simple
+but relatively effective algorithm. As we check the signal, we try to detect
+long parts of it that are composed of pulses roughly classifiable into
+a maximum of three different duration classes, plus or minus a given percentage.
+Most encodings are somewhat self-clocked, so they tend to have just two or
+three classes of pulse lengths.
+
+However, often, pulses of the same theoretical
+length have slightly different lengths in the case of high and low level
+(RF on or off), so the detector classifies them separately for robustness.
+
+Once the raw signal is detected, the registered protocol decoders are called
+against it, in the hope some of the decoders will make sense of the signal.
+
+# Usage
+
+In the main screen, the application shows the longest coherent signal detected so far. The user can switch to other views pressing the LEFT and RIGHT keys. The BACK key will return back to the main screen. Long pressing BACK will quit the application.
+
+## Main raw signal screen
+
+* A long press of the OK button resets the current signal, in order to capture a new one.
+* The UP and DOWN buttons change the scale. Default is 100us per pixel, but it will be adapted to the signal just captured.
+* A long press of the LEFT and RIGHT keys will pan the signal, to see what was transmitted before/after the current shown range.
+* A short press to OK will recenter the signal and set the scale back to the default for the specific pulse duration detected.
+
+Under the detected sequence, you will see a small triangle marking a
+specific sample. This mark means that the sequence looked coherent up
+to that point, and starting from there it could be just noise. However the
+signal decoders will not get just up to this point, but will get more:
+sometimes the low level detector can't make sense of a signal that the
+protocol-specific decoder can understand fully.
+
+If the protocol is decoded, the bottom-left corner of the screen
+will show the name of the protocol, and going in the next screen
+with the right arrow will show information about the decoded signal.
+
+In the bottom-right corner the application displays an amount of time
+in microseconds. This is the average length of the shortest pulse length
+detected among the three classes. Usually the *data rate* of the protocol
+is something like (1000000/this-number*2), but it depends on the encoding
+and could actually be (1000000/this-number*N) with (N > 2) (here 1000000
+is the number of microseconds in one second, and N is the number of clock
+cycles needed to represent a bit).
+
+## Info screen
+
+If a signal was detected, the info view will show the details about the signal. If the signal has more data that a single screen can fit, pressing OK will show the next fields. Pressing DOWN will go to a sub-view with an oscilloscope-alike representation of the signal, from there you can:
+
+1. Resend the signal, by pressing OK.
+2. Save the signal as (.sub) file, by long pressing OK.
+
+When resending, you can select a different frequency and modulation if you
+wish.
+
+## Frequency and modulation screen
+
+In this view you can just set the frequency and modulation you want to use.
+There are special modulations for TPMS signals: they need an higher data
+rate.
+
+* Many cheap remotes (gate openers, remotes, ...) are on the 433.92Mhz or nearby and use OOK modulation.
+* Weather stations are often too in the 433.92Mhz OOK.
+* For car keys, try 433.92 OOK650 and 868.35 Mhz in OOK or 2FSK.
+* For TPMS try 433.92 in TPMS1 or TPMS2 modulations (FSK and OOK custom modulations optimized for these signals, that have a relatively high data rate).
+
+## Signal creator
+
+In this view, you can do two things:
+
+1. Select one of the protocols supporting message creation, and create a signal from scratch.
+2. If there is already a detected signal, you can modify the signal.
+
+This is how it works:
+
+1. Select one of the protocols (the one of the currently detected signal will be already provided as default, if any, and if it supports message creation).
+2. Fill the fields. Use LEFT and RIGHT to change the values of integers, or just press OK and enter the new value with the Fliper keyboard widget.
+3. When you are done, long press OK to build the message. Then press BACK in order to see it.
+4. Go to the INFO view, and then DOWN to the signal sending/saving subview in order to send or save it.
+
+## Direct sampling screen
+
+This final screen shows in real time the high and low level that the Flipper
+RF chip, the CC1101, is receiving. This will makes very easy to understand
+if a given frequency is targeted by something other than noise. This mode is
+fun to watch, resembling an old CRT TV set.
+
+# License
+
+The code is released under the BSD license.
+
+# Disclaimer
+
+This application is only provided as an educational tool. The author is not liable in case the application is used to reverse engineer protocols protected by IP or for any other illegal purpose.
+
+# Credits
+
+A big thank you to the RTL433 author, Benjamin Larsson - https://github.com/merbanan I used the code and tools he developed in many ways:
+* To capture TPMS data with rtl433 and save to a file, to later play the IQ files and speedup the development.
+* As a sourve of documentation for protocols.
+* As an awesome way to visualize and understand protocols, via these great web tools - https://triq.org/
+* To have tons of fun with RTLSDR in general, now and in the past.
+
+The application icon was designed by Stefano Liuzzo.

+ 400 - 0
main_apps_sources/protoview/app.c

@@ -0,0 +1,400 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+
+RawSamplesBuffer *RawSamples, *DetectedSamples;
+extern const SubGhzProtocolRegistry protoview_protocol_registry;
+
+/* Draw some text with a border. If the outside color is black and the inside
+ * color is white, it just writes the border of the text, but the function can
+ * also be used to write a bold variation of the font setting both the
+ * colors to black, or alternatively to write a black text with a white
+ * border so that it is visible if there are black stuff on the background. */
+/* The callback actually just passes the control to the actual active
+ * view callback, after setting up basic stuff like cleaning the screen
+ * and setting color to black. */
+static void render_callback(Canvas* const canvas, void* ctx) {
+    ProtoViewApp* app = ctx;
+    furi_mutex_acquire(app->view_updating_mutex, FuriWaitForever);
+
+    /* Clear screen. */
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, 0, 0, 127, 63);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+
+    /* Call who is in charge right now. */
+    switch(app->current_view) {
+    case ViewRawPulses:
+        render_view_raw_pulses(canvas, app);
+        break;
+    case ViewInfo:
+        render_view_info(canvas, app);
+        break;
+    case ViewFrequencySettings:
+    case ViewModulationSettings:
+        render_view_settings(canvas, app);
+        break;
+    case ViewDirectSampling:
+        render_view_direct_sampling(canvas, app);
+        break;
+    case ViewBuildMessage:
+        render_view_build_message(canvas, app);
+        break;
+    default:
+        furi_crash(TAG "Invalid view selected");
+        break;
+    }
+
+    /* Draw the alert box if set. */
+    ui_draw_alert_if_needed(canvas, app);
+    furi_mutex_release(app->view_updating_mutex);
+}
+
+/* Here all we do is putting the events into the queue that will be handled
+ * in the while() loop of the app entry point function. */
+static void input_callback(InputEvent* input_event, void* ctx) {
+    ProtoViewApp* app = ctx;
+    furi_message_queue_put(app->event_queue, input_event, FuriWaitForever);
+}
+
+/* Called to switch view (when left/right is pressed). Handles
+ * changing the current view ID and calling the enter/exit view
+ * callbacks if needed.
+ *
+ * The 'switchto' parameter can be the identifier of a view, or the
+ * special views ViewGoNext and ViewGoPrev in order to move to
+ * the logical next/prev view. */
+static void app_switch_view(ProtoViewApp* app, ProtoViewCurrentView switchto) {
+    furi_mutex_acquire(app->view_updating_mutex, FuriWaitForever);
+
+    /* Switch to the specified view. */
+    ProtoViewCurrentView old = app->current_view;
+    if(switchto == ViewGoNext) {
+        app->current_view++;
+        if(app->current_view == ViewLast) app->current_view = 0;
+    } else if(switchto == ViewGoPrev) {
+        if(app->current_view == 0)
+            app->current_view = ViewLast - 1;
+        else
+            app->current_view--;
+    } else {
+        app->current_view = switchto;
+    }
+    ProtoViewCurrentView new = app->current_view;
+
+    /* Call the exit view callbacks. */
+    if(old == ViewDirectSampling) view_exit_direct_sampling(app);
+    if(old == ViewBuildMessage) view_exit_build_message(app);
+    if(old == ViewInfo) view_exit_info(app);
+    /* The frequency/modulation settings are actually a single view:
+     * as long as the user stays between the two modes of this view we
+     * don't need to call the exit-view callback. */
+    if((old == ViewFrequencySettings && new != ViewModulationSettings) ||
+       (old == ViewModulationSettings && new != ViewFrequencySettings))
+        view_exit_settings(app);
+
+    /* Reset the view private data each time, before calling the enter
+     * callbacks that may want to setup some state. */
+    memset(app->view_privdata, 0, PROTOVIEW_VIEW_PRIVDATA_LEN);
+
+    /* Call the enter view callbacks after all the exit callback
+     * of the old view was already executed. */
+    if(new == ViewDirectSampling) view_enter_direct_sampling(app);
+    if(new == ViewBuildMessage) view_enter_build_message(app);
+
+    /* Set the current subview of the view we just left to zero. This is
+     * the main subview of the old view. When we re-enter the view we are
+     * lefting, we want to see the main thing again. */
+    app->current_subview[old] = 0;
+
+    /* If there is an alert on screen, dismiss it: if the user is
+     * switching view she already read it. */
+    ui_dismiss_alert(app);
+    furi_mutex_release(app->view_updating_mutex);
+}
+
+/* Allocate the application state and initialize a number of stuff.
+ * This is called in the entry point to create the application state. */
+ProtoViewApp* protoview_app_alloc() {
+    furi_hal_power_suppress_charge_enter();
+
+    ProtoViewApp* app = malloc(sizeof(ProtoViewApp));
+
+    // Init shared data structures
+    RawSamples = raw_samples_alloc();
+    DetectedSamples = raw_samples_alloc();
+
+    //init setting
+    app->setting = subghz_setting_alloc();
+    subghz_setting_load(app->setting, EXT_PATH("subghz/assets/setting_user"));
+
+    // GUI
+    app->gui = furi_record_open(RECORD_GUI);
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+    app->view_port = view_port_alloc();
+    view_port_draw_callback_set(app->view_port, render_callback, app);
+    view_port_input_callback_set(app->view_port, input_callback, app);
+    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+    app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+    app->view_dispatcher = NULL;
+    app->text_input = NULL;
+    app->show_text_input = false;
+    app->alert_dismiss_time = 0;
+    app->current_view = ViewRawPulses;
+    app->view_updating_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    for(int j = 0; j < ViewLast; j++) app->current_subview[j] = 0;
+    app->direct_sampling_enabled = false;
+    app->view_privdata = malloc(PROTOVIEW_VIEW_PRIVDATA_LEN);
+    memset(app->view_privdata, 0, PROTOVIEW_VIEW_PRIVDATA_LEN);
+
+    // Signal found and visualization defaults
+    app->signal_bestlen = 0;
+    app->signal_last_scan_idx = 0;
+    app->signal_decoded = false;
+    app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
+    app->signal_offset = 0;
+    app->msg_info = NULL;
+
+    // Init Worker & Protocol
+    app->txrx = malloc(sizeof(ProtoViewTxRx));
+
+    /* Setup rx state. */
+    app->txrx->freq_mod_changed = false;
+    app->txrx->debug_timer_sampling = false;
+    app->txrx->last_g0_change_time = DWT->CYCCNT;
+    app->txrx->last_g0_value = false;
+
+    app->frequency = subghz_setting_get_default_frequency(app->setting);
+    app->modulation = 0; /* Defaults to ProtoViewModulations[0]. */
+
+    // Init & set radio_device
+    subghz_devices_init();
+    app->radio_device =
+        radio_device_loader_set(app->radio_device, SubGhzRadioDeviceTypeExternalCC1101);
+
+    subghz_devices_reset(app->radio_device);
+    subghz_devices_idle(app->radio_device);
+
+    app->running = 1;
+
+    return app;
+}
+
+/* Free what the application allocated. It is not clear to me if the
+ * Flipper OS, once the application exits, will be able to reclaim space
+ * even if we forget to free something here. */
+void protoview_app_free(ProtoViewApp* app) {
+    furi_assert(app);
+
+    subghz_devices_sleep(app->radio_device);
+    radio_device_loader_end(app->radio_device);
+
+    subghz_devices_deinit();
+
+    // View related.
+    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_record_close(RECORD_NOTIFICATION);
+    furi_message_queue_free(app->event_queue);
+    furi_mutex_free(app->view_updating_mutex);
+    app->gui = NULL;
+
+    // Frequency setting.
+    subghz_setting_free(app->setting);
+
+    // Worker stuff.
+    free(app->txrx);
+
+    // Raw samples buffers.
+    raw_samples_free(RawSamples);
+    raw_samples_free(DetectedSamples);
+    furi_hal_power_suppress_charge_exit();
+
+    free(app);
+}
+
+/* Called periodically. Do signal processing here. Data we process here
+ * will be later displayed by the render callback. The side effect of this
+ * function is to scan for signals and set DetectedSamples. */
+static void timer_callback(void* ctx) {
+    ProtoViewApp* app = ctx;
+    uint32_t delta, lastidx = app->signal_last_scan_idx;
+
+    /* scan_for_signal(), called by this function, deals with a
+     * circular buffer. To never miss anything, even if a signal spawns
+     * cross-boundaries, it is enough if we scan each time the buffer fills
+     * for 50% more compared to the last scan. Thanks to this check we
+     * can avoid scanning too many times to just find the same data. */
+    if(lastidx < RawSamples->idx) {
+        delta = RawSamples->idx - lastidx;
+    } else {
+        delta = RawSamples->total - lastidx + RawSamples->idx;
+    }
+    if(delta < RawSamples->total / 2) return;
+    app->signal_last_scan_idx = RawSamples->idx;
+    scan_for_signal(app, RawSamples, ProtoViewModulations[app->modulation].duration_filter);
+}
+
+/* This is the navigation callback we use in the view dispatcher used
+ * to display the "text input" widget, that is the keyboard to get text.
+ * The text input view is implemented to ignore the "back" short press,
+ * so the event is not consumed and is handled by the view dispatcher.
+ * However the view dispatcher implementation has the strange behavior that
+ * if no navigation callback is set, it will not stop when handling back.
+ *
+ * We just need a dummy callback returning false. We believe the
+ * implementation should be changed and if no callback is set, it should be
+ * the same as returning false. */
+static bool keyboard_view_dispatcher_navigation_callback(void* ctx) {
+    UNUSED(ctx);
+    return false;
+}
+
+/* App entry point, as specified in application.fam. */
+int32_t protoview_app_entry(void* p) {
+    UNUSED(p);
+    ProtoViewApp* app = protoview_app_alloc();
+
+    /* Create a timer. We do data analysis in the callback. */
+    FuriTimer* timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, app);
+    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 8);
+
+    /* Start listening to signals immediately. */
+    radio_begin(app);
+    radio_rx(app);
+
+    /* This is the main event loop: here we get the events that are pushed
+     * in the queue by input_callback(), and process them one after the
+     * other. The timeout is 100 milliseconds, so if not input is received
+     * before such time, we exit the queue_get() function and call
+     * view_port_update() in order to refresh our screen content. */
+    InputEvent input;
+    while(app->running) {
+        FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100);
+        if(qstat == FuriStatusOk) {
+            if(DEBUG_MSG)
+                FURI_LOG_E(TAG, "Main Loop - Input: type %d key %u", input.type, input.key);
+
+            /* Handle navigation here. Then handle view-specific inputs
+             * in the view specific handling function. */
+            if(input.type == InputTypeShort && input.key == InputKeyBack) {
+                if(app->current_view != ViewRawPulses) {
+                    /* If this is not the main app view, go there. */
+                    app_switch_view(app, ViewRawPulses);
+                } else {
+                    /* If we are in the main app view, warn the user
+                     * they needs to long press to really quit. */
+                    ui_show_alert(app, "Long press to exit", 1000);
+                }
+            } else if(input.type == InputTypeLong && input.key == InputKeyBack) {
+                app->running = 0;
+            } else if(
+                input.type == InputTypeShort && input.key == InputKeyRight &&
+                ui_get_current_subview(app) == 0) {
+                /* Go to the next view. */
+                app_switch_view(app, ViewGoNext);
+            } else if(
+                input.type == InputTypeShort && input.key == InputKeyLeft &&
+                ui_get_current_subview(app) == 0) {
+                /* Go to the previous view. */
+                app_switch_view(app, ViewGoPrev);
+            } else {
+                /* This is where we pass the control to the currently
+                 * active view input processing. */
+                switch(app->current_view) {
+                case ViewRawPulses:
+                    process_input_raw_pulses(app, input);
+                    break;
+                case ViewInfo:
+                    process_input_info(app, input);
+                    break;
+                case ViewFrequencySettings:
+                case ViewModulationSettings:
+                    process_input_settings(app, input);
+                    break;
+                case ViewDirectSampling:
+                    process_input_direct_sampling(app, input);
+                    break;
+                case ViewBuildMessage:
+                    process_input_build_message(app, input);
+                    break;
+                default:
+                    furi_crash(TAG "Invalid view selected");
+                    break;
+                }
+            }
+        } else {
+            /* Useful to understand if the app is still alive when it
+             * does not respond because of bugs. */
+            if(DEBUG_MSG) {
+                static int c = 0;
+                c++;
+                if(!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
+            }
+        }
+        if(app->show_text_input) {
+            /* Remove our viewport: we need to use a view dispatcher
+             * in order to show the standard Flipper keyboard. */
+            gui_remove_view_port(app->gui, app->view_port);
+
+            /* Allocate a view dispatcher, add a text input view to it,
+             * and activate it. */
+            app->view_dispatcher = view_dispatcher_alloc();
+            view_dispatcher_enable_queue(app->view_dispatcher);
+            /* We need to set a navigation callback for the view dispatcher
+             * otherwise when the user presses back on the keyboard to
+             * abort, the dispatcher will not stop. */
+            view_dispatcher_set_navigation_event_callback(
+                app->view_dispatcher, keyboard_view_dispatcher_navigation_callback);
+            app->text_input = text_input_alloc();
+            view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+            view_dispatcher_add_view(
+                app->view_dispatcher, 0, text_input_get_view(app->text_input));
+            view_dispatcher_switch_to_view(app->view_dispatcher, 0);
+
+            /* Setup the text input view. The different parameters are set
+             * in the app structure by the view that wanted to show the
+             * input text. The callback, buffer and buffer len must be set.  */
+            text_input_set_header_text(app->text_input, "Save signal filename");
+            text_input_set_result_callback(
+                app->text_input,
+                app->text_input_done_callback,
+                app,
+                app->text_input_buffer,
+                app->text_input_buffer_len,
+                false);
+
+            /* Run the dispatcher with the keyboard. */
+            view_dispatcher_attach_to_gui(
+                app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+            view_dispatcher_run(app->view_dispatcher);
+
+            /* Undo all it: remove the view from the dispatcher, free it
+             * so that it removes itself from the current gui, finally
+             * restore our viewport. */
+            view_dispatcher_remove_view(app->view_dispatcher, 0);
+            text_input_free(app->text_input);
+            view_dispatcher_free(app->view_dispatcher);
+            app->view_dispatcher = NULL;
+            gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+            app->show_text_input = false;
+        } else {
+            view_port_update(app->view_port);
+        }
+    }
+
+    /* App no longer running. Shut down and free. */
+    if(app->txrx->txrx_state == TxRxStateRx) {
+        FURI_LOG_E(TAG, "Putting CC1101 to sleep before exiting.");
+        radio_rx_end(app);
+        radio_sleep(app);
+    }
+
+    furi_timer_free(timer);
+    protoview_app_free(app);
+    return 0;
+}

+ 377 - 0
main_apps_sources/protoview/app.h

@@ -0,0 +1,377 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/gui.h>
+#include <stdlib.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
+#include <notification/notification_messages.h>
+#include <lib/subghz/subghz_setting.h>
+#include <lib/subghz/registry.h>
+#include "raw_samples.h"
+#include "helpers/radio_device_loader.h"
+
+#define TAG "ProtoView"
+#define PROTOVIEW_RAW_VIEW_DEFAULT_SCALE 100 // 100us is 1 pixel by default
+#define BITMAP_SEEK_NOT_FOUND UINT32_MAX // Returned by function as sentinel
+#define PROTOVIEW_VIEW_PRIVDATA_LEN 64 // View specific private data len
+
+#define DEBUG_MSG 0
+
+/* Forward declarations. */
+
+typedef struct ProtoViewApp ProtoViewApp;
+typedef struct ProtoViewMsgInfo ProtoViewMsgInfo;
+typedef struct ProtoViewFieldSet ProtoViewFieldSet;
+typedef struct ProtoViewDecoder ProtoViewDecoder;
+
+/* ============================== enumerations ============================== */
+
+/* Subghz system state */
+typedef enum {
+    TxRxStateIDLE,
+    TxRxStateRx,
+    TxRxStateTx,
+    TxRxStateSleep,
+} TxRxState;
+
+/* Currently active view. */
+typedef enum {
+    ViewRawPulses,
+    ViewInfo,
+    ViewFrequencySettings,
+    ViewModulationSettings,
+    ViewBuildMessage,
+    ViewDirectSampling,
+    ViewLast, /* Just a sentinel to wrap around. */
+
+    /* The following are special views that are not iterated, but
+     * have meaning for the API. */
+    ViewGoNext,
+    ViewGoPrev,
+} ProtoViewCurrentView;
+
+/* ================================== RX/TX ================================= */
+
+typedef struct {
+    const char* name; // Name to show to the user.
+    const char* id; // Identifier in the Flipper API/file.
+    FuriHalSubGhzPreset preset; // The preset ID.
+    uint8_t* custom; /* If not null, a set of registers for
+                                       the CC1101, specifying a custom preset.*/
+    uint32_t duration_filter; /* Ignore pulses and gaps that are less
+                                       than the specified microseconds. This
+                                       depends on the data rate. */
+} ProtoViewModulation;
+
+extern ProtoViewModulation ProtoViewModulations[]; /* In app_subghz.c */
+
+/* This is the context of our subghz worker and associated thread.
+ * It receives data and we get our protocol "feed" callback called
+ * with the level (1 or 0) and duration. */
+struct ProtoViewTxRx {
+    bool freq_mod_changed; /* The user changed frequency and/or modulation
+                                   from the interface. There is to restart the
+                                   radio with the right parameters. */
+    TxRxState txrx_state; /* Receiving, idle or sleeping? */
+
+    /* Timer sampling mode state. */
+    bool debug_timer_sampling; /* Read data from GDO0 in a busy loop. Only
+                                   for testing. */
+    uint32_t last_g0_change_time; /* Last high->low (or reverse) switch. */
+    bool last_g0_value; /* Current value (high or low): we are
+                                     checking the duration in the timer
+                                     handler. */
+};
+
+typedef struct ProtoViewTxRx ProtoViewTxRx;
+
+/* ============================== Main app state ============================ */
+
+#define ALERT_MAX_LEN 32
+struct ProtoViewApp {
+    /* GUI */
+    Gui* gui;
+    NotificationApp* notification;
+    ViewPort* view_port; /* We just use a raw viewport and we render
+                                everything into the low level canvas. */
+    ProtoViewCurrentView current_view; /* Active left-right view ID. */
+    FuriMutex* view_updating_mutex; /* The Flipper GUI calls the screen redraw
+                                       callback in a different thread. We
+                                       use this mutex to protect the redraw
+                                       from changes in app->view_privdata. */
+    int current_subview[ViewLast]; /* Active up-down subview ID. */
+    FuriMessageQueue* event_queue; /* Keypress events go here. */
+
+    /* Input text state. */
+    ViewDispatcher* view_dispatcher; /* Used only when we want to show
+                                        the text_input view for a moment.
+                                        Otherwise it is set to null. */
+    TextInput* text_input;
+    bool show_text_input;
+    char* text_input_buffer;
+    uint32_t text_input_buffer_len;
+    void (*text_input_done_callback)(void*);
+
+    /* Alert state. */
+    uint32_t alert_dismiss_time; /* Millisecond when the alert will be
+                                       no longer shown. Or zero if the alert
+                                       is currently not set at all. */
+    char alert_text[ALERT_MAX_LEN]; /* Alert content. */
+
+    /* Radio related. */
+    ProtoViewTxRx* txrx; /* Radio state. */
+    SubGhzSetting* setting; /* A list of valid frequencies. */
+
+    const SubGhzDevice* radio_device;
+
+    /* Generic app state. */
+    int running; /* Once false exists the app. */
+    uint32_t signal_bestlen; /* Longest coherent signal observed so far. */
+    uint32_t signal_last_scan_idx; /* Index of the buffer last time we
+                                      performed the scan. */
+    bool signal_decoded; /* Was the current signal decoded? */
+    ProtoViewMsgInfo* msg_info; /* Decoded message info if not NULL. */
+    bool direct_sampling_enabled; /* This special view needs an explicit
+                                     acknowledge to work. */
+    void* view_privdata; /* This is a piece of memory of total size
+                               PROTOVIEW_VIEW_PRIVDATA_LEN that it is
+                               initialized to zero when we switch to
+                               a a new view. While the view we are using
+                               is the same, it can be used by the view to
+                               store any kind of info inside, just casting
+                               the pointer to a few specific-data structure. */
+
+    /* Raw view apps state. */
+    uint32_t us_scale; /* microseconds per pixel. */
+    uint32_t signal_offset; /* Long press left/right panning in raw view. */
+
+    /* Configuration view app state. */
+    uint32_t frequency; /* Current frequency. */
+    uint8_t modulation; /* Current modulation ID, array index in the
+                                ProtoViewModulations table. */
+};
+
+/* =========================== Protocols decoders =========================== */
+
+/* This stucture is filled by the decoder for specific protocols with the
+ * informations about the message. ProtoView will display such information
+ * in the message info view. */
+#define PROTOVIEW_MSG_STR_LEN 32
+typedef struct ProtoViewMsgInfo {
+    ProtoViewDecoder* decoder; /* The decoder that decoded the message. */
+    ProtoViewFieldSet* fieldset; /* Decoded fields. */
+    /* Low level information of the detected signal: the following are filled
+     * by the protocol decoding function: */
+    uint32_t start_off; /* Pulses start offset in the bitmap. */
+    uint32_t pulses_count; /* Number of pulses of the full message. */
+    /* The following are passed already filled to the decoder. */
+    uint32_t short_pulse_dur; /* Microseconds duration of the short pulse. */
+    /* The following are filled by ProtoView core after the decoder returned
+     * success. */
+    uint8_t* bits; /* Bitmap with the signal. */
+    uint32_t bits_bytes; /* Number of full bytes in the bitmap, that
+                                   is 'pulses_count/8' rounded to the next
+                                   integer. */
+} ProtoViewMsgInfo;
+
+/* This structures describe a set of protocol fields. It is used by decoders
+ * supporting message building to receive and return information about the
+ * protocol. */
+typedef enum {
+    FieldTypeStr,
+    FieldTypeSignedInt,
+    FieldTypeUnsignedInt,
+    FieldTypeBinary,
+    FieldTypeHex,
+    FieldTypeBytes,
+    FieldTypeFloat,
+} ProtoViewFieldType;
+
+typedef struct {
+    ProtoViewFieldType type;
+    uint32_t len; // Depends on type:
+        // Bits for integers (signed,unsigned,binary,hex).
+        // Number of characters for strings.
+        // Number of nibbles for bytes (1 for each 4 bits).
+        // Number of digits after dot for floats.
+    char* name; // Field name.
+    union {
+        char* str; // String type.
+        int64_t value; // Signed integer type.
+        uint64_t uvalue; // Unsigned integer type.
+        uint8_t* bytes; // Raw bytes type.
+        float fvalue; // Float type.
+    };
+} ProtoViewField;
+
+typedef struct ProtoViewFieldSet {
+    ProtoViewField** fields;
+    uint32_t numfields;
+} ProtoViewFieldSet;
+
+typedef struct ProtoViewDecoder {
+    const char* name; /* Protocol name. */
+    /* The decode function takes a buffer that is actually a bitmap, with
+     * high and low levels represented as 0 and 1. The number of high/low
+     * pulses represented by the bitmap is passed as the 'numbits' argument,
+     * while 'numbytes' represents the total size of the bitmap pointed by
+     * 'bits'. So 'numbytes' is mainly useful to pass as argument to other
+     * functions that perform bit extraction with bound checking, such as
+     * bitmap_get() and so forth. */
+    bool (*decode)(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info);
+    /* This method is used by the decoder to return the fields it needs
+     * in order to build a new message. This way the message builder view
+     * can ask the user to fill the right set of fields of the specified
+     * type. */
+    void (*get_fields)(ProtoViewFieldSet* fields);
+    /* This method takes the fields supported by the decoder, and
+     * renders a message in 'samples'. */
+    void (*build_message)(RawSamplesBuffer* samples, ProtoViewFieldSet* fields);
+} ProtoViewDecoder;
+
+extern RawSamplesBuffer *RawSamples, *DetectedSamples;
+
+/* app_subghz.c */
+void radio_begin(ProtoViewApp* app);
+uint32_t radio_rx(ProtoViewApp* app);
+void radio_idle(ProtoViewApp* app);
+void radio_rx_end(ProtoViewApp* app);
+void radio_sleep(ProtoViewApp* app);
+void raw_sampling_worker_start(ProtoViewApp* app);
+void raw_sampling_worker_stop(ProtoViewApp* app);
+void radio_tx_signal(ProtoViewApp* app, FuriHalSubGhzAsyncTxCallback data_feeder, void* ctx);
+void protoview_rx_callback(bool level, uint32_t duration, void* context);
+
+/* signal.c */
+uint32_t duration_delta(uint32_t a, uint32_t b);
+void reset_current_signal(ProtoViewApp* app);
+void scan_for_signal(ProtoViewApp* app, RawSamplesBuffer* source, uint32_t min_duration);
+bool bitmap_get(uint8_t* b, uint32_t blen, uint32_t bitpos);
+void bitmap_set(uint8_t* b, uint32_t blen, uint32_t bitpos, bool val);
+void bitmap_copy(
+    uint8_t* d,
+    uint32_t dlen,
+    uint32_t doff,
+    uint8_t* s,
+    uint32_t slen,
+    uint32_t soff,
+    uint32_t count);
+void bitmap_set_pattern(uint8_t* b, uint32_t blen, uint32_t off, const char* pat);
+void bitmap_reverse_bytes_bits(uint8_t* p, uint32_t len);
+bool bitmap_match_bits(uint8_t* b, uint32_t blen, uint32_t bitpos, const char* bits);
+uint32_t bitmap_seek_bits(
+    uint8_t* b,
+    uint32_t blen,
+    uint32_t startpos,
+    uint32_t maxbits,
+    const char* bits);
+bool bitmap_match_bitmap(
+    uint8_t* b1,
+    uint32_t b1len,
+    uint32_t b1off,
+    uint8_t* b2,
+    uint32_t b2len,
+    uint32_t b2off,
+    uint32_t cmplen);
+void bitmap_to_string(char* dst, uint8_t* b, uint32_t blen, uint32_t off, uint32_t len);
+uint32_t convert_from_line_code(
+    uint8_t* buf,
+    uint64_t buflen,
+    uint8_t* bits,
+    uint32_t len,
+    uint32_t offset,
+    const char* zero_pattern,
+    const char* one_pattern);
+uint32_t convert_from_diff_manchester(
+    uint8_t* buf,
+    uint64_t buflen,
+    uint8_t* bits,
+    uint32_t len,
+    uint32_t off,
+    bool previous);
+void init_msg_info(ProtoViewMsgInfo* i, ProtoViewApp* app);
+void free_msg_info(ProtoViewMsgInfo* i);
+
+/* signal_file.c */
+bool save_signal(ProtoViewApp* app, const char* filename);
+
+/* view_*.c */
+void render_view_raw_pulses(Canvas* const canvas, ProtoViewApp* app);
+void process_input_raw_pulses(ProtoViewApp* app, InputEvent input);
+void render_view_settings(Canvas* const canvas, ProtoViewApp* app);
+void process_input_settings(ProtoViewApp* app, InputEvent input);
+void render_view_info(Canvas* const canvas, ProtoViewApp* app);
+void process_input_info(ProtoViewApp* app, InputEvent input);
+void render_view_direct_sampling(Canvas* const canvas, ProtoViewApp* app);
+void process_input_direct_sampling(ProtoViewApp* app, InputEvent input);
+void render_view_build_message(Canvas* const canvas, ProtoViewApp* app);
+void process_input_build_message(ProtoViewApp* app, InputEvent input);
+void view_enter_build_message(ProtoViewApp* app);
+void view_exit_build_message(ProtoViewApp* app);
+void view_enter_direct_sampling(ProtoViewApp* app);
+void view_exit_direct_sampling(ProtoViewApp* app);
+void view_exit_settings(ProtoViewApp* app);
+void view_exit_info(ProtoViewApp* app);
+void adjust_raw_view_scale(ProtoViewApp* app, uint32_t short_pulse_dur);
+
+/* ui.c */
+int ui_get_current_subview(ProtoViewApp* app);
+void ui_show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview);
+bool ui_process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subview);
+void ui_show_keyboard(
+    ProtoViewApp* app,
+    char* buffer,
+    uint32_t buflen,
+    void (*done_callback)(void*));
+void ui_dismiss_keyboard(ProtoViewApp* app);
+void ui_show_alert(ProtoViewApp* app, const char* text, uint32_t ttl);
+void ui_dismiss_alert(ProtoViewApp* app);
+void ui_draw_alert_if_needed(Canvas* canvas, ProtoViewApp* app);
+void canvas_draw_str_with_border(
+    Canvas* canvas,
+    uint8_t x,
+    uint8_t y,
+    const char* str,
+    Color text_color,
+    Color border_color);
+
+/* fields.c */
+void fieldset_free(ProtoViewFieldSet* fs);
+ProtoViewFieldSet* fieldset_new(void);
+void fieldset_add_int(ProtoViewFieldSet* fs, const char* name, int64_t val, uint8_t bits);
+void fieldset_add_uint(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits);
+void fieldset_add_hex(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits);
+void fieldset_add_bin(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits);
+void fieldset_add_str(ProtoViewFieldSet* fs, const char* name, const char* s, size_t len);
+void fieldset_add_bytes(
+    ProtoViewFieldSet* fs,
+    const char* name,
+    const uint8_t* bytes,
+    uint32_t count);
+void fieldset_add_float(
+    ProtoViewFieldSet* fs,
+    const char* name,
+    float val,
+    uint32_t digits_after_dot);
+const char* field_get_type_name(ProtoViewField* f);
+int field_to_string(char* buf, size_t len, ProtoViewField* f);
+bool field_set_from_string(ProtoViewField* f, char* buf, size_t len);
+bool field_incr_value(ProtoViewField* f, int incr);
+void fieldset_copy_matching_fields(ProtoViewFieldSet* dst, ProtoViewFieldSet* src);
+void field_set_from_field(ProtoViewField* dst, ProtoViewField* src);
+
+/* crc.c */
+uint8_t crc8(const uint8_t* data, size_t len, uint8_t init, uint8_t poly);
+uint8_t sum_bytes(const uint8_t* data, size_t len, uint8_t init);
+uint8_t xor_bytes(const uint8_t* data, size_t len, uint8_t init);

+ 206 - 0
main_apps_sources/protoview/app_subghz.c

@@ -0,0 +1,206 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+#include "custom_presets.h"
+
+#include <flipper_format/flipper_format_i.h>
+#include <furi_hal_rtc.h>
+#include <furi_hal_spi.h>
+#include <furi_hal_interrupt.h>
+
+void raw_sampling_timer_start(ProtoViewApp* app);
+void raw_sampling_timer_stop(ProtoViewApp* app);
+
+ProtoViewModulation ProtoViewModulations[] = {
+    {"OOK 650Khz", "FuriHalSubGhzPresetOok650Async", FuriHalSubGhzPresetOok650Async, NULL, 30},
+    {"OOK 270Khz", "FuriHalSubGhzPresetOok270Async", FuriHalSubGhzPresetOok270Async, NULL, 30},
+    {"2FSK 2.38Khz",
+     "FuriHalSubGhzPreset2FSKDev238Async",
+     FuriHalSubGhzPreset2FSKDev238Async,
+     NULL,
+     30},
+    {"2FSK 47.6Khz",
+     "FuriHalSubGhzPreset2FSKDev476Async",
+     FuriHalSubGhzPreset2FSKDev476Async,
+     NULL,
+     30},
+    {"TPMS 1 (FSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms1_fsk_async_regs, 30},
+    {"TPMS 2 (OOK)", NULL, 0, (uint8_t*)protoview_subghz_tpms2_ook_async_regs, 30},
+    {"TPMS 3 (GFSK)", NULL, 0, (uint8_t*)protoview_subghz_tpms3_gfsk_async_regs, 30},
+    {"OOK 40kBaud", NULL, 0, (uint8_t*)protoview_subghz_40k_ook_async_regs, 15},
+    {"FSK 40kBaud", NULL, 0, (uint8_t*)protoview_subghz_40k_fsk_async_regs, 15},
+    {NULL, NULL, 0, NULL, 0} /* End of list sentinel. */
+};
+
+/* Called after the application initialization in order to setup the
+ * subghz system and put it into idle state. */
+void radio_begin(ProtoViewApp* app) {
+    furi_assert(app);
+    subghz_devices_reset(app->radio_device);
+    subghz_devices_idle(app->radio_device);
+
+    /* The CC1101 preset can be either one of the standard presets, if
+     * the modulation "custom" field is NULL, or a custom preset we
+     * defined in custom_presets.h. */
+    if(ProtoViewModulations[app->modulation].custom == NULL) {
+        subghz_devices_load_preset(
+            app->radio_device, ProtoViewModulations[app->modulation].preset, NULL);
+    } else {
+        subghz_devices_load_preset(
+            app->radio_device,
+            FuriHalSubGhzPresetCustom,
+            ProtoViewModulations[app->modulation].custom);
+    }
+    furi_hal_gpio_init(
+        subghz_devices_get_data_gpio(app->radio_device), GpioModeInput, GpioPullNo, GpioSpeedLow);
+    app->txrx->txrx_state = TxRxStateIDLE;
+}
+
+/* ================================= Reception ============================== */
+
+/* We avoid the subghz provided abstractions and put the data in our
+ * simple abstraction: the RawSamples circular buffer. */
+void protoview_rx_callback(bool level, uint32_t duration, void* context) {
+    UNUSED(context);
+    /* Add data to the circular buffer. */
+    raw_samples_add(RawSamples, level, duration);
+    // FURI_LOG_E(TAG, "FEED: %d %d", (int)level, (int)duration);
+    return;
+}
+
+/* Setup the CC1101 to start receiving using a background worker. */
+uint32_t radio_rx(ProtoViewApp* app) {
+    furi_assert(app);
+
+    if(!subghz_devices_is_frequency_valid(app->radio_device, app->frequency)) {
+        furi_crash(TAG " Incorrect RX frequency.");
+    }
+
+    if(app->txrx->txrx_state == TxRxStateRx) return app->frequency;
+
+    subghz_devices_idle(app->radio_device); /* Put it into idle state in case it is sleeping. */
+    uint32_t value = subghz_devices_set_frequency(app->radio_device, app->frequency);
+    FURI_LOG_E(TAG, "Switched to frequency: %lu", value);
+
+    subghz_devices_flush_rx(app->radio_device);
+    subghz_devices_set_rx(app->radio_device);
+
+    if(!app->txrx->debug_timer_sampling) {
+        subghz_devices_start_async_rx(app->radio_device, protoview_rx_callback, NULL);
+    } else {
+        furi_hal_gpio_init(
+            subghz_devices_get_data_gpio(app->radio_device),
+            GpioModeInput,
+            GpioPullNo,
+            GpioSpeedLow);
+        raw_sampling_worker_start(app);
+    }
+    app->txrx->txrx_state = TxRxStateRx;
+    return value;
+}
+
+/* Stop receiving (if active) and put the radio on idle state. */
+void radio_rx_end(ProtoViewApp* app) {
+    furi_assert(app);
+
+    if(app->txrx->txrx_state == TxRxStateRx) {
+        if(!app->txrx->debug_timer_sampling) {
+            subghz_devices_stop_async_rx(app->radio_device);
+        } else {
+            raw_sampling_worker_stop(app);
+        }
+    }
+    subghz_devices_idle(app->radio_device);
+    app->txrx->txrx_state = TxRxStateIDLE;
+}
+
+/* Put radio on sleep. */
+void radio_sleep(ProtoViewApp* app) {
+    furi_assert(app);
+    if(app->txrx->txrx_state == TxRxStateRx) {
+        /* Stop the asynchronous receiving system before putting the
+         * chip into sleep. */
+        radio_rx_end(app);
+    }
+    subghz_devices_sleep(app->radio_device);
+    app->txrx->txrx_state = TxRxStateSleep;
+}
+
+/* =============================== Transmission ============================= */
+
+/* This function suspends the current RX state, switches to TX mode,
+ * transmits the signal provided by the callback data_feeder, and later
+ * restores the RX state if there was one. */
+void radio_tx_signal(ProtoViewApp* app, FuriHalSubGhzAsyncTxCallback data_feeder, void* ctx) {
+    TxRxState oldstate = app->txrx->txrx_state;
+
+    if(oldstate == TxRxStateRx) radio_rx_end(app);
+    radio_begin(app);
+
+    subghz_devices_idle(app->radio_device);
+    uint32_t value = subghz_devices_set_frequency(app->radio_device, app->frequency);
+    FURI_LOG_E(TAG, "Switched to frequency: %lu", value);
+
+    subghz_devices_start_async_tx(app->radio_device, data_feeder, ctx);
+    while(!subghz_devices_is_async_complete_tx(app->radio_device)) furi_delay_ms(10);
+    subghz_devices_stop_async_tx(app->radio_device);
+    subghz_devices_idle(app->radio_device);
+
+    radio_begin(app);
+    if(oldstate == TxRxStateRx) radio_rx(app);
+}
+
+/* ============================= Raw sampling mode =============================
+ * This is a special mode that uses a high frequency timer to sample the
+ * CC1101 pin directly. It's useful for debugging purposes when we want
+ * to get the raw data from the chip and completely bypass the subghz
+ * Flipper system.
+ * ===========================================================================*/
+
+void protoview_timer_isr(void* ctx) {
+    ProtoViewApp* app = ctx;
+
+    bool level = furi_hal_gpio_read(subghz_devices_get_data_gpio(app->radio_device));
+    if(app->txrx->last_g0_value != level) {
+        uint32_t now = DWT->CYCCNT;
+        uint32_t dur = now - app->txrx->last_g0_change_time;
+        dur /= furi_hal_cortex_instructions_per_microsecond();
+        if(dur > 15000) dur = 15000;
+        raw_samples_add(RawSamples, app->txrx->last_g0_value, dur);
+        app->txrx->last_g0_value = level;
+        app->txrx->last_g0_change_time = now;
+    }
+    LL_TIM_ClearFlag_UPDATE(TIM2);
+}
+
+void raw_sampling_worker_start(ProtoViewApp* app) {
+    UNUSED(app);
+
+    furi_hal_bus_enable(FuriHalBusTIM2);
+
+    LL_TIM_InitTypeDef tim_init = {
+        .Prescaler = 63, /* CPU frequency is ~64Mhz. */
+        .CounterMode = LL_TIM_COUNTERMODE_UP,
+        .Autoreload = 5, /* Sample every 5 us */
+    };
+
+    LL_TIM_Init(TIM2, &tim_init);
+    LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
+    LL_TIM_DisableCounter(TIM2);
+    LL_TIM_SetCounter(TIM2, 0);
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, protoview_timer_isr, app);
+    LL_TIM_EnableIT_UPDATE(TIM2);
+    LL_TIM_EnableCounter(TIM2);
+    FURI_LOG_E(TAG, "Timer enabled");
+}
+
+void raw_sampling_worker_stop(ProtoViewApp* app) {
+    UNUSED(app);
+    FURI_CRITICAL_ENTER();
+    LL_TIM_DisableCounter(TIM2);
+    LL_TIM_DisableIT_UPDATE(TIM2);
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL);
+    furi_hal_bus_disable(FuriHalBusTIM2);
+    FURI_CRITICAL_EXIT();
+}

BIN
main_apps_sources/protoview/appicon.png


+ 14 - 0
main_apps_sources/protoview/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="protoview",
+    name="ProtoView",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="protoview_app_entry",
+    requires=["gui"],
+    stack_size=8*1024,
+    order=50,
+    fap_icon="appicon.png",
+    fap_category="Sub-GHz",
+    fap_author="@antirez & (fixes by @xMasterX)",
+    fap_version="1.0",
+    fap_description="Digital signal detection, visualization, editing and reply tool",
+)

+ 36 - 0
main_apps_sources/protoview/crc.c

@@ -0,0 +1,36 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include <stdint.h>
+#include <stddef.h>
+
+/* CRC8 with the specified initialization value 'init' and
+ * polynomial 'poly'. */
+uint8_t crc8(const uint8_t* data, size_t len, uint8_t init, uint8_t poly) {
+    uint8_t crc = init;
+    size_t i, j;
+    for(i = 0; i < len; i++) {
+        crc ^= data[i];
+        for(j = 0; j < 8; j++) {
+            if((crc & 0x80) != 0)
+                crc = (uint8_t)((crc << 1) ^ poly);
+            else
+                crc <<= 1;
+        }
+    }
+    return crc;
+}
+
+/* Sum all the specified bytes modulo 256.
+ * Initialize the sum with 'init' (usually 0). */
+uint8_t sum_bytes(const uint8_t* data, size_t len, uint8_t init) {
+    for(size_t i = 0; i < len; i++) init += data[i];
+    return init;
+}
+
+/* Perform the bitwise xor of all the specified bytes.
+ * Initialize xor value with 'init' (usually 0). */
+uint8_t xor_bytes(const uint8_t* data, size_t len, uint8_t init) {
+    for(size_t i = 0; i < len; i++) init ^= data[i];
+    return init;
+}

+ 317 - 0
main_apps_sources/protoview/custom_presets.h

@@ -0,0 +1,317 @@
+#include <cc1101_regs.h>
+/* ========================== DATA RATE SETTINGS ===============================
+ *
+ * This is how to configure registers MDMCFG3 and MDMCFG4.
+ *
+ * MDMCFG3 is the data rate mantissa, the exponent is in MDMCFG4,
+ * last 4 bits of the register.
+ *
+ * The rate (assuming 26Mhz crystal) is calculated as follows:
+ *
+ * ((256+MDMCFG3)*(2^MDMCFG4:0..3bits)) / 2^28 * 26000000.
+ *
+ * For instance for the default values of MDMCFG3[0..3] (34) and MDMCFG4 (12):
+ *
+ * ((256+34)*(2^12))/(2^28)*26000000 = 115051.2688000000, that is 115KBaud
+ *
+ * ============================ BANDWIDTH FILTER ===============================
+ *
+ * Bandwidth filter setting:
+ *
+ * BW filter as just 16 possibilities depending on how the first nibble
+ * (first 4 bits) of the MDMCFG4 bits are set. Instead of providing the
+ * formula, it is simpler to show all the values of the nibble and the
+ * corresponding bandwidth filter.
+ *
+ * 0 812khz
+ * 1 650khz
+ * 2 541khz
+ * 3 464khz
+ * 4 406khz
+ * 5 325khz
+ * 6 270khz
+ * 7 232khz
+ * 8 203khz
+ * 9 162khz
+ * a 135khz
+ * b 116khz
+ * c 102khz
+ * d 82 khz
+ * e 68 khz
+ * f 58 khz
+ *
+ * ============================== FSK DEVIATION ================================
+ *
+ * FSK deviation is controlled by the DEVIATION register. In Ruby:
+ *
+ * dev = (26000000.0/2**17)*(8+(deviation&7))*(2**(deviation>>4&7))
+ *
+ * deviation&7 (last three bits) is the deviation mantissa, while
+ * deviation>>4&7 (bits 6,5,4) are the exponent.
+ *
+ * Deviations values according to certain configuration of DEVIATION:
+ *
+ * 0x04 ->   2.380371 kHz
+ * 0x24 ->   9.521484 kHz
+ * 0x34 ->  19.042969 Khz
+ * 0x40 ->  25.390625 Khz
+ * 0x43 ->  34.912109 Khz
+ * 0x45 ->  41.259765 Khz
+ * 0x47 ->  47.607422 kHz
+ */
+
+/* 20 KBaud, 2FSK, 28.56 kHz deviation, 325 Khz bandwidth filter. */
+static uint8_t protoview_subghz_tpms1_fsk_async_regs[][2] = {
+    /* GPIO GD0 */
+    {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
+
+    /* Frequency Synthesizer Control */
+    {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
+
+    /* Packet engine */
+    {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
+    {CC1101_PKTCTRL1, 0x04},
+
+    // // Modem Configuration
+    {CC1101_MDMCFG0, 0x00},
+    {CC1101_MDMCFG1, 0x02},
+    {CC1101_MDMCFG2,
+     0x04}, // Format 2-FSK/FM, No preamble/sync, Disable (current optimized). Other code reading TPMS uses GFSK, but should be the same when in RX mode.
+    {CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud
+    {CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz
+    {CC1101_DEVIATN, 0x41}, // Deviation 28.56 kHz
+
+    /* Main Radio Control State Machine */
+    {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
+
+    /* Frequency Offset Compensation Configuration */
+    {CC1101_FOCCFG,
+     0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+    /* Automatic Gain Control */
+    {CC1101_AGCCTRL0,
+     0x91}, //10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
+    {CC1101_AGCCTRL1,
+     0x00}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
+    {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB
+
+    /* Wake on radio and timeouts control */
+    {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
+
+    /* Frontend configuration */
+    {CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer
+    {CC1101_FREND1, 0x56},
+
+    /* End  */
+    {0, 0},
+
+    /* CC1101 2FSK PATABLE. */
+    {0xC0, 0},
+    {0, 0},
+    {0, 0},
+    {0, 0}};
+
+/* This is like the default Flipper OOK 640Khz bandwidth preset, but
+ * the bandwidth is changed to 10kBaud to accomodate TPMS frequency. */
+static const uint8_t protoview_subghz_tpms2_ook_async_regs[][2] = {
+    /* GPIO GD0 */
+    {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
+
+    /* FIFO and internals */
+    {CC1101_FIFOTHR, 0x07}, // The only important bit is ADC_RETENTION
+
+    /* Packet engine */
+    {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
+
+    /* Frequency Synthesizer Control */
+    {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
+
+    // Modem Configuration
+    {CC1101_MDMCFG0, 0x00}, // Channel spacing is 25kHz
+    {CC1101_MDMCFG1, 0x00}, // Channel spacing is 25kHz
+    {CC1101_MDMCFG2, 0x30}, // Format ASK/OOK, No preamble/sync
+    {CC1101_MDMCFG3, 0x93}, // Data rate is 10kBaud
+    {CC1101_MDMCFG4, 0x18}, // Rx BW filter is 650.000kHz
+
+    /* Main Radio Control State Machine */
+    {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
+
+    /* Frequency Offset Compensation Configuration */
+    {CC1101_FOCCFG,
+     0x18}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+    /* Automatic Gain Control */
+    {CC1101_AGCCTRL0,
+     0x91}, // 10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
+    {CC1101_AGCCTRL1,
+     0x0}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
+    {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB
+
+    /* Wake on radio and timeouts control */
+    {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
+
+    /* Frontend configuration */
+    {CC1101_FREND0, 0x11}, // Adjusts current TX LO buffer + high is PATABLE[1]
+    {CC1101_FREND1, 0xB6}, //
+
+    /* End  */
+    {0, 0},
+
+    /* CC1101 OOK PATABLE. */
+    {0, 0xC0},
+    {0, 0},
+    {0, 0},
+    {0, 0}};
+
+/* GFSK 19k dev, 325 Khz filter, 20kBaud. Different AGI settings.
+ * Works well with Toyota. */
+static uint8_t protoview_subghz_tpms3_gfsk_async_regs[][2] = {
+    /* GPIO GD0 */
+    {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
+
+    /* Frequency Synthesizer Control */
+    {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
+
+    /* Packet engine */
+    {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
+    {CC1101_PKTCTRL1, 0x04},
+
+    // // Modem Configuration
+    {CC1101_MDMCFG0, 0x00},
+    {CC1101_MDMCFG1, 0x02}, // 2 is the channel spacing exponet: not used
+    {CC1101_MDMCFG2, 0x10}, // GFSK without any other check
+    {CC1101_MDMCFG3, 0x93}, // Data rate is 20kBaud
+    {CC1101_MDMCFG4, 0x59}, // Rx bandwidth filter is 325 kHz
+    {CC1101_DEVIATN, 0x34}, // Deviation 19.04 Khz.
+
+    /* Main Radio Control State Machine */
+    {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
+
+    /* Frequency Offset Compensation Configuration */
+    {CC1101_FOCCFG,
+     0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+    /* Automatic Gain Control */
+    {CC1101_AGCCTRL0, 0x80},
+    {CC1101_AGCCTRL1, 0x58},
+    {CC1101_AGCCTRL2, 0x87},
+
+    /* Wake on radio and timeouts control */
+    {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
+
+    /* Frontend configuration */
+    {CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer
+    {CC1101_FREND1, 0x56},
+
+    /* End  */
+    {0, 0},
+
+    /* CC1101 2FSK PATABLE. */
+    {0xC0, 0},
+    {0, 0},
+    {0, 0},
+    {0, 0}};
+
+/* 40 KBaud, 2FSK, 28 kHz deviation, 270 Khz bandwidth filter. */
+static uint8_t protoview_subghz_40k_fsk_async_regs[][2] = {
+    /* GPIO GD0 */
+    {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
+
+    /* Frequency Synthesizer Control */
+    {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
+
+    /* Packet engine */
+    {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
+    {CC1101_PKTCTRL1, 0x04},
+
+    // // Modem Configuration
+    {CC1101_MDMCFG0, 0x00},
+    {CC1101_MDMCFG1, 0x02},
+    {CC1101_MDMCFG2,
+     0x04}, // Format 2-FSK/FM, No preamble/sync, Disable (current optimized). Other code reading TPMS uses GFSK, but should be the same when in RX mode.
+    {CC1101_MDMCFG3, 0x93}, // Data rate is 40kBaud
+    {CC1101_MDMCFG4, 0x6A}, // 6 = BW filter 270kHz, A = Data rate exp
+    {CC1101_DEVIATN, 0x41}, // Deviation 28kHz
+
+    /* Main Radio Control State Machine */
+    {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
+
+    /* Frequency Offset Compensation Configuration */
+    {CC1101_FOCCFG,
+     0x16}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+    /* Automatic Gain Control */
+    {CC1101_AGCCTRL0,
+     0x91}, //10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
+    {CC1101_AGCCTRL1,
+     0x00}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
+    {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB
+
+    /* Wake on radio and timeouts control */
+    {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
+
+    /* Frontend configuration */
+    {CC1101_FREND0, 0x10}, // Adjusts current TX LO buffer
+    {CC1101_FREND1, 0x56},
+
+    /* End  */
+    {0, 0},
+
+    /* CC1101 2FSK PATABLE. */
+    {0xC0, 0},
+    {0, 0},
+    {0, 0},
+    {0, 0}};
+
+/* This is like the default Flipper OOK 640Khz bandwidth preset, but
+ * the bandwidth is changed to 40kBaud, in order to receive signals
+ * with a pulse width ~25us/30us. */
+static const uint8_t protoview_subghz_40k_ook_async_regs[][2] = {
+    /* GPIO GD0 */
+    {CC1101_IOCFG0, 0x0D}, // GD0 as async serial data output/input
+
+    /* FIFO and internals */
+    {CC1101_FIFOTHR, 0x07}, // The only important bit is ADC_RETENTION
+
+    /* Packet engine */
+    {CC1101_PKTCTRL0, 0x32}, // Async, continious, no whitening
+
+    /* Frequency Synthesizer Control */
+    {CC1101_FSCTRL1, 0x06}, // IF = (26*10^6) / (2^10) * 0x06 = 152343.75Hz
+
+    // Modem Configuration
+    {CC1101_MDMCFG0, 0x00}, // Channel spacing is 25kHz
+    {CC1101_MDMCFG1, 0x00}, // Channel spacing is 25kHz
+    {CC1101_MDMCFG2, 0x30}, // Format ASK/OOK, No preamble/sync
+    {CC1101_MDMCFG3, 0x93}, // Data rate is 40kBaud
+    {CC1101_MDMCFG4, 0x1A}, // Rx BW filter is 650.000kHz
+
+    /* Main Radio Control State Machine */
+    {CC1101_MCSM0, 0x18}, // Autocalibrate on idle-to-rx/tx, PO_TIMEOUT is 64 cycles(149-155us)
+
+    /* Frequency Offset Compensation Configuration */
+    {CC1101_FOCCFG,
+     0x18}, // no frequency offset compensation, POST_K same as PRE_K, PRE_K is 4K, GATE is off
+
+    /* Automatic Gain Control */
+    {CC1101_AGCCTRL0,
+     0x91}, // 10 - Medium hysteresis, medium asymmetric dead zone, medium gain ; 01 - 16 samples agc; 00 - Normal AGC, 01 - 8dB boundary
+    {CC1101_AGCCTRL1,
+     0x0}, // 0; 0 - LNA 2 gain is decreased to minimum before decreasing LNA gain; 00 - Relative carrier sense threshold disabled; 0000 - RSSI to MAIN_TARGET
+    {CC1101_AGCCTRL2, 0x07}, // 00 - DVGA all; 000 - MAX LNA+LNA2; 111 - MAIN_TARGET 42 dB
+
+    /* Wake on radio and timeouts control */
+    {CC1101_WORCTRL, 0xFB}, // WOR_RES is 2^15 periods (0.91 - 0.94 s) 16.5 - 17.2 hours
+
+    /* Frontend configuration */
+    {CC1101_FREND0, 0x11}, // Adjusts current TX LO buffer + high is PATABLE[1]
+    {CC1101_FREND1, 0xB6}, //
+
+    /* End  */
+    {0, 0},
+
+    /* CC1101 OOK PATABLE. */
+    {0, 0xC0},
+    {0, 0},
+    {0, 0},
+    {0, 0}};

+ 369 - 0
main_apps_sources/protoview/fields.c

@@ -0,0 +1,369 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Protocol fields implementation. */
+
+#include "app.h"
+
+/* Create a new field of the specified type. Without populating its
+ * type-specific value. */
+static ProtoViewField* field_new(ProtoViewFieldType type, const char* name) {
+    ProtoViewField* f = malloc(sizeof(*f));
+    f->type = type;
+    f->name = strdup(name);
+    return f;
+}
+
+/* Free only the auxiliary data of a field, used to represent the
+ * current type. Name and type are not touched. */
+static void field_free_aux_data(ProtoViewField* f) {
+    switch(f->type) {
+    case FieldTypeStr:
+        free(f->str);
+        break;
+    case FieldTypeBytes:
+        free(f->bytes);
+        break;
+    default:
+        break; // Nothing to free for other types.
+    }
+}
+
+/* Free a field an associated data. */
+static void field_free(ProtoViewField* f) {
+    field_free_aux_data(f);
+    free(f->name);
+    free(f);
+}
+
+/* Return the type of the field as string. */
+const char* field_get_type_name(ProtoViewField* f) {
+    switch(f->type) {
+    case FieldTypeStr:
+        return "str";
+    case FieldTypeSignedInt:
+        return "int";
+    case FieldTypeUnsignedInt:
+        return "uint";
+    case FieldTypeBinary:
+        return "bin";
+    case FieldTypeHex:
+        return "hex";
+    case FieldTypeBytes:
+        return "bytes";
+    case FieldTypeFloat:
+        return "float";
+    }
+    return "unknown";
+}
+
+/* Set a string representation of the specified field in buf. */
+int field_to_string(char* buf, size_t len, ProtoViewField* f) {
+    switch(f->type) {
+    case FieldTypeStr:
+        return snprintf(buf, len, "%s", f->str);
+    case FieldTypeSignedInt:
+        return snprintf(buf, len, "%lld", (long long)f->value);
+    case FieldTypeUnsignedInt:
+        return snprintf(buf, len, "%llu", (unsigned long long)f->uvalue);
+    case FieldTypeBinary: {
+        uint64_t test_bit = (1 << (f->len - 1));
+        uint64_t idx = 0;
+        while(idx < len - 1 && test_bit) {
+            buf[idx++] = (f->uvalue & test_bit) ? '1' : '0';
+            test_bit >>= 1;
+        }
+        buf[idx] = 0;
+        return idx;
+    }
+    case FieldTypeHex:
+        return snprintf(buf, len, "%*llX", (int)(f->len + 7) / 8, f->uvalue);
+    case FieldTypeFloat:
+        return snprintf(buf, len, "%.*f", (int)f->len, (double)f->fvalue);
+    case FieldTypeBytes: {
+        uint64_t idx = 0;
+        while(idx < len - 1 && idx < f->len) {
+            const char* charset = "0123456789ABCDEF";
+            uint32_t nibble = idx & 1 ? (f->bytes[idx / 2] & 0xf) : (f->bytes[idx / 2] >> 4);
+            buf[idx++] = charset[nibble];
+        }
+        buf[idx] = 0;
+        return idx;
+    }
+    }
+    return 0;
+}
+
+/* Set the field value from its string representation in 'buf'.
+ * The field type must already be set and the field should be valid.
+ * The string represenation 'buf' must be null termianted. Note that
+ * even when representing binary values containing zero, this values
+ * are taken as representations, so that would be the string "00" as
+ * the Bytes type representation.
+ *
+ * The function returns true if the filed was successfully set to the
+ * new value, otherwise if the specified value is invalid for the
+ * field type, false is returned. */
+bool field_set_from_string(ProtoViewField* f, char* buf, size_t len) {
+    // Initialize values to zero since the Flipper sscanf() implementation
+    // is fuzzy... may populate only part of the value.
+    long long val = 0;
+    unsigned long long uval = 0;
+    float fval = 0;
+
+    switch(f->type) {
+    case FieldTypeStr:
+        free(f->str);
+        f->len = len;
+        f->str = malloc(len + 1);
+        memcpy(f->str, buf, len + 1);
+        break;
+    case FieldTypeSignedInt:
+        if(!sscanf(buf, "%lld", &val)) return false;
+        f->value = val;
+        break;
+    case FieldTypeUnsignedInt:
+        if(!sscanf(buf, "%llu", &uval)) return false;
+        f->uvalue = uval;
+        break;
+    case FieldTypeBinary: {
+        uint64_t bit_to_set = (1 << (len - 1));
+        uint64_t idx = 0;
+        uval = 0;
+        while(buf[idx]) {
+            if(buf[idx] == '1')
+                uval |= bit_to_set;
+            else if(buf[idx] != '0')
+                return false;
+            bit_to_set >>= 1;
+            idx++;
+        }
+        f->uvalue = uval;
+    } break;
+    case FieldTypeHex:
+        if(!sscanf(buf, "%llx", &uval) && !sscanf(buf, "%llX", &uval)) return false;
+        f->uvalue = uval;
+        break;
+    case FieldTypeFloat:
+        if(!sscanf(buf, "%f", &fval)) return false;
+        f->fvalue = fval;
+        break;
+    case FieldTypeBytes: {
+        if(len > f->len) return false;
+        uint64_t idx = 0;
+        while(buf[idx]) {
+            uint8_t nibble = 0;
+            char c = toupper(buf[idx]);
+            if(c >= '0' && c <= '9')
+                nibble = c - '0';
+            else if(c >= 'A' && c <= 'F')
+                nibble = 10 + (c - 'A');
+            else
+                return false;
+
+            if(idx & 1) {
+                f->bytes[idx / 2] = (f->bytes[idx / 2] & 0xF0) | nibble;
+            } else {
+                f->bytes[idx / 2] = (f->bytes[idx / 2] & 0x0F) | (nibble << 4);
+            }
+            idx++;
+        }
+        buf[idx] = 0;
+    } break;
+    }
+    return true;
+}
+
+/* Set the 'dst' field to contain a copy of the value of the 'src'
+ * field. The field name is not modified. */
+void field_set_from_field(ProtoViewField* dst, ProtoViewField* src) {
+    field_free_aux_data(dst);
+    dst->type = src->type;
+    dst->len = src->len;
+    switch(src->type) {
+    case FieldTypeStr:
+        dst->str = strdup(src->str);
+        break;
+    case FieldTypeBytes:
+        dst->bytes = malloc(src->len);
+        memcpy(dst->bytes, src->bytes, dst->len);
+        break;
+    case FieldTypeSignedInt:
+        dst->value = src->value;
+        break;
+    case FieldTypeUnsignedInt:
+    case FieldTypeBinary:
+    case FieldTypeHex:
+        dst->uvalue = src->uvalue;
+        break;
+    case FieldTypeFloat:
+        dst->fvalue = src->fvalue;
+        break;
+    }
+}
+
+/* Increment the specified field value of 'incr'. If the field type
+ * does not support increments false is returned, otherwise the
+ * action is performed. */
+bool field_incr_value(ProtoViewField* f, int incr) {
+    switch(f->type) {
+    case FieldTypeStr:
+        return false;
+    case FieldTypeSignedInt: {
+        /* Wrap around depending on the number of bits (f->len)
+             * the integer was declared to have. */
+        int64_t max = (1ULL << (f->len - 1)) - 1;
+        int64_t min = -max - 1;
+        int64_t v = (int64_t)f->value + incr;
+        if(v > max) v = min + (v - max - 1);
+        if(v < min) v = max + (v - min + 1);
+        f->value = v;
+        break;
+    }
+    case FieldTypeBinary:
+    case FieldTypeHex:
+    case FieldTypeUnsignedInt: {
+        /* Wrap around like for the unsigned case, but here
+             * is simpler. */
+        uint64_t max = (1ULL << f->len) - 1; // Broken for 64 bits.
+        uint64_t uv = (uint64_t)f->value + incr;
+        if(uv > max) uv = uv & max;
+        f->uvalue = uv;
+        break;
+    }
+    case FieldTypeFloat:
+        f->fvalue += incr;
+        break;
+    case FieldTypeBytes: {
+        // For bytes we only support single unit increments.
+        if(incr != -1 && incr != 1) return false;
+        for(int j = f->len - 1; j >= 0; j--) {
+            uint8_t nibble = (j & 1) ? (f->bytes[j / 2] & 0x0F) : ((f->bytes[j / 2] & 0xF0) >> 4);
+
+            nibble += incr;
+            nibble &= 0x0F;
+
+            f->bytes[j / 2] = (j & 1) ? ((f->bytes[j / 2] & 0xF0) | nibble) :
+                                        ((f->bytes[j / 2] & 0x0F) | (nibble << 4));
+
+            /* Propagate the operation on overflow of this nibble. */
+            if((incr == 1 && nibble == 0) || (incr == -1 && nibble == 0xf)) {
+                continue;
+            }
+            break; // Otherwise stop the loop here.
+        }
+        break;
+    }
+    }
+    return true;
+}
+
+/* Free a field set and its contained fields. */
+void fieldset_free(ProtoViewFieldSet* fs) {
+    for(uint32_t j = 0; j < fs->numfields; j++) field_free(fs->fields[j]);
+    free(fs->fields);
+    free(fs);
+}
+
+/* Allocate and init an empty field set. */
+ProtoViewFieldSet* fieldset_new(void) {
+    ProtoViewFieldSet* fs = malloc(sizeof(*fs));
+    fs->numfields = 0;
+    fs->fields = NULL;
+    return fs;
+}
+
+/* Append an already allocated field at the end of the specified field set. */
+static void fieldset_add_field(ProtoViewFieldSet* fs, ProtoViewField* field) {
+    fs->numfields++;
+    fs->fields = realloc(fs->fields, sizeof(ProtoViewField*) * fs->numfields);
+    fs->fields[fs->numfields - 1] = field;
+}
+
+/* Allocate and append an integer field. */
+void fieldset_add_int(ProtoViewFieldSet* fs, const char* name, int64_t val, uint8_t bits) {
+    ProtoViewField* f = field_new(FieldTypeSignedInt, name);
+    f->value = val;
+    f->len = bits;
+    fieldset_add_field(fs, f);
+}
+
+/* Allocate and append an unsigned field. */
+void fieldset_add_uint(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits) {
+    ProtoViewField* f = field_new(FieldTypeUnsignedInt, name);
+    f->uvalue = uval;
+    f->len = bits;
+    fieldset_add_field(fs, f);
+}
+
+/* Allocate and append a hex field. This is an unsigned number but
+ * with an hex representation. */
+void fieldset_add_hex(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits) {
+    ProtoViewField* f = field_new(FieldTypeHex, name);
+    f->uvalue = uval;
+    f->len = bits;
+    fieldset_add_field(fs, f);
+}
+
+/* Allocate and append a bin field. This is an unsigned number but
+ * with a binary representation. */
+void fieldset_add_bin(ProtoViewFieldSet* fs, const char* name, uint64_t uval, uint8_t bits) {
+    ProtoViewField* f = field_new(FieldTypeBinary, name);
+    f->uvalue = uval;
+    f->len = bits;
+    fieldset_add_field(fs, f);
+}
+
+/* Allocate and append a string field. The string 's' does not need to point
+ * to a null terminated string, but must have at least 'len' valid bytes
+ * starting from the pointer. The field object will be correctly null
+ * terminated. */
+void fieldset_add_str(ProtoViewFieldSet* fs, const char* name, const char* s, size_t len) {
+    ProtoViewField* f = field_new(FieldTypeStr, name);
+    f->len = len;
+    f->str = malloc(len + 1);
+    memcpy(f->str, s, len);
+    f->str[len] = 0;
+    fieldset_add_field(fs, f);
+}
+
+/* Allocate and append a bytes field. Note that 'count' is specified in
+ * nibbles (bytes*2). */
+void fieldset_add_bytes(
+    ProtoViewFieldSet* fs,
+    const char* name,
+    const uint8_t* bytes,
+    uint32_t count_nibbles) {
+    uint32_t numbytes = (count_nibbles + count_nibbles % 2) / 2;
+    ProtoViewField* f = field_new(FieldTypeBytes, name);
+    f->bytes = malloc(numbytes);
+    memcpy(f->bytes, bytes, numbytes);
+    f->len = count_nibbles;
+    fieldset_add_field(fs, f);
+}
+
+/* Allocate and append a float field. */
+void fieldset_add_float(
+    ProtoViewFieldSet* fs,
+    const char* name,
+    float val,
+    uint32_t digits_after_dot) {
+    ProtoViewField* f = field_new(FieldTypeFloat, name);
+    f->fvalue = val;
+    f->len = digits_after_dot;
+    fieldset_add_field(fs, f);
+}
+
+/* For each field of the destination filedset 'dst', look for a matching
+ * field name/type in the source fieldset 'src', and if one is found copy
+ * its value into the 'dst' field. */
+void fieldset_copy_matching_fields(ProtoViewFieldSet* dst, ProtoViewFieldSet* src) {
+    for(uint32_t j = 0; j < dst->numfields; j++) {
+        for(uint32_t i = 0; i < src->numfields; i++) {
+            if(dst->fields[j]->type == src->fields[i]->type &&
+               !strcmp(dst->fields[j]->name, src->fields[i]->name)) {
+                field_set_from_field(dst->fields[j], src->fields[i]);
+            }
+        }
+    }
+}

+ 64 - 0
main_apps_sources/protoview/helpers/radio_device_loader.c

@@ -0,0 +1,64 @@
+#include "radio_device_loader.h"
+
+#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+
+static void radio_device_loader_power_on() {
+    uint8_t attempts = 0;
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        //CC1101 power-up time
+        furi_delay_ms(10);
+    }
+}
+
+static void radio_device_loader_power_off() {
+    if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+}
+
+bool radio_device_loader_is_connect_external(const char* name) {
+    bool is_connect = false;
+    bool is_otg_enabled = furi_hal_power_is_otg_enabled();
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_on();
+    }
+
+    const SubGhzDevice* device = subghz_devices_get_by_name(name);
+    if(device) {
+        is_connect = subghz_devices_is_connect(device);
+    }
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_off();
+    }
+    return is_connect;
+}
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type) {
+    const SubGhzDevice* radio_device;
+
+    if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
+       radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
+        radio_device_loader_power_on();
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
+        subghz_devices_begin(radio_device);
+    } else if(current_radio_device == NULL) {
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    } else {
+        radio_device_loader_end(current_radio_device);
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    }
+
+    return radio_device;
+}
+
+void radio_device_loader_end(const SubGhzDevice* radio_device) {
+    furi_assert(radio_device);
+    radio_device_loader_power_off();
+    if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
+        subghz_devices_end(radio_device);
+    }
+}

+ 15 - 0
main_apps_sources/protoview/helpers/radio_device_loader.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <lib/subghz/devices/devices.h>
+
+/** SubGhzRadioDeviceType */
+typedef enum {
+    SubGhzRadioDeviceTypeInternal,
+    SubGhzRadioDeviceTypeExternalCC1101,
+} SubGhzRadioDeviceType;
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type);
+
+void radio_device_loader_end(const SubGhzDevice* radio_device);

BIN
main_apps_sources/protoview/images/ProtoViewSignal.jpg


BIN
main_apps_sources/protoview/images/protoview_1.jpg


BIN
main_apps_sources/protoview/images/protoview_2.jpg


BIN
main_apps_sources/protoview/img/1.png


BIN
main_apps_sources/protoview/img/2.png


BIN
main_apps_sources/protoview/img/3.png


+ 93 - 0
main_apps_sources/protoview/protocols/b4b1.c

@@ -0,0 +1,93 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * PT/SC remotes. Usually 443.92 Mhz OOK.
+ *
+ * This line code is used in many remotes such as Princeton chips
+ * named PT2262, Silian Microelectronics SC5262 and others.
+ * Basically every 4 pulsee represent a bit, where 1000 means 0, and
+ * 1110 means 1. Usually we can read 24 bits of data.
+ * In this specific implementation we check for a prelude that is
+ * 1 bit high, 31 bits low, but the check is relaxed. */
+
+#include "../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    if(numbits < 30) return false;
+
+    /* Test different pulse + gap + first byte possibilities. */
+    const char* sync_patterns[6] = {
+        "100000000000000000000000000000011101", /* 30 times gap + one. */
+        "100000000000000000000000000000010001", /* 30 times gap + zero. */
+        "1000000000000000000000000000000011101", /* 31 times gap + one. */
+        "1000000000000000000000000000000010001", /* 31 times gap + zero. */
+        "10000000000000000000000000000000011101", /* 32 times gap + one. */
+        "10000000000000000000000000000000010001", /* 32 times gap + zero. */
+    };
+
+    uint32_t off;
+    int j;
+    for(j = 0; j < 3; j++) {
+        off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_patterns[j]);
+        if(off != BITMAP_SEEK_NOT_FOUND) break;
+    }
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    if(DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 preamble id:%d at: %lu", j, off);
+    info->start_off = off;
+
+    // Seek data setction. Why -5? Last 5 half-bit-times are data.
+    off += strlen(sync_patterns[j]) - 5;
+
+    uint8_t d[3]; /* 24 bits of data. */
+    uint32_t decoded = convert_from_line_code(d, sizeof(d), bits, numbytes, off, "1000", "1110");
+
+    if(DEBUG_MSG) FURI_LOG_E(TAG, "B4B1 decoded: %lu", decoded);
+    if(decoded < 24) return false;
+
+    off += 24 * 4; // seek to end symbol offset to calculate the length.
+    off++; // In this protocol there is a final pulse as terminator.
+    info->pulses_count = off - info->start_off;
+
+    fieldset_add_bytes(info->fieldset, "id", d, 5);
+    fieldset_add_uint(info->fieldset, "button", d[2] & 0xf, 4);
+    return true;
+}
+
+/* Give fields and defaults for the signal creator. */
+static void get_fields(ProtoViewFieldSet* fieldset) {
+    uint8_t default_id[3] = {0xAB, 0xCD, 0xE0};
+    fieldset_add_bytes(fieldset, "id", default_id, 5);
+    fieldset_add_uint(fieldset, "button", 1, 4);
+}
+
+/* Create a signal. */
+static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fs) {
+    uint32_t te = 334; // Short pulse duration in microseconds.
+
+    // Sync: 1 te pulse, 31 te gap.
+    raw_samples_add(samples, true, te);
+    raw_samples_add(samples, false, te * 31);
+
+    // ID + button state
+    uint8_t data[3];
+    memcpy(data, fs->fields[0]->bytes, 3);
+    data[2] = (data[2] & 0xF0) | (fs->fields[1]->uvalue & 0xF);
+    for(uint32_t j = 0; j < 24; j++) {
+        if(bitmap_get(data, sizeof(data), j)) {
+            raw_samples_add(samples, true, te * 3);
+            raw_samples_add(samples, false, te);
+        } else {
+            raw_samples_add(samples, true, te);
+            raw_samples_add(samples, false, te * 3);
+        }
+    }
+
+    // Signal terminator. Just a single short pulse.
+    raw_samples_add(samples, true, te);
+}
+
+ProtoViewDecoder B4B1Decoder = {
+    .name = "PT/SC remote",
+    .decode = decode,
+    .get_fields = get_fields,
+    .build_message = build_message};

+ 121 - 0
main_apps_sources/protoview/protocols/keeloq.c

@@ -0,0 +1,121 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Microchip HCS200/HCS300/HSC301 KeeLoq, rolling code remotes.
+ *
+ * Usually 443.92 Mhz OOK, ~200us or ~400us pulse len, depending
+ * on the configuration.
+ *
+ * Preamble: 12 pairs of alternating pulse/gap.
+ * Sync: long gap of around 10 times the duration of the short-pulse.
+ * Data: pulse width encoded data. Each bit takes three cycles:
+ *
+ * 0 = 110
+ * 1 = 100
+ *
+ * There are a total of 66 bits transmitted.
+ *  0..31: 32 bits of encrypted rolling code.
+ * 32..59: Remote ID, 28 bits
+ * 60..63: Buttons pressed
+ * 64..64: Low battery if set
+ * 65..65: Always set to 1
+ *
+ * Bits in bytes are inverted: least significant bit is first.
+ * For some reason there is no checksum whatsoever, so we only decode
+ * if we find everything well formed.
+ */
+
+#include "../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    /* In the sync pattern, we require the 12 high/low pulses and at least
+     * half the gap we expect (5 pulses times, one is the final zero in the
+     * 24 symbols high/low sequence, then other 4). */
+    const char* sync_pattern = "101010101010101010101010"
+                               "0000";
+    uint8_t sync_len = 24 + 4;
+    if(numbits - sync_len + sync_len < 3 * 66) return false;
+    uint32_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+
+    info->start_off = off;
+    off += sync_len; // Seek start of message.
+
+    /* Now there is half the gap left, but we allow from 3 to 7, instead of 5
+     * symbols of gap, to avoid missing the signal for a matter of wrong
+     * timing. */
+    uint8_t gap_len = 0;
+    while(gap_len <= 7 && bitmap_get(bits, numbytes, off + gap_len) == 0) gap_len++;
+    if(gap_len < 3 || gap_len > 7) return false;
+
+    off += gap_len;
+    FURI_LOG_E(TAG, "Keeloq preamble+sync found");
+
+    uint8_t raw[9] = {0};
+    uint32_t decoded = convert_from_line_code(
+        raw, sizeof(raw), bits, numbytes, off, "110", "100"); /* Pulse width modulation. */
+    FURI_LOG_E(TAG, "Keeloq decoded bits: %lu", decoded);
+    if(decoded < 66) return false; /* Require the full 66 bits. */
+
+    info->pulses_count = (off + 66 * 3) - info->start_off;
+
+    bitmap_reverse_bytes_bits(raw, sizeof(raw)); /* Keeloq is LSB first. */
+
+    int buttons = raw[7] >> 4;
+    int lowbat = (raw[8] & 0x1) == 0; // Actual bit meaning: good battery level
+    int alwaysone = (raw[8] & 0x2) != 0;
+
+    fieldset_add_bytes(info->fieldset, "encr", raw, 8);
+    raw[7] = raw[7] << 4; // Make ID bits contiguous
+    fieldset_add_bytes(info->fieldset, "id", raw + 4, 7); // 28 bits, 7 nibbles
+    fieldset_add_bin(info->fieldset, "s[2,1,0,3]", buttons, 4);
+    fieldset_add_bin(info->fieldset, "low battery", lowbat, 1);
+    fieldset_add_bin(info->fieldset, "always one", alwaysone, 1);
+    return true;
+}
+
+static void get_fields(ProtoViewFieldSet* fieldset) {
+    uint8_t remote_id[4] = {0xab, 0xcd, 0xef, 0xa0};
+    uint8_t encr[4] = {0xab, 0xab, 0xab, 0xab};
+    fieldset_add_bytes(fieldset, "encr", encr, 8);
+    fieldset_add_bytes(fieldset, "id", remote_id, 7);
+    fieldset_add_bin(fieldset, "s[2,1,0,3]", 2, 4);
+    fieldset_add_bin(fieldset, "low battery", 0, 1);
+    fieldset_add_bin(fieldset, "always one", 1, 1);
+}
+
+static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fieldset) {
+    uint32_t te = 380; // Short pulse duration in microseconds.
+
+    // Sync: 12 pairs of pulse/gap + 9 times gap
+    for(int j = 0; j < 12; j++) {
+        raw_samples_add(samples, true, te);
+        raw_samples_add(samples, false, te);
+    }
+    raw_samples_add(samples, false, te * 9);
+
+    // Data, 66 bits.
+    uint8_t data[9] = {0};
+    memcpy(data, fieldset->fields[0]->bytes, 4); // Encrypted part.
+    memcpy(data + 4, fieldset->fields[1]->bytes, 4); // ID.
+    data[7] = data[7] >> 4 | fieldset->fields[2]->uvalue << 4; // s[2,1,0,3]
+    int low_battery = fieldset->fields[3] != 0;
+    int always_one = fieldset->fields[4] != 0;
+    low_battery = !low_battery; // Bit real meaning is good battery level.
+    data[8] |= low_battery;
+    data[8] |= (always_one << 1);
+    bitmap_reverse_bytes_bits(data, sizeof(data)); /* Keeloq is LSB first. */
+
+    for(int j = 0; j < 66; j++) {
+        if(bitmap_get(data, 9, j)) {
+            raw_samples_add(samples, true, te);
+            raw_samples_add(samples, false, te * 2);
+        } else {
+            raw_samples_add(samples, true, te * 2);
+            raw_samples_add(samples, false, te);
+        }
+    }
+}
+
+ProtoViewDecoder KeeloqDecoder =
+    {.name = "Keeloq", .decode = decode, .get_fields = get_fields, .build_message = build_message};

+ 84 - 0
main_apps_sources/protoview/protocols/oregon2.c

@@ -0,0 +1,84 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Oregon remote termometers. Usually 443.92 Mhz OOK.
+ *
+ * The protocol is described here:
+ * https://wmrx00.sourceforge.net/Arduino/OregonScientific-RF-Protocols.pdf
+ * This implementation is not very complete. */
+
+#include "../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    if(numbits < 32) return false;
+    const char* sync_pattern = "01100110"
+                               "01100110"
+                               "10010110"
+                               "10010110";
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Oregon2 preamble+sync found");
+
+    info->start_off = off;
+    off += 32; /* Skip preamble. */
+
+    uint8_t buffer[8], raw[8] = {0};
+    uint32_t decoded =
+        convert_from_line_code(buffer, sizeof(buffer), bits, numbytes, off, "1001", "0110");
+    FURI_LOG_E(TAG, "Oregon2 decoded bits: %lu", decoded);
+
+    if(decoded < 11 * 4) return false; /* Minimum len to extract some data. */
+    info->pulses_count = (off + 11 * 4 * 4) - info->start_off;
+
+    char temp[3] = {0}, hum[2] = {0};
+    uint8_t deviceid[2];
+    for(int j = 0; j < 64; j += 4) {
+        uint8_t nib[1];
+        nib[0] =
+            (bitmap_get(buffer, 8, j + 0) | bitmap_get(buffer, 8, j + 1) << 1 |
+             bitmap_get(buffer, 8, j + 2) << 2 | bitmap_get(buffer, 8, j + 3) << 3);
+        if(DEBUG_MSG) FURI_LOG_E(TAG, "Not inverted nibble[%d]: %x", j / 4, (unsigned int)nib[0]);
+        raw[j / 8] |= nib[0] << (4 - (j % 4));
+        switch(j / 4) {
+        case 1:
+            deviceid[0] |= nib[0];
+            break;
+        case 0:
+            deviceid[0] |= nib[0] << 4;
+            break;
+        case 3:
+            deviceid[1] |= nib[0];
+            break;
+        case 2:
+            deviceid[1] |= nib[0] << 4;
+            break;
+        case 10:
+            temp[0] = nib[0];
+            break;
+        /* Fixme: take the temperature sign from nibble 11. */
+        case 9:
+            temp[1] = nib[0];
+            break;
+        case 8:
+            temp[2] = nib[0];
+            break;
+        case 13:
+            hum[0] = nib[0];
+            break;
+        case 12:
+            hum[1] = nib[0];
+            break;
+        }
+    }
+
+    float tempval = ((temp[0] - '0') * 10) + (temp[1] - '0') + ((float)(temp[2] - '0') * 0.1);
+    int humval = (hum[0] - '0') * 10 + (hum[1] - '0');
+
+    fieldset_add_bytes(info->fieldset, "Sensor ID", deviceid, 4);
+    fieldset_add_float(info->fieldset, "Temperature", tempval, 1);
+    fieldset_add_uint(info->fieldset, "Humidity", humval, 7);
+    return true;
+}
+
+ProtoViewDecoder Oregon2Decoder =
+    {.name = "Oregon2", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 205 - 0
main_apps_sources/protoview/protocols/pvchat.c

@@ -0,0 +1,205 @@
+#include "../app.h"
+
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * ----------------------------------------------------------
+ * ProtoView chat protocol. This is just a fun test protocol
+ * that can be used between two Flippers in order to send
+ * and receive text messages.
+ * ----------------------------------------------------------
+ *
+ * Protocol description
+ * ====================
+ *
+ * The protocol works with different data rates. However here is defined
+ * to use a short pulse/gap duration of 300us and a long pulse/gap
+ * duration of 600us. Even with the Flipper hardware, the protocol works
+ * with 100/200us, but becomes less reliable and standard presets can't
+ * be used because of the higher data rate.
+ *
+ * In the following description we have that:
+ *
+ * "1" represents a pulse of one-third bit time (300us)
+ * "0" represents a gap of one-third bit time (300us)
+ * 
+ * The message starts with a preamble + a sync pattern:
+ * 
+ * preamble = 1010101010101010 x 3
+ * sync     = 1100110011001010
+ * 
+ * The a variable amount of bytes follow, where each bit
+ * is encoded in the following way:
+ * 
+ * zero 100 (300 us pulse, 600 us gap)
+ * one  110 (600 us pulse, 300 us gap)
+ * 
+ * Bytes are sent MSB first, so receiving, in sequence, bits
+ * 11100001, means byte E1.
+ * 
+ * This is the data format:
+ * 
+ * +--+------+-------+--+--+--+
+ * |SL|Sender|Message|FF|AA|CS|
+ * +--+------+-------+--+--+--+
+ *  |    |      |
+ *  |    |      \_ N bytes of message terminated by FF AA + 1 byte of checksum
+ *  |    |      
+ *  |    \_ SL bytes of sender name
+ *  \
+ *   \_ 1 byte of sender len, 8 bit unsigned integer.
+ * 
+ * 
+ * Checksum = sum of bytes modulo 256, with checksum set
+ *            to 0 for the computation.
+ *
+ * Design notes
+ * ============
+ *
+ * The protocol is designed in order to have certain properties:
+ *
+ * 1. Pulses and gaps can only be 100 or 200 microseconds, so the
+ *    message can be described, encoded and decoded with only two
+ *    fixed durations.
+ *
+ * 2. The preamble + sync is designed to have a well recognizable
+ *    pattern that can't be reproduced just for accident inside
+ *    the encoded pattern. There is no combinatio of encoded bits
+ *    leading to the preamble+sync. Also the sync pattern final
+ *    part can't be mistaken for actual bits of data, since it
+ *    contains alternating short pulses/gaps at 100us.
+ *
+ * 3. Data encoding wastes some bandwidth in order to be more
+ *    robust. Even so, with a 300us clock period, a single bit
+ *    bit takes 900us, reaching a data transfer of 138 characters per
+ *    second. More than enough for the simple chat we have here.
+ */
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    const char* sync_pattern = "1010101010101010" // Preamble
+                               "1100110011001010"; // Sync
+    uint8_t sync_len = 32;
+
+    /* This is a variable length message, however the minimum length
+     * requires a sender len byte (of value zero) and the terminator
+     * FF 00 plus checksum: a total of 4 bytes. */
+    if(numbits - sync_len < 8 * 4) return false;
+
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Chat preamble+sync found");
+
+    /* If there is room on the left, let's mark the start of the message
+     * a bit before: we don't try to detect all the preamble, but only
+     * the first part, however it is likely present. */
+    if(off >= 16) {
+        off -= 16;
+        sync_len += 16;
+    }
+
+    info->start_off = off;
+    off += sync_len; /* Skip preamble and sync. */
+
+    uint8_t raw[64] = {(uint8_t)'.'};
+    uint32_t decoded =
+        convert_from_line_code(raw, sizeof(raw), bits, numbytes, off, "100", "110"); /* PWM */
+    FURI_LOG_E(TAG, "Chat decoded bits: %lu", decoded);
+
+    if(decoded < 8 * 4) return false; /* Min message len. */
+
+    // The message needs to have a two bytes terminator before
+    // the checksum.
+    uint32_t j;
+    for(j = 0; j < sizeof(raw) - 1; j++)
+        if(raw[j] == 0xff && raw[j + 1] == 0xaa) break;
+
+    if(j == sizeof(raw) - 1) {
+        FURI_LOG_E(TAG, "Chat: terminator not found");
+        return false; // No terminator found.
+    }
+
+    uint32_t datalen = j + 3; // If the terminator was found at j, then
+        // we need to sum three more bytes to have
+        // the len: FF itself, AA, checksum.
+    info->pulses_count = sync_len + 8 * 3 * datalen;
+
+    // Check if the control sum matches.
+    if(sum_bytes(raw, datalen - 1, 0) != raw[datalen - 1]) {
+        FURI_LOG_E(TAG, "Chat: checksum mismatch");
+        return false;
+    }
+
+    // Check if the length of the sender looks sane
+    uint8_t senderlen = raw[0];
+    if(senderlen >= sizeof(raw)) {
+        FURI_LOG_E(TAG, "Chat: invalid sender length");
+        return false; // Overflow
+    }
+
+    fieldset_add_str(info->fieldset, "sender", (char*)raw + 1, senderlen);
+    fieldset_add_str(
+        info->fieldset, "message", (char*)raw + 1 + senderlen, datalen - senderlen - 4);
+    return true;
+}
+
+/* Give fields and defaults for the signal creator. */
+static void get_fields(ProtoViewFieldSet* fieldset) {
+    fieldset_add_str(fieldset, "sender", "Carol", 5);
+    fieldset_add_str(fieldset, "message", "Anyone hearing?", 15);
+}
+
+/* Create a signal. */
+static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fs) {
+    uint32_t te = 300; /* Short pulse duration in microseconds.
+                          Our protocol needs three symbol times to send
+                          a bit, so 300 us per bit = 3.33 kBaud. */
+
+    // Preamble: 24 alternating 300us pulse/gap pairs.
+    for(int j = 0; j < 24; j++) {
+        raw_samples_add(samples, true, te);
+        raw_samples_add(samples, false, te);
+    }
+
+    // Sync: 3 alternating 600 us pulse/gap pairs.
+    for(int j = 0; j < 3; j++) {
+        raw_samples_add(samples, true, te * 2);
+        raw_samples_add(samples, false, te * 2);
+    }
+
+    // Sync: plus 2 alternating 300 us pluse/gap pairs.
+    for(int j = 0; j < 2; j++) {
+        raw_samples_add(samples, true, te);
+        raw_samples_add(samples, false, te);
+    }
+
+    // Data: build the array.
+    uint32_t datalen = 1 + fs->fields[0]->len + // Userlen + Username
+                       fs->fields[1]->len + 3; // Message + FF + 00 + CRC
+    uint8_t *data = malloc(datalen), *p = data;
+    *p++ = fs->fields[0]->len;
+    memcpy(p, fs->fields[0]->str, fs->fields[0]->len);
+    p += fs->fields[0]->len;
+    memcpy(p, fs->fields[1]->str, fs->fields[1]->len);
+    p += fs->fields[1]->len;
+    *p++ = 0xff;
+    *p++ = 0xaa;
+    *p = sum_bytes(data, datalen - 1, 0);
+
+    // Emit bits
+    for(uint32_t j = 0; j < datalen * 8; j++) {
+        if(bitmap_get(data, datalen, j)) {
+            raw_samples_add(samples, true, te * 2);
+            raw_samples_add(samples, false, te);
+        } else {
+            raw_samples_add(samples, true, te);
+            raw_samples_add(samples, false, te * 2);
+        }
+    }
+    free(data);
+}
+
+ProtoViewDecoder ProtoViewChatDecoder = {
+    .name = "ProtoView chat",
+    .decode = decode,
+    .get_fields = get_fields,
+    .build_message = build_message};

+ 58 - 0
main_apps_sources/protoview/protocols/tpms/citroen.c

@@ -0,0 +1,58 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Citroen TPMS. Usually 443.92 Mhz FSK.
+ *
+ * Preamble of ~14 high/low 52 us pulses
+ * Sync of high 100us pulse then 50us low
+ * Then Manchester bits, 10 bytes total.
+ * Simple XOR checksum. */
+
+#include "../../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    /* We consider a preamble of 17 symbols. They are more, but the decoding
+     * is more likely to happen if we don't pretend to receive from the
+     * very start of the message. */
+    uint32_t sync_len = 17;
+    const char* sync_pattern = "10101010101010110";
+    if(numbits - sync_len < 8 * 10) return false; /* Expect 10 bytes. */
+
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Renault TPMS preamble+sync found");
+
+    info->start_off = off;
+    off += sync_len; /* Skip preamble + sync. */
+
+    uint8_t raw[10];
+    uint32_t decoded = convert_from_line_code(
+        raw, sizeof(raw), bits, numbytes, off, "01", "10"); /* Manchester. */
+    FURI_LOG_E(TAG, "Citroen TPMS decoded bits: %lu", decoded);
+
+    if(decoded < 8 * 10) return false; /* Require the full 10 bytes. */
+
+    /* Check the CRC. It's a simple XOR of bytes 1-9, the first byte
+     * is not included. The meaning of the first byte is unknown and
+     * we don't display it. */
+    uint8_t crc = 0;
+    for(int j = 1; j < 10; j++) crc ^= raw[j];
+    if(crc != 0) return false; /* Require sane checksum. */
+
+    info->pulses_count = (off + 8 * 10 * 2) - info->start_off;
+
+    int repeat = raw[5] & 0xf;
+    float kpa = (float)raw[6] * 1.364;
+    int temp = raw[7] - 50;
+    int battery = raw[8]; /* This may be the battery. It's not clear. */
+
+    fieldset_add_bytes(info->fieldset, "Tire ID", raw + 1, 4 * 2);
+    fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
+    fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
+    fieldset_add_uint(info->fieldset, "Repeat", repeat, 4);
+    fieldset_add_uint(info->fieldset, "Battery", battery, 8);
+    return true;
+}
+
+ProtoViewDecoder CitroenTPMSDecoder =
+    {.name = "Citroen TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 61 - 0
main_apps_sources/protoview/protocols/tpms/ford.c

@@ -0,0 +1,61 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Ford tires TPMS. Usually 443.92 Mhz FSK (in Europe).
+ *
+ * 52 us short pules
+ * Preamble: 0101010101010101010101010101
+ * Sync: 0110 (that is 52 us gap + 104 us pulse + 52 us gap)
+ * Data: 8 bytes Manchester encoded
+ * 01 = zero
+ * 10 = one
+ */
+
+#include "../../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    const char* sync_pattern = "010101010101"
+                               "0110";
+    uint8_t sync_len = 12 + 4; /* We just use 12 preamble symbols + sync. */
+    if(numbits - sync_len < 8 * 8) return false;
+
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Fort TPMS preamble+sync found");
+
+    info->start_off = off;
+    off += sync_len; /* Skip preamble and sync. */
+
+    uint8_t raw[8];
+    uint32_t decoded = convert_from_line_code(
+        raw, sizeof(raw), bits, numbytes, off, "01", "10"); /* Manchester. */
+    FURI_LOG_E(TAG, "Ford TPMS decoded bits: %lu", decoded);
+
+    if(decoded < 8 * 8) return false; /* Require the full 8 bytes. */
+
+    /* CRC is just the sum of the first 7 bytes MOD 256. */
+    uint8_t crc = 0;
+    for(int j = 0; j < 7; j++) crc += raw[j];
+    if(crc != raw[7]) return false; /* Require sane CRC. */
+
+    info->pulses_count = (off + 8 * 8 * 2) - info->start_off;
+
+    float psi = 0.25 * (((raw[6] & 0x20) << 3) | raw[4]);
+
+    /* Temperature apperas to be valid only if the most significant
+     * bit of the value is not set. Otherwise its meaning is unknown.
+     * Likely useful to alternatively send temperature or other info. */
+    int temp = raw[5] & 0x80 ? 0 : raw[5] - 56;
+    int flags = raw[5] & 0x7f;
+    int car_moving = (raw[6] & 0x44) == 0x44;
+
+    fieldset_add_bytes(info->fieldset, "Tire ID", raw, 4 * 2);
+    fieldset_add_float(info->fieldset, "Pressure psi", psi, 2);
+    fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
+    fieldset_add_hex(info->fieldset, "Flags", flags, 7);
+    fieldset_add_uint(info->fieldset, "Moving", car_moving, 1);
+    return true;
+}
+
+ProtoViewDecoder FordTPMSDecoder =
+    {.name = "Ford TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 119 - 0
main_apps_sources/protoview/protocols/tpms/renault.c

@@ -0,0 +1,119 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Renault tires TPMS. Usually 443.92 Mhz FSK.
+ *
+ * Preamble + sync + Manchester bits. ~48us short pulse.
+ * 9 Bytes in total not counting the preamble. */
+
+#include "../../app.h"
+
+#define USE_TEST_VECTOR 0
+static const char* test_vector =
+    "...01010101010101010110" // Preamble + sync
+
+    /* The following is Marshal encoded, so each two characters are
+     * actaully one bit. 01 = 0, 10 = 1. */
+    "010110010110" // Flags.
+    "10011001101010011001" // Pressure, multiply by 0.75 to obtain kpa.
+    // 244 kpa here.
+    "1010010110011010" // Temperature, subtract 30 to obtain celsius. 22C here.
+    "1001010101101001"
+    "0101100110010101"
+    "1001010101100110" // Tire ID. 0x7AD779 here.
+    "0101010101010101"
+    "0101010101010101" // Two FF bytes (usually). Unknown.
+    "0110010101010101"; // CRC8 with (poly 7, initialization 0).
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    if(USE_TEST_VECTOR) { /* Test vector to check that decoding works. */
+        bitmap_set_pattern(bits, numbytes, 0, test_vector);
+        numbits = strlen(test_vector);
+    }
+
+    if(numbits - 12 < 9 * 8) return false;
+
+    const char* sync_pattern = "01010101010101010110";
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Renault TPMS preamble+sync found");
+
+    info->start_off = off;
+    off += 20; /* Skip preamble. */
+
+    uint8_t raw[9];
+    uint32_t decoded = convert_from_line_code(
+        raw, sizeof(raw), bits, numbytes, off, "01", "10"); /* Manchester. */
+    FURI_LOG_E(TAG, "Renault TPMS decoded bits: %lu", decoded);
+
+    if(decoded < 8 * 9) return false; /* Require the full 9 bytes. */
+    if(crc8(raw, 8, 0, 7) != raw[8]) return false; /* Require sane CRC. */
+
+    info->pulses_count = (off + 8 * 9 * 2) - info->start_off;
+
+    uint8_t flags = raw[0] >> 2;
+    float kpa = 0.75 * ((uint32_t)((raw[0] & 3) << 8) | raw[1]);
+    int temp = raw[2] - 30;
+
+    fieldset_add_bytes(info->fieldset, "Tire ID", raw + 3, 3 * 2);
+    fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
+    fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
+    fieldset_add_hex(info->fieldset, "Flags", flags, 6);
+    fieldset_add_bytes(info->fieldset, "Unknown1", raw + 6, 2);
+    fieldset_add_bytes(info->fieldset, "Unknown2", raw + 7, 2);
+    return true;
+}
+
+/* Give fields and defaults for the signal creator. */
+static void get_fields(ProtoViewFieldSet* fieldset) {
+    uint8_t default_id[3] = {0xAB, 0xCD, 0xEF};
+    fieldset_add_bytes(fieldset, "Tire ID", default_id, 3 * 2);
+    fieldset_add_float(fieldset, "Pressure kpa", 123, 2);
+    fieldset_add_int(fieldset, "Temperature C", 20, 8);
+    // We don't know what flags are, but 1B is a common value.
+    fieldset_add_hex(fieldset, "Flags", 0x1b, 6);
+    fieldset_add_bytes(fieldset, "Unknown1", (uint8_t*)"\xff", 2);
+    fieldset_add_bytes(fieldset, "Unknown2", (uint8_t*)"\xff", 2);
+}
+
+/* Create a Renault TPMS signal, according to the fields provided. */
+static void build_message(RawSamplesBuffer* samples, ProtoViewFieldSet* fieldset) {
+    uint32_t te = 50; // Short pulse duration in microseconds.
+
+    // Preamble + sync
+    const char* psync = "01010101010101010101010101010110";
+    const char* p = psync;
+    while(*p) {
+        raw_samples_add_or_update(samples, *p == '1', te);
+        p++;
+    }
+
+    // Data, 9 bytes
+    uint8_t data[9] = {0};
+    unsigned int raw_pressure = fieldset->fields[1]->fvalue * 4 / 3;
+    data[0] = fieldset->fields[3]->uvalue << 2; // Flags
+    data[0] |= (raw_pressure >> 8) & 3; // Pressure kpa high 2 bits
+    data[1] = raw_pressure & 0xff; // Pressure kpa low 8 bits
+    data[2] = fieldset->fields[2]->value + 30; // Temperature C
+    memcpy(data + 3, fieldset->fields[0]->bytes, 6); // ID, 24 bits.
+    data[6] = fieldset->fields[4]->bytes[0]; // Unknown 1
+    data[7] = fieldset->fields[5]->bytes[0]; // Unknown 2
+    data[8] = crc8(data, 8, 0, 7);
+
+    // Generate Manchester code for each bit
+    for(uint32_t j = 0; j < 9 * 8; j++) {
+        if(bitmap_get(data, sizeof(data), j)) {
+            raw_samples_add_or_update(samples, true, te);
+            raw_samples_add_or_update(samples, false, te);
+        } else {
+            raw_samples_add_or_update(samples, false, te);
+            raw_samples_add_or_update(samples, true, te);
+        }
+    }
+}
+
+ProtoViewDecoder RenaultTPMSDecoder = {
+    .name = "Renault TPMS",
+    .decode = decode,
+    .get_fields = get_fields,
+    .build_message = build_message};

+ 70 - 0
main_apps_sources/protoview/protocols/tpms/schrader.c

@@ -0,0 +1,70 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Schrader TPMS. Usually 443.92 Mhz OOK, 120us pulse len.
+ *
+ * 500us high pulse + Preamble + Manchester coded bits where:
+ * 1 = 10
+ * 0 = 01
+ *
+ * 60 bits of data total (first 4 nibbles is the preamble, 0xF).
+ *
+ * Used in FIAT-Chrysler, Mercedes, ... */
+
+#include "../../app.h"
+
+#define USE_TEST_VECTOR 0
+static const char* test_vector =
+    "000000111101010101011010010110010110101001010110100110011001100101010101011010100110100110011010101010101010101010101010101010101010101010101010";
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    if(USE_TEST_VECTOR) { /* Test vector to check that decoding works. */
+        bitmap_set_pattern(bits, numbytes, 0, test_vector);
+        numbits = strlen(test_vector);
+    }
+
+    if(numbits < 64) return false; /* Preamble + data. */
+
+    const char* sync_pattern = "1111010101"
+                               "01011010";
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Schrader TPMS gap+preamble found");
+
+    info->start_off = off;
+    off += 10; /* Skip just the long pulse and the first 3 bits of sync, so
+                  that we have the first byte of data with the sync nibble
+                  0011 = 0x3. */
+
+    uint8_t raw[8];
+    uint8_t id[4];
+    uint32_t decoded = convert_from_line_code(
+        raw, sizeof(raw), bits, numbytes, off, "01", "10"); /* Manchester code. */
+    FURI_LOG_E(TAG, "Schrader TPMS decoded bits: %lu", decoded);
+
+    if(decoded < 64) return false; /* Require the full 8 bytes. */
+
+    raw[0] |= 0xf0; // Fix the preamble nibble for checksum computation.
+    uint8_t cksum = crc8(raw, sizeof(raw) - 1, 0xf0, 0x7);
+    if(cksum != raw[7]) {
+        FURI_LOG_E(TAG, "Schrader TPMS checksum mismatch");
+        return false;
+    }
+
+    info->pulses_count = (off + 8 * 8 * 2) - info->start_off;
+
+    float kpa = (float)raw[5] * 2.5;
+    int temp = raw[6] - 50;
+    id[0] = raw[1] & 7;
+    id[1] = raw[2];
+    id[2] = raw[3];
+    id[3] = raw[4];
+
+    fieldset_add_bytes(info->fieldset, "Tire ID", id, 4 * 2);
+    fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
+    fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
+    return true;
+}
+
+ProtoViewDecoder SchraderTPMSDecoder =
+    {.name = "Schrader TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 62 - 0
main_apps_sources/protoview/protocols/tpms/schrader_eg53ma4.c

@@ -0,0 +1,62 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Schrader variant EG53MA4 TPMS.
+ * Usually 443.92 Mhz OOK, 100us pulse len.
+ *
+ * Preamble: alternating pulse/gap, 100us.
+ * Sync (as pulses and gaps): "01100101", already part of the data stream
+ * (first nibble) corresponding to 0x4
+ *
+ * A total of 10 bytes payload, Manchester encoded.
+ *
+ * 0 = 01
+ * 1 = 10
+ *
+ * Used in certain Open cars and others.
+ */
+
+#include "../../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    const char* sync_pattern = "010101010101"
+                               "01100101";
+    uint8_t sync_len = 12 + 8; /* We just use 12 preamble symbols + sync. */
+    if(numbits - sync_len + 8 < 8 * 10) return false;
+
+    uint64_t off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync_pattern);
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+    FURI_LOG_E(TAG, "Schrader EG53MA4 TPMS preamble+sync found");
+
+    info->start_off = off;
+    off += sync_len - 8; /* Skip preamble, not sync that is part of the data. */
+
+    uint8_t raw[10];
+    uint32_t decoded = convert_from_line_code(
+        raw, sizeof(raw), bits, numbytes, off, "01", "10"); /* Manchester code. */
+    FURI_LOG_E(TAG, "Schrader EG53MA4 TPMS decoded bits: %lu", decoded);
+
+    if(decoded < 10 * 8) return false; /* Require the full 10 bytes. */
+
+    /* CRC is just all bytes added mod 256. */
+    uint8_t crc = 0;
+    for(int j = 0; j < 9; j++) crc += raw[j];
+    if(crc != raw[9]) return false; /* Require sane CRC. */
+
+    info->pulses_count = (off + 10 * 8 * 2) - info->start_off;
+
+    /* To convert the raw pressure to kPa, RTL433 uses 2.5, but is likely
+     * wrong. Searching on Google for users experimenting with the value
+     * reported, the value appears to be 2.75. */
+    float kpa = (float)raw[7] * 2.75;
+    int temp_f = raw[8];
+    int temp_c = (temp_f - 32) * 5 / 9; /* Convert Fahrenheit to Celsius. */
+
+    fieldset_add_bytes(info->fieldset, "Tire ID", raw + 4, 3 * 2);
+    fieldset_add_float(info->fieldset, "Pressure kpa", kpa, 2);
+    fieldset_add_int(info->fieldset, "Temperature C", temp_c, 8);
+    return true;
+}
+
+ProtoViewDecoder SchraderEG53MA4TPMSDecoder =
+    {.name = "Schrader EG53MA4 TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 81 - 0
main_apps_sources/protoview/protocols/tpms/toyota.c

@@ -0,0 +1,81 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * Toyota tires TPMS. Usually 443.92 Mhz FSK (In Europe).
+ *
+ * Preamble + sync + 64 bits of data. ~48us short pulse length.
+ *
+ * The preamble + sync is something like:
+ *
+ *   10101010101 (preamble) + 001111[1] (sync)
+ *
+ * Note: the final [1] means that sometimes it is four 1s, sometimes
+ * five, depending on the short pulse length detection and the exact
+ * duration of the high long pulse. After the sync, a differential
+ * Manchester encoded payload follows. However the Flipper's CC1101
+ * often can't decode correctly the initial alternating pattern 101010101,
+ * so what we do is to seek just the sync, that is "001111" or "0011111",
+ * however we now that it must be followed by one differenitally encoded
+ * bit, so we can use also the first symbol of data to force a more robust
+ * detection, and look for one of the following:
+ *
+ * [001111]00
+ * [0011111]00
+ * [001111]01
+ * [0011111]01
+ */
+
+#include "../../app.h"
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    if(numbits - 6 < 64 * 2)
+        return false; /* Ask for 64 bit of data (each bit
+                                           is two symbols in the bitmap). */
+
+    char* sync[] = {"00111100", "001111100", "00111101", "001111101", NULL};
+
+    int j;
+    uint32_t off = 0;
+    for(j = 0; sync[j]; j++) {
+        off = bitmap_seek_bits(bits, numbytes, 0, numbits, sync[j]);
+        if(off != BITMAP_SEEK_NOT_FOUND) {
+            info->start_off = off;
+            off += strlen(sync[j]) - 2;
+            break;
+        }
+    }
+    if(off == BITMAP_SEEK_NOT_FOUND) return false;
+
+    FURI_LOG_E(TAG, "Toyota TPMS sync[%s] found", sync[j]);
+
+    uint8_t raw[9];
+    uint32_t decoded = convert_from_diff_manchester(raw, sizeof(raw), bits, numbytes, off, true);
+    FURI_LOG_E(TAG, "Toyota TPMS decoded bits: %lu", decoded);
+
+    if(decoded < 8 * 9) return false; /* Require the full 8 bytes. */
+    if(crc8(raw, 8, 0x80, 7) != raw[8]) return false; /* Require sane CRC. */
+
+    /* We detected a valid signal. However now info->start_off is actually
+     * pointing to the sync part, not the preamble of alternating 0 and 1.
+     * Protoview decoders get called with some space to the left, in order
+     * for the decoder itself to fix the signal if neeeded, so that its
+     * logical representation will be more accurate and better to save
+     * and retransmit. */
+    if(info->start_off >= 12) {
+        info->start_off -= 12;
+        bitmap_set_pattern(bits, numbytes, info->start_off, "010101010101");
+    }
+
+    info->pulses_count = (off + 8 * 9 * 2) - info->start_off;
+
+    float psi = (float)((raw[4] & 0x7f) << 1 | raw[5] >> 7) * 0.25 - 7;
+    int temp = ((raw[5] & 0x7f) << 1 | raw[6] >> 7) - 40;
+
+    fieldset_add_bytes(info->fieldset, "Tire ID", raw, 4 * 2);
+    fieldset_add_float(info->fieldset, "Pressure psi", psi, 2);
+    fieldset_add_int(info->fieldset, "Temperature C", temp, 8);
+    return true;
+}
+
+ProtoViewDecoder ToyotaTPMSDecoder =
+    {.name = "Toyota TPMS", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 326 - 0
main_apps_sources/protoview/protocols/unknown.c

@@ -0,0 +1,326 @@
+#include "../app.h"
+
+/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license.
+ *
+ * ----------------------------------------------------------------------------
+ * The "unknown" decoder fires as the last one, once we are sure no other
+ * decoder was able to identify the signal. The goal is to detect the
+ * preamble and line code used in the received signal, then turn the
+ * decoded bits into bytes.
+ *
+ * The techniques used for the detection are described in the comments
+ * below.
+ * ----------------------------------------------------------------------------
+ */
+
+/* Scan the signal bitmap looking for a PWM modulation. In this case
+ * for PWM we are referring to two exact patterns of high and low
+ * signal (each bit in the bitmap is worth the smallest gap/pulse duration
+ * we detected) that repeat each other in a given segment of the message.
+ *
+ * This modulation is quite common, for instance sometimes zero and
+ * one are rappresented by a 700us pulse followed by 350 gap,
+ * and 350us pulse followed by a 700us gap. So the signal bitmap received
+ * by the decoder would contain 110 and 100 symbols.
+ *
+ * The way this function work is commented inline.
+ *
+ * The function returns the number of consecutive symbols found, having
+ * a symbol length of 'symlen' (3 in the above example), and stores
+ * in *s1i the offset of the first symbol found, and in *s2i the offset
+ * of the second symbol. The function can't tell which is one and which
+ * zero. */
+static uint32_t find_pwm(
+    uint8_t* bits,
+    uint32_t numbytes,
+    uint32_t numbits,
+    uint32_t symlen,
+    uint32_t* s1i,
+    uint32_t* s2i) {
+    uint32_t best_count = 0; /* Max number of symbols found in this try. */
+    uint32_t best_idx1 = 0; /* First symbol offset of longest sequence found.
+                              * This is also the start sequence offset. */
+    uint32_t best_idx2 = 0; /* Second symbol offset. */
+
+    /* Try all the possible symbol offsets that are less of our
+     * symbol len. This is likely not really useful but we take
+     * a conservative approach. Because if have have, for instance,
+     * repeating symbols "100" and "110", they will form a sequence
+     * that is choerent at different offsets, but out-of-sync.
+     *
+     * Anyway at the end of the function we try to fix the sync. */
+    for(uint32_t off = 0; off < symlen; off++) {
+        uint32_t c = 0; // Number of contiguous symbols found.
+        uint32_t c1 = 0, c2 = 0; // Occurrences of first/second symbol.
+        *s1i = off; // Assume we start at one symbol boundaty.
+        *s2i = UINT32_MAX; // Second symbol first index still unknown.
+        uint32_t next = off;
+
+        /* We scan the whole bitmap in one pass, resetting the state
+         * each time we find a pattern that is not one of the two
+         * symbols we found so far. */
+        while(next < numbits - symlen) {
+            bool match1 = bitmap_match_bitmap(bits, numbytes, next, bits, numbytes, *s1i, symlen);
+            if(!match1 && *s2i == UINT32_MAX) {
+                /* It's not the first sybol. We don't know how the
+                 * second look like. Assume we found an occurrence of
+                 * the second symbol. */
+                *s2i = next;
+            }
+
+            bool match2 = bitmap_match_bitmap(bits, numbytes, next, bits, numbytes, *s2i, symlen);
+
+            /* One or the other should match. */
+            if(match1 || match2) {
+                c++;
+                if(match1) c1++;
+                if(match2) c2++;
+                if(c > best_count && c1 >= best_count / 5 && // Require enough presence of both
+                   c2 >= best_count / 5) // zero and one.
+                {
+                    best_count = c;
+                    best_idx1 = *s1i;
+                    best_idx2 = *s2i;
+                }
+                next += symlen;
+            } else {
+                /* No match. Continue resetting the signal info. */
+                c = 0; // Start again to count contiguous symbols.
+                c1 = 0;
+                c2 = 0;
+                *s1i = next; // First symbol always at start.
+                *s2i = UINT32_MAX; // Second symbol unknown.
+            }
+        }
+    }
+
+    /* We don't know if we are really synchronized with the bits at this point.
+     * For example if zero bit is 100 and one bit is 110 in a specific
+     * line code, our detector could randomly believe it's 001 and 101.
+     * However PWD line codes normally start with a pulse in both symbols.
+     * If that is the case, let's align. */
+    uint32_t shift;
+    for(shift = 0; shift < symlen; shift++) {
+        if(bitmap_get(bits, numbytes, best_idx1 + shift) &&
+           bitmap_get(bits, numbytes, best_idx2 + shift))
+            break;
+    }
+    if(shift != symlen) {
+        best_idx1 += shift;
+        best_idx2 += shift;
+    }
+
+    *s1i = best_idx1;
+    *s2i = best_idx2;
+    return best_count;
+}
+
+/* Find the longest sequence that looks like Manchester coding.
+ *
+ * Manchester coding requires each pairs of bits to be either
+ * 01 or 10. We'll have to try odd and even offsets to be
+ * sure to find it.
+ *
+ * Note that this will also detect differential Manchester, but
+ * will report it as Manchester. I can't think of any way to
+ * distinguish between the two line codes, because shifting them
+ * one symbol will make one to look like the other.
+ *
+ * Only option could be to decode the message with both line
+ * codes and use statistical properties (common byte values)
+ * to determine what's more likely, but this looks very fragile.
+ *
+ * Fortunately differential Manchester is more rarely used,
+ * so we can assume Manchester most of the times. Yet we are left
+ * with the indetermination about zero being pulse-gap or gap-pulse
+ * or the other way around.
+ *
+ * If the 'only_raising' parameter is true, the function detects
+ * only sequences going from gap to pulse: this is useful in order
+ * to locate preambles of alternating gaps and pulses. */
+static uint32_t find_alternating_bits(
+    uint8_t* bits,
+    uint32_t numbytes,
+    uint32_t numbits,
+    uint32_t* start,
+    bool only_raising) {
+    uint32_t best_count = 0; // Max number of symbols found
+    uint32_t best_off = 0; // Max symbols start offset.
+    for(int odd = 0; odd < 2; odd++) {
+        uint32_t count = 0; // Symbols found so far
+        uint32_t start_off = odd;
+        uint32_t j = odd;
+        while(j < numbits - 1) {
+            bool bit1 = bitmap_get(bits, numbytes, j);
+            bool bit2 = bitmap_get(bits, numbytes, j + 1);
+            if((!only_raising && bit1 != bit2) || (only_raising && !bit1 && bit2)) {
+                count++;
+                if(count > best_count) {
+                    best_count = count;
+                    best_off = start_off;
+                }
+            } else {
+                /* End of sequence. Continue with the next
+                 * part of the signal. */
+                count = 0;
+                start_off = j + 2;
+            }
+            j += 2;
+        }
+    }
+    *start = best_off;
+    return best_count;
+}
+
+/* Wrapper to find Manchester code. */
+static uint32_t
+    find_manchester(uint8_t* bits, uint32_t numbytes, uint32_t numbits, uint32_t* start) {
+    return find_alternating_bits(bits, numbytes, numbits, start, false);
+}
+
+/* Wrapper to find preamble sections. */
+static uint32_t
+    find_preamble(uint8_t* bits, uint32_t numbytes, uint32_t numbits, uint32_t* start) {
+    return find_alternating_bits(bits, numbytes, numbits, start, true);
+}
+
+typedef enum {
+    LineCodeNone,
+    LineCodeManchester,
+    LineCodePWM3,
+    LineCodePWM4,
+} LineCodeGuess;
+
+static char* get_linecode_name(LineCodeGuess lc) {
+    switch(lc) {
+    case LineCodeNone:
+        return "none";
+    case LineCodeManchester:
+        return "Manchester";
+    case LineCodePWM3:
+        return "PWM3";
+    case LineCodePWM4:
+        return "PWM4";
+    }
+    return "unknown";
+}
+
+static bool decode(uint8_t* bits, uint32_t numbytes, uint32_t numbits, ProtoViewMsgInfo* info) {
+    /* No decoder was able to detect this message. Let's try if we can
+     * find some structure. To start, we'll see if it looks like is
+     * manchester coded, or PWM with symbol len of 3 or 4. */
+
+    /* For PWM, start1 and start2 are the offsets at which the two
+     * sequences composing the message appear the first time.
+     * So start1 is also the message start offset. Start2 is not used
+     * for Manchester, that does not have two separated symbols like
+     * PWM. */
+    uint32_t start1 = 0, start2 = 0;
+    uint32_t msgbits; // Number of message bits in the bitmap, so
+        // this will be the number of symbols, not actual
+        // bits after the message is decoded.
+    uint32_t tmp1, tmp2; // Temp vars to store the start.
+    uint32_t minbits = 16; // Less than that gets undetected.
+    uint32_t pwm_len; // Bits per symbol, in the case of PWM.
+    LineCodeGuess linecode = LineCodeNone;
+
+    // Try PWM3
+    uint32_t pwm3_bits = find_pwm(bits, numbytes, numbits, 3, &tmp1, &tmp2);
+    if(pwm3_bits >= minbits) {
+        linecode = LineCodePWM3;
+        start1 = tmp1;
+        start2 = tmp2;
+        pwm_len = 3;
+        msgbits = pwm3_bits * pwm_len;
+    }
+
+    // Try PWM4
+    uint32_t pwm4_bits = find_pwm(bits, numbytes, numbits, 4, &tmp1, &tmp2);
+    if(pwm4_bits >= minbits && pwm4_bits > pwm3_bits) {
+        linecode = LineCodePWM4;
+        start1 = tmp1;
+        start2 = tmp2;
+        pwm_len = 4;
+        msgbits = pwm3_bits * pwm_len;
+    }
+
+    // Try Manchester
+    uint32_t manchester_bits = find_manchester(bits, numbytes, numbits, &tmp1);
+    if(manchester_bits > minbits && manchester_bits > pwm3_bits && manchester_bits > pwm4_bits) {
+        linecode = LineCodeManchester;
+        start1 = tmp1;
+        msgbits = manchester_bits * 2;
+        //FURI_LOG_T(TAG, "MANCHESTER START: %lu", tmp1);
+    }
+
+    if(linecode == LineCodeNone) return false;
+
+    /* Often there is a preamble before the signal. We'll try to find
+     * it, and if it is not too far away from our signal, we'll claim
+     * our signal starts at the preamble. */
+    uint32_t preamble_len = find_preamble(bits, numbytes, numbits, &tmp1);
+    uint32_t min_preamble_len = 10;
+    uint32_t max_preamble_distance = 32;
+    uint32_t preamble_start = 0;
+    bool preamble_found = false;
+
+    /* Note that because of the following checks, if the Manchester detector
+     * detected the preamble bits as data, we are ok with that, since it
+     * means that the synchronization is not designed to "break" the bits
+     * flow. In this case we ignore the preamble and return all as data. */
+    if(preamble_len >= min_preamble_len && // Not too short.
+       tmp1 < start1 && // Should be before the data.
+       start1 - tmp1 <= max_preamble_distance) // Not too far.
+    {
+        preamble_start = tmp1;
+        preamble_found = true;
+    }
+
+    info->start_off = preamble_found ? preamble_start : start1;
+    info->pulses_count = (start1 + msgbits) - info->start_off;
+    info->pulses_count += 20; /* Add a few more, so that if the user resends
+                               * the message, it is more likely we will
+                               * transfer all that is needed, like a message
+                               * terminator (that we don't detect). */
+
+    /*if(preamble_found) FURI_LOG_T(TAG, "PREAMBLE AT: %lu", preamble_start);
+    FURI_LOG_T(TAG, "START: %lu", info->start_off);
+    FURI_LOG_T(TAG, "MSGBITS: %lu", msgbits);
+    FURI_LOG_T(TAG, "DATASTART: %lu", start1);
+    FURI_LOG_T(TAG, "PULSES: %lu", info->pulses_count);*/
+
+    /* We think there is a message and we know where it starts and the
+     * line code used. We can turn it into bits and bytes. */
+    uint32_t decoded;
+    uint8_t data[32];
+    uint32_t datalen;
+
+    char symbol1[5], symbol2[5];
+    if(linecode == LineCodePWM3 || linecode == LineCodePWM4) {
+        bitmap_to_string(symbol1, bits, numbytes, start1, pwm_len);
+        bitmap_to_string(symbol2, bits, numbytes, start2, pwm_len);
+    } else if(linecode == LineCodeManchester) {
+        memcpy(symbol1, "01", 3);
+        memcpy(symbol2, "10", 3);
+    }
+
+    decoded = convert_from_line_code(data, sizeof(data), bits, numbytes, start1, symbol1, symbol2);
+    datalen = (decoded + 7) / 8;
+
+    char* linecode_name = get_linecode_name(linecode);
+    fieldset_add_str(info->fieldset, "line code", linecode_name, strlen(linecode_name));
+    fieldset_add_uint(info->fieldset, "data bits", decoded, 8);
+    if(preamble_found) fieldset_add_uint(info->fieldset, "preamble len", preamble_len, 8);
+    fieldset_add_str(info->fieldset, "first symbol", symbol1, strlen(symbol1));
+    fieldset_add_str(info->fieldset, "second symbol", symbol2, strlen(symbol2));
+    for(uint32_t j = 0; j < datalen; j++) {
+        char label[16];
+        snprintf(label, sizeof(label), "data[%lu]", j);
+        fieldset_add_bytes(info->fieldset, label, data + j, 2);
+    }
+    return true;
+}
+
+ProtoViewDecoder UnknownDecoder =
+    {.name = "Unknown", .decode = decode, .get_fields = NULL, .build_message = NULL};

+ 93 - 0
main_apps_sources/protoview/raw_samples.c

@@ -0,0 +1,93 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include <inttypes.h>
+#include <furi/core/string.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include "raw_samples.h"
+
+/* Allocate and initialize a samples buffer. */
+RawSamplesBuffer* raw_samples_alloc(void) {
+    RawSamplesBuffer* buf = malloc(sizeof(*buf));
+    buf->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    raw_samples_reset(buf);
+    return buf;
+}
+
+/* Free a sample buffer. Should be called when the mutex is released. */
+void raw_samples_free(RawSamplesBuffer* s) {
+    furi_mutex_free(s->mutex);
+    free(s);
+}
+
+/* This just set all the samples to zero and also resets the internal
+ * index. There is no need to call it after raw_samples_alloc(), but only
+ * when one wants to reset the whole buffer of samples. */
+void raw_samples_reset(RawSamplesBuffer* s) {
+    furi_mutex_acquire(s->mutex, FuriWaitForever);
+    s->total = RAW_SAMPLES_NUM;
+    s->idx = 0;
+    s->short_pulse_dur = 0;
+    memset(s->samples, 0, sizeof(s->samples));
+    furi_mutex_release(s->mutex);
+}
+
+/* Set the raw sample internal index so that what is currently at
+ * offset 'offset', will appear to be at 0 index. */
+void raw_samples_center(RawSamplesBuffer* s, uint32_t offset) {
+    s->idx = (s->idx + offset) % RAW_SAMPLES_NUM;
+}
+
+/* Add the specified sample in the circular buffer. */
+void raw_samples_add(RawSamplesBuffer* s, bool level, uint32_t dur) {
+    furi_mutex_acquire(s->mutex, FuriWaitForever);
+    s->samples[s->idx].level = level;
+    s->samples[s->idx].dur = dur;
+    s->idx = (s->idx + 1) % RAW_SAMPLES_NUM;
+    furi_mutex_release(s->mutex);
+}
+
+/* This is like raw_samples_add(), however in case a sample of the
+ * same level of the previous one is added, the duration of the last
+ * sample is updated instead. Needed mainly for the decoders build_message()
+ * methods: it is simpler to write an encoder of a signal like that,
+ * just creating messages piece by piece.
+ *
+ * This function is a bit slower so the internal data sampling should
+ * be performed with raw_samples_add(). */
+void raw_samples_add_or_update(RawSamplesBuffer* s, bool level, uint32_t dur) {
+    furi_mutex_acquire(s->mutex, FuriWaitForever);
+    uint32_t previdx = (s->idx - 1) % RAW_SAMPLES_NUM;
+    if(s->samples[previdx].level == level && s->samples[previdx].dur != 0) {
+        /* Update the last sample: it has the same level. */
+        s->samples[previdx].dur += dur;
+    } else {
+        /* Add a new sample. */
+        s->samples[s->idx].level = level;
+        s->samples[s->idx].dur = dur;
+        s->idx = (s->idx + 1) % RAW_SAMPLES_NUM;
+    }
+    furi_mutex_release(s->mutex);
+}
+
+/* Get the sample from the buffer. It is possible to use out of range indexes
+ * as 'idx' because the modulo operation will rewind back from the start. */
+void raw_samples_get(RawSamplesBuffer* s, uint32_t idx, bool* level, uint32_t* dur) {
+    furi_mutex_acquire(s->mutex, FuriWaitForever);
+    idx = (s->idx + idx) % RAW_SAMPLES_NUM;
+    *level = s->samples[idx].level;
+    *dur = s->samples[idx].dur;
+    furi_mutex_release(s->mutex);
+}
+
+/* Copy one buffer to the other, including current index. */
+void raw_samples_copy(RawSamplesBuffer* dst, RawSamplesBuffer* src) {
+    furi_mutex_acquire(src->mutex, FuriWaitForever);
+    furi_mutex_acquire(dst->mutex, FuriWaitForever);
+    dst->idx = src->idx;
+    dst->short_pulse_dur = src->short_pulse_dur;
+    memcpy(dst->samples, src->samples, sizeof(dst->samples));
+    furi_mutex_release(src->mutex);
+    furi_mutex_release(dst->mutex);
+}

+ 33 - 0
main_apps_sources/protoview/raw_samples.h

@@ -0,0 +1,33 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+/* Our circular buffer of raw samples, used in order to display
+ * the signal. */
+
+#define RAW_SAMPLES_NUM \
+    2048 /* Use a power of two: we take the modulo
+                                of the index quite often to normalize inside
+                                the range, and division is slow. */
+typedef struct RawSamplesBuffer {
+    FuriMutex* mutex;
+    struct {
+        uint16_t level : 1;
+        uint16_t dur : 15;
+    } samples[RAW_SAMPLES_NUM];
+    uint32_t idx; /* Current idx (next to write). */
+    uint32_t total; /* Total samples: same as RAW_SAMPLES_NUM, we provide
+                       this field for a cleaner interface with the user, but
+                       we always use RAW_SAMPLES_NUM when taking the modulo so
+                       the compiler can optimize % as bit masking. */
+    /* Signal features. */
+    uint32_t short_pulse_dur; /* Duration of the shortest pulse. */
+} RawSamplesBuffer;
+
+RawSamplesBuffer* raw_samples_alloc(void);
+void raw_samples_reset(RawSamplesBuffer* s);
+void raw_samples_center(RawSamplesBuffer* s, uint32_t offset);
+void raw_samples_add(RawSamplesBuffer* s, bool level, uint32_t dur);
+void raw_samples_add_or_update(RawSamplesBuffer* s, bool level, uint32_t dur);
+void raw_samples_get(RawSamplesBuffer* s, uint32_t idx, bool* level, uint32_t* dur);
+void raw_samples_copy(RawSamplesBuffer* dst, RawSamplesBuffer* src);
+void raw_samples_free(RawSamplesBuffer* s);

+ 692 - 0
main_apps_sources/protoview/signal.c

@@ -0,0 +1,692 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+
+bool decode_signal(RawSamplesBuffer* s, uint64_t len, ProtoViewMsgInfo* info);
+
+/* =============================================================================
+ * Protocols table.
+ *
+ * Supported protocols go here, with the relevant implementation inside
+ * protocols/<name>.c
+ * ===========================================================================*/
+
+extern ProtoViewDecoder Oregon2Decoder;
+extern ProtoViewDecoder B4B1Decoder;
+extern ProtoViewDecoder RenaultTPMSDecoder;
+extern ProtoViewDecoder ToyotaTPMSDecoder;
+extern ProtoViewDecoder SchraderTPMSDecoder;
+extern ProtoViewDecoder SchraderEG53MA4TPMSDecoder;
+extern ProtoViewDecoder CitroenTPMSDecoder;
+extern ProtoViewDecoder FordTPMSDecoder;
+extern ProtoViewDecoder KeeloqDecoder;
+extern ProtoViewDecoder ProtoViewChatDecoder;
+extern ProtoViewDecoder UnknownDecoder;
+
+ProtoViewDecoder* Decoders[] = {
+    &Oregon2Decoder, /* Oregon sensors v2.1 protocol. */
+    &B4B1Decoder, /* PT, SC, ... 24 bits remotes. */
+    &RenaultTPMSDecoder, /* Renault TPMS. */
+    &ToyotaTPMSDecoder, /* Toyota TPMS. */
+    &SchraderTPMSDecoder, /* Schrader TPMS. */
+    &SchraderEG53MA4TPMSDecoder, /* Schrader EG53MA4 TPMS. */
+    &CitroenTPMSDecoder, /* Citroen TPMS. */
+    &FordTPMSDecoder, /* Ford TPMS. */
+    &KeeloqDecoder, /* Keeloq remote. */
+    &ProtoViewChatDecoder, /* Protoview simple text messages. */
+
+    /* Warning: the following decoder must stay at the end of the
+     * list. Otherwise would detect most signals and prevent the actaul
+     * decoders from handling them. */
+    &UnknownDecoder, /* General protocol detector. */
+    NULL};
+
+/* =============================================================================
+ * Raw signal detection
+ * ===========================================================================*/
+
+/* Return the time difference between a and b, always >= 0 since
+ * the absolute value is returned. */
+uint32_t duration_delta(uint32_t a, uint32_t b) {
+    return a > b ? a - b : b - a;
+}
+
+/* Reset the current signal, so that a new one can be detected. */
+void reset_current_signal(ProtoViewApp* app) {
+    app->signal_bestlen = 0;
+    app->signal_offset = 0;
+    app->signal_decoded = false;
+    raw_samples_reset(DetectedSamples);
+    raw_samples_reset(RawSamples);
+    free_msg_info(app->msg_info);
+    app->msg_info = NULL;
+}
+
+/* This function starts scanning samples at offset idx looking for the
+ * longest run of pulses, either high or low, that are not much different
+ * from each other, for a maximum of three duration classes.
+ * So for instance 50 successive pulses that are roughly long 340us or 670us
+ * will be sensed as a coherent signal (example: 312, 361, 700, 334, 667, ...)
+ *
+ * The classes are counted separtely for high and low signals (RF on / off)
+ * because many devices tend to have different pulse lenghts depending on
+ * the level of the pulse.
+ *
+ * For instance Oregon2 sensors, in the case of protocol 2.1 will send
+ * pulses of ~400us (RF on) VS ~580us (RF off). */
+#define SEARCH_CLASSES 3
+uint32_t search_coherent_signal(RawSamplesBuffer* s, uint32_t idx, uint32_t min_duration) {
+    struct {
+        uint32_t dur[2]; /* dur[0] = low, dur[1] = high */
+        uint32_t count[2]; /* Associated observed frequency. */
+    } classes[SEARCH_CLASSES];
+
+    memset(classes, 0, sizeof(classes));
+
+    // Set a min/max duration limit for samples to be considered part of a
+    // coherent signal. The maximum length is fixed while the minimum
+    // is passed as argument, as depends on the data rate and in general
+    // on the signal to analyze.
+    uint32_t max_duration = 4000;
+
+    uint32_t len = 0; /* Observed len of coherent samples. */
+    s->short_pulse_dur = 0;
+    for(uint32_t j = idx; j < idx + s->total; j++) {
+        bool level;
+        uint32_t dur;
+        raw_samples_get(s, j, &level, &dur);
+
+        if(dur < min_duration || dur > max_duration) break; /* return. */
+
+        /* Let's see if it matches a class we already have or if we
+         * can populate a new (yet empty) class. */
+        uint32_t k;
+        for(k = 0; k < SEARCH_CLASSES; k++) {
+            if(classes[k].count[level] == 0) {
+                classes[k].dur[level] = dur;
+                classes[k].count[level] = 1;
+                break; /* Sample accepted. */
+            } else {
+                uint32_t classavg = classes[k].dur[level];
+                uint32_t count = classes[k].count[level];
+                uint32_t delta = duration_delta(dur, classavg);
+                /* Is the difference in duration between this signal and
+                 * the class we are inspecting less than a given percentage?
+                 * If so, accept this signal. */
+                if(delta < classavg / 5) { /* 100%/5 = 20%. */
+                    /* It is useful to compute the average of the class
+                     * we are observing. We know how many samples we got so
+                     * far, so we can recompute the average easily.
+                     * By always having a better estimate of the pulse len
+                     * we can avoid missing next samples in case the first
+                     * observed samples are too off. */
+                    classavg = ((classavg * count) + dur) / (count + 1);
+                    classes[k].dur[level] = classavg;
+                    classes[k].count[level]++;
+                    break; /* Sample accepted. */
+                }
+            }
+        }
+
+        if(k == SEARCH_CLASSES) break; /* No match, return. */
+
+        /* If we are here, we accepted this sample. Try with the next
+         * one. */
+        len++;
+    }
+
+    /* Update the buffer setting the shortest pulse we found
+     * among the three classes. This will be used when scaling
+     * for visualization. */
+    uint32_t short_dur[2] = {0, 0};
+    for(int j = 0; j < SEARCH_CLASSES; j++) {
+        for(int level = 0; level < 2; level++) {
+            if(classes[j].dur[level] == 0) continue;
+            if(classes[j].count[level] < 3) continue;
+            if(short_dur[level] == 0 || short_dur[level] > classes[j].dur[level]) {
+                short_dur[level] = classes[j].dur[level];
+            }
+        }
+    }
+
+    /* Use the average between high and low short pulses duration.
+     * Often they are a bit different, and using the average is more robust
+     * when we do decoding sampling at short_pulse_dur intervals. */
+    if(short_dur[0] == 0) short_dur[0] = short_dur[1];
+    if(short_dur[1] == 0) short_dur[1] = short_dur[0];
+    s->short_pulse_dur = (short_dur[0] + short_dur[1]) / 2;
+
+    return len;
+}
+
+/* Called when we detect a message. Just blinks when the message was
+ * not decoded. Vibrates, too, when the message was correctly decoded. */
+void notify_signal_detected(ProtoViewApp* app, bool decoded) {
+    static const NotificationSequence decoded_seq = {
+        &message_vibro_on,
+        &message_green_255,
+        &message_delay_50,
+        &message_green_0,
+        &message_vibro_off,
+        NULL};
+
+    static const NotificationSequence unknown_seq = {
+        &message_red_255, &message_delay_50, &message_red_0, NULL};
+
+    if(decoded)
+        notification_message(app->notification, &decoded_seq);
+    else
+        notification_message(app->notification, &unknown_seq);
+}
+
+/* Search the source buffer with the stored signal (last N samples received)
+ * in order to find a coherent signal. If a signal that does not appear to
+ * be just noise is found, it is set in DetectedSamples global signal
+ * buffer, that is what is rendered on the screen. */
+void scan_for_signal(ProtoViewApp* app, RawSamplesBuffer* source, uint32_t min_duration) {
+    /* We need to work on a copy: the source buffer may be populated
+     * by the background thread receiving data. */
+    RawSamplesBuffer* copy = raw_samples_alloc();
+    raw_samples_copy(copy, source);
+
+    /* Try to seek on data that looks to have a regular high low high low
+     * pattern. */
+    uint32_t minlen = 18; /* Min run of coherent samples. With less
+                                       than a few samples it's very easy to
+                                       mistake noise for signal. */
+
+    uint32_t i = 0;
+
+    while(i < copy->total - 1) {
+        uint32_t thislen = search_coherent_signal(copy, i, min_duration);
+
+        /* For messages that are long enough, attempt decoding. */
+        if(thislen > minlen) {
+            /* Allocate the message information that some decoder may
+             * fill, in case it is able to decode a message. */
+            ProtoViewMsgInfo* info = malloc(sizeof(ProtoViewMsgInfo));
+            init_msg_info(info, app);
+            info->short_pulse_dur = copy->short_pulse_dur;
+
+            uint32_t saved_idx = copy->idx; /* Save index, see later. */
+
+            /* decode_signal() expects the detected signal to start
+             * from index zero .*/
+            raw_samples_center(copy, i);
+            bool decoded = decode_signal(copy, thislen, info);
+            copy->idx = saved_idx; /* Restore the index as we are scanning
+                                      the signal in the loop. */
+
+            /* Accept this signal as the new signal if either it's longer
+             * than the previous undecoded one, or the previous one was
+             * unknown and this is decoded. */
+            bool oldsignal_not_decoded = app->signal_decoded == false ||
+                                         app->msg_info->decoder == &UnknownDecoder;
+
+            if(oldsignal_not_decoded &&
+               (thislen > app->signal_bestlen || (decoded && info->decoder != &UnknownDecoder))) {
+                free_msg_info(app->msg_info);
+                app->msg_info = info;
+                app->signal_bestlen = thislen;
+                app->signal_decoded = decoded;
+                raw_samples_copy(DetectedSamples, copy);
+                raw_samples_center(DetectedSamples, i);
+                FURI_LOG_E(
+                    TAG,
+                    "===> Displayed sample updated (%d samples %lu us)",
+                    (int)thislen,
+                    DetectedSamples->short_pulse_dur);
+
+                adjust_raw_view_scale(app, DetectedSamples->short_pulse_dur);
+                if(app->msg_info->decoder != &UnknownDecoder) notify_signal_detected(app, decoded);
+            } else {
+                /* If the structure was not filled, discard it. Otherwise
+                 * now the owner is app->msg_info. */
+                free_msg_info(info);
+            }
+        }
+        i += thislen ? thislen : 1;
+    }
+    raw_samples_free(copy);
+}
+
+/* =============================================================================
+ * Decoding
+ *
+ * The following code will translates the raw singals as received by
+ * the CC1101 into logical signals: a bitmap of 0s and 1s sampled at
+ * the detected data clock interval.
+ *
+ * Then the converted signal is passed to the protocols decoders, that look
+ * for protocol-specific information. We stop at the first decoder that is
+ * able to decode the data, so protocols here should be registered in
+ * order of complexity and specificity, with the generic ones at the end.
+ * ===========================================================================*/
+
+/* Set the 'bitpos' bit to value 'val', in the specified bitmap
+ * 'b' of len 'blen'.
+ * Out of range bits will silently be discarded. */
+void bitmap_set(uint8_t* b, uint32_t blen, uint32_t bitpos, bool val) {
+    uint32_t byte = bitpos / 8;
+    uint32_t bit = 7 - (bitpos & 7);
+    if(byte >= blen) return;
+    if(val)
+        b[byte] |= 1 << bit;
+    else
+        b[byte] &= ~(1 << bit);
+}
+
+/* Get the bit 'bitpos' of the bitmap 'b' of 'blen' bytes.
+ * Out of range bits return false (not bit set). */
+bool bitmap_get(uint8_t* b, uint32_t blen, uint32_t bitpos) {
+    uint32_t byte = bitpos / 8;
+    uint32_t bit = 7 - (bitpos & 7);
+    if(byte >= blen) return 0;
+    return (b[byte] & (1 << bit)) != 0;
+}
+
+/* Copy 'count' bits from the bitmap 's' of 'slen' total bytes, to the
+ * bitmap 'd' of 'dlen' total bytes. The bits are copied starting from
+ * offset 'soff' of the source bitmap to the offset 'doff' of the
+ * destination bitmap. */
+void bitmap_copy(
+    uint8_t* d,
+    uint32_t dlen,
+    uint32_t doff,
+    uint8_t* s,
+    uint32_t slen,
+    uint32_t soff,
+    uint32_t count) {
+    /* If we are byte-aligned in both source and destination, use a fast
+     * path for the number of bytes we can consume this way. */
+    if((doff & 7) == 0 && (soff & 7) == 0) {
+        uint32_t didx = doff / 8;
+        uint32_t sidx = soff / 8;
+        while(count > 8 && didx < dlen && sidx < slen) {
+            d[didx++] = s[sidx++];
+            count -= 8;
+        }
+        doff = didx * 8;
+        soff = sidx * 8;
+        /* Note that if we entered this path, the count at the end
+         * of the loop will be < 8. */
+    }
+
+    /* Copy the bits needed to reach an offset where we can copy
+     * two half bytes of src to a full byte of destination. */
+    while(count > 8 && (doff & 7) != 0) {
+        bool bit = bitmap_get(s, slen, soff++);
+        bitmap_set(d, dlen, doff++, bit);
+        count--;
+    }
+
+    /* If we are here and count > 8, we have an offset that is byte aligned
+     * to the destination bitmap, but not aligned to the source bitmap.
+     * We can copy fast enough by shifting each two bytes of the original
+     * bitmap.
+     *
+     * This is how it works:
+     *
+     *  dst:
+     *  +--------+--------+--------+
+     *  | 0      | 1      | 2      |
+     *  |        |        |        | <- data to fill
+     *  +--------+--------+--------+
+     *            ^
+     *            |
+     *            doff = 8
+     *
+     *  src:
+     *  +--------+--------+--------+
+     *  | 0      | 1      | 2      |
+     *  |hellowor|ld!HELLO|WORLDS!!| <- data to copy
+     *  +--------+--------+--------+
+     *               ^
+     *               |
+     *               soff = 11
+     *
+     *  skew = 11%8 = 3
+     *  each destination byte in dst will receive:
+     *
+     *  dst[doff/8] = (src[soff/8] << skew) | (src[soff/8+1] >> (8-skew))
+     *
+     *  dstbyte = doff/8 = 8/8 = 1
+     *  srcbyte = soff/8 = 11/8 = 1
+     *
+     *  so dst[1] will get:
+     *  src[1] << 3, that is "ld!HELLO" << 3 = "HELLO..."
+     *      xored with
+     *  src[2] << 5, that is "WORLDS!!" >> 5 = ".....WOR"
+     *  That is "HELLOWOR"
+     */
+    if(count > 8) {
+        uint8_t skew = soff % 8; /* Don't worry, compiler will optimize. */
+        uint32_t didx = doff / 8;
+        uint32_t sidx = soff / 8;
+        while(count > 8 && didx < dlen && sidx < slen) {
+            d[didx] = ((s[sidx] << skew) | (s[sidx + 1] >> (8 - skew)));
+            sidx++;
+            didx++;
+            soff += 8;
+            doff += 8;
+            count -= 8;
+        }
+    }
+
+    /* Here count is guaranteed to be < 8.
+     * Copy the final bits bit by bit. */
+    while(count) {
+        bool bit = bitmap_get(s, slen, soff++);
+        bitmap_set(d, dlen, doff++, bit);
+        count--;
+    }
+}
+
+/* We decode bits assuming the first bit we receive is the MSB
+ * (see bitmap_set/get functions). Certain devices send data
+ * encoded in the reverse way. */
+void bitmap_reverse_bytes_bits(uint8_t* p, uint32_t len) {
+    for(uint32_t j = 0; j < len; j++) {
+        uint32_t b = p[j];
+        /* Step 1: swap the two nibbles: 12345678 -> 56781234 */
+        b = (b & 0xf0) >> 4 | (b & 0x0f) << 4;
+        /* Step 2: swap adjacent pairs : 56781234 -> 78563412 */
+        b = (b & 0xcc) >> 2 | (b & 0x33) << 2;
+        /* Step 3: swap adjacent bits  : 78563412 -> 87654321 */
+        b = (b & 0xaa) >> 1 | (b & 0x55) << 1;
+        p[j] = b;
+    }
+}
+
+/* Return true if the specified sequence of bits, provided as a string in the
+ * form "11010110..." is found in the 'b' bitmap of 'blen' bits at 'bitpos'
+ * position. */
+bool bitmap_match_bits(uint8_t* b, uint32_t blen, uint32_t bitpos, const char* bits) {
+    for(size_t j = 0; bits[j]; j++) {
+        bool expected = (bits[j] == '1') ? true : false;
+        if(bitmap_get(b, blen, bitpos + j) != expected) return false;
+    }
+    return true;
+}
+
+/* Search for the specified bit sequence (see bitmap_match_bits() for details)
+ * in the bitmap 'b' of 'blen' bytes, looking forward at most 'maxbits' ahead.
+ * Returns the offset (in bits) of the match, or BITMAP_SEEK_NOT_FOUND if not
+ * found.
+ *
+ * Note: there are better algorithms, such as Boyer-Moore. Here we hope that
+ * for the kind of patterns we search we'll have a lot of early stops so
+ * we use a vanilla approach. */
+uint32_t bitmap_seek_bits(
+    uint8_t* b,
+    uint32_t blen,
+    uint32_t startpos,
+    uint32_t maxbits,
+    const char* bits) {
+    uint32_t endpos = startpos + blen * 8;
+    uint32_t end2 = startpos + maxbits;
+    if(end2 < endpos) endpos = end2;
+    for(uint32_t j = startpos; j < endpos; j++)
+        if(bitmap_match_bits(b, blen, j, bits)) return j;
+    return BITMAP_SEEK_NOT_FOUND;
+}
+
+/* Compare bitmaps b1 and b2 (possibly overlapping or the same bitmap),
+ * at the specified offsets, for cmplen bits. Returns true if the
+ * exact same bits are found, otherwise false. */
+bool bitmap_match_bitmap(
+    uint8_t* b1,
+    uint32_t b1len,
+    uint32_t b1off,
+    uint8_t* b2,
+    uint32_t b2len,
+    uint32_t b2off,
+    uint32_t cmplen) {
+    for(uint32_t j = 0; j < cmplen; j++) {
+        bool bit1 = bitmap_get(b1, b1len, b1off + j);
+        bool bit2 = bitmap_get(b2, b2len, b2off + j);
+        if(bit1 != bit2) return false;
+    }
+    return true;
+}
+
+/* Convert 'len' bitmap bits of the bitmap 'bitmap' into a null terminated
+ * string, stored at 'dst', that must have space at least for len+1 bytes.
+ * The bits are extracted from the specified offset. */
+void bitmap_to_string(char* dst, uint8_t* b, uint32_t blen, uint32_t off, uint32_t len) {
+    for(uint32_t j = 0; j < len; j++) dst[j] = bitmap_get(b, blen, off + j) ? '1' : '0';
+    dst[len] = 0;
+}
+
+/* Set the pattern 'pat' into the bitmap 'b' of max length 'blen' bytes,
+ * starting from the specified offset.
+ *
+ * The pattern is given as a string of 0s and 1s characters, like "01101001".
+ * This function is useful in order to set the test vectors in the protocol
+ * decoders, to see if the decoding works regardless of the fact we are able
+ * to actually receive a given signal. */
+void bitmap_set_pattern(uint8_t* b, uint32_t blen, uint32_t off, const char* pat) {
+    uint32_t i = 0;
+    while(pat[i]) {
+        bitmap_set(b, blen, i + off, pat[i] == '1');
+        i++;
+    }
+}
+
+/* Take the raw signal and turn it into a sequence of bits inside the
+ * buffer 'b'. Note that such 0s and 1s are NOT the actual data in the
+ * signal, but is just a low level representation of the line code. Basically
+ * if the short pulse we find in the signal is 320us, we convert high and
+ * low levels in the raw sample in this way:
+ *
+ * If for instance we see a high level lasting ~600 us, we will add
+ * two 1s bit. If then the signal goes down for 330us, we will add one zero,
+ * and so forth. So for each period of high and low we find the closest
+ * multiple and set the relevant number of bits.
+ * 
+ * In case of a short pulse of 320us detected, 320*2 is the closest to a
+ * high pulse of 600us, so 2 bits will be set.
+ *
+ * In other terms what this function does is sampling the signal at
+ * fixed 'rate' intervals.
+ *
+ * This representation makes it simple to decode the signal at a higher
+ * level later, translating it from Marshal coding or other line codes
+ * to the actual bits/bytes.
+ *
+ * The 'idx' argument marks the detected signal start index into the
+ * raw samples buffer. The 'count' tells the function how many raw
+ * samples to convert into bits. The function returns the number of
+ * bits set into the buffer 'b'. The 'rate' argument, in microseconds, is
+ * the detected short-pulse duration. We expect the line code to be
+ * meaningful when interpreted at multiples of 'rate'. */
+uint32_t convert_signal_to_bits(
+    uint8_t* b,
+    uint32_t blen,
+    RawSamplesBuffer* s,
+    uint32_t idx,
+    uint32_t count,
+    uint32_t rate) {
+    if(rate == 0) return 0; /* We can't perform the conversion. */
+    uint32_t bitpos = 0;
+    for(uint32_t j = 0; j < count; j++) {
+        uint32_t dur;
+        bool level;
+        raw_samples_get(s, j + idx, &level, &dur);
+
+        uint32_t numbits = dur / rate; /* full bits that surely fit. */
+        uint32_t rest = dur % rate; /* How much we are left with. */
+        if(rest > rate / 2) numbits++; /* There is another one. */
+
+        /* Limit how much a single sample can spawn. There are likely no
+         * protocols doing such long pulses when the rate is low. */
+        if(numbits > 1024) numbits = 1024;
+
+        if(0) /* Super verbose, so not under the DEBUG_MSG define. */
+            FURI_LOG_E(TAG, "%lu converted into %lu (%d) bits", dur, numbits, (int)level);
+
+        /* If the signal is too short, let's claim it an interference
+         * and ignore it completely. */
+        if(numbits == 0) continue;
+
+        while(numbits--) bitmap_set(b, blen, bitpos++, level);
+    }
+    return bitpos;
+}
+
+/* This function converts the line code used to the final data representation.
+ * The representation is put inside 'buf', for up to 'buflen' bytes of total
+ * data. For instance in order to convert manchester you can use "10" and "01"
+ * as zero and one patterns. However this function does not handle differential
+ * encodings. See below for convert_from_diff_manchester().
+ *
+ * The function returns the number of bits converted. It will stop as soon
+ * as it finds a pattern that does not match zero or one patterns, or when
+ * the end of the bitmap pointed by 'bits' is reached (the length is
+ * specified in bytes by the caller, via the 'len' parameters).
+ *
+ * The decoding starts at the specified offset (in bits) 'off'. */
+uint32_t convert_from_line_code(
+    uint8_t* buf,
+    uint64_t buflen,
+    uint8_t* bits,
+    uint32_t len,
+    uint32_t off,
+    const char* zero_pattern,
+    const char* one_pattern) {
+    uint32_t decoded = 0; /* Number of bits extracted. */
+    len *= 8; /* Convert bytes to bits. */
+    while(off < len) {
+        bool bitval;
+        if(bitmap_match_bits(bits, len, off, zero_pattern)) {
+            bitval = false;
+            off += strlen(zero_pattern);
+        } else if(bitmap_match_bits(bits, len, off, one_pattern)) {
+            bitval = true;
+            off += strlen(one_pattern);
+        } else {
+            break;
+        }
+        bitmap_set(buf, buflen, decoded++, bitval);
+        if(decoded / 8 == buflen) break; /* No space left on target buffer. */
+    }
+    return decoded;
+}
+
+/* Convert the differential Manchester code to bits. This is similar to
+ * convert_from_line_code() but specific for diff-Manchester. The user must
+ * supply the value of the previous symbol before this stream, since
+ * in differential codings the next bits depend on the previous one.
+ *
+ * Parameters and return values are like convert_from_line_code(). */
+uint32_t convert_from_diff_manchester(
+    uint8_t* buf,
+    uint64_t buflen,
+    uint8_t* bits,
+    uint32_t len,
+    uint32_t off,
+    bool previous) {
+    uint32_t decoded = 0;
+    len *= 8; /* Conver to bits. */
+    for(uint32_t j = off; j < len; j += 2) {
+        bool b0 = bitmap_get(bits, len, j);
+        bool b1 = bitmap_get(bits, len, j + 1);
+        if(b0 == previous) break; /* Each new bit must switch value. */
+        bitmap_set(buf, buflen, decoded++, b0 == b1);
+        previous = b1;
+        if(decoded / 8 == buflen) break; /* No space left on target buffer. */
+    }
+    return decoded;
+}
+
+/* Free the message info and allocated data. */
+void free_msg_info(ProtoViewMsgInfo* i) {
+    if(i == NULL) return;
+    fieldset_free(i->fieldset);
+    free(i->bits);
+    free(i);
+}
+
+/* Reset the message info structure before passing it to the decoding
+ * functions. */
+void init_msg_info(ProtoViewMsgInfo* i, ProtoViewApp* app) {
+    UNUSED(app);
+    memset(i, 0, sizeof(ProtoViewMsgInfo));
+    i->bits = NULL;
+    i->fieldset = fieldset_new();
+}
+
+/* This function is called when a new signal is detected. It converts it
+ * to a bitstream, and the calls the protocol specific functions for
+ * decoding. If the signal was decoded correctly by some protocol, true
+ * is returned. Otherwise false is returned. */
+bool decode_signal(RawSamplesBuffer* s, uint64_t len, ProtoViewMsgInfo* info) {
+    uint32_t bitmap_bits_size = 4096 * 8;
+    uint32_t bitmap_size = bitmap_bits_size / 8;
+
+    /* We call the decoders with an offset a few samples before the actual
+     * signal detected and for a len of a few bits after its end. */
+    uint32_t before_samples = 32;
+    uint32_t after_samples = 100;
+
+    uint8_t* bitmap = malloc(bitmap_size);
+    uint32_t bits = convert_signal_to_bits(
+        bitmap,
+        bitmap_size,
+        s,
+        -before_samples,
+        len + before_samples + after_samples,
+        s->short_pulse_dur);
+
+    if(DEBUG_MSG) { /* Useful for debugging purposes. Don't remove. */
+        char* str = malloc(1024);
+        uint32_t j;
+        for(j = 0; j < bits && j < 1023; j++) {
+            str[j] = bitmap_get(bitmap, bitmap_size, j) ? '1' : '0';
+        }
+        str[j] = 0;
+        FURI_LOG_E(TAG, "%lu bits sampled: %s", bits, str);
+        free(str);
+    }
+
+    /* Try all the decoders available. */
+    int j = 0;
+
+    bool decoded = false;
+    while(Decoders[j]) {
+        uint32_t start_time = furi_get_tick();
+        decoded = Decoders[j]->decode(bitmap, bitmap_size, bits, info);
+        uint32_t delta = furi_get_tick() - start_time;
+        FURI_LOG_E(TAG, "Decoder %s took %lu ms", Decoders[j]->name, (unsigned long)delta);
+        if(decoded) {
+            info->decoder = Decoders[j];
+            break;
+        }
+        j++;
+    }
+
+    if(!decoded) {
+        FURI_LOG_E(TAG, "No decoding possible");
+    } else {
+        FURI_LOG_E(TAG, "+++ Decoded %s", info->decoder->name);
+        /* The message was correctly decoded: fill the info structure
+         * with the decoded signal. The decoder may not implement offset/len
+         * filling of the structure. In such case we have no info and
+         * pulses_count will be set to zero. */
+        if(info->pulses_count) {
+            info->bits_bytes = (info->pulses_count + 7) / 8; // Round to full byte.
+            info->bits = malloc(info->bits_bytes);
+            bitmap_copy(
+                info->bits,
+                info->bits_bytes,
+                0,
+                bitmap,
+                bitmap_size,
+                info->start_off,
+                info->pulses_count);
+        }
+    }
+    free(bitmap);
+    return decoded;
+}

+ 138 - 0
main_apps_sources/protoview/signal_file.c

@@ -0,0 +1,138 @@
+/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved
+ * Copyright (C) 2023 Maciej Wojtasik -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+#include <stream/stream.h>
+#include <flipper_format/flipper_format_i.h>
+
+/* ========================= Signal file operations ========================= */
+
+/* This function saves the current logical signal on disk. What is saved here
+ * is not the signal as level and duration as we received it from CC1101,
+ * but it's logical representation stored in the app->msg_info bitmap, where
+ * each 1 or 0 means a puls or gap for the specified short pulse duration time
+ * (te). */
+bool save_signal(ProtoViewApp* app, const char* filename) {
+    /* We have a message at all? */
+    if(app->msg_info == NULL || app->msg_info->pulses_count == 0) return false;
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* file = flipper_format_file_alloc(storage);
+    Stream* stream = flipper_format_get_raw_stream(file);
+    FuriString* file_content = NULL;
+    bool success = true;
+
+    if(flipper_format_file_open_always(file, filename)) {
+        /* Write the file header. */
+        FuriString* file_content = furi_string_alloc();
+        const char* preset_id = ProtoViewModulations[app->modulation].id;
+
+        furi_string_printf(
+            file_content,
+            "Filetype: Flipper SubGhz RAW File\n"
+            "Version: 1\n"
+            "Frequency: %ld\n"
+            "Preset: %s\n",
+            app->frequency,
+            preset_id ? preset_id : "FuriHalSubGhzPresetCustom");
+
+        /* For custom modulations, we need to emit a set of registers. */
+        if(preset_id == NULL) {
+            FuriString* custom = furi_string_alloc();
+            uint8_t* regs = ProtoViewModulations[app->modulation].custom;
+            furi_string_printf(
+                custom,
+                "Custom_preset_module: CC1101\n"
+                "Custom_preset_data: ");
+            for(int j = 0; regs[j]; j += 2) {
+                furi_string_cat_printf(custom, "%02X %02X ", (int)regs[j], (int)regs[j + 1]);
+            }
+            // Add patable
+            furi_string_cat(custom, "00 00 C0 00 00 00 00 00 00 00 ");
+            //size_t len = furi_string_size(file_content);
+            //furi_string_set_char(custom, len - 1, '\n');
+            furi_string_cat(custom, "\n");
+            furi_string_cat(file_content, custom);
+            furi_string_free(custom);
+        }
+
+        /* We always save raw files. */
+        furi_string_cat_printf(
+            file_content,
+            "Protocol: RAW\n"
+            "RAW_Data: -10000\n"); // Start with 10 ms of gap
+
+        /* Write header. */
+        size_t len = furi_string_size(file_content);
+        if(stream_write(stream, (uint8_t*)furi_string_get_cstr(file_content), len) != len) {
+            FURI_LOG_W(TAG, "Short write to file");
+            success = false;
+            goto write_err;
+        }
+        furi_string_reset(file_content);
+
+        /* Write raw data sections. The Flipper subghz parser can't handle
+         * too much data on a single line, so we generate a new one
+         * every few samples. */
+        uint32_t this_line_samples = 0;
+        uint32_t max_line_samples = 100;
+        uint32_t idx = 0; // Iindex in the signal bitmap.
+        ProtoViewMsgInfo* i = app->msg_info;
+        while(idx < i->pulses_count) {
+            bool level = bitmap_get(i->bits, i->bits_bytes, idx);
+            uint32_t te_times = 1;
+            idx++;
+            /* Count the duration of the current pulse/gap. */
+            while(idx < i->pulses_count && bitmap_get(i->bits, i->bits_bytes, idx) == level) {
+                te_times++;
+                idx++;
+            }
+            // Invariant: after the loop 'idx' is at the start of the
+            // next gap or pulse.
+
+            int32_t dur = (int32_t)i->short_pulse_dur * te_times;
+            if(level == 0) dur = -dur; /* Negative is gap in raw files. */
+
+            /* Emit the sample. If this is the first sample of the line,
+             * also emit the RAW_Data: field. */
+            if(this_line_samples == 0) furi_string_cat_printf(file_content, "RAW_Data: ");
+            furi_string_cat_printf(file_content, "%d ", (int)dur);
+            this_line_samples++;
+
+            /* Store the current set of samples on disk, when we reach a
+             * given number or the end of the signal. */
+            bool end_reached = (idx == i->pulses_count);
+            if(this_line_samples == max_line_samples || end_reached) {
+                /* If that's the end, terminate the signal with a long
+                 * gap. */
+                if(end_reached) furi_string_cat_printf(file_content, "-10000 ");
+
+                /* We always have a trailing space in the last sample. Make it
+                 * a newline. */
+                size_t len = furi_string_size(file_content);
+                furi_string_set_char(file_content, len - 1, '\n');
+
+                if(stream_write(stream, (uint8_t*)furi_string_get_cstr(file_content), len) !=
+                   len) {
+                    FURI_LOG_W(TAG, "Short write to file");
+                    success = false;
+                    goto write_err;
+                }
+
+                /* Prepare for next line. */
+                furi_string_reset(file_content);
+                this_line_samples = 0;
+            }
+        }
+    } else {
+        success = false;
+        FURI_LOG_W(TAG, "Unable to open file");
+    }
+
+write_err:
+    furi_record_close(RECORD_STORAGE);
+    flipper_format_free(file);
+    if(file_content != NULL) furi_string_free(file_content);
+    return success;
+}

+ 145 - 0
main_apps_sources/protoview/ui.c

@@ -0,0 +1,145 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+
+/* =========================== Subview handling ================================
+ * Note that these are not the Flipper subviews, but the subview system
+ * implemented inside ProtoView.
+ * ========================================================================== */
+
+/* Return the ID of the currently selected subview, of the current
+ * view. */
+int ui_get_current_subview(ProtoViewApp* app) {
+    return app->current_subview[app->current_view];
+}
+
+/* Called by view rendering callback that has subviews, to show small triangles
+ * facing down/up if there are other subviews the user can access with up
+ * and down. */
+void ui_show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview) {
+    int subview = ui_get_current_subview(app);
+    if(subview != 0) canvas_draw_triangle(canvas, 120, 5, 8, 5, CanvasDirectionBottomToTop);
+    if(subview != last_subview - 1)
+        canvas_draw_triangle(canvas, 120, 59, 8, 5, CanvasDirectionTopToBottom);
+}
+
+/* Handle up/down keys when we are in a subview. If the function catched
+ * such keypress, it returns true, so that the actual view input callback
+ * knows it can just return ASAP without doing anything. */
+bool ui_process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subview) {
+    int subview = ui_get_current_subview(app);
+    if(input.type == InputTypePress) {
+        if(input.key == InputKeyUp) {
+            if(subview != 0) app->current_subview[app->current_view]--;
+            return true;
+        } else if(input.key == InputKeyDown) {
+            if(subview != last_subview - 1) app->current_subview[app->current_view]++;
+            return true;
+        }
+    }
+    return false;
+}
+
+/* ============================= Text input ====================================
+ * Normally we just use our own private UI widgets. However for the text input
+ * widget, that is quite complex, visualizes a keyboard and must be standardized
+ * for user coherent experience, we use the one provided by the Flipper
+ * framework. The following two functions allow to show the keyboard to get
+ * text and later dismiss it.
+ * ========================================================================== */
+
+/* Show the keyboard, take the user input and store it into the specified
+ * 'buffer' of 'buflen' total bytes. When the user is done, the done_callback
+ * is called passing the application context to it. Such callback needs
+ * to do whatever it wants with the input buffer and dismissi the keyboard
+ * calling: dismiss_keyboard(app);
+ *
+ * Note: if the buffer is not a null-termined zero string, what it contains will
+ * be used as initial input for the user. */
+void ui_show_keyboard(
+    ProtoViewApp* app,
+    char* buffer,
+    uint32_t buflen,
+    void (*done_callback)(void*)) {
+    app->show_text_input = true;
+    app->text_input_buffer = buffer;
+    app->text_input_buffer_len = buflen;
+    app->text_input_done_callback = done_callback;
+}
+
+void ui_dismiss_keyboard(ProtoViewApp* app) {
+    view_dispatcher_stop(app->view_dispatcher);
+}
+
+/* ================================= Alert ================================== */
+
+/* Set an alert message to be shown over any currently active view, for
+ * the specified amount of time of 'ttl' milliseconds. */
+void ui_show_alert(ProtoViewApp* app, const char* text, uint32_t ttl) {
+    app->alert_dismiss_time = furi_get_tick() + furi_ms_to_ticks(ttl);
+    snprintf(app->alert_text, ALERT_MAX_LEN, "%s", text);
+}
+
+/* Cancel the alert before its time has elapsed. */
+void ui_dismiss_alert(ProtoViewApp* app) {
+    app->alert_dismiss_time = 0;
+}
+
+/* Show the alert if an alert is set. This is called after the currently
+ * active view displayed its stuff, so we overwrite the screen with the
+ * alert message. */
+void ui_draw_alert_if_needed(Canvas* canvas, ProtoViewApp* app) {
+    if(app->alert_dismiss_time == 0) {
+        /* No active alert. */
+        return;
+    } else if(app->alert_dismiss_time < furi_get_tick()) {
+        /* Alert just expired. */
+        ui_dismiss_alert(app);
+        return;
+    }
+
+    /* Show the alert. A box with black border and a text inside. */
+    canvas_set_font(canvas, FontPrimary);
+    uint8_t w = canvas_string_width(canvas, app->alert_text);
+    uint8_t h = 8; // Font height.
+    uint8_t text_x = 64 - (w / 2);
+    uint8_t text_y = 32 + 4;
+    uint8_t padding = 3;
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_box(
+        canvas, text_x - padding, text_y - padding - h, w + padding * 2, h + padding * 2);
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(
+        canvas,
+        text_x - padding + 1,
+        text_y - padding - h + 1,
+        w + padding * 2 - 2,
+        h + padding * 2 - 2);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_str(canvas, text_x, text_y, app->alert_text);
+}
+
+/* =========================== Canvas extensions ============================ */
+
+void canvas_draw_str_with_border(
+    Canvas* canvas,
+    uint8_t x,
+    uint8_t y,
+    const char* str,
+    Color text_color,
+    Color border_color) {
+    struct {
+        uint8_t x;
+        uint8_t y;
+    } dir[8] = {{-1, -1}, {0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}};
+
+    /* Rotate in all the directions writing the same string to create a
+     * border, then write the actual string in the other color in the
+     * middle. */
+    canvas_set_color(canvas, border_color);
+    for(int j = 0; j < 8; j++) canvas_draw_str(canvas, x + dir[j].x, y + dir[j].y, str);
+    canvas_set_color(canvas, text_color);
+    canvas_draw_str(canvas, x, y, str);
+    canvas_set_color(canvas, ColorBlack);
+}

+ 248 - 0
main_apps_sources/protoview/view_build.c

@@ -0,0 +1,248 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+
+extern ProtoViewDecoder* Decoders[]; // Defined in signal.c.
+
+/* Our view private data. */
+#define USER_VALUE_LEN 64
+typedef struct {
+    ProtoViewDecoder* decoder; /* Decoder we are using to create a
+                                       message. */
+    uint32_t cur_decoder; /* Decoder index when we are yet selecting
+                                       a decoder. Used when decoder is NULL. */
+    ProtoViewFieldSet* fieldset; /* The fields to populate. */
+    uint32_t cur_field; /* Field we are editing right now. This
+                                       is the index inside the 'fieldset'
+                                       fields. */
+    char* user_value; /* Keyboard input to replace the current
+                                       field value goes here. */
+} BuildViewPrivData;
+
+/* Not all the decoders support message bulding, so we can't just
+ * increment / decrement the cur_decoder index here. */
+static void select_next_decoder(ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    do {
+        privdata->cur_decoder++;
+        if(Decoders[privdata->cur_decoder] == NULL) privdata->cur_decoder = 0;
+    } while(Decoders[privdata->cur_decoder]->get_fields == NULL);
+}
+
+/* Like select_next_decoder() but goes backward. */
+static void select_prev_decoder(ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    do {
+        if(privdata->cur_decoder == 0) {
+            /* Go one after the last one to wrap around. */
+            while(Decoders[privdata->cur_decoder]) privdata->cur_decoder++;
+        }
+        privdata->cur_decoder--;
+    } while(Decoders[privdata->cur_decoder]->get_fields == NULL);
+}
+
+/* Render the view to select the decoder, among the ones that
+ * support message building. */
+static void render_view_select_decoder(Canvas* const canvas, ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 9, "Signal creator");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 19, "up/down: select, ok: choose");
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(
+        canvas, 64, 38, AlignCenter, AlignCenter, Decoders[privdata->cur_decoder]->name);
+}
+
+/* Render the view that allows the user to populate the fields needed
+ * for the selected decoder to build a message. */
+static void render_view_set_fields(Canvas* const canvas, ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    char buf[32];
+    snprintf(
+        buf,
+        sizeof(buf),
+        "%s field %d/%d",
+        privdata->decoder->name,
+        (int)privdata->cur_field + 1,
+        (int)privdata->fieldset->numfields);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_box(canvas, 0, 0, 128, 21);
+    canvas_set_color(canvas, ColorWhite);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 1, 9, buf);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 1, 19, "up/down: next field, ok: edit");
+
+    /* Write the field name, type, current content. */
+    canvas_set_color(canvas, ColorBlack);
+    ProtoViewField* field = privdata->fieldset->fields[privdata->cur_field];
+    snprintf(
+        buf, sizeof(buf), "%s %s:%d", field->name, field_get_type_name(field), (int)field->len);
+    buf[0] = toupper(buf[0]);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64, 30, AlignCenter, AlignCenter, buf);
+    canvas_set_font(canvas, FontSecondary);
+
+    /* Render the current value between "" */
+    unsigned int written = (unsigned int)field_to_string(buf + 1, sizeof(buf) - 1, field);
+    buf[0] = '"';
+    if(written + 3 < sizeof(buf)) memcpy(buf + written + 1, "\"\x00", 2);
+    canvas_draw_str_aligned(canvas, 63, 45, AlignCenter, AlignCenter, buf);
+
+    /* Footer instructions. */
+    canvas_draw_str(canvas, 0, 62, "Long ok: create, < > incr/decr");
+}
+
+/* Render the build message view. */
+void render_view_build_message(Canvas* const canvas, ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+
+    if(privdata->decoder)
+        render_view_set_fields(canvas, app);
+    else
+        render_view_select_decoder(canvas, app);
+}
+
+/* Handle input for the decoder selection. */
+static void process_input_select_decoder(ProtoViewApp* app, InputEvent input) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    if(input.type == InputTypeShort) {
+        if(input.key == InputKeyOk) {
+            privdata->decoder = Decoders[privdata->cur_decoder];
+            privdata->fieldset = fieldset_new();
+            privdata->decoder->get_fields(privdata->fieldset);
+
+            /* If the currently decoded message was produced with the
+             * same decoder the user selected, let's populate the
+             * defaults with the current values. So the user will
+             * actaully edit the current message. */
+            if(app->signal_decoded && app->msg_info->decoder == privdata->decoder) {
+                fieldset_copy_matching_fields(privdata->fieldset, app->msg_info->fieldset);
+            }
+
+            /* Now we use the subview system in order to protect the
+               message editing mode from accidental < or > presses.
+               Since we are technically into a subview now, we'll have
+               control of < and >. */
+            InputEvent ii = {.type = InputTypePress, .key = InputKeyDown};
+            ui_process_subview_updown(app, ii, 2);
+        } else if(input.key == InputKeyDown) {
+            select_next_decoder(app);
+        } else if(input.key == InputKeyUp) {
+            select_prev_decoder(app);
+        }
+    }
+}
+
+/* Called after the user typed the new field value in the keyboard.
+ * Let's save it and remove the keyboard view. */
+static void text_input_done_callback(void* context) {
+    ProtoViewApp* app = context;
+    BuildViewPrivData* privdata = app->view_privdata;
+
+    if(field_set_from_string(
+           privdata->fieldset->fields[privdata->cur_field],
+           privdata->user_value,
+           strlen(privdata->user_value)) == false) {
+        ui_show_alert(app, "Invalid value", 1500);
+    }
+
+    free(privdata->user_value);
+    privdata->user_value = NULL;
+    ui_dismiss_keyboard(app);
+}
+
+/* Handles the effects of < and > keys in field editing mode.
+ * Instead of force the user to enter the text input mode, delete
+ * the old value, enter the one, we allow to increment and
+ * decrement the current field in a much simpler way.
+ *
+ * The current filed is changed by 'incr' amount. */
+static bool increment_current_field(ProtoViewApp* app, int incr) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    ProtoViewFieldSet* fs = privdata->fieldset;
+    ProtoViewField* f = fs->fields[privdata->cur_field];
+    return field_incr_value(f, incr);
+}
+
+/* Handle input for fields editing mode. */
+static void process_input_set_fields(ProtoViewApp* app, InputEvent input) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    ProtoViewFieldSet* fs = privdata->fieldset;
+
+    if(input.type == InputTypeShort && input.key == InputKeyOk) {
+        /* Show the keyboard to let the user type the new
+         * value. */
+        if(privdata->user_value == NULL) privdata->user_value = malloc(USER_VALUE_LEN);
+        field_to_string(privdata->user_value, USER_VALUE_LEN, fs->fields[privdata->cur_field]);
+        ui_show_keyboard(app, privdata->user_value, USER_VALUE_LEN, text_input_done_callback);
+    } else if(input.type == InputTypeShort && input.key == InputKeyDown) {
+        privdata->cur_field = (privdata->cur_field + 1) % fs->numfields;
+    } else if(input.type == InputTypeShort && input.key == InputKeyUp) {
+        if(privdata->cur_field == 0)
+            privdata->cur_field = fs->numfields - 1;
+        else
+            privdata->cur_field--;
+    } else if(input.type == InputTypeShort && input.key == InputKeyRight) {
+        increment_current_field(app, 1);
+    } else if(input.type == InputTypeShort && input.key == InputKeyLeft) {
+        increment_current_field(app, -1);
+    } else if(input.type == InputTypeRepeat && input.key == InputKeyRight) {
+        // The reason why we don't use a large increment directly
+        // is that certain field types only support +1 -1 increments.
+        int times = 10;
+        while(times--) increment_current_field(app, 1);
+    } else if(input.type == InputTypeRepeat && input.key == InputKeyLeft) {
+        int times = 10;
+        while(times--) increment_current_field(app, -1);
+    } else if(input.type == InputTypeLong && input.key == InputKeyOk) {
+        // Build the message in a fresh raw buffer.
+        if(privdata->decoder->build_message) {
+            RawSamplesBuffer* rs = raw_samples_alloc();
+            privdata->decoder->build_message(rs, privdata->fieldset);
+            app->signal_decoded = false; // So that the new signal will be
+                // accepted as the current signal.
+            scan_for_signal(app, rs, 5);
+            raw_samples_free(rs);
+            ui_show_alert(app, "Done: press back key", 3000);
+        }
+    }
+}
+
+/* Handle input for the build message view. */
+void process_input_build_message(ProtoViewApp* app, InputEvent input) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    if(privdata->decoder)
+        process_input_set_fields(app, input);
+    else
+        process_input_select_decoder(app, input);
+}
+
+/* Enter view callback. */
+void view_enter_build_message(ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+
+    // When we enter the view, the current decoder is just set to zero.
+    // Seek the next valid if needed.
+    if(Decoders[privdata->cur_decoder]->get_fields == NULL) {
+        select_next_decoder(app);
+    }
+
+    // However if there is currently a decoded message, and the
+    // decoder of such message supports message building, let's
+    // select it.
+    if(app->signal_decoded && app->msg_info->decoder->get_fields &&
+       app->msg_info->decoder->build_message) {
+        while(Decoders[privdata->cur_decoder] != app->msg_info->decoder) select_next_decoder(app);
+    }
+}
+
+/* Called on exit for cleanup. */
+void view_exit_build_message(ProtoViewApp* app) {
+    BuildViewPrivData* privdata = app->view_privdata;
+    if(privdata->fieldset) fieldset_free(privdata->fieldset);
+    if(privdata->user_value) free(privdata->user_value);
+}

+ 173 - 0
main_apps_sources/protoview/view_direct_sampling.c

@@ -0,0 +1,173 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+#include <cc1101_regs.h>
+
+static void direct_sampling_timer_start(ProtoViewApp* app);
+static void direct_sampling_timer_stop(ProtoViewApp* app);
+
+#define CAPTURED_BITMAP_BITS (128 * 64)
+#define CAPTURED_BITMAP_BYTES (CAPTURED_BITMAP_BITS / 8)
+#define DEFAULT_USEC_PER_PIXEL 50
+#define USEC_PER_PIXEL_SMALL_CHANGE 5
+#define USEC_PER_PIXEL_LARGE_CHANGE 25
+#define USEC_PER_PIXEL_MIN 5
+#define USEC_PER_PIXEL_MAX 300
+typedef struct {
+    uint8_t* captured; // Bitmap with the last captured screen.
+    uint32_t captured_idx; // Current index to write into the bitmap
+    uint32_t usec_per_pixel; // Number of useconds a pixel should represent
+    bool show_usage_info;
+} DirectSamplingViewPrivData;
+
+/* Read directly from the G0 CC1101 pin, and draw a black or white
+ * dot depending on the level. */
+void render_view_direct_sampling(Canvas* const canvas, ProtoViewApp* app) {
+    DirectSamplingViewPrivData* privdata = app->view_privdata;
+
+    if(!app->direct_sampling_enabled && privdata->show_usage_info) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 2, 9, "Direct sampling displays the");
+        canvas_draw_str(canvas, 2, 18, "the captured signal in real");
+        canvas_draw_str(canvas, 2, 27, "time, like in a CRT TV set.");
+        canvas_draw_str(canvas, 2, 36, "Use UP/DOWN to change the");
+        canvas_draw_str(canvas, 2, 45, "resolution (usec/pixel).");
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 5, 60, "To start/stop, press OK");
+        return;
+    }
+    privdata->show_usage_info = false;
+
+    /* Draw on screen. */
+    int idx = 0;
+    for(int y = 0; y < 64; y++) {
+        for(int x = 0; x < 128; x++) {
+            bool level = bitmap_get(privdata->captured, CAPTURED_BITMAP_BYTES, idx++);
+            if(level) canvas_draw_dot(canvas, x, y);
+        }
+    }
+
+    char buf[32];
+    snprintf(buf, sizeof(buf), "%lu usec/px", privdata->usec_per_pixel);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_with_border(canvas, 1, 60, buf, ColorWhite, ColorBlack);
+}
+
+/* Handle input */
+void process_input_direct_sampling(ProtoViewApp* app, InputEvent input) {
+    DirectSamplingViewPrivData* privdata = app->view_privdata;
+
+    if(input.type == InputTypePress && input.key == InputKeyOk) {
+        app->direct_sampling_enabled = !app->direct_sampling_enabled;
+    }
+
+    if((input.key == InputKeyUp || input.key == InputKeyDown) &&
+       (input.type == InputTypePress || input.type == InputTypeRepeat)) {
+        uint32_t change = input.type == InputTypePress ? USEC_PER_PIXEL_SMALL_CHANGE :
+                                                         USEC_PER_PIXEL_LARGE_CHANGE;
+        if(input.key == InputKeyUp) change = -change;
+        privdata->usec_per_pixel += change;
+        if(privdata->usec_per_pixel < USEC_PER_PIXEL_MIN)
+            privdata->usec_per_pixel = USEC_PER_PIXEL_MIN;
+        else if(privdata->usec_per_pixel > USEC_PER_PIXEL_MAX)
+            privdata->usec_per_pixel = USEC_PER_PIXEL_MAX;
+        /* Update the timer frequency. */
+        direct_sampling_timer_stop(app);
+        direct_sampling_timer_start(app);
+    }
+}
+
+/* Enter view. Stop the subghz thread to prevent access as we read
+ * the CC1101 data directly. */
+void view_enter_direct_sampling(ProtoViewApp* app) {
+    /* Set view defaults. */
+    DirectSamplingViewPrivData* privdata = app->view_privdata;
+    privdata->usec_per_pixel = DEFAULT_USEC_PER_PIXEL;
+    privdata->captured = malloc(CAPTURED_BITMAP_BYTES);
+    privdata->show_usage_info = true;
+
+    if(app->txrx->txrx_state == TxRxStateRx && !app->txrx->debug_timer_sampling) {
+        subghz_devices_stop_async_rx(app->radio_device);
+
+        /* To read data asynchronously directly from the view, we need
+         * to put the CC1101 back into reception mode (the previous call
+         * to stop the async RX will put it into idle) and configure the
+         * G0 pin for reading. */
+        subghz_devices_set_rx(app->radio_device);
+        furi_hal_gpio_init(
+            subghz_devices_get_data_gpio(app->radio_device),
+            GpioModeInput,
+            GpioPullNo,
+            GpioSpeedLow);
+    } else {
+        raw_sampling_worker_stop(app);
+    }
+
+    // Start the timer to capture raw data
+    direct_sampling_timer_start(app);
+}
+
+/* Exit view. Restore the subghz thread. */
+void view_exit_direct_sampling(ProtoViewApp* app) {
+    DirectSamplingViewPrivData* privdata = app->view_privdata;
+    if(privdata->captured) free(privdata->captured);
+    app->direct_sampling_enabled = false;
+
+    direct_sampling_timer_stop(app);
+
+    /* Restart normal data feeding. */
+    if(app->txrx->txrx_state == TxRxStateRx && !app->txrx->debug_timer_sampling) {
+        subghz_devices_start_async_rx(app->radio_device, protoview_rx_callback, NULL);
+    } else {
+        furi_hal_gpio_init(
+            subghz_devices_get_data_gpio(app->radio_device),
+            GpioModeInput,
+            GpioPullNo,
+            GpioSpeedLow);
+        raw_sampling_worker_start(app);
+    }
+}
+
+/* =========================== Timer implementation ========================= */
+
+static void ds_timer_isr(void* ctx) {
+    ProtoViewApp* app = ctx;
+    DirectSamplingViewPrivData* privdata = app->view_privdata;
+
+    if(app->direct_sampling_enabled) {
+        bool level = furi_hal_gpio_read(subghz_devices_get_data_gpio(app->radio_device));
+        bitmap_set(privdata->captured, CAPTURED_BITMAP_BYTES, privdata->captured_idx, level);
+        privdata->captured_idx = (privdata->captured_idx + 1) % CAPTURED_BITMAP_BITS;
+    }
+    LL_TIM_ClearFlag_UPDATE(TIM2);
+}
+
+static void direct_sampling_timer_start(ProtoViewApp* app) {
+    DirectSamplingViewPrivData* privdata = app->view_privdata;
+
+    furi_hal_bus_enable(FuriHalBusTIM2);
+
+    LL_TIM_InitTypeDef tim_init = {
+        .Prescaler = 63, /* CPU frequency is ~64Mhz. */
+        .CounterMode = LL_TIM_COUNTERMODE_UP,
+        .Autoreload = privdata->usec_per_pixel};
+
+    LL_TIM_Init(TIM2, &tim_init);
+    LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
+    LL_TIM_DisableCounter(TIM2);
+    LL_TIM_SetCounter(TIM2, 0);
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, ds_timer_isr, app);
+    LL_TIM_EnableIT_UPDATE(TIM2);
+    LL_TIM_EnableCounter(TIM2);
+}
+
+static void direct_sampling_timer_stop(ProtoViewApp* app) {
+    UNUSED(app);
+    FURI_CRITICAL_ENTER();
+    LL_TIM_DisableCounter(TIM2);
+    LL_TIM_DisableIT_UPDATE(TIM2);
+    furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL);
+    furi_hal_bus_disable(FuriHalBusTIM2);
+    FURI_CRITICAL_EXIT();
+}

+ 332 - 0
main_apps_sources/protoview/view_info.c

@@ -0,0 +1,332 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+#include <gui/view.h>
+#include <lib/toolbox/random_name.h>
+
+/* This view has subviews accessible navigating up/down. This
+ * enumaration is used to track the currently active subview. */
+enum {
+    SubViewInfoMain,
+    SubViewInfoSave,
+    SubViewInfoLast, /* Just a sentinel. */
+};
+
+/* Our view private data. */
+#define SAVE_FILENAME_LEN 64
+typedef struct {
+    /* Our save view displays an oscilloscope-alike resampled signal,
+     * so that the user can see what they are saving. With left/right
+     * you can move to next rows. Here we store where we are. */
+    uint32_t signal_display_start_row;
+    char* filename;
+    uint8_t cur_info_page; // Info page to display. Useful when there are
+        // too many fields populated by the decoder that
+        // a single page is not enough.
+} InfoViewPrivData;
+
+/* Draw the text label and value of the specified info field at x,y. */
+static void render_info_field(Canvas* const canvas, ProtoViewField* f, uint8_t x, uint8_t y) {
+    char buf[64];
+    char strval[32];
+
+    field_to_string(strval, sizeof(strval), f);
+    snprintf(buf, sizeof(buf), "%s: %s", f->name, strval);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, x, y, buf);
+}
+
+/* Render the view with the detected message information. */
+#define INFO_LINES_PER_PAGE 5
+static void render_subview_main(Canvas* const canvas, ProtoViewApp* app) {
+    InfoViewPrivData* privdata = app->view_privdata;
+    uint8_t pages =
+        (app->msg_info->fieldset->numfields + (INFO_LINES_PER_PAGE - 1)) / INFO_LINES_PER_PAGE;
+    privdata->cur_info_page %= pages;
+    uint8_t current_page = privdata->cur_info_page;
+    char buf[32];
+
+    /* Protocol name as title. */
+    canvas_set_font(canvas, FontPrimary);
+    uint8_t y = 8, lineheight = 10;
+
+    if(pages > 1) {
+        snprintf(
+            buf, sizeof(buf), "%s %u/%u", app->msg_info->decoder->name, current_page + 1, pages);
+        canvas_draw_str(canvas, 0, y, buf);
+    } else {
+        canvas_draw_str(canvas, 0, y, app->msg_info->decoder->name);
+    }
+    y += lineheight;
+
+    /* Draw the info fields. */
+    uint8_t max_lines = INFO_LINES_PER_PAGE;
+    uint32_t j = current_page * max_lines;
+    while(j < app->msg_info->fieldset->numfields) {
+        render_info_field(canvas, app->msg_info->fieldset->fields[j++], 0, y);
+        y += lineheight;
+        if(--max_lines == 0) break;
+    }
+
+    /* Draw a vertical "save" label. Temporary solution, to switch to
+     * something better ASAP. */
+    y = 37;
+    lineheight = 7;
+    canvas_draw_str(canvas, 119, y, "s");
+    y += lineheight;
+    canvas_draw_str(canvas, 119, y, "a");
+    y += lineheight;
+    canvas_draw_str(canvas, 119, y, "v");
+    y += lineheight;
+    canvas_draw_str(canvas, 119, y, "e");
+    y += lineheight;
+}
+
+/* Render view with save option. */
+static void render_subview_save(Canvas* const canvas, ProtoViewApp* app) {
+    InfoViewPrivData* privdata = app->view_privdata;
+
+    /* Display our signal in digital form: here we don't show the
+     * signal with the exact timing of the received samples, but as it
+     * is in its logic form, in exact multiples of the short pulse length. */
+    uint8_t rows = 6;
+    uint8_t rowheight = 11;
+    uint8_t bitwidth = 4;
+    uint8_t bitheight = 5;
+    uint32_t idx = privdata->signal_display_start_row * (128 / 4);
+    bool prevbit = false;
+    for(uint8_t y = bitheight + 12; y <= rows * rowheight; y += rowheight) {
+        for(uint8_t x = 0; x < 128; x += 4) {
+            bool bit = bitmap_get(app->msg_info->bits, app->msg_info->bits_bytes, idx);
+            uint8_t prevy = y + prevbit * (bitheight * -1) - 1;
+            uint8_t thisy = y + bit * (bitheight * -1) - 1;
+            canvas_draw_line(canvas, x, prevy, x, thisy);
+            canvas_draw_line(canvas, x, thisy, x + bitwidth - 1, thisy);
+            prevbit = bit;
+            if(idx >= app->msg_info->pulses_count) {
+                canvas_set_color(canvas, ColorWhite);
+                canvas_draw_dot(canvas, x + 1, thisy);
+                canvas_draw_dot(canvas, x + 3, thisy);
+                canvas_set_color(canvas, ColorBlack);
+            }
+            idx++; // Draw next bit
+        }
+    }
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 6, "ok: send, long ok: save");
+}
+
+/* Render the selected subview of this view. */
+void render_view_info(Canvas* const canvas, ProtoViewApp* app) {
+    if(app->signal_decoded == false) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 30, 36, "No signal decoded");
+        return;
+    }
+
+    ui_show_available_subviews(canvas, app, SubViewInfoLast);
+    switch(app->current_subview[app->current_view]) {
+    case SubViewInfoMain:
+        render_subview_main(canvas, app);
+        break;
+    case SubViewInfoSave:
+        render_subview_save(canvas, app);
+        break;
+    }
+}
+
+/* The user typed the file name. Let's save it and remove the keyboard
+ * view. */
+static void text_input_done_callback(void* context) {
+    ProtoViewApp* app = context;
+    InfoViewPrivData* privdata = app->view_privdata;
+
+    FuriString* save_path =
+        furi_string_alloc_printf("%s/%s.sub", EXT_PATH("subghz"), privdata->filename);
+    save_signal(app, furi_string_get_cstr(save_path));
+    furi_string_free(save_path);
+
+    free(privdata->filename);
+    privdata->filename = NULL; // Don't free it again on view exit
+    ui_dismiss_keyboard(app);
+    ui_show_alert(app, "Signal saved", 1500);
+}
+
+/* Replace all the occurrences of character c1 with c2 in the specified
+ * string. */
+void str_replace(char* buf, char c1, char c2) {
+    char* p = buf;
+    while(*p) {
+        if(*p == c1) *p = c2;
+        p++;
+    }
+}
+
+/* Set a random filename the user can edit. */
+void set_signal_random_filename(ProtoViewApp* app, char* buf, size_t buflen) {
+    char suffix[6];
+    set_random_name(suffix, sizeof(suffix));
+    snprintf(buf, buflen, "%.10s-%s-%d", app->msg_info->decoder->name, suffix, rand() % 1000);
+    str_replace(buf, ' ', '_');
+    str_replace(buf, '-', '_');
+    str_replace(buf, '/', '_');
+}
+
+/* ========================== Signal transmission =========================== */
+
+/* This is the context we pass to the data yield callback for
+ * asynchronous tx. */
+typedef enum {
+    SendSignalSendStartGap,
+    SendSignalSendBits,
+    SendSignalSendEndGap,
+    SendSignalEndTransmission
+} SendSignalState;
+
+#define PROTOVIEW_SENDSIGNAL_START_GAP 10000 /* microseconds. */
+#define PROTOVIEW_SENDSIGNAL_END_GAP 10000 /* microseconds. */
+
+typedef struct {
+    SendSignalState state; // Current state.
+    uint32_t curpos; // Current bit position of data to send.
+    ProtoViewApp* app; // App reference.
+    uint32_t start_gap_dur; // Gap to send at the start.
+    uint32_t end_gap_dur; // Gap to send at the end.
+} SendSignalCtx;
+
+/* Setup the state context for the callback responsible to feed data
+ * to the subghz async tx system. */
+static void send_signal_init(SendSignalCtx* ss, ProtoViewApp* app) {
+    ss->state = SendSignalSendStartGap;
+    ss->curpos = 0;
+    ss->app = app;
+    ss->start_gap_dur = PROTOVIEW_SENDSIGNAL_START_GAP;
+    ss->end_gap_dur = PROTOVIEW_SENDSIGNAL_END_GAP;
+}
+
+/* Send signal data feeder callback. When the asynchronous transmission is
+ * active, this function is called to return new samples from the currently
+ * decoded signal in app->msg_info. The subghz subsystem aspects this function,
+ * that is the data feeder, to return LevelDuration types (that is a structure
+ * with level, that is pulse or gap, and duration in microseconds).
+ *
+ * The position into the transmission is stored in the context 'ctx', that
+ * references a SendSignalCtx structure.
+ *
+ * In the SendSignalCtx structure 'ss' we remember at which bit of the
+ * message we are, in ss->curoff. We also send a start and end gap in order
+ * to make sure the transmission is clear.
+ */
+LevelDuration radio_tx_feed_data(void* ctx) {
+    SendSignalCtx* ss = ctx;
+
+    /* Send start gap. */
+    if(ss->state == SendSignalSendStartGap) {
+        ss->state = SendSignalSendBits;
+        return level_duration_make(0, ss->start_gap_dur);
+    }
+
+    /* Send data. */
+    if(ss->state == SendSignalSendBits) {
+        uint32_t dur = 0, j;
+        uint32_t level = 0;
+
+        /* Let's see how many consecutive bits we have with the same
+         * level. */
+        for(j = 0; ss->curpos + j < ss->app->msg_info->pulses_count; j++) {
+            uint32_t l =
+                bitmap_get(ss->app->msg_info->bits, ss->app->msg_info->bits_bytes, ss->curpos + j);
+            if(j == 0) {
+                /* At the first bit of this sequence, we store the
+                 * level of the sequence. */
+                level = l;
+                dur += ss->app->msg_info->short_pulse_dur;
+                continue;
+            }
+
+            /* As long as the level is the same, we update the duration.
+             * Otherwise stop the loop and return this sample. */
+            if(l != level) break;
+            dur += ss->app->msg_info->short_pulse_dur;
+        }
+        ss->curpos += j;
+
+        /* If this was the last set of bits, change the state to
+         * send the final gap. */
+        if(ss->curpos >= ss->app->msg_info->pulses_count) ss->state = SendSignalSendEndGap;
+        return level_duration_make(level, dur);
+    }
+
+    /* Send end gap. */
+    if(ss->state == SendSignalSendEndGap) {
+        ss->state = SendSignalEndTransmission;
+        return level_duration_make(0, ss->end_gap_dur);
+    }
+
+    /* End transmission. Here state is guaranteed
+     * to be SendSignalEndTransmission */
+    return level_duration_reset();
+}
+
+/* Vibrate and produce a click sound when a signal is sent. */
+void notify_signal_sent(ProtoViewApp* app) {
+    static const NotificationSequence sent_seq = {
+        &message_blue_255,
+        &message_vibro_on,
+        &message_note_g1,
+        &message_delay_10,
+        &message_sound_off,
+        &message_vibro_off,
+        &message_blue_0,
+        NULL};
+    notification_message(app->notification, &sent_seq);
+}
+
+/* Handle input for the info view. */
+void process_input_info(ProtoViewApp* app, InputEvent input) {
+    /* If we don't have a decoded signal, we don't allow to go up/down
+     * in the subviews: they are only useful when a loaded signal. */
+    if(app->signal_decoded && ui_process_subview_updown(app, input, SubViewInfoLast)) return;
+
+    InfoViewPrivData* privdata = app->view_privdata;
+    int subview = ui_get_current_subview(app);
+
+    /* Main subview. */
+    if(subview == SubViewInfoMain) {
+        if(input.type == InputTypeLong && input.key == InputKeyOk) {
+            /* Reset the current sample to capture the next. */
+            reset_current_signal(app);
+        } else if(input.type == InputTypeShort && input.key == InputKeyOk) {
+            /* Show next info page. */
+            privdata->cur_info_page++;
+        }
+    } else if(subview == SubViewInfoSave) {
+        /* Save subview. */
+        if(input.type == InputTypePress && input.key == InputKeyRight) {
+            privdata->signal_display_start_row++;
+        } else if(input.type == InputTypePress && input.key == InputKeyLeft) {
+            if(privdata->signal_display_start_row != 0) privdata->signal_display_start_row--;
+        } else if(input.type == InputTypeLong && input.key == InputKeyOk) {
+            // We have have the buffer already allocated, in case the
+            // user aborted with BACK a previous saving.
+            if(privdata->filename == NULL) privdata->filename = malloc(SAVE_FILENAME_LEN);
+            set_signal_random_filename(app, privdata->filename, SAVE_FILENAME_LEN);
+            ui_show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN, text_input_done_callback);
+        } else if(input.type == InputTypeShort && input.key == InputKeyOk) {
+            SendSignalCtx send_state;
+            send_signal_init(&send_state, app);
+            radio_tx_signal(app, radio_tx_feed_data, &send_state);
+            notify_signal_sent(app);
+        }
+    }
+}
+
+/* Called on view exit. */
+void view_exit_info(ProtoViewApp* app) {
+    InfoViewPrivData* privdata = app->view_privdata;
+    // When the user aborts the keyboard input, we are left with the
+    // filename buffer allocated.
+    if(privdata->filename) free(privdata->filename);
+}

+ 114 - 0
main_apps_sources/protoview/view_raw_signal.c

@@ -0,0 +1,114 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+
+/* Render the received signal.
+ *
+ * The screen of the flipper is 128 x 64. Even using 4 pixels per line
+ * (where low level signal is one pixel high, high level is 4 pixels
+ * high) and 4 pixels of spacing between the different lines, we can
+ * plot comfortably 8 lines.
+ *
+ * The 'idx' argument is the first sample to render in the circular
+ * buffer. */
+void render_signal(ProtoViewApp* app, Canvas* const canvas, RawSamplesBuffer* buf, uint32_t idx) {
+    canvas_set_color(canvas, ColorBlack);
+
+    int rows = 8;
+    uint32_t time_per_pixel = app->us_scale;
+    uint32_t start_idx = idx;
+    bool level = 0;
+    uint32_t dur = 0, sample_num = 0;
+    for(int row = 0; row < rows; row++) {
+        for(int x = 0; x < 128; x++) {
+            int y = 3 + row * 8;
+            if(dur < time_per_pixel / 2) {
+                /* Get more data. */
+                raw_samples_get(buf, idx++, &level, &dur);
+                sample_num++;
+            }
+
+            canvas_draw_line(canvas, x, y, x, y - (level * 3));
+
+            /* Write a small triangle under the last sample detected. */
+            if(app->signal_bestlen != 0 && sample_num + start_idx == app->signal_bestlen + 1) {
+                canvas_draw_dot(canvas, x, y + 2);
+                canvas_draw_dot(canvas, x - 1, y + 3);
+                canvas_draw_dot(canvas, x, y + 3);
+                canvas_draw_dot(canvas, x + 1, y + 3);
+                sample_num++; /* Make sure we don't mark the next, too. */
+            }
+
+            /* Remove from the current level duration the time we
+             * just plot. */
+            if(dur > time_per_pixel)
+                dur -= time_per_pixel;
+            else
+                dur = 0;
+        }
+    }
+}
+
+/* Raw pulses rendering. This is our default view. */
+void render_view_raw_pulses(Canvas* const canvas, ProtoViewApp* app) {
+    /* Show signal. */
+    render_signal(app, canvas, DetectedSamples, app->signal_offset);
+
+    /* Show signal information. */
+    char buf[64];
+    snprintf(buf, sizeof(buf), "%luus", (unsigned long)DetectedSamples->short_pulse_dur);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_with_border(canvas, 97, 63, buf, ColorWhite, ColorBlack);
+    if(app->signal_decoded) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_with_border(
+            canvas, 1, 61, app->msg_info->decoder->name, ColorWhite, ColorBlack);
+    }
+}
+
+/* Handle input for the raw pulses view. */
+void process_input_raw_pulses(ProtoViewApp* app, InputEvent input) {
+    if(input.type == InputTypeRepeat) {
+        /* Handle panning of the signal window. Long pressing
+         * right will show successive samples, long pressing left
+         * previous samples. */
+        if(input.key == InputKeyRight)
+            app->signal_offset++;
+        else if(input.key == InputKeyLeft)
+            app->signal_offset--;
+    } else if(input.type == InputTypeLong) {
+        if(input.key == InputKeyOk) {
+            /* Reset the current sample to capture the next. */
+            reset_current_signal(app);
+        }
+    } else if(input.type == InputTypeShort) {
+        if(input.key == InputKeyOk) {
+            app->signal_offset = 0;
+            adjust_raw_view_scale(app, DetectedSamples->short_pulse_dur);
+        } else if(input.key == InputKeyDown) {
+            /* Rescaling. The set becomes finer under 50us per pixel. */
+            uint32_t scale_step = app->us_scale >= 50 ? 50 : 10;
+            if(app->us_scale < 500) app->us_scale += scale_step;
+        } else if(input.key == InputKeyUp) {
+            uint32_t scale_step = app->us_scale > 50 ? 50 : 10;
+            if(app->us_scale > 10) app->us_scale -= scale_step;
+        }
+    }
+}
+
+/* Adjust raw view scale depending on short pulse duration. */
+void adjust_raw_view_scale(ProtoViewApp* app, uint32_t short_pulse_dur) {
+    if(short_pulse_dur == 0)
+        app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
+    else if(short_pulse_dur < 75)
+        app->us_scale = 10;
+    else if(short_pulse_dur < 145)
+        app->us_scale = 30;
+    else if(short_pulse_dur < 400)
+        app->us_scale = 100;
+    else if(short_pulse_dur < 1000)
+        app->us_scale = 200;
+    else
+        app->us_scale = PROTOVIEW_RAW_VIEW_DEFAULT_SCALE;
+}

+ 119 - 0
main_apps_sources/protoview/view_settings.c

@@ -0,0 +1,119 @@
+/* Copyright (C) 2022-2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include "app.h"
+
+/* Renders a single view with frequency and modulation setting. However
+ * this are logically two different views, and only one of the settings
+ * will be highlighted. */
+void render_view_settings(Canvas* const canvas, ProtoViewApp* app) {
+    canvas_set_font(canvas, FontPrimary);
+    if(app->current_view == ViewFrequencySettings)
+        canvas_draw_str_with_border(canvas, 1, 10, "Frequency", ColorWhite, ColorBlack);
+    else
+        canvas_draw_str(canvas, 1, 10, "Frequency");
+
+    if(app->current_view == ViewModulationSettings)
+        canvas_draw_str_with_border(canvas, 70, 10, "Modulation", ColorWhite, ColorBlack);
+    else
+        canvas_draw_str(canvas, 70, 10, "Modulation");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 10, 61, "Use up and down to modify");
+
+    if(app->txrx->debug_timer_sampling)
+        canvas_draw_str(canvas, 3, 52, "(DEBUG timer sampling is ON)");
+
+    /* Show frequency. We can use big numbers font since it's just a number. */
+    if(app->current_view == ViewFrequencySettings) {
+        char buf[16];
+        snprintf(buf, sizeof(buf), "%.2f", (double)app->frequency / 1000000);
+        canvas_set_font(canvas, FontBigNumbers);
+        canvas_draw_str(canvas, 30, 40, buf);
+    } else if(app->current_view == ViewModulationSettings) {
+        int current = app->modulation;
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 33, 39, ProtoViewModulations[current].name);
+    }
+}
+
+/* Handle input for the settings view. */
+void process_input_settings(ProtoViewApp* app, InputEvent input) {
+    if(input.type == InputTypeLong && input.key == InputKeyOk) {
+        /* Long pressing to OK sets the default frequency and
+         * modulation. */
+        app->frequency = subghz_setting_get_default_frequency(app->setting);
+        app->modulation = 0;
+    } else if(0 && input.type == InputTypeLong && input.key == InputKeyDown) {
+        /* Long pressing to down switches between normal and debug
+         * timer sampling mode. NOTE: this feature is disabled for users,
+         * only useful for devs (if useful at all). */
+
+        /* We have to stop the previous sampling system. */
+        radio_rx_end(app);
+
+        /* Then switch mode and start the new one. */
+        app->txrx->debug_timer_sampling = !app->txrx->debug_timer_sampling;
+        radio_begin(app);
+        radio_rx(app);
+    } else if(input.type == InputTypePress && (input.key != InputKeyDown || input.key != InputKeyUp)) {
+        /* Handle up and down to change frequency or modulation. */
+        if(app->current_view == ViewFrequencySettings) {
+            size_t curidx = 0, i;
+            size_t count = subghz_setting_get_frequency_count(app->setting);
+
+            /* Scan the list of frequencies to check for the index of the
+             * currently set frequency. */
+            for(i = 0; i < count; i++) {
+                uint32_t freq = subghz_setting_get_frequency(app->setting, i);
+                if(freq == app->frequency) {
+                    curidx = i;
+                    break;
+                }
+            }
+            if(i == count) return; /* Should never happen. */
+
+            if(input.key == InputKeyUp) {
+                curidx = curidx == 0 ? count - 1 : curidx - 1;
+            } else if(input.key == InputKeyDown) {
+                curidx = (curidx + 1) % count;
+            } else {
+                return;
+            }
+            app->frequency = subghz_setting_get_frequency(app->setting, curidx);
+        } else if(app->current_view == ViewModulationSettings) {
+            uint32_t count = 0;
+            uint32_t modid = app->modulation;
+
+            while(ProtoViewModulations[count].name != NULL) count++;
+            if(input.key == InputKeyUp) {
+                modid = modid == 0 ? count - 1 : modid - 1;
+            } else if(input.key == InputKeyDown) {
+                modid = (modid + 1) % count;
+            } else {
+                return;
+            }
+            app->modulation = modid;
+        }
+    } else {
+        return;
+    }
+
+    /* Apply changes when switching to other views. */
+    app->txrx->freq_mod_changed = true;
+}
+
+/* When the user switches to some other view, if they changed the parameters
+ * we need to restart the radio with the right frequency and modulation. */
+void view_exit_settings(ProtoViewApp* app) {
+    if(app->txrx->freq_mod_changed) {
+        FURI_LOG_E(
+            TAG,
+            "Setting view, setting frequency/modulation to %lu %s",
+            app->frequency,
+            ProtoViewModulations[app->modulation].name);
+        radio_rx_end(app);
+        radio_begin(app);
+        radio_rx(app);
+        app->txrx->freq_mod_changed = false;
+    }
+}

+ 7 - 0
main_apps_sources/spectrum_analyzer/README.md

@@ -0,0 +1,7 @@
+This application allows you to plot a chart showing the relationship between amplitude and frequency, detecting nearby signal sources. If there is a nearby source broadcasting a signal at the observed frequency, the graph will go up sharply.
+
+The app has the following controls:
+
+- The OK button adjusts the width of the spectrum.
+- The Up and Down buttons zoom in and out.
+- The Left and Right buttons switch between different frequency bands.

+ 14 - 0
main_apps_sources/spectrum_analyzer/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="spectrum_analyzer",
+    name="Spectrum Analyzer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="spectrum_analyzer_app",
+    requires=["gui"],
+    stack_size=2 * 1024,
+    order=12,
+    fap_icon="spectrum_10px.png",
+    fap_category="Sub-GHz",
+    fap_author="@xMasterX & @theY4Kman & @ALEEF02 (original by @jolcese)",
+    fap_version="1.1",
+    fap_description="Displays a spectrogram chart to visually represent RF signals around you.",
+)

+ 64 - 0
main_apps_sources/spectrum_analyzer/helpers/radio_device_loader.c

@@ -0,0 +1,64 @@
+#include "radio_device_loader.h"
+
+#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+
+static void radio_device_loader_power_on() {
+    uint8_t attempts = 0;
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        //CC1101 power-up time
+        furi_delay_ms(10);
+    }
+}
+
+static void radio_device_loader_power_off() {
+    if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
+}
+
+bool radio_device_loader_is_connect_external(const char* name) {
+    bool is_connect = false;
+    bool is_otg_enabled = furi_hal_power_is_otg_enabled();
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_on();
+    }
+
+    const SubGhzDevice* device = subghz_devices_get_by_name(name);
+    if(device) {
+        is_connect = subghz_devices_is_connect(device);
+    }
+
+    if(!is_otg_enabled) {
+        radio_device_loader_power_off();
+    }
+    return is_connect;
+}
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type) {
+    const SubGhzDevice* radio_device;
+
+    if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
+       radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
+        radio_device_loader_power_on();
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
+        subghz_devices_begin(radio_device);
+    } else if(current_radio_device == NULL) {
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    } else {
+        radio_device_loader_end(current_radio_device);
+        radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    }
+
+    return radio_device;
+}
+
+void radio_device_loader_end(const SubGhzDevice* radio_device) {
+    furi_assert(radio_device);
+    radio_device_loader_power_off();
+    if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
+        subghz_devices_end(radio_device);
+    }
+}

+ 15 - 0
main_apps_sources/spectrum_analyzer/helpers/radio_device_loader.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <lib/subghz/devices/devices.h>
+
+/** SubGhzRadioDeviceType */
+typedef enum {
+    SubGhzRadioDeviceTypeInternal,
+    SubGhzRadioDeviceTypeExternalCC1101,
+} SubGhzRadioDeviceType;
+
+const SubGhzDevice* radio_device_loader_set(
+    const SubGhzDevice* current_radio_device,
+    SubGhzRadioDeviceType radio_device_type);
+
+void radio_device_loader_end(const SubGhzDevice* radio_device);

BIN
main_apps_sources/spectrum_analyzer/img/1.png


BIN
main_apps_sources/spectrum_analyzer/img/2.png


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