Просмотр исходного кода

Merge pull request #43 from m7i-org/dev

Add longwave_clock app
WillyJL 10 месяцев назад
Родитель
Сommit
f326755664
66 измененных файлов с 4291 добавлено и 0 удалено
  1. 52 0
      longwave_clock/.github/workflows/build-longwaveclock.yml
  2. 4 0
      longwave_clock/.gitignore
  3. 1 0
      longwave_clock/.gitsubtree
  4. 2 0
      longwave_clock/CHANGELOG.md
  5. 661 0
      longwave_clock/LICENSE
  6. 74 0
      longwave_clock/README.md
  7. 16 0
      longwave_clock/application.fam
  8. BIN
      longwave_clock/images/lwc_dcf.png
  9. BIN
      longwave_clock/images/lwc_msf.png
  10. BIN
      longwave_clock/images/lwc_sender.png
  11. BIN
      longwave_clock/lcw.png
  12. 9 0
      longwave_clock/screenshots/bin/00_take_screenshots.sh
  13. 21 0
      longwave_clock/screenshots/bin/10_to_gif.sh
  14. BIN
      longwave_clock/screenshots/modules/module_600.jpg
  15. BIN
      longwave_clock/screenshots/modules/module_775.jpg
  16. BIN
      longwave_clock/screenshots/res/gpio_simple.png
  17. BIN
      longwave_clock/screenshots/res/wave_1.png
  18. BIN
      longwave_clock/screenshots/res/wave_2.png
  19. BIN
      longwave_clock/screenshots/res/wave_3.png
  20. BIN
      longwave_clock/screenshots/v0.1/animation.gif
  21. BIN
      longwave_clock/screenshots/v0.1/big_dcf77.png
  22. BIN
      longwave_clock/screenshots/v0.1/big_menu.png
  23. BIN
      longwave_clock/screenshots/v0.1/big_msf.png
  24. BIN
      longwave_clock/screenshots/v0.1/dcf77_1.png
  25. BIN
      longwave_clock/screenshots/v0.1/dcf77_2.png
  26. BIN
      longwave_clock/screenshots/v0.1/menu_dcf77.png
  27. BIN
      longwave_clock/screenshots/v0.1/menu_msf.png
  28. BIN
      longwave_clock/screenshots/v0.1/msf_1.png
  29. BIN
      longwave_clock/screenshots/v0.1/msf_2.png
  30. 139 0
      longwave_clock/src/app_state.c
  31. 57 0
      longwave_clock/src/app_state.h
  32. 20 0
      longwave_clock/src/flipper.h
  33. 67 0
      longwave_clock/src/gpio.c
  34. 46 0
      longwave_clock/src/gpio.h
  35. 238 0
      longwave_clock/src/logic_dcf77.c
  36. 36 0
      longwave_clock/src/logic_dcf77.h
  37. 126 0
      longwave_clock/src/logic_general.c
  38. 134 0
      longwave_clock/src/logic_general.h
  39. 300 0
      longwave_clock/src/logic_msf.c
  40. 37 0
      longwave_clock/src/logic_msf.h
  41. 32 0
      longwave_clock/src/longwave_clock_app.c
  42. 6 0
      longwave_clock/src/longwave_clock_app.h
  43. 110 0
      longwave_clock/src/module_date.c
  44. 21 0
      longwave_clock/src/module_date.h
  45. 37 0
      longwave_clock/src/module_lights.c
  46. 15 0
      longwave_clock/src/module_lights.h
  47. 177 0
      longwave_clock/src/module_rollbits.c
  48. 20 0
      longwave_clock/src/module_rollbits.h
  49. 135 0
      longwave_clock/src/module_time.c
  50. 20 0
      longwave_clock/src/module_time.h
  51. 13 0
      longwave_clock/src/protocols.c
  52. 13 0
      longwave_clock/src/protocols.h
  53. 30 0
      longwave_clock/src/scene_about.c
  54. 10 0
      longwave_clock/src/scene_about.h
  55. 557 0
      longwave_clock/src/scene_dcf77.c
  56. 24 0
      longwave_clock/src/scene_dcf77.h
  57. 31 0
      longwave_clock/src/scene_info.c
  58. 10 0
      longwave_clock/src/scene_info.h
  59. 90 0
      longwave_clock/src/scene_main_menu.c
  60. 24 0
      longwave_clock/src/scene_main_menu.h
  61. 614 0
      longwave_clock/src/scene_msf.c
  62. 25 0
      longwave_clock/src/scene_msf.h
  63. 124 0
      longwave_clock/src/scene_sub_menu.c
  64. 10 0
      longwave_clock/src/scene_sub_menu.h
  65. 59 0
      longwave_clock/src/scenes.c
  66. 44 0
      longwave_clock/src/scenes.h

+ 52 - 0
longwave_clock/.github/workflows/build-longwaveclock.yml

@@ -0,0 +1,52 @@
+name: "longwave_clock: build for multiple SDK sources"
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - '**'
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: Official Dev channel
+            sdk-channel: dev
+          - name: Official Release channel
+            sdk-channel: release
+          - name: Unleashed Dev
+            sdk-index-url: https://up.unleashedflip.com/directory.json
+            sdk-channel: dev
+          - name: Unleashed Release
+            sdk-index-url: https://up.unleashedflip.com/directory.json
+            sdk-channel: release
+          - name: Momentum Dev
+            sdk-index-url: https://up.momentum-fw.dev/firmware/directory.json
+            sdk-channel: dev
+          - name: Momentum Release
+            sdk-index-url: https://up.momentum-fw.dev/firmware/directory.json
+            sdk-channel: release
+    name: 'longwave_clock: ufbt build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1.3
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+          sdk-index-url: ${{ matrix.sdk-index-url }}
+          app-dir: .
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: longwave_clock-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}
+      - name: Lint sources
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1.3
+        with:
+          # skip SDK setup, we already did it in previous step
+          skip-setup: true
+          task: lint

+ 4 - 0
longwave_clock/.gitignore

@@ -0,0 +1,4 @@
+.*
+!.gitignore
+!.github
+!.gitsubtree

+ 1 - 0
longwave_clock/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/m7i-org/flipper_longwave_clock main /

+ 2 - 0
longwave_clock/CHANGELOG.md

@@ -0,0 +1,2 @@
+v0.1:
+First version, working DCF77 and MSF both demo and GPIO modes.

+ 661 - 0
longwave_clock/LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are 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.
+
+  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.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  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 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero 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 your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  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 AGPL, see
+<https://www.gnu.org/licenses/>.

+ 74 - 0
longwave_clock/README.md

