Merge remote-tracking branch 'origin/dev' into release-candidate
							
								
								
									
										5
									
								
								.github/workflows/amap_analyse.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -11,6 +11,7 @@ on:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  TARGETS: f7
 | 
					  TARGETS: f7
 | 
				
			||||||
 | 
					  FBT_TOOLCHAIN_PATH: /opt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  amap_analyse:
 | 
					  amap_analyse:
 | 
				
			||||||
@ -39,7 +40,7 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
@ -78,7 +79,7 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - name: 'Upload report to DB'
 | 
					      - name: 'Upload report to DB'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          FBT_TOOLCHAIN_PATH=/opt source scripts/toolchain/fbtenv.sh
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
          get_size()
 | 
					          get_size()
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            SECTION="$1";
 | 
					            SECTION="$1";
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -12,6 +12,7 @@ on:
 | 
				
			|||||||
env:
 | 
					env:
 | 
				
			||||||
  TARGETS: f7
 | 
					  TARGETS: f7
 | 
				
			||||||
  DEFAULT_TARGET: f7
 | 
					  DEFAULT_TARGET: f7
 | 
				
			||||||
 | 
					  FBT_TOOLCHAIN_PATH: /runner/_work
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  main:
 | 
					  main:
 | 
				
			||||||
@ -24,7 +25,7 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
@ -35,6 +36,7 @@ jobs:
 | 
				
			|||||||
          mkdir artifacts
 | 
					          mkdir artifacts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Get commit details'
 | 
					      - name: 'Get commit details'
 | 
				
			||||||
 | 
					        id: names
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          if [[ ${{ github.event_name }} == 'pull_request' ]]; then
 | 
					          if [[ ${{ github.event_name }} == 'pull_request' ]]; then
 | 
				
			||||||
            TYPE="pull"
 | 
					            TYPE="pull"
 | 
				
			||||||
