Sfoglia il codice sorgente

update apps and add new apps

MX 2 anni fa
parent
commit
06a12df3dd
94 ha cambiato i file con 4751 aggiunte e 734 eliminazioni
  1. 3 1
      ReadMe.md
  2. BIN
      apps/GPIO/gpio_controller.fap
  3. BIN
      apps/GPIO/ublox.fap
  4. BIN
      apps/Games/chess.fap
  5. BIN
      apps/NFC/seader.fap
  6. BIN
      apps/NFC/vb_migrate.fap
  7. BIN
      apps/RFID/key_generator.fap
  8. BIN
      apps/Sub-GHz/esubghz_chat.fap
  9. BIN
      apps/Tools/vb_migrate.fap
  10. 1 1
      apps_source_code/fz-em4100-generator/README.md
  11. 1 1
      apps_source_code/fz-em4100-generator/application.fam
  12. 6 2
      apps_source_code/fz-em4100-generator/key_generator.c
  13. 2 2
      non_catalog_apps/chess/application.fam
  14. 1 1
      non_catalog_apps/chess/flipchess.h
  15. BIN
      non_catalog_apps/chess/flipchess_10px.png
  16. 111 101
      non_catalog_apps/chess/views/flipchess_scene_1.c
  17. 14 9
      non_catalog_apps/esubghz_chat/README.md
  18. 1 1
      non_catalog_apps/esubghz_chat/application.fam
  19. 0 0
      non_catalog_apps/esubghz_chat/assets/chat_10px.png
  20. 105 0
      non_catalog_apps/esubghz_chat/crypto_wrapper.c
  21. 35 0
      non_catalog_apps/esubghz_chat/crypto_wrapper.h
  22. 130 584
      non_catalog_apps/esubghz_chat/esubghz_chat.c
  23. 101 0
      non_catalog_apps/esubghz_chat/esubghz_chat_i.h
  24. 65 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_chat_box.c
  25. 123 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_chat_input.c
  26. 127 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_freq_input.c
  27. 90 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_hex_key_input.c
  28. 110 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_display.c
  29. 172 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_menu.c
  30. 126 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_pass_input.c
  31. 30 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_scene.c
  32. 29 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_scene.h
  33. 7 0
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_scene_config.h
  34. 5 0
      non_catalog_apps/flipperzero_vb_migrate/CHANGES.md
  35. 25 0
      non_catalog_apps/flipperzero_vb_migrate/README_catalog.md
  36. 2 7
      non_catalog_apps/flipperzero_vb_migrate/application.fam
  37. 1 1
      non_catalog_apps/flipperzero_vb_migrate/vb_migrate_i.h
  38. 3 0
      non_catalog_apps/gpio_controller/README.md
  39. 11 0
      non_catalog_apps/gpio_controller/application.fam
  40. 239 0
      non_catalog_apps/gpio_controller/gpio_controller.c
  41. 69 0
      non_catalog_apps/gpio_controller/gpio_items.c
  42. 29 0
      non_catalog_apps/gpio_controller/gpio_items.h
  43. BIN
      non_catalog_apps/gpio_controller/icon10px.png
  44. BIN
      non_catalog_apps/gpio_controller/images/1w_pin.png
  45. BIN
      non_catalog_apps/gpio_controller/images/3v_pin.png
  46. BIN
      non_catalog_apps/gpio_controller/images/5v_pin.png
  47. BIN
      non_catalog_apps/gpio_controller/images/a4_pin.png
  48. BIN
      non_catalog_apps/gpio_controller/images/a6_pin.png
  49. BIN
      non_catalog_apps/gpio_controller/images/a7_pin.png
  50. BIN
      non_catalog_apps/gpio_controller/images/arrow_down.png
  51. BIN
      non_catalog_apps/gpio_controller/images/arrow_up.png
  52. BIN
      non_catalog_apps/gpio_controller/images/b2_pin.png
  53. BIN
      non_catalog_apps/gpio_controller/images/b3_pin.png
  54. BIN
      non_catalog_apps/gpio_controller/images/c0_pin.png
  55. BIN
      non_catalog_apps/gpio_controller/images/c1_pin.png
  56. BIN
      non_catalog_apps/gpio_controller/images/c3_pin.png
  57. BIN
      non_catalog_apps/gpio_controller/images/gear_highlighted.png
  58. BIN
      non_catalog_apps/gpio_controller/images/gear_unhighlighted.png
  59. BIN
      non_catalog_apps/gpio_controller/images/gnd_pin.png
  60. BIN
      non_catalog_apps/gpio_controller/images/rx_pin.png
  61. BIN
      non_catalog_apps/gpio_controller/images/sio_pin.png
  62. BIN
      non_catalog_apps/gpio_controller/images/swc_pin.png
  63. BIN
      non_catalog_apps/gpio_controller/images/tx_pin.png
  64. 1 1
      non_catalog_apps/seader/application.fam
  65. 8 11
      non_catalog_apps/seader/readme.md
  66. 5 11
      non_catalog_apps/seader/scenes/seader_scene_card_menu.c
  67. 58 0
      non_catalog_apps/ublox/.gitignore
  68. 674 0
      non_catalog_apps/ublox/LICENSE
  69. 2 0
      non_catalog_apps/ublox/README.md
  70. 17 0
      non_catalog_apps/ublox/application.fam
  71. 5 0
      non_catalog_apps/ublox/helpers/ublox_custom_event.h
  72. 48 0
      non_catalog_apps/ublox/helpers/ublox_types.h
  73. 191 0
      non_catalog_apps/ublox/images/.clang-format
  74. BIN
      non_catalog_apps/ublox/images/ublox_wiring.png
  75. BIN
      non_catalog_apps/ublox/images/ublox_wiring.xcf
  76. 30 0
      non_catalog_apps/ublox/scenes/ublox_scene.c
  77. 29 0
      non_catalog_apps/ublox/scenes/ublox_scene.h
  78. 51 0
      non_catalog_apps/ublox/scenes/ublox_scene_about.c
  79. 5 0
      non_catalog_apps/ublox/scenes/ublox_scene_config.h
  80. 132 0
      non_catalog_apps/ublox/scenes/ublox_scene_data_display.c
  81. 346 0
      non_catalog_apps/ublox/scenes/ublox_scene_data_display_config.c
  82. 57 0
      non_catalog_apps/ublox/scenes/ublox_scene_start.c
  83. 22 0
      non_catalog_apps/ublox/scenes/ublox_scene_wiring.c
  84. 102 0
      non_catalog_apps/ublox/ublox.c
  85. 3 0
      non_catalog_apps/ublox/ublox.h
  86. BIN
      non_catalog_apps/ublox/ublox_app_icon.png
  87. 141 0
      non_catalog_apps/ublox/ublox_device.c
  88. 140 0
      non_catalog_apps/ublox/ublox_device.h
  89. 57 0
      non_catalog_apps/ublox/ublox_i.h
  90. 481 0
      non_catalog_apps/ublox/ublox_worker.c
  91. 41 0
      non_catalog_apps/ublox/ublox_worker.h
  92. 27 0
      non_catalog_apps/ublox/ublox_worker_i.h
  93. 271 0
      non_catalog_apps/ublox/views/data_display_view.c
  94. 33 0
      non_catalog_apps/ublox/views/data_display_view.h

+ 3 - 1
ReadMe.md

@@ -20,7 +20,7 @@ Sources of "integrated/bundled" apps are added now in this repo too, to allow pu
 
 The Flipper and its community wouldn't be as rich as it is without your contributions and support. Thank you for all you have done.
 
-### Apps checked & updated at `25 Jul 04:13 GMT +3`
+### Apps checked & updated at `29 Jul 05:07 GMT +3`
 
 ## Games
 - [Pong (By nmrr)](https://github.com/nmrr/flipperzero-pong) - Modified by [SimplyMinimal](https://github.com/SimplyMinimal/FlipperZero-Pong)
@@ -100,6 +100,8 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 - [Evil captive portal (By bigbrodude6119)](https://github.com/bigbrodude6119/flipper-zero-evil-portal) - WIP
 - [ESP Flasher (By 0xchocolate)](https://github.com/0xchocolate/flipperzero-esp-flasher)
 - [ESP32-C6 Gravity terminal (By chris-bc)](https://github.com/chris-bc/Flipper-Gravity)
+- [u-blox GPS (By liamhays)](https://github.com/liamhays/ublox)
+- [GPIO Controller (By Lokno)](https://github.com/Lokno/gpio_controller) -> `A visual tool to control the general purpose pins of the Flipper Zero`
 
 ## Tools / Misc / NFC / RFID / Infrared / etc..
 - [Calculator (By n-o-T-I-n-s-a-n-e)](https://github.com/n-o-T-I-n-s-a-n-e)

BIN
apps/GPIO/gpio_controller.fap


BIN
apps/GPIO/ublox.fap


BIN
apps/Games/chess.fap


BIN
apps/NFC/seader.fap


BIN
apps/NFC/vb_migrate.fap


BIN
apps/RFID/key_generator.fap


BIN
apps/Sub-GHz/esubghz_chat.fap


BIN
apps/Tools/vb_migrate.fap


+ 1 - 1
apps_source_code/fz-em4100-generator/README.md

@@ -1,4 +1,4 @@
-# fz-em4100-generator [![FlipC.org](https://flipc.org/Milk-Cool/fz-em4100-generator/badge)](https://flipc.org/Milk-Cool/fz-em4100-generator)
+# fz-em4100-generator
 A program that generates universal keys from a EM4100 key
 
 ## Installing

+ 1 - 1
apps_source_code/fz-em4100-generator/application.fam

@@ -10,6 +10,6 @@ App(
     fap_icon="icon.png",
     fap_author="@Milk-Cool",
     fap_weburl="https://github.com/Milk-Cool/fz-em4100-generator",
-    fap_version="1.0",
+    fap_version="1.1",
     fap_description="Generates EM4100 key lists from selected rfid key file for RFID fuzzer app",
 )

+ 6 - 2
apps_source_code/fz-em4100-generator/key_generator.c

@@ -12,6 +12,9 @@
 
 #include "key_generator_icons.h"
 
+#define DIR_PATH "/ext/rfidfuzzer"
+#define FILE_PATH "/ext/rfidfuzzer/generated.txt"
+
 FuriString* file_path;
 FuriString* key;
 
@@ -74,9 +77,10 @@ int32_t key_generator_main(void* p) {
         flipper_format_file_close(format);
         flipper_format_free(format);
 
+        if(!storage_dir_exists(storage, DIR_PATH)) storage_simply_mkdir(storage, DIR_PATH);
+
         File* file = storage_file_alloc(storage);
-        bool ok =
-            storage_file_open(file, "/ext/rfidfuzzer/generated.txt", FSAM_WRITE, FSOM_OPEN_ALWAYS);
+        bool ok = storage_file_open(file, FILE_PATH, FSAM_WRITE, FSOM_OPEN_ALWAYS);
         if(ok) {
             storage_file_write(file, (uint8_t*)furi_string_get_cstr(key), 2);
             storage_file_write(file, (uint8_t*)"11111111\r\n", 10);

+ 2 - 2
non_catalog_apps/chess/application.fam

@@ -14,6 +14,6 @@ App(
     fap_category="Games",
     fap_author="Struan Clark (xtruan)",
     fap_weburl="https://github.com/xtruan/flipper-chess",
-    fap_version=(1, 6),
+    fap_version=(1, 8),
     fap_description="Chess for Flipper",
-)
+)

+ 1 - 1
non_catalog_apps/chess/flipchess.h

@@ -16,7 +16,7 @@
 #include "views/flipchess_startscreen.h"
 #include "views/flipchess_scene_1.h"
 
-#define FLIPCHESS_VERSION "v1.6.0"
+#define FLIPCHESS_VERSION "v1.8.0"
 
 #define TEXT_BUFFER_SIZE 96
 #define TEXT_SIZE (TEXT_BUFFER_SIZE - 1)

BIN
non_catalog_apps/chess/flipchess_10px.png


+ 111 - 101
non_catalog_apps/chess/views/flipchess_scene_1.c

@@ -217,118 +217,128 @@ uint8_t flipchess_turn(FlipChessScene1Model* model) {
 
     if(model->game.state != SCL_GAME_STATE_PLAYING) {
         model->paramExit = FlipChessStatusNone;
-        return model->paramExit;
-    }
-
-    char movePromote = 'q';
-
-    if(flipchess_isPlayerTurn(model)) {
-        // if(stringsEqual(string, "undo", 5))
-        //     moveType = FlipChessStatusMoveUndo;
-        // else if(stringsEqual(string, "quit", 5))
-        //     break;
-
-        if(model->turnState == 0 && model->squareSelected != 255) {
-            model->squareFrom = model->squareSelected;
-            model->turnState = 1;
-        } else if(model->turnState == 1 && model->squareSelected != 255) {
-            model->squareTo = model->squareSelected;
-            model->turnState = 2;
-            model->squareSelectedLast = model->squareSelected;
-            //model->squareSelected = 255;
-        }
 
-        if(model->turnState == 1 && model->squareFrom != 255) {
-            if((model->game.board[model->squareFrom] != '.') &&
-               (SCL_pieceIsWhite(model->game.board[model->squareFrom]) ==
-                SCL_boardWhitesTurn(model->game.board))) {
-                SCL_boardGetMoves(model->game.board, model->squareFrom, model->moveHighlight);
+    } else {
+        char movePromote = 'q';
+
+        if(flipchess_isPlayerTurn(model)) {
+            // if(stringsEqual(string, "undo", 5))
+            //     moveType = FlipChessStatusMoveUndo;
+            // else if(stringsEqual(string, "quit", 5))
+            //     break;
+
+            if(model->turnState == 0 && model->squareSelected != 255) {
+                model->squareFrom = model->squareSelected;
+                model->turnState = 1;
+            } else if(model->turnState == 1 && model->squareSelected != 255) {
+                model->squareTo = model->squareSelected;
+                model->turnState = 2;
+                model->squareSelectedLast = model->squareSelected;
+                //model->squareSelected = 255;
             }
-        } else if(model->turnState == 2) {
-            if(SCL_squareSetContains(model->moveHighlight, model->squareTo)) {
-                moveType = FlipChessStatusMovePlayer;
+
+            if(model->turnState == 1 && model->squareFrom != 255) {
+                if((model->game.board[model->squareFrom] != '.') &&
+                   (SCL_pieceIsWhite(model->game.board[model->squareFrom]) ==
+                    SCL_boardWhitesTurn(model->game.board))) {
+                    SCL_boardGetMoves(model->game.board, model->squareFrom, model->moveHighlight);
+                }
+            } else if(model->turnState == 2) {
+                if(SCL_squareSetContains(model->moveHighlight, model->squareTo)) {
+                    moveType = FlipChessStatusMovePlayer;
+                }
+                model->turnState = 0;
+                SCL_squareSetClear(model->moveHighlight);
             }
+
+        } else {
+            model->squareSelected = 255;
+            flipchess_makeAIMove(
+                model->game.board, &(model->squareFrom), &(model->squareTo), &movePromote, model);
+            moveType = FlipChessStatusMoveAI;
             model->turnState = 0;
+        }
+
+        if(moveType == FlipChessStatusMovePlayer || moveType == FlipChessStatusMoveAI) {
+            flipchess_shiftMessages(model);
+
+            SCL_moveToString(
+                model->game.board,
+                model->squareFrom,
+                model->squareTo,
+                movePromote,
+                model->moveString);
+
+            SCL_gameMakeMove(&(model->game), model->squareFrom, model->squareTo, movePromote);
+
+            SCL_squareSetClear(model->moveHighlight);
+            SCL_squareSetAdd(model->moveHighlight, model->squareFrom);
+            SCL_squareSetAdd(model->moveHighlight, model->squareTo);
+        } else if(moveType == FlipChessStatusMoveUndo) {
+            flipchess_shiftMessages(model);
+
+            if(model->paramPlayerW != 0 || model->paramPlayerB != 0)
+                SCL_gameUndoMove(&(model->game));
+
+            SCL_gameUndoMove(&(model->game));
             SCL_squareSetClear(model->moveHighlight);
         }
 
-    } else {
-        model->squareSelected = 255;
-        flipchess_makeAIMove(
-            model->game.board, &(model->squareFrom), &(model->squareTo), &movePromote, model);
-        moveType = FlipChessStatusMoveAI;
-        model->turnState = 0;
-    }
+        switch(model->game.state) {
+        case SCL_GAME_STATE_WHITE_WIN:
+            model->msg = "white wins";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-    if(moveType == FlipChessStatusMovePlayer || moveType == FlipChessStatusMoveAI) {
-        flipchess_shiftMessages(model);
+        case SCL_GAME_STATE_BLACK_WIN:
+            model->msg = "black wins";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-        SCL_moveToString(
-            model->game.board, model->squareFrom, model->squareTo, movePromote, model->moveString);
+        case SCL_GAME_STATE_DRAW_STALEMATE:
+            model->msg = "stalemate";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-        SCL_gameMakeMove(&(model->game), model->squareFrom, model->squareTo, movePromote);
+        case SCL_GAME_STATE_DRAW_REPETITION:
+            model->msg = "draw-repetition";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-        SCL_squareSetClear(model->moveHighlight);
-        SCL_squareSetAdd(model->moveHighlight, model->squareFrom);
-        SCL_squareSetAdd(model->moveHighlight, model->squareTo);
-    } else if(moveType == FlipChessStatusMoveUndo) {
-        flipchess_shiftMessages(model);
+        case SCL_GAME_STATE_DRAW_DEAD:
+            model->msg = "draw-dead pos.";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-        if(model->paramPlayerW != 0 || model->paramPlayerB != 0) SCL_gameUndoMove(&(model->game));
+        case SCL_GAME_STATE_DRAW:
+            model->msg = "draw";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-        SCL_gameUndoMove(&(model->game));
-        SCL_squareSetClear(model->moveHighlight);
-    }
+        case SCL_GAME_STATE_DRAW_50:
+            model->msg = "draw-50 moves";
+            model->paramExit = FlipChessStatusReturn;
+            break;
 
-    switch(model->game.state) {
-    case SCL_GAME_STATE_WHITE_WIN:
-        model->msg = "white wins";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    case SCL_GAME_STATE_BLACK_WIN:
-        model->msg = "black wins";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    case SCL_GAME_STATE_DRAW_STALEMATE:
-        model->msg = "draw (stalemate)";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    case SCL_GAME_STATE_DRAW_REPETITION:
-        model->msg = "draw (repetition)";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    case SCL_GAME_STATE_DRAW_DEAD:
-        model->msg = "draw (dead pos.)";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    case SCL_GAME_STATE_DRAW:
-        model->msg = "draw";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    case SCL_GAME_STATE_DRAW_50:
-        model->msg = "draw (50 moves)";
-        model->paramExit = FlipChessStatusReturn;
-        break;
-
-    default:
-        if(model->game.ply > 0) {
-            model->msg =
-                (SCL_boardWhitesTurn(model->game.board) ? "black played" : "white played");
-
-            uint8_t s0, s1;
-            char p;
-
-            SCL_recordGetMove(model->game.record, model->game.ply - 1, &s0, &s1, &p);
-            SCL_moveToString(model->game.board, s0, s1, p, model->moveString);
+        default:
+            if(model->game.ply > 0) {
+                const uint8_t whitesTurn = SCL_boardWhitesTurn(model->game.board);
+
+                if(SCL_boardCheck(model->game.board, whitesTurn)) {
+                    model->msg = (whitesTurn ? "black: check!" : "white: check!");
+                } else {
+                    model->msg = (whitesTurn ? "black played" : "white played");
+                }
+
+                uint8_t s0, s1;
+                char p;
+
+                SCL_recordGetMove(model->game.record, model->game.ply - 1, &s0, &s1, &p);
+                SCL_moveToString(model->game.board, s0, s1, p, model->moveString);
+            }
+            break;
+            model->paramExit = moveType;
         }
-        break;
-        model->paramExit = moveType;
     }
 
     model->thinking = 0;
@@ -681,9 +691,9 @@ void flipchess_scene_1_enter(void* context) {
             }
 
             // if return status, return from scene immediately
-            if(init == FlipChessStatusReturn) {
-                instance->callback(FlipChessCustomEventScene1Back, instance->context);
-            }
+            // if(init == FlipChessStatusReturn) {
+            //     instance->callback(FlipChessCustomEventScene1Back, instance->context);
+            // }
         },
         true);
 }
@@ -714,4 +724,4 @@ void flipchess_scene_1_free(FlipChessScene1* instance) {
 View* flipchess_scene_1_get_view(FlipChessScene1* instance) {
     furi_assert(instance);
     return instance->view;
-}
+}

+ 14 - 9
non_catalog_apps/esubghz_chat/README.md

@@ -5,7 +5,7 @@ feature that is available on the CLI. In addition it allows for basic
 encryption of messages.
 
 The plugin has been tested on the official firmware (version 0.87.0) and on
-Unleashed (version unlshd-057). Due to limitations of the official firmware,
+Unleashed (version unlshd-059). Due to limitations of the official firmware,
 the behavior is slightly different there.
 
 Currently the use of an external antenna is not supported.
@@ -20,9 +20,10 @@ bugs. You have been warned.
 Once opened the plugin will ask for a frequency to operate on which must be
 entered in HZ.
 
-On the next screen the plugin will ask for a password to derive the
-cryptographic key from. If nothing (on Unleashed) or a single space (on OFW) is
-entered, the encryption is disabled.
+On the next screen the plugin will ask for the method of deriving the key. If
+"No encryption" is selected, the encryption is disabled. If "Generate Key" is
+selected, a random key is generated. Otherwise, the plugin will ask for the
+selected input method. Currently only a password and a hex key are supported.
 
 Finally the a message can be input. After the message is confirmed, the plugin
 will switch to the chat view, where sent and received messages are displayed.
@@ -32,10 +33,12 @@ button.
 
 In the chat view the keyboard can be locked by pressing and holding the OK
 button for a few seconds. To unlock the keyboard again quickly press the back
-button three times.
+button three times. By pressing the Right button the key display is opened.
+Here the currently used key is displayed in hex. This can be used to input the
+same key on another flipper.
 
-Pressing the back button when entering the frequency, the password or a message
-will terminate the plugin.
+Pressing the back button when entering the frequency, when selecting the method
+for deriving the key or when entering a message will terminate the plugin.
 
 ## Interoperability
 
@@ -50,8 +53,8 @@ Messages are encrypted using 256 bit AES in GCM mode. Each message gets its own
 random IV. On reception the tag generated by GCM is verified and the message
 discarded if it doesn't match.
 
-The key for the encryption is derived from the password by applying SHA-256 to
-the password once.
+If a password is used, the key for the encryption is derived from the password
+by applying SHA-256 to the password once.
 
 Note that deriving the key with SHA-256 means that the security of your
 messages depends entirely on the strength of the password. The plugin does not
@@ -73,3 +76,5 @@ expect to gain any security by using encryption.
 The implementations of AES and GCM are taken directly from
 https://github.com/mko-x/SharedAES-GCM. They were released to the public domain
 by Markus Kosmal.
+
+The app icon was made by [xMasterX](https://github.com/xMasterX).

+ 1 - 1
non_catalog_apps/esubghz_chat/application.fam

@@ -9,7 +9,7 @@ App(
     ],
     stack_size=8 * 1024,
     fap_category="Sub-GHz",
-    fap_icon="chat_10px.png",
+    fap_icon="assets/chat_10px.png",
     fap_icon_assets="assets",
     fap_icon_assets_symbol="esubghz_chat",
 )

+ 0 - 0
non_catalog_apps/esubghz_chat/chat_10px.png → non_catalog_apps/esubghz_chat/assets/chat_10px.png


+ 105 - 0
non_catalog_apps/esubghz_chat/crypto_wrapper.c

@@ -0,0 +1,105 @@
+#include <furi_hal.h>
+
+#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+#include "crypto/gcm.h"
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+
+#include "crypto_wrapper.h"
+
+struct ESugGhzChatCryptoCtx {
+	uint8_t key[KEY_BITS / 8];
+#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+	gcm_context gcm_ctx;
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+};
+
+void crypto_init(void)
+{
+#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+	/* init the GCM and AES tables */
+	gcm_initialize();
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+}
+
+void crypto_explicit_bzero(void *s, size_t len)
+{
+	memset(s, 0, len);
+	asm volatile("" ::: "memory");
+}
+
+ESubGhzChatCryptoCtx *crypto_ctx_alloc(void)
+{
+	ESubGhzChatCryptoCtx *ret = malloc(sizeof(ESubGhzChatCryptoCtx));
+
+	if (ret != NULL) {
+		memset(ret, 0, sizeof(ESubGhzChatCryptoCtx));
+	}
+
+	return ret;
+}
+
+void crypto_ctx_free(ESubGhzChatCryptoCtx *ctx)
+{
+	crypto_ctx_clear(ctx);
+	free(ctx);
+}
+
+void crypto_ctx_clear(ESubGhzChatCryptoCtx *ctx)
+{
+	crypto_explicit_bzero(ctx, sizeof(ESubGhzChatCryptoCtx));
+}
+
+bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key)
+{
+	memcpy(ctx->key, key, KEY_BITS / 8);
+#ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+	return true;
+#else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+	return (gcm_setkey(&(ctx->gcm_ctx), key, KEY_BITS / 8) == 0);
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+}
+
+void crypto_ctx_get_key(ESubGhzChatCryptoCtx *ctx, uint8_t *key)
+{
+	memcpy(key, ctx->key, KEY_BITS / 8);
+}
+
+bool crypto_ctx_decrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
+		uint8_t *out)
+{
+	if (in_len < MSG_OVERHEAD + 1) {
+		return false;
+	}
+
+#ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+	return (furi_hal_crypto_gcm_decrypt_and_verify(ctx->key,
+			in, in + IV_BYTES, out,
+			in_len - MSG_OVERHEAD,
+			in + in_len - TAG_BYTES) == FuriHalCryptoGCMStateOk);
+#else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+	return (gcm_auth_decrypt(&(ctx->gcm_ctx),
+			in, IV_BYTES,
+			NULL, 0,
+			in + IV_BYTES, out, in_len - MSG_OVERHEAD,
+			in + in_len - TAG_BYTES, TAG_BYTES) == 0);
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+}
+
+bool crypto_ctx_encrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
+		uint8_t *out)
+{
+	furi_hal_random_fill_buf(out, IV_BYTES);
+
+#ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+	return (furi_hal_crypto_gcm_encrypt_and_tag(ctx->key,
+			out, in, out + IV_BYTES,
+			in_len,
+			out + IV_BYTES + in_len) == FuriHalCryptoGCMStateOk);
+#else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+	return (gcm_crypt_and_tag(&(ctx->gcm_ctx), ENCRYPT,
+			out, IV_BYTES,
+			NULL, 0,
+			in, out + IV_BYTES, in_len,
+			out + IV_BYTES + in_len, TAG_BYTES) == 0);
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+}

