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

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

0xchocolate 3 лет назад
Родитель
Сommit
912d180dff
100 измененных файлов с 2469 добавлено и 1258 удалено
  1. 42 66
      .github/CODEOWNERS
  2. 3 0
      .github/workflows/amap_analyse.yml
  3. 8 18
      .github/workflows/build.yml
  4. 1 0
      .github/workflows/check_submodules.yml
  5. 1 1
      .github/workflows/lint_c.yml
  6. 1 1
      .github/workflows/lint_python.yml
  7. 5 2
      .github/workflows/pvs_studio.yml
  8. 3 0
      .gitmodules
  9. 6 1
      .vscode/example/launch.json
  10. 2 2
      .vscode/example/settings.json
  11. 4 4
      ReadMe.md
  12. 30 42
      SConstruct
  13. 2 3
      applications/ReadMe.md
  14. 54 45
      applications/debug/bt_debug_app/views/bt_test.c
  15. 5 5
      applications/debug/display_test/display_test.c
  16. 6 7
      applications/debug/display_test/view_display_test.c
  17. 2 3
      applications/debug/file_browser_test/file_browser_app.c
  18. 1 1
      applications/debug/file_browser_test/file_browser_app_i.h
  19. 1 4
      applications/debug/file_browser_test/scenes/file_browser_scene_browser.c
  20. 8 3
      applications/debug/file_browser_test/scenes/file_browser_scene_result.c
  21. 1 1
      applications/debug/file_browser_test/scenes/file_browser_scene_start.c
  22. 38 33
      applications/debug/lfrfid_debug/views/lfrfid_debug_view_tune.c
  23. 30 30
      applications/debug/uart_echo/uart_echo.c
  24. 13 13
      applications/debug/unit_tests/flipper_format/flipper_format_string_test.c
  25. 40 14
      applications/debug/unit_tests/flipper_format/flipper_format_test.c
  26. 469 0
      applications/debug/unit_tests/furi/furi_string_test.c
  27. 46 42
      applications/debug/unit_tests/infrared/infrared_test.c
  28. 128 9
      applications/debug/unit_tests/nfc/nfc_test.c
  29. 9 9
      applications/debug/unit_tests/rpc/rpc_test.c
  30. 24 24
      applications/debug/unit_tests/storage/dirwalk_test.c
  31. 16 16
      applications/debug/unit_tests/storage/storage_test.c
  32. 18 18
      applications/debug/unit_tests/stream/stream_test.c
  33. 22 19
      applications/debug/unit_tests/subghz/subghz_test.c
  34. 5 5
      applications/debug/unit_tests/test_index.c
  35. 5 0
      applications/examples/application.fam
  36. 10 0
      applications/examples/example_images/application.fam
  37. 81 0
      applications/examples/example_images/example_images.c
  38. BIN
      applications/examples/example_images/images/dolphin_71x25.png
  39. 2 3
      applications/main/archive/archive.c
  40. 1 1
      applications/main/archive/archive_i.h
  41. 129 110
      applications/main/archive/helpers/archive_browser.c
  42. 4 1
      applications/main/archive/helpers/archive_browser.h
  43. 56 54
      applications/main/archive/helpers/archive_favorites.c
  44. 2 2
      applications/main/archive/helpers/archive_favorites.h
  45. 14 14
      applications/main/archive/helpers/archive_files.c
  46. 39 11
      applications/main/archive/helpers/archive_files.h
  47. 5 4
      applications/main/archive/scenes/archive_scene_browser.c
  48. 4 5
      applications/main/archive/scenes/archive_scene_delete.c
  49. 11 11
      applications/main/archive/scenes/archive_scene_rename.c
  50. 71 44
      applications/main/archive/views/archive_browser_view.c
  51. 2 1
      applications/main/archive/views/archive_browser_view.h
  52. 5 6
      applications/main/bad_usb/bad_usb_app.c
  53. 1 1
      applications/main/bad_usb/bad_usb_app_i.h
  54. 69 29
      applications/main/bad_usb/bad_usb_script.c
  55. 2 2
      applications/main/bad_usb/bad_usb_script.h
  56. 4 5
      applications/main/bad_usb/scenes/bad_usb_scene_work.c
  57. 35 28
      applications/main/bad_usb/views/bad_usb_view.c
  58. 1 0
      applications/main/fap_loader/application.fam
  59. 1 1
      applications/main/fap_loader/elf_cpp/elf_hashtable.cpp
  60. 74 53
      applications/main/fap_loader/fap_loader_app.c
  61. 27 0
      applications/main/fap_loader/fap_loader_app.h
  62. 1 0
      applications/main/gpio/gpio_app_i.h
  63. 2 0
      applications/main/gpio/scenes/gpio_scene_start.c
  64. 24 2
      applications/main/gpio/scenes/gpio_scene_usb_uart_config.c
  65. 8 10
      applications/main/gpio/usb_uart_bridge.c
  66. 20 12
      applications/main/gpio/views/gpio_test.c
  67. 12 8
      applications/main/gpio/views/gpio_usb_uart.c
  68. 16 17
      applications/main/ibutton/ibutton.c
  69. 28 28
      applications/main/ibutton/ibutton_cli.c
  70. 2 2
      applications/main/ibutton/ibutton_i.h
  71. 1 2
      applications/main/ibutton/scenes/ibutton_scene_add_type.c
  72. 9 9
      applications/main/ibutton/scenes/ibutton_scene_delete_confirm.c
  73. 17 38
      applications/main/ibutton/scenes/ibutton_scene_emulate.c
  74. 9 11
      applications/main/ibutton/scenes/ibutton_scene_info.c
  75. 1 1
      applications/main/ibutton/scenes/ibutton_scene_read.c
  76. 9 9
      applications/main/ibutton/scenes/ibutton_scene_rpc.c
  77. 11 12
      applications/main/ibutton/scenes/ibutton_scene_save_name.c
  78. 2 4
      applications/main/ibutton/scenes/ibutton_scene_save_success.c
  79. 1 1
      applications/main/ibutton/scenes/ibutton_scene_start.c
  80. 17 40
      applications/main/ibutton/scenes/ibutton_scene_write.c
  81. 46 43
      applications/main/infrared/infrared.c
  82. 39 41
      applications/main/infrared/infrared_brute_force.c
  83. 1 0
      applications/main/infrared/infrared_brute_force.h
  84. 251 54
      applications/main/infrared/infrared_cli.c
  85. 1 1
      applications/main/infrared/infrared_i.h
  86. 24 25
      applications/main/infrared/infrared_remote.c
  87. 1 1
      applications/main/infrared/infrared_remote.h
  88. 5 6
      applications/main/infrared/infrared_remote_button.c
  89. 56 20
      applications/main/infrared/infrared_signal.c
  90. 5 1
      applications/main/infrared/infrared_signal.h
  91. 2 1
      applications/main/infrared/scenes/common/infrared_scene_universal_common.c
  92. 1 0
      applications/main/infrared/scenes/infrared_scene_config.h
  93. 6 6
      applications/main/infrared/scenes/infrared_scene_edit_rename.c
  94. 4 2
      applications/main/infrared/scenes/infrared_scene_learn_enter_name.c
  95. 1 1
      applications/main/infrared/scenes/infrared_scene_rpc.c
  96. 1 1
      applications/main/infrared/scenes/infrared_scene_start.c
  97. 10 4
      applications/main/infrared/scenes/infrared_scene_universal.c
  98. 109 0
      applications/main/infrared/scenes/infrared_scene_universal_ac.c
  99. 0 1
      applications/main/infrared/views/infrared_progress_view.c
  100. 17 17
      applications/main/lfrfid/lfrfid.c

+ 42 - 66
.github/CODEOWNERS

@@ -2,82 +2,58 @@
 * @skotopes @DrZlo13 @hedger
 
 # Apps
-/applications/about/ @skotopes @DrZlo13 @hedger
-/applications/accessor/ @skotopes @DrZlo13 @hedger
-/applications/archive/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/bad_usb/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/bt/ @skotopes @DrZlo13 @hedger @gornekich
-/applications/cli/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/crypto/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/debug_tools/ @skotopes @DrZlo13 @hedger
-/applications/desktop/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/dialogs/ @skotopes @DrZlo13 @hedger
-/applications/dolphin/ @skotopes @DrZlo13 @hedger
-/applications/gpio/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/gui/ @skotopes @DrZlo13 @hedger
-/applications/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov
-/applications/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
-/applications/input/ @skotopes @DrZlo13 @hedger
-/applications/lfrfid/ @skotopes @DrZlo13 @hedger
-/applications/lfrfid_debug/ @skotopes @DrZlo13 @hedger
-/applications/loader/ @skotopes @DrZlo13 @hedger
-/applications/music_player/ @skotopes @DrZlo13 @hedger
-/applications/nfc/ @skotopes @DrZlo13 @hedger @gornekich
-/applications/notification/ @skotopes @DrZlo13 @hedger
-/applications/power/ @skotopes @DrZlo13 @hedger
-/applications/rpc/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/snake_game/ @skotopes @DrZlo13 @hedger
-/applications/storage/ @skotopes @DrZlo13 @hedger
-/applications/storage_settings/ @skotopes @DrZlo13 @hedger
-/applications/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm
-/applications/system/ @skotopes @DrZlo13 @hedger
-/applications/u2f/ @skotopes @DrZlo13 @hedger @nminaylov
-/applications/unit_tests/ @skotopes @DrZlo13 @hedger
-/applications/updater/ @skotopes @DrZlo13 @hedger
-
-# Assets
-/assets/ @skotopes @DrZlo13 @hedger
-
-# Furi Core
-/furi/ @skotopes @DrZlo13 @hedger
-
-# Debug tools and plugins
-/debug/ @skotopes @DrZlo13 @hedger
+/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/debug/accessor/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/debug/battery_test_app/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/debug/file_browser_test/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/debug/lfrfid_debug/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/debug/text_box_test/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/debug/uart_echo/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/debug/usb_mouse/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/debug/usb_test/ @skotopes @DrZlo13 @hedger @nminaylov
+
+/applications/main/archive/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/main/bad_usb/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/main/gpio/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov
+/applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
+/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/main/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm
+/applications/main/u2f/ @skotopes @DrZlo13 @hedger @nminaylov
+
+/applications/plugins/bt_hid_app/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/plugins/picopass/ @skotopes @DrZlo13 @hedger @gornekich
+
+/applications/services/bt/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/services/cli/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/services/crypto/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/services/desktop/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/services/dolphin/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/services/power/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/services/rpc/ @skotopes @DrZlo13 @hedger @nminaylov
+
+/applications/services/bt_settings_app/ @skotopes @DrZlo13 @hedger @gornekich
+/applications/services/desktop_settings/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/services/dolphin_passport/ @skotopes @DrZlo13 @hedger @nminaylov
+/applications/services/power_settings_app/ @skotopes @DrZlo13 @hedger @gornekich
+
+/applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @nminaylov
 
 # Documentation
 /documentation/ @skotopes @DrZlo13 @hedger @drunkbatya
-
-# Firmware targets
-/firmware/ @skotopes @DrZlo13 @hedger
+/scripts/toolchain/ @skotopes @DrZlo13 @hedger @drunkbatya
 
 # Lib
-/lib/FreeRTOS-Kernel/ @skotopes @DrZlo13 @hedger
-/lib/FreeRTOS-glue/ @skotopes @DrZlo13 @hedger
 /lib/ST25RFAL002/ @skotopes @DrZlo13 @hedger @gornekich
 /lib/STM32CubeWB/ @skotopes @DrZlo13 @hedger @gornekich
-/lib/app-scened-template/ @skotopes @DrZlo13 @hedger
-/lib/callback-connector/ @skotopes @DrZlo13 @hedger
 /lib/digital_signal/ @skotopes @DrZlo13 @hedger @gornekich
-/lib/drivers/ @skotopes @DrZlo13 @hedger
-/lib/fatfs/ @skotopes @DrZlo13 @hedger
-/lib/flipper_format/ @skotopes @DrZlo13 @hedger
-/lib/fnv1a-hash/ @skotopes @DrZlo13 @hedger
-/lib/heatshrink/ @skotopes @DrZlo13 @hedger
 /lib/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
+/lib/lfrfid/ @skotopes @DrZlo13 @hedger @nminaylov
 /lib/libusb_stm32/ @skotopes @DrZlo13 @hedger @nminaylov
-/lib/littlefs/ @skotopes @DrZlo13 @hedger
-/lib/lfs_config.h @skotopes @DrZlo13 @hedger
+/lib/mbedtls/ @skotopes @DrZlo13 @hedger @nminaylov
 /lib/micro-ecc/ @skotopes @DrZlo13 @hedger @nminaylov
-/lib/microtar/ @skotopes @DrZlo13 @hedger
-/lib/mlib/ @skotopes @DrZlo13 @hedger
-/lib/nanopb/ @skotopes @DrZlo13 @hedger
+/lib/nanopb/ @skotopes @DrZlo13 @hedger @nminaylov
 /lib/nfc/ @skotopes @DrZlo13 @hedger @gornekich
-/lib/one_wire/ @skotopes @DrZlo13 @hedger
-/lib/qrcode/ @skotopes @DrZlo13 @hedger
+/lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov
 /lib/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm
-/lib/toolbox/ @skotopes @DrZlo13 @hedger
-/lib/u8g2/ @skotopes @DrZlo13 @hedger
-/lib/update_util/ @skotopes @DrZlo13 @hedger
-
-# Helper scripts
-/scripts/ @skotopes @DrZlo13 @hedger

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

@@ -62,6 +62,8 @@ jobs:
 
       - 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 \
@@ -97,3 +99,4 @@ jobs:
             ${{ secrets.AMAP_MARIADB_PORT }} \
             ${{ secrets.AMAP_MARIADB_DATABASE }} \
             artifacts/flipper-z-f7-firmware-$SUFFIX.elf.map.all
+

+ 8 - 18
.github/workflows/build.yml

@@ -56,14 +56,14 @@ jobs:
       - name: 'Bundle scripts'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         run: |
-          tar czpf artifacts/flipper-z-any-scripts-${SUFFIX}.tgz scripts
+          tar czpf artifacts/flipper-z-any-scripts-${SUFFIX}.tgz scripts debug
 
       - name: 'Build the firmware'
         run: |
           set -e
           for TARGET in ${TARGETS}; do
-            FBT_TOOLCHAIN_PATH=/opt ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
-                updater_package ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
+            FBT_TOOLCHAIN_PATH=/runner/_work ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
+                copro_dist updater_package ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
           done
 
       - name: 'Move upload files'
@@ -74,17 +74,6 @@ jobs:
             mv dist/${TARGET}-*/* artifacts/
           done
 
-      - name: 'Bundle self-update package'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        run: |
-          set -e
-          for UPDATEBUNDLE in artifacts/*/; do
-            BUNDLE_NAME="$(echo "$UPDATEBUNDLE" | cut -d'/' -f2)"
-            echo Packaging "${BUNDLE_NAME}"
-            tar czpf "artifacts/flipper-z-${BUNDLE_NAME}.tgz" -C artifacts "${BUNDLE_NAME}"
-            rm -rf "artifacts/${BUNDLE_NAME}"
-          done
-
       - name: "Check for uncommitted changes"
         run: |
           git diff --exit-code
@@ -97,8 +86,7 @@ jobs:
       - name: 'Bundle core2 firmware'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         run: |
-          FBT_TOOLCHAIN_PATH=/opt ./fbt copro_dist
-          tar czpf "artifacts/flipper-z-any-core2_firmware-${SUFFIX}.tgz" -C assets core2_firmware
+          cp build/core2_firmware.tgz "artifacts/flipper-z-any-core2_firmware-${SUFFIX}.tgz"
 
       - name: 'Copy .map file'
         run: |
@@ -107,6 +95,8 @@ jobs:
       - name: 'Upload artifacts to update server'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         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 --delete --mkpath \
@@ -137,7 +127,7 @@ jobs:
             **Compiled firmware for commit `${{steps.names.outputs.commit_sha}}`:**
             - [📦 Update package](https://update.flipperzero.one/builds/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz)
             - [📥 DFU file](https://update.flipperzero.one/builds/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-full-${{steps.names.outputs.suffix}}.dfu)
-            - [☁️ Web updater](https://my.flipp.dev/?url=https://update.flipperzero.one/builds/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz&channel=${{steps.names.outputs.branch_name}}&version=${{steps.names.outputs.commit_sha}})
+            - [☁️ Web/App updater](https://my.flipp.dev/?url=https://update.flipperzero.one/builds/firmware/${{steps.names.outputs.branch_name}}/flipper-z-${{steps.names.outputs.default_target}}-update-${{steps.names.outputs.suffix}}.tgz&channel=${{steps.names.outputs.branch_name}}&version=${{steps.names.outputs.commit_sha}})
           edit-mode: replace
 
   compact:
@@ -174,6 +164,6 @@ jobs:
         run: |
           set -e
           for TARGET in ${TARGETS}; do
-            FBT_TOOLCHAIN_PATH=/opt ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
+            FBT_TOOLCHAIN_PATH=/runner/_work ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
                 updater_package DEBUG=0 COMPACT=1
           done

+ 1 - 0
.github/workflows/check_submodules.yml

@@ -27,6 +27,7 @@ jobs:
 
       - name: 'Check protobuf branch'
         run: |
+          git submodule update --init
           SUB_PATH="assets/protobuf";
           SUB_BRANCH="dev";
           SUB_COMMITS_MIN=40;

+ 1 - 1
.github/workflows/lint_c.yml

@@ -30,7 +30,7 @@ jobs:
 
       - name: 'Check code formatting'
         id: syntax_check
-        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/opt ./fbt lint
+        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/runner/_work ./fbt lint
 
       - name: Report code formatting errors
         if: failure() && steps.syntax_check.outputs.errors && github.event.pull_request

+ 1 - 1
.github/workflows/lint_python.yml

@@ -26,4 +26,4 @@ jobs:
           ref: ${{ github.event.pull_request.head.sha }}
 
       - name: 'Check code formatting'
-        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/opt ./fbt lint_py
+        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/runner/_work ./fbt lint_py

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

@@ -57,14 +57,15 @@ jobs:
 
       - name: 'Generate compile_comands.json'
         run: |
-          FBT_TOOLCHAIN_PATH=/opt ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking
+          FBT_TOOLCHAIN_PATH=/runner/_work ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking
 
       - name: 'Static code analysis'
         run: |
-          FBT_TOOLCHAIN_PATH=/opt source scripts/toolchain/fbtenv.sh
+          FBT_TOOLCHAIN_PATH=/runner/_work source scripts/toolchain/fbtenv.sh
           pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
           pvs-studio-analyzer analyze \
               @.pvsoptions \
+              --disableLicenseExpirationCheck \
               -j$(grep -c processor /proc/cpuinfo) \
               -f build/f7-firmware-DC/compile_commands.json \
               -o PVS-Studio.log
@@ -75,6 +76,8 @@ jobs:
       - name: 'Upload artifacts to update server'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         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 \

+ 3 - 0
.gitmodules

@@ -31,3 +31,6 @@
 [submodule "lib/cxxheaderparser"]
 	path = lib/cxxheaderparser
 	url = https://github.com/robotpy/cxxheaderparser.git
+[submodule "applications/plugins/dap_link/lib/free-dap"]
+	path = applications/plugins/dap_link/lib/free-dap
+	url = https://github.com/ataradov/free-dap.git

+ 6 - 1
.vscode/example/launch.json

@@ -9,6 +9,10 @@
             "type": "command",
             "command": "shellCommand.execute",
             "args": {
+                "useSingleResult": true,
+                "env": {
+                    "PATH": "${workspaceFolder};${env:PATH}"
+                },
                 "command": "./fbt get_blackmagic",
                 "description": "Get Blackmagic device",
             }
@@ -24,13 +28,14 @@
             "servertype": "openocd",
             "device": "stlink",
             "svdFile": "./debug/STM32WB55_CM4.svd",
+            // If you're debugging early in the boot process, before OS scheduler is running,
+            // you have to comment out the following line.
             "rtos": "FreeRTOS",
             "configFiles": [
                 "interface/stlink.cfg",
                 "./debug/stm32wbx.cfg",
             ],
             "postAttachCommands": [
-                // "attach 1",
                 // "compare-sections",
                 "source debug/flipperapps.py",
                 // "source debug/FreeRTOS/FreeRTOS.py",

+ 2 - 2
.vscode/example/settings.json

@@ -14,7 +14,7 @@
     "cortex-debug.openocdPath.osx": "${workspaceFolder}/toolchain/x86_64-darwin/openocd/bin/openocd",
     "cortex-debug.gdbPath.windows": "${workspaceFolder}/toolchain/i686-windows/bin/arm-none-eabi-gdb-py.bat",
     "cortex-debug.gdbPath.linux": "${workspaceFolder}/toolchain/x86_64-linux/bin/arm-none-eabi-gdb-py",
-    "cortex-debug.gdbPath.osx": "${workspaceFolder}/toolchain/x86_64-darwin/bin/arm-none-eabi-gdb",
+    "cortex-debug.gdbPath.osx": "${workspaceFolder}/toolchain/x86_64-darwin/bin/arm-none-eabi-gdb-py",
     "editor.formatOnSave": true,
     "files.associations": {
         "*.scons": "python",
@@ -22,4 +22,4 @@
         "SConstruct": "python",
         "*.fam": "python",
     }
-}
+}

+ 4 - 4
ReadMe.md

@@ -24,7 +24,7 @@ Check out details on [how to build firmware](documentation/fbt.md), [write appli
 
 Flipper Zero's firmware consists of two components:
 
-- Core2 firmware set - proprietary components by ST: FUS + radio stack. FUS is flashed at factory and you should never update it.
+- Core2 firmware set - proprietary components by ST: FUS + radio stack. FUS is flashed at factory, and you should never update it.
 - Core1 Firmware - HAL + OS + Drivers + Applications.
 
 They both must be flashed in the order described.
@@ -52,7 +52,7 @@ Prerequisites:
 - [arm-gcc-none-eabi](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads)
 - openocd
 
-One liner: `./fbt firmware_flash`
+One-liner: `./fbt firmware_flash`
 
 ## With USB DFU 
 
@@ -128,7 +128,7 @@ Connect your device via ST-Link and run:
 - `debug`           - Debug tool: GDB-plugins, SVD-file and etc
 - `documentation`   - Documentation generation system configs and input files
 - `firmware`        - Firmware source code
-- `lib`             - Our and 3rd party libraries, drivers and etc...
+- `lib`             - Our and 3rd party libraries, drivers, etc.
 - `scripts`         - Supplementary scripts and python libraries home
 
-Also pay attention to `ReadMe.md` files inside of those directories.
+Also pay attention to `ReadMe.md` files inside those directories.

+ 30 - 42
SConstruct

@@ -7,7 +7,6 @@
 # construction of certain targets behind command-line options.
 
 import os
-import subprocess
 
 DefaultEnvironment(tools=[])
 
@@ -15,17 +14,22 @@ EnsurePythonVersion(3, 8)
 
 # Progress(["OwO\r", "owo\r", "uwu\r", "owo\r"], interval=15)
 
-
 # This environment is created only for loading options & validating file/dir existence
 fbt_variables = SConscript("site_scons/commandline.scons")
-cmd_environment = Environment(tools=[], variables=fbt_variables)
-Help(fbt_variables.GenerateHelpText(cmd_environment))
+cmd_environment = Environment(
+    toolpath=["#/scripts/fbt_tools"],
+    tools=[
+        ("fbt_help", {"vars": fbt_variables}),
+    ],
+    variables=fbt_variables,
+)
 
 # Building basic environment - tools, utility methods, cross-compilation
 # settings, gcc flags for Cortex-M4, basic builders and more
 coreenv = SConscript(
     "site_scons/environ.scons",
     exports={"VAR_ENV": cmd_environment},
+    toolpath=["#/scripts/fbt_tools"],
 )
 SConscript("site_scons/cc.scons", exports={"ENV": coreenv})
 
@@ -35,39 +39,13 @@ coreenv["ROOT_DIR"] = Dir(".")
 
 # Create a separate "dist" environment and add construction envs to it
 distenv = coreenv.Clone(
-    tools=["fbt_dist", "openocd", "blackmagic", "jflash"],
-    OPENOCD_GDB_PIPE=[
-        "|openocd -c 'gdb_port pipe; log_output debug/openocd.log' ${[SINGLEQUOTEFUNC(OPENOCD_OPTS)]}"
+    tools=[
+        "fbt_dist",
+        "fbt_debugopts",
+        "openocd",
+        "blackmagic",
+        "jflash",
     ],
-    GDBOPTS_BASE=[
-        "-ex",
-        "target extended-remote ${GDBREMOTE}",
-        "-ex",
-        "set confirm off",
-    ],
-    GDBOPTS_BLACKMAGIC=[
-        "-ex",
-        "monitor swdp_scan",
-        "-ex",
-        "monitor debug_bmp enable",
-        "-ex",
-        "attach 1",
-        "-ex",
-        "set mem inaccessible-by-default off",
-    ],
-    GDBPYOPTS=[
-        "-ex",
-        "source debug/FreeRTOS/FreeRTOS.py",
-        "-ex",
-        "source debug/flipperapps.py",
-        "-ex",
-        "source debug/PyCortexMDebug/PyCortexMDebug.py",
-        "-ex",
-        "svd_load ${SVD_FILE}",
-        "-ex",
-        "compare-sections",
-    ],
-    JFLASHPROJECT="${ROOT_DIR.abspath}/debug/fw.jflash",
     ENV=os.environ,
 )
 
@@ -164,7 +142,7 @@ basic_dist = distenv.DistCommand("fw_dist", distenv["DIST_DEPENDS"])
 distenv.Default(basic_dist)
 
 dist_dir = distenv.GetProjetDirName()
-plugin_dist = [
+fap_dist = [
     distenv.Install(
         f"#/dist/{dist_dir}/apps/debug_elf",
         firmware_env["FW_EXTAPPS"]["debug"].values(),
@@ -174,9 +152,9 @@ plugin_dist = [
         for dist_entry in firmware_env["FW_EXTAPPS"]["dist"].values()
     ),
 ]
-Depends(plugin_dist, firmware_env["FW_EXTAPPS"]["validators"].values())
-Alias("plugin_dist", plugin_dist)
-# distenv.Default(plugin_dist)
+Depends(fap_dist, firmware_env["FW_EXTAPPS"]["validators"].values())
+Alias("fap_dist", fap_dist)
+# distenv.Default(fap_dist)
 
 plugin_resources_dist = list(
     distenv.Install(f"#/assets/resources/apps/{dist_entry[0]}", dist_entry[1])
@@ -187,9 +165,10 @@ distenv.Depends(firmware_env["FW_RESOURCES"], plugin_resources_dist)
 
 # Target for bundling core2 package for qFlipper
 copro_dist = distenv.CoproBuilder(
-    distenv.Dir("assets/core2_firmware"),
+    "#/build/core2_firmware.tgz",
     [],
 )
+distenv.AlwaysBuild(copro_dist)
 distenv.Alias("copro_dist", copro_dist)
 
 firmware_flash = distenv.AddOpenOCDFlashTarget(firmware_env)
@@ -234,10 +213,19 @@ distenv.PhonyTarget(
 distenv.PhonyTarget(
     "debug_other",
     "${GDBPYCOM}",
-    GDBPYOPTS='-ex "source debug/PyCortexMDebug/PyCortexMDebug.py" ',
+    GDBOPTS="${GDBOPTS_BASE}",
     GDBREMOTE="${OPENOCD_GDB_PIPE}",
+    GDBPYOPTS='-ex "source debug/PyCortexMDebug/PyCortexMDebug.py" ',
+)
+
+distenv.PhonyTarget(
+    "debug_other_blackmagic",
+    "${GDBPYCOM}",
+    GDBOPTS="${GDBOPTS_BASE}  ${GDBOPTS_BLACKMAGIC}",
+    GDBREMOTE="$${BLACKMAGIC_ADDR}",
 )
 
+
 # Just start OpenOCD
 distenv.PhonyTarget(
     "openocd",

+ 2 - 3
applications/ReadMe.md

@@ -1,8 +1,5 @@
 # Structure
 
-- `application.h`       - Firmware application list header
-
-
 ## debug 
 
 Applications for factory testing the Flipper.
@@ -53,6 +50,8 @@ Extra apps for Plugins & App Loader menus.
 
 Background services providing system APIs to applications.
 
+- `applications.h`      - Firmware application list header
+
 - `bt`                  - BLE service and application
 - `cli`                 - Console service and API
 - `crypto`              - Crypto cli tools

+ 54 - 45
applications/debug/bt_debug_app/views/bt_test.c

@@ -3,14 +3,13 @@
 #include <gui/canvas.h>
 #include <gui/elements.h>
 #include <m-array.h>
-#include <m-string.h>
 #include <furi.h>
 #include <stdint.h>
 
 struct BtTestParam {
     const char* label;
     uint8_t current_value_index;
-    string_t current_value_text;
+    FuriString* current_value_text;
     uint8_t values_count;
     BtTestParamChangeCallback change_callback;
     void* context;
@@ -85,7 +84,8 @@ static void bt_test_draw_callback(Canvas* canvas, void* _model) {
                 canvas_draw_str(canvas, 50, param_text_y, "<");
             }
 
-            canvas_draw_str(canvas, 61, param_text_y, string_get_cstr(param->current_value_text));
+            canvas_draw_str(
+                canvas, 61, param_text_y, furi_string_get_cstr(param->current_value_text));
 
             if(param->current_value_index < (param->values_count - 1)) {
                 canvas_draw_str(canvas, 113, param_text_y, ">");
@@ -154,7 +154,9 @@ static bool bt_test_input_callback(InputEvent* event, void* context) {
 
 void bt_test_process_up(BtTest* bt_test) {
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             uint8_t params_on_screen = 3;
             if(model->position > 0) {
                 model->position--;
@@ -168,13 +170,15 @@ void bt_test_process_up(BtTest* bt_test) {
                     model->window_position = model->position - (params_on_screen - 1);
                 }
             }
-            return true;
-        });
+        },
+        true);
 }
 
 void bt_test_process_down(BtTest* bt_test) {
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             uint8_t params_on_screen = 3;
             if(model->position < (BtTestParamArray_size(model->params) - 1)) {
                 model->position++;
@@ -187,8 +191,8 @@ void bt_test_process_down(BtTest* bt_test) {
                 model->position = 0;
                 model->window_position = 0;
             }
-            return true;
-        });
+        },
+        true);
 }
 
 BtTestParam* bt_test_get_selected_param(BtTestModel* model) {
@@ -213,7 +217,9 @@ BtTestParam* bt_test_get_selected_param(BtTestModel* model) {
 void bt_test_process_left(BtTest* bt_test) {
     BtTestParam* param;
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             param = bt_test_get_selected_param(model);
             if(param->current_value_index > 0) {
                 param->current_value_index--;
@@ -225,8 +231,8 @@ void bt_test_process_left(BtTest* bt_test) {
                     model->packets_num_tx = 0;
                 }
             }
-            return true;
-        });
+        },
+        true);
     if(param->change_callback) {
         param->change_callback(param);
     }
@@ -235,7 +241,9 @@ void bt_test_process_left(BtTest* bt_test) {
 void bt_test_process_right(BtTest* bt_test) {
     BtTestParam* param;
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             param = bt_test_get_selected_param(model);
             if(param->current_value_index < (param->values_count - 1)) {
                 param->current_value_index++;
@@ -247,8 +255,8 @@ void bt_test_process_right(BtTest* bt_test) {
                     model->packets_num_tx = 0;
                 }
             }
-            return true;
-        });
+        },
+        true);
     if(param->change_callback) {
         param->change_callback(param);
     }
@@ -257,7 +265,9 @@ void bt_test_process_right(BtTest* bt_test) {
 void bt_test_process_ok(BtTest* bt_test) {
     BtTestState state;
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             if(model->state == BtTestStateStarted) {
                 model->state = BtTestStateStopped;
                 model->message = BT_TEST_START_MESSAGE;
@@ -269,8 +279,8 @@ void bt_test_process_ok(BtTest* bt_test) {
                 model->message = BT_TEST_STOP_MESSAGE;
             }
             state = model->state;
-            return true;
-        });
+        },
+        true);
     if(bt_test->change_state_callback) {
         bt_test->change_state_callback(state, bt_test->context);
     }
@@ -278,13 +288,15 @@ void bt_test_process_ok(BtTest* bt_test) {
 
 void bt_test_process_back(BtTest* bt_test) {
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             model->state = BtTestStateStopped;
             model->rssi = 0.0f;
             model->packets_num_rx = 0;
             model->packets_num_tx = 0;
-            return false;
-        });
+        },
+        false);
     if(bt_test->back_callback) {
         bt_test->back_callback(bt_test->context);
     }
@@ -299,7 +311,9 @@ BtTest* bt_test_alloc() {
     view_set_input_callback(bt_test->view, bt_test_input_callback);
 
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             model->state = BtTestStateStopped;
             model->message = "Ok - Start";
             BtTestParamArray_init(model->params);
@@ -308,8 +322,8 @@ BtTest* bt_test_alloc() {
             model->rssi = 0.0f;
             model->packets_num_tx = 0;
             model->packets_num_rx = 0;
-            return true;
-        });
+        },
+        true);
 
     return bt_test;
 }
@@ -318,15 +332,17 @@ void bt_test_free(BtTest* bt_test) {
     furi_assert(bt_test);
 
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             BtTestParamArray_it_t it;
             for(BtTestParamArray_it(it, model->params); !BtTestParamArray_end_p(it);
                 BtTestParamArray_next(it)) {
-                string_clear(BtTestParamArray_ref(it)->current_value_text);
+                furi_string_free(BtTestParamArray_ref(it)->current_value_text);
             }
             BtTestParamArray_clear(model->params);
-            return false;
-        });
+        },
+        false);
     view_free(bt_test->view);
     free(bt_test);
 }
@@ -347,16 +363,18 @@ BtTestParam* bt_test_param_add(
     furi_assert(bt_test);
 
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
+        bt_test->view,
+        BtTestModel * model,
+        {
             param = BtTestParamArray_push_new(model->params);
             param->label = label;
             param->values_count = values_count;
             param->change_callback = change_callback;
             param->context = context;
             param->current_value_index = 0;
-            string_init(param->current_value_text);
-            return true;
-        });
+            param->current_value_text = furi_string_alloc();
+        },
+        true);
 
     return param;
 }
@@ -364,28 +382,19 @@ BtTestParam* bt_test_param_add(
 void bt_test_set_rssi(BtTest* bt_test, float rssi) {
     furi_assert(bt_test);
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
-            model->rssi = rssi;
-            return true;
-        });
+        bt_test->view, BtTestModel * model, { model->rssi = rssi; }, true);
 }
 
 void bt_test_set_packets_tx(BtTest* bt_test, uint32_t packets_num) {
     furi_assert(bt_test);
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
-            model->packets_num_tx = packets_num;
-            return true;
-        });
+        bt_test->view, BtTestModel * model, { model->packets_num_tx = packets_num; }, true);
 }
 
 void bt_test_set_packets_rx(BtTest* bt_test, uint32_t packets_num) {
     furi_assert(bt_test);
     with_view_model(
-        bt_test->view, (BtTestModel * model) {
-            model->packets_num_rx = packets_num;
-            return true;
-        });
+        bt_test->view, BtTestModel * model, { model->packets_num_rx = packets_num; }, true);
 }
 
 void bt_test_set_change_state_callback(BtTest* bt_test, BtTestChangeStateCallback callback) {
@@ -410,7 +419,7 @@ void bt_test_set_current_value_index(BtTestParam* param, uint8_t current_value_i
 }
 
 void bt_test_set_current_value_text(BtTestParam* param, const char* current_value_text) {
-    string_set_str(param->current_value_text, current_value_text);
+    furi_string_set(param->current_value_text, current_value_text);
 }
 
 uint8_t bt_test_get_current_value_index(BtTestParam* param) {

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

@@ -113,11 +113,11 @@ static void display_config_set_regulation_ratio(VariableItem* item) {
 static void display_config_set_contrast(VariableItem* item) {
     DisplayTest* instance = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
-    string_t temp;
-    string_init(temp);
-    string_cat_printf(temp, "%d", index);
-    variable_item_set_current_value_text(item, string_get_cstr(temp));
-    string_clear(temp);
+    FuriString* temp;
+    temp = furi_string_alloc();
+    furi_string_cat_printf(temp, "%d", index);
+    variable_item_set_current_value_text(item, furi_string_get_cstr(temp));
+    furi_string_free(temp);
     instance->config_contrast = index;
     display_test_reload_config(instance);
 }

+ 6 - 7
applications/debug/display_test/view_display_test.c

@@ -110,7 +110,9 @@ static bool view_display_test_input_callback(InputEvent* event, void* context) {
     bool consumed = false;
     if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
         with_view_model(
-            instance->view, (ViewDisplayTestModel * model) {
+            instance->view,
+            ViewDisplayTestModel * model,
+            {
                 if(event->key == InputKeyLeft && model->test > 0) {
                     model->test--;
                     consumed = true;
@@ -129,8 +131,8 @@ static bool view_display_test_input_callback(InputEvent* event, void* context) {
                     model->flip_flop = !model->flip_flop;
                     consumed = true;
                 }
-                return consumed;
-            });
+            },
+            consumed);
     }
 
     return consumed;
@@ -149,10 +151,7 @@ static void view_display_test_exit(void* context) {
 static void view_display_test_timer_callback(void* context) {
     ViewDisplayTest* instance = context;
     with_view_model(
-        instance->view, (ViewDisplayTestModel * model) {
-            model->counter++;
-            return true;
-        });
+        instance->view, ViewDisplayTestModel * model, { model->counter++; }, true);
 }
 
 ViewDisplayTest* view_display_test_alloc() {

+ 2 - 3
applications/debug/file_browser_test/file_browser_app.c

@@ -1,7 +1,6 @@
 #include "assets_icons.h"
 #include "file_browser_app_i.h"
 #include "gui/modules/file_browser.h"
-#include "m-string.h"
 #include <furi.h>
 #include <furi_hal.h>
 #include <storage/storage.h>
@@ -47,7 +46,7 @@ FileBrowserApp* file_browser_app_alloc(char* arg) {
 
     app->widget = widget_alloc();
 
-    string_init(app->file_path);
+    app->file_path = furi_string_alloc();
     app->file_browser = file_browser_alloc(app->file_path);
     file_browser_configure(app->file_browser, "*", true, &I_badusb_10px, true);
 
@@ -84,7 +83,7 @@ void file_browser_app_free(FileBrowserApp* app) {
     furi_record_close(RECORD_NOTIFICATION);
     furi_record_close(RECORD_DIALOGS);
 
-    string_clear(app->file_path);
+    furi_string_free(app->file_path);
 
     free(app);
 }

+ 1 - 1
applications/debug/file_browser_test/file_browser_app_i.h

@@ -22,7 +22,7 @@ struct FileBrowserApp {
     Widget* widget;
     FileBrowser* file_browser;
 
-    string_t file_path;
+    FuriString* file_path;
 };
 
 typedef enum {

+ 1 - 4
applications/debug/file_browser_test/scenes/file_browser_scene_browser.c

@@ -1,8 +1,5 @@
 #include "../file_browser_app_i.h"
-#include <core/check.h>
-#include <core/log.h>
-#include "furi_hal.h"
-#include "m-string.h"
+#include <furi.h>
 
 #define DEFAULT_PATH "/"
 #define EXTENSION "*"

+ 8 - 3
applications/debug/file_browser_test/scenes/file_browser_scene_result.c

@@ -1,6 +1,5 @@
 #include "../file_browser_app_i.h"
-#include "furi_hal.h"
-#include "m-string.h"
+#include <furi.h>
 
 void file_browser_scene_result_ok_callback(InputType type, void* context) {
     furi_assert(context);
@@ -24,7 +23,13 @@ void file_browser_scene_result_on_enter(void* context) {
     FileBrowserApp* app = context;
 
     widget_add_string_multiline_element(
-        app->widget, 64, 10, AlignCenter, AlignTop, FontSecondary, string_get_cstr(app->file_path));
+        app->widget,
+        64,
+        10,
+        AlignCenter,
+        AlignTop,
+        FontSecondary,
+        furi_string_get_cstr(app->file_path));
 
     view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewResult);
 }

+ 1 - 1
applications/debug/file_browser_test/scenes/file_browser_scene_start.c

@@ -19,7 +19,7 @@ bool file_browser_scene_start_on_event(void* context, SceneManagerEvent event) {
     bool consumed = false;
 
     if(event.type == SceneManagerEventTypeCustom) {
-        string_set_str(app->file_path, ANY_PATH("badusb/demo_windows.txt"));
+        furi_string_set(app->file_path, ANY_PATH("badusb/demo_windows.txt"));
         scene_manager_next_scene(app->scene_manager, FileBrowserSceneBrowser);
         consumed = true;
     } else if(event.type == SceneManagerEventTypeTick) {

+ 38 - 33
applications/debug/lfrfid_debug/views/lfrfid_debug_view_tune.c

@@ -52,23 +52,29 @@ static void lfrfid_debug_view_tune_draw_callback(Canvas* canvas, void* _model) {
 
 static void lfrfid_debug_view_tune_button_up(LfRfidTuneView* tune_view) {
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             if(model->pos > 0) model->pos--;
-            return true;
-        });
+        },
+        true);
 }
 
 static void lfrfid_debug_view_tune_button_down(LfRfidTuneView* tune_view) {
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             if(model->pos < 1) model->pos++;
-            return true;
-        });
+        },
+        true);
 }
 
 static void lfrfid_debug_view_tune_button_left(LfRfidTuneView* tune_view) {
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             if(model->pos == 0) {
                 if(model->fine) {
                     model->ARR -= 1;
@@ -84,13 +90,15 @@ static void lfrfid_debug_view_tune_button_left(LfRfidTuneView* tune_view) {
             }
 
             model->dirty = true;
-            return true;
-        });
+        },
+        true);
 }
 
 static void lfrfid_debug_view_tune_button_right(LfRfidTuneView* tune_view) {
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             if(model->pos == 0) {
                 if(model->fine) {
                     model->ARR += 1;
@@ -106,16 +114,13 @@ static void lfrfid_debug_view_tune_button_right(LfRfidTuneView* tune_view) {
             }
 
             model->dirty = true;
-            return true;
-        });
+        },
+        true);
 }
 
 static void lfrfid_debug_view_tune_button_ok(LfRfidTuneView* tune_view) {
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
-            model->fine = !model->fine;
-            return true;
-        });
+        tune_view->view, LfRfidTuneViewModel * model, { model->fine = !model->fine; }, true);
 }
 
 static bool lfrfid_debug_view_tune_input_callback(InputEvent* event, void* context) {
@@ -158,14 +163,16 @@ LfRfidTuneView* lfrfid_debug_view_tune_alloc() {
     view_allocate_model(tune_view->view, ViewModelTypeLocking, sizeof(LfRfidTuneViewModel));
 
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             model->dirty = true;
             model->fine = false;
             model->ARR = 511;
             model->CCR = 255;
             model->pos = 0;
-            return true;
-        });
+        },
+        true);
 
     view_set_draw_callback(tune_view->view, lfrfid_debug_view_tune_draw_callback);
     view_set_input_callback(tune_view->view, lfrfid_debug_view_tune_input_callback);
