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

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/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov
+
 # Assets
 /assets/resources/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
 

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

@@ -14,7 +14,7 @@ body:
     description: |
       Please describe your feature request in as many details as possible.
         - 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:
     required: true
 - 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
           ref: ${{ github.event.pull_request.head.sha }}
 
-      - name: 'Make artifacts directory'
-        run: |
-          rm -rf artifacts
-          mkdir artifacts
-
       - name: 'Get commit details'
         id: names
         run: |
@@ -46,6 +41,15 @@ jobs:
             TYPE="other"
           fi
           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'
         if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -56,8 +60,9 @@ jobs:
         run: |
           set -e
           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
 
       - name: 'Move upload files'
@@ -82,9 +87,32 @@ jobs:
         run: |
           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: |
-          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'
         if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -158,6 +186,6 @@ jobs:
         run: |
           set -e
           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

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

@@ -43,43 +43,31 @@ jobs:
           fi
           python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 
-      - name: 'Make reports directory'
+      - name: 'Supply PVS credentials'
         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 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: |
-          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'
-        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
         with:
           issue-number: ${{ github.event.pull_request.number }}
@@ -87,12 +75,19 @@ jobs:
           body-includes: 'PVS-Studio report for commit'
 
       - 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
         with:
           comment-id: ${{ steps.fc.outputs.comment-id }}
           issue-number: ${{ github.event.pull_request.number }}
           body: |
             **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
+
+      - 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
 
 jobs:
-  run_units_on_test_bench:
+  run_units_on_bench:
     runs-on: [self-hosted, FlipperZeroTest]
     steps:
       - name: 'Decontaminate previous build leftovers'
@@ -29,81 +29,39 @@ jobs:
         run: |
           echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT
 
-      - name: 'Flashing target firmware'
-        id: first_full_flash
-        run: |
-          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
-          source scripts/toolchain/fbtenv.sh
-          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
-
-      - name: 'Validating updater'
-        id: second_full_flash
-        if: success()
-        run: |
-          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
-          source scripts/toolchain/fbtenv.sh
-          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
-
       - name: 'Flash unit tests firmware'
         id: flashing
         if: success()
-        run: |
+        run: |                               
           ./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'
         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
 
-      - name: 'Copy assets and unit tests data to flipper'
+      - name: 'Copy assets and unit data, reboot and wait for flipper'
         id: copy
-        if: steps.connect.outcome == 'success'
+        if: steps.format_ext.outcome == 'success'
         run: |
           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/power.py -p ${{steps.device.outputs.flipper}} reboot
+          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 
       - name: 'Run units and validate results'
+        id: run_units
         if: steps.copy.outcome == 'success'
+        timeout-minutes: 2.5
         run: |
           source scripts/toolchain/fbtenv.sh
           python3 scripts/testing/units.py ${{steps.device.outputs.flipper}}
 
-      - name: 'Get last release tag'
-        id: release_tag
-        if: always()
-        run: |
-          echo "tag=$(git tag -l --sort=-version:refname | grep -v "rc\|RC" | head -1)" >> $GITHUB_OUTPUT
-
-      - name: 'Decontaminate previous build leftovers'
-        if: always()
+      - name: 'Check GDB output'
+        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: 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
+*.swo
 *.gdb_history
 
 
@@ -30,6 +31,10 @@ Brewfile.lock.json
 # Visual Studio Code
 .vscode/
 
+# Kate
+.kateproject
+.kateconfig
+
 # legendary cmake's
 build
 CMakeLists.txt

+ 23 - 0
.pvsconfig

@@ -1,4 +1,5 @@
 # MLib macros we can't do much about.
+//-V:M_LET:1048,1044
 //-V:M_EACH:1048,1044
 //-V:ARRAY_DEF:760,747,568,776,729,712,654
 //-V:LIST_DEF:760,747,568,712,729,654,776
@@ -16,8 +17,30 @@
 # Potentially null argument warnings
 //-V:memset:575
 //-V:memcpy:575
+//-V:memcmp:575
+//-V:strlen:575
 //-V:strcpy:575
+//-V:strncpy:575
 //-V:strchr:575
 
 # For loop warning on M_FOREACH
 //-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",
             "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",
             "group": "build",
@@ -138,6 +144,18 @@
                 "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
             "label": "Serial Console",
@@ -145,7 +163,7 @@
             "command": "./fbt cli",
             "group": "none",
             "isBackground": true,