+ 35 - 0
non_catalog_apps/esubghz_chat/crypto_wrapper.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define KEY_BITS 256
+#define IV_BYTES 12
+#define TAG_BYTES 16
+
+#define MSG_OVERHEAD (IV_BYTES + TAG_BYTES)
+
+typedef struct ESugGhzChatCryptoCtx ESubGhzChatCryptoCtx;
+
+void crypto_init(void);
+
+/* Function to clear sensitive memory. */
+void crypto_explicit_bzero(void *s, size_t len);
+
+ESubGhzChatCryptoCtx *crypto_ctx_alloc(void);
+void crypto_ctx_free(ESubGhzChatCryptoCtx *ctx);
+
+void crypto_ctx_clear(ESubGhzChatCryptoCtx *ctx);
+
+bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key);
+void crypto_ctx_get_key(ESubGhzChatCryptoCtx *ctx, uint8_t *key);
+
+bool crypto_ctx_decrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
+		uint8_t *out);
+bool crypto_ctx_encrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
+		uint8_t *out);
+
+#ifdef __cplusplus
+}
+#endif

+ 130 - 584
non_catalog_apps/esubghz_chat/esubghz_chat.c

@@ -1,107 +1,20 @@
-#include <furi.h>
 #include <furi_hal.h>
 #include <gui/elements.h>
 #include <gui/gui.h>
-#include <gui/modules/text_box.h>
-#include <gui/modules/text_input.h>
-#include <gui/view_dispatcher_i.h>
-#include <gui/view_port_i.h>
-#include <gui/scene_manager.h>
-#include <toolbox/sha256.h>
-#include <notification/notification_messages.h>
 #include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
-#include <lib/subghz/subghz_tx_rx_worker.h>
 
 #include "esubghz_chat_icons.h"
 
-#include "crypto/gcm.h"
-
-#define APPLICATION_NAME "ESubGhzChat"
-
-#define DEFAULT_FREQ 433920000
-
-#define KEY_BITS 256
-#define IV_BYTES 12
-#define TAG_BYTES 16
-
-#define RX_TX_BUFFER_SIZE 1024
-
-#define CHAT_BOX_STORE_SIZE 4096
-#define TEXT_INPUT_STORE_SIZE 256
+#include "esubghz_chat_i.h"
 
+#define CHAT_LEAVE_DELAY 10
 #define TICK_INTERVAL 50
 #define MESSAGE_COMPLETION_TIMEOUT 500
 #define TIMEOUT_BETWEEN_MESSAGES 500
-#define CHAT_LEAVE_DELAY 10
 
 #define KBD_UNLOCK_CNT 3
 #define KBD_UNLOCK_TIMEOUT 1000
 
-typedef struct {
-	SceneManager *scene_manager;
-	ViewDispatcher *view_dispatcher;
-	NotificationApp *notification;
-
-	// UI elements
-	TextBox *chat_box;
-	FuriString *chat_box_store;
-	TextInput *text_input;
-	char text_input_store[TEXT_INPUT_STORE_SIZE + 1];
-
-	// for Sub-GHz
-	uint32_t frequency;
-	SubGhzTxRxWorker *subghz_worker;
-	const SubGhzDevice *subghz_device;
-
-	// message assembly before TX
-	FuriString *name_prefix;
-	FuriString *msg_input;
-
-	// encryption
-	bool encrypted;
-	gcm_context gcm_ctx;
-
-	// RX and TX buffers
-	uint8_t rx_buffer[RX_TX_BUFFER_SIZE];
-	uint8_t tx_buffer[RX_TX_BUFFER_SIZE];
-	char rx_str_buffer[RX_TX_BUFFER_SIZE + 1];
-	volatile uint32_t last_time_rx_data;
-
-	// for locking
-	ViewPortDrawCallback orig_draw_cb;
-	ViewPortInputCallback orig_input_cb;
-	bool kbd_locked;
-	uint32_t kbd_lock_msg_ticks;
-	uint8_t kbd_lock_count;
-	bool kbd_ok_input_ongoing;
-} ESubGhzChatState;
-
-typedef enum {
-	ESubGhzChatScene_FreqInput,
-	ESubGhzChatScene_PassInput,
-	ESubGhzChatScene_ChatInput,
-	ESubGhzChatScene_ChatBox,
-	ESubGhzChatScene_MAX
-} ESubGhzChatScene;
-
-typedef enum {
-	ESubGhzChatView_Input,
-	ESubGhzChatView_ChatBox,
-} ESubGhzChatView;
-
-typedef enum {
-	ESubGhzChatEvent_FreqEntered,
-	ESubGhzChatEvent_PassEntered,
-	ESubGhzChatEvent_MsgEntered
-} ESubGhzChatEvent;
-
-/* Function to clear sensitive memory. */
-static void esubghz_chat_explicit_bzero(void *s, size_t len)
-{
-	memset(s, 0, len);
-	asm volatile("" ::: "memory");
-}
-
 /* Callback for RX events from the Sub-GHz worker. Records the current ticks as
  * the time of the last reception. */
 static void have_read_cb(void* context)
@@ -115,21 +28,17 @@ static void have_read_cb(void* context)
 /* Decrypts a message for post_rx(). */
 static bool post_rx_decrypt(ESubGhzChatState *state, size_t rx_size)
 {
-	if (rx_size < IV_BYTES + TAG_BYTES + 1) {
-		return false;
+	bool ret = crypto_ctx_decrypt(state->crypto_ctx,
+			state->rx_buffer, rx_size,
+			(uint8_t*) state->rx_str_buffer);
+
+	if (ret) {
+		state->rx_str_buffer[rx_size - (MSG_OVERHEAD)] = 0;
+	} else {
+		state->rx_str_buffer[0] = 0;
 	}
 
-	int ret = gcm_auth_decrypt(&(state->gcm_ctx),
-			state->rx_buffer, IV_BYTES,
-			NULL, 0,
-			state->rx_buffer + IV_BYTES,
-			(uint8_t *) state->rx_str_buffer,
-			rx_size - (IV_BYTES + TAG_BYTES),
-			state->rx_buffer + rx_size - TAG_BYTES,
-			TAG_BYTES);
-	state->rx_str_buffer[rx_size - (IV_BYTES + TAG_BYTES)] = 0;
-
-	return (ret == 0);
+	return ret;
 }
 
 /* Post RX handler, decrypts received messages, displays them in the text box
@@ -176,25 +85,20 @@ static void post_rx(ESubGhzChatState *state, size_t rx_size)
 
 /* Reads the message from msg_input, encrypts it if necessary and then
  * transmits it. */
-static void tx_msg_input(ESubGhzChatState *state)
+void tx_msg_input(ESubGhzChatState *state)
 {
 	/* encrypt message if necessary */
 	size_t msg_len = strlen(furi_string_get_cstr(state->msg_input));
 	size_t tx_size = msg_len;
 	if (state->encrypted) {
-		tx_size += IV_BYTES + TAG_BYTES;
+		tx_size += MSG_OVERHEAD;
 		furi_check(tx_size <= sizeof(state->tx_buffer));
 
-		furi_hal_random_fill_buf(state->tx_buffer, IV_BYTES);
-		gcm_crypt_and_tag(&(state->gcm_ctx), ENCRYPT,
-				state->tx_buffer, IV_BYTES,
-				NULL, 0,
-				(unsigned char *)
+		crypto_ctx_encrypt(state->crypto_ctx,
+				(uint8_t *)
 				furi_string_get_cstr(state->msg_input),
-				state->tx_buffer + IV_BYTES,
 				msg_len,
-				state->tx_buffer + IV_BYTES + msg_len,
-				TAG_BYTES);
+				state->tx_buffer);
 	} else {
 		tx_size += 2;
 		furi_check(tx_size <= sizeof(state->tx_buffer));
@@ -212,72 +116,14 @@ static void tx_msg_input(ESubGhzChatState *state)
 			tx_size);
 }
 
-/* Sends FreqEntered event to scene manager and displays the frequency in the
- * text box. */
-static void freq_input_cb(void *context)
-{
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	furi_string_cat_printf(state->chat_box_store, "Frequency: %lu",
-			state->frequency);
-
-	scene_manager_handle_custom_event(state->scene_manager,
-			ESubGhzChatEvent_FreqEntered);
-}
-
-/* Validates the entered frequency. */
-static bool freq_input_validator(const char *text, FuriString *error,
-		void *context)
-{
-	furi_assert(text);
-	furi_assert(error);
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-        int ret = sscanf(text, "%lu", &(state->frequency));
-	if (ret != 1) {
-		furi_string_printf(error, "Please enter\nfrequency\nin Hz!");
-		return false;
-	}
-
-	if (!subghz_devices_is_frequency_valid(state->subghz_device,
-				state->frequency)) {
-		furi_string_printf(error, "Frequency\n%lu\n is invalid!",
-				state->frequency);
-		return false;
-	}
-
-#ifdef FW_ORIGIN_Official
-	if (!furi_hal_region_is_frequency_allowed(state->frequency)) {
-#else /* FW_ORIGIN_Official */
-	if (!furi_hal_subghz_is_tx_allowed(state->frequency)) {
-#endif /* FW_ORIGIN_Official */
-		furi_string_printf(error, "TX forbidden\non frequency\n%lu!",
-				state->frequency);
-		return false;
-	}
-
-	return true;
-}
-
-/* Sends PassEntered event to scene manager and displays whether or not
- * encryption has been enabled in the text box. Also clears the text input
- * buffer to remove the password and starts the Sub-GHz worker. After starting
- * the worker a join message is transmitted. */
-static void pass_input_cb(void *context)
+/* Displays whether or not encryption has been enabled in the text box. Also
+ * clears the text input buffer to remove the password and starts the Sub-GHz
+ * worker. After starting the worker a join message is transmitted. */
+void enter_chat(ESubGhzChatState *state)
 {
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
 	furi_string_cat_printf(state->chat_box_store, "\nEncrypted: %s",
 			(state->encrypted ? "yes" : "no"));
 
-	/* clear the text input buffer to remove the password */
-	esubghz_chat_explicit_bzero(state->text_input_store,
-			sizeof(state->text_input_store));
-
 	subghz_tx_rx_worker_start(state->subghz_worker, state->subghz_device,
 			state->frequency);
 
@@ -290,90 +136,14 @@ static void pass_input_cb(void *context)
 
 	/* clear message input buffer */
 	furi_string_set_char(state->msg_input, 0, 0);
-
-	scene_manager_handle_custom_event(state->scene_manager,
-			ESubGhzChatEvent_PassEntered);
-}
-
-/* If a password was entered this derives a key from the password using a
- * single pass of SHA256 and initiates the AES-GCM context for encryption. If
- * the initiation fails, the password is rejected. */
-static bool pass_input_validator(const char *text, FuriString *error,
-		void *context)
-{
-	furi_assert(text);
-	furi_assert(error);
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-#ifdef FW_ORIGIN_Official
-	if (strlen(text) == 0) {
-		furi_string_printf(error, "Enter a\npassword!");
-		return false;
-	}
-
-	if (strcmp(text, " ") == 0) {
-#else /* FW_ORIGIN_Official */
-	if (strlen(text) == 0) {
-#endif /* FW_ORIGIN_Official */
-		state->encrypted = false;
-		return true;
-	}
-
-	unsigned char key[KEY_BITS / 8];
-
-	/* derive a key from the password */
-	sha256((unsigned char *) text, strlen(text), key);
-
-	/* initiate the AES-GCM context */
-	int ret = gcm_setkey(&(state->gcm_ctx), key, KEY_BITS / 8);
-
-	/* cleanup */
-	esubghz_chat_explicit_bzero(key, sizeof(key));
-
-	if (ret != 0) {
-		esubghz_chat_explicit_bzero(&(state->gcm_ctx),
-				sizeof(state->gcm_ctx));
-		furi_string_printf(error, "Failed to\nset key!");
-		return false;
-	}
-
-	state->encrypted = true;
-
-	return true;
 }
 
-/* If no message was entred this simply emits a MsgEntered event to the scene
- * manager to switch to the text box. If a message was entered it is appended
- * to the name string. The result is encrypted, if encryption is enabled, and
- * then copied into the TX buffer. The contents of the TX buffer are then
- * transmitted. The sent message is appended to the text box and a MsgEntered
- * event is sent to the scene manager to switch to the text box view. */
-static void chat_input_cb(void *context)
+/* Sends a leave message */
+void exit_chat(ESubGhzChatState *state)
 {
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	/* no message, just switch to the text box view */
-#ifdef FW_ORIGIN_Official
-	if (strcmp(state->text_input_store, " ") == 0) {
-#else /* FW_ORIGIN_Official */
-	if (strlen(state->text_input_store) == 0) {
-#endif /* FW_ORIGIN_Official */
-		scene_manager_handle_custom_event(state->scene_manager,
-				ESubGhzChatEvent_MsgEntered);
-		return;
-	}
-
-	/* concatenate the name prefix and the actual message */
+	/* concatenate the name prefix and leave message */
 	furi_string_set(state->msg_input, state->name_prefix);
-	furi_string_cat_str(state->msg_input, ": ");
-	furi_string_cat_str(state->msg_input, state->text_input_store);
-
-	/* append the message to the chat box */
-	furi_string_cat_printf(state->chat_box_store, "\n%s",
-		furi_string_get_cstr(state->msg_input));
+	furi_string_cat_str(state->msg_input, " left chat.");
 
 	/* encrypt and transmit message */
 	tx_msg_input(state);
@@ -381,333 +151,10 @@ static void chat_input_cb(void *context)
 	/* clear message input buffer */
 	furi_string_set_char(state->msg_input, 0, 0);
 
-	/* switch to text box view */
-	scene_manager_handle_custom_event(state->scene_manager,
-			ESubGhzChatEvent_MsgEntered);
-}
-
-/* Prepares the frequency input scene. */
-static void scene_on_enter_freq_input(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_freq_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	snprintf(state->text_input_store, TEXT_INPUT_STORE_SIZE, "%lu",
-			(uint32_t) DEFAULT_FREQ);
-	text_input_reset(state->text_input);
-	text_input_set_result_callback(
-			state->text_input,
-			freq_input_cb,
-			state,
-			state->text_input_store,
-			sizeof(state->text_input_store),
-			true);
-	text_input_set_validator(
-			state->text_input,
-			freq_input_validator,
-			state);
-	text_input_set_header_text(
-			state->text_input,
-			"Frequency");
-
-	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
-}
-
-/* Handles scene manager events for the frequency input scene. */
-static bool scene_on_event_freq_input(void* context, SceneManagerEvent event)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_freq_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	bool consumed = false;
-
-	switch(event.type) {
-	case SceneManagerEventTypeCustom:
-		switch(event.event) {
-		/* switch to password input scene */
-		case ESubGhzChatEvent_FreqEntered:
-			scene_manager_next_scene(state->scene_manager,
-					ESubGhzChatScene_PassInput);
-			consumed = true;
-			break;
-		}
-		break;
-
-	case SceneManagerEventTypeBack:
-		/* stop the application if the user presses back here */
-		view_dispatcher_stop(state->view_dispatcher);
-		consumed = true;
-		break;
-
-	default:
-		consumed = false;
-		break;
-	}
-
-	return consumed;
-}
-
-/* Cleans up the frequency input scene. */
-static void scene_on_exit_freq_input(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_freq_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	text_input_reset(state->text_input);
+	/* wait for leave message to be delivered */
+	furi_delay_ms(CHAT_LEAVE_DELAY);
 }
 
-/* Prepares the password input scene. */
-static void scene_on_enter_pass_input(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_pass_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	state->text_input_store[0] = 0;
-	text_input_reset(state->text_input);
-	text_input_set_result_callback(
-			state->text_input,
-			pass_input_cb,
-			state,
-			state->text_input_store,
-			sizeof(state->text_input_store),
-			true);
-	text_input_set_validator(
-			state->text_input,
-			pass_input_validator,
-			state);
-	text_input_set_header_text(
-			state->text_input,
-#ifdef FW_ORIGIN_Official
-			"Password (space for no encr.)");
-#else /* FW_ORIGIN_Official */
-			"Password (empty for no encr.)");
-	text_input_set_minimum_length(state->text_input, 0);
-#endif /* FW_ORIGIN_Official */
-
-	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
-}
-
-/* Handles scene manager events for the password input scene. */
-static bool scene_on_event_pass_input(void* context, SceneManagerEvent event)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_pass_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	bool consumed = false;
-
-	switch(event.type) {
-	case SceneManagerEventTypeCustom:
-		switch(event.event) {
-		/* switch to message input scene */
-		case ESubGhzChatEvent_PassEntered:
-			scene_manager_next_scene(state->scene_manager,
-					ESubGhzChatScene_ChatInput);
-			consumed = true;
-			break;
-		}
-		break;
-
-	case SceneManagerEventTypeBack:
-		/* stop the application if the user presses back here */
-		view_dispatcher_stop(state->view_dispatcher);
-		consumed = true;
-		break;
-
-	default:
-		consumed = false;
-		break;
-	}
-
-	return consumed;
-}
-
-/* Cleans up the password input scene. */
-static void scene_on_exit_pass_input(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_pass_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	text_input_reset(state->text_input);
-}
-
-/* Prepares the message input scene. */
-static void scene_on_enter_chat_input(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_chat_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	state->text_input_store[0] = 0;
-	text_input_reset(state->text_input);
-	text_input_set_result_callback(
-			state->text_input,
-			chat_input_cb,
-			state,
-			state->text_input_store,
-			sizeof(state->text_input_store),
-			true);
-	text_input_set_validator(
-			state->text_input,
-			NULL,
-			NULL);
-	text_input_set_header_text(
-			state->text_input,
-#ifdef FW_ORIGIN_Official
-			"Message (space for none)");
-#else /* FW_ORIGIN_Official */
-			"Message");
-	text_input_set_minimum_length(state->text_input, 0);
-#endif /* FW_ORIGIN_Official */
-
-	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
-}
-
-/* Handles scene manager events for the message input scene. */
-static bool scene_on_event_chat_input(void* context, SceneManagerEvent event)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_chat_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	bool consumed = false;
-
-	switch(event.type) {
-	case SceneManagerEventTypeCustom:
-		switch(event.event) {
-		/* switch to text box scene */
-		case ESubGhzChatEvent_MsgEntered:
-			scene_manager_next_scene(state->scene_manager,
-					ESubGhzChatScene_ChatBox);
-			consumed = true;
-			break;
-		}
-		break;
-
-	case SceneManagerEventTypeBack:
-		/* stop the application and send a leave message if the user
-		 * presses back here */
-
-		/* concatenate the name prefix and leave message */
-		furi_string_set(state->msg_input, state->name_prefix);
-		furi_string_cat_str(state->msg_input, " left chat.");
-
-		/* encrypt and transmit message */
-		tx_msg_input(state);
-
-		/* clear message input buffer */
-		furi_string_set_char(state->msg_input, 0, 0);
-
-		/* wait for leave message to be delivered */
-                furi_delay_ms(CHAT_LEAVE_DELAY);
-
-		view_dispatcher_stop(state->view_dispatcher);
-		consumed = true;
-		break;
-
-	default:
-		consumed = false;
-		break;
-	}
-
-	return consumed;
-}
-
-/* Cleans up the password input scene. */
-static void scene_on_exit_chat_input(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_chat_input");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	text_input_reset(state->text_input);
-}
-
-/* Prepares the text box scene. */
-static void scene_on_enter_chat_box(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_chat_box");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	text_box_reset(state->chat_box);
-	text_box_set_text(state->chat_box,
-			furi_string_get_cstr(state->chat_box_store));
-	text_box_set_focus(state->chat_box, TextBoxFocusEnd);
-
-	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_ChatBox);
-}
-
-/* Handles scene manager events for the text box scene. No events are handled
- * here. */
-static bool scene_on_event_chat_box(void* context, SceneManagerEvent event)
-{
-	UNUSED(event);
-
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_chat_box");
-
-	furi_assert(context);
-
-	return false;
-}
-
-/* Cleans up the text box scene. */
-static void scene_on_exit_chat_box(void* context)
-{
-	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_chat_box");
-
-	furi_assert(context);
-	ESubGhzChatState* state = context;
-
-	text_box_reset(state->chat_box);
-}
-
-/* Scene entry handlers. */
-static void (*const esubghz_chat_scene_on_enter_handlers[])(void*) = {
-	scene_on_enter_freq_input,
-	scene_on_enter_pass_input,
-	scene_on_enter_chat_input,
-	scene_on_enter_chat_box
-};
-
-/* Scene event handlers. */
-static bool (*const esubghz_chat_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
-	scene_on_event_freq_input,
-	scene_on_event_pass_input,
-	scene_on_event_chat_input,
-	scene_on_event_chat_box
-};
-
-/* Scene exit handlers. */
-static void (*const esubghz_chat_scene_on_exit_handlers[])(void*) = {
-	scene_on_exit_freq_input,
-	scene_on_exit_pass_input,
-	scene_on_exit_chat_input,
-	scene_on_exit_chat_box
-};
-
-/* Handlers for the scene manager. */
-static const SceneManagerHandlers esubghz_chat_scene_event_handlers = {
-	.on_enter_handlers = esubghz_chat_scene_on_enter_handlers,
-	.on_event_handlers = esubghz_chat_scene_on_event_handlers,
-	.on_exit_handlers = esubghz_chat_scene_on_exit_handlers,
-	.scene_num = ESubGhzChatScene_MAX};
-
 /* Whether or not to display the locked message. */
 static bool kbd_lock_msg_display(ESubGhzChatState *state)
 {
@@ -892,7 +339,7 @@ static void esubghz_hooked_input_callback(InputEvent* event, void* context)
 			return;
 		}
 