@@ -184,24 +191,28 @@ View* lfrfid_debug_view_tune_get_view(LfRfidTuneView* tune_view) {
 
 void lfrfid_debug_view_tune_clean(LfRfidTuneView* tune_view) {
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             model->dirty = true;
             model->fine = false;
             model->ARR = 511;
             model->CCR = 255;
             model->pos = 0;
-            return true;
-        });
+        },
+        true);
 }
 
 bool lfrfid_debug_view_tune_is_dirty(LfRfidTuneView* tune_view) {
     bool result = false;
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
+        tune_view->view,
+        LfRfidTuneViewModel * model,
+        {
             result = model->dirty;
             model->dirty = false;
-            return false;
-        });
+        },
+        false);
 
     return result;
 }
@@ -209,10 +220,7 @@ bool lfrfid_debug_view_tune_is_dirty(LfRfidTuneView* tune_view) {
 uint32_t lfrfid_debug_view_tune_get_arr(LfRfidTuneView* tune_view) {
     uint32_t result = false;
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
-            result = model->ARR;
-            return false;
-        });
+        tune_view->view, LfRfidTuneViewModel * model, { result = model->ARR; }, false);
 
     return result;
 }
@@ -220,10 +228,7 @@ uint32_t lfrfid_debug_view_tune_get_arr(LfRfidTuneView* tune_view) {
 uint32_t lfrfid_debug_view_tune_get_ccr(LfRfidTuneView* tune_view) {
     uint32_t result = false;
     with_view_model(
-        tune_view->view, (LfRfidTuneViewModel * model) {
-            result = model->CCR;
-            return false;
-        });
+        tune_view->view, LfRfidTuneViewModel * model, { result = model->CCR; }, false);
 
     return result;
 }

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

@@ -1,10 +1,8 @@
 #include <furi.h>
-#include <m-string.h>
 #include <gui/gui.h>
 #include <notification/notification.h>
 #include <notification/notification_messages.h>
 #include <gui/elements.h>
-#include <stream_buffer.h>
 #include <furi_hal_uart.h>
 #include <furi_hal_console.h>
 #include <gui/view_dispatcher.h>
@@ -21,11 +19,11 @@ typedef struct {
     ViewDispatcher* view_dispatcher;
     View* view;
     FuriThread* worker_thread;
-    StreamBufferHandle_t rx_stream;
+    FuriStreamBuffer* rx_stream;
 } UartEchoApp;
 
 typedef struct {
-    string_t text;
+    FuriString* text;
 } ListElement;
 
 struct UartDumpModel {
@@ -64,10 +62,11 @@ static void uart_echo_view_draw_callback(Canvas* canvas, void* _model) {
             canvas,
             0,
             (i + 1) * (canvas_current_font_height(canvas) - 1),
-            string_get_cstr(model->list[i]->text));
+            furi_string_get_cstr(model->list[i]->text));
 
         if(i == model->line) {
-            uint8_t width = canvas_string_width(canvas, string_get_cstr(model->list[i]->text));
+            uint8_t width =
+                canvas_string_width(canvas, furi_string_get_cstr(model->list[i]->text));
 
             canvas_draw_box(
                 canvas,
@@ -92,13 +91,11 @@ static uint32_t uart_echo_exit(void* context) {
 
 static void uart_echo_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) {
     furi_assert(context);
-    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
     UartEchoApp* app = context;
 
     if(ev == UartIrqEventRXNE) {
-        xStreamBufferSendFromISR(app->rx_stream, &data, 1, &xHigherPriorityTaskWoken);
+        furi_stream_buffer_send(app->rx_stream, &data, 1, 0);
         furi_thread_flags_set(furi_thread_get_id(app->worker_thread), WorkerEventRx);
-        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
     }
 }
 
@@ -113,7 +110,7 @@ static void uart_echo_push_to_list(UartDumpModel* model, const char data) {
         model->escape = true;
     } else if((data >= ' ' && data <= '~') || (data == '\n' || data == '\r')) {
         bool new_string_needed = false;
-        if(string_size(model->list[model->line]->text) >= COLUMNS_ON_SCREEN) {
+        if(furi_string_size(model->list[model->line]->text) >= COLUMNS_ON_SCREEN) {
             new_string_needed = true;
         } else if((data == '\n' || data == '\r')) {
             // pack line breaks
@@ -132,13 +129,13 @@ static void uart_echo_push_to_list(UartDumpModel* model, const char data) {
                     model->list[i - 1] = model->list[i];
                 }
 
-                string_reset(first->text);
+                furi_string_reset(first->text);
                 model->list[model->line] = first;
             }
         }
 
         if(data != '\n' && data != '\r') {
-            string_push_back(model->list[model->line]->text, data);
+            furi_string_push_back(model->list[model->line]->text, data);
         }
     }
     model->last_char = data;
@@ -158,25 +155,24 @@ static int32_t uart_echo_worker(void* context) {
             size_t length = 0;
             do {
                 uint8_t data[64];
-                length = xStreamBufferReceive(app->rx_stream, data, 64, 0);
+                length = furi_stream_buffer_receive(app->rx_stream, data, 64, 0);
                 if(length > 0) {
                     furi_hal_uart_tx(FuriHalUartIdUSART1, data, length);
                     with_view_model(
-                        app->view, (UartDumpModel * model) {
+                        app->view,
+                        UartDumpModel * model,
+                        {
                             for(size_t i = 0; i < length; i++) {
                                 uart_echo_push_to_list(model, data[i]);
                             }
-                            return false;
-                        });
+                        },
+                        false);
                 }
             } while(length > 0);
 
             notification_message(app->notification, &sequence_notification);
             with_view_model(
-                app->view, (UartDumpModel * model) {
-                    UNUSED(model);
-                    return true;
-                });
+                app->view, UartDumpModel * model, { UNUSED(model); }, true);
         }
     }
 