@@ -0,0 +1,74 @@
+# Longwave Clock
+
+This is a Flipper Zero app to receive and decode, or simulate, multiple time signal broadcasts with different protocols and time formats. For receiving via GPIO, an inexpensive receiver connected to a receiving pin is required.
+
+![](screenshots/v0.1/animation.gif)
+
+## Protocol support
+
+### DCF77 (Europe, Germany)
+
+![](screenshots/v0.1/menu_dcf77.png) ![](screenshots/v0.1/dcf77_1.png) ![](screenshots/v0.1/dcf77_2.png)
+
+[DCF77](https://en.wikipedia.org/wiki/DCF77) is broadcasted from Frankfurt am Main in Germany ([50.0155,9.0108](https://www.openstreetmap.org/?mlat=50.0155&mlon=9.0108#map=4/50.01/9.01)) and requires an antenna tuned to 77.5 kHz.
+
+- The radio transmission can be received all over Europe (~2000 km from the sender).
+- 1 bit per second is transmitted by reducing carrier power at the beginning of every second.
+- The transmission encodes time, date as well as catastrophe and weather information (encrypted, not decoded).
+
+### MSF (Europe, United Kingdom)
+
+![](screenshots/v0.1/menu_msf.png) ![](screenshots/v0.1/msf_1.png) ![](screenshots/v0.1/msf_2.png)
+
+[MSF (Time from NPL/Rugby clock)](https://en.wikipedia.org/wiki/Time_from_NPL_(MSF)) is broadcasted from Anthorn in the UK ([54.9116,-3.2785](https://www.openstreetmap.org/?mlat=54.9116&mlon=-3.2785#map=5/54.91/-3.27)) and requires an antenna tuned to 60 kHz (as does [WWVB](#wwvb-north-america-us)).
+
+- The radio transmission can be received over most of western and northern Europe.
+- The transmission encodes time, date as well as DUT1 bits (difference between atomic and astronomical time).
+- The app only supports the slow code at 120 bits per minute, of which only 60bits  are encoded.
+- Same frequency as WWVB - in Europe you will receive this signal through a WWVB receiver.
+
+### WWVB (North America, US)
+
+[WWVB](https://en.wikipedia.org/wiki/WWVB) transmits on 60 kHz and is on the backlog for the Longwave app.
+
+If you're based in the US and would like to help: PRs are welcome!
+
+## GPIO modules
+
+The app supports a demonstration mode as well as GPIO mode. For GPIO mode, external modules are required.
+
+In GPIO mode the following configuration is available (per protocol):
+- GPIO data: use "inverted" if the module outputs logic high to the data pin when the sender signal is low (hopefully rare).
+- Data pin: this configures the receiving pin on the flipper, C0 is the default and recommended pin.
+
+### Supported modules
+
+The following shows modules that I own and have been successfully tested for reception.
+
+#### 77.5 kHz module (DCF77)
+
+![](screenshots/modules/module_775.jpg) 
+
+You can find this type of module by searching for "DCF77 module" in any electronics online shop.
+
+#### 60.0 kHz module (MSF, WWVB)
+
+![](screenshots/modules/module_600.jpg) 
+
+Search for "WWVB module" in any electronics online shop. 
+This applies even if you want to receive MSF instead: they use the same frequency.
+
+### Pinout
+
+The modules I checked are pretty much the same, here is the common pinout configuration I found and tested.
+Please check with the manufacturer, as yours might be different and you may cause damage by wiring the module incorrectly. 
+
+- **VDD**: flipper pin 9 (3V3)
+- **GND** / unlabeled: flipper pin 11 (GND)
+- **PON** / **P**: power on, asks for "logic low", using flipper pin 11 (GND)
+- **OUT** / **T**: the data signal, using flipper pin 16 (C0)
+
+## Picture credits
+
+- The [Font “HaxrCorp 4089”](https://fontstruct.com/fontstructions/show/192981) by “sahwar” is licensed under a CC-BY-SA license.
+- The picture of a flipper zero is (C) [Flipper Devices Inc](https://flipperzero.one/).

+ 16 - 0
longwave_clock/application.fam

@@ -0,0 +1,16 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="longwave_clock",  # Must be unique
+    name="[GPIO] Longwave Clock",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="longwave_clock_app",
+    stack_size=2 * 1024,
+    fap_icon_assets="images",
+    fap_category="GPIO",
+    fap_version="0.1",
+    fap_icon="lcw.png",
+    fap_description="Decode or demonstrate long wave time signals",
+    fap_author="@m7i-org",
+    fap_weburl="https://github.com/flipper_longwave_clock",
+)

BIN
longwave_clock/images/lwc_dcf.png


BIN
longwave_clock/images/lwc_msf.png


BIN
longwave_clock/images/lwc_sender.png


BIN
longwave_clock/lcw.png


+ 9 - 0
longwave_clock/screenshots/bin/00_take_screenshots.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+sleep 2; 
+echo -n START; 
+while true; do 
+  echo -n '.'
+  scrot '%Y-%m-%d-%H:%M:%S.png' &
+  sleep 0.9
+done;

+ 21 - 0
longwave_clock/screenshots/bin/10_to_gif.sh

@@ -0,0 +1,21 @@
+#!/bin/bash
+
+res="$(cd "$(dirname $0)"; pwd;)/../res"
+t=$(mktemp -d);
+mkdir $t/1 $t/2 $t/3 $t/4
+echo "Working in $t";
+
+n=0;
+for i in $(ls *.png | sort); do 
+  convert $i -alpha remove +repage -crop 512x256+1120+626 +repage $t/1/$i;
+  convert $t/1/$i -resize 250x125 $t/2/$i;
+  composite -geometry +195+420 $t/2/$i $res/gpio_simple.png $t/3/$i
+  composite -geometry +415+0 $res/wave_$((n%3+1)).png $t/3/$i $t/4/$i
+  n=$(($n+1));
+done;
+
+convert -delay 100 -loop 0 -layers Optimize $t/4/*.png animation.gif
+# Open in GIMP
+# 1. Image>Mode>Indexed>Generate optimum palette
+# 2. Filters>Animation>Optimize (for GIF)
+# 3. File>Export As...

BIN
longwave_clock/screenshots/modules/module_600.jpg


BIN
longwave_clock/screenshots/modules/module_775.jpg


BIN
longwave_clock/screenshots/res/gpio_simple.png


BIN
longwave_clock/screenshots/res/wave_1.png


BIN
longwave_clock/screenshots/res/wave_2.png


BIN
longwave_clock/screenshots/res/wave_3.png


BIN
longwave_clock/screenshots/v0.1/animation.gif


BIN
longwave_clock/screenshots/v0.1/big_dcf77.png


BIN
longwave_clock/screenshots/v0.1/big_menu.png


BIN
longwave_clock/screenshots/v0.1/big_msf.png


BIN
longwave_clock/screenshots/v0.1/dcf77_1.png


BIN
longwave_clock/screenshots/v0.1/dcf77_2.png


BIN
longwave_clock/screenshots/v0.1/menu_dcf77.png


BIN
longwave_clock/screenshots/v0.1/menu_msf.png


BIN
longwave_clock/screenshots/v0.1/msf_1.png


BIN
longwave_clock/screenshots/v0.1/msf_2.png


+ 139 - 0
longwave_clock/src/app_state.c

@@ -0,0 +1,139 @@
+#include "app_state.h"
+#include "longwave_clock_app.h"
+
+static LWCScene protocol_scene[] = {LWCDCF77Scene, LWCMSFScene};
+
+App* app_alloc() {
+    App* app = malloc(sizeof(App));
+    app->scene_manager = scene_manager_alloc(&lwc_scene_manager_handlers, app);
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, lwc_custom_callback);
+    view_dispatcher_set_navigation_event_callback(app->view_dispatcher, lwc_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, lwc_tick_event_callback, furi_ms_to_ticks(100));
+
+    app->main_menu = submenu_alloc();
+    app->sub_menu = variable_item_list_alloc();
+    app->about = text_box_alloc();
+    app->info_text = text_box_alloc();
+    app->dcf77_view = lwc_dcf77_scene_alloc();
+    app->msf_view = lwc_msf_scene_alloc();
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, LWCMainMenuView, submenu_get_view(app->main_menu));
+    view_dispatcher_add_view(
+        app->view_dispatcher, LWCSubMenuView, variable_item_list_get_view(app->sub_menu));
+    view_dispatcher_add_view(app->view_dispatcher, LWCAboutView, text_box_get_view(app->about));
+    view_dispatcher_add_view(app->view_dispatcher, LWCInfoView, text_box_get_view(app->info_text));
+    view_dispatcher_add_view(app->view_dispatcher, LWCDCF77View, app->dcf77_view);
+    view_dispatcher_add_view(app->view_dispatcher, LWCMSFView, app->msf_view);
+    return app;
+}
+
+AppState* app_state_alloc() {
+    AppState* state = malloc(sizeof(AppState));
+
+    state->storage = furi_record_open(RECORD_STORAGE);
+
+    for(uint8_t i = 0; i < __lwc_number_of_protocols; i++) {
+        state->proto_configs[i] = malloc(sizeof(ProtoConfig));
+        File* file = storage_file_alloc(state->storage);
+        bool read = false;
+        if(storage_file_open(
+               file, get_protocol_config_filename((LWCType)i), FSAM_READ, FSOM_OPEN_EXISTING)) {
+            read = storage_file_read(file, state->proto_configs[i], sizeof(ProtoConfig)) ==
+                   sizeof(ProtoConfig);
+        }
+
+        if(!read) {
+            state->proto_configs[i]->run_mode = Demo;
+            state->proto_configs[i]->data_pin = GPIOPinC0;
+            state->proto_configs[i]->data_mode = Regular;
+        }
+        storage_file_close(file);
+        storage_file_free(file);
+    }
+
+    state->display_on = false;
+    state->gpio = NULL;
+
+    return state;
+}
+
+void store_proto_config(AppState* app_state) {
+    File* file = storage_file_alloc(app_state->storage);
+    if(storage_file_open(
+           file,
+           get_protocol_config_filename(app_state->lwc_type),
+           FSAM_WRITE,
+           FSOM_CREATE_ALWAYS)) {
+        if(!storage_file_write(
+               file, app_state->proto_configs[app_state->lwc_type], sizeof(ProtoConfig))) {
+            FURI_LOG_E(TAG, "Failed to write to open proto config file.");
+        }
+    } else {
+        FURI_LOG_E(TAG, "Failed to open proto config file for writing.");
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+}
+
+void app_init_lwc(App* app, LWCType type) {
+    app->state->lwc_type = type;
+}
+
+void app_quit(App* app) {
+    scene_manager_stop(app->scene_manager);
+}
+
+void app_free(App* app) {
+    furi_assert(app);
+
+    FURI_LOG_D(TAG, "Removing the view dispatcher views.");
+    view_dispatcher_remove_view(app->view_dispatcher, LWCMainMenuView);
+    view_dispatcher_remove_view(app->view_dispatcher, LWCSubMenuView);
+    view_dispatcher_remove_view(app->view_dispatcher, LWCAboutView);
+    view_dispatcher_remove_view(app->view_dispatcher, LWCInfoView);
+    view_dispatcher_remove_view(app->view_dispatcher, LWCDCF77View);
+    view_dispatcher_remove_view(app->view_dispatcher, LWCMSFView);
+
+    FURI_LOG_D(TAG, "Removing the scene manager and view dispatcher.");
+    scene_manager_free(app->scene_manager);
+    view_dispatcher_free(app->view_dispatcher);
+
+    FURI_LOG_D(TAG, "Removing the single scenes...");
+    submenu_free(app->main_menu);
+    variable_item_list_free(app->sub_menu);
+    text_box_free(app->about);
+    text_box_free(app->info_text);
+    FURI_LOG_D(TAG, "Removing the DCF77 scene...");
+    lwc_dcf77_scene_free(app->dcf77_view);
+    FURI_LOG_D(TAG, "Removing the MSF scene...");
+    lwc_msf_scene_free(app->msf_view);
+
+    FURI_LOG_D(TAG, "Desubscribing from notification...");
+    furi_record_close(RECORD_NOTIFICATION);
+
+    FURI_LOG_D(TAG, "Removing the protocol configs.");
+    for(uint8_t i = 0; i < __lwc_number_of_protocols; i++) {
+        free(app->state->proto_configs[i]);
+    }
+    furi_record_close(RECORD_STORAGE);
+
+    FURI_LOG_D(TAG, "Removing the AppState*.");
+    free(app->state);
+
+    FURI_LOG_D(TAG, "Freeing the App*...");
+    free(app);
+}
+
+ProtoConfig* lwc_get_protocol_config(AppState* app_state) {
+    return app_state->proto_configs[app_state->lwc_type];
+}
+
+LWCScene lwc_get_start_scene_for_protocol(AppState* app_state) {
+    return protocol_scene[app_state->lwc_type];
+}

+ 57 - 0
longwave_clock/src/app_state.h

@@ -0,0 +1,57 @@
+#ifndef LWC_STATE_HEADERS
+#define LWC_STATE_HEADERS
+
+#include "flipper.h"
+#include "logic_dcf77.h"
+#include "logic_msf.h"
+#include "protocols.h"
+#include "scenes.h"
+#include "gpio.h"
+
+typedef struct ProtoConfig {
+    LWCRunMode run_mode;
+    LWCDataMode data_mode;
+    LWCDataPin data_pin;
+} ProtoConfig;
+
+typedef enum {
+    EventReceiveSync,
+    EventReceiveZero,
+    EventReceiveOne,
+    EventReceiveUnknown
+} LWCEventType;
+
+typedef struct AppState {
+    LWCType lwc_type;
+    ProtoConfig* proto_configs[__lwc_number_of_protocols];
+    GPIOContext* gpio;
+    FuriTimer* seconds_timer;
+    MinuteData* simulation_data;
+    Storage* storage;
+    bool display_on;
+} AppState;
+
+typedef struct App {
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+    Submenu* main_menu;
+    NotificationApp* notifications;
+    VariableItemList* sub_menu;
+    TextBox* about;
+    TextBox* info_text;
+    View* dcf77_view;
+    View* msf_view;
+    AppState* state;
+} App;
+
+App* app_alloc();
+AppState* app_state_alloc();
+void app_quit(App* app);
+void app_free(App* app);
+void app_init_lwc(App* app, LWCType rtype);
+LWCScene lwc_get_start_scene_for_protocol(AppState* app_state);
+void store_proto_config(AppState* app_state);
+
+ProtoConfig* lwc_get_protocol_config(AppState* app_state);
+
+#endif

+ 20 - 0
longwave_clock/src/flipper.h

@@ -0,0 +1,20 @@
+#ifndef FLIPPER_HEADERS
+#define FLIPPER_HEADERS
+
+#include <furi.h>
+
+#include <gui/gui.h>
+#include <gui/icon_i.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+#include "notification/notification.h"
+#include "notification/notification_messages.h"
+#include <applications/services/gui/view.h>
+#include <storage/storage.h>
+
+#endif

+ 67 - 0
longwave_clock/src/gpio.c

@@ -0,0 +1,67 @@
+#include "gpio.h"
+
+uint32_t last_switch = 0;
+uint32_t last_time_up_tick = 0;
+uint32_t last_time_down_tick = 0;
+
+static void gpio_interrupt_callback(void* context) {
+    furi_assert(context);
+    GPIOContext* gpio_context = context;
+
+    uint32_t current_tick = furi_get_tick();
+
+    if(last_switch > 0) {
+        bool shift_up = furi_hal_gpio_read(gpio_context->pin) ^ gpio_context->inverted;
+
+        if(shift_up) {
+            last_time_down_tick = current_tick - last_switch;
+        } else {
+            last_time_up_tick = current_tick - last_switch;
+        }
+
+        GPIOEvent event = {
+            .shift_up = shift_up,
+            .time_passed_down = last_time_down_tick,
+            .time_passed_up = last_time_up_tick};
+        furi_message_queue_put(gpio_context->queue, &event, 0);
+    }
+
+    last_switch = current_tick;
+}
+
+static const GpioPin* data_pins_to_gpio_pins[] =
+    {&gpio_ext_pa7, &gpio_ext_pa4, &gpio_ext_pb2, &gpio_ext_pc1, &gpio_ext_pc0};
+
+GPIOContext*
+    gpio_start_listening(const LWCDataPin data_pin, bool inverted, void* callback_context) {
+    GPIOContext* result = malloc(sizeof(GPIOContext));
+
+    result->inverted = inverted;
+    result->pin = data_pins_to_gpio_pins[data_pin];
+    result->context = callback_context;
+    result->queue = furi_message_queue_alloc(32, sizeof(GPIOEvent));
+
+    furi_hal_gpio_add_int_callback(result->pin, gpio_interrupt_callback, result);
+    furi_hal_gpio_enable_int_callback(result->pin);
+    furi_hal_gpio_init(result->pin, GpioModeInterruptRiseFall, GpioPullUp, GpioSpeedVeryHigh);
+
+    return result;
+}
+
+void gpio_stop_listening(GPIOContext* context) {
+    furi_hal_gpio_disable_int_callback(context->pin);
+    furi_hal_gpio_remove_int_callback(context->pin);
+    furi_hal_gpio_init_simple(context->pin, GpioModeAnalog);
+}
+
+bool gpio_callback_with_event(GPIOContext* context, void (*callback)(GPIOEvent, void*)) {
+    GPIOEvent event;
+    FuriStatus event_status = furi_message_queue_get(context->queue, &event, 0);
+
+    if(event_status == FuriStatusOk) {
+        callback(event, context->context);
+        return true;
+    } else {
+        return false;
+    }
+}

+ 46 - 0
longwave_clock/src/gpio.h

@@ -0,0 +1,46 @@
+#ifndef GPIO_HEADERS
+#define GPIO_HEADERS
+
+#include "flipper.h"
+#include "logic_general.h"
+
+typedef enum {
+    Demo,
+    GPIO,
+    __lwc_number_of_run_modes
+} LWCRunMode;
+
+typedef enum {
+    Regular,
+    Inverted,
+    __lwc_number_of_data_modes
+} LWCDataMode;
+
+typedef enum {
+    GPIOPinA7,
+    GPIOPinA4,
+    GPIOPinB2,
+    GPIOPinC1,
+    GPIOPinC0,
+    __lwc_number_of_data_pins
+} LWCDataPin;
+
+typedef struct {
+    const GpioPin* pin;
+    void* context;
+    bool inverted;
+    FuriMessageQueue* queue;
+} GPIOContext;
+
+typedef struct {
+    bool shift_up;
+    uint32_t time_passed_up;
+    uint32_t time_passed_down;
+} GPIOEvent;
+
+GPIOContext*
+    gpio_start_listening(const LWCDataPin data_pin, bool inverted, void* callback_context);
+void gpio_stop_listening(GPIOContext* context);
+bool gpio_callback_with_event(GPIOContext* context, void (*callback)(GPIOEvent, void*));
+
+#endif

+ 238 - 0
longwave_clock/src/logic_dcf77.c

@@ -0,0 +1,238 @@
+#include "logic_dcf77.h"
+#include "longwave_clock_app.h"
+#include <math.h>
+#include <stdlib.h>
+
+#define BIT_START             0
+#define BIT_WEATHER           1
+#define BIT_CALL              15
+#define BIT_TZCHANGE          16
+#define BIT_CEST              17
+#define BIT_CET               18
+#define BIT_LEAP              19
+#define BIT_TIME_CONSTANT     20
+#define BIT_MINUTES_1s_START  21
+#define BIT_MINUTES_10s_START 25
+#define BIT_MINUTES_CHECKSUM  28
+#define BIT_HOURS_1s_START    29
+#define BIT_HOURS_10s_START   33
+#define BIT_HOURS_CHECKSUM    35
+#define BIT_DOM_1s_START      36
+#define BIT_DOM_10s_START     40
+#define BIT_DOW_START         42
+#define BIT_MONTH_1s_START    45
+#define BIT_MONTH_10s_START   49
+#define BIT_YEAR_1s_START     50
+#define BIT_YEAR_10s_START    54
+#define BIT_DATE_CHECKSUM     58
+
+DecodingPhase dcf77_get_decoding_phase(MinuteData* minute_data) {
+    if(minute_data->index == -1) {
+        return DecodingUnknown;
+    } else if(minute_data->index <= BIT_WEATHER) {
+        return DecodingTime;
+    } else if(minute_data->index <= BIT_CALL) {
+        return DecodingWeather;
+    } else if(minute_data->index == BIT_TZCHANGE) {
+        return DecodingMeta;
+    } else if(minute_data->index <= BIT_DOM_1s_START) {
+        return DecodingTime;
+    } else {
+        return DecodingDate;
+    }
+}
+
+DecodingTimePhase dcf77_get_decoding_time_phase(MinuteData* minute_data) {
+    if(minute_data->index <= BIT_WEATHER) {
+        return DecodingTimeSeconds;
+    } else if(minute_data->index <= BIT_CET + 1) {
+        return DecodingTimeTimezone;
+    } else if(minute_data->index <= BIT_TIME_CONSTANT) {
+        return DecodingNoTime;
+    } else if(minute_data->index == BIT_TIME_CONSTANT + 1) {
+        return DecodingTimeConstant;
+    } else if(minute_data->index <= BIT_MINUTES_10s_START) {
+        return DecodingTimeMinutes1s;
+    } else if(minute_data->index <= BIT_MINUTES_CHECKSUM) {
+        return DecodingTimeMinutes10s;
+    } else if(minute_data->index == BIT_MINUTES_CHECKSUM + 1) {
+        return DecodingTimeMinutes;
+    } else if(minute_data->index <= BIT_HOURS_10s_START) {
+        return DecodingTimeHours1s;
+    } else if(minute_data->index <= BIT_HOURS_CHECKSUM) {
+        return DecodingTimeHours10s;
+    } else {
+        return DecodingTimeHours;
+    }
+}
+
+DecodingDatePhase dcf77_get_decoding_date_phase(MinuteData* minute_data) {
+    if(minute_data->index <= BIT_DOM_10s_START) {
+        return DecodingDateDayOfMonth1s;
+    } else if(minute_data->index <= BIT_DOW_START) {
+        return DecodingDateDayOfMonth10s;
+    } else if(minute_data->index <= BIT_MONTH_1s_START) {
+        return DecodingDateDayOfWeek;
+    } else if(minute_data->index <= BIT_MONTH_10s_START) {
+        return DecodingDateMonth1s;
+    } else if(minute_data->index <= BIT_YEAR_1s_START) {
+        return DecodingDateMonth10s;
+    } else if(minute_data->index <= BIT_YEAR_10s_START) {
+        return DecodingDateYear1s;
+    } else if(minute_data->index <= BIT_DATE_CHECKSUM) {
+        return DecodingDateYear10s;
+    } else if(minute_data->index == BIT_DATE_CHECKSUM + 1) {
+        return DecodingDateDate;
+    } else {
+        return DecodingNoDate;
+    }
+}
+
+Timezone dcf77_decode_timezone(MinuteData* minute_data) {
+    int cest = minute_data->buffer[BIT_CEST];
+    int cet = minute_data->buffer[BIT_CET];
+
+    if(cet == 1 && cest == 0) {
+        return CETTimezone;
+    } else if(cet == 0 && cest == 1) {
+        return CESTTimezone;
+    } else {
+        return UnknownTimezone;
+    }
+}
+
+int dcf77_decode_minutes_1s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_MINUTES_1s_START, BIT_MINUTES_10s_START, 1));
+}
+
+int dcf77_decode_minutes_10s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_MINUTES_10s_START, BIT_MINUTES_CHECKSUM, 1));
+}
+
+int dcf77_get_minutes_checksum(MinuteData* minute_data) {
+    return get_checksum(minute_data, BIT_MINUTES_1s_START, BIT_MINUTES_CHECKSUM + 1, 1);
+}
+
+int dcf77_decode_hours_1s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_HOURS_1s_START, BIT_HOURS_10s_START, 1));
+}
+
+int dcf77_decode_hours_10s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_HOURS_10s_START, BIT_HOURS_CHECKSUM, 1));
+}
+
+int dcf77_get_hours_checksum(MinuteData* minute_data) {
+    return get_checksum(minute_data, BIT_HOURS_1s_START, BIT_HOURS_CHECKSUM + 1, 1);
+}
+
+int dcf77_decode_day_of_month_1s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_DOM_1s_START, BIT_DOM_10s_START, 1));
+}
+
+int dcf77_decode_day_of_month_10s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_DOM_10s_START, BIT_DOW_START, 1));
+}
+
+int dcf77_decode_day_of_week(MinuteData* minute_data) {
+    return get_power2_number(minute_data, BIT_DOW_START, BIT_MONTH_1s_START, 1);
+}
+
+int dcf77_decode_month_1s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_MONTH_1s_START, BIT_MONTH_10s_START, 1));
+}
+
+int dcf77_decode_month_10s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_MONTH_10s_START, BIT_YEAR_1s_START, 1));
+}
+
+int dcf77_decode_year_1s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_YEAR_1s_START, BIT_YEAR_10s_START, 1));
+}
+
+int dcf77_decode_year_10s(MinuteData* minute_data) {
+    return get_if_single_digit(
+        get_power2_number(minute_data, BIT_YEAR_10s_START, BIT_DATE_CHECKSUM, 1));
+}
+
+int dcf77_get_date_checksum(MinuteData* minute_data) {
+    return get_checksum(minute_data, BIT_DOM_1s_START, BIT_DATE_CHECKSUM + 1, 1);
+}
+
+static void set_simulated_dcf77_encoded_number(
+    MinuteData* simulated_data,
+    int start_index,
+    int count_1s,
+    int count_10s,
+    int number) {
+    int number_1s = number % 10;
+    int number_10s = (number - number_1s) / 10;
+
+    int index = start_index;
+
+    for(int i = 0; i < count_1s; i++) {
+        simulated_data->buffer[index] = number_1s & 1;
+        index++;
+        number_1s >>= 1;
+    }
+
+    for(int i = 0; i < count_10s; i++) {
+        simulated_data->buffer[index] = number_10s & 1;
+        index++;
+        number_10s >>= 1;
+    }
+}
+
+void dcf77_set_simulated_minute_data(
+    MinuteData* simulated_data,
+    int seconds,
+    int minutes,
+    int hours,
+    int day_of_week,
+    int day_of_month,
+    int month,
+    int year) {
+    simulated_data->index = seconds;
+    simulated_data->buffer[BIT_START] = 0;
+    for(int i = BIT_START + 1; i < BIT_CALL; i++) {
+        simulated_data->buffer[i] = random() % 2;
+    }
+    simulated_data->buffer[BIT_CALL] = 0;
+    simulated_data->buffer[BIT_TZCHANGE] = 0;
+
+    if(month > 3 && month < 11) {
+        simulated_data->buffer[BIT_CEST] = 1;
+        simulated_data->buffer[BIT_CET] = 0;
+    } else {
+        simulated_data->buffer[BIT_CEST] = 0;
+        simulated_data->buffer[BIT_CET] = 1;
+    }
+    simulated_data->buffer[BIT_LEAP] = 0;
+    simulated_data->buffer[BIT_TIME_CONSTANT] = 1;
+
+    set_simulated_dcf77_encoded_number(simulated_data, BIT_MINUTES_1s_START, 4, 3, minutes);
+
+    simulated_data->buffer[BIT_MINUTES_CHECKSUM] =
+        get_checksum(simulated_data, BIT_MINUTES_1s_START, BIT_MINUTES_CHECKSUM, 1);
+
+    set_simulated_dcf77_encoded_number(simulated_data, BIT_HOURS_1s_START, 4, 2, hours);
+
+    simulated_data->buffer[BIT_HOURS_CHECKSUM] =
+        get_checksum(simulated_data, BIT_HOURS_1s_START, BIT_HOURS_CHECKSUM, 1);
+
+    set_simulated_dcf77_encoded_number(simulated_data, BIT_DOM_1s_START, 4, 2, day_of_month);
+    set_simulated_dcf77_encoded_number(simulated_data, BIT_DOW_START, 3, 0, day_of_week);
+    set_simulated_dcf77_encoded_number(simulated_data, BIT_MONTH_1s_START, 4, 1, month);
+    set_simulated_dcf77_encoded_number(simulated_data, BIT_YEAR_1s_START, 4, 4, year % 100);
+
+    simulated_data->buffer[BIT_DATE_CHECKSUM] =
+        get_checksum(simulated_data, BIT_DOM_1s_START, BIT_DATE_CHECKSUM, 1);
+}

+ 36 - 0
longwave_clock/src/logic_dcf77.h

@@ -0,0 +1,36 @@
+#ifndef LOGIC_DCF77_HEADERS
+#define LOGIC_DCF77_HEADERS
+
+#include "logic_general.h"
+
+DecodingPhase dcf77_get_decoding_phase(MinuteData* minute_data);
+DecodingTimePhase dcf77_get_decoding_time_phase(MinuteData* minute_data);
+DecodingDatePhase dcf77_get_decoding_date_phase(MinuteData* minute_data);
+
+Timezone dcf77_decode_timezone(MinuteData* minute_data);
+int dcf77_decode_minutes_1s(MinuteData* minute_data);
+int dcf77_decode_minutes_10s(MinuteData* minute_data);
+int dcf77_get_minutes_checksum(MinuteData* minute_data);
+int dcf77_decode_hours_1s(MinuteData* minute_data);
+int dcf77_decode_hours_10s(MinuteData* minute_data);
+int dcf77_get_hours_checksum(MinuteData* minute_data);
+int dcf77_decode_year_1s(MinuteData* minute_data);
+int dcf77_decode_year_10s(MinuteData* minute_data);
+int dcf77_decode_month_1s(MinuteData* minute_data);
+int dcf77_decode_month_10s(MinuteData* minute_data);
+int dcf77_decode_day_of_month_1s(MinuteData* minute_data);
+int dcf77_decode_day_of_month_10s(MinuteData* minute_data);
+int dcf77_decode_day_of_week(MinuteData* minute_data);
+int dcf77_get_date_checksum(MinuteData* minute_data);
+
+void dcf77_set_simulated_minute_data(
+    MinuteData* simulated_data,
+    int seconds,
+    int minutes,
+    int hours,
+    int day_of_week,
+    int day_of_month,
+    int month,
+    int year);
+
+#endif

+ 126 - 0
longwave_clock/src/logic_general.c

@@ -0,0 +1,126 @@
+#include "logic_general.h"
+#include <stdlib.h>
+
+static void minute_data_up_index(MinuteData* minute_data) {
+    if(minute_data->index < minute_data->max - 1) {
+        minute_data->index++;
+    } else {
+        minute_data_reset(minute_data);
+    }
+}
+
+void minute_data_start_minute(MinuteData* minute_data) {
+    minute_data->index = 0;
+    minute_data->half_seconds = 0;
+    minute_data->error_count = 0;
+}
+
+MinuteDataError minute_data_500ms_passed(MinuteData* minute_data) {
+    if(minute_data->index >= 0 && minute_data->index < 59) {
+        minute_data->half_seconds++;
+        if(minute_data->half_seconds > minute_data->index * 2 + 1) {
+            minute_data->buffer[minute_data->index] = -1;
+            minute_data_up_index(minute_data);
+            minute_data->error_count++;
+            if(minute_data->error_count > 2) {
+                minute_data_reset(minute_data);
+                return MinuteDataErrorDesync;
+            } else {
+                return MinuteDataErrorUnknownBit;
+            }
+        }
+    }
+    return MinuteDataErrorNone;
+}
+
+void minute_data_add_bit(MinuteData* minute_data, int bit) {
+    if(minute_data->index >= 0) {
+        minute_data->buffer[minute_data->index] = bit;
+        if((bit == 0 || bit == 1) && minute_data->error_count > 0) {
+            minute_data->error_count--;
+        }
+        minute_data_up_index(minute_data);
+    }
+}
+
+int minute_data_get_length(MinuteData* minute_data) {
+    return minute_data->index;
+}
+
+int minute_data_get_bit(MinuteData* minute_data, int position) {
+    return minute_data->buffer[position];
+}
+
+int get_if_single_digit(int value) {
+    if(value < 10) {
+        return value;
+    } else {
+        return -1;
+    }
+}
+
+int get_power2_number(MinuteData* minute_data, int start_index, int stop_index, int increment) {
+    int power2 = 1;
+    int result = 0;
+    int index = start_index;
+
+    do {
+        int value = minute_data->buffer[index];
+        if(value == 0 || value == 1) {
+            result += value * power2;
+        } else {
+            return -1;
+        }
+        index = index + increment;
+        power2 = power2 * 2;
+    } while(index != stop_index);
+
+    return result;
+}
+
+int get_checksum(MinuteData* minute_data, int start_index, int stop_index, int increment) {
+    int result = 0;
+    int index = start_index;
+
+    do {
+        int value = minute_data->buffer[index];
+        if(value == 1) {
+            result = 1 - result;
+        } else if(value == 0) {
+        } else {
+            return -1;
+        }
+        index = index + increment;
+    } while(index != stop_index);
+    return result;
+}
+
+int add_to_checksum(MinuteData* minute_data, int index, int current_checksum) {
+    if(current_checksum == -1) {
+        return -1;
+    } else {
+        int value = minute_data->buffer[index];
+        if(value == 1) {
+            return 1 - current_checksum;
+        } else if(value == 0) {
+            return current_checksum;
+        } else {
+            return -1;
+        }
+    }
+}
+
+void minute_data_reset(MinuteData* minute_data) {
+    minute_data->index = -1;
+}
+
+MinuteData* minute_data_alloc(int max) {
+    MinuteData* minute_data = malloc(sizeof(MinuteData));
+    minute_data_reset(minute_data);
+    minute_data->max = max;
+    return minute_data;
+}
+
+void minute_data_free(MinuteData* minute_data) {
+    free(minute_data);
+}

+ 134 - 0
longwave_clock/src/logic_general.h

@@ -0,0 +1,134 @@
+#ifndef LOGIC_HEADERS
+#define LOGIC_HEADERS
+
+#include <stdint.h>
+#define MINUTE        60
+#define BUFFER        32
+#define MIN_INTERRUPT 3
+
+typedef enum {
+    BitZero,
+    BitOne,
+    BitChecksum,
+    BitChecksumError,
+    BitConstant,
+    BitConstantError,
+    BitEndMinute,
+    BitUnknown,
+    BitEndSync,
+    BitStartEmpty,
+    BitStartWeather,
+    BitStartDUT,
+    BitStartTimezone,
+    BitStartMinute,
+    BitStartHour,
+    BitStartDayOfMonth,
+    BitStartDayOfWeek,
+    BitStartMonth,
+    BitStartYear,
+} Bit;
+
+typedef enum {
+    DecodingUnknown,
+    DecodingIrrelevant,
+    DecodingWeather,
+    DecodingDUT,
+    DecodingTime,
+    DecodingDate,
+    DecodingMeta
+} DecodingPhase;
+
+typedef enum {
+    DecodingNoTime,
+    DecodingTimeTimezone,
+    DecodingTimeMinutes,
+    DecodingTimeMinutes10s,
+    DecodingTimeMinutes1s,
+    DecodingTimeHours,
+    DecodingTimeHours10s,
+    DecodingTimeHours1s,
+    DecodingTimeSeconds,
+    DecodingTimeConstant,
+    DecodingTimeChecksum
+} DecodingTimePhase;
+
+typedef enum {
+    DecodingNoDate,
+    DecodingDateDayOfMonth1s,
+    DecodingDateDayOfMonth10s,
+    DecodingDateDayOfMonth,
+    DecodingDateDayOfWeek,
+    DecodingDateMonth1s,
+    DecodingDateMonth10s,
+    DecodingDateMonth,
+    DecodingDateYear1s,
+    DecodingDateYear10s,
+    DecodingDateYear,
+    DecodingDateCentury,
+    DecodingDateDate,
+    DecodingDateConstant,
+    DecodingDateInYearChecksum,
+    DecodingDateYearChecksum,
+    DecodingDateDayOfWeekChecksum
+} DecodingDatePhase;
+
+typedef enum {
+    UnknownTimezone,
+    UTCTimezone,
+    CETTimezone,
+    CESTTimezone
+} Timezone;
+
+typedef struct {
+    int index;
+    int half_seconds;
+    int error_count;
+    int buffer[MINUTE * 2];
+    int max;
+} MinuteData;
+
+typedef enum {
+    MinuteDataErrorNone,
+    MinuteDataErrorUnknownBit,
+    MinuteDataErrorDesync
+} MinuteDataError;
+
+void minute_data_start_minute(MinuteData* minute_data);
+MinuteDataError minute_data_500ms_passed(MinuteData* minute_data);
+void minute_data_add_bit(MinuteData* minute_data, int bit);
+int minute_data_get_length(MinuteData* minute_data);
+int minute_data_get_bit(MinuteData* minute_data, int position);
+int get_power2_number(MinuteData* minute_data, int start_index, int stop_index, int increment);
+int get_checksum(MinuteData* minute_data, int start_index, int stop_index, int increment);
+int add_to_checksum(MinuteData* minute_data, int index, int current_checksum);
+void minute_data_reset(MinuteData* minute_data);
+int get_if_single_digit(int value);
+
+MinuteData* minute_data_alloc(int max);
+void minute_data_free(MinuteData* minute_data);
+
+typedef struct {
+    MinuteData* minute_data;
+    DecodingPhase decoding;
+    DecodingTimePhase decoding_time;
+    DecodingDatePhase decoding_date;
+    int8_t year_10s;
+    int8_t year_1s;
+    int8_t month_10s;
+    int8_t month_1s;
+    int8_t day_of_month_1s;
+    int8_t day_of_month_10s;
+    int8_t day_of_week;
+    int8_t hours_10s;
+    int8_t hours_1s;
+    int8_t minutes_10s;
+    int8_t minutes_1s;
+    int8_t seconds;
+    Timezone timezone;
+    uint8_t received_interrupt;
+    uint8_t received_count;
+    uint8_t last_received;
+    Bit buffer[BUFFER];
+} LWCViewModel;
+
+#endif

+ 300 - 0
longwave_clock/src/logic_msf.c

@@ -0,0 +1,300 @@
+#include "logic_msf.h"
+
+#define BIT_START             0
+#define BIT_DUT1_PLUS         2
+#define BIT_DUT1_MINUS        18
+#define BIT_YEAR_10s_START    34
+#define BIT_YEAR_1s_START     42
+#define BIT_MONTH_10s_START   50
+#define BIT_MONTH_1s_START    52
+#define BIT_DOM_10s_START     60
+#define BIT_DOM_1s_START      64
+#define BIT_DOW_START         72
+#define BIT_HOURS_10s_START   78
+#define BIT_HOURS_1s_START    82
+#define BIT_MINUTES_10s_START 90
+#define BIT_MINUTES_1s_START  96
+#define BIT_MINUTE_MARKER     104
+#define BIT_STW               106
+#define BIT_YEAR_CHECKSUM     109
+#define BIT_INYEAR_CHECKSUM   111
+#define BIT_DOW_CHECKSUM      113
+#define BIT_TIME_CHECKSUM     115
+#define BIT_SUMMERTIME        117
+#define BIT_END               118
+
+DecodingPhase msf_get_decoding_phase(MinuteData* minute_data) {
+    if(minute_data->index == -1) {
+        return DecodingUnknown;
+    } else {
+        int last_index = minute_data->index - 1;
+
+        if(last_index < BIT_DUT1_PLUS) {
+            return DecodingTime;
+        } else if(last_index < BIT_YEAR_10s_START) {
+            return DecodingDUT;
+        } else if(last_index < BIT_HOURS_10s_START) {
+            return DecodingDate;
+        } else if(last_index < BIT_YEAR_CHECKSUM - 1) {
+            return DecodingTime;
+        } else if(last_index < BIT_TIME_CHECKSUM - 1) {
+            return DecodingDate;
+        } else {
+            return DecodingTime;
+        }
+    }
+}
+
+DecodingTimePhase msf_get_decoding_time_phase(MinuteData* minute_data) {
+    int last_index = minute_data->index - 1;
+
+    if(last_index <= BIT_DUT1_PLUS) {
+        return DecodingTimeSeconds;
+    } else if(last_index < BIT_HOURS_1s_START) {
+        return DecodingTimeHours10s;
+    } else if(last_index < BIT_MINUTES_10s_START) {
+        return DecodingTimeHours1s;
+    } else if(last_index < BIT_MINUTES_1s_START) {
+        return DecodingTimeMinutes10s;
+    } else if(last_index < BIT_MINUTE_MARKER) {
+        return DecodingTimeMinutes1s;
+    } else if(last_index < BIT_YEAR_CHECKSUM - 1) {
+        return DecodingTimeConstant;
+    } else if(last_index < BIT_SUMMERTIME - 1) {
+        return DecodingTimeChecksum;
+    } else if(last_index < BIT_END) {
+        return DecodingTimeTimezone;
+    } else {
+        return DecodingTimeConstant;
+    }
+}
+
+DecodingDatePhase msf_get_decoding_date_phase(MinuteData* minute_data) {
+    int last_index = minute_data->index - 1;
+
+    if(last_index < BIT_YEAR_1s_START) {
+        return DecodingDateYear10s;
+    } else if(last_index < BIT_MONTH_10s_START) {
+        return DecodingDateYear1s;
+    } else if(last_index < BIT_MONTH_1s_START) {
+        return DecodingDateMonth10s;
+    } else if(last_index < BIT_DOM_10s_START) {
+        return DecodingDateMonth1s;
+    } else if(last_index < BIT_DOM_1s_START) {
+        return DecodingDateDayOfMonth10s;
+    } else if(last_index < BIT_DOW_START) {
+        return DecodingDateDayOfMonth1s;
+    } else if(last_index < BIT_HOURS_10s_START) {
+        return DecodingDateDayOfWeek;
+    } else if(last_index < BIT_INYEAR_CHECKSUM - 1) {
+        return DecodingDateYearChecksum;
+    } else if(last_index < BIT_DOW_CHECKSUM - 1) {
+        return DecodingDateInYearChecksum;
+    } else {
+        return DecodingDateDayOfWeekChecksum;
+    }
+}
+
+static int
+    get_if_single_digit_and_0_interlude(MinuteData* minute_data, int start_index, int stop_index) {
+    for(int i = start_index - 1; i > stop_index - 1; i -= 2) {
+        if(minute_data_get_bit(minute_data, i) != 0) {
+            return -1;
+        }
+    }
+    return get_if_single_digit(
+        get_power2_number(minute_data, start_index - 2, stop_index - 2, -2));
+}
+
+int msf_decode_year_10s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(minute_data, BIT_YEAR_1s_START, BIT_YEAR_10s_START);
+}
+
+int msf_decode_year_1s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(
+        minute_data, BIT_MONTH_10s_START, BIT_YEAR_1s_START);
+}
+
+int msf_decode_month_10s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(
+        minute_data, BIT_MONTH_1s_START, BIT_MONTH_10s_START);
+}
+
+int msf_decode_month_1s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(minute_data, BIT_DOM_10s_START, BIT_MONTH_1s_START);
+}
+
+int msf_decode_day_of_month_10s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(minute_data, BIT_DOM_1s_START, BIT_DOM_10s_START);
+}
+
+int msf_decode_day_of_month_1s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(minute_data, BIT_DOW_START, BIT_DOM_1s_START);
+}
+
+int msf_decode_day_of_week(MinuteData* minute_data) {
+    int result =
+        get_if_single_digit_and_0_interlude(minute_data, BIT_HOURS_10s_START, BIT_DOW_START);
+
+    if(result == 0) {
+        result = 7;
+    }
+
+    return result;
+}
+
+int msf_decode_hours_10s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(
+        minute_data, BIT_HOURS_1s_START, BIT_HOURS_10s_START);
+}
+
+int msf_decode_hours_1s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(
+        minute_data, BIT_MINUTES_10s_START, BIT_HOURS_1s_START);
+}
+
+int msf_decode_minutes_10s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(
+        minute_data, BIT_MINUTES_1s_START, BIT_MINUTES_10s_START);
+}
+
+int msf_decode_minutes_1s(MinuteData* minute_data) {
+    return get_if_single_digit_and_0_interlude(
+        minute_data, BIT_MINUTE_MARKER, BIT_MINUTES_1s_START);
+}
+
+Timezone msf_decode_timezone(MinuteData* minute_data) {
+    if(minute_data->buffer[BIT_SUMMERTIME - 1] != 1) {
+        return UnknownTimezone;
+    } else if(minute_data->buffer[BIT_SUMMERTIME] == 1) {
+        return CETTimezone;
+    } else if(minute_data->buffer[BIT_SUMMERTIME] == 0) {
+        return UTCTimezone;
+    } else {
+        return UnknownTimezone;
+    }
+}
+
+int msf_get_year_checksum(MinuteData* minute_data) {
+    if(minute_data->buffer[BIT_YEAR_CHECKSUM - 1] != 1) {
+        return -1;
+    }
+    return add_to_checksum(
+        minute_data,
+        BIT_YEAR_CHECKSUM,
+        get_checksum(minute_data, BIT_YEAR_10s_START, BIT_MONTH_10s_START, 2));
+}
+
+int msf_get_inyear_checksum(MinuteData* minute_data) {
+    if(minute_data->buffer[BIT_INYEAR_CHECKSUM - 1] != 1) {
+        return -1;
+    }
+    return add_to_checksum(
+        minute_data,
+        BIT_INYEAR_CHECKSUM,
+        get_checksum(minute_data, BIT_MONTH_10s_START, BIT_DOW_START, 2));
+}
+
+int msf_get_dow_checksum(MinuteData* minute_data) {
+    if(minute_data->buffer[BIT_DOW_CHECKSUM - 1] != 1) {
+        return -1;
+    }
+    return add_to_checksum(
+        minute_data,
+        BIT_DOW_CHECKSUM,
+        get_checksum(minute_data, BIT_DOW_START, BIT_HOURS_10s_START, 2));
+}
+
+int msf_get_time_checksum(MinuteData* minute_data) {
+    if(minute_data->buffer[BIT_TIME_CHECKSUM - 1] != 1) {
+        return -1;
+    }
+    return add_to_checksum(
+        minute_data,
+        BIT_TIME_CHECKSUM,
+        get_checksum(minute_data, BIT_HOURS_10s_START, BIT_MINUTE_MARKER, 2));
+}
+
+static void set_simulated_msf_encoded_number(
+    MinuteData* simulated_data,
+    int last_index,
+    int count_1s,
+    int count_10s,
+    int number) {
+    int number_1s = number % 10;
+    int number_10s = (number - number_1s) / 10;
+
+    int index = last_index;
+
+    for(int i = 0; i < count_1s; i++) {
+        simulated_data->buffer[index] = number_1s & 1;
+        simulated_data->buffer[index + 1] = 0;
+        index -= 2;
+        number_1s >>= 1;
+    }
+
+    for(int i = 0; i < count_10s; i++) {
+        simulated_data->buffer[index] = number_10s & 1;
+        simulated_data->buffer[index + 1] = 0;
+        index -= 2;
+        number_10s >>= 1;
+    }
+}
+
+void msf_set_simulated_minute_data(
+    MinuteData* simulated_data,
+    int seconds,
+    int minutes,
+    int hours,
+    int day_of_week,
+    int day_of_month,
+    int month,
+    int year) {
+    simulated_data->index = seconds * 2;
+
+    simulated_data->buffer[BIT_START] = 1;
+    simulated_data->buffer[BIT_START + 1] = 1;
+    for(int i = BIT_DUT1_PLUS; i < BIT_YEAR_10s_START; i++) {
+        simulated_data->buffer[i] = 0;
+    }
+    for(int i = BIT_YEAR_10s_START + 1; i < BIT_YEAR_10s_START; i += 2) {
+        simulated_data->buffer[i] = 0;
+    }
+
+    set_simulated_msf_encoded_number(simulated_data, BIT_MONTH_10s_START - 2, 4, 4, year);
+    set_simulated_msf_encoded_number(simulated_data, BIT_DOM_10s_START - 2, 4, 1, month);
+    set_simulated_msf_encoded_number(simulated_data, BIT_DOW_START - 2, 4, 2, day_of_month);
+    if(day_of_week == 7) {
+        day_of_week = 0;
+    }
+    set_simulated_msf_encoded_number(simulated_data, BIT_HOURS_10s_START - 2, 3, 0, day_of_week);
+
+    set_simulated_msf_encoded_number(simulated_data, BIT_MINUTES_10s_START - 2, 4, 2, hours);
+    set_simulated_msf_encoded_number(simulated_data, BIT_MINUTE_MARKER - 2, 4, 3, minutes);
+
+    simulated_data->buffer[BIT_MINUTE_MARKER] = 0;
+    simulated_data->buffer[BIT_MINUTE_MARKER + 1] = 0;
+    simulated_data->buffer[BIT_STW] = 0;
+
+    for(int i = BIT_STW; i < BIT_END; i += 2) {
+        simulated_data->buffer[i] = 1;
+    }
+
+    simulated_data->buffer[BIT_YEAR_CHECKSUM] =
+        get_checksum(simulated_data, BIT_YEAR_10s_START, BIT_MONTH_10s_START, 2) == 1 ? 0 : 1;
+    simulated_data->buffer[BIT_INYEAR_CHECKSUM] =
+        get_checksum(simulated_data, BIT_MONTH_10s_START, BIT_DOW_START, 2) == 1 ? 0 : 1;
+    simulated_data->buffer[BIT_DOW_CHECKSUM] =
+        get_checksum(simulated_data, BIT_DOW_START, BIT_HOURS_10s_START, 2) == 1 ? 0 : 1;
+    simulated_data->buffer[BIT_TIME_CHECKSUM] =
+        get_checksum(simulated_data, BIT_HOURS_10s_START, BIT_MINUTE_MARKER, 2) == 1 ? 0 : 1;
+
+    if(month > 3 && month < 11) {
+        simulated_data->buffer[BIT_SUMMERTIME] = 1;
+    } else {
+        simulated_data->buffer[BIT_SUMMERTIME] = 0;
+    }
+
+    simulated_data->buffer[BIT_END] = 0;
+    simulated_data->buffer[BIT_END + 1] = 0;
+}

+ 37 - 0
longwave_clock/src/logic_msf.h

@@ -0,0 +1,37 @@
+#ifndef LOGIC_MSF_HEADERS
+#define LOGIC_MSF_HEADERS
+
+#include "logic_general.h"
+
+DecodingPhase msf_get_decoding_phase(MinuteData* minute_data);
+DecodingTimePhase msf_get_decoding_time_phase(MinuteData* minute_data);
+DecodingDatePhase msf_get_decoding_date_phase(MinuteData* minute_data);
+
+int msf_decode_year_10s(MinuteData* minute_data);
+int msf_decode_year_1s(MinuteData* minute_data);
+int msf_decode_month_10s(MinuteData* minute_data);
+int msf_decode_month_1s(MinuteData* minute_data);
+int msf_decode_day_of_month_10s(MinuteData* minute_data);
+int msf_decode_day_of_month_1s(MinuteData* minute_data);
+int msf_decode_day_of_week(MinuteData* minute_data);
+int msf_decode_hours_10s(MinuteData* minute_data);
+int msf_decode_hours_1s(MinuteData* minute_data);
+int msf_decode_minutes_10s(MinuteData* minute_data);
+int msf_decode_minutes_1s(MinuteData* minute_data);
+Timezone msf_decode_timezone(MinuteData* minute_data);
+int msf_get_year_checksum(MinuteData* minute_data);
+int msf_get_inyear_checksum(MinuteData* minute_data);
+int msf_get_dow_checksum(MinuteData* minute_data);
+int msf_get_time_checksum(MinuteData* minute_data);
+
+void msf_set_simulated_minute_data(
+    MinuteData* simulated_data,
+    int seconds,
+    int minutes,
+    int hours,
+    int day_of_week,
+    int day_of_month,
+    int month,
+    int year);
+
+#endif

+ 32 - 0
longwave_clock/src/longwave_clock_app.c

@@ -0,0 +1,32 @@
+#include "longwave_clock_app.h"
+#include "app_state.h"
+#include "flipper.h"
+#include "scene_dcf77.h"
+#include "scene_main_menu.h"
+#include "scenes.h"
+
+const char* TAG = "LongWaveClock";
+
+/** entrypoint */
+int32_t longwave_clock_app(void* p) {
+    UNUSED(p);
+    FURI_LOG_I(TAG, "Long Wave Clock app launched");
+
+    FURI_LOG_D(TAG, "Allocating App* and AppState*");
+    App* app = app_alloc();
+    app->state = app_state_alloc();
+
+    FURI_LOG_D(TAG, "Opening GUI record and attaching to it...");
+    Gui* gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    scene_manager_next_scene(app->scene_manager, LWCMainMenuScene);
+
+    FURI_LOG_D(TAG, "Handing over to view dispatcher.");
+    view_dispatcher_run(app->view_dispatcher);
+    FURI_LOG_D(TAG, "Returning from view dispatcher, freeing the app.");
+
+    app_free(app);
+
+    FURI_LOG_I(TAG, "Long Wave Clock app ended.");
+    return 0;
+}

+ 6 - 0
longwave_clock/src/longwave_clock_app.h

@@ -0,0 +1,6 @@
+#ifndef LWC_APP_HEADERS
+#define LWC_APP_HEADERS
+
+extern const char* TAG;
+
+#endif

+ 110 - 0
longwave_clock/src/module_date.c

@@ -0,0 +1,110 @@
+
+#include "module_date.h"
+
+#define RIGHT_DOW     44
+#define LEFT_YEAR     46
+#define WIDTH_YEAR    26
+#define MID_CENTURY   53
+#define MID_YEAR_10s  62
+#define MID_YEAR_1s   69
+#define MID_DATE_S1   74
+#define LEFT_MONTH    76
+#define WIDTH_INYEAR  30
+#define MID_MONTH_10s 79
+#define MID_MONTH_1s  86
+#define MID_DATE_S2   91
+#define MID_DAY_10s   96
+#define MID_DAY_1s    103
+#define DIGIT_HALFW   3
+#define DIGIT_HEIGHT  9
+#define WIDTH_DOW     25
+#define WIDTH_DATE    88
+
+char* day_name[] = {"-   ", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
+
+static void draw_dow_right(Canvas* canvas, uint16_t top, uint16_t right, int8_t day_in_week) {
+    if(day_in_week == -1) {
+        day_in_week = 0;
+    }
+
+    char buf[5];
+    snprintf(buf, sizeof(buf), "%s,", day_name[day_in_week]);
+    canvas_draw_str_aligned(canvas, right, top, AlignRight, AlignTop, buf);
+}
+
+static void draw_digit_centered(Canvas* canvas, uint16_t top, uint16_t mid_left, int8_t digit) {
+    if(digit < 0) {
+        canvas_draw_str_aligned(canvas, mid_left, top, AlignCenter, AlignTop, "-");
+    } else {
+        char buf[4];
+        snprintf(buf, sizeof(buf), "%d", digit);
+        canvas_draw_str_aligned(canvas, mid_left, top, AlignCenter, AlignTop, buf);
+    }
+}
+
+static void draw_selection_digit(Canvas* canvas, uint16_t top, uint16_t mid_left) {
+    canvas_draw_frame(canvas, mid_left - DIGIT_HALFW, top + DIGIT_HEIGHT, DIGIT_HALFW * 2 + 1, 2);
+}
+
+void draw_decoded_date(
+    Canvas* canvas,
+    uint16_t top,
+    DecodingDatePhase selection,
+    int8_t century,
+    int8_t year_10s,
+    int8_t year_1s,
+    int8_t month_10s,
+    int8_t month_1s,
+    int8_t day_in_month_10s,
+    int8_t day_in_month_1s,
+    int8_t day_in_week) {
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, MID_DATE_S1, top, AlignCenter, AlignTop, ".");
+    canvas_draw_str_aligned(canvas, MID_DATE_S2, top, AlignCenter, AlignTop, ".");
+    draw_dow_right(canvas, top, RIGHT_DOW, day_in_week);
+    draw_digit_centered(canvas, top, MID_CENTURY, century);
+    draw_digit_centered(canvas, top, MID_YEAR_10s, year_10s);
+    draw_digit_centered(canvas, top, MID_YEAR_1s, year_1s);
+    draw_digit_centered(canvas, top, MID_MONTH_10s, month_10s);
+    draw_digit_centered(canvas, top, MID_MONTH_1s, month_1s);
+    draw_digit_centered(canvas, top, MID_DAY_10s, day_in_month_10s);
+    draw_digit_centered(canvas, top, MID_DAY_1s, day_in_month_1s);
+
+    switch(selection) {
+    case DecodingDateYear10s:
+        draw_selection_digit(canvas, top, MID_YEAR_10s);
+        break;
+    case DecodingDateYear1s:
+        draw_selection_digit(canvas, top, MID_YEAR_1s);
+        break;
+    case DecodingDateMonth10s:
+        draw_selection_digit(canvas, top, MID_MONTH_10s);
+        break;
+    case DecodingDateMonth1s:
+        draw_selection_digit(canvas, top, MID_MONTH_1s);
+        break;
+    case DecodingDateDayOfMonth10s:
+        draw_selection_digit(canvas, top, MID_DAY_10s);
+        break;
+    case DecodingDateDayOfMonth1s:
+        draw_selection_digit(canvas, top, MID_DAY_1s);
+        break;
+    case DecodingDateDayOfWeek:
+        canvas_draw_frame(canvas, RIGHT_DOW - WIDTH_DOW, top + DIGIT_HEIGHT, WIDTH_DOW, 2);
+        break;
+    case DecodingDateDayOfWeekChecksum:
+        canvas_draw_frame(canvas, RIGHT_DOW - WIDTH_DOW, top + DIGIT_HEIGHT, WIDTH_DOW, 2);
+        break;
+    case DecodingDateDate:
+        canvas_draw_frame(canvas, RIGHT_DOW - WIDTH_DOW, top + DIGIT_HEIGHT, WIDTH_DATE, 2);
+        break;
+    case DecodingDateYearChecksum:
+        canvas_draw_frame(canvas, LEFT_YEAR, top + DIGIT_HEIGHT, WIDTH_YEAR, 2);
+        break;
+    case DecodingDateInYearChecksum:
+        canvas_draw_frame(canvas, LEFT_MONTH, top + DIGIT_HEIGHT, WIDTH_INYEAR, 2);
+        break;
+    default:
+        break;
+    }
+}

+ 21 - 0
longwave_clock/src/module_date.h

@@ -0,0 +1,21 @@
+
+#ifndef MODULE_DATE_HEADERS
+#define MODULE_DATE_HEADERS
+
+#include "flipper.h"
+#include "logic_general.h"
+
+void draw_decoded_date(
+    Canvas* canvas,
+    uint16_t top,
+    DecodingDatePhase selection,
+    int8_t century,
+    int8_t year_10s,
+    int8_t year_1s,
+    int8_t month_10s,
+    int8_t month_1s,
+    int8_t day_in_month_10s,
+    int8_t day_in_month_1s,
+    int8_t day_in_week);
+
+#endif

+ 37 - 0
longwave_clock/src/module_lights.c

@@ -0,0 +1,37 @@
+#include "module_lights.h"
+
+void lwc_app_backlight_on(App* app) {
+    if(!app->state->display_on) {
+        notification_message_block(app->notifications, &sequence_display_backlight_on);
+    }
+}
+
+void lwc_app_backlight_on_persist(App* app) {
+    if(!app->state->display_on) {
+        app->state->display_on = true;
+        notification_message_block(app->notifications, &sequence_display_backlight_enforce_on);
+    }
+}
+
+void lwc_app_backlight_on_reset(App* app) {
+    if(app->state->display_on) {
+        app->state->display_on = false;
+        notification_message_block(app->notifications, &sequence_display_backlight_enforce_auto);
+    }
+}
+
+void lwc_app_led_on_receive_clear(App* app) {
+    notification_message_block(app->notifications, &sequence_blink_blue_10);
+}
+
+void lwc_app_led_on_receive_unknown(App* app) {
+    notification_message_block(app->notifications, &sequence_blink_yellow_100);
+}
+
+void lwc_app_led_on_sync(App* app) {
+    notification_message_block(app->notifications, &sequence_blink_green_100);
+}
+
+void lwc_app_led_on_desync(App* app) {
+    notification_message_block(app->notifications, &sequence_blink_red_100);
+}

+ 15 - 0
longwave_clock/src/module_lights.h

@@ -0,0 +1,15 @@
+#ifndef MODULE_LIGHTS_HEADERS
+#define MODULE_LIGHTS_HEADERS
+
+#include "flipper.h"
+#include "app_state.h"
+
+void lwc_app_backlight_on(App* app);
+void lwc_app_backlight_on_persist(App* app);
+void lwc_app_backlight_on_reset(App* app);
+void lwc_app_led_on_receive_clear(App* app);
+void lwc_app_led_on_receive_unknown(App* app);
+void lwc_app_led_on_sync(App* app);
+void lwc_app_led_on_desync(App* app);
+
+#endif

+ 177 - 0
longwave_clock/src/module_rollbits.c

@@ -0,0 +1,177 @@
+
+#include "module_rollbits.h"
+#include "longwave_clock_icons.h"
+
+#define FRAME_HEIGHT   13
+#define START_HEIGHT   10
+#define MIN_X          2
+#define Y_POS          11
+#define ICON_WIDTH     9
+#define ICON_TOP       0
+#define ICON_BOTTOM    10
+#define ICON_SPACING   4
+#define WIDTH_DIGIT    6
+#define WIDTH_CHECKSUM 2
+#define WIDTH_MINUTE   10
+#define WIDTH_ASYNC    13
+#define WIDTH_START    2
+#define MID_SCREEN     64
+#define SUM_HEIGHT_1   17
+#define SUM_HEIGHT_2   19
+#define SUM_HEIGHT_3   21
+#define STRIKE_HEIGHT  7
+
+char* start_description[] =
+    {"BBK+Weather", "DUT", "TZ", "Minute", "Hour", "Date", "Day", "Month", "Year"};
+
+static void draw_before_start(Canvas* canvas, uint16_t top, const char* str) {
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(
+        canvas, MID_SCREEN, top + FRAME_HEIGHT + START_HEIGHT, AlignCenter, AlignBottom, str);
+    canvas_set_font(canvas, FontKeyboard);
+}
+
+void draw_decoded_bits(
+    Canvas* canvas,
+    LWCType type,
+    uint16_t top,
+    Bit buffer[],
+    uint8_t length_buffer,
+    uint8_t count_signals,
+    uint8_t last_index,
+    bool waiting_for_interrupt,
+    bool waiting_for_sync) {
+    if(waiting_for_interrupt) {
+        draw_before_start(canvas, top, "Waiting first interrupt...");
+    } else if(waiting_for_sync) {
+        draw_before_start(canvas, top, "Waiting for sync...");
+    }
+    uint8_t left = canvas_width(canvas) - ICON_WIDTH;
+    canvas_draw_icon(canvas, left, ICON_TOP, &I_lwc_sender);
+
+    if(type == DCF77) {
+        canvas_draw_icon(canvas, left, ICON_BOTTOM, &I_lwc_dcf);
+    } else if(type == MSF) {
+        canvas_draw_icon(canvas, left, ICON_BOTTOM, &I_lwc_msf);
+    }
+
+    left = left - ICON_SPACING;
+
+    canvas_draw_line(canvas, 0, top, left, top);
+    canvas_draw_line(canvas, 0, top + FRAME_HEIGHT, left, top + FRAME_HEIGHT);
+    canvas_set_font(canvas, FontKeyboard);
+
+    uint8_t index = last_index;
+    uint8_t current_count = count_signals;
+    bool drawn_start = false;
+    bool drawn_end = false;
+
+    while(left > MIN_X + WIDTH_DIGIT && current_count > 0) {
+        Bit bit = buffer[index];
+
+        current_count--;
+        if(index == 0) {
+            index = length_buffer - 1;
+        } else {
+            index--;
+        }
+
+        if(bit < BitChecksum) {
+            left = left - WIDTH_DIGIT;
+            if(bit == BitZero) {
+                canvas_draw_str(canvas, left, Y_POS, "0");
+            } else {
+                canvas_draw_str(canvas, left, Y_POS, "1");
+            }
+        } else if(bit < BitEndMinute) {
+            left = left - WIDTH_CHECKSUM;
+            canvas_draw_line(canvas, left, top, left, top + FRAME_HEIGHT);
+            if(!waiting_for_sync) {
+                if(bit <= BitChecksumError) {
+                    canvas_draw_line(
+                        canvas, left + 2, top + SUM_HEIGHT_2, left + 6, top + SUM_HEIGHT_2);
+                    canvas_draw_line(
+                        canvas, left + 4, top + SUM_HEIGHT_1, left + 4, top + SUM_HEIGHT_3);
+                } else {
+                    canvas_draw_line(
+                        canvas, left + 2, top + SUM_HEIGHT_2, left + 6, top + SUM_HEIGHT_2);
+                    canvas_draw_line(
+                        canvas, left + 2, top + SUM_HEIGHT_3, left + 6, top + SUM_HEIGHT_3);
+                }
+            }
+            if(bit % 2 == 1) {
+                canvas_draw_line(canvas, left, top + STRIKE_HEIGHT, left + 8, top + STRIKE_HEIGHT);
+            }
+        } else if(bit == BitEndMinute) {
+            if(left < MIN_X + WIDTH_MINUTE) {
+                break;
+            }
+            left = left - WIDTH_MINUTE;
+            canvas_draw_box(
+                canvas, left + 1, top, WIDTH_MINUTE - 2, FRAME_HEIGHT + START_HEIGHT + 1);
+            drawn_end = true;
+        } else if(bit == BitEndSync) {
+            if(left < MIN_X + WIDTH_ASYNC) {
+                break;
+            }
+            left = left - WIDTH_ASYNC;
+            canvas_draw_line(canvas, left + 1, top, left + 4, top + 3);
+            canvas_draw_line(canvas, left + WIDTH_ASYNC - 5, top, left + WIDTH_ASYNC - 2, top + 3);
+
+            canvas_draw_line(canvas, left + 4, top + 3, left + 1, top + 6);
+            canvas_draw_line(
+                canvas, left + WIDTH_ASYNC - 2, top + 3, left + WIDTH_ASYNC - 5, top + 6);
+
+            canvas_draw_line(canvas, left + 1, top + 6, left + 4, top + 9);
+            canvas_draw_line(
+                canvas, left + WIDTH_ASYNC - 5, top + 6, left + WIDTH_ASYNC - 2, top + 9);
+
+            canvas_draw_line(canvas, left + 4, top + 9, left + 1, top + 12);
+            canvas_draw_line(
+                canvas, left + WIDTH_ASYNC - 2, top + 9, left + WIDTH_ASYNC - 5, top + 12);
+            drawn_end = true;
+        } else if(bit == BitUnknown) {
+            left = left - WIDTH_DIGIT;
+            canvas_draw_str(canvas, left, Y_POS, "?");
+        } else if(bit >= BitStartEmpty) {
+            left = left - WIDTH_START;
+            uint8_t extra_height;
+            if(bit > BitStartEmpty && !waiting_for_sync) {
+                extra_height = START_HEIGHT;
+                canvas_set_font(canvas, FontSecondary);
+                canvas_draw_str(
+                    canvas,
+                    left + 2,
+                    top + FRAME_HEIGHT + START_HEIGHT,
+                    start_description[bit - BitStartEmpty - 1]);
+                canvas_set_font(canvas, FontKeyboard);
+            } else {
+                extra_height = 0;
+            }
+            canvas_draw_line(canvas, left, top, left, top + FRAME_HEIGHT + extra_height);
+            drawn_start = true;
+        }
+    }
+
+    while(!drawn_start && !drawn_end && !waiting_for_sync && current_count > 0) {
+        Bit bit = buffer[index];
+
+        current_count--;
+        if(index == 0) {
+            index = length_buffer - 1;
+        } else {
+            index--;
+        }
+
+        if(bit == BitEndMinute) {
+            drawn_end = true;
+        } else if(bit >= BitStartEmpty) {
+            canvas_draw_line(canvas, 0, top, 0, top + FRAME_HEIGHT + START_HEIGHT);
+            canvas_set_font(canvas, FontSecondary);
+            canvas_draw_str(
+                canvas, 2, FRAME_HEIGHT + START_HEIGHT, start_description[bit - BitStartEmpty - 1]);
+            canvas_set_font(canvas, FontKeyboard);
+            drawn_start = true;
+        }
+    }
+}

+ 20 - 0
longwave_clock/src/module_rollbits.h

@@ -0,0 +1,20 @@
+
+#ifndef MODULE_ROLLBITS_HEADERS
+#define MODULE_ROLLBITS_HEADERS
+
+#include "flipper.h"
+#include "src/protocols.h"
+#include "logic_general.h"
+
+void draw_decoded_bits(
+    Canvas* canvas,
+    LWCType type,
+    uint16_t top,
+    Bit buffer[],
+    uint8_t length_buffer,
+    uint8_t count_signals,
+    uint8_t last_index,
+    bool waiting_for_interrupt,
+    bool waiting_for_sync);
+
+#endif

+ 135 - 0
longwave_clock/src/module_time.c

@@ -0,0 +1,135 @@
+#include "module_time.h"
+
+#define TIME_DIGIT_HALFW  6
+#define TIME_DIGIT_HEIGHT 15
+#define LEFT_TIME         2
+#define WIDTH_TIME        90
+#define MID_TIME_S1       28
+#define MID_TIME_S2       60
+#define MID_TIME          46
+#define MID_HOURS         14
+#define MID_HOURS_10s     8
+#define MID_HOURS_1s      20
+#define MID_MINUTES       46
+#define MID_MINUTES_10s   40
+#define MID_MINUTES_1s    52
+#define MID_SECONDS       78
+#define RIGHT_TZ          126
+#define TIMEZONE_W        32
+
+static char* timezone_before_repr[] = {"- : - ", "+00:01", "+01:01", "+02:01"};
+static char* timezone_exact_repr[] = {"- : - ", "+00:00", "+01:00", "+02:00"};
+
+static void draw_selection_digit(Canvas* canvas, uint16_t top, uint16_t mid_left) {
+    canvas_draw_frame(
+        canvas, mid_left - TIME_DIGIT_HALFW, top + TIME_DIGIT_HEIGHT, TIME_DIGIT_HALFW * 2, 2);
+}
+
+static void draw_selection_two_digits(Canvas* canvas, uint16_t top, uint16_t mid_left) {
+    canvas_draw_frame(
+        canvas, mid_left - TIME_DIGIT_HALFW * 2, top + TIME_DIGIT_HEIGHT, TIME_DIGIT_HALFW * 4, 2);
+}
+
+static void draw_selection_timezone(Canvas* canvas, uint16_t top, uint16_t right) {
+    canvas_draw_frame(canvas, right - TIMEZONE_W, top + TIME_DIGIT_HEIGHT, TIMEZONE_W, 2);
+}
+
+static void draw_selection_time(Canvas* canvas, uint16_t top, uint16_t right) {
+    canvas_draw_frame(canvas, right, top + TIME_DIGIT_HEIGHT, WIDTH_TIME, 2);
+}
+
+static void draw_digit_centered(Canvas* canvas, uint16_t top, uint16_t mid_left, int8_t digit) {
+    if(digit < 0) {
+        canvas_draw_str_aligned(canvas, mid_left, top, AlignCenter, AlignTop, "-");
+    } else {
+        char buf[4];
+        snprintf(buf, sizeof(buf), "%d", digit);
+        canvas_draw_str_aligned(canvas, mid_left, top, AlignCenter, AlignTop, buf);
+    }
+}
+
+static void
+    draw_two_digits_centered(Canvas* canvas, uint16_t top, uint16_t mid_left, int8_t digits) {
+    if(digits < 0) {
+        canvas_draw_str_aligned(canvas, mid_left, top, AlignCenter, AlignTop, "--");
+    } else {
+        char buf[4];
+        snprintf(buf, sizeof(buf), "%02d", digits);
+        canvas_draw_str_aligned(canvas, mid_left, top, AlignCenter, AlignTop, buf);
+    }
+}
+
+static void draw_timezone(
+    Canvas* canvas,
+    uint16_t top,
+    uint16_t right,
+    Timezone timezone,
+    bool for_next_minute) {
+    canvas_set_font(canvas, FontPrimary);
+
+    if(for_next_minute) {
+        canvas_draw_str_aligned(
+            canvas, right, top, AlignRight, AlignBottom, timezone_before_repr[timezone]);
+    } else {
+        canvas_draw_str_aligned(
+            canvas, right, top, AlignRight, AlignBottom, timezone_exact_repr[timezone]);
+    }
+}
+
+void draw_decoded_time(
+    Canvas* canvas,
+    uint16_t top,
+    DecodingTimePhase selection,
+    int8_t hours_10s,
+    int8_t hours_1s,
+    int8_t minutes_10s,
+    int8_t minutes_1s,
+    int8_t seconds,
+    Timezone timezone,
+    bool for_next_minute) {
+    canvas_set_font(canvas, FontBigNumbers);
+    canvas_draw_str_aligned(canvas, MID_TIME_S1, top, AlignCenter, AlignTop, ":");
+    canvas_draw_str_aligned(canvas, MID_TIME_S2, top, AlignCenter, AlignTop, ":");
+
+    draw_digit_centered(canvas, top, MID_HOURS_10s, hours_10s);
+    draw_digit_centered(canvas, top, MID_HOURS_1s, hours_1s);
+
+    draw_digit_centered(canvas, top, MID_MINUTES_10s, minutes_10s);
+    draw_digit_centered(canvas, top, MID_MINUTES_1s, minutes_1s);
+
+    draw_two_digits_centered(canvas, top, MID_SECONDS, seconds);
+
+    draw_timezone(canvas, top + TIME_DIGIT_HEIGHT - 1, RIGHT_TZ, timezone, for_next_minute);
+
+    switch(selection) {
+    case DecodingTimeTimezone:
+        draw_selection_timezone(canvas, top, RIGHT_TZ);
+        break;
+    case DecodingTimeHours:
+        draw_selection_two_digits(canvas, top, MID_HOURS);
+        break;
+    case DecodingTimeMinutes:
+        draw_selection_two_digits(canvas, top, MID_MINUTES);
+        break;
+    case DecodingTimeSeconds:
+        draw_selection_two_digits(canvas, top, MID_SECONDS);
+        break;
+    case DecodingTimeHours10s:
+        draw_selection_digit(canvas, top, MID_HOURS_10s);
+        break;
+    case DecodingTimeHours1s:
+        draw_selection_digit(canvas, top, MID_HOURS_1s);
+        break;
+    case DecodingTimeMinutes10s:
+        draw_selection_digit(canvas, top, MID_MINUTES_10s);
+        break;
+    case DecodingTimeMinutes1s:
+        draw_selection_digit(canvas, top, MID_MINUTES_1s);
+        break;
+    case DecodingTimeChecksum:
+        draw_selection_time(canvas, top, LEFT_TIME);
+        break;
+    default:
+        break;
+    }
+}

+ 20 - 0
longwave_clock/src/module_time.h

@@ -0,0 +1,20 @@
+
+#ifndef MODULE_TIME_HEADERS
+#define MODULE_TIME_HEADERS
+
+#include "flipper.h"
+#include "logic_general.h"
+
+void draw_decoded_time(
+    Canvas* canvas,
+    uint16_t top,
+    DecodingTimePhase selection,
+    int8_t hours_10s,
+    int8_t hours_1s,
+    int8_t minutes_10s,
+    int8_t minutes_1s,
+    int8_t seconds,
+    Timezone timezone,
+    bool for_next_minute);
+
+#endif

+ 13 - 0
longwave_clock/src/protocols.c

@@ -0,0 +1,13 @@
+#include "protocols.h"
+#include "storage/storage.h"
+
+static char* protocol_names[] = {"DCF77 (DE, 77.5kHz)", "MSF (UK, 60.0kHz)"};
+static char* protocol_config[] = {APP_DATA_PATH("dcf77.conf"), APP_DATA_PATH("msf.conf")};
+
+const char* get_protocol_name(LWCType type) {
+    return protocol_names[type];
+}
+
+const char* get_protocol_config_filename(LWCType type) {
+    return protocol_config[type];
+}

+ 13 - 0
longwave_clock/src/protocols.h

@@ -0,0 +1,13 @@
+#ifndef PROTOCOL_HEADERS
+#define PROTOCOL_HEADERS
+
+typedef enum {
+    DCF77,
+    MSF,
+    __lwc_number_of_protocols
+} LWCType;
+
+const char* get_protocol_name(LWCType type);
+const char* get_protocol_config_filename(LWCType type);
+
+#endif

+ 30 - 0
longwave_clock/src/scene_about.c

@@ -0,0 +1,30 @@
+#include "scene_about.h"
+#include "app_state.h"
+#include "scenes.h"
+#include "flipper.h"
+
+void lwc_about_scene_on_enter(void* context) {
+    App* app = context;
+
+    text_box_set_text(
+        app->about,
+        "Listen to longwave senders transmitting atomic precision time signals, "
+        "either using GPIO and special modules or using demo mode to simulate reception.\n\n"
+        "Made by Andrea Micheloni\n@m7i-org | m7i.org\n\n"
+        "...because it's always time for learning.");
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, LWCAboutView);
+}
+
+bool lwc_about_scene_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+
+    return false;
+}
+
+void lwc_about_scene_on_exit(void* context) {
+    App* app = context;
+
+    text_box_reset(app->about);
+}

+ 10 - 0
longwave_clock/src/scene_about.h

@@ -0,0 +1,10 @@
+#ifndef SCENE_ABOUT_HEADERS
+#define SCENE_ABOUT_HEADERS
+
+#include "flipper.h"
+
+void lwc_about_scene_on_enter(void* context);
+bool lwc_about_scene_on_event(void* context, SceneManagerEvent event);
+void lwc_about_scene_on_exit(void* context);
+
+#endif

+ 557 - 0
longwave_clock/src/scene_dcf77.c

@@ -0,0 +1,557 @@
+#include "scene_dcf77.h"
+#include "app_state.h"
+#include "furi_hal_rtc.h"
+#include "longwave_clock_app.h"
+#include "module_date.h"
+#include "module_lights.h"
+#include "module_rollbits.h"
+#include "module_time.h"
+#include "scenes.h"
+
+#define TOP_BAR             0
+#define TOP_TIME            30
+#define TOP_DATE            53
+#define DURATION_DIFF(x, y) (((x) < (y)) ? ((y) - (x)) : ((x) - (y)))
+
+/** main menu events */
+
+static bool lwc_dcf77_scene_input(InputEvent* input_event, void* context) {
+    UNUSED(input_event);
+    UNUSED(context);
+    return false;
+}
+
+static void lwc_dcf77_scene_draw(Canvas* canvas, void* context) {
+    LWCViewModel* model = context;
+
+    canvas_clear(canvas);
+
+    draw_decoded_bits(
+        canvas,
+        DCF77,
+        TOP_BAR,
+        model->buffer,
+        BUFFER,
+        model->received_count,
+        model->last_received,
+        model->received_interrupt < MIN_INTERRUPT,
+        model->decoding == DecodingUnknown);
+
+    draw_decoded_time(
+        canvas,
+        TOP_TIME,
+        model->decoding_time,
+        model->hours_10s,
+        model->hours_1s,
+        model->minutes_10s,
+        model->minutes_1s,
+        model->seconds,
+        model->timezone,
+        model->seconds > 23);
+
+    draw_decoded_date(
+        canvas,
+        TOP_DATE,
+        model->decoding_date,
+        20,
+        model->year_10s,
+        model->year_1s,
+        model->month_10s,
+        model->month_1s,
+        model->day_of_month_10s,
+        model->day_of_month_1s,
+        model->day_of_week);
+}
+
+static void dcf77_add_gui_bit(LWCViewModel* model, Bit bit) {
+    if(model->last_received == BUFFER - 1) {
+        model->last_received = 0;
+    } else {
+        model->last_received++;
+    }
+    if(model->received_count < BUFFER) {
+        model->received_count++;
+    }
+
+    model->buffer[model->last_received] = bit;
+}
+
+static void dcf77_process_time_bit(LWCViewModel* model, DecodingPhase current_phase) {
+    DecodingTimePhase new_decoding_time;
+    if(current_phase == DecodingTime) {
+        new_decoding_time = dcf77_get_decoding_time_phase(model->minute_data);
+    } else {
+        new_decoding_time = DecodingNoTime;
+    }
+
+    if(model->decoding_time != new_decoding_time) {
+        switch(new_decoding_time) {
+        case DecodingTimeTimezone:
+            dcf77_add_gui_bit(model, BitStartTimezone);
+            break;
+        case DecodingTimeMinutes1s:
+            dcf77_add_gui_bit(model, BitStartMinute);
+            break;
+        case DecodingTimeMinutes10s:
+            dcf77_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingTimeMinutes:
+            if(dcf77_get_minutes_checksum(model->minute_data) != 0) {
+                model->minutes_1s = -1;
+                model->minutes_10s = -1;
+                dcf77_add_gui_bit(model, BitChecksumError);
+            } else {
+                model->minutes_10s = dcf77_decode_minutes_10s(model->minute_data);
+                dcf77_add_gui_bit(model, BitChecksum);
+            }
+            break;
+        case DecodingTimeHours1s:
+            dcf77_add_gui_bit(model, BitStartHour);
+            break;
+        case DecodingTimeHours10s:
+            dcf77_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingTimeHours:
+            if(dcf77_get_hours_checksum(model->minute_data) != 0) {
+                model->hours_1s = -1;
+                model->hours_10s = -1;
+                dcf77_add_gui_bit(model, BitChecksumError);
+            } else {
+                model->hours_10s = dcf77_decode_hours_10s(model->minute_data);
+                dcf77_add_gui_bit(model, BitChecksum);
+            }
+            break;
+        case DecodingTimeConstant:
+            dcf77_add_gui_bit(model, BitConstant);
+            break;
+        case DecodingTimeSeconds:
+            dcf77_add_gui_bit(model, BitConstant);
+            break;
+        default:
+        }
+        switch(model->decoding_time) {
+        case DecodingTimeTimezone:
+            model->timezone = dcf77_decode_timezone(model->minute_data);
+            break;
+        case DecodingTimeMinutes1s:
+            model->minutes_1s = dcf77_decode_minutes_1s(model->minute_data);
+            break;
+        case DecodingTimeHours1s:
+            model->hours_1s = dcf77_decode_hours_1s(model->minute_data);
+            break;
+        default:
+        }
+        model->decoding_time = new_decoding_time;
+    }
+}
+
+static void dcf77_process_date_bit(LWCViewModel* model, DecodingPhase current_phase) {
+    DecodingDatePhase new_decoding_date;
+    if(current_phase == DecodingDate) {
+        new_decoding_date = dcf77_get_decoding_date_phase(model->minute_data);
+    } else {
+        new_decoding_date = DecodingNoDate;
+    }
+
+    if(model->decoding_date != new_decoding_date) {
+        switch(new_decoding_date) {
+        case DecodingDateDayOfMonth1s:
+            dcf77_add_gui_bit(model, BitStartDayOfMonth);
+            break;
+        case DecodingDateDayOfMonth10s:
+            dcf77_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateDayOfWeek:
+            dcf77_add_gui_bit(model, BitStartDayOfWeek);
+            break;
+        case DecodingDateMonth1s:
+            dcf77_add_gui_bit(model, BitStartMonth);
+            break;
+        case DecodingDateMonth10s:
+            dcf77_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateYear1s:
+            dcf77_add_gui_bit(model, BitStartYear);
+            break;
+        case DecodingDateYear10s:
+            dcf77_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateDate:
+            if(dcf77_get_date_checksum(model->minute_data) != 0) {
+                model->year_10s = -1;
+                model->year_1s = -1;
+                model->month_10s = -1;
+                model->month_1s = -1;
+                model->day_of_month_1s = -1;
+                model->day_of_month_10s = -1;
+                model->day_of_week = -1;
+                dcf77_add_gui_bit(model, BitChecksumError);
+            } else {
+                model->year_10s = dcf77_decode_year_10s(model->minute_data);
+                dcf77_add_gui_bit(model, BitChecksum);
+            }
+            break;
+        default:
+        }
+        switch(model->decoding_date) {
+        case DecodingDateDayOfMonth1s:
+            model->day_of_month_1s = dcf77_decode_day_of_month_1s(model->minute_data);
+            break;
+        case DecodingDateDayOfMonth10s:
+            model->day_of_month_10s = dcf77_decode_day_of_month_10s(model->minute_data);
+            break;
+        case DecodingDateDayOfWeek:
+            model->day_of_week = dcf77_decode_day_of_week(model->minute_data);
+            break;
+        case DecodingDateMonth1s:
+            model->month_1s = dcf77_decode_month_1s(model->minute_data);
+            break;
+        case DecodingDateMonth10s:
+            model->month_10s = dcf77_decode_month_10s(model->minute_data);
+            break;
+        case DecodingDateYear1s:
+            model->year_1s = dcf77_decode_year_1s(model->minute_data);
+            break;
+        default:
+        }
+
+        model->decoding_date = new_decoding_date;
+    }
+}
+
+static void dcf77_receive_bit(void* context, int8_t received_bit) {
+    View* view = context;
+
+    FURI_LOG_D(TAG, "Received a %d", received_bit);
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            model->received_interrupt = MIN_INTERRUPT;
+            minute_data_add_bit(model->minute_data, received_bit);
+            int8_t seconds = minute_data_get_length(model->minute_data);
+            if(seconds > 0) {
+                model->seconds = seconds - 1;
+            }
+
+            DecodingPhase old_phase = model->decoding;
+
+            model->decoding = dcf77_get_decoding_phase(model->minute_data);
+            bool change_phase = model->decoding != old_phase;
+            switch(model->decoding) {
+            case DecodingUnknown:
+                if(change_phase) {
+                    model->decoding_time = DecodingNoTime;
+                    model->decoding_date = DecodingNoDate;
+                }
+                break;
+            case DecodingMeta:
+                if(change_phase) {
+                    model->decoding_time = DecodingNoTime;
+                    model->decoding_date = DecodingNoDate;
+                }
+                dcf77_add_gui_bit(model, BitConstant);
+                break;
+            case DecodingWeather:
+                if(change_phase) {
+                    model->decoding_time = DecodingNoTime;
+                    model->decoding_date = DecodingNoDate;
+                    dcf77_add_gui_bit(model, BitStartWeather);
+                }
+                break;
+            case DecodingTime:
+                dcf77_process_time_bit(model, model->decoding);
+                break;
+            case DecodingDate:
+                if(change_phase) {
+                    model->decoding_time = DecodingNoTime;
+                }
+
+                dcf77_process_date_bit(model, model->decoding);
+                break;
+            default:
+                break;
+            }
+            dcf77_add_gui_bit(model, (Bit)received_bit);
+        },
+        true);
+}
+
+static void dcf77_receive_end_of_minute(void* context) {
+    View* view = context;
+
+    FURI_LOG_I(TAG, "Found the DCF77 synchronization, starting a new minute!");
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            dcf77_add_gui_bit(model, BitEndMinute);
+            minute_data_start_minute(model->minute_data);
+            model->decoding = DecodingTime;
+            model->decoding_time = DecodingTimeSeconds;
+        },
+        true);
+}
+
+static void dcf77_receive_desync(void* context) {
+    View* view = context;
+
+    FURI_LOG_I(TAG, "Got a DESYNC error, resetting all!");
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            model->decoding = DecodingUnknown;
+            model->decoding_time = DecodingNoTime;
+            model->decoding_date = DecodingNoDate;
+            dcf77_add_gui_bit(model, BitEndSync);
+        },
+        true);
+}
+
+static void dcf77_receive_unknown(void* context) {
+    View* view = context;
+
+    FURI_LOG_I(TAG, "Not received any bit, assuming a missing receipt!");
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            minute_data_add_bit(model->minute_data, -1);
+            dcf77_add_gui_bit(model, BitUnknown);
+        },
+        true);
+}
+
+static void dcf77_receive_interrupt(void* context) {
+    View* view = context;
+
+    with_view_model(view, LWCViewModel * model, { model->received_interrupt++; }, true);
+}
+
+static void dcf77_receive_gpio(GPIOEvent event, void* context) {
+    App* app = context;
+
+    if(event.shift_up) {
+        if(DURATION_DIFF(event.time_passed_down, 1850) < 100) {
+            scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceiveSync);
+        } else {
+            scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceiveInterrupt);
+        }
+    } else if(event.time_passed_down > 400) {
+        if(DURATION_DIFF(event.time_passed_up, 200) < 50) {
+            scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceive1);
+        } else if(DURATION_DIFF(event.time_passed_up, 100) < 50) {
+            scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceive0);
+        }
+    } else {
+        scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceiveInterrupt);
+    }
+}
+
+bool lwc_dcf77_scene_on_event(void* context, SceneManagerEvent event) {
+    App* app = context;
+    bool consumed = false;
+
+    switch(event.type) {
+    case SceneManagerEventTypeCustom:
+        LWCDCF77Event dcf77_event = event.event;
+
+        switch(dcf77_event) {
+        case LCWDCF77EventReceive0:
+            lwc_app_led_on_receive_clear(app);
+            dcf77_receive_bit(app->dcf77_view, 0);
+            consumed = true;
+            break;
+        case LCWDCF77EventReceive1:
+            lwc_app_led_on_receive_clear(app);
+            dcf77_receive_bit(app->dcf77_view, 1);
+            consumed = true;
+            break;
+        case LCWDCF77EventReceiveSync:
+            lwc_app_led_on_sync(app);
+            dcf77_receive_end_of_minute(app->dcf77_view);
+            consumed = true;
+            break;
+        case LCWDCF77EventReceiveDesync:
+            lwc_app_led_on_desync(app);
+            dcf77_receive_desync(app->dcf77_view);
+            consumed = true;
+            break;
+        case LCWDCF77EventReceiveUnknown:
+            dcf77_receive_unknown(app->dcf77_view);
+            lwc_app_led_on_receive_unknown(app);
+            consumed = true;
+            break;
+        case LCWDCF77EventReceiveInterrupt:
+            dcf77_receive_interrupt(app->dcf77_view);
+            consumed = true;
+            break;
+        default:
+        }
+        break;
+    case SceneManagerEventTypeTick:
+        if(app->state->gpio != NULL) {
+            for(int i = 10;
+                i > 0 && gpio_callback_with_event(app->state->gpio, dcf77_receive_gpio);
+                i--) {
+            }
+        }
+        consumed = true;
+        break;
+    default:
+        consumed = false;
+        break;
+    }
+    return consumed;
+}
+
+static void dcf77_refresh_simulated_data(MinuteData* simulation_data, DateTime datetime) {
+    FURI_LOG_I(TAG, "Setting simulated data for minute %d", datetime.minute);
+
+    dcf77_set_simulated_minute_data(
+        simulation_data,
+        datetime.second,
+        datetime.minute,
+        datetime.hour,
+        datetime.weekday,
+        datetime.day,
+        datetime.month,
+        datetime.year % 100);
+}
+
+static void dcf77_500ms_callback(void* context) {
+    App* app = context;
+
+    if(lwc_get_protocol_config(app->state)->run_mode == Demo) {
+        MinuteData* simulation = app->state->simulation_data;
+        DateTime now;
+        furi_hal_rtc_get_datetime(&now);
+
+        if(now.second < MINUTE - 1 && simulation->index != now.second) {
+            simulation->index++;
+            if(now.second == 0) {
+                dcf77_refresh_simulated_data(app->state->simulation_data, now);
+                scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceiveSync);
+            }
+
+            if(minute_data_get_bit(app->state->simulation_data, now.second) == 0) {
+                scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceive0);
+            } else {
+                scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceive1);
+            }
+        }
+    }
+
+    MinuteDataError result;
+
+    with_view_model(
+        app->dcf77_view,
+        LWCViewModel * model,
+        { result = minute_data_500ms_passed(model->minute_data); },
+        false);
+    switch(result) {
+    case MinuteDataErrorNone:
+        break;
+    case MinuteDataErrorUnknownBit:
+        scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceiveUnknown);
+        break;
+    case MinuteDataErrorDesync:
+        scene_manager_handle_custom_event(app->scene_manager, LCWDCF77EventReceiveDesync);
+        break;
+    }
+}
+
+void lwc_dcf77_scene_on_enter(void* context) {
+    App* app = context;
+    ProtoConfig* config = lwc_get_protocol_config(app->state);
+
+    with_view_model(
+        app->dcf77_view,
+        LWCViewModel * model,
+        {
+            model->decoding = DecodingUnknown;
+            model->decoding_time = DecodingNoTime;
+            model->decoding_date = DecodingNoDate;
+            model->year_10s = -1;
+            model->year_1s = -1;
+            model->month_10s = -1;
+            model->month_1s = -1;
+            model->day_of_month_1s = -1;
+            model->day_of_month_10s = -1;
+            model->day_of_week = -1;
+            model->hours_10s = -1;
+            model->hours_1s = -1;
+            model->minutes_10s = -1;
+            model->minutes_1s = -1;
+            model->seconds = -1;
+            model->timezone = UnknownTimezone;
+            model->last_received = BUFFER - 1;
+            model->received_interrupt = config->run_mode == Demo ? MIN_INTERRUPT : 0;
+            model->received_count = 0;
+            minute_data_reset(model->minute_data);
+        },
+        true);
+    switch(config->run_mode) {
+    case Demo:
+        app->state->simulation_data = minute_data_alloc(60);
+        DateTime now;
+        furi_hal_rtc_get_datetime(&now);
+        dcf77_refresh_simulated_data(app->state->simulation_data, now);
+        break;
+    case GPIO:
+        app->state->gpio =
+            gpio_start_listening(config->data_pin, config->data_mode == Inverted, app);
+        break;
+    default:
+        break;
+    }
+
+    app->state->seconds_timer = furi_timer_alloc(dcf77_500ms_callback, FuriTimerTypePeriodic, app);
+    furi_timer_start(app->state->seconds_timer, furi_kernel_get_tick_frequency() / 2);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, LWCDCF77View);
+}
+
+void lwc_dcf77_scene_on_exit(void* context) {
+    App* app = context;
+
+    notification_message_block(app->notifications, &sequence_display_backlight_enforce_auto);
+    furi_timer_stop(app->state->seconds_timer);
+    furi_timer_free(app->state->seconds_timer);
+
+    switch(lwc_get_protocol_config(app->state)->run_mode) {
+    case Demo:
+        minute_data_free(app->state->simulation_data);
+        break;
+    case GPIO:
+        gpio_stop_listening(app->state->gpio);
+        break;
+    default:
+        break;
+    }
+}
+
+View* lwc_dcf77_scene_alloc() {
+    View* view = view_alloc();
+
+    view_allocate_model(view, ViewModelTypeLocking, sizeof(LWCViewModel));
+    with_view_model(
+        view, LWCViewModel * model, { model->minute_data = minute_data_alloc(60); }, true);
+    view_set_context(view, view);
+    view_set_input_callback(view, lwc_dcf77_scene_input);
+    view_set_draw_callback(view, lwc_dcf77_scene_draw);
+
+    return view;
+}
+
+void lwc_dcf77_scene_free(View* view) {
+    furi_assert(view);
+    with_view_model(view, LWCViewModel * model, { minute_data_free(model->minute_data); }, false);
+
+    view_free(view);
+}