-		/* handle ongoing inputs when chaning to chat view */
+		/* handle ongoing inputs when changing to chat view */
 		if (event->type == InputTypePress) {
 			state->kbd_ok_input_ongoing = true;
 		} else if (event->type == InputTypeRelease) {
@@ -900,6 +347,57 @@ static void esubghz_hooked_input_callback(InputEvent* event, void* context)
 		}
 	}
 
+	if (event->key == InputKeyLeft) {
+		/* if we are in the chat view and no input is ongoing, allow
+		 * switching to msg input */
+		if (state->view_dispatcher->current_view ==
+				text_box_get_view(state->chat_box) &&
+				!(state->kbd_left_input_ongoing)) {
+			/* go to msg input upon short press of Left button */
+			if (event->type == InputTypeShort) {
+				view_dispatcher_send_custom_event(state->view_dispatcher,
+						ESubGhzChatEvent_GotoMsgInput);
+			}
+
+			/* do not handle any Left key events to prevent
+			 * blocking of other keys */
+			return;
+		}
+
+		/* handle ongoing inputs when changing to chat view */
+		if (event->type == InputTypePress) {
+			state->kbd_left_input_ongoing = true;
+		} else if (event->type == InputTypeRelease) {
+			state->kbd_left_input_ongoing = false;
+		}
+	}
+
+	if (event->key == InputKeyRight) {
+		/* if we are in the chat view and no input is ongoing, allow
+		 * switching to key display */
+		if (state->view_dispatcher->current_view ==
+				text_box_get_view(state->chat_box) &&
+				!(state->kbd_right_input_ongoing)) {
+			/* go to key display upon short press of Right button
+			 */
+			if (event->type == InputTypeShort) {
+				view_dispatcher_send_custom_event(state->view_dispatcher,
+						ESubGhzChatEvent_GotoKeyDisplay);
+			}
+
+			/* do not handle any Right key events to prevent
+			 * blocking of other keys */
+			return;
+		}
+
+		/* handle ongoing inputs when changing to chat view */
+		if (event->type == InputTypePress) {
+			state->kbd_right_input_ongoing = true;
+		} else if (event->type == InputTypeRelease) {
+			state->kbd_right_input_ongoing = false;
+		}
+	}
+
 	/* call original callback */
 	state->orig_input_cb(event, state->view_dispatcher);
 }
@@ -964,8 +462,8 @@ static void chat_box_free(ESubGhzChatState *state)
 
 int32_t esubghz_chat(void)
 {
-	/* init the GCM and AES tables */
-	gcm_initialize();
+	/* init the crypto system */
+	crypto_init();
 
 	int32_t err = -1;
 
@@ -994,20 +492,40 @@ int32_t esubghz_chat(void)
 		goto err_alloc_hs;
 	}
 
+	state->menu = menu_alloc();
+	if (state->menu == NULL) {
+		goto err_alloc_menu;
+	}
+
 	state->text_input = text_input_alloc();
 	if (state->text_input == NULL) {
 		goto err_alloc_ti;
 	}
 
+	state->hex_key_input = byte_input_alloc();
+	if (state->hex_key_input == NULL) {
+		goto err_alloc_hki;
+	}
+
 	if (!chat_box_alloc(state)) {
 		goto err_alloc_cb;
 	}
 
+	state->key_display = dialog_ex_alloc();
+	if (state->key_display == NULL) {
+		goto err_alloc_kd;
+	}
+
 	state->subghz_worker = subghz_tx_rx_worker_alloc();
 	if (state->subghz_worker == NULL) {
 		goto err_alloc_worker;
 	}
 
+	state->crypto_ctx = crypto_ctx_alloc();
+	if (state->crypto_ctx == NULL) {
+		goto err_alloc_crypto;
+	}
+
 	/* set the have_read callback of the Sub-GHz worker */
 	subghz_tx_rx_worker_set_callback_have_read(state->subghz_worker,
 			have_read_cb, state);
@@ -1051,10 +569,16 @@ int32_t esubghz_chat(void)
 			TICK_INTERVAL);
 
 	/* add our two views to the view dispatcher */
+	view_dispatcher_add_view(state->view_dispatcher, ESubGhzChatView_Menu,
+			menu_get_view(state->menu));
 	view_dispatcher_add_view(state->view_dispatcher, ESubGhzChatView_Input,
 			text_input_get_view(state->text_input));
+	view_dispatcher_add_view(state->view_dispatcher, ESubGhzChatView_HexKeyInput,
+			byte_input_get_view(state->hex_key_input));
 	view_dispatcher_add_view(state->view_dispatcher, ESubGhzChatView_ChatBox,
 			text_box_get_view(state->chat_box));
+	view_dispatcher_add_view(state->view_dispatcher, ESubGhzChatView_KeyDisplay,
+			dialog_ex_get_view(state->key_display));
 
 	/* get the GUI record and attach the view dispatcher to the GUI */
 	/* no error handling here, don't know how */
@@ -1071,6 +595,7 @@ int32_t esubghz_chat(void)
 
 	/* if it is running, stop the Sub-GHz worker */
 	if (subghz_tx_rx_worker_is_running(state->subghz_worker)) {
+		exit_chat(state);
 		subghz_tx_rx_worker_stop(state->subghz_worker);
 	}
 
@@ -1080,18 +605,27 @@ int32_t esubghz_chat(void)
 	furi_record_close(RECORD_GUI);
 
 	/* remove our two views from the view dispatcher */
+	view_dispatcher_remove_view(state->view_dispatcher,
+			ESubGhzChatView_Menu);
 	view_dispatcher_remove_view(state->view_dispatcher,
 			ESubGhzChatView_Input);
+	view_dispatcher_remove_view(state->view_dispatcher,
+			ESubGhzChatView_HexKeyInput);
 	view_dispatcher_remove_view(state->view_dispatcher,
 			ESubGhzChatView_ChatBox);
+	view_dispatcher_remove_view(state->view_dispatcher,
+			ESubGhzChatView_KeyDisplay);
 
 	/* close notification record */
 	furi_record_close(RECORD_NOTIFICATION);
 
 	/* clear the key and potential password */
-	esubghz_chat_explicit_bzero(state->text_input_store,
+	crypto_explicit_bzero(state->text_input_store,
 			sizeof(state->text_input_store));
-	esubghz_chat_explicit_bzero(&(state->gcm_ctx), sizeof(state->gcm_ctx));
+	crypto_explicit_bzero(state->hex_key_input_store,
+			sizeof(state->hex_key_input_store));
+	crypto_explicit_bzero(state->key_hex_str, sizeof(state->key_hex_str));
+	crypto_ctx_clear(state->crypto_ctx);
 
 	/* deinit devices */
 	subghz_devices_deinit();
@@ -1101,15 +635,27 @@ int32_t esubghz_chat(void)
 
 	/* free everything we allocated */
 
+	crypto_ctx_free(state->crypto_ctx);
+
+err_alloc_crypto:
 	subghz_tx_rx_worker_free(state->subghz_worker);
 
 err_alloc_worker:
+	dialog_ex_free(state->key_display);
+
+err_alloc_kd:
 	chat_box_free(state);
 
 err_alloc_cb:
+	byte_input_free(state->hex_key_input);
+
+err_alloc_hki:
 	text_input_free(state->text_input);
 
 err_alloc_ti:
+	menu_free(state->menu);
+
+err_alloc_menu:
 	helper_strings_free(state);
 
 err_alloc_hs:

+ 101 - 0
non_catalog_apps/esubghz_chat/esubghz_chat_i.h

@@ -0,0 +1,101 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/view_dispatcher_i.h>
+#include <gui/view_port_i.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/byte_input.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/menu.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/text_input.h>
+#include <notification/notification_messages.h>
+#include <lib/subghz/subghz_tx_rx_worker.h>
+#include <toolbox/sha256.h>
+
+#include "crypto_wrapper.h"
+#include "scenes/esubghz_chat_scene.h"
+
+#define APPLICATION_NAME "ESubGhzChat"
+
+#define DEFAULT_FREQ 433920000
+
+#define RX_TX_BUFFER_SIZE 1024
+
+#define CHAT_BOX_STORE_SIZE 4096
+#define TEXT_INPUT_STORE_SIZE 256
+
+#define KEY_HEX_STR_SIZE ((KEY_BITS / 8) * 3)
+
+typedef struct {
+	SceneManager *scene_manager;
+	ViewDispatcher *view_dispatcher;
+	NotificationApp *notification;
+
+	// UI elements
+	Menu *menu;
+	TextBox *chat_box;
+	FuriString *chat_box_store;
+	TextInput *text_input;
+	char text_input_store[TEXT_INPUT_STORE_SIZE + 1];
+	ByteInput *hex_key_input;
+	uint8_t hex_key_input_store[KEY_BITS / 8];
+	DialogEx *key_display;
+	char key_hex_str[KEY_HEX_STR_SIZE + 1];
+
+	// for Sub-GHz
+	uint32_t frequency;
+	SubGhzTxRxWorker *subghz_worker;
+	const SubGhzDevice *subghz_device;
+
+	// message assembly before TX
+	FuriString *name_prefix;
+	FuriString *msg_input;
+
+	// encryption
+	bool encrypted;
+	ESubGhzChatCryptoCtx *crypto_ctx;
+
+	// RX and TX buffers
+	uint8_t rx_buffer[RX_TX_BUFFER_SIZE];
+	uint8_t tx_buffer[RX_TX_BUFFER_SIZE];
+	char rx_str_buffer[RX_TX_BUFFER_SIZE + 1];
+	volatile uint32_t last_time_rx_data;
+
+	// for locking
+	ViewPortDrawCallback orig_draw_cb;
+	ViewPortInputCallback orig_input_cb;
+	bool kbd_locked;
+	uint32_t kbd_lock_msg_ticks;
+	uint8_t kbd_lock_count;
+
+	// for ongoing inputs
+	bool kbd_ok_input_ongoing;
+	bool kbd_left_input_ongoing;
+	bool kbd_right_input_ongoing;
+} ESubGhzChatState;
+
+typedef enum {
+	ESubGhzChatEvent_FreqEntered,
+	ESubGhzChatEvent_KeyMenuNoEncryption,
+	ESubGhzChatEvent_KeyMenuPassword,
+	ESubGhzChatEvent_KeyMenuHexKey,
+	ESubGhzChatEvent_KeyMenuGenKey,
+	ESubGhzChatEvent_PassEntered,
+	ESubGhzChatEvent_HexKeyEntered,
+	ESubGhzChatEvent_MsgEntered,
+	ESubGhzChatEvent_GotoMsgInput,
+	ESubGhzChatEvent_GotoKeyDisplay,
+	ESubGhzChatEvent_KeyDisplayBack
+} ESubGhzChatEvent;
+
+typedef enum {
+	ESubGhzChatView_Menu,
+	ESubGhzChatView_Input,
+	ESubGhzChatView_HexKeyInput,
+	ESubGhzChatView_ChatBox,
+	ESubGhzChatView_KeyDisplay,
+} ESubGhzChatView;
+
+void tx_msg_input(ESubGhzChatState *state);
+void enter_chat(ESubGhzChatState *state);

+ 65 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_chat_box.c

@@ -0,0 +1,65 @@
+#include "../esubghz_chat_i.h"
+
+/* Prepares the text box scene. */
+void scene_on_enter_chat_box(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_chat_box");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	text_box_reset(state->chat_box);
+	text_box_set_text(state->chat_box,
+			furi_string_get_cstr(state->chat_box_store));
+	text_box_set_focus(state->chat_box, TextBoxFocusEnd);
+
+	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_ChatBox);
+}
+
+/* Handles scene manager events for the text box scene. */
+bool scene_on_event_chat_box(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_chat_box");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to message input scene */
+		case ESubGhzChatEvent_GotoMsgInput:
+			if (!scene_manager_previous_scene(
+						state->scene_manager)) {
+				view_dispatcher_stop(state->view_dispatcher);
+			}
+			consumed = true;
+			break;
+		case ESubGhzChatEvent_GotoKeyDisplay:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_KeyDisplay);
+			consumed = true;
+			break;
+		}
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the text box scene. */
+void scene_on_exit_chat_box(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_chat_box");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	text_box_reset(state->chat_box);
+}

+ 123 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_chat_input.c

@@ -0,0 +1,123 @@
+#include "../esubghz_chat_i.h"
+
+/* If no message was entred this simply emits a MsgEntered event to the scene
+ * manager to switch to the text box. If a message was entered it is appended
+ * to the name string. The result is encrypted, if encryption is enabled, and
+ * then copied into the TX buffer. The contents of the TX buffer are then
+ * transmitted. The sent message is appended to the text box and a MsgEntered
+ * event is sent to the scene manager to switch to the text box view. */
+static void chat_input_cb(void *context)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	/* no message, just switch to the text box view */
+#ifdef FW_ORIGIN_Official
+	if (strcmp(state->text_input_store, " ") == 0) {
+#else /* FW_ORIGIN_Official */
+	if (strlen(state->text_input_store) == 0) {
+#endif /* FW_ORIGIN_Official */
+		scene_manager_handle_custom_event(state->scene_manager,
+				ESubGhzChatEvent_MsgEntered);
+		return;
+	}
+
+	/* concatenate the name prefix and the actual message */
+	furi_string_set(state->msg_input, state->name_prefix);
+	furi_string_cat_str(state->msg_input, ": ");
+	furi_string_cat_str(state->msg_input, state->text_input_store);
+
+	/* append the message to the chat box */
+	furi_string_cat_printf(state->chat_box_store, "\n%s",
+		furi_string_get_cstr(state->msg_input));
+
+	/* encrypt and transmit message */
+	tx_msg_input(state);
+
+	/* clear message input buffer */
+	furi_string_set_char(state->msg_input, 0, 0);
+
+	/* switch to text box view */
+	scene_manager_handle_custom_event(state->scene_manager,
+			ESubGhzChatEvent_MsgEntered);
+}
+
+/* Prepares the message input scene. */
+void scene_on_enter_chat_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_chat_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	state->text_input_store[0] = 0;
+	text_input_reset(state->text_input);
+	text_input_set_result_callback(
+			state->text_input,
+			chat_input_cb,
+			state,
+			state->text_input_store,
+			sizeof(state->text_input_store),
+			true);
+	text_input_set_validator(
+			state->text_input,
+			NULL,
+			NULL);
+	text_input_set_header_text(
+			state->text_input,
+#ifdef FW_ORIGIN_Official
+			"Message (space for none)");
+#else /* FW_ORIGIN_Official */
+			"Message");
+	text_input_set_minimum_length(state->text_input, 0);
+#endif /* FW_ORIGIN_Official */
+
+	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
+}
+
+/* Handles scene manager events for the message input scene. */
+bool scene_on_event_chat_input(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_chat_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to text box scene */
+		case ESubGhzChatEvent_MsgEntered:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_ChatBox);
+			consumed = true;
+			break;
+		}
+		break;
+
+	case SceneManagerEventTypeBack:
+		/* stop the application if the user presses back here */
+		view_dispatcher_stop(state->view_dispatcher);
+		consumed = true;
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the password input scene. */
+void scene_on_exit_chat_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_chat_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	text_input_reset(state->text_input);
+}

+ 127 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_freq_input.c

@@ -0,0 +1,127 @@
+#include "../esubghz_chat_i.h"
+
+/* Sends FreqEntered event to scene manager and displays the frequency in the
+ * text box. */
+static void freq_input_cb(void *context)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	furi_string_cat_printf(state->chat_box_store, "Frequency: %lu",
+			state->frequency);
+
+	scene_manager_handle_custom_event(state->scene_manager,
+			ESubGhzChatEvent_FreqEntered);
+}
+
+/* Validates the entered frequency. */
+static bool freq_input_validator(const char *text, FuriString *error,
+		void *context)
+{
+	furi_assert(text);
+	furi_assert(error);
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+        int ret = sscanf(text, "%lu", &(state->frequency));
+	if (ret != 1) {
+		furi_string_printf(error, "Please enter\nfrequency\nin Hz!");
+		return false;
+	}
+
+	if (!subghz_devices_is_frequency_valid(state->subghz_device,
+				state->frequency)) {
+		furi_string_printf(error, "Frequency\n%lu\n is invalid!",
+				state->frequency);
+		return false;
+	}
+
+#ifdef FW_ORIGIN_Official
+	if (!furi_hal_region_is_frequency_allowed(state->frequency)) {
+#else /* FW_ORIGIN_Official */
+	if (!furi_hal_subghz_is_tx_allowed(state->frequency)) {
+#endif /* FW_ORIGIN_Official */
+		furi_string_printf(error, "TX forbidden\non frequency\n%lu!",
+				state->frequency);
+		return false;
+	}
+
+	return true;
+}
+
+/* Prepares the frequency input scene. */
+void scene_on_enter_freq_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_freq_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	snprintf(state->text_input_store, TEXT_INPUT_STORE_SIZE, "%lu",
+			(uint32_t) DEFAULT_FREQ);
+	text_input_reset(state->text_input);
+	text_input_set_result_callback(
+			state->text_input,
+			freq_input_cb,
+			state,
+			state->text_input_store,
+			sizeof(state->text_input_store),
+			true);
+	text_input_set_validator(
+			state->text_input,
+			freq_input_validator,
+			state);
+	text_input_set_header_text(
+			state->text_input,
+			"Frequency");
+
+	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
+}
+
+/* Handles scene manager events for the frequency input scene. */
+bool scene_on_event_freq_input(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_freq_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to password input scene */
+		case ESubGhzChatEvent_FreqEntered:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_KeyMenu);
+			consumed = true;
+			break;
+		}
+		break;
+
+	case SceneManagerEventTypeBack:
+		/* stop the application if the user presses back here */
+		view_dispatcher_stop(state->view_dispatcher);
+		consumed = true;
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the frequency input scene. */
+void scene_on_exit_freq_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_freq_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	text_input_reset(state->text_input);
+}

