MX 2 лет назад
Родитель
Сommit
75de93921a
42 измененных файлов с 2899 добавлено и 715 удалено
  1. 3 1
      ReadMe.md
  2. BIN
      apps/GPIO/esp32_gravity.fap
  3. BIN
      apps/Games/chess_clock.fap
  4. BIN
      apps/Games/flipchess.fap
  5. 0 4
      non_catalog_apps/chess/README.md
  6. 1 1
      non_catalog_apps/chess/application.fam
  7. 607 636
      non_catalog_apps/chess/chess/smallchesslib.h
  8. 9 5
      non_catalog_apps/chess/flipchess.c
  9. 3 5
      non_catalog_apps/chess/flipchess.h
  10. 1 5
      non_catalog_apps/chess/scenes/flipchess_scene_menu.c
  11. 109 58
      non_catalog_apps/chess/views/flipchess_scene_1.c
  12. 15 0
      non_catalog_apps/chess_clock/application.fam
  13. 182 0
      non_catalog_apps/chess_clock/chess_clock.c
  14. BIN
      non_catalog_apps/chess_clock/chess_clock.png
  15. 1 0
      non_catalog_apps/esp32_gravity/.vscode/settings.json
  16. 22 0
      non_catalog_apps/esp32_gravity/LICENSE
  17. 88 0
      non_catalog_apps/esp32_gravity/README.md
  18. 16 0
      non_catalog_apps/esp32_gravity/application.fam
  19. BIN
      non_catalog_apps/esp32_gravity/assets/KeyBackspaceSelected_16x9.png
  20. BIN
      non_catalog_apps/esp32_gravity/assets/KeyBackspace_16x9.png
  21. BIN
      non_catalog_apps/esp32_gravity/assets/KeySaveSelected_24x11.png
  22. BIN
      non_catalog_apps/esp32_gravity/assets/KeySave_24x11.png
  23. BIN
      non_catalog_apps/esp32_gravity/assets/WarningDolphin_45x42.png
  24. 45 0
      non_catalog_apps/esp32_gravity/esp_flip_const.h
  25. 40 0
      non_catalog_apps/esp32_gravity/esp_flip_struct.h
  26. 30 0
      non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene.c
  27. 29 0
      non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene.h
  28. 3 0
      non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_config.h
  29. 98 0
      non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_console_output.c
  30. 342 0
      non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_start.c
  31. 124 0
      non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_text_input.c
  32. BIN
      non_catalog_apps/esp32_gravity/uart_terminal.png
  33. 104 0
      non_catalog_apps/esp32_gravity/uart_terminal_app.c
  34. 11 0
      non_catalog_apps/esp32_gravity/uart_terminal_app.h
  35. 55 0
      non_catalog_apps/esp32_gravity/uart_terminal_app_i.h
  36. 7 0
      non_catalog_apps/esp32_gravity/uart_terminal_custom_event.h
  37. 97 0
      non_catalog_apps/esp32_gravity/uart_terminal_uart.c
  38. 14 0
      non_catalog_apps/esp32_gravity/uart_terminal_uart.h
  39. 683 0
      non_catalog_apps/esp32_gravity/uart_text_input.c
  40. 82 0
      non_catalog_apps/esp32_gravity/uart_text_input.h
  41. 57 0
      non_catalog_apps/esp32_gravity/uart_validators.c
  42. 21 0
      non_catalog_apps/esp32_gravity/uart_validators.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 `13 Jul 23:51 GMT +3`