@@ -186,7 +182,7 @@ static int32_t uart_echo_worker(void* context) {
 static UartEchoApp* uart_echo_app_alloc() {
     UartEchoApp* app = malloc(sizeof(UartEchoApp));
 
-    app->rx_stream = xStreamBufferCreate(2048, 1);
+    app->rx_stream = furi_stream_buffer_alloc(2048, 1);
 
     // Gui
     app->gui = furi_record_open(RECORD_GUI);
@@ -203,15 +199,17 @@ static UartEchoApp* uart_echo_app_alloc() {
     view_set_input_callback(app->view, uart_echo_view_input_callback);
     view_allocate_model(app->view, ViewModelTypeLocking, sizeof(UartDumpModel));
     with_view_model(
-        app->view, (UartDumpModel * model) {
+        app->view,
+        UartDumpModel * model,
+        {
             for(size_t i = 0; i < LINES_ON_SCREEN; i++) {
                 model->line = 0;
                 model->escape = false;
                 model->list[i] = malloc(sizeof(ListElement));
-                string_init(model->list[i]->text);
+                model->list[i]->text = furi_string_alloc();
             }
-            return true;
-        });
+        },
+        true);
 
     view_set_previous_callback(app->view, uart_echo_exit);
     view_dispatcher_add_view(app->view_dispatcher, 0, app->view);
@@ -245,13 +243,15 @@ static void uart_echo_app_free(UartEchoApp* app) {
     view_dispatcher_remove_view(app->view_dispatcher, 0);
 
     with_view_model(
-        app->view, (UartDumpModel * model) {
+        app->view,
+        UartDumpModel * model,
+        {
             for(size_t i = 0; i < LINES_ON_SCREEN; i++) {
-                string_clear(model->list[i]->text);
+                furi_string_free(model->list[i]->text);
                 free(model->list[i]);
             }
-            return true;
-        });
+        },
+        true);
     view_free(app->view);
     view_dispatcher_free(app->view_dispatcher);
 
@@ -260,7 +260,7 @@ static void uart_echo_app_free(UartEchoApp* app) {
     furi_record_close(RECORD_NOTIFICATION);
     app->gui = NULL;
 
-    vStreamBufferDelete(app->rx_stream);
+    furi_stream_buffer_free(app->rx_stream);
 
     // Free rest
     free(app);

+ 13 - 13
applications/debug/unit_tests/flipper_format/flipper_format_string_test.c

@@ -58,7 +58,7 @@ static const char* test_data_win = "Filetype: Flipper Format test\r\n"
 #define ARRAY_W_BSIZE(x) (x), (sizeof(x))
 
 MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
-    string_t tmpstr;
+    FuriString* tmpstr;
     uint32_t version;
     uint32_t uint32_data[COUNT_OF(test_uint_data)];
     int32_t int32_data[COUNT_OF(test_int_data)];
@@ -101,14 +101,14 @@ MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
     mu_assert_int_eq(position_before, stream_tell(flipper_format_get_raw_stream(flipper_format)));
 
     // read test
-    string_init(tmpstr);
+    tmpstr = furi_string_alloc();
 
     mu_check(flipper_format_read_header(flipper_format, tmpstr, &version));
-    mu_assert_string_eq(test_filetype, string_get_cstr(tmpstr));
+    mu_assert_string_eq(test_filetype, furi_string_get_cstr(tmpstr));
     mu_assert_int_eq(test_version, version);
 
     mu_check(flipper_format_read_string(flipper_format, test_string_key, tmpstr));
-    mu_assert_string_eq(test_string_data, string_get_cstr(tmpstr));
+    mu_assert_string_eq(test_string_data, furi_string_get_cstr(tmpstr));
 
     mu_check(flipper_format_get_value_count(flipper_format, test_int_key, &count));
     mu_assert_int_eq(COUNT_OF(test_int_data), count);
@@ -133,7 +133,7 @@ MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
 
     mu_check(!flipper_format_read_string(flipper_format, "Key that doesn't exist", tmpstr));
 
-    string_clear(tmpstr);
+    furi_string_free(tmpstr);
 
     // update data
     mu_check(flipper_format_rewind(flipper_format));
@@ -155,14 +155,14 @@ MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
     uint8_t hex_updated_data[COUNT_OF(test_hex_updated_data)];
 
     mu_check(flipper_format_rewind(flipper_format));
-    string_init(tmpstr);
+    tmpstr = furi_string_alloc();
 
     mu_check(flipper_format_read_header(flipper_format, tmpstr, &version));
-    mu_assert_string_eq(test_filetype, string_get_cstr(tmpstr));
+    mu_assert_string_eq(test_filetype, furi_string_get_cstr(tmpstr));
     mu_assert_int_eq(test_version, version);
 
     mu_check(flipper_format_read_string(flipper_format, test_string_key, tmpstr));
-    mu_assert_string_eq(test_string_updated_data, string_get_cstr(tmpstr));
+    mu_assert_string_eq(test_string_updated_data, furi_string_get_cstr(tmpstr));
 
     mu_check(flipper_format_get_value_count(flipper_format, test_int_key, &count));
     mu_assert_int_eq(COUNT_OF(test_int_updated_data), count);
@@ -190,7 +190,7 @@ MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
 
     mu_check(!flipper_format_read_string(flipper_format, "Key that doesn't exist", tmpstr));
 
-    string_clear(tmpstr);
+    furi_string_free(tmpstr);
 
     // update data
     mu_check(flipper_format_rewind(flipper_format));
@@ -214,14 +214,14 @@ MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
     uint8_t hex_new_data[COUNT_OF(test_hex_new_data)];
 
     mu_check(flipper_format_rewind(flipper_format));
-    string_init(tmpstr);
+    tmpstr = furi_string_alloc();
 
     mu_check(flipper_format_read_header(flipper_format, tmpstr, &version));
-    mu_assert_string_eq(test_filetype, string_get_cstr(tmpstr));
+    mu_assert_string_eq(test_filetype, furi_string_get_cstr(tmpstr));
     mu_assert_int_eq(test_version, version);
 
     mu_check(flipper_format_read_string(flipper_format, test_string_key, tmpstr));
-    mu_assert_string_eq(test_string_updated_2_data, string_get_cstr(tmpstr));
+    mu_assert_string_eq(test_string_updated_2_data, furi_string_get_cstr(tmpstr));
 
     mu_check(flipper_format_get_value_count(flipper_format, test_int_key, &count));
     mu_assert_int_eq(COUNT_OF(test_int_updated_2_data), count);
@@ -255,7 +255,7 @@ MU_TEST_1(flipper_format_read_and_update_test, FlipperFormat* flipper_format) {
 
     mu_check(!flipper_format_read_string(flipper_format, "Key that doesn't exist", tmpstr));
 
-    string_clear(tmpstr);
+    furi_string_free(tmpstr);
 
     // delete key test
     mu_check(flipper_format_rewind(flipper_format));

+ 40 - 14
applications/debug/unit_tests/flipper_format/flipper_format_test.c

@@ -57,6 +57,23 @@ static const char* test_data_win = "Filetype: Flipper File test\r\n"
                                    "Hex data: DE AD BE";
 
 #define READ_TEST_FLP "ff_flp.test"
+#define READ_TEST_ODD "ff_oddities.test"
+static const char* test_data_odd = "Filetype: Flipper File test\n"
+                                   // Tabs before newline
+                                   "Version: 666\t\t\n"
+                                   "# This is comment\n"
+                                   // Windows newline in a UNIX file
+                                   "String data: String\r\n"
+                                   // Trailing whitespace
+                                   "Int32 data: 1234 -6345 7813 0 \n"
+                                   // Extra whitespace
+                                   "Uint32 data:   1234  0   5678   9098  7654321  \n"
+                                   // Mixed whitespace
+                                   "Float data: 1.5\t \t1000.0\n"
+                                   // Leading tabs after key
+                                   "Bool data:\t\ttrue   false\n"
+                                   // Mixed trailing whitespace
+                                   "Hex data: DE AD BE\t    ";
 
 // data created by user on linux machine
 static const char* test_file_linux = TEST_DIR READ_TEST_NIX;
@@ -64,6 +81,8 @@ static const char* test_file_linux = TEST_DIR READ_TEST_NIX;
 static const char* test_file_windows = TEST_DIR READ_TEST_WIN;
 // data created by flipper itself
 static const char* test_file_flipper = TEST_DIR READ_TEST_FLP;
+// data containing odd user input
+static const char* test_file_oddities = TEST_DIR READ_TEST_ODD;
 
 static bool storage_write_string(const char* path, const char* data) {
     Storage* storage = furi_record_open(RECORD_STORAGE);
@@ -102,8 +121,8 @@ static bool test_read(const char* file_name) {
     bool result = false;
 
     FlipperFormat* file = flipper_format_file_alloc(storage);
-    string_t string_value;
-    string_init(string_value);
+    FuriString* string_value;
+    string_value = furi_string_alloc();
     uint32_t uint32_value;
     void* scratchpad = malloc(512);
 
@@ -111,11 +130,11 @@ static bool test_read(const char* file_name) {
         if(!flipper_format_file_open_existing(file, file_name)) break;
 
         if(!flipper_format_read_header(file, string_value, &uint32_value)) break;
-        if(string_cmp_str(string_value, test_filetype) != 0) break;
+        if(furi_string_cmp_str(string_value, test_filetype) != 0) break;
         if(uint32_value != test_version) break;
 
         if(!flipper_format_read_string(file, test_string_key, string_value)) break;
-        if(string_cmp_str(string_value, test_string_data) != 0) break;
+        if(furi_string_cmp_str(string_value, test_string_data) != 0) break;
 
         if(!flipper_format_get_value_count(file, test_int_key, &uint32_value)) break;
         if(uint32_value != COUNT_OF(test_int_data)) break;
@@ -150,7 +169,7 @@ static bool test_read(const char* file_name) {
     } while(false);
 
     free(scratchpad);
-    string_clear(string_value);
+    furi_string_free(string_value);
 
     flipper_format_free(file);
 
@@ -164,8 +183,8 @@ static bool test_read_updated(const char* file_name) {
     bool result = false;
 
     FlipperFormat* file = flipper_format_file_alloc(storage);
-    string_t string_value;
-    string_init(string_value);
+    FuriString* string_value;
+    string_value = furi_string_alloc();
     uint32_t uint32_value;
     void* scratchpad = malloc(512);
 
@@ -173,11 +192,11 @@ static bool test_read_updated(const char* file_name) {
         if(!flipper_format_file_open_existing(file, file_name)) break;
 
         if(!flipper_format_read_header(file, string_value, &uint32_value)) break;
-        if(string_cmp_str(string_value, test_filetype) != 0) break;
+        if(furi_string_cmp_str(string_value, test_filetype) != 0) break;
         if(uint32_value != test_version) break;
 
         if(!flipper_format_read_string(file, test_string_key, string_value)) break;
-        if(string_cmp_str(string_value, test_string_updated_data) != 0) break;
+        if(furi_string_cmp_str(string_value, test_string_updated_data) != 0) break;
 
         if(!flipper_format_get_value_count(file, test_int_key, &uint32_value)) break;
         if(uint32_value != COUNT_OF(test_int_updated_data)) break;
@@ -228,7 +247,7 @@ static bool test_read_updated(const char* file_name) {
     } while(false);
 
     free(scratchpad);
-    string_clear(string_value);
+    furi_string_free(string_value);
 
     flipper_format_free(file);
 
@@ -401,14 +420,14 @@ static bool test_read_multikey(const char* file_name) {
     bool result = false;
     FlipperFormat* file = flipper_format_file_alloc(storage);
 
-    string_t string_value;
-    string_init(string_value);
+    FuriString* string_value;
+    string_value = furi_string_alloc();
     uint32_t uint32_value;
 
     do {
         if(!flipper_format_file_open_existing(file, file_name)) break;
         if(!flipper_format_read_header(file, string_value, &uint32_value)) break;
-        if(string_cmp_str(string_value, test_filetype) != 0) break;
+        if(furi_string_cmp_str(string_value, test_filetype) != 0) break;
         if(uint32_value != test_version) break;
 
         bool error = false;
@@ -429,7 +448,7 @@ static bool test_read_multikey(const char* file_name) {
         result = true;
     } while(false);
 
-    string_clear(string_value);
+    furi_string_free(string_value);
 
     flipper_format_free(file);
     furi_record_close(RECORD_STORAGE);
@@ -503,6 +522,12 @@ MU_TEST(flipper_format_multikey_test) {
     mu_assert(test_read_multikey(TEST_DIR "ff_multiline.test"), "Multikey read test error");
 }
 
+MU_TEST(flipper_format_oddities_test) {
+    mu_assert(
+        storage_write_string(test_file_oddities, test_data_odd), "Write test error [Oddities]");
+    mu_assert(test_read(test_file_linux), "Read test error [Oddities]");
+}
+
 MU_TEST_SUITE(flipper_format) {
     tests_setup();
     MU_RUN_TEST(flipper_format_write_test);
@@ -516,6 +541,7 @@ MU_TEST_SUITE(flipper_format) {
     MU_RUN_TEST(flipper_format_update_2_test);
     MU_RUN_TEST(flipper_format_update_2_result_test);
     MU_RUN_TEST(flipper_format_multikey_test);
+    MU_RUN_TEST(flipper_format_oddities_test);
     tests_teardown();
 }
 

+ 469 - 0
applications/debug/unit_tests/furi/furi_string_test.c

@@ -0,0 +1,469 @@
+#include <furi.h>
+#include "../minunit.h"
+
+static void test_setup(void) {
+}
+
+static void test_teardown(void) {
+}
+
+static FuriString* furi_string_alloc_vprintf_test(const char format[], ...) {
+    va_list args;
+    va_start(args, format);
+    FuriString* string = furi_string_alloc_vprintf(format, args);
+    va_end(args);
+    return string;
+}
+
+MU_TEST(mu_test_furi_string_alloc_free) {
+    FuriString* tmp;
+    FuriString* string;
+
+    // test alloc and free
+    string = furi_string_alloc();
+    mu_check(string != NULL);
+    mu_check(furi_string_empty(string));
+    furi_string_free(string);
+
+    // test furi_string_alloc_set_str and free
+    string = furi_string_alloc_set_str("test");
+    mu_check(string != NULL);
+    mu_check(!furi_string_empty(string));
+    mu_check(furi_string_cmp(string, "test") == 0);
+    furi_string_free(string);
+
+    // test furi_string_alloc_set and free
+    tmp = furi_string_alloc_set("more");
+    string = furi_string_alloc_set(tmp);
+    furi_string_free(tmp);
+    mu_check(string != NULL);
+    mu_check(!furi_string_empty(string));
+    mu_check(furi_string_cmp(string, "more") == 0);
+    furi_string_free(string);
+
+    // test alloc_printf and free
+    string = furi_string_alloc_printf("test %d %s %c 0x%02x", 1, "two", '3', 0x04);
+    mu_check(string != NULL);
+    mu_check(!furi_string_empty(string));
+    mu_check(furi_string_cmp(string, "test 1 two 3 0x04") == 0);
+    furi_string_free(string);
+
+    // test alloc_vprintf and free
+    string = furi_string_alloc_vprintf_test("test %d %s %c 0x%02x", 4, "five", '6', 0x07);
+    mu_check(string != NULL);
+    mu_check(!furi_string_empty(string));
+    mu_check(furi_string_cmp(string, "test 4 five 6 0x07") == 0);
+    furi_string_free(string);
+
+    // test alloc_move and free
+    tmp = furi_string_alloc_set("move");
+    string = furi_string_alloc_move(tmp);
+    mu_check(string != NULL);
+    mu_check(!furi_string_empty(string));
+    mu_check(furi_string_cmp(string, "move") == 0);
+    furi_string_free(string);
+}
+
+MU_TEST(mu_test_furi_string_mem) {
+    FuriString* string = furi_string_alloc_set("test");
+    mu_check(string != NULL);
+    mu_check(!furi_string_empty(string));
+
+    // TODO: how to test furi_string_reserve?
+
+    // test furi_string_reset
+    furi_string_reset(string);
+    mu_check(furi_string_empty(string));
+
+    // test furi_string_swap
+    furi_string_set(string, "test");
+    FuriString* swap_string = furi_string_alloc_set("swap");
+    furi_string_swap(string, swap_string);
+    mu_check(furi_string_cmp(string, "swap") == 0);
+    mu_check(furi_string_cmp(swap_string, "test") == 0);
+    furi_string_free(swap_string);
+
+    // test furi_string_move
+    FuriString* move_string = furi_string_alloc_set("move");
+    furi_string_move(string, move_string);
+    mu_check(furi_string_cmp(string, "move") == 0);
+    // move_string is now empty
+    // and tested by leaked memory check at the end of the tests
+
+    furi_string_set(string, "abracadabra");
+
+    // test furi_string_hash
+    mu_assert_int_eq(0xc3bc16d7, furi_string_hash(string));
+
+    // test furi_string_size
+    mu_assert_int_eq(11, furi_string_size(string));
+
+    // test furi_string_empty
+    mu_check(!furi_string_empty(string));
+    furi_string_reset(string);
+    mu_check(furi_string_empty(string));
+
+    furi_string_free(string);
+}
+
+MU_TEST(mu_test_furi_string_getters) {
+    FuriString* string = furi_string_alloc_set("test");
+
+    // test furi_string_get_char
+    mu_check(furi_string_get_char(string, 0) == 't');
+    mu_check(furi_string_get_char(string, 1) == 'e');
+    mu_check(furi_string_get_char(string, 2) == 's');
+    mu_check(furi_string_get_char(string, 3) == 't');
+
+    // test furi_string_get_cstr
+    mu_assert_string_eq("test", furi_string_get_cstr(string));
+    furi_string_free(string);
+}
+
+static FuriString* furi_string_vprintf_test(FuriString* string, const char format[], ...) {
+    va_list args;
+    va_start(args, format);
+    furi_string_vprintf(string, format, args);
+    va_end(args);
+    return string;
+}
+
+MU_TEST(mu_test_furi_string_setters) {
+    FuriString* tmp;
+    FuriString* string = furi_string_alloc();
+
+    // test furi_string_set_str
+    furi_string_set_str(string, "test");
+    mu_assert_string_eq("test", furi_string_get_cstr(string));
+
+    // test furi_string_set
+    tmp = furi_string_alloc_set("more");
+    furi_string_set(string, tmp);
+    furi_string_free(tmp);
+    mu_assert_string_eq("more", furi_string_get_cstr(string));
+
+    // test furi_string_set_strn
+    furi_string_set_strn(string, "test", 2);
+    mu_assert_string_eq("te", furi_string_get_cstr(string));
+
+    // test furi_string_set_char
+    furi_string_set_char(string, 0, 'a');
+    furi_string_set_char(string, 1, 'b');
+    mu_assert_string_eq("ab", furi_string_get_cstr(string));
+
+    // test furi_string_set_n
+    tmp = furi_string_alloc_set("dodecahedron");
+    furi_string_set_n(string, tmp, 4, 5);
+    furi_string_free(tmp);
+    mu_assert_string_eq("cahed", furi_string_get_cstr(string));
+
+    // test furi_string_printf
+    furi_string_printf(string, "test %d %s %c 0x%02x", 1, "two", '3', 0x04);
+    mu_assert_string_eq("test 1 two 3 0x04", furi_string_get_cstr(string));
+
+    // test furi_string_vprintf
+    furi_string_vprintf_test(string, "test %d %s %c 0x%02x", 4, "five", '6', 0x07);
+    mu_assert_string_eq("test 4 five 6 0x07", furi_string_get_cstr(string));
+
+    furi_string_free(string);
+}
+
+static FuriString* furi_string_cat_vprintf_test(FuriString* string, const char format[], ...) {
+    va_list args;
+    va_start(args, format);
+    furi_string_cat_vprintf(string, format, args);
+    va_end(args);
+    return string;
+}
+
+MU_TEST(mu_test_furi_string_appends) {
+    FuriString* tmp;
+    FuriString* string = furi_string_alloc();
+
+    // test furi_string_push_back
+    furi_string_push_back(string, 't');
+    furi_string_push_back(string, 'e');
+    furi_string_push_back(string, 's');
+    furi_string_push_back(string, 't');
+    mu_assert_string_eq("test", furi_string_get_cstr(string));
+    furi_string_push_back(string, '!');
+    mu_assert_string_eq("test!", furi_string_get_cstr(string));
+
+    // test furi_string_cat_str
+    furi_string_cat_str(string, "test");
+    mu_assert_string_eq("test!test", furi_string_get_cstr(string));
+
+    // test furi_string_cat
+    tmp = furi_string_alloc_set("more");
+    furi_string_cat(string, tmp);
+    furi_string_free(tmp);
+    mu_assert_string_eq("test!testmore", furi_string_get_cstr(string));
+
+    // test furi_string_cat_printf
+    furi_string_cat_printf(string, "test %d %s %c 0x%02x", 1, "two", '3', 0x04);
+    mu_assert_string_eq("test!testmoretest 1 two 3 0x04", furi_string_get_cstr(string));
+
+    // test furi_string_cat_vprintf
+    furi_string_cat_vprintf_test(string, "test %d %s %c 0x%02x", 4, "five", '6', 0x07);
+    mu_assert_string_eq(
+        "test!testmoretest 1 two 3 0x04test 4 five 6 0x07", furi_string_get_cstr(string));
+
+    furi_string_free(string);
+}
+
+MU_TEST(mu_test_furi_string_compare) {
+    FuriString* string_1 = furi_string_alloc_set("string_1");
+    FuriString* string_2 = furi_string_alloc_set("string_2");
+
+    // test furi_string_cmp
+    mu_assert_int_eq(0, furi_string_cmp(string_1, string_1));
+    mu_assert_int_eq(0, furi_string_cmp(string_2, string_2));
+    mu_assert_int_eq(-1, furi_string_cmp(string_1, string_2));
+    mu_assert_int_eq(1, furi_string_cmp(string_2, string_1));
+
+    // test furi_string_cmp_str
+    mu_assert_int_eq(0, furi_string_cmp_str(string_1, "string_1"));
+    mu_assert_int_eq(0, furi_string_cmp_str(string_2, "string_2"));
+    mu_assert_int_eq(-1, furi_string_cmp_str(string_1, "string_2"));
+    mu_assert_int_eq(1, furi_string_cmp_str(string_2, "string_1"));
+
+    // test furi_string_cmpi
+    furi_string_set(string_1, "string");
+    furi_string_set(string_2, "StrIng");
+    mu_assert_int_eq(0, furi_string_cmpi(string_1, string_1));
+    mu_assert_int_eq(0, furi_string_cmpi(string_2, string_2));
+    mu_assert_int_eq(0, furi_string_cmpi(string_1, string_2));
+    mu_assert_int_eq(0, furi_string_cmpi(string_2, string_1));
+    furi_string_set(string_1, "string_1");
+    furi_string_set(string_2, "StrIng_2");
+    mu_assert_int_eq(32, furi_string_cmp(string_1, string_2));
+    mu_assert_int_eq(-32, furi_string_cmp(string_2, string_1));
+    mu_assert_int_eq(-1, furi_string_cmpi(string_1, string_2));
+    mu_assert_int_eq(1, furi_string_cmpi(string_2, string_1));
+
+    // test furi_string_cmpi_str
+    furi_string_set(string_1, "string");
+    mu_assert_int_eq(0, furi_string_cmp_str(string_1, "string"));
+    mu_assert_int_eq(32, furi_string_cmp_str(string_1, "String"));
+    mu_assert_int_eq(32, furi_string_cmp_str(string_1, "STring"));
+    mu_assert_int_eq(32, furi_string_cmp_str(string_1, "STRing"));
+    mu_assert_int_eq(32, furi_string_cmp_str(string_1, "STRIng"));
+    mu_assert_int_eq(32, furi_string_cmp_str(string_1, "STRINg"));
+    mu_assert_int_eq(32, furi_string_cmp_str(string_1, "STRING"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "string"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "String"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "STring"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "STRing"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "STRIng"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "STRINg"));
+    mu_assert_int_eq(0, furi_string_cmpi_str(string_1, "STRING"));
+
+    furi_string_free(string_1);
+    furi_string_free(string_2);
+}
+
+MU_TEST(mu_test_furi_string_search) {
+    //                                            012345678901234567
+    FuriString* haystack = furi_string_alloc_set("test321test123test");
+    FuriString* needle = furi_string_alloc_set("test");
+
+    // test furi_string_search
+    mu_assert_int_eq(0, furi_string_search(haystack, needle));
+    mu_assert_int_eq(7, furi_string_search(haystack, needle, 1));
+    mu_assert_int_eq(14, furi_string_search(haystack, needle, 8));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search(haystack, needle, 15));
+
+    FuriString* tmp = furi_string_alloc_set("testnone");
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search(haystack, tmp));
+    furi_string_free(tmp);
+
+    // test furi_string_search_str
+    mu_assert_int_eq(0, furi_string_search_str(haystack, "test"));
+    mu_assert_int_eq(7, furi_string_search_str(haystack, "test", 1));
+    mu_assert_int_eq(14, furi_string_search_str(haystack, "test", 8));
+    mu_assert_int_eq(4, furi_string_search_str(haystack, "321"));
+    mu_assert_int_eq(11, furi_string_search_str(haystack, "123"));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search_str(haystack, "testnone"));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search_str(haystack, "test", 15));
+
+    // test furi_string_search_char
+    mu_assert_int_eq(0, furi_string_search_char(haystack, 't'));
+    mu_assert_int_eq(1, furi_string_search_char(haystack, 'e'));
+    mu_assert_int_eq(2, furi_string_search_char(haystack, 's'));
+    mu_assert_int_eq(3, furi_string_search_char(haystack, 't', 1));
+    mu_assert_int_eq(7, furi_string_search_char(haystack, 't', 4));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search_char(haystack, 'x'));
+
+    // test furi_string_search_rchar
+    mu_assert_int_eq(17, furi_string_search_rchar(haystack, 't'));
+    mu_assert_int_eq(15, furi_string_search_rchar(haystack, 'e'));
+    mu_assert_int_eq(16, furi_string_search_rchar(haystack, 's'));
+    mu_assert_int_eq(13, furi_string_search_rchar(haystack, '3'));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search_rchar(haystack, '3', 14));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_search_rchar(haystack, 'x'));
+
+    furi_string_free(haystack);
+    furi_string_free(needle);
+}
+
+MU_TEST(mu_test_furi_string_equality) {
+    FuriString* string = furi_string_alloc_set("test");
+    FuriString* string_eq = furi_string_alloc_set("test");
+    FuriString* string_neq = furi_string_alloc_set("test2");
+
+    // test furi_string_equal
+    mu_check(furi_string_equal(string, string_eq));
+    mu_check(!furi_string_equal(string, string_neq));
+
+    // test furi_string_equal_str
+    mu_check(furi_string_equal_str(string, "test"));
+    mu_check(!furi_string_equal_str(string, "test2"));
+    mu_check(furi_string_equal_str(string_neq, "test2"));
+    mu_check(!furi_string_equal_str(string_neq, "test"));
+
+    furi_string_free(string);
+    furi_string_free(string_eq);
+    furi_string_free(string_neq);
+}
+
+MU_TEST(mu_test_furi_string_replace) {
+    FuriString* needle = furi_string_alloc_set("test");
+    FuriString* replace = furi_string_alloc_set("replace");
+    FuriString* string = furi_string_alloc_set("test123test");
+
+    // test furi_string_replace_at
+    furi_string_replace_at(string, 4, 3, "!biglongword!");
+    mu_assert_string_eq("test!biglongword!test", furi_string_get_cstr(string));
+
+    // test furi_string_replace
+    mu_assert_int_eq(17, furi_string_replace(string, needle, replace, 1));
+    mu_assert_string_eq("test!biglongword!replace", furi_string_get_cstr(string));
+    mu_assert_int_eq(0, furi_string_replace(string, needle, replace));
+    mu_assert_string_eq("replace!biglongword!replace", furi_string_get_cstr(string));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_replace(string, needle, replace));
+    mu_assert_string_eq("replace!biglongword!replace", furi_string_get_cstr(string));
+
+    // test furi_string_replace_str
+    mu_assert_int_eq(20, furi_string_replace_str(string, "replace", "test", 1));
+    mu_assert_string_eq("replace!biglongword!test", furi_string_get_cstr(string));
+    mu_assert_int_eq(0, furi_string_replace_str(string, "replace", "test"));
+    mu_assert_string_eq("test!biglongword!test", furi_string_get_cstr(string));
+    mu_assert_int_eq(FURI_STRING_FAILURE, furi_string_replace_str(string, "replace", "test"));
+    mu_assert_string_eq("test!biglongword!test", furi_string_get_cstr(string));
+
+    // test furi_string_replace_all
+    furi_string_replace_all(string, needle, replace);
+    mu_assert_string_eq("replace!biglongword!replace", furi_string_get_cstr(string));
+
+    // test furi_string_replace_all_str
+    furi_string_replace_all_str(string, "replace", "test");
+    mu_assert_string_eq("test!biglongword!test", furi_string_get_cstr(string));
+
+    furi_string_free(string);
+    furi_string_free(needle);
+    furi_string_free(replace);
+}
+
+MU_TEST(mu_test_furi_string_start_end) {
+    FuriString* string = furi_string_alloc_set("start_end");
+    FuriString* start = furi_string_alloc_set("start");
+    FuriString* end = furi_string_alloc_set("end");
+
+    // test furi_string_start_with
+    mu_check(furi_string_start_with(string, start));
+    mu_check(!furi_string_start_with(string, end));
+
+    // test furi_string_start_with_str
+    mu_check(furi_string_start_with_str(string, "start"));
+    mu_check(!furi_string_start_with_str(string, "end"));
+
+    // test furi_string_end_with
+    mu_check(furi_string_end_with(string, end));
+    mu_check(!furi_string_end_with(string, start));
+
+    // test furi_string_end_with_str
+    mu_check(furi_string_end_with_str(string, "end"));
+    mu_check(!furi_string_end_with_str(string, "start"));
+
+    furi_string_free(string);
+    furi_string_free(start);
+    furi_string_free(end);
+}
+
+MU_TEST(mu_test_furi_string_trim) {
+    FuriString* string = furi_string_alloc_set("biglongstring");
+
+    // test furi_string_left
+    furi_string_left(string, 7);
+    mu_assert_string_eq("biglong", furi_string_get_cstr(string));
+
+    // test furi_string_right
+    furi_string_right(string, 3);
+    mu_assert_string_eq("long", furi_string_get_cstr(string));
+
+    // test furi_string_mid
+    furi_string_mid(string, 1, 2);
+    mu_assert_string_eq("on", furi_string_get_cstr(string));
+
+    // test furi_string_trim
+    furi_string_set(string, "   \n\r\tbiglongstring \n\r\t  ");
+    furi_string_trim(string);
+    mu_assert_string_eq("biglongstring", furi_string_get_cstr(string));
+    furi_string_set(string, "aaaabaaaabbaaabaaaabbtestaaaaaabbaaabaababaa");
+    furi_string_trim(string, "ab");
+    mu_assert_string_eq("test", furi_string_get_cstr(string));
+
+    furi_string_free(string);
+}
+
+MU_TEST(mu_test_furi_string_utf8) {
+    FuriString* utf8_string = furi_string_alloc_set("イルカ");
+
+    // test furi_string_utf8_length
+    mu_assert_int_eq(9, furi_string_size(utf8_string));
+    mu_assert_int_eq(3, furi_string_utf8_length(utf8_string));
+
+    // test furi_string_utf8_decode
+    const uint8_t dolphin_emoji_array[4] = {0xF0, 0x9F, 0x90, 0xAC};
+    FuriStringUTF8State state = FuriStringUTF8StateStarting;
+    FuriStringUnicodeValue value = 0;
+    furi_string_utf8_decode(dolphin_emoji_array[0], &state, &value);
+    mu_assert_int_eq(FuriStringUTF8StateDecoding3, state);
+    furi_string_utf8_decode(dolphin_emoji_array[1], &state, &value);
+    mu_assert_int_eq(FuriStringUTF8StateDecoding2, state);
+    furi_string_utf8_decode(dolphin_emoji_array[2], &state, &value);
+    mu_assert_int_eq(FuriStringUTF8StateDecoding1, state);
+    furi_string_utf8_decode(dolphin_emoji_array[3], &state, &value);
+    mu_assert_int_eq(FuriStringUTF8StateStarting, state);
+    mu_assert_int_eq(0x1F42C, value);
+
+    // test furi_string_utf8_push
+    furi_string_set(utf8_string, "");
+    furi_string_utf8_push(utf8_string, value);
+    mu_assert_string_eq("🐬", furi_string_get_cstr(utf8_string));
+
+    furi_string_free(utf8_string);
+}
+
+MU_TEST_SUITE(test_suite) {
+    MU_SUITE_CONFIGURE(&test_setup, &test_teardown);
+
+    MU_RUN_TEST(mu_test_furi_string_alloc_free);
+    MU_RUN_TEST(mu_test_furi_string_mem);
+    MU_RUN_TEST(mu_test_furi_string_getters);
+    MU_RUN_TEST(mu_test_furi_string_setters);
+    MU_RUN_TEST(mu_test_furi_string_appends);
+    MU_RUN_TEST(mu_test_furi_string_compare);
+    MU_RUN_TEST(mu_test_furi_string_search);
+    MU_RUN_TEST(mu_test_furi_string_equality);
+    MU_RUN_TEST(mu_test_furi_string_replace);
+    MU_RUN_TEST(mu_test_furi_string_start_end);
+    MU_RUN_TEST(mu_test_furi_string_trim);
+    MU_RUN_TEST(mu_test_furi_string_utf8);
+}
+
+int run_minunit_test_furi_string() {
+    MU_RUN_SUITE(test_suite);
+
+    return MU_EXIT_CODE;
+}

+ 46 - 42
applications/debug/unit_tests/infrared/infrared_test.c

@@ -11,7 +11,7 @@
 typedef struct {
     InfraredDecoderHandler* decoder_handler;
     InfraredEncoderHandler* encoder_handler;
-    string_t file_path;
+    FuriString* file_path;
     FlipperFormat* ff;
 } InfraredTest;
 
@@ -23,7 +23,7 @@ static void infrared_test_alloc() {
     test->decoder_handler = infrared_alloc_decoder();
     test->encoder_handler = infrared_alloc_encoder();
     test->ff = flipper_format_buffered_file_alloc(storage);
-    string_init(test->file_path);
+    test->file_path = furi_string_alloc();
 }
 
 static void infrared_test_free() {
@@ -31,18 +31,18 @@ static void infrared_test_free() {
     infrared_free_decoder(test->decoder_handler);
     infrared_free_encoder(test->encoder_handler);
     flipper_format_free(test->ff);
-    string_clear(test->file_path);
+    furi_string_free(test->file_path);
     furi_record_close(RECORD_STORAGE);
     free(test);
     test = NULL;
 }
 
 static bool infrared_test_prepare_file(const char* protocol_name) {
-    string_t file_type;
-    string_init(file_type);
+    FuriString* file_type;
+    file_type = furi_string_alloc();
     bool success = false;
 
-    string_printf(
+    furi_string_printf(
         test->file_path,
         "%s%s%s%s",
         IR_TEST_FILES_DIR,
@@ -52,14 +52,15 @@ static bool infrared_test_prepare_file(const char* protocol_name) {
 
     do {
         uint32_t format_version;
-        if(!flipper_format_buffered_file_open_existing(test->ff, string_get_cstr(test->file_path)))
+        if(!flipper_format_buffered_file_open_existing(
+               test->ff, furi_string_get_cstr(test->file_path)))
             break;
         if(!flipper_format_read_header(test->ff, file_type, &format_version)) break;
-        if(string_cmp_str(file_type, "IR tests file") || format_version != 1) break;
+        if(furi_string_cmp_str(file_type, "IR tests file") || format_version != 1) break;
         success = true;
     } while(false);
 
-    string_clear(file_type);
+    furi_string_free(file_type);
     return success;
 }
 
@@ -68,18 +69,18 @@ static bool infrared_test_load_raw_signal(
     const char* signal_name,
     uint32_t** timings,
     uint32_t* timings_count) {
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
     bool success = false;
 
     do {
         bool is_name_found = false;
         for(; !is_name_found && flipper_format_read_string(ff, "name", buf);
-            is_name_found = !string_cmp_str(buf, signal_name))
+            is_name_found = !furi_string_cmp(buf, signal_name))
             ;
 
         if(!is_name_found) break;
-        if(!flipper_format_read_string(ff, "type", buf) || string_cmp_str(buf, "raw")) break;
+        if(!flipper_format_read_string(ff, "type", buf) || furi_string_cmp_str(buf, "raw")) break;
         if(!flipper_format_get_value_count(ff, "data", timings_count)) break;
         if(!*timings_count) break;
 
@@ -91,18 +92,18 @@ static bool infrared_test_load_raw_signal(
         success = true;
     } while(false);
 
-    string_clear(buf);
+    furi_string_free(buf);
     return success;
 }
 
 static bool infrared_test_read_message(FlipperFormat* ff, InfraredMessage* message) {
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
     bool success = false;
 
     do {
         if(!flipper_format_read_string(ff, "protocol", buf)) break;
-        message->protocol = infrared_get_protocol_by_name(string_get_cstr(buf));
+        message->protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
         if(!infrared_is_protocol_valid(message->protocol)) break;
         if(!flipper_format_read_hex(ff, "address", (uint8_t*)&message->address, sizeof(uint32_t)))
             break;
@@ -112,7 +113,7 @@ static bool infrared_test_read_message(FlipperFormat* ff, InfraredMessage* messa
         success = true;
     } while(false);
 
-    string_clear(buf);
+    furi_string_free(buf);
     return success;
 }
 
@@ -121,18 +122,19 @@ static bool infrared_test_load_messages(
     const char* signal_name,
     InfraredMessage** messages,
     uint32_t* messages_count) {
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
     bool success = false;
 
     do {
         bool is_name_found = false;
         for(; !is_name_found && flipper_format_read_string(ff, "name", buf);
-            is_name_found = !string_cmp_str(buf, signal_name))
+            is_name_found = !furi_string_cmp(buf, signal_name))
             ;
 
         if(!is_name_found) break;
-        if(!flipper_format_read_string(ff, "type", buf) || string_cmp_str(buf, "parsed_array"))
+        if(!flipper_format_read_string(ff, "type", buf) ||
+           furi_string_cmp_str(buf, "parsed_array"))
             break;
         if(!flipper_format_read_uint32(ff, "count", messages_count, 1)) break;
         if(!*messages_count) break;
@@ -151,7 +153,7 @@ static bool infrared_test_load_messages(
         success = true;
     } while(false);
 
-    string_clear(buf);
+    furi_string_free(buf);
     return success;
 }
 
@@ -213,26 +215,26 @@ static void infrared_test_run_encoder(InfraredProtocol protocol, uint32_t test_i
     InfraredMessage* input_messages;
     uint32_t input_messages_count;
 
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
 
     const char* protocol_name = infrared_get_protocol_name(protocol);
     mu_assert(infrared_test_prepare_file(protocol_name), "Failed to prepare test file");
 
-    string_printf(buf, "encoder_input%d", test_index);
+    furi_string_printf(buf, "encoder_input%ld", test_index);
     mu_assert(
         infrared_test_load_messages(
-            test->ff, string_get_cstr(buf), &input_messages, &input_messages_count),
+            test->ff, furi_string_get_cstr(buf), &input_messages, &input_messages_count),
         "Failed to load messages from file");
 
-    string_printf(buf, "encoder_expected%d", test_index);
+    furi_string_printf(buf, "encoder_expected%ld", test_index);
     mu_assert(
         infrared_test_load_raw_signal(
-            test->ff, string_get_cstr(buf), &expected_timings, &expected_timings_count),
+            test->ff, furi_string_get_cstr(buf), &expected_timings, &expected_timings_count),
         "Failed to load raw signal from file");
 
     flipper_format_buffered_file_close(test->ff);
-    string_clear(buf);
+    furi_string_free(buf);
 
     uint32_t j = 0;
     timings = malloc(sizeof(uint32_t) * timings_count);
@@ -267,22 +269,22 @@ static void infrared_test_run_encoder_decoder(InfraredProtocol protocol, uint32_
     uint32_t input_messages_count;
     bool level = false;
 
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
 
     timings = malloc(sizeof(uint32_t) * timings_count);
 
     const char* protocol_name = infrared_get_protocol_name(protocol);
     mu_assert(infrared_test_prepare_file(protocol_name), "Failed to prepare test file");
 
-    string_printf(buf, "encoder_decoder_input%d", test_index);
+    furi_string_printf(buf, "encoder_decoder_input%ld", test_index);
     mu_assert(
         infrared_test_load_messages(
-            test->ff, string_get_cstr(buf), &input_messages, &input_messages_count),
+            test->ff, furi_string_get_cstr(buf), &input_messages, &input_messages_count),
         "Failed to load messages from file");
 
     flipper_format_buffered_file_close(test->ff);
-    string_clear(buf);
+    furi_string_free(buf);
 
     for(uint32_t message_counter = 0; message_counter < input_messages_count; ++message_counter) {
         const InfraredMessage* message_encoded = &input_messages[message_counter];
@@ -327,25 +329,27 @@ static void infrared_test_run_decoder(InfraredProtocol protocol, uint32_t test_i
     InfraredMessage* messages;
     uint32_t messages_count;
 
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
 
     mu_assert(
         infrared_test_prepare_file(infrared_get_protocol_name(protocol)),
         "Failed to prepare test file");
 
-    string_printf(buf, "decoder_input%d", test_index);
+    furi_string_printf(buf, "decoder_input%ld", test_index);
     mu_assert(
-        infrared_test_load_raw_signal(test->ff, string_get_cstr(buf), &timings, &timings_count),
+        infrared_test_load_raw_signal(
+            test->ff, furi_string_get_cstr(buf), &timings, &timings_count),
         "Failed to load raw signal from file");
 
-    string_printf(buf, "decoder_expected%d", test_index);
+    furi_string_printf(buf, "decoder_expected%ld", test_index);
     mu_assert(
-        infrared_test_load_messages(test->ff, string_get_cstr(buf), &messages, &messages_count),
+        infrared_test_load_messages(
+            test->ff, furi_string_get_cstr(buf), &messages, &messages_count),
         "Failed to load messages from file");
 
     flipper_format_buffered_file_close(test->ff);
-    string_clear(buf);
+    furi_string_free(buf);
 
     InfraredMessage message_decoded_check_local;
     bool level = 0;

+ 128 - 9
applications/debug/unit_tests/nfc/nfc_test.c

@@ -3,6 +3,7 @@
 #include <storage/storage.h>
 #include <lib/flipper_format/flipper_format.h>
 #include <lib/nfc/protocols/nfca.h>
+#include <lib/nfc/helpers/mf_classic_dict.h>
 #include <lib/digital_signal/digital_signal.h>
 
 #include <lib/flipper_format/flipper_format_i.h>
@@ -15,6 +16,7 @@
 #define NFC_TEST_RESOURCES_DIR EXT_PATH("unit_tests/nfc/")
 #define NFC_TEST_SIGNAL_SHORT_FILE "nfc_nfca_signal_short.nfc"
 #define NFC_TEST_SIGNAL_LONG_FILE "nfc_nfca_signal_long.nfc"
+#define NFC_TEST_DICT_PATH EXT_PATH("unit_tests/mf_classic_dict.nfc")
 
 static const char* nfc_test_file_type = "Flipper NFC test";
 static const uint32_t nfc_test_file_version = 1;
@@ -52,14 +54,15 @@ static bool nfc_test_read_signal_from_file(const char* file_name) {
     bool success = false;
 
     FlipperFormat* file = flipper_format_file_alloc(nfc_test->storage);
-    string_t file_type;
-    string_init(file_type);
+    FuriString* file_type;
+    file_type = furi_string_alloc();
     uint32_t file_version = 0;
 
     do {
         if(!flipper_format_file_open_existing(file, file_name)) break;
         if(!flipper_format_read_header(file, file_type, &file_version)) break;
-        if(string_cmp_str(file_type, nfc_test_file_type) || file_version != nfc_test_file_version)
+        if(furi_string_cmp_str(file_type, nfc_test_file_type) ||
+           file_version != nfc_test_file_version)
             break;
         if(!flipper_format_read_uint32(file, "Data length", &nfc_test->test_data_len, 1)) break;
         if(nfc_test->test_data_len > NFC_TEST_DATA_MAX_LEN) break;
@@ -75,7 +78,7 @@ static bool nfc_test_read_signal_from_file(const char* file_name) {
         success = true;
     } while(false);
 
-    string_clear(file_type);
+    furi_string_free(file_type);
     flipper_format_free(file);
 
     return success;
@@ -110,7 +113,7 @@ static bool nfc_test_digital_signal_test_encode(
         // Check timings
         if(time > encode_max_time) {
             FURI_LOG_E(
-                TAG, "Encoding time: %d us while accepted value: %d us", time, encode_max_time);
+                TAG, "Encoding time: %ld us while accepted value: %ld us", time, encode_max_time);
             break;
         }
 
@@ -130,7 +133,7 @@ static bool nfc_test_digital_signal_test_encode(
             ref_timings_sum += ref[i];
             if(timings_diff > timing_tolerance) {
                 FURI_LOG_E(
-                    TAG, "Too big differece in %d timings. Ref: %d, DUT: %d", i, ref[i], dut[i]);
+                    TAG, "Too big differece in %d timings. Ref: %ld, DUT: %ld", i, ref[i], dut[i]);
                 timing_check_success = false;
                 break;
             }
@@ -141,16 +144,16 @@ static bool nfc_test_digital_signal_test_encode(
         if(sum_diff > timings_sum_tolerance) {
             FURI_LOG_E(
                 TAG,
-                "Too big difference in timings sum. Ref: %d, DUT: %d",
+                "Too big difference in timings sum. Ref: %ld, DUT: %ld",
                 ref_timings_sum,
                 dut_timings_sum);
             break;
         }
 
-        FURI_LOG_I(TAG, "Encoding time: %d us. Acceptable time: %d us", time, encode_max_time);
+        FURI_LOG_I(TAG, "Encoding time: %ld us. Acceptable time: %ld us", time, encode_max_time);
         FURI_LOG_I(
             TAG,
-            "Timings sum difference: %d [1/64MHZ]. Acceptable difference: %d [1/64MHz]",
+            "Timings sum difference: %ld [1/64MHZ]. Acceptable difference: %ld [1/64MHz]",
             sum_diff,
             timings_sum_tolerance);
         success = true;
@@ -170,10 +173,126 @@ MU_TEST(nfc_digital_signal_test) {
         "NFC long digital signal test failed\r\n");
 }
 
+MU_TEST(mf_classic_dict_test) {
+    MfClassicDict* instance = NULL;
+    uint64_t key = 0;
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+
+    instance = mf_classic_dict_alloc(MfClassicDictTypeUnitTest);
+    mu_assert(instance != NULL, "mf_classic_dict_alloc\r\n");
+
+    mu_assert(
+        mf_classic_dict_get_total_keys(instance) == 0,
+        "mf_classic_dict_get_total_keys == 0 assert failed\r\n");
+
+    furi_string_set(temp_str, "2196FAD8115B");
+    mu_assert(
+        mf_classic_dict_add_key_str(instance, temp_str),
+        "mf_classic_dict_add_key == true assert failed\r\n");
+
+    mu_assert(
+        mf_classic_dict_get_total_keys(instance) == 1,
+        "mf_classic_dict_get_total_keys == 1 assert failed\r\n");
+
+    mu_assert(mf_classic_dict_rewind(instance), "mf_classic_dict_rewind == 1 assert failed\r\n");
+
+    mu_assert(
+        mf_classic_dict_get_key_at_index_str(instance, temp_str, 0),
+        "mf_classic_dict_get_key_at_index_str == true assert failed\r\n");
+    mu_assert(
+        furi_string_cmp(temp_str, "2196FAD8115B") == 0,
+        "string_cmp(temp_str, \"2196FAD8115B\") == 0 assert failed\r\n");
+
+    mu_assert(mf_classic_dict_rewind(instance), "mf_classic_dict_rewind == 1 assert failed\r\n");
+
+    mu_assert(
+        mf_classic_dict_get_key_at_index(instance, &key, 0),
+        "mf_classic_dict_get_key_at_index == true assert failed\r\n");
+    mu_assert(key == 0x2196FAD8115B, "key == 0x2196FAD8115B assert failed\r\n");
+
+    mu_assert(mf_classic_dict_rewind(instance), "mf_classic_dict_rewind == 1 assert failed\r\n");
+
+    mu_assert(
+        mf_classic_dict_delete_index(instance, 0),
+        "mf_classic_dict_delete_index == true assert failed\r\n");
+
+    mf_classic_dict_free(instance);
+    furi_string_free(temp_str);
+}
+
+MU_TEST(mf_classic_dict_load_test) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    mu_assert(storage != NULL, "storage != NULL assert failed\r\n");
+
+    // Delete unit test dict file if exists
+    if(storage_file_exists(storage, NFC_TEST_DICT_PATH)) {
+        mu_assert(
+            storage_simply_remove(storage, NFC_TEST_DICT_PATH),
+            "remove == true assert failed\r\n");
+    }
+
+    // Create unit test dict file
+    Stream* file_stream = file_stream_alloc(storage);
+    mu_assert(file_stream != NULL, "file_stream != NULL assert failed\r\n");
+    mu_assert(
+        file_stream_open(file_stream, NFC_TEST_DICT_PATH, FSAM_WRITE, FSOM_OPEN_ALWAYS),
+        "file_stream_open == true assert failed\r\n");
+
+    // Write unit test dict file
+    char key_str[] = "a0a1a2a3a4a5";
+    mu_assert(
+        stream_write_cstring(file_stream, key_str) == strlen(key_str),
+        "write == true assert failed\r\n");
+    // Close unit test dict file
+    mu_assert(file_stream_close(file_stream), "file_stream_close == true assert failed\r\n");
+
+    // Load unit test dict file
+    MfClassicDict* instance = NULL;
+    instance = mf_classic_dict_alloc(MfClassicDictTypeUnitTest);
+    mu_assert(instance != NULL, "mf_classic_dict_alloc\r\n");
+    uint32_t total_keys = mf_classic_dict_get_total_keys(instance);
+    mu_assert(total_keys == 1, "total_keys == 1 assert failed\r\n");
+
+    // Read key
+    uint64_t key_ref = 0xa0a1a2a3a4a5;
+    uint64_t key_dut = 0;
+    FuriString* temp_str = furi_string_alloc();
+    mu_assert(
+        mf_classic_dict_get_next_key_str(instance, temp_str),
+        "get_next_key_str == true assert failed\r\n");
+    mu_assert(furi_string_cmp_str(temp_str, key_str) == 0, "invalid key loaded\r\n");
+    mu_assert(mf_classic_dict_rewind(instance), "mf_classic_dict_rewind == 1 assert failed\r\n");
+    mu_assert(
+        mf_classic_dict_get_next_key(instance, &key_dut),
+        "get_next_key == true assert failed\r\n");
+    mu_assert(key_dut == key_ref, "invalid key loaded\r\n");
+    furi_string_free(temp_str);
+    mf_classic_dict_free(instance);
+
+    // Check that MfClassicDict added new line to the end of the file
+    mu_assert(
+        file_stream_open(file_stream, NFC_TEST_DICT_PATH, FSAM_READ, FSOM_OPEN_EXISTING),
+        "file_stream_open == true assert failed\r\n");
+    mu_assert(stream_seek(file_stream, -1, StreamOffsetFromEnd), "seek == true assert failed\r\n");
+    uint8_t last_char = 0;
+    mu_assert(stream_read(file_stream, &last_char, 1) == 1, "read == true assert failed\r\n");
+    mu_assert(last_char == '\n', "last_char == '\\n' assert failed\r\n");
+    mu_assert(file_stream_close(file_stream), "file_stream_close == true assert failed\r\n");
+
+    // Delete unit test dict file
+    mu_assert(
+        storage_simply_remove(storage, NFC_TEST_DICT_PATH), "remove == true assert failed\r\n");
+    stream_free(file_stream);
+    furi_record_close(RECORD_STORAGE);
+}
+
 MU_TEST_SUITE(nfc) {
     nfc_test_alloc();
 
     MU_RUN_TEST(nfc_digital_signal_test);
+    MU_RUN_TEST(mf_classic_dict_test);
+    MU_RUN_TEST(mf_classic_dict_load_test);
 
     nfc_test_free();
 }

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

@@ -10,7 +10,6 @@
 #include <furi.h>
 #include "../minunit.h"
 #include <stdint.h>
-#include <stream_buffer.h>
 #include <pb.h>
 #include <pb_encode.h>
 #include <m-list.h>
@@ -34,7 +33,7 @@ static uint32_t command_id = 0;
 
 typedef struct {
     RpcSession* session;
-    StreamBufferHandle_t output_stream;
+    FuriStreamBuffer* output_stream;
     SemaphoreHandle_t close_session_semaphore;
     SemaphoreHandle_t terminate_semaphore;
     TickType_t timeout;
@@ -90,7 +89,7 @@ static void test_rpc_setup(void) {
     }
     furi_check(rpc_session[0].session);
 
-    rpc_session[0].output_stream = xStreamBufferCreate(1000, 1);
+    rpc_session[0].output_stream = furi_stream_buffer_alloc(1000, 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();
@@ -110,7 +109,7 @@ static void test_rpc_setup_second_session(void) {
     }
     furi_check(rpc_session[1].session);
 
-    rpc_session[1].output_stream = xStreamBufferCreate(1000, 1);
+    rpc_session[1].output_stream = furi_stream_buffer_alloc(1000, 1);
     rpc_session_set_send_bytes_callback(rpc_session[1].session, output_bytes_callback);
     rpc_session[1].close_session_semaphore = xSemaphoreCreateBinary();
     rpc_session[1].terminate_semaphore = xSemaphoreCreateBinary();
@@ -126,7 +125,7 @@ static void test_rpc_teardown(void) {
     rpc_session_close(rpc_session[0].session);
     furi_check(xSemaphoreTake(rpc_session[0].terminate_semaphore, portMAX_DELAY));
     furi_record_close(RECORD_RPC);
-    vStreamBufferDelete(rpc_session[0].output_stream);
+    furi_stream_buffer_free(rpc_session[0].output_stream);
     vSemaphoreDelete(rpc_session[0].close_session_semaphore);
     vSemaphoreDelete(rpc_session[0].terminate_semaphore);
     ++command_id;
@@ -141,7 +140,7 @@ static void test_rpc_teardown_second_session(void) {
     xSemaphoreTake(rpc_session[1].terminate_semaphore, 0);
     rpc_session_close(rpc_session[1].session);
     furi_check(xSemaphoreTake(rpc_session[1].terminate_semaphore, portMAX_DELAY));
-    vStreamBufferDelete(rpc_session[1].output_stream);
+    furi_stream_buffer_free(rpc_session[1].output_stream);
     vSemaphoreDelete(rpc_session[1].close_session_semaphore);
     vSemaphoreDelete(rpc_session[1].terminate_semaphore);
     ++command_id;
@@ -268,8 +267,8 @@ static PB_CommandStatus test_rpc_storage_get_file_error(File* file) {
 static void output_bytes_callback(void* ctx, uint8_t* got_bytes, size_t got_size) {
     RpcSessionContext* callbacks_context = ctx;
 
-    size_t bytes_sent =
-        xStreamBufferSend(callbacks_context->output_stream, got_bytes, got_size, FuriWaitForever);
+    size_t bytes_sent = furi_stream_buffer_send(
+        callbacks_context->output_stream, got_bytes, got_size, FuriWaitForever);
     (void)bytes_sent;
     furi_check(bytes_sent == got_size);
 }
@@ -534,7 +533,8 @@ static bool test_rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_
     TickType_t now = xTaskGetTickCount();
     int32_t time_left = session_context->timeout - now;
     time_left = MAX(time_left, 0);
-    bytes_received = xStreamBufferReceive(session_context->output_stream, buf, count, time_left);
+    bytes_received =
+        furi_stream_buffer_receive(session_context->output_stream, buf, count, time_left);
     return (count == bytes_received);
 }
 

+ 24 - 24
applications/debug/unit_tests/storage/dirwalk_test.c

@@ -75,7 +75,7 @@ typedef struct {
     bool visited;
 } StorageTestPath;
 
-DICT_DEF2(StorageTestPathDict, string_t, STRING_OPLIST, StorageTestPath, M_POD_OPLIST)
+DICT_DEF2(StorageTestPathDict, FuriString*, FURI_STRING_OPLIST, StorageTestPath, M_POD_OPLIST)
 
 static StorageTestPathDict_t*
     storage_test_paths_alloc(const StorageTestPathDesc paths[], size_t paths_count) {
@@ -83,15 +83,15 @@ static StorageTestPathDict_t*
     StorageTestPathDict_init(*data);
 
     for(size_t i = 0; i < paths_count; i++) {
-        string_t key;
-        string_init_set(key, paths[i].path);
+        FuriString* key;
+        key = furi_string_alloc_set(paths[i].path);
         StorageTestPath value = {
             .is_dir = paths[i].is_dir,
             .visited = false,
         };
 
         StorageTestPathDict_set_at(*data, key, value);
-        string_clear(key);
+        furi_string_free(key);
     }
 
     return data;
@@ -102,7 +102,7 @@ static void storage_test_paths_free(StorageTestPathDict_t* data) {
     free(data);
 }
 
-static bool storage_test_paths_mark(StorageTestPathDict_t* data, string_t path, bool is_dir) {
+static bool storage_test_paths_mark(StorageTestPathDict_t* data, FuriString* path, bool is_dir) {
     bool found = false;
 
     StorageTestPath* record = StorageTestPathDict_get(*data, path);
@@ -148,27 +148,27 @@ static bool write_file_13DA(Storage* storage, const char* path) {
 }
 
 static void storage_dirs_create(Storage* storage, const char* base) {
-    string_t path;
-    string_init(path);
+    FuriString* path;
+    path = furi_string_alloc();
 
     storage_common_mkdir(storage, base);
 
     for(size_t i = 0; i < COUNT_OF(storage_test_dirwalk_paths); i++) {
-        string_printf(path, "%s/%s", base, storage_test_dirwalk_paths[i]);
-        storage_common_mkdir(storage, string_get_cstr(path));
+        furi_string_printf(path, "%s/%s", base, storage_test_dirwalk_paths[i]);
+        storage_common_mkdir(storage, furi_string_get_cstr(path));
     }
 
     for(size_t i = 0; i < COUNT_OF(storage_test_dirwalk_files); i++) {
-        string_printf(path, "%s/%s", base, storage_test_dirwalk_files[i]);
-        write_file_13DA(storage, string_get_cstr(path));
+        furi_string_printf(path, "%s/%s", base, storage_test_dirwalk_files[i]);
+        write_file_13DA(storage, furi_string_get_cstr(path));
     }
 
-    string_clear(path);
+    furi_string_free(path);
 }
 
 MU_TEST_1(test_dirwalk_full, Storage* storage) {
-    string_t path;
-    string_init(path);
+    FuriString* path;
+    path = furi_string_alloc();
     FileInfo fileinfo;
 
     StorageTestPathDict_t* paths =
@@ -178,12 +178,12 @@ MU_TEST_1(test_dirwalk_full, Storage* storage) {
     mu_check(dir_walk_open(dir_walk, EXT_PATH("dirwalk")));
 
     while(dir_walk_read(dir_walk, path, &fileinfo) == DirWalkOK) {
-        string_right(path, strlen(EXT_PATH("dirwalk/")));
+        furi_string_right(path, strlen(EXT_PATH("dirwalk/")));
         mu_check(storage_test_paths_mark(paths, path, (fileinfo.flags & FSF_DIRECTORY)));
     }
 
     dir_walk_free(dir_walk);
-    string_clear(path);
+    furi_string_free(path);
 
     mu_check(storage_test_paths_check(paths) == false);
 
@@ -191,8 +191,8 @@ MU_TEST_1(test_dirwalk_full, Storage* storage) {
 }
 
 MU_TEST_1(test_dirwalk_no_recursive, Storage* storage) {
-    string_t path;
-    string_init(path);
+    FuriString* path;
+    path = furi_string_alloc();
     FileInfo fileinfo;
 
     StorageTestPathDict_t* paths = storage_test_paths_alloc(
@@ -203,12 +203,12 @@ MU_TEST_1(test_dirwalk_no_recursive, Storage* storage) {
     mu_check(dir_walk_open(dir_walk, EXT_PATH("dirwalk")));
 
     while(dir_walk_read(dir_walk, path, &fileinfo) == DirWalkOK) {
-        string_right(path, strlen(EXT_PATH("dirwalk/")));
+        furi_string_right(path, strlen(EXT_PATH("dirwalk/")));
         mu_check(storage_test_paths_mark(paths, path, (fileinfo.flags & FSF_DIRECTORY)));
     }
 
     dir_walk_free(dir_walk);
-    string_clear(path);
+    furi_string_free(path);
 
     mu_check(storage_test_paths_check(paths) == false);
 
@@ -230,8 +230,8 @@ static bool test_dirwalk_filter_no_folder_ext(const char* name, FileInfo* filein
 }
 
 MU_TEST_1(test_dirwalk_filter, Storage* storage) {
-    string_t path;
-    string_init(path);
+    FuriString* path;
+    path = furi_string_alloc();
     FileInfo fileinfo;
 
     StorageTestPathDict_t* paths = storage_test_paths_alloc(
@@ -242,12 +242,12 @@ MU_TEST_1(test_dirwalk_filter, Storage* storage) {
     mu_check(dir_walk_open(dir_walk, EXT_PATH("dirwalk")));
 
     while(dir_walk_read(dir_walk, path, &fileinfo) == DirWalkOK) {
-        string_right(path, strlen(EXT_PATH("dirwalk/")));
+        furi_string_right(path, strlen(EXT_PATH("dirwalk/")));
         mu_check(storage_test_paths_mark(paths, path, (fileinfo.flags & FSF_DIRECTORY)));
     }
 
     dir_walk_free(dir_walk);
-    string_clear(path);
+    furi_string_free(path);
 
     mu_check(storage_test_paths_check(paths) == false);
 

+ 16 - 16
applications/debug/unit_tests/storage/storage_test.c

@@ -58,7 +58,7 @@ MU_TEST(storage_file_open_lock) {
     storage_file_close(file);
 
     // file_locker thread stop
-    mu_check(furi_thread_join(locker_thread) == FuriStatusOk);
+    mu_check(furi_thread_join(locker_thread));
     furi_thread_free(locker_thread);
 
     // clean data
@@ -148,7 +148,7 @@ MU_TEST(storage_dir_open_lock) {
     storage_dir_close(file);
 
     // file_locker thread stop
-    mu_check(furi_thread_join(locker_thread) == FuriStatusOk);
+    mu_check(furi_thread_join(locker_thread));
     furi_thread_free(locker_thread);
 
     // clean data
@@ -211,22 +211,22 @@ static bool check_file_13DA(Storage* storage, const char* path) {
 }
 
 static void storage_dir_create(Storage* storage, const char* base) {
-    string_t path;
-    string_init(path);
+    FuriString* path;
+    path = furi_string_alloc();
 
     storage_common_mkdir(storage, base);
 
     for(size_t i = 0; i < COUNT_OF(storage_copy_test_paths); i++) {
-        string_printf(path, "%s/%s", base, storage_copy_test_paths[i]);
-        storage_common_mkdir(storage, string_get_cstr(path));
+        furi_string_printf(path, "%s/%s", base, storage_copy_test_paths[i]);
+        storage_common_mkdir(storage, furi_string_get_cstr(path));
     }
 
     for(size_t i = 0; i < COUNT_OF(storage_copy_test_files); i++) {
-        string_printf(path, "%s/%s", base, storage_copy_test_files[i]);
-        write_file_13DA(storage, string_get_cstr(path));
+        furi_string_printf(path, "%s/%s", base, storage_copy_test_files[i]);
+        write_file_13DA(storage, furi_string_get_cstr(path));
     }
 
-    string_clear(path);
+    furi_string_free(path);
 }
 
 static void storage_dir_remove(Storage* storage, const char* base) {
@@ -235,15 +235,15 @@ static void storage_dir_remove(Storage* storage, const char* base) {
 
 static bool storage_dir_rename_check(Storage* storage, const char* base) {
     bool result = false;
-    string_t path;
-    string_init(path);
+    FuriString* path;
+    path = furi_string_alloc();
 
     result = (storage_common_stat(storage, base, NULL) == FSE_OK);
 
     if(result) {
         for(size_t i = 0; i < COUNT_OF(storage_copy_test_paths); i++) {
-            string_printf(path, "%s/%s", base, storage_copy_test_paths[i]);
-            result = (storage_common_stat(storage, string_get_cstr(path), NULL) == FSE_OK);
+            furi_string_printf(path, "%s/%s", base, storage_copy_test_paths[i]);
+            result = (storage_common_stat(storage, furi_string_get_cstr(path), NULL) == FSE_OK);
             if(!result) {
                 break;
             }
@@ -252,15 +252,15 @@ static bool storage_dir_rename_check(Storage* storage, const char* base) {
 
     if(result) {
         for(size_t i = 0; i < COUNT_OF(storage_copy_test_files); i++) {
-            string_printf(path, "%s/%s", base, storage_copy_test_files[i]);
-            result = check_file_13DA(storage, string_get_cstr(path));
+            furi_string_printf(path, "%s/%s", base, storage_copy_test_files[i]);
+            result = check_file_13DA(storage, furi_string_get_cstr(path));
             if(!result) {
                 break;
             }
         }
     }
 
-    string_clear(path);
+    furi_string_free(path);
     return result;
 }
 

+ 18 - 18
applications/debug/unit_tests/stream/stream_test.c

@@ -18,8 +18,8 @@ static const char* stream_test_right_data =
 MU_TEST_1(stream_composite_subtest, Stream* stream) {
     const size_t data_size = 128;
     uint8_t data[data_size];
-    string_t string_lee;
-    string_init_set(string_lee, "lee");
+    FuriString* string_lee;
+    string_lee = furi_string_alloc_set("lee");
 
     // test that stream is empty
     // "" -> ""
@@ -267,7 +267,7 @@ MU_TEST_1(stream_composite_subtest, Stream* stream) {
     mu_assert_int_eq(9, stream_tell(stream));
     mu_check(stream_eof(stream));
 
-    string_clear(string_lee);
+    furi_string_free(string_lee);
 }
 
 MU_TEST(stream_composite_test) {
@@ -416,10 +416,10 @@ MU_TEST(stream_buffered_write_after_read_test) {
 }
 
 MU_TEST(stream_buffered_large_file_test) {
-    string_t input_data;
-    string_t output_data;
-    string_init(input_data);
-    string_init(output_data);
+    FuriString* input_data;
+    FuriString* output_data;
+    input_data = furi_string_alloc();
+    output_data = furi_string_alloc();
 
     Storage* storage = furi_record_open(RECORD_STORAGE);
 
@@ -429,7 +429,7 @@ MU_TEST(stream_buffered_large_file_test) {
     const size_t rep_count = data_size / line_size + 1;
 
     for(size_t i = 0; i < rep_count; ++i) {
-        string_cat_printf(input_data, "%s\n", stream_test_data);
+        furi_string_cat_printf(input_data, "%s\n", stream_test_data);
     }
 
     // write test data to file
@@ -437,8 +437,8 @@ MU_TEST(stream_buffered_large_file_test) {
     mu_check(buffered_file_stream_open(
         stream, EXT_PATH("filestream.str"), FSAM_READ_WRITE, FSOM_CREATE_ALWAYS));
     mu_assert_int_eq(0, stream_size(stream));
-    mu_assert_int_eq(string_size(input_data), stream_write_string(stream, input_data));
-    mu_assert_int_eq(string_size(input_data), stream_size(stream));
+    mu_assert_int_eq(furi_string_size(input_data), stream_write_string(stream, input_data));
+    mu_assert_int_eq(furi_string_size(input_data), stream_size(stream));
 
     const size_t substr_start = 8;
     const size_t substr_len = 11;
@@ -475,23 +475,23 @@ MU_TEST(stream_buffered_large_file_test) {
 
     // read the whole file
     mu_check(stream_rewind(stream));
-    string_t tmp;
-    string_init(tmp);
+    FuriString* tmp;
+    tmp = furi_string_alloc();
     while(stream_read_line(stream, tmp)) {
-        string_cat(output_data, tmp);
+        furi_string_cat(output_data, tmp);
     }
-    string_clear(tmp);
+    furi_string_free(tmp);
 
     // check against generated data
-    mu_assert_int_eq(string_size(input_data), string_size(output_data));
-    mu_check(string_equal_p(input_data, output_data));
+    mu_assert_int_eq(furi_string_size(input_data), furi_string_size(output_data));
+    mu_check(furi_string_equal(input_data, output_data));
     mu_check(stream_eof(stream));
 
     stream_free(stream);
 
     furi_record_close(RECORD_STORAGE);
-    string_clear(input_data);
-    string_clear(output_data);
+    furi_string_free(input_data);
+    furi_string_free(output_data);
 }
 
 MU_TEST_SUITE(stream_suite) {

+ 22 - 19
applications/debug/unit_tests/subghz/subghz_test.c

@@ -5,7 +5,7 @@
 #include <lib/subghz/transmitter.h>
 #include <lib/subghz/subghz_keystore.h>
 #include <lib/subghz/subghz_file_encoder_worker.h>
-#include <lib/subghz/protocols/registry.h>
+#include <lib/subghz/protocols/protocol_items.h>
 #include <flipper_format/flipper_format_i.h>
 
 #define TAG "SubGhz TEST"
@@ -28,12 +28,12 @@ static void subghz_test_rx_callback(
     void* context) {
     UNUSED(receiver);
     UNUSED(context);
-    string_t text;
-    string_init(text);
+    FuriString* text;
+    text = furi_string_alloc();
     subghz_protocol_decoder_base_get_string(decoder_base, text);
     subghz_receiver_reset(receiver_handler);
-    FURI_LOG_T(TAG, "\r\n%s", string_get_cstr(text));
-    string_clear(text);
+    FURI_LOG_T(TAG, "\r\n%s", furi_string_get_cstr(text));
+    furi_string_free(text);
     subghz_test_decoder_count++;
 }
 
@@ -43,6 +43,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_protocol_registry(
+        environment_handler, (void*)&subghz_protocol_registry);
 
     receiver_handler = subghz_receiver_alloc_init(environment_handler);
     subghz_receiver_set_filter(receiver_handler, SubGhzProtocolFlag_Decodable);
@@ -141,8 +143,8 @@ static bool subghz_decode_random_test(const char* path) {
 static bool subghz_encoder_test(const char* path) {
     subghz_test_decoder_count = 0;
     uint32_t test_start = furi_get_tick();
-    string_t temp_str;
-    string_init(temp_str);
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
     bool file_load = false;
 
     Storage* storage = furi_record_open(RECORD_STORAGE);
@@ -167,11 +169,11 @@ static bool subghz_encoder_test(const char* path) {
     } while(false);
     if(file_load) {
         SubGhzTransmitter* transmitter =
-            subghz_transmitter_alloc_init(environment_handler, string_get_cstr(temp_str));
+            subghz_transmitter_alloc_init(environment_handler, furi_string_get_cstr(temp_str));
         subghz_transmitter_deserialize(transmitter, fff_data_file);
 
         SubGhzProtocolDecoderBase* decoder = subghz_receiver_search_decoder_base_by_name(
-            receiver_handler, string_get_cstr(temp_str));
+            receiver_handler, furi_string_get_cstr(temp_str));
 
         if(decoder) {
             LevelDuration level_duration;
@@ -192,10 +194,11 @@ static bool subghz_encoder_test(const char* path) {
     flipper_format_free(fff_data_file);
     FURI_LOG_T(TAG, "\r\n Decoder count parse \033[0;33m%d\033[0m ", subghz_test_decoder_count);
     if(furi_get_tick() - test_start > TEST_TIMEOUT) {
-        printf("\033[0;31mTest encoder %s ERROR TimeOut\033[0m\r\n", string_get_cstr(temp_str));
+        printf(
+            "\033[0;31mTest encoder %s ERROR TimeOut\033[0m\r\n", furi_string_get_cstr(temp_str));
         subghz_test_decoder_count = 0;
     }
-    string_clear(temp_str);
+    furi_string_free(temp_str);
 
     return subghz_test_decoder_count ? true : false;
 }
@@ -412,11 +415,11 @@ MU_TEST(subghz_decoder_honeywell_wdb_test) {
         "Test decoder " SUBGHZ_PROTOCOL_HONEYWELL_WDB_NAME " error\r\n");
 }
 
-MU_TEST(subghz_decoder_magellen_test) {
+MU_TEST(subghz_decoder_magellan_test) {
     mu_assert(
         subghz_decoder_test(
-            EXT_PATH("unit_tests/subghz/magellen_raw.sub"), SUBGHZ_PROTOCOL_MAGELLEN_NAME),
-        "Test decoder " SUBGHZ_PROTOCOL_MAGELLEN_NAME " error\r\n");
+            EXT_PATH("unit_tests/subghz/magellan_raw.sub"), SUBGHZ_PROTOCOL_MAGELLAN_NAME),
+        "Test decoder " SUBGHZ_PROTOCOL_MAGELLAN_NAME " error\r\n");
 }
 
 MU_TEST(subghz_decoder_intertechno_v3_test) {
@@ -537,10 +540,10 @@ MU_TEST(subghz_encoder_honeywell_wdb_test) {
         "Test encoder " SUBGHZ_PROTOCOL_HONEYWELL_WDB_NAME " error\r\n");
 }
 
-MU_TEST(subghz_encoder_magellen_test) {
+MU_TEST(subghz_encoder_magellan_test) {
     mu_assert(
-        subghz_encoder_test(EXT_PATH("unit_tests/subghz/magellen.sub")),
-        "Test encoder " SUBGHZ_PROTOCOL_MAGELLEN_NAME " error\r\n");
+        subghz_encoder_test(EXT_PATH("unit_tests/subghz/magellan.sub")),
+        "Test encoder " SUBGHZ_PROTOCOL_MAGELLAN_NAME " error\r\n");
 }
 
 MU_TEST(subghz_encoder_intertechno_v3_test) {
@@ -592,7 +595,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_decoder_doitrand_test);
     MU_RUN_TEST(subghz_decoder_phoenix_v2_test);
     MU_RUN_TEST(subghz_decoder_honeywell_wdb_test);
-    MU_RUN_TEST(subghz_decoder_magellen_test);
+    MU_RUN_TEST(subghz_decoder_magellan_test);
     MU_RUN_TEST(subghz_decoder_intertechno_v3_test);
     MU_RUN_TEST(subghz_decoder_clemsa_test);
 
@@ -613,7 +616,7 @@ MU_TEST_SUITE(subghz) {
     MU_RUN_TEST(subghz_encoder_doitrand_test);
     MU_RUN_TEST(subghz_encoder_phoenix_v2_test);
     MU_RUN_TEST(subghz_encoder_honeywell_wdb_test);
-    MU_RUN_TEST(subghz_encoder_magellen_test);
+    MU_RUN_TEST(subghz_encoder_magellan_test);
     MU_RUN_TEST(subghz_encoder_intertechno_v3_test);
     MU_RUN_TEST(subghz_encoder_clemsa_test);
 

+ 5 - 5
applications/debug/unit_tests/test_index.c

@@ -1,5 +1,3 @@
-#include "m-string.h"
-
 #include <stdio.h>
 #include <furi.h>
 #include <furi_hal.h>
@@ -11,6 +9,7 @@
 #define TAG "UnitTests"
 
 int run_minunit_test_furi();
+int run_minunit_test_furi_string();
 int run_minunit_test_infrared();
 int run_minunit_test_rpc();
 int run_minunit_test_flipper_format();
@@ -33,6 +32,7 @@ typedef struct {
 
 const UnitTest unit_tests[] = {
     {.name = "furi", .entry = run_minunit_test_furi},
+    {.name = "furi_string", .entry = run_minunit_test_furi_string},
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "stream", .entry = run_minunit_test_stream},
     {.name = "dirwalk", .entry = run_minunit_test_dirwalk},
@@ -63,7 +63,7 @@ void minunit_print_fail(const char* str) {
     printf(FURI_LOG_CLR_E "%s\r\n" FURI_LOG_CLR_RESET, str);
 }
 
-void unit_tests_cli(Cli* cli, string_t args, void* context) {
+void unit_tests_cli(Cli* cli, FuriString* args, void* context) {
     UNUSED(cli);
     UNUSED(args);
     UNUSED(context);
@@ -91,8 +91,8 @@ void unit_tests_cli(Cli* cli, string_t args, void* context) {
                 break;
             }
 
-            if(string_size(args)) {
-                if(string_cmp_str(args, unit_tests[i].name) == 0) {
+            if(furi_string_size(args)) {
+                if(furi_string_cmp_str(args, unit_tests[i].name) == 0) {
                     failed_tests += unit_tests[i].entry();
                 } else {
                     printf("Skipping %s\r\n", unit_tests[i].name);

+ 5 - 0
applications/examples/application.fam

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

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

@@ -0,0 +1,10 @@
+App(
+    appid="example_images",
+    name="Example: Images",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="example_images_main",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    fap_category="Examples",
+    fap_icon_assets="images",
+)

+ 81 - 0
applications/examples/example_images/example_images.c

@@ -0,0 +1,81 @@
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <gui/gui.h>
+#include <input/input.h>
+
+/* Magic happens here -- this file is generated by fbt.
+ * Just set fap_icon_assets in application.fam and #include {APPID}_icons.h */
+#include "example_images_icons.h"
+
+typedef struct {
+    uint8_t x, y;
+} ImagePosition;
+
+static ImagePosition image_position = {.x = 0, .y = 0};
+
+// Screen is 128x64 px
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    UNUSED(ctx);
+
+    canvas_clear(canvas);
+    canvas_draw_icon(canvas, image_position.x % 128, image_position.y % 64, &I_dolphin_71x25);
+}
+
+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_images_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 InputKeyLeft:
+                    image_position.x -= 2;
+                    break;
+                case InputKeyRight:
+                    image_position.x += 2;
+                    break;
+                case InputKeyUp:
+                    image_position.y -= 2;
+                    break;
+                case InputKeyDown:
+                    image_position.y += 2;
+                    break;
+                default:
+                    running = false;
+                    break;
+                }
+            }
+        }
+        view_port_update(view_port);
+    }
+
+    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;
+}

BIN
applications/examples/example_images/images/dolphin_71x25.png


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

@@ -1,5 +1,4 @@
 #include "archive_i.h"
-#include "m-string.h"
 
 bool archive_custom_event_callback(void* context, uint32_t event) {
     furi_assert(context);
@@ -18,7 +17,7 @@ ArchiveApp* archive_alloc() {
 
     archive->gui = furi_record_open(RECORD_GUI);
     archive->text_input = text_input_alloc();
-    string_init(archive->fav_move_str);
+    archive->fav_move_str = furi_string_alloc();
 
     archive->view_dispatcher = view_dispatcher_alloc();
     archive->scene_manager = scene_manager_alloc(&archive_scene_handlers, archive);
@@ -58,7 +57,7 @@ void archive_free(ArchiveApp* archive) {
     view_dispatcher_free(archive->view_dispatcher);
     scene_manager_free(archive->scene_manager);
     browser_free(archive->browser);
-    string_clear(archive->fav_move_str);
+    furi_string_free(archive->fav_move_str);
 
     text_input_free(archive->text_input);
 

+ 1 - 1
applications/main/archive/archive_i.h

@@ -28,7 +28,7 @@ struct ArchiveApp {
     TextInput* text_input;
     Widget* widget;
     FuriPubSubSubscription* loader_stop_subscription;
-    string_t fav_move_str;
+    FuriString* fav_move_str;
     char text_store[MAX_NAME_LEN];
     char file_extension[MAX_EXT_LEN + 1];
 };

+ 129 - 110
applications/main/archive/helpers/archive_browser.c

@@ -5,7 +5,7 @@
 #include <core/common_defines.h>
 #include <core/log.h>
 #include "gui/modules/file_browser_worker.h"
-#include "m-string.h"
+#include <fap_loader/fap_loader_app.h>
 #include <math.h>
 
 static void
@@ -19,9 +19,11 @@ static void
 
     if((item_cnt == 0) && (archive_is_home(browser)) && (tab != ArchiveTabBrowser)) {
         archive_switch_tab(browser, browser->last_tab_switch_dir);
-    } else if(!string_start_with_str_p(browser->path, "/app:")) {
+    } else if(!furi_string_start_with_str(browser->path, "/app:")) {
         with_view_model(
-            browser->view, (ArchiveBrowserViewModel * model) {
+            browser->view,
+            ArchiveBrowserViewModel * model,
+            {
                 files_array_reset(model->files);
                 model->item_cnt = item_cnt;
                 model->item_idx = (file_idx > 0) ? file_idx : 0;
@@ -31,8 +33,8 @@ static void
                 model->list_offset = 0;
                 model->list_loading = true;
                 model->folder_loading = false;
-                return false;
-            });
+            },
+            false);
         archive_update_offset(browser);
 
         file_browser_worker_load(browser->worker, load_offset, FILE_LIST_BUF_LEN);
@@ -44,25 +46,25 @@ static void archive_list_load_cb(void* context, uint32_t list_load_offset) {
     ArchiveBrowserView* browser = (ArchiveBrowserView*)context;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             files_array_reset(model->files);
             model->array_offset = list_load_offset;
-            return false;
-        });
+        },
+        false);
 }
 
-static void archive_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last) {
+static void
+    archive_list_item_cb(void* context, FuriString* item_path, bool is_folder, bool is_last) {
     furi_assert(context);
     ArchiveBrowserView* browser = (ArchiveBrowserView*)context;
 
     if(!is_last) {
-        archive_add_file_item(browser, is_folder, string_get_cstr(item_path));
+        archive_add_file_item(browser, is_folder, furi_string_get_cstr(item_path));
     } else {
         with_view_model(
-            browser->view, (ArchiveBrowserViewModel * model) {
-                model->list_loading = false;
-                return true;
-            });
+            browser->view, ArchiveBrowserViewModel * model, { model->list_loading = false; }, true);
     }
 }
 
@@ -71,15 +73,12 @@ static void archive_long_load_cb(void* context) {
     ArchiveBrowserView* browser = (ArchiveBrowserView*)context;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            model->folder_loading = true;
-            return true;
-        });
+        browser->view, ArchiveBrowserViewModel * model, { model->folder_loading = true; }, true);
 }
 
 static void archive_file_browser_set_path(
     ArchiveBrowserView* browser,
-    string_t path,
+    FuriString* path,
     const char* filter_ext,
     bool skip_assets) {
     furi_assert(browser);
@@ -112,7 +111,9 @@ void archive_update_offset(ArchiveBrowserView* browser) {
     furi_assert(browser);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             uint16_t bounds = model->item_cnt > 3 ? 2 : model->item_cnt;
 
             if((model->item_cnt > 3u) && (model->item_idx >= ((int32_t)model->item_cnt - 1))) {
@@ -124,33 +125,34 @@ void archive_update_offset(ArchiveBrowserView* browser) {
                 model->list_offset =
                     CLAMP(model->item_idx - 1, (int32_t)model->item_cnt - bounds, 0);
             }
-
-            return true;
-        });
+        },
+        true);
 }
 
 void archive_update_focus(ArchiveBrowserView* browser, const char* target) {
     furi_assert(browser);
     furi_assert(target);
 
-    archive_get_items(browser, string_get_cstr(browser->path));
+    archive_get_items(browser, furi_string_get_cstr(browser->path));
 
     if(!archive_file_get_array_size(browser) && archive_is_home(browser)) {
         archive_switch_tab(browser, TAB_RIGHT);
     } else {
         with_view_model(
-            browser->view, (ArchiveBrowserViewModel * model) {
+            browser->view,
+            ArchiveBrowserViewModel * model,
+            {
                 uint16_t idx = 0;
                 while(idx < files_array_size(model->files)) {
                     ArchiveFile_t* current = files_array_get(model->files, idx);
-                    if(!string_search(current->path, target)) {
+                    if(!furi_string_search(current->path, target)) {
                         model->item_idx = idx + model->array_offset;
                         break;
                     }
                     ++idx;
                 }
-                return false;
-            });
+            },
+            false);
 
         archive_update_offset(browser);
     }
@@ -161,10 +163,10 @@ size_t archive_file_get_array_size(ArchiveBrowserView* browser) {
 
     uint16_t size = 0;
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            size = files_array_size(model->files);
-            return false;
-        });
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        { size = files_array_size(model->files); },
+        false);
     return size;
 }
 
@@ -172,11 +174,13 @@ void archive_set_item_count(ArchiveBrowserView* browser, uint32_t count) {
     furi_assert(browser);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             model->item_cnt = count;
             model->item_idx = CLAMP(model->item_idx, (int32_t)model->item_cnt - 1, 0);
-            return false;
-        });
+        },
+        false);
     archive_update_offset(browser);
 }
 
@@ -185,7 +189,9 @@ void archive_file_array_rm_selected(ArchiveBrowserView* browser) {
     uint32_t items_cnt = 0;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             files_array_remove_v(
                 model->files,
                 model->item_idx - model->array_offset,
@@ -193,8 +199,8 @@ void archive_file_array_rm_selected(ArchiveBrowserView* browser) {
             model->item_cnt--;
             model->item_idx = CLAMP(model->item_idx, (int32_t)model->item_cnt - 1, 0);
             items_cnt = model->item_cnt;
-            return false;
-        });
+        },
+        false);
 
     if((items_cnt == 0) && (archive_is_home(browser))) {
         archive_switch_tab(browser, TAB_RIGHT);
@@ -207,7 +213,9 @@ void archive_file_array_swap(ArchiveBrowserView* browser, int8_t dir) {
     furi_assert(browser);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             ArchiveFile_t temp;
             size_t array_size = files_array_size(model->files) - 1;
             uint8_t swap_idx = CLAMP((size_t)(model->item_idx + dir), array_size, 0u);
@@ -225,18 +233,18 @@ void archive_file_array_swap(ArchiveBrowserView* browser, int8_t dir) {
             } else {
                 files_array_swap_at(model->files, model->item_idx, swap_idx);
             }
-            return false;
-        });
+        },
+        false);
 }
 
 void archive_file_array_rm_all(ArchiveBrowserView* browser) {
     furi_assert(browser);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            files_array_reset(model->files);
-            return false;
-        });
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        { files_array_reset(model->files); },
+        false);
 }
 
 void archive_file_array_load(ArchiveBrowserView* browser, int8_t dir) {
@@ -245,7 +253,9 @@ void archive_file_array_load(ArchiveBrowserView* browser, int8_t dir) {
     int32_t offset_new = 0;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             if(model->item_cnt > FILE_LIST_BUF_LEN) {
                 if(dir < 0) {
                     offset_new = model->item_idx - FILE_LIST_BUF_LEN / 4 * 3;
@@ -261,8 +271,8 @@ void archive_file_array_load(ArchiveBrowserView* browser, int8_t dir) {
                     offset_new = 0;
                 }
             }
-            return false;
-        });
+        },
+        false);
 
     file_browser_worker_load(browser->worker, offset_new, FILE_LIST_BUF_LEN);
 }
@@ -270,40 +280,41 @@ void archive_file_array_load(ArchiveBrowserView* browser, int8_t dir) {
 ArchiveFile_t* archive_get_current_file(ArchiveBrowserView* browser) {
     furi_assert(browser);
 
-    ArchiveFile_t* selected;
+    ArchiveFile_t* selected = NULL;
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             selected = files_array_size(model->files) ?
                            files_array_get(model->files, model->item_idx - model->array_offset) :
                            NULL;
-            return false;
-        });
+        },
+        false);
     return selected;
 }
 
 ArchiveFile_t* archive_get_file_at(ArchiveBrowserView* browser, size_t idx) {
     furi_assert(browser);
 
-    ArchiveFile_t* selected;
+    ArchiveFile_t* selected = NULL;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             idx = CLAMP(idx - model->array_offset, files_array_size(model->files), 0u);
             selected = files_array_size(model->files) ? files_array_get(model->files, idx) : NULL;
-            return false;
-        });
+        },
+        false);
     return selected;
 }
 
 ArchiveTabEnum archive_get_tab(ArchiveBrowserView* browser) {
     furi_assert(browser);
 
-    ArchiveTabEnum tab_id;
+    ArchiveTabEnum tab_id = 0;
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            tab_id = model->tab_idx;
-            return false;
-        });
+        browser->view, ArchiveBrowserViewModel * model, { tab_id = model->tab_idx; }, false);
     return tab_id;
 }
 
@@ -315,22 +326,19 @@ bool archive_is_home(ArchiveBrowserView* browser) {
     }
 
     const char* default_path = archive_get_default_path(archive_get_tab(browser));
-    return (string_cmp_str(browser->path, default_path) == 0);
+    return (furi_string_cmp_str(browser->path, default_path) == 0);
 }
 
 const char* archive_get_name(ArchiveBrowserView* browser) {
     ArchiveFile_t* selected = archive_get_current_file(browser);
-    return string_get_cstr(selected->path);
+    return furi_string_get_cstr(selected->path);
 }
 
 void archive_set_tab(ArchiveBrowserView* browser, ArchiveTabEnum tab) {
     furi_assert(browser);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            model->tab_idx = tab;
-            return false;
-        });
+        browser->view, ArchiveBrowserViewModel * model, { model->tab_idx = tab; }, false);
 }
 
 void archive_add_app_item(ArchiveBrowserView* browser, const char* name) {
@@ -339,75 +347,92 @@ void archive_add_app_item(ArchiveBrowserView* browser, const char* name) {
 
     ArchiveFile_t item;
     ArchiveFile_t_init(&item);
-    string_set_str(item.path, name);
+    furi_string_set(item.path, name);
     archive_set_file_type(&item, name, false, true);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             files_array_push_back(model->files, item);
             model->item_cnt = files_array_size(model->files);
-            return false;
-        });
+        },
+        false);
     ArchiveFile_t_clear(&item);
 }
 
+static bool archive_get_fap_meta(FuriString* file_path, FuriString* fap_name, uint8_t** icon_ptr) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    bool success = false;
+    if(fap_loader_load_name_and_icon(file_path, storage, icon_ptr, fap_name)) {
+        success = true;
+    }
+    furi_record_close(RECORD_STORAGE);
+    return success;
+}
+
 void archive_add_file_item(ArchiveBrowserView* browser, bool is_folder, const char* name) {
     furi_assert(browser);
     furi_assert(name);
 
     ArchiveFile_t item;
-
     ArchiveFile_t_init(&item);
-    string_init_set_str(item.path, name);
-    archive_set_file_type(&item, string_get_cstr(browser->path), is_folder, false);
 
+    furi_string_set(item.path, name);
+    archive_set_file_type(&item, furi_string_get_cstr(browser->path), is_folder, false);
+    if(item.type == ArchiveFileTypeApplication) {
+        item.custom_icon_data = malloc(FAP_MANIFEST_MAX_ICON_SIZE);
+        if(!archive_get_fap_meta(item.path, item.custom_name, &item.custom_icon_data)) {
+            free(item.custom_icon_data);
+            item.custom_icon_data = NULL;
+        }
+    }
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            files_array_push_back(model->files, item);
-            return false;
-        });
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        { files_array_push_back(model->files, item); },
+        false);
     ArchiveFile_t_clear(&item);
 }
 
 void archive_show_file_menu(ArchiveBrowserView* browser, bool show) {
     furi_assert(browser);
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             if(show) {
                 if(archive_is_item_in_array(model, model->item_idx)) {
                     model->menu = true;
                     model->menu_idx = 0;
                     ArchiveFile_t* selected =
                         files_array_get(model->files, model->item_idx - model->array_offset);
-                    selected->fav = archive_is_favorite("%s", string_get_cstr(selected->path));
+                    selected->fav =
+                        archive_is_favorite("%s", furi_string_get_cstr(selected->path));
                 }
             } else {
                 model->menu = false;
                 model->menu_idx = 0;
             }
-
-            return true;
-        });
+        },
+        true);
 }
 
 void archive_favorites_move_mode(ArchiveBrowserView* browser, bool active) {
     furi_assert(browser);
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            model->move_fav = active;
-            return true;
-        });
+        browser->view, ArchiveBrowserViewModel * model, { model->move_fav = active; }, true);
 }
 