+ 90 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_hex_key_input.c

@@ -0,0 +1,90 @@
+#include "../esubghz_chat_i.h"
+
+/* Sets the entered bytes as the key, enters the chat and sends a HexKeyEntered
+ * event to the scene manager. */
+static void hex_key_input_cb(void* context)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	/* initiate the crypto context */
+	bool ret = crypto_ctx_set_key(state->crypto_ctx,
+			state->hex_key_input_store);
+
+	/* cleanup */
+	crypto_explicit_bzero(state->hex_key_input_store,
+			sizeof(state->hex_key_input_store));
+
+	if (!ret) {
+		crypto_ctx_clear(state->crypto_ctx);
+		return;
+	}
+
+	state->encrypted = true;
+
+	enter_chat(state);
+
+	scene_manager_handle_custom_event(state->scene_manager,
+			ESubGhzChatEvent_HexKeyEntered);
+}
+
+/* Prepares the hex key input scene. */
+void scene_on_enter_hex_key_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_hex_key_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	byte_input_set_result_callback(state->hex_key_input,
+			hex_key_input_cb,
+			NULL,
+			state,
+			state->hex_key_input_store,
+			sizeof(state->hex_key_input_store));
+
+	view_dispatcher_switch_to_view(state->view_dispatcher,
+			ESubGhzChatView_HexKeyInput);
+}
+
+/* Handles scene manager events for the hex key input scene. */
+bool scene_on_event_hex_key_input(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_hex_key_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to message input scene */
+		case ESubGhzChatEvent_HexKeyEntered:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_ChatInput);
+			consumed = true;
+			break;
+		}
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the hex key input scene. */
+void scene_on_exit_hex_key_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_hex_key_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	crypto_explicit_bzero(state->hex_key_input_store,
+			sizeof(state->hex_key_input_store));
+}

+ 110 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_display.c

@@ -0,0 +1,110 @@
+#include "../esubghz_chat_i.h"
+
+void key_display_result_cb(DialogExResult result, void* context)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	switch(result) {
+	case DialogExResultLeft:
+		scene_manager_handle_custom_event(state->scene_manager,
+				ESubGhzChatEvent_KeyDisplayBack);
+		break;
+
+	default:
+		break;
+	}
+}
+
+/* Prepares the key display scene. */
+void scene_on_enter_key_display(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_key_display");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	if (state->encrypted) {
+		uint8_t key[KEY_BITS / 8];
+		crypto_ctx_get_key(state->crypto_ctx, key);
+		snprintf(state->key_hex_str, KEY_HEX_STR_SIZE,
+				"%02hX%02hX%02hX%02hX"
+				"%02hX%02hX%02hX%02hX\n"
+				"%02hX%02hX%02hX%02hX"
+				"%02hX%02hX%02hX%02hX\n"
+				"%02hX%02hX%02hX%02hX"
+				"%02hX%02hX%02hX%02hX\n"
+				"%02hX%02hX%02hX%02hX"
+				"%02hX%02hX%02hX%02hX",
+				key[0], key[1], key[2], key[3],
+				key[4], key[5], key[6], key[7],
+				key[8], key[9], key[10], key[11],
+				key[12], key[13], key[14], key[15],
+				key[16], key[17], key[18], key[19],
+				key[20], key[21], key[22], key[23],
+				key[24], key[25], key[26], key[27],
+				key[28], key[29], key[30], key[31]);
+		crypto_explicit_bzero(key, sizeof(key));
+	} else {
+		strcpy(state->key_hex_str, "No Key");
+	}
+
+	dialog_ex_reset(state->key_display);
+
+	dialog_ex_set_text(state->key_display, state->key_hex_str, 64, 2,
+			AlignCenter, AlignTop);
+
+	dialog_ex_set_icon(state->key_display, 0, 0, NULL);
+
+	dialog_ex_set_left_button_text(state->key_display, "Back");
+
+	dialog_ex_set_result_callback(state->key_display,
+			key_display_result_cb);
+	dialog_ex_set_context(state->key_display, state);
+
+	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_KeyDisplay);
+}
+
+/* Handles scene manager events for the key display scene. */
+bool scene_on_event_key_display(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_key_display");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to message input scene */
+		case ESubGhzChatEvent_KeyDisplayBack:
+			if (!scene_manager_previous_scene(
+						state->scene_manager)) {
+				view_dispatcher_stop(state->view_dispatcher);
+			}
+			consumed = true;
+			break;
+		}
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the key display scene. */
+void scene_on_exit_key_display(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_key_display");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	dialog_ex_reset(state->key_display);
+	crypto_explicit_bzero(state->key_hex_str, sizeof(state->key_hex_str));
+}

+ 172 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_menu.c

@@ -0,0 +1,172 @@
+#include "../esubghz_chat_i.h"
+
+typedef enum {
+	ESubGhzChatKeyMenuItems_NoEncryption,
+	ESubGhzChatKeyMenuItems_Password,
+	ESubGhzChatKeyMenuItems_HexKey,
+	ESubGhzChatKeyMenuItems_GenKey,
+} ESubGhzChatKeyMenuItems;
+
+static void key_menu_cb(void* context, uint32_t index)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	uint8_t key[KEY_BITS / 8];
+
+	switch(index) {
+	case ESubGhzChatKeyMenuItems_NoEncryption:
+		state->encrypted = false;
+		enter_chat(state);
+
+		scene_manager_handle_custom_event(state->scene_manager,
+				ESubGhzChatEvent_KeyMenuNoEncryption);
+		break;
+
+	case ESubGhzChatKeyMenuItems_Password:
+		scene_manager_handle_custom_event(state->scene_manager,
+				ESubGhzChatEvent_KeyMenuPassword);
+		break;
+
+	case ESubGhzChatKeyMenuItems_HexKey:
+		scene_manager_handle_custom_event(state->scene_manager,
+				ESubGhzChatEvent_KeyMenuHexKey);
+		break;
+
+	case ESubGhzChatKeyMenuItems_GenKey:
+		/* generate a random key */
+		furi_hal_random_fill_buf(key, KEY_BITS / 8);
+
+		/* initiate the crypto context */
+		bool ret = crypto_ctx_set_key(state->crypto_ctx, key);
+
+		/* cleanup */
+		crypto_explicit_bzero(key, sizeof(key));
+
+		if (!ret) {
+			crypto_ctx_clear(state->crypto_ctx);
+			return;
+		}
+
+		/* set encrypted flag and enter the chat */
+		state->encrypted = true;
+		enter_chat(state);
+
+		scene_manager_handle_custom_event(state->scene_manager,
+				ESubGhzChatEvent_KeyMenuGenKey);
+		break;
+
+	default:
+		break;
+	}
+}
+
+/* Prepares the key menu scene. */
+void scene_on_enter_key_menu(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_key_menu");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	menu_reset(state->menu);
+
+	menu_add_item(
+		state->menu,
+		"No encryption",
+		NULL,
+		ESubGhzChatKeyMenuItems_NoEncryption,
+		key_menu_cb,
+		state
+	);
+	menu_add_item(
+		state->menu,
+		"Password",
+		NULL,
+		ESubGhzChatKeyMenuItems_Password,
+		key_menu_cb,
+		state
+	);
+	menu_add_item(
+		state->menu,
+		"Hex Key",
+		NULL,
+		ESubGhzChatKeyMenuItems_HexKey,
+		key_menu_cb,
+		state
+	);
+	menu_add_item(
+		state->menu,
+		"Generate Key",
+		NULL,
+		ESubGhzChatKeyMenuItems_GenKey,
+		key_menu_cb,
+		state
+	);
+
+	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Menu);
+}
+
+/* Handles scene manager events for the key menu scene. */
+bool scene_on_event_key_menu(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_key_menu");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to message input scene */
+		case ESubGhzChatEvent_KeyMenuNoEncryption:
+		case ESubGhzChatEvent_KeyMenuGenKey:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_ChatInput);
+			consumed = true;
+			break;
+
+		/* switch to password input scene */
+		case ESubGhzChatEvent_KeyMenuPassword:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_PassInput);
+			consumed = true;
+			break;
+
+		/* switch to hex key input scene */
+		case ESubGhzChatEvent_KeyMenuHexKey:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_HexKeyInput);
+			consumed = true;
+			break;
+		}
+
+		break;
+
+	case SceneManagerEventTypeBack:
+		/* stop the application if the user presses back here */
+		view_dispatcher_stop(state->view_dispatcher);
+		consumed = true;
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the key menu scene. */
+void scene_on_exit_key_menu(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_key_menu");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	menu_reset(state->menu);
+}
+

+ 126 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_pass_input.c

@@ -0,0 +1,126 @@
+#include "../esubghz_chat_i.h"
+
+/* Sends PassEntered event to scene manager and enters the chat. */
+static void pass_input_cb(void *context)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	crypto_explicit_bzero(state->text_input_store,
+			sizeof(state->text_input_store));
+
+	enter_chat(state);
+
+	scene_manager_handle_custom_event(state->scene_manager,
+			ESubGhzChatEvent_PassEntered);
+}
+
+/* If a password was entered this derives a key from the password using a
+ * single pass of SHA256 and initiates the AES-GCM context for encryption. If
+ * the initiation fails, the password is rejected. */
+static bool pass_input_validator(const char *text, FuriString *error,
+		void *context)
+{
+	furi_assert(text);
+	furi_assert(error);
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	if (strlen(text) == 0) {
+		furi_string_printf(error, "Enter a\npassword!");
+		return false;
+	}
+
+	unsigned char key[KEY_BITS / 8];
+
+	/* derive a key from the password */
+	sha256((unsigned char *) text, strlen(text), key);
+
+	/* initiate the crypto context */
+	bool ret = crypto_ctx_set_key(state->crypto_ctx, key);
+
+	/* cleanup */
+	crypto_explicit_bzero(key, sizeof(key));
+
+	if (!ret) {
+		crypto_ctx_clear(state->crypto_ctx);
+		furi_string_printf(error, "Failed to\nset key!");
+		return false;
+	}
+
+	state->encrypted = true;
+
+	return true;
+}
+
+/* Prepares the password input scene. */
+void scene_on_enter_pass_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_enter_pass_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	state->text_input_store[0] = 0;
+	text_input_reset(state->text_input);
+	text_input_set_result_callback(
+			state->text_input,
+			pass_input_cb,
+			state,
+			state->text_input_store,
+			sizeof(state->text_input_store),
+			true);
+	text_input_set_validator(
+			state->text_input,
+			pass_input_validator,
+			state);
+	text_input_set_header_text(
+			state->text_input,
+			"Password");
+
+	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
+}
+
+/* Handles scene manager events for the password input scene. */
+bool scene_on_event_pass_input(void* context, SceneManagerEvent event)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_event_pass_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	bool consumed = false;
+
+	switch(event.type) {
+	case SceneManagerEventTypeCustom:
+		switch(event.event) {
+		/* switch to message input scene */
+		case ESubGhzChatEvent_PassEntered:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_ChatInput);
+			consumed = true;
+			break;
+		}
+		break;
+
+	default:
+		consumed = false;
+		break;
+	}
+
+	return consumed;
+}
+
+/* Cleans up the password input scene. */
+void scene_on_exit_pass_input(void* context)
+{
+	FURI_LOG_T(APPLICATION_NAME, "scene_on_exit_pass_input");
+
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	text_input_reset(state->text_input);
+	crypto_explicit_bzero(state->text_input_store,
+			sizeof(state->text_input_store));
+}

+ 30 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_scene.c