@ -45,14 +47,6 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
          python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 | 
					          python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Generate suffixes for comment'
 | 
					 | 
				
			||||||
        id: names
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          echo "::set-output name=branch_name::${BRANCH_NAME}"
 | 
					 | 
				
			||||||
          echo "::set-output name=commit_sha::${COMMIT_SHA}"
 | 
					 | 
				
			||||||
          echo "::set-output name=default_target::${DEFAULT_TARGET}"
 | 
					 | 
				
			||||||
          echo "::set-output name=suffix::${SUFFIX}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: 'Bundle scripts'
 | 
					      - name: 'Bundle scripts'
 | 
				
			||||||
        if: ${{ !github.event.pull_request.head.repo.fork }}
 | 
					        if: ${{ !github.event.pull_request.head.repo.fork }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
@ -62,7 +56,7 @@ jobs:
 | 
				
			|||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          set -e
 | 
					          set -e
 | 
				
			||||||
          for TARGET in ${TARGETS}; do
 | 
					          for TARGET in ${TARGETS}; do
 | 
				
			||||||
            FBT_TOOLCHAIN_PATH=/runner/_work ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
 | 
					                ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
 | 
				
			||||||
                copro_dist updater_package ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
 | 
					                copro_dist updater_package ${{ startsWith(github.ref, 'refs/tags') && 'DEBUG=0 COMPACT=1' || '' }}
 | 
				
			||||||
          done
 | 
					          done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -143,7 +137,7 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          submodules: true
 | 
					          submodules: true
 | 
				
			||||||
@ -164,6 +158,6 @@ jobs:
 | 
				
			|||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          set -e
 | 
					          set -e
 | 
				
			||||||
          for TARGET in ${TARGETS}; do
 | 
					          for TARGET in ${TARGETS}; do
 | 
				
			||||||
            FBT_TOOLCHAIN_PATH=/runner/_work ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
 | 
					                ./fbt TARGET_HW="$(echo "${TARGET}" | sed 's/f//')" \
 | 
				
			||||||
                updater_package DEBUG=0 COMPACT=1
 | 
					                updater_package DEBUG=0 COMPACT=1
 | 
				
			||||||
          done
 | 
					          done
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/check_submodules.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -20,7 +20,7 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
@ -36,12 +36,12 @@ jobs:
 | 
				
			|||||||
          BRANCHES=$(git branch -r --contains "$SUBMODULE_HASH");
 | 
					          BRANCHES=$(git branch -r --contains "$SUBMODULE_HASH");
 | 
				
			||||||
          COMMITS_IN_BRANCH="$(git rev-list --count dev)";
 | 
					          COMMITS_IN_BRANCH="$(git rev-list --count dev)";
 | 
				
			||||||
          if [ $COMMITS_IN_BRANCH -lt $SUB_COMMITS_MIN ]; then
 | 
					          if [ $COMMITS_IN_BRANCH -lt $SUB_COMMITS_MIN ]; then
 | 
				
			||||||
            echo "::set-output name=fails::error";
 | 
					            echo "name=fails::error" >> $GITHUB_OUTPUT
 | 
				
			||||||
            echo "::error::Error: Too low commits in $SUB_BRANCH of submodule $SUB_PATH: $COMMITS_IN_BRANCH(expected $SUB_COMMITS_MIN+)";
 | 
					            echo "::error::Error: Too low commits in $SUB_BRANCH of submodule $SUB_PATH: $COMMITS_IN_BRANCH(expected $SUB_COMMITS_MIN+)";
 | 
				
			||||||
            exit 1;
 | 
					            exit 1;
 | 
				
			||||||
          fi
 | 
					          fi
 | 
				
			||||||
          if ! grep -q "/$SUB_BRANCH" <<< "$BRANCHES"; then
 | 
					          if ! grep -q "/$SUB_BRANCH" <<< "$BRANCHES"; then
 | 
				
			||||||
            echo "::set-output name=fails::error";
 | 
					            echo "name=fails::error" >> $GITHUB_OUTPUT
 | 
				
			||||||
            echo "::error::Error: Submodule $SUB_PATH is not on branch $SUB_BRANCH";
 | 
					            echo "::error::Error: Submodule $SUB_PATH is not on branch $SUB_BRANCH";
 | 
				
			||||||
            exit 1;
 | 
					            exit 1;
 | 
				
			||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/lint_c.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -11,6 +11,8 @@ on:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  TARGETS: f7
 | 
					  TARGETS: f7
 | 
				
			||||||
 | 
					  FBT_TOOLCHAIN_PATH: /runner/_work
 | 
				
			||||||
 | 
					  SET_GH_OUTPUT: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint_c_cpp:
 | 
					  lint_c_cpp:
 | 
				
			||||||
@ -23,14 +25,14 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Check code formatting'
 | 
					      - name: 'Check code formatting'
 | 
				
			||||||
        id: syntax_check
 | 
					        id: syntax_check
 | 
				
			||||||
        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/runner/_work ./fbt lint
 | 
					        run: ./fbt lint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Report code formatting errors
 | 
					      - name: Report code formatting errors
 | 
				
			||||||
        if: failure() && steps.syntax_check.outputs.errors && github.event.pull_request
 | 
					        if: failure() && steps.syntax_check.outputs.errors && github.event.pull_request
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/workflows/lint_python.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -9,6 +9,10 @@ on:
 | 
				
			|||||||
      - '*'
 | 
					      - '*'
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					    FBT_TOOLCHAIN_PATH: /runner/_work
 | 
				
			||||||
 | 
					    SET_GH_OUTPUT: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  lint_python:
 | 
					  lint_python:
 | 
				
			||||||
    runs-on: [self-hosted,FlipperZeroShell]
 | 
					    runs-on: [self-hosted,FlipperZeroShell]
 | 
				
			||||||
@ -20,10 +24,10 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Check code formatting'
 | 
					      - name: 'Check code formatting'
 | 
				
			||||||
        run: SET_GH_OUTPUT=1 FBT_TOOLCHAIN_PATH=/runner/_work ./fbt lint_py
 | 
					        run: ./fbt lint_py
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										45
									
								
								.github/workflows/merge_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					name: 'Check FL ticket in PR name'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  FBT_TOOLCHAIN_PATH: /runner/_work
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  merge_report:
 | 
				
			||||||
 | 
					    runs-on: [self-hosted,FlipperZeroShell]
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: 'Decontaminate previous build leftovers'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          if [ -d .git ]; then
 | 
				
			||||||
 | 
					            git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)"
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          fetch-depth: 0
 | 
				
			||||||
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Get commit details'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          if [[ ${{ github.event_name }} == 'pull_request' ]]; then
 | 
				
			||||||
 | 
					            TYPE="pull"
 | 
				
			||||||
 | 
					          elif [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
 | 
				
			||||||
 | 
					            TYPE="tag"
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            TYPE="other"
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					          python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Check ticket and report'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
 | 
					          python3 -m pip install slack_sdk
 | 
				
			||||||
 | 
					          python3 scripts/merge_report_qa.py \
 | 
				
			||||||
 | 
					              ${{ secrets.QA_REPORT_SLACK_TOKEN }} \
 | 
				
			||||||
 | 
					              ${{ secrets.QA_REPORT_SLACK_CHANNEL }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								.github/workflows/pvs_studio.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -12,6 +12,7 @@ on:
 | 
				
			|||||||
env:
 | 
					env:
 | 
				
			||||||
  TARGETS: f7
 | 
					  TARGETS: f7
 | 
				
			||||||
  DEFAULT_TARGET: f7
 | 
					  DEFAULT_TARGET: f7
 | 
				
			||||||
 | 
					  FBT_TOOLCHAIN_PATH: /runner/_work
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  analyse_c_cpp:
 | 
					  analyse_c_cpp:
 | 
				
			||||||
@ -25,12 +26,13 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Checkout code'
 | 
					      - name: 'Checkout code'
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Get commit details'
 | 
					      - name: 'Get commit details'
 | 
				
			||||||
 | 
					        id: names
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          if [[ ${{ github.event_name }} == 'pull_request' ]]; then
 | 
					          if [[ ${{ github.event_name }} == 'pull_request' ]]; then
 | 
				
			||||||
            TYPE="pull"
 | 
					            TYPE="pull"
 | 
				
			||||||
@ -41,15 +43,6 @@ jobs:
 | 
				
			|||||||
          fi
 | 
					          fi
 | 
				
			||||||
          python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 | 
					          python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Generate suffixes for comment'
 | 
					 | 
				
			||||||
        if: ${{ !github.event.pull_request.head.repo.fork && github.event.pull_request }}
 | 
					 | 
				
			||||||
        id: names
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          echo "::set-output name=branch_name::${BRANCH_NAME}"
 | 
					 | 
				
			||||||
          echo "::set-output name=commit_sha::${COMMIT_SHA}"
 | 
					 | 
				
			||||||
          echo "::set-output name=default_target::${DEFAULT_TARGET}"
 | 
					 | 
				
			||||||
          echo "::set-output name=suffix::${SUFFIX}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: 'Make reports directory'
 | 
					      - name: 'Make reports directory'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          rm -rf reports/
 | 
					          rm -rf reports/
 | 
				
			||||||
@ -57,11 +50,11 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - name: 'Generate compile_comands.json'
 | 
					      - name: 'Generate compile_comands.json'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          FBT_TOOLCHAIN_PATH=/runner/_work ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking _fap_icons
 | 
					          ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking _fap_icons
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Static code analysis'
 | 
					      - name: 'Static code analysis'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          FBT_TOOLCHAIN_PATH=/runner/_work source scripts/toolchain/fbtenv.sh
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
          pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
 | 
					          pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
 | 
				
			||||||
          pvs-studio-analyzer analyze \
 | 
					          pvs-studio-analyzer analyze \
 | 
				
			||||||
              @.pvsoptions \
 | 
					              @.pvsoptions \
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										87
									
								
								.github/workflows/unit_tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -6,13 +6,20 @@ on:
 | 
				
			|||||||
env:
 | 
					env:
 | 
				
			||||||
  TARGETS: f7
 | 
					  TARGETS: f7
 | 
				
			||||||
  DEFAULT_TARGET: f7
 | 
					  DEFAULT_TARGET: f7
 | 
				
			||||||
 | 
					  FBT_TOOLCHAIN_PATH: /opt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  run_units_on_test_bench:
 | 
					  run_units_on_test_bench:
 | 
				
			||||||
    runs-on: [self-hosted, FlipperZeroTest]
 | 
					    runs-on: [self-hosted, FlipperZeroTest]
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: 'Decontaminate previous build leftovers'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          if [ -d .git ]; then
 | 
				
			||||||
 | 
					            git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)"
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Checkout code
 | 
					      - name: Checkout code
 | 
				
			||||||
        uses: actions/checkout@v2
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
@ -22,35 +29,81 @@ jobs:
 | 
				
			|||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT
 | 
					          echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Flashing target firmware'
 | 
				
			||||||
 | 
					        id: first_full_flash
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
 | 
				
			||||||
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
 | 
					          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Validating updater'
 | 
				
			||||||
 | 
					        id: second_full_flash
 | 
				
			||||||
 | 
					        if: success()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          ./fbt flash_usb_full PORT=${{steps.device.outputs.flipper}} FORCE=1
 | 
				
			||||||
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
 | 
					          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Flash unit tests firmware'
 | 
					      - name: 'Flash unit tests firmware'
 | 
				
			||||||
        id: flashing
 | 
					        id: flashing
 | 
				
			||||||
 | 
					        if: success()
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
 | 
					          ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Wait for flipper to finish updating'
 | 
					      - name: 'Wait for flipper to finish updating'
 | 
				
			||||||
        id: connect
 | 
					        id: connect
 | 
				
			||||||
        if: steps.flashing.outcome == 'success'
 | 
					        if: steps.flashing.outcome == 'success'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          . scripts/toolchain/fbtenv.sh
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
          ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 | 
					          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: 'Format flipper SD card'
 | 
					 | 
				
			||||||
        id: format
 | 
					 | 
				
			||||||
        if: steps.connect.outcome == 'success'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          . scripts/toolchain/fbtenv.sh
 | 
					 | 
				
			||||||
          ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Copy assets and unit tests data to flipper'
 | 
					      - name: 'Copy assets and unit tests data to flipper'
 | 
				
			||||||
        id: copy
 | 
					        id: copy
 | 
				
			||||||
        if: steps.format.outcome == 'success'
 | 
					        if: steps.connect.outcome == 'success'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          . scripts/toolchain/fbtenv.sh
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
          ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/resources /ext
 | 
					          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} -f send assets/unit_tests /ext/unit_tests
 | 
				
			||||||
          ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Run units and validate results'
 | 
					      - name: 'Run units and validate results'
 | 
				
			||||||
        if: steps.copy.outcome == 'success'
 | 
					        if: steps.copy.outcome == 'success'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          . scripts/toolchain/fbtenv.sh
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
          ./scripts/testing/units.py ${{steps.device.outputs.flipper}}
 | 
					          python3 scripts/testing/units.py ${{steps.device.outputs.flipper}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Get last release tag'
 | 
				
			||||||
 | 
					        id: release_tag
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          echo "tag=$(git tag -l --sort=-version:refname | grep -v "rc\|RC" | head -1)" >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Decontaminate previous build leftovers'
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          if [ -d .git ]; then
 | 
				
			||||||
 | 
					            git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)"
 | 
				
			||||||
 | 
					          fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Checkout latest release'
 | 
				
			||||||
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          fetch-depth: 0
 | 
				
			||||||
 | 
					          ref: ${{ steps.release_tag.outputs.tag }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Flash last release'
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Wait for flipper to finish updating'
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
 | 
					          python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: 'Format flipper SD card'
 | 
				
			||||||
 | 
					        id: format
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          source scripts/toolchain/fbtenv.sh
 | 
				
			||||||
 | 
					          python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										32
									
								
								.vscode/example/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -128,6 +128,38 @@
 | 
				
			|||||||
            "group": "build",
 | 
					            "group": "build",
 | 
				
			||||||
            "type": "shell",
 | 
					            "type": "shell",
 | 
				
			||||||
            "command": "./fbt COMPACT=1 DEBUG=0 launch_app APPSRC=${relativeFileDirname}"
 | 
					            "command": "./fbt COMPACT=1 DEBUG=0 launch_app APPSRC=${relativeFileDirname}"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "label": "[Debug] Launch App on Flipper with Serial Console",
 | 
				
			||||||
 | 
					            "dependsOrder": "sequence",
 | 
				
			||||||
 | 
					            "group": "build",
 | 
				
			||||||
 | 
					            "dependsOn": [
 | 
				
			||||||
 | 
					                "[Debug] Launch App on Flipper",
 | 
				
			||||||
 | 
					                "Serial Console"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Press Ctrl+] to quit
 | 
				
			||||||
 | 
					            "label": "Serial Console",
 | 
				
			||||||
 | 
					            "type": "shell",
 | 
				
			||||||
 | 
					            "command": "./fbt cli",
 | 
				
			||||||
 | 
					            "group": "none",
 | 
				
			||||||
 | 
					            "isBackground": true,
 | 
				
			||||||
 | 
								"options": {
 | 
				
			||||||
 | 
					                "env": {
 | 
				
			||||||
 | 
					                    "FBT_NO_SYNC": "0"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "presentation": {
 | 
				
			||||||
 | 
					                "reveal": "always",
 | 
				
			||||||
 | 
					                "revealProblems": "never",
 | 
				
			||||||
 | 
					                "showReuseMessage": false,
 | 
				
			||||||
 | 
					                "panel": "dedicated",
 | 
				
			||||||
 | 
					                "focus": true,
 | 
				
			||||||
 | 
					                "echo": true,
 | 
				
			||||||
 | 
					                "close": true,
 | 
				
			||||||
 | 
					                "group": "Logger"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								applications/debug/locale_test/application.fam
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					App(
 | 
				
			||||||
 | 
					    appid="locale_test",
 | 
				
			||||||
 | 
					    name="Locale Test",
 | 
				
			||||||
 | 
					    apptype=FlipperAppType.DEBUG,
 | 
				
			||||||
 | 
					    entry_point="locale_test_app",
 | 
				
			||||||
 | 
					    cdefines=["APP_LOCALE"],
 | 
				
			||||||
 | 
					    requires=["gui", "locale"],
 | 
				
			||||||
 | 
					    stack_size=2 * 1024,
 | 
				
			||||||
 | 
					    order=70,
 | 
				
			||||||
 | 
					    fap_category="Debug",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										102
									
								
								applications/debug/locale_test/locale_test.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					#include <furi.h>
 | 
				
			||||||
 | 
					#include <gui/gui.h>
 | 
				
			||||||
 | 
					#include <gui/elements.h>
 | 
				
			||||||
 | 
					#include <gui/view_dispatcher.h>
 | 
				
			||||||
 | 
					#include <gui/modules/dialog_ex.h>
 | 
				
			||||||
 | 
					#include <locale/locale.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    Gui* gui;
 | 
				
			||||||
 | 
					    ViewDispatcher* view_dispatcher;
 | 
				
			||||||
 | 
					    View* view;
 | 
				
			||||||
 | 
					} LocaleTestApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void locale_test_view_draw_callback(Canvas* canvas, void* _model) {
 | 
				
			||||||
 | 
					    UNUSED(_model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare canvas
 | 
				
			||||||
 | 
					    canvas_set_color(canvas, ColorBlack);
 | 
				
			||||||
 | 
					    canvas_set_font(canvas, FontSecondary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FuriString* tmp_string = furi_string_alloc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    float temp = 25.3f;
 | 
				
			||||||
 | 
					    LocaleMeasurementUnits units = locale_get_measurement_unit();
 | 
				
			||||||
 | 
					    if(units == LocaleMeasurementUnitsMetric) {
 | 
				
			||||||
 | 
					        furi_string_printf(tmp_string, "Temp: %5.1fC", (double)temp);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        temp = locale_celsius_to_fahrenheit(temp);
 | 
				
			||||||
 | 
					        furi_string_printf(tmp_string, "Temp: %5.1fF", (double)temp);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    canvas_draw_str(canvas, 0, 10, furi_string_get_cstr(tmp_string));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FuriHalRtcDateTime datetime;
 | 
				
			||||||
 | 
					    furi_hal_rtc_get_datetime(&datetime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locale_format_time(tmp_string, &datetime, locale_get_time_format(), false);
 | 
				
			||||||
 | 
					    canvas_draw_str(canvas, 0, 25, furi_string_get_cstr(tmp_string));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locale_format_date(tmp_string, &datetime, locale_get_date_format(), "/");
 | 
				
			||||||
 | 
					    canvas_draw_str(canvas, 0, 40, furi_string_get_cstr(tmp_string));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_string_free(tmp_string);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool locale_test_view_input_callback(InputEvent* event, void* context) {
 | 
				
			||||||
 | 
					    UNUSED(event);
 | 
				
			||||||
 | 
					    UNUSED(context);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint32_t locale_test_exit(void* context) {
 | 
				
			||||||
 | 
					    UNUSED(context);
 | 
				
			||||||
 | 
					    return VIEW_NONE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static LocaleTestApp* locale_test_alloc() {
 | 
				
			||||||
 | 
					    LocaleTestApp* app = malloc(sizeof(LocaleTestApp));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Gui
 | 
				
			||||||
 | 
					    app->gui = furi_record_open(RECORD_GUI);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // View dispatcher
 | 
				
			||||||
 | 
					    app->view_dispatcher = view_dispatcher_alloc();
 | 
				
			||||||
 | 
					    view_dispatcher_enable_queue(app->view_dispatcher);
 | 
				
			||||||
 | 
					    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Views
 | 
				
			||||||
 | 
					    app->view = view_alloc();
 | 
				
			||||||
 | 
					    view_set_draw_callback(app->view, locale_test_view_draw_callback);
 | 
				
			||||||
 | 
					    view_set_input_callback(app->view, locale_test_view_input_callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    view_set_previous_callback(app->view, locale_test_exit);
 | 
				
			||||||
 | 
					    view_dispatcher_add_view(app->view_dispatcher, 0, app->view);
 | 
				
			||||||
 | 
					    view_dispatcher_switch_to_view(app->view_dispatcher, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return app;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void locale_test_free(LocaleTestApp* app) {
 | 
				
			||||||
 | 
					    furi_assert(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Free views
 | 
				
			||||||
 | 
					    view_dispatcher_remove_view(app->view_dispatcher, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    view_free(app->view);
 | 
				
			||||||
 | 
					    view_dispatcher_free(app->view_dispatcher);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Close gui record
 | 
				
			||||||
 | 
					    furi_record_close(RECORD_GUI);
 | 
				
			||||||
 | 
					    app->gui = NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Free rest
 | 
				
			||||||
 | 
					    free(app);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int32_t locale_test_app(void* p) {
 | 
				
			||||||
 | 
					    UNUSED(p);
 | 
				
			||||||
 | 
					    LocaleTestApp* app = locale_test_alloc();
 | 
				
			||||||
 | 
					    view_dispatcher_run(app->view_dispatcher);
 | 
				
			||||||
 | 
					    locale_test_free(app);
 | 
				
			||||||
 | 
					    return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										110
									
								
								applications/debug/unit_tests/bt/bt_test.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					#include <furi.h>
 | 
				
			||||||
 | 
					#include <furi_hal.h>
 | 
				
			||||||
 | 
					#include "../minunit.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <bt/bt_service/bt_keys_storage.h>
 | 
				
			||||||
 | 
					#include <storage/storage.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define BT_TEST_KEY_STORAGE_FILE_PATH EXT_PATH("unit_tests/bt_test.keys")
 | 
				
			||||||
 | 
					#define BT_TEST_NVM_RAM_BUFF_SIZE (507 * 4) // The same as in ble NVM storage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    Storage* storage;
 | 
				
			||||||
 | 
					    BtKeysStorage* bt_keys_storage;
 | 
				
			||||||
 | 
					    uint8_t* nvm_ram_buff_dut;
 | 
				
			||||||
 | 
					    uint8_t* nvm_ram_buff_ref;
 | 
				
			||||||
 | 
					} BtTest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BtTest* bt_test = NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_test_alloc() {
 | 
				
			||||||
 | 
					    bt_test = malloc(sizeof(BtTest));
 | 
				
			||||||
 | 
					    bt_test->storage = furi_record_open(RECORD_STORAGE);
 | 
				
			||||||
 | 
					    bt_test->nvm_ram_buff_dut = malloc(BT_TEST_NVM_RAM_BUFF_SIZE);
 | 
				
			||||||
 | 
					    bt_test->nvm_ram_buff_ref = malloc(BT_TEST_NVM_RAM_BUFF_SIZE);
 | 
				
			||||||
 | 
					    bt_test->bt_keys_storage = bt_keys_storage_alloc(BT_TEST_KEY_STORAGE_FILE_PATH);
 | 
				
			||||||
 | 
					    bt_keys_storage_set_ram_params(
 | 
				
			||||||
 | 
					        bt_test->bt_keys_storage, bt_test->nvm_ram_buff_dut, BT_TEST_NVM_RAM_BUFF_SIZE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_test_free() {
 | 
				
			||||||
 | 
					    furi_assert(bt_test);
 | 
				
			||||||
 | 
					    free(bt_test->nvm_ram_buff_ref);
 | 
				
			||||||
 | 
					    free(bt_test->nvm_ram_buff_dut);
 | 
				
			||||||
 | 
					    bt_keys_storage_free(bt_test->bt_keys_storage);
 | 
				
			||||||
 | 
					    furi_record_close(RECORD_STORAGE);
 | 
				
			||||||
 | 
					    free(bt_test);
 | 
				
			||||||
 | 
					    bt_test = NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void bt_test_keys_storage_profile() {
 | 
				
			||||||
 | 
					    // Emulate nvm change on initial connection
 | 
				
			||||||
 | 
					    const int nvm_change_size_on_connection = 88;
 | 
				
			||||||
 | 
					    for(size_t i = 0; i < nvm_change_size_on_connection; i++) {
 | 
				
			||||||
 | 
					        bt_test->nvm_ram_buff_dut[i] = rand();
 | 
				
			||||||
 | 
					        bt_test->nvm_ram_buff_ref[i] = bt_test->nvm_ram_buff_dut[i];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Emulate update storage on initial connect
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        bt_keys_storage_update(
 | 
				
			||||||
 | 
					            bt_test->bt_keys_storage, bt_test->nvm_ram_buff_dut, nvm_change_size_on_connection),
 | 
				
			||||||
 | 
					        "Failed to update key storage on initial connect");
 | 
				
			||||||
 | 
					    memset(bt_test->nvm_ram_buff_dut, 0, BT_TEST_NVM_RAM_BUFF_SIZE);
 | 
				
			||||||
 | 
					    mu_assert(bt_keys_storage_load(bt_test->bt_keys_storage), "Failed to load NVM");
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        memcmp(
 | 
				
			||||||
 | 
					            bt_test->nvm_ram_buff_ref, bt_test->nvm_ram_buff_dut, nvm_change_size_on_connection) ==
 | 
				
			||||||
 | 
					            0,
 | 
				
			||||||
 | 
					        "Wrong buffer loaded");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const int nvm_disconnect_update_offset = 84;
 | 
				
			||||||
 | 
					    const int nvm_disconnect_update_size = 324;
 | 
				
			||||||
 | 
					    const int nvm_total_size = nvm_change_size_on_connection -
 | 
				
			||||||
 | 
					                               (nvm_change_size_on_connection - nvm_disconnect_update_offset) +
 | 
				
			||||||
 | 
					                               nvm_disconnect_update_size;
 | 
				
			||||||
 | 
					    // Emulate update storage on initial disconnect
 | 
				
			||||||
 | 
					    for(size_t i = nvm_disconnect_update_offset;
 | 
				
			||||||
 | 
					        i < nvm_disconnect_update_offset + nvm_disconnect_update_size;
 | 
				
			||||||
 | 
					        i++) {
 | 
				
			||||||
 | 
					        bt_test->nvm_ram_buff_dut[i] = rand();
 | 
				
			||||||
 | 
					        bt_test->nvm_ram_buff_ref[i] = bt_test->nvm_ram_buff_dut[i];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        bt_keys_storage_update(
 | 
				
			||||||
 | 
					            bt_test->bt_keys_storage,
 | 
				
			||||||
 | 
					            &bt_test->nvm_ram_buff_dut[nvm_disconnect_update_offset],
 | 
				
			||||||
 | 
					            nvm_disconnect_update_size),
 | 
				
			||||||
 | 
					        "Failed to update key storage on initial disconnect");
 | 
				
			||||||
 | 
					    memset(bt_test->nvm_ram_buff_dut, 0, BT_TEST_NVM_RAM_BUFF_SIZE);
 | 
				
			||||||
 | 
					    mu_assert(bt_keys_storage_load(bt_test->bt_keys_storage), "Failed to load NVM");
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        memcmp(bt_test->nvm_ram_buff_ref, bt_test->nvm_ram_buff_dut, nvm_total_size) == 0,
 | 
				
			||||||
 | 
					        "Wrong buffer loaded");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void bt_test_keys_remove_test_file() {
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        storage_simply_remove(bt_test->storage, BT_TEST_KEY_STORAGE_FILE_PATH),
 | 
				
			||||||
 | 
					        "Can't remove test file");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MU_TEST(bt_test_keys_storage_serial_profile) {
 | 
				
			||||||
 | 
					    furi_assert(bt_test);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_test_keys_remove_test_file();
 | 
				
			||||||
 | 
					    bt_test_keys_storage_profile();
 | 
				
			||||||
 | 
					    bt_test_keys_remove_test_file();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MU_TEST_SUITE(test_bt) {
 | 
				
			||||||
 | 
					    bt_test_alloc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MU_RUN_TEST(bt_test_keys_storage_serial_profile);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_test_free();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int run_minunit_test_bt() {
 | 
				
			||||||
 | 
					    MU_RUN_SUITE(test_bt);
 | 
				
			||||||
 | 
					    return MU_EXIT_CODE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,7 +6,7 @@
 | 
				
			|||||||
#include <lib/nfc/helpers/mf_classic_dict.h>
 | 
					#include <lib/nfc/helpers/mf_classic_dict.h>
 | 
				
			||||||
#include <lib/digital_signal/digital_signal.h>
 | 
					#include <lib/digital_signal/digital_signal.h>
 | 
				
			||||||
#include <lib/nfc/nfc_device.h>
 | 
					#include <lib/nfc/nfc_device.h>
 | 
				
			||||||
#include <applications/main/nfc/helpers/nfc_generators.h>
 | 
					#include <lib/nfc/helpers/nfc_generators.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <lib/flipper_format/flipper_format_i.h>
 | 
					#include <lib/flipper_format/flipper_format_i.h>
 | 
				
			||||||
#include <lib/toolbox/stream/file_stream.h>
 | 
					#include <lib/toolbox/stream/file_stream.h>
 | 
				
			||||||
@ -102,7 +102,10 @@ static bool nfc_test_digital_signal_test_encode(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    do {
 | 
					    do {
 | 
				
			||||||
        // Read test data
 | 
					        // Read test data
 | 
				
			||||||
        if(!nfc_test_read_signal_from_file(file_name)) break;
 | 
					        if(!nfc_test_read_signal_from_file(file_name)) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Failed to read signal from file");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Encode signal
 | 
					        // Encode signal
 | 
				
			||||||
        FURI_CRITICAL_ENTER();
 | 
					        FURI_CRITICAL_ENTER();
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@
 | 
				
			|||||||
#define CAME_ATOMO_DIR_NAME EXT_PATH("subghz/assets/came_atomo")
 | 
					#define CAME_ATOMO_DIR_NAME EXT_PATH("subghz/assets/came_atomo")
 | 
				
			||||||
#define NICE_FLOR_S_DIR_NAME EXT_PATH("subghz/assets/nice_flor_s")
 | 
					#define NICE_FLOR_S_DIR_NAME EXT_PATH("subghz/assets/nice_flor_s")
 | 
				
			||||||
#define TEST_RANDOM_DIR_NAME EXT_PATH("unit_tests/subghz/test_random_raw.sub")
 | 
					#define TEST_RANDOM_DIR_NAME EXT_PATH("unit_tests/subghz/test_random_raw.sub")
 | 
				
			||||||
#define TEST_RANDOM_COUNT_PARSE 244
 | 
					#define TEST_RANDOM_COUNT_PARSE 253
 | 
				
			||||||
#define TEST_TIMEOUT 10000
 | 
					#define TEST_TIMEOUT 10000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static SubGhzEnvironment* environment_handler;
 | 
					static SubGhzEnvironment* environment_handler;
 | 
				
			||||||
@ -587,6 +587,13 @@ MU_TEST(subghz_decoder_ansonic_test) {
 | 
				
			|||||||
        "Test decoder " SUBGHZ_PROTOCOL_ANSONIC_NAME " error\r\n");
 | 
					        "Test decoder " SUBGHZ_PROTOCOL_ANSONIC_NAME " error\r\n");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MU_TEST(subghz_decoder_smc5326_test) {
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        subghz_decoder_test(
 | 
				
			||||||
 | 
					            EXT_PATH("unit_tests/subghz/smc5326_raw.sub"), SUBGHZ_PROTOCOL_SMC5326_NAME),
 | 
				
			||||||
 | 
					        "Test decoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//test encoders
 | 
					//test encoders
 | 
				
			||||||
MU_TEST(subghz_encoder_princeton_test) {
 | 
					MU_TEST(subghz_encoder_princeton_test) {
 | 
				
			||||||
    mu_assert(
 | 
					    mu_assert(
 | 
				
			||||||
@ -714,6 +721,12 @@ MU_TEST(subghz_encoder_ansonic_test) {
 | 
				
			|||||||
        "Test encoder " SUBGHZ_PROTOCOL_ANSONIC_NAME " error\r\n");
 | 
					        "Test encoder " SUBGHZ_PROTOCOL_ANSONIC_NAME " error\r\n");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MU_TEST(subghz_encoder_smc5326_test) {
 | 
				
			||||||
 | 
					    mu_assert(
 | 
				
			||||||
 | 
					        subghz_encoder_test(EXT_PATH("unit_tests/subghz/smc5326.sub")),
 | 
				
			||||||
 | 
					        "Test encoder " SUBGHZ_PROTOCOL_SMC5326_NAME " error\r\n");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MU_TEST(subghz_random_test) {
 | 
					MU_TEST(subghz_random_test) {
 | 
				
			||||||
    mu_assert(subghz_decode_random_test(TEST_RANDOM_DIR_NAME), "Random test error\r\n");
 | 
					    mu_assert(subghz_decode_random_test(TEST_RANDOM_DIR_NAME), "Random test error\r\n");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -757,6 +770,7 @@ MU_TEST_SUITE(subghz) {
 | 
				
			|||||||
    MU_RUN_TEST(subghz_decoder_intertechno_v3_test);
 | 
					    MU_RUN_TEST(subghz_decoder_intertechno_v3_test);
 | 
				
			||||||
    MU_RUN_TEST(subghz_decoder_clemsa_test);
 | 
					    MU_RUN_TEST(subghz_decoder_clemsa_test);
 | 
				
			||||||
    MU_RUN_TEST(subghz_decoder_ansonic_test);
 | 
					    MU_RUN_TEST(subghz_decoder_ansonic_test);
 | 
				
			||||||
 | 
					    MU_RUN_TEST(subghz_decoder_smc5326_test);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MU_RUN_TEST(subghz_encoder_princeton_test);
 | 
					    MU_RUN_TEST(subghz_encoder_princeton_test);
 | 
				
			||||||
    MU_RUN_TEST(subghz_encoder_came_test);
 | 
					    MU_RUN_TEST(subghz_encoder_came_test);
 | 
				
			||||||
@ -779,6 +793,7 @@ MU_TEST_SUITE(subghz) {
 | 
				
			|||||||
    MU_RUN_TEST(subghz_encoder_intertechno_v3_test);
 | 
					    MU_RUN_TEST(subghz_encoder_intertechno_v3_test);
 | 
				
			||||||
    MU_RUN_TEST(subghz_encoder_clemsa_test);
 | 
					    MU_RUN_TEST(subghz_encoder_clemsa_test);
 | 
				
			||||||
    MU_RUN_TEST(subghz_encoder_ansonic_test);
 | 
					    MU_RUN_TEST(subghz_encoder_ansonic_test);
 | 
				
			||||||
 | 
					    MU_RUN_TEST(subghz_encoder_smc5326_test);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MU_RUN_TEST(subghz_random_test);
 | 
					    MU_RUN_TEST(subghz_random_test);
 | 
				
			||||||
    subghz_test_deinit();
 | 
					    subghz_test_deinit();
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,7 @@ int run_minunit_test_protocol_dict();
 | 
				
			|||||||
int run_minunit_test_lfrfid_protocols();
 | 
					int run_minunit_test_lfrfid_protocols();
 | 
				
			||||||
int run_minunit_test_nfc();
 | 
					int run_minunit_test_nfc();
 | 
				
			||||||
int run_minunit_test_bit_lib();
 | 
					int run_minunit_test_bit_lib();
 | 
				
			||||||
 | 
					int run_minunit_test_bt();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef int (*UnitTestEntry)();
 | 
					typedef int (*UnitTestEntry)();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -49,6 +50,7 @@ const UnitTest unit_tests[] = {
 | 
				
			|||||||
    {.name = "protocol_dict", .entry = run_minunit_test_protocol_dict},
 | 
					    {.name = "protocol_dict", .entry = run_minunit_test_protocol_dict},
 | 
				
			||||||
    {.name = "lfrfid", .entry = run_minunit_test_lfrfid_protocols},
 | 
					    {.name = "lfrfid", .entry = run_minunit_test_lfrfid_protocols},
 | 
				
			||||||
    {.name = "bit_lib", .entry = run_minunit_test_bit_lib},
 | 
					    {.name = "bit_lib", .entry = run_minunit_test_bit_lib},
 | 
				
			||||||
 | 
					    {.name = "bt", .entry = run_minunit_test_bt},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void minunit_print_progress() {
 | 
					void minunit_print_progress() {
 | 
				
			||||||
 | 
				
			|||||||
@ -143,6 +143,8 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) {
 | 
				
			|||||||
            break;
 | 
					            break;
 | 
				
			||||||
        case ArchiveBrowserEventFileMenuDelete:
 | 
					        case ArchiveBrowserEventFileMenuDelete:
 | 
				
			||||||
            if(archive_get_tab(browser) != ArchiveTabFavorites) {
 | 
					            if(archive_get_tab(browser) != ArchiveTabFavorites) {
 | 
				
			||||||
 | 
					                scene_manager_set_scene_state(
 | 
				
			||||||
 | 
					                    archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH);
 | 
				
			||||||
                scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneDelete);
 | 
					                scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneDelete);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            consumed = true;
 | 
					            consumed = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,9 @@
 | 
				
			|||||||
#include "archive_browser_view.h"
 | 
					#include "archive_browser_view.h"
 | 
				
			||||||
#include "../helpers/archive_browser.h"
 | 
					#include "../helpers/archive_browser.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define SCROLL_INTERVAL (333)
 | 
				
			||||||
 | 
					#define SCROLL_DELAY (2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static const char* ArchiveTabNames[] = {
 | 
					static const char* ArchiveTabNames[] = {
 | 
				
			||||||
    [ArchiveTabFavorites] = "Favorites",
 | 
					    [ArchiveTabFavorites] = "Favorites",
 | 
				
			||||||
    [ArchiveTabIButton] = "iButton",
 | 
					    [ArchiveTabIButton] = "iButton",
 | 
				
			||||||
@ -146,13 +149,18 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
 | 
				
			|||||||
            furi_string_set(str_buf, "---");
 | 
					            furi_string_set(str_buf, "---");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        elements_string_fit_width(
 | 
					        size_t scroll_counter = model->scroll_counter;
 | 
				
			||||||
            canvas, str_buf, (scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(model->item_idx == idx) {
 | 
					        if(model->item_idx == idx) {
 | 
				
			||||||
            archive_draw_frame(canvas, i, scrollbar, model->move_fav);
 | 
					            archive_draw_frame(canvas, i, scrollbar, model->move_fav);
 | 
				
			||||||
 | 
					            if(scroll_counter < SCROLL_DELAY) {
 | 
				
			||||||
 | 
					                scroll_counter = 0;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                scroll_counter -= SCROLL_DELAY;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            canvas_set_color(canvas, ColorBlack);
 | 
					            canvas_set_color(canvas, ColorBlack);
 | 
				
			||||||
 | 
					            scroll_counter = 0;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(custom_icon_data) {
 | 
					        if(custom_icon_data) {
 | 
				
			||||||
@ -162,8 +170,15 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
 | 
				
			|||||||
            canvas_draw_icon(
 | 
					            canvas_draw_icon(
 | 
				
			||||||
                canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]);
 | 
					                canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        canvas_draw_str(
 | 
					
 | 
				
			||||||
            canvas, 15 + x_offset, 24 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buf));
 | 
					        elements_scrollable_text_line(
 | 
				
			||||||
 | 
					            canvas,
 | 
				
			||||||
 | 
					            15 + x_offset,
 | 
				
			||||||
 | 
					            24 + i * FRAME_HEIGHT,
 | 
				
			||||||
 | 
					            ((scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset),
 | 
				
			||||||
 | 
					            str_buf,
 | 
				
			||||||
 | 
					            scroll_counter,
 | 
				
			||||||
 | 
					            (model->item_idx != idx));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        furi_string_free(str_buf);
 | 
					        furi_string_free(str_buf);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -329,6 +344,7 @@ static bool archive_view_input(InputEvent* event, void* context) {
 | 
				
			|||||||
                        if(move_fav_mode) {
 | 
					                        if(move_fav_mode) {
 | 
				
			||||||
                            browser->callback(ArchiveBrowserEventFavMoveUp, browser->context);
 | 
					                            browser->callback(ArchiveBrowserEventFavMoveUp, browser->context);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        model->scroll_counter = 0;
 | 
				
			||||||
                    } else if(event->key == InputKeyDown) {
 | 
					                    } else if(event->key == InputKeyDown) {
 | 
				
			||||||
                        model->item_idx = (model->item_idx + 1) % model->item_cnt;
 | 
					                        model->item_idx = (model->item_idx + 1) % model->item_cnt;
 | 
				
			||||||
                        if(is_file_list_load_required(model)) {
 | 
					                        if(is_file_list_load_required(model)) {
 | 
				
			||||||
@ -338,6 +354,7 @@ static bool archive_view_input(InputEvent* event, void* context) {
 | 
				
			|||||||
                        if(move_fav_mode) {
 | 
					                        if(move_fav_mode) {
 | 
				
			||||||
                            browser->callback(ArchiveBrowserEventFavMoveDown, browser->context);
 | 
					                            browser->callback(ArchiveBrowserEventFavMoveDown, browser->context);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        model->scroll_counter = 0;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                true);
 | 
					                true);
 | 
				
			||||||
@ -377,6 +394,27 @@ static bool archive_view_input(InputEvent* event, void* context) {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void browser_scroll_timer(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    ArchiveBrowserView* browser = context;
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        browser->view, ArchiveBrowserViewModel * model, { model->scroll_counter++; }, true);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void browser_view_enter(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    ArchiveBrowserView* browser = context;
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        browser->view, ArchiveBrowserViewModel * model, { model->scroll_counter = 0; }, true);
 | 
				
			||||||
 | 
					    furi_timer_start(browser->scroll_timer, SCROLL_INTERVAL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void browser_view_exit(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    ArchiveBrowserView* browser = context;
 | 
				
			||||||
 | 
					    furi_timer_stop(browser->scroll_timer);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ArchiveBrowserView* browser_alloc() {
 | 
					ArchiveBrowserView* browser_alloc() {
 | 
				
			||||||
    ArchiveBrowserView* browser = malloc(sizeof(ArchiveBrowserView));
 | 
					    ArchiveBrowserView* browser = malloc(sizeof(ArchiveBrowserView));
 | 
				
			||||||
    browser->view = view_alloc();
 | 
					    browser->view = view_alloc();
 | 
				
			||||||
@ -384,6 +422,10 @@ ArchiveBrowserView* browser_alloc() {
 | 
				
			|||||||
    view_set_context(browser->view, browser);
 | 
					    view_set_context(browser->view, browser);
 | 
				
			||||||
    view_set_draw_callback(browser->view, archive_view_render);
 | 
					    view_set_draw_callback(browser->view, archive_view_render);
 | 
				
			||||||
    view_set_input_callback(browser->view, archive_view_input);
 | 
					    view_set_input_callback(browser->view, archive_view_input);
 | 
				
			||||||
 | 
					    view_set_enter_callback(browser->view, browser_view_enter);
 | 
				
			||||||
 | 
					    view_set_exit_callback(browser->view, browser_view_exit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    browser->scroll_timer = furi_timer_alloc(browser_scroll_timer, FuriTimerTypePeriodic, browser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    browser->path = furi_string_alloc_set(archive_get_default_path(TAB_DEFAULT));
 | 
					    browser->path = furi_string_alloc_set(archive_get_default_path(TAB_DEFAULT));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -402,6 +444,8 @@ ArchiveBrowserView* browser_alloc() {
 | 
				
			|||||||
void browser_free(ArchiveBrowserView* browser) {
 | 
					void browser_free(ArchiveBrowserView* browser) {
 | 
				
			||||||
    furi_assert(browser);
 | 
					    furi_assert(browser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_free(browser->scroll_timer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(browser->worker_running) {
 | 
					    if(browser->worker_running) {
 | 
				
			||||||
        file_browser_worker_free(browser->worker);
 | 
					        file_browser_worker_free(browser->worker);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -81,6 +81,7 @@ struct ArchiveBrowserView {
 | 
				
			|||||||
    FuriString* path;
 | 
					    FuriString* path;
 | 
				
			||||||
    InputKey last_tab_switch_dir;
 | 
					    InputKey last_tab_switch_dir;
 | 
				
			||||||
    bool is_root;
 | 
					    bool is_root;
 | 
				
			||||||
 | 
					    FuriTimer* scroll_timer;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef struct {
 | 
					typedef struct {
 | 
				
			||||||
@ -97,6 +98,7 @@ typedef struct {
 | 
				
			|||||||
    int32_t item_idx;
 | 
					    int32_t item_idx;
 | 
				
			||||||
    int32_t array_offset;
 | 
					    int32_t array_offset;
 | 
				
			||||||
    int32_t list_offset;
 | 
					    int32_t list_offset;
 | 
				
			||||||
 | 
					    size_t scroll_counter;
 | 
				
			||||||
} ArchiveBrowserViewModel;
 | 
					} ArchiveBrowserViewModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void archive_browser_set_callback(
 | 
					void archive_browser_set_callback(
 | 
				
			||||||
 | 
				
			|||||||
@ -46,6 +46,7 @@ Nfc* nfc_alloc() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Nfc device
 | 
					    // Nfc device
 | 
				
			||||||
    nfc->dev = nfc_device_alloc();
 | 
					    nfc->dev = nfc_device_alloc();
 | 
				
			||||||
 | 
					    furi_string_set(nfc->dev->folder, NFC_APP_FOLDER);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Open GUI record
 | 
					    // Open GUI record
 | 
				
			||||||
    nfc->gui = furi_record_open(RECORD_GUI);
 | 
					    nfc->gui = furi_record_open(RECORD_GUI);
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,7 @@
 | 
				
			|||||||
#include <lib/nfc/nfc_device.h>
 | 
					#include <lib/nfc/nfc_device.h>
 | 
				
			||||||
#include <lib/nfc/helpers/mf_classic_dict.h>
 | 
					#include <lib/nfc/helpers/mf_classic_dict.h>
 | 
				
			||||||
#include <lib/nfc/parsers/nfc_supported_card.h>
 | 
					#include <lib/nfc/parsers/nfc_supported_card.h>
 | 
				
			||||||
 | 
					#include <lib/nfc/helpers/nfc_generators.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "views/dict_attack.h"
 | 
					#include "views/dict_attack.h"
 | 
				
			||||||
#include "views/detect_reader.h"
 | 
					#include "views/detect_reader.h"
 | 
				
			||||||
@ -43,6 +44,7 @@
 | 
				
			|||||||
ARRAY_DEF(MfClassicUserKeys, char*, M_PTR_OPLIST);
 | 
					ARRAY_DEF(MfClassicUserKeys, char*, M_PTR_OPLIST);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define NFC_TEXT_STORE_SIZE 128
 | 
					#define NFC_TEXT_STORE_SIZE 128
 | 
				
			||||||
 | 
					#define NFC_APP_FOLDER ANY_PATH("nfc")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef enum {
 | 
					typedef enum {
 | 
				
			||||||
    NfcRpcStateIdle,
 | 
					    NfcRpcStateIdle,
 | 
				
			||||||
@ -50,9 +52,6 @@ typedef enum {
 | 
				
			|||||||
    NfcRpcStateEmulated,
 | 
					    NfcRpcStateEmulated,
 | 
				
			||||||
} NfcRpcState;
 | 
					} NfcRpcState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Forward declaration due to circular dependency
 | 
					 | 
				
			||||||
typedef struct NfcGenerator NfcGenerator;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Nfc {
 | 
					struct Nfc {
 | 
				
			||||||
    NfcWorker* worker;
 | 
					    NfcWorker* worker;
 | 
				
			||||||
    ViewDispatcher* view_dispatcher;
 | 
					    ViewDispatcher* view_dispatcher;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
#include "../nfc_i.h"
 | 
					#include "../nfc_i.h"
 | 
				
			||||||
#include "../helpers/nfc_generators.h"
 | 
					#include "lib/nfc/helpers/nfc_generators.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void nfc_scene_generate_info_dialog_callback(DialogExResult result, void* context) {
 | 
					void nfc_scene_generate_info_dialog_callback(DialogExResult result, void* context) {
 | 
				
			||||||
    Nfc* nfc = context;
 | 
					    Nfc* nfc = context;
 | 
				
			||||||
@ -39,7 +39,12 @@ bool nfc_scene_generate_info_on_event(void* context, SceneManagerEvent event) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if(event.type == SceneManagerEventTypeCustom) {
 | 
					    if(event.type == SceneManagerEventTypeCustom) {
 | 
				
			||||||
        if(event.event == DialogExResultRight) {
 | 
					        if(event.event == DialogExResultRight) {
 | 
				
			||||||
            scene_manager_next_scene(nfc->scene_manager, nfc->generator->next_scene);
 | 
					            // Switch either to NfcSceneMfClassicMenu or NfcSceneMfUltralightMenu
 | 
				
			||||||
 | 
					            if(nfc->dev->dev_data.protocol == NfcDeviceProtocolMifareClassic) {
 | 
				
			||||||
 | 
					                scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicMenu);
 | 
				
			||||||
 | 
					            } else if(nfc->dev->dev_data.protocol == NfcDeviceProtocolMifareUl) {
 | 
				
			||||||
 | 
					                scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightMenu);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            consumed = true;
 | 
					            consumed = true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
#include "../nfc_i.h"
 | 
					#include "../nfc_i.h"
 | 
				
			||||||
#include "../helpers/nfc_generators.h"
 | 
					#include "lib/nfc/helpers/nfc_generators.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum SubmenuIndex {
 | 
					enum SubmenuIndex {
 | 
				
			||||||
    SubmenuIndexNFCA4,
 | 
					    SubmenuIndexNFCA4,
 | 
				
			||||||
 | 
				
			|||||||
@ -28,6 +28,13 @@ typedef enum {
 | 
				
			|||||||
    SubGhzHopperStateRSSITimeOut,
 | 
					    SubGhzHopperStateRSSITimeOut,
 | 
				
			||||||
} SubGhzHopperState;
 | 
					} SubGhzHopperState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** SubGhzSpeakerState state */
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    SubGhzSpeakerStateDisable,
 | 
				
			||||||
 | 
					    SubGhzSpeakerStateShutdown,
 | 
				
			||||||
 | 
					    SubGhzSpeakerStateEnable,
 | 
				
			||||||
 | 
					} SubGhzSpeakerState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** SubGhzRxKeyState state */
 | 
					/** SubGhzRxKeyState state */
 | 
				
			||||||
typedef enum {
 | 
					typedef enum {
 | 
				
			||||||
    SubGhzRxKeyStateIDLE,
 | 
					    SubGhzRxKeyStateIDLE,
 | 
				
			||||||
 | 
				
			|||||||
@ -259,6 +259,7 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) {
 | 
				
			|||||||
        case SubGhzCustomEventViewReadRAWSendStop:
 | 
					        case SubGhzCustomEventViewReadRAWSendStop:
 | 
				
			||||||
            subghz->state_notifications = SubGhzNotificationStateIDLE;
 | 
					            subghz->state_notifications = SubGhzNotificationStateIDLE;
 | 
				
			||||||
            if(subghz->txrx->txrx_state == SubGhzTxRxStateTx) {
 | 
					            if(subghz->txrx->txrx_state == SubGhzTxRxStateTx) {
 | 
				
			||||||
 | 
					                subghz_speaker_unmute(subghz);
 | 
				
			||||||
                subghz_tx_stop(subghz);
 | 
					                subghz_tx_stop(subghz);
 | 
				
			||||||
                subghz_sleep(subghz);
 | 
					                subghz_sleep(subghz);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -376,10 +377,12 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) {
 | 
				
			|||||||
                    subghz_read_raw_add_data_rssi(subghz->subghz_read_raw, rssi, false);
 | 
					                    subghz_read_raw_add_data_rssi(subghz->subghz_read_raw, rssi, false);
 | 
				
			||||||
                    subghz_protocol_raw_save_to_file_pause(
 | 
					                    subghz_protocol_raw_save_to_file_pause(
 | 
				
			||||||
                        (SubGhzProtocolDecoderRAW*)subghz->txrx->decoder_result, true);
 | 
					                        (SubGhzProtocolDecoderRAW*)subghz->txrx->decoder_result, true);
 | 
				
			||||||
 | 
					                    subghz_speaker_mute(subghz);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    subghz_read_raw_add_data_rssi(subghz->subghz_read_raw, rssi, true);
 | 
					                    subghz_read_raw_add_data_rssi(subghz->subghz_read_raw, rssi, true);
 | 
				
			||||||
                    subghz_protocol_raw_save_to_file_pause(
 | 
					                    subghz_protocol_raw_save_to_file_pause(
 | 
				
			||||||
                        (SubGhzProtocolDecoderRAW*)subghz->txrx->decoder_result, false);
 | 
					                        (SubGhzProtocolDecoderRAW*)subghz->txrx->decoder_result, false);
 | 
				
			||||||
 | 
					                    subghz_speaker_unmute(subghz);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ enum SubGhzSettingIndex {
 | 
				
			|||||||
    SubGhzSettingIndexFrequency,
 | 
					    SubGhzSettingIndexFrequency,
 | 
				
			||||||
    SubGhzSettingIndexHopping,
 | 
					    SubGhzSettingIndexHopping,
 | 
				
			||||||
    SubGhzSettingIndexModulation,
 | 
					    SubGhzSettingIndexModulation,
 | 
				
			||||||
 | 
					    SubGhzSettingIndexSound,
 | 
				
			||||||
    SubGhzSettingIndexLock,
 | 
					    SubGhzSettingIndexLock,
 | 
				
			||||||
    SubGhzSettingIndexRAWThesholdRSSI,
 | 
					    SubGhzSettingIndexRAWThesholdRSSI,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -48,6 +49,16 @@ const uint32_t hopping_value[HOPPING_COUNT] = {
 | 
				
			|||||||
    SubGhzHopperStateRunnig,
 | 
					    SubGhzHopperStateRunnig,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define SPEAKER_COUNT 2
 | 
				
			||||||
 | 
					const char* const speaker_text[SPEAKER_COUNT] = {
 | 
				
			||||||
 | 
					    "OFF",
 | 
				
			||||||
 | 
					    "ON",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const uint32_t speaker_value[SPEAKER_COUNT] = {
 | 
				
			||||||
 | 
					    SubGhzSpeakerStateShutdown,
 | 
				
			||||||
 | 
					    SubGhzSpeakerStateEnable,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
uint8_t subghz_scene_receiver_config_next_frequency(const uint32_t value, void* context) {
 | 
					uint8_t subghz_scene_receiver_config_next_frequency(const uint32_t value, void* context) {
 | 
				
			||||||
    furi_assert(context);
 | 
					    furi_assert(context);
 | 
				
			||||||
    SubGhz* subghz = context;
 | 
					    SubGhz* subghz = context;
 | 
				
			||||||
@ -167,6 +178,14 @@ static void subghz_scene_receiver_config_set_hopping_running(VariableItem* item)
 | 
				
			|||||||
    subghz->txrx->hopper_state = hopping_value[index];
 | 
					    subghz->txrx->hopper_state = hopping_value[index];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void subghz_scene_receiver_config_set_speaker(VariableItem* item) {
 | 
				
			||||||
 | 
					    SubGhz* subghz = variable_item_get_context(item);
 | 
				
			||||||
 | 
					    uint8_t index = variable_item_get_current_value_index(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, speaker_text[index]);
 | 
				
			||||||
 | 
					    subghz->txrx->speaker_state = speaker_value[index];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void subghz_scene_receiver_config_set_raw_threshold_rssi(VariableItem* item) {
 | 
					static void subghz_scene_receiver_config_set_raw_threshold_rssi(VariableItem* item) {
 | 
				
			||||||
    SubGhz* subghz = variable_item_get_context(item);
 | 
					    SubGhz* subghz = variable_item_get_context(item);
 | 
				
			||||||
    uint8_t index = variable_item_get_current_value_index(item);
 | 
					    uint8_t index = variable_item_get_current_value_index(item);
 | 
				
			||||||
@ -235,6 +254,16 @@ void subghz_scene_receiver_config_on_enter(void* context) {
 | 
				
			|||||||
    variable_item_set_current_value_text(
 | 
					    variable_item_set_current_value_text(
 | 
				
			||||||
        item, subghz_setting_get_preset_name(subghz->setting, value_index));
 | 
					        item, subghz_setting_get_preset_name(subghz->setting, value_index));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    item = variable_item_list_add(
 | 
				
			||||||
 | 
					        subghz->variable_item_list,
 | 
				
			||||||
 | 
					        "Sound:",
 | 
				
			||||||
 | 
					        SPEAKER_COUNT,
 | 
				
			||||||
 | 
					        subghz_scene_receiver_config_set_speaker,
 | 
				
			||||||
 | 
					        subghz);
 | 
				
			||||||
 | 
					    value_index = value_index_uint32(subghz->txrx->speaker_state, speaker_value, SPEAKER_COUNT);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_index(item, value_index);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, speaker_text[value_index]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
 | 
					    if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
 | 
				
			||||||
       SubGhzCustomEventManagerSet) {
 | 
					       SubGhzCustomEventManagerSet) {
 | 
				
			||||||
        variable_item_list_add(subghz->variable_item_list, "Lock Keyboard", 1, NULL, NULL);
 | 
					        variable_item_list_add(subghz->variable_item_list, "Lock Keyboard", 1, NULL, NULL);
 | 
				
			||||||
 | 
				
			|||||||
@ -177,6 +177,7 @@ SubGhz* subghz_alloc() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    subghz->txrx->txrx_state = SubGhzTxRxStateSleep;
 | 
					    subghz->txrx->txrx_state = SubGhzTxRxStateSleep;
 | 
				
			||||||
    subghz->txrx->hopper_state = SubGhzHopperStateOFF;
 | 
					    subghz->txrx->hopper_state = SubGhzHopperStateOFF;
 | 
				
			||||||
 | 
					    subghz->txrx->speaker_state = SubGhzSpeakerStateDisable;
 | 
				
			||||||
    subghz->txrx->rx_key_state = SubGhzRxKeyStateIDLE;
 | 
					    subghz->txrx->rx_key_state = SubGhzRxKeyStateIDLE;
 | 
				
			||||||
    subghz->txrx->raw_threshold_rssi = SUBGHZ_RAW_TRESHOLD_MIN;
 | 
					    subghz->txrx->raw_threshold_rssi = SUBGHZ_RAW_TRESHOLD_MIN;
 | 
				
			||||||
    subghz->txrx->history = subghz_history_alloc();
 | 
					    subghz->txrx->history = subghz_history_alloc();
 | 
				
			||||||
 | 
				
			|||||||
@ -86,6 +86,7 @@ uint32_t subghz_rx(SubGhz* subghz, uint32_t frequency) {
 | 
				
			|||||||
    uint32_t value = furi_hal_subghz_set_frequency_and_path(frequency);
 | 
					    uint32_t value = furi_hal_subghz_set_frequency_and_path(frequency);
 | 
				
			||||||
    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
 | 
					    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow);
 | 
				
			||||||
    furi_hal_subghz_flush_rx();
 | 
					    furi_hal_subghz_flush_rx();
 | 
				
			||||||
 | 
					    subghz_speaker_on(subghz);
 | 
				
			||||||
    furi_hal_subghz_rx();
 | 
					    furi_hal_subghz_rx();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, subghz->txrx->worker);
 | 
					    furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, subghz->txrx->worker);
 | 
				
			||||||
@ -104,6 +105,7 @@ static bool subghz_tx(SubGhz* subghz, uint32_t frequency) {
 | 
				
			|||||||
    furi_hal_subghz_set_frequency_and_path(frequency);
 | 
					    furi_hal_subghz_set_frequency_and_path(frequency);
 | 
				
			||||||
    furi_hal_gpio_write(&gpio_cc1101_g0, false);
 | 
					    furi_hal_gpio_write(&gpio_cc1101_g0, false);
 | 
				
			||||||
    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
 | 
					    furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
 | 
				
			||||||
 | 
					    subghz_speaker_on(subghz);
 | 
				
			||||||
    bool ret = furi_hal_subghz_tx();
 | 
					    bool ret = furi_hal_subghz_tx();
 | 
				
			||||||
    subghz->txrx->txrx_state = SubGhzTxRxStateTx;
 | 
					    subghz->txrx->txrx_state = SubGhzTxRxStateTx;
 | 
				
			||||||
    return ret;
 | 
					    return ret;
 | 
				
			||||||
@ -119,11 +121,13 @@ void subghz_idle(SubGhz* subghz) {
 | 
				
			|||||||
void subghz_rx_end(SubGhz* subghz) {
 | 
					void subghz_rx_end(SubGhz* subghz) {
 | 
				
			||||||
    furi_assert(subghz);
 | 
					    furi_assert(subghz);
 | 
				
			||||||
    furi_assert(subghz->txrx->txrx_state == SubGhzTxRxStateRx);
 | 
					    furi_assert(subghz->txrx->txrx_state == SubGhzTxRxStateRx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(subghz_worker_is_running(subghz->txrx->worker)) {
 | 
					    if(subghz_worker_is_running(subghz->txrx->worker)) {
 | 
				
			||||||
        subghz_worker_stop(subghz->txrx->worker);
 | 
					        subghz_worker_stop(subghz->txrx->worker);
 | 
				
			||||||
        furi_hal_subghz_stop_async_rx();
 | 
					        furi_hal_subghz_stop_async_rx();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    furi_hal_subghz_idle();
 | 
					    furi_hal_subghz_idle();
 | 
				
			||||||
 | 
					    subghz_speaker_off(subghz);
 | 
				
			||||||
    subghz->txrx->txrx_state = SubGhzTxRxStateIDLE;
 | 
					    subghz->txrx->txrx_state = SubGhzTxRxStateIDLE;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -212,6 +216,7 @@ void subghz_tx_stop(SubGhz* subghz) {
 | 
				
			|||||||
            subghz, subghz->txrx->fff_data, furi_string_get_cstr(subghz->file_path));
 | 
					            subghz, subghz->txrx->fff_data, furi_string_get_cstr(subghz->file_path));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    subghz_idle(subghz);
 | 
					    subghz_idle(subghz);
 | 
				
			||||||
 | 
					    subghz_speaker_off(subghz);
 | 
				
			||||||
    notification_message(subghz->notifications, &sequence_reset_red);
 | 
					    notification_message(subghz->notifications, &sequence_reset_red);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -585,3 +590,40 @@ void subghz_hopper_update(SubGhz* subghz) {
 | 
				
			|||||||
        subghz_rx(subghz, subghz->txrx->preset->frequency);
 | 
					        subghz_rx(subghz, subghz->txrx->preset->frequency);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void subghz_speaker_on(SubGhz* subghz) {
 | 
				
			||||||
 | 
					    if(subghz->txrx->speaker_state == SubGhzSpeakerStateEnable) {
 | 
				
			||||||
 | 
					        if(furi_hal_speaker_acquire(30)) {
 | 
				
			||||||
 | 
					            furi_hal_subghz_set_async_mirror_pin(&gpio_speaker);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            subghz->txrx->speaker_state = SubGhzSpeakerStateDisable;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void subghz_speaker_off(SubGhz* subghz) {
 | 
				
			||||||
 | 
					    if(subghz->txrx->speaker_state != SubGhzSpeakerStateDisable) {
 | 
				
			||||||
 | 
					        if(furi_hal_speaker_is_mine()) {
 | 
				
			||||||
 | 
					            furi_hal_subghz_set_async_mirror_pin(NULL);
 | 
				
			||||||
 | 
					            furi_hal_speaker_release();
 | 
				
			||||||
 | 
					            if(subghz->txrx->speaker_state == SubGhzSpeakerStateShutdown)
 | 
				
			||||||
 | 
					                subghz->txrx->speaker_state = SubGhzSpeakerStateDisable;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void subghz_speaker_mute(SubGhz* subghz) {
 | 
				
			||||||
 | 
					    if(subghz->txrx->speaker_state == SubGhzSpeakerStateEnable) {
 | 
				
			||||||
 | 
					        if(furi_hal_speaker_is_mine()) {
 | 
				
			||||||
 | 
					            furi_hal_subghz_set_async_mirror_pin(NULL);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void subghz_speaker_unmute(SubGhz* subghz) {
 | 
				
			||||||
 | 
					    if(subghz->txrx->speaker_state == SubGhzSpeakerStateEnable) {
 | 
				
			||||||
 | 
					        if(furi_hal_speaker_is_mine()) {
 | 
				
			||||||
 | 
					            furi_hal_subghz_set_async_mirror_pin(&gpio_speaker);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -53,6 +53,7 @@ struct SubGhzTxRx {
 | 
				
			|||||||
    uint16_t idx_menu_chosen;
 | 
					    uint16_t idx_menu_chosen;
 | 
				
			||||||
    SubGhzTxRxState txrx_state;
 | 
					    SubGhzTxRxState txrx_state;
 | 
				
			||||||
    SubGhzHopperState hopper_state;
 | 
					    SubGhzHopperState hopper_state;
 | 
				
			||||||
 | 
					    SubGhzSpeakerState speaker_state;
 | 
				
			||||||
    uint8_t hopper_timeout;
 | 
					    uint8_t hopper_timeout;
 | 
				
			||||||
    uint8_t hopper_idx_frequency;
 | 
					    uint8_t hopper_idx_frequency;
 | 
				
			||||||
    SubGhzRxKeyState rx_key_state;
 | 
					    SubGhzRxKeyState rx_key_state;
 | 
				
			||||||
@ -131,3 +132,7 @@ void subghz_file_name_clear(SubGhz* subghz);
 | 
				
			|||||||
bool subghz_path_is_file(FuriString* path);
 | 
					bool subghz_path_is_file(FuriString* path);
 | 
				
			||||||
uint32_t subghz_random_serial(void);
 | 
					uint32_t subghz_random_serial(void);
 | 
				
			||||||
void subghz_hopper_update(SubGhz* subghz);
 | 
					void subghz_hopper_update(SubGhz* subghz);
 | 
				
			||||||
 | 
					void subghz_speaker_on(SubGhz* subghz);
 | 
				
			||||||
 | 
					void subghz_speaker_off(SubGhz* subghz);
 | 
				
			||||||
 | 
					void subghz_speaker_mute(SubGhz* subghz);
 | 
				
			||||||
 | 
					void subghz_speaker_unmute(SubGhz* subghz);
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@
 | 
				
			|||||||
#include <m-array.h>
 | 
					#include <m-array.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FRAME_HEIGHT 12
 | 
					#define FRAME_HEIGHT 12
 | 
				
			||||||
#define MAX_LEN_PX 100
 | 
					#define MAX_LEN_PX 111
 | 
				
			||||||
#define MENU_ITEMS 4u
 | 
					#define MENU_ITEMS 4u
 | 
				
			||||||
#define UNLOCK_CNT 3
 | 
					#define UNLOCK_CNT 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -186,7 +186,7 @@ void subghz_view_receiver_draw(Canvas* canvas, SubGhzViewReceiverModel* model) {
 | 
				
			|||||||
        size_t idx = CLAMP((uint16_t)(i + model->list_offset), model->history_item, 0);
 | 
					        size_t idx = CLAMP((uint16_t)(i + model->list_offset), model->history_item, 0);
 | 
				
			||||||
        item_menu = SubGhzReceiverMenuItemArray_get(model->history->data, idx);
 | 
					        item_menu = SubGhzReceiverMenuItemArray_get(model->history->data, idx);
 | 
				
			||||||
        furi_string_set(str_buff, item_menu->item_str);
 | 
					        furi_string_set(str_buff, item_menu->item_str);
 | 
				
			||||||
        elements_string_fit_width(canvas, str_buff, scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX);
 | 
					        elements_string_fit_width(canvas, str_buff, scrollbar ? MAX_LEN_PX - 7 : MAX_LEN_PX);
 | 
				
			||||||
        if(model->idx == idx) {
 | 
					        if(model->idx == idx) {
 | 
				
			||||||
            subghz_view_receiver_draw_frame(canvas, i, scrollbar);
 | 
					            subghz_view_receiver_draw_frame(canvas, i, scrollbar);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								applications/plugins/clock/application.fam
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					App(
 | 
				
			||||||
 | 
					    appid="clock",
 | 
				
			||||||
 | 
					    name="Clock",
 | 
				
			||||||
 | 
					    apptype=FlipperAppType.PLUGIN,
 | 
				
			||||||
 | 
					    entry_point="clock_app",
 | 
				
			||||||
 | 
					    requires=["gui"],
 | 
				
			||||||
 | 
					    stack_size=2 * 1024,
 | 
				
			||||||
 | 
					    fap_icon="clock.png",
 | 
				
			||||||
 | 
					    fap_category="Tools",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/clock/clock.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 KiB  | 
							
								
								
									
										136
									
								
								applications/plugins/clock/clock_app.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					#include <furi.h>
 | 
				
			||||||
 | 
					#include <furi_hal.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gui/gui.h>
 | 
				
			||||||
 | 
					#include <locale/locale.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    ClockEventTypeTick,
 | 
				
			||||||
 | 
					    ClockEventTypeKey,
 | 
				
			||||||
 | 
					} ClockEventType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    ClockEventType type;
 | 
				
			||||||
 | 
					    InputEvent input;
 | 
				
			||||||
 | 
					} ClockEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    FuriString* buffer;
 | 
				
			||||||
 | 
					    FuriHalRtcDateTime datetime;
 | 
				
			||||||
 | 
					    LocaleTimeFormat timeformat;
 | 
				
			||||||
 | 
					    LocaleDateFormat dateformat;
 | 
				
			||||||
 | 
					} ClockData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    FuriMutex* mutex;
 | 
				
			||||||
 | 
					    FuriMessageQueue* queue;
 | 
				
			||||||
 | 
					    ClockData* data;
 | 
				
			||||||
 | 
					} Clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void clock_input_callback(InputEvent* input_event, FuriMessageQueue* queue) {
 | 
				
			||||||
 | 
					    furi_assert(queue);
 | 
				
			||||||
 | 
					    ClockEvent event = {.type = ClockEventTypeKey, .input = *input_event};
 | 
				
			||||||
 | 
					    furi_message_queue_put(queue, &event, FuriWaitForever);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void clock_render_callback(Canvas* canvas, void* ctx) {
 | 
				
			||||||
 | 
					    Clock* clock = ctx;
 | 
				
			||||||
 | 
					    if(furi_mutex_acquire(clock->mutex, 200) != FuriStatusOk) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ClockData* data = clock->data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    canvas_set_font(canvas, FontBigNumbers);
 | 
				
			||||||
 | 
					    locale_format_time(data->buffer, &data->datetime, data->timeformat, true);
 | 
				
			||||||
 | 
					    canvas_draw_str_aligned(
 | 
				
			||||||
 | 
					        canvas, 64, 28, AlignCenter, AlignCenter, furi_string_get_cstr(data->buffer));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Special case to cover missing glyphs in FontBigNumbers
 | 
				
			||||||
 | 
					    if(data->timeformat == LocaleTimeFormat12h) {
 | 
				
			||||||
 | 
					        size_t time_width = canvas_string_width(canvas, furi_string_get_cstr(data->buffer));
 | 
				
			||||||
 | 
					        canvas_set_font(canvas, FontPrimary);
 | 
				
			||||||
 | 
					        canvas_draw_str_aligned(
 | 
				
			||||||
 | 
					            canvas,
 | 
				
			||||||
 | 
					            64 + (time_width / 2) - 10,
 | 
				
			||||||
 | 
					            31,
 | 
				
			||||||
 | 
					            AlignLeft,
 | 
				
			||||||
 | 
					            AlignCenter,
 | 
				
			||||||
 | 
					            (data->datetime.hour > 12) ? "PM" : "AM");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    canvas_set_font(canvas, FontSecondary);
 | 
				
			||||||
 | 
					    locale_format_date(data->buffer, &data->datetime, data->dateformat, "/");
 | 
				
			||||||
 | 
					    canvas_draw_str_aligned(
 | 
				
			||||||
 | 
					        canvas, 64, 42, AlignCenter, AlignTop, furi_string_get_cstr(data->buffer));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_mutex_release(clock->mutex);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void clock_tick(void* ctx) {
 | 
				
			||||||
 | 
					    furi_assert(ctx);
 | 
				
			||||||
 | 
					    FuriMessageQueue* queue = ctx;
 | 
				
			||||||
 | 
					    ClockEvent event = {.type = ClockEventTypeTick};
 | 
				
			||||||
 | 
					    // It's OK to loose this event if system overloaded
 | 
				
			||||||
 | 
					    furi_message_queue_put(queue, &event, 0);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int32_t clock_app(void* p) {
 | 
				
			||||||
 | 
					    UNUSED(p);
 | 
				
			||||||
 | 
					    Clock* clock = malloc(sizeof(Clock));
 | 
				
			||||||
 | 
					    clock->data = malloc(sizeof(ClockData));
 | 
				
			||||||
 | 
					    clock->data->buffer = furi_string_alloc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    clock->queue = furi_message_queue_alloc(8, sizeof(ClockEvent));
 | 
				
			||||||
 | 
					    clock->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_hal_rtc_get_datetime(&clock->data->datetime);
 | 
				
			||||||
 | 
					    clock->data->timeformat = locale_get_time_format();
 | 
				
			||||||
 | 
					    clock->data->dateformat = locale_get_date_format();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Set ViewPort callbacks
 | 
				
			||||||
 | 
					    ViewPort* view_port = view_port_alloc();
 | 
				
			||||||
 | 
					    view_port_draw_callback_set(view_port, clock_render_callback, clock);
 | 
				
			||||||
 | 
					    view_port_input_callback_set(view_port, clock_input_callback, clock->queue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, clock->queue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Open GUI and register view_port
 | 
				
			||||||
 | 
					    Gui* gui = furi_record_open(RECORD_GUI);
 | 
				
			||||||
 | 
					    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_start(timer, 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Main loop
 | 
				
			||||||
 | 
					    ClockEvent event;
 | 
				
			||||||
 | 
					    for(bool processing = true; processing;) {
 | 
				
			||||||
 | 
					        furi_check(furi_message_queue_get(clock->queue, &event, FuriWaitForever) == FuriStatusOk);
 | 
				
			||||||
 | 
					        furi_mutex_acquire(clock->mutex, FuriWaitForever);
 | 
				
			||||||
 | 
					        if(event.type == ClockEventTypeKey) {
 | 
				
			||||||
 | 
					            if(event.input.type == InputTypeShort && event.input.key == InputKeyBack) {
 | 
				
			||||||
 | 
					                processing = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if(event.type == ClockEventTypeTick) {
 | 
				
			||||||
 | 
					            furi_hal_rtc_get_datetime(&clock->data->datetime);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        furi_mutex_release(clock->mutex);
 | 
				
			||||||
 | 
					        view_port_update(view_port);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_free(timer);
 | 
				
			||||||
 | 
					    view_port_enabled_set(view_port, false);
 | 
				
			||||||
 | 
					    gui_remove_view_port(gui, view_port);
 | 
				
			||||||
 | 
					    view_port_free(view_port);
 | 
				
			||||||
 | 
					    furi_record_close(RECORD_GUI);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_message_queue_free(clock->queue);
 | 
				
			||||||
 | 
					    furi_mutex_free(clock->mutex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_string_free(clock->data->buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    free(clock->data);
 | 
				
			||||||
 | 
					    free(clock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -9,10 +9,10 @@ enum HidDebugSubmenuIndex {
 | 
				
			|||||||
    HidSubmenuIndexKeynote,
 | 
					    HidSubmenuIndexKeynote,
 | 
				
			||||||
    HidSubmenuIndexKeyboard,
 | 
					    HidSubmenuIndexKeyboard,
 | 
				
			||||||
    HidSubmenuIndexMedia,
 | 
					    HidSubmenuIndexMedia,
 | 
				
			||||||
    BtHidSubmenuIndexTikTok,
 | 
					    HidSubmenuIndexTikTok,
 | 
				
			||||||
    HidSubmenuIndexMouse,
 | 
					    HidSubmenuIndexMouse,
 | 
				
			||||||
 | 
					    HidSubmenuIndexMouseJiggler,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
typedef enum { ConnTypeSubmenuIndexBluetooth, ConnTypeSubmenuIndexUsb } ConnTypeDebugSubmenuIndex;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void hid_submenu_callback(void* context, uint32_t index) {
 | 
					static void hid_submenu_callback(void* context, uint32_t index) {
 | 
				
			||||||
    furi_assert(context);
 | 
					    furi_assert(context);
 | 
				
			||||||
@ -29,9 +29,12 @@ static void hid_submenu_callback(void* context, uint32_t index) {
 | 
				
			|||||||
    } else if(index == HidSubmenuIndexMouse) {
 | 
					    } else if(index == HidSubmenuIndexMouse) {
 | 
				
			||||||
        app->view_id = HidViewMouse;
 | 
					        app->view_id = HidViewMouse;
 | 
				
			||||||
        view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouse);
 | 
					        view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouse);
 | 
				
			||||||
    } else if(index == BtHidSubmenuIndexTikTok) {
 | 
					    } else if(index == HidSubmenuIndexTikTok) {
 | 
				
			||||||
        app->view_id = BtHidViewTikTok;
 | 
					        app->view_id = BtHidViewTikTok;
 | 
				
			||||||
        view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewTikTok);
 | 
					        view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewTikTok);
 | 
				
			||||||
 | 
					    } else if(index == HidSubmenuIndexMouseJiggler) {
 | 
				
			||||||
 | 
					        app->view_id = HidViewMouseJiggler;
 | 
				
			||||||
 | 
					        view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouseJiggler);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -48,6 +51,7 @@ static void bt_hid_connection_status_changed_callback(BtStatus status, void* con
 | 
				
			|||||||
    hid_keyboard_set_connected_status(hid->hid_keyboard, connected);
 | 
					    hid_keyboard_set_connected_status(hid->hid_keyboard, connected);
 | 
				
			||||||
    hid_media_set_connected_status(hid->hid_media, connected);
 | 
					    hid_media_set_connected_status(hid->hid_media, connected);
 | 
				
			||||||
    hid_mouse_set_connected_status(hid->hid_mouse, connected);
 | 
					    hid_mouse_set_connected_status(hid->hid_mouse, connected);
 | 
				
			||||||
 | 
					    hid_mouse_jiggler_set_connected_status(hid->hid_mouse_jiggler, connected);
 | 
				
			||||||
    hid_tiktok_set_connected_status(hid->hid_tiktok, connected);
 | 
					    hid_tiktok_set_connected_status(hid->hid_tiktok, connected);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -104,10 +108,16 @@ Hid* hid_alloc(HidTransport transport) {
 | 
				
			|||||||
        submenu_add_item(
 | 
					        submenu_add_item(
 | 
				
			||||||
            app->device_type_submenu,
 | 
					            app->device_type_submenu,
 | 
				
			||||||
            "TikTok Controller",
 | 
					            "TikTok Controller",
 | 
				
			||||||
            BtHidSubmenuIndexTikTok,
 | 
					            HidSubmenuIndexTikTok,
 | 
				
			||||||
            hid_submenu_callback,
 | 
					            hid_submenu_callback,
 | 
				
			||||||
            app);
 | 
					            app);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    submenu_add_item(
 | 
				
			||||||
 | 
					        app->device_type_submenu,
 | 
				
			||||||
 | 
					        "Mouse Jiggler",
 | 
				
			||||||
 | 
					        HidSubmenuIndexMouseJiggler,
 | 
				
			||||||
 | 
					        hid_submenu_callback,
 | 
				
			||||||
 | 
					        app);
 | 
				
			||||||
    view_set_previous_callback(submenu_get_view(app->device_type_submenu), hid_exit);
 | 
					    view_set_previous_callback(submenu_get_view(app->device_type_submenu), hid_exit);
 | 
				
			||||||
    view_dispatcher_add_view(
 | 
					    view_dispatcher_add_view(
 | 
				
			||||||
        app->view_dispatcher, HidViewSubmenu, submenu_get_view(app->device_type_submenu));
 | 
					        app->view_dispatcher, HidViewSubmenu, submenu_get_view(app->device_type_submenu));
 | 
				
			||||||
@ -160,6 +170,15 @@ Hid* hid_app_alloc_view(void* context) {
 | 
				
			|||||||
    view_dispatcher_add_view(
 | 
					    view_dispatcher_add_view(
 | 
				
			||||||
        app->view_dispatcher, HidViewMouse, hid_mouse_get_view(app->hid_mouse));
 | 
					        app->view_dispatcher, HidViewMouse, hid_mouse_get_view(app->hid_mouse));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Mouse jiggler view
 | 
				
			||||||
 | 
					    app->hid_mouse_jiggler = hid_mouse_jiggler_alloc(app);
 | 
				
			||||||
 | 
					    view_set_previous_callback(
 | 
				
			||||||
 | 
					        hid_mouse_jiggler_get_view(app->hid_mouse_jiggler), hid_exit_confirm_view);
 | 
				
			||||||
 | 
					    view_dispatcher_add_view(
 | 
				
			||||||
 | 
					        app->view_dispatcher,
 | 
				
			||||||
 | 
					        HidViewMouseJiggler,
 | 
				
			||||||
 | 
					        hid_mouse_jiggler_get_view(app->hid_mouse_jiggler));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return app;
 | 
					    return app;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -182,6 +201,8 @@ void hid_free(Hid* app) {
 | 
				
			|||||||
    hid_media_free(app->hid_media);
 | 
					    hid_media_free(app->hid_media);
 | 
				
			||||||
    view_dispatcher_remove_view(app->view_dispatcher, HidViewMouse);
 | 
					    view_dispatcher_remove_view(app->view_dispatcher, HidViewMouse);
 | 
				
			||||||
    hid_mouse_free(app->hid_mouse);
 | 
					    hid_mouse_free(app->hid_mouse);
 | 
				
			||||||
 | 
					    view_dispatcher_remove_view(app->view_dispatcher, HidViewMouseJiggler);
 | 
				
			||||||
 | 
					    hid_mouse_jiggler_free(app->hid_mouse_jiggler);
 | 
				
			||||||
    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewTikTok);
 | 
					    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewTikTok);
 | 
				
			||||||
    hid_tiktok_free(app->hid_tiktok);
 | 
					    hid_tiktok_free(app->hid_tiktok);
 | 
				
			||||||
    view_dispatcher_free(app->view_dispatcher);
 | 
					    view_dispatcher_free(app->view_dispatcher);
 | 
				
			||||||
@ -346,9 +367,17 @@ int32_t hid_ble_app(void* p) {
 | 
				
			|||||||
    Hid* app = hid_alloc(HidTransportBle);
 | 
					    Hid* app = hid_alloc(HidTransportBle);
 | 
				
			||||||
    app = hid_app_alloc_view(app);
 | 
					    app = hid_app_alloc_view(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_disconnect(app->bt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Wait 2nd core to update nvm storage
 | 
				
			||||||
 | 
					    furi_delay_ms(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_keys_storage_set_storage_path(app->bt, HID_BT_KEYS_STORAGE_PATH);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) {
 | 
					    if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) {
 | 
				
			||||||
        FURI_LOG_E(TAG, "Failed to switch profile");
 | 
					        FURI_LOG_E(TAG, "Failed to switch to HID profile");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    furi_hal_bt_start_advertising();
 | 
					    furi_hal_bt_start_advertising();
 | 
				
			||||||
    bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app);
 | 
					    bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -357,7 +386,17 @@ int32_t hid_ble_app(void* p) {
 | 
				
			|||||||
    view_dispatcher_run(app->view_dispatcher);
 | 
					    view_dispatcher_run(app->view_dispatcher);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bt_set_status_changed_callback(app->bt, NULL, NULL);
 | 
					    bt_set_status_changed_callback(app->bt, NULL, NULL);
 | 
				
			||||||
    bt_set_profile(app->bt, BtProfileSerial);
 | 
					
 | 
				
			||||||
 | 
					    bt_disconnect(app->bt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Wait 2nd core to update nvm storage
 | 
				
			||||||
 | 
					    furi_delay_ms(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_keys_storage_set_default_path(app->bt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(!bt_set_profile(app->bt, BtProfileSerial)) {
 | 
				
			||||||
 | 
					        FURI_LOG_E(TAG, "Failed to switch to Serial profile");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    hid_free(app);
 | 
					    hid_free(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@
 | 
				
			|||||||
#include <gui/view.h>
 | 
					#include <gui/view.h>
 | 
				
			||||||
#include <gui/view_dispatcher.h>
 | 
					#include <gui/view_dispatcher.h>
 | 
				
			||||||
#include <notification/notification.h>
 | 
					#include <notification/notification.h>
 | 
				
			||||||
 | 
					#include <storage/storage.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <gui/modules/submenu.h>
 | 
					#include <gui/modules/submenu.h>
 | 
				
			||||||
#include <gui/modules/dialog_ex.h>
 | 
					#include <gui/modules/dialog_ex.h>
 | 
				
			||||||
@ -19,8 +20,11 @@
 | 
				
			|||||||
#include "views/hid_keyboard.h"
 | 
					#include "views/hid_keyboard.h"
 | 
				
			||||||
#include "views/hid_media.h"
 | 
					#include "views/hid_media.h"
 | 
				
			||||||
#include "views/hid_mouse.h"
 | 
					#include "views/hid_mouse.h"
 | 
				
			||||||
 | 
					#include "views/hid_mouse_jiggler.h"
 | 
				
			||||||
#include "views/hid_tiktok.h"
 | 
					#include "views/hid_tiktok.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define HID_BT_KEYS_STORAGE_PATH EXT_PATH("apps/Tools/.bt_hid.keys")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef enum {
 | 
					typedef enum {
 | 
				
			||||||
    HidTransportUsb,
 | 
					    HidTransportUsb,
 | 
				
			||||||
    HidTransportBle,
 | 
					    HidTransportBle,
 | 
				
			||||||
@ -39,6 +43,7 @@ struct Hid {
 | 
				
			|||||||
    HidKeyboard* hid_keyboard;
 | 
					    HidKeyboard* hid_keyboard;
 | 
				
			||||||
    HidMedia* hid_media;
 | 
					    HidMedia* hid_media;
 | 
				
			||||||
    HidMouse* hid_mouse;
 | 
					    HidMouse* hid_mouse;
 | 
				
			||||||
 | 
					    HidMouseJiggler* hid_mouse_jiggler;
 | 
				
			||||||
    HidTikTok* hid_tiktok;
 | 
					    HidTikTok* hid_tiktok;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    HidTransport transport;
 | 
					    HidTransport transport;
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ typedef enum {
 | 
				
			|||||||
    HidViewKeyboard,
 | 
					    HidViewKeyboard,
 | 
				
			||||||
    HidViewMedia,
 | 
					    HidViewMedia,
 | 
				
			||||||
    HidViewMouse,
 | 
					    HidViewMouse,
 | 
				
			||||||
 | 
					    HidViewMouseJiggler,
 | 
				
			||||||
    BtHidViewTikTok,
 | 
					    BtHidViewTikTok,
 | 
				
			||||||
    HidViewExitConfirm,
 | 
					    HidViewExitConfirm,
 | 
				
			||||||
} HidView;
 | 
					} HidView;
 | 
				
			||||||
							
								
								
									
										149
									
								
								applications/plugins/hid_app/views/hid_mouse_jiggler.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,149 @@
 | 
				
			|||||||
 | 
					#include "hid_mouse_jiggler.h"
 | 
				
			||||||
 | 
					#include <gui/elements.h>
 | 
				
			||||||
 | 
					#include "../hid.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "hid_icons.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define TAG "HidMouseJiggler"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct HidMouseJiggler {
 | 
				
			||||||
 | 
					    View* view;
 | 
				
			||||||
 | 
					    Hid* hid;
 | 
				
			||||||
 | 
					    FuriTimer* timer;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    bool connected;
 | 
				
			||||||
 | 
					    bool running;
 | 
				
			||||||
 | 
					    uint8_t counter;
 | 
				
			||||||
 | 
					} HidMouseJigglerModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void hid_mouse_jiggler_draw_callback(Canvas* canvas, void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    HidMouseJigglerModel* model = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Header
 | 
				
			||||||
 | 
					    if(model->connected) {
 | 
				
			||||||
 | 
					        canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    canvas_set_font(canvas, FontPrimary);
 | 
				
			||||||
 | 
					    elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Mouse Jiggler");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    canvas_set_font(canvas, FontPrimary);
 | 
				
			||||||
 | 
					    elements_multiline_text(canvas, AlignLeft, 35, "Press Start\nto jiggle");
 | 
				
			||||||
 | 
					    canvas_set_font(canvas, FontSecondary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Ok
 | 
				
			||||||
 | 
					    canvas_draw_icon(canvas, 63, 25, &I_Space_65x18);
 | 
				
			||||||
 | 
					    if(model->running) {
 | 
				
			||||||
 | 
					        elements_slightly_rounded_box(canvas, 66, 27, 60, 13);
 | 
				
			||||||
 | 
					        canvas_set_color(canvas, ColorWhite);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9);
 | 
				
			||||||
 | 
					    if(model->running) {
 | 
				
			||||||
 | 
					        elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Stop");
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Start");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    canvas_set_color(canvas, ColorBlack);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Back
 | 
				
			||||||
 | 
					    canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8);
 | 
				
			||||||
 | 
					    elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Quit");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void hid_mouse_jiggler_timer_callback(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    HidMouseJiggler* hid_mouse_jiggler = context;
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        hid_mouse_jiggler->view,
 | 
				
			||||||
 | 
					        HidMouseJigglerModel * model,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if(model->running) {
 | 
				
			||||||
 | 
					                model->counter++;
 | 
				
			||||||
 | 
					                hid_hal_mouse_move(
 | 
				
			||||||
 | 
					                    hid_mouse_jiggler->hid,
 | 
				
			||||||
 | 
					                    (model->counter % 2 == 0) ? MOUSE_MOVE_SHORT : -MOUSE_MOVE_SHORT,
 | 
				
			||||||
 | 
					                    0);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        false);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void hid_mouse_jiggler_enter_callback(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    HidMouseJiggler* hid_mouse_jiggler = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_start(hid_mouse_jiggler->timer, 500);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void hid_mouse_jiggler_exit_callback(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    HidMouseJiggler* hid_mouse_jiggler = context;
 | 
				
			||||||
 | 
					    furi_timer_stop(hid_mouse_jiggler->timer);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool hid_mouse_jiggler_input_callback(InputEvent* event, void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    HidMouseJiggler* hid_mouse_jiggler = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool consumed = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(event->key == InputKeyOk) {
 | 
				
			||||||
 | 
					        with_view_model(
 | 
				
			||||||
 | 
					            hid_mouse_jiggler->view,
 | 
				
			||||||
 | 
					            HidMouseJigglerModel * model,
 | 
				
			||||||
 | 
					            { model->running = !model->running; },
 | 
				
			||||||
 | 
					            true);
 | 
				
			||||||
 | 
					        consumed = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return consumed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HidMouseJiggler* hid_mouse_jiggler_alloc(Hid* hid) {
 | 
				
			||||||
 | 
					    HidMouseJiggler* hid_mouse_jiggler = malloc(sizeof(HidMouseJiggler));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hid_mouse_jiggler->view = view_alloc();
 | 
				
			||||||
 | 
					    view_set_context(hid_mouse_jiggler->view, hid_mouse_jiggler);
 | 
				
			||||||
 | 
					    view_allocate_model(
 | 
				
			||||||
 | 
					        hid_mouse_jiggler->view, ViewModelTypeLocking, sizeof(HidMouseJigglerModel));
 | 
				
			||||||
 | 
					    view_set_draw_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_draw_callback);
 | 
				
			||||||
 | 
					    view_set_input_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_input_callback);
 | 
				
			||||||
 | 
					    view_set_enter_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_enter_callback);
 | 
				
			||||||
 | 
					    view_set_exit_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_exit_callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hid_mouse_jiggler->hid = hid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hid_mouse_jiggler->timer = furi_timer_alloc(
 | 
				
			||||||
 | 
					        hid_mouse_jiggler_timer_callback, FuriTimerTypePeriodic, hid_mouse_jiggler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return hid_mouse_jiggler;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void hid_mouse_jiggler_free(HidMouseJiggler* hid_mouse_jiggler) {
 | 
				
			||||||
 | 
					    furi_assert(hid_mouse_jiggler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_stop(hid_mouse_jiggler->timer);
 | 
				
			||||||
 | 
					    furi_timer_free(hid_mouse_jiggler->timer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    view_free(hid_mouse_jiggler->view);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    free(hid_mouse_jiggler);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					View* hid_mouse_jiggler_get_view(HidMouseJiggler* hid_mouse_jiggler) {
 | 
				
			||||||
 | 
					    furi_assert(hid_mouse_jiggler);
 | 
				
			||||||
 | 
					    return hid_mouse_jiggler->view;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void hid_mouse_jiggler_set_connected_status(HidMouseJiggler* hid_mouse_jiggler, bool connected) {
 | 
				
			||||||
 | 
					    furi_assert(hid_mouse_jiggler);
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        hid_mouse_jiggler->view,
 | 
				
			||||||
 | 
					        HidMouseJigglerModel * model,
 | 
				
			||||||
 | 
					        { model->connected = connected; },
 | 
				
			||||||
 | 
					        true);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								applications/plugins/hid_app/views/hid_mouse_jiggler.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gui/view.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define MOUSE_MOVE_SHORT 5
 | 
				
			||||||
 | 
					#define MOUSE_MOVE_LONG 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct Hid Hid;
 | 
				
			||||||
 | 
					typedef struct HidMouseJiggler HidMouseJiggler;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HidMouseJiggler* hid_mouse_jiggler_alloc(Hid* bt_hid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void hid_mouse_jiggler_free(HidMouseJiggler* hid_mouse_jiggler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					View* hid_mouse_jiggler_get_view(HidMouseJiggler* hid_mouse_jiggler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void hid_mouse_jiggler_set_connected_status(HidMouseJiggler* hid_mouse_jiggler, bool connected);
 | 
				
			||||||
@ -47,7 +47,7 @@ static int32_t music_player_worker_thread_callback(void* context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    NoteBlockArray_it_t it;
 | 
					    NoteBlockArray_it_t it;
 | 
				
			||||||
    NoteBlockArray_it(it, instance->notes);
 | 
					    NoteBlockArray_it(it, instance->notes);
 | 
				
			||||||
 | 
					    if(furi_hal_speaker_acquire(1000)) {
 | 
				
			||||||
        while(instance->should_work) {
 | 
					        while(instance->should_work) {
 | 
				
			||||||
            if(NoteBlockArray_end_p(it)) {
 | 
					            if(NoteBlockArray_end_p(it)) {
 | 
				
			||||||
                NoteBlockArray_it(it, instance->notes);
 | 
					                NoteBlockArray_it(it, instance->notes);
 | 
				
			||||||
@ -57,8 +57,8 @@ static int32_t music_player_worker_thread_callback(void* context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE;
 | 
					                float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE;
 | 
				
			||||||
                float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4);
 | 
					                float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4);
 | 
				
			||||||
            float duration =
 | 
					                float duration = 60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm /
 | 
				
			||||||
                60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm / note_block->duration;
 | 
					                                 note_block->duration;
 | 
				
			||||||
                uint32_t dots = note_block->dots;
 | 
					                uint32_t dots = note_block->dots;
 | 
				
			||||||
                while(dots > 0) {
 | 
					                while(dots > 0) {
 | 
				
			||||||
                    duration += duration / 2;
 | 
					                    duration += duration / 2;
 | 
				
			||||||
@ -88,6 +88,10 @@ static int32_t music_player_worker_thread_callback(void* context) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        furi_hal_speaker_stop();
 | 
					        furi_hal_speaker_stop();
 | 
				
			||||||
 | 
					        furi_hal_speaker_release();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        FURI_LOG_E(TAG, "Speaker system is busy with another process.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -49,6 +49,7 @@ NfcMagic* nfc_magic_alloc() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Nfc device
 | 
					    // Nfc device
 | 
				
			||||||
    nfc_magic->nfc_dev = nfc_device_alloc();
 | 
					    nfc_magic->nfc_dev = nfc_device_alloc();
 | 
				
			||||||
 | 
					    furi_string_set(nfc_magic->nfc_dev->folder, NFC_APP_FOLDER);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Open GUI record
 | 
					    // Open GUI record
 | 
				
			||||||
    nfc_magic->gui = furi_record_open(RECORD_GUI);
 | 
					    nfc_magic->gui = furi_record_open(RECORD_GUI);
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,8 @@
 | 
				
			|||||||
#include <lib/nfc/nfc_device.h>
 | 
					#include <lib/nfc/nfc_device.h>
 | 
				
			||||||
#include "nfc_magic_icons.h"
 | 
					#include "nfc_magic_icons.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define NFC_APP_FOLDER ANY_PATH("nfc")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum NfcMagicCustomEvent {
 | 
					enum NfcMagicCustomEvent {
 | 
				
			||||||
    // Reserve first 100 events for button types and indexes, starting from 0
 | 
					    // Reserve first 100 events for button types and indexes, starting from 0
 | 
				
			||||||
    NfcMagicCustomEventReserved = 100,
 | 
					    NfcMagicCustomEventReserved = 100,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
#include <furi.h>
 | 
					#include <furi.h>
 | 
				
			||||||
#include <furi_hal.h>
 | 
					#include <furi_hal.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define WS_VERSION_APP "0.5"
 | 
					#define WS_VERSION_APP "0.6.1"
 | 
				
			||||||
#define WS_DEVELOPED "SkorP"
 | 
					#define WS_DEVELOPED "SkorP"
 | 
				
			||||||
#define WS_GITHUB "https://github.com/flipperdevices/flipperzero-firmware"
 | 
					#define WS_GITHUB "https://github.com/flipperdevices/flipperzero-firmware"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/weather_station/images/Humid_8x13.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/weather_station/images/Timer_11x11.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.5 KiB  | 
@ -134,8 +134,8 @@ static void ws_protocol_ambient_weather_remote_controller(WSBlockGeneric* instan
 | 
				
			|||||||
    instance->id = (instance->data >> 32) & 0xFF;
 | 
					    instance->id = (instance->data >> 32) & 0xFF;
 | 
				
			||||||
    instance->battery_low = (instance->data >> 31) & 1;
 | 
					    instance->battery_low = (instance->data >> 31) & 1;
 | 
				
			||||||
    instance->channel = ((instance->data >> 28) & 0x07) + 1;
 | 
					    instance->channel = ((instance->data >> 28) & 0x07) + 1;
 | 
				
			||||||
    instance->temp = ws_block_generic_fahrenheit_to_celsius(
 | 
					    instance->temp =
 | 
				
			||||||
        ((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f);
 | 
					        locale_fahrenheit_to_celsius(((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f);
 | 
				
			||||||
    instance->humidity = (instance->data >> 8) & 0xFF;
 | 
					    instance->humidity = (instance->data >> 8) & 0xFF;
 | 
				
			||||||
    instance->btn = WS_NO_BTN;
 | 
					    instance->btn = WS_NO_BTN;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -143,8 +143,8 @@ static void ws_protocol_infactory_remote_controller(WSBlockGeneric* instance) {
 | 
				
			|||||||
    instance->id = instance->data >> 32;
 | 
					    instance->id = instance->data >> 32;
 | 
				
			||||||
    instance->battery_low = (instance->data >> 26) & 1;
 | 
					    instance->battery_low = (instance->data >> 26) & 1;
 | 
				
			||||||
    instance->btn = WS_NO_BTN;
 | 
					    instance->btn = WS_NO_BTN;
 | 
				
			||||||
    instance->temp = ws_block_generic_fahrenheit_to_celsius(
 | 
					    instance->temp =
 | 
				
			||||||
        ((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f);
 | 
					        locale_fahrenheit_to_celsius(((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f);
 | 
				
			||||||
    instance->humidity =
 | 
					    instance->humidity =
 | 
				
			||||||
        (((instance->data >> 8) & 0x0F) * 10) + ((instance->data >> 4) & 0x0F); // BCD, 'A0'=100%rH
 | 
					        (((instance->data >> 8) & 0x0F) * 10) + ((instance->data >> 4) & 0x0F); // BCD, 'A0'=100%rH
 | 
				
			||||||
    instance->channel = instance->data & 0x03;
 | 
					    instance->channel = instance->data & 0x03;
 | 
				
			||||||
 | 
				
			|||||||
@ -135,6 +135,10 @@ static void ws_protocol_nexus_th_remote_controller(WSBlockGeneric* instance) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    instance->humidity = instance->data & 0xFF;
 | 
					    instance->humidity = instance->data & 0xFF;
 | 
				
			||||||
 | 
					    if(instance->humidity > 95)
 | 
				
			||||||
 | 
					        instance->humidity = 95;
 | 
				
			||||||
 | 
					    else if(instance->humidity < 20)
 | 
				
			||||||
 | 
					        instance->humidity = 20;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void ws_protocol_decoder_nexus_th_feed(void* context, bool level, uint32_t duration) {
 | 
					void ws_protocol_decoder_nexus_th_feed(void* context, bool level, uint32_t duration) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										331
									
								
								applications/plugins/weather_station/protocols/oregon_v1.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,331 @@
 | 
				
			|||||||
 | 
					#include "oregon_v1.h"
 | 
				
			||||||
 | 
					#include <lib/toolbox/manchester_decoder.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define TAG "WSProtocolOregon_V1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Help
 | 
				
			||||||
 | 
					 * https://github.dev/merbanan/rtl_433/blob/bb1be7f186ac0fdb7dc5d77693847d96fb95281e/src/devices/oregon_scientific_v1.c
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * OSv1 protocol.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * MC with nominal bit width of 2930 us.
 | 
				
			||||||
 | 
					 * Pulses are somewhat longer than nominal half-bit width, 1748 us / 3216 us,
 | 
				
			||||||
 | 
					 * Gaps are somewhat shorter than nominal half-bit width, 1176 us / 2640 us.
 | 
				
			||||||
 | 
					 * After 12 preamble bits there is 4200 us gap, 5780 us pulse, 5200 us gap.
 | 
				
			||||||
 | 
					 * And next 32 bit data
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * Care must be taken with the gap after the sync pulse since it
 | 
				
			||||||
 | 
					 * is outside of the normal clocking.  Because of this a data stream
 | 
				
			||||||
 | 
					 * beginning with a 0 will have data in this gap.   
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * Data is in reverse order of bits
 | 
				
			||||||
 | 
					 *      RevBit(data32bit)=> tib23atad
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 *      tib23atad => xxxxxxxx | busuTTTT | ttttzzzz | ccuuiiii 
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      - i: ID
 | 
				
			||||||
 | 
					 *      - x: CRC;
 | 
				
			||||||
 | 
					 *      - u: unknown;
 | 
				
			||||||
 | 
					 *      - b: battery low; flag to indicate low battery voltage
 | 
				
			||||||
 | 
					 *      - s: temperature sign
 | 
				
			||||||
 | 
					 *      - T: BCD, Temperature; in °C * 10
 | 
				
			||||||
 | 
					 *      - t: BCD, Temperature; in °C * 1
 | 
				
			||||||
 | 
					 *      - z: BCD, Temperature; in °C * 0.1
 | 
				
			||||||
 | 
					 *      - c: Channel 00=CH1, 01=CH2, 10=CH3
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define OREGON_V1_HEADER_OK 0xFF
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const SubGhzBlockConst ws_protocol_oregon_v1_const = {
 | 
				
			||||||
 | 
					    .te_short = 1465,
 | 
				
			||||||
 | 
					    .te_long = 2930,
 | 
				
			||||||
 | 
					    .te_delta = 350,
 | 
				
			||||||
 | 
					    .min_count_bit_for_found = 32,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct WSProtocolDecoderOregon_V1 {
 | 
				
			||||||
 | 
					    SubGhzProtocolDecoderBase base;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SubGhzBlockDecoder decoder;
 | 
				
			||||||
 | 
					    WSBlockGeneric generic;
 | 
				
			||||||
 | 
					    ManchesterState manchester_state;
 | 
				
			||||||
 | 
					    uint16_t header_count;
 | 
				
			||||||
 | 
					    uint8_t first_bit;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct WSProtocolEncoderOregon_V1 {
 | 
				
			||||||
 | 
					    SubGhzProtocolEncoderBase base;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SubGhzProtocolBlockEncoder encoder;
 | 
				
			||||||
 | 
					    WSBlockGeneric generic;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    Oregon_V1DecoderStepReset = 0,
 | 
				
			||||||
 | 
					    Oregon_V1DecoderStepFoundPreamble,
 | 
				
			||||||
 | 
					    Oregon_V1DecoderStepParse,
 | 
				
			||||||
 | 
					} Oregon_V1DecoderStep;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SubGhzProtocolDecoder ws_protocol_oregon_v1_decoder = {
 | 
				
			||||||
 | 
					    .alloc = ws_protocol_decoder_oregon_v1_alloc,
 | 
				
			||||||
 | 
					    .free = ws_protocol_decoder_oregon_v1_free,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .feed = ws_protocol_decoder_oregon_v1_feed,
 | 
				
			||||||
 | 
					    .reset = ws_protocol_decoder_oregon_v1_reset,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .get_hash_data = ws_protocol_decoder_oregon_v1_get_hash_data,
 | 
				
			||||||
 | 
					    .serialize = ws_protocol_decoder_oregon_v1_serialize,
 | 
				
			||||||
 | 
					    .deserialize = ws_protocol_decoder_oregon_v1_deserialize,
 | 
				
			||||||
 | 
					    .get_string = ws_protocol_decoder_oregon_v1_get_string,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SubGhzProtocolEncoder ws_protocol_oregon_v1_encoder = {
 | 
				
			||||||
 | 
					    .alloc = NULL,
 | 
				
			||||||
 | 
					    .free = NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .deserialize = NULL,
 | 
				
			||||||
 | 
					    .stop = NULL,
 | 
				
			||||||
 | 
					    .yield = NULL,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SubGhzProtocol ws_protocol_oregon_v1 = {
 | 
				
			||||||
 | 
					    .name = WS_PROTOCOL_OREGON_V1_NAME,
 | 
				
			||||||
 | 
					    .type = SubGhzProtocolWeatherStation,
 | 
				
			||||||
 | 
					    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
 | 
				
			||||||
 | 
					            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .decoder = &ws_protocol_oregon_v1_decoder,
 | 
				
			||||||
 | 
					    .encoder = &ws_protocol_oregon_v1_encoder,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void* ws_protocol_decoder_oregon_v1_alloc(SubGhzEnvironment* environment) {
 | 
				
			||||||
 | 
					    UNUSED(environment);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = malloc(sizeof(WSProtocolDecoderOregon_V1));
 | 
				
			||||||
 | 
					    instance->base.protocol = &ws_protocol_oregon_v1;
 | 
				
			||||||
 | 
					    instance->generic.protocol_name = instance->base.protocol->name;
 | 
				
			||||||
 | 
					    return instance;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_free(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    free(instance);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_reset(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    instance->decoder.parser_step = Oregon_V1DecoderStepReset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool ws_protocol_oregon_v1_check(WSProtocolDecoderOregon_V1* instance) {
 | 
				
			||||||
 | 
					    if(!instance->decoder.decode_data) return false;
 | 
				
			||||||
 | 
					    uint64_t data = subghz_protocol_blocks_reverse_key(instance->decoder.decode_data, 32);
 | 
				
			||||||
 | 
					    uint16_t crc = (data & 0xff) + ((data >> 8) & 0xff) + ((data >> 16) & 0xff);
 | 
				
			||||||
 | 
					    crc = (crc & 0xff) + ((crc >> 8) & 0xff);
 | 
				
			||||||
 | 
					    return (crc == ((data >> 24) & 0xFF));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Analysis of received data
 | 
				
			||||||
 | 
					 * @param instance Pointer to a WSBlockGeneric* instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					static void ws_protocol_oregon_v1_remote_controller(WSBlockGeneric* instance) {
 | 
				
			||||||
 | 
					    uint64_t data = subghz_protocol_blocks_reverse_key(instance->data, 32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    instance->id = data & 0xFF;
 | 
				
			||||||
 | 
					    instance->channel = ((data >> 6) & 0x03) + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    float temp_raw =
 | 
				
			||||||
 | 
					        ((data >> 8) & 0x0F) * 0.1f + ((data >> 12) & 0x0F) + ((data >> 16) & 0x0F) * 10.0f;
 | 
				
			||||||
 | 
					    if(!((data >> 21) & 1)) {
 | 
				
			||||||
 | 
					        instance->temp = temp_raw;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        instance->temp = -temp_raw;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    instance->battery_low = !(instance->data >> 23) & 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    instance->btn = WS_NO_BTN;
 | 
				
			||||||
 | 
					    instance->humidity = WS_NO_HUMIDITY;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_feed(void* context, bool level, uint32_t duration) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    ManchesterEvent event = ManchesterEventReset;
 | 
				
			||||||
 | 
					    switch(instance->decoder.parser_step) {
 | 
				
			||||||
 | 
					    case Oregon_V1DecoderStepReset:
 | 
				
			||||||
 | 
					        if((level) && (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					                       ws_protocol_oregon_v1_const.te_delta)) {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = Oregon_V1DecoderStepFoundPreamble;
 | 
				
			||||||
 | 
					            instance->decoder.te_last = duration;
 | 
				
			||||||
 | 
					            instance->header_count = 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    case Oregon_V1DecoderStepFoundPreamble:
 | 
				
			||||||
 | 
					        if(level) {
 | 
				
			||||||
 | 
					            //keep high levels, if they suit our durations
 | 
				
			||||||
 | 
					            if((DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					                ws_protocol_oregon_v1_const.te_delta) ||
 | 
				
			||||||
 | 
					               (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short * 4) <
 | 
				
			||||||
 | 
					                ws_protocol_oregon_v1_const.te_delta)) {
 | 
				
			||||||
 | 
					                instance->decoder.te_last = duration;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = Oregon_V1DecoderStepReset;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if(
 | 
				
			||||||
 | 
					            //checking low levels
 | 
				
			||||||
 | 
					            (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					             ws_protocol_oregon_v1_const.te_delta) &&
 | 
				
			||||||
 | 
					            (DURATION_DIFF(instance->decoder.te_last, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					             ws_protocol_oregon_v1_const.te_delta)) {
 | 
				
			||||||
 | 
					            // Found header
 | 
				
			||||||
 | 
					            instance->header_count++;
 | 
				
			||||||
 | 
					        } else if(
 | 
				
			||||||
 | 
					            (DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short * 3) <
 | 
				
			||||||
 | 
					             ws_protocol_oregon_v1_const.te_delta) &&
 | 
				
			||||||
 | 
					            (DURATION_DIFF(instance->decoder.te_last, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					             ws_protocol_oregon_v1_const.te_delta)) {
 | 
				
			||||||
 | 
					            // check header
 | 
				
			||||||
 | 
					            if(instance->header_count > 7) {
 | 
				
			||||||
 | 
					                instance->header_count = OREGON_V1_HEADER_OK;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if(
 | 
				
			||||||
 | 
					            (instance->header_count == OREGON_V1_HEADER_OK) &&
 | 
				
			||||||
 | 
					            (DURATION_DIFF(instance->decoder.te_last, ws_protocol_oregon_v1_const.te_short * 4) <
 | 
				
			||||||
 | 
					             ws_protocol_oregon_v1_const.te_delta)) {
 | 
				
			||||||
 | 
					            //found all the necessary patterns
 | 
				
			||||||
 | 
					            instance->decoder.decode_data = 0;
 | 
				
			||||||
 | 
					            instance->decoder.decode_count_bit = 1;
 | 
				
			||||||
 | 
					            manchester_advance(
 | 
				
			||||||
 | 
					                instance->manchester_state,
 | 
				
			||||||
 | 
					                ManchesterEventReset,
 | 
				
			||||||
 | 
					                &instance->manchester_state,
 | 
				
			||||||
 | 
					                NULL);
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = Oregon_V1DecoderStepParse;
 | 
				
			||||||
 | 
					            if(duration < ws_protocol_oregon_v1_const.te_short * 4) {
 | 
				
			||||||
 | 
					                instance->first_bit = 1;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                instance->first_bit = 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = Oregon_V1DecoderStepReset;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    case Oregon_V1DecoderStepParse:
 | 
				
			||||||
 | 
					        if(level) {
 | 
				
			||||||
 | 
					            if(DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					               ws_protocol_oregon_v1_const.te_delta) {
 | 
				
			||||||
 | 
					                event = ManchesterEventShortHigh;
 | 
				
			||||||
 | 
					            } else if(
 | 
				
			||||||
 | 
					                DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_long) <
 | 
				
			||||||
 | 
					                ws_protocol_oregon_v1_const.te_delta) {
 | 
				
			||||||
 | 
					                event = ManchesterEventLongHigh;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = Oregon_V1DecoderStepReset;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if(DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_short) <
 | 
				
			||||||
 | 
					               ws_protocol_oregon_v1_const.te_delta) {
 | 
				
			||||||
 | 
					                event = ManchesterEventShortLow;
 | 
				
			||||||
 | 
					            } else if(
 | 
				
			||||||
 | 
					                DURATION_DIFF(duration, ws_protocol_oregon_v1_const.te_long) <
 | 
				
			||||||
 | 
					                ws_protocol_oregon_v1_const.te_delta) {
 | 
				
			||||||
 | 
					                event = ManchesterEventLongLow;
 | 
				
			||||||
 | 
					            } else if(duration >= ((uint32_t)ws_protocol_oregon_v1_const.te_long * 2)) {
 | 
				
			||||||
 | 
					                if(instance->decoder.decode_count_bit ==
 | 
				
			||||||
 | 
					                   ws_protocol_oregon_v1_const.min_count_bit_for_found) {
 | 
				
			||||||
 | 
					                    if(instance->first_bit) {
 | 
				
			||||||
 | 
					                        instance->decoder.decode_data = ~instance->decoder.decode_data | (1 << 31);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    if(ws_protocol_oregon_v1_check(instance)) {
 | 
				
			||||||
 | 
					                        instance->generic.data = instance->decoder.decode_data;
 | 
				
			||||||
 | 
					                        instance->generic.data_count_bit = instance->decoder.decode_count_bit;
 | 
				
			||||||
 | 
					                        ws_protocol_oregon_v1_remote_controller(&instance->generic);
 | 
				
			||||||
 | 
					                        if(instance->base.callback)
 | 
				
			||||||
 | 
					                            instance->base.callback(&instance->base, instance->base.context);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                instance->decoder.decode_data = 0;
 | 
				
			||||||
 | 
					                instance->decoder.decode_count_bit = 0;
 | 
				
			||||||
 | 
					                manchester_advance(
 | 
				
			||||||
 | 
					                    instance->manchester_state,
 | 
				
			||||||
 | 
					                    ManchesterEventReset,
 | 
				
			||||||
 | 
					                    &instance->manchester_state,
 | 
				
			||||||
 | 
					                    NULL);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = Oregon_V1DecoderStepReset;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(event != ManchesterEventReset) {
 | 
				
			||||||
 | 
					            bool data;
 | 
				
			||||||
 | 
					            bool data_ok = manchester_advance(
 | 
				
			||||||
 | 
					                instance->manchester_state, event, &instance->manchester_state, &data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(data_ok) {
 | 
				
			||||||
 | 
					                instance->decoder.decode_data = (instance->decoder.decode_data << 1) | !data;
 | 
				
			||||||
 | 
					                instance->decoder.decode_count_bit++;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint8_t ws_protocol_decoder_oregon_v1_get_hash_data(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    return subghz_protocol_blocks_get_hash_data(
 | 
				
			||||||
 | 
					        &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_oregon_v1_serialize(
 | 
				
			||||||
 | 
					    void* context,
 | 
				
			||||||
 | 
					    FlipperFormat* flipper_format,
 | 
				
			||||||
 | 
					    SubGhzRadioPreset* preset) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    return ws_block_generic_serialize(&instance->generic, flipper_format, preset);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_oregon_v1_deserialize(void* context, FlipperFormat* flipper_format) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    bool ret = false;
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					        if(!ws_block_generic_deserialize(&instance->generic, flipper_format)) {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(instance->generic.data_count_bit !=
 | 
				
			||||||
 | 
					           ws_protocol_oregon_v1_const.min_count_bit_for_found) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Wrong number of bits in key");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ret = true;
 | 
				
			||||||
 | 
					    } while(false);
 | 
				
			||||||
 | 
					    return ret;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_get_string(void* context, FuriString* output) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderOregon_V1* instance = context;
 | 
				
			||||||
 | 
					    furi_string_printf(
 | 
				
			||||||
 | 
					        output,
 | 
				
			||||||
 | 
					        "%s %dbit\r\n"
 | 
				
			||||||
 | 
					        "Key:0x%lX%08lX\r\n"
 | 
				
			||||||
 | 
					        "Sn:0x%lX Ch:%d  Bat:%d\r\n"
 | 
				
			||||||
 | 
					        "Temp:%3.1f C Hum:%d%%",
 | 
				
			||||||
 | 
					        instance->generic.protocol_name,
 | 
				
			||||||
 | 
					        instance->generic.data_count_bit,
 | 
				
			||||||
 | 
					        (uint32_t)(instance->generic.data >> 32),
 | 
				
			||||||
 | 
					        (uint32_t)(instance->generic.data),
 | 
				
			||||||
 | 
					        instance->generic.id,
 | 
				
			||||||
 | 
					        instance->generic.channel,
 | 
				
			||||||
 | 
					        instance->generic.battery_low,
 | 
				
			||||||
 | 
					        (double)instance->generic.temp,
 | 
				
			||||||
 | 
					        instance->generic.humidity);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										79
									
								
								applications/plugins/weather_station/protocols/oregon_v1.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <lib/subghz/protocols/base.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/const.h>
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/decoder.h>
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/encoder.h>
 | 
				
			||||||
 | 
					#include "ws_generic.h"
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/math.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define WS_PROTOCOL_OREGON_V1_NAME "Oregon-v1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct WSProtocolDecoderOregon_V1 WSProtocolDecoderOregon_V1;
 | 
				
			||||||
 | 
					typedef struct WSProtocolEncoderOregon_V1 WSProtocolEncoderOregon_V1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern const SubGhzProtocolDecoder ws_protocol_oregon_v1_decoder;
 | 
				
			||||||
 | 
					extern const SubGhzProtocolEncoder ws_protocol_oregon_v1_encoder;
 | 
				
			||||||
 | 
					extern const SubGhzProtocol ws_protocol_oregon_v1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Allocate WSProtocolDecoderOregon_V1.
 | 
				
			||||||
 | 
					 * @param environment Pointer to a SubGhzEnvironment instance
 | 
				
			||||||
 | 
					 * @return WSProtocolDecoderOregon_V1* pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void* ws_protocol_decoder_oregon_v1_alloc(SubGhzEnvironment* environment);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Free WSProtocolDecoderOregon_V1.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_free(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Reset decoder WSProtocolDecoderOregon_V1.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_reset(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Parse a raw sequence of levels and durations received from the air.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 * @param level Signal level true-high false-low
 | 
				
			||||||
 | 
					 * @param duration Duration of this level in, us
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_feed(void* context, bool level, uint32_t duration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Getting the hash sum of the last randomly received parcel.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 * @return hash Hash sum
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					uint8_t ws_protocol_decoder_oregon_v1_get_hash_data(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Serialize data WSProtocolDecoderOregon_V1.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 * @param flipper_format Pointer to a FlipperFormat instance
 | 
				
			||||||
 | 
					 * @param preset The modulation on which the signal was received, SubGhzRadioPreset
 | 
				
			||||||
 | 
					 * @return true On success
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_oregon_v1_serialize(
 | 
				
			||||||
 | 
					    void* context,
 | 
				
			||||||
 | 
					    FlipperFormat* flipper_format,
 | 
				
			||||||
 | 
					    SubGhzRadioPreset* preset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Deserialize data WSProtocolDecoderOregon_V1.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 * @param flipper_format Pointer to a FlipperFormat instance
 | 
				
			||||||
 | 
					 * @return true On success
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_oregon_v1_deserialize(void* context, FlipperFormat* flipper_format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Getting a textual representation of the received data.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderOregon_V1 instance
 | 
				
			||||||
 | 
					 * @param output Resulting text
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_oregon_v1_get_string(void* context, FuriString* output);
 | 
				
			||||||
@ -13,6 +13,8 @@ const SubGhzProtocol* weather_station_protocol_registry_items[] = {
 | 
				
			|||||||
    &ws_protocol_acurite_592txr,
 | 
					    &ws_protocol_acurite_592txr,
 | 
				
			||||||
    &ws_protocol_ambient_weather,
 | 
					    &ws_protocol_ambient_weather,
 | 
				
			||||||
    &ws_protocol_auriol_th,
 | 
					    &ws_protocol_auriol_th,
 | 
				
			||||||
 | 
					    &ws_protocol_oregon_v1,
 | 
				
			||||||
 | 
					    &ws_protocol_tx_8300,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SubGhzProtocolRegistry weather_station_protocol_registry = {
 | 
					const SubGhzProtocolRegistry weather_station_protocol_registry = {
 | 
				
			||||||
 | 
				
			|||||||
@ -13,5 +13,7 @@
 | 
				
			|||||||
#include "acurite_592txr.h"
 | 
					#include "acurite_592txr.h"
 | 
				
			||||||
#include "ambient_weather.h"
 | 
					#include "ambient_weather.h"
 | 
				
			||||||
#include "auriol_hg0601a.h"
 | 
					#include "auriol_hg0601a.h"
 | 
				
			||||||
 | 
					#include "oregon_v1.h"
 | 
				
			||||||
 | 
					#include "tx_8300.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
extern const SubGhzProtocolRegistry weather_station_protocol_registry;
 | 
					extern const SubGhzProtocolRegistry weather_station_protocol_registry;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										293
									
								
								applications/plugins/weather_station/protocols/tx_8300.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,293 @@
 | 
				
			|||||||
 | 
					#include "tx_8300.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define TAG "WSProtocolTX_8300"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Help
 | 
				
			||||||
 | 
					 * https://github.com/merbanan/rtl_433/blob/master/src/devices/ambientweather_tx8300.c
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Ambient Weather TX-8300 (also sold as TFA 30.3211.02).
 | 
				
			||||||
 | 
					 * 1970us pulse with variable gap (third pulse 3920 us).
 | 
				
			||||||
 | 
					 * Above 79% humidity, gap after third pulse is 5848 us.
 | 
				
			||||||
 | 
					 * - Bit 1 : 1970us pulse with 3888 us gap
 | 
				
			||||||
 | 
					 * - Bit 0 : 1970us pulse with 1936 us gap
 | 
				
			||||||
 | 
					 * 74 bit (2 bit preamble and 72 bit data => 9 bytes => 18 nibbles)
 | 
				
			||||||
 | 
					 * The preamble seems to be a repeat counter (00, and 01 seen),
 | 
				
			||||||
 | 
					 * the first 4 bytes are data,
 | 
				
			||||||
 | 
					 * the second 4 bytes the same data inverted,
 | 
				
			||||||
 | 
					 * the last byte is a checksum.
 | 
				
			||||||
 | 
					 * Preamble format (2 bits):
 | 
				
			||||||
 | 
					 *     [1 bit (0)] [1 bit rolling count]
 | 
				
			||||||
 | 
					 * Payload format (32 bits):
 | 
				
			||||||
 | 
					 *     HHHHhhhh ??CCNIII IIIITTTT ttttuuuu
 | 
				
			||||||
 | 
					 * - H = First BCD digit humidity (the MSB might be distorted by the demod)
 | 
				
			||||||
 | 
					 * - h = Second BCD digit humidity, invalid humidity seems to be 0x0e
 | 
				
			||||||
 | 
					 * - ? = Likely battery flag, 2 bits
 | 
				
			||||||
 | 
					 * - C = Channel, 2 bits
 | 
				
			||||||
 | 
					 * - N = Negative temperature sign bit
 | 
				
			||||||
 | 
					 * - I = ID, 7-bit
 | 
				
			||||||
 | 
					 * - T = First BCD digit temperature
 | 
				
			||||||
 | 
					 * - t = Second BCD digit temperature
 | 
				
			||||||
 | 
					 * - u = Third BCD digit temperature
 | 
				
			||||||
 | 
					 * The Checksum seems to covers the 4 data bytes and is something like Fletcher-8.
 | 
				
			||||||
 | 
					 **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define TX_8300_PACKAGE_SIZE 32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const SubGhzBlockConst ws_protocol_tx_8300_const = {
 | 
				
			||||||
 | 
					    .te_short = 1940,
 | 
				
			||||||
 | 
					    .te_long = 3880,
 | 
				
			||||||
 | 
					    .te_delta = 250,
 | 
				
			||||||
 | 
					    .min_count_bit_for_found = 72,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct WSProtocolDecoderTX_8300 {
 | 
				
			||||||
 | 
					    SubGhzProtocolDecoderBase base;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SubGhzBlockDecoder decoder;
 | 
				
			||||||
 | 
					    WSBlockGeneric generic;
 | 
				
			||||||
 | 
					    uint32_t package_1;
 | 
				
			||||||
 | 
					    uint32_t package_2;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct WSProtocolEncoderTX_8300 {
 | 
				
			||||||
 | 
					    SubGhzProtocolEncoderBase base;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SubGhzProtocolBlockEncoder encoder;
 | 
				
			||||||
 | 
					    WSBlockGeneric generic;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    TX_8300DecoderStepReset = 0,
 | 
				
			||||||
 | 
					    TX_8300DecoderStepCheckPreambule,
 | 
				
			||||||
 | 
					    TX_8300DecoderStepSaveDuration,
 | 
				
			||||||
 | 
					    TX_8300DecoderStepCheckDuration,
 | 
				
			||||||
 | 
					} TX_8300DecoderStep;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SubGhzProtocolDecoder ws_protocol_tx_8300_decoder = {
 | 
				
			||||||
 | 
					    .alloc = ws_protocol_decoder_tx_8300_alloc,
 | 
				
			||||||
 | 
					    .free = ws_protocol_decoder_tx_8300_free,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .feed = ws_protocol_decoder_tx_8300_feed,
 | 
				
			||||||
 | 
					    .reset = ws_protocol_decoder_tx_8300_reset,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .get_hash_data = ws_protocol_decoder_tx_8300_get_hash_data,
 | 
				
			||||||
 | 
					    .serialize = ws_protocol_decoder_tx_8300_serialize,
 | 
				
			||||||
 | 
					    .deserialize = ws_protocol_decoder_tx_8300_deserialize,
 | 
				
			||||||
 | 
					    .get_string = ws_protocol_decoder_tx_8300_get_string,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SubGhzProtocolEncoder ws_protocol_tx_8300_encoder = {
 | 
				
			||||||
 | 
					    .alloc = NULL,
 | 
				
			||||||
 | 
					    .free = NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .deserialize = NULL,
 | 
				
			||||||
 | 
					    .stop = NULL,
 | 
				
			||||||
 | 
					    .yield = NULL,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SubGhzProtocol ws_protocol_tx_8300 = {
 | 
				
			||||||
 | 
					    .name = WS_PROTOCOL_TX_8300_NAME,
 | 
				
			||||||
 | 
					    .type = SubGhzProtocolWeatherStation,
 | 
				
			||||||
 | 
					    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
 | 
				
			||||||
 | 
					            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .decoder = &ws_protocol_tx_8300_decoder,
 | 
				
			||||||
 | 
					    .encoder = &ws_protocol_tx_8300_encoder,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void* ws_protocol_decoder_tx_8300_alloc(SubGhzEnvironment* environment) {
 | 
				
			||||||
 | 
					    UNUSED(environment);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = malloc(sizeof(WSProtocolDecoderTX_8300));
 | 
				
			||||||
 | 
					    instance->base.protocol = &ws_protocol_tx_8300;
 | 
				
			||||||
 | 
					    instance->generic.protocol_name = instance->base.protocol->name;
 | 
				
			||||||
 | 
					    return instance;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_free(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					    free(instance);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_reset(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					    instance->decoder.parser_step = TX_8300DecoderStepReset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool ws_protocol_tx_8300_check_crc(WSProtocolDecoderTX_8300* instance) {
 | 
				
			||||||
 | 
					    if(!instance->package_2) return false;
 | 
				
			||||||
 | 
					    if(instance->package_1 != ~instance->package_2) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uint16_t x = 0;
 | 
				
			||||||
 | 
					    uint16_t y = 0;
 | 
				
			||||||
 | 
					    for(int i = 0; i < 32; i += 4) {
 | 
				
			||||||
 | 
					        x += (instance->package_1 >> i) & 0x0F;
 | 
				
			||||||
 | 
					        y += (instance->package_1 >> i) & 0x05;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    uint8_t crc = (~x & 0xF) << 4 | (~y & 0xF);
 | 
				
			||||||
 | 
					    return (crc == ((instance->decoder.decode_data) & 0xFF));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Analysis of received data
 | 
				
			||||||
 | 
					 * @param instance Pointer to a WSBlockGeneric* instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					static void ws_protocol_tx_8300_remote_controller(WSBlockGeneric* instance) {
 | 
				
			||||||
 | 
					    instance->humidity = (((instance->data >> 28) & 0x0F) * 10) + ((instance->data >> 24) & 0x0F);
 | 
				
			||||||
 | 
					    instance->btn = WS_NO_BTN;
 | 
				
			||||||
 | 
					    if(!((instance->data >> 22) & 0x03))
 | 
				
			||||||
 | 
					        instance->battery_low = 0;
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					        instance->battery_low = 1;
 | 
				
			||||||
 | 
					    instance->channel = (instance->data >> 20) & 0x03;
 | 
				
			||||||
 | 
					    instance->id = (instance->data >> 12) & 0x7F;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    float temp_raw = ((instance->data >> 8) & 0x0F) * 10.0f + ((instance->data >> 4) & 0x0F) +
 | 
				
			||||||
 | 
					                     (instance->data & 0x0F) * 0.1f;
 | 
				
			||||||
 | 
					    if(!((instance->data >> 19) & 1)) {
 | 
				
			||||||
 | 
					        instance->temp = temp_raw;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        instance->temp = -temp_raw;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_feed(void* context, bool level, uint32_t duration) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch(instance->decoder.parser_step) {
 | 
				
			||||||
 | 
					    case TX_8300DecoderStepReset:
 | 
				
			||||||
 | 
					        if((level) && (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short * 2) <
 | 
				
			||||||
 | 
					                       ws_protocol_tx_8300_const.te_delta)) {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = TX_8300DecoderStepCheckPreambule;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case TX_8300DecoderStepCheckPreambule:
 | 
				
			||||||
 | 
					        if((!level) && ((DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short * 2) <
 | 
				
			||||||
 | 
					                         ws_protocol_tx_8300_const.te_delta) ||
 | 
				
			||||||
 | 
					                        (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short * 3) <
 | 
				
			||||||
 | 
					                         ws_protocol_tx_8300_const.te_delta))) {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = TX_8300DecoderStepSaveDuration;
 | 
				
			||||||
 | 
					            instance->decoder.decode_data = 0;
 | 
				
			||||||
 | 
					            instance->decoder.decode_count_bit = 1;
 | 
				
			||||||
 | 
					            instance->package_1 = 0;
 | 
				
			||||||
 | 
					            instance->package_2 = 0;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = TX_8300DecoderStepReset;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case TX_8300DecoderStepSaveDuration:
 | 
				
			||||||
 | 
					        if(level) {
 | 
				
			||||||
 | 
					            instance->decoder.te_last = duration;
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = TX_8300DecoderStepCheckDuration;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = TX_8300DecoderStepReset;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case TX_8300DecoderStepCheckDuration:
 | 
				
			||||||
 | 
					        if(!level) {
 | 
				
			||||||
 | 
					            if(duration >= ((uint32_t)ws_protocol_tx_8300_const.te_short * 5)) {
 | 
				
			||||||
 | 
					                //Found syncPostfix
 | 
				
			||||||
 | 
					                if((instance->decoder.decode_count_bit ==
 | 
				
			||||||
 | 
					                    ws_protocol_tx_8300_const.min_count_bit_for_found) &&
 | 
				
			||||||
 | 
					                   ws_protocol_tx_8300_check_crc(instance)) {
 | 
				
			||||||
 | 
					                    instance->generic.data = instance->package_1;
 | 
				
			||||||
 | 
					                    instance->generic.data_count_bit = instance->decoder.decode_count_bit;
 | 
				
			||||||
 | 
					                    ws_protocol_tx_8300_remote_controller(&instance->generic);
 | 
				
			||||||
 | 
					                    if(instance->base.callback)
 | 
				
			||||||
 | 
					                        instance->base.callback(&instance->base, instance->base.context);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                instance->decoder.decode_data = 0;
 | 
				
			||||||
 | 
					                instance->decoder.decode_count_bit = 1;
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = TX_8300DecoderStepReset;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            } else if(
 | 
				
			||||||
 | 
					                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_tx_8300_const.te_short) <
 | 
				
			||||||
 | 
					                 ws_protocol_tx_8300_const.te_delta) &&
 | 
				
			||||||
 | 
					                (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_long) <
 | 
				
			||||||
 | 
					                 ws_protocol_tx_8300_const.te_delta * 2)) {
 | 
				
			||||||
 | 
					                subghz_protocol_blocks_add_bit(&instance->decoder, 1);
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = TX_8300DecoderStepSaveDuration;
 | 
				
			||||||
 | 
					            } else if(
 | 
				
			||||||
 | 
					                (DURATION_DIFF(instance->decoder.te_last, ws_protocol_tx_8300_const.te_short) <
 | 
				
			||||||
 | 
					                 ws_protocol_tx_8300_const.te_delta) &&
 | 
				
			||||||
 | 
					                (DURATION_DIFF(duration, ws_protocol_tx_8300_const.te_short) <
 | 
				
			||||||
 | 
					                 ws_protocol_tx_8300_const.te_delta)) {
 | 
				
			||||||
 | 
					                subghz_protocol_blocks_add_bit(&instance->decoder, 0);
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = TX_8300DecoderStepSaveDuration;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                instance->decoder.parser_step = TX_8300DecoderStepReset;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(instance->decoder.decode_count_bit == TX_8300_PACKAGE_SIZE) {
 | 
				
			||||||
 | 
					                instance->package_1 = instance->decoder.decode_data;
 | 
				
			||||||
 | 
					                instance->decoder.decode_data = 0;
 | 
				
			||||||
 | 
					            } else if(instance->decoder.decode_count_bit == TX_8300_PACKAGE_SIZE * 2) {
 | 
				
			||||||
 | 
					                instance->package_2 = instance->decoder.decode_data;
 | 
				
			||||||
 | 
					                instance->decoder.decode_data = 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            instance->decoder.parser_step = TX_8300DecoderStepReset;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint8_t ws_protocol_decoder_tx_8300_get_hash_data(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					    return subghz_protocol_blocks_get_hash_data(
 | 
				
			||||||
 | 
					        &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_tx_8300_serialize(
 | 
				
			||||||
 | 
					    void* context,
 | 
				
			||||||
 | 
					    FlipperFormat* flipper_format,
 | 
				
			||||||
 | 
					    SubGhzRadioPreset* preset) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					    return ws_block_generic_serialize(&instance->generic, flipper_format, preset);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_tx_8300_deserialize(void* context, FlipperFormat* flipper_format) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					    bool ret = false;
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					        if(!ws_block_generic_deserialize(&instance->generic, flipper_format)) {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(instance->generic.data_count_bit != ws_protocol_tx_8300_const.min_count_bit_for_found) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Wrong number of bits in key");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ret = true;
 | 
				
			||||||
 | 
					    } while(false);
 | 
				
			||||||
 | 
					    return ret;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_get_string(void* context, FuriString* output) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSProtocolDecoderTX_8300* instance = context;
 | 
				
			||||||
 | 
					    furi_string_printf(
 | 
				
			||||||
 | 
					        output,
 | 
				
			||||||
 | 
					        "%s %dbit\r\n"
 | 
				
			||||||
 | 
					        "Key:0x%lX%08lX\r\n"
 | 
				
			||||||
 | 
					        "Sn:0x%lX Ch:%d  Bat:%d\r\n"
 | 
				
			||||||
 | 
					        "Temp:%3.1f C Hum:%d%%",
 | 
				
			||||||
 | 
					        instance->generic.protocol_name,
 | 
				
			||||||
 | 
					        instance->generic.data_count_bit,
 | 
				
			||||||
 | 
					        (uint32_t)(instance->generic.data >> 32),
 | 
				
			||||||
 | 
					        (uint32_t)(instance->generic.data),
 | 
				
			||||||
 | 
					        instance->generic.id,
 | 
				
			||||||
 | 
					        instance->generic.channel,
 | 
				
			||||||
 | 
					        instance->generic.battery_low,
 | 
				
			||||||
 | 
					        (double)instance->generic.temp,
 | 
				
			||||||
 | 
					        instance->generic.humidity);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										79
									
								
								applications/plugins/weather_station/protocols/tx_8300.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <lib/subghz/protocols/base.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/const.h>
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/decoder.h>
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/encoder.h>
 | 
				
			||||||
 | 
					#include "ws_generic.h"
 | 
				
			||||||
 | 
					#include <lib/subghz/blocks/math.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define WS_PROTOCOL_TX_8300_NAME "TX8300"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct WSProtocolDecoderTX_8300 WSProtocolDecoderTX_8300;
 | 
				
			||||||
 | 
					typedef struct WSProtocolEncoderTX_8300 WSProtocolEncoderTX_8300;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern const SubGhzProtocolDecoder ws_protocol_tx_8300_decoder;
 | 
				
			||||||
 | 
					extern const SubGhzProtocolEncoder ws_protocol_tx_8300_encoder;
 | 
				
			||||||
 | 
					extern const SubGhzProtocol ws_protocol_tx_8300;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Allocate WSProtocolDecoderTX_8300.
 | 
				
			||||||
 | 
					 * @param environment Pointer to a SubGhzEnvironment instance
 | 
				
			||||||
 | 
					 * @return WSProtocolDecoderTX_8300* pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void* ws_protocol_decoder_tx_8300_alloc(SubGhzEnvironment* environment);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Free WSProtocolDecoderTX_8300.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_free(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Reset decoder WSProtocolDecoderTX_8300.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_reset(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Parse a raw sequence of levels and durations received from the air.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 * @param level Signal level true-high false-low
 | 
				
			||||||
 | 
					 * @param duration Duration of this level in, us
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_feed(void* context, bool level, uint32_t duration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Getting the hash sum of the last randomly received parcel.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 * @return hash Hash sum
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					uint8_t ws_protocol_decoder_tx_8300_get_hash_data(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Serialize data WSProtocolDecoderTX_8300.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 * @param flipper_format Pointer to a FlipperFormat instance
 | 
				
			||||||
 | 
					 * @param preset The modulation on which the signal was received, SubGhzRadioPreset
 | 
				
			||||||
 | 
					 * @return true On success
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_tx_8300_serialize(
 | 
				
			||||||
 | 
					    void* context,
 | 
				
			||||||
 | 
					    FlipperFormat* flipper_format,
 | 
				
			||||||
 | 
					    SubGhzRadioPreset* preset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Deserialize data WSProtocolDecoderTX_8300.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 * @param flipper_format Pointer to a FlipperFormat instance
 | 
				
			||||||
 | 
					 * @return true On success
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					bool ws_protocol_decoder_tx_8300_deserialize(void* context, FlipperFormat* flipper_format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Getting a textual representation of the received data.
 | 
				
			||||||
 | 
					 * @param context Pointer to a WSProtocolDecoderTX_8300 instance
 | 
				
			||||||
 | 
					 * @param output Resulting text
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void ws_protocol_decoder_tx_8300_get_string(void* context, FuriString* output);
 | 
				
			||||||
@ -99,6 +99,17 @@ bool ws_block_generic_serialize(
 | 
				
			|||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //DATE AGE set
 | 
				
			||||||
 | 
					        FuriHalRtcDateTime curr_dt;
 | 
				
			||||||
 | 
					        furi_hal_rtc_get_datetime(&curr_dt);
 | 
				
			||||||
 | 
					        uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        temp_data = curr_ts;
 | 
				
			||||||
 | 
					        if(!flipper_format_write_uint32(flipper_format, "Ts", &temp_data, 1)) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Unable to add timestamp");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        temp_data = instance->channel;
 | 
					        temp_data = instance->channel;
 | 
				
			||||||
        if(!flipper_format_write_uint32(flipper_format, "Ch", &temp_data, 1)) {
 | 
					        if(!flipper_format_write_uint32(flipper_format, "Ch", &temp_data, 1)) {
 | 
				
			||||||
            FURI_LOG_E(TAG, "Unable to add Channel");
 | 
					            FURI_LOG_E(TAG, "Unable to add Channel");
 | 
				
			||||||
@ -168,6 +179,12 @@ bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipp
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        instance->humidity = (uint8_t)temp_data;
 | 
					        instance->humidity = (uint8_t)temp_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(!flipper_format_read_uint32(flipper_format, "Ts", (uint32_t*)&temp_data, 1)) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Missing timestamp");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        instance->timestamp = (uint32_t)temp_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(!flipper_format_read_uint32(flipper_format, "Ch", (uint32_t*)&temp_data, 1)) {
 | 
					        if(!flipper_format_read_uint32(flipper_format, "Ch", (uint32_t*)&temp_data, 1)) {
 | 
				
			||||||
            FURI_LOG_E(TAG, "Missing Channel");
 | 
					            FURI_LOG_E(TAG, "Missing Channel");
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
@ -192,7 +209,3 @@ bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipp
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return res;
 | 
					    return res;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
float ws_block_generic_fahrenheit_to_celsius(float fahrenheit) {
 | 
					 | 
				
			||||||
    return (fahrenheit - 32.0f) / 1.8f;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -8,6 +8,7 @@
 | 
				
			|||||||
#include "furi.h"
 | 
					#include "furi.h"
 | 
				
			||||||
#include "furi_hal.h"
 | 
					#include "furi_hal.h"
 | 
				
			||||||
#include <lib/subghz/types.h>
 | 
					#include <lib/subghz/types.h>
 | 
				
			||||||
 | 
					#include <locale/locale.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef __cplusplus
 | 
					#ifdef __cplusplus
 | 
				
			||||||
extern "C" {
 | 
					extern "C" {
 | 
				
			||||||
@ -29,6 +30,7 @@ struct WSBlockGeneric {
 | 
				
			|||||||
    uint8_t data_count_bit;
 | 
					    uint8_t data_count_bit;
 | 
				
			||||||
    uint8_t battery_low;
 | 
					    uint8_t battery_low;
 | 
				
			||||||
    uint8_t humidity;
 | 
					    uint8_t humidity;
 | 
				
			||||||
 | 
					    uint32_t timestamp;
 | 
				
			||||||
    uint8_t channel;
 | 
					    uint8_t channel;
 | 
				
			||||||
    uint8_t btn;
 | 
					    uint8_t btn;
 | 
				
			||||||
    float temp;
 | 
					    float temp;
 | 
				
			||||||
@ -61,8 +63,6 @@ bool ws_block_generic_serialize(
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipper_format);
 | 
					bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipper_format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
float ws_block_generic_fahrenheit_to_celsius(float fahrenheit);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#ifdef __cplusplus
 | 
					#ifdef __cplusplus
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
@ -8,7 +8,7 @@
 | 
				
			|||||||
#include <m-array.h>
 | 
					#include <m-array.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FRAME_HEIGHT 12
 | 
					#define FRAME_HEIGHT 12
 | 
				
			||||||
#define MAX_LEN_PX 100
 | 
					#define MAX_LEN_PX 112
 | 
				
			||||||
#define MENU_ITEMS 4u
 | 
					#define MENU_ITEMS 4u
 | 
				
			||||||
#define UNLOCK_CNT 3
 | 
					#define UNLOCK_CNT 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -189,7 +189,7 @@ void ws_view_receiver_draw(Canvas* canvas, WSReceiverModel* model) {
 | 
				
			|||||||
            canvas_set_color(canvas, ColorBlack);
 | 
					            canvas_set_color(canvas, ColorBlack);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        canvas_draw_icon(canvas, 4, 2 + i * FRAME_HEIGHT, ReceiverItemIcons[item_menu->type]);
 | 
					        canvas_draw_icon(canvas, 4, 2 + i * FRAME_HEIGHT, ReceiverItemIcons[item_menu->type]);
 | 
				
			||||||
        canvas_draw_str(canvas, 15, 9 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buff));
 | 
					        canvas_draw_str(canvas, 14, 9 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buff));
 | 
				
			||||||
        furi_string_reset(str_buff);
 | 
					        furi_string_reset(str_buff);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if(scrollbar) {
 | 
					    if(scrollbar) {
 | 
				
			||||||
 | 
				
			|||||||
@ -9,9 +9,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
struct WSReceiverInfo {
 | 
					struct WSReceiverInfo {
 | 
				
			||||||
    View* view;
 | 
					    View* view;
 | 
				
			||||||
 | 
					    FuriTimer* timer;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef struct {
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    uint32_t curr_ts;
 | 
				
			||||||
    FuriString* protocol_name;
 | 
					    FuriString* protocol_name;
 | 
				
			||||||
    WSBlockGeneric* generic;
 | 
					    WSBlockGeneric* generic;
 | 
				
			||||||
} WSReceiverInfoModel;
 | 
					} WSReceiverInfoModel;
 | 
				
			||||||
@ -28,6 +30,10 @@ void ws_view_receiver_info_update(WSReceiverInfo* ws_receiver_info, FlipperForma
 | 
				
			|||||||
            flipper_format_read_string(fff, "Protocol", model->protocol_name);
 | 
					            flipper_format_read_string(fff, "Protocol", model->protocol_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ws_block_generic_deserialize(model->generic, fff);
 | 
					            ws_block_generic_deserialize(model->generic, fff);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            FuriHalRtcDateTime curr_dt;
 | 
				
			||||||
 | 
					            furi_hal_rtc_get_datetime(&curr_dt);
 | 
				
			||||||
 | 
					            model->curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        true);
 | 
					        true);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -44,46 +50,102 @@ void ws_view_receiver_info_draw(Canvas* canvas, WSReceiverInfoModel* model) {
 | 
				
			|||||||
        "%s %db",
 | 
					        "%s %db",
 | 
				
			||||||
        furi_string_get_cstr(model->protocol_name),
 | 
					        furi_string_get_cstr(model->protocol_name),
 | 
				
			||||||
        model->generic->data_count_bit);
 | 
					        model->generic->data_count_bit);
 | 
				
			||||||
    canvas_draw_str(canvas, 5, 8, buffer);
 | 
					    canvas_draw_str(canvas, 0, 8, buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(model->generic->channel != WS_NO_CHANNEL) {
 | 
					    if(model->generic->channel != WS_NO_CHANNEL) {
 | 
				
			||||||
        snprintf(buffer, sizeof(buffer), "Ch: %01d", model->generic->channel);
 | 
					        snprintf(buffer, sizeof(buffer), "Ch: %01d", model->generic->channel);
 | 
				
			||||||
        canvas_draw_str(canvas, 105, 8, buffer);
 | 
					        canvas_draw_str(canvas, 106, 8, buffer);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(model->generic->id != WS_NO_ID) {
 | 
					    if(model->generic->id != WS_NO_ID) {
 | 
				
			||||||
        snprintf(buffer, sizeof(buffer), "Sn: 0x%02lX", model->generic->id);
 | 
					        snprintf(buffer, sizeof(buffer), "Sn: 0x%02lX", model->generic->id);
 | 
				
			||||||
        canvas_draw_str(canvas, 5, 20, buffer);
 | 
					        canvas_draw_str(canvas, 0, 20, buffer);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(model->generic->btn != WS_NO_BTN) {
 | 
					    if(model->generic->btn != WS_NO_BTN) {
 | 
				
			||||||
        snprintf(buffer, sizeof(buffer), "Btn: %01d", model->generic->btn);
 | 
					        snprintf(buffer, sizeof(buffer), "Btn: %01d", model->generic->btn);
 | 
				
			||||||
        canvas_draw_str(canvas, 62, 20, buffer);
 | 
					        canvas_draw_str(canvas, 57, 20, buffer);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(model->generic->battery_low != WS_NO_BATT) {
 | 
					    if(model->generic->battery_low != WS_NO_BATT) {
 | 
				
			||||||
        snprintf(
 | 
					        snprintf(
 | 
				
			||||||
            buffer, sizeof(buffer), "Batt: %s", (!model->generic->battery_low ? "ok" : "low"));
 | 
					            buffer, sizeof(buffer), "Batt: %s", (!model->generic->battery_low ? "ok" : "low"));
 | 
				
			||||||
        canvas_draw_str(canvas, 90, 20, buffer);
 | 
					        canvas_draw_str_aligned(canvas, 126, 17, AlignRight, AlignCenter, buffer);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    snprintf(buffer, sizeof(buffer), "Data: 0x%llX", model->generic->data);
 | 
					    snprintf(buffer, sizeof(buffer), "Data: 0x%llX", model->generic->data);
 | 
				
			||||||
    canvas_draw_str(canvas, 5, 32, buffer);
 | 
					    canvas_draw_str(canvas, 0, 32, buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    elements_bold_rounded_frame(canvas, 2, 37, 123, 25);
 | 
					    elements_bold_rounded_frame(canvas, 0, 38, 127, 25);
 | 
				
			||||||
    canvas_set_font(canvas, FontPrimary);
 | 
					    canvas_set_font(canvas, FontPrimary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(model->generic->temp != WS_NO_TEMPERATURE) {
 | 
					    if(model->generic->temp != WS_NO_TEMPERATURE) {
 | 
				
			||||||
        canvas_draw_icon(canvas, 18, 42, &I_Therm_7x16);
 | 
					        canvas_draw_icon(canvas, 6, 43, &I_Therm_7x16);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        uint8_t temp_x1 = 0;
 | 
				
			||||||
 | 
					        uint8_t temp_x2 = 0;
 | 
				
			||||||
 | 
					        if(furi_hal_rtc_get_locale_units() == FuriHalRtcLocaleUnitsMetric) {
 | 
				
			||||||
            snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp);
 | 
					            snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp);
 | 
				
			||||||
        canvas_draw_str_aligned(canvas, 63, 46, AlignRight, AlignTop, buffer);
 | 
					            if(model->generic->temp < -9.0f) {
 | 
				
			||||||
        canvas_draw_circle(canvas, 55, 45, 1);
 | 
					                temp_x1 = 49;
 | 
				
			||||||
 | 
					                temp_x2 = 40;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                temp_x1 = 47;
 | 
				
			||||||
 | 
					                temp_x2 = 38;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            snprintf(
 | 
				
			||||||
 | 
					                buffer,
 | 
				
			||||||
 | 
					                sizeof(buffer),
 | 
				
			||||||
 | 
					                "%3.1f F",
 | 
				
			||||||
 | 
					                (double)locale_celsius_to_fahrenheit(model->generic->temp));
 | 
				
			||||||
 | 
					            if((model->generic->temp < -27.77f) || (model->generic->temp > 37.77f)) {
 | 
				
			||||||
 | 
					                temp_x1 = 50;
 | 
				
			||||||
 | 
					                temp_x2 = 42;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                temp_x1 = 48;
 | 
				
			||||||
 | 
					                temp_x2 = 40;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        canvas_draw_str_aligned(canvas, temp_x1, 47, AlignRight, AlignTop, buffer);
 | 
				
			||||||
 | 
					        canvas_draw_circle(canvas, temp_x2, 46, 1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(model->generic->humidity != WS_NO_HUMIDITY) {
 | 
					    if(model->generic->humidity != WS_NO_HUMIDITY) {
 | 
				
			||||||
        canvas_draw_icon(canvas, 75, 42, &I_Humid_10x15);
 | 
					        canvas_draw_icon(canvas, 53, 44, &I_Humid_8x13);
 | 
				
			||||||
        snprintf(buffer, sizeof(buffer), "%d%%", model->generic->humidity);
 | 
					        snprintf(buffer, sizeof(buffer), "%d%%", model->generic->humidity);
 | 
				
			||||||
        canvas_draw_str(canvas, 91, 54, buffer);
 | 
					        canvas_draw_str(canvas, 64, 55, buffer);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if((int)model->generic->timestamp > 0 && model->curr_ts) {
 | 
				
			||||||
 | 
					        int ts_diff = (int)model->curr_ts - (int)model->generic->timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        canvas_draw_icon(canvas, 91, 46, &I_Timer_11x11);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(ts_diff > 60) {
 | 
				
			||||||
 | 
					            int tmp_sec = ts_diff;
 | 
				
			||||||
 | 
					            int cnt_min = 1;
 | 
				
			||||||
 | 
					            for(int i = 1; tmp_sec > 60; i++) {
 | 
				
			||||||
 | 
					                tmp_sec = tmp_sec - 60;
 | 
				
			||||||
 | 
					                cnt_min = i;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(model->curr_ts % 2 == 0) {
 | 
				
			||||||
 | 
					                canvas_draw_str_aligned(canvas, 105, 51, AlignLeft, AlignCenter, "Old");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                if(cnt_min >= 59) {
 | 
				
			||||||
 | 
					                    canvas_draw_str_aligned(canvas, 105, 51, AlignLeft, AlignCenter, "Old");
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    snprintf(buffer, sizeof(buffer), "%dm", cnt_min);
 | 
				
			||||||
 | 
					                    canvas_draw_str_aligned(canvas, 114, 51, AlignCenter, AlignCenter, buffer);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            snprintf(buffer, sizeof(buffer), "%d", ts_diff);
 | 
				
			||||||
 | 
					            canvas_draw_str_aligned(canvas, 112, 51, AlignCenter, AlignCenter, buffer);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -98,14 +160,19 @@ bool ws_view_receiver_info_input(InputEvent* event, void* context) {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void ws_view_receiver_info_enter(void* context) {
 | 
					static void ws_view_receiver_info_enter(void* context) {
 | 
				
			||||||
    furi_assert(context);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void ws_view_receiver_info_exit(void* context) {
 | 
					 | 
				
			||||||
    furi_assert(context);
 | 
					    furi_assert(context);
 | 
				
			||||||
    WSReceiverInfo* ws_receiver_info = context;
 | 
					    WSReceiverInfo* ws_receiver_info = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_start(ws_receiver_info->timer, 1000);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void ws_view_receiver_info_exit(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    WSReceiverInfo* ws_receiver_info = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_stop(ws_receiver_info->timer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_view_model(
 | 
					    with_view_model(
 | 
				
			||||||
        ws_receiver_info->view,
 | 
					        ws_receiver_info->view,
 | 
				
			||||||
        WSReceiverInfoModel * model,
 | 
					        WSReceiverInfoModel * model,
 | 
				
			||||||
@ -113,6 +180,20 @@ void ws_view_receiver_info_exit(void* context) {
 | 
				
			|||||||
        false);
 | 
					        false);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void ws_view_receiver_info_timer(void* context) {
 | 
				
			||||||
 | 
					    WSReceiverInfo* ws_receiver_info = context;
 | 
				
			||||||
 | 
					    // Force redraw
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        ws_receiver_info->view,
 | 
				
			||||||
 | 
					        WSReceiverInfoModel * model,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            FuriHalRtcDateTime curr_dt;
 | 
				
			||||||
 | 
					            furi_hal_rtc_get_datetime(&curr_dt);
 | 
				
			||||||
 | 
					            model->curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        true);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WSReceiverInfo* ws_view_receiver_info_alloc() {
 | 
					WSReceiverInfo* ws_view_receiver_info_alloc() {
 | 
				
			||||||
    WSReceiverInfo* ws_receiver_info = malloc(sizeof(WSReceiverInfo));
 | 
					    WSReceiverInfo* ws_receiver_info = malloc(sizeof(WSReceiverInfo));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -135,12 +216,17 @@ WSReceiverInfo* ws_view_receiver_info_alloc() {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        true);
 | 
					        true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ws_receiver_info->timer =
 | 
				
			||||||
 | 
					        furi_timer_alloc(ws_view_receiver_info_timer, FuriTimerTypePeriodic, ws_receiver_info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ws_receiver_info;
 | 
					    return ws_receiver_info;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void ws_view_receiver_info_free(WSReceiverInfo* ws_receiver_info) {
 | 
					void ws_view_receiver_info_free(WSReceiverInfo* ws_receiver_info) {
 | 
				
			||||||
    furi_assert(ws_receiver_info);
 | 
					    furi_assert(ws_receiver_info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_free(ws_receiver_info->timer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_view_model(
 | 
					    with_view_model(
 | 
				
			||||||
        ws_receiver_info->view,
 | 
					        ws_receiver_info->view,
 | 
				
			||||||
        WSReceiverInfoModel * model,
 | 
					        WSReceiverInfoModel * model,
 | 
				
			||||||
 | 
				
			|||||||
@ -117,6 +117,8 @@ Bt* bt_alloc() {
 | 
				
			|||||||
    if(!bt_settings_load(&bt->bt_settings)) {
 | 
					    if(!bt_settings_load(&bt->bt_settings)) {
 | 
				
			||||||
        bt_settings_save(&bt->bt_settings);
 | 
					        bt_settings_save(&bt->bt_settings);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // Keys storage
 | 
				
			||||||
 | 
					    bt->keys_storage = bt_keys_storage_alloc(BT_KEYS_STORAGE_PATH);
 | 
				
			||||||
    // Alloc queue
 | 
					    // Alloc queue
 | 
				
			||||||
    bt->message_queue = furi_message_queue_alloc(8, sizeof(BtMessage));
 | 
					    bt->message_queue = furi_message_queue_alloc(8, sizeof(BtMessage));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -285,8 +287,10 @@ static bool bt_on_gap_event_callback(GapEvent event, void* context) {
 | 
				
			|||||||
static void bt_on_key_storage_change_callback(uint8_t* addr, uint16_t size, void* context) {
 | 
					static void bt_on_key_storage_change_callback(uint8_t* addr, uint16_t size, void* context) {
 | 
				
			||||||
    furi_assert(context);
 | 
					    furi_assert(context);
 | 
				
			||||||
    Bt* bt = context;
 | 
					    Bt* bt = context;
 | 
				
			||||||
    FURI_LOG_I(TAG, "Changed addr start: %p, size changed: %d", addr, size);
 | 
					    BtMessage message = {
 | 
				
			||||||
    BtMessage message = {.type = BtMessageTypeKeysStorageUpdated};
 | 
					        .type = BtMessageTypeKeysStorageUpdated,
 | 
				
			||||||
 | 
					        .data.key_storage_data.start_address = addr,
 | 
				
			||||||
 | 
					        .data.key_storage_data.size = size};
 | 
				
			||||||
    furi_check(
 | 
					    furi_check(
 | 
				
			||||||
        furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
 | 
					        furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -331,6 +335,8 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
 | 
				
			|||||||
            furi_profile = FuriHalBtProfileSerial;
 | 
					            furi_profile = FuriHalBtProfileSerial;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bt_keys_storage_load(bt->keys_storage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(furi_hal_bt_change_app(furi_profile, bt_on_gap_event_callback, bt)) {
 | 
					        if(furi_hal_bt_change_app(furi_profile, bt_on_gap_event_callback, bt)) {
 | 
				
			||||||
            FURI_LOG_I(TAG, "Bt App started");
 | 
					            FURI_LOG_I(TAG, "Bt App started");
 | 
				
			||||||
            if(bt->bt_settings.enabled) {
 | 
					            if(bt->bt_settings.enabled) {
 | 
				
			||||||
@ -358,6 +364,7 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
static void bt_close_connection(Bt* bt) {
 | 
					static void bt_close_connection(Bt* bt) {
 | 
				
			||||||
    bt_close_rpc_connection(bt);
 | 
					    bt_close_rpc_connection(bt);
 | 
				
			||||||
 | 
					    furi_hal_bt_stop_advertising();
 | 
				
			||||||
    furi_event_flag_set(bt->api_event, BT_API_UNLOCK_EVENT);
 | 
					    furi_event_flag_set(bt->api_event, BT_API_UNLOCK_EVENT);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -372,8 +379,8 @@ int32_t bt_srv(void* p) {
 | 
				
			|||||||
        return 0;
 | 
					        return 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Read keys
 | 
					    // Load keys
 | 
				
			||||||
    if(!bt_keys_storage_load(bt)) {
 | 
					    if(!bt_keys_storage_load(bt->keys_storage)) {
 | 
				
			||||||
        FURI_LOG_W(TAG, "Failed to load bonding keys");
 | 
					        FURI_LOG_W(TAG, "Failed to load bonding keys");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -418,13 +425,16 @@ int32_t bt_srv(void* p) {
 | 
				
			|||||||
            // Display PIN code
 | 
					            // Display PIN code
 | 
				
			||||||
            bt_pin_code_show(bt, message.data.pin_code);
 | 
					            bt_pin_code_show(bt, message.data.pin_code);
 | 
				
			||||||
        } else if(message.type == BtMessageTypeKeysStorageUpdated) {
 | 
					        } else if(message.type == BtMessageTypeKeysStorageUpdated) {
 | 
				
			||||||
            bt_keys_storage_save(bt);
 | 
					            bt_keys_storage_update(
 | 
				
			||||||
 | 
					                bt->keys_storage,
 | 
				
			||||||
 | 
					                message.data.key_storage_data.start_address,
 | 
				
			||||||
 | 
					                message.data.key_storage_data.size);
 | 
				
			||||||
        } else if(message.type == BtMessageTypeSetProfile) {
 | 
					        } else if(message.type == BtMessageTypeSetProfile) {
 | 
				
			||||||
            bt_change_profile(bt, &message);
 | 
					            bt_change_profile(bt, &message);
 | 
				
			||||||
        } else if(message.type == BtMessageTypeDisconnect) {
 | 
					        } else if(message.type == BtMessageTypeDisconnect) {
 | 
				
			||||||
            bt_close_connection(bt);
 | 
					            bt_close_connection(bt);
 | 
				
			||||||
        } else if(message.type == BtMessageTypeForgetBondedDevices) {
 | 
					        } else if(message.type == BtMessageTypeForgetBondedDevices) {
 | 
				
			||||||
            bt_keys_storage_delete(bt);
 | 
					            bt_keys_storage_delete(bt->keys_storage);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
 | 
				
			|||||||
@ -56,6 +56,19 @@ void bt_set_status_changed_callback(Bt* bt, BtStatusChangedCallback callback, vo
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
void bt_forget_bonded_devices(Bt* bt);
 | 
					void bt_forget_bonded_devices(Bt* bt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Set keys storage file path
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param bt                    Bt instance
 | 
				
			||||||
 | 
					 * @param keys_storage_path     Path to file with saved keys
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void bt_keys_storage_set_storage_path(Bt* bt, const char* keys_storage_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Set default keys storage file path
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param bt                    Bt instance
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void bt_keys_storage_set_default_path(Bt* bt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef __cplusplus
 | 
					#ifdef __cplusplus
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
				
			|||||||
@ -39,3 +39,18 @@ void bt_forget_bonded_devices(Bt* bt) {
 | 
				
			|||||||
    furi_check(
 | 
					    furi_check(
 | 
				
			||||||
        furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
 | 
					        furi_message_queue_put(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_set_storage_path(Bt* bt, const char* keys_storage_path) {
 | 
				
			||||||
 | 
					    furi_assert(bt);
 | 
				
			||||||
 | 
					    furi_assert(bt->keys_storage);
 | 
				
			||||||
 | 
					    furi_assert(keys_storage_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_keys_storage_set_file_path(bt->keys_storage, keys_storage_path);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_set_default_path(Bt* bt) {
 | 
				
			||||||
 | 
					    furi_assert(bt);
 | 
				
			||||||
 | 
					    furi_assert(bt->keys_storage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bt_keys_storage_set_file_path(bt->keys_storage, BT_KEYS_STORAGE_PATH);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,8 +13,14 @@
 | 
				
			|||||||
#include <power/power_service/power.h>
 | 
					#include <power/power_service/power.h>
 | 
				
			||||||
#include <rpc/rpc.h>
 | 
					#include <rpc/rpc.h>
 | 
				
			||||||
#include <notification/notification.h>
 | 
					#include <notification/notification.h>
 | 
				
			||||||
 | 
					#include <storage/storage.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <bt/bt_settings.h>
 | 
					#include <bt/bt_settings.h>
 | 
				
			||||||
 | 
					#include <bt/bt_service/bt_keys_storage.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "bt_keys_filename.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define BT_KEYS_STORAGE_PATH INT_PATH(BT_KEYS_STORAGE_FILE_NAME)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define BT_API_UNLOCK_EVENT (1UL << 0)
 | 
					#define BT_API_UNLOCK_EVENT (1UL << 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,10 +35,16 @@ typedef enum {
 | 
				
			|||||||
    BtMessageTypeForgetBondedDevices,
 | 
					    BtMessageTypeForgetBondedDevices,
 | 
				
			||||||
} BtMessageType;
 | 
					} BtMessageType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
					    uint8_t* start_address;
 | 
				
			||||||
 | 
					    uint16_t size;
 | 
				
			||||||
 | 
					} BtKeyStorageUpdateData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef union {
 | 
					typedef union {
 | 
				
			||||||
    uint32_t pin_code;
 | 
					    uint32_t pin_code;
 | 
				
			||||||
    uint8_t battery_level;
 | 
					    uint8_t battery_level;
 | 
				
			||||||
    BtProfile profile;
 | 
					    BtProfile profile;
 | 
				
			||||||
 | 
					    BtKeyStorageUpdateData key_storage_data;
 | 
				
			||||||
} BtMessageData;
 | 
					} BtMessageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef struct {
 | 
					typedef struct {
 | 
				
			||||||
@ -46,6 +58,7 @@ struct Bt {
 | 
				
			|||||||
    uint16_t bt_keys_size;
 | 
					    uint16_t bt_keys_size;
 | 
				
			||||||
    uint16_t max_packet_size;
 | 
					    uint16_t max_packet_size;
 | 
				
			||||||
    BtSettings bt_settings;
 | 
					    BtSettings bt_settings;
 | 
				
			||||||
 | 
					    BtKeysStorage* keys_storage;
 | 
				
			||||||
    BtStatus status;
 | 
					    BtStatus status;
 | 
				
			||||||
    BtProfile profile;
 | 
					    BtProfile profile;
 | 
				
			||||||
    FuriMessageQueue* message_queue;
 | 
					    FuriMessageQueue* message_queue;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,49 +1,24 @@
 | 
				
			|||||||
#include "bt_keys_storage.h"
 | 
					#include "bt_keys_storage.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <furi.h>
 | 
					#include <furi.h>
 | 
				
			||||||
 | 
					#include <furi_hal_bt.h>
 | 
				
			||||||
#include <lib/toolbox/saved_struct.h>
 | 
					#include <lib/toolbox/saved_struct.h>
 | 
				
			||||||
#include <storage/storage.h>
 | 
					#include <storage/storage.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define BT_KEYS_STORAGE_PATH INT_PATH(BT_KEYS_STORAGE_FILE_NAME)
 | 
					 | 
				
			||||||
#define BT_KEYS_STORAGE_VERSION (0)
 | 
					#define BT_KEYS_STORAGE_VERSION (0)
 | 
				
			||||||
#define BT_KEYS_STORAGE_MAGIC (0x18)
 | 
					#define BT_KEYS_STORAGE_MAGIC (0x18)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool bt_keys_storage_load(Bt* bt) {
 | 
					#define TAG "BtKeyStorage"
 | 
				
			||||||
    furi_assert(bt);
 | 
					 | 
				
			||||||
    bool file_loaded = false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    furi_hal_bt_get_key_storage_buff(&bt->bt_keys_addr_start, &bt->bt_keys_size);
 | 
					struct BtKeysStorage {
 | 
				
			||||||
    furi_hal_bt_nvm_sram_sem_acquire();
 | 
					    uint8_t* nvm_sram_buff;
 | 
				
			||||||
    file_loaded = saved_struct_load(
 | 
					    uint16_t nvm_sram_buff_size;
 | 
				
			||||||
        BT_KEYS_STORAGE_PATH,
 | 
					    FuriString* file_path;
 | 
				
			||||||
        bt->bt_keys_addr_start,
 | 
					};
 | 
				
			||||||
        bt->bt_keys_size,
 | 
					 | 
				
			||||||
        BT_KEYS_STORAGE_MAGIC,
 | 
					 | 
				
			||||||
        BT_KEYS_STORAGE_VERSION);
 | 
					 | 
				
			||||||
    furi_hal_bt_nvm_sram_sem_release();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return file_loaded;
 | 
					bool bt_keys_storage_delete(BtKeysStorage* instance) {
 | 
				
			||||||
}
 | 
					    furi_assert(instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool bt_keys_storage_save(Bt* bt) {
 | 
					 | 
				
			||||||
    furi_assert(bt);
 | 
					 | 
				
			||||||
    furi_assert(bt->bt_keys_addr_start);
 | 
					 | 
				
			||||||
    bool file_saved = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    furi_hal_bt_nvm_sram_sem_acquire();
 | 
					 | 
				
			||||||
    file_saved = saved_struct_save(
 | 
					 | 
				
			||||||
        BT_KEYS_STORAGE_PATH,
 | 
					 | 
				
			||||||
        bt->bt_keys_addr_start,
 | 
					 | 
				
			||||||
        bt->bt_keys_size,
 | 
					 | 
				
			||||||
        BT_KEYS_STORAGE_MAGIC,
 | 
					 | 
				
			||||||
        BT_KEYS_STORAGE_VERSION);
 | 
					 | 
				
			||||||
    furi_hal_bt_nvm_sram_sem_release();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return file_saved;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
bool bt_keys_storage_delete(Bt* bt) {
 | 
					 | 
				
			||||||
    furi_assert(bt);
 | 
					 | 
				
			||||||
    bool delete_succeed = false;
 | 
					    bool delete_succeed = false;
 | 
				
			||||||
    bool bt_is_active = furi_hal_bt_is_active();
 | 
					    bool bt_is_active = furi_hal_bt_is_active();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,3 +30,117 @@ bool bt_keys_storage_delete(Bt* bt) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return delete_succeed;
 | 
					    return delete_succeed;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path) {
 | 
				
			||||||
 | 
					    furi_assert(keys_storage_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    BtKeysStorage* instance = malloc(sizeof(BtKeysStorage));
 | 
				
			||||||
 | 
					    // Set default nvm ram parameters
 | 
				
			||||||
 | 
					    furi_hal_bt_get_key_storage_buff(&instance->nvm_sram_buff, &instance->nvm_sram_buff_size);
 | 
				
			||||||
 | 
					    // Set key storage file
 | 
				
			||||||
 | 
					    instance->file_path = furi_string_alloc();
 | 
				
			||||||
 | 
					    furi_string_set_str(instance->file_path, keys_storage_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return instance;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_free(BtKeysStorage* instance) {
 | 
				
			||||||
 | 
					    furi_assert(instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_string_free(instance->file_path);
 | 
				
			||||||
 | 
					    free(instance);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_set_file_path(BtKeysStorage* instance, const char* path) {
 | 
				
			||||||
 | 
					    furi_assert(instance);
 | 
				
			||||||
 | 
					    furi_assert(path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_string_set_str(instance->file_path, path);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint16_t size) {
 | 
				
			||||||
 | 
					    furi_assert(instance);
 | 
				
			||||||
 | 
					    furi_assert(buff);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    instance->nvm_sram_buff = buff;
 | 
				
			||||||
 | 
					    instance->nvm_sram_buff_size = size;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool bt_keys_storage_load(BtKeysStorage* instance) {
 | 
				
			||||||
 | 
					    furi_assert(instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool loaded = false;
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					        // Get payload size
 | 
				
			||||||
 | 
					        size_t payload_size = 0;
 | 
				
			||||||
 | 
					        if(!saved_struct_get_payload_size(
 | 
				
			||||||
 | 
					               furi_string_get_cstr(instance->file_path),
 | 
				
			||||||
 | 
					               BT_KEYS_STORAGE_MAGIC,
 | 
				
			||||||
 | 
					               BT_KEYS_STORAGE_VERSION,
 | 
				
			||||||
 | 
					               &payload_size)) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Failed to read payload size");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(payload_size > instance->nvm_sram_buff_size) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Saved data doesn't fit ram buffer");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Load saved data to ram
 | 
				
			||||||
 | 
					        furi_hal_bt_nvm_sram_sem_acquire();
 | 
				
			||||||
 | 
					        bool data_loaded = saved_struct_load(
 | 
				
			||||||
 | 
					            furi_string_get_cstr(instance->file_path),
 | 
				
			||||||
 | 
					            instance->nvm_sram_buff,
 | 
				
			||||||
 | 
					            payload_size,
 | 
				
			||||||
 | 
					            BT_KEYS_STORAGE_MAGIC,
 | 
				
			||||||
 | 
					            BT_KEYS_STORAGE_VERSION);
 | 
				
			||||||
 | 
					        furi_hal_bt_nvm_sram_sem_release();
 | 
				
			||||||
 | 
					        if(!data_loaded) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Failed to load struct");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        loaded = true;
 | 
				
			||||||
 | 
					    } while(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return loaded;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size) {
 | 
				
			||||||
 | 
					    furi_assert(instance);
 | 
				
			||||||
 | 
					    furi_assert(start_addr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool updated = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FURI_LOG_I(
 | 
				
			||||||
 | 
					        TAG,
 | 
				
			||||||
 | 
					        "Base address: %p. Start update address: %p. Size changed: %ld",
 | 
				
			||||||
 | 
					        (void*)instance->nvm_sram_buff,
 | 
				
			||||||
 | 
					        start_addr,
 | 
				
			||||||
 | 
					        size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					        size_t new_size = start_addr - instance->nvm_sram_buff + size;
 | 
				
			||||||
 | 
					        if(new_size > instance->nvm_sram_buff_size) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "NVM RAM buffer overflow");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        furi_hal_bt_nvm_sram_sem_acquire();
 | 
				
			||||||
 | 
					        bool data_updated = saved_struct_save(
 | 
				
			||||||
 | 
					            furi_string_get_cstr(instance->file_path),
 | 
				
			||||||
 | 
					            instance->nvm_sram_buff,
 | 
				
			||||||
 | 
					            new_size,
 | 
				
			||||||
 | 
					            BT_KEYS_STORAGE_MAGIC,
 | 
				
			||||||
 | 
					            BT_KEYS_STORAGE_VERSION);
 | 
				
			||||||
 | 
					        furi_hal_bt_nvm_sram_sem_release();
 | 
				
			||||||
 | 
					        if(!data_updated) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "Failed to update key storage");
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        updated = true;
 | 
				
			||||||
 | 
					    } while(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return updated;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,20 @@
 | 
				
			|||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "bt_i.h"
 | 
					#include <stdint.h>
 | 
				
			||||||
#include "bt_keys_filename.h"
 | 
					#include <stdbool.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool bt_keys_storage_load(Bt* bt);
 | 
					typedef struct BtKeysStorage BtKeysStorage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool bt_keys_storage_save(Bt* bt);
 | 
					BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool bt_keys_storage_delete(Bt* bt);
 | 
					void bt_keys_storage_free(BtKeysStorage* instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_set_file_path(BtKeysStorage* instance, const char* path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint16_t size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool bt_keys_storage_load(BtKeysStorage* instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool bt_keys_storage_delete(BtKeysStorage* instance);
 | 
				
			||||||
 | 
				
			|||||||
@ -52,6 +52,7 @@ struct AnimationManager {
 | 
				
			|||||||
    FuriString* freezed_animation_name;
 | 
					    FuriString* freezed_animation_name;
 | 
				
			||||||
    int32_t freezed_animation_time_left;
 | 
					    int32_t freezed_animation_time_left;
 | 
				
			||||||
    ViewStack* view_stack;
 | 
					    ViewStack* view_stack;
 | 
				
			||||||
 | 
					    bool dummy_mode;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static StorageAnimation*
 | 
					static StorageAnimation*
 | 
				
			||||||
@ -93,6 +94,12 @@ void animation_manager_set_interact_callback(
 | 
				
			|||||||
    animation_manager->interact_callback = callback;
 | 
					    animation_manager->interact_callback = callback;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void animation_manager_set_dummy_mode_state(AnimationManager* animation_manager, bool enabled) {
 | 
				
			||||||
 | 
					    furi_assert(animation_manager);
 | 
				
			||||||
 | 
					    animation_manager->dummy_mode = enabled;
 | 
				
			||||||
 | 
					    animation_manager_start_new_idle(animation_manager);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void animation_manager_check_blocking_callback(const void* message, void* context) {
 | 
					static void animation_manager_check_blocking_callback(const void* message, void* context) {
 | 
				
			||||||
    const StorageEvent* storage_event = message;
 | 
					    const StorageEvent* storage_event = message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -363,7 +370,9 @@ static bool animation_manager_is_valid_idle_animation(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
static StorageAnimation*
 | 
					static StorageAnimation*
 | 
				
			||||||
    animation_manager_select_idle_animation(AnimationManager* animation_manager) {
 | 
					    animation_manager_select_idle_animation(AnimationManager* animation_manager) {
 | 
				
			||||||
    UNUSED(animation_manager);
 | 
					    if(animation_manager->dummy_mode) {
 | 
				
			||||||
 | 
					        return animation_storage_find_animation(HARDCODED_ANIMATION_NAME);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    StorageAnimationList_t animation_list;
 | 
					    StorageAnimationList_t animation_list;
 | 
				
			||||||
    StorageAnimationList_init(animation_list);
 | 
					    StorageAnimationList_init(animation_list);
 | 
				
			||||||
    animation_storage_fill_animation_list(&animation_list);
 | 
					    animation_storage_fill_animation_list(&animation_list);
 | 
				
			||||||
 | 
				
			|||||||
@ -157,3 +157,11 @@ void animation_manager_unload_and_stall_animation(AnimationManager* animation_ma
 | 
				
			|||||||
 * @animation_manager   instance
 | 
					 * @animation_manager   instance
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
void animation_manager_load_and_continue_animation(AnimationManager* animation_manager);
 | 
					void animation_manager_load_and_continue_animation(AnimationManager* animation_manager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Enable or disable dummy mode backgrounds of animation manager.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @animation_manager    instance
 | 
				
			||||||
 | 
					 * @enabled              bool
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void animation_manager_set_dummy_mode_state(AnimationManager* animation_manager, bool enabled);
 | 
				
			||||||
 | 
				
			|||||||
@ -144,6 +144,7 @@ void desktop_unlock(Desktop* desktop) {
 | 
				
			|||||||
void desktop_set_dummy_mode_state(Desktop* desktop, bool enabled) {
 | 
					void desktop_set_dummy_mode_state(Desktop* desktop, bool enabled) {
 | 
				
			||||||
    view_port_enabled_set(desktop->dummy_mode_icon_viewport, enabled);
 | 
					    view_port_enabled_set(desktop->dummy_mode_icon_viewport, enabled);
 | 
				
			||||||
    desktop_main_set_dummy_mode_state(desktop->main_view, enabled);
 | 
					    desktop_main_set_dummy_mode_state(desktop->main_view, enabled);
 | 
				
			||||||
 | 
					    animation_manager_set_dummy_mode_state(desktop->animation_manager, enabled);
 | 
				
			||||||
    desktop->settings.dummy_mode = enabled;
 | 
					    desktop->settings.dummy_mode = enabled;
 | 
				
			||||||
    DESKTOP_SETTINGS_SAVE(&desktop->settings);
 | 
					    DESKTOP_SETTINGS_SAVE(&desktop->settings);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -330,6 +331,8 @@ int32_t desktop_srv(void* p) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    view_port_enabled_set(desktop->dummy_mode_icon_viewport, desktop->settings.dummy_mode);
 | 
					    view_port_enabled_set(desktop->dummy_mode_icon_viewport, desktop->settings.dummy_mode);
 | 
				
			||||||
    desktop_main_set_dummy_mode_state(desktop->main_view, desktop->settings.dummy_mode);
 | 
					    desktop_main_set_dummy_mode_state(desktop->main_view, desktop->settings.dummy_mode);
 | 
				
			||||||
 | 
					    animation_manager_set_dummy_mode_state(
 | 
				
			||||||
 | 
					        desktop->animation_manager, desktop->settings.dummy_mode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
 | 
					    scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#define TAG "DesktopSrv"
 | 
					#define TAG "DesktopSrv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define MUSIC_PLAYER_APP EXT_PATH("/apps/Misc/music_player.fap")
 | 
				
			||||||
 | 
					#define SNAKE_GAME_APP EXT_PATH("/apps/Games/snake_game.fap")
 | 
				
			||||||
 | 
					#define CLOCK_APP EXT_PATH("/apps/Tools/clock.fap")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void desktop_scene_main_new_idle_animation_callback(void* context) {
 | 
					static void desktop_scene_main_new_idle_animation_callback(void* context) {
 | 
				
			||||||
    furi_assert(context);
 | 
					    furi_assert(context);
 | 
				
			||||||
    Desktop* desktop = context;
 | 
					    Desktop* desktop = context;
 | 
				
			||||||
@ -60,6 +64,19 @@ static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* fl
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void desktop_scene_main_open_app_or_profile(Desktop* desktop, const char* path) {
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					        LoaderStatus status = loader_start(desktop->loader, FAP_LOADER_APP_NAME, path);
 | 
				
			||||||
 | 
					        if(status == LoaderStatusOk) break;
 | 
				
			||||||
 | 
					        FURI_LOG_E(TAG, "loader_start failed: %d", status);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        status = loader_start(desktop->loader, "Passport", NULL);
 | 
				
			||||||
 | 
					        if(status != LoaderStatusOk) {
 | 
				
			||||||
 | 
					            FURI_LOG_E(TAG, "loader_start failed: %d", status);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } while(false);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void desktop_scene_main_callback(DesktopEvent event, void* context) {
 | 
					void desktop_scene_main_callback(DesktopEvent event, void* context) {
 | 
				
			||||||
    Desktop* desktop = (Desktop*)context;
 | 
					    Desktop* desktop = (Desktop*)context;
 | 
				
			||||||
    view_dispatcher_send_custom_event(desktop->view_dispatcher, event);
 | 
					    view_dispatcher_send_custom_event(desktop->view_dispatcher, event);
 | 
				
			||||||
@ -181,12 +198,16 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        case DesktopMainEventOpenGameMenu: {
 | 
					        case DesktopMainEventOpenGame: {
 | 
				
			||||||
            LoaderStatus status = loader_start(
 | 
					            desktop_scene_main_open_app_or_profile(desktop, SNAKE_GAME_APP);
 | 
				
			||||||
                desktop->loader, FAP_LOADER_APP_NAME, EXT_PATH("/apps/Games/snake_game.fap"));
 | 
					            break;
 | 
				
			||||||
            if(status != LoaderStatusOk) {
 | 
					 | 
				
			||||||
                FURI_LOG_E(TAG, "loader_start failed: %d", status);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        case DesktopMainEventOpenClock: {
 | 
				
			||||||
 | 
					            desktop_scene_main_open_app_or_profile(desktop, CLOCK_APP);
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case DesktopMainEventOpenMusicPlayer: {
 | 
				
			||||||
 | 
					            desktop_scene_main_open_app_or_profile(desktop, MUSIC_PLAYER_APP);
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        case DesktopLockedEventUpdate:
 | 
					        case DesktopLockedEventUpdate:
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,9 @@ typedef enum {
 | 
				
			|||||||
    DesktopMainEventOpenPassport,
 | 
					    DesktopMainEventOpenPassport,
 | 
				
			||||||
    DesktopMainEventOpenPowerOff,
 | 
					    DesktopMainEventOpenPowerOff,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    DesktopMainEventOpenGameMenu,
 | 
					    DesktopMainEventOpenGame,
 | 
				
			||||||
 | 
					    DesktopMainEventOpenClock,
 | 
				
			||||||
 | 
					    DesktopMainEventOpenMusicPlayer,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    DesktopLockedEventUnlocked,
 | 
					    DesktopLockedEventUnlocked,
 | 
				
			||||||
    DesktopLockedEventUpdate,
 | 
					    DesktopLockedEventUpdate,
 | 
				
			||||||
 | 
				
			|||||||
@ -72,13 +72,13 @@ bool desktop_main_input_callback(InputEvent* event, void* context) {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        if(event->type == InputTypeShort) {
 | 
					        if(event->type == InputTypeShort) {
 | 
				
			||||||
            if(event->key == InputKeyOk) {
 | 
					            if(event->key == InputKeyOk) {
 | 
				
			||||||
                main_view->callback(DesktopMainEventOpenGameMenu, main_view->context);
 | 
					                main_view->callback(DesktopMainEventOpenGame, main_view->context);
 | 
				
			||||||
            } else if(event->key == InputKeyUp) {
 | 
					            } else if(event->key == InputKeyUp) {
 | 
				
			||||||
                main_view->callback(DesktopMainEventOpenLockMenu, main_view->context);
 | 
					                main_view->callback(DesktopMainEventOpenLockMenu, main_view->context);
 | 
				
			||||||
            } else if(event->key == InputKeyDown) {
 | 
					            } else if(event->key == InputKeyDown) {
 | 
				
			||||||
                main_view->callback(DesktopMainEventOpenPassport, main_view->context);
 | 
					                main_view->callback(DesktopMainEventOpenMusicPlayer, main_view->context);
 | 
				
			||||||
            } else if(event->key == InputKeyLeft) {
 | 
					            } else if(event->key == InputKeyLeft) {
 | 
				
			||||||
                main_view->callback(DesktopMainEventOpenPassport, main_view->context);
 | 
					                main_view->callback(DesktopMainEventOpenClock, main_view->context);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            // Right key is handled by animation manager
 | 
					            // Right key is handled by animation manager
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -50,6 +50,10 @@ void view_holder_free(ViewHolder* view_holder) {
 | 
				
			|||||||
void view_holder_set_view(ViewHolder* view_holder, View* view) {
 | 
					void view_holder_set_view(ViewHolder* view_holder, View* view) {
 | 
				
			||||||
    furi_assert(view_holder);
 | 
					    furi_assert(view_holder);
 | 
				
			||||||
    if(view_holder->view) {
 | 
					    if(view_holder->view) {
 | 
				
			||||||
 | 
					        if(view_holder->view->exit_callback) {
 | 
				
			||||||
 | 
					            view_holder->view->exit_callback(view_holder->view->context);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        view_set_update_callback(view_holder->view, NULL);
 | 
					        view_set_update_callback(view_holder->view, NULL);
 | 
				
			||||||
        view_set_update_callback_context(view_holder->view, NULL);
 | 
					        view_set_update_callback_context(view_holder->view, NULL);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -59,6 +63,10 @@ void view_holder_set_view(ViewHolder* view_holder, View* view) {
 | 
				
			|||||||
    if(view_holder->view) {
 | 
					    if(view_holder->view) {
 | 
				
			||||||
        view_set_update_callback(view_holder->view, view_holder_update);
 | 
					        view_set_update_callback(view_holder->view, view_holder_update);
 | 
				
			||||||
        view_set_update_callback_context(view_holder->view, view_holder);
 | 
					        view_set_update_callback_context(view_holder->view, view_holder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(view_holder->view->enter_callback) {
 | 
				
			||||||
 | 
					            view_holder->view->enter_callback(view_holder->view->context);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -547,6 +547,53 @@ void elements_string_fit_width(Canvas* canvas, FuriString* string, uint8_t width
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void elements_scrollable_text_line(
 | 
				
			||||||
 | 
					    Canvas* canvas,
 | 
				
			||||||
 | 
					    uint8_t x,
 | 
				
			||||||
 | 
					    uint8_t y,
 | 
				
			||||||
 | 
					    uint8_t width,
 | 
				
			||||||
 | 
					    FuriString* string,
 | 
				
			||||||
 | 
					    size_t scroll,
 | 
				
			||||||
 | 
					    bool ellipsis) {
 | 
				
			||||||
 | 
					    FuriString* line = furi_string_alloc_set(string);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    size_t len_px = canvas_string_width(canvas, furi_string_get_cstr(line));
 | 
				
			||||||
 | 
					    if(len_px > width) {
 | 
				
			||||||
 | 
					        if(ellipsis) {
 | 
				
			||||||
 | 
					            width -= canvas_string_width(canvas, "...");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Calculate scroll size
 | 
				
			||||||
 | 
					        size_t scroll_size = furi_string_size(line);
 | 
				
			||||||
 | 
					        size_t right_width = 0;
 | 
				
			||||||
 | 
					        for(size_t i = scroll_size; i > 0; i--) {
 | 
				
			||||||
 | 
					            right_width += canvas_glyph_width(canvas, furi_string_get_char(line, i));
 | 
				
			||||||
 | 
					            if(right_width > width) break;
 | 
				
			||||||
 | 
					            scroll_size--;
 | 
				
			||||||
 | 
					            if(!scroll_size) break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Ensure that we have something to scroll
 | 
				
			||||||
 | 
					        if(scroll_size) {
 | 
				
			||||||
 | 
					            scroll_size += 3;
 | 
				
			||||||
 | 
					            scroll = scroll % scroll_size;
 | 
				
			||||||
 | 
					            furi_string_right(line, scroll);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        len_px = canvas_string_width(canvas, furi_string_get_cstr(line));
 | 
				
			||||||
 | 
					        while(len_px > width) {
 | 
				
			||||||
 | 
					            furi_string_left(line, furi_string_size(line) - 1);
 | 
				
			||||||
 | 
					            len_px = canvas_string_width(canvas, furi_string_get_cstr(line));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(ellipsis) {
 | 
				
			||||||
 | 
					            furi_string_cat(line, "...");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    canvas_draw_str(canvas, x, y, furi_string_get_cstr(line));
 | 
				
			||||||
 | 
					    furi_string_free(line);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void elements_text_box(
 | 
					void elements_text_box(
 | 
				
			||||||
    Canvas* canvas,
 | 
					    Canvas* canvas,
 | 
				
			||||||
    uint8_t x,
 | 
					    uint8_t x,
 | 
				
			||||||
 | 
				
			|||||||
@ -192,6 +192,25 @@ void elements_bubble_str(
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
void elements_string_fit_width(Canvas* canvas, FuriString* string, uint8_t width);
 | 
					void elements_string_fit_width(Canvas* canvas, FuriString* string, uint8_t width);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Draw scrollable text line
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param      canvas    The canvas
 | 
				
			||||||
 | 
					 * @param[in]  x         X coordinate
 | 
				
			||||||
 | 
					 * @param[in]  y         Y coordinate
 | 
				
			||||||
 | 
					 * @param[in]  width     The width
 | 
				
			||||||
 | 
					 * @param      string    The string
 | 
				
			||||||
 | 
					 * @param[in]  scroll    The scroll counter: 0 - no scroll, any other number - scroll. Just count up, everything else will be calculated on the inside.
 | 
				
			||||||
 | 
					 * @param[in]  ellipsis  The ellipsis flag: true to add ellipse
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void elements_scrollable_text_line(
 | 
				
			||||||
 | 
					    Canvas* canvas,
 | 
				
			||||||
 | 
					    uint8_t x,
 | 
				
			||||||
 | 
					    uint8_t y,
 | 
				
			||||||
 | 
					    uint8_t width,
 | 
				
			||||||
 | 
					    FuriString* string,
 | 
				
			||||||
 | 
					    size_t scroll,
 | 
				
			||||||
 | 
					    bool ellipsis);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Draw text box element
 | 
					/** Draw text box element
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param       canvas          Canvas instance
 | 
					 * @param       canvas          Canvas instance
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,7 @@ struct ButtonMenu {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
typedef struct {
 | 
					typedef struct {
 | 
				
			||||||
    ButtonMenuItemArray_t items;
 | 
					    ButtonMenuItemArray_t items;
 | 
				
			||||||
    uint8_t position;
 | 
					    size_t position;
 | 
				
			||||||
    const char* header;
 | 
					    const char* header;
 | 
				
			||||||
} ButtonMenuModel;
 | 
					} ButtonMenuModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -102,11 +102,9 @@ static void button_menu_view_draw_callback(Canvas* canvas, void* _model) {
 | 
				
			|||||||
    ButtonMenuModel* model = (ButtonMenuModel*)_model;
 | 
					    ButtonMenuModel* model = (ButtonMenuModel*)_model;
 | 
				
			||||||
    canvas_set_font(canvas, FontSecondary);
 | 
					    canvas_set_font(canvas, FontSecondary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    uint8_t item_position = 0;
 | 
					    const size_t active_screen = model->position / BUTTONS_PER_SCREEN;
 | 
				
			||||||
    int8_t active_screen = model->position / BUTTONS_PER_SCREEN;
 | 
					    const size_t items_size = ButtonMenuItemArray_size(model->items);
 | 
				
			||||||
    size_t items_size = ButtonMenuItemArray_size(model->items);
 | 
					    const size_t max_screen = items_size ? (items_size - 1) / BUTTONS_PER_SCREEN : 0;
 | 
				
			||||||
    int8_t max_screen = ((int16_t)items_size - 1) / BUTTONS_PER_SCREEN;
 | 
					 | 
				
			||||||
    ButtonMenuItemArray_it_t it;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(active_screen > 0) {
 | 
					    if(active_screen > 0) {
 | 
				
			||||||
        canvas_draw_icon(canvas, 28, 1, &I_InfraredArrowUp_4x8);
 | 
					        canvas_draw_icon(canvas, 28, 1, &I_InfraredArrowUp_4x8);
 | 
				
			||||||
@ -125,6 +123,9 @@ static void button_menu_view_draw_callback(Canvas* canvas, void* _model) {
 | 
				
			|||||||
        furi_string_free(disp_str);
 | 
					        furi_string_free(disp_str);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    size_t item_position = 0;
 | 
				
			||||||
 | 
					    ButtonMenuItemArray_it_t it;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
 | 
					    for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
 | 
				
			||||||
        ButtonMenuItemArray_next(it), ++item_position) {
 | 
					        ButtonMenuItemArray_next(it), ++item_position) {
 | 
				
			||||||
        if(active_screen == (item_position / BUTTONS_PER_SCREEN)) {
 | 
					        if(active_screen == (item_position / BUTTONS_PER_SCREEN)) {
 | 
				
			||||||
@ -195,14 +196,14 @@ static void button_menu_process_ok(ButtonMenu* button_menu, InputType type) {
 | 
				
			|||||||
    if(item) {
 | 
					    if(item) {
 | 
				
			||||||
        if(item->type == ButtonMenuItemTypeControl) {
 | 
					        if(item->type == ButtonMenuItemTypeControl) {
 | 
				
			||||||
            if(type == InputTypeShort) {
 | 
					            if(type == InputTypeShort) {
 | 
				
			||||||
                if(item && item->callback) {
 | 
					                if(item->callback) {
 | 
				
			||||||
                    item->callback(item->callback_context, item->index, type);
 | 
					                    item->callback(item->callback_context, item->index, type);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if(item->type == ButtonMenuItemTypeCommon) {
 | 
					        if(item->type == ButtonMenuItemTypeCommon) {
 | 
				
			||||||
            if((type == InputTypePress) || (type == InputTypeRelease)) {
 | 
					            if((type == InputTypePress) || (type == InputTypeRelease)) {
 | 
				
			||||||
                if(item && item->callback) {
 | 
					                if(item->callback) {
 | 
				
			||||||
                    item->callback(item->callback_context, item->index, type);
 | 
					                    item->callback(item->callback_context, item->index, type);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -341,7 +342,7 @@ void button_menu_set_selected_item(ButtonMenu* button_menu, uint32_t index) {
 | 
				
			|||||||
        button_menu->view,
 | 
					        button_menu->view,
 | 
				
			||||||
        ButtonMenuModel * model,
 | 
					        ButtonMenuModel * model,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            uint8_t item_position = 0;
 | 
					            size_t item_position = 0;
 | 
				
			||||||
            ButtonMenuItemArray_it_t it;
 | 
					            ButtonMenuItemArray_it_t it;
 | 
				
			||||||
            for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
 | 
					            for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
 | 
				
			||||||
                ButtonMenuItemArray_next(it), ++item_position) {
 | 
					                ButtonMenuItemArray_next(it), ++item_position) {
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#define CUSTOM_ICON_MAX_SIZE 32
 | 
					#define CUSTOM_ICON_MAX_SIZE 32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define SCROLL_INTERVAL (333)
 | 
				
			||||||
 | 
					#define SCROLL_DELAY (2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef enum {
 | 
					typedef enum {
 | 
				
			||||||
    BrowserItemTypeLoading,
 | 
					    BrowserItemTypeLoading,
 | 
				
			||||||
    BrowserItemTypeBack,
 | 
					    BrowserItemTypeBack,
 | 
				
			||||||
@ -95,6 +98,7 @@ struct FileBrowser {
 | 
				
			|||||||
    void* item_context;
 | 
					    void* item_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    FuriString* result_path;
 | 
					    FuriString* result_path;
 | 
				
			||||||
 | 
					    FuriTimer* scroll_timer;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef struct {
 | 
					typedef struct {
 | 
				
			||||||
@ -110,6 +114,7 @@ typedef struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const Icon* file_icon;
 | 
					    const Icon* file_icon;
 | 
				
			||||||
    bool hide_ext;
 | 
					    bool hide_ext;
 | 
				
			||||||
 | 
					    size_t scroll_counter;
 | 
				
			||||||
} FileBrowserModel;
 | 
					} FileBrowserModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static const Icon* BrowserItemIcons[] = {
 | 
					static const Icon* BrowserItemIcons[] = {
 | 
				
			||||||
@ -129,6 +134,27 @@ static void
 | 
				
			|||||||
    browser_list_item_cb(void* context, FuriString* item_path, bool is_folder, bool is_last);
 | 
					    browser_list_item_cb(void* context, FuriString* item_path, bool is_folder, bool is_last);
 | 
				
			||||||
static void browser_long_load_cb(void* context);
 | 
					static void browser_long_load_cb(void* context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void file_browser_scroll_timer_callback(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    FileBrowser* browser = context;
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        browser->view, FileBrowserModel * model, { model->scroll_counter++; }, true);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void file_browser_view_enter_callback(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    FileBrowser* browser = context;
 | 
				
			||||||
 | 
					    with_view_model(
 | 
				
			||||||
 | 
					        browser->view, FileBrowserModel * model, { model->scroll_counter = 0; }, true);
 | 
				
			||||||
 | 
					    furi_timer_start(browser->scroll_timer, SCROLL_INTERVAL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void file_browser_view_exit_callback(void* context) {
 | 
				
			||||||
 | 
					    furi_assert(context);
 | 
				
			||||||
 | 
					    FileBrowser* browser = context;
 | 
				
			||||||
 | 
					    furi_timer_stop(browser->scroll_timer);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FileBrowser* file_browser_alloc(FuriString* result_path) {
 | 
					FileBrowser* file_browser_alloc(FuriString* result_path) {
 | 
				
			||||||
    furi_assert(result_path);
 | 
					    furi_assert(result_path);
 | 
				
			||||||
    FileBrowser* browser = malloc(sizeof(FileBrowser));
 | 
					    FileBrowser* browser = malloc(sizeof(FileBrowser));
 | 
				
			||||||
@ -137,6 +163,11 @@ FileBrowser* file_browser_alloc(FuriString* result_path) {
 | 
				
			|||||||
    view_set_context(browser->view, browser);
 | 
					    view_set_context(browser->view, browser);
 | 
				
			||||||
    view_set_draw_callback(browser->view, file_browser_view_draw_callback);
 | 
					    view_set_draw_callback(browser->view, file_browser_view_draw_callback);
 | 
				
			||||||
    view_set_input_callback(browser->view, file_browser_view_input_callback);
 | 
					    view_set_input_callback(browser->view, file_browser_view_input_callback);
 | 
				
			||||||
 | 
					    view_set_enter_callback(browser->view, file_browser_view_enter_callback);
 | 
				
			||||||
 | 
					    view_set_exit_callback(browser->view, file_browser_view_exit_callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    browser->scroll_timer =
 | 
				
			||||||
 | 
					        furi_timer_alloc(file_browser_scroll_timer_callback, FuriTimerTypePeriodic, browser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    browser->result_path = result_path;
 | 
					    browser->result_path = result_path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -149,6 +180,8 @@ FileBrowser* file_browser_alloc(FuriString* result_path) {
 | 
				
			|||||||
void file_browser_free(FileBrowser* browser) {
 | 
					void file_browser_free(FileBrowser* browser) {
 | 
				
			||||||
    furi_assert(browser);
 | 
					    furi_assert(browser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    furi_timer_free(browser->scroll_timer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_view_model(
 | 
					    with_view_model(
 | 
				
			||||||
        browser->view, FileBrowserModel * model, { items_array_clear(model->items); }, false);
 | 
					        browser->view, FileBrowserModel * model, { items_array_clear(model->items); }, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -468,13 +501,17 @@ static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) {
 | 
				
			|||||||
            furi_string_set(filename, ". .");
 | 
					            furi_string_set(filename, ". .");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        elements_string_fit_width(
 | 
					        size_t scroll_counter = model->scroll_counter;
 | 
				
			||||||
            canvas, filename, (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if(model->item_idx == idx) {
 | 
					        if(model->item_idx == idx) {
 | 
				
			||||||
            browser_draw_frame(canvas, i, show_scrollbar);
 | 
					            browser_draw_frame(canvas, i, show_scrollbar);
 | 
				
			||||||
 | 
					            if(scroll_counter < SCROLL_DELAY) {
 | 
				
			||||||
 | 
					                scroll_counter = 0;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                scroll_counter -= SCROLL_DELAY;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            canvas_set_color(canvas, ColorBlack);
 | 
					            canvas_set_color(canvas, ColorBlack);
 | 
				
			||||||
 | 
					            scroll_counter = 0;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(custom_icon_data) {
 | 
					        if(custom_icon_data) {
 | 
				
			||||||
@ -487,8 +524,14 @@ static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) {
 | 
				
			|||||||
            canvas_draw_icon(
 | 
					            canvas_draw_icon(
 | 
				
			||||||
                canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]);
 | 
					                canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        canvas_draw_str(
 | 
					        elements_scrollable_text_line(
 | 
				
			||||||
            canvas, 15, Y_OFFSET + 9 + i * FRAME_HEIGHT, furi_string_get_cstr(filename));
 | 
					            canvas,
 | 
				
			||||||
 | 
					            15,
 | 
				
			||||||
 | 
					            Y_OFFSET + 9 + i * FRAME_HEIGHT,
 | 
				
			||||||
 | 
					            (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX),
 | 
				
			||||||
 | 
					            filename,
 | 
				
			||||||
 | 
					            scroll_counter,
 | 
				
			||||||
 | 
					            (model->item_idx != idx));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(show_scrollbar) {
 | 
					    if(show_scrollbar) {
 | 
				
			||||||
@ -543,6 +586,7 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) {
 | 
				
			|||||||
                            file_browser_worker_load(
 | 
					                            file_browser_worker_load(
 | 
				
			||||||
                                browser->worker, load_offset, ITEM_LIST_LEN_MAX);
 | 
					                                browser->worker, load_offset, ITEM_LIST_LEN_MAX);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        model->scroll_counter = 0;
 | 
				
			||||||
                    } else if(event->key == InputKeyDown) {
 | 
					                    } else if(event->key == InputKeyDown) {
 | 
				
			||||||
                        model->item_idx = (model->item_idx + 1) % model->item_cnt;
 | 
					                        model->item_idx = (model->item_idx + 1) % model->item_cnt;
 | 
				
			||||||
                        if(browser_is_list_load_required(model)) {
 | 
					                        if(browser_is_list_load_required(model)) {
 | 
				
			||||||
@ -554,6 +598,7 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) {
 | 
				
			|||||||
                            file_browser_worker_load(
 | 
					                            file_browser_worker_load(
 | 
				
			||||||
                                browser->worker, load_offset, ITEM_LIST_LEN_MAX);
 | 
					                                browser->worker, load_offset, ITEM_LIST_LEN_MAX);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        model->scroll_counter = 0;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                true);
 | 
					                true);
 | 
				
			||||||
 | 
				
			|||||||
@ -20,8 +20,8 @@ ARRAY_DEF(SubmenuItemArray, SubmenuItem, M_POD_OPLIST);
 | 
				
			|||||||
typedef struct {
 | 
					typedef struct {
 | 
				
			||||||
    SubmenuItemArray_t items;
 | 
					    SubmenuItemArray_t items;
 | 
				
			||||||
    const char* header;
 | 
					    const char* header;
 | 
				
			||||||
    uint8_t position;
 | 
					    size_t position;
 | 
				
			||||||
    uint8_t window_position;
 | 
					    size_t window_position;
 | 
				
			||||||
} SubmenuModel;
 | 
					} SubmenuModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void submenu_process_up(Submenu* submenu);
 | 
					static void submenu_process_up(Submenu* submenu);
 | 
				
			||||||
@ -36,19 +36,19 @@ static void submenu_view_draw_callback(Canvas* canvas, void* _model) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    canvas_clear(canvas);
 | 
					    canvas_clear(canvas);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    uint8_t position = 0;
 | 
					 | 
				
			||||||
    SubmenuItemArray_it_t it;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if(model->header) {
 | 
					    if(model->header) {
 | 
				
			||||||
        canvas_set_font(canvas, FontPrimary);
 | 
					        canvas_set_font(canvas, FontPrimary);
 | 
				
			||||||
        canvas_draw_str(canvas, 4, 11, model->header);
 | 
					        canvas_draw_str(canvas, 4, 11, model->header);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    canvas_set_font(canvas, FontSecondary);
 | 
					    canvas_set_font(canvas, FontSecondary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    size_t position = 0;
 | 
				
			||||||
 | 
					    SubmenuItemArray_it_t it;
 | 
				
			||||||
    for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
 | 
					    for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
 | 
				
			||||||
        SubmenuItemArray_next(it)) {
 | 
					        SubmenuItemArray_next(it)) {
 | 
				
			||||||
        uint8_t item_position = position - model->window_position;
 | 
					        const size_t item_position = position - model->window_position;
 | 
				
			||||||
        uint8_t items_on_screen = model->header ? 3 : 4;
 | 
					        const size_t items_on_screen = model->header ? 3 : 4;
 | 
				
			||||||
        uint8_t y_offset = model->header ? 16 : 0;
 | 
					        uint8_t y_offset = model->header ? 16 : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(item_position < items_on_screen) {
 | 
					        if(item_position < items_on_screen) {
 | 
				
			||||||
@ -198,7 +198,7 @@ void submenu_set_selected_item(Submenu* submenu, uint32_t index) {
 | 
				
			|||||||
        submenu->view,
 | 
					        submenu->view,
 | 
				
			||||||
        SubmenuModel * model,
 | 
					        SubmenuModel * model,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            uint32_t position = 0;
 | 
					            size_t position = 0;
 | 
				
			||||||
            SubmenuItemArray_it_t it;
 | 
					            SubmenuItemArray_it_t it;
 | 
				
			||||||
            for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
 | 
					            for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
 | 
				
			||||||
                SubmenuItemArray_next(it)) {
 | 
					                SubmenuItemArray_next(it)) {
 | 
				
			||||||
@ -208,7 +208,9 @@ void submenu_set_selected_item(Submenu* submenu, uint32_t index) {
 | 
				
			|||||||
                position++;
 | 
					                position++;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if(position >= SubmenuItemArray_size(model->items)) {
 | 
					            const size_t items_size = SubmenuItemArray_size(model->items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(position >= items_size) {
 | 
				
			||||||
                position = 0;
 | 
					                position = 0;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -219,16 +221,12 @@ void submenu_set_selected_item(Submenu* submenu, uint32_t index) {
 | 
				
			|||||||
                model->window_position -= 1;
 | 
					                model->window_position -= 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            uint8_t items_on_screen = model->header ? 3 : 4;
 | 
					            const size_t items_on_screen = model->header ? 3 : 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if(SubmenuItemArray_size(model->items) <= items_on_screen) {
 | 
					            if(items_size <= items_on_screen) {
 | 
				
			||||||
                model->window_position = 0;
 | 
					                model->window_position = 0;
 | 
				
			||||||
            } else {
 | 
					            } else if(model->window_position >= items_size - items_on_screen) {
 | 
				
			||||||
                if(model->window_position >=
 | 
					                model->window_position = items_size - items_on_screen;
 | 
				
			||||||
                   (SubmenuItemArray_size(model->items) - items_on_screen)) {
 | 
					 | 
				
			||||||
                    model->window_position =
 | 
					 | 
				
			||||||
                        (SubmenuItemArray_size(model->items) - items_on_screen);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        true);
 | 
					        true);
 | 
				
			||||||
@ -239,16 +237,18 @@ void submenu_process_up(Submenu* submenu) {
 | 
				
			|||||||
        submenu->view,
 | 
					        submenu->view,
 | 
				
			||||||
        SubmenuModel * model,
 | 
					        SubmenuModel * model,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            uint8_t items_on_screen = model->header ? 3 : 4;
 | 
					            const size_t items_on_screen = model->header ? 3 : 4;
 | 
				
			||||||
 | 
					            const size_t items_size = SubmenuItemArray_size(model->items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if(model->position > 0) {
 | 
					            if(model->position > 0) {
 | 
				
			||||||
                model->position--;
 | 
					                model->position--;
 | 
				
			||||||
                if(((model->position - model->window_position) < 1) &&
 | 
					                if((model->position - model->window_position < 1) &&
 | 
				
			||||||
                   model->window_position > 0) {
 | 
					                   (model->window_position > 0)) {
 | 
				
			||||||
                    model->window_position--;
 | 
					                    model->window_position--;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                model->position = SubmenuItemArray_size(model->items) - 1;
 | 
					                model->position = items_size - 1;
 | 
				
			||||||
                if(model->position > (items_on_screen - 1)) {
 | 
					                if(model->position > items_on_screen - 1) {
 | 
				
			||||||
                    model->window_position = model->position - (items_on_screen - 1);
 | 
					                    model->window_position = model->position - (items_on_screen - 1);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -261,12 +261,13 @@ void submenu_process_down(Submenu* submenu) {
 | 
				
			|||||||
        submenu->view,
 | 
					        submenu->view,
 | 
				
			||||||
        SubmenuModel * model,
 | 
					        SubmenuModel * model,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            uint8_t items_on_screen = model->header ? 3 : 4;
 | 
					            const size_t items_on_screen = model->header ? 3 : 4;
 | 
				
			||||||
            if(model->position < (SubmenuItemArray_size(model->items) - 1)) {
 | 
					            const size_t items_size = SubmenuItemArray_size(model->items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if(model->position < items_size - 1) {
 | 
				
			||||||
                model->position++;
 | 
					                model->position++;
 | 
				
			||||||
                if((model->position - model->window_position) > (items_on_screen - 2) &&
 | 
					                if((model->position - model->window_position > items_on_screen - 2) &&
 | 
				
			||||||
                   model->window_position <
 | 
					                   (model->window_position < items_size - items_on_screen)) {
 | 
				
			||||||
                       (SubmenuItemArray_size(model->items) - items_on_screen)) {
 | 
					 | 
				
			||||||
                    model->window_position++;
 | 
					                    model->window_position++;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
@ -284,7 +285,8 @@ void submenu_process_ok(Submenu* submenu) {
 | 
				
			|||||||
        submenu->view,
 | 
					        submenu->view,
 | 
				
			||||||
        SubmenuModel * model,
 | 
					        SubmenuModel * model,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if(model->position < (SubmenuItemArray_size(model->items))) {
 | 
					            const size_t items_size = SubmenuItemArray_size(model->items);
 | 
				
			||||||
 | 
					            if(model->position < items_size) {
 | 
				
			||||||
                item = SubmenuItemArray_get(model->items, model->position);
 | 
					                item = SubmenuItemArray_get(model->items, model->position);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								applications/services/locale/application.fam
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					App(
 | 
				
			||||||
 | 
					    appid="locale",
 | 
				
			||||||
 | 
					    name="LocaleSrv",
 | 
				
			||||||
 | 
					    apptype=FlipperAppType.STARTUP,
 | 
				
			||||||
 | 
					    entry_point="locale_on_system_start",
 | 
				
			||||||
 | 
					    cdefines=["SRV_LOCALE"],
 | 
				
			||||||
 | 
					    order=90,
 | 
				
			||||||
 | 
					    sdk_headers=["locale.h"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										112
									
								
								applications/services/locale/locale.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					#include "locale.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define TAG "LocaleSrv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LocaleMeasurementUnits locale_get_measurement_unit(void) {
 | 
				
			||||||
 | 
					    return (LocaleMeasurementUnits)furi_hal_rtc_get_locale_units();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void locale_set_measurement_unit(LocaleMeasurementUnits format) {
 | 
				
			||||||
 | 
					    furi_hal_rtc_set_locale_units((FuriHalRtcLocaleUnits)format);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LocaleTimeFormat locale_get_time_format(void) {
 | 
				
			||||||
 | 
					    return (LocaleTimeFormat)furi_hal_rtc_get_locale_timeformat();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void locale_set_time_format(LocaleTimeFormat format) {
 | 
				
			||||||
 | 
					    furi_hal_rtc_set_locale_timeformat((FuriHalRtcLocaleTimeFormat)format);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LocaleDateFormat locale_get_date_format(void) {
 | 
				
			||||||
 | 
					    return (LocaleDateFormat)furi_hal_rtc_get_locale_dateformat();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void locale_set_date_format(LocaleDateFormat format) {
 | 
				
			||||||
 | 
					    furi_hal_rtc_set_locale_dateformat((FuriHalRtcLocaleDateFormat)format);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					float locale_fahrenheit_to_celsius(float temp_f) {
 | 
				
			||||||
 | 
					    return (temp_f - 32.f) / 1.8f;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					float locale_celsius_to_fahrenheit(float temp_c) {
 | 
				
			||||||
 | 
					    return (temp_c * 1.8f + 32.f);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void locale_format_time(
 | 
				
			||||||
 | 
					    FuriString* out_str,
 | 
				
			||||||
 | 
					    const FuriHalRtcDateTime* datetime,
 | 
				
			||||||
 | 
					    const LocaleTimeFormat format,
 | 
				
			||||||
 | 
					    const bool show_seconds) {
 | 
				
			||||||
 | 
					    furi_assert(out_str);
 | 
				
			||||||
 | 
					    furi_assert(datetime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uint8_t hours = datetime->hour;
 | 
				
			||||||
 | 
					    uint8_t am_pm = 0;
 | 
				
			||||||
 | 
					    if(format == LocaleTimeFormat12h) {
 | 
				
			||||||
 | 
					        if(hours > 12) {
 | 
				
			||||||
 | 
					            hours -= 12;
 | 
				
			||||||
 | 
					            am_pm = 2;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            am_pm = 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(hours == 0) {
 | 
				
			||||||
 | 
					            hours = 12;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(show_seconds) {
 | 
				
			||||||
 | 
					        furi_string_printf(out_str, "%02u:%02u:%02u", hours, datetime->minute, datetime->second);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        furi_string_printf(out_str, "%02u:%02u", hours, datetime->minute);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(am_pm > 0) {
 | 
				
			||||||
 | 
					        furi_string_cat_printf(out_str, " %s", (am_pm == 1) ? ("AM") : ("PM"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void locale_format_date(
 | 
				
			||||||
 | 
					    FuriString* out_str,
 | 
				
			||||||
 | 
					    const FuriHalRtcDateTime* datetime,
 | 
				
			||||||
 | 
					    const LocaleDateFormat format,
 | 
				
			||||||
 | 
					    const char* separator) {
 | 
				
			||||||
 | 
					    furi_assert(out_str);
 | 
				
			||||||
 | 
					    furi_assert(datetime);
 | 
				
			||||||
 | 
					    furi_assert(separator);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(format == LocaleDateFormatDMY) {
 | 
				
			||||||
 | 
					        furi_string_printf(
 | 
				
			||||||
 | 
					            out_str,
 | 
				
			||||||
 | 
					            "%02u%s%02u%s%04u",
 | 
				
			||||||
 | 
					            datetime->day,
 | 
				
			||||||
 | 
					            separator,
 | 
				
			||||||
 | 
					            datetime->month,
 | 
				
			||||||
 | 
					            separator,
 | 
				
			||||||
 | 
					            datetime->year);
 | 
				
			||||||
 | 
					    } else if(format == LocaleDateFormatMDY) {
 | 
				
			||||||
 | 
					        furi_string_printf(
 | 
				
			||||||
 | 
					            out_str,
 | 
				
			||||||
 | 
					            "%02u%s%02u%s%04u",
 | 
				
			||||||
 | 
					            datetime->month,
 | 
				
			||||||
 | 
					            separator,
 | 
				
			||||||
 | 
					            datetime->day,
 | 
				
			||||||
 | 
					            separator,
 | 
				
			||||||
 | 
					            datetime->year);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        furi_string_printf(
 | 
				
			||||||
 | 
					            out_str,
 | 
				
			||||||
 | 
					            "%04u%s%02u%s%02u",
 | 
				
			||||||
 | 
					            datetime->year,
 | 
				
			||||||
 | 
					            separator,
 | 
				
			||||||
 | 
					            datetime->month,
 | 
				
			||||||
 | 
					            separator,
 | 
				
			||||||
 | 
					            datetime->day);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int32_t locale_on_system_start(void* p) {
 | 
				
			||||||
 | 
					    UNUSED(p);
 | 
				
			||||||
 | 
					    return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										107
									
								
								applications/services/locale/locale.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <stdbool.h>
 | 
				
			||||||
 | 
					#include <furi.h>
 | 
				
			||||||
 | 
					#include <furi_hal.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef __cplusplus
 | 
				
			||||||
 | 
					extern "C" {
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    LocaleMeasurementUnitsMetric = 0, /**< Metric measurement units */
 | 
				
			||||||
 | 
					    LocaleMeasurementUnitsImperial = 1, /**< Imperial measurement units */
 | 
				
			||||||
 | 
					} LocaleMeasurementUnits;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    LocaleTimeFormat24h = 0, /**< 24-hour format */
 | 
				
			||||||
 | 
					    LocaleTimeFormat12h = 1, /**< 12-hour format */
 | 
				
			||||||
 | 
					} LocaleTimeFormat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum {
 | 
				
			||||||
 | 
					    LocaleDateFormatDMY = 0, /**< Day/Month/Year */
 | 
				
			||||||
 | 
					    LocaleDateFormatMDY = 1, /**< Month/Day/Year */
 | 
				
			||||||
 | 
					    LocaleDateFormatYMD = 2, /**< Year/Month/Day */
 | 
				
			||||||
 | 
					} LocaleDateFormat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Get Locale measurement units
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @return     The locale measurement units.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					LocaleMeasurementUnits locale_get_measurement_unit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Set locale measurement units
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[in]  format  The locale measurements units
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void locale_set_measurement_unit(LocaleMeasurementUnits format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Convert Fahrenheit to Celsius
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[in]  temp_f  The Temperature in Fahrenheit
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @return     The Temperature in Celsius 
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					float locale_fahrenheit_to_celsius(float temp_f);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Convert Celsius to Fahrenheit
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[in]  temp_c  The Temperature in Celsius 
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @return     The Temperature in Fahrenheit
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					float locale_celsius_to_fahrenheit(float temp_c);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Get Locale time format
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @return     The locale time format.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					LocaleTimeFormat locale_get_time_format();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Set Locale Time Format
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[in]  format  The Locale Time Format
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void locale_set_time_format(LocaleTimeFormat format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Format time to furi string
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[out] out_str       The FuriString to store formatted time
 | 
				
			||||||
 | 
					 * @param[in]  datetime      Pointer to the datetime
 | 
				
			||||||
 | 
					 * @param[in]  format        The Locale Time Format
 | 
				
			||||||
 | 
					 * @param[in]  show_seconds  The show seconds flag
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void locale_format_time(
 | 
				
			||||||
 | 
					    FuriString* out_str,
 | 
				
			||||||
 | 
					    const FuriHalRtcDateTime* datetime,
 | 
				
			||||||
 | 
					    const LocaleTimeFormat format,
 | 
				
			||||||
 | 
					    const bool show_seconds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Get Locale DateFormat
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @return     The Locale DateFormat.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					LocaleDateFormat locale_get_date_format();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Set Locale DateFormat
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[in]  format  The Locale DateFormat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void locale_set_date_format(LocaleDateFormat format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Format date to furi string
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param[out] out_str    The FuriString to store formatted date
 | 
				
			||||||
 | 
					 * @param[in]  datetime   Pointer to the datetime
 | 
				
			||||||
 | 
					 * @param[in]  format     The format
 | 
				
			||||||
 | 
					 * @param[in]  separator  The separator
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					void locale_format_date(
 | 
				
			||||||
 | 
					    FuriString* out_str,
 | 
				
			||||||
 | 
					    const FuriHalRtcDateTime* datetime,
 | 
				
			||||||
 | 
					    const LocaleDateFormat format,
 | 
				
			||||||
 | 
					    const char* separator);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef __cplusplus
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
@ -150,11 +150,16 @@ void notification_vibro_off() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void notification_sound_on(float freq, float volume) {
 | 
					void notification_sound_on(float freq, float volume) {
 | 
				
			||||||
 | 
					    if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
 | 
				
			||||||
        furi_hal_speaker_start(freq, volume);
 | 
					        furi_hal_speaker_start(freq, volume);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void notification_sound_off() {
 | 
					void notification_sound_off() {
 | 
				
			||||||
 | 
					    if(furi_hal_speaker_is_mine()) {
 | 
				
			||||||
        furi_hal_speaker_stop();
 | 
					        furi_hal_speaker_stop();
 | 
				
			||||||
 | 
					        furi_hal_speaker_release();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// display timer
 | 
					// display timer
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ App(
 | 
				
			|||||||
    name="System",
 | 
					    name="System",
 | 
				
			||||||
    apptype=FlipperAppType.SETTINGS,
 | 
					    apptype=FlipperAppType.SETTINGS,
 | 
				
			||||||
    entry_point="system_settings_app",
 | 
					    entry_point="system_settings_app",
 | 
				
			||||||
    requires=["gui"],
 | 
					    requires=["gui", "locale"],
 | 
				
			||||||
    stack_size=1 * 1024,
 | 
					    stack_size=1 * 1024,
 | 
				
			||||||
    order=70,
 | 
					    order=70,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
#include "system_settings.h"
 | 
					#include "system_settings.h"
 | 
				
			||||||
#include <loader/loader.h>
 | 
					#include <loader/loader.h>
 | 
				
			||||||
#include <lib/toolbox/value_index.h>
 | 
					#include <lib/toolbox/value_index.h>
 | 
				
			||||||
 | 
					#include <locale/locale.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const char* const log_level_text[] = {
 | 
					const char* const log_level_text[] = {
 | 
				
			||||||
    "Default",
 | 
					    "Default",
 | 
				
			||||||
@ -70,6 +71,59 @@ static void heap_trace_mode_changed(VariableItem* item) {
 | 
				
			|||||||
    furi_hal_rtc_set_heap_track_mode(heap_trace_mode_value[index]);
 | 
					    furi_hal_rtc_set_heap_track_mode(heap_trace_mode_value[index]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const char* const mesurement_units_text[] = {
 | 
				
			||||||
 | 
					    "Metric",
 | 
				
			||||||
 | 
					    "Imperial",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uint32_t mesurement_units_value[] = {
 | 
				
			||||||
 | 
					    LocaleMeasurementUnitsMetric,
 | 
				
			||||||
 | 
					    LocaleMeasurementUnitsImperial,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void mesurement_units_changed(VariableItem* item) {
 | 
				
			||||||
 | 
					    // SystemSettings* app = variable_item_get_context(item);
 | 
				
			||||||
 | 
					    uint8_t index = variable_item_get_current_value_index(item);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, mesurement_units_text[index]);
 | 
				
			||||||
 | 
					    locale_set_measurement_unit(mesurement_units_value[index]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const char* const time_format_text[] = {
 | 
				
			||||||
 | 
					    "24h",
 | 
				
			||||||
 | 
					    "12h",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uint32_t time_format_value[] = {
 | 
				
			||||||
 | 
					    LocaleTimeFormat24h,
 | 
				
			||||||
 | 
					    LocaleTimeFormat12h,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void time_format_changed(VariableItem* item) {
 | 
				
			||||||
 | 
					    // SystemSettings* app = variable_item_get_context(item);
 | 
				
			||||||
 | 
					    uint8_t index = variable_item_get_current_value_index(item);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, time_format_text[index]);
 | 
				
			||||||
 | 
					    locale_set_time_format(time_format_value[index]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const char* const date_format_text[] = {
 | 
				
			||||||
 | 
					    "D/M/Y",
 | 
				
			||||||
 | 
					    "M/D/Y",
 | 
				
			||||||
 | 
					    "Y/M/D",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uint32_t date_format_value[] = {
 | 
				
			||||||
 | 
					    LocaleDateFormatDMY,
 | 
				
			||||||
 | 
					    LocaleDateFormatMDY,
 | 
				
			||||||
 | 
					    LocaleDateFormatYMD,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void date_format_changed(VariableItem* item) {
 | 
				
			||||||
 | 
					    // SystemSettings* app = variable_item_get_context(item);
 | 
				
			||||||
 | 
					    uint8_t index = variable_item_get_current_value_index(item);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, date_format_text[index]);
 | 
				
			||||||
 | 
					    locale_set_date_format(date_format_value[index]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static uint32_t system_settings_exit(void* context) {
 | 
					static uint32_t system_settings_exit(void* context) {
 | 
				
			||||||
    UNUSED(context);
 | 
					    UNUSED(context);
 | 
				
			||||||
    return VIEW_NONE;
 | 
					    return VIEW_NONE;
 | 
				
			||||||
@ -91,6 +145,31 @@ SystemSettings* system_settings_alloc() {
 | 
				
			|||||||
    uint8_t value_index;
 | 
					    uint8_t value_index;
 | 
				
			||||||
    app->var_item_list = variable_item_list_alloc();
 | 
					    app->var_item_list = variable_item_list_alloc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    item = variable_item_list_add(
 | 
				
			||||||
 | 
					        app->var_item_list,
 | 
				
			||||||
 | 
					        "Units",
 | 
				
			||||||
 | 
					        COUNT_OF(mesurement_units_text),
 | 
				
			||||||
 | 
					        mesurement_units_changed,
 | 
				
			||||||
 | 
					        app);
 | 
				
			||||||
 | 
					    value_index = value_index_uint32(
 | 
				
			||||||
 | 
					        locale_get_measurement_unit(), mesurement_units_value, COUNT_OF(mesurement_units_value));
 | 
				
			||||||
 | 
					    variable_item_set_current_value_index(item, value_index);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, mesurement_units_text[value_index]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    item = variable_item_list_add(
 | 
				
			||||||
 | 
					        app->var_item_list, "Time Format", COUNT_OF(time_format_text), time_format_changed, app);
 | 
				
			||||||
 | 
					    value_index = value_index_uint32(
 | 
				
			||||||
 | 
					        locale_get_time_format(), time_format_value, COUNT_OF(time_format_value));
 | 
				
			||||||
 | 
					    variable_item_set_current_value_index(item, value_index);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, time_format_text[value_index]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    item = variable_item_list_add(
 | 
				
			||||||
 | 
					        app->var_item_list, "Date Format", COUNT_OF(date_format_text), date_format_changed, app);
 | 
				
			||||||
 | 
					    value_index = value_index_uint32(
 | 
				
			||||||
 | 
					        locale_get_date_format(), date_format_value, COUNT_OF(date_format_value));
 | 
				
			||||||
 | 
					    variable_item_set_current_value_index(item, value_index);
 | 
				
			||||||
 | 
					    variable_item_set_current_value_text(item, date_format_text[value_index]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    item = variable_item_list_add(
 | 
					    item = variable_item_list_add(
 | 
				
			||||||
        app->var_item_list, "Log Level", COUNT_OF(log_level_text), log_level_changed, app);
 | 
					        app->var_item_list, "Log Level", COUNT_OF(log_level_text), log_level_changed, app);
 | 
				
			||||||
    value_index = value_index_uint32(
 | 
					    value_index = value_index_uint32(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_0.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_1.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_10.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_11.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_12.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_2.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_3.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_4.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_5.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_6.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_7.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_8.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/frame_9.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										23
									
								
								assets/dolphin/external/L1_Happy_holidays_128x64/meta.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					Filetype: Flipper Animation
 | 
				
			||||||
 | 
					Version: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Width: 128
 | 
				
			||||||
 | 
					Height: 64
 | 
				
			||||||
 | 
					Passive frames: 10
 | 
				
			||||||
 | 
					Active frames: 18
 | 
				
			||||||
 | 
					Frames order: 0 1 2 1 0 1 2 1 0 1 2 3 4 5 6 5 4 7 2 8 9 10 11 10 9 10 11 12
 | 
				
			||||||
 | 
					Active cycles: 1
 | 
				
			||||||
 | 
					Frame rate: 2
 | 
				
			||||||
 | 
					Duration: 3600
 | 
				
			||||||
 | 
					Active cooldown: 7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Bubble slots: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Slot: 0
 | 
				
			||||||
 | 
					X: 11
 | 
				
			||||||
 | 
					Y: 19
 | 
				
			||||||
 | 
					Text:  HAPPY\nHOLIDAYS!
 | 
				
			||||||
 | 
					AlignH: Right
 | 
				
			||||||
 | 
					AlignV: Center
 | 
				
			||||||
 | 
					StartFrame: 22
 | 
				
			||||||
 | 
					EndFrame: 27
 | 
				
			||||||
							
								
								
									
										13
									
								
								assets/dolphin/external/manifest.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -36,10 +36,10 @@ Min level: 1
 | 
				
			|||||||
Max level: 1
 | 
					Max level: 1
 | 
				
			||||||
Weight: 3
 | 
					Weight: 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Name: L2_Wake_up_128x64
 | 
					Name: L1_Happy_holidays_128x64
 | 
				
			||||||
Min butthurt: 0
 | 
					Min butthurt: 0
 | 
				
			||||||
Max butthurt: 12
 | 
					Max butthurt: 14
 | 
				
			||||||
Min level: 2
 | 
					Min level: 1
 | 
				
			||||||
Max level: 3
 | 
					Max level: 3
 | 
				
			||||||
Weight: 4
 | 
					Weight: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -92,6 +92,13 @@ Min level: 1
 | 
				
			|||||||
Max level: 3
 | 
					Max level: 3
 | 
				
			||||||
Weight: 3
 | 
					Weight: 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name: L2_Wake_up_128x64
 | 
				
			||||||
 | 
					Min butthurt: 0
 | 
				
			||||||
 | 
					Max butthurt: 12
 | 
				
			||||||
 | 
					Min level: 2
 | 
				
			||||||
 | 
					Max level: 3
 | 
				
			||||||
 | 
					Weight: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Name: L2_Furippa2_128x64
 | 
					Name: L2_Furippa2_128x64
 | 
				
			||||||
Min butthurt: 0
 | 
					Min butthurt: 0
 | 
				
			||||||
Max butthurt: 6
 | 
					Max butthurt: 6
 | 
				
			||||||
 | 
				
			|||||||
@ -244,6 +244,7 @@ FEE470A4CB58
 | 
				
			|||||||
1F1A0A111B5B
 | 
					1F1A0A111B5B
 | 
				
			||||||
1F1FFE000000
 | 
					1F1FFE000000
 | 
				
			||||||
2031D1E57A3B
 | 
					2031D1E57A3B
 | 
				
			||||||
 | 
					# HID Key B
 | 
				
			||||||
204752454154
 | 
					204752454154
 | 
				
			||||||
21A600056CB0
 | 
					21A600056CB0
 | 
				
			||||||
22729A9BD40F
 | 
					22729A9BD40F
 | 
				
			||||||
@ -292,6 +293,7 @@ FEE470A4CB58
 | 
				
			|||||||
45635EF66EF3
 | 
					45635EF66EF3
 | 
				
			||||||
476242304C53
 | 
					476242304C53
 | 
				
			||||||
484558414354
 | 
					484558414354
 | 
				
			||||||
 | 
					# HID Key A
 | 
				
			||||||
484944204953
 | 
					484944204953
 | 
				
			||||||
484A57696F4A
 | 
					484A57696F4A
 | 
				
			||||||
48734389EDC3
 | 
					48734389EDC3
 | 
				
			||||||
@ -1226,6 +1228,7 @@ C41514DEFC07
 | 
				
			|||||||
ABFEDC124578
 | 
					ABFEDC124578
 | 
				
			||||||
046154274C11
 | 
					046154274C11
 | 
				
			||||||
5429D67E1F57
 | 
					5429D67E1F57
 | 
				
			||||||
 | 
					# SMARTair Key B
 | 
				
			||||||
E7316853E731
 | 
					E7316853E731
 | 
				
			||||||
CD7FFFF81C4A
 | 
					CD7FFFF81C4A
 | 
				
			||||||
F253C30568C4
 | 
					F253C30568C4
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										47
									
								
								assets/resources/picopass/assets/iclass_elite_dict.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					## From https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/iclass_default_keys.dic
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# AA1
 | 
				
			||||||
 | 
					AEA684A6DAB23278
 | 
				
			||||||
 | 
					# key1/Kc from PicoPass 2k documentation
 | 
				
			||||||
 | 
					7665544332211000
 | 
				
			||||||
 | 
					# SAGEM
 | 
				
			||||||
 | 
					0123456789ABCDEF
 | 
				
			||||||
 | 
					# from loclass demo file.
 | 
				
			||||||
 | 
					5b7c62c491c11b39
 | 
				
			||||||
 | 
					# Kd from PicoPass 2k documentation
 | 
				
			||||||
 | 
					F0E1D2C3B4A59687
 | 
				
			||||||
 | 
					# PicoPass Default Exchange Key
 | 
				
			||||||
 | 
					5CBCF1DA45D5FB4F
 | 
				
			||||||
 | 
					# From HID multiclassSE reader
 | 
				
			||||||
 | 
					31ad7ebd2f282168
 | 
				
			||||||
 | 
					# From pastebin: https://pastebin.com/uHqpjiuU
 | 
				
			||||||
 | 
					6EFD46EFCBB3C875
 | 
				
			||||||
 | 
					E033CA419AEE43F9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# iCopy-x DRM keys
 | 
				
			||||||
 | 
					# iCL tags
 | 
				
			||||||
 | 
					2020666666668888
 | 
				
			||||||
 | 
					# iCS tags reversed from the SOs
 | 
				
			||||||
 | 
					6666202066668888
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# default picopass KD / Page 0 / Book 1
 | 
				
			||||||
 | 
					FDCB5A52EA8F3090
 | 
				
			||||||
 | 
					237FF9079863DF44
 | 
				
			||||||
 | 
					5ADC25FB27181D32
 | 
				
			||||||
 | 
					83B881F2936B2E49
 | 
				
			||||||
 | 
					43644E61EE866BA5
 | 
				
			||||||
 | 
					897034143D016080
 | 
				
			||||||
 | 
					82D17B44C0122963
 | 
				
			||||||
 | 
					4895CA7DE65E2025
 | 
				
			||||||
 | 
					DADAD4C57BE271B7
 | 
				
			||||||
 | 
					E41E9EDEF5719ABF
 | 
				
			||||||
 | 
					293D275EC3AF9C7F
 | 
				
			||||||
 | 
					C3C169251B8A70FB
 | 
				
			||||||
 | 
					F41DAF58B20C8B91
 | 
				
			||||||
 | 
					28877A609EC0DD2B
 | 
				
			||||||
 | 
					66584C91EE80D5E5
 | 
				
			||||||
 | 
					C1B74D7478053AE2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# default iCLASS RFIDeas
 | 
				
			||||||
 | 
					6B65797374726B72
 | 
				
			||||||
							
								
								
									
										8
									
								
								assets/unit_tests/subghz/smc5326.sub
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					Filetype: Flipper SubGhz Key File
 | 
				
			||||||
 | 
					Version: 1
 | 
				
			||||||
 | 
					Frequency: 433920000
 | 
				
			||||||
 | 
					Preset: FuriHalSubGhzPresetOok650Async
 | 
				
			||||||
 | 
					Protocol: SMC5326
 | 
				
			||||||
 | 
					Bit: 25
 | 
				
			||||||
 | 
					Key: 00 00 00 00 01 7D 55 80
 | 
				
			||||||
 | 
					TE: 210
 | 
				
			||||||
							
								
								
									
										7
									
								
								assets/unit_tests/subghz/smc5326_raw.sub
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					Filetype: Flipper SubGhz RAW File
 | 
				
			||||||
 | 
					Version: 1
 | 
				
			||||||
 | 
					Frequency: 433920000
 | 
				
			||||||
 | 
					Preset: FuriHalSubGhzPresetOok650Async
 | 
				
			||||||
 | 
					Protocol: RAW
 | 
				
			||||||
 | 
					RAW_Data: 2442 -312 275 -972 949 -310 941 -322 923 -342 921 -352 923 -334 281 -954 945 -350 279 -958 907 -354 289 -980 909 -352 281 -962 907 -330 311 -964 913 -350 317 -930 933 -344 921 -352 893 -330 311 -954 943 -318 315 -958 909 -324 947 -7854 953 -322 289 -948 939 -354 927 -332 911 -324 943 -344 917 -318 317 -964 905 -344 303 -942 947 -312 319 -960 913 -348 281 -958 941 -322 295 -978 905 -350 279 -962 931 -328 947 -324 939 -346 267 -964 935 -348 283 -938 953 -318 931 -7868 935 -346 269 -968 953 -310 941 -322 921 -330 935 -342 931 -318 311 -962 939 -290 337 -950 909 -352 317 -924 943 -324 313 -938 941 -318 317 -932 939 -344 301 -938 933 -350 921 -322 959 -310 301 -942 933 -352 317 -926 957 -314 919 -7868 943 -314 317 -958 909 -322 951 -344 919 -352 921 -324 937 -326 281 -964 941 -318 317 -930 939 -344 301 -938 933 -352 281 -962 953 -314 317 -922 933 -330 315 -954 943 -318 921 -342 943 -320 291 -980 909 -354 281 -962 943 -296 967 -7836 943 -332 309 -950 935 -318 929 -340 943 -320 921 -344 921 -354 283 -960 943 -296 309 -964 945 -318 279 -964 941 -322 333 -944 939 -314 279 -992 903 -342 319 -932 933 -330 931 -340 929 -348 281 -964 935 -334 281 -970 927 -346 921 -7862 951 -314 319 -922 953 -320 923 -346 921 -320 965 -298 943 -324 313 -942 941 -320 317 -930 941 -344 303 -940 945 -312 321 -940 953 -314 303 -960 933 -348 287 -962 911 -352 917 -350 905 -324 333 -918 971 -322 317 -924 945 -324 937 -7872 919 -324 317 -942 941 -318 933 -330 943 -324 943 -310 951 -318 317 -930 939 -344 301 -938 933 -352 317 -926 953 -314 319 -924 939 -324 331 -950 907 -354 315 -926 945 -324 939 -312 953 -318 317 -930 937 -344 301 -940 947 -348 909 -7864 949 -310 319 -956 915 -350 919 -348 905 -322 963 -296 935 -348 317 -922 951 -322 295 -976 939 -314 281 -996 915 -326 307 -940 959 -310 301 -966 935 -346 285 -958 915 -348 921 -348 903 -354 303 -948 911 -350 315 -926 945 -324 941 -7874 943 -290 319 -942 973 -318 929 -314 937 -328 941 -324 939 -310 303 -962 933 -352 285 -962 949 -314 319 -924 951 -320 293 -948 941 -354 283 -962 943 -294 309 -966 943 -320 931 -328 943 -326 311 -940 939 -320 309 -958 933 -338 943 -7840 933 -352 277 -964 941 -322 923 -344 923 -350 931 -310 955 -320 291 -974 907 -350 281 -958 963 -298 313 -956 945 -314 311 -960 937 -312 311 -966 909 -324 319 -944 941 -354 929 -298 945 -324 315 -940
 | 
				
			||||||
 | 
					RAW_Data: 943 -354 281 -964 905 -330 933 -7868 951 -324 315 -938 943 -354 893 -330 943 -324 943 -344 919 -318 317 -962 903 -344 301 -974 903 -350 317 -932 931 -342 269 -972 949 -346 285 -938 955 -310 301 -964 935 -348 921 -320 921 -344 301 -940 935 -350 317 -930 929 -318 937 -7872 939 -344 301 -940 947 -346 917 -322 921 -344 923 -352 927 -334 281 -970 925 -334 277 -982 943 -318 317 -932 931 -344 301 -936 935 -350 281 -960 957 -312 303 -960 935 -346 907 -322 929 -344 301 -942 935 -350 317 -924 955 -312 951 -7858 919 -342 309 -940 949 -348 909 -322 923 -344 923 -352 923 -336 317 -924 945 -312 311 -966 921 -340 317 -924 947 -350 281 -958 941 -322 291 -976 905 -350 279 -960 935 -342 943 -320 919 -330 311 -958 943 -320 315 -932 935 -344 919 -7866 957 -312 303 -964 917 -342 945 -320 923 -344 923 -354 929 -298 315 -956 941 -318 315 -960 911 -324 317 -942 939 -354 281 -964 941 -294 311 -968 943 -318 317 -932 937 -330 931 -350 919 -348 283 -960 917 -350 317 -922 939 -322 965 -7864 921 -324 329 -950 909 -354 923 -336 913 -322 947 -344 919 -354 281 -962 941 -294 311 -960 935 -354 281 -962 939 -294 311 -964 937 -354 281 -964 941 -296 309 -964 939 -318 931 -330 945 -324 315 -940 939 -354 281 -964 909 -344 921 -7862 963 -304 307 -976 933 -320 929 -328 941 -324 939 -348 915 -320 317 -930 939 -344 301 -940 965 -320 319 -926 953 -312 303 -960 933 -312 321 -960 913 -348 319 -924 943 -320 959 -310 921 -354 319 -924 943 -324 311 -938 941 -318 957 -7862 943 -318 317 -932 933 -344 925 -352 897 -332 943 -324 943 -346 267 -966 951 -310 321 -960 911 -350 281 -958 949 -320 291 -978 937 -316 279 -964 949 -326 309 -944 943 -314 959 -318 933 -336 317 -934 933 -344 267 -964 937 -350 905 -7896 943 -318 319 -926 955 -314 919 -350 935 -324 941 -294 967 -312 303 -962 933 -348 285 -960 917 -348 317 -922 941 -322 329 -950 907 -354 315 -926 943 -326 313 -940 941 -352 893 -332 949 -324 315 -938 941 -352 283 -962 943 -310 925 -7890 931 -344 269 -968 949 -310 943 -320 923 -350 937 -310 955 -318 317 -930 935 -344 301 -942 947 -346 285 -958 915 -346 317 -924 951 -322 295 -982 905 -352 317 -924 945 -324 941 -346 917 -318 317 -962 905 -330 311 -956 937 -352 897 -7878 939 -354 283 -960 941 -294 965 -312 953 -318 385 -201512 165 -198 265 -526 229 -298 755 -164 61687 -17310 131 -1056 99 -296 195 -296 65 -66 1617
 | 
				
			||||||
@ -166,3 +166,5 @@ RAW_Data: 317 -144 57 -486 53 -282 115 -585 97 -72 229 -174 257 -440 225 -86 173
 | 
				
			|||||||
RAW_Data: 191 -240 85 -72 637 -408 213 -510 261 -168 143 -126 79 -106 167 -72 117 -218 251 -168 119 -96 215 -182 191 -238 517 -116 201 -144 255 -154 97 -94 215 -72 95 -120 71 -288 261 -106 434 -96 606 -232 229 -432 85 -174 343 -58 329 -156 55 -116 259 -144 488 -56 307 -339 115 -202 334 -88 113 -86 57 -174 143 -144 401 -376 85 -240 267 -82 95 -216 137 -158 85 -144 143 -58 221 -308 295 -114 87 -114 301 -120 358 -517 71 -262 191 -144 57 -140 165 -407 53 -262 217 -120 238 -358 119 -357 71 -72 119 -96 428 -72 95 -72 167 -72 93 -240 335 -96 357 -240 173 -230 143 -114 87 -200 143 -232 287 -150 97 -288 71 -72 93 -288 115 -58 143 -230 109 -264 71 -72 119 -72 238 -242 97 -78 163 -86 115 -518 79 -560 205 -449 969 -144 507 -86 231 -114 345 -58 979 -110 85 -288 287 -404 229 -202 57 -274 233 -86 115 -202 632 -230 85 -312 369 -392 460 -450 75 -280 85 -202 201 -86 229 -174 143 -144 233 -528 115 -212 127 -202 287 -172 403 -172 139 -128 165 -138 261 -392 143 -480 142 -189 291 -80 53 -283 167 -140 113 -1008 191 -144 119 -120 71 -193 241 -462 201 -58 143 -344 539 -316 113 -174 85 -116 113 -250 239 -168 405 -168 239 -158 85 -144 115 -86 57 -86 341 -144 171 -202 85 -202 115 -114 719 -88 55 -318 257 -56 254 -86 171 -116 459 -174 171 -329 95 -134 85 -314 431 -306 77 -316 401 -86 173 -404 281 -1073 488 -94 217 -78 101 -98 214 -120 215 -340 403 -535 143 -564 115 -116 199 -58 85 -174 315 -58 335 -136 55 -260 143 -144 229 -460 143 -58 143 -144 171 -202 115 -374 291 -130 339 -82 143 -58 171 -58 201 -86 85 -174 1022 -56 85 -82 255 -240 103 -202 431 -278 95 -216 119 -72 71 -96 71 -559 57 -144 171 -88 113 -86 231 -414 131 -192 237 -360 95 -168 145 -168 213 -120 167 -96 143 -110 57 -86 259 -56 87 -777 295 -96 57 -86 173 -86 171 -404 143 -172 231 -200 57 -441 55 -58 173 -56 87 -86 171 -72 287 -72 119 -262 119 -144 71 -72 121 -310 71 -302 113 -54 193 -80 307 -58 257 -232 143 -56 143 -116 219 -72 695 -70 71 -460 85 -232 719 -363 57 -402 604 -230 287 -138 83 -172 259 -58 171 -174 55 -88 489 -114 143 -116 171 -116 143 -58 199 -144 145 -343 374 -186 235 -140 77 -86 143 -202 143 -144 113 -144 143 -58 732 -96 263 -264 71 -206 95 -168 215 -144 271 -80 139 -88 85 -414 75 -100
 | 
					RAW_Data: 191 -240 85 -72 637 -408 213 -510 261 -168 143 -126 79 -106 167 -72 117 -218 251 -168 119 -96 215 -182 191 -238 517 -116 201 -144 255 -154 97 -94 215 -72 95 -120 71 -288 261 -106 434 -96 606 -232 229 -432 85 -174 343 -58 329 -156 55 -116 259 -144 488 -56 307 -339 115 -202 334 -88 113 -86 57 -174 143 -144 401 -376 85 -240 267 -82 95 -216 137 -158 85 -144 143 -58 221 -308 295 -114 87 -114 301 -120 358 -517 71 -262 191 -144 57 -140 165 -407 53 -262 217 -120 238 -358 119 -357 71 -72 119 -96 428 -72 95 -72 167 -72 93 -240 335 -96 357 -240 173 -230 143 -114 87 -200 143 -232 287 -150 97 -288 71 -72 93 -288 115 -58 143 -230 109 -264 71 -72 119 -72 238 -242 97 -78 163 -86 115 -518 79 -560 205 -449 969 -144 507 -86 231 -114 345 -58 979 -110 85 -288 287 -404 229 -202 57 -274 233 -86 115 -202 632 -230 85 -312 369 -392 460 -450 75 -280 85 -202 201 -86 229 -174 143 -144 233 -528 115 -212 127 -202 287 -172 403 -172 139 -128 165 -138 261 -392 143 -480 142 -189 291 -80 53 -283 167 -140 113 -1008 191 -144 119 -120 71 -193 241 -462 201 -58 143 -344 539 -316 113 -174 85 -116 113 -250 239 -168 405 -168 239 -158 85 -144 115 -86 57 -86 341 -144 171 -202 85 -202 115 -114 719 -88 55 -318 257 -56 254 -86 171 -116 459 -174 171 -329 95 -134 85 -314 431 -306 77 -316 401 -86 173 -404 281 -1073 488 -94 217 -78 101 -98 214 -120 215 -340 403 -535 143 -564 115 -116 199 -58 85 -174 315 -58 335 -136 55 -260 143 -144 229 -460 143 -58 143 -144 171 -202 115 -374 291 -130 339 -82 143 -58 171 -58 201 -86 85 -174 1022 -56 85 -82 255 -240 103 -202 431 -278 95 -216 119 -72 71 -96 71 -559 57 -144 171 -88 113 -86 231 -414 131 -192 237 -360 95 -168 145 -168 213 -120 167 -96 143 -110 57 -86 259 -56 87 -777 295 -96 57 -86 173 -86 171 -404 143 -172 231 -200 57 -441 55 -58 173 -56 87 -86 171 -72 287 -72 119 -262 119 -144 71 -72 121 -310 71 -302 113 -54 193 -80 307 -58 257 -232 143 -56 143 -116 219 -72 695 -70 71 -460 85 -232 719 -363 57 -402 604 -230 287 -138 83 -172 259 -58 171 -174 55 -88 489 -114 143 -116 171 -116 143 -58 199 -144 145 -343 374 -186 235 -140 77 -86 143 -202 143 -144 113 -144 143 -58 732 -96 263 -264 71 -206 95 -168 215 -144 271 -80 139 -88 85 -414 75 -100
 | 
				
			||||||
RAW_Data: 285 -96 627 -362 53 -84 201 -374 113 -202 115 -202 421 -316 85 -58 139 -224 87 -86 229 -58 243 -178 267 -288 95 -336 171 -96 213 -288 71 -405 95 -96 95 -384 95 -72 213 -72 95 -96 95 -272 87 -1083 85 -58 113 -88 257 -116 143 -292 175 -318 95 -120 95 -144 95 -72 71 -216 368 -116 373 -172 115 -58 85 -116 143 -86 85 -144 201 -86 201 -202 257 -144 201 -174 113 -144 115 -144 257 -202 585 -364 173 -138 287 -422 431 -86 85 -96 869 -186 95 -52 115 -86 115 -58 55 -276 365 -86 85 -489 171 -140 577 -106 718 -144 391 -232 195 -82 143 -172 109 -120 167 -96 280 -216 145 -240 215 -186 163 -96 141 -172 159 -603 257 -108 629 -192 119 -80 87 -172 57 -144 286 -86 57 -230 344 -58 113 -537 75 -96 537 -86 403 -196 167 -264 119 -238 119 -120 167 -96 95 -478 95 -120 167 -216 1085 -96 358 -72 263 -72 69 -120 143 -96 71 -96 191 -362 55 -144 57 -260 113 -58 85 -174 55 -88 257 -86 231 -194 55 -58 115 -56 55 -339 55 -58 374 -172 139 -82 419 -98 119 -261 71 -72 71 -240 713 -86 143 -218 295 -72 53 -56 431 -58 317 -144 161 -144 373 -144 173 -144 57 -114 85 -116 195 -72 708 -172 115 -86 191 -96 506 -120 71 -174 85 -58 363 -114 317 -230 316 -200 87 -114 57 -230 115 -315 173 -280 694 -212 453 -256 143 -202 113 -540 352 -116 257 -116 457 -56 109 -58 143 -230 259 -144 259 -525 119 -408 247 -112 389 -72 431 -96 137 -236 97 -474 201 -298 71 -82 55 -116 55 -112 199 -174 191 -86 143 -144 115 -114 317 -86 85 -230 87 -114 259 -84 107 -130 143 -94 153 -86 135 -94 215 -72 239 -94 435 -96 263 -142 166 -334 87 -194 179 -96 115 -284 135 -56 57 -144 463 -204 143 -316 201 -58 403 -86 141 -288 85 -202 139 -397 171 -174 305 -202 85 -144 373 -253 161 -492 181 -191 95 -216 315 -191 71 -166 97 -126 337 -96 71 -96 189 -168 295 -84 197 -86 259 -345 137 -144 167 -796 115 -344 455 -72 119 -96 119 -550 209 -88 85 -86 143 -340 167 -260 143 -537 85 -226 51 -537 57 -260 315 -461 51 -84 199 -358 383 -96 143 -257 115 -86 173 -86 201 -144 143 -316 85 -86 479 -88 85 -72 71 -104 115 -116 267 -72 137 -144 143 -116 85 -86 373 -288 115 -200 87 -114 259 -114 259 -462 143 -144 171 -86 57 -58 137 -144 57 -634 343 -72 205 -86 143 -258 57 -232 113 -230 461 -58 185 -74 537 -86
 | 
					RAW_Data: 285 -96 627 -362 53 -84 201 -374 113 -202 115 -202 421 -316 85 -58 139 -224 87 -86 229 -58 243 -178 267 -288 95 -336 171 -96 213 -288 71 -405 95 -96 95 -384 95 -72 213 -72 95 -96 95 -272 87 -1083 85 -58 113 -88 257 -116 143 -292 175 -318 95 -120 95 -144 95 -72 71 -216 368 -116 373 -172 115 -58 85 -116 143 -86 85 -144 201 -86 201 -202 257 -144 201 -174 113 -144 115 -144 257 -202 585 -364 173 -138 287 -422 431 -86 85 -96 869 -186 95 -52 115 -86 115 -58 55 -276 365 -86 85 -489 171 -140 577 -106 718 -144 391 -232 195 -82 143 -172 109 -120 167 -96 280 -216 145 -240 215 -186 163 -96 141 -172 159 -603 257 -108 629 -192 119 -80 87 -172 57 -144 286 -86 57 -230 344 -58 113 -537 75 -96 537 -86 403 -196 167 -264 119 -238 119 -120 167 -96 95 -478 95 -120 167 -216 1085 -96 358 -72 263 -72 69 -120 143 -96 71 -96 191 -362 55 -144 57 -260 113 -58 85 -174 55 -88 257 -86 231 -194 55 -58 115 -56 55 -339 55 -58 374 -172 139 -82 419 -98 119 -261 71 -72 71 -240 713 -86 143 -218 295 -72 53 -56 431 -58 317 -144 161 -144 373 -144 173 -144 57 -114 85 -116 195 -72 708 -172 115 -86 191 -96 506 -120 71 -174 85 -58 363 -114 317 -230 316 -200 87 -114 57 -230 115 -315 173 -280 694 -212 453 -256 143 -202 113 -540 352 -116 257 -116 457 -56 109 -58 143 -230 259 -144 259 -525 119 -408 247 -112 389 -72 431 -96 137 -236 97 -474 201 -298 71 -82 55 -116 55 -112 199 -174 191 -86 143 -144 115 -114 317 -86 85 -230 87 -114 259 -84 107 -130 143 -94 153 -86 135 -94 215 -72 239 -94 435 -96 263 -142 166 -334 87 -194 179 -96 115 -284 135 -56 57 -144 463 -204 143 -316 201 -58 403 -86 141 -288 85 -202 139 -397 171 -174 305 -202 85 -144 373 -253 161 -492 181 -191 95 -216 315 -191 71 -166 97 -126 337 -96 71 -96 189 -168 295 -84 197 -86 259 -345 137 -144 167 -796 115 -344 455 -72 119 -96 119 -550 209 -88 85 -86 143 -340 167 -260 143 -537 85 -226 51 -537 57 -260 315 -461 51 -84 199 -358 383 -96 143 -257 115 -86 173 -86 201 -144 143 -316 85 -86 479 -88 85 -72 71 -104 115 -116 267 -72 137 -144 143 -116 85 -86 373 -288 115 -200 87 -114 259 -114 259 -462 143 -144 171 -86 57 -58 137 -144 57 -634 343 -72 205 -86 143 -258 57 -232 113 -230 461 -58 185 -74 537 -86
 | 
				
			||||||
RAW_Data: 535 -142 57 -58 55 -116 115 -432 85 -172 259 -192 167 -120 117 -72 119 -240 334 -72 71 -267 285 -144 119 -374 85 -88 85 -114 143 -202 229 -58 143 -202 115 -202 171 -86 71 -144 87 -56 173 -373 143 -116 113 -462 169 -80 215 -148 115 -336 85 -230 163 -432 85 -374 639 -174 85 -58 57 -82 295 -352 269 -532 414 -322 95 -287 263 -268 115 -56 259 -76 85 -282 401 -305 516 -114 115 -202 171 -86 451 -110 85 -346 201 -274 149 -202 85 -364 366 -258 57 -114 259 -172 142 -144 85 -116 85 -480 171 -144 57 -352 115 -116 535 -404 315 -202 163 -158 517 -316 215 -98 85 -346 85 -144 87 -86 257 -82 167 -58 85 -116 113 -894 233 -186 77 -266 147 -72 71 -82 57 -86 171 -58 57 -86 201 -364 143 -202 115 -114 85 -88 113 -86 87 -230 57 -76 613 -72 85 -96 209 -346 458 -58 547 -490 201 -315 315 -116 75 -168 359 -335 95 -384 93 -120 71 -312 251 -366 233 -96 189 -240 263 -192 271 -58 115 -58 229 -346 459 -174 113 -144 173 -144 218 -224 57 -116 215 -72 103 -202 513 -210 433 -116 113 -174 650 -273 147 -450 375 -86 115 -172 536 -84 85 -230 85 -58 195 -468 287 -110 551 -214 167 -311 213 -250 85 -58 85 -355 113 -230 115 -144 117 -288 195 -202 57 -376 123 -144 236 -168 553 -284 119 -72 143 -188 161 -120 93 -312 335 -58 55 -260 105 -244 143 -120 381 -268 173 -268 635 -168 453 -318 71 -167 71 -406 191 -172 215 -408 119 -144 93 -120 97 -130 143 -192 308 -122 147 -550 313 -96 139 -162 167 -96 431 -80 83 -112 201 -86 287 -86 229 -116 57 -288 113 -174 143 -116 113 -144 115 -518 57 -230 57 -172 231 -86 113 -314 183 -144 119 -72 165 -446 81 -86 135 -190 143 -96 71 -72 411 -96 143 -120 69 -216 349 -72 95 -96 517 -646 163 -86 113 -116 171 -116 143 -116 113 -287 259 -114 517 -168 141 -116 105 -72 95 -96 311 -118 159 -310 191 -54 143 -258 115 -450 219 -54 339 -372 239 -72 167 -174 113 -58 57 -144 259 -172 143 -336 113 -174 85 -230 83 -668 85 -202 113 -144 57 -116 373 -316 719 -288 115 -58 75 -120 139 -144 229 -144 57 -144 171 -192 391 -202 403 -58 315 -188 259 -56 115 -144 85 -404 57 -58 105 -102 429 -406 81 -172 57 -144 287 -230 287 -220 317 -458 283 -58 113 -86 269 -72 281 -58 85 -202 113 -52 421 -58 229 -480 259 -58 143 -660 155 -638 123 -86 57 -86 143 -346 143 -144 57 -144
 | 
					RAW_Data: 535 -142 57 -58 55 -116 115 -432 85 -172 259 -192 167 -120 117 -72 119 -240 334 -72 71 -267 285 -144 119 -374 85 -88 85 -114 143 -202 229 -58 143 -202 115 -202 171 -86 71 -144 87 -56 173 -373 143 -116 113 -462 169 -80 215 -148 115 -336 85 -230 163 -432 85 -374 639 -174 85 -58 57 -82 295 -352 269 -532 414 -322 95 -287 263 -268 115 -56 259 -76 85 -282 401 -305 516 -114 115 -202 171 -86 451 -110 85 -346 201 -274 149 -202 85 -364 366 -258 57 -114 259 -172 142 -144 85 -116 85 -480 171 -144 57 -352 115 -116 535 -404 315 -202 163 -158 517 -316 215 -98 85 -346 85 -144 87 -86 257 -82 167 -58 85 -116 113 -894 233 -186 77 -266 147 -72 71 -82 57 -86 171 -58 57 -86 201 -364 143 -202 115 -114 85 -88 113 -86 87 -230 57 -76 613 -72 85 -96 209 -346 458 -58 547 -490 201 -315 315 -116 75 -168 359 -335 95 -384 93 -120 71 -312 251 -366 233 -96 189 -240 263 -192 271 -58 115 -58 229 -346 459 -174 113 -144 173 -144 218 -224 57 -116 215 -72 103 -202 513 -210 433 -116 113 -174 650 -273 147 -450 375 -86 115 -172 536 -84 85 -230 85 -58 195 -468 287 -110 551 -214 167 -311 213 -250 85 -58 85 -355 113 -230 115 -144 117 -288 195 -202 57 -376 123 -144 236 -168 553 -284 119 -72 143 -188 161 -120 93 -312 335 -58 55 -260 105 -244 143 -120 381 -268 173 -268 635 -168 453 -318 71 -167 71 -406 191 -172 215 -408 119 -144 93 -120 97 -130 143 -192 308 -122 147 -550 313 -96 139 -162 167 -96 431 -80 83 -112 201 -86 287 -86 229 -116 57 -288 113 -174 143 -116 113 -144 115 -518 57 -230 57 -172 231 -86 113 -314 183 -144 119 -72 165 -446 81 -86 135 -190 143 -96 71 -72 411 -96 143 -120 69 -216 349 -72 95 -96 517 -646 163 -86 113 -116 171 -116 143 -116 113 -287 259 -114 517 -168 141 -116 105 -72 95 -96 311 -118 159 -310 191 -54 143 -258 115 -450 219 -54 339 -372 239 -72 167 -174 113 -58 57 -144 259 -172 143 -336 113 -174 85 -230 83 -668 85 -202 113 -144 57 -116 373 -316 719 -288 115 -58 75 -120 139 -144 229 -144 57 -144 171 -192 391 -202 403 -58 315 -188 259 -56 115 -144 85 -404 57 -58 105 -102 429 -406 81 -172 57 -144 287 -230 287 -220 317 -458 283 -58 113 -86 269 -72 281 -58 85 -202 113 -52 421 -58 229 -480 259 -58 143 -660 155 -638 123 -86 57 -86 143 -346 143 -144 57 -144
 | 
				
			||||||
 | 
					RAW_Data: 2442 -312 275 -972 949 -310 941 -322 923 -342 921 -352 923 -334 281 -954 945 -350 279 -958 907 -354 289 -980 909 -352 281 -962 907 -330 311 -964 913 -350 317 -930 933 -344 921 -352 893 -330 311 -954 943 -318 315 -958 909 -324 947 -7854 953 -322 289 -948 939 -354 927 -332 911 -324 943 -344 917 -318 317 -964 905 -344 303 -942 947 -312 319 -960 913 -348 281 -958 941 -322 295 -978 905 -350 279 -962 931 -328 947 -324 939 -346 267 -964 935 -348 283 -938 953 -318 931 -7868 935 -346 269 -968 953 -310 941 -322 921 -330 935 -342 931 -318 311 -962 939 -290 337 -950 909 -352 317 -924 943 -324 313 -938 941 -318 317 -932 939 -344 301 -938 933 -350 921 -322 959 -310 301 -942 933 -352 317 -926 957 -314 919 -7868 943 -314 317 -958 909 -322 951 -344 919 -352 921 -324 937 -326 281 -964 941 -318 317 -930 939 -344 301 -938 933 -352 281 -962 953 -314 317 -922 933 -330 315 -954 943 -318 921 -342 943 -320 291 -980 909 -354 281 -962 943 -296 967 -7836 943 -332 309 -950 935 -318 929 -340 943 -320 921 -344 921 -354 283 -960 943 -296 309 -964 945 -318 279 -964 941 -322 333 -944 939 -314 279 -992 903 -342 319 -932 933 -330 931 -340 929 -348 281 -964 935 -334 281 -970 927 -346 921 -7862 951 -314 319 -922 953 -320 923 -346 921 -320 965 -298 943 -324 313 -942 941 -320 317 -930 941 -344 303 -940 945 -312 321 -940 953 -314 303 -960 933 -348 287 -962 911 -352 917 -350 905 -324 333 -918 971 -322 317 -924 945 -324 937 -7872 919 -324 317 -942 941 -318 933 -330 943 -324 943 -310 951 -318 317 -930 939 -344 301 -938 933 -352 317 -926 953 -314 319 -924 939 -324 331 -950 907 -354 315 -926 945 -324 939 -312 953 -318 317 -930 937 -344 301 -940 947 -348 909 -7864 949 -310 319 -956 915 -350 919 -348 905 -322 963 -296 935 -348 317 -922 951 -322 295 -976 939 -314 281 -996 915 -326 307 -940 959 -310 301 -966 935 -346 285 -958 915 -348 921 -348 903 -354 303 -948 911 -350 315 -926 945 -324 941 -7874 943 -290 319 -942 973 -318 929 -314 937 -328 941 -324 939 -310 303 -962 933 -352 285 -962 949 -314 319 -924 951 -320 293 -948 941 -354 283 -962 943 -294 309 -966 943 -320 931 -328 943 -326 311 -940 939 -320 309 -958 933 -338 943 -7840 933 -352 277 -964 941 -322 923 -344 923 -350 931 -310 955 -320 291 -974 907 -350 281 -958 963 -298 313 -956 945 -314 311 -960 937 -312 311 -966 909 -324 319 -944 941 -354 929 -298 945 -324 315 -940
 | 
				
			||||||
 | 
					RAW_Data: 943 -354 281 -964 905 -330 933 -7868 951 -324 315 -938 943 -354 893 -330 943 -324 943 -344 919 -318 317 -962 903 -344 301 -974 903 -350 317 -932 931 -342 269 -972 949 -346 285 -938 955 -310 301 -964 935 -348 921 -320 921 -344 301 -940 935 -350 317 -930 929 -318 937 -7872 939 -344 301 -940 947 -346 917 -322 921 -344 923 -352 927 -334 281 -970 925 -334 277 -982 943 -318 317 -932 931 -344 301 -936 935 -350 281 -960 957 -312 303 -960 935 -346 907 -322 929 -344 301 -942 935 -350 317 -924 955 -312 951 -7858 919 -342 309 -940 949 -348 909 -322 923 -344 923 -352 923 -336 317 -924 945 -312 311 -966 921 -340 317 -924 947 -350 281 -958 941 -322 291 -976 905 -350 279 -960 935 -342 943 -320 919 -330 311 -958 943 -320 315 -932 935 -344 919 -7866 957 -312 303 -964 917 -342 945 -320 923 -344 923 -354 929 -298 315 -956 941 -318 315 -960 911 -324 317 -942 939 -354 281 -964 941 -294 311 -968 943 -318 317 -932 937 -330 931 -350 919 -348 283 -960 917 -350 317 -922 939 -322 965 -7864 921 -324 329 -950 909 -354 923 -336 913 -322 947 -344 919 -354 281 -962 941 -294 311 -960 935 -354 281 -962 939 -294 311 -964 937 -354 281 -964 941 -296 309 -964 939 -318 931 -330 945 -324 315 -940 939 -354 281 -964 909 -344 921 -7862 963 -304 307 -976 933 -320 929 -328 941 -324 939 -348 915 -320 317 -930 939 -344 301 -940 965 -320 319 -926 953 -312 303 -960 933 -312 321 -960 913 -348 319 -924 943 -320 959 -310 921 -354 319 -924 943 -324 311 -938 941 -318 957 -7862 943 -318 317 -932 933 -344 925 -352 897 -332 943 -324 943 -346 267 -966 951 -310 321 -960 911 -350 281 -958 949 -320 291 -978 937 -316 279 -964 949 -326 309 -944 943 -314 959 -318 933 -336 317 -934 933 -344 267 -964 937 -350 905 -7896 943 -318 319 -926 955 -314 919 -350 935 -324 941 -294 967 -312 303 -962 933 -348 285 -960 917 -348 317 -922 941 -322 329 -950 907 -354 315 -926 943 -326 313 -940 941 -352 893 -332 949 -324 315 -938 941 -352 283 -962 943 -310 925 -7890 931 -344 269 -968 949 -310 943 -320 923 -350 937 -310 955 -318 317 -930 935 -344 301 -942 947 -346 285 -958 915 -346 317 -924 951 -322 295 -982 905 -352 317 -924 945 -324 941 -346 917 -318 317 -962 905 -330 311 -956 937 -352 897 -7878 939 -354 283 -960 941 -294 965 -312 953 -318 385 -201512 165 -198 265 -526 229 -298 755 -164 61687 -17310 131 -1056 99 -296 195 -296 65 -66 1617
 | 
				
			||||||
 | 
				
			|||||||