+ 24 - 0
longwave_clock/src/scene_dcf77.h

@@ -0,0 +1,24 @@
+#ifndef SCENE_DCF77_HEADERS
+#define SCENE_DCF77_HEADERS
+
+#include "flipper.h"
+
+#include "logic_dcf77.h"
+
+typedef enum {
+    LCWDCF77EventReceive0,
+    LCWDCF77EventReceive1,
+    LCWDCF77EventReceiveSync,
+    LCWDCF77EventReceiveDesync,
+    LCWDCF77EventReceiveInterrupt,
+    LCWDCF77EventReceiveUnknown
+} LWCDCF77Event;
+
+View* lwc_dcf77_scene_alloc();
+void lwc_dcf77_scene_free(View* view);
+
+void lwc_dcf77_scene_on_enter(void* context);
+bool lwc_dcf77_scene_on_event(void* context, SceneManagerEvent event);
+void lwc_dcf77_scene_on_exit(void* context);
+
+#endif

+ 31 - 0
longwave_clock/src/scene_info.c

@@ -0,0 +1,31 @@
+#include "scene_info.h"
+#include "app_state.h"
+#include "scenes.h"
+
+void lwc_info_scene_on_enter(void* context) {
+    App* app = context;
+
+    text_box_set_text(
+        app->info_text,
+        "Long wave time signal senders broadcast the exact time and date over LW radio. "
+        "If you're in range, you can receive their signals with an inexpensive receiver tuned to their frequency. "
+        "These come with a big enough ferrite core, and some low pass filtering. "
+        "Keep the ferrite core away from the flipper (3-5cm) and away from E/M radiation (at least 1 metre away from a monitor). "
+        "Best reception next to a window or outside with no obstructions. Nights are best. "
+        "Keep the ferrite core level to ground and  perpendicular to the sender if possible.");
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, LWCInfoView);
+}
+
+bool lwc_info_scene_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+
+    return false;
+}
+
+void lwc_info_scene_on_exit(void* context) {
+    App* app = context;
+
+    text_box_reset(app->about);
+}