@@ -0,0 +1,30 @@
+#include "esubghz_chat_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) scene_on_enter_##name,
+void (*const esubghz_chat_scene_on_enter_handlers[])(void*) = {
+#include "esubghz_chat_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) scene_on_event_##name,
+bool (*const esubghz_chat_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "esubghz_chat_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) scene_on_exit_##name,
+void (*const esubghz_chat_scene_on_exit_handlers[])(void* context) = {
+#include "esubghz_chat_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers esubghz_chat_scene_event_handlers = {
+    .on_enter_handlers = esubghz_chat_scene_on_enter_handlers,
+    .on_event_handlers = esubghz_chat_scene_on_event_handlers,
+    .on_exit_handlers = esubghz_chat_scene_on_exit_handlers,
+    .scene_num = ESubGhzChatScene_MAX,
+};

+ 29 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) ESubGhzChatScene_##id,
+typedef enum {
+#include "esubghz_chat_scene_config.h"
+	ESubGhzChatScene_MAX
+} ESubGhzChatScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers esubghz_chat_scene_event_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void scene_on_enter_##name(void*);
+#include "esubghz_chat_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+	bool scene_on_event_##name(void* context, SceneManagerEvent event);
+#include "esubghz_chat_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void scene_on_exit_##name(void* context);
+#include "esubghz_chat_scene_config.h"
+#undef ADD_SCENE

+ 7 - 0
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_scene_config.h

@@ -0,0 +1,7 @@
+ADD_SCENE(esubghz_chat, freq_input, FreqInput)
+ADD_SCENE(esubghz_chat, key_menu, KeyMenu)
+ADD_SCENE(esubghz_chat, pass_input, PassInput)
+ADD_SCENE(esubghz_chat, hex_key_input, HexKeyInput)
+ADD_SCENE(esubghz_chat, chat_input, ChatInput)
+ADD_SCENE(esubghz_chat, chat_box, ChatBox)
+ADD_SCENE(esubghz_chat, key_display, KeyDisplay)

+ 5 - 0
non_catalog_apps/flipperzero_vb_migrate/CHANGES.md

@@ -0,0 +1,5 @@
+# v1.1
+Updated data directory path and built for API 26.3
+
+# v1.0 Release to the Web
+Brand new UI, support for all VBs

+ 25 - 0
non_catalog_apps/flipperzero_vb_migrate/README_catalog.md

@@ -0,0 +1,25 @@
+# VB Lab Migration Assistant for Flipper Zero
+
+This app is designed to make transferring your characters from VB Lab and VBC
+Lab more convenient.
+
+Send me a Ko-fi if you found this useful: https://ko-fi.com/C0C81P4PX
+
+## Background
+The Vital Bracelet Arena app is going to be the new companion app for the Vital
+Bracelet series of fitness bracelet toys, however the app does not support
+account linking from VB Lab and VBC Lab, and requires you to transfer characters
+one by one from those apps through your Vital Bracelet. Because the Vital
+Bracelet can only hold at most two characters at a time and requires inserting
+Dim/VBM to complete the transfer, transferring all of your characters may take a
+significant amount of time. The VB Lab Migration Assistant is designed to make
+this process faster by allowing you to store an unlimited amount of characters
+on the Flipper and to bypass the Dim loading process, therefore allowing you to
+transfer your characters without having to flip back and forth between the apps
+or wasting time loading data that will never be used.
+
+## Usage
+See README.md in source repository: https://github.com/GMMan/flipperzero-vb-migrate/blob/master/README.md
+
+## Credits
+Graphics by Aderek: https://twitter.com/AderekArt

+ 2 - 7
non_catalog_apps/flipperzero_vb_migrate/application.fam

@@ -21,17 +21,12 @@ App(
     name="VB Migration Assistant",
     apptype=FlipperAppType.EXTERNAL,
     entry_point="vb_migrate_app",
-    requires=["gui", "storage"],
+    requires=["gui", "dialogs", "notification", "storage"],
     stack_size=2 * 1024,
     fap_version=(1, 1),
     fap_libs=[],
-    fap_private_libs=[
-        Lib(
-            name="nfc",
-        ),
-    ],
     fap_icon="vb_migrate_10px.png",
-    fap_category="NFC",
+    fap_category="Tools",
     fap_description="Makes transferring characters with VB Lab less cumbersome",
     fap_author="cyanic",
     fap_weburl="https://github.com/GMMan/flipperzero-vb-migrate",

+ 1 - 1
non_catalog_apps/flipperzero_vb_migrate/vb_migrate_i.h

@@ -34,7 +34,7 @@
 #include <notification/notification.h>
 #include <dialogs/dialogs.h>
 
-#include "lib/nfc/nfc_worker.h"
+#include <lib/nfc/nfc_worker.h>
 
 #include "vb_migrate.h"
 #include "scenes/vb_migrate_scene.h"

+ 3 - 0
non_catalog_apps/gpio_controller/README.md

@@ -0,0 +1,3 @@
+# gpio_controller
+
+A visual tool to control the general purpose pins of the Flipper Zero

+ 11 - 0
non_catalog_apps/gpio_controller/application.fam

@@ -0,0 +1,11 @@
+App(
+    appid="gpio_controller",
+    name="GPIO Controller",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="gpio_controller_main",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    fap_category="GPIO",
+    fap_icon="icon10px.png",
+    fap_icon_assets="images",
+)

+ 239 - 0
non_catalog_apps/gpio_controller/gpio_controller.c

@@ -0,0 +1,239 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <input/input.h>
+
+/* Magic happens here -- this file is generated by fbt.
+ * Just set fap_icon_assets in application.fam and #include {APPID}_icons.h */
+#include "gpio_controller_icons.h"
+
+#include "gpio_items.h"
+
+typedef struct {
+    int selected;
+    GPIOItems* gpio_items;
+} ViewerState;
+
+static ViewerState vstate = {.selected = 0};
+
+typedef enum {
+    PIN_5V = 0,
+    PIN_A7,
+    PIN_A6,
+    PIN_A4,
+    PIN_B3,
+    PIN_B2,
+    PIN_C3,
+    GEARIC,
+    PIN_3V,
+    PIN_SWC,
+    PIN_SIO,
+    PIN_TX,
+    PIN_RX,
+    PIN_C1,
+    PIN_C0,
+    PIN_1W,
+    PIN_GND_08,
+    PIN_GND_11,
+    PIN_GND_18,
+    NONE
+}ViewElement;
+
+ //  5V  A7  A6  A4  B3  B2  C3 GND SET
+ //
+ //
+ //  3V SWC GND SIO  TX  RX  C1  C0  1W GND
+
+// next element when pressing up or down from a given element
+// ground pins cannot be selected
+static uint8_t y_mapping[] = { 
+    PIN_3V,  // <- PIN_5V
+    PIN_SWC, // <- PIN_A7
+    NONE,    // <- PIN_A6
+    PIN_SIO, // <- PIN_A4
+    PIN_TX,  // <- PIN_B3
+    PIN_RX,  // <- PIN_B2
+    PIN_C1,  // <- PIN_C3
+    PIN_1W,  // <- GEARIC
+    PIN_5V,  // <- PIN_3V
+    PIN_A7,  // <- PIN_SWC
+    PIN_A4,  // <- PIN_SIO
+    PIN_B3,  // <- PIN_TX
+    PIN_B2,  // <- PIN_RX
+    PIN_C3,  // <- PIN_C1
+    NONE,    // <- PIN_C0
+    GEARIC   // <- PIN_1W
+};
+
+static uint8_t x_pos[] = {
+      0, // PIN_5V
+     14, // PIN_A7
+     28, // PIN_A6
+     42, // PIN_A4
+     56, // PIN_B3
+     70, // PIN_B2
+     84, // PIN_C3
+    112, // GEARIC
+      0, // PIN_3V
+     14, // PIN_SWC
+     42, // PIN_SIO
+     56, // PIN_TX
+     70, // PIN_RX
+     84, // PIN_C1
+     98, // PIN_C0
+    112, // PIN_1W
+     98, // PIN_GND_08
+     28, // PIN_GND_11
+    126  // PIN_GND_18
+};
+
+static int gp_pins[] = {
+     -1, // PIN_5V
+      0, // PIN_A7
+      1, // PIN_A6
+      2, // PIN_A4
+      3, // PIN_B3
+      4, // PIN_B2
+      5, // PIN_C3
+     -1, // GEARIC
+     -1, // PIN_3V
+     -1, // PIN_SWC
+     -1, // PIN_SIO
+     -1, // PIN_TX
+     -1, // PIN_RX
+      6, // PIN_C1
+      7, // PIN_C0
+     -1, // PIN_1W
+};
+
+static Icon * icons[] = {
+    (Icon*)&I_5v_pin,   // PIN_5V
+    (Icon*)&I_a7_pin,   // PIN_A7
+    (Icon*)&I_a6_pin,   // PIN_A6
+    (Icon*)&I_a4_pin,   // PIN_A4
+    (Icon*)&I_b3_pin,   // PIN_B3
+    (Icon*)&I_b2_pin,   // PIN_B2
+    (Icon*)&I_c3_pin,   // PIN_C3
+    (Icon*)&I_gear_unhighlighted, // GEARIC
+    (Icon*)&I_3v_pin,   // PIN_3V
+    (Icon*)&I_swc_pin,  // PIN_SWC
+    (Icon*)&I_sio_pin,  // PIN_SIO
+    (Icon*)&I_tx_pin,   // PIN_TX
+    (Icon*)&I_rx_pin,   // PIN_RX
+    (Icon*)&I_c1_pin,   // PIN_C1
+    (Icon*)&I_c0_pin,   // PIN_C0
+    (Icon*)&I_1w_pin    // PIN_1W
+};
+
+static uint8_t bot_row_y = 48;
+
+// Screen is 128x64 px
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    UNUSED(ctx);
+
+    canvas_clear(canvas);
+
+    // draw ground pins
+    canvas_draw_icon(canvas, x_pos[PIN_GND_08], -1, &I_gnd_pin);
+    canvas_draw_icon(canvas, x_pos[PIN_GND_11], bot_row_y, &I_gnd_pin);
+    canvas_draw_icon(canvas, x_pos[PIN_GND_18], bot_row_y, &I_gnd_pin);
+
+    // draw gear
+    canvas_draw_icon(canvas, x_pos[GEARIC], 0, (vstate.selected == GEARIC ? &I_gear_highlighted : &I_gear_unhighlighted));
+
+    // draw top row of pins
+    for( int i = 0; i < GEARIC; i++ )
+    {
+        int y = vstate.selected == i ? 0 : -3;
+        canvas_draw_icon(canvas, x_pos[i], y, icons[i]);
+    }
+
+    // draw bottom row of pins
+    for( int i = PIN_3V; i <= PIN_1W; i++ )
+    {
+        int y = bot_row_y - (vstate.selected == i ? 3 : 0);
+        canvas_draw_icon(canvas, x_pos[i], y, icons[i]);
+    }
+}
+
+static void app_input_callback(InputEvent* input_event, void* ctx) {
+    furi_assert(ctx);
+
+    FuriMessageQueue* event_queue = ctx;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+int32_t gpio_controller_main(void* p) {
+    UNUSED(p);
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    // Configure view port
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, app_draw_callback, view_port);
+    view_port_input_callback_set(view_port, app_input_callback, event_queue);
+
+    // Register view port in GUI
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    InputEvent event;
+
+    vstate.gpio_items = gpio_items_alloc();
+    gpio_items_configure_all_pins(vstate.gpio_items, GpioModeOutputPushPull);
+
+    bool running = true;
+    while(running) {
+        if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
+            if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
+                switch(event.key) {
+                case InputKeyLeft:
+                    vstate.selected--;
+                    if(vstate.selected == GEARIC) vstate.selected = PIN_1W;
+                    else if(vstate.selected < 0) vstate.selected = GEARIC;
+                    break;
+                case InputKeyRight:
+                    if(vstate.selected <= GEARIC)
+                    {
+                        vstate.selected++;
+                        vstate.selected = vstate.selected > GEARIC ? PIN_5V : vstate.selected;
+                    }
+                    else
+                    {
+                        vstate.selected++;
+                        vstate.selected = vstate.selected > PIN_1W ? PIN_3V : vstate.selected;
+                    }
+                    break;
+                case InputKeyUp:
+                case InputKeyDown:
+                    if (y_mapping[vstate.selected] != NONE) vstate.selected = y_mapping[vstate.selected];
+                    break;
+                case InputKeyBack:
+                    running = false;
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+        else if(event.key == InputKeyOk)
+        {
+            if( gp_pins[vstate.selected] >= 0 && (event.type == InputTypePress || event.type == InputTypeRelease) )
+            {
+                gpio_items_set_pin(vstate.gpio_items, gp_pins[vstate.selected], event.type == InputTypePress);
+            }
+        }
+        view_port_update(view_port);
+    }
+
+    gpio_items_free(vstate.gpio_items);
+
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+
+    furi_record_close(RECORD_GUI);
+
+    return 0;
+}

+ 69 - 0
non_catalog_apps/gpio_controller/gpio_items.c

@@ -0,0 +1,69 @@
+#include "gpio_items.h"
+
+#include <furi_hal_resources.h>
+
+struct GPIOItems {
+    GpioPinRecord* pins;
+    size_t count;
+};
+
+GPIOItems* gpio_items_alloc() {
+    GPIOItems* items = malloc(sizeof(GPIOItems));
+
+    items->count = 0;
+    for(size_t i = 0; i < gpio_pins_count; i++) {
+        if(!gpio_pins[i].debug) {
+            items->count++;
+        }
+    }
+
+    items->pins = malloc(sizeof(GpioPinRecord) * items->count);
+    for(size_t i = 0; i < items->count; i++) {
+        if(!gpio_pins[i].debug) {
+            items->pins[i].pin = gpio_pins[i].pin;
+            items->pins[i].name = gpio_pins[i].name;
+        }
+    }
+    return items;
+}
+
+void gpio_items_free(GPIOItems* items) {
+    free(items->pins);
+    free(items);
+}
+
+uint8_t gpio_items_get_count(GPIOItems* items) {
+    return items->count;
+}
+
+void gpio_items_configure_pin(GPIOItems* items, uint8_t index, GpioMode mode) {
+    furi_assert(index < items->count);
+    furi_hal_gpio_write(items->pins[index].pin, false);
+    furi_hal_gpio_init(items->pins[index].pin, mode, GpioPullNo, GpioSpeedVeryHigh);
+}
+
+void gpio_items_configure_all_pins(GPIOItems* items, GpioMode mode) {
+    for(uint8_t i = 0; i < items->count; i++) {
+        gpio_items_configure_pin(items, i, mode);
+    }
+}
+
+void gpio_items_set_pin(GPIOItems* items, uint8_t index, bool level) {
+    furi_assert(index < items->count);
+    furi_hal_gpio_write(items->pins[index].pin, level);
+}
+
+void gpio_items_set_all_pins(GPIOItems* items, bool level) {
+    for(uint8_t i = 0; i < items->count; i++) {
+        gpio_items_set_pin(items, i, level);
+    }
+}
+
+const char* gpio_items_get_pin_name(GPIOItems* items, uint8_t index) {
+    furi_assert(index < items->count + 1);
+    if(index == items->count) {
+        return "ALL";
+    } else {
+        return items->pins[index].name;
+    }
+}

+ 29 - 0
non_catalog_apps/gpio_controller/gpio_items.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <furi_hal_gpio.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct GPIOItems GPIOItems;
+
+GPIOItems* gpio_items_alloc();
+
+void gpio_items_free(GPIOItems* items);
+
+uint8_t gpio_items_get_count(GPIOItems* items);
+
+void gpio_items_configure_pin(GPIOItems* items, uint8_t index, GpioMode mode);
+
+void gpio_items_configure_all_pins(GPIOItems* items, GpioMode mode);
+
+void gpio_items_set_pin(GPIOItems* items, uint8_t index, bool level);
+
+void gpio_items_set_all_pins(GPIOItems* items, bool level);
+
+const char* gpio_items_get_pin_name(GPIOItems* items, uint8_t index);
+
+#ifdef __cplusplus
+}
+#endif

BIN
non_catalog_apps/gpio_controller/icon10px.png


BIN
non_catalog_apps/gpio_controller/images/1w_pin.png


BIN
non_catalog_apps/gpio_controller/images/3v_pin.png


BIN
non_catalog_apps/gpio_controller/images/5v_pin.png


BIN
non_catalog_apps/gpio_controller/images/a4_pin.png


BIN
non_catalog_apps/gpio_controller/images/a6_pin.png


BIN
non_catalog_apps/gpio_controller/images/a7_pin.png


BIN
non_catalog_apps/gpio_controller/images/arrow_down.png


BIN
non_catalog_apps/gpio_controller/images/arrow_up.png


BIN
non_catalog_apps/gpio_controller/images/b2_pin.png


BIN
non_catalog_apps/gpio_controller/images/b3_pin.png


BIN
non_catalog_apps/gpio_controller/images/c0_pin.png


BIN
non_catalog_apps/gpio_controller/images/c1_pin.png


BIN
non_catalog_apps/gpio_controller/images/c3_pin.png


BIN
non_catalog_apps/gpio_controller/images/gear_highlighted.png


BIN
non_catalog_apps/gpio_controller/images/gear_unhighlighted.png


BIN
non_catalog_apps/gpio_controller/images/gnd_pin.png


BIN
non_catalog_apps/gpio_controller/images/rx_pin.png


BIN
non_catalog_apps/gpio_controller/images/sio_pin.png


BIN
non_catalog_apps/gpio_controller/images/swc_pin.png


BIN
non_catalog_apps/gpio_controller/images/tx_pin.png


+ 1 - 1
non_catalog_apps/seader/application.fam

@@ -23,7 +23,7 @@ App(
     ],
     fap_icon="icons/logo.png",
     fap_category="NFC",
-    fap_version="1.0",
+    fap_version="1.1",
     fap_author="bettse",
 #    fap_extbuild=(
 #        ExtFile(

+ 8 - 11
non_catalog_apps/seader/readme.md

@@ -8,28 +8,25 @@ File issues in [GitHub](https://github.com/bettse/seader/issues).
 
 ## Hardware
 
-Put **[SAM](https://www.cdw.com/product/hp-sim-for-hid-iclass-for-hip2-reader-security-sim/4854794)** into **[adapter](https://a.co/d/1E9Zk1h)** (because of chip on top) and plug into **[reader](https://www.mikroe.com/smart-card-2-click)**. Connect reader to Flipper Zero (See `Connections` below).
+### Option 1: NARD flipper add-on
+
+Buy it assembled at [Red Team Tools](https://www.redteamtools.com/nard-sam-expansion-board-for-flipper-zero-with-hid-seos-iclass-sam/), with or without SAM.
 
-Alternatively, [NARD flipper add-on](https://github.com/killergeek/nard) works.
 
-### Connections
+Or build it yourself from the files in the [NARD repo](https://github.com/killergeek/nard).
 
-| Smart Card 2 Click | Flipper     |
-| ------------------ | ----------- |
-| 5v                 | 1           |
-| GND                | 8 / 11 / 18 |
-| TX                 | 16          |
-| RX                 | 15          |
+### Option 2: Smart Card 2 Click
 
+Put **[SAM](https://www.cdw.com/product/hp-sim-for-hid-iclass-for-hip2-reader-security-sim/4854794)** into **[adapter](https://a.co/d/1E9Zk1h)** (because of chip on top) and plug into **[reader](https://www.mikroe.com/smart-card-2-click)**. Connect reader to Flipper Zero (See `Connections` below).
 
 ## Development
 
-### Update ASN1
+### To Build ASN1
 
  * Install git version of [asnc1](https://github.com/vlm/asn1c) (`brew install asn1c --head` on macos)
  * Run `asn1c -D ./lib/asn1 -no-gen-example -pdu=all seader.asn` in in root to generate asn1c files
 
-### Update App
+### To Build App
 
  * Install [UFBT](https://github.com/flipperdevices/flipperzero-ufbt)
  * `ufbt` to build

+ 5 - 11
non_catalog_apps/seader/scenes/seader_scene_card_menu.c

@@ -34,22 +34,16 @@ void seader_scene_card_menu_on_enter(void* context) {
         SubmenuIndexSaveRFID,
         seader_scene_card_menu_submenu_callback,
         seader);
-    if(seader->is_debug_enabled) {
-        if(credential->sio[0] == 0x30) {
-            submenu_add_item(
-                submenu,
-                "Save SR",
-                SubmenuIndexSaveSR,
-                seader_scene_card_menu_submenu_callback,
-                seader);
-        }
+    if(credential->sio[0] == 0x30) {
         submenu_add_item(
             submenu,
-            "Save MFC",
-            SubmenuIndexSaveMFC,
+            "Save SR",
+            SubmenuIndexSaveSR,
             seader_scene_card_menu_submenu_callback,
             seader);
     }
+    submenu_add_item(
+        submenu, "Save MFC", SubmenuIndexSaveMFC, seader_scene_card_menu_submenu_callback, seader);
 
     submenu_set_selected_item(
         seader->submenu,

+ 58 - 0
non_catalog_apps/ublox/.gitignore

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

+ 674 - 0
non_catalog_apps/ublox/LICENSE

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

+ 2 - 0
non_catalog_apps/ublox/README.md

@@ -0,0 +1,2 @@
+# ublox
+Flipper Zero u-blox GPS app

+ 17 - 0
non_catalog_apps/ublox/application.fam

@@ -0,0 +1,17 @@
+App(
+    appid="ublox",
+    name="u-blox GPS",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="ublox_app",
+    cdefines=["APP_UBLOX"],
+    requires=[
+        "gui",
+	"i2c",
+	"locale",
+    ],
+    stack_size=2 * 1024,
+    order=20,
+    fap_icon="ublox_app_icon.png",
+    fap_category="GPIO", 
+    fap_icon_assets="images",
+)

+ 5 - 0
non_catalog_apps/ublox/helpers/ublox_custom_event.h

@@ -0,0 +1,5 @@
+#pragma once
+
+typedef enum {
+  UbloxCustomEventResetOdometer,
+} UbloxCustomEvent;

+ 48 - 0
non_catalog_apps/ublox/helpers/ublox_types.h

@@ -0,0 +1,48 @@
+#pragma once
+
+typedef enum {
+  UbloxDataDisplayViewModeHandheld,
+  UbloxDataDisplayViewModeCar,
+} UbloxDataDisplayViewMode;
+
+  
+typedef enum {
+  UbloxDataDisplayBacklightOn,
+  UbloxDataDisplayBacklightDefault,
+} UbloxDataDisplayBacklightMode;
+
+typedef uint32_t UbloxDataDisplayRefreshRate;
+
+typedef enum {
+  UbloxDataDisplayNotifyOn,
+  UbloxDataDisplayNotifyOff,
+} UbloxDataDisplayNotifyMode;
+
+typedef enum {
+  UbloxOdometerModeRunning = 0,
+  UbloxOdometerModeCycling = 1,
+  UbloxOdometerModeSwimming = 2,
+  UbloxOdometerModeCar = 3,
+} UbloxOdometerMode;
+
+typedef enum {
+  UbloxPlatformModelPortable = 0,
+  UbloxPlatformModelPedestrian = 3,
+  UbloxPlatformModelAutomotive = 4,
+  UbloxPlatformModelAtSea = 5,
+  UbloxPlatformModelAirborne2g = 7,
+  UbloxPlatformModelWrist = 9,
+} UbloxPlatformModel;
+  
+
+typedef struct UbloxDataDisplayState {
+  UbloxDataDisplayViewMode view_mode;
+  UbloxDataDisplayBacklightMode backlight_mode;
+  UbloxDataDisplayRefreshRate refresh_rate;
+  UbloxDataDisplayNotifyMode notify_mode;
+} UbloxDataDisplayState;
+
+typedef struct UbloxDeviceState {
+  UbloxOdometerMode odometer_mode;
+  UbloxPlatformModel platform_model;
+} UbloxDeviceState;

+ 191 - 0
non_catalog_apps/ublox/images/.clang-format

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

BIN
non_catalog_apps/ublox/images/ublox_wiring.png


BIN
non_catalog_apps/ublox/images/ublox_wiring.xcf


+ 30 - 0
non_catalog_apps/ublox/scenes/ublox_scene.c

@@ -0,0 +1,30 @@
+#include "ublox_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const ublox_on_enter_handlers[])(void*) = {
+#include "ublox_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const ublox_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "ublox_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const ublox_on_exit_handlers[])(void* context) = {
+#include "ublox_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers ublox_scene_handlers = {
+    .on_enter_handlers = ublox_on_enter_handlers,
+    .on_event_handlers = ublox_on_event_handlers,
+    .on_exit_handlers = ublox_on_exit_handlers,
+    .scene_num = UbloxSceneNum,
+};

+ 29 - 0
non_catalog_apps/ublox/scenes/ublox_scene.h

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

+ 51 - 0
non_catalog_apps/ublox/scenes/ublox_scene_about.c

@@ -0,0 +1,51 @@
+#include "../ublox_i.h"
+
+void ublox_scene_about_on_enter(void* context) {
+    Ublox* ublox = context;
+
+    FuriString* s = furi_string_alloc();
+    widget_add_text_box_element(
+        ublox->widget,
+        0,
+        0,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!                                                      \e!\n",
+        false);
+    widget_add_text_box_element(
+        ublox->widget,
+        0,
+        2,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!          u-blox GPS         \e!\n",
+        false);
+
+    furi_string_printf(
+        s,
+        "%s\n",
+        "u-blox GPS\n\nMade by: Liam Hays\n\nGitHub: https://github.com/liamhays/ublox\n\nVersion: WIP\n\nLicense: GPL-3.0");
+
+    widget_add_text_scroll_element(ublox->widget, 0, 16, 128, 50, furi_string_get_cstr(s));
+
+    furi_string_free(s);
+
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewWidget);
+}
+
+bool ublox_scene_about_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void ublox_scene_about_on_exit(void* context) {
+    furi_assert(context);
+    Ublox* ublox = context;
+
+    widget_reset(ublox->widget);
+}

+ 5 - 0
non_catalog_apps/ublox/scenes/ublox_scene_config.h

@@ -0,0 +1,5 @@
+ADD_SCENE(ublox, start, Start)
+ADD_SCENE(ublox, data_display, DataDisplay)
+ADD_SCENE(ublox, data_display_config, DataDisplayConfig)
+ADD_SCENE(ublox, wiring, Wiring)
+ADD_SCENE(ublox, about, About)

+ 132 - 0
non_catalog_apps/ublox/scenes/ublox_scene_data_display.c

@@ -0,0 +1,132 @@
+// This is a personal academic project. Dear PVS-Studio, please check it.
+
+// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
+#include "../ublox_i.h"
+#include "../ublox_worker_i.h"
+
+#define TAG "ublox_scene_data_display"
+
+const NotificationSequence sequence_new_reading = {
+  //&message_vibro_on,
+  &message_green_255,
+  &message_delay_100,
+  &message_green_0,
+  //&message_vibro_off,
+  NULL,
+};
+
+void ublox_scene_data_display_worker_callback(UbloxWorkerEvent event, void* context) {
+  Ublox* ublox = context;
+
+  view_dispatcher_send_custom_event(ublox->view_dispatcher, event);
+}
+
+void ublox_scene_data_display_view_callback(void* context, InputKey key) {
+  Ublox* ublox = context;
+
+  // just reuse generic events
+  if (key == InputKeyLeft) {
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeLeft);
+  } else if (key == InputKeyOk) {
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeCenter);
+  }
+}
+
+static void timer_callback(void* context) {
+  Ublox* ublox = context;
+  FURI_LOG_I(TAG, "mem free before timer callback: %u", memmgr_get_free_heap());
+  // every time, try to set the view back to a GPS-found mode
+
+  // TODO: maybe different states for each message, to fix the leak?
+  ublox_worker_start(ublox->worker, UbloxWorkerStateRead,
+		     ublox_scene_data_display_worker_callback, ublox);
+  FURI_LOG_I(TAG, "mem free after timer callback: %u", memmgr_get_free_heap());
+}
+
+void ublox_scene_data_display_on_enter(void* context) {
+  Ublox* ublox = context;
+
+  data_display_set_callback(ublox->data_display, ublox_scene_data_display_view_callback, ublox);
+  if ((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeHandheld) {
+    data_display_set_state(ublox->data_display, DataDisplayHandheldMode);
+  } else if ((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeCar) {
+    data_display_set_state(ublox->data_display, DataDisplayCarMode);
+  }
+  
+  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewDataDisplay);
+  
+  ublox->timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, ublox);
+
+  // convert from seconds to milliseconds
+  furi_timer_start(ublox->timer, furi_ms_to_ticks((ublox->data_display_state).refresh_rate * 1000));
+
+  ublox_worker_start(ublox->worker, UbloxWorkerStateRead,
+		     ublox_scene_data_display_worker_callback, ublox);
+}
+
+bool ublox_scene_data_display_on_event(void* context, SceneManagerEvent event) {
+  Ublox* ublox = context;
+  bool consumed = false;
+  //FURI_LOG_I(TAG, "mem free before event branch: %u", memmgr_get_free_heap());
+  if (event.type == SceneManagerEventTypeCustom) {
+    if (event.event == GuiButtonTypeLeft) {
+      FURI_LOG_I(TAG, "left button pressed");
+      scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplayConfig);
+      consumed = true;
+      
+    } else if (event.event == GuiButtonTypeCenter) {
+      furi_timer_stop(ublox->timer);
+      // MUST stop the worker first! (idk why though)
+      ublox_worker_stop(ublox->worker);
+      FURI_LOG_I(TAG, "reset odometer");
+      ublox_worker_start(ublox->worker, UbloxWorkerStateResetOdometer,
+			 ublox_scene_data_display_worker_callback, ublox);
+      furi_timer_start(ublox->timer, furi_ms_to_ticks((ublox->data_display_state).refresh_rate * 1000));
+      
+    } else if (event.event == UbloxWorkerEventDataReady) {
+      if ((ublox->data_display_state).notify_mode == UbloxDataDisplayNotifyOn) {
+	notification_message(ublox->notifications, &sequence_new_reading);
+      }
+
+      // Not setting the NAV messages in the data display seems to help...?
+      // (so many things have "seemed to help" that it's immensely confusing).
+
+      // disabling refresh in the with_view_model() call in the update function kinda helps
+      data_display_set_nav_messages(ublox->data_display, ublox->nav_pvt, ublox->nav_odo);
+
+      // There used to be a "set view_mode" if/else here. I don't
+      // think it was necessary but I do think it was contributing to
+      // the memory leak. With it removed, we get little to no leakage
+      // (and the leakage "automatically" fixes itself: some memory
+      // will be incorrectly allocated but then fixed).
+
+      // The upshot is that we used to refresh the data display scene
+      // 3 times in this callback. I suspect that the view handling
+      // code in the OS is good but not perfect, and holds on to
+      // memory for longer than it needs to. Reducing the number of
+      // total updates has helped.
+      
+    } else if (event.event == UbloxWorkerEventFailed) {
+      FURI_LOG_I(TAG, "UbloxWorkerEventFailed");
+      data_display_set_state(ublox->data_display, DataDisplayGPSNotFound);
+    }
+  }
+  //FURI_LOG_I(TAG, "mem free after event branch: %u", memmgr_get_free_heap());
+  return consumed;
+}
+
+void ublox_scene_data_display_on_exit(void* context) {
+  Ublox* ublox = context;
+
+  furi_timer_stop(ublox->timer);
+  furi_timer_free(ublox->timer);
+
+  ublox_worker_stop(ublox->worker);
+  
+  data_display_reset(ublox->data_display);
+  // Use any existing data
+  data_display_set_nav_messages(ublox->data_display, ublox->nav_pvt, ublox->nav_odo);
+  FURI_LOG_I(TAG, "leaving data display scene");
+}
+    
+  

+ 346 - 0
non_catalog_apps/ublox/scenes/ublox_scene_data_display_config.c

@@ -0,0 +1,346 @@
+#include "../ublox_i.h"
+
+#define TAG "ublox_scene_data_display_config"
+
+enum UbloxSettingIndex {
+  UbloxSettingIndexRefreshRate,
+  UbloxSettingIndexBacklightMode,
+  UbloxSettingIndexDisplayMode,
+  UbloxSettingIndexNotify,
+  UbloxSettingIndexPlatformModel,
+  UbloxSettingIndexOdometerMode,
+};
+
+enum UbloxDataDisplayConfigIndex {
+  UbloxDataDisplayConfigIndexDisplayMode,
+  UbloxDataDisplayConfigIndexRefreshRate,
+  UbloxDataDisplayConfigIndexBacklightMode,
+};
+
+#define DISPLAY_VIEW_MODE_COUNT 2
+const char* const display_view_mode_text[DISPLAY_VIEW_MODE_COUNT] = {
+  "Handheld",
+  "Car",
+};
+
+const UbloxDataDisplayViewMode display_view_mode_value[DISPLAY_VIEW_MODE_COUNT] = {
+  UbloxDataDisplayViewModeHandheld,
+  UbloxDataDisplayViewModeCar,
+};
+
+#define BACKLIGHT_MODE_COUNT 2
+const char* const backlight_mode_text[BACKLIGHT_MODE_COUNT] = {
+  "Default",
+  "On",
+};
+
+const UbloxDataDisplayBacklightMode backlight_mode_value[BACKLIGHT_MODE_COUNT] = {
+  UbloxDataDisplayBacklightDefault,
+  UbloxDataDisplayBacklightOn,
+};
+
+#define REFRESH_RATE_COUNT 8
+// double const means that the data is constant and that the pointer
+// is constant.
+const char* const refresh_rate_text[REFRESH_RATE_COUNT] = {
+  "2s",
+  "5s",
+  "10s",
+  "15s",
+  "20s",
+  "30s",
+  "45s",
+  "60s",
+};
+
+// might need to be ms?
+const UbloxDataDisplayRefreshRate refresh_rate_values[REFRESH_RATE_COUNT] = {
+  2,
+  5,
+  10,
+  15,
+  20,
+  30,
+  45,
+  60,
+};
+
+#define NOTIFY_MODE_COUNT 2
+const char* const notify_mode_text[NOTIFY_MODE_COUNT] = {
+  "On",
+  "Off",
+};
+
+const UbloxDataDisplayNotifyMode notify_mode_values[NOTIFY_MODE_COUNT] = {
+  UbloxDataDisplayNotifyOn,
+  UbloxDataDisplayNotifyOff,
+};
+
+#define ODOMETER_MODE_COUNT 4
+const char* const odometer_mode_text[ODOMETER_MODE_COUNT] = {
+  "Run",
+  "Cycle",
+  "Swim",
+  "Car",
+};
+
+const UbloxOdometerMode odometer_mode_values[ODOMETER_MODE_COUNT] = {
+  UbloxOdometerModeRunning,
+  UbloxOdometerModeCycling,
+  UbloxOdometerModeSwimming,
+  UbloxOdometerModeCar,
+};
+
+#define PLATFORM_MODEL_COUNT 6
+const char* const platform_model_text[PLATFORM_MODEL_COUNT] = {
+  "Portable",
+  "Pedestrian",
+  "Automotive",
+  "At Sea",
+  "Airborne <2g",
+  "Wrist",
+};
+
+const UbloxPlatformModel platform_model_values[PLATFORM_MODEL_COUNT] = {
+  UbloxPlatformModelPortable,
+  UbloxPlatformModelPedestrian,
+  UbloxPlatformModelAutomotive,
+  UbloxPlatformModelAtSea,
+  UbloxPlatformModelAirborne2g,
+  UbloxPlatformModelWrist,
+};
+
+uint8_t ublox_scene_data_display_config_next_refresh_rate(const UbloxDataDisplayRefreshRate value, void* context) {
+  furi_assert(context);
+  
+  uint8_t index = 0;
+  for (int i = 0; i < REFRESH_RATE_COUNT; i++) {
+    if (value == refresh_rate_values[i]) {
+      index = i;
+      break;
+    } else {
+      index = 0;
+    }
+  }
+  return index;
+}
+
+static void ublox_scene_data_display_config_set_refresh_rate(VariableItem* item) {
+  Ublox* ublox = variable_item_get_context(item);
+  uint8_t index = variable_item_get_current_value_index(item);
+
+  variable_item_set_current_value_text(item, refresh_rate_text[index]);
+  (ublox->data_display_state).refresh_rate = refresh_rate_values[index];
+  FURI_LOG_I(TAG, "set refresh rate to %lds", (ublox->data_display_state).refresh_rate);
+}
+
+static uint8_t ublox_scene_data_display_config_next_backlight_mode(const UbloxDataDisplayBacklightMode value, void* context) {
+  furi_assert(context);
+
+  uint8_t index = 0;
+  for (int i = 0; i < BACKLIGHT_MODE_COUNT; i++) {
+    if (value == backlight_mode_value[i]) {
+      index = i;
+      break;
+    } else {
+      index = 0;
+    }
+  }
+  return index;
+}
+
+
+static void ublox_scene_data_display_config_set_backlight_mode(VariableItem* item) {
+  Ublox* ublox = variable_item_get_context(item);
+  uint8_t index = variable_item_get_current_value_index(item);
+
+  variable_item_set_current_value_text(item, backlight_mode_text[index]);
+  (ublox->data_display_state).backlight_mode = backlight_mode_value[index];
+
+  if ((ublox->data_display_state).backlight_mode == UbloxDataDisplayBacklightOn) {
+    notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_on);
+  } else if ((ublox->data_display_state).backlight_mode == UbloxDataDisplayBacklightDefault) {
+    notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_auto);
+  }
+}
+
+static uint8_t ublox_scene_data_display_config_next_display_view_mode(const UbloxDataDisplayViewMode value, void* context) {
+  furi_assert(context);
+
+  uint8_t index = 0;
+  for (int i = 0; i < DISPLAY_VIEW_MODE_COUNT; i++) {
+    if (value == display_view_mode_value[i]) {
+      index = i;
+      break;
+    } else {
+      index = 0;
+    }
+  }
+  return index;
+}
+    
+static void ublox_scene_data_display_config_set_display_view_mode(VariableItem* item) {
+  Ublox* ublox = variable_item_get_context(item);
+  uint8_t index = variable_item_get_current_value_index(item);
+
+  variable_item_set_current_value_text(item, display_view_mode_text[index]);
+  (ublox->data_display_state).view_mode = display_view_mode_value[index];
+}
+
+static uint8_t ublox_scene_data_display_config_next_notify_mode(const UbloxDataDisplayNotifyMode value, void* context) {
+  furi_assert(context);
+
+  uint8_t index = 0;
+  for (int i = 0; i < NOTIFY_MODE_COUNT; i++) {
+    if (value == notify_mode_values[i]) {
+      index = i;
+      break;
+    } else {
+      index = 0;
+    }
+  }
+  return index;
+}
+
+static void ublox_scene_data_display_config_set_notify_mode(VariableItem* item) {
+  Ublox* ublox = variable_item_get_context(item);
+  uint8_t index = variable_item_get_current_value_index(item);
+
+  variable_item_set_current_value_text(item, notify_mode_text[index]);
+  (ublox->data_display_state).notify_mode = notify_mode_values[index];
+}
+
+static uint8_t ublox_scene_data_display_config_next_odometer_mode(const UbloxOdometerMode value, void* context) {
+  furi_assert(context);
+
+  uint8_t index = 0;
+  for (int i = 0; i < ODOMETER_MODE_COUNT; i++) {
+    if (value == odometer_mode_values[i]) {
+      index = i;
+      break;
+    } else {
+      index = 0;
+    }
+  }
+  return index;
+}
+
+static void ublox_scene_data_display_config_set_odometer_mode(VariableItem* item) {
+  Ublox* ublox = variable_item_get_context(item);
+  uint8_t index = variable_item_get_current_value_index(item);
+
+  variable_item_set_current_value_text(item, odometer_mode_text[index]);
+  (ublox->device_state).odometer_mode = odometer_mode_values[index];
+  // device has to be re-configured at next read
+  ublox->gps_initted = false;
+}
+
+static uint8_t ublox_scene_data_display_config_next_platform_model(const UbloxPlatformModel value, void* context) {
+  furi_assert(context);
+
+  uint8_t index = 0;
+  for (int i = 0; i < PLATFORM_MODEL_COUNT; i++) {
+    if (value == platform_model_values[i]) {
+      index = i;
+      break;
+    } else {
+      index = 0;
+    }
+  }
+  return index;
+}
+
+static void ublox_scene_data_display_config_set_platform_model(VariableItem* item) {
+  Ublox* ublox = variable_item_get_context(item);
+  uint8_t index = variable_item_get_current_value_index(item);
+
+  variable_item_set_current_value_text(item, platform_model_text[index]);
+  (ublox->device_state).platform_model = platform_model_values[index];
+  // device has to be re-configured at next read
+  ublox->gps_initted = false;
+}
+
+void ublox_scene_data_display_config_on_enter(void* context) {
+  Ublox* ublox = context;
+  VariableItem* item;
+  uint8_t value_index;
+
+  item = variable_item_list_add(ublox->variable_item_list,
+				"Refresh Rate:",
+				REFRESH_RATE_COUNT,
+				ublox_scene_data_display_config_set_refresh_rate,
+				ublox);
+
+  value_index = ublox_scene_data_display_config_next_refresh_rate((ublox->data_display_state).refresh_rate,
+								  ublox);
+  variable_item_set_current_value_index(item, value_index);
+  variable_item_set_current_value_text(item, refresh_rate_text[value_index]);
+  
+  item = variable_item_list_add(ublox->variable_item_list,
+				"Backlight:",
+				BACKLIGHT_MODE_COUNT,
+				ublox_scene_data_display_config_set_backlight_mode,
+				ublox);
+  value_index = ublox_scene_data_display_config_next_backlight_mode((ublox->data_display_state).backlight_mode,
+								    ublox);
+  variable_item_set_current_value_index(item, value_index);
+  variable_item_set_current_value_text(item, backlight_mode_text[value_index]);
+  
+  
+  item = variable_item_list_add(ublox->variable_item_list,
+				"Display Mode:",
+				DISPLAY_VIEW_MODE_COUNT,
+				ublox_scene_data_display_config_set_display_view_mode,
+				ublox);
+  value_index = ublox_scene_data_display_config_next_display_view_mode((ublox->data_display_state).view_mode,
+								       ublox);
+  variable_item_set_current_value_index(item, value_index);
+  variable_item_set_current_value_text(item, display_view_mode_text[value_index]);
+
+  item = variable_item_list_add(ublox->variable_item_list,
+				"Notify:",
+				NOTIFY_MODE_COUNT,
+				ublox_scene_data_display_config_set_notify_mode,
+				ublox);
+  value_index = ublox_scene_data_display_config_next_notify_mode((ublox->data_display_state).notify_mode,
+								 ublox);
+  variable_item_set_current_value_index(item, value_index);
+  variable_item_set_current_value_text(item, notify_mode_text[value_index]);
+
+  item = variable_item_list_add(ublox->variable_item_list,
+				"Platform Model:",
+				PLATFORM_MODEL_COUNT,
+				ublox_scene_data_display_config_set_platform_model,
+				ublox);
+  value_index = ublox_scene_data_display_config_next_platform_model((ublox->device_state).platform_model,
+								   ublox);
+  variable_item_set_current_value_index(item, value_index);
+  variable_item_set_current_value_text(item, platform_model_text[value_index]);
+  
+  item = variable_item_list_add(ublox->variable_item_list,
+				"Odo Mode:",
+				ODOMETER_MODE_COUNT,
+				ublox_scene_data_display_config_set_odometer_mode,
+				ublox);
+  value_index = ublox_scene_data_display_config_next_odometer_mode((ublox->device_state).odometer_mode,
+								   ublox);
+  variable_item_set_current_value_index(item, value_index);
+  variable_item_set_current_value_text(item, odometer_mode_text[value_index]);
+
+  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewVariableItemList);
+}
+
+
+bool ublox_scene_data_display_config_on_event(void* context, SceneManagerEvent event) {
+  UNUSED(context);
+  UNUSED(event);
+  return false;
+}
+
+void ublox_scene_data_display_config_on_exit(void* context) {
+  Ublox* ublox = context;
+  variable_item_list_set_selected_item(ublox->variable_item_list, 0);
+  variable_item_list_reset(ublox->variable_item_list);
+}
+
+  

+ 57 - 0
non_catalog_apps/ublox/scenes/ublox_scene_start.c

@@ -0,0 +1,57 @@
+#include "../ublox_i.h"
+
+enum SubmenuIndex {
+  SubmenuIndexDataDisplay,
+  SubmenuIndexWiring,
+  SubmenuIndexAbout,
+};
+
+void ublox_scene_start_submenu_callback(void* context, uint32_t index) {
+  Ublox* ublox = context;
+
+  view_dispatcher_send_custom_event(ublox->view_dispatcher, index);
+}
+
+void ublox_scene_start_on_enter(void* context) {
+  Ublox* ublox = context;
+  Submenu* submenu = ublox->submenu;
+
+  submenu_add_item(submenu, "Data Display", SubmenuIndexDataDisplay, ublox_scene_start_submenu_callback, ublox);
+  submenu_add_item(submenu, "Wiring", SubmenuIndexWiring, ublox_scene_start_submenu_callback, ublox);
+  submenu_add_item(submenu, "About", SubmenuIndexAbout, ublox_scene_start_submenu_callback, ublox);
+  
+  submenu_set_selected_item(submenu, scene_manager_get_scene_state(ublox->scene_manager, UbloxSceneStart));
+
+  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewMenu);
+}
+
+bool ublox_scene_start_on_event(void* context, SceneManagerEvent event) {
+  Ublox* ublox = context;
+  UNUSED(ublox);
+  bool consumed = false;
+  
+  if (event.type == SceneManagerEventTypeCustom) {
+    if (event.event == SubmenuIndexDataDisplay) {
+      scene_manager_set_scene_state(ublox->scene_manager, UbloxSceneDataDisplay, SubmenuIndexDataDisplay);
+      scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplay);
+      consumed = true;
+    } else if (event.event == SubmenuIndexWiring) {
+      scene_manager_set_scene_state(ublox->scene_manager, UbloxSceneWiring, SubmenuIndexWiring);
+      scene_manager_next_scene(ublox->scene_manager, UbloxSceneWiring);
+      consumed = true;
+    } else if (event.event == SubmenuIndexAbout) {
+      scene_manager_set_scene_state(ublox->scene_manager, UbloxSceneAbout, SubmenuIndexAbout);
+      scene_manager_next_scene(ublox->scene_manager, UbloxSceneAbout);
+      consumed = true;
+    }
+  }
+
+  return consumed;
+}
+
+void ublox_scene_start_on_exit(void* context) {
+  Ublox* ublox = context;
+
+  submenu_reset(ublox->submenu);
+}
+    

+ 22 - 0
non_catalog_apps/ublox/scenes/ublox_scene_wiring.c

@@ -0,0 +1,22 @@
+#include "../ublox_i.h"
+
+void ublox_scene_wiring_on_enter(void* context) {
+  furi_assert(context);
+
+  Ublox* ublox = context;
+  widget_add_icon_element(ublox->widget, 0, 0, &I_ublox_wiring);
+  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewWidget);
+}
+
+bool ublox_scene_wiring_on_event(void* context, SceneManagerEvent event) {
+  UNUSED(context);
+  UNUSED(event);
+  return false;
+}
+
+void ublox_scene_wiring_on_exit(void* context) {
+  furi_assert(context);
+
+  Ublox* ublox = context;
+  widget_reset(ublox->widget);
+}