-static bool archive_is_dir_exists(string_t path) {
-    if(string_equal_str_p(path, STORAGE_ANY_PATH_PREFIX)) {
+static bool archive_is_dir_exists(FuriString* path) {
+    if(furi_string_equal(path, STORAGE_ANY_PATH_PREFIX)) {
         return true;
     }
     bool state = false;
     FileInfo file_info;
     Storage* storage = furi_record_open(RECORD_STORAGE);
-    if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) {
+    if(storage_common_stat(storage, furi_string_get_cstr(path), &file_info) == FSE_OK) {
         if(file_info.flags & FSF_DIRECTORY) {
             state = true;
         }
@@ -431,16 +456,16 @@ void archive_switch_tab(ArchiveBrowserView* browser, InputKey key) {
     browser->is_root = true;
     archive_set_tab(browser, tab);
 
-    string_set_str(browser->path, archive_get_default_path(tab));
+    furi_string_set(browser->path, archive_get_default_path(tab));
     bool tab_empty = true;
     if(tab == ArchiveTabFavorites) {
         if(archive_favorites_count(browser) > 0) {
             tab_empty = false;
         }
-    } else if(string_start_with_str_p(browser->path, "/app:")) {
-        char* app_name = strchr(string_get_cstr(browser->path), ':');
+    } else if(furi_string_start_with_str(browser->path, "/app:")) {
+        char* app_name = strchr(furi_string_get_cstr(browser->path), ':');
         if(app_name != NULL) {
-            if(archive_app_is_available(browser, string_get_cstr(browser->path))) {
+            if(archive_app_is_available(browser, furi_string_get_cstr(browser->path))) {
                 tab_empty = false;
             }
         }
@@ -451,8 +476,6 @@ void archive_switch_tab(ArchiveBrowserView* browser, InputKey key) {
             archive_file_browser_set_path(
                 browser, browser->path, archive_get_tab_ext(tab), skip_assets);
             tab_empty = false; // Empty check will be performed later
-        } else {
-            tab_empty = true;
         }
     }
 
@@ -460,29 +483,28 @@ void archive_switch_tab(ArchiveBrowserView* browser, InputKey key) {
         archive_switch_tab(browser, key);
     } else {
         with_view_model(
-            browser->view, (ArchiveBrowserViewModel * model) {
+            browser->view,
+            ArchiveBrowserViewModel * model,
+            {
                 model->item_idx = 0;
                 model->array_offset = 0;
-                return false;
-            });
-        archive_get_items(browser, string_get_cstr(browser->path));
+            },
+            false);
+        archive_get_items(browser, furi_string_get_cstr(browser->path));
         archive_update_offset(browser);
     }
 }
 
-void archive_enter_dir(ArchiveBrowserView* browser, string_t path) {
+void archive_enter_dir(ArchiveBrowserView* browser, FuriString* path) {
     furi_assert(browser);
     furi_assert(path);
 
     int32_t idx_temp = 0;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            idx_temp = model->item_idx;
-            return false;
-        });
+        browser->view, ArchiveBrowserViewModel * model, { idx_temp = model->item_idx; }, false);
 
-    string_set(browser->path, path);
+    furi_string_set(browser->path, path);
     file_browser_worker_folder_enter(browser->worker, path, idx_temp);
 }
 
@@ -498,9 +520,6 @@ void archive_refresh_dir(ArchiveBrowserView* browser) {
     int32_t idx_temp = 0;
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            idx_temp = model->item_idx;
-            return false;
-        });
+        browser->view, ArchiveBrowserViewModel * model, { idx_temp = model->item_idx; }, false);
     file_browser_worker_folder_refresh(browser->worker, idx_temp);
 }

+ 4 - 1
applications/main/archive/helpers/archive_browser.h

@@ -16,6 +16,7 @@ static const char* tab_default_paths[] = {
     [ArchiveTabInfrared] = ANY_PATH("infrared"),
     [ArchiveTabBadUsb] = ANY_PATH("badusb"),
     [ArchiveTabU2f] = "/app:u2f",
+    [ArchiveTabApplications] = ANY_PATH("apps"),
     [ArchiveTabBrowser] = STORAGE_ANY_PATH_PREFIX,
 };
 