-			"options": {
+            "options": {
                 "env": {
                     "FBT_NO_SYNC": "0"
                 }
@@ -162,4 +180,4 @@
             }
         }
     ]
-}
+}

+ 5 - 2
.vscode/extensions.json

@@ -11,5 +11,8 @@
 		"augustocdias.tasks-shell-input"
 	],
 	// 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.
 
 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.
 
-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.
 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.
 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.
 Use project wiki to document new/reverse engineered standards.
 
@@ -52,7 +52,7 @@ Almost everything in flipper firmware is built around this concept.
 
 ## Naming
 
-### Type names are CamelCase
+### Type names are PascalCase
 
 Examples:
 
@@ -89,7 +89,7 @@ Enforced by linter.
 Suffixes:
 
 - `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
 

+ 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.
 - Your PR must comply with our [Coding Style](CODING_STYLE.md)
 - 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.
 
@@ -59,7 +59,7 @@ Commit the changes once you are happy with them. Make sure that code compilation
 ### Pull Request
 
 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.
 - 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.

+ 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()
         ),
     ),
-    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(
@@ -165,6 +168,14 @@ Alias("fap_dist", fap_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
 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
 firmware_debug = distenv.PhonyTarget(
     "debug",

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

@@ -31,9 +31,10 @@ void AccessorApp::run(void) {
     onewire_host_stop(onewire_host);
 }
 
-AccessorApp::AccessorApp() {
+AccessorApp::AccessorApp()
+    : text_store{0} {
     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();
 }
 

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

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

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

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

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

@@ -11,4 +11,5 @@ App(
     stack_size=1 * 1024,
     order=130,
     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* app = malloc(sizeof(BtDebugApp));
 
-    // Load settings
-    bt_settings_load(&app->settings);
-
     // 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();
+    // Was bt active?
+    const bool was_active = furi_hal_bt_is_active();
     // Stop advertising
     furi_hal_bt_stop_advertising();
 
     view_dispatcher_run(app->view_dispatcher);
 
     // Restart advertising
-    if(app->settings.enabled) {
+    if(was_active) {
         furi_hal_bt_start_advertising();
     }
     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/view.h>
 #include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+
 #include <dialogs/dialogs.h>
 
-#include <gui/modules/submenu.h>
 #include "views/bt_carrier_test.h"
 #include "views/bt_packet_test.h"
-#include <bt/bt_settings.h>
 
 typedef struct {
-    BtSettings settings;
     Gui* gui;
     ViewDispatcher* view_dispatcher;
     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_test.h"
 #include "bt_test_types.h"
-#include "furi_hal_bt.h"
+#include <furi_hal_bt.h>
 
 struct BtCarrierTest {
     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_test.h"
 #include "bt_test_types.h"
-#include "furi_hal_bt.h"
+#include <furi_hal_bt.h>
 
 struct BtPacketTest {
     BtTest* bt_test;

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

@@ -2,8 +2,11 @@
 
 #include <gui/canvas.h>
 #include <gui/elements.h>
+
+#include <lib/toolbox/float_tools.h>
 #include <m-array.h>
 #include <furi.h>
+#include <inttypes.h>
 #include <stdint.h>
 
 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));
     canvas_draw_str(canvas, 6, 60, model->message);
     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);
             canvas_draw_str_aligned(canvas, 124, 60, AlignRight, AlignBottom, info_str);
         }
     } else if(model->state == BtTestStateStopped) {
         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);
         } 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);
         }
     }
@@ -153,7 +156,7 @@ static bool bt_test_input_callback(InputEvent* event, void* context) {
 }
 
 void bt_test_process_up(BtTest* bt_test) {
-    with_view_model(
+    with_view_model( // -V658
         bt_test->view,
         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",
     cdefines=["APP_DISPLAY_TEST"],
     requires=["gui"],
+    fap_libs=["misc"],
     stack_size=1 * 1024,
     order=120,
     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_regulation_ratio,
         instance->config_bias);
-    gui_update(instance->gui);
 }
 
 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 "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 <lib/toolbox/path.h>
+#include <furi.h>
+#include <furi_hal.h>
 
 static bool file_browser_app_custom_event_callback(void* context, uint32_t event) {
     furi_assert(context);
@@ -48,7 +49,7 @@ FileBrowserApp* file_browser_app_alloc(char* arg) {
 
     app->file_path = furi_string_alloc();
     app->file_browser = file_browser_alloc(app->file_path);
-    file_browser_configure(app->file_browser, "*", NULL, true, &I_badusb_10px, true);
+    file_browser_configure(app->file_browser, "*", NULL, true, false, &I_badusb_10px, true);
 
     view_dispatcher_add_view(
         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",
     name="LF-RFID Debug",
     apptype=FlipperAppType.DEBUG,
+    targets=["f7"],
     entry_point="lfrfid_debug_app",
     requires=[
         "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.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);
             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 <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() {
-    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
-    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);
-    ptr = NULL;
-    heap_size = memmgr_get_free_heap();
-    mu_assert(heap_equal(heap_size, heap_size_old + alloc_size), "free failed");
 
     // 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);
 
     // 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);
 }

+ 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);
 
     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};
     memcpy(manufacturer_block, nfc_dev->dev_data.mf_classic_data.block[0].value, 16);
     mu_assert(
         memcmp(manufacturer_block, uid, uid_len) == 0,
         "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(
             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);
 }
 
+MU_TEST(mf_mini_file_test) {
+    mf_classic_generator_test(4, MfClassicTypeMini);
+}
+
 MU_TEST(mf_classic_1k_4b_file_test) {
     mf_classic_generator_test(4, MfClassicType1k);
 }
@@ -486,6 +514,7 @@ MU_TEST_SUITE(nfc) {
     nfc_test_alloc();
 
     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_4k_4b_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);
 
-    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[0].close_session_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_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"
+    mu_check(stream_seek(stream, 4, StreamOffsetFromStart));
     mu_check(stream_write_string(stream, string_lee) == 3);
     mu_check(stream_size(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 CAME_ATOMO_DIR_NAME EXT_PATH("subghz/assets/came_atomo")
 #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_COUNT_PARSE 253
+#define TEST_RANDOM_COUNT_PARSE 329
 #define TEST_TIMEOUT 10000
 
 static SubGhzEnvironment* environment_handler;
@@ -43,6 +44,8 @@ static void subghz_test_init(void) {
         environment_handler, CAME_ATOMO_DIR_NAME);
     subghz_environment_set_nice_flor_s_rainbow_table_file_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(
         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_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()) {
         furi_delay_ms(10);
     }
@@ -486,6 +492,14 @@ MU_TEST(subghz_decoder_linear_test) {
         "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_assert(
         subghz_decoder_test(
@@ -594,6 +608,43 @@ MU_TEST(subghz_decoder_smc5326_test) {
         "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
 MU_TEST(subghz_encoder_princeton_test) {
     mu_assert(
@@ -637,6 +688,12 @@ MU_TEST(subghz_encoder_linear_test) {
         "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_assert(
         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");
 }
 
+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_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_star_line_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_secplus_v1_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_ansonic_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_came_test);
@@ -779,6 +854,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_encoder_nice_flo_test);
     MU_RUN_TEST(subghz_encoder_keelog_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_holtek_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_ansonic_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);
     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_infrared();
 int run_minunit_test_rpc();
+int run_minunit_test_manifest();
 int run_minunit_test_flipper_format();
 int run_minunit_test_flipper_format_string();
 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_nfc();
 int run_minunit_test_bit_lib();
+int run_minunit_test_float_tools();
 int run_minunit_test_bt();
 
 typedef int (*UnitTestEntry)();
@@ -40,6 +42,7 @@ const UnitTest unit_tests[] = {
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "stream", .entry = run_minunit_test_stream},
     {.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_string", .entry = run_minunit_test_flipper_format_string},
     {.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 = "lfrfid", .entry = run_minunit_test_lfrfid_protocols},
     {.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},
 };
 
@@ -66,14 +70,13 @@ void minunit_print_progress() {
 }
 
 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) {
     UNUSED(cli);
     UNUSED(args);
     UNUSED(context);
-    uint32_t failed_tests = 0;
     minunit_run = 0;
     minunit_assert = 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_cmp_str(args, unit_tests[i].name) == 0) {
-                    failed_tests += unit_tests[i].entry();
+                    unit_tests[i].entry();
                 } else {
                     printf("Skipping %s\r\n", unit_tests[i].name);
                 }
             } 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(
-    appid="sample_apps",
-    name="Sample apps bundle",
+    appid="example_apps",
+    name="Example apps bundle",
     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++;
 
-    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) {
             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_apps.h"
 #include "archive_browser.h"
+#include "../views/archive_browser_view.h"
+
 #include <core/common_defines.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 <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);
 
-    if(need_refresh) {
+    if(need_refresh) { //-V547
         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: {
             const char* name = archive_get_name(browser);
             if(favorites) {
-                archive_favorites_delete(name);
+                archive_favorites_delete("%s", name);
                 archive_file_array_rm_selected(browser);
                 archive_show_file_menu(browser, false);
             } else if(archive_is_known_app(selected->type)) {

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

@@ -1,14 +1,15 @@
 #pragma once
 
+#include "../helpers/archive_files.h"
+#include "../helpers/archive_favorites.h"
+
 #include <gui/gui_i.h>
 #include <gui/view.h>
 #include <gui/canvas.h>
 #include <gui/elements.h>
-#include <furi.h>
+#include <gui/modules/file_browser_worker.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_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_settings_filename.h"
 #include <furi.h>
 #include <furi_hal.h>
 #include <storage/storage.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) {
     furi_assert(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);
 }
 
+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* 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)) {
         furi_string_set(app->file_path, arg);
     }
 
+    bad_usb_load_settings(app);
+
     app->gui = furi_record_open(RECORD_GUI);
     app->notifications = furi_record_open(RECORD_NOTIFICATION);
     app->dialogs = furi_record_open(RECORD_DIALOGS);
@@ -53,6 +103,10 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
     view_dispatcher_add_view(
         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();
     view_dispatcher_add_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()) {
         app->error = BadUsbAppErrorCloseRpc;
+        app->usb_if_prev = NULL;
         scene_manager_next_scene(app->scene_manager, BadUsbSceneError);
     } 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)) {
+            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);
         } 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);
         }
     }
@@ -77,6 +137,15 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
 void bad_usb_app_free(BadUsbApp* 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
     view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewWork);
     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);
     widget_free(app->widget);
 
+    // Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewConfig);
+    submenu_free(app->submenu);
+
     // View dispatcher
     view_dispatcher_free(app->view_dispatcher);
     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_DIALOGS);
 
+    bad_usb_save_settings(app);
+
     furi_string_free(app->file_path);
+    furi_string_free(app->keyboard_layout);
 
     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/widget.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 {
     BadUsbAppErrorNoFiles,
@@ -30,14 +33,19 @@ struct BadUsbApp {
     NotificationApp* notifications;
     DialogsApp* dialogs;
     Widget* widget;
+    Submenu* submenu;
 
     BadUsbAppError error;
     FuriString* file_path;
+    FuriString* keyboard_layout;
     BadUsb* bad_usb_view;
     BadUsbScript* bad_usb_script;
+
+    FuriHalUsbInterface* usb_if_prev;
 };
 
 typedef enum {
     BadUsbAppViewError,
     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_NEXT_LINE (-3)
 
+#define BADUSB_ASCII_TO_KEY(script, x) \
+    (((uint8_t)x < 128) ? (script->layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
+
 typedef enum {
     WorkerEvtToggle = (1 << 0),
     WorkerEvtEnd = (1 << 1),
@@ -28,6 +31,7 @@ struct BadUsbScript {
     BadUsbState st;
     FuriString* file_path;
     uint32_t defdelay;
+    uint16_t layout[128];
     FuriThread* thread;
     uint8_t file_buf[FILE_BUFFER_LEN + 1];
     uint8_t buf_start;
@@ -50,6 +54,7 @@ static const DuckyKey ducky_keys[] = {
     {"ALT-SHIFT", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT},
     {"ALT-GUI", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_GUI},
     {"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},
     {"CONTROL", KEY_MOD_LEFT_CTRL},
@@ -71,8 +76,8 @@ static const DuckyKey ducky_keys[] = {
     {"BREAK", HID_KEYBOARD_PAUSE},
     {"PAUSE", HID_KEYBOARD_PAUSE},
     {"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},
     {"ESC", HID_KEYBOARD_ESCAPE},
     {"ESCAPE", HID_KEYBOARD_ESCAPE},
@@ -204,10 +209,10 @@ static bool ducky_altstring(const char* param) {
     return state;
 }
 
-static bool ducky_string(const char* param) {
+static bool ducky_string(BadUsbScript* bad_usb, const char* param) {
     uint32_t 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) {
             furi_hal_hid_kb_press(keycode);
             furi_hal_hid_kb_release(keycode);
@@ -217,16 +222,16 @@ static bool ducky_string(const char* param) {
     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) &&
            (ducky_is_line_end(param[key_cmd_len]))) {
             return ducky_keys[i].keycode;
         }
     }
     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;
 }
@@ -275,7 +280,7 @@ static int32_t
     } else if(strncmp(line_tmp, ducky_cmd_string, strlen(ducky_cmd_string)) == 0) {
         // STRING
         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) {
             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) {
         // SYSRQ
         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);
         furi_hal_hid_kb_release_all();
         return (0);
     } else {
         // 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(error != NULL) {
                 snprintf(error, error_len, "No keycode defined for %s", line_tmp);
@@ -328,7 +333,7 @@ static int32_t
         if((key & 0xFF00) != 0) {
             // It's a modifier key
             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_release(key);
@@ -417,7 +422,7 @@ static int32_t ducky_script_execute_next(BadUsbScript* bad_usb, File* script_fil
             return 0;
         } else if(delay_val < 0) { // Script error
             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;
         } else {
             return (delay_val + bad_usb->defdelay);
@@ -485,8 +490,6 @@ static int32_t bad_usb_worker(void* context) {
     BadUsbWorkerState worker_state = BadUsbStateInit;
     int32_t delay_val = 0;
 
-    FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config();
-
     FURI_LOG_I(WORKER_TAG, "Init");
     File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
     bad_usb->line = furi_string_alloc();
@@ -596,7 +599,9 @@ static int32_t bad_usb_worker(void* context) {
                 }
                 bad_usb->st.state = worker_state;
                 continue;
-            } else if((flags == FuriFlagErrorTimeout) || (flags == FuriFlagErrorResource)) {
+            } else if(
+                (flags == (unsigned)FuriFlagErrorTimeout) ||
+                (flags == (unsigned)FuriFlagErrorResource)) {
                 if(delay_val > 0) {
                     bad_usb->st.delay_remain--;
                     continue;
@@ -635,8 +640,6 @@ static int32_t bad_usb_worker(void* context) {
 
     furi_hal_hid_set_state_callback(NULL, NULL);
 
-    furi_hal_usb_set_config(usb_mode_prev, NULL);
-
     storage_file_close(script_file);
     storage_file_free(script_file);
     furi_string_free(bad_usb->line);
@@ -647,12 +650,19 @@ static int32_t bad_usb_worker(void* context) {
     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) {
     furi_assert(file_path);
 
-    BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript)); //-V773
+    BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript));
     bad_usb->file_path = furi_string_alloc();
     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.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);
     furi_thread_start(bad_usb->thread);
     return bad_usb;
-}
+} //-V773
 
 void bad_usb_script_close(BadUsbScript* bad_usb) {
     furi_assert(bad_usb);
@@ -671,6 +681,30 @@ void bad_usb_script_close(BadUsbScript* 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) {
     furi_assert(bad_usb);
     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_set_keyboard_layout(BadUsbScript* bad_usb, FuriString* layout_path);
+
 void bad_usb_script_start(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, work, Work)
 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 "furi_hal_power.h"
-#include "furi_hal_usb.h"
+#include <furi_hal_power.h>
+#include <furi_hal_usb.h>
 #include <storage/storage.h>
 
 static bool bad_usb_file_select(BadUsbApp* bad_usb) {
     furi_assert(bad_usb);
 
     DialogsFileBrowserOptions browser_options;
-    dialog_file_browser_set_basic_options(&browser_options, BAD_USB_APP_EXTENSION, &I_badusb_10px);
-    browser_options.base_path = BAD_USB_APP_PATH_FOLDER;
+    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
     bool res = dialog_file_browser_show(
@@ -21,12 +23,18 @@ void bad_usb_scene_file_select_on_enter(void* context) {
     BadUsbApp* bad_usb = context;
 
     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)) {
+        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);
     } else {
         furi_hal_usb_enable();
-        //scene_manager_previous_scene(bad_usb->scene_manager);
         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_app_i.h"
 #include "../views/bad_usb_view.h"
-#include "furi_hal.h"
+#include <furi_hal.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);
     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) {
@@ -15,8 +15,13 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
     bool consumed = false;
 
     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) {
         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;
     file_name = furi_string_alloc();
-
     path_extract_filename(app->file_path, file_name, true);
     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);
 
+    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_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);
 }
 
 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_script.h"
+#include <toolbox/path.h>
 #include <gui/elements.h>
 #include <assets_icons.h>
 
@@ -7,12 +8,13 @@
 
 struct BadUsb {
     View* view;
-    BadUsbOkCallback callback;
+    BadUsbButtonCallback callback;
     void* context;
 };
 
 typedef struct {
     char file_name[MAX_NAME_LEN];
+    char layout[MAX_NAME_LEN];
     BadUsbState state;
     uint8_t anim_frame;
 } BadUsbModel;
@@ -25,9 +27,23 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
     elements_string_fit_width(canvas, disp_str, 128 - 2);
     canvas_set_font(canvas, FontSecondary);
     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);
 
-    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) ||
        (model->state.state == BadUsbStateNotConnected)) {
@@ -38,23 +54,28 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
         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) {
-        canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
         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) {
-        canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
         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) {
-        canvas_draw_icon(canvas, 4, 22, &I_Error_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
         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) {
-        canvas_draw_icon(canvas, 4, 22, &I_Error_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
         canvas_set_font(canvas, FontPrimary);
         canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
         canvas_set_font(canvas, FontSecondary);
@@ -64,49 +85,49 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
         furi_string_reset(disp_str);
         canvas_draw_str_aligned(canvas, 127, 56, AlignRight, AlignBottom, model->state.error);
     } 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_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) {
         if(model->anim_frame == 0) {
-            canvas_draw_icon(canvas, 4, 19, &I_EviSmile1_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
         } else {
-            canvas_draw_icon(canvas, 4, 19, &I_EviSmile2_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviSmile2_18x21);
         }
         canvas_set_font(canvas, FontBigNumbers);
         furi_string_printf(
             disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
         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);
-        canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
+        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
     } 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_draw_str_aligned(canvas, 114, 36, AlignRight, AlignBottom, "100");
+        canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "100");
         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) {
         if(model->anim_frame == 0) {
-            canvas_draw_icon(canvas, 4, 19, &I_EviWaiting1_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
         } else {
-            canvas_draw_icon(canvas, 4, 19, &I_EviWaiting2_18x21);
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
         }
         canvas_set_font(canvas, FontBigNumbers);
         furi_string_printf(
             disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
         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);
-        canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
+        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
         canvas_set_font(canvas, FontSecondary);
         furi_string_printf(disp_str, "delay %lus", model->state.delay_remain);
         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);
     } else {
-        canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
     }
 
     furi_string_free(disp_str);
@@ -118,10 +139,10 @@ static bool bad_usb_input_callback(InputEvent* event, void* context) {
     bool consumed = false;
 
     if(event->type == InputTypeShort) {
-        if(event->key == InputKeyOk) {
+        if((event->key == InputKeyLeft) || (event->key == InputKeyOk)) {
             consumed = true;
             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;
 }
 
-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(callback);
     with_view_model(
@@ -174,6 +195,14 @@ void bad_usb_set_file_name(BadUsb* bad_usb, const char* name) {
         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) {
     furi_assert(st);
     with_view_model(

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

@@ -4,7 +4,7 @@
 #include "../bad_usb_script.h"
 
 typedef struct BadUsb BadUsb;
-typedef void (*BadUsbOkCallback)(InputType type, void* context);
+typedef void (*BadUsbButtonCallback)(InputKey key, void* context);
 
 BadUsb* bad_usb_alloc();
 
@@ -12,8 +12,10 @@ void bad_usb_free(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_layout(BadUsb* bad_usb, const char* layout);
+
 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) {
-    FapLoader* loader = malloc(sizeof(FapLoader)); //-V773
+    FapLoader* loader = malloc(sizeof(FapLoader)); //-V799
     loader->fap_path = furi_string_alloc_set(path);
     loader->storage = furi_record_open(RECORD_STORAGE);
     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);
     view_dispatcher_add_view(loader->view_dispatcher, 0, loading_get_view(loader->loading));
     return loader;
-}
+} //-V773
 
 static void fap_loader_free(FapLoader* loader) {
     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));
 
     app->gui = furi_record_open(RECORD_GUI);
+    app->gpio_items = gpio_items_alloc();
 
     app->view_dispatcher = view_dispatcher_alloc();
     app->scene_manager = scene_manager_alloc(&gpio_scene_handlers, app);
@@ -47,7 +48,7 @@ GpioApp* gpio_app_alloc() {
         app->view_dispatcher,
         GpioAppViewVarItemList,
         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(
         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_NOTIFICATION);
 
+    gpio_items_free(app->gpio_items);
     free(app);
 }
 

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

@@ -1,7 +1,7 @@
 #pragma once
 
 #include "gpio_app.h"
-#include "gpio_item.h"
+#include "gpio_items.h"
 #include "scenes/gpio_scene.h"
 #include "gpio_custom_event.h"
 #include "usb_uart_bridge.h"
@@ -28,6 +28,7 @@ struct GpioApp {
     VariableItem* var_item_flow;
     GpioTest* gpio_test;
     GpioUsbUart* gpio_usb_uart;
+    GPIOItems* gpio_items;
     UsbUartBridge* usb_uart_bridge;
     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 "furi_hal_power.h"
-#include "furi_hal_usb.h"
+#include <furi_hal_power.h>
+#include <furi_hal_usb.h>
 #include <dolphin/dolphin.h>
 
 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) {
+    furi_assert(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);
     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) {
-    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 "../gpio_app_i.h"
-#include "furi_hal.h"
+#include <furi_hal.h>
 
 typedef enum {
     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* baudrate_mode[] = {"Host"};
 static const uint32_t baudrate_list[] = {
+    1200,
     2400,
+    4800,
     9600,
     19200,
+    28800,
     38400,
     57600,
     115200,

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

@@ -1,10 +1,10 @@
 #include "usb_uart_bridge.h"
-#include "furi_hal.h"
-#include <furi_hal_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 "cli/cli.h"
+#include <furi_hal.h>
+#include <furi_hal_usb_cdc.h>
 
 #define USB_CDC_PKT_LEN CDC_DATA_SZ
 #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_item.h"
+#include "../gpio_items.h"
 
 #include <gui/elements.h>
 
@@ -11,6 +11,7 @@ struct GpioTest {
 
 typedef struct {
     uint8_t pin_idx;
+    GPIOItems* gpio_items;
 } GpioTestModel;
 
 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(
         canvas, 64, 16, AlignCenter, AlignTop, "Press < or > to change pin");
     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) {
@@ -64,7 +70,7 @@ static bool gpio_test_process_right(GpioTest* gpio_test) {
         gpio_test->view,
         GpioTestModel * model,
         {
-            if(model->pin_idx < GPIO_ITEM_COUNT) {
+            if(model->pin_idx < gpio_items_get_count(model->gpio_items)) {
                 model->pin_idx++;
             }
         },
@@ -80,17 +86,17 @@ static bool gpio_test_process_ok(GpioTest* gpio_test, InputEvent* event) {
         GpioTestModel * model,
         {
             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 {
-                    gpio_item_set_all_pins(true);
+                    gpio_items_set_all_pins(model->gpio_items, true);
                 }
                 consumed = true;
             } 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 {
-                    gpio_item_set_all_pins(false);
+                    gpio_items_set_all_pins(model->gpio_items, false);
                 }
                 consumed = true;
             }
@@ -101,11 +107,15 @@ static bool gpio_test_process_ok(GpioTest* gpio_test, InputEvent* event) {
     return consumed;
 }
 
-GpioTest* gpio_test_alloc() {
+GpioTest* gpio_test_alloc(GPIOItems* gpio_items) {
     GpioTest* gpio_test = malloc(sizeof(GpioTest));
 
     gpio_test->view = view_alloc();
     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_draw_callback(gpio_test->view, gpio_test_draw_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
 
+#include "../gpio_items.h"
+
 #include <gui/view.h>
 
 typedef struct GpioTest GpioTest;
 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);
 

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

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

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

@@ -2,6 +2,7 @@ App(
     appid="ibutton",
     name="iButton",
     apptype=FlipperAppType.APP,
+    targets=["f7"],
     entry_point="ibutton_app",
     cdefines=["APP_IBUTTON"],
     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);
 
-    if(!result) {
+    if(!result) { //-V547
         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) {
-    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) {
@@ -343,7 +343,7 @@ int32_t ibutton_app(void* p) {
     } else {
         view_dispatcher_attach_to_gui(
             ibutton->view_dispatcher, ibutton->gui, ViewDispatcherTypeFullscreen);
-        if(key_loaded) {
+        if(key_loaded) { //-V547
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate);
             DOLPHIN_DEED(DolphinDeedIbuttonEmulate);
         } 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) {
     UNUSED(cli);
-    OneWireHost* onewire = onewire_host_alloc();
+    OneWireHost* onewire = onewire_host_alloc(&ibutton_gpio);
     uint8_t address[8];
     bool done = false;
 

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

@@ -3,6 +3,7 @@ App(
     name="Infrared",
     apptype=FlipperAppType.APP,
     entry_point="infrared_app",
+    targets=["f7"],
     cdefines=["APP_INFRARED"],
     requires=[
         "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) {
-    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) {
@@ -455,7 +455,7 @@ int32_t infrared_app(void* p) {
     } else {
         view_dispatcher_attach_to_gui(
             infrared->view_dispatcher, infrared->gui, ViewDispatcherTypeFullscreen);
-        if(is_remote_loaded) {
+        if(is_remote_loaded) { //-V547
             scene_manager_next_scene(infrared->scene_manager, InfraredSceneRemote);
         } else {
             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)) {
             InfraredBruteForceRecord* record =
                 InfraredBruteForceRecordDict_get(brute_force->records, signal_name);
-            if(record) {
+            if(record) { //-V547
                 ++(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;
         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);
         for(size_t i = 0; i < timings_cnt; ++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 list <remote_name>\r\n");
     // 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) {
@@ -276,7 +276,9 @@ static bool infrared_cli_decode_file(FlipperFormat* input_file, FlipperFormat* o
         }
         InfraredRawSignal* raw_signal = infrared_signal_get_raw_signal(signal);
         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(
                raw_signal, decoder, output_file, furi_string_get_cstr(tmp)))
             break;
@@ -382,7 +384,7 @@ static void infrared_cli_list_remote_signals(FuriString* remote_name) {
         while(flipper_format_read_string(ff, "name", signal_name)) {
             furi_string_set_str(key, furi_string_get_cstr(signal_name));
             int* v = dict_signals_get(signals_dict, key);
-            if(v != NULL) {
+            if(v != NULL) { //-V547
                 (*v)++;
                 max = M_MAX(*v, max);
             } else {
@@ -436,7 +438,7 @@ static void
             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");
 
         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();
 
     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;
-        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);
         infrared_remote_clear_buttons(remote);
         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);
             }
         }
-    }
+        success = true;
+    } while(false);
 
     furi_string_free(buf);
     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)) {
         FURI_LOG_E(
             TAG,
-            "Timings amount is out of range (0 - %X): %X",
+            "Timings amount is out of range (0 - %X): %zX",
             MAX_TIMINGS_AMOUNT,
             raw->timings_size);
         return false;
@@ -275,8 +275,8 @@ bool infrared_signal_search_and_read(
             is_name_found = furi_string_equal(name, tmp);
             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;
     } 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_ac, UniversalAC)
 ADD_SCENE(infrared, universal_audio, UniversalAudio)
+ADD_SCENE(infrared, universal_projector, UniversalProjector)
 ADD_SCENE(infrared, debug, Debug)
 ADD_SCENE(infrared, error_databases, ErrorDatabases)
 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);
                 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) {
                     printf("%lu ", raw->timings[i]);
                 }

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

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

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

@@ -4,6 +4,7 @@ typedef enum {
     SubmenuIndexUniversalTV,
     SubmenuIndexUniversalAC,
     SubmenuIndexUniversalAudio,
+    SubmenuIndexUniversalProjector,
 } SubmenuIndex;
 
 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,
         infrared_scene_universal_submenu_callback,
         context);
+    submenu_add_item(
+        submenu,
+        "Projectors",
+        SubmenuIndexUniversalProjector,
+        infrared_scene_universal_submenu_callback,
+        context);
     submenu_add_item(
         submenu,
         "Air Conditioners",
         SubmenuIndexUniversalAC,
         infrared_scene_universal_submenu_callback,
         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);
 }
@@ -53,7 +61,11 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) {
         } else if(event.event == SubmenuIndexUniversalAudio) {
             scene_manager_next_scene(scene_manager, InfraredSceneUniversalAudio);
             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;
@@ -62,4 +74,4 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) {
 void infrared_scene_universal_on_exit(void* context) {
     Infrared* infrared = context;
     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 <stdlib.h>
-#include <string.h>
-
 #include <gui/canvas.h>
 #include <gui/elements.h>
 
+#include <stdlib.h>
+#include <string.h>
+
 #define INFRARED_DEBUG_TEXT_LENGTH 64
 
 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/modules/button_panel.h>
+#include <input/input.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>
 
 struct InfraredProgressView {

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