+ 102 - 0
non_catalog_apps/ublox/ublox.c

@@ -0,0 +1,102 @@
+#include "ublox_i.h"
+
+bool ublox_custom_event_callback(void* context, uint32_t event) {
+  furi_assert(context);
+  Ublox* ublox = context;
+  return scene_manager_handle_custom_event(ublox->scene_manager, event);
+}
+
+bool ublox_back_event_callback(void* context) {
+  furi_assert(context);
+  Ublox* ublox = context;
+  return scene_manager_handle_back_event(ublox->scene_manager);
+}
+
+Ublox* ublox_alloc() {
+  Ublox* ublox = malloc(sizeof(Ublox));
+
+  ublox->view_dispatcher = view_dispatcher_alloc();
+  ublox->scene_manager = scene_manager_alloc(&ublox_scene_handlers, ublox);
+  view_dispatcher_enable_queue(ublox->view_dispatcher);
+  view_dispatcher_set_event_callback_context(ublox->view_dispatcher, ublox);
+  view_dispatcher_set_custom_event_callback(ublox->view_dispatcher, ublox_custom_event_callback);
+  view_dispatcher_set_navigation_event_callback(ublox->view_dispatcher, ublox_back_event_callback);
+
+  ublox->worker = ublox_worker_alloc();
+  
+  ublox->gui = furi_record_open(RECORD_GUI);
+
+  ublox->submenu = submenu_alloc();
+  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewMenu, submenu_get_view(ublox->submenu));
+
+  ublox->widget = widget_alloc();
+  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewWidget, widget_get_view(ublox->widget));
+
+  ublox->data_display = data_display_alloc();
+  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewDataDisplay, data_display_get_view(ublox->data_display));
+
+  ublox->variable_item_list = variable_item_list_alloc();
+  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewVariableItemList, variable_item_list_get_view(ublox->variable_item_list));
+
+  ublox->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+  // Establish default data display state
+  (ublox->data_display_state).view_mode = UbloxDataDisplayViewModeHandheld;
+  (ublox->data_display_state).backlight_mode = UbloxDataDisplayBacklightDefault;
+  (ublox->data_display_state).refresh_rate = 2;
+  (ublox->data_display_state).notify_mode = UbloxDataDisplayNotifyOn;
+
+  (ublox->device_state).odometer_mode = UbloxOdometerModeRunning;
+  // "suitable for most applications" according to u-blox.
+  (ublox->device_state).platform_model = UbloxPlatformModelPortable;
+  ublox->gps_initted = false;
+  return ublox;
+}
+
+void ublox_free(Ublox* ublox) {
+  furi_assert(ublox);
+
+  ublox_worker_stop(ublox->worker);
+  ublox_worker_free(ublox->worker);
+    
+  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewMenu);
+  submenu_free(ublox->submenu);
+
+  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewWidget);
+  widget_free(ublox->widget);
+
+  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewDataDisplay);
+  data_display_free(ublox->data_display);
+
+  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewVariableItemList);
+  variable_item_list_free(ublox->variable_item_list);
+  
+  view_dispatcher_free(ublox->view_dispatcher);
+
+  scene_manager_free(ublox->scene_manager);
+
+  furi_record_close(RECORD_GUI);
+  furi_record_close(RECORD_NOTIFICATION);
+  ublox->gui = NULL;
+
+  free(ublox);
+}
+
+int32_t ublox_app(void* p) {
+  UNUSED(p);
+  
+  Ublox* ublox = ublox_alloc();
+
+  view_dispatcher_attach_to_gui(ublox->view_dispatcher, ublox->gui, ViewDispatcherTypeFullscreen);
+  
+  scene_manager_next_scene(ublox->scene_manager, UbloxSceneStart);
+
+  view_dispatcher_run(ublox->view_dispatcher);
+  
+  // force restore the default backlight
+  notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_auto);
+
+  ublox_free(ublox);
+
+  return 0;
+}

+ 3 - 0
non_catalog_apps/ublox/ublox.h

@@ -0,0 +1,3 @@
+#pragma once
+
+typedef struct Ublox Ublox;

BIN
non_catalog_apps/ublox/ublox_app_icon.png


+ 141 - 0
non_catalog_apps/ublox/ublox_device.c

@@ -0,0 +1,141 @@
+// This is a personal academic project. Dear PVS-Studio, please check it.
+
+// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
+#include "ublox_device.h"
+
+#define TAG "ublox_device"
+
+UbloxMessage* ublox_frame_to_bytes(UbloxFrame* frame) {
+  uint32_t message_size = 8 + frame->len;
+  // Found the issue! frame_bytes isn't being freed. This function
+  // should always have returned a pointer.
+  uint8_t* frame_bytes = malloc(message_size);
+  frame->sync1 = 0xb5;
+  frame->sync2 = 0x62;
+  frame_bytes[0] = frame->sync1;
+  frame_bytes[1] = frame->sync2;
+  frame_bytes[2] = frame->class;
+  frame_bytes[3] = frame->id;
+  frame_bytes[4] = frame->len & 0xff;
+  frame_bytes[5] = (frame->len & 0xff) >> 8;
+
+  
+  if (frame->len != 0) {
+    for (int i = 0; i < frame->len; i++) {
+      frame_bytes[6+i] = frame->payload[i];
+    }
+  }
+
+  frame->ck_a = 0;
+  frame->ck_b = 0;
+  // checksum is calculated over class, id, length, and payload
+  for (int i = 2; i < 2 + (frame->len+4); i++) {
+    frame->ck_a = frame->ck_a + frame_bytes[i];
+    frame->ck_b = frame->ck_b + frame->ck_a;
+  }
+
+  frame_bytes[message_size - 2] = frame->ck_a;
+  frame_bytes[message_size - 1] = frame->ck_b;
+
+  UbloxMessage* m = malloc(sizeof(UbloxMessage));
+  m->message = frame_bytes;
+  m->length = message_size;
+
+  return m;
+}
+
+void ublox_message_free(UbloxMessage* message) {
+  if (message != NULL) {
+    if (message->message != NULL) {
+      free(message->message);
+    } /*else {
+      FURI_LOG_I(TAG, "message free: message->message == NULL");
+      }*/
+    free(message);
+  } /*else {
+    FURI_LOG_I(TAG, "message free: message == NULL");
+    }*/
+}
+
+
+// Pointer, because we are assigning a pointer in the returned frame.
+UbloxFrame* ublox_bytes_to_frame(UbloxMessage* message) {
+  if (message->length < 8) {
+    FURI_LOG_I(TAG, "message length in bytes_to_frame < 8, = 0x%x", message->length);
+    // minimum 8 bytes in a message (message with no payload)
+    return NULL;
+  }
+  
+  UbloxFrame* frame = malloc(sizeof(UbloxFrame));
+
+  
+  if (message->message[0] != 0xb5) {
+    FURI_LOG_E(TAG, "message[0] != 0xb5, = 0x%x", message->message[0]);
+    free(frame);
+    return NULL;
+  }
+
+  frame->sync1 = message->message[0];
+
+  if (message->message[1] != 0x62) {
+    FURI_LOG_E(TAG, "Message[1] != 0x62, = 0x%x", message->message[1]);
+    free(frame);
+    return NULL;
+  }
+
+  frame->sync2 = message->message[1];
+
+  frame->class = message->message[2];
+  frame->id = message->message[3];
+
+  // little-endian
+  frame->len = (message->message[5] << 8) | (message->message[4]);
+
+  // frame->len must be initialized before malloc (duh, but I made that mistake...)
+  frame->payload = malloc(frame->len);
+  //FURI_LOG_I(TAG, "frame->len: %d", frame->len);
+  for (int i = 6; i < 6 + frame->len; i++) {
+    frame->payload[i - 6] = message->message[i];
+  }
+
+  frame->ck_a = message->message[6 + frame->len];
+  frame->ck_b = message->message[6 + frame->len+1];
+
+  // Test checksum
+  uint8_t ck_a = 0, ck_b = 0;
+  for (int i = 2; i < 2 + (frame->len+4); i++) {
+    ck_a = ck_a + message->message[i];
+    ck_b = ck_b + ck_a;
+  }
+
+  if (ck_a != frame->ck_a) {
+    FURI_LOG_E(TAG, "checksum A doesn't match! expected 0x%x, got 0x%x", ck_a, frame->ck_a);
+    free(frame);
+    free(frame->payload);
+    return NULL;
+  }
+
+  if (ck_b != frame->ck_b) {
+    FURI_LOG_E(TAG, "checksum B doesn't match! expected 0x%x, got 0x%x", ck_b, frame->ck_b);
+    free(frame);
+    free(frame->payload);
+    return NULL;
+  }
+
+  return frame;
+}
+
+  
+void ublox_frame_free(UbloxFrame* frame) {
+  if (frame != NULL) {
+    if (frame->payload != NULL) {
+      free(frame->payload);
+    }/* else {
+      FURI_LOG_I(TAG, "frame free: frame->payload == NULL");
+      }*/
+    free(frame);
+  }/* else {
+    FURI_LOG_I(TAG, "frame free: frame == NULL");
+    }*/
+}
+

