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

Merge branch 'dev' of github.com:0xchocolate/flipperzero-firmware-with-wifi-marauder-companion into feature_wifi_marauder_app

0xchocolate 2 лет назад
Родитель
Сommit
527d6497de
100 измененных файлов с 2065 добавлено и 650 удалено
  1. 2 0
      .github/CODEOWNERS
  2. 1 1
      .github/ISSUE_TEMPLATE/03_feature_request.yml
  3. BIN
      .github/assets/dark_theme_banner.png
  4. BIN
      .github/assets/light_theme_banner.png
  5. 0 103
      .github/workflows/amap_analyse.yml
  6. 39 11
      .github/workflows/build.yml
  7. 28 33
      .github/workflows/pvs_studio.yml
  8. 15 57
      .github/workflows/unit_tests.yml
  9. 78 0
      .github/workflows/updater_test.yml
  10. 5 0
      .gitignore
  11. 23 0
      .pvsconfig
  12. 1 1
      .pvsoptions
  13. 20 2
      .vscode/example/tasks.json
  14. 5 2
      .vscode/extensions.json
  15. 0 6
      Brewfile
  16. 8 8
      CODING_STYLE.md
  17. 3 3
      CONTRIBUTING.md
  18. 0 21
      Makefile
  19. 0 51
      RoadMap.md
  20. 28 3
      SConstruct
  21. 3 2
      applications/debug/accessor/accessor_app.cpp
  22. 1 0
      applications/debug/accessor/application.fam
  23. 0 3
      applications/debug/accessor/helpers/wiegand.cpp
  24. 1 0
      applications/debug/battery_test_app/application.fam
  25. 148 0
      applications/debug/battery_test_app/views/battery_info.c
  26. 23 0
      applications/debug/battery_test_app/views/battery_info.h
  27. 3 4
      applications/debug/bt_debug_app/bt_debug_app.c
  28. 2 3
      applications/debug/bt_debug_app/bt_debug_app.h
  29. 1 1
      applications/debug/bt_debug_app/views/bt_carrier_test.c
  30. 1 1
      applications/debug/bt_debug_app/views/bt_packet_test.c
  31. 7 4
      applications/debug/bt_debug_app/views/bt_test.c
  32. 10 0
      applications/debug/direct_draw/application.fam
  33. 112 0
      applications/debug/direct_draw/direct_draw.c
  34. 1 0
      applications/debug/display_test/application.fam
  35. 0 1
      applications/debug/display_test/display_test.c
  36. 9 0
      applications/debug/example_custom_font/application.fam
  37. 98 0
      applications/debug/example_custom_font/example_custom_font.c
  38. 6 5
      applications/debug/file_browser_test/file_browser_app.c
  39. 1 0
      applications/debug/lfrfid_debug/application.fam
  40. 5 1
      applications/debug/rpc_debug_app/scenes/rpc_debug_app_scene_input_error_code.c
  41. 60 0
      applications/debug/unit_tests/float_tools/float_tools_test.c
  42. 20 81
      applications/debug/unit_tests/furi/furi_memmgr_test.c
  43. 75 0
      applications/debug/unit_tests/manifest/manifest.c
  44. 31 2
      applications/debug/unit_tests/nfc/nfc_test.c
  45. 1 1
      applications/debug/unit_tests/rpc/rpc_test.c
  46. 25 1
      applications/debug/unit_tests/stream/stream_test.c
  47. 80 2
      applications/debug/unit_tests/subghz/subghz_test.c
  48. 28 22
      applications/debug/unit_tests/test_index.c
  49. 2 2
      applications/examples/application.fam
  50. 44 0
      applications/examples/example_thermo/README.md
  51. 10 0
      applications/examples/example_thermo/application.fam
  52. 356 0
      applications/examples/example_thermo/example_thermo.c
  53. BIN
      applications/examples/example_thermo/example_thermo_10px.png
  54. 1 1
      applications/main/archive/helpers/archive_apps.c
  55. 3 2
      applications/main/archive/helpers/archive_browser.c
  56. 1 1
      applications/main/archive/helpers/archive_favorites.c
  57. 1 1
      applications/main/archive/scenes/archive_scene_browser.c
  58. 5 4
      applications/main/archive/views/archive_browser_view.h
  59. 78 2
      applications/main/bad_usb/bad_usb_app.c
  60. 11 3
      applications/main/bad_usb/bad_usb_app_i.h
  61. 54 20
      applications/main/bad_usb/bad_usb_script.c
  62. 2 0
      applications/main/bad_usb/bad_usb_script.h
  63. 3 0
      applications/main/bad_usb/bad_usb_settings_filename.h
  64. 53 0
      applications/main/bad_usb/scenes/bad_usb_scene_config.c
  65. 2 0
      applications/main/bad_usb/scenes/bad_usb_scene_config.h
  66. 50 0
      applications/main/bad_usb/scenes/bad_usb_scene_config_layout.c
  67. 13 5
      applications/main/bad_usb/scenes/bad_usb_scene_file_select.c
  68. 18 11
      applications/main/bad_usb/scenes/bad_usb_scene_work.c
  69. 60 31
      applications/main/bad_usb/views/bad_usb_view.c
  70. 4 2
      applications/main/bad_usb/views/bad_usb_view.h
  71. 2 2
      applications/main/fap_loader/fap_loader_app.c
  72. 3 1
      applications/main/gpio/gpio_app.c
  73. 2 1
      applications/main/gpio/gpio_app_i.h
  74. 0 51
      applications/main/gpio/gpio_item.c
  75. 0 15
      applications/main/gpio/gpio_item.h
  76. 69 0
      applications/main/gpio/gpio_items.c
  77. 29 0
      applications/main/gpio/gpio_items.h
  78. 2 2
      applications/main/gpio/scenes/gpio_scene_start.c
  79. 5 3
      applications/main/gpio/scenes/gpio_scene_test.c
  80. 4 1
      applications/main/gpio/scenes/gpio_scene_usb_uart_config.c
  81. 4 4
      applications/main/gpio/usb_uart_bridge.c
  82. 20 10
      applications/main/gpio/views/gpio_test.c
  83. 3 1
      applications/main/gpio/views/gpio_test.h
  84. 1 1
      applications/main/gpio/views/gpio_usb_uart.c
  85. 1 0
      applications/main/ibutton/application.fam
  86. 3 3
      applications/main/ibutton/ibutton.c
  87. 1 1
      applications/main/ibutton/ibutton_cli.c
  88. 1 0
      applications/main/infrared/application.fam
  89. 2 2
      applications/main/infrared/infrared.c
  90. 1 1
      applications/main/infrared/infrared_brute_force.c
  91. 7 5
      applications/main/infrared/infrared_cli.c
  92. 7 7
      applications/main/infrared/infrared_remote.c
  93. 3 3
      applications/main/infrared/infrared_signal.c
  94. 1 0
      applications/main/infrared/scenes/infrared_scene_config.h
  95. 1 1
      applications/main/infrared/scenes/infrared_scene_debug.c
  96. 1 1
      applications/main/infrared/scenes/infrared_scene_rpc.c
  97. 14 2
      applications/main/infrared/scenes/infrared_scene_universal.c
  98. 86 0
      applications/main/infrared/scenes/infrared_scene_universal_projector.c
  99. 3 3
      applications/main/infrared/views/infrared_debug_view.c
  100. 10 8
      applications/main/infrared/views/infrared_progress_view.c

+ 2 - 0
.github/CODEOWNERS

@@ -42,6 +42,8 @@
 
 
 /applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @nminaylov @gornekich @Astrrra @gsurkov @Skorpionm
 /applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @nminaylov @gornekich @Astrrra @gsurkov @Skorpionm
 
 
+/applications/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov
+
 # Assets
 # Assets
 /assets/resources/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
 /assets/resources/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
 
 

+ 1 - 1
.github/ISSUE_TEMPLATE/03_feature_request.yml

@@ -14,7 +14,7 @@ body:
     description: |
     description: |
       Please describe your feature request in as many details as possible.
       Please describe your feature request in as many details as possible.
         - Describe what it should do.
         - Describe what it should do.
-        - Note whetever it is to extend existing functionality or introduce new functionality.
+        - Note whether it is to extend existing functionality or introduce new functionality.
   validations:
   validations:
     required: true
     required: true
 - type: textarea
 - type: textarea

BIN
.github/assets/dark_theme_banner.png


BIN
.github/assets/light_theme_banner.png


+ 0 - 103
.github/workflows/amap_analyse.yml

@@ -1,103 +0,0 @@
-name: 'Analyze .map file with Amap'
-
-on:
-  push:
-    branches:
-      - dev
-      - "release*"
-    tags:
-      - '*'
-  pull_request:
-
-env:
-  TARGETS: f7
-  FBT_TOOLCHAIN_PATH: /opt
-
-jobs:
-  amap_analyse:
-    if: ${{ !github.event.pull_request.head.repo.fork }}
-    runs-on: [self-hosted,FlipperZeroMacShell]
-    timeout-minutes: 15
-    steps:
-      - name: 'Wait Build workflow'
-        uses: fountainhead/action-wait-for-check@v1.0.0
-        id: wait-for-build
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          checkName: 'main'
-          ref: ${{ github.event.pull_request.head.sha || github.sha }}
-          intervalSeconds: 20
-
-      - name: 'Check Build workflow status'
-        if: steps.wait-for-build.outputs.conclusion == 'failure'
-        run: |
-          exit 1
-
-      - 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: 'Make artifacts directory'
-        run: |
-          rm -rf artifacts
-          mkdir artifacts
-
-      - name: 'Download build artifacts'
-        run: |
-          mkdir -p ~/.ssh
-          ssh-keyscan -p ${{ secrets.RSYNC_DEPLOY_PORT }} -H ${{ secrets.RSYNC_DEPLOY_HOST }} > ~/.ssh/known_hosts
-          echo "${{ secrets.RSYNC_DEPLOY_KEY }}" > deploy_key;
-          chmod 600 ./deploy_key;
-          rsync -avzP \
-              -e 'ssh -p ${{ secrets.RSYNC_DEPLOY_PORT }} -i ./deploy_key' \
-              ${{ secrets.RSYNC_DEPLOY_USER }}@${{ secrets.RSYNC_DEPLOY_HOST }}:"${{ secrets.RSYNC_DEPLOY_BASE_PATH }}${BRANCH_NAME}/" artifacts/;
-          rm ./deploy_key;
-
-      - name: 'Make .map file analyze'
-        run: |
-          cd artifacts/
-          /Applications/amap/Contents/MacOS/amap -f "flipper-z-f7-firmware-${SUFFIX}.elf.map"
-
-      - name: 'Upload report to DB'
-        run: |
-          source scripts/toolchain/fbtenv.sh
-          get_size()
-          {
-            SECTION="$1";
-            arm-none-eabi-size \
-              -A artifacts/flipper-z-f7-firmware-$SUFFIX.elf \
-              | grep "^$SECTION" | awk '{print $2}'
-          }
-          export BSS_SIZE="$(get_size ".bss")"
-          export TEXT_SIZE="$(get_size ".text")"
-          export RODATA_SIZE="$(get_size ".rodata")"
-          export DATA_SIZE="$(get_size ".data")"
-          export FREE_FLASH_SIZE="$(get_size ".free_flash")"
-          python3 -m pip install mariadb==1.1.4
-          python3 scripts/amap_mariadb_insert.py \
-            ${{ secrets.AMAP_MARIADB_USER }} \
-            ${{ secrets.AMAP_MARIADB_PASSWORD }} \
-            ${{ secrets.AMAP_MARIADB_HOST }} \
-            ${{ secrets.AMAP_MARIADB_PORT }} \
-            ${{ secrets.AMAP_MARIADB_DATABASE }} \
-            artifacts/flipper-z-f7-firmware-$SUFFIX.elf.map.all
-

+ 39 - 11
.github/workflows/build.yml

@@ -30,11 +30,6 @@ jobs:
           fetch-depth: 0
           fetch-depth: 0
           ref: ${{ github.event.pull_request.head.sha }}
           ref: ${{ github.event.pull_request.head.sha }}
 
 
-      - name: 'Make artifacts directory'
-        run: |
-          rm -rf artifacts
-          mkdir artifacts
-
       - name: 'Get commit details'
       - name: 'Get commit details'
         id: names
         id: names
         run: |
         run: |
@@ -46,6 +41,15 @@ jobs:
             TYPE="other"
             TYPE="other"
           fi
           fi
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
+          echo random_hash=$(openssl rand -base64 40 | shasum -a 256 | awk '{print $1}') >> $GITHUB_OUTPUT
+          echo "event_type=$TYPE" >> $GITHUB_OUTPUT
+
+      - name: 'Make artifacts directory'
+        run: |
+          rm -rf artifacts
+          rm -rf map_analyser_files
+          mkdir artifacts
+          mkdir map_analyser_files
 
 
       - name: 'Bundle scripts'
       - name: 'Bundle scripts'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -56,8 +60,9 @@ jobs:
         run: |
         run: |
           set -e
           set -e
           for TARGET in ${TARGETS}; do
           for TARGET in ${TARGETS}; do
-                ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
-                copro_dist updater_package ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
+            TARGET="$(echo "${TARGET}" | sed 's/f//')"; \
+            ./fbt TARGET_HW=$TARGET copro_dist updater_package \
+            ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
           done
           done
 
 
       - name: 'Move upload files'
       - name: 'Move upload files'
@@ -82,9 +87,32 @@ jobs:
         run: |
         run: |
           cp build/core2_firmware.tgz "artifacts/flipper-z-any-core2_firmware-${SUFFIX}.tgz"
           cp build/core2_firmware.tgz "artifacts/flipper-z-any-core2_firmware-${SUFFIX}.tgz"
 
 
-      - name: 'Copy .map file'
+      - name: 'Copy map analyser files'
+        if: ${{ !github.event.pull_request.head.repo.fork }}
         run: |
         run: |
-          cp build/f7-firmware-*/firmware.elf.map "artifacts/flipper-z-f7-firmware-${SUFFIX}.elf.map"
+          cp build/f7-firmware-*/firmware.elf.map map_analyser_files/firmware.elf.map
+          cp build/f7-firmware-*/firmware.elf map_analyser_files/firmware.elf
+          cp ${{ github.event_path }} map_analyser_files/event.json
+
+      - name: 'Upload map analyser files to storage'
+        if: ${{ !github.event.pull_request.head.repo.fork }}
+        uses: prewk/s3-cp-action@v2
+        with:
+          aws_s3_endpoint: "${{ secrets.MAP_REPORT_AWS_ENDPOINT }}"
+          aws_access_key_id: "${{ secrets.MAP_REPORT_AWS_ACCESS_KEY }}"
+          aws_secret_access_key: "${{ secrets.MAP_REPORT_AWS_SECRET_KEY }}"
+          source: "./map_analyser_files/"
+          dest: "s3://${{ secrets.MAP_REPORT_AWS_BUCKET }}/${{steps.names.outputs.random_hash}}"
+          flags: "--recursive --acl public-read"
+
+      - name: 'Trigger map file reporter'
+        if: ${{ !github.event.pull_request.head.repo.fork }}
+        uses: peter-evans/repository-dispatch@v2
+        with:
+          repository: flipperdevices/flipper-map-reporter
+          token: ${{ secrets.REPOSITORY_DISPATCH_TOKEN }}
+          event-type: map-file-analyse
+          client-payload: '{"random_hash": "${{steps.names.outputs.random_hash}}", "event_type": "${{steps.names.outputs.event_type}}"}'
 
 
       - name: 'Upload artifacts to update server'
       - name: 'Upload artifacts to update server'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -158,6 +186,6 @@ jobs:
         run: |
         run: |
           set -e
           set -e
           for TARGET in ${TARGETS}; do
           for TARGET in ${TARGETS}; do
-                ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
-                updater_package DEBUG=0 COMPACT=1
+            TARGET="$(echo "${TARGET}" | sed 's/f//')"; \
+            ./fbt TARGET_HW=$TARGET DEBUG=0 COMPACT=1 fap_dist updater_package
           done
           done

+ 28 - 33
.github/workflows/pvs_studio.yml

@@ -43,43 +43,31 @@ jobs:
           fi
           fi
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 
 
-      - name: 'Make reports directory'
+      - name: 'Supply PVS credentials'
         run: |
         run: |
-          rm -rf reports/
-          mkdir reports
-
-      - name: 'Generate compile_comands.json'
-        run: |
-          ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking _fap_icons
-
-      - name: 'Static code analysis'
-        run: |
-          source scripts/toolchain/fbtenv.sh
           pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
           pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
-          pvs-studio-analyzer analyze \
-              @.pvsoptions \
-              -j$(grep -c processor /proc/cpuinfo) \
-              -f build/f7-firmware-DC/compile_commands.json \
-              -o PVS-Studio.log
-
-      - name: 'Convert PVS-Studio output to html page'
-        run: plog-converter -a GA:1,2,3 -t fullhtml PVS-Studio.log -o reports/${DEFAULT_TARGET}-${SUFFIX}
 
 
-      - name: 'Upload artifacts to update server'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
+      - name: 'Convert PVS-Studio output to html and detect warnings'
+        id: pvs-warn
         run: |
         run: |
-          mkdir -p ~/.ssh
-          ssh-keyscan -p ${{ secrets.RSYNC_DEPLOY_PORT }} -H ${{ secrets.RSYNC_DEPLOY_HOST }} > ~/.ssh/known_hosts
-          echo "${{ secrets.RSYNC_DEPLOY_KEY }}" > deploy_key;
-          chmod 600 ./deploy_key;
-          rsync -avrzP --mkpath \
-              -e 'ssh -p ${{ secrets.RSYNC_DEPLOY_PORT }} -i ./deploy_key' \
-              reports/ ${{ secrets.RSYNC_DEPLOY_USER }}@${{ secrets.RSYNC_DEPLOY_HOST }}:/home/data/firmware-pvs-studio-report/"${BRANCH_NAME}/";
-          rm ./deploy_key;
+          WARNINGS=0
+          ./fbt COMPACT=1 PVSNOBROWSER=1 firmware_pvs || WARNINGS=1
+          echo "warnings=${WARNINGS}" >> $GITHUB_OUTPUT
+
+      - name: 'Upload report'
+        if: ${{ !github.event.pull_request.head.repo.fork && (steps.pvs-warn.outputs.warnings != 0) }}
+        uses: prewk/s3-cp-action@v2
+        with:
+          aws_s3_endpoint: "${{ secrets.PVS_AWS_ENDPOINT }}"
+          aws_access_key_id: "${{ secrets.PVS_AWS_ACCESS_KEY }}"
+          aws_secret_access_key: "${{ secrets.PVS_AWS_SECRET_KEY }}"
+          source: "./build/f7-firmware-DC/pvsreport"
+          dest: "s3://${{ secrets.PVS_AWS_BUCKET }}/${{steps.names.outputs.branch_name}}/${{steps.names.outputs.default_target}}-${{steps.names.outputs.suffix}}/"
+          flags: "--recursive --acl public-read"
 
 
       - name: 'Find Previous Comment'
       - name: 'Find Previous Comment'
-        if: ${{ !github.event.pull_request.head.repo.fork && github.event.pull_request }}
-        uses: peter-evans/find-comment@v1
+        if: ${{ !github.event.pull_request.head.repo.fork && github.event.pull_request && (steps.pvs-warn.outputs.warnings != 0) }}
+        uses: peter-evans/find-comment@v2
         id: fc
         id: fc
         with:
         with:
           issue-number: ${{ github.event.pull_request.number }}
           issue-number: ${{ github.event.pull_request.number }}
@@ -87,12 +75,19 @@ jobs:
           body-includes: 'PVS-Studio report for commit'
           body-includes: 'PVS-Studio report for commit'
 
 
       - name: 'Create or update comment'
       - name: 'Create or update comment'
-        if: ${{ !github.event.pull_request.head.repo.fork && github.event.pull_request}}
+        if: ${{ !github.event.pull_request.head.repo.fork && github.event.pull_request && (steps.pvs-warn.outputs.warnings != 0) }}
         uses: peter-evans/create-or-update-comment@v1
         uses: peter-evans/create-or-update-comment@v1
         with:
         with:
           comment-id: ${{ steps.fc.outputs.comment-id }}
           comment-id: ${{ steps.fc.outputs.comment-id }}
           issue-number: ${{ github.event.pull_request.number }}
           issue-number: ${{ github.event.pull_request.number }}
           body: |
           body: |
             **PVS-Studio report for commit `${{steps.names.outputs.commit_sha}}`:**
             **PVS-Studio report for commit `${{steps.names.outputs.commit_sha}}`:**
-            - [Report](https://update.flipperzero.one/builds/firmware-pvs-studio-report/${{steps.names.outputs.branch_name}}/${{steps.names.outputs.default_target}}-${{steps.names.outputs.suffix}}/index.html)
+            - [Report](https://pvs.flipp.dev/${{steps.names.outputs.branch_name}}/${{steps.names.outputs.default_target}}-${{steps.names.outputs.suffix}}/index.html)
           edit-mode: replace
           edit-mode: replace
+
+      - name: 'Raise exception'
+        if: ${{ steps.pvs-warn.outputs.warnings != 0 }}
+        run: |
+          echo "Please fix all PVS warnings before merge"
+          exit 1
+

+ 15 - 57
.github/workflows/unit_tests.yml

@@ -9,7 +9,7 @@ env:
   FBT_TOOLCHAIN_PATH: /opt
   FBT_TOOLCHAIN_PATH: /opt
 
 
 jobs:
 jobs:
-  run_units_on_test_bench:
+  run_units_on_bench:
     runs-on: [self-hosted, FlipperZeroTest]
     runs-on: [self-hosted, FlipperZeroTest]
     steps:
     steps:
       - name: 'Decontaminate previous build leftovers'
       - name: 'Decontaminate previous build leftovers'
@@ -29,81 +29,39 @@ jobs:
         run: |
         run: |
           echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT
           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'
       - name: 'Flash unit tests firmware'
         id: flashing
         id: flashing
         if: success()
         if: success()
-        run: |
+        run: |                               
           ./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
+      - name: 'Wait for flipper and format ext'
+        id: format_ext
         if: steps.flashing.outcome == 'success'
         if: steps.flashing.outcome == 'success'
         run: |
         run: |
           source scripts/toolchain/fbtenv.sh
           source scripts/toolchain/fbtenv.sh
           python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
           python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
 
 
-      - name: 'Copy assets and unit tests data to flipper'
+      - name: 'Copy assets and unit data, reboot and wait for flipper'
         id: copy
         id: copy
-        if: steps.connect.outcome == 'success'
+        if: steps.format_ext.outcome == 'success'
         run: |
         run: |
           source scripts/toolchain/fbtenv.sh
           source scripts/toolchain/fbtenv.sh
+          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} -f send assets/resources /ext
           python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} -f send assets/unit_tests /ext/unit_tests
           python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} -f send assets/unit_tests /ext/unit_tests
+          python3 scripts/power.py -p ${{steps.device.outputs.flipper}} reboot
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 
 
       - name: 'Run units and validate results'
       - name: 'Run units and validate results'
+        id: run_units
         if: steps.copy.outcome == 'success'
         if: steps.copy.outcome == 'success'
+        timeout-minutes: 2.5
         run: |
         run: |
           source scripts/toolchain/fbtenv.sh
           source scripts/toolchain/fbtenv.sh
           python3 scripts/testing/units.py ${{steps.device.outputs.flipper}}
           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()
+      - name: 'Check GDB output'
+        if: failure()
         run: |
         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
+          ./fbt gdb_trace_all OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1

+ 78 - 0
.github/workflows/updater_test.yml

@@ -0,0 +1,78 @@
+name: 'Updater test'
+
+on:
+  pull_request:
+
+env:
+  TARGETS: f7
+  DEFAULT_TARGET: f7
+  FBT_TOOLCHAIN_PATH: /opt
+
+jobs:
+  test_updater_on_bench:
+    runs-on: [self-hosted, FlipperZeroTestMac1]
+    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 flipper from device manager (mock)'
+        id: device
+        run: |
+          echo "flipper=/dev/tty.usbmodemflip_Rekigyn1" >> $GITHUB_OUTPUT
+          echo "stlink=0F020D026415303030303032" >> $GITHUB_OUTPUT
+
+      - name: 'Flashing target firmware'
+        id: first_full_flash
+        run: |
+          source scripts/toolchain/fbtenv.sh
+          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+
+      - name: 'Validating updater'
+        id: second_full_flash
+        if: success()
+        run: |
+          source scripts/toolchain/fbtenv.sh
+          ./fbt flash_usb PORT=${{steps.device.outputs.flipper}} FORCE=1
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+
+      - name: 'Get last release tag'
+        id: release_tag
+        if: failure()
+        run: |
+          echo "tag=$(git tag -l --sort=-version:refname | grep -v "rc\|RC" | head -1)" >> $GITHUB_OUTPUT
+
+      - name: 'Decontaminate previous build leftovers'
+        if: failure()
+        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: failure()
+        with:
+          fetch-depth: 0
+          ref: ${{ steps.release_tag.outputs.tag }}
+
+      - name: 'Flash last release'
+        if: failure()
+        run: |
+          ./fbt flash OPENOCD_ADAPTER_SERIAL=${{steps.device.outputs.stlink}} FORCE=1
+
+      - name: 'Wait for flipper and format ext'
+        if: failure()
+        run: |
+          source scripts/toolchain/fbtenv.sh
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
+          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext

+ 5 - 0
.gitignore

@@ -1,4 +1,5 @@
 *.swp
 *.swp
+*.swo
 *.gdb_history
 *.gdb_history
 
 
 
 
@@ -30,6 +31,10 @@ Brewfile.lock.json
 # Visual Studio Code
 # Visual Studio Code
 .vscode/
 .vscode/
 
 