+### Apps checked & updated at `14 Jul 17:04 GMT +3`
 
 ## Games
 - [Pong (By nmrr)](https://github.com/nmrr/flipperzero-pong) - Modified by [SimplyMinimal](https://github.com/SimplyMinimal/FlipperZero-Pong)
@@ -51,6 +51,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 - [Etch-A-Sketch (By SimplyMinimal)](https://github.com/SimplyMinimal/FlipperZero-Etch-A-Sketch)
 - [Paint (By n-o-T-I-n-s-a-n-e)](https://github.com/n-o-T-I-n-s-a-n-e)
 - [Chess (By xtruan)](https://github.com/xtruan/flipper-chess)
+- [Chess Clock (By ihatecsv)](https://github.com/ihatecsv/flipper_chess_clock)
 
 ## Media
 - [Tuning Fork (By besya)](https://github.com/besya/flipperzero-tuning-fork) - Fixes [(by Willy-JL)](https://github.com/ClaraCrazy/Flipper-Xtreme/commit/44023851f7349b6ae9ca9f9bd9228d795a7e04c0)
@@ -97,6 +98,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 - [Plantower PMSx003 sensor reader (By 3cky)](https://github.com/3cky/flipperzero-airmon)
 - [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)
 
 ## 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/esp32_gravity.fap


BIN
apps/Games/chess_clock.fap


BIN
apps/Games/flipchess.fap


+ 0 - 4
non_catalog_apps/chess/README.md

@@ -4,10 +4,6 @@
 - Built against `0.86.1` Flipper Zero firmware release
 - Uses [smallchesslib](https://codeberg.org/drummyfish/smallchesslib)
 
-### DONATE IF YOU LIKE THE GAME
-  - ETH (or ERC-20): `xtruan.eth` or `0xa9Ad79502cdaf4F6881f3C2ef260713e5B771CE2`
-  - BTC: `16RP5Ui5QrWrVh2rR7NKAPwE5A4uFjCfbs`
-
 ### Installation
 
 - Download [last release fap file](https://github.com/xtruan/flipper-chess/releases/latest)

+ 1 - 1
non_catalog_apps/chess/application.fam

@@ -6,7 +6,7 @@ App(
         requires=[
         "gui",
     ],
-    stack_size=2 * 1024,
+    stack_size=4 * 1024,
     order=40,
     fap_icon="flipchess_10px.png",
     fap_icon_assets="icons",

Разница между файлами не показана из-за своего большого размера
+ 607 - 636
non_catalog_apps/chess/chess/smallchesslib.h


+ 9 - 5
non_catalog_apps/chess/flipchess.c

@@ -32,7 +32,7 @@ static void text_input_callback(void* context) {
                 strcpy(app->import_game_text, app->input_text);
 
                 int status = FlipChessStatusSuccess;
-                
+
                 if(status == FlipChessStatusSuccess) {
                     //notification_message(app->notification, &sequence_blink_cyan_100);
                     flipchess_play_happy_bump(app);
@@ -73,7 +73,8 @@ FlipChess* flipchess_app_alloc() {
         app->view_dispatcher, flipchess_navigation_event_callback);
     view_dispatcher_set_tick_event_callback(
         app->view_dispatcher, flipchess_tick_event_callback, 100);
-    view_dispatcher_set_custom_event_callback(app->view_dispatcher, flipchess_custom_event_callback);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, flipchess_custom_event_callback);
     app->submenu = submenu_alloc();
 
     // Settings
@@ -96,7 +97,9 @@ FlipChess* flipchess_app_alloc() {
         flipchess_startscreen_get_view(app->flipchess_startscreen));
     app->flipchess_scene_1 = flipchess_scene_1_alloc();
     view_dispatcher_add_view(
-        app->view_dispatcher, FlipChessViewIdScene1, flipchess_scene_1_get_view(app->flipchess_scene_1));
+        app->view_dispatcher,
+        FlipChessViewIdScene1,
+        flipchess_scene_1_get_view(app->flipchess_scene_1));
     app->variable_item_list = variable_item_list_alloc();
     view_dispatcher_add_view(
         app->view_dispatcher,
@@ -163,11 +166,12 @@ int32_t flipchess_app(void* p) {
         app->scene_manager, FlipChessSceneStartscreen); //Start with start screen
     //scene_manager_next_scene(app->scene_manager, FlipChessSceneMenu); //if you want to directly start with Menu
 
-    furi_hal_power_suppress_charge_enter();
+    furi_hal_random_init();
+    // furi_hal_power_suppress_charge_enter();
 
     view_dispatcher_run(app->view_dispatcher);
 
-    furi_hal_power_suppress_charge_exit();
+    // furi_hal_power_suppress_charge_exit();
     flipchess_app_free(app);
 
     return 0;

+ 3 - 5
non_catalog_apps/chess/flipchess.h

@@ -2,6 +2,7 @@
 
 #include <furi.h>
 #include <furi_hal.h>
+#include <furi_hal_random.h>
 #include <gui/gui.h>
 #include <input/input.h>
 #include <stdlib.h>
@@ -15,7 +16,7 @@
 #include "views/flipchess_startscreen.h"
 #include "views/flipchess_scene_1.h"
 
-#define FLIPCHESS_VERSION "v0.1.0"
+#define FLIPCHESS_VERSION "v0.1.2"
 
 #define TEXT_BUFFER_SIZE 256
 
@@ -61,10 +62,7 @@ typedef enum {
     FlipChessPlayerAI3 = 3,
 } FlipChessPlayerMode;
 
-typedef enum {
-    FlipChessTextInputDefault,
-    FlipChessTextInputGame
-} FlipChessTextInputState;
+typedef enum { FlipChessTextInputDefault, FlipChessTextInputGame } FlipChessTextInputState;
 
 typedef enum {
     FlipChessStatusSuccess = 0,

+ 1 - 5
non_catalog_apps/chess/scenes/flipchess_scene_menu.c

@@ -29,11 +29,7 @@ void flipchess_scene_menu_on_enter(void* context) {
     //     app);
 
     submenu_add_item(
-        app->submenu, 
-        "Settings", 
-        SubmenuIndexSettings, 
-        flipchess_scene_menu_submenu_callback, 
-        app);
+        app->submenu, "Settings", SubmenuIndexSettings, flipchess_scene_menu_submenu_callback, app);
 
     submenu_set_selected_item(
         app->submenu, scene_manager_get_scene_state(app->scene_manager, FlipChessSceneMenu));

+ 109 - 58
non_catalog_apps/chess/views/flipchess_scene_1.c

@@ -1,7 +1,7 @@
 #include "../flipchess.h"
 #include <furi.h>
-#include <furi_hal.h>
-#include <furi_hal_random.h>
+// #include <furi_hal.h>
+// #include <furi_hal_random.h>
 #include <input/input.h>
 #include <gui/elements.h>
 //#include <dolphin/dolphin.h>
@@ -16,8 +16,10 @@
 
 #include "../chess/smallchesslib.h"
 
-#define MAX_TEXT_LEN 30 // 30 = max length of text
+#define ENABLE_960 0 // setting to 1 enables 960 chess
+#define MAX_TEXT_LEN 15 // 15 = max length of text
 #define MAX_TEXT_BUF (MAX_TEXT_LEN + 1) // max length of text + null terminator
+#define THREAD_WAIT_TIME 20 // time to wait for draw thread to finish
 
 struct FlipChessScene1 {
     View* view;
@@ -28,25 +30,22 @@ typedef struct {
     uint8_t paramPlayerW;
     uint8_t paramPlayerB;
 
-    // uint8_t paramBoard = 1;
     uint8_t paramAnalyze; // depth of analysis
     uint8_t paramMoves;
-    //uint8_t paramXboard = 0;
     uint8_t paramInfo;
-    //uint8_t paramDraw = 1;
     uint8_t paramFlipBoard;
-    //uint8_t paramHelp = 0;
     uint8_t paramExit;
     uint16_t paramStep;
     char* paramFEN;
     char* paramPGN;
-    //uint16_t paramRandom = 0;
-    //uint8_t paramBlind = 0;
 
     int clockSeconds;
     SCL_Game game;
     SCL_Board startState;
+
+#if ENABLE_960
     int16_t random960PosNumber;
+#endif
 
     //uint8_t picture[SCL_BOARD_PICTURE_WIDTH * SCL_BOARD_PICTURE_WIDTH];
     uint8_t squareSelected;
@@ -55,9 +54,10 @@ typedef struct {
     char* msg;
     char* msg2;
     char* msg3;
-    char moveString[16];
-    char moveString2[16];
-    char moveString3[16];
+    char moveString[MAX_TEXT_BUF];
+    char moveString2[MAX_TEXT_BUF];
+    char moveString3[MAX_TEXT_BUF];
+    uint8_t thinking;
 
     SCL_SquareSet moveHighlight;
     uint8_t squareFrom;
@@ -66,7 +66,7 @@ typedef struct {
 
 } FlipChessScene1Model;
 
-uint8_t picture[SCL_BOARD_PICTURE_WIDTH * SCL_BOARD_PICTURE_WIDTH];
+static uint8_t picture[SCL_BOARD_PICTURE_WIDTH * SCL_BOARD_PICTURE_WIDTH];
 
 void flipchess_putImagePixel(uint8_t pixel, uint16_t index) {
     picture[index] = pixel;
@@ -138,15 +138,15 @@ int16_t flipchess_makeAIMove(
 
 bool flipchess_isPlayerTurn(FlipChessScene1Model* model) {
     return (SCL_boardWhitesTurn(model->game.board) && model->paramPlayerW == 0) ||
-       (!SCL_boardWhitesTurn(model->game.board) && model->paramPlayerB == 0);
+           (!SCL_boardWhitesTurn(model->game.board) && model->paramPlayerB == 0);
 }
 
 void flipchess_shiftMessages(FlipChessScene1Model* model) {
     // shift messages
     model->msg3 = model->msg2;
     model->msg2 = model->msg;
-    strncpy(model->moveString3, model->moveString2, 15);
-    strncpy(model->moveString2, model->moveString, 15);
+    strncpy(model->moveString3, model->moveString2, MAX_TEXT_LEN);
+    strncpy(model->moveString2, model->moveString, MAX_TEXT_LEN);
 }
 
 void flipchess_drawBoard(FlipChessScene1Model* model) {
@@ -164,7 +164,7 @@ uint8_t flipchess_turn(FlipChessScene1Model* model) {
     uint8_t moveType = 0;
 
     // if(model->paramInfo) {
-        
+
     //     if(model->random960PosNumber >= 0)
     //         printf("960 random position number: %d\n", model->random960PosNumber);
 
@@ -206,22 +206,23 @@ uint8_t flipchess_turn(FlipChessScene1Model* model) {
     //     }
     // }
 
-    if(model->game.state != SCL_GAME_STATE_PLAYING || model->paramExit)
-        return FlipChessStatusReturn;
+    if(model->game.state != SCL_GAME_STATE_PLAYING) {
+        model->paramExit = FlipChessStatusReturn;
+        return model->paramExit;
+    }
 
     char movePromote = 'q';
 
     if(flipchess_isPlayerTurn(model)) {
-        
         // if(stringsEqual(string, "undo", 5))
         //     moveType = 3;
         // else if(stringsEqual(string, "quit", 5))
         //     break;
-        
-        if (model->turnState == 0 && model->squareSelected != 255) {
+
+        if(model->turnState == 0 && model->squareSelected != 255) {
             model->squareFrom = model->squareSelected;
             model->turnState = 1;
-        } else if (model->turnState == 1 && model->squareSelected != 255) {
+        } else if(model->turnState == 1 && model->squareSelected != 255) {
             model->squareTo = model->squareSelected;
             model->turnState = 2;
             model->squareSelectedLast = model->squareSelected;
@@ -234,14 +235,14 @@ uint8_t flipchess_turn(FlipChessScene1Model* model) {
                 SCL_boardWhitesTurn(model->game.board))) {
                 SCL_boardGetMoves(model->game.board, model->squareFrom, model->moveHighlight);
             }
-        } else if (model->turnState == 2) {
+        } else if(model->turnState == 2) {
             if(SCL_squareSetContains(model->moveHighlight, model->squareTo)) {
                 moveType = 1;
             }
             model->turnState = 0;
             SCL_squareSetClear(model->moveHighlight);
         }
-        
+
     } else {
         model->squareSelected = 255;
         flipchess_makeAIMove(
@@ -273,35 +274,43 @@ uint8_t flipchess_turn(FlipChessScene1Model* model) {
     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");
+            model->msg =
+                (SCL_boardWhitesTurn(model->game.board) ? "black played" : "white played");
 
             uint8_t s0, s1;
             char p;
@@ -310,9 +319,11 @@ uint8_t flipchess_turn(FlipChessScene1Model* model) {
             SCL_moveToString(model->game.board, s0, s1, p, model->moveString);
         }
         break;
+        model->paramExit = moveType;
     }
 
-    return moveType;
+    model->thinking = 0;
+    return model->paramExit;
 }
 
 void flipchess_scene_1_set_callback(
@@ -335,7 +346,11 @@ void flipchess_scene_1_draw(Canvas* canvas, FlipChessScene1Model* model) {
 
     // Message
     canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 68, 10, model->msg);
+    if(model->thinking) {
+        canvas_draw_str(canvas, 68, 10, "thinking...");
+    } else {
+        canvas_draw_str(canvas, 68, 10, model->msg);
+    }
     canvas_draw_str(canvas, 68, 19, model->moveString);
     canvas_draw_str(canvas, 68, 31, model->msg2);
     canvas_draw_str(canvas, 68, 40, model->moveString2);
@@ -363,15 +378,18 @@ static int flipchess_scene_1_model_init(
     model->paramMoves = 0;
     model->paramInfo = 1;
     model->paramFlipBoard = 0;
-    model->paramExit = 0;
+    model->paramExit = FlipChessStatusSuccess;
     model->paramStep = 0;
     model->paramFEN = NULL;
     model->paramPGN = NULL;
-
     model->clockSeconds = -1;
+
     SCL_Board emptyStartState = SCL_BOARD_START_STATE;
     memcpy(model->startState, &emptyStartState, sizeof(SCL_Board));
+
+#if ENABLE_960
     model->random960PosNumber = -1;
+#endif
 
     model->squareSelected = 255;
     model->squareSelectedLast = 28; // start selector near middle
@@ -382,6 +400,7 @@ static int flipchess_scene_1_model_init(
     model->moveString2[0] = '\0';
     model->msg3 = "";
     model->moveString3[0] = '\0';
+    model->thinking = 0;
 
     SCL_SquareSet emptySquareSet = SCL_SQUARE_SET_EMPTY;
     memcpy(model->moveHighlight, &emptySquareSet, sizeof(SCL_SquareSet));
@@ -389,14 +408,14 @@ static int flipchess_scene_1_model_init(
     model->squareTo = 255;
     model->turnState = 0;
 
-    furi_hal_random_init();
     SCL_randomBetterSeed(furi_hal_random_get());
 
+#if ENABLE_960
 #if SCL_960_CASTLING
     if(model->random960PosNumber < 0) model->random960PosNumber = SCL_randomBetter();
 #endif
-
     if(model->random960PosNumber >= 0) model->random960PosNumber %= 960;
+#endif
 
     if(model->paramFEN != NULL)
         SCL_boardFromFEN(model->startState, model->paramFEN);
@@ -406,9 +425,12 @@ static int flipchess_scene_1_model_init(
         SCL_boardInit(model->startState);
         SCL_recordApply(record, model->startState, model->paramStep);
     }
+
+#if ENABLE_960
 #if SCL_960_CASTLING
     else
         SCL_boardInit960(model->startState, model->random960PosNumber);
+#endif
 #endif
 
     SCL_gameInit(&(model->game), model->startState);
@@ -473,8 +495,13 @@ bool flipchess_scene_1_input(InputEvent* event, void* context) {
                 instance->view,
                 FlipChessScene1Model * model,
                 {
-                    UNUSED(model);
-                    instance->callback(FlipChessCustomEventScene1Back, instance->context);
+                    if(model->turnState == 1) {
+                        model->turnState = 0;
+                        SCL_squareSetClear(model->moveHighlight);
+                        flipchess_drawBoard(model);
+                    } else {
+                        instance->callback(FlipChessCustomEventScene1Back, instance->context);
+                    }
                 },
                 true);
             break;
@@ -483,7 +510,7 @@ bool flipchess_scene_1_input(InputEvent* event, void* context) {
                 instance->view,
                 FlipChessScene1Model * model,
                 {
-                    if (model->squareSelectedLast != 255 && model->squareSelected == 255) {
+                    if(model->squareSelectedLast != 255 && model->squareSelected == 255) {
                         model->squareSelected = model->squareSelectedLast;
                     } else {
                         model->squareSelected = (model->squareSelected + 1) % 64;
@@ -497,7 +524,7 @@ bool flipchess_scene_1_input(InputEvent* event, void* context) {
                 instance->view,
                 FlipChessScene1Model * model,
                 {
-                    if (model->squareSelectedLast != 255 && model->squareSelected == 255) {
+                    if(model->squareSelectedLast != 255 && model->squareSelected == 255) {
                         model->squareSelected = model->squareSelectedLast;
                     } else {
                         model->squareSelected = (model->squareSelected + 56) % 64;
@@ -511,7 +538,7 @@ bool flipchess_scene_1_input(InputEvent* event, void* context) {
                 instance->view,
                 FlipChessScene1Model * model,
                 {
-                    if (model->squareSelectedLast != 255 && model->squareSelected == 255) {
+                    if(model->squareSelectedLast != 255 && model->squareSelected == 255) {
                         model->squareSelected = model->squareSelectedLast;
                     } else {
                         model->squareSelected = (model->squareSelected + 63) % 64;
@@ -524,7 +551,8 @@ bool flipchess_scene_1_input(InputEvent* event, void* context) {
             with_view_model(
                 instance->view,
                 FlipChessScene1Model * model,
-                {if (model->squareSelectedLast != 255 && model->squareSelected == 255) {
+                {
+                    if(model->squareSelectedLast != 255 && model->squareSelected == 255) {
                         model->squareSelected = model->squareSelectedLast;
                     } else {
                         model->squareSelected = (model->squareSelected + 8) % 64;
@@ -535,27 +563,51 @@ bool flipchess_scene_1_input(InputEvent* event, void* context) {
             break;
         case InputKeyOk:
             with_view_model(
-                instance->view, FlipChessScene1Model * model, 
-                { 
+                instance->view,
+                FlipChessScene1Model * model,
+                {
+                    // if(model->paramExit == FlipChessStatusReturn) {
+                    //     instance->callback(FlipChessCustomEventScene1Back, instance->context);
+                    //     break;
+                    // }
+                    if(!flipchess_isPlayerTurn(model)) {
+                        model->thinking = 1;
+                    }
+                },
+                true);
+            furi_thread_flags_wait(0, FuriFlagWaitAny, THREAD_WAIT_TIME);
+
+            with_view_model(
+                instance->view,
+                FlipChessScene1Model * model,
+                {
                     // first turn of round, probably player but could be AI
-                    uint8_t turn = flipchess_turn(model);
-                    if(turn == FlipChessStatusReturn) {
-                        instance->callback(FlipChessCustomEventScene1Back, instance->context);
-                    } else {
-                        flipchess_drawBoard(model);
+                    flipchess_turn(model);
+                    flipchess_drawBoard(model);
+                },
+                true);
+
+            with_view_model(
+                instance->view,
+                FlipChessScene1Model * model,
+                {
+                    if(!flipchess_isPlayerTurn(model)) {
+                        model->thinking = 1;
                     }
- 
+                },
+                true);
+            furi_thread_flags_wait(0, FuriFlagWaitAny, THREAD_WAIT_TIME);
+
+            with_view_model(
+                instance->view,
+                FlipChessScene1Model * model,
+                {
                     // if player played, let AI play
-                    if (!flipchess_isPlayerTurn(model))
-                    {
-                        turn = flipchess_turn(model);
-                        if(turn == FlipChessStatusReturn) {
-                            instance->callback(FlipChessCustomEventScene1Back, instance->context);
-                        } else {
-                            flipchess_drawBoard(model);
-                        }
+                    if(!flipchess_isPlayerTurn(model)) {
+                        flipchess_turn(model);
+                        flipchess_drawBoard(model);
                     }
-                }, 
+                },
                 true);
             break;
         case InputKeyMAX:
@@ -570,7 +622,7 @@ void flipchess_scene_1_exit(void* context) {
     FlipChessScene1* instance = (FlipChessScene1*)context;
 
     with_view_model(
-        instance->view, FlipChessScene1Model * model, { model->squareSelected = 255; }, true);
+        instance->view, FlipChessScene1Model * model, { model->paramExit = 0; }, true);
 }
 
 void flipchess_scene_1_enter(void* context) {
@@ -585,8 +637,7 @@ void flipchess_scene_1_enter(void* context) {
         instance->view,
         FlipChessScene1Model * model,
         {
-            int init =
-                flipchess_scene_1_model_init(model, app->white_mode, app->black_mode);
+            int init = flipchess_scene_1_model_init(model, app->white_mode, app->black_mode);
 
             // nonzero status
             if(init == FlipChessStatusSuccess) {

+ 15 - 0
non_catalog_apps/chess_clock/application.fam

@@ -0,0 +1,15 @@
+App(
+    appid="chess_clock",
+    name="Chess Clock",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="chess_clock_app",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    order=45,
+    fap_icon="chess_clock.png",
+    fap_category="Games",
+    fap_author="@ihatecsv",
+    fap_weburl="https://github.com/ihatecsv",
+    fap_version="1.0",
+    fap_description="A simple chess clock",
+)

+ 182 - 0
non_catalog_apps/chess_clock/chess_clock.c

@@ -0,0 +1,182 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include <notification/notification_messages.h>
+#include <gui/elements.h>
+#include <furi_hal.h>
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} PluginEvent;
+
+typedef enum {
+    ChessClockStateSetup,
+    ChessClockStatePlayer1,
+    ChessClockStatePlayer2,
+    ChessClockStateGameOver,
+} ChessClockState;
+
+typedef struct {
+    ChessClockState state;
+    uint32_t total_time;
+    uint32_t time_left_p1;
+    uint32_t time_left_p2;
+    uint32_t last_tick;
+} PluginState;
+
+static void render_callback(Canvas* const canvas, void* ctx) {
+    furi_assert(ctx);
+    PluginState* plugin_state = ctx;
+
+    char text[50];
+
+    canvas_set_font(canvas, FontPrimary);
+    switch(plugin_state->state) {
+    case ChessClockStateSetup:
+    case ChessClockStatePlayer1:
+    case ChessClockStatePlayer2:
+        snprintf(
+            text,
+            sizeof(text),
+            "Player 1: %02lu:%02lu:%02lu\nPlayer 2: %02lu:%02lu:%02lu",
+            plugin_state->time_left_p1 / 60000,
+            (plugin_state->time_left_p1 / 1000) % 60,
+            (plugin_state->time_left_p1 % 1000) / 10,
+            plugin_state->time_left_p2 / 60000,
+            (plugin_state->time_left_p2 / 1000) % 60,
+            (plugin_state->time_left_p2 % 1000) / 10);
+        break;
+    case ChessClockStateGameOver:
+        snprintf(text, sizeof(text), "Player %d wins!", plugin_state->time_left_p1 > 0 ? 1 : 2);
+        break;
+    }
+    elements_multiline_text_aligned(canvas, 64, 32, AlignCenter, AlignCenter, text);
+}
+
+static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
+    furi_assert(event_queue);
+
+    PluginEvent event = {.type = EventTypeKey, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+int32_t chess_clock_app() {
+    NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
+    PluginState* plugin_state = malloc(sizeof(PluginState));
+    plugin_state->state = ChessClockStateSetup;
+    plugin_state->total_time = 300000;
+    plugin_state->time_left_p1 = plugin_state->total_time;
+    plugin_state->time_left_p2 = plugin_state->total_time;
+    plugin_state->last_tick = 0;
+
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, render_callback, plugin_state);
+    view_port_input_callback_set(view_port, input_callback, event_queue);
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    PluginEvent event;
+    for(bool processing = true; processing;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 10);
+
+        uint32_t now = furi_get_tick();
+
+        if(event_status == FuriStatusOk && event.type == EventTypeKey &&
+           event.input.key == InputKeyBack) {
+            if(event.input.type == InputTypeShort) {
+                plugin_state->state = ChessClockStateSetup;
+                plugin_state->time_left_p1 = plugin_state->total_time;
+                plugin_state->time_left_p2 = plugin_state->total_time;
+                notification_message(notification, &sequence_reset_rgb);
+            } else if(event.input.type == InputTypeLong) {
+                processing = false;
+            }
+        }
+
+        switch(plugin_state->state) {
+        case ChessClockStateSetup:
+            if(event_status == FuriStatusOk && event.type == EventTypeKey) {
+                if(event.input.key == InputKeyUp && event.input.type == InputTypePress) {
+                    plugin_state->total_time += 15000;
+                    plugin_state->time_left_p1 = plugin_state->total_time;
+                    plugin_state->time_left_p2 = plugin_state->total_time;
+                } else if(event.input.key == InputKeyDown && event.input.type == InputTypePress) {
+                    if(plugin_state->total_time > 15000) {
+                        plugin_state->total_time -= 15000;
+                        plugin_state->time_left_p1 = plugin_state->total_time;
+                        plugin_state->time_left_p2 = plugin_state->total_time;
+                    }
+                } else if(event.input.key == InputKeyOk && event.input.type == InputTypePress) {
+                    plugin_state->state = ChessClockStatePlayer1;
+                    plugin_state->last_tick = now;
+                }
+            }
+            break;
+
+        case ChessClockStatePlayer1:
+            if(event_status == FuriStatusOk && event.type == EventTypeKey &&
+               event.input.key == InputKeyOk && event.input.type == InputTypePress) {
+                plugin_state->state = ChessClockStatePlayer2;
+                plugin_state->last_tick = now;
+            } else {
+                plugin_state->time_left_p1 -= now - plugin_state->last_tick;
+                if(plugin_state->time_left_p1 <= 0 ||
+                   plugin_state->time_left_p1 >= UINT32_MAX - 1000) {
+                    plugin_state->time_left_p1 = 0;
+                    plugin_state->state = ChessClockStateGameOver;
+                    notification_message(notification, &sequence_set_only_blue_255);
+                    notification_message(notification, &sequence_single_vibro);
+                }
+                plugin_state->last_tick = now;
+            }
+            break;
+
+        case ChessClockStatePlayer2:
+            if(event_status == FuriStatusOk && event.type == EventTypeKey &&
+               event.input.key == InputKeyOk && event.input.type == InputTypePress) {
+                plugin_state->state = ChessClockStatePlayer1;
+                plugin_state->last_tick = now;
+            } else {
+                plugin_state->time_left_p2 -= now - plugin_state->last_tick;
+                if(plugin_state->time_left_p2 <= 0 ||
+                   plugin_state->time_left_p2 >= UINT32_MAX - 1000) {
+                    plugin_state->time_left_p2 = 0;
+                    plugin_state->state = ChessClockStateGameOver;
+                    notification_message(notification, &sequence_set_only_green_255);
+                    notification_message(notification, &sequence_single_vibro);
+                }
+                plugin_state->last_tick = now;
+            }
+            break;
+
+        case ChessClockStateGameOver:
+            if(event_status == FuriStatusOk && event.type == EventTypeKey &&
+               event.input.key == InputKeyBack && event.input.type == InputTypeShort) {
+                plugin_state->state = ChessClockStateSetup;
+                plugin_state->time_left_p1 = plugin_state->total_time;
+                plugin_state->time_left_p2 = plugin_state->total_time;
+            }
+            break;
+        }
+
+        view_port_update(view_port);
+    }
+
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+    free(plugin_state);
+
+    return 0;
+}

BIN
non_catalog_apps/chess_clock/chess_clock.png


+ 1 - 0
non_catalog_apps/esp32_gravity/.vscode/settings.json

@@ -0,0 +1 @@
+{}

+ 22 - 0
non_catalog_apps/esp32_gravity/LICENSE

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

+ 88 - 0
non_catalog_apps/esp32_gravity/README.md

@@ -0,0 +1,88 @@
+# Gravity Companion for Flipper Zero
+
+This Flipper Zero application (FAP) is a companion application to ESP32-Gravity.
+
+ESP32-Gravity is a wireless exploration framework written for the ESP32-C6. It
+supports a variety of wireless exploration, offensive and defensive features.
+
+ESP32-Gravity can be used without a Flipper, but Flipper-Gravity can't be used
+without ESP32-Gravity. You can download it from:
+(https://github.com/chris-bc/esp32c6-gravity|https://github.com/chris-bc/esp32c6-gravity)
+
+## TODO
+* Bug: Console scrolls to top mid-scroll when text is added to it
+    * Replicate: Start scan, Run help and try to read it
+
+## Menu Structure
+* Beacon: RickRoll Random Infinite target-ssids (APs)
+* Probe: Any target-ssids
+* Sniff: On Off
+* target-ssids: add remove list
+* scan: on off
+* hop: on off default
+* view: sta ap sta+ap
+* select: sta ap
+* clear: sta ap
+* get/set: <variables>
+* deauth: off, frame sta, device sta, spoof sta, frame broadcast, device broadcast, spoof broadcast
+* mana: on off clear
+* mana verbose: on off
+* mana loud: on off
+* help: commands help
+
+# UART Terminal for Flipper Zero
+[Flipper Zero](https://flipperzero.one/) app to control various devices via UART interface.
+## Download fap
+| **FW Official** | **FW Unleashed** |
+| - | - |
+| [![FAP Factory](https://flipc.org/api/v1/cool4uma/UART_Terminal/badge)](https://flipc.org/cool4uma/UART_Terminal) | [![FAP Factory](https://flipc.org/api/v1/cool4uma/UART_Terminal/badge?firmware=unleashed)](https://flipc.org/cool4uma/UART_Terminal?firmware=unleashed) |
+
+## Capabilities
+- Read log and command output by uart
+- Send commands by uart
+- Send AT commands
+- Set baud rate
+- Fast commands
+
+## Connecting
+| Flipper Zero pin | UART interface  |
+| ---------------- | --------------- |
+| 13 TX            | RX              |
+| 14 RX            | TX              |
+|8, 18 GND         | GND             |
+
+Info: If possible, do not power your devices from 3V3 (pin 9) Flipper Zero. It does not support hot plugging.
+
+## Keyboard
+UART_terminal uses its own special keyboard for work, which has all the symbols necessary for working in the console.
+
+To accommodate more characters on a small display, some characters are called up by holding.
+
+![kbf](https://user-images.githubusercontent.com/122148894/212286637-7063f1ee-c6ff-46b9-8dc5-79a5f367fab1.png)
+
+## Supported send AT commands
+In the "Send AT command" mode, the keyboard settings are changed for the convenience of entering AT commands.
+
+![AT](https://user-images.githubusercontent.com/122148894/230785072-319fe5c9-deca-49f9-bfe4-5ace89d38d53.png)
+
+
+## How to install
+Copy the contents of the repository to the applications_user/uart_terminal folder Flipper Zero firmware and build app with the command ./fbt fap_uart_terminal.
+
+Or use the tool [uFBT](https://github.com/flipperdevices/flipperzero-ufbt) for building applications for Flipper Zero.
+
+Download ready [fap](https://github.com/playmean/fap-list)
+
+## How it works
+
+
+![1f](https://user-images.githubusercontent.com/122148894/211161450-6d177638-3bfa-42a8-9c73-0cf3af5e5ca7.jpg)
+
+
+![2f](https://user-images.githubusercontent.com/122148894/211161456-4d2be15b-4a05-4450-a62e-edcaab3772fd.jpg)
+
+
+
+## INFO:
+
+~60% of the source code is taken from the [Wifi Marauder](https://github.com/0xchocolate/flipperzero-firmware-with-wifi-marauder-companion) project. Many thanks to the developers of the Wifi Marauder project.

+ 16 - 0
non_catalog_apps/esp32_gravity/application.fam

@@ -0,0 +1,16 @@
+App(
+    appid="esp32_gravity",
+    name="[ESP32] Gravity",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="uart_terminal_app",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    order=90,
+    fap_icon="uart_terminal.png",
+    fap_category="GPIO",
+    fap_icon_assets="assets",
+    fap_icon_assets_symbol="uart_terminal",
+    fap_author="https://github.com/chris-bc",
+    fap_version="1.0",
+    fap_description="App to control ESP32-C6 Gravity wireless exploration platform.",
+)

BIN
non_catalog_apps/esp32_gravity/assets/KeyBackspaceSelected_16x9.png


BIN
non_catalog_apps/esp32_gravity/assets/KeyBackspace_16x9.png


BIN
non_catalog_apps/esp32_gravity/assets/KeySaveSelected_24x11.png


BIN
non_catalog_apps/esp32_gravity/assets/KeySave_24x11.png


BIN
non_catalog_apps/esp32_gravity/assets/WarningDolphin_45x42.png


+ 45 - 0
non_catalog_apps/esp32_gravity/esp_flip_const.h

@@ -0,0 +1,45 @@
+/* Command usage string - SHORT_* is compressed help text for Flipper */
+const char USAGE_BEACON[] =
+    "Beacon spam attack. Usage: beacon [ RICKROLL | RANDOM [ COUNT ] | INFINITE | USER | OFF ]";
+const char USAGE_TARGET_SSIDS[] =
+    "Manage SSID targets. Usage: target-ssids [ ( ADD | REMOVE ) <ssid_name> ]";
+const char USAGE_PROBE[] = "Probe flood attack. Usage: probe [ ANY | SSIDS | OFF ]";
+const char USAGE_SNIFF[] = "Display interesting packets. Usage: sniff [ ON | OFF ]";
+const char USAGE_DEAUTH[] =
+    "Deauth attack. Usage: deauth [ <millis> ] [ FRAME | DEVICE | SPOOF ] [ STA | BROADCAST | OFF ]";
+const char USAGE_MANA[] =
+    "Mana attack. Usage: mana ( CLEAR | ( [ VERBOSE ] [ ON | OFF ] ) | ( AUTH [ NONE | WEP | WPA ] ) | ( LOUD [ ON | OFF ] ) )";
+const char USAGE_STALK[] = "Toggle target tracking/homing. Usage: stalk";
+const char USAGE_AP_DOS[] = "802.11 denial-of-service attack. Usage: ap-dos [ ON | OFF ]";
+const char USAGE_AP_CLONE[] =
+    "Clone and attempt takeover of the specified AP. Usage: ap-clone [ <AP MAC> | APs | OFF ]";
+const char USAGE_SCAN[] = "Scan for wireless devices. Usage: scan [ <ssid> | ON | OFF ]";
+const char USAGE_HOP[] =
+    "Configure channel hopping. Usage: hop [ <millis> ] [ ON | OFF | DEFAULT | KILL ]";
+const char USAGE_SET[] = "Set a variable. Usage: set <variable> <value>";
+const char USAGE_GET[] = "Get a variable. Usage: get <variable>";
+const char USAGE_VIEW[] = "List available targets. Usage: view ( AP | STA )+";
+const char USAGE_SELECT[] = "Select an element. Usage: select ( AP | STA ) <elementId>+";
+const char USAGE_CLEAR[] = "Clear stored APs or STAs. Usage: clear ( AP | STA | ALL )";
+const char USAGE_HANDSHAKE[] =
+    "Toggle monitoring for encryption material. Usage handshake [ ON | OFF ]";
+const char USAGE_COMMANDS[] = "Display a *brief* summary of Gravity commands";
+
+const char SHORT_BEACON[] = "beacon RANDOM <count>";
+const char SHORT_TARGET_SSIDS[] = "(ADD | REMOVE) <apName>";
+const char SHORT_PROBE[] = "probe ANY | SSIDS | OFF";
+const char SHORT_SNIFF[] = "sniff [ ON | OFF ]";
+const char SHORT_DEAUTH[] = "deauth <millis>";
+const char SHORT_MANA[] = "Mana attack";
+const char SHORT_STALK[] = "Track RSSI";
+const char SHORT_AP_DOS[] = "ap-dos [ ON | OFF ]";
+const char SHORT_AP_CLONE[] = "ap-clone <AP MAC>";
+const char SHORT_SCAN[] = "scan <SSID Name>";
+const char SHORT_HOP[] = "hop <millis>";
+const char SHORT_SET[] = "set <variable> <value>";
+const char SHORT_GET[] = "get <variable>";
+const char SHORT_VIEW[] = "VIEW ( AP | STA )+";
+const char SHORT_SELECT[] = "select ( AP | STA ) <id>+";
+const char SHORT_CLEAR[] = "clear ( AP | STA | ALL )";
+const char SHORT_HANDSHAKE[] = "handshake [ ON | OFF ]";
+const char SHORT_COMMANDS[] = "Brief command summary";

+ 40 - 0
non_catalog_apps/esp32_gravity/esp_flip_struct.h

@@ -0,0 +1,40 @@
+/*  Globals to track module status information */
+enum AttackMode {
+    ATTACK_BEACON,
+    ATTACK_PROBE,
+    ATTACK_SNIFF,
+    ATTACK_DEAUTH,
+    ATTACK_MANA,
+    ATTACK_MANA_VERBOSE,
+    ATTACK_MANA_LOUD,
+    ATTACK_AP_DOS,
+    ATTACK_AP_CLONE,
+    ATTACK_SCAN,
+    ATTACK_HANDSHAKE,
+    ATTACK_RANDOMISE_MAC, // True
+    ATTACKS_COUNT
+};
+typedef enum AttackMode AttackMode;
+
+enum GravityCommand {
+    GRAVITY_NONE,
+    GRAVITY_BEACON,
+    GRAVITY_TARGET_SSIDS,
+    GRAVITY_PROBE,
+    GRAVITY_SNIFF,
+    GRAVITY_DEAUTH,
+    GRAVITY_MANA,
+    GRAVITY_STALK,
+    GRAVITY_AP_DOS,
+    GRAVITY_AP_CLONE,
+    GRAVITY_SCAN,
+    GRAVITY_HOP,
+    GRAVITY_SET,
+    GRAVITY_GET,
+    GRAVITY_VIEW,
+    GRAVITY_SELECT,
+    GRAVITY_CLEAR,
+    GRAVITY_HANDSHAKE,
+    GRAVITY_COMMANDS
+};
+typedef enum GravityCommand GravityCommand;

+ 30 - 0
non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene.c

@@ -0,0 +1,30 @@
+#include "uart_terminal_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const uart_terminal_scene_on_enter_handlers[])(void*) = {
+#include "uart_terminal_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 uart_terminal_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "uart_terminal_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 uart_terminal_scene_on_exit_handlers[])(void* context) = {
+#include "uart_terminal_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers uart_terminal_scene_handlers = {
+    .on_enter_handlers = uart_terminal_scene_on_enter_handlers,
+    .on_event_handlers = uart_terminal_scene_on_event_handlers,
+    .on_exit_handlers = uart_terminal_scene_on_exit_handlers,
+    .scene_num = UART_TerminalSceneNum,
+};

+ 29 - 0
non_catalog_apps/esp32_gravity/scenes/uart_terminal_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) UART_TerminalScene##id,
+typedef enum {
+#include "uart_terminal_scene_config.h"
+    UART_TerminalSceneNum,
+} UART_TerminalScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers uart_terminal_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "uart_terminal_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 "uart_terminal_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 "uart_terminal_scene_config.h"
+#undef ADD_SCENE

+ 3 - 0
non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_config.h

@@ -0,0 +1,3 @@
+ADD_SCENE(uart_terminal, start, Start)
+ADD_SCENE(uart_terminal, console_output, ConsoleOutput)
+ADD_SCENE(uart_terminal, text_input, UART_TextInput)

+ 98 - 0
non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_console_output.c

@@ -0,0 +1,98 @@
+#include "../uart_terminal_app_i.h"
+
+void uart_terminal_console_output_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) {
+    furi_assert(context);
+    UART_TerminalApp* app = context;
+
+    // If text box store gets too big, then truncate it
+    app->text_box_store_strlen += len;
+    if(app->text_box_store_strlen >= UART_TERMINAL_TEXT_BOX_STORE_SIZE - 1) {
+        furi_string_right(app->text_box_store, app->text_box_store_strlen / 2);
+        app->text_box_store_strlen = furi_string_size(app->text_box_store) + len;
+    }
+
+    // Null-terminate buf and append to text box store
+    buf[len] = '\0';
+    furi_string_cat_printf(app->text_box_store, "%s", buf);
+
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, UART_TerminalEventRefreshConsoleOutput);
+}
+
+void uart_terminal_scene_console_output_on_enter(void* context) {
+    UART_TerminalApp* app = context;
+
+    TextBox* text_box = app->text_box;
+    text_box_reset(app->text_box);
+    text_box_set_font(text_box, TextBoxFontText);
+    if(app->focus_console_start) {
+        text_box_set_focus(text_box, TextBoxFocusStart);
+    } else {
+        text_box_set_focus(text_box, TextBoxFocusEnd);
+    }
+
+    if(app->is_command) {
+        furi_string_reset(
+            app->text_box_store); /* GRAVITY If this callback is called each time the view is displayed, I think this will clear the screen */
+        app->text_box_store_strlen = 0;
+
+        if(0 == strncmp("help", app->selected_tx_string, strlen("help"))) {
+            const char* help_msg =
+                "ESP32 Gravity terminal for Flipper\n\nCopypasted from UART terminal by cool4uma\n\nThis app is a modified\nUART terminal,\nThanks cool4uma and 0xchocolate(github)\nfor great code and app.\n\n";
+            furi_string_cat_str(app->text_box_store, help_msg);
+            app->text_box_store_strlen += strlen(help_msg);
+        }
+
+        if(app->show_stopscan_tip) {
+            const char* help_msg = "Press BACK to return\n";
+            furi_string_cat_str(app->text_box_store, help_msg);
+            app->text_box_store_strlen += strlen(help_msg);
+        }
+    }
+
+    // Set starting text - for "View Log", this will just be what was already in the text box store
+    text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store));
+
+    scene_manager_set_scene_state(app->scene_manager, UART_TerminalSceneConsoleOutput, 0);
+    view_dispatcher_switch_to_view(app->view_dispatcher, UART_TerminalAppViewConsoleOutput);
+
+    // Register callback to receive data
+    uart_terminal_uart_set_handle_rx_data_cb(
+        app->uart, uart_terminal_console_output_handle_rx_data_cb); // setup callback for rx thread
+
+    // Send command with CR+LF or newline '\n'
+    /* GRAVITY: Ignore the "cls" command */
+    if(app->is_command && app->selected_tx_string && strcmp(app->selected_tx_string, "cls")) {
+        if(app->TERMINAL_MODE == 1) {
+            uart_terminal_uart_tx(
+                (uint8_t*)(app->selected_tx_string), strlen(app->selected_tx_string));
+            uart_terminal_uart_tx((uint8_t*)("\r\n"), 2);
+        } else {
+            uart_terminal_uart_tx(
+                (uint8_t*)(app->selected_tx_string), strlen(app->selected_tx_string));
+            uart_terminal_uart_tx((uint8_t*)("\n"), 1);
+        }
+    }
+}
+
+bool uart_terminal_scene_console_output_on_event(void* context, SceneManagerEvent event) {
+    UART_TerminalApp* app = context;
+
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store));
+        consumed = true;
+    } else if(event.type == SceneManagerEventTypeTick) {
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void uart_terminal_scene_console_output_on_exit(void* context) {
+    UART_TerminalApp* app = context;
+
+    // Unregister rx callback
+    uart_terminal_uart_set_handle_rx_data_cb(app->uart, NULL);
+}

+ 342 - 0
non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_start.c

@@ -0,0 +1,342 @@
+#include "../uart_terminal_app_i.h"
+#include <dolphin/dolphin.h>
+
+// For each command, define whether additional arguments are needed
+// (enabling text input to fill them out), and whether the console
+// text box should focus at the start of the output or the end
+typedef enum { NO_ARGS = 0, INPUT_ARGS, TOGGLE_ARGS } InputArgs;
+
+typedef enum { FOCUS_CONSOLE_END = 0, FOCUS_CONSOLE_START, FOCUS_CONSOLE_TOGGLE } FocusConsole;
+
+#define SHOW_STOPSCAN_TIP (true)
+#define NO_TIP (false)
+
+#define MAX_OPTIONS (9)
+typedef struct {
+    const char* item_string;
+    const char* options_menu[MAX_OPTIONS];
+    int num_options_menu;
+    const char* actual_commands[MAX_OPTIONS];
+    InputArgs needs_keyboard;
+    FocusConsole focus_console;
+    bool show_stopscan_tip;
+} UART_TerminalItem;
+
+// NUM_MENU_ITEMS defined in uart_terminal_app_i.h - if you add an entry here, increment it!
+/* CBC: Looking for a way to best use TOGGLE_ARGS, how's this:
+        ** If actual_commands[i] ends with space, display a keyboard to fill in the blank ***
+*/
+const UART_TerminalItem items[NUM_MENU_ITEMS] = {
+    {"Console", {"View", "Clear"}, 2, {"", "cls"}, NO_ARGS, FOCUS_CONSOLE_END, NO_TIP},
+    {"Beacon",
+     {"Status", "RickRoll", "Random", "Infinite", "target-ssids", "Off"},
+     6,
+     {"beacon", "beacon rickroll", "beacon random ", "beacon infinite", "beacon user", "beacon off"},
+     TOGGLE_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Probe",
+     {"Status", "Any", "target-ssids", "Off"},
+     4,
+     {"probe", "probe any", "probe ssids", "probe off"},
+     NO_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Sniff",
+     {"Status", "On", "Off"},
+     3,
+     {"sniff", "sniff on", "sniff off"},
+     NO_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"target-ssids",
+     {"Add", "Remove", "List"},
+     3,
+     {"target-ssids add ", "target-ssids remove ", "target-ssids"},
+     TOGGLE_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Scan",
+     {"Status", "On", "Off", "<ssid>"},
+     4,
+     {"scan", "scan on", "scan off", "scan "},
+     TOGGLE_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Hop",
+     {"Status", "On", "Off", "Default", "Set "},
+     5,
+     {"hop", "hop on", "hop off", "hop default", "hop "},
+     TOGGLE_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"View",
+     {"STA", "AP", "STA+AP"},
+     3,
+     {"view sta", "view ap", "view sta ap"},
+     NO_ARGS,
+     FOCUS_CONSOLE_START,
+     NO_TIP},
+    {"Select",
+     {"STA", "AP"},
+     2,
+     {"select sta ", "select ap "},
+     INPUT_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Clear", {"STA", "AP"}, 2, {"clear sta", "clear ap"}, NO_ARGS, FOCUS_CONSOLE_END, NO_TIP},
+    {"Get",
+     {"SSID_LEN_MIN", "SSID_LEN_MAX", "DEFAULT_SSID_COUNT", "Channel", "MAC", "MAC_RAND"},
+     6,
+     {"get ssid_len_min",
+      "get ssid_len_max",
+      "get default_ssid_count",
+      "get channel",
+      "get mac",
+      "get mac_rand"},
+     NO_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Set",
+     {"SSID_LEN_MIN", "SSID_LEN_MAX", "DEFAULT_SSID_COUNT", "Channel", "MAC", "MAC_RAND"},
+     6,
+     {"set ssid_len_min ",
+      "set ssid_len_max ",
+      "set default_ssid_count ",
+      "set channel ",
+      "set mac ",
+      "set mac_rand "},
+     INPUT_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Deauth",
+     {"Status",
+      "Set Delay",
+      "Off",
+      "Frame STA",
+      "Device STA",
+      "Spoof STA",
+      "Frame B'Cast",
+      "Device B'Cast",
+      "Spoof B'Cast"},
+     9,
+     {"deauth",
+      "deauth ",
+      "deauth off",
+      "deauth frame sta",
+      "deauth device sta",
+      "deauth spoof sta",
+      "deauth frame broadcast",
+      "deauth device broadcast",
+      "deauth spoof broadcast"},
+     TOGGLE_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Mana",
+     {"Status", "On", "Off", "Clear"},
+     4,
+     {"mana", "mana on", "mana off", "mana clear"},
+     NO_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Mana Verbose",
+     {"Status", "On", "Off"},
+     3,
+     {"mana verbose", "mana verbose on", "mana verbose off"},
+     NO_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Mana Loud",
+     {"Status", "On", "Off"},
+     3,
+     {"mana loud", "mana loud on", "mana loud off"},
+     NO_ARGS,
+     FOCUS_CONSOLE_END,
+     NO_TIP},
+    {"Help", {"Commands", "Help"}, 2, {"commands", "help"}, NO_ARGS, FOCUS_CONSOLE_START, NO_TIP},
+};
+
+char* strToken(char* cmdLine, char sep, int tokenNum) {
+    size_t i;
+    int tokenCount = 0;
+    for(i = 0; i < strlen(cmdLine) && tokenCount != tokenNum; ++i) {
+        if(cmdLine[i] == sep) {
+            ++tokenCount;
+        }
+    }
+    if(cmdLine[i - 1] == sep || cmdLine[i - 1] == '\0') {
+        /* Found the end of the token, now find the beginning */
+        int j;
+        for(j = (i - 2); j > 0 && cmdLine[j] != sep; --j) {
+        }
+        /* Token runs from index j to (i - 2) */
+        char* retVal = malloc(sizeof(char) * (i - j));
+        if(retVal == NULL) {
+            printf("GRAVITY: Failed to malloc token\n");
+            return NULL;
+        }
+        strncpy(retVal, cmdLine, (i - j - 1));
+        retVal[i - j - 1] = '\0';
+        return retVal;
+    } else {
+        /* No token */
+        if(tokenNum == 1) {
+            return cmdLine;
+        } else {
+            return NULL;
+        }
+    }
+    return NULL;
+}
+
+/* Callback when an option is selected */
+static void uart_terminal_scene_start_var_list_enter_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    UART_TerminalApp* app = context;
+
+    furi_assert(index < NUM_MENU_ITEMS);
+    const UART_TerminalItem* item = &items[index];
+
+    dolphin_deed(DolphinDeedGpioUartBridge);
+
+    const int selected_option_index = app->selected_option_index[index];
+    furi_assert(selected_option_index < item->num_options_menu);
+    app->selected_tx_string = item->actual_commands[selected_option_index];
+    /* Don't clear screen if command is an empty string */
+    app->is_command = (strlen(app->selected_tx_string) > 0);
+    app->is_custom_tx_string = false;
+    app->selected_menu_index = index;
+    app->focus_console_start = (item->focus_console == FOCUS_CONSOLE_TOGGLE) ?
+                                   (selected_option_index == 0) :
+                                   item->focus_console;
+    app->show_stopscan_tip = item->show_stopscan_tip;
+
+    /* GRAVITY: Set app->gravityMode based on first word in command */
+
+    //char *cmd = strsep(&origCmd, " ");
+    /* GRAVITY: strsep is disabled by Flipper's SDK. RYO */
+    char* cmd = strToken((char*)app->selected_tx_string, ' ', 1);
+    if(!strcmp(cmd, "beacon")) {
+        app->gravityCommand = GRAVITY_BEACON;
+    } else if(!strcmp(cmd, "target-ssids")) {
+        app->gravityCommand = GRAVITY_TARGET_SSIDS;
+    } else if(!strcmp(cmd, "probe")) {
+        app->gravityCommand = GRAVITY_PROBE;
+    } else if(!strcmp(cmd, "sniff")) {
+        app->gravityCommand = GRAVITY_SNIFF;
+    } else if(!strcmp(cmd, "deauth")) {
+        app->gravityCommand = GRAVITY_DEAUTH;
+    } else if(!strcmp(cmd, "mana")) {
+        app->gravityCommand = GRAVITY_MANA;
+    } else if(!strcmp(cmd, "stalk")) {
+        app->gravityCommand = GRAVITY_STALK;
+    } else if(!strcmp(cmd, "ap-dos")) {
+        app->gravityCommand = GRAVITY_AP_DOS;
+    } else if(!strcmp(cmd, "ap-clone")) {
+        app->gravityCommand = GRAVITY_AP_CLONE;
+    } else if(!strcmp(cmd, "scan")) {
+        app->gravityCommand = GRAVITY_SCAN;
+    } else if(!strcmp(cmd, "hop")) {
+        app->gravityCommand = GRAVITY_HOP;
+    } else if(!strcmp(cmd, "set")) {
+        app->gravityCommand = GRAVITY_SET;
+    } else if(!strcmp(cmd, "get")) {
+        app->gravityCommand = GRAVITY_GET;
+    } else if(!strcmp(cmd, "view")) {
+        app->gravityCommand = GRAVITY_VIEW;
+    } else if(!strcmp(cmd, "select")) {
+        app->gravityCommand = GRAVITY_SELECT;
+    } else if(!strcmp(cmd, "clear")) {
+        app->gravityCommand = GRAVITY_CLEAR;
+    } else if(!strcmp(cmd, "handshake")) {
+        app->gravityCommand = GRAVITY_HANDSHAKE;
+    } else if(!strcmp(cmd, "commands")) {
+        app->gravityCommand = GRAVITY_COMMANDS;
+    } else {
+        app->gravityCommand = GRAVITY_NONE;
+    }
+
+    free(cmd);
+
+    /* GRAVITY: For TOGGLE_ARGS display a keyboard if actual_command ends with ' ' */
+    int cmdLen = strlen(app->selected_tx_string);
+    bool needs_keyboard =
+        ((item->needs_keyboard == INPUT_ARGS) ||
+         (item->needs_keyboard == TOGGLE_ARGS && (app->selected_tx_string[cmdLen - 1] == ' ')));
+    if(needs_keyboard) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, UART_TerminalEventStartKeyboard);
+    } else {
+        view_dispatcher_send_custom_event(app->view_dispatcher, UART_TerminalEventStartConsole);
+    }
+}
+
+/* Callback when a selected option is changed (I Think) */
+static void uart_terminal_scene_start_var_list_change_callback(VariableItem* item) {
+    furi_assert(item);
+
+    UART_TerminalApp* app = variable_item_get_context(item);
+    furi_assert(app);
+
+    const UART_TerminalItem* menu_item = &items[app->selected_menu_index];
+    uint8_t item_index = variable_item_get_current_value_index(item);
+    furi_assert(item_index < menu_item->num_options_menu);
+    variable_item_set_current_value_text(item, menu_item->options_menu[item_index]);
+    app->selected_option_index[app->selected_menu_index] = item_index;
+}
+
+/* Callback on entering the scene (initialisation) */
+void uart_terminal_scene_start_on_enter(void* context) {
+    UART_TerminalApp* app = context;
+    VariableItemList* var_item_list = app->var_item_list;
+
+    variable_item_list_set_enter_callback(
+        var_item_list, uart_terminal_scene_start_var_list_enter_callback, app);
+
+    VariableItem* item;
+    for(int i = 0; i < NUM_MENU_ITEMS; ++i) {
+        item = variable_item_list_add(
+            var_item_list,
+            items[i].item_string,
+            items[i].num_options_menu,
+            uart_terminal_scene_start_var_list_change_callback,
+            app);
+        variable_item_set_current_value_index(item, app->selected_option_index[i]);
+        variable_item_set_current_value_text(
+            item, items[i].options_menu[app->selected_option_index[i]]);
+    }
+
+    variable_item_list_set_selected_item(
+        var_item_list, scene_manager_get_scene_state(app->scene_manager, UART_TerminalSceneStart));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, UART_TerminalAppViewVarItemList);
+}
+
+/* Event handler callback - Handle scene change and tick events */
+bool uart_terminal_scene_start_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UART_TerminalApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == UART_TerminalEventStartKeyboard) {
+            scene_manager_set_scene_state(
+                app->scene_manager, UART_TerminalSceneStart, app->selected_menu_index);
+            scene_manager_next_scene(app->scene_manager, UART_TerminalAppViewTextInput);
+        } else if(event.event == UART_TerminalEventStartConsole) {
+            scene_manager_set_scene_state(
+                app->scene_manager, UART_TerminalSceneStart, app->selected_menu_index);
+            scene_manager_next_scene(app->scene_manager, UART_TerminalAppViewConsoleOutput);
+        }
+        consumed = true;
+    } else if(event.type == SceneManagerEventTypeTick) {
+        app->selected_menu_index = variable_item_list_get_selected_item_index(app->var_item_list);
+        consumed = true;
+    }
+    return consumed;
+}
+
+/* Clean up on exit */
+void uart_terminal_scene_start_on_exit(void* context) {
+    UART_TerminalApp* app = context;
+    variable_item_list_reset(app->var_item_list);
+}

+ 124 - 0
non_catalog_apps/esp32_gravity/scenes/uart_terminal_scene_text_input.c

@@ -0,0 +1,124 @@
+#include "../uart_terminal_app_i.h"
+
+/* GRAVITY: Import usage strings */
+#include "../esp_flip_const.h"
+
+void uart_terminal_scene_text_input_callback(void* context) {
+    UART_TerminalApp* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, UART_TerminalEventStartConsole);
+}
+
+void uart_terminal_scene_text_input_on_enter(void* context) {
+    UART_TerminalApp* app = context;
+
+    if(false == app->is_custom_tx_string) {
+        // Fill text input with selected string so that user can add to it
+        size_t length = strlen(app->selected_tx_string);
+        furi_assert(length < UART_TERMINAL_TEXT_INPUT_STORE_SIZE);
+        bzero(app->text_input_store, UART_TERMINAL_TEXT_INPUT_STORE_SIZE);
+        strncpy(app->text_input_store, app->selected_tx_string, length);
+
+        // Add space - because flipper keyboard currently doesn't have a space
+        //app->text_input_store[length] = ' ';
+        app->text_input_store[length + 1] = '\0';
+        app->is_custom_tx_string = true;
+    }
+
+    // Setup view
+    UART_TextInput* text_input = app->text_input;
+    // Add help message to header
+    char* helpStr = NULL;
+    switch(app->gravityCommand) {
+    case GRAVITY_BEACON:
+        helpStr = (char*)SHORT_BEACON;
+        break;
+    case GRAVITY_TARGET_SSIDS:
+        helpStr = (char*)SHORT_TARGET_SSIDS;
+        break;
+    case GRAVITY_PROBE:
+        helpStr = (char*)SHORT_PROBE;
+        break;
+    case GRAVITY_SNIFF:
+        helpStr = (char*)SHORT_SNIFF;
+        break;
+    case GRAVITY_DEAUTH:
+        helpStr = (char*)SHORT_DEAUTH;
+        break;
+    case GRAVITY_MANA:
+        helpStr = (char*)SHORT_MANA;
+        break;
+    case GRAVITY_STALK:
+        helpStr = (char*)SHORT_STALK;
+        break;
+    case GRAVITY_AP_DOS:
+        helpStr = (char*)SHORT_AP_DOS;
+        break;
+    case GRAVITY_AP_CLONE:
+        helpStr = (char*)SHORT_AP_CLONE;
+        break;
+    case GRAVITY_SCAN:
+        helpStr = (char*)SHORT_SCAN;
+        break;
+    case GRAVITY_HOP:
+        helpStr = (char*)SHORT_HOP;
+        break;
+    case GRAVITY_SET:
+        helpStr = (char*)SHORT_SET;
+        break;
+    case GRAVITY_GET:
+        helpStr = (char*)SHORT_GET;
+        break;
+    case GRAVITY_VIEW:
+        helpStr = (char*)SHORT_VIEW;
+        break;
+    case GRAVITY_SELECT:
+        helpStr = (char*)SHORT_SELECT;
+        break;
+    case GRAVITY_CLEAR:
+        helpStr = (char*)SHORT_CLEAR;
+        break;
+    case GRAVITY_HANDSHAKE:
+        helpStr = (char*)SHORT_HANDSHAKE;
+        break;
+    case GRAVITY_COMMANDS:
+        helpStr = (char*)SHORT_COMMANDS;
+        break;
+    default:
+        helpStr = "Send command to UART";
+        break;
+    }
+
+    uart_text_input_set_header_text(text_input, helpStr);
+    uart_text_input_set_result_callback(
+        text_input,
+        uart_terminal_scene_text_input_callback,
+        app,
+        app->text_input_store,
+        UART_TERMINAL_TEXT_INPUT_STORE_SIZE,
+        false);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, UART_TerminalAppViewTextInput);
+}
+
+bool uart_terminal_scene_text_input_on_event(void* context, SceneManagerEvent event) {
+    UART_TerminalApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == UART_TerminalEventStartConsole) {
+            // Point to custom string to send
+            app->selected_tx_string = app->text_input_store;
+            scene_manager_next_scene(app->scene_manager, UART_TerminalAppViewConsoleOutput);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void uart_terminal_scene_text_input_on_exit(void* context) {
+    UART_TerminalApp* app = context;
+
+    uart_text_input_reset(app->text_input);
+}

BIN
non_catalog_apps/esp32_gravity/uart_terminal.png


+ 104 - 0
non_catalog_apps/esp32_gravity/uart_terminal_app.c

@@ -0,0 +1,104 @@
+#include "uart_terminal_app_i.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+
+static bool uart_terminal_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    UART_TerminalApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool uart_terminal_app_back_event_callback(void* context) {
+    furi_assert(context);
+    UART_TerminalApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void uart_terminal_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    UART_TerminalApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+UART_TerminalApp* uart_terminal_app_alloc() {
+    UART_TerminalApp* app = malloc(sizeof(UART_TerminalApp));
+
+    app->gui = furi_record_open(RECORD_GUI);
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->scene_manager = scene_manager_alloc(&uart_terminal_scene_handlers, app);
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, uart_terminal_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, uart_terminal_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, uart_terminal_app_tick_event_callback, 100);
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    app->var_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        UART_TerminalAppViewVarItemList,
+        variable_item_list_get_view(app->var_item_list));
+
+    for(int i = 0; i < NUM_MENU_ITEMS; ++i) {
+        app->selected_option_index[i] = 0;
+    }
+
+    app->text_box = text_box_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, UART_TerminalAppViewConsoleOutput, text_box_get_view(app->text_box));
+    app->text_box_store = furi_string_alloc();
+    furi_string_reserve(app->text_box_store, UART_TERMINAL_TEXT_BOX_STORE_SIZE);
+
+    app->text_input = uart_text_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        UART_TerminalAppViewTextInput,
+        uart_text_input_get_view(app->text_input));
+
+    scene_manager_next_scene(app->scene_manager, UART_TerminalSceneStart);
+
+    return app;
+}
+
+void uart_terminal_app_free(UART_TerminalApp* app) {
+    furi_assert(app);
+
+    // Views
+    view_dispatcher_remove_view(app->view_dispatcher, UART_TerminalAppViewVarItemList);
+    view_dispatcher_remove_view(app->view_dispatcher, UART_TerminalAppViewConsoleOutput);
+    view_dispatcher_remove_view(app->view_dispatcher, UART_TerminalAppViewTextInput);
+    text_box_free(app->text_box);
+    furi_string_free(app->text_box_store);
+    uart_text_input_free(app->text_input);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    uart_terminal_uart_free(app->uart);
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+
+    free(app);
+}
+
+int32_t uart_terminal_app(void* p) {
+    UNUSED(p);
+    UART_TerminalApp* uart_terminal_app = uart_terminal_app_alloc();
+
+    uart_terminal_app->uart = uart_terminal_uart_init(uart_terminal_app);
+
+    view_dispatcher_run(uart_terminal_app->view_dispatcher);
+
+    uart_terminal_app_free(uart_terminal_app);
+
+    return 0;
+}

+ 11 - 0
non_catalog_apps/esp32_gravity/uart_terminal_app.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct UART_TerminalApp UART_TerminalApp;
+
+#ifdef __cplusplus
+}
+#endif

+ 55 - 0
non_catalog_apps/esp32_gravity/uart_terminal_app_i.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include "uart_terminal_app.h"
+#include "scenes/uart_terminal_scene.h"
+#include "uart_terminal_custom_event.h"
+#include "uart_terminal_uart.h"
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/variable_item_list.h>
+#include "uart_text_input.h"
+
+#define NUM_MENU_ITEMS (17)
+
+#define UART_TERMINAL_TEXT_BOX_STORE_SIZE (8192)
+#define UART_TERMINAL_TEXT_INPUT_STORE_SIZE (512)
+#define UART_CH (FuriHalUartIdUSART1)
+
+/* GRAVITY: Import GravityMode etc. */
+#include "esp_flip_struct.h"
+
+struct UART_TerminalApp {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+
+    char text_input_store[UART_TERMINAL_TEXT_INPUT_STORE_SIZE + 1];
+    FuriString* text_box_store;
+    size_t text_box_store_strlen;
+    TextBox* text_box;
+    UART_TextInput* text_input;
+
+    VariableItemList* var_item_list;
+
+    UART_TerminalUart* uart;
+    int selected_menu_index;
+    int selected_option_index[NUM_MENU_ITEMS];
+    const char* selected_tx_string;
+    bool is_command;
+    bool is_custom_tx_string;
+    bool focus_console_start;
+    bool show_stopscan_tip;
+    int BAUDRATE;
+    int TERMINAL_MODE; //1=AT mode, 0=other mode
+
+    GravityCommand gravityCommand; /* Gravity command */
+};
+
+typedef enum {
+    UART_TerminalAppViewVarItemList,
+    UART_TerminalAppViewConsoleOutput,
+    UART_TerminalAppViewTextInput,
+} UART_TerminalAppView;

+ 7 - 0
non_catalog_apps/esp32_gravity/uart_terminal_custom_event.h

@@ -0,0 +1,7 @@
+#pragma once
+
+typedef enum {
+    UART_TerminalEventRefreshConsoleOutput = 0,
+    UART_TerminalEventStartConsole,
+    UART_TerminalEventStartKeyboard,
+} UART_TerminalCustomEvent;

+ 97 - 0
non_catalog_apps/esp32_gravity/uart_terminal_uart.c

@@ -0,0 +1,97 @@
+#include "uart_terminal_app_i.h"
+#include "uart_terminal_uart.h"
+
+//#define UART_CH (FuriHalUartIdUSART1)
+//#define BAUDRATE (115200)
+
+struct UART_TerminalUart {
+    UART_TerminalApp* app;
+    FuriThread* rx_thread;
+    FuriStreamBuffer* rx_stream;
+    uint8_t rx_buf[RX_BUF_SIZE + 1];
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context);
+};
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+void uart_terminal_uart_set_handle_rx_data_cb(
+    UART_TerminalUart* uart,
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context)) {
+    furi_assert(uart);
+    uart->handle_rx_data_cb = handle_rx_data_cb;
+}
+
+#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone)
+
+void uart_terminal_uart_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) {
+    UART_TerminalUart* uart = (UART_TerminalUart*)context;
+
+    if(ev == UartIrqEventRXNE) {
+        furi_stream_buffer_send(uart->rx_stream, &data, 1, 0);
+        furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone);
+    }
+}
+
+static int32_t uart_worker(void* context) {
+    UART_TerminalUart* uart = (void*)context;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check((events & FuriFlagError) == 0);
+        if(events & WorkerEvtStop) break;
+        if(events & WorkerEvtRxDone) {
+            size_t len = furi_stream_buffer_receive(uart->rx_stream, uart->rx_buf, RX_BUF_SIZE, 0);
+            if(len > 0) {
+                if(uart->handle_rx_data_cb) uart->handle_rx_data_cb(uart->rx_buf, len, uart->app);
+            }
+        }
+    }
+
+    furi_stream_buffer_free(uart->rx_stream);
+
+    return 0;
+}
+
+void uart_terminal_uart_tx(uint8_t* data, size_t len) {
+    furi_hal_uart_tx(UART_CH, data, len);
+}
+
+UART_TerminalUart* uart_terminal_uart_init(UART_TerminalApp* app) {
+    UART_TerminalUart* uart = malloc(sizeof(UART_TerminalUart));
+    uart->app = app;
+    // Init all rx stream and thread early to avoid crashes
+    uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
+    uart->rx_thread = furi_thread_alloc();
+    furi_thread_set_name(uart->rx_thread, "UART_TerminalUartRxThread");
+    furi_thread_set_stack_size(uart->rx_thread, 1024);
+    furi_thread_set_context(uart->rx_thread, uart);
+    furi_thread_set_callback(uart->rx_thread, uart_worker);
+
+    furi_thread_start(uart->rx_thread);
+
+    furi_hal_console_disable();
+    if(app->BAUDRATE == 0) {
+        app->BAUDRATE = 115200;
+    }
+    furi_hal_uart_set_br(UART_CH, app->BAUDRATE);
+    furi_hal_uart_set_irq_cb(UART_CH, uart_terminal_uart_on_irq_cb, uart);
+
+    return uart;
+}
+
+void uart_terminal_uart_free(UART_TerminalUart* uart) {
+    furi_assert(uart);
+
+    furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtStop);
+    furi_thread_join(uart->rx_thread);
+    furi_thread_free(uart->rx_thread);
+
+    furi_hal_uart_set_irq_cb(UART_CH, NULL, NULL);
+    furi_hal_console_enable();
+
+    free(uart);
+}

+ 14 - 0
non_catalog_apps/esp32_gravity/uart_terminal_uart.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "furi_hal.h"
+
+#define RX_BUF_SIZE (320)
+
+typedef struct UART_TerminalUart UART_TerminalUart;
+
+void uart_terminal_uart_set_handle_rx_data_cb(
+    UART_TerminalUart* uart,
+    void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context));
+void uart_terminal_uart_tx(uint8_t* data, size_t len);
+UART_TerminalUart* uart_terminal_uart_init(UART_TerminalApp* app);
+void uart_terminal_uart_free(UART_TerminalUart* uart);

+ 683 - 0
non_catalog_apps/esp32_gravity/uart_text_input.c

@@ -0,0 +1,683 @@
+#include "uart_text_input.h"
+#include <gui/elements.h>
+#include "uart_terminal_icons.h"
+#include "uart_terminal_app_i.h"
+#include <furi.h>
+
+struct UART_TextInput {
+    View* view;
+    FuriTimer* timer;
+};
+
+typedef struct {
+    const char text;
+    const uint8_t x;
+    const uint8_t y;
+} UART_TextInputKey;
+
+typedef struct {
+    const char* header;
+    char* text_buffer;
+    size_t text_buffer_size;
+    bool clear_default_text;
+
+    UART_TextInputCallback callback;
+    void* callback_context;
+
+    uint8_t selected_row;
+    uint8_t selected_column;
+
+    UART_TextInputValidatorCallback validator_callback;
+    void* validator_callback_context;
+    FuriString* validator_text;
+    bool valadator_message_visible;
+} UART_TextInputModel;
+
+static const uint8_t keyboard_origin_x = 1;
+static const uint8_t keyboard_origin_y = 29;
+static const uint8_t keyboard_row_count = 4;
+
+#define mode_AT "Send AT command to UART"
+
+#define ENTER_KEY '\r'
+#define BACKSPACE_KEY '\b'
+
+static const UART_TextInputKey keyboard_keys_row_1[] = {
+    {'{', 1, 0},
+    {'(', 9, 0},
+    {'[', 17, 0},
+    {'|', 25, 0},
+    {'@', 33, 0},
+    {'&', 41, 0},
+    {'#', 49, 0},
+    {';', 57, 0},
+    {'^', 65, 0},
+    {'*', 73, 0},
+    {'`', 81, 0},
+    {'"', 89, 0},
+    {'~', 97, 0},
+    {'\'', 105, 0},
+    {'.', 113, 0},
+    {'/', 120, 0},
+};
+
+static const UART_TextInputKey keyboard_keys_row_2[] = {
+    {'q', 1, 10},
+    {'w', 9, 10},
+    {'e', 17, 10},
+    {'r', 25, 10},
+    {'t', 33, 10},
+    {'y', 41, 10},
+    {'u', 49, 10},
+    {'i', 57, 10},
+    {'o', 65, 10},
+    {'p', 73, 10},
+    {'0', 81, 10},
+    {'1', 89, 10},
+    {'2', 97, 10},
+    {'3', 105, 10},
+    {'=', 113, 10},
+    {'-', 120, 10},
+};
+
+static const UART_TextInputKey keyboard_keys_row_3[] = {
+    {'a', 1, 21},
+    {'s', 9, 21},
+    {'d', 18, 21},
+    {'f', 25, 21},
+    {'g', 33, 21},
+    {'h', 41, 21},
+    {'j', 49, 21},
+    {'k', 57, 21},
+    {'l', 65, 21},
+    {BACKSPACE_KEY, 72, 13},
+    {'4', 89, 21},
+    {'5', 97, 21},
+    {'6', 105, 21},
+    {'$', 113, 21},
+    {'%', 120, 21},
+
+};
+
+static const UART_TextInputKey keyboard_keys_row_4[] = {
+    {'z', 1, 33},
+    {'x', 9, 33},
+    {'c', 18, 33},
+    {'v', 25, 33},
+    {'b', 33, 33},
+    {'n', 41, 33},
+    {'m', 49, 33},
+    {'_', 57, 33},
+    {ENTER_KEY, 64, 24},
+    {'7', 89, 33},
+    {'8', 97, 33},
+    {'9', 105, 33},
+    {'!', 113, 33},
+    {'+', 120, 33},
+};
+
+static uint8_t get_row_size(uint8_t row_index) {
+    uint8_t row_size = 0;
+
+    switch(row_index + 1) {
+    case 1:
+        row_size = sizeof(keyboard_keys_row_1) / sizeof(UART_TextInputKey);
+        break;
+    case 2:
+        row_size = sizeof(keyboard_keys_row_2) / sizeof(UART_TextInputKey);
+        break;
+    case 3:
+        row_size = sizeof(keyboard_keys_row_3) / sizeof(UART_TextInputKey);
+        break;
+    case 4:
+        row_size = sizeof(keyboard_keys_row_4) / sizeof(UART_TextInputKey);
+        break;
+    }
+
+    return row_size;
+}
+
+static const UART_TextInputKey* get_row(uint8_t row_index) {
+    const UART_TextInputKey* row = NULL;
+
+    switch(row_index + 1) {
+    case 1:
+        row = keyboard_keys_row_1;
+        break;
+    case 2:
+        row = keyboard_keys_row_2;
+        break;
+    case 3:
+        row = keyboard_keys_row_3;
+        break;
+    case 4:
+        row = keyboard_keys_row_4;
+        break;
+    }
+
+    return row;
+}
+
+static char get_selected_char(UART_TextInputModel* model) {
+    return get_row(model->selected_row)[model->selected_column].text;
+}
+
+static bool char_is_lowercase(char letter) {
+    return (letter >= 0x61 && letter <= 0x7A);
+}
+
+static bool char_is_uppercase(char letter) {
+    return (letter >= 0x41 && letter <= 0x5A);
+}
+
+static char char_to_lowercase(const char letter) {
+    switch(letter) {
+    case ' ':
+        return 0x5f;
+        break;
+    case ')':
+        return 0x28;
+        break;
+    case '}':
+        return 0x7b;
+        break;
+    case ']':
+        return 0x5b;
+        break;
+    case '\\':
+        return 0x2f;
+        break;
+    case ':':
+        return 0x3b;
+        break;
+    case ',':
+        return 0x2e;
+        break;
+    case '?':
+        return 0x21;
+        break;
+    case '>':
+        return 0x3c;
+        break;
+    }
+    if(char_is_uppercase(letter)) {
+        return (letter + 0x20);
+    } else {
+        return letter;
+    }
+}
+
+static char char_to_uppercase(const char letter) {
+    switch(letter) {
+    case '_':
+        return 0x20;
+        break;
+    case '(':
+        return 0x29;
+        break;
+    case '{':
+        return 0x7d;
+        break;
+    case '[':
+        return 0x5d;
+        break;
+    case '/':
+        return 0x5c;
+        break;
+    case ';':
+        return 0x3a;
+        break;
+    case '.':
+        return 0x2c;
+        break;
+    case '!':
+        return 0x3f;
+        break;
+    case '<':
+        return 0x3e;
+        break;
+    }
+    if(char_is_lowercase(letter)) {
+        return (letter - 0x20);
+    } else {
+        return letter;
+    }
+}
+
+static void uart_text_input_backspace_cb(UART_TextInputModel* model) {
+    uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
+    if(text_length > 0) {
+        model->text_buffer[text_length - 1] = 0;
+    }
+}
+
+static void uart_text_input_view_draw_callback(Canvas* canvas, void* _model) {
+    UART_TextInputModel* model = _model;
+    //uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
+    uint8_t needed_string_width = canvas_width(canvas) - 8;
+    uint8_t start_pos = 4;
+
+    const char* text = model->text_buffer;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_str(canvas, 2, 7, model->header);
+    elements_slightly_rounded_frame(canvas, 1, 8, 126, 12);
+
+    if(canvas_string_width(canvas, text) > needed_string_width) {
+        canvas_draw_str(canvas, start_pos, 17, "...");
+        start_pos += 6;
+        needed_string_width -= 8;
+    }
+
+    while(text != 0 && canvas_string_width(canvas, text) > needed_string_width) {
+        text++;
+    }
+
+    if(model->clear_default_text) {
+        elements_slightly_rounded_box(
+            canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10);
+        canvas_set_color(canvas, ColorWhite);
+    } else {
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 18, "|");
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 18, "|");
+    }
+    canvas_draw_str(canvas, start_pos, 17, text);
+
+    canvas_set_font(canvas, FontKeyboard);
+
+    for(uint8_t row = 0; row <= keyboard_row_count; row++) {
+        const uint8_t column_count = get_row_size(row);
+        const UART_TextInputKey* keys = get_row(row);
+
+        for(size_t column = 0; column < column_count; column++) {
+            if(keys[column].text == ENTER_KEY) {
+                canvas_set_color(canvas, ColorBlack);
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySaveSelected_24x11);
+                } else {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySave_24x11);
+                }
+            } else if(keys[column].text == BACKSPACE_KEY) {
+                canvas_set_color(canvas, ColorBlack);
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspaceSelected_16x9);
+                } else {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspace_16x9);
+                }
+            } else {
+                if(model->selected_row == row && model->selected_column == column) {
+                    canvas_set_color(canvas, ColorBlack);
+                    canvas_draw_box(
+                        canvas,
+                        keyboard_origin_x + keys[column].x - 1,
+                        keyboard_origin_y + keys[column].y - 8,
+                        7,
+                        10);
+                    canvas_set_color(canvas, ColorWhite);
+                } else {
+                    canvas_set_color(canvas, ColorBlack);
+                }
+                if(0 == strcmp(model->header, mode_AT)) {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        char_to_uppercase(keys[column].text));
+                } else {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        keys[column].text);
+                }
+            }
+        }
+    }
+    if(model->valadator_message_visible) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_set_color(canvas, ColorWhite);
+        canvas_draw_box(canvas, 8, 10, 110, 48);
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42);
+        canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
+        canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
+        elements_multiline_text(canvas, 62, 20, furi_string_get_cstr(model->validator_text));
+        canvas_set_font(canvas, FontKeyboard);
+    }
+}
+
+static void
+    uart_text_input_handle_up(UART_TextInput* uart_text_input, UART_TextInputModel* model) {
+    UNUSED(uart_text_input);
+    if(model->selected_row > 0) {
+        model->selected_row--;
+        if(model->selected_column > get_row_size(model->selected_row) - 6) {
+            model->selected_column = model->selected_column + 1;
+        }
+    }
+}
+
+static void
+    uart_text_input_handle_down(UART_TextInput* uart_text_input, UART_TextInputModel* model) {
+    UNUSED(uart_text_input);
+    if(model->selected_row < keyboard_row_count - 1) {
+        model->selected_row++;
+        if(model->selected_column > get_row_size(model->selected_row) - 4) {
+            model->selected_column = model->selected_column - 1;
+        }
+    }
+}
+
+static void
+    uart_text_input_handle_left(UART_TextInput* uart_text_input, UART_TextInputModel* model) {
+    UNUSED(uart_text_input);
+    if(model->selected_column > 0) {
+        model->selected_column--;
+    } else {
+        model->selected_column = get_row_size(model->selected_row) - 1;
+    }
+}
+
+static void
+    uart_text_input_handle_right(UART_TextInput* uart_text_input, UART_TextInputModel* model) {
+    UNUSED(uart_text_input);
+    if(model->selected_column < get_row_size(model->selected_row) - 1) {
+        model->selected_column++;
+    } else {
+        model->selected_column = 0;
+    }
+}
+
+static void uart_text_input_handle_ok(
+    UART_TextInput* uart_text_input,
+    UART_TextInputModel* model,
+    bool shift) {
+    char selected = get_selected_char(model);
+    uint8_t text_length = strlen(model->text_buffer);
+
+    if(0 == strcmp(model->header, mode_AT)) {
+        selected = char_to_uppercase(selected);
+    }
+
+    if(shift) {
+        if(0 == strcmp(model->header, mode_AT)) {
+            selected = char_to_lowercase(selected);
+        } else {
+            selected = char_to_uppercase(selected);
+        }
+    }
+
+    if(selected == ENTER_KEY) {
+        if(model->validator_callback &&
+           (!model->validator_callback(
+               model->text_buffer, model->validator_text, model->validator_callback_context))) {
+            model->valadator_message_visible = true;
+            furi_timer_start(uart_text_input->timer, furi_kernel_get_tick_frequency() * 4);
+        } else if(model->callback != 0 && text_length > 0) {
+            model->callback(model->callback_context);
+        }
+    } else if(selected == BACKSPACE_KEY) {
+        uart_text_input_backspace_cb(model);
+    } else {
+        if(model->clear_default_text) {
+            text_length = 0;
+        }
+        if(text_length < (model->text_buffer_size - 1)) {
+            model->text_buffer[text_length] = selected;
+            model->text_buffer[text_length + 1] = 0;
+        }
+    }
+    model->clear_default_text = false;
+}
+
+static bool uart_text_input_view_input_callback(InputEvent* event, void* context) {
+    UART_TextInput* uart_text_input = context;
+    furi_assert(uart_text_input);
+
+    bool consumed = false;
+
+    // Acquire model
+    UART_TextInputModel* model = view_get_model(uart_text_input->view);
+
+    if((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) &&
+       model->valadator_message_visible) {
+        model->valadator_message_visible = false;
+        consumed = true;
+    } else if(event->type == InputTypeShort) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyOk:
+            uart_text_input_handle_ok(uart_text_input, model, false);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    } else if(event->type == InputTypeLong) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyOk:
+            uart_text_input_handle_ok(uart_text_input, model, true);
+            break;
+        case InputKeyBack:
+            uart_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    } else if(event->type == InputTypeRepeat) {
+        consumed = true;
+        switch(event->key) {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyBack:
+            uart_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+
+    // Commit model
+    view_commit_model(uart_text_input->view, consumed);
+
+    return consumed;
+}
+
+void uart_text_input_timer_callback(void* context) {
+    furi_assert(context);
+    UART_TextInput* uart_text_input = context;
+
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { model->valadator_message_visible = false; },
+        true);
+}
+
+UART_TextInput* uart_text_input_alloc() {
+    UART_TextInput* uart_text_input = malloc(sizeof(UART_TextInput));
+    uart_text_input->view = view_alloc();
+    view_set_context(uart_text_input->view, uart_text_input);
+    view_allocate_model(uart_text_input->view, ViewModelTypeLocking, sizeof(UART_TextInputModel));
+    view_set_draw_callback(uart_text_input->view, uart_text_input_view_draw_callback);
+    view_set_input_callback(uart_text_input->view, uart_text_input_view_input_callback);
+
+    uart_text_input->timer =
+        furi_timer_alloc(uart_text_input_timer_callback, FuriTimerTypeOnce, uart_text_input);
+
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { model->validator_text = furi_string_alloc(); },
+        false);
+
+    uart_text_input_reset(uart_text_input);
+
+    return uart_text_input;
+}
+
+void uart_text_input_free(UART_TextInput* uart_text_input) {
+    furi_assert(uart_text_input);
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { furi_string_free(model->validator_text); },
+        false);
+
+    // Send stop command
+    furi_timer_stop(uart_text_input->timer);
+    // Release allocated memory
+    furi_timer_free(uart_text_input->timer);
+
+    view_free(uart_text_input->view);
+
+    free(uart_text_input);
+}
+
+void uart_text_input_reset(UART_TextInput* uart_text_input) {
+    furi_assert(uart_text_input);
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->text_buffer_size = 0;
+            model->header = "";
+            model->selected_row = 0;
+            model->selected_column = 0;
+            model->clear_default_text = false;
+            model->text_buffer = NULL;
+            model->text_buffer_size = 0;
+            model->callback = NULL;
+            model->callback_context = NULL;
+            model->validator_callback = NULL;
+            model->validator_callback_context = NULL;
+            furi_string_reset(model->validator_text);
+            model->valadator_message_visible = false;
+        },
+        true);
+}
+
+View* uart_text_input_get_view(UART_TextInput* uart_text_input) {
+    furi_assert(uart_text_input);
+    return uart_text_input->view;
+}
+
+void uart_text_input_set_result_callback(
+    UART_TextInput* uart_text_input,
+    UART_TextInputCallback callback,
+    void* callback_context,
+    char* text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text) {
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->callback = callback;
+            model->callback_context = callback_context;
+            model->text_buffer = text_buffer;
+            model->text_buffer_size = text_buffer_size;
+            model->clear_default_text = clear_default_text;
+            if(text_buffer && text_buffer[0] != '\0') {
+                // Set focus on Save
+                model->selected_row = 2;
+                model->selected_column = 8;
+            }
+        },
+        true);
+}
+
+void uart_text_input_set_validator(
+    UART_TextInput* uart_text_input,
+    UART_TextInputValidatorCallback callback,
+    void* callback_context) {
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->validator_callback = callback;
+            model->validator_callback_context = callback_context;
+        },
+        true);
+}
+
+UART_TextInputValidatorCallback
+    uart_text_input_get_validator_callback(UART_TextInput* uart_text_input) {
+    UART_TextInputValidatorCallback validator_callback = NULL;
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { validator_callback = model->validator_callback; },
+        false);
+    return validator_callback;
+}
+
+void* uart_text_input_get_validator_callback_context(UART_TextInput* uart_text_input) {
+    void* validator_callback_context = NULL;
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { validator_callback_context = model->validator_callback_context; },
+        false);
+    return validator_callback_context;
+}
+
+void uart_text_input_set_header_text(UART_TextInput* uart_text_input, const char* text) {
+    with_view_model(
+        uart_text_input->view, UART_TextInputModel * model, { model->header = text; }, true);
+}

+ 82 - 0
non_catalog_apps/esp32_gravity/uart_text_input.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include <gui/view.h>
+#include "uart_validators.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Text input anonymous structure */
+typedef struct UART_TextInput UART_TextInput;
+typedef void (*UART_TextInputCallback)(void* context);
+typedef bool (*UART_TextInputValidatorCallback)(const char* text, FuriString* error, void* context);
+
+/** Allocate and initialize text input 
+ * 
+ * This text input is used to enter string
+ *
+ * @return     UART_TextInput instance
+ */
+UART_TextInput* uart_text_input_alloc();
+
+/** Deinitialize and free text input
+ *
+ * @param      uart_text_input  UART_TextInput instance
+ */
+void uart_text_input_free(UART_TextInput* uart_text_input);
+
+/** Clean text input view Note: this function does not free memory
+ *
+ * @param      uart_text_input  Text input instance
+ */
+void uart_text_input_reset(UART_TextInput* uart_text_input);
+
+/** Get text input view
+ *
+ * @param      uart_text_input  UART_TextInput instance
+ *
+ * @return     View instance that can be used for embedding
+ */
+View* uart_text_input_get_view(UART_TextInput* uart_text_input);
+
+/** Set text input result callback
+ *
+ * @param      uart_text_input          UART_TextInput instance
+ * @param      callback            callback fn
+ * @param      callback_context    callback context
+ * @param      text_buffer         pointer to YOUR text buffer, that we going
+ *                                 to modify
+ * @param      text_buffer_size    YOUR text buffer size in bytes. Max string
+ *                                 length will be text_buffer_size-1.
+ * @param      clear_default_text  clear text from text_buffer on first OK
+ *                                 event
+ */
+void uart_text_input_set_result_callback(
+    UART_TextInput* uart_text_input,
+    UART_TextInputCallback callback,
+    void* callback_context,
+    char* text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text);
+
+void uart_text_input_set_validator(
+    UART_TextInput* uart_text_input,
+    UART_TextInputValidatorCallback callback,
+    void* callback_context);
+
+UART_TextInputValidatorCallback
+    uart_text_input_get_validator_callback(UART_TextInput* uart_text_input);
+
+void* uart_text_input_get_validator_callback_context(UART_TextInput* uart_text_input);
+
+/** Set text input header text
+ *
+ * @param      uart_text_input  UART_TextInput instance
+ * @param      text        text to be shown
+ */
+void uart_text_input_set_header_text(UART_TextInput* uart_text_input, const char* text);
+
+#ifdef __cplusplus
+}
+#endif

+ 57 - 0
non_catalog_apps/esp32_gravity/uart_validators.c

@@ -0,0 +1,57 @@
+#include <furi.h>
+#include "uart_validators.h"
+#include <storage/storage.h>
+
+struct ValidatorIsFile {
+    char* app_path_folder;
+    const char* app_extension;
+    char* current_name;
+};
+
+bool validator_is_file_callback(const char* text, FuriString* error, void* context) {
+    furi_assert(context);
+    ValidatorIsFile* instance = context;
+
+    if(instance->current_name != NULL) {
+        if(strcmp(instance->current_name, text) == 0) {
+            return true;
+        }
+    }
+
+    bool ret = true;
+    FuriString* path = furi_string_alloc_printf(
+        "%s/%s%s", instance->app_path_folder, text, instance->app_extension);
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    if(storage_common_stat(storage, furi_string_get_cstr(path), NULL) == FSE_OK) {
+        ret = false;
+        furi_string_printf(error, "This name\nexists!\nChoose\nanother one.");
+    } else {
+        ret = true;
+    }
+    furi_string_free(path);
+    furi_record_close(RECORD_STORAGE);
+
+    return ret;
+}
+
+ValidatorIsFile* validator_is_file_alloc_init(
+    const char* app_path_folder,
+    const char* app_extension,
+    const char* current_name) {
+    ValidatorIsFile* instance = malloc(sizeof(ValidatorIsFile));
+
+    instance->app_path_folder = strdup(app_path_folder);
+    instance->app_extension = app_extension;
+    if(current_name != NULL) {
+        instance->current_name = strdup(current_name);
+    }
+
+    return instance;
+}
+
+void validator_is_file_free(ValidatorIsFile* instance) {
+    furi_assert(instance);
+    free(instance->app_path_folder);
+    free(instance->current_name);
+    free(instance);
+}

+ 21 - 0
non_catalog_apps/esp32_gravity/uart_validators.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include <core/common_defines.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+typedef struct ValidatorIsFile ValidatorIsFile;
+
+ValidatorIsFile* validator_is_file_alloc_init(
+    const char* app_path_folder,
+    const char* app_extension,
+    const char* current_name);
+
+void validator_is_file_free(ValidatorIsFile* instance);
+
+bool validator_is_file_callback(const char* text, FuriString* error, void* context);
+
+#ifdef __cplusplus
+}
+#endif

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