+ 140 - 0
non_catalog_apps/ublox/ublox_device.h

@@ -0,0 +1,140 @@
+// This is a personal academic project. Dear PVS-Studio, please check it.
+
+// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
+#pragma once
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#include <furi.h>
+
+#define UBLOX_I2C_ADDRESS 0x42
+#define I2C_TIMEOUT_MS 20
+
+#define UBX_NAV_CLASS 0x01
+#define UBX_RXM_CLASS 0x02
+#define UBX_INF_CLASS 0x04
+#define UBX_ACK_CLASS 0x05
+#define UBX_CFG_CLASS 0x06
+#define UBX_UPD_CLASS 0x09
+#define UBX_MON_CLASS 0x0A
+#define UBX_AID_CLASS 0x0B
+#define UBX_TIM_CLASS 0x0D
+#define UBX_ESF_CLASS 0x10
+#define UBX_MGA_CLASS 0x13
+#define UBX_LOG_CLASS 0x21
+#define UBX_SEC_CLASS 0x27
+#define UBX_HNR_CLASS 0x28
+
+// The following are respective to a class.
+
+// ACK_CLASS
+#define UBX_ACK_ACK_MESSAGE 0x01
+// ACK and NAK have the same length
+#define UBX_ACK_ACK_MESSAGE_LENGTH (8+2)
+
+// NAV_CLASS
+#define UBX_NAV_PVT_MESSAGE 0x07
+#define UBX_NAV_PVT_MESSAGE_LENGTH (8+92)
+#define UBX_NAV_SAT_MESSAGE 0x35
+#define UBX_NAV_ODO_MESSAGE 0x09
+#define UBX_NAV_ODO_MESSAGE_LENGTH (8+20)
+#define UBX_NAV_RESETODO_MESSAGE 0x10
+
+// CFG_CLASS
+#define UBX_CFG_PMS_MESSAGE 0x86
+#define UBX_CFG_PMS_MESSAGE_LENGTH (8+8)
+#define UBX_CFG_ODO_MESSAGE 0x1e
+#define UBX_CFG_ODO_MESSAGE_LENGTH (8+20)
+#define UBX_CFG_NAV5_MESSAGE 0x24
+#define UBX_CFG_NAV5_MESSAGE_LENGTH (8+36)
+/** A frame is a message sent to the GPS. This app supports u-blox 8 devices. */
+  
+typedef struct UbloxFrame {
+  uint8_t sync1; // always 0xb5, or ISO-8859.1 for 'µ'
+  uint8_t sync2; // always 0x62, or ASCII for 'b'
+  // class and id together indicate what kind of message is being sent.
+  uint8_t class; // message class
+  uint8_t id; // message id
+
+  uint16_t len; // length of the payload only, 2 bytes, little-endian
+  uint8_t* payload; // any number of bytes
+
+  // 2 bytes of checksum
+  uint8_t ck_a;
+  uint8_t ck_b;
+
+  // metadata
+  bool valid;
+} UbloxFrame;
+
+typedef struct UbloxMessage {
+  uint8_t* message;
+  uint8_t length;
+} UbloxMessage;
+
+// Field names taken directly from u-blox protocol manual.
+typedef struct Ublox_NAV_PVT_Message {
+  uint32_t iTOW;
+  uint16_t year;
+  uint8_t month;
+  uint8_t day;
+  uint8_t hour;
+  uint8_t min;
+  uint8_t sec;
+  uint8_t valid;
+  uint32_t tAcc;
+  int32_t nano;
+  uint8_t fixType;
+  uint8_t flags;
+  uint8_t flags2;
+  uint8_t numSV;
+  int32_t lon;
+  int32_t lat;
+  int32_t height;
+  int32_t hMSL;
+  uint32_t hAcc;
+  uint32_t vAcc;
+  int32_t velN;
+  int32_t velE;
+  int32_t velD;
+  int32_t gSpeed;
+  int32_t headMot;
+  uint32_t sAcc;
+  uint32_t headAcc;
+  uint16_t pDOP;
+  uint16_t flags3;
+  uint8_t reserved1;
+  uint8_t reserved2;
+  uint8_t reserved3;
+  uint8_t reserved4;
+  int32_t headVeh;
+  int16_t magDec;
+  uint16_t magAcc;
+} Ublox_NAV_PVT_Message;
+
+typedef struct Ublox_NAV_ODO_Message {
+  uint8_t version;
+  uint8_t reserved1;
+  uint8_t reserved2;
+  uint8_t reserved3;
+  uint32_t iTOW;
+  uint32_t distance;
+  uint32_t totalDistance;
+  uint32_t distanceStd;
+} Ublox_NAV_ODO_Message;
+
+/** For a given UbloxFrame, populate the sync bytes, calculate the
+ * checksum bytes (storing them in `frame`), allocate a uint8_t array,
+ * and fill it with the contents of the frame in an order ready to
+ * send to the GPS. */
+UbloxMessage* ublox_frame_to_bytes(UbloxFrame* frame);
+
+/** For an array of uint8_ts, convert them to a frame. Returns NULL if
+    something goes wrong, either with invalid data or malloc errors.*/
+
+UbloxFrame* ublox_bytes_to_frame(UbloxMessage* message);
+
+void ublox_message_free(UbloxMessage* message);
+void ublox_frame_free(UbloxFrame* frame);

+ 57 - 0
non_catalog_apps/ublox/ublox_i.h

@@ -0,0 +1,57 @@
+#pragma once
+
+#include "ublox.h"
+#include "ublox_worker.h"
+#include "ublox_device.h"
+
+#include "helpers/ublox_types.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/variable_item_list.h>
+
+#include <notification/notification_messages.h>
+
+#include "scenes/ublox_scene.h"
+#include <ublox_icons.h>
+#include "views/data_display_view.h"
+#include "helpers/ublox_custom_event.h"
+
+struct Ublox {
+  ViewDispatcher* view_dispatcher;
+  Gui* gui;
+  SceneManager* scene_manager;
+
+  Submenu* submenu;
+  Widget* widget;
+  VariableItemList* variable_item_list;
+  DataDisplayView* data_display;
+  NotificationApp* notifications;
+  
+  UbloxWorker* worker;
+  FuriTimer* timer;
+  
+  Ublox_NAV_PVT_Message nav_pvt;
+  Ublox_NAV_ODO_Message nav_odo;
+  
+  UbloxDataDisplayState data_display_state;
+  UbloxDeviceState device_state;
+  bool gps_initted;
+};
+
+typedef enum {
+  UbloxViewMenu,
+  UbloxViewWidget,
+  UbloxViewDataDisplay,
+  UbloxViewVariableItemList,
+} UbloxView;
+
+Ublox* ublox_alloc();
+

+ 481 - 0
non_catalog_apps/ublox/ublox_worker.c