+# Kate
+.kateproject
+.kateconfig
+
 # legendary cmake's
 # legendary cmake's
 build
 build
 CMakeLists.txt
 CMakeLists.txt

+ 23 - 0
.pvsconfig

@@ -1,4 +1,5 @@
 # MLib macros we can't do much about.
 # MLib macros we can't do much about.
+//-V:M_LET:1048,1044
 //-V:M_EACH:1048,1044
 //-V:M_EACH:1048,1044
 //-V:ARRAY_DEF:760,747,568,776,729,712,654
 //-V:ARRAY_DEF:760,747,568,776,729,712,654
 //-V:LIST_DEF:760,747,568,712,729,654,776
 //-V:LIST_DEF:760,747,568,712,729,654,776
@@ -16,8 +17,30 @@
 # Potentially null argument warnings
 # Potentially null argument warnings
 //-V:memset:575
 //-V:memset:575
 //-V:memcpy:575
 //-V:memcpy:575
+//-V:memcmp:575
+//-V:strlen:575
 //-V:strcpy:575
 //-V:strcpy:575
+//-V:strncpy:575
 //-V:strchr:575
 //-V:strchr:575
 
 
 # For loop warning on M_FOREACH
 # For loop warning on M_FOREACH
 //-V:for:1044
 //-V:for:1044
+
+# Bitwise OR
+//-V:bit:792
+
+# Do not complain about similar code
+//-V::525
+
+# Common embedded development pointer operations
+//-V::566
+//-V::1032
+
+# Warnings about length mismatch
+//-V:property_value_out:666
+
+# Model-related warnings
+//-V:with_view_model:1044,1048
+
+# Functions that always return the same error code
+//-V:picopass_device_decrypt:1048

+ 1 - 1
.pvsoptions