+ 10 - 0
longwave_clock/src/scene_info.h

@@ -0,0 +1,10 @@
+#ifndef SCENE_INFO_HEADERS
+#define SCENE_INFO_HEADERS
+
+#include "flipper.h"
+
+void lwc_info_scene_on_enter(void* context);
+bool lwc_info_scene_on_event(void* context, SceneManagerEvent event);
+void lwc_info_scene_on_exit(void* context);
+
+#endif

+ 90 - 0
longwave_clock/src/scene_main_menu.c

@@ -0,0 +1,90 @@
+#include "flipper.h"
+#include "protocols.h"
+
+#include "app_state.h"
+#include "scenes.h"
+#include "scene_main_menu.h"
+
+/* main menu scene */
+static LWCMainMenuEvent protocol_event[] = {LWCMainMenuSelectDCF77, LWCMainMenuSelectMSF};
+
+/** main menu callback - sends custom events to the scene manager based on the selection */
+void lwc_menu_callback(void* context, uint32_t index) {
+    App* app = context;
+    switch(index) {
+    case LWCMainMenuDCF77:
+        scene_manager_handle_custom_event(app->scene_manager, LWCMainMenuSelectDCF77);
+        break;
+    case LWCMainMenuMSF:
+        scene_manager_handle_custom_event(app->scene_manager, LWCMainMenuSelectMSF);
+        break;
+    case LWCMainMenuInfo:
+        scene_manager_handle_custom_event(app->scene_manager, LWCMainMenuSelectInfo);
+        break;
+    case LWCMainMenuAbout:
+        scene_manager_handle_custom_event(app->scene_manager, LWCMainMenuSelectAbout);
+        break;
+    }
+}
+
+/** main menu scene - resets the submenu, and gives it content, callbacks and selection enums */
+void lwc_main_menu_scene_on_enter(void* context) {
+    App* app = context;
+
+    if(submenu_get_selected_item(app->main_menu) == 0) {
+        for(uint8_t i = 0; i < __lwc_number_of_protocols; i++) {
+            submenu_add_item(
+                app->main_menu,
+                get_protocol_name((LWCType)(i)),
+                protocol_event[i],
+                lwc_menu_callback,
+                app);
+        }
+        submenu_add_item(
+            app->main_menu, "General infos", LWCMainMenuSelectInfo, lwc_menu_callback, app);
+        submenu_add_item(app->main_menu, "About", LWCMainMenuSelectAbout, lwc_menu_callback, app);
+    }
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, LWCMainMenuView);
+}
+
+/** main menu event handler - switches scene based on the event */
+bool lwc_main_menu_scene_on_event(void* context, SceneManagerEvent event) {
+    App* app = context;
+    bool consumed = false;
+    switch(event.type) {
+    case SceneManagerEventTypeCustom:
+        switch(event.event) {
+        case LWCMainMenuSelectDCF77:
+            app_init_lwc(app, DCF77);
+            scene_manager_next_scene(app->scene_manager, LWCSubMenuScene);
+            consumed = true;
+            break;
+        case LWCMainMenuSelectMSF:
+            app_init_lwc(app, MSF);
+            scene_manager_next_scene(app->scene_manager, LWCSubMenuScene);
+            consumed = true;
+            break;
+        case LWCMainMenuSelectInfo:
+            scene_manager_next_scene(app->scene_manager, LWCInfoScene);
+            consumed = true;
+            break;
+        case LWCMainMenuSelectAbout:
+            scene_manager_next_scene(app->scene_manager, LWCAboutScene);
+            consumed = true;
+            break;
+        }
+        break;
+    default:
+        consumed = false;
+        break;
+    }
+    return consumed;
+}
+
+void lwc_main_menu_scene_on_exit(void* context) {
+    App* app = context;
+    if(submenu_get_selected_item(app->main_menu) == 0) {
+        submenu_reset(app->main_menu);
+    }
+}

