فهرست منبع

Merge remote-tracking branch 'origin/dev' into feature_wifi_marauder_app

0xchocolate 3 سال پیش
والد
کامیت
f07fa8f76f
100فایلهای تغییر یافته به همراه2504 افزوده شده و 290 حذف شده
  1. 3 0
      .github/CODEOWNERS
  2. 3 2
      .github/workflows/amap_analyse.yml
  3. 6 12
      .github/workflows/build.yml
  4. 3 3
      .github/workflows/check_submodules.yml
  5. 4 2
      .github/workflows/lint_c.yml
  6. 6 2
      .github/workflows/lint_python.yml
  7. 45 0
      .github/workflows/merge_report.yml
  8. 5 12
      .github/workflows/pvs_studio.yml
  9. 70 17
      .github/workflows/unit_tests.yml
  10. 33 1
      .vscode/example/tasks.json
  11. 1 1
      applications/debug/file_browser_test/file_browser_app.c
  12. 11 0
      applications/debug/locale_test/application.fam
  13. 102 0
      applications/debug/locale_test/locale_test.c
  14. 5 5
      applications/debug/uart_echo/uart_echo.c
  15. 110 0
      applications/debug/unit_tests/bt/bt_test.c
  16. 116 0
      applications/debug/unit_tests/furi_hal/furi_hal_tests.c
  17. 1 1
      applications/debug/unit_tests/minunit.h
  18. 6 3
      applications/debug/unit_tests/nfc/nfc_test.c
  19. 62 0
      applications/debug/unit_tests/power/power_test.c
  20. 16 1
      applications/debug/unit_tests/subghz/subghz_test.c
  21. 6 0
      applications/debug/unit_tests/test_index.c
  22. 9 4
      applications/main/archive/helpers/archive_browser.c
  23. 2 0
      applications/main/archive/scenes/archive_scene_browser.c
  24. 48 4
      applications/main/archive/views/archive_browser_view.c
  25. 2 0
      applications/main/archive/views/archive_browser_view.h
  26. 2 0
      applications/main/bad_usb/scenes/bad_usb_scene_file_select.c
  27. 1 0
      applications/main/fap_loader/fap_loader_app.c
  28. 1 0
      applications/main/ibutton/ibutton.c
  29. 0 1
      applications/main/ibutton/scenes/ibutton_scene_read.c
  30. 2 0
      applications/main/ibutton/scenes/ibutton_scene_read_success.c
  31. 1 0
      applications/main/infrared/scenes/infrared_scene_remote_list.c
  32. 1 0
      applications/main/lfrfid/lfrfid.c
  33. 1 0
      applications/main/nfc/nfc.c
  34. 2 3
      applications/main/nfc/nfc_i.h
  35. 3 3
      applications/main/nfc/scenes/nfc_scene_emulate_uid.c
  36. 7 2
      applications/main/nfc/scenes/nfc_scene_generate_info.c
  37. 5 4
      applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c
  38. 10 4
      applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c
  39. 14 12
      applications/main/nfc/scenes/nfc_scene_nfc_data_info.c
  40. 1 1
      applications/main/nfc/scenes/nfc_scene_rpc.c
  41. 1 1
      applications/main/nfc/scenes/nfc_scene_set_type.c
  42. 13 0
      applications/main/subghz/helpers/subghz_error_type.h
  43. 7 0
      applications/main/subghz/helpers/subghz_types.h
  44. 3 0
      applications/main/subghz/scenes/subghz_scene_read_raw.c
  45. 29 0
      applications/main/subghz/scenes/subghz_scene_receiver_config.c
  46. 9 0
      applications/main/subghz/scenes/subghz_scene_rpc.c
  47. 1 0
      applications/main/subghz/subghz.c
  48. 43 0
      applications/main/subghz/subghz_i.c
  49. 6 0
      applications/main/subghz/subghz_i.h
  50. 4 3
      applications/main/subghz/views/receiver.c
  51. 0 7
      applications/main/subghz/views/subghz_frequency_analyzer.c
  52. 10 0
      applications/plugins/clock/application.fam
  53. BIN
      applications/plugins/clock/clock.png
  54. 136 0
      applications/plugins/clock/clock_app.c
  55. 45 6
      applications/plugins/hid_app/hid.c
  56. 5 0
      applications/plugins/hid_app/hid.h
  57. 1 0
      applications/plugins/hid_app/views.h
  58. 149 0
      applications/plugins/hid_app/views/hid_mouse_jiggler.c
  59. 17 0
      applications/plugins/hid_app/views/hid_mouse_jiggler.h
  60. 1 0
      applications/plugins/music_player/music_player.c
  61. 42 38
      applications/plugins/music_player/music_player_worker.c
  62. 1 0
      applications/plugins/nfc_magic/nfc_magic.c
  63. 2 0
      applications/plugins/nfc_magic/nfc_magic_i.h
  64. 1 0
      applications/plugins/picopass/picopass_device.c
  65. 1 1
      applications/plugins/weather_station/helpers/weather_station_types.h
  66. BIN
      applications/plugins/weather_station/images/Humid_8x13.png
  67. BIN
      applications/plugins/weather_station/images/Timer_11x11.png
  68. 2 2
      applications/plugins/weather_station/protocols/ambient_weather.c
  69. 2 2
      applications/plugins/weather_station/protocols/infactory.c
  70. 4 0
      applications/plugins/weather_station/protocols/nexus_th.c
  71. 28 9
      applications/plugins/weather_station/protocols/oregon2.c
  72. 331 0
      applications/plugins/weather_station/protocols/oregon_v1.c
  73. 79 0
      applications/plugins/weather_station/protocols/oregon_v1.h
  74. 2 0
      applications/plugins/weather_station/protocols/protocol_items.c
  75. 2 0
      applications/plugins/weather_station/protocols/protocol_items.h
  76. 293 0
      applications/plugins/weather_station/protocols/tx_8300.c
  77. 79 0
      applications/plugins/weather_station/protocols/tx_8300.h
  78. 17 4
      applications/plugins/weather_station/protocols/ws_generic.c
  79. 2 2
      applications/plugins/weather_station/protocols/ws_generic.h
  80. 3 3
      applications/plugins/weather_station/views/weather_station_receiver.c
  81. 101 15
      applications/plugins/weather_station/views/weather_station_receiver_info.c
  82. 16 6
      applications/services/bt/bt_service/bt.c
  83. 13 0
      applications/services/bt/bt_service/bt.h
  84. 15 0
      applications/services/bt/bt_service/bt_api.c
  85. 13 0
      applications/services/bt/bt_service/bt_i.h
  86. 124 35
      applications/services/bt/bt_service/bt_keys_storage.c
  87. 15 5
      applications/services/bt/bt_service/bt_keys_storage.h
  88. 32 39
      applications/services/cli/cli_vcp.c
  89. 10 1
      applications/services/desktop/animations/animation_manager.c
  90. 8 0
      applications/services/desktop/animations/animation_manager.h
  91. 3 0
      applications/services/desktop/desktop.c
  92. 27 6
      applications/services/desktop/scenes/desktop_scene_main.c
  93. 3 1
      applications/services/desktop/views/desktop_events.h
  94. 3 3
      applications/services/desktop/views/desktop_view_main.c
  95. 1 0
      applications/services/dialogs/dialogs.c
  96. 4 0
      applications/services/dialogs/dialogs.h
  97. 2 0
      applications/services/dialogs/dialogs_api.c
  98. 2 0
      applications/services/dialogs/dialogs_message.h
  99. 7 1
      applications/services/dialogs/dialogs_module_file_browser.c
  100. 8 0
      applications/services/dialogs/view_holder.c

+ 3 - 0
.github/CODEOWNERS

@@ -42,6 +42,9 @@
 
 /applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @nminaylov @gornekich @Astrrra @gsurkov @Skorpionm
 
+# Assets
+/assets/resources/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
+
 # Documentation
 /documentation/ @skotopes @DrZlo13 @hedger @drunkbatya
 /scripts/toolchain/ @skotopes @DrZlo13 @hedger @drunkbatya

+ 3 - 2
.github/workflows/amap_analyse.yml

@@ -11,6 +11,7 @@ on:
 
 env:
   TARGETS: f7
+  FBT_TOOLCHAIN_PATH: /opt
 
 jobs:
   amap_analyse:
@@ -39,7 +40,7 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
@@ -78,7 +79,7 @@ jobs:
 
       - name: 'Upload report to DB'
         run: |
-          FBT_TOOLCHAIN_PATH=/opt source scripts/toolchain/fbtenv.sh
+          source scripts/toolchain/fbtenv.sh
           get_size()
           {
             SECTION="$1";

+ 6 - 12
.github/workflows/build.yml

@@ -12,6 +12,7 @@ on:
 env:
   TARGETS: f7
   DEFAULT_TARGET: f7
+  FBT_TOOLCHAIN_PATH: /runner/_work
 
 jobs:
   main:
@@ -24,7 +25,7 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
@@ -35,6 +36,7 @@ jobs:
           mkdir artifacts
 
       - name: 'Get commit details'
+        id: names
         run: |
           if [[ ${{ github.event_name }} == 'pull_request' ]]; then
             TYPE="pull"
@@ -45,14 +47,6 @@ jobs:
           fi
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 
-      - name: 'Generate suffixes for comment'
-        id: names
-        run: |
-          echo "::set-output name=branch_name::${BRANCH_NAME}"
-          echo "::set-output name=commit_sha::${COMMIT_SHA}"
-          echo "::set-output name=default_target::${DEFAULT_TARGET}"
-          echo "::set-output name=suffix::${SUFFIX}"
-
       - name: 'Bundle scripts'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         run: |
@@ -62,7 +56,7 @@ jobs:
         run: |
           set -e
           for TARGET in ${TARGETS}; do
-            FBT_TOOLCHAIN_PATH=/runner/_work ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
+                ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
                 copro_dist updater_package ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
           done
 
@@ -143,7 +137,7 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           submodules: true
@@ -164,6 +158,6 @@ jobs:
         run: |
           set -e
           for TARGET in ${TARGETS}; do
-            FBT_TOOLCHAIN_PATH=/runner/_work ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
+                ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
                 updater_package DEBUG=0 COMPACT=1
           done

+ 3 - 3
.github/workflows/check_submodules.yml

@@ -20,7 +20,7 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
@@ -36,12 +36,12 @@ jobs:
           BRANCHES=$(git branch -r --contains "$SUBMODULE_HASH");
           COMMITS_IN_BRANCH="$(git rev-list --count dev)";
           if [ $COMMITS_IN_BRANCH -lt $SUB_COMMITS_MIN ]; then
-            echo "::set-output name=fails::error";
+            echo "name=fails::error" >> $GITHUB_OUTPUT
             echo "::error::Error: Too low commits in $SUB_BRANCH of submodule $SUB_PATH: $COMMITS_IN_BRANCH(expected $SUB_COMMITS_MIN+)";
             exit 1;
           fi
           if ! grep -q "/$SUB_BRANCH" <<< "$BRANCHES"; then
-            echo "::set-output name=fails::error";
+            echo "name=fails::error" >> $GITHUB_OUTPUT
             echo "::error::Error: Submodule $SUB_PATH is not on branch $SUB_BRANCH";
             exit 1;
           fi

+ 4 - 2
.github/workflows/lint_c.yml

@@ -11,6 +11,8 @@ on:
 
 env:
   TARGETS: f7
+  FBT_TOOLCHAIN_PATH: /runner/_work
+  SET_GH_OUTPUT: 1
 
 jobs:
   lint_c_cpp:
@@ -23,14 +25,14 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
 
       - name: 'Check code formatting'
         id: syntax_check
-        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/runner/_work ./fbt lint
+        run: ./fbt lint
 
       - name: Report code formatting errors
         if: failure() && steps.syntax_check.outputs.errors && github.event.pull_request

+ 6 - 2
.github/workflows/lint_python.yml

@@ -9,6 +9,10 @@ on:
       - '*'
   pull_request:
 
+env:
+    FBT_TOOLCHAIN_PATH: /runner/_work
+    SET_GH_OUTPUT: 1
+
 jobs:
   lint_python:
     runs-on: [self-hosted,FlipperZeroShell]
@@ -20,10 +24,10 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
 
       - name: 'Check code formatting'
-        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/runner/_work ./fbt lint_py
+        run: ./fbt lint_py

+ 45 - 0
.github/workflows/merge_report.yml

@@ -0,0 +1,45 @@
+name: 'Check FL ticket in PR name'
+
+on:
+  push:
+    branches:
+      - dev
+
+env:
+  FBT_TOOLCHAIN_PATH: /runner/_work
+
+jobs:
+  merge_report:
+    runs-on: [self-hosted,FlipperZeroShell]
+    steps:
+      - name: 'Decontaminate previous build leftovers'
+        run: |
+          if [ -d .git ]; then
+            git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)"
+          fi
+
+      - name: 'Checkout code'
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+          ref: ${{ github.event.pull_request.head.sha }}
+
+      - name: 'Get commit details'
+        run: |
+          if [[ ${{ github.event_name }} == 'pull_request' ]]; then
+            TYPE="pull"
+          elif [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
+            TYPE="tag"
+          else
+            TYPE="other"
+          fi
+          python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
+
+      - name: 'Check ticket and report'
+        run: |
+          source scripts/toolchain/fbtenv.sh
+          python3 -m pip install slack_sdk
+          python3 scripts/merge_report_qa.py \
+              ${{ secrets.QA_REPORT_SLACK_TOKEN }} \
+              ${{ secrets.QA_REPORT_SLACK_CHANNEL }}
+

+ 5 - 12
.github/workflows/pvs_studio.yml

@@ -12,6 +12,7 @@ on:
 env:
   TARGETS: f7
   DEFAULT_TARGET: f7
+  FBT_TOOLCHAIN_PATH: /runner/_work
 
 jobs:
   analyse_c_cpp:
@@ -25,12 +26,13 @@ jobs:
           fi
 
       - name: 'Checkout code'
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
 
       - name: 'Get commit details'
+        id: names
         run: |
           if [[ ${{ github.event_name }} == 'pull_request' ]]; then
             TYPE="pull"
@@ -41,15 +43,6 @@ jobs:
           fi
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 
-      - name: 'Generate suffixes for comment'
-        if: ${{ !github.event.pull_request.head.repo.fork && github.event.pull_request }}
-        id: names
-        run: |
-          echo "::set-output name=branch_name::${BRANCH_NAME}"
-          echo "::set-output name=commit_sha::${COMMIT_SHA}"
-          echo "::set-output name=default_target::${DEFAULT_TARGET}"
-          echo "::set-output name=suffix::${SUFFIX}"
-
       - name: 'Make reports directory'
         run: |
           rm -rf reports/
@@ -57,11 +50,11 @@ jobs:
 
       - name: 'Generate compile_comands.json'
         run: |
-          FBT_TOOLCHAIN_PATH=/runner/_work ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking _fap_icons
+          ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking _fap_icons
 
       - name: 'Static code analysis'
         run: |
-          FBT_TOOLCHAIN_PATH=/runner/_work source scripts/toolchain/fbtenv.sh
+          source scripts/toolchain/fbtenv.sh
           pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
           pvs-studio-analyzer analyze \
               @.pvsoptions \

+ 70 - 17
.github/workflows/unit_tests.yml

@@ -6,13 +6,20 @@ on:
 env:
   TARGETS: f7
   DEFAULT_TARGET: f7
+  FBT_TOOLCHAIN_PATH: /opt
 
 jobs:
   run_units_on_test_bench:
     runs-on: [self-hosted, FlipperZeroTest]
     steps:
+      - name: 'Decontaminate previous build leftovers'
+        run: |
+          if [ -d .git ]; then
+            git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)"
+          fi
+
       - name: Checkout code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
@@ -22,35 +29,81 @@ jobs:
         run: |
           echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT
 
+      - name: 'Flashing target firmware'
+        id: first_full_flash
+        run: |
+          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+
+      - name: 'Validating updater'
+        id: second_full_flash
+        if: success()
+        run: |
+          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+
       - name: 'Flash unit tests firmware'
         id: flashing
+        if: success()
         run: |
-          FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
+          ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
 
       - name: 'Wait for flipper to finish updating'
         id: connect
         if: steps.flashing.outcome == 'success'
         run: |
-          . scripts/toolchain/fbtenv.sh
-          ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
-
-      - name: 'Format flipper SD card'
-        id: format
-        if: steps.connect.outcome == 'success'
-        run: |
-          . scripts/toolchain/fbtenv.sh
-          ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 
       - name: 'Copy assets and unit tests data to flipper'
         id: copy
-        if: steps.format.outcome == 'success'
+        if: steps.connect.outcome == 'success'
         run: |
-          . scripts/toolchain/fbtenv.sh
-          ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/resources /ext
-          ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} -f send assets/unit_tests /ext/unit_tests
 
       - name: 'Run units and validate results'
         if: steps.copy.outcome == 'success'
         run: |
-          . scripts/toolchain/fbtenv.sh
-          ./scripts/testing/units.py ${{steps.device.outputs.flipper}}
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/testing/units.py ${{steps.device.outputs.flipper}}
+
+      - name: 'Get last release tag'
+        id: release_tag
+        if: always()
+        run: |
+          echo "tag=$(git tag -l --sort=-version:refname | grep -v "rc\|RC" | head -1)" >> $GITHUB_OUTPUT
+
+      - name: 'Decontaminate previous build leftovers'
+        if: always()
+        run: |
+          if [ -d .git ]; then
+            git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)"
+          fi
+
+      - name: 'Checkout latest release'
+        uses: actions/checkout@v3
+        if: always()
+        with:
+          fetch-depth: 0
+          ref: ${{ steps.release_tag.outputs.tag }}
+
+      - name: 'Flash last release'
+        if: always()
+        run: |
+          ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
+
+      - name: 'Wait for flipper to finish updating'
+        if: always()
+        run: |
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+
+      - name: 'Format flipper SD card'
+        id: format
+        if: always()
+        run: |
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext

+ 33 - 1
.vscode/example/tasks.json

@@ -128,6 +128,38 @@
             "group": "build",
             "type": "shell",
             "command": "./fbt COMPACT=1 DEBUG=0 launch_app APPSRC=${relativeFileDirname}"
+        },
+        {
+            "label": "[Debug] Launch App on Flipper with Serial Console",
+            "dependsOrder": "sequence",
+            "group": "build",
+            "dependsOn": [
+                "[Debug] Launch App on Flipper",
+                "Serial Console"
+            ]
+        },
+        {
+            // Press Ctrl+] to quit
+            "label": "Serial Console",
+            "type": "shell",
+            "command": "./fbt cli",
+            "group": "none",
+            "isBackground": true,
+			"options": {
+                "env": {
+                    "FBT_NO_SYNC": "0"
+                }
+            },
+            "presentation": {
+                "reveal": "always",
+                "revealProblems": "never",
+                "showReuseMessage": false,
+                "panel": "dedicated",
+                "focus": true,
+                "echo": true,
+                "close": true,
+                "group": "Logger"
+            }
         }
     ]
-}
+}

+ 1 - 1
applications/debug/file_browser_test/file_browser_app.c

@@ -48,7 +48,7 @@ FileBrowserApp* file_browser_app_alloc(char* arg) {
 
     app->file_path = furi_string_alloc();
     app->file_browser = file_browser_alloc(app->file_path);
-    file_browser_configure(app->file_browser, "*", true, &I_badusb_10px, true);
+    file_browser_configure(app->file_browser, "*", NULL, true, &I_badusb_10px, true);
 
     view_dispatcher_add_view(
         app->view_dispatcher, FileBrowserAppViewStart, widget_get_view(app->widget));

+ 11 - 0
applications/debug/locale_test/application.fam

@@ -0,0 +1,11 @@
+App(
+    appid="locale_test",
+    name="Locale Test",
+    apptype=FlipperAppType.DEBUG,
+    entry_point="locale_test_app",
+    cdefines=["APP_LOCALE"],
+    requires=["gui", "locale"],
+    stack_size=2 * 1024,
+    order=70,
+    fap_category="Debug",
+)

+ 102 - 0
applications/debug/locale_test/locale_test.c

@@ -0,0 +1,102 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/dialog_ex.h>
+#include <locale/locale.h>
+
+typedef struct {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    View* view;
+} LocaleTestApp;
+
+static void locale_test_view_draw_callback(Canvas* canvas, void* _model) {
+    UNUSED(_model);
+
+    // Prepare canvas
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontSecondary);
+
+    FuriString* tmp_string = furi_string_alloc();
+
+    float temp = 25.3f;
+    LocaleMeasurementUnits units = locale_get_measurement_unit();
+    if(units == LocaleMeasurementUnitsMetric) {
+        furi_string_printf(tmp_string, "Temp: %5.1fC", (double)temp);
+    } else {
+        temp = locale_celsius_to_fahrenheit(temp);
+        furi_string_printf(tmp_string, "Temp: %5.1fF", (double)temp);
+    }
+    canvas_draw_str(canvas, 0, 10, furi_string_get_cstr(tmp_string));
+
+    FuriHalRtcDateTime datetime;
+    furi_hal_rtc_get_datetime(&datetime);
+
+    locale_format_time(tmp_string, &datetime, locale_get_time_format(), false);
+    canvas_draw_str(canvas, 0, 25, furi_string_get_cstr(tmp_string));
+
+    locale_format_date(tmp_string, &datetime, locale_get_date_format(), "/");
+    canvas_draw_str(canvas, 0, 40, furi_string_get_cstr(tmp_string));
+
+    furi_string_free(tmp_string);
+}
+
+static bool locale_test_view_input_callback(InputEvent* event, void* context) {
+    UNUSED(event);
+    UNUSED(context);
+    return false;
+}
+
+static uint32_t locale_test_exit(void* context) {
+    UNUSED(context);
+    return VIEW_NONE;
+}
+
+static LocaleTestApp* locale_test_alloc() {
+    LocaleTestApp* app = malloc(sizeof(LocaleTestApp));
+
+    // Gui
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // View dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Views
+    app->view = view_alloc();
+    view_set_draw_callback(app->view, locale_test_view_draw_callback);
+    view_set_input_callback(app->view, locale_test_view_input_callback);
+
+    view_set_previous_callback(app->view, locale_test_exit);
+    view_dispatcher_add_view(app->view_dispatcher, 0, app->view);
+    view_dispatcher_switch_to_view(app->view_dispatcher, 0);
+
+    return app;
+}
+
+static void locale_test_free(LocaleTestApp* app) {
+    furi_assert(app);
+
+    // Free views
+    view_dispatcher_remove_view(app->view_dispatcher, 0);
+
+    view_free(app->view);
+    view_dispatcher_free(app->view_dispatcher);
+
+    // Close gui record
+    furi_record_close(RECORD_GUI);
+    app->gui = NULL;
+
+    // Free rest
+    free(app);
+}
+
+int32_t locale_test_app(void* p) {
+    UNUSED(p);
+    LocaleTestApp* app = locale_test_alloc();
+    view_dispatcher_run(app->view_dispatcher);
+    locale_test_free(app);
+    return 0;
+}