@@ -1 +1 @@
---rules-config .pvsconfig -e lib/fatfs -e lib/fnv1a-hash -e lib/FreeRTOS-Kernel -e lib/heatshrink -e lib/libusb_stm32 -e lib/littlefs -e lib/mbedtls -e lib/micro-ecc -e lib/microtar -e lib/mlib -e lib/qrcode -e lib/ST25RFAL002 -e lib/STM32CubeWB -e lib/u8g2 -e */arm-none-eabi/*
+--ignore-ccache -C gccarm --rules-config .pvsconfig -e lib/fatfs -e lib/fnv1a-hash -e lib/FreeRTOS-Kernel -e lib/heatshrink -e lib/libusb_stm32 -e lib/littlefs -e lib/mbedtls -e lib/micro-ecc -e lib/microtar -e lib/mlib -e lib/qrcode -e lib/ST25RFAL002 -e lib/STM32CubeWB -e lib/u8g2 -e lib/nanopb -e */arm-none-eabi/* -e applications/plugins/dap_link/lib/free-dap

+ 20 - 2
.vscode/example/tasks.json

@@ -105,6 +105,12 @@
             "type": "shell",
             "type": "shell",
             "command": "./fbt COMPACT=1 DEBUG=0 FORCE=1 flash_usb_full"
             "command": "./fbt COMPACT=1 DEBUG=0 FORCE=1 flash_usb_full"
         },
         },
+        {
+            "label": "[Debug] Create PVS-Studio report",
+            "group": "build",
+            "type": "shell",
+            "command": "./fbt firmware_pvs"
+        },
         {
         {
             "label": "[Debug] Build FAPs",
             "label": "[Debug] Build FAPs",
             "group": "build",
             "group": "build",
@@ -138,6 +144,18 @@
                 "Serial Console"
                 "Serial Console"
             ]
             ]
         },
         },
+        {
+            "label": "[Debug] Build and upload all FAPs to Flipper over USB",
+            "group": "build",
+            "type": "shell",
+            "command": "./fbt fap_deploy"
+        },
+        {
+            "label": "[Release] Build and upload all FAPs to Flipper over USB",
+            "group": "build",
+            "type": "shell",
+            "command": "./fbt COMPACT=1 DEBUG=0 fap_deploy"
+        },
         {
         {
             // Press Ctrl+] to quit
             // Press Ctrl+] to quit
             "label": "Serial Console",
             "label": "Serial Console",
@@ -145,7 +163,7 @@
             "command": "./fbt cli",
             "command": "./fbt cli",
             "group": "none",
             "group": "none",
             "isBackground": true,
             "isBackground": true,
-			"options": {
+            "options": {
                 "env": {
                 "env": {
                     "FBT_NO_SYNC": "0"
                     "FBT_NO_SYNC": "0"
                 }
                 }
@@ -162,4 +180,4 @@
             }
             }
         }
         }
     ]
     ]
-}
+}

+ 5 - 2
.vscode/extensions.json

@@ -11,5 +11,8 @@
 		"augustocdias.tasks-shell-input"
 		"augustocdias.tasks-shell-input"
 	],
 	],
 	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
 	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
-	"unwantedRecommendations": []
-}
+	"unwantedRecommendations": [
+		"twxs.cmake",
+		"ms-vscode.cmake-tools"
+	]
+}

+ 0 - 6
Brewfile

@@ -1,6 +0,0 @@
-cask "gcc-arm-embedded"
-brew "protobuf"
-brew "gdb"
-brew "open-ocd"
-brew "clang-format"
-brew "dfu-util"

+ 8 - 8
CODING_STYLE.md

@@ -3,15 +3,15 @@
 Nice to see you reading this document, we really appreciate it.
 Nice to see you reading this document, we really appreciate it.
 
 
 As all documents of this kind it's unable to cover everything.
 As all documents of this kind it's unable to cover everything.
-But it will cover general rules that we enforcing on PR review.
+But it will cover general rules that we are enforcing on PR review.
 
 
-Also we already have automatic rules checking and formatting,
-but it got it's limitations and this guide is still mandatory.
+Also, we already have automatic rules checking and formatting,
+but it got its limitations and this guide is still mandatory.
 
 
-Some part of this project do have it's own naming and coding guides.
+Some part of this project do have its own naming and coding guides.
 For example: assets. Take a look into `ReadMe.md` in assets folder for more details.
 For example: assets. Take a look into `ReadMe.md` in assets folder for more details.
 
 
-Also 3rd party libraries are none of our concern.
+Also, 3rd party libraries are none of our concern.
 
 
 And yes, this set is not final and we are open to discussion.
 And yes, this set is not final and we are open to discussion.
 If you want to add/remove/change something here please feel free to open new ticket.
 If you want to add/remove/change something here please feel free to open new ticket.
@@ -30,7 +30,7 @@ Our guide is inspired by, but not claiming to be compatible with:
 
 
 Code we write is intended to be public.
 Code we write is intended to be public.
 Avoid one-liners from hell and keep code complexity under control.
 Avoid one-liners from hell and keep code complexity under control.
-Try to make code self explanatory and add comments if needed.
+Try to make code self-explanatory and add comments if needed.
 Leave references to standards that you are implementing.
 Leave references to standards that you are implementing.
 Use project wiki to document new/reverse engineered standards.
 Use project wiki to document new/reverse engineered standards.
 
 
@@ -52,7 +52,7 @@ Almost everything in flipper firmware is built around this concept.
 
 
 ## Naming
 ## Naming
 
 
-### Type names are CamelCase
+### Type names are PascalCase
 
 
 Examples:
 Examples:
 
 
@@ -89,7 +89,7 @@ Enforced by linter.
 Suffixes:
 Suffixes:
 
 
 - `alloc` - allocate and init instance. C style constructor. Returns pointer to instance.
 - `alloc` - allocate and init instance. C style constructor. Returns pointer to instance.
-- `free` - deinit and release instance. C style destructor. Takes pointer to instance.
+- `free` - de-init and release instance. C style destructor. Takes pointer to instance.
 
 
 # C++ coding style
 # C++ coding style
 
 

+ 3 - 3
CONTRIBUTING.md

@@ -23,8 +23,8 @@ Before writing code and creating PR make sure that it aligns with our mission an
 - PR that contains code intended to commit crimes is not going to be accepted.
 - PR that contains code intended to commit crimes is not going to be accepted.
 - Your PR must comply with our [Coding Style](CODING_STYLE.md)
 - Your PR must comply with our [Coding Style](CODING_STYLE.md)
 - Your PR must contain code compatible with project [LICENSE](LICENSE).
 - Your PR must contain code compatible with project [LICENSE](LICENSE).
-- PR will only be merged if it pass CI/CD.
-- PR will only be merged if it pass review by code owner.
+- PR will only be merged if it passes CI/CD.
+- PR will only be merged if it passes review by code owner.
 
 
 Feel free to ask questions in issues if you're not sure.
 Feel free to ask questions in issues if you're not sure.
 
 
@@ -59,7 +59,7 @@ Commit the changes once you are happy with them. Make sure that code compilation
 ### Pull Request
 ### Pull Request
 
 
 When you're done making the changes, open a pull request, often referred to as a PR. 
 When you're done making the changes, open a pull request, often referred to as a PR. 
-- Fill out the "Ready for review" template so we can review your PR. This template helps reviewers understand your changes and the purpose of your pull request. 
+- Fill out the "Ready for review" template, so we can review your PR. This template helps reviewers understand your changes and the purpose of your pull request. 
 - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
 - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
 - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
 - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
 Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information.
 Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information.

+ 0 - 21
Makefile

@@ -1,21 +0,0 @@
-$(info +-------------------------------------------------+)
-$(info |                                                 |)
-$(info |      Hello, this is Flipper team speaking!      |)
-$(info |                                                 |)
-$(info |       We've migrated to new build system        |)
-$(info |          It's nice and based on scons           |)
-$(info |                                                 |)
-$(info |      Crash course:                              |)
-$(info |                                                 |)
-$(info |        `./fbt`                                  |)
-$(info |        `./fbt flash`                            |)
-$(info |        `./fbt debug`                            |)
-$(info |                                                 |)
-$(info |      More details in documentation/fbt.md       |)
-$(info |                                                 |)
-$(info |      Also Please leave your feedback here:      |)
-$(info |           https://flipp.dev/4RDu                |)
-$(info |                      or                         |)
-$(info |           https://flipp.dev/2XM8                |)
-$(info |                                                 |)
-$(info +-------------------------------------------------+)

+ 0 - 51
RoadMap.md

@@ -1,51 +0,0 @@
-# RoadMap
-
-# Where we are (0.x.x branch)
-
-Our goal for 0.x.x branch is to build stable usable apps and API.
-First public release that we support in this branch is 0.43.1. Your device most likely came with this version.
-You can develop applications but keep in mind that API is not final yet.
-
-## What's already implemented
-
-**Applications**
-
-- SubGhz: all most common protocols, reading RAW for everything else
-- 125kHz RFID: all most common protocols
-- NFC: reading/emulating Mifare Ultralight, reading MiFare Classic and DESFire, basic EMV, basic NFC-B,F,V
-- Infrared: all most common RC protocols, RAW format for everything else
-- GPIO: UART bridge, basic GPIO controls
-- iButton: DS1990, Cyfral, Metacom
-- Bad USB: Full USB Rubber Ducky support, some extras for windows alt codes
-- U2F: Full U2F specification support
-
-**Extras**
-
-- BLE Keyboard
-- Snake game
-
-**System and HAL**
-
-- Furi Core
-- Furi HAL 
-
-# Where we're going (Version 1)
-
-Main goal for 1.0.0 is to provide first stable version for both Users and Developers.
-
-## What we're planning to implement in 1.0.0
-
-- Loading applications from SD (tested as PoC, work scheduled for Q2)
-- More protocols (gathering feedback)
-- User documentation (work in progress)
-- FuriCore: get rid of CMSIS API, replace hard real time timers, improve stability and performance (work in progress)
-- FuriHal: deep sleep mode, stable API, examples, documentation (work in progress)
-- Application improvements (a ton of things that we want to add and improve that are too numerous to list here)
-
-## When will it happen and where I can see the progress?
-
-Release 1.0.0 will most likely happen around the end of Q3
-
-Development progress can be tracked in our public Miro board:
-
-https://miro.com/app/board/uXjVO_3D6xU=/?moveToWidget=3458764522498020058&cot=14

+ 28 - 3
SConstruct

@@ -148,9 +148,12 @@ fap_dist = [
             for app_artifact in firmware_env["FW_EXTAPPS"].applications.values()
             for app_artifact in firmware_env["FW_EXTAPPS"].applications.values()
         ),
         ),
     ),
     ),
-    distenv.Install(
-        f"#/dist/{dist_dir}/apps",
-        "#/assets/resources/apps",
+    *(
+        distenv.Install(
+            f"#/dist/{dist_dir}/apps/{app_artifact.app.fap_category}",
+            app_artifact.compact[0],
+        )
+        for app_artifact in firmware_env["FW_EXTAPPS"].applications.values()
     ),
     ),
 ]
 ]
 Depends(
 Depends(
@@ -165,6 +168,14 @@ Alias("fap_dist", fap_dist)
 
 
 distenv.Depends(firmware_env["FW_RESOURCES"], firmware_env["FW_EXTAPPS"].resources_dist)
 distenv.Depends(firmware_env["FW_RESOURCES"], firmware_env["FW_EXTAPPS"].resources_dist)
 
 
+# Copy all faps to device
+
+fap_deploy = distenv.PhonyTarget(
+    "fap_deploy",
+    "${PYTHON3} ${ROOT_DIR}/scripts/storage.py send ${SOURCE} /ext/apps",
+    source=Dir("#/assets/resources/apps"),
+)
+
 
 
 # Target for bundling core2 package for qFlipper
 # Target for bundling core2 package for qFlipper
 copro_dist = distenv.CoproBuilder(
 copro_dist = distenv.CoproBuilder(
@@ -194,6 +205,20 @@ firmware_bm_flash = distenv.PhonyTarget(
     ],
     ],
 )
 )
 
 
+gdb_backtrace_all_threads = distenv.PhonyTarget(
+    "gdb_trace_all",
+    "$GDB $GDBOPTS $SOURCES $GDBFLASH",
+    source=firmware_env["FW_ELF"],
+    GDBOPTS="${GDBOPTS_BASE}",
+    GDBREMOTE="${OPENOCD_GDB_PIPE}",
+    GDBFLASH=[
+        "-ex",
+        "thread apply all bt",
+        "-ex",
+        "quit",
+    ],
+)
+
 # Debugging firmware
 # Debugging firmware
 firmware_debug = distenv.PhonyTarget(
 firmware_debug = distenv.PhonyTarget(
     "debug",
     "debug",

+ 3 - 2
applications/debug/accessor/accessor_app.cpp

@@ -31,9 +31,10 @@ void AccessorApp::run(void) {
     onewire_host_stop(onewire_host);
     onewire_host_stop(onewire_host);
 }
 }
 
 
-AccessorApp::AccessorApp() {
+AccessorApp::AccessorApp()
+    : text_store{0} {
     notification = static_cast<NotificationApp*>(furi_record_open(RECORD_NOTIFICATION));
     notification = static_cast<NotificationApp*>(furi_record_open(RECORD_NOTIFICATION));
-    onewire_host = onewire_host_alloc();
+    onewire_host = onewire_host_alloc(&ibutton_gpio);
     furi_hal_power_enable_otg();
     furi_hal_power_enable_otg();
 }
 }
 
 

+ 1 - 0
applications/debug/accessor/application.fam

@@ -2,6 +2,7 @@ App(
     appid="accessor",
     appid="accessor",
     name="Accessor",
     name="Accessor",
     apptype=FlipperAppType.DEBUG,
     apptype=FlipperAppType.DEBUG,
+    targets=["f7"],
     entry_point="accessor_app",
     entry_point="accessor_app",
     cdefines=["APP_ACCESSOR"],
     cdefines=["APP_ACCESSOR"],
     requires=["gui"],
     requires=["gui"],

+ 0 - 3
applications/debug/accessor/helpers/wiegand.cpp

@@ -171,9 +171,6 @@ bool WIEGAND::DoWiegandConversion() {
                     return true;
                     return true;
                 } else {
                 } else {
                     _lastWiegand = sysTick;
                     _lastWiegand = sysTick;
-                    _bitCount = 0;
-                    _cardTemp = 0;
-                    _cardTempHigh = 0;
                     return false;
                     return false;
                 }
                 }
 
 

+ 1 - 0
applications/debug/battery_test_app/application.fam

@@ -11,4 +11,5 @@ App(
     stack_size=1 * 1024,
     stack_size=1 * 1024,
     order=130,
     order=130,
     fap_category="Debug",
     fap_category="Debug",
+    fap_libs=["assets"],
 )
 )

+ 148 - 0
applications/debug/battery_test_app/views/battery_info.c

@@ -0,0 +1,148 @@
+#include "battery_info.h"
+#include <furi.h>
+#include <gui/elements.h>
+#include <assets_icons.h>
+
+#define LOW_CHARGE_THRESHOLD 10
+#define HIGH_DRAIN_CURRENT_THRESHOLD 100
+
+struct BatteryInfo {
+    View* view;
+};
+
+static void draw_stat(Canvas* canvas, int x, int y, const Icon* icon, char* val) {
+    canvas_draw_frame(canvas, x - 7, y + 7, 30, 13);
+    canvas_draw_icon(canvas, x, y, icon);
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, x - 4, y + 16, 24, 6);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_str_aligned(canvas, x + 8, y + 22, AlignCenter, AlignBottom, val);
+};
+
+static void draw_battery(Canvas* canvas, BatteryInfoModel* data, int x, int y) {
+    char emote[20] = {};
+    char header[20] = {};
+    char value[20] = {};
+
+    int32_t drain_current = data->gauge_current * (-1000);
+    uint32_t charge_current = data->gauge_current * 1000;
+
+    // Draw battery
+    canvas_draw_icon(canvas, x, y, &I_BatteryBody_52x28);
+    if(charge_current > 0) {
+        canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceCharging_29x14);
+    } else if(drain_current > HIGH_DRAIN_CURRENT_THRESHOLD) {
+        canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceConfused_29x14);
+    } else if(data->charge < LOW_CHARGE_THRESHOLD) {
+        canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceNopower_29x14);
+    } else {
+        canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceNormal_29x14);
+    }
+
+    // Draw bubble
+    elements_bubble(canvas, 53, 0, 71, 39);
+
+    // Set text
+    if(charge_current > 0) {
+        snprintf(emote, sizeof(emote), "%s", "Yummy!");
+        snprintf(header, sizeof(header), "%s", "Charging at");
+        snprintf(
+            value,
+            sizeof(value),
+            "%lu.%luV   %lumA",
+            (uint32_t)(data->vbus_voltage),
+            (uint32_t)(data->vbus_voltage * 10) % 10,
+            charge_current);
+    } else if(drain_current > 0) {
+        snprintf(
+            emote,
+            sizeof(emote),
+            "%s",
+            drain_current > HIGH_DRAIN_CURRENT_THRESHOLD ? "Oh no!" : "Om-nom-nom!");
+        snprintf(header, sizeof(header), "%s", "Consumption is");
+        snprintf(
+            value,
+            sizeof(value),
+            "%ld %s",
+            drain_current,
+            drain_current > HIGH_DRAIN_CURRENT_THRESHOLD ? "mA!" : "mA");
+    } else if(drain_current != 0) {
+        snprintf(header, 20, "...");
+    } else if(data->charging_voltage < 4.2) {
+        // Non-default battery charging limit, mention it
+        snprintf(emote, sizeof(emote), "Charged!");
+        snprintf(header, sizeof(header), "Limited to");
+        snprintf(
+            value,
+            sizeof(value),
+            "%lu.%luV",
+            (uint32_t)(data->charging_voltage),
+            (uint32_t)(data->charging_voltage * 10) % 10);
+    } else {
+        snprintf(header, sizeof(header), "Charged!");
+    }
+
+    canvas_draw_str_aligned(canvas, 92, y + 3, AlignCenter, AlignCenter, emote);
+    canvas_draw_str_aligned(canvas, 92, y + 15, AlignCenter, AlignCenter, header);
+    canvas_draw_str_aligned(canvas, 92, y + 27, AlignCenter, AlignCenter, value);
+};
+
+static void battery_info_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    BatteryInfoModel* model = context;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    draw_battery(canvas, model, 0, 5);
+
+    char batt_level[10];
+    char temperature[10];
+    char voltage[10];
+    char health[10];
+
+    snprintf(batt_level, sizeof(batt_level), "%lu%%", (uint32_t)model->charge);
+    snprintf(temperature, sizeof(temperature), "%lu C", (uint32_t)model->gauge_temperature);
+    snprintf(
+        voltage,
+        sizeof(voltage),
+        "%lu.%01lu V",
+        (uint32_t)model->gauge_voltage,
+        (uint32_t)(model->gauge_voltage * 10) % 10UL);
+    snprintf(health, sizeof(health), "%d%%", model->health);
+
+    draw_stat(canvas, 8, 42, &I_Battery_16x16, batt_level);
+    draw_stat(canvas, 40, 42, &I_Temperature_16x16, temperature);
+    draw_stat(canvas, 72, 42, &I_Voltage_16x16, voltage);
+    draw_stat(canvas, 104, 42, &I_Health_16x16, health);
+}
+
+BatteryInfo* battery_info_alloc() {
+    BatteryInfo* battery_info = malloc(sizeof(BatteryInfo));
+    battery_info->view = view_alloc();
+    view_set_context(battery_info->view, battery_info);
+    view_allocate_model(battery_info->view, ViewModelTypeLocking, sizeof(BatteryInfoModel));
+    view_set_draw_callback(battery_info->view, battery_info_draw_callback);
+
+    return battery_info;
+}
+
+void battery_info_free(BatteryInfo* battery_info) {
+    furi_assert(battery_info);
+    view_free(battery_info->view);
+    free(battery_info);
+}
+
+View* battery_info_get_view(BatteryInfo* battery_info) {
+    furi_assert(battery_info);
+    return battery_info->view;
+}
+
+void battery_info_set_data(BatteryInfo* battery_info, BatteryInfoModel* data) {
+    furi_assert(battery_info);
+    furi_assert(data);
+    with_view_model(
+        battery_info->view,
+        BatteryInfoModel * model,
+        { memcpy(model, data, sizeof(BatteryInfoModel)); },
+        true);
+}

+ 23 - 0
applications/debug/battery_test_app/views/battery_info.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct BatteryInfo BatteryInfo;
+
+typedef struct {
+    float vbus_voltage;
+    float gauge_voltage;
+    float gauge_current;
+    float gauge_temperature;
+    float charging_voltage;
+    uint8_t charge;
+    uint8_t health;
+} BatteryInfoModel;
+
+BatteryInfo* battery_info_alloc();
+
+void battery_info_free(BatteryInfo* battery_info);
+
+View* battery_info_get_view(BatteryInfo* battery_info);
+
+void battery_info_set_data(BatteryInfo* battery_info, BatteryInfoModel* data);

+ 3 - 4
applications/debug/bt_debug_app/bt_debug_app.c

@@ -31,9 +31,6 @@ uint32_t bt_debug_start_view(void* context) {
 BtDebugApp* bt_debug_app_alloc() {
 BtDebugApp* bt_debug_app_alloc() {
     BtDebugApp* app = malloc(sizeof(BtDebugApp));
     BtDebugApp* app = malloc(sizeof(BtDebugApp));
 
 
-    // Load settings
-    bt_settings_load(&app->settings);
-
     // Gui
     // Gui
     app->gui = furi_record_open(RECORD_GUI);
     app->gui = furi_record_open(RECORD_GUI);
 
 
@@ -105,13 +102,15 @@ int32_t bt_debug_app(void* p) {
     }
     }
 
 
     BtDebugApp* app = bt_debug_app_alloc();
     BtDebugApp* app = bt_debug_app_alloc();
+    // Was bt active?
+    const bool was_active = furi_hal_bt_is_active();
     // Stop advertising
     // Stop advertising
     furi_hal_bt_stop_advertising();
     furi_hal_bt_stop_advertising();
 
 
     view_dispatcher_run(app->view_dispatcher);
     view_dispatcher_run(app->view_dispatcher);
 
 
     // Restart advertising
     // Restart advertising
-    if(app->settings.enabled) {
+    if(was_active) {
         furi_hal_bt_start_advertising();
         furi_hal_bt_start_advertising();
     }
     }
     bt_debug_app_free(app);
     bt_debug_app_free(app);

+ 2 - 3
applications/debug/bt_debug_app/bt_debug_app.h

@@ -4,15 +4,14 @@
 #include <gui/gui.h>
 #include <gui/gui.h>
 #include <gui/view.h>
 #include <gui/view.h>
 #include <gui/view_dispatcher.h>
 #include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+
 #include <dialogs/dialogs.h>
 #include <dialogs/dialogs.h>
 
 
-#include <gui/modules/submenu.h>
 #include "views/bt_carrier_test.h"
 #include "views/bt_carrier_test.h"
 #include "views/bt_packet_test.h"
 #include "views/bt_packet_test.h"
-#include <bt/bt_settings.h>
 
 
 typedef struct {
 typedef struct {
-    BtSettings settings;
     Gui* gui;
     Gui* gui;
     ViewDispatcher* view_dispatcher;
     ViewDispatcher* view_dispatcher;
     Submenu* submenu;
     Submenu* submenu;

+ 1 - 1
applications/debug/bt_debug_app/views/bt_carrier_test.c

@@ -1,7 +1,7 @@
 #include "bt_carrier_test.h"
 #include "bt_carrier_test.h"
 #include "bt_test.h"
 #include "bt_test.h"
 #include "bt_test_types.h"
 #include "bt_test_types.h"
-#include "furi_hal_bt.h"
+#include <furi_hal_bt.h>
 
 
 struct BtCarrierTest {
 struct BtCarrierTest {
     BtTest* bt_test;
     BtTest* bt_test;

+ 1 - 1
applications/debug/bt_debug_app/views/bt_packet_test.c

@@ -1,7 +1,7 @@
 #include "bt_packet_test.h"
 #include "bt_packet_test.h"
 #include "bt_test.h"
 #include "bt_test.h"
 #include "bt_test_types.h"
 #include "bt_test_types.h"
-#include "furi_hal_bt.h"
+#include <furi_hal_bt.h>
 
 
 struct BtPacketTest {
 struct BtPacketTest {
     BtTest* bt_test;
     BtTest* bt_test;

+ 7 - 4
applications/debug/bt_debug_app/views/bt_test.c

@@ -2,8 +2,11 @@
 
 
 #include <gui/canvas.h>
 #include <gui/canvas.h>
 #include <gui/elements.h>
 #include <gui/elements.h>
+
+#include <lib/toolbox/float_tools.h>
 #include <m-array.h>
 #include <m-array.h>
 #include <furi.h>
 #include <furi.h>
+#include <inttypes.h>
 #include <stdint.h>
 #include <stdint.h>
 
 
 struct BtTestParam {
 struct BtTestParam {
@@ -98,16 +101,16 @@ static void bt_test_draw_callback(Canvas* canvas, void* _model) {
     elements_scrollbar(canvas, model->position, BtTestParamArray_size(model->params));
     elements_scrollbar(canvas, model->position, BtTestParamArray_size(model->params));
     canvas_draw_str(canvas, 6, 60, model->message);
     canvas_draw_str(canvas, 6, 60, model->message);
     if(model->state == BtTestStateStarted) {
     if(model->state == BtTestStateStarted) {
-        if(model->rssi != 0.0f) {
+        if(!float_is_equal(model->rssi, 0.0f)) {
             snprintf(info_str, sizeof(info_str), "RSSI:%3.1f dB", (double)model->rssi);
             snprintf(info_str, sizeof(info_str), "RSSI:%3.1f dB", (double)model->rssi);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
         }
         }
     } else if(model->state == BtTestStateStopped) {
     } else if(model->state == BtTestStateStopped) {
         if(model->packets_num_rx) {
         if(model->packets_num_rx) {
-            snprintf(info_str, sizeof(info_str), "%ld pack rcv", model->packets_num_rx);
+            snprintf(info_str, sizeof(info_str), "%" PRIu32 " pack rcv", model->packets_num_rx);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
         } else if(model->packets_num_tx) {
         } else if(model->packets_num_tx) {
-            snprintf(info_str, sizeof(info_str), "%ld pack sent", model->packets_num_tx);
+            snprintf(info_str, sizeof(info_str), "%" PRIu32 " pack sent", model->packets_num_tx);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
         }
         }
     }
     }
@@ -153,7 +156,7 @@ static bool bt_test_input_callback(InputEvent* event, void* context) {
 }
 }
 
 
 void bt_test_process_up(BtTest* bt_test) {
 void bt_test_process_up(BtTest* bt_test) {
-    with_view_model(
+    with_view_model( // -V658
         bt_test->view,
         bt_test->view,
         BtTestModel * model,
         BtTestModel * model,
         {
         {

+ 10 - 0
applications/debug/direct_draw/application.fam

@@ -0,0 +1,10 @@
+App(
+    appid="direct_draw",
+    name="Direct Draw",
+    apptype=FlipperAppType.DEBUG,
+    entry_point="direct_draw_app",
+    requires=["gui", "input"],
+    stack_size=2 * 1024,
+    order=70,
+    fap_category="Debug",
+)

+ 112 - 0
applications/debug/direct_draw/direct_draw.c

@@ -0,0 +1,112 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/canvas_i.h>
+#include <input/input.h>
+
+#define BUFFER_SIZE (32U)
+
+typedef struct {
+    FuriPubSub* input;
+    FuriPubSubSubscription* input_subscription;
+    Gui* gui;
+    Canvas* canvas;
+    bool stop;
+    uint32_t counter;
+} DirectDraw;
+
+static void gui_input_events_callback(const void* value, void* ctx) {
+    furi_assert(value);
+    furi_assert(ctx);
+
+    DirectDraw* instance = ctx;
+    const InputEvent* event = value;
+
+    if(event->key == InputKeyBack && event->type == InputTypeShort) {
+        instance->stop = true;
+    }
+}
+
+static DirectDraw* direct_draw_alloc() {
+    DirectDraw* instance = malloc(sizeof(DirectDraw));
+
+    instance->input = furi_record_open(RECORD_INPUT_EVENTS);
+    instance->gui = furi_record_open(RECORD_GUI);
+    instance->canvas = gui_direct_draw_acquire(instance->gui);
+
+    instance->input_subscription =
+        furi_pubsub_subscribe(instance->input, gui_input_events_callback, instance);
+
+    return instance;
+}
+
+static void direct_draw_free(DirectDraw* instance) {
+    furi_pubsub_unsubscribe(instance->input, instance->input_subscription);
+
+    instance->canvas = NULL;
+    gui_direct_draw_release(instance->gui);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_INPUT_EVENTS);
+}
+
+static void direct_draw_block(Canvas* canvas, uint32_t size, uint32_t counter) {
+    size += 16;
+    uint8_t width = canvas_width(canvas) - size;
+    uint8_t height = canvas_height(canvas) - size;
+
+    uint8_t x = counter % width;
+    if((counter / width) % 2) {
+        x = width - x;
+    }
+
+    uint8_t y = counter % height;
+    if((counter / height) % 2) {
+        y = height - y;
+    }
+
+    canvas_draw_box(canvas, x, y, size, size);
+}
+
+static void direct_draw_run(DirectDraw* instance) {
+    size_t start = DWT->CYCCNT;
+    size_t counter = 0;
+    float fps = 0;
+
+    vTaskPrioritySet(furi_thread_get_current_id(), FuriThreadPriorityIdle);
+
+    do {
+        size_t elapsed = DWT->CYCCNT - start;
+        char buffer[BUFFER_SIZE] = {0};
+
+        if(elapsed >= 64000000) {
+            fps = (float)counter / ((float)elapsed / 64000000.0f);
+
+            start = DWT->CYCCNT;
+            counter = 0;
+        }
+        snprintf(buffer, BUFFER_SIZE, "FPS: %.1f", (double)fps);
+
+        canvas_reset(instance->canvas);
+        canvas_set_color(instance->canvas, ColorXOR);
+        direct_draw_block(instance->canvas, instance->counter % 16, instance->counter);
+        direct_draw_block(instance->canvas, instance->counter * 2 % 16, instance->counter * 2);
+        direct_draw_block(instance->canvas, instance->counter * 3 % 16, instance->counter * 3);
+        direct_draw_block(instance->canvas, instance->counter * 4 % 16, instance->counter * 4);
+        direct_draw_block(instance->canvas, instance->counter * 5 % 16, instance->counter * 5);
+        canvas_draw_str(instance->canvas, 10, 10, buffer);
+        canvas_commit(instance->canvas);
+
+        counter++;
+        instance->counter++;
+        furi_thread_yield();
+    } while(!instance->stop);
+}
+
+int32_t direct_draw_app(void* p) {
+    UNUSED(p);
+
+    DirectDraw* instance = direct_draw_alloc();
+    direct_draw_run(instance);
+    direct_draw_free(instance);
+
+    return 0;
+}

+ 1 - 0
applications/debug/display_test/application.fam

@@ -5,6 +5,7 @@ App(
     entry_point="display_test_app",
     entry_point="display_test_app",
     cdefines=["APP_DISPLAY_TEST"],
     cdefines=["APP_DISPLAY_TEST"],
     requires=["gui"],
     requires=["gui"],
+    fap_libs=["misc"],
     stack_size=1 * 1024,
     stack_size=1 * 1024,
     order=120,
     order=120,
     fap_category="Debug",
     fap_category="Debug",

+ 0 - 1
applications/debug/display_test/display_test.c

@@ -91,7 +91,6 @@ static void display_test_reload_config(DisplayTest* instance) {
         instance->config_contrast,
         instance->config_contrast,
         instance->config_regulation_ratio,
         instance->config_regulation_ratio,
         instance->config_bias);
         instance->config_bias);
-    gui_update(instance->gui);
 }
 }
 
 
 static void display_config_set_bias(VariableItem* item) {
 static void display_config_set_bias(VariableItem* item) {

+ 9 - 0
applications/debug/example_custom_font/application.fam

@@ -0,0 +1,9 @@
+App(
+    appid="example_custom_font",
+    name="Example: custom font",
+    apptype=FlipperAppType.DEBUG,
+    entry_point="example_custom_font_main",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    fap_category="Debug",
+)

+ 98 - 0
applications/debug/example_custom_font/example_custom_font.c

@@ -0,0 +1,98 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <input/input.h>
+
+//This arrays contains the font itself. You can use any u8g2 font you want
+
+/*
+Fontname: -Raccoon-Fixed4x6-Medium-R-Normal--6-60-75-75-P-40-ISO10646-1
+Copyright: 
+Glyphs: 95/203
+BBX Build Mode: 0
+*/
+const uint8_t u8g2_font_tom_thumb_4x6_tr[725] =
+    "_\0\2\2\2\3\3\4\4\3\6\0\377\5\377\5\0\0\352\1\330\2\270 \5\340\315\0!\6\265\310"
+    "\254\0\42\6\213\313$\25#\10\227\310\244\241\206\12$\10\227\310\215\70b\2%\10\227\310d\324F\1"
+    "&\10\227\310(\65R\22'\5\251\313\10(\6\266\310\251\62)\10\226\310\304\224\24\0*\6\217\312\244"
+    "\16+\7\217\311\245\225\0,\6\212\310)\0-\5\207\312\14.\5\245\310\4/\7\227\310Ve\4\60"
+    "\7\227\310-k\1\61\6\226\310\255\6\62\10\227\310h\220\312\1\63\11\227\310h\220\62X\0\64\10\227"
+    "\310$\65b\1\65\10\227\310\214\250\301\2\66\10\227\310\315\221F\0\67\10\227\310\314TF\0\70\10\227"
+    "\310\214\64\324\10\71\10\227\310\214\64\342\2:\6\255\311\244\0;\7\222\310e\240\0<\10\227\310\246\32"
+    "d\20=\6\217\311l\60>\11\227\310d\220A*\1\77\10\227\310\314\224a\2@\10\227\310UC\3"
+    "\1A\10\227\310UC\251\0B\10\227\310\250\264\322\2C\7\227\310\315\32\10D\10\227\310\250d-\0"
+    "E\10\227\310\214\70\342\0F\10\227\310\214\70b\4G\10\227\310\315\221\222\0H\10\227\310$\65\224\12"
+    "I\7\227\310\254X\15J\7\227\310\226\252\2K\10\227\310$\265\222\12L\7\227\310\304\346\0M\10\227"
+    "\310\244\61\224\12N\10\227\310\244q\250\0O\7\227\310UV\5P\10\227\310\250\264b\4Q\10\227\310"
+    "Uj$\1R\10\227\310\250\64V\1S\10\227\310m\220\301\2T\7\227\310\254\330\2U\7\227\310$"
+    "W\22V\10\227\310$\253L\0W\10\227\310$\65\206\12X\10\227\310$\325R\1Y\10\227\310$U"
+    "V\0Z\7\227\310\314T\16[\7\227\310\214X\16\134\10\217\311d\220A\0]\7\227\310\314r\4^"
+    "\5\213\313\65_\5\207\310\14`\6\212\313\304\0a\7\223\310\310\65\2b\10\227\310D\225\324\2c\7"
+    "\223\310\315\14\4d\10\227\310\246\245\222\0e\6\223\310\235\2f\10\227\310\246\264b\2g\10\227\307\35"
+    "\61%\0h\10\227\310D\225\254\0i\6\265\310\244\1j\10\233\307f\30U\5k\10\227\310\304\264T"
+    "\1l\7\227\310\310\326\0m\7\223\310<R\0n\7\223\310\250d\5o\7\223\310U\252\2p\10\227"
+    "\307\250\244V\4q\10\227\307-\225d\0r\6\223\310\315\22s\10\223\310\215\70\22\0t\10\227\310\245"
+    "\25\243\0u\7\223\310$+\11v\10\223\310$\65R\2w\7\223\310\244q\4x\7\223\310\244\62\25"
+    "y\11\227\307$\225dJ\0z\7\223\310\254\221\6{\10\227\310\251\32D\1|\6\265\310(\1}\11"
+    "\227\310\310\14RR\0~\6\213\313\215\4\0\0\0\4\377\377\0";
+
+// Screen is 128x64 px
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    UNUSED(ctx);
+
+    canvas_clear(canvas);
+
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_tom_thumb_4x6_tr);
+
+    canvas_draw_str(canvas, 0, 6, "This is a tiny custom font");
+    canvas_draw_str(canvas, 0, 12, "012345.?! ,:;\"\'@#$%");
+}
+
+static void app_input_callback(InputEvent* input_event, void* ctx) {
+    furi_assert(ctx);
+
+    FuriMessageQueue* event_queue = ctx;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+int32_t example_custom_font_main(void* p) {
+    UNUSED(p);
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    // Configure view port
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, app_draw_callback, view_port);
+    view_port_input_callback_set(view_port, app_input_callback, event_queue);
+
+    // Register view port in GUI
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    InputEvent event;
+
+    bool running = true;
+
+    while(running) {
+        if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
+            if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
+                switch(event.key) {
+                case InputKeyBack:
+                    running = false;
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+    }
+
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    view_port_free(view_port);
+    furi_message_queue_free(event_queue);
+
+    furi_record_close(RECORD_GUI);
+
+    return 0;
+}

+ 6 - 5
applications/debug/file_browser_test/file_browser_app.c

@@ -1,10 +1,11 @@
-#include <file_browser_test_icons.h>
 #include "file_browser_app_i.h"
 #include "file_browser_app_i.h"
-#include "gui/modules/file_browser.h"
-#include <furi.h>
-#include <furi_hal.h>
+#include <file_browser_test_icons.h>
+
+#include <gui/modules/file_browser.h>
 #include <storage/storage.h>
 #include <storage/storage.h>
 #include <lib/toolbox/path.h>
 #include <lib/toolbox/path.h>
+#include <furi.h>
+#include <furi_hal.h>
 
 
 static bool file_browser_app_custom_event_callback(void* context, uint32_t event) {
 static bool file_browser_app_custom_event_callback(void* context, uint32_t event) {
     furi_assert(context);
     furi_assert(context);
@@ -48,7 +49,7 @@ FileBrowserApp* file_browser_app_alloc(char* arg) {
 
 
     app->file_path = furi_string_alloc();
     app->file_path = furi_string_alloc();
     app->file_browser = file_browser_alloc(app->file_path);
     app->file_browser = file_browser_alloc(app->file_path);
-    file_browser_configure(app->file_browser, "*", NULL, true, &I_badusb_10px, true);
+    file_browser_configure(app->file_browser, "*", NULL, true, false, &I_badusb_10px, true);
 
 
     view_dispatcher_add_view(
     view_dispatcher_add_view(
         app->view_dispatcher, FileBrowserAppViewStart, widget_get_view(app->widget));
         app->view_dispatcher, FileBrowserAppViewStart, widget_get_view(app->widget));

+ 1 - 0
applications/debug/lfrfid_debug/application.fam

@@ -2,6 +2,7 @@ App(
     appid="lfrfid_debug",
     appid="lfrfid_debug",
     name="LF-RFID Debug",
     name="LF-RFID Debug",
     apptype=FlipperAppType.DEBUG,
     apptype=FlipperAppType.DEBUG,
+    targets=["f7"],
     entry_point="lfrfid_debug_app",
     entry_point="lfrfid_debug_app",
     requires=[
     requires=[
         "gui",
         "gui",

+ 5 - 1
applications/debug/rpc_debug_app/scenes/rpc_debug_app_scene_input_error_code.c

@@ -44,7 +44,11 @@ bool rpc_debug_app_scene_input_error_code_on_event(void* context, SceneManagerEv
 
 
     if(event.type == SceneManagerEventTypeCustom) {
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == RpcDebugAppCustomEventInputErrorCode) {
         if(event.event == RpcDebugAppCustomEventInputErrorCode) {
-            rpc_system_app_set_error_code(app->rpc, (uint32_t)atol(app->text_store));
+            char* end;
+            int error_code = strtol(app->text_store, &end, 10);
+            if(!*end) {
+                rpc_system_app_set_error_code(app->rpc, error_code);
+            }
             scene_manager_previous_scene(app->scene_manager);
             scene_manager_previous_scene(app->scene_manager);
             consumed = true;
             consumed = true;
         }
         }

+ 60 - 0
applications/debug/unit_tests/float_tools/float_tools_test.c

@@ -0,0 +1,60 @@
+#include <float.h>
+#include <float_tools.h>
+
+#include "../minunit.h"
+
+MU_TEST(float_tools_equal_test) {
+    mu_check(float_is_equal(FLT_MAX, FLT_MAX));
+    mu_check(float_is_equal(FLT_MIN, FLT_MIN));
+    mu_check(float_is_equal(-FLT_MAX, -FLT_MAX));
+    mu_check(float_is_equal(-FLT_MIN, -FLT_MIN));
+
+    mu_check(!float_is_equal(FLT_MIN, FLT_MAX));
+    mu_check(!float_is_equal(-FLT_MIN, FLT_MAX));
+    mu_check(!float_is_equal(FLT_MIN, -FLT_MAX));
+    mu_check(!float_is_equal(-FLT_MIN, -FLT_MAX));
+
+    const float pi = 3.14159f;
+    mu_check(float_is_equal(pi, pi));
+    mu_check(float_is_equal(-pi, -pi));
+    mu_check(!float_is_equal(pi, -pi));
+    mu_check(!float_is_equal(-pi, pi));
+
+    const float one_third = 1.f / 3.f;
+    const float one_third_dec = 0.3333333f;
+    mu_check(one_third != one_third_dec);
+    mu_check(float_is_equal(one_third, one_third_dec));
+
+    const float big_num = 1.e12f;
+    const float med_num = 95.389f;
+    const float smol_num = 1.e-12f;
+    mu_check(float_is_equal(big_num, big_num));
+    mu_check(float_is_equal(med_num, med_num));
+    mu_check(float_is_equal(smol_num, smol_num));
+    mu_check(!float_is_equal(smol_num, big_num));
+    mu_check(!float_is_equal(med_num, smol_num));
+    mu_check(!float_is_equal(big_num, med_num));
+
+    const float more_than_one = 1.f + FLT_EPSILON;
+    const float less_than_one = 1.f - FLT_EPSILON;
+    mu_check(!float_is_equal(more_than_one, less_than_one));
+    mu_check(!float_is_equal(more_than_one, -less_than_one));
+    mu_check(!float_is_equal(-more_than_one, less_than_one));
+    mu_check(!float_is_equal(-more_than_one, -less_than_one));
+
+    const float slightly_more_than_one = 1.f + FLT_EPSILON / 2.f;
+    const float slightly_less_than_one = 1.f - FLT_EPSILON / 2.f;
+    mu_check(float_is_equal(slightly_more_than_one, slightly_less_than_one));
+    mu_check(float_is_equal(-slightly_more_than_one, -slightly_less_than_one));
+    mu_check(!float_is_equal(slightly_more_than_one, -slightly_less_than_one));
+    mu_check(!float_is_equal(-slightly_more_than_one, slightly_less_than_one));
+}
+
+MU_TEST_SUITE(float_tools_suite) {
+    MU_RUN_TEST(float_tools_equal_test);
+}
+
+int run_minunit_test_float_tools() {
+    MU_RUN_SUITE(float_tools_suite);
+    return MU_EXIT_CODE;
+}

+ 20 - 81
applications/debug/unit_tests/furi/furi_memmgr_test.c

@@ -3,98 +3,37 @@
 #include <string.h>
 #include <string.h>
 #include <stdbool.h>
 #include <stdbool.h>
 
 
-// this test is not accurate, but gives a basic understanding
-// that memory management is working fine
-
-// do not include memmgr.h here
-// we also test that we are linking against stdlib
-extern size_t memmgr_get_free_heap(void);
-extern size_t memmgr_get_minimum_free_heap(void);
-
-// current heap management realization consume:
-// X bytes after allocate and 0 bytes after allocate and free,
-// where X = sizeof(void*) + sizeof(size_t), look to BlockLink_t
-const size_t heap_overhead_max_size = sizeof(void*) + sizeof(size_t);
-
-bool heap_equal(size_t heap_size, size_t heap_size_old) {
-    // heap borders with overhead
-    const size_t heap_low = heap_size_old - heap_overhead_max_size;
-    const size_t heap_high = heap_size_old + heap_overhead_max_size;
-
-    // not exact, so we must test it against bigger numbers than "overhead size"
-    const bool result = ((heap_size >= heap_low) && (heap_size <= heap_high));
-
-    // debug allocation info
-    if(!result) {
-        printf("\n(hl: %zu) <= (p: %zu) <= (hh: %zu)\n", heap_low, heap_size, heap_high);
-    }
-
-    return result;
-}
-
 void test_furi_memmgr() {
 void test_furi_memmgr() {
-    size_t heap_size = 0;
-    size_t heap_size_old = 0;
-    const int alloc_size = 128;
-
-    void* ptr = NULL;
-    void* original_ptr = NULL;
-
-    // do not include furi memmgr.h case
-#ifdef FURI_MEMMGR_GUARD
-    mu_fail("do not link against furi memmgr.h");
-#endif
+    void* ptr;
 
 
     // allocate memory case
     // allocate memory case
-    heap_size_old = memmgr_get_free_heap();
-    ptr = malloc(alloc_size);
-    heap_size = memmgr_get_free_heap();
-    mu_assert_pointers_not_eq(ptr, NULL);
-    mu_assert(heap_equal(heap_size, heap_size_old - alloc_size), "allocate failed");
-
-    // free memory case
-    heap_size_old = memmgr_get_free_heap();
+    ptr = malloc(100);
+    mu_check(ptr != NULL);
+    // test that memory is zero-initialized after allocation
+    for(int i = 0; i < 100; i++) {
+        mu_assert_int_eq(0, ((uint8_t*)ptr)[i]);
+    }
     free(ptr);
     free(ptr);
-    ptr = NULL;
-    heap_size = memmgr_get_free_heap();
-    mu_assert(heap_equal(heap_size, heap_size_old + alloc_size), "free failed");
 
 
     // reallocate memory case
     // reallocate memory case
-
-    // get filled array with some data
-    original_ptr = malloc(alloc_size);
-    mu_assert_pointers_not_eq(original_ptr, NULL);
-    for(int i = 0; i < alloc_size; i++) {
-        *(unsigned char*)(original_ptr + i) = i;
+    ptr = malloc(100);
+    memset(ptr, 66, 100);
+    ptr = realloc(ptr, 200);
+    mu_check(ptr != NULL);
+
+    // test that memory is really reallocated
+    for(int i = 0; i < 100; i++) {
+        mu_assert_int_eq(66, ((uint8_t*)ptr)[i]);
     }
     }
 
 
-    // malloc array and copy data
-    ptr = malloc(alloc_size);
-    mu_assert_pointers_not_eq(ptr, NULL);
-    memcpy(ptr, original_ptr, alloc_size);
-
-    // reallocate array
-    heap_size_old = memmgr_get_free_heap();
-    ptr = realloc(ptr, alloc_size * 2);
-    heap_size = memmgr_get_free_heap();
-    mu_assert(heap_equal(heap_size, heap_size_old - alloc_size), "reallocate failed");
-    mu_assert_int_eq(memcmp(original_ptr, ptr, alloc_size), 0);
-    free(original_ptr);
+    // TODO: fix realloc to copy only old size, and write testcase that leftover of reallocated memory is zero-initialized
     free(ptr);
     free(ptr);
 
 
     // allocate and zero-initialize array (calloc)
     // allocate and zero-initialize array (calloc)
-    original_ptr = malloc(alloc_size);
-    mu_assert_pointers_not_eq(original_ptr, NULL);
-
-    for(int i = 0; i < alloc_size; i++) {
-        *(unsigned char*)(original_ptr + i) = 0;
+    ptr = calloc(100, 2);
+    mu_check(ptr != NULL);
+    for(int i = 0; i < 100 * 2; i++) {
+        mu_assert_int_eq(0, ((uint8_t*)ptr)[i]);
     }
     }
-    heap_size_old = memmgr_get_free_heap();
-    ptr = calloc(1, alloc_size);
-    heap_size = memmgr_get_free_heap();
-    mu_assert(heap_equal(heap_size, heap_size_old - alloc_size), "callocate failed");
-    mu_assert_int_eq(memcmp(original_ptr, ptr, alloc_size), 0);
-
-    free(original_ptr);
     free(ptr);
     free(ptr);
 }
 }

+ 75 - 0
applications/debug/unit_tests/manifest/manifest.c

@@ -0,0 +1,75 @@
+#include <furi.c>
+#include "../minunit.h"
+#include <update_util/resources/manifest.h>
+
+#define TAG "Manifest"
+
+MU_TEST(manifest_type_test) {
+    mu_assert(ResourceManifestEntryTypeUnknown == 0, "ResourceManifestEntryTypeUnknown != 0\r\n");
+    mu_assert(ResourceManifestEntryTypeVersion == 1, "ResourceManifestEntryTypeVersion != 1\r\n");
+    mu_assert(
+        ResourceManifestEntryTypeTimestamp == 2, "ResourceManifestEntryTypeTimestamp != 2\r\n");
+    mu_assert(
+        ResourceManifestEntryTypeDirectory == 3, "ResourceManifestEntryTypeDirectory != 3\r\n");
+    mu_assert(ResourceManifestEntryTypeFile == 4, "ResourceManifestEntryTypeFile != 4\r\n");
+}
+
+MU_TEST(manifest_iteration_test) {
+    bool result = true;
+    size_t counters[5] = {0};
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    ResourceManifestReader* manifest_reader = resource_manifest_reader_alloc(storage);
+    do {
+        // Open manifest file
+        if(!resource_manifest_reader_open(manifest_reader, EXT_PATH("unit_tests/Manifest"))) {
+            result = false;
+            break;
+        }
+
+        // Iterate forward
+        ResourceManifestEntry* entry_ptr = NULL;
+        while((entry_ptr = resource_manifest_reader_next(manifest_reader))) {
+            FURI_LOG_D(TAG, "F:%u:%s", entry_ptr->type, furi_string_get_cstr(entry_ptr->name));
+            if(entry_ptr->type > 4) {
+                mu_fail("entry_ptr->type > 4\r\n");
+                result = false;
+                break;
+            }
+            counters[entry_ptr->type]++;
+        }
+        if(!result) break;
+
+        // Iterate backward
+        while((entry_ptr = resource_manifest_reader_previous(manifest_reader))) {
+            FURI_LOG_D(TAG, "B:%u:%s", entry_ptr->type, furi_string_get_cstr(entry_ptr->name));
+            if(entry_ptr->type > 4) {
+                mu_fail("entry_ptr->type > 4\r\n");
+                result = false;
+                break;
+            }
+            counters[entry_ptr->type]--;
+        }
+    } while(false);
+
+    resource_manifest_reader_free(manifest_reader);
+    furi_record_close(RECORD_STORAGE);
+
+    mu_assert(counters[ResourceManifestEntryTypeUnknown] == 0, "Unknown counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeVersion] == 0, "Version counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeTimestamp] == 0, "Timestamp counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeDirectory] == 0, "Directory counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeFile] == 0, "File counter != 0\r\n");
+
+    mu_assert(result, "Manifest forward iterate failed\r\n");
+}
+
+MU_TEST_SUITE(manifest_suite) {
+    MU_RUN_TEST(manifest_type_test);
+    MU_RUN_TEST(manifest_iteration_test);
+}
+
+int run_minunit_test_manifest() {
+    MU_RUN_SUITE(manifest_suite);
+    return MU_EXIT_CODE;
+}

+ 31 - 2
applications/debug/unit_tests/nfc/nfc_test.c

@@ -348,13 +348,37 @@ static void mf_classic_generator_test(uint8_t uid_len, MfClassicType type) {
     memcpy(atqa, nfc_dev->dev_data.nfc_data.atqa, 2);
     memcpy(atqa, nfc_dev->dev_data.nfc_data.atqa, 2);
 
 
     MfClassicData* mf_data = &nfc_dev->dev_data.mf_classic_data;
     MfClassicData* mf_data = &nfc_dev->dev_data.mf_classic_data;
-    // Check the manufacturer block (should be uid[uid_len] + 0xFF[rest])
+    // Check the manufacturer block (should be uid[uid_len] + BCC (for 4byte only) + SAK + ATQA0 + ATQA1 + 0xFF[rest])
     uint8_t manufacturer_block[16] = {0};
     uint8_t manufacturer_block[16] = {0};
     memcpy(manufacturer_block, nfc_dev->dev_data.mf_classic_data.block[0].value, 16);
     memcpy(manufacturer_block, nfc_dev->dev_data.mf_classic_data.block[0].value, 16);
     mu_assert(
     mu_assert(
         memcmp(manufacturer_block, uid, uid_len) == 0,
         memcmp(manufacturer_block, uid, uid_len) == 0,
         "manufacturer_block uid doesn't match the file\r\n");
         "manufacturer_block uid doesn't match the file\r\n");
-    for(uint8_t i = uid_len; i < 16; i++) {
+
+    uint8_t position = 0;
+    if(uid_len == 4) {
+        position = uid_len;
+
+        uint8_t bcc = 0;
+
+        for(int i = 0; i < uid_len; i++) {
+            bcc ^= uid[i];
+        }
+
+        mu_assert(manufacturer_block[position] == bcc, "manufacturer_block bcc assert failed\r\n");
+    } else {
+        position = uid_len - 1;
+    }
+
+    mu_assert(manufacturer_block[position + 1] == sak, "manufacturer_block sak assert failed\r\n");
+
+    mu_assert(
+        manufacturer_block[position + 2] == atqa[0], "manufacturer_block atqa0 assert failed\r\n");
+
+    mu_assert(
+        manufacturer_block[position + 3] == atqa[1], "manufacturer_block atqa1 assert failed\r\n");
+
+    for(uint8_t i = position + 4; i < 16; i++) {
         mu_assert(
         mu_assert(
             manufacturer_block[i] == 0xFF, "manufacturer_block[i] == 0xFF assert failed\r\n");
             manufacturer_block[i] == 0xFF, "manufacturer_block[i] == 0xFF assert failed\r\n");
     }
     }
@@ -466,6 +490,10 @@ static void mf_classic_generator_test(uint8_t uid_len, MfClassicType type) {
     nfc_device_free(nfc_keys);
     nfc_device_free(nfc_keys);
 }
 }
 
 
+MU_TEST(mf_mini_file_test) {
+    mf_classic_generator_test(4, MfClassicTypeMini);
+}
+
 MU_TEST(mf_classic_1k_4b_file_test) {
 MU_TEST(mf_classic_1k_4b_file_test) {
     mf_classic_generator_test(4, MfClassicType1k);
     mf_classic_generator_test(4, MfClassicType1k);
 }
 }
@@ -486,6 +514,7 @@ MU_TEST_SUITE(nfc) {
     nfc_test_alloc();
     nfc_test_alloc();
 
 
     MU_RUN_TEST(nfca_file_test);
     MU_RUN_TEST(nfca_file_test);
+    MU_RUN_TEST(mf_mini_file_test);
     MU_RUN_TEST(mf_classic_1k_4b_file_test);
     MU_RUN_TEST(mf_classic_1k_4b_file_test);
     MU_RUN_TEST(mf_classic_4k_4b_file_test);
     MU_RUN_TEST(mf_classic_4k_4b_file_test);
     MU_RUN_TEST(mf_classic_1k_7b_file_test);
     MU_RUN_TEST(mf_classic_1k_7b_file_test);

+ 1 - 1
applications/debug/unit_tests/rpc/rpc_test.c

@@ -89,7 +89,7 @@ static void test_rpc_setup(void) {
     }
     }
     furi_check(rpc_session[0].session);
     furi_check(rpc_session[0].session);
 
 
-    rpc_session[0].output_stream = furi_stream_buffer_alloc(1000, 1);
+    rpc_session[0].output_stream = furi_stream_buffer_alloc(4096, 1);
     rpc_session_set_send_bytes_callback(rpc_session[0].session, output_bytes_callback);
     rpc_session_set_send_bytes_callback(rpc_session[0].session, output_bytes_callback);
     rpc_session[0].close_session_semaphore = xSemaphoreCreateBinary();
     rpc_session[0].close_session_semaphore = xSemaphoreCreateBinary();
     rpc_session[0].terminate_semaphore = xSemaphoreCreateBinary();
     rpc_session[0].terminate_semaphore = xSemaphoreCreateBinary();

+ 25 - 1
applications/debug/unit_tests/stream/stream_test.c

@@ -72,8 +72,32 @@ MU_TEST_1(stream_composite_subtest, Stream* stream) {
     mu_check(stream_seek(stream, -3, StreamOffsetFromEnd));
     mu_check(stream_seek(stream, -3, StreamOffsetFromEnd));
     mu_check(stream_tell(stream) == 4);
     mu_check(stream_tell(stream) == 4);
 
 
-    // write string with replacemet
+    // test seeks to char. content: '1337_69'
+    stream_rewind(stream);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 1);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 2);
+    mu_check(stream_seek_to_char(stream, '_', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 4);
+    mu_check(stream_seek_to_char(stream, '9', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 6);
+    mu_check(!stream_seek_to_char(stream, '9', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 6);
+    mu_check(stream_seek_to_char(stream, '_', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 4);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 2);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 1);
+    mu_check(!stream_seek_to_char(stream, '3', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 1);
+    mu_check(stream_seek_to_char(stream, '1', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 0);
+
+    // write string with replacement
     // "1337_69" -> "1337lee"
     // "1337_69" -> "1337lee"
+    mu_check(stream_seek(stream, 4, StreamOffsetFromStart));
     mu_check(stream_write_string(stream, string_lee) == 3);
     mu_check(stream_write_string(stream, string_lee) == 3);
     mu_check(stream_size(stream) == 7);
     mu_check(stream_size(stream) == 7);
     mu_check(stream_tell(stream) == 7);
     mu_check(stream_tell(stream) == 7);

+ 80 - 2
applications/debug/unit_tests/subghz/subghz_test.c

@@ -12,8 +12,9 @@
 #define KEYSTORE_DIR_NAME EXT_PATH("subghz/assets/keeloq_mfcodes")
 #define KEYSTORE_DIR_NAME EXT_PATH("subghz/assets/keeloq_mfcodes")
 #define CAME_ATOMO_DIR_NAME EXT_PATH("subghz/assets/came_atomo")
 #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 NICE_FLOR_S_DIR_NAME EXT_PATH("subghz/assets/nice_flor_s")
+#define ALUTECH_AT_4N_DIR_NAME EXT_PATH("subghz/assets/alutech_at_4n")
 #define TEST_RANDOM_DIR_NAME EXT_PATH("unit_tests/subghz/test_random_raw.sub")
 #define TEST_RANDOM_DIR_NAME EXT_PATH("unit_tests/subghz/test_random_raw.sub")
-#define TEST_RANDOM_COUNT_PARSE 253
+#define TEST_RANDOM_COUNT_PARSE 329
 #define TEST_TIMEOUT 10000
 #define TEST_TIMEOUT 10000
 
 
 static SubGhzEnvironment* environment_handler;
 static SubGhzEnvironment* environment_handler;
@@ -43,6 +44,8 @@ static void subghz_test_init(void) {
         environment_handler, CAME_ATOMO_DIR_NAME);
         environment_handler, CAME_ATOMO_DIR_NAME);
     subghz_environment_set_nice_flor_s_rainbow_table_file_name(
     subghz_environment_set_nice_flor_s_rainbow_table_file_name(
         environment_handler, NICE_FLOR_S_DIR_NAME);
         environment_handler, NICE_FLOR_S_DIR_NAME);
+    subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
+        environment_handler, ALUTECH_AT_4N_DIR_NAME);
     subghz_environment_set_protocol_registry(
     subghz_environment_set_protocol_registry(
         environment_handler, (void*)&subghz_protocol_registry);
         environment_handler, (void*)&subghz_protocol_registry);
 
 
@@ -318,7 +321,10 @@ bool subghz_hal_async_tx_test_run(SubGhzHalAsyncTxTestType type) {
     furi_hal_subghz_load_preset(FuriHalSubGhzPresetOok650Async);
     furi_hal_subghz_load_preset(FuriHalSubGhzPresetOok650Async);
     furi_hal_subghz_set_frequency_and_path(433920000);
     furi_hal_subghz_set_frequency_and_path(433920000);
 
 
-    furi_hal_subghz_start_async_tx(subghz_hal_async_tx_test_yield, &test);
+    if(!furi_hal_subghz_start_async_tx(subghz_hal_async_tx_test_yield, &test)) {
+        return false;
+    }
+
     while(!furi_hal_subghz_is_async_tx_complete()) {
     while(!furi_hal_subghz_is_async_tx_complete()) {
         furi_delay_ms(10);
         furi_delay_ms(10);
     }
     }
@@ -486,6 +492,14 @@ MU_TEST(subghz_decoder_linear_test) {
         "Test decoder " SUBGHZ_PROTOCOL_LINEAR_NAME " error\r\n");
         "Test decoder " SUBGHZ_PROTOCOL_LINEAR_NAME " error\r\n");
 }
 }
 
 
+MU_TEST(subghz_decoder_linear_delta3_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/linear_delta3_raw.sub"),
+            SUBGHZ_PROTOCOL_LINEAR_DELTA3_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_LINEAR_DELTA3_NAME " error\r\n");
+}
+
 MU_TEST(subghz_decoder_megacode_test) {
 MU_TEST(subghz_decoder_megacode_test) {
     mu_assert(
     mu_assert(
         subghz_decoder_test(
         subghz_decoder_test(
@@ -594,6 +608,43 @@ MU_TEST(subghz_decoder_smc5326_test) {
         "Test decoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
         "Test decoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
 }
 }
 
 
+MU_TEST(subghz_decoder_holtek_ht12x_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/holtek_ht12x_raw.sub"), SUBGHZ_PROTOCOL_HOLTEK_HT12X_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_HOLTEK_HT12X_NAME " error\r\n");
+}
+
+MU_TEST(subghz_decoder_dooya_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/dooya_raw.sub"), SUBGHZ_PROTOCOL_DOOYA_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_DOOYA_NAME " error\r\n");
+}
+
+MU_TEST(subghz_decoder_alutech_at_4n_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/alutech_at_4n_raw.sub"),
+            SUBGHZ_PROTOCOL_ALUTECH_AT_4N_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_ALUTECH_AT_4N_NAME " error\r\n");
+}
+
+MU_TEST(subghz_decoder_nice_one_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/nice_one_raw.sub"), SUBGHZ_PROTOCOL_NICE_FLOR_S_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_NICE_FLOR_S_NAME " error\r\n");
+}
+
+MU_TEST(subghz_decoder_kinggates_stylo4k_test) {
+    mu_assert(
+        subghz_decoder_test(
+            EXT_PATH("unit_tests/subghz/kinggates_stylo4k_raw.sub"),
+            SUBGHZ_PROTOCOL_KINGGATES_STYLO_4K_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_KINGGATES_STYLO_4K_NAME " error\r\n");
+}
+
 //test encoders
 //test encoders
 MU_TEST(subghz_encoder_princeton_test) {
 MU_TEST(subghz_encoder_princeton_test) {
     mu_assert(
     mu_assert(
@@ -637,6 +688,12 @@ MU_TEST(subghz_encoder_linear_test) {
         "Test encoder " SUBGHZ_PROTOCOL_LINEAR_NAME " error\r\n");
         "Test encoder " SUBGHZ_PROTOCOL_LINEAR_NAME " error\r\n");
 }
 }
 
 
+MU_TEST(subghz_encoder_linear_delta3_test) {
+    mu_assert(
+        subghz_encoder_test(EXT_PATH("unit_tests/subghz/linear_delta3.sub")),
+        "Test encoder " SUBGHZ_PROTOCOL_LINEAR_DELTA3_NAME " error\r\n");
+}
+
 MU_TEST(subghz_encoder_megacode_test) {
 MU_TEST(subghz_encoder_megacode_test) {
     mu_assert(
     mu_assert(
         subghz_encoder_test(EXT_PATH("unit_tests/subghz/megacode.sub")),
         subghz_encoder_test(EXT_PATH("unit_tests/subghz/megacode.sub")),
@@ -727,6 +784,18 @@ MU_TEST(subghz_encoder_smc5326_test) {
         "Test encoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
         "Test encoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
 }
 }
 
 
+MU_TEST(subghz_encoder_holtek_ht12x_test) {
+    mu_assert(
+        subghz_encoder_test(EXT_PATH("unit_tests/subghz/holtek_ht12x.sub")),
+        "Test encoder " SUBGHZ_PROTOCOL_HOLTEK_HT12X_NAME " error\r\n");
+}
+
+MU_TEST(subghz_encoder_dooya_test) {
+    mu_assert(
+        subghz_encoder_test(EXT_PATH("unit_tests/subghz/dooya.sub")),
+        "Test encoder " SUBGHZ_PROTOCOL_DOOYA_NAME " error\r\n");
+}
+
 MU_TEST(subghz_random_test) {
 MU_TEST(subghz_random_test) {
     mu_assert(subghz_decode_random_test(TEST_RANDOM_DIR_NAME), "Random test error\r\n");
     mu_assert(subghz_decode_random_test(TEST_RANDOM_DIR_NAME), "Random test error\r\n");
 }
 }
@@ -756,6 +825,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_decoder_somfy_telis_test);
     MU_RUN_TEST(subghz_decoder_somfy_telis_test);
     MU_RUN_TEST(subghz_decoder_star_line_test);
     MU_RUN_TEST(subghz_decoder_star_line_test);
     MU_RUN_TEST(subghz_decoder_linear_test);
     MU_RUN_TEST(subghz_decoder_linear_test);
+    MU_RUN_TEST(subghz_decoder_linear_delta3_test);
     MU_RUN_TEST(subghz_decoder_megacode_test);
     MU_RUN_TEST(subghz_decoder_megacode_test);
     MU_RUN_TEST(subghz_decoder_secplus_v1_test);
     MU_RUN_TEST(subghz_decoder_secplus_v1_test);
     MU_RUN_TEST(subghz_decoder_secplus_v2_test);
     MU_RUN_TEST(subghz_decoder_secplus_v2_test);
@@ -771,6 +841,11 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_decoder_clemsa_test);
     MU_RUN_TEST(subghz_decoder_clemsa_test);
     MU_RUN_TEST(subghz_decoder_ansonic_test);
     MU_RUN_TEST(subghz_decoder_ansonic_test);
     MU_RUN_TEST(subghz_decoder_smc5326_test);
     MU_RUN_TEST(subghz_decoder_smc5326_test);
+    MU_RUN_TEST(subghz_decoder_holtek_ht12x_test);
+    MU_RUN_TEST(subghz_decoder_dooya_test);
+    MU_RUN_TEST(subghz_decoder_alutech_at_4n_test);
+    MU_RUN_TEST(subghz_decoder_nice_one_test);
+    MU_RUN_TEST(subghz_decoder_kinggates_stylo4k_test);
 
 
     MU_RUN_TEST(subghz_encoder_princeton_test);
     MU_RUN_TEST(subghz_encoder_princeton_test);
     MU_RUN_TEST(subghz_encoder_came_test);
     MU_RUN_TEST(subghz_encoder_came_test);
@@ -779,6 +854,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_encoder_nice_flo_test);
     MU_RUN_TEST(subghz_encoder_nice_flo_test);
     MU_RUN_TEST(subghz_encoder_keelog_test);
     MU_RUN_TEST(subghz_encoder_keelog_test);
     MU_RUN_TEST(subghz_encoder_linear_test);
     MU_RUN_TEST(subghz_encoder_linear_test);
+    MU_RUN_TEST(subghz_encoder_linear_delta3_test);
     MU_RUN_TEST(subghz_encoder_megacode_test);
     MU_RUN_TEST(subghz_encoder_megacode_test);
     MU_RUN_TEST(subghz_encoder_holtek_test);
     MU_RUN_TEST(subghz_encoder_holtek_test);
     MU_RUN_TEST(subghz_encoder_secplus_v1_test);
     MU_RUN_TEST(subghz_encoder_secplus_v1_test);
@@ -794,6 +870,8 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_encoder_clemsa_test);
     MU_RUN_TEST(subghz_encoder_clemsa_test);
     MU_RUN_TEST(subghz_encoder_ansonic_test);
     MU_RUN_TEST(subghz_encoder_ansonic_test);
     MU_RUN_TEST(subghz_encoder_smc5326_test);
     MU_RUN_TEST(subghz_encoder_smc5326_test);
+    MU_RUN_TEST(subghz_encoder_holtek_ht12x_test);
+    MU_RUN_TEST(subghz_encoder_dooya_test);
 
 
     MU_RUN_TEST(subghz_random_test);
     MU_RUN_TEST(subghz_random_test);
     subghz_test_deinit();
     subghz_test_deinit();

+ 28 - 22
applications/debug/unit_tests/test_index.c

@@ -13,6 +13,7 @@ int run_minunit_test_furi_hal();
 int run_minunit_test_furi_string();
 int run_minunit_test_furi_string();
 int run_minunit_test_infrared();
 int run_minunit_test_infrared();
 int run_minunit_test_rpc();
 int run_minunit_test_rpc();
+int run_minunit_test_manifest();
 int run_minunit_test_flipper_format();
 int run_minunit_test_flipper_format();
 int run_minunit_test_flipper_format_string();
 int run_minunit_test_flipper_format_string();
 int run_minunit_test_stream();
 int run_minunit_test_stream();
@@ -24,6 +25,7 @@ int run_minunit_test_protocol_dict();
 int run_minunit_test_lfrfid_protocols();
 int run_minunit_test_lfrfid_protocols();
 int run_minunit_test_nfc();
 int run_minunit_test_nfc();
 int run_minunit_test_bit_lib();
 int run_minunit_test_bit_lib();
+int run_minunit_test_float_tools();
 int run_minunit_test_bt();
 int run_minunit_test_bt();
 
 
 typedef int (*UnitTestEntry)();
 typedef int (*UnitTestEntry)();
@@ -40,6 +42,7 @@ const UnitTest unit_tests[] = {
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "stream", .entry = run_minunit_test_stream},
     {.name = "stream", .entry = run_minunit_test_stream},
     {.name = "dirwalk", .entry = run_minunit_test_dirwalk},
     {.name = "dirwalk", .entry = run_minunit_test_dirwalk},
+    {.name = "manifest", .entry = run_minunit_test_manifest},
     {.name = "flipper_format", .entry = run_minunit_test_flipper_format},
     {.name = "flipper_format", .entry = run_minunit_test_flipper_format},
     {.name = "flipper_format_string", .entry = run_minunit_test_flipper_format_string},
     {.name = "flipper_format_string", .entry = run_minunit_test_flipper_format_string},
     {.name = "rpc", .entry = run_minunit_test_rpc},
     {.name = "rpc", .entry = run_minunit_test_rpc},
@@ -50,6 +53,7 @@ const UnitTest unit_tests[] = {
     {.name = "protocol_dict", .entry = run_minunit_test_protocol_dict},
     {.name = "protocol_dict", .entry = run_minunit_test_protocol_dict},
     {.name = "lfrfid", .entry = run_minunit_test_lfrfid_protocols},
     {.name = "lfrfid", .entry = run_minunit_test_lfrfid_protocols},
     {.name = "bit_lib", .entry = run_minunit_test_bit_lib},
     {.name = "bit_lib", .entry = run_minunit_test_bit_lib},
+    {.name = "float_tools", .entry = run_minunit_test_float_tools},
     {.name = "bt", .entry = run_minunit_test_bt},
     {.name = "bt", .entry = run_minunit_test_bt},
 };
 };
 
 
@@ -66,14 +70,13 @@ void minunit_print_progress() {
 }
 }
 
 
 void minunit_print_fail(const char* str) {
 void minunit_print_fail(const char* str) {
-    printf(FURI_LOG_CLR_E "%s\r\n" FURI_LOG_CLR_RESET, str);
+    printf(_FURI_LOG_CLR_E "%s\r\n" _FURI_LOG_CLR_RESET, str);
 }
 }
 
 
 void unit_tests_cli(Cli* cli, FuriString* args, void* context) {
 void unit_tests_cli(Cli* cli, FuriString* args, void* context) {
     UNUSED(cli);
     UNUSED(cli);
     UNUSED(args);
     UNUSED(args);
     UNUSED(context);
     UNUSED(context);
-    uint32_t failed_tests = 0;
     minunit_run = 0;
     minunit_run = 0;
     minunit_assert = 0;
     minunit_assert = 0;
     minunit_fail = 0;
     minunit_fail = 0;
@@ -99,32 +102,35 @@ void unit_tests_cli(Cli* cli, FuriString* args, void* context) {
 
 
             if(furi_string_size(args)) {
             if(furi_string_size(args)) {
                 if(furi_string_cmp_str(args, unit_tests[i].name) == 0) {
                 if(furi_string_cmp_str(args, unit_tests[i].name) == 0) {
-                    failed_tests += unit_tests[i].entry();
+                    unit_tests[i].entry();
                 } else {
                 } else {
                     printf("Skipping %s\r\n", unit_tests[i].name);
                     printf("Skipping %s\r\n", unit_tests[i].name);
                 }
                 }
             } else {
             } else {
-                failed_tests += unit_tests[i].entry();
+                unit_tests[i].entry();
             }
             }
         }
         }
-        printf("\r\nFailed tests: %lu\r\n", failed_tests);
-
-        // Time report
-        cycle_counter = (furi_get_tick() - cycle_counter);
-        printf("Consumed: %lu ms\r\n", cycle_counter);
-
-        // Wait for tested services and apps to deallocate memory
-        furi_delay_ms(200);
-        uint32_t heap_after = memmgr_get_free_heap();
-        printf("Leaked: %ld\r\n", heap_before - heap_after);
-
-        // Final Report
-        if(failed_tests == 0) {
-            notification_message(notification, &sequence_success);
-            printf("Status: PASSED\r\n");
-        } else {
-            notification_message(notification, &sequence_error);
-            printf("Status: FAILED\r\n");
+
+        if(minunit_run != 0) {
+            printf("\r\nFailed tests: %u\r\n", minunit_fail);
+
+            // Time report
+            cycle_counter = (furi_get_tick() - cycle_counter);
+            printf("Consumed: %lu ms\r\n", cycle_counter);
+
+            // Wait for tested services and apps to deallocate memory
+            furi_delay_ms(200);
+            uint32_t heap_after = memmgr_get_free_heap();
+            printf("Leaked: %ld\r\n", heap_before - heap_after);
+
+            // Final Report
+            if(minunit_fail == 0) {
+                notification_message(notification, &sequence_success);
+                printf("Status: PASSED\r\n");
+            } else {
+                notification_message(notification, &sequence_error);
+                printf("Status: FAILED\r\n");
+            }
         }
         }
     }
     }
 
 

+ 2 - 2
applications/examples/application.fam

@@ -1,5 +1,5 @@
 App(
 App(
-    appid="sample_apps",
-    name="Sample apps bundle",
+    appid="example_apps",
+    name="Example apps bundle",
     apptype=FlipperAppType.METAPACKAGE,
     apptype=FlipperAppType.METAPACKAGE,
 )
 )

+ 44 - 0
applications/examples/example_thermo/README.md

@@ -0,0 +1,44 @@
+# 1-Wire Thermometer
+This example application demonstrates the use of the 1-Wire library with a DS18B20 thermometer. 
+It also covers basic GUI, input handling, threads and localisation.
+
+## Electrical connections
+Before launching the application, connect the sensor to Flipper's external GPIO according to the table below:
+| DS18B20 | Flipper |
+| :-----: | :-----: |
+| VDD | 9 |
+| GND | 18 |
+| DQ  | 17 |
+
+*NOTE 1*: GND is also available on pins 8 and 11.
+
+*NOTE 2*: For any other pin than 17, connect an external 4.7k pull-up resistor to pin 9.
+
+## Launching the application
+In order to launch this demo, follow the steps below:
+1. Make sure your Flipper has an SD card installed.
+2. Connect your Flipper to the computer via a USB cable.
+3. Run `./fbt launch_app APPSRC=example_thermo` in your terminal emulator of choice.
+
+## Changing the data pin
+It is possible to use other GPIO pin as a 1-Wire data pin. In order to change it, set the `THERMO_GPIO_PIN` macro to any of the options listed below:
+
+```c
+/* Possible GPIO pin choices:
+ - gpio_ext_pc0
+ - gpio_ext_pc1
+ - gpio_ext_pc3
+ - gpio_ext_pb2
+ - gpio_ext_pb3
+ - gpio_ext_pa4
+ - gpio_ext_pa6
+ - gpio_ext_pa7
+ - ibutton_gpio
+*/
+
+#define THERMO_GPIO_PIN (ibutton_gpio)
+```
+Do not forget about the external pull-up resistor as these pins do not have one built-in.
+
+With the changes been made, recompile and launch the application again. 
+The on-screen text should reflect it by asking to connect the thermometer to another pin.

+ 10 - 0
applications/examples/example_thermo/application.fam

@@ -0,0 +1,10 @@
+App(
+    appid="example_thermo",
+    name="Example: Thermometer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="example_thermo_main",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    fap_icon="example_thermo_10px.png",
+    fap_category="Examples",
+)

+ 356 - 0
applications/examples/example_thermo/example_thermo.c

@@ -0,0 +1,356 @@
+/*
+ * This file contains an example application that reads and displays
+ * the temperature from a DS18B20 1-wire thermometer.
+ *
+ * It also covers basic GUI, input handling, threads and localisation.
+ *
+ * References:
+ * [1] DS18B20 Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/DS18B20.pdf
+ */
+
+#include <gui/gui.h>
+#include <gui/view_port.h>
+
+#include <core/thread.h>
+#include <core/kernel.h>
+
+#include <locale/locale.h>
+
+#include <one_wire/maxim_crc.h>
+#include <one_wire/one_wire_host.h>
+
+#define UPDATE_PERIOD_MS 1000UL
+#define TEXT_STORE_SIZE 64U
+
+#define DS18B20_CMD_CONVERT 0x44U
+#define DS18B20_CMD_READ_SCRATCHPAD 0xbeU
+
+#define DS18B20_CFG_RESOLUTION_POS 5U
+#define DS18B20_CFG_RESOLUTION_MASK 0x03U
+#define DS18B20_DECIMAL_PART_MASK 0x0fU
+
+#define DS18B20_SIGN_MASK 0xf0U
+
+/* Possible GPIO pin choices:
+ - gpio_ext_pc0
+ - gpio_ext_pc1
+ - gpio_ext_pc3
+ - gpio_ext_pb2
+ - gpio_ext_pb3
+ - gpio_ext_pa4
+ - gpio_ext_pa6
+ - gpio_ext_pa7
+ - ibutton_gpio
+*/
+
+#define THERMO_GPIO_PIN (ibutton_gpio)
+
+/* Flags which the reader thread responds to */
+typedef enum {
+    ReaderThreadFlagExit = 1,
+} ReaderThreadFlag;
+
+typedef union {
+    struct {
+        uint8_t temp_lsb; /* Least significant byte of the temperature */
+        uint8_t temp_msb; /* Most significant byte of the temperature */
+        uint8_t user_alarm_high; /* User register 1 (Temp high alarm) */
+        uint8_t user_alarm_low; /* User register 2 (Temp low alarm) */
+        uint8_t config; /* Configuration register */
+        uint8_t reserved[3]; /* Not used */
+        uint8_t crc; /* CRC checksum for error detection */
+    } fields;
+    uint8_t bytes[9];
+} DS18B20Scratchpad;
+
+/* Application context structure */
+typedef struct {
+    Gui* gui;
+    ViewPort* view_port;
+    FuriThread* reader_thread;
+    FuriMessageQueue* event_queue;
+    OneWireHost* onewire;
+    float temp_celsius;
+    bool has_device;
+} ExampleThermoContext;
+
+/*************** 1-Wire Communication and Processing *****************/
+
+/* Commands the thermometer to begin measuring the temperature. */
+static void example_thermo_request_temperature(ExampleThermoContext* context) {
+    OneWireHost* onewire = context->onewire;
+
+    /* All 1-wire transactions must happen in a critical section, i.e
+       not interrupted by other threads. */
+    FURI_CRITICAL_ENTER();
+
+    bool success = false;
+    do {
+        /* Each communication with a 1-wire device starts by a reset.
+           The functon will return true if a device responded with a presence pulse. */
+        if(!onewire_host_reset(onewire)) break;
+        /* After the reset, a ROM operation must follow.
+           If there is only one device connected, the "Skip ROM" command is most appropriate
+           (it can also be used to address all of the connected devices in some cases).*/
+        onewire_host_skip(onewire);
+        /* After the ROM operation, a device-specific command is issued.
+           In this case, it's a request to start measuring the temperature. */
+        onewire_host_write(onewire, DS18B20_CMD_CONVERT);
+
+        success = true;
+    } while(false);
+
+    context->has_device = success;
+
+    FURI_CRITICAL_EXIT();
+}
+
+/* Reads the measured temperature from the thermometer. */
+static void example_thermo_read_temperature(ExampleThermoContext* context) {
+    /* If there was no device detected, don't try to read the temperature */
+    if(!context->has_device) {
+        return;
+    }
+
+    OneWireHost* onewire = context->onewire;
+
+    /* All 1-wire transactions must happen in a critical section, i.e
+       not interrupted by other threads. */
+    FURI_CRITICAL_ENTER();
+
+    bool success = false;
+
+    do {
+        DS18B20Scratchpad buf;
+
+        /* Attempt reading the temperature 10 times before giving up */
+        size_t attempts_left = 10;
+        do {
+            /* Each communication with a 1-wire device starts by a reset.
+            The functon will return true if a device responded with a presence pulse. */
+            if(!onewire_host_reset(onewire)) continue;
+
+            /* After the reset, a ROM operation must follow.
+            If there is only one device connected, the "Skip ROM" command is most appropriate
+            (it can also be used to address all of the connected devices in some cases).*/
+            onewire_host_skip(onewire);
+
+            /* After the ROM operation, a device-specific command is issued.
+            This time, it will be the "Read Scratchpad" command which will
+            prepare the device's internal buffer memory for reading. */
+            onewire_host_write(onewire, DS18B20_CMD_READ_SCRATCHPAD);
+
+            /* The actual reading happens here. A total of 9 bytes is read. */
+            onewire_host_read_bytes(onewire, buf.bytes, sizeof(buf.bytes));
+
+            /* Calculate the checksum and compare it with one provided by the device. */
+            const uint8_t crc = maxim_crc8(buf.bytes, sizeof(buf.bytes) - 1, MAXIM_CRC8_INIT);
+
+            /* Checksums match, exit the loop */
+            if(crc == buf.fields.crc) break;
+
+        } while(--attempts_left);
+
+        if(attempts_left == 0) break;
+
+        /* Get the measurement resolution from the configuration register. (See [1] page 9) */
+        const uint8_t resolution_mode = (buf.fields.config >> DS18B20_CFG_RESOLUTION_POS) &
+                                        DS18B20_CFG_RESOLUTION_MASK;
+
+        /* Generate a mask for undefined bits in the decimal part. (See [1] page 6) */
+        const uint8_t decimal_mask =
+            (DS18B20_DECIMAL_PART_MASK << (DS18B20_CFG_RESOLUTION_MASK - resolution_mode)) &
+            DS18B20_DECIMAL_PART_MASK;
+
+        /* Get the integer and decimal part of the temperature (See [1] page 6) */
+        const uint8_t integer_part = (buf.fields.temp_msb << 4U) | (buf.fields.temp_lsb >> 4U);
+        const uint8_t decimal_part = buf.fields.temp_lsb & decimal_mask;
+
+        /* Calculate the sign of the temperature (See [1] page 6) */
+        const bool is_negative = (buf.fields.temp_msb & DS18B20_SIGN_MASK) != 0;
+
+        /* Combine the integer and decimal part together */
+        const float temp_celsius_abs = integer_part + decimal_part / 16.f;
+
+        /* Set the appropriate sign */
+        context->temp_celsius = is_negative ? -temp_celsius_abs : temp_celsius_abs;
+
+        success = true;
+    } while(false);
+
+    context->has_device = success;
+
+    FURI_CRITICAL_EXIT();
+}
+
+/* Periodically requests measurements and reads temperature. This function runs in a separare thread. */
+static int32_t example_thermo_reader_thread_callback(void* ctx) {
+    ExampleThermoContext* context = ctx;
+
+    for(;;) {
+        /* Tell the termometer to start measuring the temperature. The process may take up to 750ms. */
+        example_thermo_request_temperature(context);
+
+        /* Wait for the measurement to finish. At the same time wait for an exit signal. */
+        const uint32_t flags =
+            furi_thread_flags_wait(ReaderThreadFlagExit, FuriFlagWaitAny, UPDATE_PERIOD_MS);
+
+        /* If an exit signal was received, return from this thread. */
+        if(flags != (unsigned)FuriFlagErrorTimeout) break;
+
+        /* The measurement is now ready, read it from the termometer. */
+        example_thermo_read_temperature(context);
+    }
+
+    return 0;
+}
+
+/*************** GUI, Input and Main Loop *****************/
+
+/* Draw the GUI of the application. The screen is completely redrawn during each call. */
+static void example_thermo_draw_callback(Canvas* canvas, void* ctx) {
+    ExampleThermoContext* context = ctx;
+    char text_store[TEXT_STORE_SIZE];
+    const size_t middle_x = canvas_width(canvas) / 2U;
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, middle_x, 12, AlignCenter, AlignBottom, "Thermometer Demo");
+    canvas_draw_line(canvas, 0, 16, 128, 16);
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(
+        canvas, middle_x, 30, AlignCenter, AlignBottom, "Connnect thermometer");
+
+    snprintf(
+        text_store,
+        TEXT_STORE_SIZE,
+        "to GPIO pin %ld",
+        furi_hal_resources_get_ext_pin_number(&THERMO_GPIO_PIN));
+    canvas_draw_str_aligned(canvas, middle_x, 42, AlignCenter, AlignBottom, text_store);
+
+    canvas_set_font(canvas, FontKeyboard);
+
+    if(context->has_device) {
+        float temp;
+        char temp_units;
+
+        /* The applicaton is locale-aware.
+           Change Settings->System->Units to check it out. */
+        switch(locale_get_measurement_unit()) {
+        case LocaleMeasurementUnitsMetric:
+            temp = context->temp_celsius;
+            temp_units = 'C';
+            break;
+        case LocaleMeasurementUnitsImperial:
+            temp = locale_celsius_to_fahrenheit(context->temp_celsius);
+            temp_units = 'F';
+            break;
+        default:
+            furi_crash("Illegal measurement units");
+        }
+        /* If a reading is available, display it */
+        snprintf(text_store, TEXT_STORE_SIZE, "Temperature: %+.1f%c", (double)temp, temp_units);
+    } else {
+        /* Or show a message that no data is available */
+        strncpy(text_store, "-- No data --", TEXT_STORE_SIZE);
+    }
+
+    canvas_draw_str_aligned(canvas, middle_x, 58, AlignCenter, AlignBottom, text_store);
+}
+
+/* This function is called from the GUI thread. All it does is put the event
+   into the application's queue so it can be processed later. */
+static void example_thermo_input_callback(InputEvent* event, void* ctx) {
+    ExampleThermoContext* context = ctx;
+    furi_message_queue_put(context->event_queue, event, FuriWaitForever);
+}
+
+/* Starts the reader thread and handles the input */
+static void example_thermo_run(ExampleThermoContext* context) {
+    /* Configure the hardware in host mode */
+    onewire_host_start(context->onewire);
+
+    /* Start the reader thread. It will talk to the thermometer in the background. */
+    furi_thread_start(context->reader_thread);
+
+    /* An endless loop which handles the input*/
+    for(bool is_running = true; is_running;) {
+        InputEvent event;
+        /* Wait for an input event. Input events come from the GUI thread via a callback. */
+        const FuriStatus status =
+            furi_message_queue_get(context->event_queue, &event, FuriWaitForever);
+
+        /* This application is only interested in short button presses. */
+        if((status != FuriStatusOk) || (event.type != InputTypeShort)) {
+            continue;
+        }
+
+        /* When the user presses the "Back" button, break the loop and exit the application. */
+        if(event.key == InputKeyBack) {
+            is_running = false;
+        }
+    }
+
+    /* Signal the reader thread to cease operation and exit */
+    furi_thread_flags_set(furi_thread_get_id(context->reader_thread), ReaderThreadFlagExit);
+
+    /* Wait for the reader thread to finish */
+    furi_thread_join(context->reader_thread);
+
+    /* Reset the hardware */
+    onewire_host_stop(context->onewire);
+}
+
+/******************** Initialisation & startup *****************************/
+
+/* Allocate the memory and initialise the variables */
+static ExampleThermoContext* example_thermo_context_alloc() {
+    ExampleThermoContext* context = malloc(sizeof(ExampleThermoContext));
+
+    context->view_port = view_port_alloc();
+    view_port_draw_callback_set(context->view_port, example_thermo_draw_callback, context);
+    view_port_input_callback_set(context->view_port, example_thermo_input_callback, context);
+
+    context->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    context->reader_thread = furi_thread_alloc();
+    furi_thread_set_stack_size(context->reader_thread, 1024U);
+    furi_thread_set_context(context->reader_thread, context);
+    furi_thread_set_callback(context->reader_thread, example_thermo_reader_thread_callback);
+
+    context->gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(context->gui, context->view_port, GuiLayerFullscreen);
+
+    context->onewire = onewire_host_alloc(&THERMO_GPIO_PIN);
+
+    return context;
+}
+
+/* Release the unused resources and deallocate memory */
+static void example_thermo_context_free(ExampleThermoContext* context) {
+    view_port_enabled_set(context->view_port, false);
+    gui_remove_view_port(context->gui, context->view_port);
+
+    onewire_host_free(context->onewire);
+    furi_thread_free(context->reader_thread);
+    furi_message_queue_free(context->event_queue);
+    view_port_free(context->view_port);
+
+    furi_record_close(RECORD_GUI);
+}
+
+/* The application's entry point. Execution starts from here. */
+int32_t example_thermo_main(void* p) {
+    UNUSED(p);
+
+    /* Allocate all of the necessary structures */
+    ExampleThermoContext* context = example_thermo_context_alloc();
+
+    /* Start the applicaton's main loop. It won't return until the application was requested to exit. */
+    example_thermo_run(context);
+
+    /* Release all unneeded resources */
+    example_thermo_context_free(context);
+
+    return 0;
+}

BIN
applications/examples/example_thermo/example_thermo_10px.png


+ 1 - 1
applications/main/archive/helpers/archive_apps.c

@@ -13,7 +13,7 @@ ArchiveAppTypeEnum archive_get_app_type(const char* path) {
     }
     }
     app_name++;
     app_name++;
 
 
-    for(size_t i = 0; i < COUNT_OF(known_apps); i++) {
+    for(size_t i = 0; i < COUNT_OF(known_apps); i++) { //-V1008
         if(strncmp(app_name, known_apps[i], strlen(known_apps[i])) == 0) {
         if(strncmp(app_name, known_apps[i], strlen(known_apps[i])) == 0) {
             return i;
             return i;
         }
         }

+ 3 - 2
applications/main/archive/helpers/archive_browser.c

@@ -1,10 +1,11 @@
-#include <archive/views/archive_browser_view.h>
 #include "archive_files.h"
 #include "archive_files.h"
 #include "archive_apps.h"
 #include "archive_apps.h"
 #include "archive_browser.h"
 #include "archive_browser.h"
+#include "../views/archive_browser_view.h"
+
 #include <core/common_defines.h>
 #include <core/common_defines.h>
 #include <core/log.h>
 #include <core/log.h>
-#include "gui/modules/file_browser_worker.h"
+#include <gui/modules/file_browser_worker.h>
 #include <fap_loader/fap_loader_app.h>
 #include <fap_loader/fap_loader_app.h>
 #include <math.h>
 #include <math.h>
 
 

+ 1 - 1
applications/main/archive/helpers/archive_favorites.c

@@ -177,7 +177,7 @@ bool archive_favorites_read(void* context) {
 
 
     archive_set_item_count(browser, file_count);
     archive_set_item_count(browser, file_count);
 
 
-    if(need_refresh) {
+    if(need_refresh) { //-V547
         archive_favourites_rescan();
         archive_favourites_rescan();
     }
     }
 
 

+ 1 - 1
applications/main/archive/scenes/archive_scene_browser.c

@@ -116,7 +116,7 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) {
         case ArchiveBrowserEventFileMenuPin: {
         case ArchiveBrowserEventFileMenuPin: {
             const char* name = archive_get_name(browser);
             const char* name = archive_get_name(browser);
             if(favorites) {
             if(favorites) {
-                archive_favorites_delete(name);
+                archive_favorites_delete("%s", name);
                 archive_file_array_rm_selected(browser);
                 archive_file_array_rm_selected(browser);
                 archive_show_file_menu(browser, false);
                 archive_show_file_menu(browser, false);
             } else if(archive_is_known_app(selected->type)) {
             } else if(archive_is_known_app(selected->type)) {

+ 5 - 4
applications/main/archive/views/archive_browser_view.h

@@ -1,14 +1,15 @@
 #pragma once
 #pragma once
 
 
+#include "../helpers/archive_files.h"
+#include "../helpers/archive_favorites.h"
+
 #include <gui/gui_i.h>
 #include <gui/gui_i.h>
 #include <gui/view.h>
 #include <gui/view.h>
 #include <gui/canvas.h>
 #include <gui/canvas.h>
 #include <gui/elements.h>
 #include <gui/elements.h>
-#include <furi.h>
+#include <gui/modules/file_browser_worker.h>
 #include <storage/storage.h>
 #include <storage/storage.h>
-#include "../helpers/archive_files.h"
-#include "../helpers/archive_favorites.h"
-#include "gui/modules/file_browser_worker.h"
+#include <furi.h>
 
 
 #define MAX_LEN_PX 110
 #define MAX_LEN_PX 110
 #define MAX_NAME_LEN 255
 #define MAX_NAME_LEN 255

+ 78 - 2
applications/main/bad_usb/bad_usb_app.c

@@ -1,9 +1,12 @@
 #include "bad_usb_app_i.h"
 #include "bad_usb_app_i.h"
+#include "bad_usb_settings_filename.h"
 #include <furi.h>
 #include <furi.h>
 #include <furi_hal.h>
 #include <furi_hal.h>
 #include <storage/storage.h>
 #include <storage/storage.h>
 #include <lib/toolbox/path.h>
 #include <lib/toolbox/path.h>
 
 
+#define BAD_USB_SETTINGS_PATH BAD_USB_APP_BASE_FOLDER "/" BAD_USB_SETTINGS_FILE_NAME
+
 static bool bad_usb_app_custom_event_callback(void* context, uint32_t event) {
 static bool bad_usb_app_custom_event_callback(void* context, uint32_t event) {
     furi_assert(context);
     furi_assert(context);
     BadUsbApp* app = context;
     BadUsbApp* app = context;
@@ -22,15 +25,62 @@ static void bad_usb_app_tick_event_callback(void* context) {
     scene_manager_handle_tick_event(app->scene_manager);
     scene_manager_handle_tick_event(app->scene_manager);
 }
 }
 
 
+static void bad_usb_load_settings(BadUsbApp* app) {
+    File* settings_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
+    if(storage_file_open(settings_file, BAD_USB_SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        char chr;
+        while((storage_file_read(settings_file, &chr, 1) == 1) &&
+              !storage_file_eof(settings_file) && !isspace(chr)) {
+            furi_string_push_back(app->keyboard_layout, chr);
+        }
+    } else {
+        furi_string_reset(app->keyboard_layout);
+    }
+    storage_file_close(settings_file);
+    storage_file_free(settings_file);
+
+    if(!furi_string_empty(app->keyboard_layout)) {
+        Storage* fs_api = furi_record_open(RECORD_STORAGE);
+        FileInfo layout_file_info;
+        FS_Error file_check_err = storage_common_stat(
+            fs_api, furi_string_get_cstr(app->keyboard_layout), &layout_file_info);
+        furi_record_close(RECORD_STORAGE);
+        if(file_check_err != FSE_OK) {
+            furi_string_reset(app->keyboard_layout);
+            return;
+        }
+        if(layout_file_info.size != 256) {
+            furi_string_reset(app->keyboard_layout);
+        }
+    }
+}
+
+static void bad_usb_save_settings(BadUsbApp* app) {
+    File* settings_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
+    if(storage_file_open(settings_file, BAD_USB_SETTINGS_PATH, FSAM_WRITE, FSOM_OPEN_ALWAYS)) {
+        storage_file_write(
+            settings_file,
+            furi_string_get_cstr(app->keyboard_layout),
+            furi_string_size(app->keyboard_layout));
+        storage_file_write(settings_file, "\n", 1);
+    }
+    storage_file_close(settings_file);
+    storage_file_free(settings_file);
+}
+
 BadUsbApp* bad_usb_app_alloc(char* arg) {
 BadUsbApp* bad_usb_app_alloc(char* arg) {
     BadUsbApp* app = malloc(sizeof(BadUsbApp));
     BadUsbApp* app = malloc(sizeof(BadUsbApp));
 
 
-    app->file_path = furi_string_alloc();
+    app->bad_usb_script = NULL;
 
 
+    app->file_path = furi_string_alloc();
+    app->keyboard_layout = furi_string_alloc();
     if(arg && strlen(arg)) {
     if(arg && strlen(arg)) {
         furi_string_set(app->file_path, arg);
         furi_string_set(app->file_path, arg);
     }
     }
 
 
+    bad_usb_load_settings(app);
+
     app->gui = furi_record_open(RECORD_GUI);
     app->gui = furi_record_open(RECORD_GUI);
     app->notifications = furi_record_open(RECORD_NOTIFICATION);
     app->notifications = furi_record_open(RECORD_NOTIFICATION);
     app->dialogs = furi_record_open(RECORD_DIALOGS);
     app->dialogs = furi_record_open(RECORD_DIALOGS);
@@ -53,6 +103,10 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
     view_dispatcher_add_view(
     view_dispatcher_add_view(
         app->view_dispatcher, BadUsbAppViewError, widget_get_view(app->widget));
         app->view_dispatcher, BadUsbAppViewError, widget_get_view(app->widget));
 
 
+    app->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, BadUsbAppViewConfig, submenu_get_view(app->submenu));
+
     app->bad_usb_view = bad_usb_alloc();
     app->bad_usb_view = bad_usb_alloc();
     view_dispatcher_add_view(
     view_dispatcher_add_view(
         app->view_dispatcher, BadUsbAppViewWork, bad_usb_get_view(app->bad_usb_view));
         app->view_dispatcher, BadUsbAppViewWork, bad_usb_get_view(app->bad_usb_view));
@@ -61,12 +115,18 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
 
 
     if(furi_hal_usb_is_locked()) {
     if(furi_hal_usb_is_locked()) {
         app->error = BadUsbAppErrorCloseRpc;
         app->error = BadUsbAppErrorCloseRpc;
+        app->usb_if_prev = NULL;
         scene_manager_next_scene(app->scene_manager, BadUsbSceneError);
         scene_manager_next_scene(app->scene_manager, BadUsbSceneError);
     } else {
     } else {
+        app->usb_if_prev = furi_hal_usb_get_config();
+        furi_check(furi_hal_usb_set_config(NULL, NULL));
+
         if(!furi_string_empty(app->file_path)) {
         if(!furi_string_empty(app->file_path)) {
+            app->bad_usb_script = bad_usb_script_open(app->file_path);
+            bad_usb_script_set_keyboard_layout(app->bad_usb_script, app->keyboard_layout);
             scene_manager_next_scene(app->scene_manager, BadUsbSceneWork);
             scene_manager_next_scene(app->scene_manager, BadUsbSceneWork);
         } else {
         } else {
-            furi_string_set(app->file_path, BAD_USB_APP_PATH_FOLDER);
+            furi_string_set(app->file_path, BAD_USB_APP_BASE_FOLDER);
             scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect);
             scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect);
         }
         }
     }
     }
@@ -77,6 +137,15 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
 void bad_usb_app_free(BadUsbApp* app) {
 void bad_usb_app_free(BadUsbApp* app) {
     furi_assert(app);
     furi_assert(app);
 
 
+    if(app->bad_usb_script) {
+        bad_usb_script_close(app->bad_usb_script);
+        app->bad_usb_script = NULL;
+    }
+
+    if(app->usb_if_prev) {
+        furi_check(furi_hal_usb_set_config(app->usb_if_prev, NULL));
+    }
+
     // Views
     // Views
     view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewWork);
     view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewWork);
     bad_usb_free(app->bad_usb_view);
     bad_usb_free(app->bad_usb_view);
@@ -85,6 +154,10 @@ void bad_usb_app_free(BadUsbApp* app) {
     view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewError);
     view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewError);
     widget_free(app->widget);
     widget_free(app->widget);
 
 
+    // Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewConfig);
+    submenu_free(app->submenu);
+
     // View dispatcher
     // View dispatcher
     view_dispatcher_free(app->view_dispatcher);
     view_dispatcher_free(app->view_dispatcher);
     scene_manager_free(app->scene_manager);
     scene_manager_free(app->scene_manager);
@@ -94,7 +167,10 @@ void bad_usb_app_free(BadUsbApp* app) {
     furi_record_close(RECORD_NOTIFICATION);
     furi_record_close(RECORD_NOTIFICATION);
     furi_record_close(RECORD_DIALOGS);
     furi_record_close(RECORD_DIALOGS);
 
 
+    bad_usb_save_settings(app);
+
     furi_string_free(app->file_path);
     furi_string_free(app->file_path);
+    furi_string_free(app->keyboard_layout);
 
 
     free(app);
     free(app);
 }
 }

+ 11 - 3
applications/main/bad_usb/bad_usb_app_i.h

@@ -14,9 +14,12 @@
 #include <gui/modules/variable_item_list.h>
 #include <gui/modules/variable_item_list.h>
 #include <gui/modules/widget.h>
 #include <gui/modules/widget.h>
 #include "views/bad_usb_view.h"
 #include "views/bad_usb_view.h"
+#include <furi_hal_usb.h>
 
 
-#define BAD_USB_APP_PATH_FOLDER ANY_PATH("badusb")
-#define BAD_USB_APP_EXTENSION ".txt"
+#define BAD_USB_APP_BASE_FOLDER ANY_PATH("badusb")
+#define BAD_USB_APP_PATH_LAYOUT_FOLDER BAD_USB_APP_BASE_FOLDER "/assets/layouts"
+#define BAD_USB_APP_SCRIPT_EXTENSION ".txt"
+#define BAD_USB_APP_LAYOUT_EXTENSION ".kl"
 
 
 typedef enum {
 typedef enum {
     BadUsbAppErrorNoFiles,
     BadUsbAppErrorNoFiles,
@@ -30,14 +33,19 @@ struct BadUsbApp {
     NotificationApp* notifications;
     NotificationApp* notifications;
     DialogsApp* dialogs;
     DialogsApp* dialogs;
     Widget* widget;
     Widget* widget;
+    Submenu* submenu;
 
 
     BadUsbAppError error;
     BadUsbAppError error;
     FuriString* file_path;
     FuriString* file_path;
+    FuriString* keyboard_layout;
     BadUsb* bad_usb_view;
     BadUsb* bad_usb_view;
     BadUsbScript* bad_usb_script;
     BadUsbScript* bad_usb_script;
+
+    FuriHalUsbInterface* usb_if_prev;
 };
 };
 
 
 typedef enum {
 typedef enum {
     BadUsbAppViewError,
     BadUsbAppViewError,
     BadUsbAppViewWork,
     BadUsbAppViewWork,
-} BadUsbAppView;
+    BadUsbAppViewConfig,
+} BadUsbAppView;

+ 54 - 20
applications/main/bad_usb/bad_usb_script.c

@@ -16,6 +16,9 @@
 #define SCRIPT_STATE_END (-2)
 #define SCRIPT_STATE_END (-2)
 #define SCRIPT_STATE_NEXT_LINE (-3)
 #define SCRIPT_STATE_NEXT_LINE (-3)
 
 
+#define BADUSB_ASCII_TO_KEY(script, x) \
+    (((uint8_t)x < 128) ? (script->layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
+
 typedef enum {
 typedef enum {
     WorkerEvtToggle = (1 << 0),
     WorkerEvtToggle = (1 << 0),
     WorkerEvtEnd = (1 << 1),
     WorkerEvtEnd = (1 << 1),
@@ -28,6 +31,7 @@ struct BadUsbScript {
     BadUsbState st;
     BadUsbState st;
     FuriString* file_path;
     FuriString* file_path;
     uint32_t defdelay;
     uint32_t defdelay;
+    uint16_t layout[128];
     FuriThread* thread;
     FuriThread* thread;
     uint8_t file_buf[FILE_BUFFER_LEN + 1];
     uint8_t file_buf[FILE_BUFFER_LEN + 1];
     uint8_t buf_start;
     uint8_t buf_start;
@@ -50,6 +54,7 @@ static const DuckyKey ducky_keys[] = {
     {"ALT-SHIFT", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT},
     {"ALT-SHIFT", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT},
     {"ALT-GUI", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_GUI},
     {"ALT-GUI", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_GUI},
     {"GUI-SHIFT", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_SHIFT},
     {"GUI-SHIFT", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_SHIFT},
+    {"GUI-CTRL", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_CTRL},
 
 
     {"CTRL", KEY_MOD_LEFT_CTRL},
     {"CTRL", KEY_MOD_LEFT_CTRL},
     {"CONTROL", KEY_MOD_LEFT_CTRL},
     {"CONTROL", KEY_MOD_LEFT_CTRL},
@@ -71,8 +76,8 @@ static const DuckyKey ducky_keys[] = {
     {"BREAK", HID_KEYBOARD_PAUSE},
     {"BREAK", HID_KEYBOARD_PAUSE},
     {"PAUSE", HID_KEYBOARD_PAUSE},
     {"PAUSE", HID_KEYBOARD_PAUSE},
     {"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
     {"CAPSLOCK", HID_KEYBOARD_CAPS_LOCK},
-    {"DELETE", HID_KEYBOARD_DELETE},
-    {"BACKSPACE", HID_KEYPAD_BACKSPACE},
+    {"DELETE", HID_KEYBOARD_DELETE_FORWARD},
+    {"BACKSPACE", HID_KEYBOARD_DELETE},
     {"END", HID_KEYBOARD_END},
     {"END", HID_KEYBOARD_END},
     {"ESC", HID_KEYBOARD_ESCAPE},
     {"ESC", HID_KEYBOARD_ESCAPE},
     {"ESCAPE", HID_KEYBOARD_ESCAPE},
     {"ESCAPE", HID_KEYBOARD_ESCAPE},
@@ -204,10 +209,10 @@ static bool ducky_altstring(const char* param) {
     return state;
     return state;
 }
 }
 
 
-static bool ducky_string(const char* param) {
+static bool ducky_string(BadUsbScript* bad_usb, const char* param) {
     uint32_t i = 0;
     uint32_t i = 0;
     while(param[i] != '\0') {
     while(param[i] != '\0') {
-        uint16_t keycode = HID_ASCII_TO_KEY(param[i]);
+        uint16_t keycode = BADUSB_ASCII_TO_KEY(bad_usb, param[i]);
         if(keycode != HID_KEYBOARD_NONE) {
         if(keycode != HID_KEYBOARD_NONE) {
             furi_hal_hid_kb_press(keycode);
             furi_hal_hid_kb_press(keycode);
             furi_hal_hid_kb_release(keycode);
             furi_hal_hid_kb_release(keycode);
@@ -217,16 +222,16 @@ static bool ducky_string(const char* param) {
     return true;
     return true;
 }
 }
 
 
-static uint16_t ducky_get_keycode(const char* param, bool accept_chars) {
-    for(uint8_t i = 0; i < (sizeof(ducky_keys) / sizeof(ducky_keys[0])); i++) {
-        uint8_t key_cmd_len = strlen(ducky_keys[i].name);
+static uint16_t ducky_get_keycode(BadUsbScript* bad_usb, const char* param, bool accept_chars) {
+    for(size_t i = 0; i < (sizeof(ducky_keys) / sizeof(ducky_keys[0])); i++) {
+        size_t key_cmd_len = strlen(ducky_keys[i].name);
         if((strncmp(param, ducky_keys[i].name, key_cmd_len) == 0) &&
         if((strncmp(param, ducky_keys[i].name, key_cmd_len) == 0) &&
            (ducky_is_line_end(param[key_cmd_len]))) {
            (ducky_is_line_end(param[key_cmd_len]))) {
             return ducky_keys[i].keycode;
             return ducky_keys[i].keycode;
         }
         }
     }
     }
     if((accept_chars) && (strlen(param) > 0)) {
     if((accept_chars) && (strlen(param) > 0)) {
-        return (HID_ASCII_TO_KEY(param[0]) & 0xFF);
+        return (BADUSB_ASCII_TO_KEY(bad_usb, param[0]) & 0xFF);
     }
     }
     return 0;
     return 0;
 }
 }
@@ -275,7 +280,7 @@ static int32_t
     } else if(strncmp(line_tmp, ducky_cmd_string, strlen(ducky_cmd_string)) == 0) {
     } else if(strncmp(line_tmp, ducky_cmd_string, strlen(ducky_cmd_string)) == 0) {
         // STRING
         // STRING
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
-        state = ducky_string(line_tmp);
+        state = ducky_string(bad_usb, line_tmp);
         if(!state && error != NULL) {
         if(!state && error != NULL) {
             snprintf(error, error_len, "Invalid string %s", line_tmp);
             snprintf(error, error_len, "Invalid string %s", line_tmp);
         }
         }
@@ -311,14 +316,14 @@ static int32_t
     } else if(strncmp(line_tmp, ducky_cmd_sysrq, strlen(ducky_cmd_sysrq)) == 0) {
     } else if(strncmp(line_tmp, ducky_cmd_sysrq, strlen(ducky_cmd_sysrq)) == 0) {
         // SYSRQ
         // SYSRQ
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
-        uint16_t key = ducky_get_keycode(line_tmp, true);
+        uint16_t key = ducky_get_keycode(bad_usb, line_tmp, true);
         furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
         furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
         furi_hal_hid_kb_press(key);
         furi_hal_hid_kb_press(key);
         furi_hal_hid_kb_release_all();
         furi_hal_hid_kb_release_all();
         return (0);
         return (0);
     } else {
     } else {
         // Special keys + modifiers
         // Special keys + modifiers
-        uint16_t key = ducky_get_keycode(line_tmp, false);
+        uint16_t key = ducky_get_keycode(bad_usb, line_tmp, false);
         if(key == HID_KEYBOARD_NONE) {
         if(key == HID_KEYBOARD_NONE) {
             if(error != NULL) {
             if(error != NULL) {
                 snprintf(error, error_len, "No keycode defined for %s", line_tmp);
                 snprintf(error, error_len, "No keycode defined for %s", line_tmp);
@@ -328,7 +333,7 @@ static int32_t
         if((key & 0xFF00) != 0) {
         if((key & 0xFF00) != 0) {
             // It's a modifier key
             // It's a modifier key
             line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
             line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
-            key |= ducky_get_keycode(line_tmp, true);
+            key |= ducky_get_keycode(bad_usb, line_tmp, true);
         }
         }
         furi_hal_hid_kb_press(key);
         furi_hal_hid_kb_press(key);
         furi_hal_hid_kb_release(key);
         furi_hal_hid_kb_release(key);
@@ -417,7 +422,7 @@ static int32_t ducky_script_execute_next(BadUsbScript* bad_usb, File* script_fil
             return 0;
             return 0;
         } else if(delay_val < 0) { // Script error
         } else if(delay_val < 0) { // Script error
             bad_usb->st.error_line = bad_usb->st.line_cur - 1;
             bad_usb->st.error_line = bad_usb->st.line_cur - 1;
-            FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_usb->st.line_cur - 1);
+            FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_usb->st.line_cur - 1U);
             return SCRIPT_STATE_ERROR;
             return SCRIPT_STATE_ERROR;
         } else {
         } else {
             return (delay_val + bad_usb->defdelay);
             return (delay_val + bad_usb->defdelay);
@@ -485,8 +490,6 @@ static int32_t bad_usb_worker(void* context) {
     BadUsbWorkerState worker_state = BadUsbStateInit;
     BadUsbWorkerState worker_state = BadUsbStateInit;
     int32_t delay_val = 0;
     int32_t delay_val = 0;
 
 
-    FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config();
-
     FURI_LOG_I(WORKER_TAG, "Init");
     FURI_LOG_I(WORKER_TAG, "Init");
     File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
     File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
     bad_usb->line = furi_string_alloc();
     bad_usb->line = furi_string_alloc();
@@ -596,7 +599,9 @@ static int32_t bad_usb_worker(void* context) {
                 }
                 }
                 bad_usb->st.state = worker_state;
                 bad_usb->st.state = worker_state;
                 continue;
                 continue;
-            } else if((flags == FuriFlagErrorTimeout) || (flags == FuriFlagErrorResource)) {
+            } else if(
+                (flags == (unsigned)FuriFlagErrorTimeout) ||
+                (flags == (unsigned)FuriFlagErrorResource)) {
                 if(delay_val > 0) {
                 if(delay_val > 0) {
                     bad_usb->st.delay_remain--;
                     bad_usb->st.delay_remain--;
                     continue;
                     continue;
@@ -635,8 +640,6 @@ static int32_t bad_usb_worker(void* context) {
 
 
     furi_hal_hid_set_state_callback(NULL, NULL);
     furi_hal_hid_set_state_callback(NULL, NULL);
 
 
-    furi_hal_usb_set_config(usb_mode_prev, NULL);
-
     storage_file_close(script_file);
     storage_file_close(script_file);
     storage_file_free(script_file);
     storage_file_free(script_file);
     furi_string_free(bad_usb->line);
     furi_string_free(bad_usb->line);
@@ -647,12 +650,19 @@ static int32_t bad_usb_worker(void* context) {
     return 0;
     return 0;
 }
 }
 
 
+static void bad_usb_script_set_default_keyboard_layout(BadUsbScript* bad_usb) {
+    furi_assert(bad_usb);
+    memset(bad_usb->layout, HID_KEYBOARD_NONE, sizeof(bad_usb->layout));
+    memcpy(bad_usb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_usb->layout)));
+}
+
 BadUsbScript* bad_usb_script_open(FuriString* file_path) {
 BadUsbScript* bad_usb_script_open(FuriString* file_path) {
     furi_assert(file_path);
     furi_assert(file_path);
 
 
-    BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript)); //-V773
+    BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript));
     bad_usb->file_path = furi_string_alloc();
     bad_usb->file_path = furi_string_alloc();
     furi_string_set(bad_usb->file_path, file_path);
     furi_string_set(bad_usb->file_path, file_path);
+    bad_usb_script_set_default_keyboard_layout(bad_usb);
 
 
     bad_usb->st.state = BadUsbStateInit;
     bad_usb->st.state = BadUsbStateInit;
     bad_usb->st.error[0] = '\0';
     bad_usb->st.error[0] = '\0';
@@ -660,7 +670,7 @@ BadUsbScript* bad_usb_script_open(FuriString* file_path) {
     bad_usb->thread = furi_thread_alloc_ex("BadUsbWorker", 2048, bad_usb_worker, bad_usb);
     bad_usb->thread = furi_thread_alloc_ex("BadUsbWorker", 2048, bad_usb_worker, bad_usb);
     furi_thread_start(bad_usb->thread);
     furi_thread_start(bad_usb->thread);
     return bad_usb;
     return bad_usb;
-}
+} //-V773
 
 
 void bad_usb_script_close(BadUsbScript* bad_usb) {
 void bad_usb_script_close(BadUsbScript* bad_usb) {
     furi_assert(bad_usb);
     furi_assert(bad_usb);
@@ -671,6 +681,30 @@ void bad_usb_script_close(BadUsbScript* bad_usb) {
     free(bad_usb);
     free(bad_usb);
 }
 }
 
 
+void bad_usb_script_set_keyboard_layout(BadUsbScript* bad_usb, FuriString* layout_path) {
+    furi_assert(bad_usb);
+
+    if((bad_usb->st.state == BadUsbStateRunning) || (bad_usb->st.state == BadUsbStateDelay)) {
+        // do not update keyboard layout while a script is running
+        return;
+    }
+
+    File* layout_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
+    if(!furi_string_empty(layout_path)) { //-V1051
+        if(storage_file_open(
+               layout_file, furi_string_get_cstr(layout_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+            uint16_t layout[128];
+            if(storage_file_read(layout_file, layout, sizeof(layout)) == sizeof(layout)) {
+                memcpy(bad_usb->layout, layout, sizeof(layout));
+            }
+        }
+        storage_file_close(layout_file);
+    } else {
+        bad_usb_script_set_default_keyboard_layout(bad_usb);
+    }
+    storage_file_free(layout_file);
+}
+
 void bad_usb_script_toggle(BadUsbScript* bad_usb) {
 void bad_usb_script_toggle(BadUsbScript* bad_usb) {
     furi_assert(bad_usb);
     furi_assert(bad_usb);
     furi_thread_flags_set(furi_thread_get_id(bad_usb->thread), WorkerEvtToggle);
     furi_thread_flags_set(furi_thread_get_id(bad_usb->thread), WorkerEvtToggle);

+ 2 - 0
applications/main/bad_usb/bad_usb_script.h

@@ -33,6 +33,8 @@ BadUsbScript* bad_usb_script_open(FuriString* file_path);
 
 
 void bad_usb_script_close(BadUsbScript* bad_usb);
 void bad_usb_script_close(BadUsbScript* bad_usb);
 
 
+void bad_usb_script_set_keyboard_layout(BadUsbScript* bad_usb, FuriString* layout_path);
+
 void bad_usb_script_start(BadUsbScript* bad_usb);
 void bad_usb_script_start(BadUsbScript* bad_usb);
 
 
 void bad_usb_script_stop(BadUsbScript* bad_usb);
 void bad_usb_script_stop(BadUsbScript* bad_usb);

+ 3 - 0
applications/main/bad_usb/bad_usb_settings_filename.h

@@ -0,0 +1,3 @@
+#pragma once
+
+#define BAD_USB_SETTINGS_FILE_NAME ".badusb.settings"

+ 53 - 0
applications/main/bad_usb/scenes/bad_usb_scene_config.c

@@ -0,0 +1,53 @@
+#include "../bad_usb_app_i.h"
+#include "furi_hal_power.h"
+#include "furi_hal_usb.h"
+
+enum SubmenuIndex {
+    SubmenuIndexKeyboardLayout,
+};
+
+void bad_usb_scene_config_submenu_callback(void* context, uint32_t index) {
+    BadUsbApp* bad_usb = context;
+    view_dispatcher_send_custom_event(bad_usb->view_dispatcher, index);
+}
+
+void bad_usb_scene_config_on_enter(void* context) {
+    BadUsbApp* bad_usb = context;
+    Submenu* submenu = bad_usb->submenu;
+
+    submenu_add_item(
+        submenu,
+        "Keyboard layout",
+        SubmenuIndexKeyboardLayout,
+        bad_usb_scene_config_submenu_callback,
+        bad_usb);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(bad_usb->scene_manager, BadUsbSceneConfig));
+
+    view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewConfig);
+}
+
+bool bad_usb_scene_config_on_event(void* context, SceneManagerEvent event) {
+    BadUsbApp* bad_usb = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(bad_usb->scene_manager, BadUsbSceneConfig, event.event);
+        consumed = true;
+        if(event.event == SubmenuIndexKeyboardLayout) {
+            scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigLayout);
+        } else {
+            furi_crash("Unknown key type");
+        }
+    }
+
+    return consumed;
+}
+
+void bad_usb_scene_config_on_exit(void* context) {
+    BadUsbApp* bad_usb = context;
+    Submenu* submenu = bad_usb->submenu;
+
+    submenu_reset(submenu);
+}

+ 2 - 0
applications/main/bad_usb/scenes/bad_usb_scene_config.h

@@ -1,3 +1,5 @@
 ADD_SCENE(bad_usb, file_select, FileSelect)
 ADD_SCENE(bad_usb, file_select, FileSelect)
 ADD_SCENE(bad_usb, work, Work)
 ADD_SCENE(bad_usb, work, Work)
 ADD_SCENE(bad_usb, error, Error)
 ADD_SCENE(bad_usb, error, Error)
+ADD_SCENE(bad_usb, config, Config)
+ADD_SCENE(bad_usb, config_layout, ConfigLayout)

+ 50 - 0
applications/main/bad_usb/scenes/bad_usb_scene_config_layout.c

@@ -0,0 +1,50 @@
+#include "../bad_usb_app_i.h"
+#include "furi_hal_power.h"
+#include "furi_hal_usb.h"
+#include <storage/storage.h>
+
+static bool bad_usb_layout_select(BadUsbApp* bad_usb) {
+    furi_assert(bad_usb);
+
+    FuriString* predefined_path;
+    predefined_path = furi_string_alloc();
+    if(!furi_string_empty(bad_usb->keyboard_layout)) {
+        furi_string_set(predefined_path, bad_usb->keyboard_layout);
+    } else {
+        furi_string_set(predefined_path, BAD_USB_APP_PATH_LAYOUT_FOLDER);
+    }
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, BAD_USB_APP_LAYOUT_EXTENSION, &I_keyboard_10px);
+    browser_options.base_path = BAD_USB_APP_PATH_LAYOUT_FOLDER;
+    browser_options.skip_assets = false;
+
+    // Input events and views are managed by file_browser
+    bool res = dialog_file_browser_show(
+        bad_usb->dialogs, bad_usb->keyboard_layout, predefined_path, &browser_options);
+
+    furi_string_free(predefined_path);
+    return res;
+}
+
+void bad_usb_scene_config_layout_on_enter(void* context) {
+    BadUsbApp* bad_usb = context;
+
+    if(bad_usb_layout_select(bad_usb)) {
+        bad_usb_script_set_keyboard_layout(bad_usb->bad_usb_script, bad_usb->keyboard_layout);
+    }
+    scene_manager_previous_scene(bad_usb->scene_manager);
+}
+
+bool bad_usb_scene_config_layout_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    // BadUsbApp* bad_usb = context;
+    return false;
+}
+
+void bad_usb_scene_config_layout_on_exit(void* context) {
+    UNUSED(context);
+    // BadUsbApp* bad_usb = context;
+}

+ 13 - 5
applications/main/bad_usb/scenes/bad_usb_scene_file_select.c

@@ -1,14 +1,16 @@
 #include "../bad_usb_app_i.h"
 #include "../bad_usb_app_i.h"
-#include "furi_hal_power.h"
-#include "furi_hal_usb.h"
+#include <furi_hal_power.h>
+#include <furi_hal_usb.h>
 #include <storage/storage.h>
 #include <storage/storage.h>
 
 
 static bool bad_usb_file_select(BadUsbApp* bad_usb) {
 static bool bad_usb_file_select(BadUsbApp* bad_usb) {
     furi_assert(bad_usb);
     furi_assert(bad_usb);
 
 
     DialogsFileBrowserOptions browser_options;
     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;
+    dialog_file_browser_set_basic_options(
+        &browser_options, BAD_USB_APP_SCRIPT_EXTENSION, &I_badusb_10px);
+    browser_options.base_path = BAD_USB_APP_BASE_FOLDER;
+    browser_options.skip_assets = true;
 
 
     // Input events and views are managed by file_browser
     // Input events and views are managed by file_browser
     bool res = dialog_file_browser_show(
     bool res = dialog_file_browser_show(
@@ -21,12 +23,18 @@ void bad_usb_scene_file_select_on_enter(void* context) {
     BadUsbApp* bad_usb = context;
     BadUsbApp* bad_usb = context;
 
 
     furi_hal_usb_disable();
     furi_hal_usb_disable();
+    if(bad_usb->bad_usb_script) {
+        bad_usb_script_close(bad_usb->bad_usb_script);
+        bad_usb->bad_usb_script = NULL;
+    }
 
 
     if(bad_usb_file_select(bad_usb)) {
     if(bad_usb_file_select(bad_usb)) {
+        bad_usb->bad_usb_script = bad_usb_script_open(bad_usb->file_path);
+        bad_usb_script_set_keyboard_layout(bad_usb->bad_usb_script, bad_usb->keyboard_layout);
+
         scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneWork);
         scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneWork);
     } else {
     } else {
         furi_hal_usb_enable();
         furi_hal_usb_enable();
-        //scene_manager_previous_scene(bad_usb->scene_manager);
         view_dispatcher_stop(bad_usb->view_dispatcher);
         view_dispatcher_stop(bad_usb->view_dispatcher);
     }
     }
 }
 }

+ 18 - 11
applications/main/bad_usb/scenes/bad_usb_scene_work.c

@@ -1,13 +1,13 @@
 #include "../bad_usb_script.h"
 #include "../bad_usb_script.h"
 #include "../bad_usb_app_i.h"
 #include "../bad_usb_app_i.h"
 #include "../views/bad_usb_view.h"
 #include "../views/bad_usb_view.h"
-#include "furi_hal.h"
+#include <furi_hal.h>
 #include "toolbox/path.h"
 #include "toolbox/path.h"
 
 
-void bad_usb_scene_work_ok_callback(InputType type, void* context) {
+void bad_usb_scene_work_button_callback(InputKey key, void* context) {
     furi_assert(context);
     furi_assert(context);
     BadUsbApp* app = context;
     BadUsbApp* app = context;
-    view_dispatcher_send_custom_event(app->view_dispatcher, type);
+    view_dispatcher_send_custom_event(app->view_dispatcher, key);
 }
 }
 
 
 bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
 bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
@@ -15,8 +15,13 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
     bool consumed = false;
     bool consumed = false;
 
 
     if(event.type == SceneManagerEventTypeCustom) {
     if(event.type == SceneManagerEventTypeCustom) {
-        bad_usb_script_toggle(app->bad_usb_script);
-        consumed = true;
+        if(event.event == InputKeyLeft) {
+            scene_manager_next_scene(app->scene_manager, BadUsbSceneConfig);
+            consumed = true;
+        } else if(event.event == InputKeyOk) {
+            bad_usb_script_toggle(app->bad_usb_script);
+            consumed = true;
+        }
     } else if(event.type == SceneManagerEventTypeTick) {
     } else if(event.type == SceneManagerEventTypeTick) {
         bad_usb_set_state(app->bad_usb_view, bad_usb_script_get_state(app->bad_usb_script));
         bad_usb_set_state(app->bad_usb_view, bad_usb_script_get_state(app->bad_usb_script));
     }
     }
@@ -28,20 +33,22 @@ void bad_usb_scene_work_on_enter(void* context) {
 
 
     FuriString* file_name;
     FuriString* file_name;
     file_name = furi_string_alloc();
     file_name = furi_string_alloc();
-
     path_extract_filename(app->file_path, file_name, true);
     path_extract_filename(app->file_path, file_name, true);
     bad_usb_set_file_name(app->bad_usb_view, furi_string_get_cstr(file_name));
     bad_usb_set_file_name(app->bad_usb_view, furi_string_get_cstr(file_name));
-    app->bad_usb_script = bad_usb_script_open(app->file_path);
-
     furi_string_free(file_name);
     furi_string_free(file_name);
 
 
+    FuriString* layout;
+    layout = furi_string_alloc();
+    path_extract_filename(app->keyboard_layout, layout, true);
+    bad_usb_set_layout(app->bad_usb_view, furi_string_get_cstr(layout));
+    furi_string_free(layout);
+
     bad_usb_set_state(app->bad_usb_view, bad_usb_script_get_state(app->bad_usb_script));
     bad_usb_set_state(app->bad_usb_view, bad_usb_script_get_state(app->bad_usb_script));
 
 
-    bad_usb_set_ok_callback(app->bad_usb_view, bad_usb_scene_work_ok_callback, app);
+    bad_usb_set_button_callback(app->bad_usb_view, bad_usb_scene_work_button_callback, app);
     view_dispatcher_switch_to_view(app->view_dispatcher, BadUsbAppViewWork);
     view_dispatcher_switch_to_view(app->view_dispatcher, BadUsbAppViewWork);
 }
 }
 
 
 void bad_usb_scene_work_on_exit(void* context) {
 void bad_usb_scene_work_on_exit(void* context) {
-    BadUsbApp* app = context;
-    bad_usb_script_close(app->bad_usb_script);
+    UNUSED(context);
 }
 }

+ 60 - 31
applications/main/bad_usb/views/bad_usb_view.c

@@ -1,5 +1,6 @@
 #include "bad_usb_view.h"
 #include "bad_usb_view.h"
 #include "../bad_usb_script.h"
 #include "../bad_usb_script.h"
+#include <toolbox/path.h>
 #include <gui/elements.h>
 #include <gui/elements.h>
 #include <assets_icons.h>
 #include <assets_icons.h>
 
 
@@ -7,12 +8,13 @@
 
 
 struct BadUsb {
 struct BadUsb {
     View* view;
     View* view;
-    BadUsbOkCallback callback;
+    BadUsbButtonCallback callback;
     void* context;
     void* context;
 };
 };
 
 
 typedef struct {
 typedef struct {
     char file_name[MAX_NAME_LEN];
     char file_name[MAX_NAME_LEN];
+    char layout[MAX_NAME_LEN];
     BadUsbState state;
     BadUsbState state;
     uint8_t anim_frame;
     uint8_t anim_frame;
 } BadUsbModel;
 } BadUsbModel;
@@ -25,9 +27,23 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
     elements_string_fit_width(canvas, disp_str, 128 - 2);
     elements_string_fit_width(canvas, disp_str, 128 - 2);
     canvas_set_font(canvas, FontSecondary);
     canvas_set_font(canvas, FontSecondary);
     canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str));
     canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str));
+
+    if(strlen(model->layout) == 0) {
+        furi_string_set(disp_str, "(default)");
+    } else {
+        furi_string_reset(disp_str);
+        furi_string_push_back(disp_str, '(');
+        for(size_t i = 0; i < strlen(model->layout); i++)
+            furi_string_push_back(disp_str, model->layout[i]);
+        furi_string_push_back(disp_str, ')');
+    }
+    elements_string_fit_width(canvas, disp_str, 128 - 2);
+    canvas_draw_str(
+        canvas, 2, 8 + canvas_current_font_height(canvas), furi_string_get_cstr(disp_str));
+
     furi_string_reset(disp_str);
     furi_string_reset(disp_str);
 
 
-    canvas_draw_icon(canvas, 22, 20, &I_UsbTree_48x22);
+    canvas_draw_icon(canvas, 22, 24, &I_UsbTree_48x22);
 
 
     if((model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone) ||
     if((model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone) ||
        (model->state.state == BadUsbStateNotConnected)) {
        (model->state.state == BadUsbStateNotConnected)) {
@@ -38,23 +54,28 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
         elements_button_center(canvas, "Cancel");
         elements_button_center(canvas, "Cancel");
     }
     }
 
 
+    if((model->state.state == BadUsbStateNotConnected) ||
+       (model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone)) {
+        elements_button_left(canvas, "Config");
+    }
+
     if(model->state.state == BadUsbStateNotConnected) {
     if(model->state.state == BadUsbStateNotConnected) {
-        canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
         canvas_set_font(canvas, FontPrimary);
         canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 27, AlignRight, AlignBottom, "Connect");
-        canvas_draw_str_aligned(canvas, 127, 39, AlignRight, AlignBottom, "to USB");
+        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Connect");
+        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "to USB");
     } else if(model->state.state == BadUsbStateWillRun) {
     } else if(model->state.state == BadUsbStateWillRun) {
-        canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
         canvas_set_font(canvas, FontPrimary);
         canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 27, AlignRight, AlignBottom, "Will run");
-        canvas_draw_str_aligned(canvas, 127, 39, AlignRight, AlignBottom, "on connect");
+        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Will run");
+        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "on connect");
     } else if(model->state.state == BadUsbStateFileError) {
     } else if(model->state.state == BadUsbStateFileError) {
-        canvas_draw_icon(canvas, 4, 22, &I_Error_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
         canvas_set_font(canvas, FontPrimary);
         canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 27, AlignRight, AlignBottom, "File");
-        canvas_draw_str_aligned(canvas, 127, 39, AlignRight, AlignBottom, "ERROR");
+        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "File");
+        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "ERROR");
     } else if(model->state.state == BadUsbStateScriptError) {
     } else if(model->state.state == BadUsbStateScriptError) {
-        canvas_draw_icon(canvas, 4, 22, &I_Error_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
         canvas_set_font(canvas, FontPrimary);
         canvas_set_font(canvas, FontPrimary);
         canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
         canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
         canvas_set_font(canvas, FontSecondary);
         canvas_set_font(canvas, FontSecondary);
@@ -64,49 +85,49 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
         furi_string_reset(disp_str);
         furi_string_reset(disp_str);
         canvas_draw_str_aligned(canvas, 127, 56, AlignRight, AlignBottom, model->state.error);
         canvas_draw_str_aligned(canvas, 127, 56, AlignRight, AlignBottom, model->state.error);
     } else if(model->state.state == BadUsbStateIdle) {
     } else if(model->state.state == BadUsbStateIdle) {
-        canvas_draw_icon(canvas, 4, 22, &I_Smile_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Smile_18x18);
         canvas_set_font(canvas, FontBigNumbers);
         canvas_set_font(canvas, FontBigNumbers);
-        canvas_draw_str_aligned(canvas, 114, 36, AlignRight, AlignBottom, "0");
-        canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
+        canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "0");
+        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
     } else if(model->state.state == BadUsbStateRunning) {
     } else if(model->state.state == BadUsbStateRunning) {
         if(model->anim_frame == 0) {
         if(model->anim_frame == 0) {
-            canvas_draw_icon(canvas, 4, 19, &I_EviSmile1_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
         } else {
         } else {
-            canvas_draw_icon(canvas, 4, 19, &I_EviSmile2_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviSmile2_18x21);
         }
         }
         canvas_set_font(canvas, FontBigNumbers);
         canvas_set_font(canvas, FontBigNumbers);
         furi_string_printf(
         furi_string_printf(
             disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
             disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
         canvas_draw_str_aligned(
         canvas_draw_str_aligned(
-            canvas, 114, 36, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+            canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
         furi_string_reset(disp_str);
         furi_string_reset(disp_str);
-        canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
+        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
     } else if(model->state.state == BadUsbStateDone) {
     } else if(model->state.state == BadUsbStateDone) {
-        canvas_draw_icon(canvas, 4, 19, &I_EviSmile1_18x21);
+        canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
         canvas_set_font(canvas, FontBigNumbers);
         canvas_set_font(canvas, FontBigNumbers);
-        canvas_draw_str_aligned(canvas, 114, 36, AlignRight, AlignBottom, "100");
+        canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "100");
         furi_string_reset(disp_str);
         furi_string_reset(disp_str);
-        canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
+        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
     } else if(model->state.state == BadUsbStateDelay) {
     } else if(model->state.state == BadUsbStateDelay) {
         if(model->anim_frame == 0) {
         if(model->anim_frame == 0) {
-            canvas_draw_icon(canvas, 4, 19, &I_EviWaiting1_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
         } else {
         } else {
-            canvas_draw_icon(canvas, 4, 19, &I_EviWaiting2_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
         }
         }
         canvas_set_font(canvas, FontBigNumbers);
         canvas_set_font(canvas, FontBigNumbers);
         furi_string_printf(
         furi_string_printf(
             disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
             disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
         canvas_draw_str_aligned(
         canvas_draw_str_aligned(
-            canvas, 114, 36, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+            canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
         furi_string_reset(disp_str);
         furi_string_reset(disp_str);
-        canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
+        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
         canvas_set_font(canvas, FontSecondary);
         canvas_set_font(canvas, FontSecondary);
         furi_string_printf(disp_str, "delay %lus", model->state.delay_remain);
         furi_string_printf(disp_str, "delay %lus", model->state.delay_remain);
         canvas_draw_str_aligned(
         canvas_draw_str_aligned(
-            canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+            canvas, 127, 50, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
         furi_string_reset(disp_str);
         furi_string_reset(disp_str);
     } else {
     } else {
-        canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
     }
     }
 
 
     furi_string_free(disp_str);
     furi_string_free(disp_str);
@@ -118,10 +139,10 @@ static bool bad_usb_input_callback(InputEvent* event, void* context) {
     bool consumed = false;
     bool consumed = false;
 
 
     if(event->type == InputTypeShort) {
     if(event->type == InputTypeShort) {
-        if(event->key == InputKeyOk) {
+        if((event->key == InputKeyLeft) || (event->key == InputKeyOk)) {
             consumed = true;
             consumed = true;
             furi_assert(bad_usb->callback);
             furi_assert(bad_usb->callback);
-            bad_usb->callback(InputTypeShort, bad_usb->context);
+            bad_usb->callback(event->key, bad_usb->context);
         }
         }
     }
     }
 
 
@@ -151,7 +172,7 @@ View* bad_usb_get_view(BadUsb* bad_usb) {
     return bad_usb->view;
     return bad_usb->view;
 }
 }
 
 
-void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* context) {
+void bad_usb_set_button_callback(BadUsb* bad_usb, BadUsbButtonCallback callback, void* context) {
     furi_assert(bad_usb);
     furi_assert(bad_usb);
     furi_assert(callback);
     furi_assert(callback);
     with_view_model(
     with_view_model(
@@ -174,6 +195,14 @@ void bad_usb_set_file_name(BadUsb* bad_usb, const char* name) {
         true);
         true);
 }
 }
 
 
+void bad_usb_set_layout(BadUsb* bad_usb, const char* layout) {
+    furi_assert(layout);
+    with_view_model(
+        bad_usb->view,
+        BadUsbModel * model,
+        { strlcpy(model->layout, layout, MAX_NAME_LEN); },
+        true);
+}
 void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st) {
 void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st) {
     furi_assert(st);
     furi_assert(st);
     with_view_model(
     with_view_model(

+ 4 - 2
applications/main/bad_usb/views/bad_usb_view.h

@@ -4,7 +4,7 @@
 #include "../bad_usb_script.h"
 #include "../bad_usb_script.h"
 
 
 typedef struct BadUsb BadUsb;
 typedef struct BadUsb BadUsb;
-typedef void (*BadUsbOkCallback)(InputType type, void* context);
+typedef void (*BadUsbButtonCallback)(InputKey key, void* context);
 
 
 BadUsb* bad_usb_alloc();
 BadUsb* bad_usb_alloc();
 
 
@@ -12,8 +12,10 @@ void bad_usb_free(BadUsb* bad_usb);
 
 
 View* bad_usb_get_view(BadUsb* bad_usb);
 View* bad_usb_get_view(BadUsb* bad_usb);
 
 
-void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* context);
+void bad_usb_set_button_callback(BadUsb* bad_usb, BadUsbButtonCallback callback, void* context);
 
 
 void bad_usb_set_file_name(BadUsb* bad_usb, const char* name);
 void bad_usb_set_file_name(BadUsb* bad_usb, const char* name);
 
 
+void bad_usb_set_layout(BadUsb* bad_usb, const char* layout);
+
 void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st);
 void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st);

+ 2 - 2
applications/main/fap_loader/fap_loader_app.c

@@ -156,7 +156,7 @@ static bool fap_loader_select_app(FapLoader* loader) {
 }
 }
 
 
 static FapLoader* fap_loader_alloc(const char* path) {
 static FapLoader* fap_loader_alloc(const char* path) {
-    FapLoader* loader = malloc(sizeof(FapLoader)); //-V773
+    FapLoader* loader = malloc(sizeof(FapLoader)); //-V799
     loader->fap_path = furi_string_alloc_set(path);
     loader->fap_path = furi_string_alloc_set(path);
     loader->storage = furi_record_open(RECORD_STORAGE);
     loader->storage = furi_record_open(RECORD_STORAGE);
     loader->dialogs = furi_record_open(RECORD_DIALOGS);
     loader->dialogs = furi_record_open(RECORD_DIALOGS);
@@ -167,7 +167,7 @@ static FapLoader* fap_loader_alloc(const char* path) {
         loader->view_dispatcher, loader->gui, ViewDispatcherTypeFullscreen);
         loader->view_dispatcher, loader->gui, ViewDispatcherTypeFullscreen);
     view_dispatcher_add_view(loader->view_dispatcher, 0, loading_get_view(loader->loading));
     view_dispatcher_add_view(loader->view_dispatcher, 0, loading_get_view(loader->loading));
     return loader;
     return loader;
-}
+} //-V773
 
 
 static void fap_loader_free(FapLoader* loader) {
 static void fap_loader_free(FapLoader* loader) {
     view_dispatcher_remove_view(loader->view_dispatcher, 0);
     view_dispatcher_remove_view(loader->view_dispatcher, 0);

+ 3 - 1
applications/main/gpio/gpio_app.c

@@ -25,6 +25,7 @@ GpioApp* gpio_app_alloc() {
     GpioApp* app = malloc(sizeof(GpioApp));
     GpioApp* app = malloc(sizeof(GpioApp));
 
 
     app->gui = furi_record_open(RECORD_GUI);
     app->gui = furi_record_open(RECORD_GUI);
+    app->gpio_items = gpio_items_alloc();
 
 
     app->view_dispatcher = view_dispatcher_alloc();
     app->view_dispatcher = view_dispatcher_alloc();
     app->scene_manager = scene_manager_alloc(&gpio_scene_handlers, app);
     app->scene_manager = scene_manager_alloc(&gpio_scene_handlers, app);
@@ -47,7 +48,7 @@ GpioApp* gpio_app_alloc() {
         app->view_dispatcher,
         app->view_dispatcher,
         GpioAppViewVarItemList,
         GpioAppViewVarItemList,
         variable_item_list_get_view(app->var_item_list));
         variable_item_list_get_view(app->var_item_list));
-    app->gpio_test = gpio_test_alloc();
+    app->gpio_test = gpio_test_alloc(app->gpio_items);
     view_dispatcher_add_view(
     view_dispatcher_add_view(
         app->view_dispatcher, GpioAppViewGpioTest, gpio_test_get_view(app->gpio_test));
         app->view_dispatcher, GpioAppViewGpioTest, gpio_test_get_view(app->gpio_test));
 
 
@@ -91,6 +92,7 @@ void gpio_app_free(GpioApp* app) {
     furi_record_close(RECORD_GUI);
     furi_record_close(RECORD_GUI);
     furi_record_close(RECORD_NOTIFICATION);
     furi_record_close(RECORD_NOTIFICATION);
 
 
+    gpio_items_free(app->gpio_items);
     free(app);
     free(app);
 }
 }
 
 

+ 2 - 1
applications/main/gpio/gpio_app_i.h

@@ -1,7 +1,7 @@
 #pragma once
 #pragma once
 
 
 #include "gpio_app.h"
 #include "gpio_app.h"
-#include "gpio_item.h"
+#include "gpio_items.h"
 #include "scenes/gpio_scene.h"
 #include "scenes/gpio_scene.h"
 #include "gpio_custom_event.h"
 #include "gpio_custom_event.h"
 #include "usb_uart_bridge.h"
 #include "usb_uart_bridge.h"
@@ -28,6 +28,7 @@ struct GpioApp {
     VariableItem* var_item_flow;
     VariableItem* var_item_flow;
     GpioTest* gpio_test;
     GpioTest* gpio_test;
     GpioUsbUart* gpio_usb_uart;
     GpioUsbUart* gpio_usb_uart;
+    GPIOItems* gpio_items;
     UsbUartBridge* usb_uart_bridge;
     UsbUartBridge* usb_uart_bridge;
     UsbUartConfig* usb_uart_cfg;
     UsbUartConfig* usb_uart_cfg;
 };
 };

+ 0 - 51
applications/main/gpio/gpio_item.c

@@ -1,51 +0,0 @@
-#include "gpio_item.h"
-
-#include <furi_hal_resources.h>
-
-typedef struct {
-    const char* name;
-    const GpioPin* pin;
-} GpioItem;
-
-static const GpioItem gpio_item[GPIO_ITEM_COUNT] = {
-    {"1.2: PA7", &gpio_ext_pa7},
-    {"1.3: PA6", &gpio_ext_pa6},
-    {"1.4: PA4", &gpio_ext_pa4},
-    {"1.5: PB3", &gpio_ext_pb3},
-    {"1.6: PB2", &gpio_ext_pb2},
-    {"1.7: PC3", &gpio_ext_pc3},
-    {"2.7: PC1", &gpio_ext_pc1},
-    {"2.8: PC0", &gpio_ext_pc0},
-};
-
-void gpio_item_configure_pin(uint8_t index, GpioMode mode) {
-    furi_assert(index < GPIO_ITEM_COUNT);
-    furi_hal_gpio_write(gpio_item[index].pin, false);
-    furi_hal_gpio_init(gpio_item[index].pin, mode, GpioPullNo, GpioSpeedVeryHigh);
-}
-
-void gpio_item_configure_all_pins(GpioMode mode) {
-    for(uint8_t i = 0; i < GPIO_ITEM_COUNT; i++) {
-        gpio_item_configure_pin(i, mode);
-    }
-}
-
-void gpio_item_set_pin(uint8_t index, bool level) {
-    furi_assert(index < GPIO_ITEM_COUNT);
-    furi_hal_gpio_write(gpio_item[index].pin, level);
-}
-
-void gpio_item_set_all_pins(bool level) {
-    for(uint8_t i = 0; i < GPIO_ITEM_COUNT; i++) {
-        gpio_item_set_pin(i, level);
-    }
-}
-
-const char* gpio_item_get_pin_name(uint8_t index) {
-    furi_assert(index < GPIO_ITEM_COUNT + 1);
-    if(index == GPIO_ITEM_COUNT) {
-        return "ALL";
-    } else {
-        return gpio_item[index].name;
-    }
-}

+ 0 - 15
applications/main/gpio/gpio_item.h

@@ -1,15 +0,0 @@
-#pragma once
-
-#include <furi_hal_gpio.h>
-
-#define GPIO_ITEM_COUNT 8
-
-void gpio_item_configure_pin(uint8_t index, GpioMode mode);
-
-void gpio_item_configure_all_pins(GpioMode mode);
-
-void gpio_item_set_pin(uint8_t index, bool level);
-
-void gpio_item_set_all_pins(bool level);
-
-const char* gpio_item_get_pin_name(uint8_t index);

+ 69 - 0
applications/main/gpio/gpio_items.c

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

+ 29 - 0
applications/main/gpio/gpio_items.h

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

+ 2 - 2
applications/main/gpio/scenes/gpio_scene_start.c

@@ -1,6 +1,6 @@
 #include "../gpio_app_i.h"
 #include "../gpio_app_i.h"
-#include "furi_hal_power.h"
-#include "furi_hal_usb.h"
+#include <furi_hal_power.h>
+#include <furi_hal_usb.h>
 #include <dolphin/dolphin.h>
 #include <dolphin/dolphin.h>
 
 
 enum GpioItem {
 enum GpioItem {

+ 5 - 3
applications/main/gpio/scenes/gpio_scene_test.c

@@ -12,8 +12,9 @@ void gpio_scene_test_ok_callback(InputType type, void* context) {
 }
 }
 
 
 void gpio_scene_test_on_enter(void* context) {
 void gpio_scene_test_on_enter(void* context) {
+    furi_assert(context);
     GpioApp* app = context;
     GpioApp* app = context;
-    gpio_item_configure_all_pins(GpioModeOutputPushPull);
+    gpio_items_configure_all_pins(app->gpio_items, GpioModeOutputPushPull);
     gpio_test_set_ok_callback(app->gpio_test, gpio_scene_test_ok_callback, app);
     gpio_test_set_ok_callback(app->gpio_test, gpio_scene_test_ok_callback, app);
     view_dispatcher_switch_to_view(app->view_dispatcher, GpioAppViewGpioTest);
     view_dispatcher_switch_to_view(app->view_dispatcher, GpioAppViewGpioTest);
 }
 }
@@ -25,6 +26,7 @@ bool gpio_scene_test_on_event(void* context, SceneManagerEvent event) {
 }
 }
 
 
 void gpio_scene_test_on_exit(void* context) {
 void gpio_scene_test_on_exit(void* context) {
-    UNUSED(context);
-    gpio_item_configure_all_pins(GpioModeAnalog);
+    furi_assert(context);
+    GpioApp* app = context;
+    gpio_items_configure_all_pins(app->gpio_items, GpioModeAnalog);
 }
 }

+ 4 - 1
applications/main/gpio/scenes/gpio_scene_usb_uart_config.c

@@ -1,6 +1,6 @@
 #include "../usb_uart_bridge.h"
 #include "../usb_uart_bridge.h"
 #include "../gpio_app_i.h"
 #include "../gpio_app_i.h"
-#include "furi_hal.h"
+#include <furi_hal.h>
 
 
 typedef enum {
 typedef enum {
     UsbUartLineIndexVcp,
     UsbUartLineIndexVcp,
@@ -14,9 +14,12 @@ static const char* uart_ch[] = {"13,14", "15,16"};
 static const char* flow_pins[] = {"None", "2,3", "6,7", "16,15"};
 static const char* flow_pins[] = {"None", "2,3", "6,7", "16,15"};
 static const char* baudrate_mode[] = {"Host"};
 static const char* baudrate_mode[] = {"Host"};
 static const uint32_t baudrate_list[] = {
 static const uint32_t baudrate_list[] = {
+    1200,
     2400,
     2400,
+    4800,
     9600,
     9600,
     19200,
     19200,
+    28800,
     38400,
     38400,
     57600,
     57600,
     115200,
     115200,

+ 4 - 4
applications/main/gpio/usb_uart_bridge.c

@@ -1,10 +1,10 @@
 #include "usb_uart_bridge.h"
 #include "usb_uart_bridge.h"
-#include "furi_hal.h"
-#include <furi_hal_usb_cdc.h>
 #include "usb_cdc.h"
 #include "usb_cdc.h"
-#include "cli/cli_vcp.h"
+#include <cli/cli_vcp.h>
+#include <cli/cli.h>
 #include <toolbox/api_lock.h>
 #include <toolbox/api_lock.h>
-#include "cli/cli.h"
+#include <furi_hal.h>
+#include <furi_hal_usb_cdc.h>
 
 
 #define USB_CDC_PKT_LEN CDC_DATA_SZ
 #define USB_CDC_PKT_LEN CDC_DATA_SZ
 #define USB_UART_RX_BUF_SIZE (USB_CDC_PKT_LEN * 5)
 #define USB_UART_RX_BUF_SIZE (USB_CDC_PKT_LEN * 5)

+ 20 - 10
applications/main/gpio/views/gpio_test.c

@@ -1,5 +1,5 @@
 #include "gpio_test.h"
 #include "gpio_test.h"
-#include "../gpio_item.h"
+#include "../gpio_items.h"
 
 
 #include <gui/elements.h>
 #include <gui/elements.h>
 
 
@@ -11,6 +11,7 @@ struct GpioTest {
 
 
 typedef struct {
 typedef struct {
     uint8_t pin_idx;
     uint8_t pin_idx;
+    GPIOItems* gpio_items;
 } GpioTestModel;
 } GpioTestModel;
 
 
 static bool gpio_test_process_left(GpioTest* gpio_test);
 static bool gpio_test_process_left(GpioTest* gpio_test);
@@ -25,7 +26,12 @@ static void gpio_test_draw_callback(Canvas* canvas, void* _model) {
     elements_multiline_text_aligned(
     elements_multiline_text_aligned(
         canvas, 64, 16, AlignCenter, AlignTop, "Press < or > to change pin");
         canvas, 64, 16, AlignCenter, AlignTop, "Press < or > to change pin");
     elements_multiline_text_aligned(
     elements_multiline_text_aligned(
-        canvas, 64, 32, AlignCenter, AlignTop, gpio_item_get_pin_name(model->pin_idx));
+        canvas,
+        64,
+        32,
+        AlignCenter,
+        AlignTop,
+        gpio_items_get_pin_name(model->gpio_items, model->pin_idx));
 }
 }
 
 
 static bool gpio_test_input_callback(InputEvent* event, void* context) {
 static bool gpio_test_input_callback(InputEvent* event, void* context) {
@@ -64,7 +70,7 @@ static bool gpio_test_process_right(GpioTest* gpio_test) {
         gpio_test->view,
         gpio_test->view,
         GpioTestModel * model,
         GpioTestModel * model,
         {
         {
-            if(model->pin_idx < GPIO_ITEM_COUNT) {
+            if(model->pin_idx < gpio_items_get_count(model->gpio_items)) {
                 model->pin_idx++;
                 model->pin_idx++;
             }
             }
         },
         },
@@ -80,17 +86,17 @@ static bool gpio_test_process_ok(GpioTest* gpio_test, InputEvent* event) {
         GpioTestModel * model,
         GpioTestModel * model,
         {
         {
             if(event->type == InputTypePress) {
             if(event->type == InputTypePress) {
-                if(model->pin_idx < GPIO_ITEM_COUNT) {
-                    gpio_item_set_pin(model->pin_idx, true);
+                if(model->pin_idx < gpio_items_get_count(model->gpio_items)) {
+                    gpio_items_set_pin(model->gpio_items, model->pin_idx, true);
                 } else {
                 } else {
-                    gpio_item_set_all_pins(true);
+                    gpio_items_set_all_pins(model->gpio_items, true);
                 }
                 }
                 consumed = true;
                 consumed = true;
             } else if(event->type == InputTypeRelease) {
             } else if(event->type == InputTypeRelease) {
-                if(model->pin_idx < GPIO_ITEM_COUNT) {
-                    gpio_item_set_pin(model->pin_idx, false);
+                if(model->pin_idx < gpio_items_get_count(model->gpio_items)) {
+                    gpio_items_set_pin(model->gpio_items, model->pin_idx, false);
                 } else {
                 } else {
-                    gpio_item_set_all_pins(false);
+                    gpio_items_set_all_pins(model->gpio_items, false);
                 }
                 }
                 consumed = true;
                 consumed = true;
             }
             }
@@ -101,11 +107,15 @@ static bool gpio_test_process_ok(GpioTest* gpio_test, InputEvent* event) {
     return consumed;
     return consumed;
 }
 }
 
 
-GpioTest* gpio_test_alloc() {
+GpioTest* gpio_test_alloc(GPIOItems* gpio_items) {
     GpioTest* gpio_test = malloc(sizeof(GpioTest));
     GpioTest* gpio_test = malloc(sizeof(GpioTest));
 
 
     gpio_test->view = view_alloc();
     gpio_test->view = view_alloc();
     view_allocate_model(gpio_test->view, ViewModelTypeLocking, sizeof(GpioTestModel));
     view_allocate_model(gpio_test->view, ViewModelTypeLocking, sizeof(GpioTestModel));
+
+    with_view_model(
+        gpio_test->view, GpioTestModel * model, { model->gpio_items = gpio_items; }, false);
+
     view_set_context(gpio_test->view, gpio_test);
     view_set_context(gpio_test->view, gpio_test);
     view_set_draw_callback(gpio_test->view, gpio_test_draw_callback);
     view_set_draw_callback(gpio_test->view, gpio_test_draw_callback);
     view_set_input_callback(gpio_test->view, gpio_test_input_callback);
     view_set_input_callback(gpio_test->view, gpio_test_input_callback);

+ 3 - 1
applications/main/gpio/views/gpio_test.h

@@ -1,11 +1,13 @@
 #pragma once
 #pragma once
 
 
+#include "../gpio_items.h"
+
 #include <gui/view.h>
 #include <gui/view.h>
 
 
 typedef struct GpioTest GpioTest;
 typedef struct GpioTest GpioTest;
 typedef void (*GpioTestOkCallback)(InputType type, void* context);
 typedef void (*GpioTestOkCallback)(InputType type, void* context);
 
 
-GpioTest* gpio_test_alloc();
+GpioTest* gpio_test_alloc(GPIOItems* gpio_items);
 
 
 void gpio_test_free(GpioTest* gpio_test);
 void gpio_test_free(GpioTest* gpio_test);
 
 

+ 1 - 1
applications/main/gpio/views/gpio_usb_uart.c

@@ -1,6 +1,6 @@
 #include "../usb_uart_bridge.h"
 #include "../usb_uart_bridge.h"
 #include "../gpio_app_i.h"
 #include "../gpio_app_i.h"
-#include "furi_hal.h"
+#include <furi_hal.h>
 #include <gui/elements.h>
 #include <gui/elements.h>
 
 
 struct GpioUsbUart {
 struct GpioUsbUart {

+ 1 - 0
applications/main/ibutton/application.fam

@@ -2,6 +2,7 @@ App(
     appid="ibutton",
     appid="ibutton",
     name="iButton",
     name="iButton",
     apptype=FlipperAppType.APP,
     apptype=FlipperAppType.APP,
+    targets=["f7"],
     entry_point="ibutton_app",
     entry_point="ibutton_app",
     cdefines=["APP_IBUTTON"],
     cdefines=["APP_IBUTTON"],
     requires=[
     requires=[

+ 3 - 3
applications/main/ibutton/ibutton.c

@@ -278,7 +278,7 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) {
 
 
     flipper_format_free(file);
     flipper_format_free(file);
 
 
-    if(!result) {
+    if(!result) { //-V547
         dialog_message_show_storage_error(ibutton->dialogs, "Cannot save\nkey file");
         dialog_message_show_storage_error(ibutton->dialogs, "Cannot save\nkey file");
     }
     }
 
 
@@ -302,7 +302,7 @@ void ibutton_text_store_set(iButton* ibutton, const char* text, ...) {
 }
 }
 
 
 void ibutton_text_store_clear(iButton* ibutton) {
 void ibutton_text_store_clear(iButton* ibutton) {
-    memset(ibutton->text_store, 0, IBUTTON_TEXT_STORE_SIZE);
+    memset(ibutton->text_store, 0, IBUTTON_TEXT_STORE_SIZE + 1);
 }
 }
 
 
 void ibutton_notification_message(iButton* ibutton, uint32_t message) {
 void ibutton_notification_message(iButton* ibutton, uint32_t message) {
@@ -343,7 +343,7 @@ int32_t ibutton_app(void* p) {
     } else {
     } else {
         view_dispatcher_attach_to_gui(
         view_dispatcher_attach_to_gui(
             ibutton->view_dispatcher, ibutton->gui, ViewDispatcherTypeFullscreen);
             ibutton->view_dispatcher, ibutton->gui, ViewDispatcherTypeFullscreen);
-        if(key_loaded) {
+        if(key_loaded) { //-V547
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate);
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate);
             DOLPHIN_DEED(DolphinDeedIbuttonEmulate);
             DOLPHIN_DEED(DolphinDeedIbuttonEmulate);
         } else {
         } else {

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

@@ -271,7 +271,7 @@ void onewire_cli_print_usage() {
 
 
 static void onewire_cli_search(Cli* cli) {
 static void onewire_cli_search(Cli* cli) {
     UNUSED(cli);
     UNUSED(cli);
-    OneWireHost* onewire = onewire_host_alloc();
+    OneWireHost* onewire = onewire_host_alloc(&ibutton_gpio);
     uint8_t address[8];
     uint8_t address[8];
     bool done = false;
     bool done = false;
 
 

+ 1 - 0
applications/main/infrared/application.fam

@@ -3,6 +3,7 @@ App(
     name="Infrared",
     name="Infrared",
     apptype=FlipperAppType.APP,
     apptype=FlipperAppType.APP,
     entry_point="infrared_app",
     entry_point="infrared_app",
+    targets=["f7"],
     cdefines=["APP_INFRARED"],
     cdefines=["APP_INFRARED"],
     requires=[
     requires=[
         "gui",
         "gui",

+ 2 - 2
applications/main/infrared/infrared.c

@@ -360,7 +360,7 @@ void infrared_text_store_set(Infrared* infrared, uint32_t bank, const char* text
 }
 }
 
 
 void infrared_text_store_clear(Infrared* infrared, uint32_t bank) {
 void infrared_text_store_clear(Infrared* infrared, uint32_t bank) {
-    memset(infrared->text_store[bank], 0, INFRARED_TEXT_STORE_SIZE);
+    memset(infrared->text_store[bank], 0, INFRARED_TEXT_STORE_SIZE + 1);
 }
 }
 
 
 void infrared_play_notification_message(Infrared* infrared, uint32_t message) {
 void infrared_play_notification_message(Infrared* infrared, uint32_t message) {
@@ -455,7 +455,7 @@ int32_t infrared_app(void* p) {
     } else {
     } else {
         view_dispatcher_attach_to_gui(
         view_dispatcher_attach_to_gui(
             infrared->view_dispatcher, infrared->gui, ViewDispatcherTypeFullscreen);
             infrared->view_dispatcher, infrared->gui, ViewDispatcherTypeFullscreen);
-        if(is_remote_loaded) {
+        if(is_remote_loaded) { //-V547
             scene_manager_next_scene(infrared->scene_manager, InfraredSceneRemote);
             scene_manager_next_scene(infrared->scene_manager, InfraredSceneRemote);
         } else {
         } else {
             scene_manager_next_scene(infrared->scene_manager, InfraredSceneStart);
             scene_manager_next_scene(infrared->scene_manager, InfraredSceneStart);

+ 1 - 1
applications/main/infrared/infrared_brute_force.c

@@ -65,7 +65,7 @@ bool infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force) {
         while(flipper_format_read_string(ff, "name", signal_name)) {
         while(flipper_format_read_string(ff, "name", signal_name)) {
             InfraredBruteForceRecord* record =
             InfraredBruteForceRecord* record =
                 InfraredBruteForceRecordDict_get(brute_force->records, signal_name);
                 InfraredBruteForceRecordDict_get(brute_force->records, signal_name);
-            if(record) {
+            if(record) { //-V547
                 ++(record->count);
                 ++(record->count);
             }
             }
         }
         }

+ 7 - 5
applications/main/infrared/infrared_cli.c

@@ -55,7 +55,7 @@ static void signal_received_callback(void* context, InfraredWorkerSignal* receiv
         size_t timings_cnt;
         size_t timings_cnt;
         infrared_worker_get_raw_signal(received_signal, &timings, &timings_cnt);
         infrared_worker_get_raw_signal(received_signal, &timings, &timings_cnt);
 
 
-        buf_cnt = snprintf(buf, sizeof(buf), "RAW, %d samples:\r\n", timings_cnt);
+        buf_cnt = snprintf(buf, sizeof(buf), "RAW, %zu samples:\r\n", timings_cnt);
         cli_write(cli, (uint8_t*)buf, buf_cnt);
         cli_write(cli, (uint8_t*)buf, buf_cnt);
         for(size_t i = 0; i < timings_cnt; ++i) {
         for(size_t i = 0; i < timings_cnt; ++i) {
             buf_cnt = snprintf(buf, sizeof(buf), "%lu ", timings[i]);
             buf_cnt = snprintf(buf, sizeof(buf), "%lu ", timings[i]);
@@ -86,7 +86,7 @@ static void infrared_cli_print_usage(void) {
     printf("\tir universal <remote_name> <signal_name>\r\n");
     printf("\tir universal <remote_name> <signal_name>\r\n");
     printf("\tir universal list <remote_name>\r\n");
     printf("\tir universal list <remote_name>\r\n");
     // TODO: Do not hardcode universal remote names
     // TODO: Do not hardcode universal remote names
-    printf("\tAvailable universal remotes: tv audio ac\r\n");
+    printf("\tAvailable universal remotes: tv audio ac projector\r\n");
 }
 }
 
 
 static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) {
 static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) {
@@ -276,7 +276,9 @@ static bool infrared_cli_decode_file(FlipperFormat* input_file, FlipperFormat* o
         }
         }
         InfraredRawSignal* raw_signal = infrared_signal_get_raw_signal(signal);
         InfraredRawSignal* raw_signal = infrared_signal_get_raw_signal(signal);
         printf(
         printf(
-            "Raw signal: %s, %u samples\r\n", furi_string_get_cstr(tmp), raw_signal->timings_size);
+            "Raw signal: %s, %zu samples\r\n",
+            furi_string_get_cstr(tmp),
+            raw_signal->timings_size);
         if(!infrared_cli_decode_raw_signal(
         if(!infrared_cli_decode_raw_signal(
                raw_signal, decoder, output_file, furi_string_get_cstr(tmp)))
                raw_signal, decoder, output_file, furi_string_get_cstr(tmp)))
             break;
             break;
@@ -382,7 +384,7 @@ static void infrared_cli_list_remote_signals(FuriString* remote_name) {
         while(flipper_format_read_string(ff, "name", signal_name)) {
         while(flipper_format_read_string(ff, "name", signal_name)) {
             furi_string_set_str(key, furi_string_get_cstr(signal_name));
             furi_string_set_str(key, furi_string_get_cstr(signal_name));
             int* v = dict_signals_get(signals_dict, key);
             int* v = dict_signals_get(signals_dict, key);
-            if(v != NULL) {
+            if(v != NULL) { //-V547
                 (*v)++;
                 (*v)++;
                 max = M_MAX(*v, max);
                 max = M_MAX(*v, max);
             } else {
             } else {
@@ -436,7 +438,7 @@ static void
             break;
             break;
         }
         }
 
 
-        printf("Sending %ld signal(s)...\r\n", record_count);
+        printf("Sending %lu signal(s)...\r\n", record_count);
         printf("Press Ctrl-C to stop.\r\n");
         printf("Press Ctrl-C to stop.\r\n");
 
 
         int records_sent = 0;
         int records_sent = 0;

+ 7 - 7
applications/main/infrared/infrared_remote.c

@@ -145,15 +145,14 @@ bool infrared_remote_load(InfraredRemote* remote, FuriString* path) {
     buf = furi_string_alloc();
     buf = furi_string_alloc();
 
 
     FURI_LOG_I(TAG, "load file: \'%s\'", furi_string_get_cstr(path));
     FURI_LOG_I(TAG, "load file: \'%s\'", furi_string_get_cstr(path));
-    bool success = flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path));
+    bool success = false;
 
 
-    if(success) {
+    do {
+        if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path))) break;
         uint32_t version;
         uint32_t version;
-        success = flipper_format_read_header(ff, buf, &version) &&
-                  !furi_string_cmp(buf, "IR signals file") && (version == 1);
-    }
+        if(!flipper_format_read_header(ff, buf, &version)) break;
+        if(!furi_string_equal(buf, "IR signals file") || (version != 1)) break;
 
 
-    if(success) {
         path_extract_filename(path, buf, true);
         path_extract_filename(path, buf, true);
         infrared_remote_clear_buttons(remote);
         infrared_remote_clear_buttons(remote);
         infrared_remote_set_name(remote, furi_string_get_cstr(buf));
         infrared_remote_set_name(remote, furi_string_get_cstr(buf));
@@ -169,7 +168,8 @@ bool infrared_remote_load(InfraredRemote* remote, FuriString* path) {
                 infrared_remote_button_free(button);
                 infrared_remote_button_free(button);
             }
             }
         }
         }
-    }
+        success = true;
+    } while(false);
 
 
     furi_string_free(buf);
     furi_string_free(buf);
     flipper_format_free(ff);
     flipper_format_free(ff);

+ 3 - 3
applications/main/infrared/infrared_signal.c

@@ -74,7 +74,7 @@ static bool infrared_signal_is_raw_valid(InfraredRawSignal* raw) {
     } else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) {
     } else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) {
         FURI_LOG_E(
         FURI_LOG_E(
             TAG,
             TAG,
-            "Timings amount is out of range (0 - %X): %X",
+            "Timings amount is out of range (0 - %X): %zX",
             MAX_TIMINGS_AMOUNT,
             MAX_TIMINGS_AMOUNT,
             raw->timings_size);
             raw->timings_size);
         return false;
         return false;
@@ -275,8 +275,8 @@ bool infrared_signal_search_and_read(
             is_name_found = furi_string_equal(name, tmp);
             is_name_found = furi_string_equal(name, tmp);
             if(is_name_found) break;
             if(is_name_found) break;
         }
         }
-        if(!is_name_found) break;
-        if(!infrared_signal_read_body(signal, ff)) break;
+        if(!is_name_found) break; //-V547
+        if(!infrared_signal_read_body(signal, ff)) break; //-V779
         success = true;
         success = true;
     } while(false);
     } while(false);
 
 

+ 1 - 0
applications/main/infrared/scenes/infrared_scene_config.h

@@ -17,6 +17,7 @@ ADD_SCENE(infrared, universal, Universal)
 ADD_SCENE(infrared, universal_tv, UniversalTV)
 ADD_SCENE(infrared, universal_tv, UniversalTV)
 ADD_SCENE(infrared, universal_ac, UniversalAC)
 ADD_SCENE(infrared, universal_ac, UniversalAC)
 ADD_SCENE(infrared, universal_audio, UniversalAudio)
 ADD_SCENE(infrared, universal_audio, UniversalAudio)
+ADD_SCENE(infrared, universal_projector, UniversalProjector)
 ADD_SCENE(infrared, debug, Debug)
 ADD_SCENE(infrared, debug, Debug)
 ADD_SCENE(infrared, error_databases, ErrorDatabases)
 ADD_SCENE(infrared, error_databases, ErrorDatabases)
 ADD_SCENE(infrared, rpc, Rpc)
 ADD_SCENE(infrared, rpc, Rpc)

+ 1 - 1
applications/main/infrared/scenes/infrared_scene_debug.c

@@ -26,7 +26,7 @@ bool infrared_scene_debug_on_event(void* context, SceneManagerEvent event) {
                 InfraredRawSignal* raw = infrared_signal_get_raw_signal(signal);
                 InfraredRawSignal* raw = infrared_signal_get_raw_signal(signal);
                 infrared_debug_view_set_text(debug_view, "RAW\n%d samples\n", raw->timings_size);
                 infrared_debug_view_set_text(debug_view, "RAW\n%d samples\n", raw->timings_size);
 
 
-                printf("RAW, %d samples:\r\n", raw->timings_size);
+                printf("RAW, %zu samples:\r\n", raw->timings_size);
                 for(size_t i = 0; i < raw->timings_size; ++i) {
                 for(size_t i = 0; i < raw->timings_size; ++i) {
                     printf("%lu ", raw->timings[i]);
                     printf("%lu ", raw->timings[i]);
                 }
                 }

+ 1 - 1
applications/main/infrared/scenes/infrared_scene_rpc.c

@@ -1,5 +1,5 @@
 #include "../infrared_i.h"
 #include "../infrared_i.h"
-#include "gui/canvas.h"
+#include <gui/canvas.h>
 
 
 typedef enum {
 typedef enum {
     InfraredRpcStateIdle,
     InfraredRpcStateIdle,

+ 14 - 2
applications/main/infrared/scenes/infrared_scene_universal.c

@@ -4,6 +4,7 @@ typedef enum {
     SubmenuIndexUniversalTV,
     SubmenuIndexUniversalTV,
     SubmenuIndexUniversalAC,
     SubmenuIndexUniversalAC,
     SubmenuIndexUniversalAudio,
     SubmenuIndexUniversalAudio,
+    SubmenuIndexUniversalProjector,
 } SubmenuIndex;
 } SubmenuIndex;
 
 
 static void infrared_scene_universal_submenu_callback(void* context, uint32_t index) {
 static void infrared_scene_universal_submenu_callback(void* context, uint32_t index) {
@@ -27,13 +28,20 @@ void infrared_scene_universal_on_enter(void* context) {
         SubmenuIndexUniversalAudio,
         SubmenuIndexUniversalAudio,
         infrared_scene_universal_submenu_callback,
         infrared_scene_universal_submenu_callback,
         context);
         context);
+    submenu_add_item(
+        submenu,
+        "Projectors",
+        SubmenuIndexUniversalProjector,
+        infrared_scene_universal_submenu_callback,
+        context);
     submenu_add_item(
     submenu_add_item(
         submenu,
         submenu,
         "Air Conditioners",
         "Air Conditioners",
         SubmenuIndexUniversalAC,
         SubmenuIndexUniversalAC,
         infrared_scene_universal_submenu_callback,
         infrared_scene_universal_submenu_callback,
         context);
         context);
-    submenu_set_selected_item(submenu, 0);
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(infrared->scene_manager, InfraredSceneUniversal));
 
 
     view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewSubmenu);
     view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewSubmenu);
 }
 }
@@ -53,7 +61,11 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) {
         } else if(event.event == SubmenuIndexUniversalAudio) {
         } else if(event.event == SubmenuIndexUniversalAudio) {
             scene_manager_next_scene(scene_manager, InfraredSceneUniversalAudio);
             scene_manager_next_scene(scene_manager, InfraredSceneUniversalAudio);
             consumed = true;
             consumed = true;
+        } else if(event.event == SubmenuIndexUniversalProjector) {
+            scene_manager_next_scene(scene_manager, InfraredSceneUniversalProjector);
+            consumed = true;
         }
         }
+        scene_manager_set_scene_state(scene_manager, InfraredSceneUniversal, event.event);
     }
     }
 
 
     return consumed;
     return consumed;
@@ -62,4 +74,4 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) {
 void infrared_scene_universal_on_exit(void* context) {
 void infrared_scene_universal_on_exit(void* context) {
     Infrared* infrared = context;
     Infrared* infrared = context;
     submenu_reset(infrared->submenu);
     submenu_reset(infrared->submenu);
-}
+}

+ 86 - 0
applications/main/infrared/scenes/infrared_scene_universal_projector.c

@@ -0,0 +1,86 @@
+#include "../infrared_i.h"
+
+#include "common/infrared_scene_universal_common.h"
+
+void infrared_scene_universal_projector_on_enter(void* context) {
+    infrared_scene_universal_common_on_enter(context);
+
+    Infrared* infrared = context;
+    ButtonPanel* button_panel = infrared->button_panel;
+    InfraredBruteForce* brute_force = infrared->brute_force;
+
+    infrared_brute_force_set_db_filename(brute_force, EXT_PATH("infrared/assets/projector.ir"));
+
+    button_panel_reserve(button_panel, 2, 2);
+    uint32_t i = 0;
+    button_panel_add_item(
+        button_panel,
+        i,
+        0,
+        0,
+        3,
+        19,
+        &I_Power_25x27,
+        &I_Power_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Power");
+    button_panel_add_item(
+        button_panel,
+        i,
+        1,
+        0,
+        36,
+        19,
+        &I_Mute_25x27,
+        &I_Mute_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Mute");
+    button_panel_add_item(
+        button_panel,
+        i,
+        0,
+        1,
+        3,
+        66,
+        &I_Vol_up_25x27,
+        &I_Vol_up_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Vol_up");
+    button_panel_add_item(
+        button_panel,
+        i,
+        1,
+        1,
+        36,
+        66,
+        &I_Vol_down_25x27,
+        &I_Vol_down_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Vol_dn");
+
+    button_panel_add_label(button_panel, 2, 11, FontPrimary, "Proj. remote");
+    button_panel_add_label(button_panel, 17, 62, FontSecondary, "Volume");
+
+    view_set_orientation(view_stack_get_view(infrared->view_stack), ViewOrientationVertical);
+    view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewStack);
+
+    infrared_show_loading_popup(infrared, true);
+    bool success = infrared_brute_force_calculate_messages(brute_force);
+    infrared_show_loading_popup(infrared, false);
+
+    if(!success) {
+        scene_manager_next_scene(infrared->scene_manager, InfraredSceneErrorDatabases);
+    }
+}
+
+bool infrared_scene_universal_projector_on_event(void* context, SceneManagerEvent event) {
+    return infrared_scene_universal_common_on_event(context, event);
+}
+
+void infrared_scene_universal_projector_on_exit(void* context) {
+    infrared_scene_universal_common_on_exit(context);
+}

+ 3 - 3
applications/main/infrared/views/infrared_debug_view.c

@@ -1,11 +1,11 @@
 #include "infrared_debug_view.h"
 #include "infrared_debug_view.h"
 
 
-#include <stdlib.h>
-#include <string.h>
-
 #include <gui/canvas.h>
 #include <gui/canvas.h>
 #include <gui/elements.h>
 #include <gui/elements.h>
 
 
+#include <stdlib.h>
+#include <string.h>
+
 #define INFRARED_DEBUG_TEXT_LENGTH 64
 #define INFRARED_DEBUG_TEXT_LENGTH 64
 
 
 struct InfraredDebugView {
 struct InfraredDebugView {

+ 10 - 8
applications/main/infrared/views/infrared_progress_view.c

@@ -1,13 +1,15 @@
-#include <core/check.h>
-#include "furi_hal_resources.h"
-#include "assets_icons.h"
-#include "gui/canvas.h"
-#include "gui/view.h"
-#include "input/input.h"
+#include "infrared_progress_view.h"
+
+#include <assets_icons.h>
+#include <gui/canvas.h>
+#include <gui/view.h>
 #include <gui/elements.h>
 #include <gui/elements.h>
+#include <gui/modules/button_panel.h>
+#include <input/input.h>
+
 #include <furi.h>
 #include <furi.h>
-#include "infrared_progress_view.h"
-#include "gui/modules/button_panel.h"
+#include <furi_hal_resources.h>
+#include <core/check.h>
 #include <stdint.h>
 #include <stdint.h>
 
 
 struct InfraredProgressView {
 struct InfraredProgressView {

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