+ 24 - 0
longwave_clock/src/scene_main_menu.h

@@ -0,0 +1,24 @@
+#ifndef SCENE_MAIN_MENU_HEADERS
+#define SCENE_MAIN_MENU_HEADERS
+
+#include "flipper.h"
+
+typedef enum {
+    LWCMainMenuDCF77,
+    LWCMainMenuMSF,
+    LWCMainMenuInfo,
+    LWCMainMenuAbout,
+} LWCMainMenuSceneIndex;
+
+typedef enum {
+    LWCMainMenuSelectDCF77,
+    LWCMainMenuSelectMSF,
+    LWCMainMenuSelectInfo,
+    LWCMainMenuSelectAbout,
+} LWCMainMenuEvent;
+
+void lwc_main_menu_scene_on_enter(void* context);
+bool lwc_main_menu_scene_on_event(void* context, SceneManagerEvent event);
+void lwc_main_menu_scene_on_exit(void* context);
+
+#endif

+ 614 - 0
longwave_clock/src/scene_msf.c

@@ -0,0 +1,614 @@
+#include "scene_msf.h"
+#include "app_state.h"
+#include "furi_hal_rtc.h"
+#include "longwave_clock_app.h"
+#include "module_date.h"
+#include "module_lights.h"
+#include "module_rollbits.h"
+#include "module_time.h"
+
+#define TOP_BAR             0
+#define TOP_TIME            30
+#define TOP_DATE            53
+#define DURATION_DIFF(x, y) (((x) < (y)) ? ((y) - (x)) : ((x) - (y)))
+
+/** main menu events */
+
+static bool lwc_msf_scene_input(InputEvent* input_event, void* context) {
+    UNUSED(input_event);
+    UNUSED(context);
+    return false;
+}
+
+static void lwc_msf_scene_draw(Canvas* canvas, void* context) {
+    LWCViewModel* model = context;
+
+    canvas_clear(canvas);
+
+    draw_decoded_bits(
+        canvas,
+        MSF,
+        TOP_BAR,
+        model->buffer,
+        BUFFER,
+        model->received_count,
+        model->last_received,
+        model->received_interrupt < MIN_INTERRUPT,
+        model->decoding == DecodingUnknown);
+
+    draw_decoded_time(
+        canvas,
+        TOP_TIME,
+        model->decoding_time,
+        model->hours_10s,
+        model->hours_1s,
+        model->minutes_10s,
+        model->minutes_1s,
+        model->seconds,
+        model->timezone,
+        model->seconds > 51);
+
+    draw_decoded_date(
+        canvas,
+        TOP_DATE,
+        model->decoding_date,
+        20,
+        model->year_10s,
+        model->year_1s,
+        model->month_10s,
+        model->month_1s,
+        model->day_of_month_10s,
+        model->day_of_month_1s,
+        model->day_of_week);
+}
+
+static void msf_add_gui_bit(LWCViewModel* model, Bit bit) {
+    if(model->last_received == BUFFER - 1) {
+        model->last_received = 0;
+    } else {
+        model->last_received++;
+    }
+    if(model->received_count < BUFFER) {
+        model->received_count++;
+    }
+
+    model->buffer[model->last_received] = bit;
+}
+
+static void msf_process_time_bit(LWCViewModel* model) {
+    DecodingTimePhase old_decoding_time = model->decoding_time;
+    DecodingTimePhase new_decoding_time = msf_get_decoding_time_phase(model->minute_data);
+
+    if(model->decoding_time != new_decoding_time) {
+        switch(old_decoding_time) {
+        case DecodingTimeHours10s:
+            model->hours_10s = msf_decode_hours_10s(model->minute_data);
+            break;
+        case DecodingTimeHours1s:
+            model->hours_1s = msf_decode_hours_1s(model->minute_data);
+            break;
+        case DecodingTimeMinutes10s:
+            model->minutes_10s = msf_decode_minutes_10s(model->minute_data);
+            break;
+        case DecodingTimeMinutes1s:
+            model->minutes_1s = msf_decode_minutes_1s(model->minute_data);
+            break;
+        case DecodingTimeTimezone:
+            model->timezone = msf_decode_timezone(model->minute_data);
+            break;
+        default:
+        }
+        switch(new_decoding_time) {
+        case DecodingTimeTimezone:
+            msf_add_gui_bit(model, BitStartTimezone);
+            break;
+        case DecodingTimeMinutes10s:
+            msf_add_gui_bit(model, BitStartMinute);
+            break;
+        case DecodingTimeMinutes1s:
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingTimeHours10s:
+            msf_add_gui_bit(model, BitStartHour);
+            break;
+        case DecodingTimeHours1s:
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingTimeConstant:
+            msf_add_gui_bit(model, BitConstant);
+            break;
+        case DecodingTimeChecksum:
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        default:
+        }
+
+        model->decoding_time = new_decoding_time;
+    } else if(new_decoding_time == DecodingTimeChecksum) {
+        if(msf_get_time_checksum(model->minute_data) != 1) {
+            model->minutes_1s = -1;
+            model->minutes_10s = -1;
+            model->hours_1s = -1;
+            model->hours_10s = -1;
+            msf_add_gui_bit(model, BitChecksumError);
+        } else {
+            msf_add_gui_bit(model, BitChecksum);
+        }
+    }
+}
+
+static void msf_process_date_bit(LWCViewModel* model, DecodingPhase current_phase) {
+    DecodingDatePhase new_decoding_date;
+    if(current_phase == DecodingDate) {
+        new_decoding_date = msf_get_decoding_date_phase(model->minute_data);
+    } else {
+        new_decoding_date = DecodingNoDate;
+    }
+
+    if(model->decoding_date != new_decoding_date) {
+        switch(new_decoding_date) {
+        case DecodingDateDayOfMonth10s:
+            model->month_1s = msf_decode_month_1s(model->minute_data);
+            msf_add_gui_bit(model, BitStartDayOfMonth);
+            break;
+        case DecodingDateDayOfMonth1s:
+            model->day_of_month_10s = msf_decode_day_of_month_10s(model->minute_data);
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateDayOfWeek:
+            model->day_of_month_1s = msf_decode_day_of_month_1s(model->minute_data);
+            msf_add_gui_bit(model, BitStartDayOfWeek);
+            break;
+        case DecodingDateMonth10s:
+            model->year_1s = msf_decode_year_1s(model->minute_data);
+            msf_add_gui_bit(model, BitStartMonth);
+            break;
+        case DecodingDateMonth1s:
+            model->month_10s = msf_decode_month_10s(model->minute_data);
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateYear10s:
+            msf_add_gui_bit(model, BitStartYear);
+            break;
+        case DecodingDateYear1s:
+            model->year_10s = msf_decode_year_10s(model->minute_data);
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateYearChecksum:
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateInYearChecksum:
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        case DecodingDateDayOfWeekChecksum:
+            msf_add_gui_bit(model, BitStartEmpty);
+            break;
+        default:
+        }
+
+        model->decoding_date = new_decoding_date;
+    } else {
+        switch(new_decoding_date) {
+        case DecodingDateYearChecksum:
+            if(msf_get_year_checksum(model->minute_data) != 1) {
+                model->year_10s = -1;
+                model->year_1s = -1;
+                msf_add_gui_bit(model, BitChecksumError);
+            } else {
+                msf_add_gui_bit(model, BitChecksum);
+            }
+            break;
+        case DecodingDateInYearChecksum:
+            if(msf_get_inyear_checksum(model->minute_data) != 1) {
+                model->month_10s = -1;
+                model->month_1s = -1;
+                model->day_of_month_1s = -1;
+                model->day_of_month_10s = -1;
+                msf_add_gui_bit(model, BitChecksumError);
+            } else {
+                msf_add_gui_bit(model, BitChecksum);
+            }
+            break;
+        case DecodingDateDayOfWeekChecksum:
+            if(msf_get_dow_checksum(model->minute_data) != 1) {
+                model->day_of_week = -1;
+                msf_add_gui_bit(model, BitChecksumError);
+            } else {
+                msf_add_gui_bit(model, BitChecksum);
+            }
+            break;
+        default:
+        }
+    }
+}
+
+static void msf_process_finish_time_phase(LWCViewModel* model, DecodingTimePhase last_time_phase) {
+    UNUSED(last_time_phase);
+    model->decoding_time = DecodingNoTime;
+}
+
+static void msf_process_finish_date_phase(LWCViewModel* model, DecodingDatePhase last_date_phase) {
+    if(last_date_phase == DecodingDateDayOfWeek) {
+        model->day_of_week = msf_decode_day_of_week(model->minute_data);
+    }
+    model->decoding_date = DecodingNoDate;
+}
+
+static void msf_receive_bit(void* context, bool is_b, int8_t received_bit) {
+    View* view = context;
+
+    FURI_LOG_D(TAG, "Received a %d", received_bit);
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            model->received_interrupt = MIN_INTERRUPT;
+            minute_data_add_bit(model->minute_data, received_bit);
+
+            if(!is_b) {
+                int8_t seconds = minute_data_get_length(model->minute_data) / 2;
+                if(seconds > 0) {
+                    model->seconds = seconds;
+                }
+            }
+
+            DecodingPhase old_phase = model->decoding;
+
+            model->decoding = msf_get_decoding_phase(model->minute_data);
+            bool change_phase = model->decoding != old_phase;
+
+            if(change_phase) {
+                switch(old_phase) {
+                case DecodingTime:
+                    msf_process_finish_time_phase(model, model->decoding_time);
+                    break;
+                case DecodingDate:
+                    msf_process_finish_date_phase(model, model->decoding_date);
+                    break;
+                case DecodingUnknown:
+                    model->decoding_time = DecodingNoTime;
+                    model->decoding_date = DecodingNoDate;
+                    break;
+                case DecodingMeta:
+                    model->decoding_time = DecodingNoTime;
+                    model->decoding_date = DecodingNoDate;
+                    msf_add_gui_bit(model, BitConstant);
+                    break;
+                default:
+                    break;
+                }
+                switch(model->decoding) {
+                case DecodingMeta:
+                    msf_add_gui_bit(model, BitConstant);
+                    break;
+                case DecodingDUT:
+                    model->decoding_time = DecodingNoTime;
+                    model->decoding_date = DecodingNoDate;
+                    msf_add_gui_bit(model, BitStartDUT);
+                    break;
+                default:
+                    break;
+                }
+            }
+            switch(model->decoding) {
+            case DecodingTime:
+                msf_process_time_bit(model);
+                break;
+            case DecodingDate:
+                msf_process_date_bit(model, model->decoding);
+                break;
+            default:
+                break;
+            }
+            msf_add_gui_bit(model, (Bit)received_bit % 2);
+        },
+        true);
+}
+
+static void msf_receive_start_of_minute(void* context) {
+    View* view = context;
+
+    FURI_LOG_I(TAG, "Found the MSF synchronization, starting a new minute!");
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            model->received_interrupt = MIN_INTERRUPT;
+            msf_add_gui_bit(model, BitEndMinute);
+            minute_data_start_minute(model->minute_data);
+            minute_data_add_bit(model->minute_data, 1);
+            msf_add_gui_bit(model, BitOne);
+            minute_data_add_bit(model->minute_data, 1);
+            msf_add_gui_bit(model, BitOne);
+            model->seconds = 0;
+        },
+        true);
+}
+
+static void msf_receive_desync(void* context) {
+    View* view = context;
+
+    FURI_LOG_I(TAG, "Got a DESYNC error, resetting all!");
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            model->decoding = DecodingUnknown;
+            model->decoding_time = DecodingNoTime;
+            model->decoding_date = DecodingNoDate;
+            msf_add_gui_bit(model, BitEndSync);
+        },
+        true);
+}
+
+static void msf_receive_unknown(void* context) {
+    View* view = context;
+
+    FURI_LOG_I(TAG, "Not received any bit, assuming a missing receipt!");
+
+    with_view_model(
+        view,
+        LWCViewModel * model,
+        {
+            minute_data_add_bit(model->minute_data, -1);
+            msf_add_gui_bit(model, BitUnknown);
+        },
+        true);
+}
+
+static void msf_receive_interrupt(void* context) {
+    View* view = context;
+
+    with_view_model(view, LWCViewModel * model, { model->received_interrupt++; }, true);
+}
+
+static void msf_receive_gpio(GPIOEvent event, void* context) {
+    App* app = context;
+
+    if(!event.shift_up) {
+        if(event.time_passed_down > 400) { // Decode A after the second marker
+            if(DURATION_DIFF(event.time_passed_up, 100) < 50) { // A0
+                scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveA0);
+            } else if(DURATION_DIFF(event.time_passed_up, 250) < 100) { //A1B0 or A1B1
+                scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveA1);
+            }
+        } else {
+            scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveInterrupt);
+        }
+    } else if(event.time_passed_down > 750) {
+        scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveB0);
+    } else if(
+        DURATION_DIFF(event.time_passed_down, 500) < 50 &&
+        DURATION_DIFF(event.time_passed_up, 500) < 50) {
+        scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveSync);
+    } else if(DURATION_DIFF(event.time_passed_down, 600) < 150) {
+        scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveB1);
+    } else {
+        scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveInterrupt);
+        FURI_LOG_E(
+            TAG,
+            "DOWN up for %lu, last down for %lu",
+            event.time_passed_up,
+            event.time_passed_down);
+    }
+}
+
+bool lwc_msf_scene_on_event(void* context, SceneManagerEvent event) {
+    App* app = context;
+    bool consumed = false;
+
+    switch(event.type) {
+    case SceneManagerEventTypeCustom:
+        LWCMSFEvent msf_event = event.event;
+
+        switch(msf_event) {
+        case LCWMSFEventReceiveA0:
+            lwc_app_led_on_receive_clear(app);
+            msf_receive_bit(app->msf_view, false, 0);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveA1:
+            lwc_app_led_on_receive_clear(app);
+            msf_receive_bit(app->msf_view, false, 1);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveB0:
+            lwc_app_led_on_receive_clear(app);
+            msf_receive_bit(app->msf_view, true, 0);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveB1:
+            lwc_app_led_on_receive_clear(app);
+            msf_receive_bit(app->msf_view, true, 1);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveSync:
+            lwc_app_led_on_sync(app);
+            msf_receive_start_of_minute(app->msf_view);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveDesync:
+            lwc_app_led_on_desync(app);
+            msf_receive_desync(app->msf_view);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveUnknown:
+            msf_receive_unknown(app->msf_view);
+            lwc_app_led_on_receive_unknown(app);
+            consumed = true;
+            break;
+        case LCWMSFEventReceiveInterrupt:
+            msf_receive_interrupt(app->msf_view);
+            consumed = true;
+            break;
+        default:
+        }
+        break;
+    case SceneManagerEventTypeTick:
+        if(app->state->gpio != NULL) {
+            for(int i = 10; i > 0 && gpio_callback_with_event(app->state->gpio, msf_receive_gpio);
+                i--) {
+            }
+        }
+        consumed = true;
+        break;
+    default:
+        consumed = false;
+        break;
+    }
+    return consumed;
+}
+
+static void msf_refresh_simulated_data(MinuteData* simulation_data, DateTime datetime) {
+    FURI_LOG_I(TAG, "Setting simulated data for minute %d", datetime.minute);
+
+    msf_set_simulated_minute_data(
+        simulation_data,
+        datetime.second,
+        datetime.minute,
+        datetime.hour,
+        datetime.weekday,
+        datetime.day,
+        datetime.month,
+        datetime.year % 100);
+}
+
+static void msf_500ms_callback(void* context) {
+    App* app = context;
+
+    if(lwc_get_protocol_config(app->state)->run_mode == Demo) {
+        MinuteData* simulation = app->state->simulation_data;
+        DateTime now;
+        furi_hal_rtc_get_datetime(&now);
+
+        if(now.second < MINUTE && simulation->index != now.second * 2) {
+            simulation->index += 2;
+            if(now.second == 0) {
+                msf_refresh_simulated_data(app->state->simulation_data, now);
+                scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveSync);
+            } else {
+                if(minute_data_get_bit(app->state->simulation_data, now.second * 2) == 0) {
+                    scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveA0);
+                } else {
+                    scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveA1);
+                }
+                if(minute_data_get_bit(app->state->simulation_data, now.second * 2 + 1) == 0) {
+                    scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveB0);
+                } else {
+                    scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveB1);
+                }
+            }
+        }
+    }
+
+    MinuteDataError result;
+
+    with_view_model(
+        app->msf_view,
+        LWCViewModel * model,
+        { result = minute_data_500ms_passed(model->minute_data); },
+        false);
+    switch(result) {
+    case MinuteDataErrorNone:
+        break;
+    case MinuteDataErrorUnknownBit:
+        scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveUnknown);
+        break;
+    case MinuteDataErrorDesync:
+        scene_manager_handle_custom_event(app->scene_manager, LCWMSFEventReceiveDesync);
+        break;
+    }
+}
+
+void lwc_msf_scene_on_enter(void* context) {
+    App* app = context;
+    ProtoConfig* config = lwc_get_protocol_config(app->state);
+
+    with_view_model(
+        app->msf_view,
+        LWCViewModel * model,
+        {
+            model->decoding = DecodingUnknown;
+            model->decoding_time = DecodingNoTime;
+            model->decoding_date = DecodingNoDate;
+            model->year_10s = -1;
+            model->year_1s = -1;
+            model->month_10s = -1;
+            model->month_1s = -1;
+            model->day_of_month_1s = -1;
+            model->day_of_month_10s = -1;
+            model->day_of_week = -1;
+            model->hours_10s = -1;
+            model->hours_1s = -1;
+            model->minutes_10s = -1;
+            model->minutes_1s = -1;
+            model->seconds = -1;
+            model->timezone = UnknownTimezone;
+            model->last_received = BUFFER - 1;
+            model->received_interrupt = config->run_mode == Demo ? MIN_INTERRUPT : 0;
+            model->received_count = 0;
+            minute_data_reset(model->minute_data);
+        },
+        true);
+    switch(config->run_mode) {
+    case Demo:
+        app->state->simulation_data = minute_data_alloc(120);
+        DateTime now;
+        furi_hal_rtc_get_datetime(&now);
+        msf_refresh_simulated_data(app->state->simulation_data, now);
+        break;
+    case GPIO:
+        app->state->gpio =
+            gpio_start_listening(config->data_pin, config->data_mode == Inverted, app);
+        break;
+    default:
+        break;
+    }
+
+    app->state->seconds_timer = furi_timer_alloc(msf_500ms_callback, FuriTimerTypePeriodic, app);
+    furi_timer_start(app->state->seconds_timer, furi_kernel_get_tick_frequency() / 2);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, LWCMSFView);
+}
+
+void lwc_msf_scene_on_exit(void* context) {
+    App* app = context;
+
+    notification_message_block(app->notifications, &sequence_display_backlight_enforce_auto);
+    furi_timer_stop(app->state->seconds_timer);
+    furi_timer_free(app->state->seconds_timer);
+
+    switch(lwc_get_protocol_config(app->state)->run_mode) {
+    case Demo:
+        minute_data_free(app->state->simulation_data);
+        break;
+    case GPIO:
+        gpio_stop_listening(app->state->gpio);
+        break;
+    default:
+        break;
+    }
+}
+
+View* lwc_msf_scene_alloc() {
+    View* view = view_alloc();
+
+    view_allocate_model(view, ViewModelTypeLocking, sizeof(LWCViewModel));
+    with_view_model(
+        view, LWCViewModel * model, { model->minute_data = minute_data_alloc(120); }, true);
+    view_set_context(view, view);
+    view_set_input_callback(view, lwc_msf_scene_input);
+    view_set_draw_callback(view, lwc_msf_scene_draw);
+
+    return view;
+}
+
+void lwc_msf_scene_free(View* view) {
+    furi_assert(view);
+    with_view_model(view, LWCViewModel * model, { minute_data_free(model->minute_data); }, false);
+
+    view_free(view);
+}