+ 5 - 5
applications/debug/uart_echo/uart_echo.c

@@ -215,26 +215,26 @@ static UartEchoApp* uart_echo_app_alloc() {
     view_dispatcher_add_view(app->view_dispatcher, 0, app->view);
     view_dispatcher_switch_to_view(app->view_dispatcher, 0);
 
+    app->worker_thread = furi_thread_alloc_ex("UsbUartWorker", 1024, uart_echo_worker, app);
+    furi_thread_start(app->worker_thread);
+
     // Enable uart listener
     furi_hal_console_disable();
     furi_hal_uart_set_br(FuriHalUartIdUSART1, 115200);
     furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, uart_echo_on_irq_cb, app);
 
-    app->worker_thread = furi_thread_alloc_ex("UsbUartWorker", 1024, uart_echo_worker, app);
-    furi_thread_start(app->worker_thread);
-
     return app;
 }
 
 static void uart_echo_app_free(UartEchoApp* app) {
     furi_assert(app);
 
+    furi_hal_console_enable(); // this will also clear IRQ callback so thread is no longer referenced
+
     furi_thread_flags_set(furi_thread_get_id(app->worker_thread), WorkerEventStop);
     furi_thread_join(app->worker_thread);
     furi_thread_free(app->worker_thread);
 
-    furi_hal_console_enable();
-
     // Free views
     view_dispatcher_remove_view(app->view_dispatcher, 0);
 

+ 110 - 0
applications/debug/unit_tests/bt/bt_test.c

@@ -0,0 +1,110 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include "../minunit.h"
+
+#include <bt/bt_service/bt_keys_storage.h>
+#include <storage/storage.h>
+
+#define BT_TEST_KEY_STORAGE_FILE_PATH EXT_PATH("unit_tests/bt_test.keys")
+#define BT_TEST_NVM_RAM_BUFF_SIZE (507 * 4) // The same as in ble NVM storage
+
+typedef struct {
+    Storage* storage;
+    BtKeysStorage* bt_keys_storage;
+    uint8_t* nvm_ram_buff_dut;
+    uint8_t* nvm_ram_buff_ref;
+} BtTest;
+
+BtTest* bt_test = NULL;
+
+void bt_test_alloc() {
+    bt_test = malloc(sizeof(BtTest));
+    bt_test->storage = furi_record_open(RECORD_STORAGE);
+    bt_test->nvm_ram_buff_dut = malloc(BT_TEST_NVM_RAM_BUFF_SIZE);
+    bt_test->nvm_ram_buff_ref = malloc(BT_TEST_NVM_RAM_BUFF_SIZE);
+    bt_test->bt_keys_storage = bt_keys_storage_alloc(BT_TEST_KEY_STORAGE_FILE_PATH);
+    bt_keys_storage_set_ram_params(
+        bt_test->bt_keys_storage, bt_test->nvm_ram_buff_dut, BT_TEST_NVM_RAM_BUFF_SIZE);
+}
+
+void bt_test_free() {
+    furi_assert(bt_test);
+    free(bt_test->nvm_ram_buff_ref);
+    free(bt_test->nvm_ram_buff_dut);
+    bt_keys_storage_free(bt_test->bt_keys_storage);
+    furi_record_close(RECORD_STORAGE);
+    free(bt_test);
+    bt_test = NULL;
+}
+
+static void bt_test_keys_storage_profile() {
+    // Emulate nvm change on initial connection
+    const int nvm_change_size_on_connection = 88;
+    for(size_t i = 0; i < nvm_change_size_on_connection; i++) {
+        bt_test->nvm_ram_buff_dut[i] = rand();
+        bt_test->nvm_ram_buff_ref[i] = bt_test->nvm_ram_buff_dut[i];
+    }
+    // Emulate update storage on initial connect
+    mu_assert(
+        bt_keys_storage_update(
+            bt_test->bt_keys_storage, bt_test->nvm_ram_buff_dut, nvm_change_size_on_connection),
+        "Failed to update key storage on initial connect");
+    memset(bt_test->nvm_ram_buff_dut, 0, BT_TEST_NVM_RAM_BUFF_SIZE);
+    mu_assert(bt_keys_storage_load(bt_test->bt_keys_storage), "Failed to load NVM");
+    mu_assert(
+        memcmp(
+            bt_test->nvm_ram_buff_ref, bt_test->nvm_ram_buff_dut, nvm_change_size_on_connection) ==
+            0,
+        "Wrong buffer loaded");
+
+    const int nvm_disconnect_update_offset = 84;
+    const int nvm_disconnect_update_size = 324;
+    const int nvm_total_size = nvm_change_size_on_connection -
+                               (nvm_change_size_on_connection - nvm_disconnect_update_offset) +
+                               nvm_disconnect_update_size;
+    // Emulate update storage on initial disconnect
+    for(size_t i = nvm_disconnect_update_offset;
+        i < nvm_disconnect_update_offset + nvm_disconnect_update_size;
+        i++) {
+        bt_test->nvm_ram_buff_dut[i] = rand();
+        bt_test->nvm_ram_buff_ref[i] = bt_test->nvm_ram_buff_dut[i];
+    }
+    mu_assert(
+        bt_keys_storage_update(
+            bt_test->bt_keys_storage,
+            &bt_test->nvm_ram_buff_dut[nvm_disconnect_update_offset],
+            nvm_disconnect_update_size),
+        "Failed to update key storage on initial disconnect");
+    memset(bt_test->nvm_ram_buff_dut, 0, BT_TEST_NVM_RAM_BUFF_SIZE);
+    mu_assert(bt_keys_storage_load(bt_test->bt_keys_storage), "Failed to load NVM");
+    mu_assert(
+        memcmp(bt_test->nvm_ram_buff_ref, bt_test->nvm_ram_buff_dut, nvm_total_size) == 0,
+        "Wrong buffer loaded");
+}
+
+static void bt_test_keys_remove_test_file() {
+    mu_assert(
+        storage_simply_remove(bt_test->storage, BT_TEST_KEY_STORAGE_FILE_PATH),
+        "Can't remove test file");
+}
+
+MU_TEST(bt_test_keys_storage_serial_profile) {
+    furi_assert(bt_test);
+
+    bt_test_keys_remove_test_file();
+    bt_test_keys_storage_profile();
+    bt_test_keys_remove_test_file();
+}
+
+MU_TEST_SUITE(test_bt) {
+    bt_test_alloc();
+
+    MU_RUN_TEST(bt_test_keys_storage_serial_profile);
+
+    bt_test_free();
+}
+
+int run_minunit_test_bt() {
+    MU_RUN_SUITE(test_bt);
+    return MU_EXIT_CODE;
+}

+ 116 - 0
applications/debug/unit_tests/furi_hal/furi_hal_tests.c

@@ -0,0 +1,116 @@
+#include <stdio.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <lp5562_reg.h>
+#include "../minunit.h"
+
+#define DATA_SIZE 4
+
+static void furi_hal_i2c_int_setup() {
+    furi_hal_i2c_acquire(&furi_hal_i2c_handle_power);
+}
+
+static void furi_hal_i2c_int_teardown() {
+    furi_hal_i2c_release(&furi_hal_i2c_handle_power);
+}
+
+MU_TEST(furi_hal_i2c_int_1b) {
+    bool ret = false;
+    uint8_t data_one = 0;
+
+    // 1 byte: read, write, read
+    ret = furi_hal_i2c_read_reg_8(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS,
+        LP5562_CHANNEL_BLUE_CURRENT_REGISTER,
+        &data_one,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "0 read_reg_8 failed");
+    mu_assert(data_one != 0, "0 invalid data");
+    ret = furi_hal_i2c_write_reg_8(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS,
+        LP5562_CHANNEL_BLUE_CURRENT_REGISTER,
+        data_one,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "1 write_reg_8 failed");
+    ret = furi_hal_i2c_read_reg_8(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS,
+        LP5562_CHANNEL_BLUE_CURRENT_REGISTER,
+        &data_one,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "2 read_reg_8 failed");
+    mu_assert(data_one != 0, "2 invalid data");
+}
+
+MU_TEST(furi_hal_i2c_int_3b) {
+    bool ret = false;
+    uint8_t data_many[DATA_SIZE] = {0};
+
+    // 3 byte: read, write, read
+    data_many[0] = LP5562_CHANNEL_BLUE_CURRENT_REGISTER;
+    ret = furi_hal_i2c_tx(
+        &furi_hal_i2c_handle_power, LP5562_ADDRESS, data_many, 1, LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "3 tx failed");
+    ret = furi_hal_i2c_rx(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS,
+        data_many + 1,
+        DATA_SIZE - 1,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "4 rx failed");
+    for(size_t i = 0; i < DATA_SIZE; i++) mu_assert(data_many[i] != 0, "4 invalid data_many");
+
+    ret = furi_hal_i2c_tx(
+        &furi_hal_i2c_handle_power, LP5562_ADDRESS, data_many, DATA_SIZE, LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "5 tx failed");
+
+    ret = furi_hal_i2c_tx(
+        &furi_hal_i2c_handle_power, LP5562_ADDRESS, data_many, 1, LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "6 tx failed");
+    ret = furi_hal_i2c_rx(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS,
+        data_many + 1,
+        DATA_SIZE - 1,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "7 rx failed");
+    for(size_t i = 0; i < DATA_SIZE; i++) mu_assert(data_many[i] != 0, "7 invalid data_many");
+}
+
+MU_TEST(furi_hal_i2c_int_1b_fail) {
+    bool ret = false;
+    uint8_t data_one = 0;
+
+    // 1 byte: fail, read, fail, write, fail, read
+    data_one = 0;
+    ret = furi_hal_i2c_read_reg_8(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS + 0x10,
+        LP5562_CHANNEL_BLUE_CURRENT_REGISTER,
+        &data_one,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(!ret, "8 read_reg_8 failed");
+    mu_assert(data_one == 0, "8 invalid data");
+    ret = furi_hal_i2c_read_reg_8(
+        &furi_hal_i2c_handle_power,
+        LP5562_ADDRESS,
+        LP5562_CHANNEL_BLUE_CURRENT_REGISTER,
+        &data_one,
+        LP5562_I2C_TIMEOUT);
+    mu_assert(ret, "9 read_reg_8 failed");
+    mu_assert(data_one != 0, "9 invalid data");
+}
+
+MU_TEST_SUITE(furi_hal_i2c_int_suite) {
+    MU_SUITE_CONFIGURE(&furi_hal_i2c_int_setup, &furi_hal_i2c_int_teardown);
+    MU_RUN_TEST(furi_hal_i2c_int_1b);
+    MU_RUN_TEST(furi_hal_i2c_int_3b);
+    MU_RUN_TEST(furi_hal_i2c_int_1b_fail);
+}
+
+int run_minunit_test_furi_hal() {
+    MU_RUN_SUITE(furi_hal_i2c_int_suite);
+    return MU_EXIT_CODE;
+}

+ 1 - 1
applications/debug/unit_tests/minunit.h

@@ -316,7 +316,7 @@ void minunit_print_fail(const char* error);
     MU__SAFE_BLOCK(                                                                               \
         double minunit_tmp_e; double minunit_tmp_r; minunit_assert++; minunit_tmp_e = (expected); \
         minunit_tmp_r = (result);                                                                 \
-        if(fabs(minunit_tmp_e - minunit_tmp_r) > MINUNIT_EPSILON) {                               \
+        if(fabs(minunit_tmp_e - minunit_tmp_r) > (double)MINUNIT_EPSILON) {                       \
             int minunit_significant_figures = 1 - log10(MINUNIT_EPSILON);                         \
             snprintf(                                                                             \
                 minunit_last_message,                                                             \

+ 6 - 3
applications/debug/unit_tests/nfc/nfc_test.c

@@ -6,7 +6,7 @@
 #include <lib/nfc/helpers/mf_classic_dict.h>
 #include <lib/digital_signal/digital_signal.h>
 #include <lib/nfc/nfc_device.h>
-#include <applications/main/nfc/helpers/nfc_generators.h>
+#include <lib/nfc/helpers/nfc_generators.h>
 
 #include <lib/flipper_format/flipper_format_i.h>
 #include <lib/toolbox/stream/file_stream.h>
@@ -102,7 +102,10 @@ static bool nfc_test_digital_signal_test_encode(
 
     do {
         // Read test data
-        if(!nfc_test_read_signal_from_file(file_name)) break;
+        if(!nfc_test_read_signal_from_file(file_name)) {
+            FURI_LOG_E(TAG, "Failed to read signal from file");
+            break;
+        }
 
         // Encode signal
         FURI_CRITICAL_ENTER();
@@ -393,7 +396,7 @@ static void mf_classic_generator_test(uint8_t uid_len, MfClassicType type) {
         "nfc_device_save == true assert failed\r\n");
     // Verify that key cache is saved
     FuriString* key_cache_name = furi_string_alloc();
-    furi_string_set_str(key_cache_name, "/ext/nfc/cache/");
+    furi_string_set_str(key_cache_name, "/ext/nfc/.cache/");
     for(size_t i = 0; i < uid_len; i++) {
         furi_string_cat_printf(key_cache_name, "%02X", uid[i]);
     }

+ 62 - 0
applications/debug/unit_tests/power/power_test.c

@@ -0,0 +1,62 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include "../minunit.h"
+
+static void power_test_deinit(void) {
+    // Try to reset to default charging voltage
+    furi_hal_power_set_battery_charging_voltage(4.208f);
+}
+
+MU_TEST(test_power_charge_voltage_exact) {
+    // Power of 16mV charge voltages get applied exactly
+    // (bq25896 charge controller works in 16mV increments)
+    //
+    // This test may need adapted if other charge controllers are used in the future.
+    for(uint16_t charge_mv = 3840; charge_mv <= 4208; charge_mv += 16) {
+        float charge_volt = (float)charge_mv / 1000.0f;
+        furi_hal_power_set_battery_charging_voltage(charge_volt);
+        mu_assert_double_eq(charge_volt, furi_hal_power_get_battery_charging_voltage());
+    }
+}
+
+MU_TEST(test_power_charge_voltage_floating_imprecision) {
+    // 4.016f should act as 4.016 V, even with floating point imprecision
+    furi_hal_power_set_battery_charging_voltage(4.016f);
+    mu_assert_double_eq(4.016f, furi_hal_power_get_battery_charging_voltage());
+}
+
+MU_TEST(test_power_charge_voltage_inexact) {
+    // Charge voltages that are not power of 16mV get truncated down
+    furi_hal_power_set_battery_charging_voltage(3.841f);
+    mu_assert_double_eq(3.840, furi_hal_power_get_battery_charging_voltage());
+
+    furi_hal_power_set_battery_charging_voltage(3.900f);
+    mu_assert_double_eq(3.888, furi_hal_power_get_battery_charging_voltage());
+
+    furi_hal_power_set_battery_charging_voltage(4.200f);
+    mu_assert_double_eq(4.192, furi_hal_power_get_battery_charging_voltage());
+}
+
+MU_TEST(test_power_charge_voltage_invalid_clamped) {
+    // Out-of-range charge voltages get clamped to 3.840 V and 4.208 V
+    furi_hal_power_set_battery_charging_voltage(3.808f);
+    mu_assert_double_eq(3.840, furi_hal_power_get_battery_charging_voltage());
+
+    // NOTE: Intentionally picking a small increment above 4.208 V to reduce the risk of an
+    // unhappy battery if this fails.
+    furi_hal_power_set_battery_charging_voltage(4.240f);
+    mu_assert_double_eq(4.208, furi_hal_power_get_battery_charging_voltage());
+}
+
+MU_TEST_SUITE(test_power_suite) {
+    MU_RUN_TEST(test_power_charge_voltage_exact);
+    MU_RUN_TEST(test_power_charge_voltage_floating_imprecision);
+    MU_RUN_TEST(test_power_charge_voltage_inexact);
+    MU_RUN_TEST(test_power_charge_voltage_invalid_clamped);
+    power_test_deinit();
+}
+
+int run_minunit_test_power() {
+    MU_RUN_SUITE(test_power_suite);
+    return MU_EXIT_CODE;
+}

+ 16 - 1
applications/debug/unit_tests/subghz/subghz_test.c

@@ -13,7 +13,7 @@
 #define CAME_ATOMO_DIR_NAME EXT_PATH("subghz/assets/came_atomo")
 #define NICE_FLOR_S_DIR_NAME EXT_PATH("subghz/assets/nice_flor_s")
 #define TEST_RANDOM_DIR_NAME EXT_PATH("unit_tests/subghz/test_random_raw.sub")
-#define TEST_RANDOM_COUNT_PARSE 244
+#define TEST_RANDOM_COUNT_PARSE 253
 #define TEST_TIMEOUT 10000
 
 static SubGhzEnvironment* environment_handler;
@@ -587,6 +587,13 @@ MU_TEST(subghz_decoder_ansonic_test) {
         "Test decoder " SUBGHZ_PROTOCOL_ANSONIC_NAME " error\r\n");
 }
 
+MU_TEST(subghz_decoder_smc5326_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/smc5326_raw.sub"), SUBGHZ_PROTOCOL_SMC5326_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
+}
+
 //test encoders
 MU_TEST(subghz_encoder_princeton_test) {
     mu_assert(
@@ -714,6 +721,12 @@ MU_TEST(subghz_encoder_ansonic_test) {
         "Test encoder " SUBGHZ_PROTOCOL_ANSONIC_NAME " error\r\n");
 }
 
+MU_TEST(subghz_encoder_smc5326_test) {
+    mu_assert(
+        subghz_encoder_test(EXT_PATH("unit_tests/subghz/smc5326.sub")),
+        "Test encoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
+}
+
 MU_TEST(subghz_random_test) {
     mu_assert(subghz_decode_random_test(TEST_RANDOM_DIR_NAME), "Random test error\r\n");
 }
@@ -757,6 +770,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_decoder_intertechno_v3_test);
     MU_RUN_TEST(subghz_decoder_clemsa_test);
     MU_RUN_TEST(subghz_decoder_ansonic_test);
+    MU_RUN_TEST(subghz_decoder_smc5326_test);
 
     MU_RUN_TEST(subghz_encoder_princeton_test);
     MU_RUN_TEST(subghz_encoder_came_test);
@@ -779,6 +793,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_encoder_intertechno_v3_test);
     MU_RUN_TEST(subghz_encoder_clemsa_test);
     MU_RUN_TEST(subghz_encoder_ansonic_test);
+    MU_RUN_TEST(subghz_encoder_smc5326_test);
 
     MU_RUN_TEST(subghz_random_test);
     subghz_test_deinit();

+ 6 - 0
applications/debug/unit_tests/test_index.c

@@ -9,6 +9,7 @@
 #define TAG "UnitTests"
 
 int run_minunit_test_furi();
+int run_minunit_test_furi_hal();
 int run_minunit_test_furi_string();
 int run_minunit_test_infrared();
 int run_minunit_test_rpc();
@@ -18,10 +19,12 @@ int run_minunit_test_stream();
 int run_minunit_test_storage();
 int run_minunit_test_subghz();
 int run_minunit_test_dirwalk();
+int run_minunit_test_power();
 int run_minunit_test_protocol_dict();
 int run_minunit_test_lfrfid_protocols();
 int run_minunit_test_nfc();
 int run_minunit_test_bit_lib();
+int run_minunit_test_bt();
 
 typedef int (*UnitTestEntry)();
 
@@ -32,6 +35,7 @@ typedef struct {
 
 const UnitTest unit_tests[] = {
     {.name = "furi", .entry = run_minunit_test_furi},
+    {.name = "furi_hal", .entry = run_minunit_test_furi_hal},
     {.name = "furi_string", .entry = run_minunit_test_furi_string},
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "stream", .entry = run_minunit_test_stream},
@@ -42,9 +46,11 @@ const UnitTest unit_tests[] = {
     {.name = "subghz", .entry = run_minunit_test_subghz},
     {.name = "infrared", .entry = run_minunit_test_infrared},
     {.name = "nfc", .entry = run_minunit_test_nfc},
+    {.name = "power", .entry = run_minunit_test_power},
     {.name = "protocol_dict", .entry = run_minunit_test_protocol_dict},
     {.name = "lfrfid", .entry = run_minunit_test_lfrfid_protocols},
     {.name = "bit_lib", .entry = run_minunit_test_bit_lib},
+    {.name = "bt", .entry = run_minunit_test_bt},
 };
 
 void minunit_print_progress() {

+ 9 - 4
applications/main/archive/helpers/archive_browser.c

@@ -80,10 +80,12 @@ static void archive_file_browser_set_path(
     ArchiveBrowserView* browser,
     FuriString* path,
     const char* filter_ext,
-    bool skip_assets) {
+    bool skip_assets,
+    bool hide_dot_files) {
     furi_assert(browser);
     if(!browser->worker_running) {
-        browser->worker = file_browser_worker_alloc(path, filter_ext, skip_assets);
+        browser->worker =
+            file_browser_worker_alloc(path, NULL, filter_ext, skip_assets, hide_dot_files);
         file_browser_worker_set_callback_context(browser->worker, browser);
         file_browser_worker_set_folder_callback(browser->worker, archive_folder_open_cb);
         file_browser_worker_set_list_callback(browser->worker, archive_list_load_cb);
@@ -92,7 +94,8 @@ static void archive_file_browser_set_path(
         browser->worker_running = true;
     } else {
         furi_assert(browser->worker);
-        file_browser_worker_set_config(browser->worker, path, filter_ext, skip_assets);
+        file_browser_worker_set_config(
+            browser->worker, path, filter_ext, skip_assets, hide_dot_files);
     }
 }
 
@@ -472,8 +475,10 @@ void archive_switch_tab(ArchiveBrowserView* browser, InputKey key) {
         tab = archive_get_tab(browser);
         if(archive_is_dir_exists(browser->path)) {
             bool skip_assets = (strcmp(archive_get_tab_ext(tab), "*") == 0) ? false : true;
+            // Hide dot files everywhere except Browser
+            bool hide_dot_files = (strcmp(archive_get_tab_ext(tab), "*") == 0) ? false : true;
             archive_file_browser_set_path(
-                browser, browser->path, archive_get_tab_ext(tab), skip_assets);
+                browser, browser->path, archive_get_tab_ext(tab), skip_assets, hide_dot_files);
             tab_empty = false; // Empty check will be performed later
         }
     }

+ 2 - 0
applications/main/archive/scenes/archive_scene_browser.c

@@ -143,6 +143,8 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) {
             break;
         case ArchiveBrowserEventFileMenuDelete:
             if(archive_get_tab(browser) != ArchiveTabFavorites) {
+                scene_manager_set_scene_state(
+                    archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH);
                 scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneDelete);
             }
             consumed = true;

+ 48 - 4
applications/main/archive/views/archive_browser_view.c

@@ -5,6 +5,9 @@
 #include "archive_browser_view.h"
 #include "../helpers/archive_browser.h"
 
+#define SCROLL_INTERVAL (333)
+#define SCROLL_DELAY (2)
+
 static const char* ArchiveTabNames[] = {
     [ArchiveTabFavorites] = "Favorites",
     [ArchiveTabIButton] = "iButton",
@@ -146,13 +149,18 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
             furi_string_set(str_buf, "---");
         }
 
-        elements_string_fit_width(
-            canvas, str_buf, (scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset);
+        size_t scroll_counter = model->scroll_counter;
 
         if(model->item_idx == idx) {
             archive_draw_frame(canvas, i, scrollbar, model->move_fav);
+            if(scroll_counter < SCROLL_DELAY) {
+                scroll_counter = 0;
+            } else {
+                scroll_counter -= SCROLL_DELAY;
+            }
         } else {
             canvas_set_color(canvas, ColorBlack);
+            scroll_counter = 0;
         }
 
         if(custom_icon_data) {
@@ -162,8 +170,15 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
             canvas_draw_icon(
                 canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]);
         }
-        canvas_draw_str(
-            canvas, 15 + x_offset, 24 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buf));
+
+        elements_scrollable_text_line(
+            canvas,
+            15 + x_offset,
+            24 + i * FRAME_HEIGHT,
+            ((scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset),
+            str_buf,
+            scroll_counter,
+            (model->item_idx != idx));
 
         furi_string_free(str_buf);
     }
@@ -329,6 +344,7 @@ static bool archive_view_input(InputEvent* event, void* context) {
                         if(move_fav_mode) {
                             browser->callback(ArchiveBrowserEventFavMoveUp, browser->context);
                         }
+                        model->scroll_counter = 0;
                     } else if(event->key == InputKeyDown) {
                         model->item_idx = (model->item_idx + 1) % model->item_cnt;
                         if(is_file_list_load_required(model)) {
@@ -338,6 +354,7 @@ static bool archive_view_input(InputEvent* event, void* context) {
                         if(move_fav_mode) {
                             browser->callback(ArchiveBrowserEventFavMoveDown, browser->context);
                         }
+                        model->scroll_counter = 0;
                     }
                 },
                 true);
@@ -377,6 +394,27 @@ static bool archive_view_input(InputEvent* event, void* context) {
     return true;
 }
 
+static void browser_scroll_timer(void* context) {
+    furi_assert(context);
+    ArchiveBrowserView* browser = context;
+    with_view_model(
+        browser->view, ArchiveBrowserViewModel * model, { model->scroll_counter++; }, true);
+}
+
+static void browser_view_enter(void* context) {
+    furi_assert(context);
+    ArchiveBrowserView* browser = context;
+    with_view_model(
+        browser->view, ArchiveBrowserViewModel * model, { model->scroll_counter = 0; }, true);
+    furi_timer_start(browser->scroll_timer, SCROLL_INTERVAL);
+}
+
+static void browser_view_exit(void* context) {
+    furi_assert(context);
+    ArchiveBrowserView* browser = context;
+    furi_timer_stop(browser->scroll_timer);
+}
+
 ArchiveBrowserView* browser_alloc() {
     ArchiveBrowserView* browser = malloc(sizeof(ArchiveBrowserView));
     browser->view = view_alloc();
@@ -384,6 +422,10 @@ ArchiveBrowserView* browser_alloc() {
     view_set_context(browser->view, browser);
     view_set_draw_callback(browser->view, archive_view_render);
     view_set_input_callback(browser->view, archive_view_input);
+    view_set_enter_callback(browser->view, browser_view_enter);
+    view_set_exit_callback(browser->view, browser_view_exit);
+
+    browser->scroll_timer = furi_timer_alloc(browser_scroll_timer, FuriTimerTypePeriodic, browser);
 
     browser->path = furi_string_alloc_set(archive_get_default_path(TAB_DEFAULT));
 
@@ -402,6 +444,8 @@ ArchiveBrowserView* browser_alloc() {
 void browser_free(ArchiveBrowserView* browser) {
     furi_assert(browser);
 
+    furi_timer_free(browser->scroll_timer);
+
     if(browser->worker_running) {
         file_browser_worker_free(browser->worker);
     }

+ 2 - 0
applications/main/archive/views/archive_browser_view.h

@@ -81,6 +81,7 @@ struct ArchiveBrowserView {
     FuriString* path;
     InputKey last_tab_switch_dir;
     bool is_root;
+    FuriTimer* scroll_timer;
 };
 
 typedef struct {
@@ -97,6 +98,7 @@ typedef struct {
     int32_t item_idx;
     int32_t array_offset;
     int32_t list_offset;
+    size_t scroll_counter;
 } ArchiveBrowserViewModel;
 
 void archive_browser_set_callback(

+ 2 - 0
applications/main/bad_usb/scenes/bad_usb_scene_file_select.c

@@ -1,12 +1,14 @@
 #include "../bad_usb_app_i.h"
 #include "furi_hal_power.h"
 #include "furi_hal_usb.h"
+#include <storage/storage.h>
 
 static bool bad_usb_file_select(BadUsbApp* bad_usb) {
     furi_assert(bad_usb);
 
     DialogsFileBrowserOptions browser_options;
     dialog_file_browser_set_basic_options(&browser_options, BAD_USB_APP_EXTENSION, &I_badusb_10px);
+    browser_options.base_path = BAD_USB_APP_PATH_FOLDER;
 
     // Input events and views are managed by file_browser
     bool res = dialog_file_browser_show(

+ 1 - 0
applications/main/fap_loader/fap_loader_app.c

@@ -148,6 +148,7 @@ static bool fap_loader_select_app(FapLoader* loader) {
         .hide_ext = true,
         .item_loader_callback = fap_loader_item_callback,
         .item_loader_context = loader,
+        .base_path = EXT_PATH("apps"),
     };
 
     return dialog_file_browser_show(

+ 1 - 0
applications/main/ibutton/ibutton.c

@@ -218,6 +218,7 @@ void ibutton_free(iButton* ibutton) {
 bool ibutton_file_select(iButton* ibutton) {
     DialogsFileBrowserOptions browser_options;
     dialog_file_browser_set_basic_options(&browser_options, IBUTTON_APP_EXTENSION, &I_ibutt_10px);
+    browser_options.base_path = IBUTTON_APP_FOLDER;
 
     bool success = dialog_file_browser_show(
         ibutton->dialogs, ibutton->file_path, ibutton->file_path, &browser_options);

+ 0 - 1
applications/main/ibutton/scenes/ibutton_scene_read.c

@@ -52,7 +52,6 @@ bool ibutton_scene_read_on_event(void* context, SceneManagerEvent event) {
 
             if(success) {
                 ibutton_notification_message(ibutton, iButtonNotificationMessageSuccess);
-                ibutton_notification_message(ibutton, iButtonNotificationMessageGreenOn);
                 scene_manager_next_scene(scene_manager, iButtonSceneReadSuccess);
                 DOLPHIN_DEED(DolphinDeedIbuttonReadSuccess);
             }

+ 2 - 0
applications/main/ibutton/scenes/ibutton_scene_read_success.c

@@ -48,6 +48,8 @@ void ibutton_scene_read_success_on_enter(void* context) {
     dialog_ex_set_context(dialog_ex, ibutton);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewDialogEx);
+
+    ibutton_notification_message(ibutton, iButtonNotificationMessageGreenOn);
 }
 
 bool ibutton_scene_read_success_on_event(void* context, SceneManagerEvent event) {

+ 1 - 0
applications/main/infrared/scenes/infrared_scene_remote_list.c

@@ -7,6 +7,7 @@ void infrared_scene_remote_list_on_enter(void* context) {
 
     DialogsFileBrowserOptions browser_options;
     dialog_file_browser_set_basic_options(&browser_options, INFRARED_APP_EXTENSION, &I_ir_10px);
+    browser_options.base_path = INFRARED_APP_FOLDER;
 
     bool success = dialog_file_browser_show(
         infrared->dialogs, infrared->file_path, infrared->file_path, &browser_options);

+ 1 - 0
applications/main/lfrfid/lfrfid.c

@@ -230,6 +230,7 @@ bool lfrfid_load_key_from_file_select(LfRfid* app) {
 
     DialogsFileBrowserOptions browser_options;
     dialog_file_browser_set_basic_options(&browser_options, LFRFID_APP_EXTENSION, &I_125_10px);
+    browser_options.base_path = LFRFID_APP_FOLDER;
 
     // Input events and views are managed by file_browser
     bool result =

+ 1 - 0
applications/main/nfc/nfc.c

@@ -46,6 +46,7 @@ Nfc* nfc_alloc() {
 
     // Nfc device
     nfc->dev = nfc_device_alloc();
+    furi_string_set(nfc->dev->folder, NFC_APP_FOLDER);
 
     // Open GUI record
     nfc->gui = furi_record_open(RECORD_GUI);

+ 2 - 3
applications/main/nfc/nfc_i.h

@@ -27,6 +27,7 @@
 #include <lib/nfc/nfc_device.h>
 #include <lib/nfc/helpers/mf_classic_dict.h>
 #include <lib/nfc/parsers/nfc_supported_card.h>
+#include <lib/nfc/helpers/nfc_generators.h>
 
 #include "views/dict_attack.h"
 #include "views/detect_reader.h"
@@ -43,6 +44,7 @@
 ARRAY_DEF(MfClassicUserKeys, char*, M_PTR_OPLIST);
 
 #define NFC_TEXT_STORE_SIZE 128
+#define NFC_APP_FOLDER ANY_PATH("nfc")
 
 typedef enum {
     NfcRpcStateIdle,
@@ -50,9 +52,6 @@ typedef enum {
     NfcRpcStateEmulated,
 } NfcRpcState;
 
-// Forward declaration due to circular dependency
-typedef struct NfcGenerator NfcGenerator;
-
 struct Nfc {
     NfcWorker* worker;
     ViewDispatcher* view_dispatcher;

+ 3 - 3
applications/main/nfc/scenes/nfc_scene_emulate_uid.c

@@ -37,8 +37,8 @@ static void nfc_scene_emulate_uid_widget_config(Nfc* nfc, bool data_received) {
     FuriString* info_str;
     info_str = furi_string_alloc();
 
-    widget_add_icon_element(widget, 0, 3, &I_RFIDDolphinSend_97x61);
-    widget_add_string_element(widget, 89, 32, AlignCenter, AlignTop, FontPrimary, "Emulating UID");
+    widget_add_icon_element(widget, 0, 3, &I_NFC_dolphin_emulation_47x61);
+    widget_add_string_element(widget, 57, 13, AlignLeft, AlignTop, FontPrimary, "Emulating UID");
     if(strcmp(nfc->dev->dev_name, "")) {
         furi_string_printf(info_str, "%s", nfc->dev->dev_name);
     } else {
@@ -48,7 +48,7 @@ static void nfc_scene_emulate_uid_widget_config(Nfc* nfc, bool data_received) {
     }
     furi_string_trim(info_str);
     widget_add_text_box_element(
-        widget, 56, 43, 70, 21, AlignCenter, AlignTop, furi_string_get_cstr(info_str), true);
+        widget, 57, 28, 67, 25, AlignCenter, AlignTop, furi_string_get_cstr(info_str), true);
     furi_string_free(info_str);
     if(data_received) {
         widget_add_button_element(

+ 7 - 2
applications/main/nfc/scenes/nfc_scene_generate_info.c

@@ -1,5 +1,5 @@
 #include "../nfc_i.h"
-#include "../helpers/nfc_generators.h"
+#include "lib/nfc/helpers/nfc_generators.h"
 
 void nfc_scene_generate_info_dialog_callback(DialogExResult result, void* context) {
     Nfc* nfc = context;
@@ -39,7 +39,12 @@ bool nfc_scene_generate_info_on_event(void* context, SceneManagerEvent event) {
 
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == DialogExResultRight) {
-            scene_manager_next_scene(nfc->scene_manager, nfc->generator->next_scene);
+            // Switch either to NfcSceneMfClassicMenu or NfcSceneMfUltralightMenu
+            if(nfc->dev->dev_data.protocol == NfcDeviceProtocolMifareClassic) {
+                scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicMenu);
+            } else if(nfc->dev->dev_data.protocol == NfcDeviceProtocolMifareUl) {
+                scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightMenu);
+            }
             consumed = true;
         }
     }

+ 5 - 4
applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c

@@ -17,13 +17,14 @@ void nfc_scene_mf_classic_emulate_on_enter(void* context) {
 
     // Setup view
     Popup* popup = nfc->popup;
+    popup_set_header(popup, "Emulating", 67, 13, AlignLeft, AlignTop);
     if(strcmp(nfc->dev->dev_name, "")) {
-        nfc_text_store_set(nfc, "Emulating\n%s", nfc->dev->dev_name);
+        nfc_text_store_set(nfc, "%s", nfc->dev->dev_name);
     } else {
-        nfc_text_store_set(nfc, "Emulating\nMf Classic", nfc->dev->dev_name);
+        nfc_text_store_set(nfc, "MIFARE\nClassic");
     }
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
-    popup_set_header(popup, nfc->text_store, 56, 31, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_NFC_dolphin_emulation_47x61);
+    popup_set_text(popup, nfc->text_store, 90, 28, AlignCenter, AlignTop);
 
     // Setup and start worker
     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup);

+ 10 - 4
applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c

@@ -16,14 +16,20 @@ void nfc_scene_mf_ultralight_emulate_on_enter(void* context) {
     Nfc* nfc = context;
 
     // Setup view
+    MfUltralightType type = nfc->dev->dev_data.mf_ul_data.type;
+    bool is_ultralight = (type == MfUltralightTypeUL11) || (type == MfUltralightTypeUL21) ||
+                         (type == MfUltralightTypeUnknown);
     Popup* popup = nfc->popup;
+    popup_set_header(popup, "Emulating", 67, 13, AlignLeft, AlignTop);
     if(strcmp(nfc->dev->dev_name, "")) {
-        nfc_text_store_set(nfc, "Emulating\n%s", nfc->dev->dev_name);
+        nfc_text_store_set(nfc, "%s", nfc->dev->dev_name);
+    } else if(is_ultralight) {
+        nfc_text_store_set(nfc, "MIFARE\nUltralight");
     } else {
-        nfc_text_store_set(nfc, "Emulating\nMf Ultralight", nfc->dev->dev_name);
+        nfc_text_store_set(nfc, "MIFARE\nNTAG");
     }
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
-    popup_set_header(popup, nfc->text_store, 56, 31, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_NFC_dolphin_emulation_47x61);
+    popup_set_text(popup, nfc->text_store, 90, 28, AlignCenter, AlignTop);
 
     // Setup and start worker
     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup);

+ 14 - 12
applications/main/nfc/scenes/nfc_scene_nfc_data_info.c

@@ -89,18 +89,20 @@ void nfc_scene_nfc_data_info_on_enter(void* context) {
             furi_string_cat_printf(temp_str, "\nPassword-protected");
         } else if(data->auth_success) {
             MfUltralightConfigPages* config_pages = mf_ultralight_get_config_pages(data);
-            furi_string_cat_printf(
-                temp_str,
-                "\nPassword: %02X %02X %02X %02X",
-                config_pages->auth_data.pwd.raw[0],
-                config_pages->auth_data.pwd.raw[1],
-                config_pages->auth_data.pwd.raw[2],
-                config_pages->auth_data.pwd.raw[3]);
-            furi_string_cat_printf(
-                temp_str,
-                "\nPACK: %02X %02X",
-                config_pages->auth_data.pack.raw[0],
-                config_pages->auth_data.pack.raw[1]);
+            if(config_pages) {
+                furi_string_cat_printf(
+                    temp_str,
+                    "\nPassword: %02X %02X %02X %02X",
+                    config_pages->auth_data.pwd.raw[0],
+                    config_pages->auth_data.pwd.raw[1],
+                    config_pages->auth_data.pwd.raw[2],
+                    config_pages->auth_data.pwd.raw[3]);
+                furi_string_cat_printf(
+                    temp_str,
+                    "\nPACK: %02X %02X",
+                    config_pages->auth_data.pack.raw[0],
+                    config_pages->auth_data.pack.raw[1]);
+            }
         }
     } else if(protocol == NfcDeviceProtocolMifareClassic) {
         MfClassicData* data = &dev_data->mf_classic_data;

+ 1 - 1
applications/main/nfc/scenes/nfc_scene_rpc.c

@@ -7,7 +7,7 @@ void nfc_scene_rpc_on_enter(void* context) {
     popup_set_header(popup, "NFC", 89, 42, AlignCenter, AlignBottom);
     popup_set_text(popup, "RPC mode", 89, 44, AlignCenter, AlignTop);
 
-    popup_set_icon(popup, 0, 12, &I_RFIDDolphinSend_97x61);
+    popup_set_icon(popup, 0, 12, &I_NFC_dolphin_emulation_47x61);
 
     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup);
 

+ 1 - 1
applications/main/nfc/scenes/nfc_scene_set_type.c

@@ -1,5 +1,5 @@
 #include "../nfc_i.h"
-#include "../helpers/nfc_generators.h"
+#include "lib/nfc/helpers/nfc_generators.h"
 
 enum SubmenuIndex {
     SubmenuIndexNFCA4,

+ 13 - 0
applications/main/subghz/helpers/subghz_error_type.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+/** SubGhzErrorType */
+typedef enum {
+    SubGhzErrorTypeNoError = 0, /** There are no errors */
+    SubGhzErrorTypeParseFile =
+        1, /** File parsing error, or wrong file structure, or missing required parameters. more accurate data can be obtained through the debug port */
+    SubGhzErrorTypeOnlyRX =
+        2, /** Transmission on this frequency is blocked by regional settings */
+} SubGhzErrorType;

+ 7 - 0
applications/main/subghz/helpers/subghz_types.h

@@ -28,6 +28,13 @@ typedef enum {
     SubGhzHopperStateRSSITimeOut,
 } SubGhzHopperState;
 
+/** SubGhzSpeakerState state */
+typedef enum {
+    SubGhzSpeakerStateDisable,
+    SubGhzSpeakerStateShutdown,
+    SubGhzSpeakerStateEnable,
+} SubGhzSpeakerState;
+
 /** SubGhzRxKeyState state */
 typedef enum {
     SubGhzRxKeyStateIDLE,

+ 3 - 0
applications/main/subghz/scenes/subghz_scene_read_raw.c

@@ -259,6 +259,7 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) {
         case SubGhzCustomEventViewReadRAWSendStop:
             subghz->state_notifications = SubGhzNotificationStateIDLE;
             if(subghz->txrx->txrx_state == SubGhzTxRxStateTx) {
+                subghz_speaker_unmute(subghz);
                 subghz_tx_stop(subghz);
                 subghz_sleep(subghz);
             }
@@ -376,10 +377,12 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) {
                     subghz_read_raw_add_data_rssi(subghz->subghz_read_raw, rssi, false);
                     subghz_protocol_raw_save_to_file_pause(
                         (SubGhzProtocolDecoderRAW*)subghz->txrx->decoder_result, true);
+                    subghz_speaker_mute(subghz);
                 } else {
                     subghz_read_raw_add_data_rssi(subghz->subghz_read_raw, rssi, true);
                     subghz_protocol_raw_save_to_file_pause(
                         (SubGhzProtocolDecoderRAW*)subghz->txrx->decoder_result, false);
+                    subghz_speaker_unmute(subghz);
                 }
             }
 

+ 29 - 0
applications/main/subghz/scenes/subghz_scene_receiver_config.c

@@ -5,6 +5,7 @@ enum SubGhzSettingIndex {
     SubGhzSettingIndexFrequency,
     SubGhzSettingIndexHopping,
     SubGhzSettingIndexModulation,
+    SubGhzSettingIndexSound,
     SubGhzSettingIndexLock,
     SubGhzSettingIndexRAWThesholdRSSI,
 };
@@ -48,6 +49,16 @@ const uint32_t hopping_value[HOPPING_COUNT] = {
     SubGhzHopperStateRunnig,
 };
 
+#define SPEAKER_COUNT 2
+const char* const speaker_text[SPEAKER_COUNT] = {
+    "OFF",
+    "ON",
+};
+const uint32_t speaker_value[SPEAKER_COUNT] = {
+    SubGhzSpeakerStateShutdown,
+    SubGhzSpeakerStateEnable,
+};
+
 uint8_t subghz_scene_receiver_config_next_frequency(const uint32_t value, void* context) {
     furi_assert(context);
     SubGhz* subghz = context;
@@ -167,6 +178,14 @@ static void subghz_scene_receiver_config_set_hopping_running(VariableItem* item)
     subghz->txrx->hopper_state = hopping_value[index];
 }
 
+static void subghz_scene_receiver_config_set_speaker(VariableItem* item) {
+    SubGhz* subghz = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, speaker_text[index]);
+    subghz->txrx->speaker_state = speaker_value[index];
+}
+
 static void subghz_scene_receiver_config_set_raw_threshold_rssi(VariableItem* item) {
     SubGhz* subghz = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
@@ -235,6 +254,16 @@ void subghz_scene_receiver_config_on_enter(void* context) {
     variable_item_set_current_value_text(
         item, subghz_setting_get_preset_name(subghz->setting, value_index));
 
+    item = variable_item_list_add(
+        subghz->variable_item_list,
+        "Sound:",
+        SPEAKER_COUNT,
+        subghz_scene_receiver_config_set_speaker,
+        subghz);
+    value_index = value_index_uint32(subghz->txrx->speaker_state, speaker_value, SPEAKER_COUNT);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, speaker_text[value_index]);
+
     if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
        SubGhzCustomEventManagerSet) {
         variable_item_list_add(subghz->variable_item_list, "Lock Keyboard", 1, NULL, NULL);

+ 9 - 0
applications/main/subghz/scenes/subghz_scene_rpc.c

@@ -43,6 +43,12 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) {
                 result = subghz_tx_start(subghz, subghz->txrx->fff_data);
                 if(result) subghz_blink_start(subghz);
             }
+            if(!result) {
+                rpc_system_app_set_error_code(subghz->rpc_ctx, SubGhzErrorTypeOnlyRX);
+                rpc_system_app_set_error_text(
+                    subghz->rpc_ctx,
+                    "Transmission on this frequency is restricted in your region");
+            }
             rpc_system_app_confirm(subghz->rpc_ctx, RpcAppEventButtonPress, result);
         } else if(event.event == SubGhzCustomEventSceneRpcButtonRelease) {
             bool result = false;
@@ -74,6 +80,9 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) {
                     popup_set_text(popup, subghz->file_name_tmp, 89, 44, AlignCenter, AlignTop);
 
                     furi_string_free(file_name);
+                } else {
+                    rpc_system_app_set_error_code(subghz->rpc_ctx, SubGhzErrorTypeParseFile);
+                    rpc_system_app_set_error_text(subghz->rpc_ctx, "Cannot parse file");
                 }
             }
             rpc_system_app_confirm(subghz->rpc_ctx, RpcAppEventLoadFile, result);

+ 1 - 0
applications/main/subghz/subghz.c

@@ -177,6 +177,7 @@ SubGhz* subghz_alloc() {
 
     subghz->txrx->txrx_state = SubGhzTxRxStateSleep;
     subghz->txrx->hopper_state = SubGhzHopperStateOFF;
+    subghz->txrx->speaker_state = SubGhzSpeakerStateDisable;
     subghz->txrx->rx_key_state = SubGhzRxKeyStateIDLE;
     subghz->txrx->raw_threshold_rssi = SUBGHZ_RAW_TRESHOLD_MIN;
     subghz->txrx->history = subghz_history_alloc();

+ 43 - 0
applications/main/subghz/subghz_i.c

@@ -86,6 +86,7 @@ uint32_t subghz_rx(SubGhz* subghz, uint32_t frequency) {
     uint32_t value = furi_hal_subghz_set_frequency_and_path(frequency);
     furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
     furi_hal_subghz_flush_rx();
+    subghz_speaker_on(subghz);
     furi_hal_subghz_rx();
 
     furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, subghz->txrx->worker);
@@ -104,6 +105,7 @@ static bool subghz_tx(SubGhz* subghz, uint32_t frequency) {
     furi_hal_subghz_set_frequency_and_path(frequency);
     furi_hal_gpio_write(&gpio_cc1101_g0, false);
     furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
+    subghz_speaker_on(subghz);
     bool ret = furi_hal_subghz_tx();
     subghz->txrx->txrx_state = SubGhzTxRxStateTx;
     return ret;
@@ -119,11 +121,13 @@ void subghz_idle(SubGhz* subghz) {
 void subghz_rx_end(SubGhz* subghz) {
     furi_assert(subghz);
     furi_assert(subghz->txrx->txrx_state == SubGhzTxRxStateRx);
+
     if(subghz_worker_is_running(subghz->txrx->worker)) {
         subghz_worker_stop(subghz->txrx->worker);
         furi_hal_subghz_stop_async_rx();
     }
     furi_hal_subghz_idle();
+    subghz_speaker_off(subghz);
     subghz->txrx->txrx_state = SubGhzTxRxStateIDLE;
 }
 
@@ -212,6 +216,7 @@ void subghz_tx_stop(SubGhz* subghz) {
             subghz, subghz->txrx->fff_data, furi_string_get_cstr(subghz->file_path));
     }
     subghz_idle(subghz);
+    subghz_speaker_off(subghz);
     notification_message(subghz->notifications, &sequence_reset_red);
 }
 
@@ -454,6 +459,7 @@ bool subghz_load_protocol_from_file(SubGhz* subghz) {
 
     DialogsFileBrowserOptions browser_options;
     dialog_file_browser_set_basic_options(&browser_options, SUBGHZ_APP_EXTENSION, &I_sub1_10px);
+    browser_options.base_path = SUBGHZ_APP_FOLDER;
 
     // Input events and views are managed by file_select
     bool res = dialog_file_browser_show(
@@ -584,3 +590,40 @@ void subghz_hopper_update(SubGhz* subghz) {
         subghz_rx(subghz, subghz->txrx->preset->frequency);
     }
 }
+
+void subghz_speaker_on(SubGhz* subghz) {
+    if(subghz->txrx->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_acquire(30)) {
+            furi_hal_subghz_set_async_mirror_pin(&gpio_speaker);
+        } else {
+            subghz->txrx->speaker_state = SubGhzSpeakerStateDisable;
+        }
+    }
+}
+
+void subghz_speaker_off(SubGhz* subghz) {
+    if(subghz->txrx->speaker_state != SubGhzSpeakerStateDisable) {
+        if(furi_hal_speaker_is_mine()) {
+            furi_hal_subghz_set_async_mirror_pin(NULL);
+            furi_hal_speaker_release();
+            if(subghz->txrx->speaker_state == SubGhzSpeakerStateShutdown)
+                subghz->txrx->speaker_state = SubGhzSpeakerStateDisable;
+        }
+    }
+}
+
+void subghz_speaker_mute(SubGhz* subghz) {
+    if(subghz->txrx->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            furi_hal_subghz_set_async_mirror_pin(NULL);
+        }
+    }
+}
+
+void subghz_speaker_unmute(SubGhz* subghz) {
+    if(subghz->txrx->speaker_state == SubGhzSpeakerStateEnable) {
+        if(furi_hal_speaker_is_mine()) {
+            furi_hal_subghz_set_async_mirror_pin(&gpio_speaker);
+        }
+    }
+}

+ 6 - 0
applications/main/subghz/subghz_i.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "helpers/subghz_types.h"
+#include "helpers/subghz_error_type.h"
 #include <lib/subghz/types.h>
 #include "subghz.h"
 #include "views/receiver.h"
@@ -52,6 +53,7 @@ struct SubGhzTxRx {
     uint16_t idx_menu_chosen;
     SubGhzTxRxState txrx_state;
     SubGhzHopperState hopper_state;
+    SubGhzSpeakerState speaker_state;
     uint8_t hopper_timeout;
     uint8_t hopper_idx_frequency;
     SubGhzRxKeyState rx_key_state;
@@ -130,3 +132,7 @@ void subghz_file_name_clear(SubGhz* subghz);
 bool subghz_path_is_file(FuriString* path);
 uint32_t subghz_random_serial(void);
 void subghz_hopper_update(SubGhz* subghz);
+void subghz_speaker_on(SubGhz* subghz);
+void subghz_speaker_off(SubGhz* subghz);
+void subghz_speaker_mute(SubGhz* subghz);
+void subghz_speaker_unmute(SubGhz* subghz);

+ 4 - 3
applications/main/subghz/views/receiver.c

@@ -8,7 +8,7 @@
 #include <m-array.h>
 
 #define FRAME_HEIGHT 12
-#define MAX_LEN_PX 100
+#define MAX_LEN_PX 111
 #define MENU_ITEMS 4u
 #define UNLOCK_CNT 3
 
@@ -186,7 +186,7 @@ void subghz_view_receiver_draw(Canvas* canvas, SubGhzViewReceiverModel* model) {
         size_t idx = CLAMP((uint16_t)(i + model->list_offset), model->history_item, 0);
         item_menu = SubGhzReceiverMenuItemArray_get(model->history->data, idx);
         furi_string_set(str_buff, item_menu->item_str);
-        elements_string_fit_width(canvas, str_buff, scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX);
+        elements_string_fit_width(canvas, str_buff, scrollbar ? MAX_LEN_PX - 7 : MAX_LEN_PX);
         if(model->idx == idx) {
             subghz_view_receiver_draw_frame(canvas, i, scrollbar);
         } else {
@@ -309,7 +309,8 @@ bool subghz_view_receiver_input(InputEvent* event, void* context) {
             subghz_receiver->view,
             SubGhzViewReceiverModel * model,
             {
-                if(model->idx != model->history_item - 1) model->idx++;
+                if((model->history_item != 0) && (model->idx != model->history_item - 1))
+                    model->idx++;
             },
             true);
     } else if(event->key == InputKeyLeft && event->type == InputTypeShort) {

+ 0 - 7
applications/main/subghz/views/subghz_frequency_analyzer.c

@@ -201,13 +201,6 @@ static void subghz_frequency_analyzer_log_frequency_sort(SubGhzFrequencyAnalyzer
         model->log_frequency, SubGhzFrequencyAnalyzerLogItemArray_compare_by_as_interface(cmp));
 }
 
-static void subghz_frequency_analyzer_log_frequency_sort(SubGhzFrequencyAnalyzerModel* model) {
-    furi_assert(model);
-    M_LET((cmp, model->log_frequency_order_by), SubGhzFrequencyAnalyzerLogItemArray_compare_by_t)
-    SubGhzFrequencyAnalyzerLogItemArray_sort_fo(
-        model->log_frequency, SubGhzFrequencyAnalyzerLogItemArray_compare_by_as_interface(cmp));
-}
-
 bool subghz_frequency_analyzer_input(InputEvent* event, void* context) {
     furi_assert(context);
     SubGhzFrequencyAnalyzer* instance = context;

+ 10 - 0
applications/plugins/clock/application.fam

@@ -0,0 +1,10 @@
+App(
+    appid="clock",
+    name="Clock",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="clock_app",
+    requires=["gui"],
+    stack_size=2 * 1024,
+    fap_icon="clock.png",
+    fap_category="Tools",
+)

BIN
applications/plugins/clock/clock.png


+ 136 - 0
applications/plugins/clock/clock_app.c

@@ -0,0 +1,136 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <locale/locale.h>
+
+typedef enum {
+    ClockEventTypeTick,
+    ClockEventTypeKey,
+} ClockEventType;
+
+typedef struct {
+    ClockEventType type;
+    InputEvent input;
+} ClockEvent;
+
+typedef struct {
+    FuriString* buffer;
+    FuriHalRtcDateTime datetime;
+    LocaleTimeFormat timeformat;
+    LocaleDateFormat dateformat;
+} ClockData;
+
+typedef struct {
+    FuriMutex* mutex;
+    FuriMessageQueue* queue;
+    ClockData* data;
+} Clock;
+
+static void clock_input_callback(InputEvent* input_event, FuriMessageQueue* queue) {
+    furi_assert(queue);
+    ClockEvent event = {.type = ClockEventTypeKey, .input = *input_event};
+    furi_message_queue_put(queue, &event, FuriWaitForever);
+}
+
+static void clock_render_callback(Canvas* canvas, void* ctx) {
+    Clock* clock = ctx;
+    if(furi_mutex_acquire(clock->mutex, 200) != FuriStatusOk) {
+        return;
+    }
+
+    ClockData* data = clock->data;
+
+    canvas_set_font(canvas, FontBigNumbers);
+    locale_format_time(data->buffer, &data->datetime, data->timeformat, true);
+    canvas_draw_str_aligned(
+        canvas, 64, 28, AlignCenter, AlignCenter, furi_string_get_cstr(data->buffer));
+
+    // Special case to cover missing glyphs in FontBigNumbers
+    if(data->timeformat == LocaleTimeFormat12h) {
+        size_t time_width = canvas_string_width(canvas, furi_string_get_cstr(data->buffer));
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(
+            canvas,
+            64 + (time_width / 2) - 10,
+            31,
+            AlignLeft,
+            AlignCenter,
+            (data->datetime.hour > 12) ? "PM" : "AM");
+    }
+
+    canvas_set_font(canvas, FontSecondary);
+    locale_format_date(data->buffer, &data->datetime, data->dateformat, "/");
+    canvas_draw_str_aligned(
+        canvas, 64, 42, AlignCenter, AlignTop, furi_string_get_cstr(data->buffer));
+
+    furi_mutex_release(clock->mutex);
+}
+
+static void clock_tick(void* ctx) {
+    furi_assert(ctx);
+    FuriMessageQueue* queue = ctx;
+    ClockEvent event = {.type = ClockEventTypeTick};
+    // It's OK to loose this event if system overloaded
+    furi_message_queue_put(queue, &event, 0);
+}
+
+int32_t clock_app(void* p) {
+    UNUSED(p);
+    Clock* clock = malloc(sizeof(Clock));
+    clock->data = malloc(sizeof(ClockData));
+    clock->data->buffer = furi_string_alloc();
+
+    clock->queue = furi_message_queue_alloc(8, sizeof(ClockEvent));
+    clock->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+
+    furi_hal_rtc_get_datetime(&clock->data->datetime);
+    clock->data->timeformat = locale_get_time_format();
+    clock->data->dateformat = locale_get_date_format();
+
+    // Set ViewPort callbacks
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, clock_render_callback, clock);
+    view_port_input_callback_set(view_port, clock_input_callback, clock->queue);
+
+    FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, clock->queue);
+
+    // Open GUI and register view_port
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    furi_timer_start(timer, 100);
+
+    // Main loop
+    ClockEvent event;
+    for(bool processing = true; processing;) {
+        furi_check(furi_message_queue_get(clock->queue, &event, FuriWaitForever) == FuriStatusOk);
+        furi_mutex_acquire(clock->mutex, FuriWaitForever);
+        if(event.type == ClockEventTypeKey) {
+            if(event.input.type == InputTypeShort && event.input.key == InputKeyBack) {
+                processing = false;
+            }
+        } else if(event.type == ClockEventTypeTick) {
+            furi_hal_rtc_get_datetime(&clock->data->datetime);
+        }
+
+        furi_mutex_release(clock->mutex);
+        view_port_update(view_port);
+    }
+
+    furi_timer_free(timer);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    view_port_free(view_port);
+    furi_record_close(RECORD_GUI);
+
+    furi_message_queue_free(clock->queue);
+    furi_mutex_free(clock->mutex);
+
+    furi_string_free(clock->data->buffer);
+
+    free(clock->data);
+    free(clock);
+
+    return 0;
+}

+ 45 - 6
applications/plugins/hid_app/hid.c

@@ -9,10 +9,10 @@ enum HidDebugSubmenuIndex {
     HidSubmenuIndexKeynote,
     HidSubmenuIndexKeyboard,
     HidSubmenuIndexMedia,
-    BtHidSubmenuIndexTikTok,
+    HidSubmenuIndexTikTok,
     HidSubmenuIndexMouse,
+    HidSubmenuIndexMouseJiggler,
 };
-typedef enum { ConnTypeSubmenuIndexBluetooth, ConnTypeSubmenuIndexUsb } ConnTypeDebugSubmenuIndex;
 
 static void hid_submenu_callback(void* context, uint32_t index) {
     furi_assert(context);
@@ -29,9 +29,12 @@ static void hid_submenu_callback(void* context, uint32_t index) {
     } else if(index == HidSubmenuIndexMouse) {
         app->view_id = HidViewMouse;
         view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouse);
-    } else if(index == BtHidSubmenuIndexTikTok) {
+    } else if(index == HidSubmenuIndexTikTok) {
         app->view_id = BtHidViewTikTok;
         view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewTikTok);
+    } else if(index == HidSubmenuIndexMouseJiggler) {
+        app->view_id = HidViewMouseJiggler;
+        view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouseJiggler);
     }
 }
 
@@ -48,6 +51,7 @@ static void bt_hid_connection_status_changed_callback(BtStatus status, void* con
     hid_keyboard_set_connected_status(hid->hid_keyboard, connected);
     hid_media_set_connected_status(hid->hid_media, connected);
     hid_mouse_set_connected_status(hid->hid_mouse, connected);
+    hid_mouse_jiggler_set_connected_status(hid->hid_mouse_jiggler, connected);
     hid_tiktok_set_connected_status(hid->hid_tiktok, connected);
 }
 
@@ -104,10 +108,16 @@ Hid* hid_alloc(HidTransport transport) {
         submenu_add_item(
             app->device_type_submenu,
             "TikTok Controller",
-            BtHidSubmenuIndexTikTok,
+            HidSubmenuIndexTikTok,
             hid_submenu_callback,
             app);
     }
+    submenu_add_item(
+        app->device_type_submenu,
+        "Mouse Jiggler",
+        HidSubmenuIndexMouseJiggler,
+        hid_submenu_callback,
+        app);
     view_set_previous_callback(submenu_get_view(app->device_type_submenu), hid_exit);
     view_dispatcher_add_view(
         app->view_dispatcher, HidViewSubmenu, submenu_get_view(app->device_type_submenu));
@@ -160,6 +170,15 @@ Hid* hid_app_alloc_view(void* context) {
     view_dispatcher_add_view(
         app->view_dispatcher, HidViewMouse, hid_mouse_get_view(app->hid_mouse));
 
+    // Mouse jiggler view
+    app->hid_mouse_jiggler = hid_mouse_jiggler_alloc(app);
+    view_set_previous_callback(
+        hid_mouse_jiggler_get_view(app->hid_mouse_jiggler), hid_exit_confirm_view);
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        HidViewMouseJiggler,
+        hid_mouse_jiggler_get_view(app->hid_mouse_jiggler));
+
     return app;
 }
 
@@ -182,6 +201,8 @@ void hid_free(Hid* app) {
     hid_media_free(app->hid_media);
     view_dispatcher_remove_view(app->view_dispatcher, HidViewMouse);
     hid_mouse_free(app->hid_mouse);
+    view_dispatcher_remove_view(app->view_dispatcher, HidViewMouseJiggler);
+    hid_mouse_jiggler_free(app->hid_mouse_jiggler);
     view_dispatcher_remove_view(app->view_dispatcher, BtHidViewTikTok);
     hid_tiktok_free(app->hid_tiktok);
     view_dispatcher_free(app->view_dispatcher);
@@ -346,9 +367,17 @@ int32_t hid_ble_app(void* p) {
     Hid* app = hid_alloc(HidTransportBle);
     app = hid_app_alloc_view(app);
 
+    bt_disconnect(app->bt);
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+
+    bt_keys_storage_set_storage_path(app->bt, HID_BT_KEYS_STORAGE_PATH);
+
     if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) {
-        FURI_LOG_E(TAG, "Failed to switch profile");
+        FURI_LOG_E(TAG, "Failed to switch to HID profile");
     }
+
     furi_hal_bt_start_advertising();
     bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app);
 
@@ -357,7 +386,17 @@ int32_t hid_ble_app(void* p) {
     view_dispatcher_run(app->view_dispatcher);
 
     bt_set_status_changed_callback(app->bt, NULL, NULL);
-    bt_set_profile(app->bt, BtProfileSerial);
+
+    bt_disconnect(app->bt);
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+
+    bt_keys_storage_set_default_path(app->bt);
+
+    if(!bt_set_profile(app->bt, BtProfileSerial)) {
+        FURI_LOG_E(TAG, "Failed to switch to Serial profile");
+    }
 
     hid_free(app);
 

+ 5 - 0
applications/plugins/hid_app/hid.h

@@ -11,6 +11,7 @@
 #include <gui/view.h>
 #include <gui/view_dispatcher.h>
 #include <notification/notification.h>
+#include <storage/storage.h>
 
 #include <gui/modules/submenu.h>
 #include <gui/modules/dialog_ex.h>
@@ -19,8 +20,11 @@
 #include "views/hid_keyboard.h"
 #include "views/hid_media.h"
 #include "views/hid_mouse.h"
+#include "views/hid_mouse_jiggler.h"
 #include "views/hid_tiktok.h"
 
+#define HID_BT_KEYS_STORAGE_PATH EXT_PATH("apps/Tools/.bt_hid.keys")
+
 typedef enum {
     HidTransportUsb,
     HidTransportBle,
@@ -39,6 +43,7 @@ struct Hid {
     HidKeyboard* hid_keyboard;
     HidMedia* hid_media;
     HidMouse* hid_mouse;
+    HidMouseJiggler* hid_mouse_jiggler;
     HidTikTok* hid_tiktok;
 
     HidTransport transport;

+ 1 - 0
applications/plugins/hid_app/views.h

@@ -4,6 +4,7 @@ typedef enum {
     HidViewKeyboard,
     HidViewMedia,
     HidViewMouse,
+    HidViewMouseJiggler,
     BtHidViewTikTok,
     HidViewExitConfirm,
 } HidView;

+ 149 - 0
applications/plugins/hid_app/views/hid_mouse_jiggler.c

@@ -0,0 +1,149 @@
+#include "hid_mouse_jiggler.h"
+#include <gui/elements.h>
+#include "../hid.h"
+
+#include "hid_icons.h"
+
+#define TAG "HidMouseJiggler"
+
+struct HidMouseJiggler {
+    View* view;
+    Hid* hid;
+    FuriTimer* timer;
+};
+
+typedef struct {
+    bool connected;
+    bool running;
+    uint8_t counter;
+} HidMouseJigglerModel;
+
+static void hid_mouse_jiggler_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    HidMouseJigglerModel* model = context;
+
+    // Header
+    if(model->connected) {
+        canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
+    } else {
+        canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
+    }
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Mouse Jiggler");
+
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text(canvas, AlignLeft, 35, "Press Start\nto jiggle");
+    canvas_set_font(canvas, FontSecondary);
+
+    // Ok
+    canvas_draw_icon(canvas, 63, 25, &I_Space_65x18);
+    if(model->running) {
+        elements_slightly_rounded_box(canvas, 66, 27, 60, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9);
+    if(model->running) {
+        elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Stop");
+    } else {
+        elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Start");
+    }
+    canvas_set_color(canvas, ColorBlack);
+
+    // Back
+    canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8);
+    elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Quit");
+}
+
+static void hid_mouse_jiggler_timer_callback(void* context) {
+    furi_assert(context);
+    HidMouseJiggler* hid_mouse_jiggler = context;
+    with_view_model(
+        hid_mouse_jiggler->view,
+        HidMouseJigglerModel * model,
+        {
+            if(model->running) {
+                model->counter++;
+                hid_hal_mouse_move(
+                    hid_mouse_jiggler->hid,
+                    (model->counter % 2 == 0) ? MOUSE_MOVE_SHORT : -MOUSE_MOVE_SHORT,
+                    0);
+            }
+        },
+        false);
+}
+
+static void hid_mouse_jiggler_enter_callback(void* context) {
+    furi_assert(context);
+    HidMouseJiggler* hid_mouse_jiggler = context;
+
+    furi_timer_start(hid_mouse_jiggler->timer, 500);
+}
+
+static void hid_mouse_jiggler_exit_callback(void* context) {
+    furi_assert(context);
+    HidMouseJiggler* hid_mouse_jiggler = context;
+    furi_timer_stop(hid_mouse_jiggler->timer);
+}
+
+static bool hid_mouse_jiggler_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    HidMouseJiggler* hid_mouse_jiggler = context;
+
+    bool consumed = false;
+
+    if(event->key == InputKeyOk) {
+        with_view_model(
+            hid_mouse_jiggler->view,
+            HidMouseJigglerModel * model,
+            { model->running = !model->running; },
+            true);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+HidMouseJiggler* hid_mouse_jiggler_alloc(Hid* hid) {
+    HidMouseJiggler* hid_mouse_jiggler = malloc(sizeof(HidMouseJiggler));
+
+    hid_mouse_jiggler->view = view_alloc();
+    view_set_context(hid_mouse_jiggler->view, hid_mouse_jiggler);
+    view_allocate_model(
+        hid_mouse_jiggler->view, ViewModelTypeLocking, sizeof(HidMouseJigglerModel));
+    view_set_draw_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_draw_callback);
+    view_set_input_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_input_callback);
+    view_set_enter_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_enter_callback);
+    view_set_exit_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_exit_callback);
+
+    hid_mouse_jiggler->hid = hid;
+
+    hid_mouse_jiggler->timer = furi_timer_alloc(
+        hid_mouse_jiggler_timer_callback, FuriTimerTypePeriodic, hid_mouse_jiggler);
+
+    return hid_mouse_jiggler;
+}
+
+void hid_mouse_jiggler_free(HidMouseJiggler* hid_mouse_jiggler) {
+    furi_assert(hid_mouse_jiggler);
+
+    furi_timer_stop(hid_mouse_jiggler->timer);
+    furi_timer_free(hid_mouse_jiggler->timer);
+
+    view_free(hid_mouse_jiggler->view);
+
+    free(hid_mouse_jiggler);
+}
+
+View* hid_mouse_jiggler_get_view(HidMouseJiggler* hid_mouse_jiggler) {
+    furi_assert(hid_mouse_jiggler);
+    return hid_mouse_jiggler->view;
+}
+
+void hid_mouse_jiggler_set_connected_status(HidMouseJiggler* hid_mouse_jiggler, bool connected) {
+    furi_assert(hid_mouse_jiggler);
+    with_view_model(
+        hid_mouse_jiggler->view,
+        HidMouseJigglerModel * model,
+        { model->connected = connected; },
+        true);
+}

+ 17 - 0
applications/plugins/hid_app/views/hid_mouse_jiggler.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <gui/view.h>
+
+#define MOUSE_MOVE_SHORT 5
+#define MOUSE_MOVE_LONG 20
+
+typedef struct Hid Hid;
+typedef struct HidMouseJiggler HidMouseJiggler;
+
+HidMouseJiggler* hid_mouse_jiggler_alloc(Hid* bt_hid);
+
+void hid_mouse_jiggler_free(HidMouseJiggler* hid_mouse_jiggler);
+
+View* hid_mouse_jiggler_get_view(HidMouseJiggler* hid_mouse_jiggler);
+
+void hid_mouse_jiggler_set_connected_status(HidMouseJiggler* hid_mouse_jiggler, bool connected);

+ 1 - 0
applications/plugins/music_player/music_player.c

@@ -313,6 +313,7 @@ int32_t music_player_app(void* p) {
             dialog_file_browser_set_basic_options(
                 &browser_options, MUSIC_PLAYER_APP_EXTENSION, &I_music_10px);
             browser_options.hide_ext = false;
+            browser_options.base_path = MUSIC_PLAYER_APP_PATH_FOLDER;
 
             DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
             bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);

+ 42 - 38
applications/plugins/music_player/music_player_worker.c

@@ -47,47 +47,51 @@ static int32_t music_player_worker_thread_callback(void* context) {
 
     NoteBlockArray_it_t it;
     NoteBlockArray_it(it, instance->notes);
-
-    while(instance->should_work) {
-        if(NoteBlockArray_end_p(it)) {
-            NoteBlockArray_it(it, instance->notes);
-            furi_delay_ms(10);
-        } else {
-            NoteBlock* note_block = NoteBlockArray_ref(it);
-
-            float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE;
-            float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4);
-            float duration =
-                60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm / note_block->duration;
-            uint32_t dots = note_block->dots;
-            while(dots > 0) {
-                duration += duration / 2;
-                dots--;
-            }
-            uint32_t next_tick = furi_get_tick() + duration;
-            float volume = instance->volume;
-
-            if(instance->callback) {
-                instance->callback(
-                    note_block->semitone,
-                    note_block->dots,
-                    note_block->duration,
-                    0.0,
-                    instance->callback_context);
-            }
-
-            furi_hal_speaker_stop();
-            furi_hal_speaker_start(frequency, volume);
-            while(instance->should_work && furi_get_tick() < next_tick) {
-                volume *= 0.9945679;
-                furi_hal_speaker_set_volume(volume);
-                furi_delay_ms(2);
+    if(furi_hal_speaker_acquire(1000)) {
+        while(instance->should_work) {
+            if(NoteBlockArray_end_p(it)) {
+                NoteBlockArray_it(it, instance->notes);
+                furi_delay_ms(10);
+            } else {
+                NoteBlock* note_block = NoteBlockArray_ref(it);
+
+                float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE;
+                float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4);
+                float duration = 60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm /
+                                 note_block->duration;
+                uint32_t dots = note_block->dots;
+                while(dots > 0) {
+                    duration += duration / 2;
+                    dots--;
+                }
+                uint32_t next_tick = furi_get_tick() + duration;
+                float volume = instance->volume;
+
+                if(instance->callback) {
+                    instance->callback(
+                        note_block->semitone,
+                        note_block->dots,
+                        note_block->duration,
+                        0.0,
+                        instance->callback_context);
+                }
+
+                furi_hal_speaker_stop();
+                furi_hal_speaker_start(frequency, volume);
+                while(instance->should_work && furi_get_tick() < next_tick) {
+                    volume *= 0.9945679;
+                    furi_hal_speaker_set_volume(volume);
+                    furi_delay_ms(2);
+                }
+                NoteBlockArray_next(it);
             }
-            NoteBlockArray_next(it);
         }
-    }
 
-    furi_hal_speaker_stop();
+        furi_hal_speaker_stop();
+        furi_hal_speaker_release();
+    } else {
+        FURI_LOG_E(TAG, "Speaker system is busy with another process.");
+    }
 
     return 0;
 }

+ 1 - 0
applications/plugins/nfc_magic/nfc_magic.c

@@ -49,6 +49,7 @@ NfcMagic* nfc_magic_alloc() {
 
     // Nfc device
     nfc_magic->nfc_dev = nfc_device_alloc();
+    furi_string_set(nfc_magic->nfc_dev->folder, NFC_APP_FOLDER);
 
     // Open GUI record
     nfc_magic->gui = furi_record_open(RECORD_GUI);

+ 2 - 0
applications/plugins/nfc_magic/nfc_magic_i.h

@@ -27,6 +27,8 @@
 #include <lib/nfc/nfc_device.h>
 #include "nfc_magic_icons.h"
 
+#define NFC_APP_FOLDER ANY_PATH("nfc")
+
 enum NfcMagicCustomEvent {
     // Reserve first 100 events for button types and indexes, starting from 0
     NfcMagicCustomEventReserved = 100,

+ 1 - 0
applications/plugins/picopass/picopass_device.c

@@ -231,6 +231,7 @@ bool picopass_file_select(PicopassDevice* dev) {
 
     DialogsFileBrowserOptions browser_options;
     dialog_file_browser_set_basic_options(&browser_options, PICOPASS_APP_EXTENSION, &I_Nfc_10px);
+    browser_options.base_path = PICOPASS_APP_FOLDER;
 
     bool res = dialog_file_browser_show(
         dev->dialogs, dev->load_path, picopass_app_folder, &browser_options);

+ 1 - 1
applications/plugins/weather_station/helpers/weather_station_types.h

@@ -3,7 +3,7 @@
 #include <furi.h>
 #include <furi_hal.h>
 
-#define WS_VERSION_APP "0.5"
+#define WS_VERSION_APP "0.6.1"
 #define WS_DEVELOPED "SkorP"
 #define WS_GITHUB "https://github.com/flipperdevices/flipperzero-firmware"
 

BIN
applications/plugins/weather_station/images/Humid_8x13.png


BIN
applications/plugins/weather_station/images/Timer_11x11.png


+ 2 - 2
applications/plugins/weather_station/protocols/ambient_weather.c

@@ -134,8 +134,8 @@ static void ws_protocol_ambient_weather_remote_controller(WSBlockGeneric* instan
     instance->id = (instance->data >> 32) & 0xFF;
     instance->battery_low = (instance->data >> 31) & 1;
     instance->channel = ((instance->data >> 28) & 0x07) + 1;
-    instance->temp = ws_block_generic_fahrenheit_to_celsius(
-        ((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f);
+    instance->temp =
+        locale_fahrenheit_to_celsius(((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f);
     instance->humidity = (instance->data >> 8) & 0xFF;
     instance->btn = WS_NO_BTN;
 

+ 2 - 2
applications/plugins/weather_station/protocols/infactory.c

@@ -143,8 +143,8 @@ static void ws_protocol_infactory_remote_controller(WSBlockGeneric* instance) {
     instance->id = instance->data >> 32;
     instance->battery_low = (instance->data >> 26) & 1;
     instance->btn = WS_NO_BTN;
-    instance->temp = ws_block_generic_fahrenheit_to_celsius(
-        ((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f);
+    instance->temp =
+        locale_fahrenheit_to_celsius(((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f);
     instance->humidity =
         (((instance->data >> 8) & 0x0F) * 10) + ((instance->data >> 4) & 0x0F); // BCD, 'A0'=100%rH
     instance->channel = instance->data & 0x03;

+ 4 - 0
applications/plugins/weather_station/protocols/nexus_th.c

@@ -135,6 +135,10 @@ static void ws_protocol_nexus_th_remote_controller(WSBlockGeneric* instance) {
     }
 
     instance->humidity = instance->data & 0xFF;
+    if(instance->humidity > 95)
+        instance->humidity = 95;
+    else if(instance->humidity < 20)
+        instance->humidity = 20;
 }
 
 void ws_protocol_decoder_nexus_th_feed(void* context, bool level, uint32_t duration) {

+ 28 - 9
applications/plugins/weather_station/protocols/oregon2.c

@@ -52,6 +52,11 @@ static const SubGhzBlockConst ws_oregon2_const = {
 #define ID_UV800 0xd874
 #define ID_THN129 0xcc43 // THN129 Temp only
 #define ID_RTHN129 0x0cd3 // RTHN129 Temp, clock sensors
+#define ID_RTHN129_1 0x9cd3
+#define ID_RTHN129_2 0xacd3
+#define ID_RTHN129_3 0xbcd3
+#define ID_RTHN129_4 0xccd3
+#define ID_RTHN129_5 0xdcd3
 #define ID_BTHGN129 0x5d53 // Baro, Temp, Hygro sensor
 #define ID_UVR128 0xec70
 #define ID_THGR328N 0xcc23 // Temp & Hygro sensor similar to THR228N with 5 channel instead of 3
@@ -137,11 +142,19 @@ static ManchesterEvent level_and_duration_to_event(bool level, uint32_t duration
 // From sensor id code return amount of bits in variable section
 // https://temofeev.ru/info/articles/o-dekodirovanii-protokola-pogodnykh-datchikov-oregon-scientific
 static uint8_t oregon2_sensor_id_var_bits(uint16_t sensor_id) {
-    if(sensor_id == ID_THR228N) return 16;
-
-    if(sensor_id == ID_THGR122N) return 24;
-
-    return 0;
+    switch(sensor_id) {
+    case ID_THR228N:
+    case ID_RTHN129_1:
+    case ID_RTHN129_2:
+    case ID_RTHN129_3:
+    case ID_RTHN129_4:
+    case ID_RTHN129_5:
+        return 16;
+    case ID_THGR122N:
+        return 24;
+    default:
+        return 0;
+    }
 }
 
 static void ws_oregon2_decode_const_data(WSBlockGeneric* ws_block) {
@@ -171,16 +184,22 @@ static float ws_oregon2_decode_temp(uint32_t data) {
 }
 
 static void ws_oregon2_decode_var_data(WSBlockGeneric* ws_b, uint16_t sensor_id, uint32_t data) {
-    if(sensor_id == ID_THR228N) {
+    switch(sensor_id) {
+    case ID_THR228N:
+    case ID_RTHN129_1:
+    case ID_RTHN129_2:
+    case ID_RTHN129_3:
+    case ID_RTHN129_4:
+    case ID_RTHN129_5:
         ws_b->temp = ws_oregon2_decode_temp(data);
         ws_b->humidity = WS_NO_HUMIDITY;
         return;
-    }
-
-    if(sensor_id == ID_THGR122N) {
+    case ID_THGR122N:
         ws_b->humidity = bcd_decode_short(data);
         ws_b->temp = ws_oregon2_decode_temp(data >> 8);
         return;
+    default:
+        break;
     }
 }
 

+ 331 - 0
applications/plugins/weather_station/protocols/oregon_v1.c

@@ -0,0 +1,331 @@
+#include "oregon_v1.h"
+#include <lib/toolbox/manchester_decoder.h>
+
+#define TAG "WSProtocolOregon_V1"
+
+/*
+ * Help
+ * https://github.dev/merbanan/rtl_433/blob/bb1be7f186ac0fdb7dc5d77693847d96fb95281e/src/devices/oregon_scientific_v1.c
+ * 
+ * OSv1 protocol.
+ * 
+ * MC with nominal bit width of 2930 us.
+ * Pulses are somewhat longer than nominal half-bit width, 1748 us / 3216 us,
+ * Gaps are somewhat shorter than nominal half-bit width, 1176 us / 2640 us.
+ * After 12 preamble bits there is 4200 us gap, 5780 us pulse, 5200 us gap.
+ * And next 32 bit data
+ * 
+ * Care must be taken with the gap after the sync pulse since it
+ * is outside of the normal clocking.  Because of this a data stream
+ * beginning with a 0 will have data in this gap.   
+ * 
+ * 
+ * Data is in reverse order of bits
+ *      RevBit(data32bit)=> tib23atad
+ * 
+ *      tib23atad => xxxxxxxx | busuTTTT | ttttzzzz | ccuuiiii 
+ *
+ *      - i: ID
+ *      - x: CRC;
+ *      - u: unknown;
+ *      - b: battery low; flag to indicate low battery voltage
+ *      - s: temperature sign
+ *      - T: BCD, Temperature; in °C * 10
+ *      - t: BCD, Temperature; in °C * 1
+ *      - z: BCD, Temperature; in °C * 0.1
+ *      - c: Channel 00=CH1, 01=CH2, 10=CH3
+ * 
+ */
+
+#define OREGON_V1_HEADER_OK 0xFF
+
+static const SubGhzBlockConst ws_protocol_oregon_v1_const = {
+    .te_short = 1465,
+    .te_long = 2930,
+    .te_delta = 350,
+    .min_count_bit_for_found = 32,
+};
+
+struct WSProtocolDecoderOregon_V1 {
+    SubGhzProtocolDecoderBase base;
+
+    SubGhzBlockDecoder decoder;
+    WSBlockGeneric generic;
+    ManchesterState manchester_state;
+    uint16_t header_count;
+    uint8_t first_bit;
+};
+
+struct WSProtocolEncoderOregon_V1 {
+    SubGhzProtocolEncoderBase base;
+
+    SubGhzProtocolBlockEncoder encoder;
+    WSBlockGeneric generic;
+};
+
+typedef enum {
+    Oregon_V1DecoderStepReset = 0,
+    Oregon_V1DecoderStepFoundPreamble,
+    Oregon_V1DecoderStepParse,
+} Oregon_V1DecoderStep;
+
+const SubGhzProtocolDecoder ws_protocol_oregon_v1_decoder = {
+    .alloc = ws_protocol_decoder_oregon_v1_alloc,
+    .free = ws_protocol_decoder_oregon_v1_free,
+
+    .feed = ws_protocol_decoder_oregon_v1_feed,
+    .reset = ws_protocol_decoder_oregon_v1_reset,
+
+    .get_hash_data = ws_protocol_decoder_oregon_v1_get_hash_data,
+    .serialize = ws_protocol_decoder_oregon_v1_serialize,
+    .deserialize = ws_protocol_decoder_oregon_v1_deserialize,
+    .get_string = ws_protocol_decoder_oregon_v1_get_string,
+};
+
+const SubGhzProtocolEncoder ws_protocol_oregon_v1_encoder = {
+    .alloc = NULL,
+    .free = NULL,
+
+    .deserialize = NULL,
+    .stop = NULL,
+    .yield = NULL,
+};
+
+const SubGhzProtocol ws_protocol_oregon_v1 = {
+    .name = WS_PROTOCOL_OREGON_V1_NAME,
+    .type = SubGhzProtocolWeatherStation,
+    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
+            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,
+
+    .decoder = &ws_protocol_oregon_v1_decoder,
+    .encoder = &ws_protocol_oregon_v1_encoder,
+};
+
+void* ws_protocol_decoder_oregon_v1_alloc(SubGhzEnvironment* environment) {
+    UNUSED(environment);
+    WSProtocolDecoderOregon_V1* instance = malloc(sizeof(WSProtocolDecoderOregon_V1));
+    instance->base.protocol = &ws_protocol_oregon_v1;
+    instance->generic.protocol_name = instance->base.protocol->name;
+    return instance;
+}
+
+void ws_protocol_decoder_oregon_v1_free(void* context) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    free(instance);
+}
+
+void ws_protocol_decoder_oregon_v1_reset(void* context) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    instance->decoder.parser_step = Oregon_V1DecoderStepReset;
+}
+
+static bool ws_protocol_oregon_v1_check(WSProtocolDecoderOregon_V1* instance) {
+    if(!instance->decoder.decode_data) return false;
+    uint64_t data = subghz_protocol_blocks_reverse_key(instance->decoder.decode_data, 32);
+    uint16_t crc = (data & 0xff) + ((data >> 8) & 0xff) + ((data >> 16) & 0xff);
+    crc = (crc & 0xff) + ((crc >> 8) & 0xff);
+    return (crc == ((data >> 24) & 0xFF));
+}
+
+/**
+ * Analysis of received data
+ * @param instance Pointer to a WSBlockGeneric* instance
+ */
+static void ws_protocol_oregon_v1_remote_controller(WSBlockGeneric* instance) {
+    uint64_t data = subghz_protocol_blocks_reverse_key(instance->data, 32);
+
+    instance->id = data & 0xFF;
+    instance->channel = ((data >> 6) & 0x03) + 1;
+
+    float temp_raw =
+        ((data >> 8) & 0x0F) * 0.1f + ((data >> 12) & 0x0F) + ((data >> 16) & 0x0F) * 10.0f;
+    if(!((data >> 21) & 1)) {
+        instance->temp = temp_raw;
+    } else {
+        instance->temp = -temp_raw;
+    }
+
+    instance->battery_low = !(instance->data >> 23) & 1;
+
+    instance->btn = WS_NO_BTN;
+    instance->humidity = WS_NO_HUMIDITY;
+}
+
+void ws_protocol_decoder_oregon_v1_feed(void* context, bool level, uint32_t duration) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    ManchesterEvent event = ManchesterEventReset;
+    switch(instance->decoder.parser_step) {
+    case Oregon_V1DecoderStepReset:
+        if((level) && (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
+                       ws_protocol_oregon_v1_const.te_delta)) {
+            instance->decoder.parser_step = Oregon_V1DecoderStepFoundPreamble;
+            instance->decoder.te_last = duration;
+            instance->header_count = 0;
+        }
+        break;
+    case Oregon_V1DecoderStepFoundPreamble:
+        if(level) {
+            //keep high levels, if they suit our durations
+            if((DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
+                ws_protocol_oregon_v1_const.te_delta) ||
+               (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short * 4) <
+                ws_protocol_oregon_v1_const.te_delta)) {
+                instance->decoder.te_last = duration;
+            } else {
+                instance->decoder.parser_step = Oregon_V1DecoderStepReset;
+            }
+        } else if(
+            //checking low levels
+            (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
+             ws_protocol_oregon_v1_const.te_delta) &&
+            (DURATION_DIFF(instance->decoder.te_last, ws_protocol_oregon_v1_const.te_short) <
+             ws_protocol_oregon_v1_const.te_delta)) {
+            // Found header
+            instance->header_count++;
+        } else if(
+            (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short * 3) <
+             ws_protocol_oregon_v1_const.te_delta) &&
+            (DURATION_DIFF(instance->decoder.te_last, ws_protocol_oregon_v1_const.te_short) <
+             ws_protocol_oregon_v1_const.te_delta)) {
+            // check header
+            if(instance->header_count > 7) {
+                instance->header_count = OREGON_V1_HEADER_OK;
+            }
+        } else if(
+            (instance->header_count == OREGON_V1_HEADER_OK) &&
+            (DURATION_DIFF(instance->decoder.te_last, ws_protocol_oregon_v1_const.te_short * 4) <
+             ws_protocol_oregon_v1_const.te_delta)) {
+            //found all the necessary patterns
+            instance->decoder.decode_data = 0;
+            instance->decoder.decode_count_bit = 1;
+            manchester_advance(
+                instance->manchester_state,
+                ManchesterEventReset,
+                &instance->manchester_state,
+                NULL);
+            instance->decoder.parser_step = Oregon_V1DecoderStepParse;
+            if(duration < ws_protocol_oregon_v1_const.te_short * 4) {
+                instance->first_bit = 1;
+            } else {
+                instance->first_bit = 0;
+            }
+        } else {
+            instance->decoder.parser_step = Oregon_V1DecoderStepReset;
+        }
+        break;
+    case Oregon_V1DecoderStepParse:
+        if(level) {
+            if(DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
+               ws_protocol_oregon_v1_const.te_delta) {
+                event = ManchesterEventShortHigh;
+            } else if(
+                DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_long) <
+                ws_protocol_oregon_v1_const.te_delta) {
+                event = ManchesterEventLongHigh;
+            } else {
+                instance->decoder.parser_step = Oregon_V1DecoderStepReset;
+            }
+        } else {
+            if(DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
+               ws_protocol_oregon_v1_const.te_delta) {
+                event = ManchesterEventShortLow;
+            } else if(
+                DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_long) <
+                ws_protocol_oregon_v1_const.te_delta) {
+                event = ManchesterEventLongLow;
+            } else if(duration >= ((uint32_t)ws_protocol_oregon_v1_const.te_long * 2)) {
+                if(instance->decoder.decode_count_bit ==
+                   ws_protocol_oregon_v1_const.min_count_bit_for_found) {
+                    if(instance->first_bit) {
+                        instance->decoder.decode_data = ~instance->decoder.decode_data | (1 << 31);
+                    }
+                    if(ws_protocol_oregon_v1_check(instance)) {
+                        instance->generic.data = instance->decoder.decode_data;
+                        instance->generic.data_count_bit = instance->decoder.decode_count_bit;
+                        ws_protocol_oregon_v1_remote_controller(&instance->generic);
+                        if(instance->base.callback)
+                            instance->base.callback(&instance->base, instance->base.context);
+                    }
+                }
+                instance->decoder.decode_data = 0;
+                instance->decoder.decode_count_bit = 0;
+                manchester_advance(
+                    instance->manchester_state,
+                    ManchesterEventReset,
+                    &instance->manchester_state,
+                    NULL);
+            } else {
+                instance->decoder.parser_step = Oregon_V1DecoderStepReset;
+            }
+        }
+        if(event != ManchesterEventReset) {
+            bool data;
+            bool data_ok = manchester_advance(
+                instance->manchester_state, event, &instance->manchester_state, &data);
+
+            if(data_ok) {
+                instance->decoder.decode_data = (instance->decoder.decode_data << 1) | !data;
+                instance->decoder.decode_count_bit++;
+            }
+        }
+
+        break;
+    }
+}
+
+uint8_t ws_protocol_decoder_oregon_v1_get_hash_data(void* context) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    return subghz_protocol_blocks_get_hash_data(
+        &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
+}
+
+bool ws_protocol_decoder_oregon_v1_serialize(
+    void* context,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    return ws_block_generic_serialize(&instance->generic, flipper_format, preset);
+}
+
+bool ws_protocol_decoder_oregon_v1_deserialize(void* context, FlipperFormat* flipper_format) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    bool ret = false;
+    do {
+        if(!ws_block_generic_deserialize(&instance->generic, flipper_format)) {
+            break;
+        }
+        if(instance->generic.data_count_bit !=
+           ws_protocol_oregon_v1_const.min_count_bit_for_found) {
+            FURI_LOG_E(TAG, "Wrong number of bits in key");
+            break;
+        }
+        ret = true;
+    } while(false);
+    return ret;
+}
+
+void ws_protocol_decoder_oregon_v1_get_string(void* context, FuriString* output) {
+    furi_assert(context);
+    WSProtocolDecoderOregon_V1* instance = context;
+    furi_string_printf(
+        output,
+        "%s %dbit\r\n"
+        "Key:0x%lX%08lX\r\n"
+        "Sn:0x%lX Ch:%d  Bat:%d\r\n"
+        "Temp:%3.1f C Hum:%d%%",
+        instance->generic.protocol_name,
+        instance->generic.data_count_bit,
+        (uint32_t)(instance->generic.data >> 32),
+        (uint32_t)(instance->generic.data),
+        instance->generic.id,
+        instance->generic.channel,
+        instance->generic.battery_low,
+        (double)instance->generic.temp,
+        instance->generic.humidity);
+}

+ 79 - 0
applications/plugins/weather_station/protocols/oregon_v1.h

@@ -0,0 +1,79 @@
+#pragma once
+
+#include <lib/subghz/protocols/base.h>
+
+#include <lib/subghz/blocks/const.h>
+#include <lib/subghz/blocks/decoder.h>
+#include <lib/subghz/blocks/encoder.h>
+#include "ws_generic.h"
+#include <lib/subghz/blocks/math.h>
+
+#define WS_PROTOCOL_OREGON_V1_NAME "Oregon-v1"
+
+typedef struct WSProtocolDecoderOregon_V1 WSProtocolDecoderOregon_V1;
+typedef struct WSProtocolEncoderOregon_V1 WSProtocolEncoderOregon_V1;
+
+extern const SubGhzProtocolDecoder ws_protocol_oregon_v1_decoder;
+extern const SubGhzProtocolEncoder ws_protocol_oregon_v1_encoder;
+extern const SubGhzProtocol ws_protocol_oregon_v1;
+
+/**
+ * Allocate WSProtocolDecoderOregon_V1.
+ * @param environment Pointer to a SubGhzEnvironment instance
+ * @return WSProtocolDecoderOregon_V1* pointer to a WSProtocolDecoderOregon_V1 instance
+ */
+void* ws_protocol_decoder_oregon_v1_alloc(SubGhzEnvironment* environment);
+
+/**
+ * Free WSProtocolDecoderOregon_V1.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ */
+void ws_protocol_decoder_oregon_v1_free(void* context);
+
+/**
+ * Reset decoder WSProtocolDecoderOregon_V1.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ */
+void ws_protocol_decoder_oregon_v1_reset(void* context);
+
+/**
+ * Parse a raw sequence of levels and durations received from the air.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ * @param level Signal level true-high false-low
+ * @param duration Duration of this level in, us
+ */
+void ws_protocol_decoder_oregon_v1_feed(void* context, bool level, uint32_t duration);
+
+/**
+ * Getting the hash sum of the last randomly received parcel.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ * @return hash Hash sum
+ */
+uint8_t ws_protocol_decoder_oregon_v1_get_hash_data(void* context);
+
+/**
+ * Serialize data WSProtocolDecoderOregon_V1.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ * @param flipper_format Pointer to a FlipperFormat instance
+ * @param preset The modulation on which the signal was received, SubGhzRadioPreset
+ * @return true On success
+ */
+bool ws_protocol_decoder_oregon_v1_serialize(
+    void* context,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset);
+
+/**
+ * Deserialize data WSProtocolDecoderOregon_V1.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ * @param flipper_format Pointer to a FlipperFormat instance
+ * @return true On success
+ */
+bool ws_protocol_decoder_oregon_v1_deserialize(void* context, FlipperFormat* flipper_format);
+
+/**
+ * Getting a textual representation of the received data.
+ * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
+ * @param output Resulting text
+ */
+void ws_protocol_decoder_oregon_v1_get_string(void* context, FuriString* output);

+ 2 - 0
applications/plugins/weather_station/protocols/protocol_items.c

@@ -13,6 +13,8 @@ const SubGhzProtocol* weather_station_protocol_registry_items[] = {
     &ws_protocol_acurite_592txr,
     &ws_protocol_ambient_weather,
     &ws_protocol_auriol_th,
+    &ws_protocol_oregon_v1,
+    &ws_protocol_tx_8300,
 };
 
 const SubGhzProtocolRegistry weather_station_protocol_registry = {

+ 2 - 0
applications/plugins/weather_station/protocols/protocol_items.h

@@ -13,5 +13,7 @@
 #include "acurite_592txr.h"
 #include "ambient_weather.h"
 #include "auriol_hg0601a.h"
+#include "oregon_v1.h"
+#include "tx_8300.h"
 
 extern const SubGhzProtocolRegistry weather_station_protocol_registry;

+ 293 - 0
applications/plugins/weather_station/protocols/tx_8300.c

@@ -0,0 +1,293 @@
+#include "tx_8300.h"
+
+#define TAG "WSProtocolTX_8300"
+
+/*
+ * Help
+ * https://github.com/merbanan/rtl_433/blob/master/src/devices/ambientweather_tx8300.c
+ *
+ * Ambient Weather TX-8300 (also sold as TFA 30.3211.02).
+ * 1970us pulse with variable gap (third pulse 3920 us).
+ * Above 79% humidity, gap after third pulse is 5848 us.
+ * - Bit 1 : 1970us pulse with 3888 us gap
+ * - Bit 0 : 1970us pulse with 1936 us gap
+ * 74 bit (2 bit preamble and 72 bit data => 9 bytes => 18 nibbles)
+ * The preamble seems to be a repeat counter (00, and 01 seen),
+ * the first 4 bytes are data,
+ * the second 4 bytes the same data inverted,
+ * the last byte is a checksum.
+ * Preamble format (2 bits):
+ *     [1 bit (0)] [1 bit rolling count]
+ * Payload format (32 bits):
+ *     HHHHhhhh ??CCNIII IIIITTTT ttttuuuu
+ * - H = First BCD digit humidity (the MSB might be distorted by the demod)
+ * - h = Second BCD digit humidity, invalid humidity seems to be 0x0e
+ * - ? = Likely battery flag, 2 bits
+ * - C = Channel, 2 bits
+ * - N = Negative temperature sign bit
+ * - I = ID, 7-bit
+ * - T = First BCD digit temperature
+ * - t = Second BCD digit temperature
+ * - u = Third BCD digit temperature
+ * The Checksum seems to covers the 4 data bytes and is something like Fletcher-8.
+ **/
+
+#define TX_8300_PACKAGE_SIZE 32
+
+static const SubGhzBlockConst ws_protocol_tx_8300_const = {
+    .te_short = 1940,
+    .te_long = 3880,
+    .te_delta = 250,
+    .min_count_bit_for_found = 72,
+};
+
+struct WSProtocolDecoderTX_8300 {
+    SubGhzProtocolDecoderBase base;
+
+    SubGhzBlockDecoder decoder;
+    WSBlockGeneric generic;
+    uint32_t package_1;
+    uint32_t package_2;
+};
+
+struct WSProtocolEncoderTX_8300 {
+    SubGhzProtocolEncoderBase base;
+
+    SubGhzProtocolBlockEncoder encoder;
+    WSBlockGeneric generic;
+};
+
+typedef enum {
+    TX_8300DecoderStepReset = 0,
+    TX_8300DecoderStepCheckPreambule,
+    TX_8300DecoderStepSaveDuration,
+    TX_8300DecoderStepCheckDuration,
+} TX_8300DecoderStep;
+
+const SubGhzProtocolDecoder ws_protocol_tx_8300_decoder = {
+    .alloc = ws_protocol_decoder_tx_8300_alloc,
+    .free = ws_protocol_decoder_tx_8300_free,
+
+    .feed = ws_protocol_decoder_tx_8300_feed,
+    .reset = ws_protocol_decoder_tx_8300_reset,
+
+    .get_hash_data = ws_protocol_decoder_tx_8300_get_hash_data,
+    .serialize = ws_protocol_decoder_tx_8300_serialize,
+    .deserialize = ws_protocol_decoder_tx_8300_deserialize,
+    .get_string = ws_protocol_decoder_tx_8300_get_string,
+};
+
+const SubGhzProtocolEncoder ws_protocol_tx_8300_encoder = {
+    .alloc = NULL,
+    .free = NULL,
+
+    .deserialize = NULL,
+    .stop = NULL,
+    .yield = NULL,
+};
+
+const SubGhzProtocol ws_protocol_tx_8300 = {
+    .name = WS_PROTOCOL_TX_8300_NAME,
+    .type = SubGhzProtocolWeatherStation,
+    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
+            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,
+
+    .decoder = &ws_protocol_tx_8300_decoder,
+    .encoder = &ws_protocol_tx_8300_encoder,
+};
+
+void* ws_protocol_decoder_tx_8300_alloc(SubGhzEnvironment* environment) {
+    UNUSED(environment);
+    WSProtocolDecoderTX_8300* instance = malloc(sizeof(WSProtocolDecoderTX_8300));
+    instance->base.protocol = &ws_protocol_tx_8300;
+    instance->generic.protocol_name = instance->base.protocol->name;
+    return instance;
+}
+
+void ws_protocol_decoder_tx_8300_free(void* context) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+    free(instance);
+}
+
+void ws_protocol_decoder_tx_8300_reset(void* context) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+    instance->decoder.parser_step = TX_8300DecoderStepReset;
+}
+
+static bool ws_protocol_tx_8300_check_crc(WSProtocolDecoderTX_8300* instance) {
+    if(!instance->package_2) return false;
+    if(instance->package_1 != ~instance->package_2) return false;
+
+    uint16_t x = 0;
+    uint16_t y = 0;
+    for(int i = 0; i < 32; i += 4) {
+        x += (instance->package_1 >> i) & 0x0F;
+        y += (instance->package_1 >> i) & 0x05;
+    }
+    uint8_t crc = (~x & 0xF) << 4 | (~y & 0xF);
+    return (crc == ((instance->decoder.decode_data) & 0xFF));
+}
+
+/**
+ * Analysis of received data
+ * @param instance Pointer to a WSBlockGeneric* instance
+ */
+static void ws_protocol_tx_8300_remote_controller(WSBlockGeneric* instance) {
+    instance->humidity = (((instance->data >> 28) & 0x0F) * 10) + ((instance->data >> 24) & 0x0F);
+    instance->btn = WS_NO_BTN;
+    if(!((instance->data >> 22) & 0x03))
+        instance->battery_low = 0;
+    else
+        instance->battery_low = 1;
+    instance->channel = (instance->data >> 20) & 0x03;
+    instance->id = (instance->data >> 12) & 0x7F;
+
+    float temp_raw = ((instance->data >> 8) & 0x0F) * 10.0f + ((instance->data >> 4) & 0x0F) +
+                     (instance->data & 0x0F) * 0.1f;
+    if(!((instance->data >> 19) & 1)) {
+        instance->temp = temp_raw;
+    } else {
+        instance->temp = -temp_raw;
+    }
+}
+
+void ws_protocol_decoder_tx_8300_feed(void* context, bool level, uint32_t duration) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+
+    switch(instance->decoder.parser_step) {
+    case TX_8300DecoderStepReset:
+        if((level) && (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short * 2) <
+                       ws_protocol_tx_8300_const.te_delta)) {
+            instance->decoder.parser_step = TX_8300DecoderStepCheckPreambule;
+        }
+        break;
+
+    case TX_8300DecoderStepCheckPreambule:
+        if((!level) && ((DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short * 2) <
+                         ws_protocol_tx_8300_const.te_delta) ||
+                        (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short * 3) <
+                         ws_protocol_tx_8300_const.te_delta))) {
+            instance->decoder.parser_step = TX_8300DecoderStepSaveDuration;
+            instance->decoder.decode_data = 0;
+            instance->decoder.decode_count_bit = 1;
+            instance->package_1 = 0;
+            instance->package_2 = 0;
+        } else {
+            instance->decoder.parser_step = TX_8300DecoderStepReset;
+        }
+        break;
+
+    case TX_8300DecoderStepSaveDuration:
+        if(level) {
+            instance->decoder.te_last = duration;
+            instance->decoder.parser_step = TX_8300DecoderStepCheckDuration;
+        } else {
+            instance->decoder.parser_step = TX_8300DecoderStepReset;
+        }
+        break;
+
+    case TX_8300DecoderStepCheckDuration:
+        if(!level) {
+            if(duration >= ((uint32_t)ws_protocol_tx_8300_const.te_short * 5)) {
+                //Found syncPostfix
+                if((instance->decoder.decode_count_bit ==
+                    ws_protocol_tx_8300_const.min_count_bit_for_found) &&
+                   ws_protocol_tx_8300_check_crc(instance)) {
+                    instance->generic.data = instance->package_1;
+                    instance->generic.data_count_bit = instance->decoder.decode_count_bit;
+                    ws_protocol_tx_8300_remote_controller(&instance->generic);
+                    if(instance->base.callback)
+                        instance->base.callback(&instance->base, instance->base.context);
+                }
+                instance->decoder.decode_data = 0;
+                instance->decoder.decode_count_bit = 1;
+                instance->decoder.parser_step = TX_8300DecoderStepReset;
+                break;
+            } else if(
+                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_tx_8300_const.te_short) <
+                 ws_protocol_tx_8300_const.te_delta) &&
+                (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_long) <
+                 ws_protocol_tx_8300_const.te_delta * 2)) {
+                subghz_protocol_blocks_add_bit(&instance->decoder, 1);
+                instance->decoder.parser_step = TX_8300DecoderStepSaveDuration;
+            } else if(
+                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_tx_8300_const.te_short) <
+                 ws_protocol_tx_8300_const.te_delta) &&
+                (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short) <
+                 ws_protocol_tx_8300_const.te_delta)) {
+                subghz_protocol_blocks_add_bit(&instance->decoder, 0);
+                instance->decoder.parser_step = TX_8300DecoderStepSaveDuration;
+            } else {
+                instance->decoder.parser_step = TX_8300DecoderStepReset;
+            }
+
+            if(instance->decoder.decode_count_bit == TX_8300_PACKAGE_SIZE) {
+                instance->package_1 = instance->decoder.decode_data;
+                instance->decoder.decode_data = 0;
+            } else if(instance->decoder.decode_count_bit == TX_8300_PACKAGE_SIZE * 2) {
+                instance->package_2 = instance->decoder.decode_data;
+                instance->decoder.decode_data = 0;
+            }
+
+        } else {
+            instance->decoder.parser_step = TX_8300DecoderStepReset;
+        }
+        break;
+    }
+}
+
+uint8_t ws_protocol_decoder_tx_8300_get_hash_data(void* context) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+    return subghz_protocol_blocks_get_hash_data(
+        &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
+}
+
+bool ws_protocol_decoder_tx_8300_serialize(
+    void* context,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+    return ws_block_generic_serialize(&instance->generic, flipper_format, preset);
+}
+
+bool ws_protocol_decoder_tx_8300_deserialize(void* context, FlipperFormat* flipper_format) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+    bool ret = false;
+    do {
+        if(!ws_block_generic_deserialize(&instance->generic, flipper_format)) {
+            break;
+        }
+        if(instance->generic.data_count_bit != ws_protocol_tx_8300_const.min_count_bit_for_found) {
+            FURI_LOG_E(TAG, "Wrong number of bits in key");
+            break;
+        }
+        ret = true;
+    } while(false);
+    return ret;
+}
+
+void ws_protocol_decoder_tx_8300_get_string(void* context, FuriString* output) {
+    furi_assert(context);
+    WSProtocolDecoderTX_8300* instance = context;
+    furi_string_printf(
+        output,
+        "%s %dbit\r\n"
+        "Key:0x%lX%08lX\r\n"
+        "Sn:0x%lX Ch:%d  Bat:%d\r\n"
+        "Temp:%3.1f C Hum:%d%%",
+        instance->generic.protocol_name,
+        instance->generic.data_count_bit,
+        (uint32_t)(instance->generic.data >> 32),
+        (uint32_t)(instance->generic.data),
+        instance->generic.id,
+        instance->generic.channel,
+        instance->generic.battery_low,
+        (double)instance->generic.temp,
+        instance->generic.humidity);
+}

+ 79 - 0
applications/plugins/weather_station/protocols/tx_8300.h

@@ -0,0 +1,79 @@
+#pragma once
+
+#include <lib/subghz/protocols/base.h>
+
+#include <lib/subghz/blocks/const.h>
+#include <lib/subghz/blocks/decoder.h>
+#include <lib/subghz/blocks/encoder.h>
+#include "ws_generic.h"
+#include <lib/subghz/blocks/math.h>
+
+#define WS_PROTOCOL_TX_8300_NAME "TX8300"
+
+typedef struct WSProtocolDecoderTX_8300 WSProtocolDecoderTX_8300;
+typedef struct WSProtocolEncoderTX_8300 WSProtocolEncoderTX_8300;
+
+extern const SubGhzProtocolDecoder ws_protocol_tx_8300_decoder;
+extern const SubGhzProtocolEncoder ws_protocol_tx_8300_encoder;
+extern const SubGhzProtocol ws_protocol_tx_8300;
+
+/**
+ * Allocate WSProtocolDecoderTX_8300.
+ * @param environment Pointer to a SubGhzEnvironment instance
+ * @return WSProtocolDecoderTX_8300* pointer to a WSProtocolDecoderTX_8300 instance
+ */
+void* ws_protocol_decoder_tx_8300_alloc(SubGhzEnvironment* environment);
+
+/**
+ * Free WSProtocolDecoderTX_8300.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ */
+void ws_protocol_decoder_tx_8300_free(void* context);
+
+/**
+ * Reset decoder WSProtocolDecoderTX_8300.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ */
+void ws_protocol_decoder_tx_8300_reset(void* context);
+
+/**
+ * Parse a raw sequence of levels and durations received from the air.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ * @param level Signal level true-high false-low
+ * @param duration Duration of this level in, us
+ */
+void ws_protocol_decoder_tx_8300_feed(void* context, bool level, uint32_t duration);
+
+/**
+ * Getting the hash sum of the last randomly received parcel.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ * @return hash Hash sum
+ */
+uint8_t ws_protocol_decoder_tx_8300_get_hash_data(void* context);
+
+/**
+ * Serialize data WSProtocolDecoderTX_8300.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ * @param flipper_format Pointer to a FlipperFormat instance
+ * @param preset The modulation on which the signal was received, SubGhzRadioPreset
+ * @return true On success
+ */
+bool ws_protocol_decoder_tx_8300_serialize(
+    void* context,
+    FlipperFormat* flipper_format,
+    SubGhzRadioPreset* preset);
+
+/**
+ * Deserialize data WSProtocolDecoderTX_8300.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ * @param flipper_format Pointer to a FlipperFormat instance
+ * @return true On success
+ */
+bool ws_protocol_decoder_tx_8300_deserialize(void* context, FlipperFormat* flipper_format);
+
+/**
+ * Getting a textual representation of the received data.
+ * @param context Pointer to a WSProtocolDecoderTX_8300 instance
+ * @param output Resulting text
+ */
+void ws_protocol_decoder_tx_8300_get_string(void* context, FuriString* output);

+ 17 - 4
applications/plugins/weather_station/protocols/ws_generic.c

@@ -99,6 +99,17 @@ bool ws_block_generic_serialize(
             break;
         }
 
+        //DATE AGE set
+        FuriHalRtcDateTime curr_dt;
+        furi_hal_rtc_get_datetime(&curr_dt);
+        uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
+
+        temp_data = curr_ts;
+        if(!flipper_format_write_uint32(flipper_format, "Ts", &temp_data, 1)) {
+            FURI_LOG_E(TAG, "Unable to add timestamp");
+            break;
+        }
+
         temp_data = instance->channel;
         if(!flipper_format_write_uint32(flipper_format, "Ch", &temp_data, 1)) {
             FURI_LOG_E(TAG, "Unable to add Channel");
@@ -168,6 +179,12 @@ bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipp
         }
         instance->humidity = (uint8_t)temp_data;
 
+        if(!flipper_format_read_uint32(flipper_format, "Ts", (uint32_t*)&temp_data, 1)) {
+            FURI_LOG_E(TAG, "Missing timestamp");
+            break;
+        }
+        instance->timestamp = (uint32_t)temp_data;
+
         if(!flipper_format_read_uint32(flipper_format, "Ch", (uint32_t*)&temp_data, 1)) {
             FURI_LOG_E(TAG, "Missing Channel");
             break;
@@ -192,7 +209,3 @@ bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipp
 
     return res;
 }
-
-float ws_block_generic_fahrenheit_to_celsius(float fahrenheit) {
-    return (fahrenheit - 32.0f) / 1.8f;
-}

+ 2 - 2
applications/plugins/weather_station/protocols/ws_generic.h

@@ -8,6 +8,7 @@
 #include "furi.h"
 #include "furi_hal.h"
 #include <lib/subghz/types.h>
+#include <locale/locale.h>
 
 #ifdef __cplusplus
 extern "C" {
@@ -29,6 +30,7 @@ struct WSBlockGeneric {
     uint8_t data_count_bit;
     uint8_t battery_low;
     uint8_t humidity;
+    uint32_t timestamp;
     uint8_t channel;
     uint8_t btn;
     float temp;
@@ -61,8 +63,6 @@ bool ws_block_generic_serialize(
  */
 bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipper_format);
 
-float ws_block_generic_fahrenheit_to_celsius(float fahrenheit);
-
 #ifdef __cplusplus
 }
 #endif

+ 3 - 3
applications/plugins/weather_station/views/weather_station_receiver.c

@@ -8,7 +8,7 @@
 #include <m-array.h>
 
 #define FRAME_HEIGHT 12
-#define MAX_LEN_PX 100
+#define MAX_LEN_PX 112
 #define MENU_ITEMS 4u
 #define UNLOCK_CNT 3
 
@@ -189,7 +189,7 @@ void ws_view_receiver_draw(Canvas* canvas, WSReceiverModel* model) {
             canvas_set_color(canvas, ColorBlack);
         }
         canvas_draw_icon(canvas, 4, 2 + i * FRAME_HEIGHT, ReceiverItemIcons[item_menu->type]);
-        canvas_draw_str(canvas, 15, 9 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buff));
+        canvas_draw_str(canvas, 14, 9 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buff));
         furi_string_reset(str_buff);
     }
     if(scrollbar) {
@@ -303,7 +303,7 @@ bool ws_view_receiver_input(InputEvent* event, void* context) {
             ws_receiver->view,
             WSReceiverModel * model,
             {
-                if(model->idx != model->history_item - 1) model->idx++;
+                if(model->history_item && model->idx != model->history_item - 1) model->idx++;
             },
             true);
     } else if(event->key == InputKeyLeft && event->type == InputTypeShort) {

+ 101 - 15
applications/plugins/weather_station/views/weather_station_receiver_info.c

@@ -9,9 +9,11 @@
 
 struct WSReceiverInfo {
     View* view;
+    FuriTimer* timer;
 };
 
 typedef struct {
+    uint32_t curr_ts;
     FuriString* protocol_name;
     WSBlockGeneric* generic;
 } WSReceiverInfoModel;
@@ -28,6 +30,10 @@ void ws_view_receiver_info_update(WSReceiverInfo* ws_receiver_info, FlipperForma
             flipper_format_read_string(fff, "Protocol", model->protocol_name);
 
             ws_block_generic_deserialize(model->generic, fff);
+
+            FuriHalRtcDateTime curr_dt;
+            furi_hal_rtc_get_datetime(&curr_dt);
+            model->curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
         },
         true);
 }
@@ -44,46 +50,102 @@ void ws_view_receiver_info_draw(Canvas* canvas, WSReceiverInfoModel* model) {
         "%s %db",
         furi_string_get_cstr(model->protocol_name),
         model->generic->data_count_bit);
-    canvas_draw_str(canvas, 5, 8, buffer);
+    canvas_draw_str(canvas, 0, 8, buffer);
 
     if(model->generic->channel != WS_NO_CHANNEL) {
         snprintf(buffer, sizeof(buffer), "Ch: %01d", model->generic->channel);
-        canvas_draw_str(canvas, 105, 8, buffer);
+        canvas_draw_str(canvas, 106, 8, buffer);
     }
 
     if(model->generic->id != WS_NO_ID) {
         snprintf(buffer, sizeof(buffer), "Sn: 0x%02lX", model->generic->id);
-        canvas_draw_str(canvas, 5, 20, buffer);
+        canvas_draw_str(canvas, 0, 20, buffer);
     }
 
     if(model->generic->btn != WS_NO_BTN) {
         snprintf(buffer, sizeof(buffer), "Btn: %01d", model->generic->btn);
-        canvas_draw_str(canvas, 62, 20, buffer);
+        canvas_draw_str(canvas, 57, 20, buffer);
     }
 
     if(model->generic->battery_low != WS_NO_BATT) {
         snprintf(
             buffer, sizeof(buffer), "Batt: %s", (!model->generic->battery_low ? "ok" : "low"));
-        canvas_draw_str(canvas, 90, 20, buffer);
+        canvas_draw_str_aligned(canvas, 126, 17, AlignRight, AlignCenter, buffer);
     }
 
     snprintf(buffer, sizeof(buffer), "Data: 0x%llX", model->generic->data);
-    canvas_draw_str(canvas, 5, 32, buffer);
+    canvas_draw_str(canvas, 0, 32, buffer);
 
-    elements_bold_rounded_frame(canvas, 2, 37, 123, 25);
+    elements_bold_rounded_frame(canvas, 0, 38, 127, 25);
     canvas_set_font(canvas, FontPrimary);
 
     if(model->generic->temp != WS_NO_TEMPERATURE) {
-        canvas_draw_icon(canvas, 18, 42, &I_Therm_7x16);
-        snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp);
-        canvas_draw_str_aligned(canvas, 63, 46, AlignRight, AlignTop, buffer);
-        canvas_draw_circle(canvas, 55, 45, 1);
+        canvas_draw_icon(canvas, 6, 43, &I_Therm_7x16);
+
+        uint8_t temp_x1 = 0;
+        uint8_t temp_x2 = 0;
+        if(furi_hal_rtc_get_locale_units() == FuriHalRtcLocaleUnitsMetric) {
+            snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp);
+            if(model->generic->temp < -9.0f) {
+                temp_x1 = 49;
+                temp_x2 = 40;
+            } else {
+                temp_x1 = 47;
+                temp_x2 = 38;
+            }
+        } else {
+            snprintf(
+                buffer,
+                sizeof(buffer),
+                "%3.1f F",
+                (double)locale_celsius_to_fahrenheit(model->generic->temp));
+            if((model->generic->temp < -27.77f) || (model->generic->temp > 37.77f)) {
+                temp_x1 = 50;
+                temp_x2 = 42;
+            } else {
+                temp_x1 = 48;
+                temp_x2 = 40;
+            }
+        }
+
+        canvas_draw_str_aligned(canvas, temp_x1, 47, AlignRight, AlignTop, buffer);
+        canvas_draw_circle(canvas, temp_x2, 46, 1);
     }
 
     if(model->generic->humidity != WS_NO_HUMIDITY) {
-        canvas_draw_icon(canvas, 75, 42, &I_Humid_10x15);
+        canvas_draw_icon(canvas, 53, 44, &I_Humid_8x13);
         snprintf(buffer, sizeof(buffer), "%d%%", model->generic->humidity);
-        canvas_draw_str(canvas, 91, 54, buffer);
+        canvas_draw_str(canvas, 64, 55, buffer);
+    }
+
+    if((int)model->generic->timestamp > 0 && model->curr_ts) {
+        int ts_diff = (int)model->curr_ts - (int)model->generic->timestamp;
+
+        canvas_draw_icon(canvas, 91, 46, &I_Timer_11x11);
+
+        if(ts_diff > 60) {
+            int tmp_sec = ts_diff;
+            int cnt_min = 1;
+            for(int i = 1; tmp_sec > 60; i++) {
+                tmp_sec = tmp_sec - 60;
+                cnt_min = i;
+            }
+
+            if(model->curr_ts % 2 == 0) {
+                canvas_draw_str_aligned(canvas, 105, 51, AlignLeft, AlignCenter, "Old");
+            } else {
+                if(cnt_min >= 59) {
+                    canvas_draw_str_aligned(canvas, 105, 51, AlignLeft, AlignCenter, "Old");
+                } else {
+                    snprintf(buffer, sizeof(buffer), "%dm", cnt_min);
+                    canvas_draw_str_aligned(canvas, 114, 51, AlignCenter, AlignCenter, buffer);
+                }
+            }
+
+        } else {
+            snprintf(buffer, sizeof(buffer), "%d", ts_diff);
+            canvas_draw_str_aligned(canvas, 112, 51, AlignCenter, AlignCenter, buffer);
+        }
     }
 }
 
@@ -98,14 +160,19 @@ bool ws_view_receiver_info_input(InputEvent* event, void* context) {
     return true;
 }
 
-void ws_view_receiver_info_enter(void* context) {
+static void ws_view_receiver_info_enter(void* context) {
     furi_assert(context);
+    WSReceiverInfo* ws_receiver_info = context;
+
+    furi_timer_start(ws_receiver_info->timer, 1000);
 }
 
-void ws_view_receiver_info_exit(void* context) {
+static void ws_view_receiver_info_exit(void* context) {
     furi_assert(context);
     WSReceiverInfo* ws_receiver_info = context;
 
+    furi_timer_stop(ws_receiver_info->timer);
+
     with_view_model(
         ws_receiver_info->view,
         WSReceiverInfoModel * model,
@@ -113,6 +180,20 @@ void ws_view_receiver_info_exit(void* context) {
         false);
 }
 
+static void ws_view_receiver_info_timer(void* context) {
+    WSReceiverInfo* ws_receiver_info = context;
+    // Force redraw
+    with_view_model(
+        ws_receiver_info->view,
+        WSReceiverInfoModel * model,
+        {
+            FuriHalRtcDateTime curr_dt;
+            furi_hal_rtc_get_datetime(&curr_dt);
+            model->curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
+        },
+        true);
+}
+
 WSReceiverInfo* ws_view_receiver_info_alloc() {
     WSReceiverInfo* ws_receiver_info = malloc(sizeof(WSReceiverInfo));
 
@@ -135,12 +216,17 @@ WSReceiverInfo* ws_view_receiver_info_alloc() {
         },
         true);
 
+    ws_receiver_info->timer =
+        furi_timer_alloc(ws_view_receiver_info_timer, FuriTimerTypePeriodic, ws_receiver_info);
+
     return ws_receiver_info;
 }
 
 void ws_view_receiver_info_free(WSReceiverInfo* ws_receiver_info) {
     furi_assert(ws_receiver_info);
 
+    furi_timer_free(ws_receiver_info->timer);
+
     with_view_model(
         ws_receiver_info->view,
         WSReceiverInfoModel * model,

+ 16 - 6
applications/services/bt/bt_service/bt.c

@@ -117,6 +117,8 @@ Bt* bt_alloc() {
     if(!bt_settings_load(&bt->bt_settings)) {
         bt_settings_save(&bt->bt_settings);
     }
+    // Keys storage
+    bt->keys_storage = bt_keys_storage_alloc(BT_KEYS_STORAGE_PATH);
     // Alloc queue
     bt->message_queue = furi_message_queue_alloc(8, sizeof(BtMessage));
 
@@ -285,8 +287,10 @@ static bool bt_on_gap_event_callback(GapEvent event, void* context) {
 static void bt_on_key_storage_change_callback(uint8_t* addr, uint16_t size, void* context) {
     furi_assert(context);
     Bt* bt = context;
-    FURI_LOG_I(TAG, "Changed addr start: %p, size changed: %d", addr, size);
-    BtMessage message = {.type = BtMessageTypeKeysStorageUpdated};
+    BtMessage message = {
+        .type = BtMessageTypeKeysStorageUpdated,
+        .data.key_storage_data.start_address = addr,
+        .data.key_storage_data.size = size};
     furi_check(
         furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
 }
@@ -331,6 +335,8 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
             furi_profile = FuriHalBtProfileSerial;
         }
 
+        bt_keys_storage_load(bt->keys_storage);
+
         if(furi_hal_bt_change_app(furi_profile, bt_on_gap_event_callback, bt)) {
             FURI_LOG_I(TAG, "Bt App started");
             if(bt->bt_settings.enabled) {
@@ -358,6 +364,7 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
 
 static void bt_close_connection(Bt* bt) {
     bt_close_rpc_connection(bt);
+    furi_hal_bt_stop_advertising();
     furi_event_flag_set(bt->api_event, BT_API_UNLOCK_EVENT);
 }
 
@@ -372,8 +379,8 @@ int32_t bt_srv(void* p) {
         return 0;
     }
 
-    // Read keys
-    if(!bt_keys_storage_load(bt)) {
+    // Load keys
+    if(!bt_keys_storage_load(bt->keys_storage)) {
         FURI_LOG_W(TAG, "Failed to load bonding keys");
     }
 
@@ -418,13 +425,16 @@ int32_t bt_srv(void* p) {
             // Display PIN code
             bt_pin_code_show(bt, message.data.pin_code);
         } else if(message.type == BtMessageTypeKeysStorageUpdated) {
-            bt_keys_storage_save(bt);
+            bt_keys_storage_update(
+                bt->keys_storage,
+                message.data.key_storage_data.start_address,
+                message.data.key_storage_data.size);
         } else if(message.type == BtMessageTypeSetProfile) {
             bt_change_profile(bt, &message);
         } else if(message.type == BtMessageTypeDisconnect) {
             bt_close_connection(bt);
         } else if(message.type == BtMessageTypeForgetBondedDevices) {
-            bt_keys_storage_delete(bt);
+            bt_keys_storage_delete(bt->keys_storage);
         }
     }
     return 0;

+ 13 - 0
applications/services/bt/bt_service/bt.h

@@ -56,6 +56,19 @@ void bt_set_status_changed_callback(Bt* bt, BtStatusChangedCallback callback, vo
  */
 void bt_forget_bonded_devices(Bt* bt);
 
+/** Set keys storage file path
+ *
+ * @param bt                    Bt instance
+ * @param keys_storage_path     Path to file with saved keys
+ */
+void bt_keys_storage_set_storage_path(Bt* bt, const char* keys_storage_path);
+
+/** Set default keys storage file path
+ *
+ * @param bt                    Bt instance
+ */
+void bt_keys_storage_set_default_path(Bt* bt);
+
 #ifdef __cplusplus
 }
 #endif

+ 15 - 0
applications/services/bt/bt_service/bt_api.c

@@ -39,3 +39,18 @@ void bt_forget_bonded_devices(Bt* bt) {
     furi_check(
         furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
 }
+
+void bt_keys_storage_set_storage_path(Bt* bt, const char* keys_storage_path) {
+    furi_assert(bt);
+    furi_assert(bt->keys_storage);
+    furi_assert(keys_storage_path);
+
+    bt_keys_storage_set_file_path(bt->keys_storage, keys_storage_path);
+}
+
+void bt_keys_storage_set_default_path(Bt* bt) {
+    furi_assert(bt);
+    furi_assert(bt->keys_storage);
+
+    bt_keys_storage_set_file_path(bt->keys_storage, BT_KEYS_STORAGE_PATH);
+}

+ 13 - 0
applications/services/bt/bt_service/bt_i.h

@@ -13,8 +13,14 @@
 #include <power/power_service/power.h>
 #include <rpc/rpc.h>
 #include <notification/notification.h>
+#include <storage/storage.h>
 
 #include <bt/bt_settings.h>
+#include <bt/bt_service/bt_keys_storage.h>
+
+#include "bt_keys_filename.h"
+
+#define BT_KEYS_STORAGE_PATH INT_PATH(BT_KEYS_STORAGE_FILE_NAME)
 
 #define BT_API_UNLOCK_EVENT (1UL << 0)
 
@@ -29,10 +35,16 @@ typedef enum {
     BtMessageTypeForgetBondedDevices,
 } BtMessageType;
 
+typedef struct {
+    uint8_t* start_address;
+    uint16_t size;
+} BtKeyStorageUpdateData;
+
 typedef union {
     uint32_t pin_code;
     uint8_t battery_level;
     BtProfile profile;
+    BtKeyStorageUpdateData key_storage_data;
 } BtMessageData;
 
 typedef struct {
@@ -46,6 +58,7 @@ struct Bt {
     uint16_t bt_keys_size;
     uint16_t max_packet_size;
     BtSettings bt_settings;
+    BtKeysStorage* keys_storage;
     BtStatus status;
     BtProfile profile;
     FuriMessageQueue* message_queue;

+ 124 - 35
applications/services/bt/bt_service/bt_keys_storage.c

@@ -1,49 +1,24 @@
 #include "bt_keys_storage.h"
 
 #include <furi.h>
+#include <furi_hal_bt.h>
 #include <lib/toolbox/saved_struct.h>
 #include <storage/storage.h>
 
-#define BT_KEYS_STORAGE_PATH INT_PATH(BT_KEYS_STORAGE_FILE_NAME)
 #define BT_KEYS_STORAGE_VERSION (0)
 #define BT_KEYS_STORAGE_MAGIC (0x18)
 
-bool bt_keys_storage_load(Bt* bt) {
-    furi_assert(bt);
-    bool file_loaded = false;
-
-    furi_hal_bt_get_key_storage_buff(&bt->bt_keys_addr_start, &bt->bt_keys_size);
-    furi_hal_bt_nvm_sram_sem_acquire();
-    file_loaded = saved_struct_load(
-        BT_KEYS_STORAGE_PATH,
-        bt->bt_keys_addr_start,
-        bt->bt_keys_size,
-        BT_KEYS_STORAGE_MAGIC,
-        BT_KEYS_STORAGE_VERSION);
-    furi_hal_bt_nvm_sram_sem_release();
-
-    return file_loaded;
-}
+#define TAG "BtKeyStorage"
 
-bool bt_keys_storage_save(Bt* bt) {
-    furi_assert(bt);
-    furi_assert(bt->bt_keys_addr_start);
-    bool file_saved = false;
-
-    furi_hal_bt_nvm_sram_sem_acquire();
-    file_saved = saved_struct_save(
-        BT_KEYS_STORAGE_PATH,
-        bt->bt_keys_addr_start,
-        bt->bt_keys_size,
-        BT_KEYS_STORAGE_MAGIC,
-        BT_KEYS_STORAGE_VERSION);
-    furi_hal_bt_nvm_sram_sem_release();
-
-    return file_saved;
-}
+struct BtKeysStorage {
+    uint8_t* nvm_sram_buff;
+    uint16_t nvm_sram_buff_size;
+    FuriString* file_path;
+};
+
+bool bt_keys_storage_delete(BtKeysStorage* instance) {
+    furi_assert(instance);
 
-bool bt_keys_storage_delete(Bt* bt) {
-    furi_assert(bt);
     bool delete_succeed = false;
     bool bt_is_active = furi_hal_bt_is_active();
 
@@ -55,3 +30,117 @@ bool bt_keys_storage_delete(Bt* bt) {
 
     return delete_succeed;
 }
+
+BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path) {
+    furi_assert(keys_storage_path);
+
+    BtKeysStorage* instance = malloc(sizeof(BtKeysStorage));
+    // Set default nvm ram parameters
+    furi_hal_bt_get_key_storage_buff(&instance->nvm_sram_buff, &instance->nvm_sram_buff_size);
+    // Set key storage file
+    instance->file_path = furi_string_alloc();
+    furi_string_set_str(instance->file_path, keys_storage_path);
+
+    return instance;
+}
+
+void bt_keys_storage_free(BtKeysStorage* instance) {
+    furi_assert(instance);
+
+    furi_string_free(instance->file_path);
+    free(instance);
+}
+
+void bt_keys_storage_set_file_path(BtKeysStorage* instance, const char* path) {
+    furi_assert(instance);
+    furi_assert(path);
+
+    furi_string_set_str(instance->file_path, path);
+}
+
+void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint16_t size) {
+    furi_assert(instance);
+    furi_assert(buff);
+
+    instance->nvm_sram_buff = buff;
+    instance->nvm_sram_buff_size = size;
+}
+
+bool bt_keys_storage_load(BtKeysStorage* instance) {
+    furi_assert(instance);
+
+    bool loaded = false;
+    do {
+        // Get payload size
+        size_t payload_size = 0;
+        if(!saved_struct_get_payload_size(
+               furi_string_get_cstr(instance->file_path),
+               BT_KEYS_STORAGE_MAGIC,
+               BT_KEYS_STORAGE_VERSION,
+               &payload_size)) {
+            FURI_LOG_E(TAG, "Failed to read payload size");
+            break;
+        }
+
+        if(payload_size > instance->nvm_sram_buff_size) {
+            FURI_LOG_E(TAG, "Saved data doesn't fit ram buffer");
+            break;
+        }
+
+        // Load saved data to ram
+        furi_hal_bt_nvm_sram_sem_acquire();
+        bool data_loaded = saved_struct_load(
+            furi_string_get_cstr(instance->file_path),
+            instance->nvm_sram_buff,
+            payload_size,
+            BT_KEYS_STORAGE_MAGIC,
+            BT_KEYS_STORAGE_VERSION);
+        furi_hal_bt_nvm_sram_sem_release();
+        if(!data_loaded) {
+            FURI_LOG_E(TAG, "Failed to load struct");
+            break;
+        }
+
+        loaded = true;
+    } while(false);
+
+    return loaded;
+}
+
+bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size) {
+    furi_assert(instance);
+    furi_assert(start_addr);
+
+    bool updated = false;
+
+    FURI_LOG_I(
+        TAG,
+        "Base address: %p. Start update address: %p. Size changed: %ld",
+        (void*)instance->nvm_sram_buff,
+        start_addr,
+        size);
+
+    do {
+        size_t new_size = start_addr - instance->nvm_sram_buff + size;
+        if(new_size > instance->nvm_sram_buff_size) {
+            FURI_LOG_E(TAG, "NVM RAM buffer overflow");
+            break;
+        }
+
+        furi_hal_bt_nvm_sram_sem_acquire();
+        bool data_updated = saved_struct_save(
+            furi_string_get_cstr(instance->file_path),
+            instance->nvm_sram_buff,
+            new_size,
+            BT_KEYS_STORAGE_MAGIC,
+            BT_KEYS_STORAGE_VERSION);
+        furi_hal_bt_nvm_sram_sem_release();
+        if(!data_updated) {
+            FURI_LOG_E(TAG, "Failed to update key storage");
+            break;
+        }
+        updated = true;
+    } while(false);
+
+    return updated;
+}

+ 15 - 5
applications/services/bt/bt_service/bt_keys_storage.h

@@ -1,10 +1,20 @@
 #pragma once
 
-#include "bt_i.h"
-#include "bt_keys_filename.h"
+#include <stdint.h>
+#include <stdbool.h>
 
-bool bt_keys_storage_load(Bt* bt);
+typedef struct BtKeysStorage BtKeysStorage;
 
-bool bt_keys_storage_save(Bt* bt);
+BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path);
 
-bool bt_keys_storage_delete(Bt* bt);
+void bt_keys_storage_free(BtKeysStorage* instance);
+
+void bt_keys_storage_set_file_path(BtKeysStorage* instance, const char* path);
+
+void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint16_t size);
+
+bool bt_keys_storage_load(BtKeysStorage* instance);
+
+bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size);
+
+bool bt_keys_storage_delete(BtKeysStorage* instance);

+ 32 - 39
applications/services/cli/cli_vcp.c

@@ -11,6 +11,12 @@
 
 #define VCP_IF_NUM 0
 
+#ifdef CLI_VCP_DEBUG
+#define VCP_DEBUG(...) FURI_LOG_D(TAG, __VA_ARGS__)
+#else
+#define VCP_DEBUG(...)
+#endif
+
 typedef enum {
     VcpEvtStop = (1 << 0),
     VcpEvtConnect = (1 << 1),
@@ -104,9 +110,8 @@ static int32_t vcp_worker(void* context) {
 
         // VCP session opened
         if(flags & VcpEvtConnect) {
-#ifdef CLI_VCP_DEBUG
-            FURI_LOG_D(TAG, "Connect");
-#endif
+            VCP_DEBUG("Connect");
+
             if(vcp->connected == false) {
                 vcp->connected = true;
                 furi_stream_buffer_send(vcp->rx_stream, &ascii_soh, 1, FuriWaitForever);
@@ -115,9 +120,8 @@ static int32_t vcp_worker(void* context) {
 
         // VCP session closed
         if(flags & VcpEvtDisconnect) {
-#ifdef CLI_VCP_DEBUG
-            FURI_LOG_D(TAG, "Disconnect");
-#endif
+            VCP_DEBUG("Disconnect");
+
             if(vcp->connected == true) {
                 vcp->connected = false;
                 furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0);
@@ -127,9 +131,8 @@ static int32_t vcp_worker(void* context) {
 
         // Rx buffer was read, maybe there is enough space for new data?
         if((flags & VcpEvtStreamRx) && (missed_rx > 0)) {
-#ifdef CLI_VCP_DEBUG
-            FURI_LOG_D(TAG, "StreamRx");
-#endif
+            VCP_DEBUG("StreamRx");
+
             if(furi_stream_buffer_spaces_available(vcp->rx_stream) >= USB_CDC_PKT_LEN) {
                 flags |= VcpEvtRx;
                 missed_rx--;
@@ -140,9 +143,8 @@ static int32_t vcp_worker(void* context) {
         if(flags & VcpEvtRx) {
             if(furi_stream_buffer_spaces_available(vcp->rx_stream) >= USB_CDC_PKT_LEN) {
                 int32_t len = furi_hal_cdc_receive(VCP_IF_NUM, vcp->data_buffer, USB_CDC_PKT_LEN);
-#ifdef CLI_VCP_DEBUG
-                FURI_LOG_D(TAG, "Rx %d", len);
-#endif
+                VCP_DEBUG("Rx %ld", len);
+
                 if(len > 0) {
                     furi_check(
                         furi_stream_buffer_send(
@@ -150,18 +152,15 @@ static int32_t vcp_worker(void* context) {
                         (size_t)len);
                 }
             } else {
-#ifdef CLI_VCP_DEBUG
-                FURI_LOG_D(TAG, "Rx missed");
-#endif
+                VCP_DEBUG("Rx missed");
                 missed_rx++;
             }
         }
 
         // New data in Tx buffer
         if(flags & VcpEvtStreamTx) {
-#ifdef CLI_VCP_DEBUG
-            FURI_LOG_D(TAG, "StreamTx");
-#endif
+            VCP_DEBUG("StreamTx");
+
             if(tx_idle) {
                 flags |= VcpEvtTx;
             }
@@ -171,9 +170,9 @@ static int32_t vcp_worker(void* context) {
         if(flags & VcpEvtTx) {
             size_t len =
                 furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0);
-#ifdef CLI_VCP_DEBUG
-            FURI_LOG_D(TAG, "Tx %d", len);
-#endif
+
+            VCP_DEBUG("Tx %d", len);
+
             if(len > 0) { // Some data left in Tx buffer. Sending it now
                 tx_idle = false;
                 furi_hal_cdc_send(VCP_IF_NUM, vcp->data_buffer, len);
@@ -216,9 +215,7 @@ static size_t cli_vcp_rx(uint8_t* buffer, size_t size, uint32_t timeout) {
         return 0;
     }
 
-#ifdef CLI_VCP_DEBUG
-    FURI_LOG_D(TAG, "rx %u start", size);
-#endif
+    VCP_DEBUG("rx %u start", size);
 
     size_t rx_cnt = 0;
 
@@ -227,19 +224,21 @@ static size_t cli_vcp_rx(uint8_t* buffer, size_t size, uint32_t timeout) {
         if(batch_size > VCP_RX_BUF_SIZE) batch_size = VCP_RX_BUF_SIZE;
 
         size_t len = furi_stream_buffer_receive(vcp->rx_stream, buffer, batch_size, timeout);
-#ifdef CLI_VCP_DEBUG
-        FURI_LOG_D(TAG, "rx %u ", batch_size);
-#endif
+        VCP_DEBUG("rx %u ", batch_size);
+
         if(len == 0) break;
+        if(vcp->running == false) {
+            // EOT command is received after VCP session close
+            rx_cnt += len;
+            break;
+        }
         furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStreamRx);
         size -= len;
         buffer += len;
         rx_cnt += len;
     }
 
-#ifdef CLI_VCP_DEBUG
-    FURI_LOG_D(TAG, "rx %u end", size);
-#endif
+    VCP_DEBUG("rx %u end", size);
     return rx_cnt;
 }
 
@@ -251,9 +250,7 @@ static void cli_vcp_tx(const uint8_t* buffer, size_t size) {
         return;
     }
 
-#ifdef CLI_VCP_DEBUG
-    FURI_LOG_D(TAG, "tx %u start", size);
-#endif
+    VCP_DEBUG("tx %u start", size);
 
     while(size > 0 && vcp->connected) {
         size_t batch_size = size;
@@ -261,17 +258,13 @@ static void cli_vcp_tx(const uint8_t* buffer, size_t size) {
 
         furi_stream_buffer_send(vcp->tx_stream, buffer, batch_size, FuriWaitForever);
         furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStreamTx);
-#ifdef CLI_VCP_DEBUG
-        FURI_LOG_D(TAG, "tx %u", batch_size);
-#endif
+        VCP_DEBUG("tx %u", batch_size);
 
         size -= batch_size;
         buffer += batch_size;
     }
 
-#ifdef CLI_VCP_DEBUG
-    FURI_LOG_D(TAG, "tx %u end", size);
-#endif
+    VCP_DEBUG("tx %u end", size);
 }
 
 static void cli_vcp_tx_stdout(const char* data, size_t size) {

+ 10 - 1
applications/services/desktop/animations/animation_manager.c

@@ -52,6 +52,7 @@ struct AnimationManager {
     FuriString* freezed_animation_name;
     int32_t freezed_animation_time_left;
     ViewStack* view_stack;
+    bool dummy_mode;
 };
 
 static StorageAnimation*
@@ -93,6 +94,12 @@ void animation_manager_set_interact_callback(
     animation_manager->interact_callback = callback;
 }
 
+void animation_manager_set_dummy_mode_state(AnimationManager* animation_manager, bool enabled) {
+    furi_assert(animation_manager);
+    animation_manager->dummy_mode = enabled;
+    animation_manager_start_new_idle(animation_manager);
+}
+
 static void animation_manager_check_blocking_callback(const void* message, void* context) {
     const StorageEvent* storage_event = message;
 
@@ -363,7 +370,9 @@ static bool animation_manager_is_valid_idle_animation(
 
 static StorageAnimation*
     animation_manager_select_idle_animation(AnimationManager* animation_manager) {
-    UNUSED(animation_manager);
+    if(animation_manager->dummy_mode) {
+        return animation_storage_find_animation(HARDCODED_ANIMATION_NAME);
+    }
     StorageAnimationList_t animation_list;
     StorageAnimationList_init(animation_list);
     animation_storage_fill_animation_list(&animation_list);

+ 8 - 0
applications/services/desktop/animations/animation_manager.h

@@ -157,3 +157,11 @@ void animation_manager_unload_and_stall_animation(AnimationManager* animation_ma
  * @animation_manager   instance
  */
 void animation_manager_load_and_continue_animation(AnimationManager* animation_manager);
+
+/**
+ * Enable or disable dummy mode backgrounds of animation manager.
+ *
+ * @animation_manager    instance
+ * @enabled              bool
+ */
+void animation_manager_set_dummy_mode_state(AnimationManager* animation_manager, bool enabled);

+ 3 - 0
applications/services/desktop/desktop.c

@@ -144,6 +144,7 @@ void desktop_unlock(Desktop* desktop) {
 void desktop_set_dummy_mode_state(Desktop* desktop, bool enabled) {
     view_port_enabled_set(desktop->dummy_mode_icon_viewport, enabled);
     desktop_main_set_dummy_mode_state(desktop->main_view, enabled);
+    animation_manager_set_dummy_mode_state(desktop->animation_manager, enabled);
     desktop->settings.dummy_mode = enabled;
     DESKTOP_SETTINGS_SAVE(&desktop->settings);
 }
@@ -330,6 +331,8 @@ int32_t desktop_srv(void* p) {
 
     view_port_enabled_set(desktop->dummy_mode_icon_viewport, desktop->settings.dummy_mode);
     desktop_main_set_dummy_mode_state(desktop->main_view, desktop->settings.dummy_mode);
+    animation_manager_set_dummy_mode_state(
+        desktop->animation_manager, desktop->settings.dummy_mode);
 
     scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
 

+ 27 - 6
applications/services/desktop/scenes/desktop_scene_main.c

@@ -12,6 +12,10 @@
 
 #define TAG "DesktopSrv"
 
+#define MUSIC_PLAYER_APP EXT_PATH("/apps/Misc/music_player.fap")
+#define SNAKE_GAME_APP EXT_PATH("/apps/Games/snake_game.fap")
+#define CLOCK_APP EXT_PATH("/apps/Tools/clock.fap")
+
 static void desktop_scene_main_new_idle_animation_callback(void* context) {
     furi_assert(context);
     Desktop* desktop = context;
@@ -60,6 +64,19 @@ static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* fl
 }
 #endif
 
+static void desktop_scene_main_open_app_or_profile(Desktop* desktop, const char* path) {
+    do {
+        LoaderStatus status = loader_start(desktop->loader, FAP_LOADER_APP_NAME, path);
+        if(status == LoaderStatusOk) break;
+        FURI_LOG_E(TAG, "loader_start failed: %d", status);
+
+        status = loader_start(desktop->loader, "Passport", NULL);
+        if(status != LoaderStatusOk) {
+            FURI_LOG_E(TAG, "loader_start failed: %d", status);
+        }
+    } while(false);
+}
+
 void desktop_scene_main_callback(DesktopEvent event, void* context) {
     Desktop* desktop = (Desktop*)context;
     view_dispatcher_send_custom_event(desktop->view_dispatcher, event);
@@ -181,12 +198,16 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) {
             }
             break;
         }
-        case DesktopMainEventOpenGameMenu: {
-            LoaderStatus status = loader_start(
-                desktop->loader, FAP_LOADER_APP_NAME, EXT_PATH("/apps/Games/snake_game.fap"));
-            if(status != LoaderStatusOk) {
-                FURI_LOG_E(TAG, "loader_start failed: %d", status);
-            }
+        case DesktopMainEventOpenGame: {
+            desktop_scene_main_open_app_or_profile(desktop, SNAKE_GAME_APP);
+            break;
+        }
+        case DesktopMainEventOpenClock: {
+            desktop_scene_main_open_app_or_profile(desktop, CLOCK_APP);
+            break;
+        }
+        case DesktopMainEventOpenMusicPlayer: {
+            desktop_scene_main_open_app_or_profile(desktop, MUSIC_PLAYER_APP);
             break;
         }
         case DesktopLockedEventUpdate:

+ 3 - 1
applications/services/desktop/views/desktop_events.h

@@ -10,7 +10,9 @@ typedef enum {
     DesktopMainEventOpenPassport,
     DesktopMainEventOpenPowerOff,
 
-    DesktopMainEventOpenGameMenu,
+    DesktopMainEventOpenGame,
+    DesktopMainEventOpenClock,
+    DesktopMainEventOpenMusicPlayer,
 
     DesktopLockedEventUnlocked,
     DesktopLockedEventUpdate,

+ 3 - 3
applications/services/desktop/views/desktop_view_main.c

@@ -72,13 +72,13 @@ bool desktop_main_input_callback(InputEvent* event, void* context) {
     } else {
         if(event->type == InputTypeShort) {
             if(event->key == InputKeyOk) {
-                main_view->callback(DesktopMainEventOpenGameMenu, main_view->context);
+                main_view->callback(DesktopMainEventOpenGame, main_view->context);
             } else if(event->key == InputKeyUp) {
                 main_view->callback(DesktopMainEventOpenLockMenu, main_view->context);
             } else if(event->key == InputKeyDown) {
-                main_view->callback(DesktopMainEventOpenPassport, main_view->context);
+                main_view->callback(DesktopMainEventOpenMusicPlayer, main_view->context);
             } else if(event->key == InputKeyLeft) {
-                main_view->callback(DesktopMainEventOpenPassport, main_view->context);
+                main_view->callback(DesktopMainEventOpenClock, main_view->context);
             }
             // Right key is handled by animation manager
         }

+ 1 - 0
applications/services/dialogs/dialogs.c

@@ -14,6 +14,7 @@ void dialog_file_browser_set_basic_options(
     options->hide_ext = true;
     options->item_loader_callback = NULL;
     options->item_loader_context = NULL;
+    options->base_path = NULL;
 }
 
 static DialogsApp* dialogs_app_alloc() {

+ 4 - 0
applications/services/dialogs/dialogs.h

@@ -18,7 +18,9 @@ typedef struct DialogsApp DialogsApp;
 /**
  * File browser dialog extra options
  * @param extension file extension to be offered for selection
+ * @param base_path root folder path for navigation with back key
  * @param skip_assets true - do not show assets folders
+ * @param hide_dot_files true - hide dot files
  * @param icon file icon pointer, NULL for default icon
  * @param hide_ext true - hide extensions for files
  * @param item_loader_callback callback function for providing custom icon & entry name
@@ -26,7 +28,9 @@ typedef struct DialogsApp DialogsApp;
  */
 typedef struct {
     const char* extension;
+    const char* base_path;
     bool skip_assets;
+    bool hide_dot_files;
     const Icon* icon;
     bool hide_ext;
     FileBrowserLoadItemCallback item_loader_callback;

+ 2 - 0
applications/services/dialogs/dialogs_api.c

@@ -20,9 +20,11 @@ bool dialog_file_browser_show(
             .file_icon = options ? options->icon : NULL,
             .hide_ext = options ? options->hide_ext : true,
             .skip_assets = options ? options->skip_assets : true,
+            .hide_dot_files = options ? options->hide_dot_files : true,
             .preselected_filename = path,
             .item_callback = options ? options->item_loader_callback : NULL,
             .item_callback_context = options ? options->item_loader_context : NULL,
+            .base_path = options ? options->base_path : NULL,
         }};
 
     DialogsAppReturn return_data;

+ 2 - 0
applications/services/dialogs/dialogs_message.h

@@ -11,11 +11,13 @@ typedef struct {
     const char* extension;
     bool skip_assets;
     bool hide_ext;
+    bool hide_dot_files;
     const Icon* file_icon;
     FuriString* result_path;
     FuriString* preselected_filename;
     FileBrowserLoadItemCallback item_callback;
     void* item_callback_context;
+    const char* base_path;
 } DialogsAppMessageDataFileBrowser;
 
 typedef struct {

+ 7 - 1
applications/services/dialogs/dialogs_module_file_browser.c

@@ -38,7 +38,13 @@ bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrow
     file_browser_set_callback(
         file_browser, dialogs_app_file_browser_callback, file_browser_context);
     file_browser_configure(
-        file_browser, data->extension, data->skip_assets, data->file_icon, data->hide_ext);
+        file_browser,
+        data->extension,
+        data->base_path,
+        data->skip_assets,
+        data->hide_dot_files,
+        data->file_icon,
+        data->hide_ext);
     file_browser_set_item_callback(file_browser, data->item_callback, data->item_callback_context);
     file_browser_start(file_browser, data->preselected_filename);
 

+ 8 - 0
applications/services/dialogs/view_holder.c

@@ -50,6 +50,10 @@ void view_holder_free(ViewHolder* view_holder) {
 void view_holder_set_view(ViewHolder* view_holder, View* view) {
     furi_assert(view_holder);
     if(view_holder->view) {
+        if(view_holder->view->exit_callback) {
+            view_holder->view->exit_callback(view_holder->view->context);
+        }
+
         view_set_update_callback(view_holder->view, NULL);
         view_set_update_callback_context(view_holder->view, NULL);
     }
@@ -59,6 +63,10 @@ void view_holder_set_view(ViewHolder* view_holder, View* view) {
     if(view_holder->view) {
         view_set_update_callback(view_holder->view, view_holder_update);
         view_set_update_callback_context(view_holder->view, view_holder);
+
+        if(view_holder->view->enter_callback) {
+            view_holder->view->enter_callback(view_holder->view->context);
+        }
     }
 }
 

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است