@@ -27,6 +28,7 @@ static const char* known_ext[] = {
     [ArchiveFileTypeInfrared] = ".ir",
     [ArchiveFileTypeBadUsb] = ".txt",
     [ArchiveFileTypeU2f] = "?",
+    [ArchiveFileTypeApplication] = ".fap",
     [ArchiveFileTypeUpdateManifest] = ".fuf",
     [ArchiveFileTypeFolder] = "?",
     [ArchiveFileTypeUnknown] = "*",
@@ -41,6 +43,7 @@ static const ArchiveFileTypeEnum known_type[] = {
     [ArchiveTabInfrared] = ArchiveFileTypeInfrared,
     [ArchiveTabBadUsb] = ArchiveFileTypeBadUsb,
     [ArchiveTabU2f] = ArchiveFileTypeU2f,
+    [ArchiveTabApplications] = ArchiveFileTypeApplication,
     [ArchiveTabBrowser] = ArchiveFileTypeUnknown,
 };
 
@@ -84,6 +87,6 @@ void archive_show_file_menu(ArchiveBrowserView* browser, bool show);
 void archive_favorites_move_mode(ArchiveBrowserView* browser, bool active);
 
 void archive_switch_tab(ArchiveBrowserView* browser, InputKey key);
-void archive_enter_dir(ArchiveBrowserView* browser, string_t name);
+void archive_enter_dir(ArchiveBrowserView* browser, FuriString* name);
 void archive_leave_dir(ArchiveBrowserView* browser);
 void archive_refresh_dir(ArchiveBrowserView* browser);

+ 56 - 54
applications/main/archive/helpers/archive_favorites.c

@@ -6,8 +6,8 @@
 
 #define ARCHIVE_FAV_FILE_BUF_LEN 32
 
-static bool archive_favorites_read_line(File* file, string_t str_result) {
-    string_reset(str_result);
+static bool archive_favorites_read_line(File* file, FuriString* str_result) {
+    furi_string_reset(str_result);
     uint8_t buffer[ARCHIVE_FAV_FILE_BUF_LEN];
     bool result = false;
 
@@ -34,7 +34,7 @@ static bool archive_favorites_read_line(File* file, string_t str_result) {
                 result = true;
                 break;
             } else {
-                string_push_back(str_result, buffer[i]);
+                furi_string_push_back(str_result, buffer[i]);
             }
         }
 
@@ -52,8 +52,8 @@ uint16_t archive_favorites_count(void* context) {
     Storage* fs_api = furi_record_open(RECORD_STORAGE);
     File* file = storage_file_alloc(fs_api);
 
-    string_t buffer;
-    string_init(buffer);
+    FuriString* buffer;
+    buffer = furi_string_alloc();
 
     bool result = storage_file_open(file, ARCHIVE_FAV_PATH, FSAM_READ, FSOM_OPEN_EXISTING);
     uint16_t lines = 0;
@@ -63,7 +63,7 @@ uint16_t archive_favorites_count(void* context) {
             if(!archive_favorites_read_line(file, buffer)) {
                 break;
             }
-            if(!string_size(buffer)) {
+            if(!furi_string_size(buffer)) {
                 continue; // Skip empty lines
             }
             ++lines;
@@ -72,7 +72,7 @@ uint16_t archive_favorites_count(void* context) {
 
     storage_file_close(file);
 
-    string_clear(buffer);
+    furi_string_free(buffer);
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
 
@@ -80,8 +80,8 @@ uint16_t archive_favorites_count(void* context) {
 }
 
 static bool archive_favourites_rescan() {
-    string_t buffer;
-    string_init(buffer);
+    FuriString* buffer;
+    buffer = furi_string_alloc();
     Storage* storage = furi_record_open(RECORD_STORAGE);
     File* file = storage_file_alloc(storage);
 
@@ -91,23 +91,25 @@ static bool archive_favourites_rescan() {
             if(!archive_favorites_read_line(file, buffer)) {
                 break;
             }
-            if(!string_size(buffer)) {
+            if(!furi_string_size(buffer)) {
                 continue;
             }
 
-            if(string_search(buffer, "/app:") == 0) {
-                if(archive_app_is_available(NULL, string_get_cstr(buffer))) {
-                    archive_file_append(ARCHIVE_FAV_TEMP_PATH, "%s\n", string_get_cstr(buffer));
+            if(furi_string_search(buffer, "/app:") == 0) {
+                if(archive_app_is_available(NULL, furi_string_get_cstr(buffer))) {
+                    archive_file_append(
+                        ARCHIVE_FAV_TEMP_PATH, "%s\n", furi_string_get_cstr(buffer));
                 }
             } else {
-                if(storage_file_exists(storage, string_get_cstr(buffer))) {
-                    archive_file_append(ARCHIVE_FAV_TEMP_PATH, "%s\n", string_get_cstr(buffer));
+                if(storage_file_exists(storage, furi_string_get_cstr(buffer))) {
+                    archive_file_append(
+                        ARCHIVE_FAV_TEMP_PATH, "%s\n", furi_string_get_cstr(buffer));
                 }
             }
         }
     }
 
-    string_clear(buffer);
+    furi_string_free(buffer);
 
     storage_file_close(file);
     storage_common_remove(storage, ARCHIVE_FAV_PATH);
@@ -127,9 +129,9 @@ bool archive_favorites_read(void* context) {
     Storage* storage = furi_record_open(RECORD_STORAGE);
     File* file = storage_file_alloc(storage);
 
-    string_t buffer;
+    FuriString* buffer;
     FileInfo file_info;
-    string_init(buffer);
+    buffer = furi_string_alloc();
 
     bool need_refresh = false;
     uint16_t file_count = 0;
@@ -143,33 +145,33 @@ bool archive_favorites_read(void* context) {
             if(!archive_favorites_read_line(file, buffer)) {
                 break;
             }
-            if(!string_size(buffer)) {
+            if(!furi_string_size(buffer)) {
                 continue;
             }
 
-            if(string_search(buffer, "/app:") == 0) {
-                if(archive_app_is_available(browser, string_get_cstr(buffer))) {
-                    archive_add_app_item(browser, string_get_cstr(buffer));
+            if(furi_string_search(buffer, "/app:") == 0) {
+                if(archive_app_is_available(browser, furi_string_get_cstr(buffer))) {
+                    archive_add_app_item(browser, furi_string_get_cstr(buffer));
                     file_count++;
                 } else {
                     need_refresh = true;
                 }
             } else {
-                if(storage_file_exists(storage, string_get_cstr(buffer))) {
-                    storage_common_stat(storage, string_get_cstr(buffer), &file_info);
+                if(storage_file_exists(storage, furi_string_get_cstr(buffer))) {
+                    storage_common_stat(storage, furi_string_get_cstr(buffer), &file_info);
                     archive_add_file_item(
-                        browser, (file_info.flags & FSF_DIRECTORY), string_get_cstr(buffer));
+                        browser, (file_info.flags & FSF_DIRECTORY), furi_string_get_cstr(buffer));
                     file_count++;
                 } else {
                     need_refresh = true;
                 }
             }
 
-            string_reset(buffer);
+            furi_string_reset(buffer);
         }
     }
     storage_file_close(file);
-    string_clear(buffer);
+    furi_string_free(buffer);
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
 
@@ -183,14 +185,14 @@ bool archive_favorites_read(void* context) {
 }
 
 bool archive_favorites_delete(const char* format, ...) {
-    string_t buffer;
-    string_t filename;
+    FuriString* buffer;
+    FuriString* filename;
     va_list args;
     va_start(args, format);
-    string_init_vprintf(filename, format, args);
+    filename = furi_string_alloc_vprintf(format, args);
     va_end(args);
 
-    string_init(buffer);
+    buffer = furi_string_alloc();
     Storage* fs_api = furi_record_open(RECORD_STORAGE);
     File* file = storage_file_alloc(fs_api);
 
@@ -201,18 +203,18 @@ bool archive_favorites_delete(const char* format, ...) {
             if(!archive_favorites_read_line(file, buffer)) {
                 break;
             }
-            if(!string_size(buffer)) {
+            if(!furi_string_size(buffer)) {
                 continue;
             }
 
-            if(string_search(buffer, filename)) {
-                archive_file_append(ARCHIVE_FAV_TEMP_PATH, "%s\n", string_get_cstr(buffer));
+            if(furi_string_search(buffer, filename)) {
+                archive_file_append(ARCHIVE_FAV_TEMP_PATH, "%s\n", furi_string_get_cstr(buffer));
             }
         }
     }
 
-    string_clear(buffer);
-    string_clear(filename);
+    furi_string_free(buffer);
+    furi_string_free(filename);
 
     storage_file_close(file);
     storage_common_remove(fs_api, ARCHIVE_FAV_PATH);
@@ -226,14 +228,14 @@ bool archive_favorites_delete(const char* format, ...) {
 }
 
 bool archive_is_favorite(const char* format, ...) {
-    string_t buffer;
-    string_t filename;
+    FuriString* buffer;
+    FuriString* filename;
     va_list args;
     va_start(args, format);
-    string_init_vprintf(filename, format, args);
+    filename = furi_string_alloc_vprintf(format, args);
     va_end(args);
 
-    string_init(buffer);
+    buffer = furi_string_alloc();
     Storage* fs_api = furi_record_open(RECORD_STORAGE);
     File* file = storage_file_alloc(fs_api);
 
@@ -245,10 +247,10 @@ bool archive_is_favorite(const char* format, ...) {
             if(!archive_favorites_read_line(file, buffer)) {
                 break;
             }
-            if(!string_size(buffer)) {
+            if(!furi_string_size(buffer)) {
                 continue;
             }
-            if(!string_search(buffer, filename)) {
+            if(!furi_string_search(buffer, filename)) {
                 found = true;
                 break;
             }
@@ -256,8 +258,8 @@ bool archive_is_favorite(const char* format, ...) {
     }
 
     storage_file_close(file);
-    string_clear(buffer);
-    string_clear(filename);
+    furi_string_free(buffer);
+    furi_string_free(filename);
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
 
@@ -271,13 +273,13 @@ bool archive_favorites_rename(const char* src, const char* dst) {
     Storage* fs_api = furi_record_open(RECORD_STORAGE);
     File* file = storage_file_alloc(fs_api);
 
-    string_t path;
-    string_t buffer;
+    FuriString* path;
+    FuriString* buffer;
 
-    string_init(buffer);
-    string_init(path);
+    buffer = furi_string_alloc();
+    path = furi_string_alloc();
 
-    string_printf(path, "%s", src);
+    furi_string_printf(path, "%s", src);
     bool result = storage_file_open(file, ARCHIVE_FAV_PATH, FSAM_READ, FSOM_OPEN_EXISTING);
 
     if(result) {
@@ -285,19 +287,19 @@ bool archive_favorites_rename(const char* src, const char* dst) {
             if(!archive_favorites_read_line(file, buffer)) {
                 break;
             }
-            if(!string_size(buffer)) {
+            if(!furi_string_size(buffer)) {
                 continue;
             }
 
             archive_file_append(
                 ARCHIVE_FAV_TEMP_PATH,
                 "%s\n",
-                string_search(buffer, path) ? string_get_cstr(buffer) : dst);
+                furi_string_search(buffer, path) ? furi_string_get_cstr(buffer) : dst);
         }
     }
 
-    string_clear(buffer);
-    string_clear(path);
+    furi_string_free(buffer);
+    furi_string_free(path);
 
     storage_file_close(file);
     storage_common_remove(fs_api, ARCHIVE_FAV_PATH);
@@ -325,7 +327,7 @@ void archive_favorites_save(void* context) {
 
     for(size_t i = 0; i < archive_file_get_array_size(browser); i++) {
         ArchiveFile_t* item = archive_get_file_at(browser, i);
-        archive_file_append(ARCHIVE_FAV_TEMP_PATH, "%s\n", string_get_cstr(item->path));
+        archive_file_append(ARCHIVE_FAV_TEMP_PATH, "%s\n", furi_string_get_cstr(item->path));
     }
 
     storage_common_remove(fs_api, ARCHIVE_FAV_PATH);

+ 2 - 2
applications/main/archive/helpers/archive_favorites.h

@@ -7,8 +7,8 @@
 
 uint16_t archive_favorites_count(void* context);
 bool archive_favorites_read(void* context);
-bool archive_favorites_delete(const char* format, ...);
-bool archive_is_favorite(const char* format, ...);
+bool archive_favorites_delete(const char* format, ...) _ATTRIBUTE((__format__(__printf__, 1, 2)));
+bool archive_is_favorite(const char* format, ...) _ATTRIBUTE((__format__(__printf__, 1, 2)));
 bool archive_favorites_rename(const char* src, const char* dst);
 void archive_add_to_favorites(const char* file_path);
 void archive_favorites_save(void* context);

+ 14 - 14
applications/main/archive/helpers/archive_files.c

@@ -15,10 +15,10 @@ void archive_set_file_type(ArchiveFile_t* file, const char* path, bool is_folder
     } else {
         for(size_t i = 0; i < COUNT_OF(known_ext); i++) {
             if((known_ext[i][0] == '?') || (known_ext[i][0] == '*')) continue;
-            if(string_search_str(file->path, known_ext[i], 0) != STRING_FAILURE) {
+            if(furi_string_search(file->path, known_ext[i], 0) != FURI_STRING_FAILURE) {
                 if(i == ArchiveFileTypeBadUsb) {
-                    if(string_search_str(file->path, archive_get_default_path(ArchiveTabBadUsb)) ==
-                       0) {
+                    if(furi_string_search(
+                           file->path, archive_get_default_path(ArchiveTabBadUsb)) == 0) {
                         file->type = i;
                         return; // *.txt file is a BadUSB script only if it is in BadUSB folder
                     }
@@ -54,10 +54,10 @@ bool archive_get_items(void* context, const char* path) {
 void archive_file_append(const char* path, const char* format, ...) {
     furi_assert(path);
 
-    string_t string;
+    FuriString* string;
     va_list args;
     va_start(args, format);
-    string_init_vprintf(string, format, args);
+    string = furi_string_alloc_vprintf(format, args);
     va_end(args);
 
     Storage* fs_api = furi_record_open(RECORD_STORAGE);
@@ -66,7 +66,7 @@ void archive_file_append(const char* path, const char* format, ...) {
     bool res = storage_file_open(file, path, FSAM_WRITE, FSOM_OPEN_APPEND);
 
     if(res) {
-        storage_file_write(file, string_get_cstr(string), string_size(string));
+        storage_file_write(file, furi_string_get_cstr(string), furi_string_size(string));
     }
 
     storage_file_close(file);
@@ -77,35 +77,35 @@ void archive_file_append(const char* path, const char* format, ...) {
 void archive_delete_file(void* context, const char* format, ...) {
     furi_assert(context);
 
-    string_t filename;
+    FuriString* filename;
     va_list args;
     va_start(args, format);
-    string_init_vprintf(filename, format, args);
+    filename = furi_string_alloc_vprintf(format, args);
     va_end(args);
 
     ArchiveBrowserView* browser = context;
     Storage* fs_api = furi_record_open(RECORD_STORAGE);
 
     FileInfo fileinfo;
-    storage_common_stat(fs_api, string_get_cstr(filename), &fileinfo);
+    storage_common_stat(fs_api, furi_string_get_cstr(filename), &fileinfo);
 
     bool res = false;
 
     if(fileinfo.flags & FSF_DIRECTORY) {
-        res = storage_simply_remove_recursive(fs_api, string_get_cstr(filename));
+        res = storage_simply_remove_recursive(fs_api, furi_string_get_cstr(filename));
     } else {
-        res = (storage_common_remove(fs_api, string_get_cstr(filename)) == FSE_OK);
+        res = (storage_common_remove(fs_api, furi_string_get_cstr(filename)) == FSE_OK);
     }
 
     furi_record_close(RECORD_STORAGE);
 
-    if(archive_is_favorite("%s", string_get_cstr(filename))) {
-        archive_favorites_delete("%s", string_get_cstr(filename));
+    if(archive_is_favorite("%s", furi_string_get_cstr(filename))) {
+        archive_favorites_delete("%s", furi_string_get_cstr(filename));
     }
 
     if(res) {
         archive_file_array_rm_selected(browser);
     }
 
-    string_clear(filename);
+    furi_string_free(filename);
 }

+ 39 - 11
applications/main/archive/helpers/archive_files.h

@@ -1,9 +1,11 @@
 #pragma once
 
 #include <m-array.h>
-#include <m-string.h>
+#include <furi.h>
 #include <storage/storage.h>
 
+#define FAP_MANIFEST_MAX_ICON_SIZE 32
+
 typedef enum {
     ArchiveFileTypeIButton,
     ArchiveFileTypeNFC,
@@ -13,41 +15,65 @@ typedef enum {
     ArchiveFileTypeBadUsb,
     ArchiveFileTypeU2f,
     ArchiveFileTypeUpdateManifest,
+    ArchiveFileTypeApplication,
     ArchiveFileTypeFolder,
     ArchiveFileTypeUnknown,
     ArchiveFileTypeLoading,
 } ArchiveFileTypeEnum;
 
 typedef struct {
-    string_t path;
+    FuriString* path;
     ArchiveFileTypeEnum type;
+    uint8_t* custom_icon_data;
+    FuriString* custom_name;
     bool fav;
     bool is_app;
 } ArchiveFile_t;
 
 static void ArchiveFile_t_init(ArchiveFile_t* obj) {
+    obj->path = furi_string_alloc();
     obj->type = ArchiveFileTypeUnknown;
-    obj->is_app = false;
+    obj->custom_icon_data = NULL;
+    obj->custom_name = furi_string_alloc();
     obj->fav = false;
-    string_init(obj->path);
+    obj->is_app = false;
 }
 
 static void ArchiveFile_t_init_set(ArchiveFile_t* obj, const ArchiveFile_t* src) {
+    obj->path = furi_string_alloc_set(src->path);
     obj->type = src->type;
-    obj->is_app = src->is_app;
+    if(src->custom_icon_data) {
+        obj->custom_icon_data = malloc(FAP_MANIFEST_MAX_ICON_SIZE);
+        memcpy(obj->custom_icon_data, src->custom_icon_data, FAP_MANIFEST_MAX_ICON_SIZE);
+    } else {
+        obj->custom_icon_data = NULL;
+    }
+    obj->custom_name = furi_string_alloc_set(src->custom_name);
     obj->fav = src->fav;
-    string_init_set(obj->path, src->path);
+    obj->is_app = src->is_app;
 }
 
 static void ArchiveFile_t_set(ArchiveFile_t* obj, const ArchiveFile_t* src) {
+    furi_string_set(obj->path, src->path);
     obj->type = src->type;
-    obj->is_app = src->is_app;
+    if(src->custom_icon_data) {
+        obj->custom_icon_data = malloc(FAP_MANIFEST_MAX_ICON_SIZE);
+        memcpy(obj->custom_icon_data, src->custom_icon_data, FAP_MANIFEST_MAX_ICON_SIZE);
+    } else {
+        obj->custom_icon_data = NULL;
+    }
+    furi_string_set(obj->custom_name, src->custom_name);
     obj->fav = src->fav;
-    string_set(obj->path, src->path);
+    obj->is_app = src->is_app;
 }
 
 static void ArchiveFile_t_clear(ArchiveFile_t* obj) {
-    string_clear(obj->path);
+    furi_string_free(obj->path);
+    if(obj->custom_icon_data) {
+        free(obj->custom_icon_data);
+        obj->custom_icon_data = NULL;
+    }
+    furi_string_free(obj->custom_name);
 }
 
 ARRAY_DEF(
@@ -60,5 +86,7 @@ ARRAY_DEF(
 
 void archive_set_file_type(ArchiveFile_t* file, const char* path, bool is_folder, bool is_app);
 bool archive_get_items(void* context, const char* path);
-void archive_file_append(const char* path, const char* format, ...);
-void archive_delete_file(void* context, const char* format, ...);
+void archive_file_append(const char* path, const char* format, ...)
+    _ATTRIBUTE((__format__(__printf__, 2, 3)));
+void archive_delete_file(void* context, const char* format, ...)
+    _ATTRIBUTE((__format__(__printf__, 2, 3)));

+ 5 - 4
applications/main/archive/scenes/archive_scene_browser.c

@@ -20,6 +20,7 @@ static const char* flipper_app_name[] = {
     [ArchiveFileTypeBadUsb] = "Bad USB",
     [ArchiveFileTypeU2f] = "U2F",
     [ArchiveFileTypeUpdateManifest] = "UpdaterApp",
+    [ArchiveFileTypeApplication] = "Applications",
 };
 
 static void archive_loader_callback(const void* message, void* context) {
@@ -40,14 +41,14 @@ static void archive_run_in_app(ArchiveBrowserView* browser, ArchiveFile_t* selec
 
     LoaderStatus status;
     if(selected->is_app) {
-        char* param = strrchr(string_get_cstr(selected->path), '/');
+        char* param = strrchr(furi_string_get_cstr(selected->path), '/');
         if(param != NULL) {
             param++;
         }
         status = loader_start(loader, flipper_app_name[selected->type], param);
     } else {
         status = loader_start(
-            loader, flipper_app_name[selected->type], string_get_cstr(selected->path));
+            loader, flipper_app_name[selected->type], furi_string_get_cstr(selected->path));
     }
 
     if(status != LoaderStatusOk) {
@@ -159,13 +160,13 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) {
             consumed = true;
             break;
         case ArchiveBrowserEventEnterFavMove:
-            string_set(archive->fav_move_str, selected->path);
+            furi_string_set(archive->fav_move_str, selected->path);
             archive_show_file_menu(browser, false);
             archive_favorites_move_mode(archive->browser, true);
             consumed = true;
             break;
         case ArchiveBrowserEventExitFavMove:
-            archive_update_focus(browser, string_get_cstr(archive->fav_move_str));
+            archive_update_focus(browser, furi_string_get_cstr(archive->fav_move_str));
             archive_favorites_move_mode(archive->browser, false);
             consumed = true;
             break;

+ 4 - 5
applications/main/archive/scenes/archive_scene_delete.c

@@ -4,7 +4,6 @@
 #include "../helpers/archive_apps.h"
 #include "../helpers/archive_browser.h"
 #include "toolbox/path.h"
-#include "m-string.h"
 
 #define SCENE_DELETE_CUSTOM_EVENT (0UL)
 #define MAX_TEXT_INPUT_LEN 22
@@ -26,18 +25,18 @@ void archive_scene_delete_on_enter(void* context) {
     widget_add_button_element(
         app->widget, GuiButtonTypeRight, "Delete", archive_scene_delete_widget_callback, app);
 
-    string_t filename;
-    string_init(filename);
+    FuriString* filename;
+    filename = furi_string_alloc();
 
     ArchiveFile_t* current = archive_get_current_file(app->browser);
     path_extract_filename(current->path, filename, false);
 
     char delete_str[64];
-    snprintf(delete_str, sizeof(delete_str), "\e#Delete %s?\e#", string_get_cstr(filename));
+    snprintf(delete_str, sizeof(delete_str), "\e#Delete %s?\e#", furi_string_get_cstr(filename));
     widget_add_text_box_element(
         app->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, delete_str, false);
 
-    string_clear(filename);
+    furi_string_free(filename);
 
     view_dispatcher_switch_to_view(app->view_dispatcher, ArchiveViewWidget);
 }

+ 11 - 11
applications/main/archive/scenes/archive_scene_rename.c

@@ -19,10 +19,10 @@ void archive_scene_rename_on_enter(void* context) {
     TextInput* text_input = archive->text_input;
     ArchiveFile_t* current = archive_get_current_file(archive->browser);
 
-    string_t filename;
-    string_init(filename);
+    FuriString* filename;
+    filename = furi_string_alloc();
     path_extract_filename(current->path, filename, true);
-    strlcpy(archive->text_store, string_get_cstr(filename), MAX_NAME_LEN);
+    strlcpy(archive->text_store, furi_string_get_cstr(filename), MAX_NAME_LEN);
 
     path_extract_extension(current->path, archive->file_extension, MAX_EXT_LEN);
 
@@ -37,10 +37,10 @@ void archive_scene_rename_on_enter(void* context) {
         false);
 
     ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
-        string_get_cstr(archive->browser->path), archive->file_extension, "");
+        furi_string_get_cstr(archive->browser->path), archive->file_extension, "");
     text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
 
-    string_clear(filename);
+    furi_string_free(filename);
 
     view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewTextInput);
 }
@@ -56,19 +56,19 @@ bool archive_scene_rename_on_event(void* context, SceneManagerEvent event) {
             const char* path_src = archive_get_name(archive->browser);
             ArchiveFile_t* file = archive_get_current_file(archive->browser);
 
-            string_t path_dst;
-            string_init(path_dst);
+            FuriString* path_dst;
+            path_dst = furi_string_alloc();
             path_extract_dirname(path_src, path_dst);
-            string_cat_printf(path_dst, "/%s%s", archive->text_store, known_ext[file->type]);
+            furi_string_cat_printf(path_dst, "/%s%s", archive->text_store, known_ext[file->type]);
 
-            storage_common_rename(fs_api, path_src, string_get_cstr(path_dst));
+            storage_common_rename(fs_api, path_src, furi_string_get_cstr(path_dst));
             furi_record_close(RECORD_STORAGE);
 
             if(file->fav) {
-                archive_favorites_rename(path_src, string_get_cstr(path_dst));
+                archive_favorites_rename(path_src, furi_string_get_cstr(path_dst));
             }
 
-            string_clear(path_dst);
+            furi_string_free(path_dst);
 
             scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneBrowser);
             consumed = true;

+ 71 - 44
applications/main/archive/views/archive_browser_view.c

@@ -14,6 +14,7 @@ static const char* ArchiveTabNames[] = {
     [ArchiveTabInfrared] = "Infrared",
     [ArchiveTabBadUsb] = "Bad USB",
     [ArchiveTabU2f] = "U2F",
+    [ArchiveTabApplications] = "Apps",
     [ArchiveTabBrowser] = "Browser",
 };
 
@@ -29,6 +30,7 @@ static const Icon* ArchiveItemIcons[] = {
     [ArchiveFileTypeFolder] = &I_dir_10px,
     [ArchiveFileTypeUnknown] = &I_unknown_10px,
     [ArchiveFileTypeLoading] = &I_loading_10px,
+    [ArchiveFileTypeApplication] = &I_unknown_10px,
 };
 
 void archive_browser_set_callback(
@@ -47,35 +49,35 @@ static void render_item_menu(Canvas* canvas, ArchiveBrowserViewModel* model) {
     canvas_set_color(canvas, ColorBlack);
     elements_slightly_rounded_frame(canvas, 70, 16, 58, 48);
 
-    string_t menu[MENU_ITEMS];
+    FuriString* menu[MENU_ITEMS];
 
-    string_init_set_str(menu[0], "Run in app");
-    string_init_set_str(menu[1], "Pin");
-    string_init_set_str(menu[2], "Rename");
-    string_init_set_str(menu[3], "Delete");
+    menu[0] = furi_string_alloc_set("Run in app");
+    menu[1] = furi_string_alloc_set("Pin");
+    menu[2] = furi_string_alloc_set("Rename");
+    menu[3] = furi_string_alloc_set("Delete");
 
     ArchiveFile_t* selected = files_array_get(model->files, model->item_idx - model->array_offset);
 
     if((selected->fav) || (model->tab_idx == ArchiveTabFavorites)) {
-        string_set_str(menu[1], "Unpin");
+        furi_string_set(menu[1], "Unpin");
     }
 
     if(!archive_is_known_app(selected->type)) {
-        string_set_str(menu[0], "---");
-        string_set_str(menu[1], "---");
-        string_set_str(menu[2], "---");
+        furi_string_set(menu[0], "---");
+        furi_string_set(menu[1], "---");
+        furi_string_set(menu[2], "---");
     } else {
         if(model->tab_idx == ArchiveTabFavorites) {
-            string_set_str(menu[2], "Move");
-            string_set_str(menu[3], "---");
+            furi_string_set(menu[2], "Move");
+            furi_string_set(menu[3], "---");
         } else if(selected->is_app) {
-            string_set_str(menu[2], "---");
+            furi_string_set(menu[2], "---");
         }
     }
 
     for(size_t i = 0; i < MENU_ITEMS; i++) {
-        canvas_draw_str(canvas, 82, 27 + i * 11, string_get_cstr(menu[i]));
-        string_clear(menu[i]);
+        canvas_draw_str(canvas, 82, 27 + i * 11, furi_string_get_cstr(menu[i]));
+        furi_string_free(menu[i]);
     }
 
     canvas_draw_icon(canvas, 74, 20 + model->menu_idx * 11, &I_ButtonRight_4x7);
@@ -118,20 +120,31 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
     bool scrollbar = model->item_cnt > 4;
 
     for(uint32_t i = 0; i < MIN(model->item_cnt, MENU_ITEMS); ++i) {
-        string_t str_buf;
-        string_init(str_buf);
+        FuriString* str_buf;
+        str_buf = furi_string_alloc();
         int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u);
         uint8_t x_offset = (model->move_fav && model->item_idx == idx) ? MOVE_OFFSET : 0;
 
         ArchiveFileTypeEnum file_type = ArchiveFileTypeLoading;
+        uint8_t* custom_icon_data = NULL;
 
         if(archive_is_item_in_array(model, idx)) {
             ArchiveFile_t* file = files_array_get(
                 model->files, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0));
-            path_extract_filename(file->path, str_buf, archive_is_known_app(file->type));
             file_type = file->type;
+            if(file_type == ArchiveFileTypeApplication) {
+                if(file->custom_icon_data) {
+                    custom_icon_data = file->custom_icon_data;
+                    furi_string_set(str_buf, file->custom_name);
+                } else {
+                    file_type = ArchiveFileTypeUnknown;
+                    path_extract_filename(file->path, str_buf, archive_is_known_app(file->type));
+                }
+            } else {
+                path_extract_filename(file->path, str_buf, archive_is_known_app(file->type));
+            }
         } else {
-            string_set_str(str_buf, "---");
+            furi_string_set(str_buf, "---");
         }
 
         elements_string_fit_width(
@@ -143,10 +156,17 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
             canvas_set_color(canvas, ColorBlack);
         }
 
-        canvas_draw_icon(canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]);
-        canvas_draw_str(canvas, 15 + x_offset, 24 + i * FRAME_HEIGHT, string_get_cstr(str_buf));
+        if(custom_icon_data) {
+            canvas_draw_bitmap(
+                canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, 11, 10, custom_icon_data);
+        } else {
+            canvas_draw_icon(
+                canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]);
+        }
+        canvas_draw_str(
+            canvas, 15 + x_offset, 24 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buf));
 
-        string_clear(str_buf);
+        furi_string_free(str_buf);
     }
 
     if(scrollbar) {
@@ -243,33 +263,37 @@ static bool archive_view_input(InputEvent* event, void* context) {
     bool in_menu;
     bool move_fav_mode;
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             in_menu = model->menu;
             move_fav_mode = model->move_fav;
-            return false;
-        });
+        },
+        false);
 
     if(in_menu) {
         if(event->type == InputTypeShort) {
             if(event->key == InputKeyUp || event->key == InputKeyDown) {
                 with_view_model(
-                    browser->view, (ArchiveBrowserViewModel * model) {
+                    browser->view,
+                    ArchiveBrowserViewModel * model,
+                    {
                         if(event->key == InputKeyUp) {
                             model->menu_idx = ((model->menu_idx - 1) + MENU_ITEMS) % MENU_ITEMS;
                         } else if(event->key == InputKeyDown) {
                             model->menu_idx = (model->menu_idx + 1) % MENU_ITEMS;
                         }
-                        return true;
-                    });
+                    },
+                    true);
             }
 
             if(event->key == InputKeyOk) {
                 uint8_t idx;
                 with_view_model(
-                    browser->view, (ArchiveBrowserViewModel * model) {
-                        idx = model->menu_idx;
-                        return false;
-                    });
+                    browser->view,
+                    ArchiveBrowserViewModel * model,
+                    { idx = model->menu_idx; },
+                    false);
                 browser->callback(file_menu_actions[idx], browser->context);
             } else if(event->key == InputKeyBack) {
                 browser->callback(ArchiveBrowserEventFileMenuClose, browser->context);
@@ -293,7 +317,9 @@ static bool archive_view_input(InputEvent* event, void* context) {
         if((event->key == InputKeyUp || event->key == InputKeyDown) &&
            (event->type == InputTypeShort || event->type == InputTypeRepeat)) {
             with_view_model(
-                browser->view, (ArchiveBrowserViewModel * model) {
+                browser->view,
+                ArchiveBrowserViewModel * model,
+                {
                     if(event->key == InputKeyUp) {
                         model->item_idx =
                             ((model->item_idx - 1) + model->item_cnt) % model->item_cnt;
@@ -314,9 +340,8 @@ static bool archive_view_input(InputEvent* event, void* context) {
                             browser->callback(ArchiveBrowserEventFavMoveDown, browser->context);
                         }
                     }
-
-                    return true;
-                });
+                },
+                true);
             archive_update_offset(browser);
         }
 
@@ -361,14 +386,16 @@ ArchiveBrowserView* browser_alloc() {
     view_set_draw_callback(browser->view, archive_view_render);
     view_set_input_callback(browser->view, archive_view_input);
 
-    string_init_set_str(browser->path, archive_get_default_path(TAB_DEFAULT));
+    browser->path = furi_string_alloc_set(archive_get_default_path(TAB_DEFAULT));
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        {
             files_array_init(model->files);
             model->tab_idx = TAB_DEFAULT;
-            return true;
-        });
+        },
+        true);
 
     return browser;
 }
@@ -381,12 +408,12 @@ void browser_free(ArchiveBrowserView* browser) {
     }
 
     with_view_model(
-        browser->view, (ArchiveBrowserViewModel * model) {
-            files_array_clear(model->files);
-            return false;
-        });
+        browser->view,
+        ArchiveBrowserViewModel * model,
+        { files_array_clear(model->files); },
+        false);
 
-    string_clear(browser->path);
+    furi_string_free(browser->path);
 
     view_free(browser->view);
     free(browser);

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

@@ -26,6 +26,7 @@ typedef enum {
     ArchiveTabIButton,
     ArchiveTabBadUsb,
     ArchiveTabU2f,
+    ArchiveTabApplications,
     ArchiveTabBrowser,
     ArchiveTabTotal,
 } ArchiveTabEnum;
@@ -77,7 +78,7 @@ struct ArchiveBrowserView {
     bool worker_running;
     ArchiveBrowserViewCallback callback;
     void* context;
-    string_t path;
+    FuriString* path;
     InputKey last_tab_switch_dir;
     bool is_root;
 };

+ 5 - 6
applications/main/bad_usb/bad_usb_app.c

@@ -1,5 +1,4 @@
 #include "bad_usb_app_i.h"
-#include "m-string.h"
 #include <furi.h>
 #include <furi_hal.h>
 #include <storage/storage.h>
@@ -26,10 +25,10 @@ static void bad_usb_app_tick_event_callback(void* context) {
 BadUsbApp* bad_usb_app_alloc(char* arg) {
     BadUsbApp* app = malloc(sizeof(BadUsbApp));
 
-    string_init(app->file_path);
+    app->file_path = furi_string_alloc();
 
     if(arg && strlen(arg)) {
-        string_set_str(app->file_path, arg);
+        furi_string_set(app->file_path, arg);
     }
 
     app->gui = furi_record_open(RECORD_GUI);
@@ -64,10 +63,10 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
         app->error = BadUsbAppErrorCloseRpc;
         scene_manager_next_scene(app->scene_manager, BadUsbSceneError);
     } else {
-        if(!string_empty_p(app->file_path)) {
+        if(!furi_string_empty(app->file_path)) {
             scene_manager_next_scene(app->scene_manager, BadUsbSceneWork);
         } else {
-            string_set_str(app->file_path, BAD_USB_APP_PATH_FOLDER);
+            furi_string_set(app->file_path, BAD_USB_APP_PATH_FOLDER);
             scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect);
         }
     }
@@ -95,7 +94,7 @@ void bad_usb_app_free(BadUsbApp* app) {
     furi_record_close(RECORD_NOTIFICATION);
     furi_record_close(RECORD_DIALOGS);
 
-    string_clear(app->file_path);
+    furi_string_free(app->file_path);
 
     free(app);
 }

+ 1 - 1
applications/main/bad_usb/bad_usb_app_i.h

@@ -31,7 +31,7 @@ struct BadUsbApp {
     Widget* widget;
 
     BadUsbAppError error;
-    string_t file_path;
+    FuriString* file_path;
     BadUsb* bad_usb_view;
     BadUsbScript* bad_usb_script;
 };

+ 69 - 29
applications/main/bad_usb/bad_usb_script.c

@@ -26,16 +26,16 @@ typedef enum {
 struct BadUsbScript {
     FuriHalUsbHidConfig hid_cfg;
     BadUsbState st;
-    string_t file_path;
+    FuriString* file_path;
     uint32_t defdelay;
     FuriThread* thread;
     uint8_t file_buf[FILE_BUFFER_LEN + 1];
     uint8_t buf_start;
     uint8_t buf_len;
     bool file_end;
-    string_t line;
+    FuriString* line;
 
-    string_t line_prev;
+    FuriString* line_prev;
     uint32_t repeat_cnt;
 };
 
@@ -109,6 +109,7 @@ static const char ducky_cmd_string[] = {"STRING "};
 static const char ducky_cmd_defdelay_1[] = {"DEFAULT_DELAY "};
 static const char ducky_cmd_defdelay_2[] = {"DEFAULTDELAY "};
 static const char ducky_cmd_repeat[] = {"REPEAT "};
+static const char ducky_cmd_sysrq[] = {"SYSRQ "};
 
 static const char ducky_cmd_altchar[] = {"ALTCHAR "};
 static const char ducky_cmd_altstr_1[] = {"ALTSTRING "};
@@ -230,9 +231,10 @@ static uint16_t ducky_get_keycode(const char* param, bool accept_chars) {
     return 0;
 }
 
-static int32_t ducky_parse_line(BadUsbScript* bad_usb, string_t line) {
-    uint32_t line_len = string_size(line);
-    const char* line_tmp = string_get_cstr(line);
+static int32_t
+    ducky_parse_line(BadUsbScript* bad_usb, FuriString* line, char* error, size_t error_len) {
+    uint32_t line_len = furi_string_size(line);
+    const char* line_tmp = furi_string_get_cstr(line);
     bool state = false;
 
     for(uint32_t i = 0; i < line_len; i++) {
@@ -260,6 +262,9 @@ static int32_t ducky_parse_line(BadUsbScript* bad_usb, string_t line) {
         if((state) && (delay_val > 0)) {
             return (int32_t)delay_val;
         }
+        if(error != NULL) {
+            snprintf(error, error_len, "Invalid number %s", line_tmp);
+        }
         return SCRIPT_STATE_ERROR;
     } else if(
         (strncmp(line_tmp, ducky_cmd_defdelay_1, strlen(ducky_cmd_defdelay_1)) == 0) ||
@@ -267,17 +272,26 @@ static int32_t ducky_parse_line(BadUsbScript* bad_usb, string_t line) {
         // DEFAULT_DELAY
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
         state = ducky_get_number(line_tmp, &bad_usb->defdelay);
+        if(!state && error != NULL) {
+            snprintf(error, error_len, "Invalid number %s", line_tmp);
+        }
         return (state) ? (0) : SCRIPT_STATE_ERROR;
     } 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);
+        if(!state && error != NULL) {
+            snprintf(error, error_len, "Invalid string %s", line_tmp);
+        }
         return (state) ? (0) : SCRIPT_STATE_ERROR;
     } else if(strncmp(line_tmp, ducky_cmd_altchar, strlen(ducky_cmd_altchar)) == 0) {
         // ALTCHAR
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
         ducky_numlock_on();
         state = ducky_altchar(line_tmp);
+        if(!state && error != NULL) {
+            snprintf(error, error_len, "Invalid altchar %s", line_tmp);
+        }
         return (state) ? (0) : SCRIPT_STATE_ERROR;
     } else if(
         (strncmp(line_tmp, ducky_cmd_altstr_1, strlen(ducky_cmd_altstr_1)) == 0) ||
@@ -286,16 +300,35 @@ static int32_t ducky_parse_line(BadUsbScript* bad_usb, string_t line) {
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
         ducky_numlock_on();
         state = ducky_altstring(line_tmp);
+        if(!state && error != NULL) {
+            snprintf(error, error_len, "Invalid altstring %s", line_tmp);
+        }
         return (state) ? (0) : SCRIPT_STATE_ERROR;
     } else if(strncmp(line_tmp, ducky_cmd_repeat, strlen(ducky_cmd_repeat)) == 0) {
         // REPEAT
         line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
         state = ducky_get_number(line_tmp, &bad_usb->repeat_cnt);
+        if(!state && error != NULL) {
+            snprintf(error, error_len, "Invalid number %s", line_tmp);
+        }
         return (state) ? (0) : SCRIPT_STATE_ERROR;
+    } 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);
+        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);
-        if(key == HID_KEYBOARD_NONE) return SCRIPT_STATE_ERROR;
+        if(key == HID_KEYBOARD_NONE) {
+            if(error != NULL) {
+                snprintf(error, error_len, "No keycode defined for %s", line_tmp);
+            }
+            return SCRIPT_STATE_ERROR;
+        }
         if((key & 0xFF00) != 0) {
             // It's a modifier key
             line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1];
@@ -305,6 +338,9 @@ static int32_t ducky_parse_line(BadUsbScript* bad_usb, string_t line) {
         furi_hal_hid_kb_release(key);
         return (0);
     }
+    if(error != NULL) {
+        strncpy(error, "Unknown error", error_len);
+    }
     return SCRIPT_STATE_ERROR;
 }
 
@@ -323,7 +359,7 @@ static bool ducky_set_usb_id(BadUsbScript* bad_usb, const char* line) {
         }
         FURI_LOG_D(
             WORKER_TAG,
-            "set id: %04X:%04X mfr:%s product:%s",
+            "set id: %04lX:%04lX mfr:%s product:%s",
             bad_usb->hid_cfg.vid,
             bad_usb->hid_cfg.pid,
             bad_usb->hid_cfg.manuf,
@@ -337,7 +373,7 @@ static bool ducky_script_preload(BadUsbScript* bad_usb, File* script_file) {
     uint8_t ret = 0;
     uint32_t line_len = 0;
 
-    string_reset(bad_usb->line);
+    furi_string_reset(bad_usb->line);
 
     do {
         ret = storage_file_read(script_file, bad_usb->file_buf, FILE_BUFFER_LEN);
@@ -347,7 +383,7 @@ static bool ducky_script_preload(BadUsbScript* bad_usb, File* script_file) {
                 line_len = 0;
             } else {
                 if(bad_usb->st.line_nb == 0) { // Save first line
-                    string_push_back(bad_usb->line, bad_usb->file_buf[i]);
+                    furi_string_push_back(bad_usb->line, bad_usb->file_buf[i]);
                 }
                 line_len++;
             }
@@ -360,7 +396,7 @@ static bool ducky_script_preload(BadUsbScript* bad_usb, File* script_file) {
         }
     } while(ret > 0);
 
-    const char* line_tmp = string_get_cstr(bad_usb->line);
+    const char* line_tmp = furi_string_get_cstr(bad_usb->line);
     bool id_set = false; // Looking for ID command at first line
     if(strncmp(line_tmp, ducky_cmd_id, strlen(ducky_cmd_id)) == 0) {
         id_set = ducky_set_usb_id(bad_usb, &line_tmp[strlen(ducky_cmd_id) + 1]);
@@ -373,7 +409,7 @@ static bool ducky_script_preload(BadUsbScript* bad_usb, File* script_file) {
     }
 
     storage_file_seek(script_file, 0, true);
-    string_reset(bad_usb->line);
+    furi_string_reset(bad_usb->line);
 
     return true;
 }
@@ -383,20 +419,21 @@ static int32_t ducky_script_execute_next(BadUsbScript* bad_usb, File* script_fil
 
     if(bad_usb->repeat_cnt > 0) {
         bad_usb->repeat_cnt--;
-        delay_val = ducky_parse_line(bad_usb, bad_usb->line_prev);
+        delay_val = ducky_parse_line(
+            bad_usb, bad_usb->line_prev, bad_usb->st.error, sizeof(bad_usb->st.error));
         if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
             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 %lu", bad_usb->st.line_cur - 1);
+            FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_usb->st.line_cur - 1);
             return SCRIPT_STATE_ERROR;
         } else {
             return (delay_val + bad_usb->defdelay);
         }
     }
 
-    string_set(bad_usb->line_prev, bad_usb->line);
-    string_reset(bad_usb->line);
+    furi_string_set(bad_usb->line_prev, bad_usb->line);
+    furi_string_reset(bad_usb->line);
 
     while(1) {
         if(bad_usb->buf_len == 0) {
@@ -413,20 +450,22 @@ static int32_t ducky_script_execute_next(BadUsbScript* bad_usb, File* script_fil
             if(bad_usb->buf_len == 0) return SCRIPT_STATE_END;
         }
         for(uint8_t i = bad_usb->buf_start; i < (bad_usb->buf_start + bad_usb->buf_len); i++) {
-            if(bad_usb->file_buf[i] == '\n' && string_size(bad_usb->line) > 0) {
+            if(bad_usb->file_buf[i] == '\n' && furi_string_size(bad_usb->line) > 0) {
                 bad_usb->st.line_cur++;
                 bad_usb->buf_len = bad_usb->buf_len + bad_usb->buf_start - (i + 1);
                 bad_usb->buf_start = i + 1;
-                delay_val = ducky_parse_line(bad_usb, bad_usb->line);
+                delay_val = ducky_parse_line(
+                    bad_usb, bad_usb->line, bad_usb->st.error, sizeof(bad_usb->st.error));
+
                 if(delay_val < 0) {
                     bad_usb->st.error_line = bad_usb->st.line_cur;
-                    FURI_LOG_E(WORKER_TAG, "Unknown command at line %lu", bad_usb->st.line_cur);
+                    FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_usb->st.line_cur);
                     return SCRIPT_STATE_ERROR;
                 } else {
                     return (delay_val + bad_usb->defdelay);
                 }
             } else {
-                string_push_back(bad_usb->line, bad_usb->file_buf[i]);
+                furi_string_push_back(bad_usb->line, bad_usb->file_buf[i]);
             }
         }
         bad_usb->buf_len = 0;
@@ -456,8 +495,8 @@ static int32_t bad_usb_worker(void* context) {
 
     FURI_LOG_I(WORKER_TAG, "Init");
     File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
-    string_init(bad_usb->line);
-    string_init(bad_usb->line_prev);
+    bad_usb->line = furi_string_alloc();
+    bad_usb->line_prev = furi_string_alloc();
 
     furi_hal_hid_set_state_callback(bad_usb_hid_state_callback, bad_usb);
 
@@ -465,7 +504,7 @@ static int32_t bad_usb_worker(void* context) {
         if(worker_state == BadUsbStateInit) { // State: initialization
             if(storage_file_open(
                    script_file,
-                   string_get_cstr(bad_usb->file_path),
+                   furi_string_get_cstr(bad_usb->file_path),
                    FSAM_READ,
                    FSOM_OPEN_EXISTING)) {
                 if((ducky_script_preload(bad_usb, script_file)) && (bad_usb->st.line_nb > 0)) {
@@ -577,22 +616,23 @@ static int32_t bad_usb_worker(void* context) {
 
     storage_file_close(script_file);
     storage_file_free(script_file);
-    string_clear(bad_usb->line);
-    string_clear(bad_usb->line_prev);
+    furi_string_free(bad_usb->line);
+    furi_string_free(bad_usb->line_prev);
 
     FURI_LOG_I(WORKER_TAG, "End");
 
     return 0;
 }
 
-BadUsbScript* bad_usb_script_open(string_t file_path) {
+BadUsbScript* bad_usb_script_open(FuriString* file_path) {
     furi_assert(file_path);
 
     BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript));
-    string_init(bad_usb->file_path);
-    string_set(bad_usb->file_path, file_path);
+    bad_usb->file_path = furi_string_alloc();
+    furi_string_set(bad_usb->file_path, file_path);
 
     bad_usb->st.state = BadUsbStateInit;
+    bad_usb->st.error[0] = '\0';
 
     bad_usb->thread = furi_thread_alloc();
     furi_thread_set_name(bad_usb->thread, "BadUsbWorker");
@@ -609,7 +649,7 @@ void bad_usb_script_close(BadUsbScript* bad_usb) {
     furi_thread_flags_set(furi_thread_get_id(bad_usb->thread), WorkerEvtEnd);
     furi_thread_join(bad_usb->thread);
     furi_thread_free(bad_usb->thread);
-    string_clear(bad_usb->file_path);
+    furi_string_free(bad_usb->file_path);
     free(bad_usb);
 }
 

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

@@ -5,7 +5,6 @@ extern "C" {
 #endif
 
 #include <furi.h>
-#include <m-string.h>
 
 typedef struct BadUsbScript BadUsbScript;
 
@@ -26,9 +25,10 @@ typedef struct {
     uint16_t line_nb;
     uint32_t delay_remain;
     uint16_t error_line;
+    char error[64];
 } BadUsbState;
 
-BadUsbScript* bad_usb_script_open(string_t file_path);
+BadUsbScript* bad_usb_script_open(FuriString* file_path);
 
 void bad_usb_script_close(BadUsbScript* bad_usb);
 

+ 4 - 5
applications/main/bad_usb/scenes/bad_usb_scene_work.c

@@ -2,7 +2,6 @@
 #include "../bad_usb_app_i.h"
 #include "../views/bad_usb_view.h"
 #include "furi_hal.h"
-#include "m-string.h"
 #include "toolbox/path.h"
 
 void bad_usb_scene_work_ok_callback(InputType type, void* context) {
@@ -27,14 +26,14 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) {
 void bad_usb_scene_work_on_enter(void* context) {
     BadUsbApp* app = context;
 
-    string_t file_name;
-    string_init(file_name);
+    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, string_get_cstr(file_name));
+    bad_usb_set_file_name(app->bad_usb_view, furi_string_get_cstr(file_name));
     app->bad_usb_script = bad_usb_script_open(app->file_path);
 
-    string_clear(file_name);
+    furi_string_free(file_name);
 
     bad_usb_set_state(app->bad_usb_view, bad_usb_script_get_state(app->bad_usb_script));
 

+ 35 - 28
applications/main/bad_usb/views/bad_usb_view.c

@@ -19,12 +19,12 @@ typedef struct {
 static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
     BadUsbModel* model = _model;
 
-    string_t disp_str;
-    string_init_set_str(disp_str, model->file_name);
+    FuriString* disp_str;
+    disp_str = furi_string_alloc_set(model->file_name);
     elements_string_fit_width(canvas, disp_str, 128 - 2);
     canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 2, 8, string_get_cstr(disp_str));
-    string_reset(disp_str);
+    canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str));
+    furi_string_reset(disp_str);
 
     canvas_draw_icon(canvas, 22, 20, &I_UsbTree_48x22);
 
@@ -49,10 +49,11 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
         canvas_set_font(canvas, FontPrimary);
         canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
         canvas_set_font(canvas, FontSecondary);
-        string_printf(disp_str, "line %u", model->state.error_line);
+        furi_string_printf(disp_str, "line %u", model->state.error_line);
         canvas_draw_str_aligned(
-            canvas, 127, 46, AlignRight, AlignBottom, string_get_cstr(disp_str));
-        string_reset(disp_str);
+            canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        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_set_font(canvas, FontBigNumbers);
@@ -65,16 +66,17 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
             canvas_draw_icon(canvas, 4, 19, &I_EviSmile2_18x21);
         }
         canvas_set_font(canvas, FontBigNumbers);
-        string_printf(disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
+        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, string_get_cstr(disp_str));
-        string_reset(disp_str);
+            canvas, 114, 36, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        furi_string_reset(disp_str);
         canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
     } else if(model->state.state == BadUsbStateDone) {
         canvas_draw_icon(canvas, 4, 19, &I_EviSmile1_18x21);
         canvas_set_font(canvas, FontBigNumbers);
         canvas_draw_str_aligned(canvas, 114, 36, AlignRight, AlignBottom, "100");
-        string_reset(disp_str);
+        furi_string_reset(disp_str);
         canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
     } else if(model->state.state == BadUsbStateDelay) {
         if(model->anim_frame == 0) {
@@ -83,21 +85,22 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) {
             canvas_draw_icon(canvas, 4, 19, &I_EviWaiting2_18x21);
         }
         canvas_set_font(canvas, FontBigNumbers);
-        string_printf(disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
+        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, string_get_cstr(disp_str));
-        string_reset(disp_str);
+            canvas, 114, 36, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        furi_string_reset(disp_str);
         canvas_draw_icon(canvas, 117, 22, &I_Percent_10x14);
         canvas_set_font(canvas, FontSecondary);
-        string_printf(disp_str, "delay %us", model->state.delay_remain);
+        furi_string_printf(disp_str, "delay %lus", model->state.delay_remain);
         canvas_draw_str_aligned(
-            canvas, 127, 46, AlignRight, AlignBottom, string_get_cstr(disp_str));
-        string_reset(disp_str);
+            canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        furi_string_reset(disp_str);
     } else {
         canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18);
     }
 
-    string_clear(disp_str);
+    furi_string_free(disp_str);
 }
 
 static bool bad_usb_input_callback(InputEvent* event, void* context) {
@@ -143,29 +146,33 @@ void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* c
     furi_assert(bad_usb);
     furi_assert(callback);
     with_view_model(
-        bad_usb->view, (BadUsbModel * model) {
+        bad_usb->view,
+        BadUsbModel * model,
+        {
             UNUSED(model);
             bad_usb->callback = callback;
             bad_usb->context = context;
-            return true;
-        });
+        },
+        true);
 }
 
 void bad_usb_set_file_name(BadUsb* bad_usb, const char* name) {
     furi_assert(name);
     with_view_model(
-        bad_usb->view, (BadUsbModel * model) {
-            strlcpy(model->file_name, name, MAX_NAME_LEN);
-            return true;
-        });
+        bad_usb->view,
+        BadUsbModel * model,
+        { strlcpy(model->file_name, name, MAX_NAME_LEN); },
+        true);
 }
 
 void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st) {
     furi_assert(st);
     with_view_model(
-        bad_usb->view, (BadUsbModel * model) {
+        bad_usb->view,
+        BadUsbModel * model,
+        {
             memcpy(&(model->state), st, sizeof(BadUsbState));
             model->anim_frame ^= 1;
-            return true;
-        });
+        },
+        true);
 }

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

@@ -3,6 +3,7 @@ App(
     name="Applications",
     apptype=FlipperAppType.APP,
     entry_point="fap_loader_app",
+    cdefines=["APP_FAP_LOADER"],
     requires=[
         "gui",
         "storage",

+ 1 - 1
applications/main/fap_loader/elf_cpp/elf_hashtable.cpp

@@ -31,7 +31,7 @@ bool elf_resolve_from_hashtable(const char* name, Elf32_Addr* address) {
 
     auto find_res = std::lower_bound(elf_api_table.cbegin(), elf_api_table.cend(), key);
     if((find_res == elf_api_table.cend() || (find_res->hash != gnu_sym_hash))) {
-        FURI_LOG_W(TAG, "Cant find symbol '%s' (hash %x)!", name, gnu_sym_hash);
+        FURI_LOG_W(TAG, "Cant find symbol '%s' (hash %lx)!", name, gnu_sym_hash);
         result = false;
     } else {
         result = true;

+ 74 - 53
applications/main/fap_loader/fap_loader_app.c

@@ -1,31 +1,34 @@
 #include <furi.h>
 #include <gui/gui.h>
 #include <gui/view_dispatcher.h>
-#include <gui/modules/loading.h>
 #include <storage/storage.h>
+#include <gui/modules/loading.h>
 #include <dialogs/dialogs.h>
-#include "elf_cpp/elf_hashtable.h"
 #include <flipper_application/flipper_application.h>
+#include "elf_cpp/elf_hashtable.h"
+#include "fap_loader_app.h"
 
 #define TAG "fap_loader_app"
 
-typedef struct {
+struct FapLoader {
     FlipperApplication* app;
     Storage* storage;
     DialogsApp* dialogs;
     Gui* gui;
-    string_t fap_path;
-} FapLoader;
-
-static bool
-    fap_loader_item_callback(string_t path, void* context, uint8_t** icon_ptr, string_t item_name) {
-    FapLoader* loader = context;
-    furi_assert(loader);
-
-    FlipperApplication* app = flipper_application_alloc(loader->storage, &hashtable_api_interface);
+    FuriString* fap_path;
+    ViewDispatcher* view_dispatcher;
+    Loading* loading;
+};
+
+bool fap_loader_load_name_and_icon(
+    FuriString* path,
+    Storage* storage,
+    uint8_t** icon_ptr,
+    FuriString* item_name) {
+    FlipperApplication* app = flipper_application_alloc(storage, &hashtable_api_interface);
 
     FlipperApplicationPreloadStatus preload_res =
-        flipper_application_preload(app, string_get_cstr(path));
+        flipper_application_preload_manifest(app, furi_string_get_cstr(path));
 
     bool load_success = false;
 
@@ -34,10 +37,10 @@ static bool
         if(manifest->has_icon) {
             memcpy(*icon_ptr, manifest->icon, FAP_MANIFEST_MAX_ICON_SIZE);
         }
-        string_set_str(item_name, manifest->name);
+        furi_string_set(item_name, manifest->name);
         load_success = true;
     } else {
-        FURI_LOG_E(TAG, "FAP Loader failed to preload %s", string_get_cstr(path));
+        FURI_LOG_E(TAG, "FAP Loader failed to preload %s", furi_string_get_cstr(path));
         load_success = false;
     }
 
@@ -45,30 +48,41 @@ static bool
     return load_success;
 }
 
+static bool fap_loader_item_callback(
+    FuriString* path,
+    void* context,
+    uint8_t** icon_ptr,
+    FuriString* item_name) {
+    FapLoader* fap_loader = context;
+    furi_assert(fap_loader);
+    return fap_loader_load_name_and_icon(path, fap_loader->storage, icon_ptr, item_name);
+}
+
 static bool fap_loader_run_selected_app(FapLoader* loader) {
     furi_assert(loader);
 
-    string_t error_message;
+    FuriString* error_message;
 
-    string_init_set(error_message, "unknown error");
+    error_message = furi_string_alloc_set("unknown error");
 
     bool file_selected = false;
     bool show_error = true;
     do {
         file_selected = true;
         loader->app = flipper_application_alloc(loader->storage, &hashtable_api_interface);
+        size_t start = furi_get_tick();
 
-        FURI_LOG_I(TAG, "FAP Loader is loading %s", string_get_cstr(loader->fap_path));
+        FURI_LOG_I(TAG, "FAP Loader is loading %s", furi_string_get_cstr(loader->fap_path));
 
         FlipperApplicationPreloadStatus preload_res =
-            flipper_application_preload(loader->app, string_get_cstr(loader->fap_path));
+            flipper_application_preload(loader->app, furi_string_get_cstr(loader->fap_path));
         if(preload_res != FlipperApplicationPreloadStatusSuccess) {
             const char* err_msg = flipper_application_preload_status_to_string(preload_res);
-            string_printf(error_message, "Preload failed: %s", err_msg);
+            furi_string_printf(error_message, "Preload failed: %s", err_msg);
             FURI_LOG_E(
                 TAG,
                 "FAP Loader failed to preload %s: %s",
-                string_get_cstr(loader->fap_path),
+                furi_string_get_cstr(loader->fap_path),
                 err_msg);
             break;
         }
@@ -77,16 +91,17 @@ static bool fap_loader_run_selected_app(FapLoader* loader) {
         FlipperApplicationLoadStatus load_status = flipper_application_map_to_memory(loader->app);
         if(load_status != FlipperApplicationLoadStatusSuccess) {
             const char* err_msg = flipper_application_load_status_to_string(load_status);
-            string_printf(error_message, "Load failed: %s", err_msg);
+            furi_string_printf(error_message, "Load failed: %s", err_msg);
             FURI_LOG_E(
                 TAG,
                 "FAP Loader failed to map to memory %s: %s",
-                string_get_cstr(loader->fap_path),
+                furi_string_get_cstr(loader->fap_path),
                 err_msg);
             break;
         }
 
-        FURI_LOG_I(TAG, "FAP Loader is staring app");
+        FURI_LOG_I(TAG, "Loaded in %ums", (size_t)(furi_get_tick() - start));
+        FURI_LOG_I(TAG, "FAP Loader is starting app");
 
         FuriThread* thread = flipper_application_spawn(loader->app, NULL);
         furi_thread_start(thread);
@@ -103,19 +118,19 @@ static bool fap_loader_run_selected_app(FapLoader* loader) {
         dialog_message_set_header(message, "Error", 64, 0, AlignCenter, AlignTop);
         dialog_message_set_buttons(message, NULL, NULL, NULL);
 
-        string_t buffer;
-        string_init(buffer);
-        string_printf(buffer, "%s", string_get_cstr(error_message));
-        string_replace_str(buffer, ":", "\n");
+        FuriString* buffer;
+        buffer = furi_string_alloc();
+        furi_string_printf(buffer, "%s", furi_string_get_cstr(error_message));
+        furi_string_replace(buffer, ":", "\n");
         dialog_message_set_text(
-            message, string_get_cstr(buffer), 64, 32, AlignCenter, AlignCenter);
+            message, furi_string_get_cstr(buffer), 64, 32, AlignCenter, AlignCenter);
 
         dialog_message_show(loader->dialogs, message);
         dialog_message_free(message);
-        string_clear(buffer);
+        furi_string_free(buffer);
     }
 
-    string_clear(error_message);
+    furi_string_free(error_message);
 
     if(file_selected) {
         flipper_application_free(loader->app);
@@ -128,7 +143,7 @@ static bool fap_loader_select_app(FapLoader* loader) {
     const DialogsFileBrowserOptions browser_options = {
         .extension = ".fap",
         .skip_assets = true,
-        .icon = &I_badusb_10px,
+        .icon = &I_unknown_10px,
         .hide_ext = true,
         .item_loader_callback = fap_loader_item_callback,
         .item_loader_context = loader,
@@ -138,39 +153,45 @@ static bool fap_loader_select_app(FapLoader* loader) {
         loader->dialogs, loader->fap_path, loader->fap_path, &browser_options);
 }
 
-int32_t fap_loader_app(void* p) {
+static FapLoader* fap_loader_alloc(const char* path) {
     FapLoader* loader = malloc(sizeof(FapLoader));
+    loader->fap_path = furi_string_alloc_set(path);
     loader->storage = furi_record_open(RECORD_STORAGE);
     loader->dialogs = furi_record_open(RECORD_DIALOGS);
     loader->gui = furi_record_open(RECORD_GUI);
+    loader->view_dispatcher = view_dispatcher_alloc();
+    loader->loading = loading_alloc();
+    view_dispatcher_attach_to_gui(
+        loader->view_dispatcher, loader->gui, ViewDispatcherTypeFullscreen);
+    view_dispatcher_add_view(loader->view_dispatcher, 0, loading_get_view(loader->loading));
+    return loader;
+}
 
-    ViewDispatcher* view_dispatcher = view_dispatcher_alloc();
-    Loading* loading = loading_alloc();
-
-    view_dispatcher_enable_queue(view_dispatcher);
-    view_dispatcher_attach_to_gui(view_dispatcher, loader->gui, ViewDispatcherTypeFullscreen);
-    view_dispatcher_add_view(view_dispatcher, 0, loading_get_view(loading));
+static void fap_loader_free(FapLoader* loader) {
+    view_dispatcher_remove_view(loader->view_dispatcher, 0);
+    loading_free(loader->loading);
+    view_dispatcher_free(loader->view_dispatcher);
+    furi_string_free(loader->fap_path);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_DIALOGS);
+    furi_record_close(RECORD_STORAGE);
+    free(loader);
+}
 
+int32_t fap_loader_app(void* p) {
+    FapLoader* loader;
     if(p) {
-        string_init_set(loader->fap_path, (const char*)p);
+        loader = fap_loader_alloc((const char*)p);
+        view_dispatcher_switch_to_view(loader->view_dispatcher, 0);
         fap_loader_run_selected_app(loader);
     } else {
-        string_init_set(loader->fap_path, EXT_PATH("apps"));
-
+        loader = fap_loader_alloc(EXT_PATH("apps"));
         while(fap_loader_select_app(loader)) {
-            view_dispatcher_switch_to_view(view_dispatcher, 0);
+            view_dispatcher_switch_to_view(loader->view_dispatcher, 0);
             fap_loader_run_selected_app(loader);
         };
     }
 
-    view_dispatcher_remove_view(view_dispatcher, 0);
-    loading_free(loading);
-    view_dispatcher_free(view_dispatcher);
-
-    string_clear(loader->fap_path);
-    furi_record_close(RECORD_GUI);
-    furi_record_close(RECORD_DIALOGS);
-    furi_record_close(RECORD_STORAGE);
-    free(loader);
+    fap_loader_free(loader);
     return 0;
-}
+}

+ 27 - 0
applications/main/fap_loader/fap_loader_app.h

@@ -0,0 +1,27 @@
+#pragma once
+#include <storage/storage.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct FapLoader FapLoader;
+
+/**
+ * @brief Load name and icon from FAP file.
+ * 
+ * @param path Path to FAP file.
+ * @param storage Storage instance.
+ * @param icon_ptr Icon pointer.
+ * @param item_name Application name.
+ * @return true if icon and name were loaded successfully.
+ */
+bool fap_loader_load_name_and_icon(
+    FuriString* path,
+    Storage* storage,
+    uint8_t** icon_ptr,
+    FuriString* item_name);
+
+#ifdef __cplusplus
+}
+#endif

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

@@ -24,6 +24,7 @@ struct GpioApp {
     Widget* widget;
 
     VariableItemList* var_item_list;
+    VariableItem* var_item_flow;
     GpioTest* gpio_test;
     GpioUsbUart* gpio_usb_uart;
     UsbUartBridge* usb_uart_bridge;

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

@@ -1,6 +1,7 @@
 #include "../gpio_app_i.h"
 #include "furi_hal_power.h"
 #include "furi_hal_usb.h"
+#include <dolphin/dolphin.h>
 
 enum GpioItem {
     GpioItemUsbUart,
@@ -88,6 +89,7 @@ bool gpio_scene_start_on_event(void* context, SceneManagerEvent event) {
         } else if(event.event == GpioStartEventUsbUart) {
             scene_manager_set_scene_state(app->scene_manager, GpioSceneStart, GpioItemUsbUart);
             if(!furi_hal_usb_is_locked()) {
+                DOLPHIN_DEED(DolphinDeedGpioUartBridge);
                 scene_manager_next_scene(app->scene_manager, GpioSceneUsbUart);
             } else {
                 scene_manager_next_scene(app->scene_manager, GpioSceneUsbUartCloseRpc);

+ 24 - 2
applications/main/gpio/scenes/gpio_scene_usb_uart_config.c

@@ -13,7 +13,7 @@ static UsbUartConfig* cfg_set;
 
 static const char* vcp_ch[] = {"0 (CLI)", "1"};
 static const char* uart_ch[] = {"13,14", "15,16"};
-static const char* flow_pins[] = {"None", "2,3", "6,7"};
+static const char* flow_pins[] = {"None", "2,3", "6,7", "16,15"};
 static const char* baudrate_mode[] = {"Host"};
 static const uint32_t baudrate_list[] = {
     2400,
@@ -33,6 +33,24 @@ bool gpio_scene_usb_uart_cfg_on_event(void* context, SceneManagerEvent event) {
     return false;
 }
 
+void line_ensure_flow_invariant(GpioApp* app) {
+    // GPIO pins PC0, PC1 (16,15) are unavailable for RTS/DTR when LPUART is
+    // selected. This function enforces that invariant by resetting flow_pins
+    // to None if it is configured to 16,15 when LPUART is selected.
+
+    uint8_t available_flow_pins = cfg_set->uart_ch == FuriHalUartIdLPUART1 ? 3 : 4;
+    VariableItem* item = app->var_item_flow;
+    variable_item_set_values_count(item, available_flow_pins);
+
+    if(cfg_set->flow_pins >= available_flow_pins) {
+        cfg_set->flow_pins = 0;
+        usb_uart_set_config(app->usb_uart_bridge, cfg_set);
+
+        variable_item_set_current_value_index(item, cfg_set->flow_pins);
+        variable_item_set_current_value_text(item, flow_pins[cfg_set->flow_pins]);
+    }
+}
+
 static void line_vcp_cb(VariableItem* item) {
     GpioApp* app = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
@@ -54,6 +72,7 @@ static void line_port_cb(VariableItem* item) {
     else if(index == 1)
         cfg_set->uart_ch = FuriHalUartIdLPUART1;
     usb_uart_set_config(app->usb_uart_bridge, cfg_set);
+    line_ensure_flow_invariant(app);
 }
 
 static void line_flow_cb(VariableItem* item) {
@@ -116,9 +135,12 @@ void gpio_scene_usb_uart_cfg_on_enter(void* context) {
     variable_item_set_current_value_index(item, cfg_set->uart_ch);
     variable_item_set_current_value_text(item, uart_ch[cfg_set->uart_ch]);
 
-    item = variable_item_list_add(var_item_list, "RTS/DTR Pins", 3, line_flow_cb, app);
+    item = variable_item_list_add(
+        var_item_list, "RTS/DTR Pins", COUNT_OF(flow_pins), line_flow_cb, app);
     variable_item_set_current_value_index(item, cfg_set->flow_pins);
     variable_item_set_current_value_text(item, flow_pins[cfg_set->flow_pins]);
+    app->var_item_flow = item;
+    line_ensure_flow_invariant(app);
 
     variable_item_list_set_selected_item(
         var_item_list, scene_manager_get_scene_state(app->scene_manager, GpioAppViewUsbUartCfg));

+ 8 - 10
applications/main/gpio/usb_uart_bridge.c

@@ -1,6 +1,5 @@
 #include "usb_uart_bridge.h"
 #include "furi_hal.h"
-#include <stream_buffer.h>
 #include <furi_hal_usb_cdc.h>
 #include "usb_cdc.h"
 #include "cli/cli_vcp.h"
@@ -15,6 +14,7 @@
 static const GpioPin* flow_pins[][2] = {
     {&gpio_ext_pa7, &gpio_ext_pa6}, // 2, 3
     {&gpio_ext_pb2, &gpio_ext_pc3}, // 6, 7
+    {&gpio_ext_pc0, &gpio_ext_pc1}, // 16, 15
 };
 
 typedef enum {
@@ -43,7 +43,7 @@ struct UsbUartBridge {
     FuriThread* thread;
     FuriThread* tx_thread;
 
-    StreamBufferHandle_t rx_stream;
+    FuriStreamBuffer* rx_stream;
 
     FuriMutex* usb_mutex;
 
@@ -74,12 +74,10 @@ static int32_t usb_uart_tx_thread(void* context);
 
 static void usb_uart_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) {
     UsbUartBridge* usb_uart = (UsbUartBridge*)context;
-    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
 
     if(ev == UartIrqEventRXNE) {
-        xStreamBufferSendFromISR(usb_uart->rx_stream, &data, 1, &xHigherPriorityTaskWoken);
+        furi_stream_buffer_send(usb_uart->rx_stream, &data, 1, 0);
         furi_thread_flags_set(furi_thread_get_id(usb_uart->thread), WorkerEvtRxDone);
-        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
     }
 }
 
@@ -156,7 +154,7 @@ static int32_t usb_uart_worker(void* context) {
 
     memcpy(&usb_uart->cfg, &usb_uart->cfg_new, sizeof(UsbUartConfig));
 
-    usb_uart->rx_stream = xStreamBufferCreate(USB_UART_RX_BUF_SIZE, 1);
+    usb_uart->rx_stream = furi_stream_buffer_alloc(USB_UART_RX_BUF_SIZE, 1);
 
     usb_uart->tx_sem = furi_semaphore_alloc(1, 1);
     usb_uart->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
@@ -189,8 +187,8 @@ static int32_t usb_uart_worker(void* context) {
         furi_check((events & FuriFlagError) == 0);
         if(events & WorkerEvtStop) break;
         if(events & WorkerEvtRxDone) {
-            size_t len =
-                xStreamBufferReceive(usb_uart->rx_stream, usb_uart->rx_buf, USB_CDC_PKT_LEN, 0);
+            size_t len = furi_stream_buffer_receive(
+                usb_uart->rx_stream, usb_uart->rx_buf, USB_CDC_PKT_LEN, 0);
             if(len > 0) {
                 if(furi_semaphore_acquire(usb_uart->tx_sem, 100) == FuriStatusOk) {
                     usb_uart->st.rx_cnt += len;
@@ -199,7 +197,7 @@ static int32_t usb_uart_worker(void* context) {
                     furi_hal_cdc_send(usb_uart->cfg.vcp_ch, usb_uart->rx_buf, len);
                     furi_check(furi_mutex_release(usb_uart->usb_mutex) == FuriStatusOk);
                 } else {
-                    xStreamBufferReset(usb_uart->rx_stream);
+                    furi_stream_buffer_reset(usb_uart->rx_stream);
                 }
             }
         }
@@ -270,7 +268,7 @@ static int32_t usb_uart_worker(void* context) {
     furi_thread_join(usb_uart->tx_thread);
     furi_thread_free(usb_uart->tx_thread);
 
-    vStreamBufferDelete(usb_uart->rx_stream);
+    furi_stream_buffer_free(usb_uart->rx_stream);
     furi_mutex_free(usb_uart->usb_mutex);
     furi_semaphore_free(usb_uart->tx_sem);
 

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

@@ -48,23 +48,27 @@ static bool gpio_test_input_callback(InputEvent* event, void* context) {
 
 static bool gpio_test_process_left(GpioTest* gpio_test) {
     with_view_model(
-        gpio_test->view, (GpioTestModel * model) {
+        gpio_test->view,
+        GpioTestModel * model,
+        {
             if(model->pin_idx) {
                 model->pin_idx--;
             }
-            return true;
-        });
+        },
+        true);
     return true;
 }
 
 static bool gpio_test_process_right(GpioTest* gpio_test) {
     with_view_model(
-        gpio_test->view, (GpioTestModel * model) {
+        gpio_test->view,
+        GpioTestModel * model,
+        {
             if(model->pin_idx < GPIO_ITEM_COUNT) {
                 model->pin_idx++;
             }
-            return true;
-        });
+        },
+        true);
     return true;
 }
 
@@ -72,7 +76,9 @@ static bool gpio_test_process_ok(GpioTest* gpio_test, InputEvent* event) {
     bool consumed = false;
 
     with_view_model(
-        gpio_test->view, (GpioTestModel * model) {
+        gpio_test->view,
+        GpioTestModel * model,
+        {
             if(event->type == InputTypePress) {
                 if(model->pin_idx < GPIO_ITEM_COUNT) {
                     gpio_item_set_pin(model->pin_idx, true);
@@ -89,8 +95,8 @@ static bool gpio_test_process_ok(GpioTest* gpio_test, InputEvent* event) {
                 consumed = true;
             }
             gpio_test->callback(event->type, gpio_test->context);
-            return true;
-        });
+        },
+        true);
 
     return consumed;
 }
@@ -122,10 +128,12 @@ void gpio_test_set_ok_callback(GpioTest* gpio_test, GpioTestOkCallback callback,
     furi_assert(gpio_test);
     furi_assert(callback);
     with_view_model(
-        gpio_test->view, (GpioTestModel * model) {
+        gpio_test->view,
+        GpioTestModel * model,
+        {
             UNUSED(model);
             gpio_test->callback = callback;
             gpio_test->context = context;
-            return false;
-        });
+        },
+        false);
 }

+ 12 - 8
applications/main/gpio/views/gpio_usb_uart.c

@@ -54,7 +54,7 @@ static void gpio_usb_uart_draw_callback(Canvas* canvas, void* _model) {
         canvas_draw_str_aligned(canvas, 116, 24, AlignRight, AlignBottom, temp_str);
     } else {
         canvas_set_font(canvas, FontSecondary);
-        canvas_draw_str_aligned(canvas, 127, 24, AlignRight, AlignBottom, "KB.");
+        canvas_draw_str_aligned(canvas, 127, 24, AlignRight, AlignBottom, "KiB.");
         canvas_set_font(canvas, FontKeyboard);
         snprintf(temp_str, 18, "%lu", model->tx_cnt / 1024);
         canvas_draw_str_aligned(canvas, 111, 24, AlignRight, AlignBottom, temp_str);
@@ -68,7 +68,7 @@ static void gpio_usb_uart_draw_callback(Canvas* canvas, void* _model) {
         canvas_draw_str_aligned(canvas, 116, 41, AlignRight, AlignBottom, temp_str);
     } else {
         canvas_set_font(canvas, FontSecondary);
-        canvas_draw_str_aligned(canvas, 127, 41, AlignRight, AlignBottom, "KB.");
+        canvas_draw_str_aligned(canvas, 127, 41, AlignRight, AlignBottom, "KiB.");
         canvas_set_font(canvas, FontKeyboard);
         snprintf(temp_str, 18, "%lu", model->rx_cnt / 1024);
         canvas_draw_str_aligned(canvas, 111, 41, AlignRight, AlignBottom, temp_str);
@@ -129,12 +129,14 @@ void gpio_usb_uart_set_callback(GpioUsbUart* usb_uart, GpioUsbUartCallback callb
     furi_assert(callback);
 
     with_view_model(
-        usb_uart->view, (GpioUsbUartModel * model) {
+        usb_uart->view,
+        GpioUsbUartModel * model,
+        {
             UNUSED(model);
             usb_uart->callback = callback;
             usb_uart->context = context;
-            return false;
-        });
+        },
+        false);
 }
 
 void gpio_usb_uart_update_state(GpioUsbUart* instance, UsbUartConfig* cfg, UsbUartState* st) {
@@ -143,7 +145,9 @@ void gpio_usb_uart_update_state(GpioUsbUart* instance, UsbUartConfig* cfg, UsbUa
     furi_assert(st);
 
     with_view_model(
-        instance->view, (GpioUsbUartModel * model) {
+        instance->view,
+        GpioUsbUartModel * model,
+        {
             model->baudrate = st->baudrate_cur;
             model->vcp_port = cfg->vcp_ch;
             model->tx_pin = (cfg->uart_ch == 0) ? (13) : (15);
@@ -152,6 +156,6 @@ void gpio_usb_uart_update_state(GpioUsbUart* instance, UsbUartConfig* cfg, UsbUa
             model->rx_active = (model->rx_cnt != st->rx_cnt);
             model->tx_cnt = st->tx_cnt;
             model->rx_cnt = st->rx_cnt;
-            return true;
-        });
+        },
+        true);
 }

+ 16 - 17
applications/main/ibutton/ibutton.c

@@ -2,7 +2,6 @@
 #include "assets_icons.h"
 #include "ibutton_i.h"
 #include "ibutton/scenes/ibutton_scene.h"
-#include "m-string.h"
 #include <toolbox/path.h>
 #include <flipper_format/flipper_format.h>
 #include <rpc/rpc_app.h>
@@ -39,25 +38,25 @@ static void ibutton_make_app_folder(iButton* ibutton) {
     }
 }
 
-bool ibutton_load_key_data(iButton* ibutton, string_t key_path, bool show_dialog) {
+bool ibutton_load_key_data(iButton* ibutton, FuriString* key_path, bool show_dialog) {
     FlipperFormat* file = flipper_format_file_alloc(ibutton->storage);
     bool result = false;
-    string_t data;
-    string_init(data);
+    FuriString* data;
+    data = furi_string_alloc();
 
     do {
-        if(!flipper_format_file_open_existing(file, string_get_cstr(key_path))) break;
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(key_path))) break;
 
         // header
         uint32_t version;
         if(!flipper_format_read_header(file, data, &version)) break;
-        if(string_cmp_str(data, IBUTTON_APP_FILE_TYPE) != 0) break;
+        if(furi_string_cmp_str(data, IBUTTON_APP_FILE_TYPE) != 0) break;
         if(version != 1) break;
 
         // key type
         iButtonKeyType type;
         if(!flipper_format_read_string(file, "Key type", data)) break;
-        if(!ibutton_key_get_type_by_string(string_get_cstr(data), &type)) break;
+        if(!ibutton_key_get_type_by_string(furi_string_get_cstr(data), &type)) break;
 
         // key data
         uint8_t key_data[IBUTTON_KEY_DATA_SIZE] = {0};
@@ -71,7 +70,7 @@ bool ibutton_load_key_data(iButton* ibutton, string_t key_path, bool show_dialog
     } while(false);
 
     flipper_format_free(file);
-    string_clear(data);
+    furi_string_free(data);
 
     if((!result) && (show_dialog)) {
         dialog_message_show_storage_error(ibutton->dialogs, "Cannot load\nkey file");
@@ -119,7 +118,7 @@ void ibutton_tick_event_callback(void* context) {
 iButton* ibutton_alloc() {
     iButton* ibutton = malloc(sizeof(iButton));
 
-    string_init(ibutton->file_path);
+    ibutton->file_path = furi_string_alloc();
 
     ibutton->scene_manager = scene_manager_alloc(&ibutton_scene_handlers, ibutton);
 
@@ -210,7 +209,7 @@ void ibutton_free(iButton* ibutton) {
     ibutton_worker_free(ibutton->key_worker);
     ibutton_key_free(ibutton->key);
 
-    string_clear(ibutton->file_path);
+    furi_string_free(ibutton->file_path);
 
     free(ibutton);
 }
@@ -240,19 +239,19 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) {
 
     do {
         // Check if we has old key
-        if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+        if(furi_string_end_with(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
             // First remove old key
             ibutton_delete_key(ibutton);
 
             // Remove old key name from path
-            size_t filename_start = string_search_rchar(ibutton->file_path, '/');
-            string_left(ibutton->file_path, filename_start);
+            size_t filename_start = furi_string_search_rchar(ibutton->file_path, '/');
+            furi_string_left(ibutton->file_path, filename_start);
         }
 
-        string_cat_printf(ibutton->file_path, "/%s%s", key_name, IBUTTON_APP_EXTENSION);
+        furi_string_cat_printf(ibutton->file_path, "/%s%s", key_name, IBUTTON_APP_EXTENSION);
 
         // Open file for write
-        if(!flipper_format_file_open_always(file, string_get_cstr(ibutton->file_path))) break;
+        if(!flipper_format_file_open_always(file, furi_string_get_cstr(ibutton->file_path))) break;
 
         // Write header
         if(!flipper_format_write_header_cstr(file, IBUTTON_APP_FILE_TYPE, 1)) break;
@@ -286,7 +285,7 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) {
 
 bool ibutton_delete_key(iButton* ibutton) {
     bool result = false;
-    result = storage_simply_remove(ibutton->storage, string_get_cstr(ibutton->file_path));
+    result = storage_simply_remove(ibutton->storage, furi_string_get_cstr(ibutton->file_path));
 
     return result;
 }
@@ -326,7 +325,7 @@ int32_t ibutton_app(void* p) {
             rpc_system_app_set_callback(ibutton->rpc_ctx, ibutton_rpc_command_callback, ibutton);
             rpc_system_app_send_started(ibutton->rpc_ctx);
         } else {
-            string_set_str(ibutton->file_path, (const char*)p);
+            furi_string_set(ibutton->file_path, (const char*)p);
             if(ibutton_load_key_data(ibutton, ibutton->file_path, true)) {
                 key_loaded = true;
                 // TODO: Display an error if the key from p could not be loaded

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

@@ -6,8 +6,8 @@
 #include <one_wire/ibutton/ibutton_worker.h>
 #include <one_wire/one_wire_host.h>
 
-static void ibutton_cli(Cli* cli, string_t args, void* context);
-static void onewire_cli(Cli* cli, string_t args, void* context);
+static void ibutton_cli(Cli* cli, FuriString* args, void* context);
+static void onewire_cli(Cli* cli, FuriString* args, void* context);
 
 // app cli function
 void ibutton_on_system_start() {
@@ -34,16 +34,16 @@ void ibutton_cli_print_usage() {
     printf("\t<key_data> are hex-formatted\r\n");
 };
 
-bool ibutton_cli_get_key_type(string_t data, iButtonKeyType* type) {
+bool ibutton_cli_get_key_type(FuriString* data, iButtonKeyType* type) {
     bool result = false;
 
-    if(string_cmp_str(data, "Dallas") == 0 || string_cmp_str(data, "dallas") == 0) {
+    if(furi_string_cmp_str(data, "Dallas") == 0 || furi_string_cmp_str(data, "dallas") == 0) {
         result = true;
         *type = iButtonKeyDS1990;
-    } else if(string_cmp_str(data, "Cyfral") == 0 || string_cmp_str(data, "cyfral") == 0) {
+    } else if(furi_string_cmp_str(data, "Cyfral") == 0 || furi_string_cmp_str(data, "cyfral") == 0) {
         result = true;
         *type = iButtonKeyCyfral;
-    } else if(string_cmp_str(data, "Metakom") == 0 || string_cmp_str(data, "metakom") == 0) {
+    } else if(furi_string_cmp_str(data, "Metakom") == 0 || furi_string_cmp_str(data, "metakom") == 0) {
         result = true;
         *type = iButtonKeyMetakom;
     }
@@ -123,17 +123,17 @@ static void ibutton_cli_worker_write_cb(void* context, iButtonWorkerWriteResult
     furi_event_flag_set(write_context->event, EVENT_FLAG_IBUTTON_COMPLETE);
 }
 
-void ibutton_cli_write(Cli* cli, string_t args) {
+void ibutton_cli_write(Cli* cli, FuriString* args) {
     iButtonKey* key = ibutton_key_alloc();
     iButtonWorker* worker = ibutton_worker_alloc();
     iButtonKeyType type;
     iButtonWriteContext write_context;
     uint8_t key_data[IBUTTON_KEY_DATA_SIZE];
-    string_t data;
+    FuriString* data;
 
     write_context.event = furi_event_flag_alloc();
 
-    string_init(data);
+    data = furi_string_alloc();
     ibutton_worker_start_thread(worker);
     ibutton_worker_write_set_callback(worker, ibutton_cli_worker_write_cb, &write_context);
 
@@ -186,7 +186,7 @@ void ibutton_cli_write(Cli* cli, string_t args) {
         ibutton_worker_stop(worker);
     } while(false);
 
-    string_clear(data);
+    furi_string_free(data);
     ibutton_worker_stop_thread(worker);
     ibutton_worker_free(worker);
     ibutton_key_free(key);
@@ -194,14 +194,14 @@ void ibutton_cli_write(Cli* cli, string_t args) {
     furi_event_flag_free(write_context.event);
 };
 
-void ibutton_cli_emulate(Cli* cli, string_t args) {
+void ibutton_cli_emulate(Cli* cli, FuriString* args) {
     iButtonKey* key = ibutton_key_alloc();
     iButtonWorker* worker = ibutton_worker_alloc();
     iButtonKeyType type;
     uint8_t key_data[IBUTTON_KEY_DATA_SIZE];
-    string_t data;
+    FuriString* data;
 
-    string_init(data);
+    data = furi_string_alloc();
     ibutton_worker_start_thread(worker);
 
     do {
@@ -234,34 +234,34 @@ void ibutton_cli_emulate(Cli* cli, string_t args) {
         ibutton_worker_stop(worker);
     } while(false);
 
-    string_clear(data);
+    furi_string_free(data);
     ibutton_worker_stop_thread(worker);
     ibutton_worker_free(worker);
     ibutton_key_free(key);
 };
 
-static void ibutton_cli(Cli* cli, string_t args, void* context) {
+static void ibutton_cli(Cli* cli, FuriString* args, void* context) {
     UNUSED(context);
-    string_t cmd;
-    string_init(cmd);
+    FuriString* cmd;
+    cmd = furi_string_alloc();
 
     if(!args_read_string_and_trim(args, cmd)) {
-        string_clear(cmd);
+        furi_string_free(cmd);
         ibutton_cli_print_usage();
         return;
     }
 
-    if(string_cmp_str(cmd, "read") == 0) {
+    if(furi_string_cmp_str(cmd, "read") == 0) {
         ibutton_cli_read(cli);
-    } else if(string_cmp_str(cmd, "write") == 0) {
+    } else if(furi_string_cmp_str(cmd, "write") == 0) {
         ibutton_cli_write(cli, args);
-    } else if(string_cmp_str(cmd, "emulate") == 0) {
+    } else if(furi_string_cmp_str(cmd, "emulate") == 0) {
         ibutton_cli_emulate(cli, args);
     } else {
         ibutton_cli_print_usage();
     }
 
-    string_clear(cmd);
+    furi_string_free(cmd);
 }
 
 void onewire_cli_print_usage() {
@@ -299,20 +299,20 @@ static void onewire_cli_search(Cli* cli) {
     onewire_host_free(onewire);
 }
 
-void onewire_cli(Cli* cli, string_t args, void* context) {
+void onewire_cli(Cli* cli, FuriString* args, void* context) {
     UNUSED(context);
-    string_t cmd;
-    string_init(cmd);
+    FuriString* cmd;
+    cmd = furi_string_alloc();
 
     if(!args_read_string_and_trim(args, cmd)) {
-        string_clear(cmd);
+        furi_string_free(cmd);
         onewire_cli_print_usage();
         return;
     }
 
-    if(string_cmp_str(cmd, "search") == 0) {
+    if(furi_string_cmp_str(cmd, "search") == 0) {
         onewire_cli_search(cli);
     }
 
-    string_clear(cmd);
+    furi_string_free(cmd);
 }

+ 2 - 2
applications/main/ibutton/ibutton_i.h

@@ -41,7 +41,7 @@ struct iButton {
     iButtonWorker* key_worker;
     iButtonKey* key;
 
-    string_t file_path;
+    FuriString* file_path;
     char text_store[IBUTTON_TEXT_STORE_SIZE + 1];
 
     Submenu* submenu;
@@ -78,7 +78,7 @@ typedef enum {
 } iButtonNotificationMessage;
 
 bool ibutton_file_select(iButton* ibutton);
-bool ibutton_load_key_data(iButton* ibutton, string_t key_path, bool show_dialog);
+bool ibutton_load_key_data(iButton* ibutton, FuriString* key_path, bool show_dialog);
 bool ibutton_save_key(iButton* ibutton, const char* key_name);
 bool ibutton_delete_key(iButton* ibutton);
 void ibutton_text_store_set(iButton* ibutton, const char* text, ...);

+ 1 - 2
applications/main/ibutton/scenes/ibutton_scene_add_type.c

@@ -1,5 +1,4 @@
 #include "../ibutton_i.h"
-#include "m-string.h"
 
 enum SubmenuIndex {
     SubmenuIndexCyfral,
@@ -47,7 +46,7 @@ bool ibutton_scene_add_type_on_event(void* context, SceneManagerEvent event) {
             furi_crash("Unknown key type");
         }
 
-        string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER);
+        furi_string_set(ibutton->file_path, IBUTTON_APP_FOLDER);
         ibutton_key_clear_data(key);
         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneAddValue);
     }

+ 9 - 9
applications/main/ibutton/scenes/ibutton_scene_delete_confirm.c

@@ -17,13 +17,13 @@ void ibutton_scene_delete_confirm_on_enter(void* context) {
     iButtonKey* key = ibutton->key;
     const uint8_t* key_data = ibutton_key_get_data_p(key);
 
-    string_t key_name;
-    string_init(key_name);
+    FuriString* key_name;
+    key_name = furi_string_alloc();
     path_extract_filename(ibutton->file_path, key_name, true);
 
-    ibutton_text_store_set(ibutton, "\e#Delete %s?\e#", string_get_cstr(key_name));
+    ibutton_text_store_set(ibutton, "\e#Delete %s?\e#", furi_string_get_cstr(key_name));
     widget_add_text_box_element(
-        widget, 0, 0, 128, 27, AlignCenter, AlignCenter, ibutton->text_store, false);
+        widget, 0, 0, 128, 27, AlignCenter, AlignCenter, ibutton->text_store, true);
     widget_add_button_element(
         widget, GuiButtonTypeLeft, "Cancel", ibutton_scene_delete_confirm_widget_callback, ibutton);
     widget_add_button_element(
@@ -47,28 +47,28 @@ void ibutton_scene_delete_confirm_on_enter(void* context) {
             key_data[6],
             key_data[7]);
         widget_add_string_element(
-            widget, 64, 45, AlignCenter, AlignBottom, FontSecondary, "Dallas");
+            widget, 64, 34, AlignCenter, AlignBottom, FontSecondary, "Dallas");
         break;
 
     case iButtonKeyCyfral:
         ibutton_text_store_set(ibutton, "%02X %02X", key_data[0], key_data[1]);
         widget_add_string_element(
-            widget, 64, 45, AlignCenter, AlignBottom, FontSecondary, "Cyfral");
+            widget, 64, 34, AlignCenter, AlignBottom, FontSecondary, "Cyfral");
         break;
 
     case iButtonKeyMetakom:
         ibutton_text_store_set(
             ibutton, "%02X %02X %02X %02X", key_data[0], key_data[1], key_data[2], key_data[3]);
         widget_add_string_element(
-            widget, 64, 45, AlignCenter, AlignBottom, FontSecondary, "Metakom");
+            widget, 64, 34, AlignCenter, AlignBottom, FontSecondary, "Metakom");
         break;
     }
     widget_add_string_element(
-        widget, 64, 33, AlignCenter, AlignBottom, FontSecondary, ibutton->text_store);
+        widget, 64, 46, AlignCenter, AlignBottom, FontSecondary, ibutton->text_store);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget);
 
-    string_clear(key_name);
+    furi_string_free(key_name);
 }
 
 bool ibutton_scene_delete_confirm_on_event(void* context, SceneManagerEvent event) {

+ 17 - 38
applications/main/ibutton/scenes/ibutton_scene_emulate.c

@@ -15,31 +15,29 @@ static void ibutton_scene_emulate_callback(void* context, bool emulated) {
 
 void ibutton_scene_emulate_on_enter(void* context) {
     iButton* ibutton = context;
-    Popup* popup = ibutton->popup;
+    Widget* widget = ibutton->widget;
     iButtonKey* key = ibutton->key;
 
     const uint8_t* key_data = ibutton_key_get_data_p(key);
 
-    string_t key_name;
-    string_init(key_name);
-    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+    FuriString* key_name;
+    key_name = furi_string_alloc();
+    if(furi_string_end_with(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
         path_extract_filename(ibutton->file_path, key_name, true);
     }
 
-    uint8_t line_count = 2;
     DOLPHIN_DEED(DolphinDeedIbuttonEmulate);
 
     // check that stored key has name
-    if(!string_empty_p(key_name)) {
-        ibutton_text_store_set(ibutton, "emulating\n%s", string_get_cstr(key_name));
-        line_count = 2;
+    if(!furi_string_empty(key_name)) {
+        ibutton_text_store_set(ibutton, "%s", furi_string_get_cstr(key_name));
     } else {
         // if not, show key data
         switch(ibutton_key_get_type(key)) {
         case iButtonKeyDS1990:
             ibutton_text_store_set(
                 ibutton,
-                "emulating\n%02X %02X %02X %02X\n%02X %02X %02X %02X",
+                "%02X %02X %02X %02X\n%02X %02X %02X %02X",
                 key_data[0],
                 key_data[1],
                 key_data[2],
@@ -48,46 +46,30 @@ void ibutton_scene_emulate_on_enter(void* context) {
                 key_data[5],
                 key_data[6],
                 key_data[7]);
-            line_count = 3;
             break;
         case iButtonKeyCyfral:
-            ibutton_text_store_set(ibutton, "emulating\n%02X %02X", key_data[0], key_data[1]);
-            line_count = 2;
+            ibutton_text_store_set(ibutton, "%02X %02X", key_data[0], key_data[1]);
             break;
         case iButtonKeyMetakom:
             ibutton_text_store_set(
-                ibutton,
-                "emulating\n%02X %02X %02X %02X",
-                key_data[0],
-                key_data[1],
-                key_data[2],
-                key_data[3]);
-            line_count = 2;
+                ibutton, "%02X %02X %02X %02X", key_data[0], key_data[1], key_data[2], key_data[3]);
             break;
         }
     }
 
-    switch(line_count) {
-    case 3:
-        popup_set_header(popup, "iButton", 82, 18, AlignCenter, AlignBottom);
-        popup_set_text(popup, ibutton->text_store, 82, 22, AlignCenter, AlignTop);
-        break;
-
-    default:
-        popup_set_header(popup, "iButton", 82, 24, AlignCenter, AlignBottom);
-        popup_set_text(popup, ibutton->text_store, 82, 28, AlignCenter, AlignTop);
-        break;
-    }
-
-    popup_set_icon(popup, 2, 10, &I_iButtonKey_49x44);
+    widget_add_string_multiline_element(
+        widget, 90, 10, AlignCenter, AlignTop, FontPrimary, "iButton\nemulating");
+    widget_add_icon_element(widget, 3, 10, &I_iButtonKey_49x44);
+    widget_add_text_box_element(
+        widget, 54, 39, 75, 22, AlignCenter, AlignCenter, ibutton->text_store, true);
 
-    view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewPopup);
+    view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget);
 
     ibutton_worker_emulate_set_callback(
         ibutton->key_worker, ibutton_scene_emulate_callback, ibutton);
     ibutton_worker_emulate_start(ibutton->key_worker, key);
 
-    string_clear(key_name);
+    furi_string_free(key_name);
 
     ibutton_notification_message(ibutton, iButtonNotificationMessageEmulateStart);
 }
@@ -122,10 +104,7 @@ bool ibutton_scene_emulate_on_event(void* context, SceneManagerEvent event) {
 
 void ibutton_scene_emulate_on_exit(void* context) {
     iButton* ibutton = context;
-    Popup* popup = ibutton->popup;
     ibutton_worker_stop(ibutton->key_worker);
-    popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom);
-    popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop);
-    popup_set_icon(popup, 0, 0, NULL);
+    widget_reset(ibutton->widget);
     ibutton_notification_message(ibutton, iButtonNotificationMessageBlinkStop);
 }

+ 9 - 11
applications/main/ibutton/scenes/ibutton_scene_info.c

@@ -8,13 +8,13 @@ void ibutton_scene_info_on_enter(void* context) {
 
     const uint8_t* key_data = ibutton_key_get_data_p(key);
 
-    string_t key_name;
-    string_init(key_name);
+    FuriString* key_name;
+    key_name = furi_string_alloc();
     path_extract_filename(ibutton->file_path, key_name, true);
 
-    ibutton_text_store_set(ibutton, "%s", string_get_cstr(key_name));
+    ibutton_text_store_set(ibutton, "%s", furi_string_get_cstr(key_name));
     widget_add_text_box_element(
-        widget, 0, 0, 128, 28, AlignCenter, AlignCenter, ibutton->text_store, false);
+        widget, 0, 0, 128, 23, AlignCenter, AlignCenter, ibutton->text_store, true);
 
     switch(ibutton_key_get_type(key)) {
     case iButtonKeyDS1990:
@@ -29,30 +29,28 @@ void ibutton_scene_info_on_enter(void* context) {
             key_data[5],
             key_data[6],
             key_data[7]);
-        widget_add_string_element(
-            widget, 64, 51, AlignCenter, AlignBottom, FontSecondary, "Dallas");
+        widget_add_string_element(widget, 64, 36, AlignCenter, AlignBottom, FontPrimary, "Dallas");
         break;
 
     case iButtonKeyMetakom:
         ibutton_text_store_set(
             ibutton, "%02X %02X %02X %02X", key_data[0], key_data[1], key_data[2], key_data[3]);
         widget_add_string_element(
-            widget, 64, 51, AlignCenter, AlignBottom, FontSecondary, "Metakom");
+            widget, 64, 36, AlignCenter, AlignBottom, FontPrimary, "Metakom");
         break;
 
     case iButtonKeyCyfral:
         ibutton_text_store_set(ibutton, "%02X %02X", key_data[0], key_data[1]);
-        widget_add_string_element(
-            widget, 64, 51, AlignCenter, AlignBottom, FontSecondary, "Cyfral");
+        widget_add_string_element(widget, 64, 36, AlignCenter, AlignBottom, FontPrimary, "Cyfral");
         break;
     }
 
     widget_add_string_element(
-        widget, 64, 35, AlignCenter, AlignBottom, FontPrimary, ibutton->text_store);
+        widget, 64, 50, AlignCenter, AlignBottom, FontSecondary, ibutton->text_store);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget);
 
-    string_clear(key_name);
+    furi_string_free(key_name);
 }
 
 bool ibutton_scene_info_on_event(void* context, SceneManagerEvent event) {

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

@@ -18,7 +18,7 @@ void ibutton_scene_read_on_enter(void* context) {
     popup_set_icon(popup, 0, 5, &I_DolphinWait_61x59);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewPopup);
-    string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER);
+    furi_string_set(ibutton->file_path, IBUTTON_APP_FOLDER);
 
     ibutton_worker_read_set_callback(worker, ibutton_scene_read_callback, ibutton);
     ibutton_worker_read_start(worker, key);

+ 9 - 9
applications/main/ibutton/scenes/ibutton_scene_rpc.c

@@ -29,19 +29,19 @@ bool ibutton_scene_rpc_on_event(void* context, SceneManagerEvent event) {
         if(event.event == iButtonCustomEventRpcLoad) {
             const char* arg = rpc_system_app_get_data(ibutton->rpc_ctx);
             bool result = false;
-            if(arg && (string_empty_p(ibutton->file_path))) {
-                string_set_str(ibutton->file_path, arg);
+            if(arg && (furi_string_empty(ibutton->file_path))) {
+                furi_string_set(ibutton->file_path, arg);
                 if(ibutton_load_key_data(ibutton, ibutton->file_path, false)) {
                     ibutton_worker_emulate_start(ibutton->key_worker, ibutton->key);
-                    string_t key_name;
-                    string_init(key_name);
-                    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+                    FuriString* key_name;
+                    key_name = furi_string_alloc();
+                    if(furi_string_end_with(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
                         path_extract_filename(ibutton->file_path, key_name, true);
                     }
 
-                    if(!string_empty_p(key_name)) {
+                    if(!furi_string_empty(key_name)) {
                         ibutton_text_store_set(
-                            ibutton, "emulating\n%s", string_get_cstr(key_name));
+                            ibutton, "emulating\n%s", furi_string_get_cstr(key_name));
                     } else {
                         ibutton_text_store_set(ibutton, "emulating");
                     }
@@ -49,10 +49,10 @@ bool ibutton_scene_rpc_on_event(void* context, SceneManagerEvent event) {
 
                     ibutton_notification_message(ibutton, iButtonNotificationMessageEmulateStart);
 
-                    string_clear(key_name);
+                    furi_string_free(key_name);
                     result = true;
                 } else {
-                    string_reset(ibutton->file_path);
+                    furi_string_reset(ibutton->file_path);
                 }
             }
             rpc_system_app_confirm(ibutton->rpc_ctx, RpcAppEventLoadFile, result);

+ 11 - 12
applications/main/ibutton/scenes/ibutton_scene_save_name.c

@@ -1,5 +1,4 @@
 #include "../ibutton_i.h"
-#include "m-string.h"
 #include <lib/toolbox/random_name.h>
 #include <toolbox/path.h>
 
@@ -12,17 +11,17 @@ void ibutton_scene_save_name_on_enter(void* context) {
     iButton* ibutton = context;
     TextInput* text_input = ibutton->text_input;
 
-    string_t key_name;
-    string_init(key_name);
-    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+    FuriString* key_name;
+    key_name = furi_string_alloc();
+    if(furi_string_end_with(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
         path_extract_filename(ibutton->file_path, key_name, true);
     }
 
-    const bool key_name_is_empty = string_empty_p(key_name);
+    const bool key_name_is_empty = furi_string_empty(key_name);
     if(key_name_is_empty) {
         set_random_name(ibutton->text_store, IBUTTON_TEXT_STORE_SIZE);
     } else {
-        ibutton_text_store_set(ibutton, "%s", string_get_cstr(key_name));
+        ibutton_text_store_set(ibutton, "%s", furi_string_get_cstr(key_name));
     }
 
     text_input_set_header_text(text_input, "Name the key");
@@ -34,19 +33,19 @@ void ibutton_scene_save_name_on_enter(void* context) {
         IBUTTON_KEY_NAME_SIZE,
         key_name_is_empty);
 
-    string_t folder_path;
-    string_init(folder_path);
+    FuriString* folder_path;
+    folder_path = furi_string_alloc();
 
-    path_extract_dirname(string_get_cstr(ibutton->file_path), folder_path);
+    path_extract_dirname(furi_string_get_cstr(ibutton->file_path), folder_path);
 
     ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
-        string_get_cstr(folder_path), IBUTTON_APP_EXTENSION, string_get_cstr(key_name));
+        furi_string_get_cstr(folder_path), IBUTTON_APP_EXTENSION, furi_string_get_cstr(key_name));
     text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewTextInput);
 
-    string_clear(key_name);
-    string_clear(folder_path);
+    furi_string_free(key_name);
+    furi_string_free(folder_path);
 }
 
 bool ibutton_scene_save_name_on_event(void* context, SceneManagerEvent event) {

+ 2 - 4
applications/main/ibutton/scenes/ibutton_scene_save_success.c

@@ -29,10 +29,8 @@ bool ibutton_scene_save_success_on_event(void* context, SceneManagerEvent event)
     if(event.type == SceneManagerEventTypeCustom) {
         consumed = true;
         if(event.event == iButtonCustomEventBack) {
-            const uint32_t possible_scenes[] = {
-                iButtonSceneReadKeyMenu, iButtonSceneSavedKeyMenu, iButtonSceneAddType};
-            scene_manager_search_and_switch_to_previous_scene_one_of(
-                ibutton->scene_manager, possible_scenes, COUNT_OF(possible_scenes));
+            scene_manager_search_and_switch_to_another_scene(
+                ibutton->scene_manager, iButtonSceneSelectKey);
         }
     }
 

+ 1 - 1
applications/main/ibutton/scenes/ibutton_scene_start.c

@@ -39,7 +39,7 @@ bool ibutton_scene_start_on_event(void* context, SceneManagerEvent event) {
         if(event.event == SubmenuIndexRead) {
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneRead);
         } else if(event.event == SubmenuIndexSaved) {
-            string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER);
+            furi_string_set(ibutton->file_path, IBUTTON_APP_FOLDER);
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSelectKey);
         } else if(event.event == SubmenuIndexAdd) {
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneAddType);

+ 17 - 40
applications/main/ibutton/scenes/ibutton_scene_write.c

@@ -1,5 +1,4 @@
 #include "../ibutton_i.h"
-#include "m-string.h"
 #include "toolbox/path.h"
 
 typedef enum {
@@ -14,31 +13,28 @@ static void ibutton_scene_write_callback(void* context, iButtonWorkerWriteResult
 
 void ibutton_scene_write_on_enter(void* context) {
     iButton* ibutton = context;
-    Popup* popup = ibutton->popup;
     iButtonKey* key = ibutton->key;
+    Widget* widget = ibutton->widget;
     iButtonWorker* worker = ibutton->key_worker;
 
     const uint8_t* key_data = ibutton_key_get_data_p(key);
 
-    string_t key_name;
-    string_init(key_name);
-    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+    FuriString* key_name;
+    key_name = furi_string_alloc();
+    if(furi_string_end_with(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
         path_extract_filename(ibutton->file_path, key_name, true);
     }
 
-    uint8_t line_count = 2;
-
     // check that stored key has name
-    if(!string_empty_p(key_name)) {
-        ibutton_text_store_set(ibutton, "writing\n%s", string_get_cstr(key_name));
-        line_count = 2;
+    if(!furi_string_empty(key_name)) {
+        ibutton_text_store_set(ibutton, "%s", furi_string_get_cstr(key_name));
     } else {
         // if not, show key data
         switch(ibutton_key_get_type(key)) {
         case iButtonKeyDS1990:
             ibutton_text_store_set(
                 ibutton,
-                "writing\n%02X %02X %02X %02X\n%02X %02X %02X %02X",
+                "%02X %02X %02X %02X\n%02X %02X %02X %02X",
                 key_data[0],
                 key_data[1],
                 key_data[2],
@@ -47,45 +43,29 @@ void ibutton_scene_write_on_enter(void* context) {
                 key_data[5],
                 key_data[6],
                 key_data[7]);
-            line_count = 3;
             break;
         case iButtonKeyCyfral:
-            ibutton_text_store_set(ibutton, "writing\n%02X %02X", key_data[0], key_data[1]);
-            line_count = 2;
+            ibutton_text_store_set(ibutton, "%02X %02X", key_data[0], key_data[1]);
             break;
         case iButtonKeyMetakom:
             ibutton_text_store_set(
-                ibutton,
-                "writing\n%02X %02X %02X %02X",
-                key_data[0],
-                key_data[1],
-                key_data[2],
-                key_data[3]);
-            line_count = 2;
+                ibutton, "%02X %02X %02X %02X", key_data[0], key_data[1], key_data[2], key_data[3]);
             break;
         }
     }
 
-    switch(line_count) {
-    case 3:
-        popup_set_header(popup, "iButton", 82, 18, AlignCenter, AlignBottom);
-        popup_set_text(popup, ibutton->text_store, 82, 22, AlignCenter, AlignTop);
-        break;
-
-    default:
-        popup_set_header(popup, "iButton", 82, 24, AlignCenter, AlignBottom);
-        popup_set_text(popup, ibutton->text_store, 82, 28, AlignCenter, AlignTop);
-        break;
-    }
-
-    popup_set_icon(popup, 2, 10, &I_iButtonKey_49x44);
+    widget_add_string_multiline_element(
+        widget, 90, 10, AlignCenter, AlignTop, FontPrimary, "iButton\nwriting");
+    widget_add_icon_element(widget, 3, 10, &I_iButtonKey_49x44);
+    widget_add_text_box_element(
+        widget, 54, 39, 75, 22, AlignCenter, AlignCenter, ibutton->text_store, true);
 
-    view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewPopup);
+    view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget);
 
     ibutton_worker_write_set_callback(worker, ibutton_scene_write_callback, ibutton);
     ibutton_worker_write_start(worker, key);
 
-    string_clear(key_name);
+    furi_string_free(key_name);
 
     ibutton_notification_message(ibutton, iButtonNotificationMessageEmulateStart);
 }
@@ -114,11 +94,8 @@ bool ibutton_scene_write_on_event(void* context, SceneManagerEvent event) {
 
 void ibutton_scene_write_on_exit(void* context) {
     iButton* ibutton = context;
-    Popup* popup = ibutton->popup;
     ibutton_worker_stop(ibutton->key_worker);
-    popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom);
-    popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop);
-    popup_set_icon(popup, 0, 0, NULL);
+    widget_reset(ibutton->widget);
 
     ibutton_notification_message(ibutton, iButtonNotificationMessageBlinkStop);
 }

+ 46 - 43
applications/main/infrared/infrared.c

@@ -65,51 +65,52 @@ static void infrared_rpc_command_callback(RpcAppSystemEvent event, void* context
     }
 }
 
-static void infrared_find_vacant_remote_name(string_t name, const char* path) {
+static void infrared_find_vacant_remote_name(FuriString* name, const char* path) {
     Storage* storage = furi_record_open(RECORD_STORAGE);
 
-    string_t base_path;
-    string_init_set_str(base_path, path);
+    FuriString* base_path;
+    base_path = furi_string_alloc_set(path);
 
-    if(string_end_with_str_p(base_path, INFRARED_APP_EXTENSION)) {
-        size_t filename_start = string_search_rchar(base_path, '/');
-        string_left(base_path, filename_start);
+    if(furi_string_end_with(base_path, INFRARED_APP_EXTENSION)) {
+        size_t filename_start = furi_string_search_rchar(base_path, '/');
+        furi_string_left(base_path, filename_start);
     }
 
-    string_printf(base_path, "%s/%s%s", path, string_get_cstr(name), INFRARED_APP_EXTENSION);
+    furi_string_printf(
+        base_path, "%s/%s%s", path, furi_string_get_cstr(name), INFRARED_APP_EXTENSION);
 
-    FS_Error status = storage_common_stat(storage, string_get_cstr(base_path), NULL);
+    FS_Error status = storage_common_stat(storage, furi_string_get_cstr(base_path), NULL);
 
     if(status == FSE_OK) {
         /* If the suggested name is occupied, try another one (name2, name3, etc) */
-        size_t dot = string_search_rchar(base_path, '.');
-        string_left(base_path, dot);
+        size_t dot = furi_string_search_rchar(base_path, '.');
+        furi_string_left(base_path, dot);
 
-        string_t path_temp;
-        string_init(path_temp);
+        FuriString* path_temp;
+        path_temp = furi_string_alloc();
 
         uint32_t i = 1;
         do {
-            string_printf(
-                path_temp, "%s%u%s", string_get_cstr(base_path), ++i, INFRARED_APP_EXTENSION);
-            status = storage_common_stat(storage, string_get_cstr(path_temp), NULL);
+            furi_string_printf(
+                path_temp, "%s%lu%s", furi_string_get_cstr(base_path), ++i, INFRARED_APP_EXTENSION);
+            status = storage_common_stat(storage, furi_string_get_cstr(path_temp), NULL);
         } while(status == FSE_OK);
 
-        string_clear(path_temp);
+        furi_string_free(path_temp);
 
         if(status == FSE_NOT_EXIST) {
-            string_cat_printf(name, "%u", i);
+            furi_string_cat_printf(name, "%lu", i);
         }
     }
 
-    string_clear(base_path);
+    furi_string_free(base_path);
     furi_record_close(RECORD_STORAGE);
 }
 
 static Infrared* infrared_alloc() {
     Infrared* infrared = malloc(sizeof(Infrared));
 
-    string_init(infrared->file_path);
+    infrared->file_path = furi_string_alloc();
 
     InfraredAppState* app_state = &infrared->app_state;
     app_state->is_learning_new_remote = false;
@@ -232,7 +233,7 @@ static void infrared_free(Infrared* infrared) {
     furi_record_close(RECORD_GUI);
     infrared->gui = NULL;
 
-    string_clear(infrared->file_path);
+    furi_string_free(infrared->file_path);
 
     free(infrared);
 }
@@ -243,19 +244,20 @@ bool infrared_add_remote_with_button(
     InfraredSignal* signal) {
     InfraredRemote* remote = infrared->remote;
 
-    string_t new_name, new_path;
-    string_init_set_str(new_name, INFRARED_DEFAULT_REMOTE_NAME);
-    string_init_set_str(new_path, INFRARED_APP_FOLDER);
+    FuriString *new_name, *new_path;
+    new_name = furi_string_alloc_set(INFRARED_DEFAULT_REMOTE_NAME);
+    new_path = furi_string_alloc_set(INFRARED_APP_FOLDER);
 
-    infrared_find_vacant_remote_name(new_name, string_get_cstr(new_path));
-    string_cat_printf(new_path, "/%s%s", string_get_cstr(new_name), INFRARED_APP_EXTENSION);
+    infrared_find_vacant_remote_name(new_name, furi_string_get_cstr(new_path));
+    furi_string_cat_printf(
+        new_path, "/%s%s", furi_string_get_cstr(new_name), INFRARED_APP_EXTENSION);
 
     infrared_remote_reset(remote);
-    infrared_remote_set_name(remote, string_get_cstr(new_name));
-    infrared_remote_set_path(remote, string_get_cstr(new_path));
+    infrared_remote_set_name(remote, furi_string_get_cstr(new_name));
+    infrared_remote_set_path(remote, furi_string_get_cstr(new_path));
 
-    string_clear(new_name);
-    string_clear(new_path);
+    furi_string_free(new_name);
+    furi_string_free(new_path);
     return infrared_remote_add_button(remote, button_name, signal);
 }
 
@@ -267,28 +269,29 @@ bool infrared_rename_current_remote(Infrared* infrared, const char* name) {
         return true;
     }
 
-    string_t new_name;
-    string_init_set_str(new_name, name);
+    FuriString* new_name;
+    new_name = furi_string_alloc_set(name);
 
     infrared_find_vacant_remote_name(new_name, remote_path);
 
-    string_t new_path;
-    string_init_set(new_path, infrared_remote_get_path(remote));
-    if(string_end_with_str_p(new_path, INFRARED_APP_EXTENSION)) {
-        size_t filename_start = string_search_rchar(new_path, '/');
-        string_left(new_path, filename_start);
+    FuriString* new_path;
+    new_path = furi_string_alloc_set(infrared_remote_get_path(remote));
+    if(furi_string_end_with(new_path, INFRARED_APP_EXTENSION)) {
+        size_t filename_start = furi_string_search_rchar(new_path, '/');
+        furi_string_left(new_path, filename_start);
     }
-    string_cat_printf(new_path, "/%s%s", string_get_cstr(new_name), INFRARED_APP_EXTENSION);
+    furi_string_cat_printf(
+        new_path, "/%s%s", furi_string_get_cstr(new_name), INFRARED_APP_EXTENSION);
 
     Storage* storage = furi_record_open(RECORD_STORAGE);
 
     FS_Error status = storage_common_rename(
-        storage, infrared_remote_get_path(remote), string_get_cstr(new_path));
-    infrared_remote_set_name(remote, string_get_cstr(new_name));
-    infrared_remote_set_path(remote, string_get_cstr(new_path));
+        storage, infrared_remote_get_path(remote), furi_string_get_cstr(new_path));
+    infrared_remote_set_name(remote, furi_string_get_cstr(new_name));
+    infrared_remote_set_path(remote, furi_string_get_cstr(new_path));
 
-    string_clear(new_name);
-    string_clear(new_path);
+    furi_string_free(new_name);
+    furi_string_free(new_path);
 
     furi_record_close(RECORD_STORAGE);
     return (status == FSE_OK || status == FSE_EXIST);
@@ -435,7 +438,7 @@ int32_t infrared_app(void* p) {
             rpc_system_app_send_started(infrared->rpc_ctx);
             is_rpc_mode = true;
         } else {
-            string_set_str(infrared->file_path, (const char*)p);
+            furi_string_set(infrared->file_path, (const char*)p);
             is_remote_loaded = infrared_remote_load(infrared->remote, infrared->file_path);
             if(!is_remote_loaded) {
                 dialog_message_show_storage_error(

+ 39 - 41
applications/main/infrared/infrared_brute_force.c

@@ -2,7 +2,6 @@
 
 #include <stdlib.h>
 #include <m-dict.h>
-#include <m-string.h>
 #include <flipper_format/flipper_format.h>
 
 #include "infrared_signal.h"
@@ -14,39 +13,45 @@ typedef struct {
 
 DICT_DEF2(
     InfraredBruteForceRecordDict,
-    string_t,
-    STRING_OPLIST,
+    FuriString*,
+    FURI_STRING_OPLIST,
     InfraredBruteForceRecord,
     M_POD_OPLIST);
 
 struct InfraredBruteForce {
     FlipperFormat* ff;
     const char* db_filename;
-    string_t current_record_name;
+    FuriString* current_record_name;
+    InfraredSignal* current_signal;
     InfraredBruteForceRecordDict_t records;
+    bool is_started;
 };
 
 InfraredBruteForce* infrared_brute_force_alloc() {
     InfraredBruteForce* brute_force = malloc(sizeof(InfraredBruteForce));
     brute_force->ff = NULL;
     brute_force->db_filename = NULL;
-    string_init(brute_force->current_record_name);
+    brute_force->current_signal = NULL;
+    brute_force->is_started = false;
+    brute_force->current_record_name = furi_string_alloc();
     InfraredBruteForceRecordDict_init(brute_force->records);
     return brute_force;
 }
 
 void infrared_brute_force_free(InfraredBruteForce* brute_force) {
-    furi_assert(!brute_force->ff);
+    furi_assert(!brute_force->is_started);
     InfraredBruteForceRecordDict_clear(brute_force->records);
-    string_clear(brute_force->current_record_name);
+    furi_string_free(brute_force->current_record_name);
     free(brute_force);
 }
 
 void infrared_brute_force_set_db_filename(InfraredBruteForce* brute_force, const char* db_filename) {
+    furi_assert(!brute_force->is_started);
     brute_force->db_filename = db_filename;
 }
 
 bool infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force) {
+    furi_assert(!brute_force->is_started);
     furi_assert(brute_force->db_filename);
     bool success = false;
 
@@ -55,8 +60,8 @@ bool infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force) {
 
     success = flipper_format_buffered_file_open_existing(ff, brute_force->db_filename);
     if(success) {
-        string_t signal_name;
-        string_init(signal_name);
+        FuriString* signal_name;
+        signal_name = furi_string_alloc();
         while(flipper_format_read_string(ff, "name", signal_name)) {
             InfraredBruteForceRecord* record =
                 InfraredBruteForceRecordDict_get(brute_force->records, signal_name);
@@ -64,7 +69,7 @@ bool infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force) {
                 ++(record->count);
             }
         }
-        string_clear(signal_name);
+        furi_string_free(signal_name);
     }
 
     flipper_format_free(ff);
@@ -76,6 +81,7 @@ bool infrared_brute_force_start(
     InfraredBruteForce* brute_force,
     uint32_t index,
     uint32_t* record_count) {
+    furi_assert(!brute_force->is_started);
     bool success = false;
     *record_count = 0;
 
@@ -87,7 +93,7 @@ bool infrared_brute_force_start(
         if(record->value.index == index) {
             *record_count = record->value.count;
             if(*record_count) {
-                string_set(brute_force->current_record_name, record->key);
+                furi_string_set(brute_force->current_record_name, record->key);
             }
             break;
         }
@@ -96,50 +102,37 @@ bool infrared_brute_force_start(
     if(*record_count) {
         Storage* storage = furi_record_open(RECORD_STORAGE);
         brute_force->ff = flipper_format_buffered_file_alloc(storage);
+        brute_force->current_signal = infrared_signal_alloc();
+        brute_force->is_started = true;
         success =
             flipper_format_buffered_file_open_existing(brute_force->ff, brute_force->db_filename);
-        if(!success) {
-            flipper_format_free(brute_force->ff);
-            brute_force->ff = NULL;
-            furi_record_close(RECORD_STORAGE);
-        }
+        if(!success) infrared_brute_force_stop(brute_force);
     }
     return success;
 }
 
 bool infrared_brute_force_is_started(InfraredBruteForce* brute_force) {
-    return brute_force->ff;
+    return brute_force->is_started;
 }
 
 void infrared_brute_force_stop(InfraredBruteForce* brute_force) {
-    furi_assert(string_size(brute_force->current_record_name));
-    furi_assert(brute_force->ff);
-
-    string_reset(brute_force->current_record_name);
+    furi_assert(brute_force->is_started);
+    furi_string_reset(brute_force->current_record_name);
+    infrared_signal_free(brute_force->current_signal);
     flipper_format_free(brute_force->ff);
-    furi_record_close(RECORD_STORAGE);
+    brute_force->current_signal = NULL;
     brute_force->ff = NULL;
+    brute_force->is_started = false;
+    furi_record_close(RECORD_STORAGE);
 }
 
 bool infrared_brute_force_send_next(InfraredBruteForce* brute_force) {
-    furi_assert(string_size(brute_force->current_record_name));
-    furi_assert(brute_force->ff);
-    bool success = false;
-
-    string_t signal_name;
-    string_init(signal_name);
-    InfraredSignal* signal = infrared_signal_alloc();
-
-    do {
-        success = infrared_signal_read(signal, brute_force->ff, signal_name);
-    } while(success && !string_equal_p(brute_force->current_record_name, signal_name));
-
+    furi_assert(brute_force->is_started);
+    const bool success = infrared_signal_search_and_read(
+        brute_force->current_signal, brute_force->ff, brute_force->current_record_name);
     if(success) {
-        infrared_signal_transmit(signal);
+        infrared_signal_transmit(brute_force->current_signal);
     }
-
-    infrared_signal_free(signal);
-    string_clear(signal_name);
     return success;
 }
 
@@ -148,8 +141,13 @@ void infrared_brute_force_add_record(
     uint32_t index,
     const char* name) {
     InfraredBruteForceRecord value = {.index = index, .count = 0};
-    string_t key;
-    string_init_set_str(key, name);
+    FuriString* key;
+    key = furi_string_alloc_set(name);
     InfraredBruteForceRecordDict_set_at(brute_force->records, key, value);
-    string_clear(key);
+    furi_string_free(key);
+}
+
+void infrared_brute_force_reset(InfraredBruteForce* brute_force) {
+    furi_assert(!brute_force->is_started);
+    InfraredBruteForceRecordDict_reset(brute_force->records);
 }

+ 1 - 0
applications/main/infrared/infrared_brute_force.h

@@ -20,3 +20,4 @@ void infrared_brute_force_add_record(
     InfraredBruteForce* brute_force,
     uint32_t index,
     const char* name);
+void infrared_brute_force_reset(InfraredBruteForce* brute_force);

+ 251 - 54
applications/main/infrared/infrared_cli.c

@@ -1,5 +1,5 @@
-#include <m-string.h>
 #include <cli/cli.h>
+#include <cli/cli_i.h>
 #include <infrared.h>
 #include <infrared_worker.h>
 #include <furi_hal_infrared.h>
@@ -7,20 +7,32 @@
 #include <toolbox/args.h>
 
 #include "infrared_signal.h"
+#include "infrared_brute_force.h"
+
+#include <m-dict.h>
 
 #define INFRARED_CLI_BUF_SIZE 10
 
-static void infrared_cli_start_ir_rx(Cli* cli, string_t args);
-static void infrared_cli_start_ir_tx(Cli* cli, string_t args);
-static void infrared_cli_process_decode(Cli* cli, string_t args);
+DICT_DEF2(dict_signals, FuriString*, FURI_STRING_OPLIST, int, M_DEFAULT_OPLIST)
+
+enum RemoteTypes { TV = 0, AC = 1 };
+
+static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args);
+static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args);
+static void infrared_cli_process_decode(Cli* cli, FuriString* args);
+static void infrared_cli_process_universal(Cli* cli, FuriString* args);
+static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type);
+static void
+    infrared_cli_brute_force_signals(Cli* cli, enum RemoteTypes remote_type, FuriString* signal);
 
 static const struct {
     const char* cmd;
-    void (*process_function)(Cli* cli, string_t args);
+    void (*process_function)(Cli* cli, FuriString* args);
 } infrared_cli_commands[] = {
     {.cmd = "rx", .process_function = infrared_cli_start_ir_rx},
     {.cmd = "tx", .process_function = infrared_cli_start_ir_tx},
     {.cmd = "decode", .process_function = infrared_cli_process_decode},
+    {.cmd = "universal", .process_function = infrared_cli_process_universal},
 };
 
 static void signal_received_callback(void* context, InfraredWorkerSignal* received_signal) {
@@ -58,25 +70,9 @@ static void signal_received_callback(void* context, InfraredWorkerSignal* receiv
     }
 }
 
-static void infrared_cli_start_ir_rx(Cli* cli, string_t args) {
-    UNUSED(cli);
-    UNUSED(args);
-    InfraredWorker* worker = infrared_worker_alloc();
-    infrared_worker_rx_start(worker);
-    infrared_worker_rx_set_received_signal_callback(worker, signal_received_callback, cli);
-
-    printf("Receiving INFRARED...\r\nPress Ctrl+C to abort\r\n");
-    while(!cli_cmd_interrupt_received(cli)) {
-        furi_delay_ms(50);
-    }
-
-    infrared_worker_rx_stop(worker);
-    infrared_worker_free(worker);
-}
-
 static void infrared_cli_print_usage(void) {
     printf("Usage:\r\n");
-    printf("\tir rx\r\n");
+    printf("\tir rx [raw]\r\n");
     printf("\tir tx <protocol> <address> <command>\r\n");
     printf("\t<command> and <address> are hex-formatted\r\n");
     printf("\tAvailable protocols:");
@@ -91,6 +87,37 @@ static void infrared_cli_print_usage(void) {
         INFRARED_MIN_FREQUENCY,
         INFRARED_MAX_FREQUENCY);
     printf("\tir decode <input_file> [<output_file>]\r\n");
+    printf("\tir universal <tv, ac> <signal name>\r\n");
+    printf("\tir universal list <tv, ac>\r\n");
+}
+
+static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) {
+    UNUSED(cli);
+
+    bool enable_decoding = true;
+
+    if(!furi_string_empty(args)) {
+        if(!furi_string_cmp_str(args, "raw")) {
+            enable_decoding = false;
+        } else {
+            printf("Wrong arguments.\r\n");
+            infrared_cli_print_usage();
+            return;
+        }
+    }
+
+    InfraredWorker* worker = infrared_worker_alloc();
+    infrared_worker_rx_enable_signal_decoding(worker, enable_decoding);
+    infrared_worker_rx_start(worker);
+    infrared_worker_rx_set_received_signal_callback(worker, signal_received_callback, cli);
+
+    printf("Receiving %s INFRARED...\r\nPress Ctrl+C to abort\r\n", enable_decoding ? "" : "RAW");
+    while(!cli_cmd_interrupt_received(cli)) {
+        furi_delay_ms(50);
+    }
+
+    infrared_worker_rx_stop(worker);
+    infrared_worker_free(worker);
 }
 
 static bool infrared_cli_parse_message(const char* str, InfraredSignal* signal) {
@@ -151,9 +178,9 @@ static bool infrared_cli_parse_raw(const char* str, InfraredSignal* signal) {
     return infrared_signal_is_valid(signal);
 }
 
-static void infrared_cli_start_ir_tx(Cli* cli, string_t args) {
+static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args) {
     UNUSED(cli);
-    const char* str = string_get_cstr(args);
+    const char* str = furi_string_get_cstr(args);
     InfraredSignal* signal = infrared_signal_alloc();
 
     bool success = infrared_cli_parse_message(str, signal) || infrared_cli_parse_raw(str, signal);
@@ -231,8 +258,8 @@ static bool infrared_cli_decode_file(FlipperFormat* input_file, FlipperFormat* o
     InfraredSignal* signal = infrared_signal_alloc();
     InfraredDecoderHandler* decoder = infrared_alloc_decoder();
 
-    string_t tmp;
-    string_init(tmp);
+    FuriString* tmp;
+    tmp = furi_string_alloc();
 
     while(infrared_signal_read(signal, input_file, tmp)) {
         ret = false;
@@ -242,7 +269,7 @@ static bool infrared_cli_decode_file(FlipperFormat* input_file, FlipperFormat* o
         }
         if(!infrared_signal_is_raw(signal)) {
             if(output_file &&
-               !infrared_cli_save_signal(signal, output_file, string_get_cstr(tmp))) {
+               !infrared_cli_save_signal(signal, output_file, furi_string_get_cstr(tmp))) {
                 break;
             } else {
                 printf("Skipping decoded signal\r\n");
@@ -250,31 +277,33 @@ 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", string_get_cstr(tmp), raw_signal->timings_size);
-        if(!infrared_cli_decode_raw_signal(raw_signal, decoder, output_file, string_get_cstr(tmp)))
+        printf(
+            "Raw signal: %s, %u 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;
         ret = true;
     }
 
     infrared_free_decoder(decoder);
     infrared_signal_free(signal);
-    string_clear(tmp);
+    furi_string_free(tmp);
 
     return ret;
 }
 
-static void infrared_cli_process_decode(Cli* cli, string_t args) {
+static void infrared_cli_process_decode(Cli* cli, FuriString* args) {
     UNUSED(cli);
     Storage* storage = furi_record_open(RECORD_STORAGE);
     FlipperFormat* input_file = flipper_format_buffered_file_alloc(storage);
     FlipperFormat* output_file = NULL;
 
     uint32_t version;
-    string_t tmp, header, input_path, output_path;
-    string_init(tmp);
-    string_init(header);
-    string_init(input_path);
-    string_init(output_path);
+    FuriString *tmp, *header, *input_path, *output_path;
+    tmp = furi_string_alloc();
+    header = furi_string_alloc();
+    input_path = furi_string_alloc();
+    output_path = furi_string_alloc();
 
     do {
         if(!args_read_probably_quoted_string_and_trim(args, input_path)) {
@@ -283,26 +312,32 @@ static void infrared_cli_process_decode(Cli* cli, string_t args) {
             break;
         }
         args_read_probably_quoted_string_and_trim(args, output_path);
-        if(!flipper_format_buffered_file_open_existing(input_file, string_get_cstr(input_path))) {
-            printf("Failed to open file for reading: \"%s\"\r\n", string_get_cstr(input_path));
+        if(!flipper_format_buffered_file_open_existing(
+               input_file, furi_string_get_cstr(input_path))) {
+            printf(
+                "Failed to open file for reading: \"%s\"\r\n", furi_string_get_cstr(input_path));
             break;
         }
         if(!flipper_format_read_header(input_file, header, &version) ||
-           (!string_start_with_str_p(header, "IR")) || version != 1) {
-            printf("Invalid or corrupted input file: \"%s\"\r\n", string_get_cstr(input_path));
+           (!furi_string_start_with_str(header, "IR")) || version != 1) {
+            printf(
+                "Invalid or corrupted input file: \"%s\"\r\n", furi_string_get_cstr(input_path));
             break;
         }
-        if(!string_empty_p(output_path)) {
-            printf("Writing output to file: \"%s\"\r\n", string_get_cstr(output_path));
+        if(!furi_string_empty(output_path)) {
+            printf("Writing output to file: \"%s\"\r\n", furi_string_get_cstr(output_path));
             output_file = flipper_format_file_alloc(storage);
         }
         if(output_file &&
-           !flipper_format_file_open_always(output_file, string_get_cstr(output_path))) {
-            printf("Failed to open file for writing: \"%s\"\r\n", string_get_cstr(output_path));
+           !flipper_format_file_open_always(output_file, furi_string_get_cstr(output_path))) {
+            printf(
+                "Failed to open file for writing: \"%s\"\r\n", furi_string_get_cstr(output_path));
             break;
         }
         if(output_file && !flipper_format_write_header(output_file, header, version)) {
-            printf("Failed to write to the output file: \"%s\"\r\n", string_get_cstr(output_path));
+            printf(
+                "Failed to write to the output file: \"%s\"\r\n",
+                furi_string_get_cstr(output_path));
             break;
         }
         if(!infrared_cli_decode_file(input_file, output_file)) {
@@ -311,31 +346,193 @@ static void infrared_cli_process_decode(Cli* cli, string_t args) {
         printf("File successfully decoded.\r\n");
     } while(false);
 
-    string_clear(tmp);
-    string_clear(header);
-    string_clear(input_path);
-    string_clear(output_path);
+    furi_string_free(tmp);
+    furi_string_free(header);
+    furi_string_free(input_path);
+    furi_string_free(output_path);
 
     flipper_format_free(input_file);
     if(output_file) flipper_format_free(output_file);
     furi_record_close(RECORD_STORAGE);
 }
 
-static void infrared_cli_start_ir(Cli* cli, string_t args, void* context) {
+static void infrared_cli_process_universal(Cli* cli, FuriString* args) {
+    enum RemoteTypes Remote;
+
+    FuriString* command;
+    FuriString* remote;
+    FuriString* signal;
+    command = furi_string_alloc();
+    remote = furi_string_alloc();
+    signal = furi_string_alloc();
+
+    do {
+        if(!args_read_string_and_trim(args, command)) {
+            infrared_cli_print_usage();
+            break;
+        }
+
+        if(furi_string_cmp_str(command, "list") == 0) {
+            args_read_string_and_trim(args, remote);
+            if(furi_string_cmp_str(remote, "tv") == 0) {
+                Remote = TV;
+            } else if(furi_string_cmp_str(remote, "ac") == 0) {
+                Remote = AC;
+            } else {
+                printf("Invalid remote type.\r\n");
+                break;
+            }
+            infrared_cli_list_remote_signals(Remote);
+            break;
+        }
+
+        if(furi_string_cmp_str(command, "tv") == 0) {
+            Remote = TV;
+        } else if(furi_string_cmp_str(command, "ac") == 0) {
+            Remote = AC;
+        } else {
+            printf("Invalid remote type.\r\n");
+            break;
+        }
+
+        args_read_string_and_trim(args, signal);
+        if(furi_string_empty(signal)) {
+            printf("Must supply a valid signal for type of remote selected.\r\n");
+            break;
+        }
+
+        infrared_cli_brute_force_signals(cli, Remote, signal);
+        break;
+
+    } while(false);
+
+    furi_string_free(command);
+    furi_string_free(remote);
+    furi_string_free(signal);
+}
+
+static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
+    dict_signals_t signals_dict;
+    FuriString* key;
+    const char* remote_file = NULL;
+    bool success = false;
+    int max = 1;
+
+    switch(remote_type) {
+    case TV:
+        remote_file = EXT_PATH("infrared/assets/tv.ir");
+        break;
+    case AC:
+        remote_file = EXT_PATH("infrared/assets/ac.ir");
+        break;
+    default:
+        break;
+    }
+
+    dict_signals_init(signals_dict);
+    key = furi_string_alloc();
+
+    success = flipper_format_buffered_file_open_existing(ff, remote_file);
+    if(success) {
+        FuriString* signal_name;
+        signal_name = furi_string_alloc();
+        printf("Valid signals:\r\n");
+        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) {
+                (*v)++;
+                max = M_MAX(*v, max);
+            } else {
+                dict_signals_set_at(signals_dict, key, 1);
+            }
+        }
+        dict_signals_it_t it;
+        for(dict_signals_it(it, signals_dict); !dict_signals_end_p(it); dict_signals_next(it)) {
+            const struct dict_signals_pair_s* pair = dict_signals_cref(it);
+            printf("\t%s\r\n", furi_string_get_cstr(pair->key));
+        }
+        furi_string_free(signal_name);
+    }
+
+    furi_string_free(key);
+    dict_signals_clear(signals_dict);
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void
+    infrared_cli_brute_force_signals(Cli* cli, enum RemoteTypes remote_type, FuriString* signal) {
+    InfraredBruteForce* brute_force = infrared_brute_force_alloc();
+    const char* remote_file = NULL;
+    uint32_t i = 0;
+    bool success = false;
+
+    switch(remote_type) {
+    case TV:
+        remote_file = EXT_PATH("infrared/assets/tv.ir");
+        break;
+    case AC:
+        remote_file = EXT_PATH("infrared/assets/ac.ir");
+        break;
+    default:
+        break;
+    }
+
+    infrared_brute_force_set_db_filename(brute_force, remote_file);
+    infrared_brute_force_add_record(brute_force, i++, furi_string_get_cstr(signal));
+
+    success = infrared_brute_force_calculate_messages(brute_force);
+    if(success) {
+        uint32_t record_count;
+        uint32_t index = 0;
+        int records_sent = 0;
+        bool running = false;
+
+        running = infrared_brute_force_start(brute_force, index, &record_count);
+        if(record_count <= 0) {
+            printf("Invalid signal.\n");
+            infrared_brute_force_reset(brute_force);
+            return;
+        }
+
+        printf("Sending %ld codes to the tv.\r\n", record_count);
+        printf("Press Ctrl-C to stop.\r\n");
+        while(running) {
+            running = infrared_brute_force_send_next(brute_force);
+
+            if(cli_cmd_interrupt_received(cli)) break;
+
+            printf("\r%d%% complete.", (int)((float)records_sent++ / (float)record_count * 100));
+            fflush(stdout);
+        }
+
+        infrared_brute_force_stop(brute_force);
+    } else {
+        printf("Invalid signal.\r\n");
+    }
+
+    infrared_brute_force_reset(brute_force);
+    infrared_brute_force_free(brute_force);
+}
+
+static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) {
     UNUSED(context);
     if(furi_hal_infrared_is_busy()) {
         printf("INFRARED is busy. Exiting.");
         return;
     }
 
-    string_t command;
-    string_init(command);
+    FuriString* command;
+    command = furi_string_alloc();
     args_read_string_and_trim(args, command);
 
     size_t i = 0;
     for(; i < COUNT_OF(infrared_cli_commands); ++i) {
         size_t cmd_len = strlen(infrared_cli_commands[i].cmd);
-        if(!strncmp(string_get_cstr(command), infrared_cli_commands[i].cmd, cmd_len)) {
+        if(!strncmp(furi_string_get_cstr(command), infrared_cli_commands[i].cmd, cmd_len)) {
             break;
         }
     }
@@ -346,7 +543,7 @@ static void infrared_cli_start_ir(Cli* cli, string_t args, void* context) {
         infrared_cli_print_usage();
     }
 
-    string_clear(command);
+    furi_string_free(command);
 }
 void infrared_on_system_start() {
 #ifdef SRV_CLI

+ 1 - 1
applications/main/infrared/infrared_i.h

@@ -96,7 +96,7 @@ struct Infrared {
     Loading* loading;
     InfraredProgressView* progress;
 
-    string_t file_path;
+    FuriString* file_path;
     char text_store[INFRARED_TEXT_STORE_NUM][INFRARED_TEXT_STORE_SIZE + 1];
     InfraredAppState app_state;
 

+ 24 - 25
applications/main/infrared/infrared_remote.c

@@ -3,7 +3,6 @@
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdlib.h>
-#include <m-string.h>
 #include <m-array.h>
 #include <toolbox/path.h>
 #include <storage/storage.h>
@@ -15,8 +14,8 @@ ARRAY_DEF(InfraredButtonArray, InfraredRemoteButton*, M_PTR_OPLIST);
 
 struct InfraredRemote {
     InfraredButtonArray_t buttons;
-    string_t name;
-    string_t path;
+    FuriString* name;
+    FuriString* path;
 };
 
 static void infrared_remote_clear_buttons(InfraredRemote* remote) {
@@ -31,39 +30,39 @@ static void infrared_remote_clear_buttons(InfraredRemote* remote) {
 InfraredRemote* infrared_remote_alloc() {
     InfraredRemote* remote = malloc(sizeof(InfraredRemote));
     InfraredButtonArray_init(remote->buttons);
-    string_init(remote->name);
-    string_init(remote->path);
+    remote->name = furi_string_alloc();
+    remote->path = furi_string_alloc();
     return remote;
 }
 
 void infrared_remote_free(InfraredRemote* remote) {
     infrared_remote_clear_buttons(remote);
     InfraredButtonArray_clear(remote->buttons);
-    string_clear(remote->path);
-    string_clear(remote->name);
+    furi_string_free(remote->path);
+    furi_string_free(remote->name);
     free(remote);
 }
 
 void infrared_remote_reset(InfraredRemote* remote) {
     infrared_remote_clear_buttons(remote);
-    string_reset(remote->name);
-    string_reset(remote->path);
+    furi_string_reset(remote->name);
+    furi_string_reset(remote->path);
 }
 
 void infrared_remote_set_name(InfraredRemote* remote, const char* name) {
-    string_set_str(remote->name, name);
+    furi_string_set(remote->name, name);
 }
 
 const char* infrared_remote_get_name(InfraredRemote* remote) {
-    return string_get_cstr(remote->name);
+    return furi_string_get_cstr(remote->name);
 }
 
 void infrared_remote_set_path(InfraredRemote* remote, const char* path) {
-    string_set_str(remote->path, path);
+    furi_string_set(remote->path, path);
 }
 
 const char* infrared_remote_get_path(InfraredRemote* remote) {
-    return string_get_cstr(remote->path);
+    return furi_string_get_cstr(remote->path);
 }
 
 size_t infrared_remote_get_button_count(InfraredRemote* remote) {
@@ -112,7 +111,7 @@ bool infrared_remote_delete_button(InfraredRemote* remote, size_t index) {
 bool infrared_remote_store(InfraredRemote* remote) {
     Storage* storage = furi_record_open(RECORD_STORAGE);
     FlipperFormat* ff = flipper_format_file_alloc(storage);
-    const char* path = string_get_cstr(remote->path);
+    const char* path = furi_string_get_cstr(remote->path);
 
     FURI_LOG_I(TAG, "store file: \'%s\'", path);
 
@@ -138,33 +137,33 @@ bool infrared_remote_store(InfraredRemote* remote) {
     return success;
 }
 
-bool infrared_remote_load(InfraredRemote* remote, string_t path) {
+bool infrared_remote_load(InfraredRemote* remote, FuriString* path) {
     Storage* storage = furi_record_open(RECORD_STORAGE);
     FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
 
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
 
-    FURI_LOG_I(TAG, "load file: \'%s\'", string_get_cstr(path));
-    bool success = flipper_format_buffered_file_open_existing(ff, string_get_cstr(path));
+    FURI_LOG_I(TAG, "load file: \'%s\'", furi_string_get_cstr(path));
+    bool success = flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path));
 
     if(success) {
         uint32_t version;
         success = flipper_format_read_header(ff, buf, &version) &&
-                  !string_cmp_str(buf, "IR signals file") && (version == 1);
+                  !furi_string_cmp(buf, "IR signals file") && (version == 1);
     }
 
     if(success) {
         path_extract_filename(path, buf, true);
         infrared_remote_clear_buttons(remote);
-        infrared_remote_set_name(remote, string_get_cstr(buf));
-        infrared_remote_set_path(remote, string_get_cstr(path));
+        infrared_remote_set_name(remote, furi_string_get_cstr(buf));
+        infrared_remote_set_path(remote, furi_string_get_cstr(path));
 
         for(bool can_read = true; can_read;) {
             InfraredRemoteButton* button = infrared_remote_button_alloc();
             can_read = infrared_signal_read(infrared_remote_button_get_signal(button), ff, buf);
             if(can_read) {
-                infrared_remote_button_set_name(button, string_get_cstr(buf));
+                infrared_remote_button_set_name(button, furi_string_get_cstr(buf));
                 InfraredButtonArray_push_back(remote->buttons, button);
             } else {
                 infrared_remote_button_free(button);
@@ -172,7 +171,7 @@ bool infrared_remote_load(InfraredRemote* remote, string_t path) {
         }
     }
 
-    string_clear(buf);
+    furi_string_free(buf);
     flipper_format_free(ff);
     furi_record_close(RECORD_STORAGE);
     return success;
@@ -181,7 +180,7 @@ bool infrared_remote_load(InfraredRemote* remote, string_t path) {
 bool infrared_remote_remove(InfraredRemote* remote) {
     Storage* storage = furi_record_open(RECORD_STORAGE);
 
-    FS_Error status = storage_common_remove(storage, string_get_cstr(remote->path));
+    FS_Error status = storage_common_remove(storage, furi_string_get_cstr(remote->path));
     infrared_remote_reset(remote);
 
     furi_record_close(RECORD_STORAGE);

+ 1 - 1
applications/main/infrared/infrared_remote.h

@@ -25,5 +25,5 @@ bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name,
 bool infrared_remote_delete_button(InfraredRemote* remote, size_t index);
 
 bool infrared_remote_store(InfraredRemote* remote);
-bool infrared_remote_load(InfraredRemote* remote, string_t path);
+bool infrared_remote_load(InfraredRemote* remote, FuriString* path);
 bool infrared_remote_remove(InfraredRemote* remote);

+ 5 - 6
applications/main/infrared/infrared_remote_button.c

@@ -1,32 +1,31 @@
 #include "infrared_remote_button.h"
 
 #include <stdlib.h>
-#include <m-string.h>
 
 struct InfraredRemoteButton {
-    string_t name;
+    FuriString* name;
     InfraredSignal* signal;
 };
 
 InfraredRemoteButton* infrared_remote_button_alloc() {
     InfraredRemoteButton* button = malloc(sizeof(InfraredRemoteButton));
-    string_init(button->name);
+    button->name = furi_string_alloc();
     button->signal = infrared_signal_alloc();
     return button;
 }
 
 void infrared_remote_button_free(InfraredRemoteButton* button) {
-    string_clear(button->name);
+    furi_string_free(button->name);
     infrared_signal_free(button->signal);
     free(button);
 }
 
 void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name) {
-    string_set_str(button->name, name);
+    furi_string_set(button->name, name);
 }
 
 const char* infrared_remote_button_get_name(InfraredRemoteButton* button) {
-    return string_get_cstr(button->name);
+    return furi_string_get_cstr(button->name);
 }
 
 void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal) {

+ 56 - 20
applications/main/infrared/infrared_signal.c

@@ -61,7 +61,7 @@ static bool infrared_signal_is_raw_valid(InfraredRawSignal* raw) {
     if((raw->frequency > INFRARED_MAX_FREQUENCY) || (raw->frequency < INFRARED_MIN_FREQUENCY)) {
         FURI_LOG_E(
             TAG,
-            "Frequency is out of range (%lX - %lX): %lX",
+            "Frequency is out of range (%X - %X): %lX",
             INFRARED_MIN_FREQUENCY,
             INFRARED_MAX_FREQUENCY,
             raw->frequency);
@@ -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 - %lX): %lX",
+            "Timings amount is out of range (0 - %X): %X",
             MAX_TIMINGS_AMOUNT,
             raw->timings_size);
         return false;
@@ -100,15 +100,15 @@ static inline bool infrared_signal_save_raw(InfraredRawSignal* raw, FlipperForma
 }
 
 static inline bool infrared_signal_read_message(InfraredSignal* signal, FlipperFormat* ff) {
-    string_t buf;
-    string_init(buf);
+    FuriString* buf;
+    buf = furi_string_alloc();
     bool success = false;
 
     do {
         if(!flipper_format_read_string(ff, "protocol", buf)) break;
 
         InfraredMessage message;
-        message.protocol = infrared_get_protocol_by_name(string_get_cstr(buf));
+        message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
 
         success = flipper_format_read_hex(ff, "address", (uint8_t*)&message.address, 4) &&
                   flipper_format_read_hex(ff, "command", (uint8_t*)&message.command, 4) &&
@@ -119,7 +119,7 @@ static inline bool infrared_signal_read_message(InfraredSignal* signal, FlipperF
         infrared_signal_set_message(signal, &message);
     } while(0);
 
-    string_clear(buf);
+    furi_string_free(buf);
     return success;
 }
 
@@ -146,6 +146,26 @@ static inline bool infrared_signal_read_raw(InfraredSignal* signal, FlipperForma
     return success;
 }
 
+static bool infrared_signal_read_body(InfraredSignal* signal, FlipperFormat* ff) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "type", tmp)) break;
+        if(furi_string_equal(tmp, "raw")) {
+            success = infrared_signal_read_raw(signal, ff);
+        } else if(furi_string_equal(tmp, "parsed")) {
+            success = infrared_signal_read_message(signal, ff);
+        } else {
+            FURI_LOG_E(TAG, "Unknown signal type");
+        }
+    } while(false);
+
+    furi_string_free(tmp);
+    return success;
+}
+
 InfraredSignal* infrared_signal_alloc() {
     InfraredSignal* signal = malloc(sizeof(InfraredSignal));
 
@@ -226,25 +246,41 @@ bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char*
     }
 }
 
-bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, string_t name) {
-    string_t buf;
-    string_init(buf);
+bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name) {
+    FuriString* tmp = furi_string_alloc();
+
+    bool success = false;
+
+    do {
+        if(!flipper_format_read_string(ff, "name", tmp)) break;
+        furi_string_set(name, tmp);
+        if(!infrared_signal_read_body(signal, ff)) break;
+        success = true;
+    } while(0);
+
+    furi_string_free(tmp);
+    return success;
+}
+
+bool infrared_signal_search_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const FuriString* name) {
     bool success = false;
+    FuriString* tmp = furi_string_alloc();
 
     do {
-        if(!flipper_format_read_string(ff, "name", buf)) break;
-        string_set(name, buf);
-        if(!flipper_format_read_string(ff, "type", buf)) break;
-        if(!string_cmp_str(buf, "raw")) {
-            success = infrared_signal_read_raw(signal, ff);
-        } else if(!string_cmp_str(buf, "parsed")) {
-            success = infrared_signal_read_message(signal, ff);
-        } else {
-            FURI_LOG_E(TAG, "Unknown type of signal (allowed - raw/parsed) ");
+        bool is_name_found = false;
+        while(flipper_format_read_string(ff, "name", tmp)) {
+            is_name_found = furi_string_equal(name, tmp);
+            if(is_name_found) break;
         }
-    } while(0);
+        if(!is_name_found) break;
+        if(!infrared_signal_read_body(signal, ff)) break;
+        success = true;
+    } while(false);
 
-    string_clear(buf);
+    furi_string_free(tmp);
     return success;
 }
 

+ 5 - 1
applications/main/infrared/infrared_signal.h

@@ -36,6 +36,10 @@ void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage*
 InfraredMessage* infrared_signal_get_message(InfraredSignal* signal);
 
 bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name);
-bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, string_t name);
+bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name);
+bool infrared_signal_search_and_read(
+    InfraredSignal* signal,
+    FlipperFormat* ff,
+    const FuriString* name);
 
 void infrared_signal_transmit(InfraredSignal* signal);

+ 2 - 1
applications/main/infrared/scenes/common/infrared_scene_universal_common.c

@@ -70,7 +70,7 @@ bool infrared_scene_universal_common_on_event(void* context, SceneManagerEvent e
                 uint32_t record_count;
                 if(infrared_brute_force_start(
                        brute_force, infrared_custom_event_get_value(event.event), &record_count)) {
-                    DOLPHIN_DEED(DolphinDeedIrBruteForce);
+                    DOLPHIN_DEED(DolphinDeedIrSend);
                     infrared_scene_universal_common_show_popup(infrared, record_count);
                 } else {
                     scene_manager_next_scene(scene_manager, InfraredSceneErrorDatabases);
@@ -87,5 +87,6 @@ void infrared_scene_universal_common_on_exit(void* context) {
     Infrared* infrared = context;
     ButtonPanel* button_panel = infrared->button_panel;
     view_stack_remove_view(infrared->view_stack, button_panel_get_view(button_panel));
+    infrared_brute_force_reset(infrared->brute_force);
     button_panel_reset(button_panel);
 }

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

@@ -15,6 +15,7 @@ ADD_SCENE(infrared, remote, Remote)
 ADD_SCENE(infrared, remote_list, RemoteList)
 ADD_SCENE(infrared, universal, Universal)
 ADD_SCENE(infrared, universal_tv, UniversalTV)
+ADD_SCENE(infrared, universal_ac, UniversalAC)
 ADD_SCENE(infrared, debug, Debug)
 ADD_SCENE(infrared, error_databases, ErrorDatabases)
 ADD_SCENE(infrared, rpc, Rpc)

+ 6 - 6
applications/main/infrared/scenes/infrared_scene_edit_rename.c

@@ -29,20 +29,20 @@ void infrared_scene_edit_rename_on_enter(void* context) {
         enter_name_length = INFRARED_MAX_REMOTE_NAME_LENGTH;
         strncpy(infrared->text_store[0], infrared_remote_get_name(remote), enter_name_length);
 
-        string_t folder_path;
-        string_init(folder_path);
+        FuriString* folder_path;
+        folder_path = furi_string_alloc();
 
-        if(string_end_with_str_p(infrared->file_path, INFRARED_APP_EXTENSION)) {
-            path_extract_dirname(string_get_cstr(infrared->file_path), folder_path);
+        if(furi_string_end_with(infrared->file_path, INFRARED_APP_EXTENSION)) {
+            path_extract_dirname(furi_string_get_cstr(infrared->file_path), folder_path);
         }
 
         ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
-            string_get_cstr(folder_path),
+            furi_string_get_cstr(folder_path),
             INFRARED_APP_EXTENSION,
             infrared_remote_get_name(remote));
         text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
 
-        string_clear(folder_path);
+        furi_string_free(folder_path);
     } else {
         furi_assert(0);
     }

+ 4 - 2
applications/main/infrared/scenes/infrared_scene_learn_enter_name.c

@@ -50,8 +50,10 @@ bool infrared_scene_learn_enter_name_on_event(void* context, SceneManagerEvent e
             if(success) {
                 scene_manager_next_scene(scene_manager, InfraredSceneLearnDone);
             } else {
-                scene_manager_search_and_switch_to_previous_scene(
-                    scene_manager, InfraredSceneRemoteList);
+                dialog_message_show_storage_error(infrared->dialogs, "Failed to save file");
+                const uint32_t possible_scenes[] = {InfraredSceneRemoteList, InfraredSceneStart};
+                scene_manager_search_and_switch_to_previous_scene_one_of(
+                    scene_manager, possible_scenes, COUNT_OF(possible_scenes));
             }
             consumed = true;
         }

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

@@ -42,7 +42,7 @@ bool infrared_scene_rpc_on_event(void* context, SceneManagerEvent event) {
             bool result = false;
             const char* arg = rpc_system_app_get_data(infrared->rpc_ctx);
             if(arg && (state == InfraredRpcStateIdle)) {
-                string_set_str(infrared->file_path, arg);
+                furi_string_set(infrared->file_path, arg);
                 result = infrared_remote_load(infrared->remote, infrared->file_path);
                 if(result) {
                     scene_manager_set_scene_state(

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

@@ -66,7 +66,7 @@ bool infrared_scene_start_on_event(void* context, SceneManagerEvent event) {
             scene_manager_next_scene(scene_manager, InfraredSceneLearn);
             consumed = true;
         } else if(submenu_index == SubmenuIndexSavedRemotes) {
-            string_set_str(infrared->file_path, INFRARED_APP_FOLDER);
+            furi_string_set(infrared->file_path, INFRARED_APP_FOLDER);
             scene_manager_next_scene(scene_manager, InfraredSceneRemoteList);
             consumed = true;
         } else if(submenu_index == SubmenuIndexDebug) {

+ 10 - 4
applications/main/infrared/scenes/infrared_scene_universal.c

@@ -2,8 +2,8 @@
 
 typedef enum {
     SubmenuIndexUniversalTV,
+    SubmenuIndexUniversalAC,
     SubmenuIndexUniversalAudio,
-    SubmenuIndexUniversalAirConditioner,
 } SubmenuIndex;
 
 static void infrared_scene_universal_submenu_callback(void* context, uint32_t index) {
@@ -21,6 +21,12 @@ void infrared_scene_universal_on_enter(void* context) {
         SubmenuIndexUniversalTV,
         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);
 
     view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewSubmenu);
@@ -35,12 +41,12 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) {
         if(event.event == SubmenuIndexUniversalTV) {
             scene_manager_next_scene(scene_manager, InfraredSceneUniversalTV);
             consumed = true;
+        } else if(event.event == SubmenuIndexUniversalAC) {
+            scene_manager_next_scene(scene_manager, InfraredSceneUniversalAC);
+            consumed = true;
         } else if(event.event == SubmenuIndexUniversalAudio) {
             //TODO Implement Audio universal remote
             consumed = true;
-        } else if(event.event == SubmenuIndexUniversalAirConditioner) {
-            //TODO Implement A/C universal remote
-            consumed = true;
         }
     }
 

+ 109 - 0
applications/main/infrared/scenes/infrared_scene_universal_ac.c

@@ -0,0 +1,109 @@
+#include "../infrared_i.h"
+
+#include "common/infrared_scene_universal_common.h"
+
+void infrared_scene_universal_ac_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/ac.ir"));
+
+    button_panel_reserve(button_panel, 2, 3);
+    uint32_t i = 0;
+    button_panel_add_item(
+        button_panel,
+        i,
+        0,
+        0,
+        3,
+        22,
+        &I_Off_25x27,
+        &I_Off_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Off");
+    button_panel_add_item(
+        button_panel,
+        i,
+        1,
+        0,
+        36,
+        22,
+        &I_Dehumidify_25x27,
+        &I_Dehumidify_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Dh");
+    button_panel_add_item(
+        button_panel,
+        i,
+        0,
+        1,
+        3,
+        59,
+        &I_CoolHi_25x27,
+        &I_CoolHi_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Cool_hi");
+    button_panel_add_item(
+        button_panel,
+        i,
+        1,
+        1,
+        36,
+        59,
+        &I_HeatHi_25x27,
+        &I_HeatHi_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Heat_hi");
+    button_panel_add_item(
+        button_panel,
+        i,
+        0,
+        2,
+        3,
+        91,
+        &I_CoolLo_25x27,
+        &I_CoolLo_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Cool_lo");
+    button_panel_add_item(
+        button_panel,
+        i,
+        1,
+        2,
+        36,
+        91,
+        &I_HeatLo_25x27,
+        &I_HeatLo_hvr_25x27,
+        infrared_scene_universal_common_item_callback,
+        context);
+    infrared_brute_force_add_record(brute_force, i++, "Heat_lo");
+
+    button_panel_add_label(button_panel, 6, 10, FontPrimary, "AC remote");
+
+    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_ac_on_event(void* context, SceneManagerEvent event) {
+    return infrared_scene_universal_common_on_event(context, event);
+}
+
+void infrared_scene_universal_ac_on_exit(void* context) {
+    infrared_scene_universal_common_on_exit(context);
+}

+ 0 - 1
applications/main/infrared/views/infrared_progress_view.c

@@ -4,7 +4,6 @@
 #include "gui/canvas.h"
 #include "gui/view.h"
 #include "input/input.h"
-#include "m-string.h"
 #include <gui/elements.h>
 #include <furi.h>
 #include "infrared_progress_view.h"

+ 17 - 17
applications/main/lfrfid/lfrfid.c

@@ -36,9 +36,9 @@ static LfRfid* lfrfid_alloc() {
     lfrfid->storage = furi_record_open(RECORD_STORAGE);
     lfrfid->dialogs = furi_record_open(RECORD_DIALOGS);
 
-    string_init(lfrfid->file_name);
-    string_init(lfrfid->raw_file_name);
-    string_init_set_str(lfrfid->file_path, LFRFID_APP_FOLDER);
+    lfrfid->file_name = furi_string_alloc();
+    lfrfid->raw_file_name = furi_string_alloc();
+    lfrfid->file_path = furi_string_alloc_set(LFRFID_APP_FOLDER);
 
     lfrfid->dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax);
 
@@ -104,9 +104,9 @@ static LfRfid* lfrfid_alloc() {
 static void lfrfid_free(LfRfid* lfrfid) {
     furi_assert(lfrfid);
 
-    string_clear(lfrfid->raw_file_name);
-    string_clear(lfrfid->file_name);
-    string_clear(lfrfid->file_path);
+    furi_string_free(lfrfid->raw_file_name);
+    furi_string_free(lfrfid->file_name);
+    furi_string_free(lfrfid->file_path);
     protocol_dict_free(lfrfid->dict);
 
     lfrfid_worker_free(lfrfid->lfworker);
@@ -183,7 +183,7 @@ int32_t lfrfid_app(void* p) {
                 app->view_dispatcher, app->gui, ViewDispatcherTypeDesktop);
             scene_manager_next_scene(app->scene_manager, LfRfidSceneRpc);
         } else {
-            string_set_str(app->file_path, args);
+            furi_string_set(app->file_path, args);
             lfrfid_load_key_data(app, app->file_path, true);
             view_dispatcher_attach_to_gui(
                 app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
@@ -210,13 +210,13 @@ bool lfrfid_save_key(LfRfid* app) {
 
     lfrfid_make_app_folder(app);
 
-    if(string_end_with_str_p(app->file_path, LFRFID_APP_EXTENSION)) {
-        size_t filename_start = string_search_rchar(app->file_path, '/');
-        string_left(app->file_path, filename_start);
+    if(furi_string_end_with(app->file_path, LFRFID_APP_EXTENSION)) {
+        size_t filename_start = furi_string_search_rchar(app->file_path, '/');
+        furi_string_left(app->file_path, filename_start);
     }
 
-    string_cat_printf(
-        app->file_path, "/%s%s", string_get_cstr(app->file_name), LFRFID_APP_EXTENSION);
+    furi_string_cat_printf(
+        app->file_path, "/%s%s", furi_string_get_cstr(app->file_name), LFRFID_APP_EXTENSION);
 
     result = lfrfid_save_key_data(app, app->file_path);
     return result;
@@ -242,14 +242,14 @@ bool lfrfid_load_key_from_file_select(LfRfid* app) {
 bool lfrfid_delete_key(LfRfid* app) {
     furi_assert(app);
 
-    return storage_simply_remove(app->storage, string_get_cstr(app->file_path));
+    return storage_simply_remove(app->storage, furi_string_get_cstr(app->file_path));
 }
 
-bool lfrfid_load_key_data(LfRfid* app, string_t path, bool show_dialog) {
+bool lfrfid_load_key_data(LfRfid* app, FuriString* path, bool show_dialog) {
     bool result = false;
 
     do {
-        app->protocol_id = lfrfid_dict_file_load(app->dict, string_get_cstr(path));
+        app->protocol_id = lfrfid_dict_file_load(app->dict, furi_string_get_cstr(path));
         if(app->protocol_id == PROTOCOL_NO) break;
 
         path_extract_filename(path, app->file_name, true);
@@ -263,8 +263,8 @@ bool lfrfid_load_key_data(LfRfid* app, string_t path, bool show_dialog) {
     return result;
 }
 
-bool lfrfid_save_key_data(LfRfid* app, string_t path) {
-    bool result = lfrfid_dict_file_save(app->dict, app->protocol_id, string_get_cstr(path));
+bool lfrfid_save_key_data(LfRfid* app, FuriString* path) {
+    bool result = lfrfid_dict_file_save(app->dict, app->protocol_id, furi_string_get_cstr(path));
 
     if(!result) {
         dialog_message_show_storage_error(app->dialogs, "Cannot save\nkey file");

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