+ 25 - 0
longwave_clock/src/scene_msf.h

@@ -0,0 +1,25 @@
+#ifndef SCENE_MSF_HEADERS
+#define SCENE_MSF_HEADERS
+
+#include "flipper.h"
+#include "logic_msf.h"
+
+typedef enum {
+    LCWMSFEventReceiveA0,
+    LCWMSFEventReceiveA1,
+    LCWMSFEventReceiveB0,
+    LCWMSFEventReceiveB1,
+    LCWMSFEventReceiveSync,
+    LCWMSFEventReceiveDesync,
+    LCWMSFEventReceiveInterrupt,
+    LCWMSFEventReceiveUnknown
+} LWCMSFEvent;
+
+View* lwc_msf_scene_alloc();
+void lwc_msf_scene_free(View* view);
+
+void lwc_msf_scene_on_enter(void* context);
+bool lwc_msf_scene_on_event(void* context, SceneManagerEvent event);
+void lwc_msf_scene_on_exit(void* context);
+
+#endif

+ 124 - 0
longwave_clock/src/scene_sub_menu.c

@@ -0,0 +1,124 @@
+#include "flipper.h"
+
+#include "app_state.h"
+#include "scene_sub_menu.h"
+#include "scenes.h"
+#include "module_lights.h"
+
+#define START_ITEM     0
+#define RUN_MODE_ITEM  1
+#define DATA_MODE_ITEM 2
+#define DATA_PIN_ITEM  3
+
+static char* run_mode_names[] = {"demo", "GPIO"};
+static char* data_mode_names[] = {"normal", "inverted"};
+static char* data_pin_names[] = {"A7", "A4", "B2", "C1", "C0"};
+
+#ifdef FW_ORIGIN_Momentum
+static char* GPIO_ONLY = "GPIO mode\nonly!";
+static char* run_mode_start_text[] = {"Start the simulation", "Start the receiver"};
+#else
+static char* start_mode_text = "Start in selected mode";
+#endif
+
+void lwc_run_mode_change_callback(VariableItem* item) {
+    App* app = variable_item_get_context(item);
+    ProtoConfig* config = lwc_get_protocol_config(app->state);
+
+    uint8_t index = variable_item_get_current_value_index(item);
+    config->run_mode = (LWCRunMode)(index);
+    variable_item_set_current_value_text(item, run_mode_names[index]);
+
+#ifdef FW_ORIGIN_Momentum
+    VariableItem* start = variable_item_list_get(app->sub_menu, START_ITEM);
+    VariableItem* data_mode = variable_item_list_get(app->sub_menu, DATA_MODE_ITEM);
+    VariableItem* data_pin = variable_item_list_get(app->sub_menu, DATA_PIN_ITEM);
+
+    variable_item_set_locked(data_mode, (LWCRunMode)(index) == Demo, GPIO_ONLY);
+    variable_item_set_locked(data_pin, (LWCRunMode)(index) == Demo, GPIO_ONLY);
+    variable_item_set_item_label(start, run_mode_start_text[index]);
+#endif
+}
+
+void lwc_data_mode_change_callback(VariableItem* item) {
+    App* app = variable_item_get_context(item);
+    ProtoConfig* config = lwc_get_protocol_config(app->state);
+
+    uint8_t index = variable_item_get_current_value_index(item);
+    config->data_mode = (LWCDataMode)(index);
+    variable_item_set_current_value_text(item, data_mode_names[index]);
+}
+
+void lwc_data_pin_change_callback(VariableItem* item) {
+    App* app = variable_item_get_context(item);
+    ProtoConfig* config = lwc_get_protocol_config(app->state);
+
+    uint8_t index = variable_item_get_current_value_index(item);
+    config->data_pin = (LWCDataPin)(index);
+    variable_item_set_current_value_text(item, data_pin_names[index]);
+}
+
+void lwc_enter_item_callback(void* context, uint32_t index) {
+    App* app = context;
+
+    if(index == START_ITEM) {
+        store_proto_config(app->state);
+        lwc_app_backlight_on_persist(app);
+        scene_manager_next_scene(app->scene_manager, lwc_get_start_scene_for_protocol(app->state));
+    }
+}
+
+void lwc_sub_menu_scene_on_enter(void* context) {
+    App* app = context;
+
+    lwc_app_backlight_on_reset(app);
+
+    ProtoConfig* config = lwc_get_protocol_config(app->state);
+
+#ifdef FW_ORIGIN_Momentum
+    variable_item_list_add(app->sub_menu, run_mode_start_text[config->run_mode], 0, NULL, app);
+#else
+    variable_item_list_add(app->sub_menu, start_mode_text, 0, NULL, app);
+#endif
+
+    variable_item_list_set_enter_callback(app->sub_menu, lwc_enter_item_callback, app);
+
+    VariableItem* run_mode = variable_item_list_add(
+        app->sub_menu, "Run mode", __lwc_number_of_run_modes, lwc_run_mode_change_callback, app);
+
+    variable_item_set_current_value_index(run_mode, config->run_mode);
+    variable_item_set_current_value_text(run_mode, run_mode_names[config->run_mode]);
+
+    VariableItem* data_mode = variable_item_list_add(
+        app->sub_menu, "GPIO data", __lwc_number_of_data_modes, lwc_data_mode_change_callback, app);
+
+    variable_item_set_current_value_index(data_mode, config->data_mode);
+    variable_item_set_current_value_text(data_mode, data_mode_names[config->data_mode]);
+
+    VariableItem* data_pin = variable_item_list_add(
+        app->sub_menu, "Data pin", __lwc_number_of_data_pins, lwc_data_pin_change_callback, app);
+
+    variable_item_set_current_value_index(data_pin, config->data_pin);
+    variable_item_set_current_value_text(data_pin, data_pin_names[config->data_pin]);
+
+#ifdef FW_ORIGIN_Momentum
+    variable_item_set_locked(data_mode, config->run_mode == Demo, GPIO_ONLY);
+    variable_item_set_locked(data_pin, config->run_mode == Demo, GPIO_ONLY);
+
+    variable_item_list_set_header(app->sub_menu, get_protocol_name(app->state->lwc_type));
+#endif
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, LWCSubMenuView);
+}
+
+/** main menu event handler - switches scene based on the event */
+bool lwc_sub_menu_scene_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void lwc_sub_menu_scene_on_exit(void* context) {
+    App* app = context;
+    variable_item_list_reset(app->sub_menu);
+}