@@ -0,0 +1,481 @@
+#include "ublox_worker_i.h"
+
+#define TAG "UbloxWorker"
+
+UbloxWorker* ublox_worker_alloc() {
+  UbloxWorker* ublox_worker = malloc(sizeof(UbloxWorker));
+
+  ublox_worker->thread = furi_thread_alloc_ex("UbloxWorker", 2*1024, ublox_worker_task, ublox_worker);
+
+  ublox_worker->callback = NULL;
+  ublox_worker->context = NULL;
+
+  ublox_worker_change_state(ublox_worker, UbloxWorkerStateReady);
+
+  return ublox_worker;
+}
+
+void ublox_worker_free(UbloxWorker* ublox_worker) {
+  furi_assert(ublox_worker);
+
+  furi_thread_free(ublox_worker->thread);
+  
+  free(ublox_worker);
+}
+
+UbloxWorkerState ublox_worker_get_state(UbloxWorker* ublox_worker) {
+  return ublox_worker->state;
+}
+
+void ublox_worker_start(UbloxWorker* ublox_worker,
+			UbloxWorkerState state,
+			UbloxWorkerCallback callback,
+			void* context) {
+  furi_assert(ublox_worker);
+
+  ublox_worker->callback = callback;
+  ublox_worker->context = context;
+
+  ublox_worker_change_state(ublox_worker, state);
+  furi_thread_start(ublox_worker->thread);
+}
+
+void ublox_worker_stop(UbloxWorker* ublox_worker) {
+  furi_assert(ublox_worker);
+  furi_assert(ublox_worker->thread);
+  FURI_LOG_I(TAG, "worker_stop");
+  
+  if (furi_thread_get_state(ublox_worker->thread) != FuriThreadStateStopped) {
+    FURI_LOG_I(TAG, "set thread state to stopped");
+    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+    furi_thread_join(ublox_worker->thread);
+  }
+}
+
+void ublox_worker_change_state(UbloxWorker* ublox_worker, UbloxWorkerState state) {
+  ublox_worker->state = state;
+}
+
+void clear_ublox_data() {
+  uint8_t tx[] = {0xff};
+  uint8_t response = 0;
+  while (response != 0xff) {
+    if (!furi_hal_i2c_trx(&furi_hal_i2c_handle_external,
+			  UBLOX_I2C_ADDRESS << 1,
+			  tx, 1,
+			  &response, 1,
+			  furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+      FURI_LOG_E(TAG, "error reading first byte of response");
+    }
+  }
+}
+
+int32_t ublox_worker_task(void* context) {
+  UbloxWorker* ublox_worker = context;
+  Ublox* ublox = ublox_worker->context;
+
+
+  if (ublox_worker->state == UbloxWorkerStateRead) {
+    furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
+    if (!ublox->gps_initted) {
+      if (ublox_worker_init_gps(ublox_worker)) {
+	ublox->gps_initted = true;
+      } else {
+	ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+	FURI_LOG_E(TAG, "init GPS failed");
+	furi_hal_i2c_release(&furi_hal_i2c_handle_external);
+	return 1;
+      }
+      // have to do this...don't know why, though, because the data
+      // should already be cleared out (also why does this even work, it
+      // seems like it should be capturing the first byte of the next
+      // message)
+      clear_ublox_data();
+    }
+
+    ublox_worker_read_pvt(ublox_worker);
+    ublox_worker_read_odo(ublox_worker);
+    furi_hal_i2c_release(&furi_hal_i2c_handle_external);
+    ublox_worker->callback(UbloxWorkerEventDataReady, ublox_worker->context);
+    /*if (ublox_worker_read_odo(ublox_worker)) {
+      ublox_worker_read_pvt(ublox_worker);
+    } else {
+      next_state = UbloxWorkerStateStop;
+      ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+      }*/
+
+  } else if (ublox_worker->state == UbloxWorkerStateResetOdometer) {
+    ublox_worker_reset_odo(ublox_worker);
+  } else if (ublox_worker->state == UbloxWorkerStateStop) {
+    FURI_LOG_I(TAG, "state stop");
+  } else if (ublox_worker->state == UbloxWorkerStateReady) {
+    FURI_LOG_I(TAG, "state ready");
+  }
+
+  ublox_worker_change_state(ublox_worker, UbloxWorkerStateReady);
+  
+
+  //FURI_LOG_I(TAG, "mem free after: %u", memmgr_get_free_heap());
+  return 0;
+}
+
+
+FuriString* print_uint8_array(uint8_t* array, int length) {
+  FuriString* s = furi_string_alloc();
+  
+  for (int i = 0; i < length - 1; i++) {
+    furi_string_cat_printf(s, "%x, ", array[i]);
+  }
+  furi_string_cat_printf(s, "%x", array[length - 1]);
+
+  return s;
+}
+
+UbloxMessage* ublox_worker_i2c_transfer(UbloxMessage* message_tx, uint8_t read_length) {
+  //FURI_LOG_I(TAG, "ublox_worker_i2c_transfer");
+  if (!furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external,
+				    UBLOX_I2C_ADDRESS << 1,
+				    furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+    FURI_LOG_E(TAG, "GPS not found!");
+    return NULL;
+  }
+  
+  if (!furi_hal_i2c_tx(&furi_hal_i2c_handle_external,
+		       UBLOX_I2C_ADDRESS << 1,
+		       message_tx->message,
+		       message_tx->length,
+		       furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+    FURI_LOG_I(TAG, "error writing message from GPS");
+    return NULL;
+  }
+  uint8_t* response = malloc((size_t)read_length);
+  // The GPS sends 0xff until it has a complete message to respond
+  // with. We have to wait until it stops sending that. (Why this
+  // works is a little bit...uh, well, I don't know. Shouldn't reading
+  // more bytes make it so that the data is completely read out and no
+  // longer available?)
+
+  // Also, we know that this function is the traceable source of the
+  // memory leak whenever it's run a second time.
+
+  // ** The leak comes after this point.
+  uint8_t tx[] = {0xff};
+
+  while (true) {
+    //FURI_LOG_I(TAG, "mem free in loop: %u", memmgr_get_free_heap());
+    if (!furi_hal_i2c_trx(&furi_hal_i2c_handle_external,
+			  UBLOX_I2C_ADDRESS << 1,
+			  tx, 1,
+			  response, 1,
+			  furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+      FURI_LOG_E(TAG, "error reading first byte of response");
+      free(response);
+      return NULL;
+    }
+    //FURI_LOG_I(TAG, "read one byte");
+    // checking with 0xb5 prevents strange bursts of junk data from becoming an issue.
+    if (response[0] != 0xff && response[0] == 0xb5) {
+      //FURI_LOG_I(TAG, "mem free before final read: %u", memmgr_get_free_heap());
+      //FURI_LOG_I(TAG, "got data that isn't 0xff");
+      if (!furi_hal_i2c_trx(&furi_hal_i2c_handle_external,
+			    UBLOX_I2C_ADDRESS << 1,
+			    tx, 1,
+			    &(response[1]), read_length - 1, // first byte already read
+			    furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+	FURI_LOG_E(TAG, "error reading rest of response");
+	free(response);
+	return NULL;
+      }
+      //FURI_LOG_I(TAG, "mem free after final read: %u", memmgr_get_free_heap());
+      break;
+    }
+  }
+
+  //FURI_LOG_I(TAG, "i2c_transfer: byte 0 = %d", response[0]);
+  UbloxMessage* message_rx = malloc(sizeof(UbloxMessage));
+  message_rx->message = response;
+  message_rx->length = read_length;
+  return message_rx; // message_rx->message needs to be freed later
+}
+
+void ublox_worker_read_pvt(UbloxWorker* ublox_worker) {
+  //FURI_LOG_I(TAG, "mem free before PVT read: %u", memmgr_get_free_heap());
+  Ublox* ublox = ublox_worker->context;
+  
+  // Read NAV-PVT by sending NAV-PVT with no payload
+  UbloxFrame* frame_tx = malloc(sizeof(UbloxFrame));
+  frame_tx->class = UBX_NAV_CLASS;
+  frame_tx->id = UBX_NAV_PVT_MESSAGE;
+  frame_tx->len = 0;
+  frame_tx->payload = NULL;
+  UbloxMessage* message_tx = ublox_frame_to_bytes(frame_tx);
+  ublox_frame_free(frame_tx);
+  
+  UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_PVT_MESSAGE_LENGTH);
+  ublox_message_free(message_tx);
+  if (message_rx == NULL) {
+    FURI_LOG_E(TAG, "read_pvt transfer failed");
+    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+    return;
+  }
+
+  UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
+  ublox_message_free(message_rx);
+
+  if (frame_rx == NULL) {
+    FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-PVT message!");
+    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+  } else {
+    // build nav-pvt struct. yes this is very ugly.
+    Ublox_NAV_PVT_Message nav_pvt = {
+      .iTOW = (frame_rx->payload[0]) | (frame_rx->payload[1] << 8) | (frame_rx->payload[2] << 16) | (frame_rx->payload[3] << 24),
+      .year = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8),
+      .month = frame_rx->payload[6],
+      .day = frame_rx->payload[7],
+      .hour = frame_rx->payload[8],
+      .min = frame_rx->payload[9],
+      .sec = frame_rx->payload[10],
+      .valid = frame_rx->payload[11],
+      .tAcc = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8) | (frame_rx->payload[14] << 16) | (frame_rx->payload[15] << 24),
+      .nano = (frame_rx->payload[16]) | (frame_rx->payload[17] << 8) | (frame_rx->payload[18] << 16) | (frame_rx->payload[19] << 24),
+      .fixType = frame_rx->payload[20],
+      .flags = frame_rx->payload[21],
+      .flags2 = frame_rx->payload[22],
+      .numSV = frame_rx->payload[23],
+      .lon = (frame_rx->payload[24]) | (frame_rx->payload[25] << 8) | (frame_rx->payload[26] << 16) | (frame_rx->payload[27] << 24),
+      .lat = (frame_rx->payload[28]) | (frame_rx->payload[29] << 8) | (frame_rx->payload[30] << 16) | (frame_rx->payload[31] << 24),
+      .height = (frame_rx->payload[32]) | (frame_rx->payload[33] << 8) | (frame_rx->payload[34] << 16) | (frame_rx->payload[35] << 24),
+      .hMSL = (frame_rx->payload[36]) | (frame_rx->payload[37] << 8) | (frame_rx->payload[38] << 16) | (frame_rx->payload[39] << 24),
+      .hAcc = (frame_rx->payload[40]) | (frame_rx->payload[41] << 8) | (frame_rx->payload[42] << 16) | (frame_rx->payload[43] << 24),
+      .vAcc = (frame_rx->payload[44]) | (frame_rx->payload[45] << 8) | (frame_rx->payload[46] << 16) | (frame_rx->payload[47] << 24),
+      .velN = (frame_rx->payload[48]) | (frame_rx->payload[49] << 8) | (frame_rx->payload[50] << 16) | (frame_rx->payload[51] << 24),
+      .velE = (frame_rx->payload[52]) | (frame_rx->payload[53] << 8) | (frame_rx->payload[54] << 16) | (frame_rx->payload[55] << 24),
+      .velD = (frame_rx->payload[56]) | (frame_rx->payload[57] << 8) | (frame_rx->payload[58] << 16) | (frame_rx->payload[59] << 24),
+      .gSpeed = (frame_rx->payload[60]) | (frame_rx->payload[61] << 8) | (frame_rx->payload[62] << 16) | (frame_rx->payload[63] << 24),
+      .headMot = (frame_rx->payload[64]) | (frame_rx->payload[65] << 8) | (frame_rx->payload[66] << 16) | (frame_rx->payload[67] << 24),
+      .sAcc = (frame_rx->payload[68]) | (frame_rx->payload[69] << 8) | (frame_rx->payload[70] << 16) | (frame_rx->payload[71] << 24),
+      .headAcc = (frame_rx->payload[72]) | (frame_rx->payload[73] << 8) | (frame_rx->payload[74] << 16) | (frame_rx->payload[75] << 24),
+      .pDOP = (frame_rx->payload[76]) | (frame_rx->payload[77] << 8),
+      .flags3 = (frame_rx->payload[78]) | (frame_rx->payload[79] << 8),
+      .reserved1 = frame_rx->payload[80],
+      .reserved2 = frame_rx->payload[81],
+      .reserved3 = frame_rx->payload[82],
+      .reserved4 = frame_rx->payload[83],
+      .headVeh = (frame_rx->payload[84]) | (frame_rx->payload[85] << 8) | (frame_rx->payload[86] << 16) | (frame_rx->payload[87] << 24),
+      .magDec = (frame_rx->payload[88]) | (frame_rx->payload[89] << 8),
+      .magAcc = (frame_rx->payload[90]) | (frame_rx->payload[91] << 8),
+    };
+
+    // Using a local variable for nav_pvt is fine, because nav_pvt in
+    // the Ublox struct is also not a pointer, so this assignment
+    // effectively compiles to a memcpy.
+    ublox->nav_pvt = nav_pvt;
+    ublox_frame_free(frame_rx);
+    //ublox_worker->callback(UbloxWorkerEventDataReady, ublox_worker->context);
+  }
+  //FURI_LOG_I(TAG, "mem free after PVT read: %u", memmgr_get_free_heap());
+}
+
+bool ublox_worker_read_odo(UbloxWorker* ublox_worker) {
+  //FURI_LOG_I(TAG, "mem free before odo read: %u", memmgr_get_free_heap());
+  Ublox* ublox = ublox_worker->context;
+  UbloxFrame* frame_tx = malloc(sizeof(UbloxFrame));
+  frame_tx->class = UBX_NAV_CLASS;
+  frame_tx->id = UBX_NAV_ODO_MESSAGE;
+  frame_tx->len = 0;
+  frame_tx->payload = NULL;
+  UbloxMessage* message_tx = ublox_frame_to_bytes(frame_tx);
+  ublox_frame_free(frame_tx);
+
+  
+  UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_ODO_MESSAGE_LENGTH);
+  ublox_message_free(message_tx);
+  if (message_rx == NULL) {
+    FURI_LOG_E(TAG, "read_odo transfer failed");
+    return false;
+  }
+  UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
+  ublox_message_free(message_rx);
+  
+  if (frame_rx == NULL) {
+    FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-ODO message!");
+    return false;
+  } else {
+    Ublox_NAV_ODO_Message nav_odo = {
+      .version = frame_rx->payload[0],
+      .reserved1 = frame_rx->payload[1],
+      .reserved2 = frame_rx->payload[2],
+      .reserved3 = frame_rx->payload[3],
+      .iTOW = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8) | (frame_rx->payload[6] << 16) | (frame_rx->payload[7] << 24),
+      .distance = (frame_rx->payload[8]) | (frame_rx->payload[9] << 8) | (frame_rx->payload[10] << 16) | (frame_rx->payload[11] << 24),
+      .totalDistance = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8) | (frame_rx->payload[14] << 16) | (frame_rx->payload[15] << 24),
+      .distanceStd = (frame_rx->payload[16]) | (frame_rx->payload[17] << 8) | (frame_rx->payload[18] << 16) | (frame_rx->payload[19] << 24),
+    };
+    ublox->nav_odo = nav_odo;
+    ublox_frame_free(frame_rx);
+    //FURI_LOG_I(TAG, "mem free after odo read: %u", memmgr_get_free_heap());
+    return true;
+  }
+
+}
+
+/** Set the power mode to "Aggressive with 1Hz", enable the odometer,
+    and configure odometer and dynamic platform model. */
+bool ublox_worker_init_gps(UbloxWorker* ublox_worker) {
+  Ublox* ublox = ublox_worker->context;
+  // Set power mode
+  /*** read initial cfg-pms configuration first ***/
+  UbloxFrame* pms_frame_tx = malloc(sizeof(UbloxFrame));
+  pms_frame_tx->class = UBX_CFG_CLASS;
+  pms_frame_tx->id = UBX_CFG_PMS_MESSAGE;
+  pms_frame_tx->len = 0;
+  pms_frame_tx->payload = NULL;
+  UbloxMessage* pms_message_tx = ublox_frame_to_bytes(pms_frame_tx);
+  ublox_frame_free(pms_frame_tx);
+
+  UbloxMessage* pms_message_rx = ublox_worker_i2c_transfer(pms_message_tx, UBX_CFG_PMS_MESSAGE_LENGTH);
+  ublox_message_free(pms_message_tx);
+  if (pms_message_rx == NULL) {
+    FURI_LOG_E(TAG, "CFG-PMS read transfer failed");
+    return false;
+  }
+
+  // set power setup value to "aggressive with 1Hz"
+  pms_message_rx->message[6+1] = 0x03;
+
+  pms_frame_tx = malloc(sizeof(UbloxFrame));
+  pms_frame_tx->class = UBX_CFG_CLASS;
+  pms_frame_tx->id = UBX_CFG_PMS_MESSAGE;
+  pms_frame_tx->len = 8;
+  pms_frame_tx->payload = pms_message_rx->message;
+
+  pms_message_tx = ublox_frame_to_bytes(pms_frame_tx);
+  ublox_frame_free(pms_frame_tx);
+  
+  UbloxMessage* ack = ublox_worker_i2c_transfer(pms_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+  if (ack == NULL) {
+    FURI_LOG_E(TAG, "ACK after CFG-PMS set transfer failed");
+    return false;
+  }
+  FURI_LOG_I(TAG, "CFG-PMS ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+  ublox_message_free(pms_message_tx);
+  ublox_message_free(pms_message_rx);
+  ublox_message_free(ack);
+  
+  /***** Odometer *****/
+  // Enable odometer by changing CFG-ODO.
+  UbloxFrame* odo_frame_tx = malloc(sizeof(UbloxFrame));
+  odo_frame_tx->class = UBX_CFG_CLASS;
+  odo_frame_tx->id = UBX_CFG_ODO_MESSAGE;
+  odo_frame_tx->len = 0;
+  odo_frame_tx->payload = NULL;
+  UbloxMessage* odo_message_tx = ublox_frame_to_bytes(odo_frame_tx);
+  ublox_frame_free(odo_frame_tx);
+
+
+  UbloxMessage* odo_message_rx = ublox_worker_i2c_transfer(odo_message_tx, UBX_CFG_ODO_MESSAGE_LENGTH);
+  ublox_message_free(odo_message_tx);
+  if (odo_message_rx == NULL) {
+    FURI_LOG_E(TAG, "CFG-ODO transfer failed");
+    return false;
+  }
+
+  odo_frame_tx = malloc(sizeof(UbloxFrame));
+  odo_frame_tx->class = UBX_CFG_CLASS;
+  odo_frame_tx->id = UBX_CFG_ODO_MESSAGE;
+  odo_frame_tx->len = 20;
+  odo_frame_tx->payload = odo_message_rx->message;
+
+  // TODO: low-pass filters in settings?
+  // enable useODO bit in flags
+  odo_frame_tx->payload[4] |= 1;
+  odo_frame_tx->payload[5] = (ublox->device_state).odometer_mode;
+  
+  odo_message_tx = ublox_frame_to_bytes(odo_frame_tx);
+  ublox_frame_free(odo_frame_tx);
+
+  ack = ublox_worker_i2c_transfer(odo_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+  if (ack == NULL) {
+    FURI_LOG_E(TAG, "ACK after CFG-ODO set transfer failed");
+    return false;
+  }
+  FURI_LOG_I(TAG, "CFG-ODO ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+
+  ublox_message_free(odo_message_tx);
+  ublox_message_free(odo_message_rx);
+  ublox_message_free(ack);
+
+  // finally configure the navigation engine
+  UbloxFrame* nav5_frame_tx = malloc(sizeof(UbloxFrame));
+  nav5_frame_tx->class = UBX_CFG_CLASS;
+  nav5_frame_tx->id = UBX_CFG_NAV5_MESSAGE;
+  nav5_frame_tx->len = 0;
+  nav5_frame_tx->payload = NULL;
+  UbloxMessage* nav5_message_tx = ublox_frame_to_bytes(nav5_frame_tx);
+  ublox_frame_free(nav5_frame_tx);
+
+  UbloxMessage* nav5_message_rx = ublox_worker_i2c_transfer(nav5_message_tx, UBX_CFG_NAV5_MESSAGE_LENGTH);
+  if (nav5_message_rx == NULL) {
+    FURI_LOG_E(TAG, "CFG-NAV5 transfer failed");
+    return false;
+  }
+
+  // first two bytes tell the GPS what changes to apply, setting this
+  // bit tells it to apply the dynamic platfrom model settings.
+  nav5_frame_tx = malloc(sizeof(UbloxFrame));
+  nav5_frame_tx->class = UBX_CFG_CLASS;
+  nav5_frame_tx->id = UBX_CFG_NAV5_MESSAGE;
+  nav5_frame_tx->len = 36;
+  nav5_frame_tx->payload = nav5_message_rx->message;
+
+  nav5_frame_tx->payload[0] |= 1;
+  nav5_frame_tx->payload[2] = (ublox->device_state).platform_model;
+
+  nav5_message_tx = ublox_frame_to_bytes(nav5_frame_tx);
+  ublox_frame_free(nav5_frame_tx);
+  
+  ack = ublox_worker_i2c_transfer(nav5_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+  if (ack == NULL) {
+    FURI_LOG_E(TAG, "ACK after CFG-NAV5 set transfer failed");
+    return false;
+  }
+  FURI_LOG_I(TAG, "CFG-NAV5 ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+  
+  ublox_message_free(nav5_message_tx);
+  ublox_message_free(nav5_message_rx);
+  ublox_message_free(ack);
+  return true;
+
+}
+
+
+void ublox_worker_reset_odo(UbloxWorker* ublox_worker) {
+  FURI_LOG_I(TAG, "ublox_worker_reset_odo");
+  UbloxFrame* odo_frame_tx = malloc(sizeof(UbloxFrame));
+  odo_frame_tx->class = UBX_NAV_CLASS;
+  odo_frame_tx->id = UBX_NAV_RESETODO_MESSAGE;
+  odo_frame_tx->len = 0;
+  odo_frame_tx->payload = NULL;
+  UbloxMessage* odo_message_tx = ublox_frame_to_bytes(odo_frame_tx);
+  ublox_frame_free(odo_frame_tx);
+  
+  UbloxMessage* ack = ublox_worker_i2c_transfer(odo_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+  if (ack == NULL) {
+    FURI_LOG_E(TAG, "ACK after NAV-RESETODO set transfer failed");
+    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+    return;
+  }
+  FURI_LOG_I(TAG, "NAV-RESETODO ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+  ublox_message_free(odo_message_tx);
+  ublox_message_free(ack);
+  // no reason to trigger an event on success, the user will see that
+  // the odometer has been reset on the next update.
+}
+  /*FuriString* s = furi_string_alloc();
+  for (int i = 0; i < 92+8; i++) {
+    furi_string_cat_printf(s, "0x%x, ", message_rx->message[i]);
+  }
+  FURI_LOG_I(TAG, "array: %s", furi_string_get_cstr(s));
+  furi_string_free(s);*/

+ 41 - 0
non_catalog_apps/ublox/ublox_worker.h

@@ -0,0 +1,41 @@
+#pragma once
+
+#include "ublox_device.h"
+
+typedef struct UbloxWorker UbloxWorker;
+
+typedef enum {
+  UbloxWorkerStateNone,
+  UbloxWorkerStateReady,
+  UbloxWorkerStateRead,
+  UbloxWorkerStateResetOdometer,
+  UbloxWorkerStateStop,
+} UbloxWorkerState;
+
+typedef enum {
+  // reserve space for application events
+  UbloxWorkerEventReserved = 50,
+  
+  UbloxWorkerEventSuccess,
+  UbloxWorkerEventFailed,
+  UbloxWorkerEventDataReady,
+} UbloxWorkerEvent;
+
+typedef void (*UbloxWorkerCallback)(UbloxWorkerEvent event, void* context);
+
+UbloxWorker* ublox_worker_alloc();
+
+UbloxWorkerState ublox_worker_get_state(UbloxWorker* ublox_worker);
+
+void ublox_worker_free(UbloxWorker* ublox_worker);
+
+void ublox_worker_start(UbloxWorker* ublox_worker,
+			UbloxWorkerState state,
+			UbloxWorkerCallback callback,
+			void* context);
+
+void ublox_worker_stop(UbloxWorker* ublox_worker);
+		       
+bool ublox_worker_init_gps();//UbloxWorker* ublox_worker);
+
+

+ 27 - 0
non_catalog_apps/ublox/ublox_worker_i.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include "ublox_worker.h"
+#include "ublox_i.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+
+struct UbloxWorker {
+  FuriThread* thread;
+  FuriTimer* timer;
+  
+  UbloxWorkerCallback callback;
+  void* context;
+
+  UbloxWorkerState state;
+};
+
+void ublox_worker_change_state(UbloxWorker* ublox_worker, UbloxWorkerState state);
+
+int32_t ublox_worker_task(void* context);
+
+void ublox_worker_read_pvt(UbloxWorker* ublox_worker);
+
+bool ublox_worker_read_odo(UbloxWorker* ublox_worker);
+
+void ublox_worker_reset_odo(UbloxWorker* ublox_worker);

+ 271 - 0
non_catalog_apps/ublox/views/data_display_view.c

@@ -0,0 +1,271 @@
+#include "data_display_view.h"
+
+#include <gui/elements.h>
+
+#define TAG "data_display_view"
+
+struct DataDisplayView {
+  View* view;
+  DataDisplayViewCallback callback;
+  void* context;
+};
+
+typedef struct {
+  DataDisplayState state;
+  Ublox_NAV_PVT_Message nav_pvt;
+  Ublox_NAV_ODO_Message nav_odo;
+} DataDisplayViewModel;
+
+static void data_display_draw_callback(Canvas* canvas, void* model) {
+  DataDisplayViewModel* m = model;
+  if (m->state == DataDisplayGPSNotFound) {
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 27, 20, "GPS not found!");
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 5, 34, "Connect u-blox 8 series GPS.");
+    
+  } else if (m->state == DataDisplayHandheldMode) {
+    // TODO: check invalidLlh flag in flags3
+    Ublox_NAV_PVT_Message message = m->nav_pvt;
+    Ublox_NAV_ODO_Message nav_odo = m->nav_odo;
+    FuriString* s = furi_string_alloc();
+    elements_button_left(canvas, "Config");
+    elements_button_center(canvas, "Reset");
+    
+    /*** Draw fix ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 9, "Fix:");
+
+    canvas_set_font(canvas, FontSecondary);
+    
+    if (message.fixType == 0) {
+      canvas_draw_str(canvas, 21, 9, "N");
+    } else if (message.fixType == 1) {
+      canvas_draw_str(canvas, 21, 9, "R");
+    } else if (message.fixType == 2) {
+      canvas_draw_str(canvas, 21, 9, "2D");
+    } else if (message.fixType == 3) {
+      canvas_draw_str(canvas, 21, 9, "3D");
+    } else if (message.fixType == 4) {
+      canvas_draw_str(canvas, 21, 9, "G+D");
+    } else if (message.fixType == 5) {
+      canvas_draw_str(canvas, 21, 9, "TO");
+    }
+
+    /*** Draw number of satellites ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 37, 9, "Sat:");
+
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(s, "%u", message.numSV);
+    canvas_draw_str(canvas, 60, 9, furi_string_get_cstr(s));
+
+    /*** Draw odometer ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 74, 9, "Odo:");
+
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(s, "%.1f", (double)(nav_odo.distance / 1e3));
+    canvas_draw_str(canvas, 100, 9, furi_string_get_cstr(s));
+    
+    /*** Draw latitude ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 22, "Lat:");
+
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(s, "%.4f", (double)(message.lat / 1e7));
+    canvas_draw_str(canvas, 22, 22, furi_string_get_cstr(s));
+    
+    /*** Draw longitude ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 35, "Lon:");
+
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(s, "%.4f", (double)(message.lon / 1e7));
+    canvas_draw_str(canvas, 23, 35, furi_string_get_cstr(s));
+
+    /*** Draw altitude ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 70, 22, "Alt:");
+
+    canvas_set_font(canvas, FontSecondary);
+    // hMSL is height above mean sea level in mm
+    if (locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+      furi_string_printf(s, "%.0fm", (double)(message.hMSL / 1e3));
+    } else {
+      furi_string_printf(s, "%.0fft", (double)(message.hMSL / 1e3 * 3.281));
+    }
+    canvas_draw_str(canvas, 91, 22, furi_string_get_cstr(s));
+    
+    /*** Draw heading ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 75, 35, "Head:");
+
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(s, "%.0f", (double)(message.headMot / 1e5));
+    canvas_draw_str(canvas, 105, 35, furi_string_get_cstr(s));
+
+    /*** Draw time ***/
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 48, "UTC:");
+
+    canvas_set_font(canvas, FontSecondary);
+    FuriHalRtcDateTime datetime = {
+      .hour = message.hour,
+      .minute = message.min,
+      .second = message.sec,
+
+    };
+
+    FuriString* s2 = furi_string_alloc();
+    // built-in date functions make strings that are too long
+    if (locale_get_date_format() == LocaleDateFormatDMY) {
+      furi_string_printf(s, "%u/%u/'%u ",
+			 message.day, message.month, (message.year % 100));
+    } else if (locale_get_date_format() == LocaleDateFormatMDY) {
+      furi_string_printf(s, "%u/%u/'%u ",
+			 message.month, message.day, (message.year % 100));
+    } else if (locale_get_date_format() == LocaleDateFormatYMD) {
+      furi_string_printf(s, "'%u/%u/%u ",
+			 (message.year % 100), message.month, message.day);
+    }
+    locale_format_time(s2, &datetime, locale_get_time_format(), false);
+    furi_string_cat(s, s2);
+    furi_string_free(s2);
+    
+
+    canvas_draw_str(canvas, 27, 48, furi_string_get_cstr(s));
+    furi_string_free(s);
+    
+  } else if (m->state == DataDisplayCarMode) {
+    Ublox_NAV_PVT_Message message = m->nav_pvt;
+    Ublox_NAV_ODO_Message nav_odo = m->nav_odo;
+    FuriString* s = furi_string_alloc();
+    elements_button_left(canvas, "Config");
+    elements_button_center(canvas, "Reset");
+
+    // TODO: imperial/metric
+    canvas_set_font(canvas, FontPrimary);
+    // gSpeed is in mm/s
+    if (locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+      canvas_draw_str(canvas, 0, 12, "Spd (km/s):");
+      furi_string_printf(s, "%.1f", (double)(message.gSpeed / 1e3));
+    } else {
+      canvas_draw_str(canvas, 0, 12, "Spd (mph):");
+      furi_string_printf(s, "%.1f", (double)(message.gSpeed / 1e3 * 2.23694));
+    }
+
+    canvas_set_font(canvas, FontBigNumbers);
+    canvas_draw_str(canvas, 60, 15, furi_string_get_cstr(s));
+
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 29, "Heading:");
+
+    canvas_set_font(canvas, FontBigNumbers);
+    furi_string_printf(s, "%.0f", (double)(message.headMot / 1e5));
+    canvas_draw_str(canvas, 60, 32, furi_string_get_cstr(s));
+
+    canvas_set_font(canvas, FontPrimary);
+    // distance is in meters
+    if (locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+      canvas_draw_str(canvas, 0, 45, "Odo (km):");
+      furi_string_printf(s, "%.2f", (double)(nav_odo.distance / 1e3));
+    } else {
+      canvas_draw_str(canvas, 0, 45, "Odo (mi):");
+      furi_string_printf(s, "%.2f", (double)(nav_odo.distance / 1e3 * 0.6214));
+    }
+
+    canvas_set_font(canvas, FontBigNumbers);
+    canvas_draw_str(canvas, 60, 49, furi_string_get_cstr(s));
+    
+    furi_string_free(s);
+
+    // TODO: compass direction next to heading?
+    // TODO: better localization
+  }
+}
+
+static bool data_display_input_callback(InputEvent* event, void* context) {
+  DataDisplayView* data_display = context;
+  bool consumed = false;
+
+  if (event->type == InputTypeShort) {
+    if (event->key == InputKeyLeft) {
+      if (data_display->callback) {      
+	data_display->callback(data_display->context, event->key);
+      }
+      consumed = true;
+    } else if (event->key == InputKeyOk) {
+      if (data_display->callback) {      
+	data_display->callback(data_display->context, event->key);
+      }
+    }
+  }
+  return consumed;
+}
+
+DataDisplayView* data_display_alloc() {
+  DataDisplayView* data_display = malloc(sizeof(DataDisplayView));
+  data_display->view = view_alloc();
+
+  view_allocate_model(data_display->view, ViewModelTypeLocking, sizeof(DataDisplayViewModel));
+  view_set_context(data_display->view, data_display);
+  
+  view_set_draw_callback(data_display->view, data_display_draw_callback);
+  view_set_input_callback(data_display->view, data_display_input_callback);
+
+
+  return data_display;
+}
+
+void data_display_free(DataDisplayView* data_display) {
+  furi_assert(data_display);
+  view_free(data_display->view);
+  free(data_display);
+}
+
+void data_display_reset(DataDisplayView* data_display) {
+  furi_assert(data_display);
+  Ublox_NAV_PVT_Message p = {0};
+  Ublox_NAV_ODO_Message o = {0};
+  with_view_model(data_display->view,
+		  DataDisplayViewModel * model,
+		  { model->state = DataDisplayHandheldMode;
+		    model->nav_pvt = p;
+		    model->nav_odo = o;
+		  },
+		  false);
+}
+
+View* data_display_get_view(DataDisplayView* data_display) {
+  furi_assert(data_display);
+  return data_display->view;
+}
+
+void data_display_set_callback(DataDisplayView* data_display, DataDisplayViewCallback callback, void* context) {
+  furi_assert(data_display);
+  furi_assert(callback);
+  data_display->callback = callback;
+  data_display->context = context;
+}
+
+void data_display_set_nav_messages(DataDisplayView* data_display, Ublox_NAV_PVT_Message pvt_message, Ublox_NAV_ODO_Message odo_message) {
+  furi_assert(data_display);
+  with_view_model(data_display->view,
+		  DataDisplayViewModel * model,
+		  {
+		      model->nav_pvt = pvt_message;
+		      model->nav_odo = odo_message;
+		  },
+		  true);
+}
+
+void data_display_set_state(DataDisplayView* data_display, DataDisplayState state) {
+  furi_assert(data_display);
+  with_view_model(data_display->view,
+		  DataDisplayViewModel * model,
+		  { model->state = state; },
+		  true);
+}

+ 33 - 0
non_catalog_apps/ublox/views/data_display_view.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include <stdint.h>
+#include <gui/view.h>
+#include <gui/modules/widget.h>
+#include <furi.h>
+#include <locale/locale.h>
+#include <furi_hal.h>
+#include "../ublox_device.h"
+
+typedef enum {
+  DataDisplayHandheldMode,
+  DataDisplayCarMode,
+  DataDisplayGPSNotFound,
+} DataDisplayState;
+
+typedef struct DataDisplayView DataDisplayView;
+
+typedef void (*DataDisplayViewCallback)(void* context, InputKey key);
+
+DataDisplayView* data_display_alloc();
+
+void data_display_free(DataDisplayView* data_display);
+
+void data_display_reset(DataDisplayView* data_display);
+
+View* data_display_get_view(DataDisplayView* data_display);
+
+void data_display_set_callback(DataDisplayView* data_display, DataDisplayViewCallback callback, void* context);
+
+void data_display_set_nav_messages(DataDisplayView* data_display, Ublox_NAV_PVT_Message pvt_message, Ublox_NAV_ODO_Message odo_message);
+
+void data_display_set_state(DataDisplayView* data_display, DataDisplayState state);