+ 10 - 0
longwave_clock/src/scene_sub_menu.h

@@ -0,0 +1,10 @@
+#ifndef SCENE_SUB_MENU_HEADERS
+#define SCENE_SUB_MENU_HEADERS
+
+#include "flipper.h"
+
+void lwc_sub_menu_scene_on_enter(void* context);
+bool lwc_sub_menu_scene_on_event(void* context, SceneManagerEvent event);
+void lwc_sub_menu_scene_on_exit(void* context);
+
+#endif

+ 59 - 0
longwave_clock/src/scenes.c

@@ -0,0 +1,59 @@
+#include "flipper.h"
+#include "app_state.h"
+#include "scenes.h"
+#include "scene_main_menu.h"
+
+/** collection of all scene on_enter handlers */
+void (*const lwc_scene_on_enter_handlers[])(void*) = {
+    lwc_main_menu_scene_on_enter,
+    lwc_sub_menu_scene_on_enter,
+    lwc_dcf77_scene_on_enter,
+    lwc_msf_scene_on_enter,
+    lwc_info_scene_on_enter,
+    lwc_about_scene_on_enter};
+
+/** collection of all scene on event handlers */
+bool (*const lwc_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
+    lwc_main_menu_scene_on_event,
+    lwc_sub_menu_scene_on_event,
+    lwc_dcf77_scene_on_event,
+    lwc_msf_scene_on_event,
+    lwc_info_scene_on_event,
+    lwc_about_scene_on_event};
+
+/** collection of all scene on exit handlers */
+void (*const lwc_scene_on_exit_handlers[])(void*) = {
+    lwc_main_menu_scene_on_exit,
+    lwc_sub_menu_scene_on_exit,
+    lwc_dcf77_scene_on_exit,
+    lwc_msf_scene_on_exit,
+    lwc_info_scene_on_exit,
+    lwc_about_scene_on_exit};
+
+/** collection of all on_enter, on_event, on_exit handlers */
+const SceneManagerHandlers lwc_scene_manager_handlers = {
+    .on_enter_handlers = lwc_scene_on_enter_handlers,
+    .on_event_handlers = lwc_scene_on_event_handlers,
+    .on_exit_handlers = lwc_scene_on_exit_handlers,
+    .scene_num = __lwc_number_of_scenes};
+
+/* callbacks */
+
+/** custom event handler */
+bool lwc_custom_callback(void* context, uint32_t custom_event) {
+    furi_assert(context);
+    App* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, custom_event);
+}
+
+bool lwc_back_event_callback(void* context) {
+    furi_assert(context);
+    App* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+void lwc_tick_event_callback(void* context) {
+    furi_assert(context);
+    App* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}

+ 44 - 0
longwave_clock/src/scenes.h

@@ -0,0 +1,44 @@
+#ifndef SCENE_HEADERS
+#define SCENE_HEADERS
+
+#include "flipper.h"
+#include "protocols.h"
+
+#include "scene_main_menu.h"
+#include "scene_sub_menu.h"
+#include "scene_about.h"
+#include "scene_info.h"
+#include "scene_dcf77.h"
+#include "scene_msf.h"
+
+/** The current scene */
+typedef enum {
+    LWCMainMenuScene,
+    LWCSubMenuScene,
+    LWCDCF77Scene,
+    LWCMSFScene,
+    LWCInfoScene,
+    LWCAboutScene,
+    __lwc_number_of_scenes
+} LWCScene;
+
+/** The current view */
+typedef enum {
+    LWCMainMenuView,
+    LWCSubMenuView,
+    LWCDCF77View,
+    LWCMSFView,
+    LWCInfoView,
+    LWCAboutView
+} LWCView;
+
+extern void (*const lwc_scene_on_enter_handlers[])(void*);
+extern bool (*const lwc_scene_on_event_handlers[])(void*, SceneManagerEvent);
+extern void (*const lwc_scene_on_exit_handlers[])(void*);
+extern const SceneManagerHandlers lwc_scene_manager_handlers;
+
+bool lwc_custom_callback(void* context, uint32_t custom_event);
+bool lwc_back_event_callback(void* context);
+void lwc_tick_event_callback(void* context);
+
+#endif