diff --git a/.clang-format b/.clang-format index c710347e7..e0b380671 100644 --- a/.clang-format +++ b/.clang-format @@ -1,14 +1,13 @@ --- BasedOnStyle: Chromium -IndentWidth: '4' -SortIncludes: 'false' -AllowAllArgumentsOnNextLine: 'true' +IndentWidth: 4 +SortIncludes: false +AllowAllArgumentsOnNextLine: true ColumnLimit: 0 -Cpp11BracedListStyle: 'true' -AllowShortLoopsOnASingleLine: 'true' -AllowShortIfStatementsOnASingleLine: 'true' +Cpp11BracedListStyle: true +AllowShortLoopsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: true SpacesInLineCommentPrefix: Minimum: 1 Maximum: 1 - ... diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..cfb5150f2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ + + +## Brief description what you did + + + +## Checklist +- [ ] Kept changes minimal and limited to necessary files +- [ ] Verified functionality remains intact and code compiles diff --git a/.github/workflows/check_formatting.yml b/.github/workflows/check_formatting.yml index 95f714913..c80daaecb 100644 --- a/.github/workflows/check_formatting.yml +++ b/.github/workflows/check_formatting.yml @@ -3,7 +3,7 @@ name: Check formatting on: [pull_request] jobs: - check_date: + check_format: runs-on: ubuntu-latest name: Check formatting strategy: @@ -13,12 +13,20 @@ jobs: - "firmware/application" - "firmware/baseband" steps: - - uses: actions/checkout@v2 - - name: print latest_commit - run: echo ${{ github.sha }} - - name: clang-format Check - uses: jidicula/clang-format-action@v4.11.0 - with: - clang-format-version: '18' - check-path: ${{ matrix.path }} - fallback-style: Chromium + - uses: actions/checkout@v4 + + - name: clang-format check (repo pinned) + run: | + set -euo pipefail + + CF="./tools/clang-format.sh" + chmod +x "$CF" || true + + "$CF" --version + + ROOT="${{ matrix.path }}" + find "$ROOT" -type f \( \ + -name '*.c' -o -name '*.cpp' -o \ + -name '*.h' -o -name '*.hpp' \ + \) -print0 \ + | xargs -0 -r "$CF" --dry-run --Werror --style=file diff --git a/.github/workflows/create_nightly_release.yml b/.github/workflows/create_nightly_release.yml index 4ed96367a..e15b14a6b 100644 --- a/.github/workflows/create_nightly_release.yml +++ b/.github/workflows/create_nightly_release.yml @@ -3,7 +3,7 @@ name: Nightly Release on: schedule: - cron: "0 0 * * *" - + workflow_dispatch: jobs: @@ -21,27 +21,36 @@ jobs: id: should_run continue-on-error: true run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "should_run=false" >> $GITHUB_OUTPUT + build: needs: check_date if: ${{ needs.check_date.outputs.should_run != 'false' }} runs-on: ubuntu-latest + name: build (${{ matrix.device_name }}) + strategy: + fail-fast: false + matrix: + include: + - device: default + device_name: HackRF + cmake_args: "" + artifact_suffix: "_hackrf" + - device: portarf + device_name: PortaRF + cmake_args: "-DFLASH_MB_SIZE=2 -DFLASH_MB_LIMIT_SIZE=2" + artifact_suffix: "_portarf" + - device: hpro + device_name: HackRF Pro + cmake_args: "-DBOARD=PRALINE" + artifact_suffix: "_hpro" steps: - - name: Get current date - id: date - run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - name: Get version date id: version_date run: echo "date=n_$(date +'%y%m%d')" >> $GITHUB_OUTPUT - - name: Checkout + - name: Checkout uses: actions/checkout@master with: fetch-depth: 0 - #ref: next - # The branch, tag or SHA to checkout. When checking out the repository that - # triggered a workflow, this defaults to the reference or SHA for that event. - # Otherwise, uses the default branch. - # https://github.com/actions/checkout - # So scheduled runs will use the default branch (next) but its now possible to trigger a workflow from another branch submodules: true - name: Git Sumbodule Update run: | @@ -51,10 +60,10 @@ jobs: - name: Make build folder run: mkdir ${{ github.workspace }}/build - name: Run the Docker image - run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev + run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev ${{ matrix.cmake_args }} - name: Create Small SD Card ZIP - No World Map run: | - mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_nightly_${{ steps.version_date.outputs.date }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd .. + mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_nightly_${{ steps.version_date.outputs.date }}${{ matrix.artifact_suffix }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd .. - name: Download world map run: | wget https://github.com/portapack-mayhem/mayhem-firmware/releases/download/world_map/world_map.zip @@ -63,13 +72,55 @@ jobs: unzip world_map.zip -d sdcard/ADSB - name: Prepare Firmware ZIP run: | - cp build/hackrf/firmware/hackrf_usb/hackrf_usb.dfu flashing/utils/hackrf_one_usb.dfu && cp build/hackrf/firmware/hackrf_usb/hackrf_usb.bin flashing/utils/hackrf_one_usb.bin + mkdir -p flashing/firmware && cp build/hackrf/firmware/hackrf_usb/hackrf_usb.dfu flashing/firmware/hackrf_usb.dfu && cp build/hackrf/firmware/hackrf_usb/hackrf_usb.bin flashing/firmware/hackrf_usb.bin - name: Create Firmware ZIP run: | zip -j firmware.zip build/firmware/portapack-mayhem-firmware.bin && cd flashing && zip -r ../firmware.zip * - name: Create SD Card ZIP run: | - mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_nightly_${{ steps.version_date.outputs.date }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard.zip . && cd .. + mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_nightly_${{ steps.version_date.outputs.date }}${{ matrix.artifact_suffix }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard.zip . && cd .. + - name: Upload firmware zip artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.device }} + path: ./firmware.zip + - name: Upload SD card artifact + uses: actions/upload-artifact@v4 + with: + name: sdcard-${{ matrix.device }} + path: ./sdcard.zip + - name: Upload SD card no-map artifact + uses: actions/upload-artifact@v4 + with: + name: sdcard-no-map-${{ matrix.device }} + path: ./sdcard-no-map.zip + - name: Upload OCI tar artifact + uses: actions/upload-artifact@v4 + with: + name: oci-tar-${{ matrix.device }} + path: build/firmware/portapack-mayhem_OCI.ppfw.tar + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Get version date + id: version_date + run: echo "date=n_$(date +'%y%m%d')" >> $GITHUB_OUTPUT + - name: Checkout + uses: actions/checkout@master + with: + fetch-depth: 0 + submodules: true + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts - name: Create changelog env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -80,14 +131,42 @@ jobs: echo EOF } >> "$GITHUB_OUTPUT" id: changelog + # Combine all 3 device firmware bins into a single zip with device selector bat + - name: Create Combined Firmware ZIP + run: | + mkdir -p firmware_combined + unzip artifacts/firmware-default/firmware.zip -d firmware_combined + mv firmware_combined/portapack-mayhem-firmware.bin firmware_combined/firmware/firmware_hackrf.bin + unzip -j artifacts/firmware-portarf/firmware.zip portapack-mayhem-firmware.bin -d firmware_combined/firmware + mv firmware_combined/firmware/portapack-mayhem-firmware.bin firmware_combined/firmware/firmware_portarf.bin + unzip -j artifacts/firmware-hpro/firmware.zip portapack-mayhem-firmware.bin -d firmware_combined/firmware + mv firmware_combined/firmware/portapack-mayhem-firmware.bin firmware_combined/firmware/firmware_hpro.bin + unzip -j artifacts/firmware-hpro/firmware.zip firmware/hackrf_usb.dfu firmware/hackrf_usb.bin -d firmware_combined_hpro + mv firmware_combined_hpro/hackrf_usb.dfu firmware_combined/firmware/hackrf_hpro_usb.dfu + mv firmware_combined_hpro/hackrf_usb.bin firmware_combined/firmware/hackrf_hpro_usb.bin + cd firmware_combined && zip -r ../firmware_all_devices.zip . && cd .. + - name: Prepare release assets + run: | + VER="${{ steps.version_date.outputs.date }}" + mkdir -p release_assets + # OCI tar assets + cp artifacts/oci-tar-default/portapack-mayhem_OCI.ppfw.tar "release_assets/OCI_hackrf_mayhem_nightly_${VER}.ppfw.tar" + cp artifacts/oci-tar-portarf/portapack-mayhem_OCI.ppfw.tar "release_assets/OCI_portarf_mayhem_nightly_${VER}.ppfw.tar" + cp artifacts/oci-tar-hpro/portapack-mayhem_OCI.ppfw.tar "release_assets/OCI_hpro_mayhem_nightly_${VER}.ppfw.tar" + # Combined firmware + cp firmware_all_devices.zip "release_assets/FIRMWARE_mayhem_nightly_${VER}.zip" + # SD card assets + cp artifacts/sdcard-default/sdcard.zip "release_assets/COPY_TO_SDCARD_hackrf_mayhem_nightly_${VER}.zip" + cp artifacts/sdcard-no-map-default/sdcard-no-map.zip "release_assets/COPY_TO_SDCARD_hackrf_mayhem_nightly_${VER}-no-world-map.zip" + cp artifacts/sdcard-portarf/sdcard.zip "release_assets/COPY_TO_SDCARD_portarf_mayhem_nightly_${VER}.zip" + cp artifacts/sdcard-no-map-portarf/sdcard-no-map.zip "release_assets/COPY_TO_SDCARD_portarf_mayhem_nightly_${VER}-no-world-map.zip" + cp artifacts/sdcard-hpro/sdcard.zip "release_assets/COPY_TO_SDCARD_hpro_mayhem_nightly_${VER}.zip" + cp artifacts/sdcard-no-map-hpro/sdcard-no-map.zip "release_assets/COPY_TO_SDCARD_hpro_mayhem_nightly_${VER}-no-world-map.zip" - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v2 with: tag_name: nightly-tag-${{ steps.date.outputs.date }} - release_name: Nightly Release - ${{ steps.date.outputs.date }} + name: Nightly Release - ${{ steps.date.outputs.date }} body: | **Nightly release - ${{ steps.date.outputs.date }}** This build is the latest and greatest, although may not be the most stable as this is a nightly release. @@ -96,43 +175,4 @@ jobs: ${{ steps.changelog.outputs.content }} draft: false prerelease: true - - name: Upload Firmware TAR Asset - id: upload-firmware-tar-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: build/firmware/portapack-mayhem_OCI.ppfw.tar - asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_OCI.ppfw.tar - asset_content_type: application/x-tar - - name: Upload Firmware Asset - id: upload-firmware-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./firmware.zip - asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_FIRMWARE.zip - asset_content_type: application/zip - - name: Upload SD Card Assets - id: upload-sd-card-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./sdcard.zip - asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD.zip - asset_content_type: application/zip - - name: Upload SD Card Assets - No Map - id: upload-sd-card-asset-no-map - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./sdcard-no-map.zip - asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD-no-world-map.zip - asset_content_type: application/zip + files: release_assets/* diff --git a/.github/workflows/create_stable_release.yml b/.github/workflows/create_stable_release.yml index c6284380f..deb2e8aa1 100644 --- a/.github/workflows/create_stable_release.yml +++ b/.github/workflows/create_stable_release.yml @@ -1,25 +1,36 @@ name: Stable Release -on: +on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest + name: build (${{ matrix.device_name }}) + strategy: + fail-fast: false + matrix: + include: + - device: default + device_name: HackRF + cmake_args: "" + artifact_suffix: "_hackrf" + - device: portarf + device_name: PortaRF + cmake_args: "-DFLASH_MB_SIZE=2 -DFLASH_MB_LIMIT_SIZE=2" + artifact_suffix: "_portarf" + - device: hpro + device_name: HackRF Pro + cmake_args: "-DBOARD=PRALINE" + artifact_suffix: "_hpro" steps: - name: Get current date id: date run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - - name: Checkout + - name: Checkout uses: actions/checkout@master with: fetch-depth: 0 - #ref: next - # The branch, tag or SHA to checkout. When checking out the repository that - # triggered a workflow, this defaults to the reference or SHA for that event. - # Otherwise, uses the default branch. - # https://github.com/actions/checkout - # So scheduled runs will use the default branch (next) but its now possible to trigger a workflow from another branch submodules: true - name: Git Sumbodule Update run: | @@ -27,18 +38,15 @@ jobs: - name: Get version id: version run: echo "version=$(cat .github/workflows/version.txt)" >> $GITHUB_OUTPUT - - name: Get past version - id: past_version - run: echo "past_version=$(cat .github/workflows/past_version.txt)" >> $GITHUB_OUTPUT - name: Build the Docker image run: docker build -t portapack-dev -f dockerfile-nogit . --tag my-image-name:$(date +%s) - name: Make build folder run: mkdir ${{ github.workspace }}/build - name: Run the Docker image - run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev + run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev ${{ matrix.cmake_args }} - name: Create Small SD Card ZIP - No World Map run: | - mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_${{ steps.version.outputs.version }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd .. + mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_${{ steps.version.outputs.version }}${{ matrix.artifact_suffix }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd .. - name: Download world map run: | wget https://github.com/portapack-mayhem/mayhem-firmware/releases/download/world_map/world_map.zip @@ -47,13 +55,58 @@ jobs: unzip world_map.zip -d sdcard/ADSB - name: Prepare Firmware ZIP run: | - cp build/hackrf/firmware/hackrf_usb/hackrf_usb.dfu flashing/utils/hackrf_one_usb.dfu && cp build/hackrf/firmware/hackrf_usb/hackrf_usb.bin flashing/utils/hackrf_one_usb.bin + mkdir -p flashing/firmware && cp build/hackrf/firmware/hackrf_usb/hackrf_usb.dfu flashing/firmware/hackrf_usb.dfu && cp build/hackrf/firmware/hackrf_usb/hackrf_usb.bin flashing/firmware/hackrf_usb.bin - name: Create Firmware ZIP run: | zip -j firmware.zip build/firmware/portapack-mayhem-firmware.bin && cd flashing && zip -r ../firmware.zip * - name: Create SD Card ZIP run: | - mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_${{ steps.version.outputs.version }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard.zip . && cd .. + mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_${{ steps.version.outputs.version }}${{ matrix.artifact_suffix }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard.zip . && cd .. + - name: Upload firmware zip artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.device }} + path: ./firmware.zip + - name: Upload SD card artifact + uses: actions/upload-artifact@v4 + with: + name: sdcard-${{ matrix.device }} + path: ./sdcard.zip + - name: Upload SD card no-map artifact + uses: actions/upload-artifact@v4 + with: + name: sdcard-no-map-${{ matrix.device }} + path: ./sdcard-no-map.zip + - name: Upload OCI tar artifact + uses: actions/upload-artifact@v4 + with: + name: oci-tar-${{ matrix.device }} + path: build/firmware/portapack-mayhem_OCI.ppfw.tar + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - name: Checkout + uses: actions/checkout@master + with: + fetch-depth: 0 + submodules: true + - name: Get version + id: version + run: echo "version=$(cat .github/workflows/version.txt)" >> $GITHUB_OUTPUT + - name: Get past version + id: past_version + run: echo "past_version=$(cat .github/workflows/past_version.txt)" >> $GITHUB_OUTPUT + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts - name: Create changelog env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -64,21 +117,49 @@ jobs: echo EOF } >> "$GITHUB_OUTPUT" id: changelog + # Combine all 3 device firmware bins into a single zip with device selector bat + - name: Create Combined Firmware ZIP + run: | + mkdir -p firmware_combined + unzip artifacts/firmware-default/firmware.zip -d firmware_combined + mv firmware_combined/portapack-mayhem-firmware.bin firmware_combined/firmware/firmware_hackrf.bin + unzip -j artifacts/firmware-portarf/firmware.zip portapack-mayhem-firmware.bin -d firmware_combined/firmware + mv firmware_combined/firmware/portapack-mayhem-firmware.bin firmware_combined/firmware/firmware_portarf.bin + unzip -j artifacts/firmware-hpro/firmware.zip portapack-mayhem-firmware.bin -d firmware_combined/firmware + mv firmware_combined/firmware/portapack-mayhem-firmware.bin firmware_combined/firmware/firmware_hpro.bin + unzip -j artifacts/firmware-hpro/firmware.zip firmware/hackrf_usb.dfu firmware/hackrf_usb.bin -d firmware_combined_hpro + mv firmware_combined_hpro/hackrf_usb.dfu firmware_combined/firmware/hackrf_hpro_usb.dfu + mv firmware_combined_hpro/hackrf_usb.bin firmware_combined/firmware/hackrf_hpro_usb.bin + cd firmware_combined && zip -r ../firmware_all_devices.zip . && cd .. + - name: Prepare release assets + run: | + VER="${{ steps.version.outputs.version }}" + mkdir -p release_assets + # OCI tar assets + cp artifacts/oci-tar-default/portapack-mayhem_OCI.ppfw.tar "release_assets/OCI_hackrf_mayhem_${VER}.ppfw.tar" + cp artifacts/oci-tar-portarf/portapack-mayhem_OCI.ppfw.tar "release_assets/OCI_portarf_mayhem_${VER}.ppfw.tar" + cp artifacts/oci-tar-hpro/portapack-mayhem_OCI.ppfw.tar "release_assets/OCI_hpro_mayhem_${VER}.ppfw.tar" + # Combined firmware + cp firmware_all_devices.zip "release_assets/FIRMWARE_mayhem_${VER}.zip" + # SD card assets + cp artifacts/sdcard-default/sdcard.zip "release_assets/COPY_TO_SDCARD_hackrf_mayhem_${VER}.zip" + cp artifacts/sdcard-no-map-default/sdcard-no-map.zip "release_assets/COPY_TO_SDCARD_hackrf_mayhem_${VER}-no-world-map.zip" + cp artifacts/sdcard-portarf/sdcard.zip "release_assets/COPY_TO_SDCARD_portarf_mayhem_${VER}.zip" + cp artifacts/sdcard-no-map-portarf/sdcard-no-map.zip "release_assets/COPY_TO_SDCARD_portarf_mayhem_${VER}-no-world-map.zip" + cp artifacts/sdcard-hpro/sdcard.zip "release_assets/COPY_TO_SDCARD_hpro_mayhem_${VER}.zip" + cp artifacts/sdcard-no-map-hpro/sdcard-no-map.zip "release_assets/COPY_TO_SDCARD_hpro_mayhem_${VER}-no-world-map.zip" - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.version.outputs.version }} - release_name: Mayhem firmware ${{ steps.version.outputs.version }} + name: Mayhem firmware ${{ steps.version.outputs.version }} body: | **Stable release - ${{ steps.version.outputs.version }}** This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmware, which itself was a fork of the [PortaPack](https://github.com/sharebrained/portapack-hackrf) firmware, an add-on for the [HackRF](http://greatscottgadgets.com/hackrf/). Please check the [readme](https://github.com/portapack-mayhem/mayhem-firmware/blob/master/README.md) for details. ## Release notes ### Revision (${{ steps.version.outputs.version }}): ${{ steps.changelog.outputs.content }} - + **Full Changelog**: https://github.com/portapack-mayhem/mayhem-firmware/compare/${{ steps.past_version.outputs.past_version }}...${{ steps.version.outputs.version }} ## Installation @@ -90,44 +171,4 @@ jobs: For certain functionality, like external apps, the world map, GPS simulator, and others you need to uncompress (using [7-zip](https://www.7-zip.org/download.html)) the files from `mayhem_vX.Y.Z_COPY_TO_SDCARD.zip` to a FAT32 formatted MicroSD card. draft: true prerelease: false - - name: Upload Firmware TAR Asset - id: upload-firmware-tar-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: build/firmware/portapack-mayhem_OCI.ppfw.tar - asset_name: mayhem_${{ steps.version.outputs.version }}_OCI.ppfw.tar - asset_content_type: application/x-tar - - name: Upload Firmware Asset - id: upload-firmware-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./firmware.zip - asset_name: mayhem_${{ steps.version.outputs.version }}_FIRMWARE.zip - asset_content_type: application/zip - - name: Upload SD Card Assets - id: upload-sd-card-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./sdcard.zip - asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD.zip - asset_content_type: application/zip - - name: Upload SD Card Assets - No Map - id: upload-sd-card-asset-no-map - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./sdcard-no-map.zip - asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD-no-world-map.zip - asset_content_type: application/zip - + files: release_assets/* diff --git a/.github/workflows/past_version.txt b/.github/workflows/past_version.txt index aaf7425f4..f706a60d8 100644 --- a/.github/workflows/past_version.txt +++ b/.github/workflows/past_version.txt @@ -1 +1 @@ -v2.3.1 +v2.3.2 diff --git a/.github/workflows/version.txt b/.github/workflows/version.txt index f706a60d8..8721bbc46 100644 --- a/.github/workflows/version.txt +++ b/.github/workflows/version.txt @@ -1 +1 @@ -v2.3.2 +v2.4.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fbd8a82f..08bda7fc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,37 @@ set(CMAKE_COLOR_MAKEFILE ON) add_compile_options(-fdiagnostics-color=always) +#Flash size related options +#This is the size of the flash memory we SUPPORT. Don't worry, the internl flash app has harder limits based on device type. +if(NOT DEFINED FLASH_MB_SIZE) + if(BOARD STREQUAL "PRALINE") + set(FLASH_MB_SIZE 4) # PRALINE has 4MB flash + else() + set(FLASH_MB_SIZE 2) # Default to 2MB if not provided. For PortaRf this should be 2MB passed to cmake with -DFLASH_MB_SIZE=2 + endif() +endif() +message("Configuring for FLASH_MB_SIZE=${FLASH_MB_SIZE}MB") +#This is the flash memory size we build. This can be less, so the FW size will be less, so can be flashed with older utils. +if(NOT DEFINED FLASH_MB_LIMIT_SIZE) + if(BOARD STREQUAL "PRALINE") + set(FLASH_MB_LIMIT_SIZE 3.5) #PRALINE 4 MB = 3.5 firmware + 0.5 MB fpga bitstream. + else() + set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash + endif() +endif() +message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB") + +if(FLASH_MB_SIZE LESS FLASH_MB_LIMIT_SIZE) + message(FATAL_ERROR "Error: The supported flash size is lower than the generated flash file. Build halted.") +endif() + +math(EXPR FLASH_BYTES_SIZE "${FLASH_MB_SIZE} * 1024 * 1024") +if(BOARD STREQUAL "PRALINE") + math(EXPR FLASH_BYTES_LIMIT_SIZE "3584 * 1024") #4 MB = 3.5 firmware + 0.5 MB fpga bitstream. +else() + math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024") +endif() + option(USE_CCACHE "Enable ccache, please keep an eye on this because sometimes it's not quite stable and generated unusable binary" OFF) project(portapack-h1) diff --git a/README.md b/README.md index f53ba77f8..7533f2d0e 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,20 @@ This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmware, which itself was a fork of the [PortaPack](https://github.com/sharebrained/portapack-hackrf) firmware, an add-on for the [HackRF](http://greatscottgadgets.com/hackrf/). A fork is a derivate, in this case one that has extra features and fixes when compared to the older versions. [](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#new-h4m-mayhem-edition) -[](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h2m-mayhem-edition) -[]([https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h2m-mayhem-edition](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h1r1r2)) +[](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h2m-mayhem-edition) + # What is this? If you are new to *HackRF+PortaPack+Mayhem*, check these: -[The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?](https://share.hackrf.app/6HKX9A) +[PortaPack H4M Crash Course](https://share.hackrf.app/SPFOZO) + +[The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?](https://share.hackrf.app/6HKX9A) +[It’s TOO Easy to Accidentally Do Illegal Stuff with This](https://share.hackrf.app/X4D5TF) +[HackRF Portapack H4M - Getting Started Guide](https://share.hackrf.app/F9MPOO) +[The new HackRF Portapack H4M](https://share.hackrf.app/0JUHZ6) -[It’s TOO Easy to Accidentally Do Illegal Stuff with This](https://share.hackrf.app/X4D5TF) [HackRF Portapack H4M - Getting Started Guide](https://share.hackrf.app/F9MPOO) [The new HackRF Portapack H4M](https://share.hackrf.app/0JUHZ6) [HackRF 101 : Everything You Need to Know to Get Started!](https://share.hackrf.app/C0J6ZR) # Frequently Asked Questions @@ -28,9 +32,9 @@ This repository expands upon the previous work by many people and aims to consta ## What to buy? -:heavy_check_mark: ![Static Badge](https://img.shields.io/badge/NEW-yellow) The fabulous H4M [complete](https://share.hackrf.app/9UMBN5) or [upgrade](https://share.hackrf.app/30CQ55), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR). +:heavy_check_mark: ![Static Badge](https://img.shields.io/badge/NEW-yellow) The fabulous H4M [complete](https://share.hackrf.app/DF2BZS) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR). -:heavy_check_mark: A recommended one is this [PortaPack H2](https://www.ebay.com/itm/116382397447), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure). +:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/QT5JMB), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure). :heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/24T3TO), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview). @@ -47,7 +51,10 @@ This repository expands upon the previous work by many people and aims to consta The current **stable release** is on the [![GitHub release (latest by date)](https://img.shields.io/github/v/release/portapack-mayhem/mayhem-firmware?label=Releases&style=social)](https://github.com/portapack-mayhem/mayhem-firmware/releases/latest) page. Follow the instructions you can find in the release description. The **latest (nightly) release** can be found [here](https://github.com/portapack-mayhem/mayhem-firmware/releases/). ## How can I collaborate -You can write [documentation](https://github.com/portapack-mayhem/mayhem-firmware/wiki), fix bugs and [answer issues](https://github.com/portapack-mayhem/mayhem-firmware/issues) or add new functionality. Please check the following [guide](https://github.com/portapack-mayhem/mayhem-firmware/wiki/How-to-collaborate) with details. + +We warmly welcome your contributions! Please refer to our **[Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)** for details. + +You can write [documentation](https://github.com/portapack-mayhem/mayhem-firmware/wiki), fix bugs and [answer issues](https://github.com/portapack-mayhem/mayhem-firmware/issues) or add new functionality. Please also check the following [guide](https://github.com/portapack-mayhem/mayhem-firmware/wiki/How-to-collaborate) with details. Consider that the hardware and firmware has been created and maintain by a [lot](https://github.com/mossmann/hackrf/graphs/contributors) of [people](https://github.com/portapack-mayhem/mayhem-firmware/graphs/contributors), so always try collaborating your time and effort first. For coding related questions, if something does not fit as an issue, please join our Discord by clicking the chat badge on [top](#portapack-mayhem). diff --git a/dockerize.sh b/dockerize.sh index 9f25d558e..63ba8eebe 100755 --- a/dockerize.sh +++ b/dockerize.sh @@ -15,8 +15,11 @@ # - Get a shell inside the build image to # inspect problems: ./dockerize.sh shell # - Give additional parameters to the container: -# ./dockerize.sh -j10 -# ./dockerize.sh ninja -j10 +# Additional parameters to make: ./dockerize.sh -j10 +# Use ninja instead of make: ./dockerize.sh ninja -j10 +# Addotopnal CMake parameters: ./dockerize.sh -DBOARD=PRALINE +# Change the default build directory: ./dockerize.sh -B build-alt +# (Alternatively --workdir works as well) # - Use a different dockerfile: # ./dockerize.sh build dockerfile-other # - Use a different cpu architecture: diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 9b75482ff..000000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -portapack.ried.cl \ No newline at end of file diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 161be343b..ada583cb6 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -48,19 +48,44 @@ if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_EQUAL ${EXPECTED_GCC_VERSION}) set(GCC_VERSION_MISMATCH 1) endif() +#generate h files +configure_file(flashsize.h.in flashsize.h) +add_compile_options(-include ${CMAKE_CURRENT_BINARY_DIR}/flashsize.h) + add_subdirectory(application) add_subdirectory(baseband) add_subdirectory(standalone) add_subdirectory(test) -# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake? -add_custom_command( - OUTPUT ${FIRMWARE_FILENAME} - COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} - DEPENDS baseband application ${MAKE_SPI_IMAGE} - ${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin - VERBATIM -) +if(BOARD STREQUAL "PRALINE") + math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024") + set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin) + set(APPEND_FPGA_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/append_fpga_bitstream.py) + + # NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake? + add_custom_command( + OUTPUT ${FIRMWARE_FILENAME} + COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE} + # PRALINE: Append FPGA bitstream to firmware at offset 0x380000 + # The base firmware is 3.5MB, FPGA bitstream gets appended at the end of it, final size is 4MB + COMMAND ${CMAKE_COMMAND} -E echo "Appending PRALINE FPGA bitstream to firmware..." + COMMAND python3 ${APPEND_FPGA_SCRIPT} ${FIRMWARE_FILENAME} ${PRALINE_FPGA_BIN} ${FIRMWARE_FILENAME}.tmp ${PRALINE_FINAL_SIZE} + COMMAND ${CMAKE_COMMAND} -E rename ${FIRMWARE_FILENAME}.tmp ${FIRMWARE_FILENAME} + DEPENDS baseband application ${MAKE_SPI_IMAGE} + ${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin + ${PRALINE_FPGA_BIN} ${APPEND_FPGA_SCRIPT} + VERBATIM + ) +else() # Sadly this part had to be duplicated (or at least I couldn't find a better way to do it) + # NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake? + add_custom_command( + OUTPUT ${FIRMWARE_FILENAME} + COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE} + DEPENDS baseband application ${MAKE_SPI_IMAGE} + ${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin + VERBATIM + ) +endif() add_custom_target( firmware diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 9026e3509..85466ef63 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -86,6 +86,9 @@ set(CPLD_20150901_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/portapack_cpld_20150901_d set(CPLD_20170522_SVF_PATH ${HARDWARE_PATH}/portapack_h1/cpld/20170522/output_files/portapack_h1_cpld.svf) set(CPLD_20170522_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/portapack_cpld_20170522_data.cpp) +set(CPLD_H4M_SVF_PATH ${HARDWARE_PATH}/portapack_h4m/CPLD/AG256SL100/output_files/portapack_h4m_cpld.svf) +set(CPLD_H4M_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/portapack_cpld_h4m_data.cpp) + set(HACKRF_CPLD_DATA_HPP ${CMAKE_CURRENT_BINARY_DIR}/hackrf_cpld_data.hpp) set(HACKRF_CPLD_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/hackrf_cpld_data.cpp) @@ -118,11 +121,16 @@ set(CSRC usb_serial_endpoints.c usb_serial_device_to_host.c i2c_device_to_host.c + hackrf_core_mini.c + ${HACKRF_PATH}/firmware/common/adc.c ${HACKRF_PATH}/firmware/common/usb.c ${HACKRF_PATH}/firmware/common/usb_queue.c ${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c ${HACKRF_PATH}/firmware/common/usb_request.c ${HACKRF_PATH}/firmware/common/usb_standard_request.c + ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c + ${HACKRF_PATH}/firmware/common/gpio_lpc.c + ${HACKRF_PATH}/firmware/common/platform_detect.c ${CHIBIOS}/os/various/chprintf.c ) @@ -205,6 +213,9 @@ set(CPPSRC irq_controls.cpp irq_lcd_frame.cpp irq_rtc.cpp + keeloq_common.cpp + keeloq_file.cpp + keeloq_keystore.cpp log_file.cpp metadata_file.cpp flipper_subfile.cpp @@ -237,6 +248,7 @@ set(CPPSRC hw/encoder.cpp hw/max2837.cpp hw/max2839.cpp + hw/max2831.cpp hw/max5864.cpp hw/rffc507x.cpp hw/rffc507x_spi.cpp @@ -246,6 +258,7 @@ set(CPPSRC ook_file.cpp ui_baseband_stats_view.cpp ui_navigation.cpp + ui_notifications.cpp ui_record_view.cpp ui_sd_card_status_view.cpp ui/ui_alphanum.cpp @@ -292,14 +305,11 @@ set(CPPSRC apps/ui_mictx.cpp apps/ui_modemsetup.cpp apps/ui_playlist.cpp - apps/ui_pocsag_tx.cpp apps/ui_rds.cpp apps/ui_recon_settings.cpp apps/ui_recon.cpp - apps/ui_sd_over_usb.cpp apps/ui_search.cpp apps/ui_settings.cpp - apps/ui_siggen.cpp apps/ui_sonde.cpp apps/ui_ss_viewer.cpp apps/ui_standalone_view.cpp @@ -319,6 +329,7 @@ set(CPPSRC config_mode.cpp ${CPLD_20150901_DATA_CPP} ${CPLD_20170522_DATA_CPP} + ${CPLD_H4M_DATA_CPP} ${HACKRF_CPLD_DATA_CPP} ui_external_items_menu_loader.cpp view_factory_base.cpp @@ -404,7 +415,11 @@ set(CPPWARN "-Wall -Wextra -Wno-psabi") # List all default C defines here, like -D_DEBUG=1 # TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI? # NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types -set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5} -D'GCC_VERSION_MISMATCH=${GCC_VERSION_MISMATCH}'") +# Use BOARD variable if set, otherwise default to HACKRF_ONE +if(NOT DEFINED BOARD) + set(BOARD "HACKRF_ONE") +endif() +set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -D${BOARD} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5} -D'GCC_VERSION_MISMATCH=${GCC_VERSION_MISMATCH}'") # List all default ASM defines here, like -D_DEBUG=1 set(DADEFS) @@ -462,6 +477,12 @@ add_custom_command( DEPENDS ${EXTRACT_CPLD_DATA} ${CPLD_20170522_SVF_PATH} ) +add_custom_command( + OUTPUT ${CPLD_H4M_DATA_CPP} + COMMAND ${EXTRACT_CPLD_DATA} ${CPLD_H4M_SVF_PATH} rev_h4m >${CPLD_H4M_DATA_CPP} + DEPENDS ${EXTRACT_CPLD_DATA} ${CPLD_H4M_SVF_PATH} +) + add_custom_command( OUTPUT ${HACKRF_CPLD_DATA_CPP} COMMAND ${HACKRF_CPLD_TOOL} --xsvf ${HACKRF_CPLD_XSVF_PATH} --portapack-data ${HACKRF_CPLD_DATA_CPP} @@ -469,6 +490,9 @@ add_custom_command( ) add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC}) +target_link_options(${PROJECT_NAME}.elf PRIVATE + "LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}" +) set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT}) add_definitions(${DEFS}) include_directories(. ${INCDIR}) diff --git a/firmware/application/app_settings.cpp b/firmware/application/app_settings.cpp index acf93b0ff..bbae046f9 100644 --- a/firmware/application/app_settings.cpp +++ b/firmware/application/app_settings.cpp @@ -68,6 +68,9 @@ void BoundSetting::parse(std::string_view value) { as() = (parsed != 0); break; } + case SettingType::Float: + as() = atof(value.data()); + break; }; } @@ -101,6 +104,11 @@ void BoundSetting::write(File& file) const { case SettingType::Bool: file.write(as() ? "1" : "0", 1); break; + + case SettingType::Float: { + auto float_str = to_string_decimal(as(), 6); + file.write(float_str.c_str(), float_str.length()); + } } file.write("\r\n", 2); diff --git a/firmware/application/app_settings.hpp b/firmware/application/app_settings.hpp index d0c30d5a3..4db5caf35 100644 --- a/firmware/application/app_settings.hpp +++ b/firmware/application/app_settings.hpp @@ -52,6 +52,7 @@ class BoundSetting { U8, String, Bool, + Float }; public: @@ -73,6 +74,9 @@ class BoundSetting { BoundSetting(std::string_view name, bool* target) : name_{name}, target_{target}, type_{SettingType::Bool} {} + BoundSetting(std::string_view name, float* target) + : name_{name}, target_{target}, type_{SettingType::Float} {} + std::string_view name() const { return name_; } void parse(std::string_view value); void write(File& file) const; diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp index a5c51f4a5..a1f4e224e 100644 --- a/firmware/application/apps/analog_audio_app.cpp +++ b/firmware/application/apps/analog_audio_app.cpp @@ -199,6 +199,55 @@ SPECOptionsView::SPECOptionsView( view->set_spec_iq_phase_calibration_value(view->get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio } +/* PralineOptionsView *******************************************************/ + +#ifdef PRALINE +PralineOptionsView::PralineOptionsView(Rect parent_rect, const Style* style) { + set_style(style); + set_parent_rect(parent_rect); + add_children({&label_sr, &options_sr, &label_dc, &options_dc, + &label_qi, &options_qi, &label_qs, &options_qs}); + + // READ the current state from hardware + fpga_reg_1 = radio::debug::fpga::register_read(1); + + // INITIALIZE UI WIDGETS based on read bits + options_dc.set_by_value((fpga_reg_1 & 0x01) ? 1 : 0); // Bit 0 + options_qi.set_by_value((fpga_reg_1 & 0x02) ? 1 : 0); // Bit 1 + options_qs.set_by_value((fpga_reg_1 & 0x04) ? 1 : 0); // Bit 2 + options_sr.set_value(receiver_model.sampling_rate()); + + options_sr.on_change = [this](int32_t v) { receiver_model.set_sampling_rate(static_cast(v)); }; + options_dc.on_change = [this](size_t, OptionsField::value_t v) { + if (v) + fpga_reg_1 |= 0x01; + else + fpga_reg_1 &= ~0x01; + update_fpga_ctrl(); + }; + + options_qi.on_change = [this](size_t, OptionsField::value_t v) { + if (v) + fpga_reg_1 |= 0x02; + else + fpga_reg_1 &= ~0x02; + update_fpga_ctrl(); + }; + + options_qs.on_change = [this](size_t, OptionsField::value_t v) { + if (v) + fpga_reg_1 |= 0x04; + else + fpga_reg_1 &= ~0x04; + update_fpga_ctrl(); + }; +} + +void PralineOptionsView::update_fpga_ctrl() { + radio::debug::fpga::register_write(1, fpga_reg_1); +} +#endif + /* AnalogAudioView *******************************************************/ AnalogAudioView::AnalogAudioView( @@ -217,7 +266,13 @@ AnalogAudioView::AnalogAudioView( &field_volume, &text_ctcss, &record_view, - &waterfall}); +#ifdef PRALINE + &waterfall, + &button_pro +#else + &waterfall +#endif + }); // Filename Datetime and Frequency record_view.set_filename_date_frequency(true); @@ -256,6 +311,10 @@ AnalogAudioView::AnalogAudioView( field_frequency.set_value(receiver_model.target_frequency() + offset); }; +#ifdef PRALINE + button_pro.on_select = [this](Button&) { this->on_show_options_praline(); }; +#endif + audio::output::start(); // This call starts the correct baseband image to run @@ -553,4 +612,11 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) { void AnalogAudioView::on_freqchg(int64_t freq) { field_frequency.set_value(freq); } + +#ifdef PRALINE +void AnalogAudioView::on_show_options_praline() { + set_options_widget(std::make_unique(options_view_rect, Theme::getInstance()->option_active)); +} +#endif + } /* namespace ui */ diff --git a/firmware/application/apps/analog_audio_app.hpp b/firmware/application/apps/analog_audio_app.hpp index 33d7b41d0..2e1b1ccfb 100644 --- a/firmware/application/apps/analog_audio_app.hpp +++ b/firmware/application/apps/analog_audio_app.hpp @@ -37,6 +37,38 @@ namespace ui { class AnalogAudioView; +#ifdef PRALINE +class PralineOptionsView : public View { + public: + PralineOptionsView(Rect parent_rect, const Style* style); + + private: + // Layout: SR (Sample Rate), DC (DC Block), QI (Q-Invert), QS (Quarter Shift), D (Decim) + Text label_sr{{UI_POS_X(0), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "SR"}; + + // Parameters: Position, Length (in chars), Range, Step, and default value + NumberField options_sr{ + {UI_POS_X(3), UI_POS_Y(0)}, + 8, // Number of digits to show + {0, 40000000}, // Range: 0 to 40,000 kHz + 100, // Step: 100 Hz + ' ' // Fix: Single quotes for char + }; + + Text label_dc{{UI_POS_X(14), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "DC"}; + OptionsField options_dc{{UI_POS_X(16), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}}; + + Text label_qi{{UI_POS_X(20), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "QI"}; + OptionsField options_qi{{UI_POS_X(22), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}}; + + Text label_qs{{UI_POS_X(26), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "QS"}; + OptionsField options_qs{{UI_POS_X(28), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}}; + + uint8_t fpga_reg_1{0x00}; // Tracks DC, QI, QS bits + void update_fpga_ctrl(); +}; +#endif + class AMOptionsView : public View { public: AMOptionsView(AnalogAudioView* view, Rect parent_rect, const Style* style); @@ -222,6 +254,11 @@ class AnalogAudioView : public View { uint8_t get_previous_zoom_option(); void set_previous_zoom_option(uint8_t zoom); +#ifdef PRALINE + Button button_pro{{UI_POS_X(12), UI_POS_Y(2), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "PRO"}; + void on_show_options_praline(); +#endif + private: static constexpr ui::Dim header_height = 3 * 16; diff --git a/firmware/application/apps/ble_rx_app.cpp b/firmware/application/apps/ble_rx_app.cpp index b16cf913b..f553061f3 100644 --- a/firmware/application/apps/ble_rx_app.cpp +++ b/firmware/application/apps/ble_rx_app.cpp @@ -645,6 +645,20 @@ BLERxView::BLERxView(NavigationView& nav) options_sort.set_selected_index(sort_index, true); options_filter.set_selected_index(filter_index, true); + // ------------------------------------------------------------------------------ + // Handle Max Recent Entries + // ------------------------------------------------------------------------------ + + // Max Recent Entries UI widget + add_child(&label_max_entries); + add_child(&field_max_entries); + field_max_entries.set_value(max_recent_entries); + + // Define what happens when the user changes the number + field_max_entries.on_change = [this](int32_t v) { + max_recent_entries = (size_t)v; + }; + // Auto-configure modem for LCR RX (will be removed later) baseband::set_btlerx(channel_number); @@ -819,6 +833,10 @@ void BLERxView::on_data(BlePacketData* packet) { recent.push_front(*it); recent.erase(it); } else { + // Enforce limit + while (recent.size() >= max_recent_entries) { + recent.pop_back(); + } recent.emplace_front(key); truncate_entries(recent); } @@ -887,6 +905,9 @@ void BLERxView::on_filter_change(std::string value) { } void BLERxView::on_file_changed(const std::filesystem::path& new_file_path) { + // Clear searchList + searchList.clear(); + file_path = new_file_path; found_count = 0; total_count = 0; @@ -919,8 +940,10 @@ void BLERxView::on_file_changed(const std::filesystem::path& new_file_path) { break; } - searchList.push_back(currentLine); - total_count++; + if (searchList.size() < max_recent_entries) { + searchList.push_back(currentLine); + total_count++; + } bytePos += bytesRead; @@ -940,6 +963,21 @@ void BLERxView::on_timer() { channel_number = (channel_number < 39) ? channel_number + 1 : 37; } } + + // Debug: Check heap status every ~1 second + static int heap_check_counter = 0; + if (++heap_check_counter >= 60) { + heap_check_counter = 0; + size_t heap_free = chCoreStatus(); + if (heap_free < 4096) { // Less than 4KB free + // Emergency: clear old entries + while (recent.size() > (max_recent_entries - 1)) { + recent.pop_back(); + } + recent_entries_view.set_dirty(); + } + } + if (ble_rx_error != BLE_RX_NO_ERROR) { if (ble_rx_error == BLE_RX_LIST_FILENAME_EMPTY_ERROR) { nav_.display_modal("Error", "List filename is empty !"); @@ -1059,7 +1097,6 @@ bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, entry.dbValue = packet->max_dB - (receiver_model.lna() + receiver_model.vga() + (receiver_model.rf_amp() ? 14 : 0)); entry.timestamp = to_string_timestamp(rtc_time::now()); entry.dataString = data_string; - entry.packetData.type = packet->type; entry.packetData.size = packet->size; entry.packetData.dataLen = packet->dataLen; diff --git a/firmware/application/apps/ble_rx_app.hpp b/firmware/application/apps/ble_rx_app.hpp index 5c4ac8c14..8cb21b189 100644 --- a/firmware/application/apps/ble_rx_app.hpp +++ b/firmware/application/apps/ble_rx_app.hpp @@ -42,6 +42,12 @@ #include "recent_entries.hpp" +// Add for heap debugging +// For ChibiOS core functions +#include "ch.h" +#include "chcore.h" +#include "chheap.h" + class BLELogger { public: Optional append(const std::filesystem::path& filename) { @@ -90,6 +96,7 @@ struct BleRecentEntry { uint64_t uniqueKey; int dbValue; BlePacketData packetData; + std::string timestamp; std::string dataString; std::string nameString; @@ -426,6 +433,20 @@ class BLERxView : public View { [this](const Message* const) { this->on_timer(); }}; + + // Widget to control the list limit + Labels label_max_entries{ + {{UI_POS_X(22), 10 * 8 - 2}, "List:", Theme::getInstance()->fg_light->foreground}}; + NumberField field_max_entries{ + {UI_POS_X(28), 10 * 8 - 2}, // Correct: Position (x, y) + 2, // Number of digits + {5, 64}, // Range (min, max) <- Coincides with max_entries + 1, // Step size + ' ' // Filler character + }; + + // Variable to store the current limit + size_t max_recent_entries = 25; }; /* BLERxView */ } /* namespace ui */ diff --git a/firmware/application/apps/ble_tx_app.cpp b/firmware/application/apps/ble_tx_app.cpp index 75e1cf879..07b5ff13c 100644 --- a/firmware/application/apps/ble_tx_app.cpp +++ b/firmware/application/apps/ble_tx_app.cpp @@ -199,6 +199,7 @@ void BLETxView::send_packet() { generateRandomMacAddress(randomMac); char advertisementData[63] = {0}; + packets[current_packet].advertisementData[62] = '\0'; strcpy(advertisementData, packets[current_packet].advertisementData); // TODO: Make this a checkbox. @@ -225,7 +226,7 @@ void BLETxView::send_packet() { uint8_t min = 0x00; uint8_t max = 0x0F; - hexDigit = min + std::rand() % (max - min + 1); + hexDigit = min + rand() % (max - min + 1); } break; default: hexDigit = 0; diff --git a/firmware/application/apps/ui_adsb_rx.cpp b/firmware/application/apps/ui_adsb_rx.cpp index 8a536bc40..7248a9702 100644 --- a/firmware/application/apps/ui_adsb_rx.cpp +++ b/firmware/application/apps/ui_adsb_rx.cpp @@ -271,10 +271,17 @@ ADSBRxDetailsView::ADSBRxDetailsView( &text_frame_pos_even, &text_frame_pos_odd, &button_aircraft_details, - &button_see_map}); + &button_see_map, + &opt_map_list}); text_icao_address.set(entry_.icao_str); + opt_map_list.on_change = [this](size_t, uint32_t mf) { + map_filter = mf; + clear_map_markers(); // reset them + pos_history.clear(); // erase the trail + }; + button_aircraft_details.on_select = [this, &nav](Button&) { aircraft_details_view_ = nav.push(entry_); nav.set_on_pop([this]() { @@ -288,7 +295,7 @@ ADSBRxDetailsView::ADSBRxDetailsView( get_map_tag(entry_), entry_.pos.altitude, GeoPos::alt_unit::FEET, - GeoPos::spd_unit::HIDDEN, + GeoPos::spd_unit::KNOTS, entry_.pos.latitude, entry_.pos.longitude, entry_.velo.heading); @@ -312,17 +319,107 @@ void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) { // AC Details view is showing, nothing to update. } else if (geomap_view_) { // Map is showing, update the current item. + if (map_filter == 1) { + // add to map trail history + add_map_trail(entry); + } geomap_view_->update_tag(get_map_tag(entry_)); - geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.velo.speed); + geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.get_ground_speed()); } else { // Details is showing, update details. refresh_ui(); } } +void ADSBRxDetailsView::get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b) { + static const struct { + int32_t alt; + uint8_t r, g, b; + } stops[] = { + {0, 220, 220, 220}, // 0 ft: White/Grey (Ground) + {2000, 255, 255, 0}, // 2k ft: Yellow + {10000, 0, 255, 0}, // 10k ft: Green + {20000, 0, 255, 255}, // 20k ft: Cyan + {30000, 60, 60, 255}, // 30k ft: Blue (Lightened for visibility on Black) + {40000, 255, 0, 255} // 40k+ ft: Magenta + }; + if (alt_ft <= stops[0].alt) { + *r = stops[0].r; + *g = stops[0].g; + *b = stops[0].b; + return; + } + const int count = sizeof(stops) / sizeof(stops[0]); + if (alt_ft >= stops[count - 1].alt) { + *r = stops[count - 1].r; + *g = stops[count - 1].g; + *b = stops[count - 1].b; + return; + } + for (int i = 0; i < count - 1; i++) { + if (alt_ft < stops[i + 1].alt) { + float t = (float)(alt_ft - stops[i].alt) / (stops[i + 1].alt - stops[i].alt); + + *r = (uint8_t)(stops[i].r + (stops[i + 1].r - stops[i].r) * t); + *g = (uint8_t)(stops[i].g + (stops[i + 1].g - stops[i].g) * t); + *b = (uint8_t)(stops[i].b + (stops[i + 1].b - stops[i].b) * t); + return; + } + } + // Fallback: in case no interval matched, default to ground color. + *r = stops[0].r; + *g = stops[0].g; + *b = stops[0].b; +} + void ADSBRxDetailsView::clear_map_markers() { if (geomap_view_) geomap_view_->clear_markers(); + if (map_filter == 1) + refresh_trail_markers(); // re add trail +} + +void ADSBRxDetailsView::refresh_trail_markers() { + if (!geomap_view_) + return; + + geomap_view_->clear_markers(); + for (const auto& pos : pos_history) { + GeoMarker marker{}; + marker.lon = pos.lon; + marker.lat = pos.lat; + marker.angle = pos.heading; + marker.tag = ""; // No tag for trail points + uint8_t r, g, b; + get_altitude_color(pos.altitude, &r, &g, &b); + marker.color = Color(r, g, b); + geomap_view_->store_marker(marker); + } +} + +void ADSBRxDetailsView::add_map_trail(const AircraftRecentEntry& entry) { + if (!geomap_view_) + return; + if (entry.pos.pos_valid == false) + return; + + if (!pos_history.empty()) { + const auto& last_pos = pos_history.back(); + const float THRESH_DEG = 0.009f; + const float THRESH_SQ = THRESH_DEG * THRESH_DEG; // 0.00002025f + + float d_lat = entry.pos.latitude - last_pos.lat; + float d_lon = entry.pos.longitude - last_pos.lon; + if ((d_lat * d_lat) + (d_lon * d_lon) < THRESH_SQ) { + return; // Moved less than ~1000m, skip (rough estimate, varies with latitude but good enough for our purposes). This prevents adding too many points when the plane is circling or taxiing. + } + } + + // Only keep the last 30 positions in the trail. + if (pos_history.size() >= 30) + pos_history.erase(pos_history.begin()); + pos_history.push_back({entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude}); + refresh_trail_markers(); } bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) { @@ -330,11 +427,16 @@ bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) { if (!geomap_view_) return false; + if (map_filter == 1) return false; // this is the 'only me' option, so skip all others + GeoMarker marker{}; marker.lon = entry.pos.longitude; marker.lat = entry.pos.latitude; marker.angle = entry.velo.heading; marker.tag = get_map_tag(entry); + uint8_t r, g, b; + get_altitude_color(entry.pos.altitude, &r, &g, &b); + marker.color = Color(r, g, b); auto markerStored = geomap_view_->store_marker(marker); return markerStored == MARKER_STORED; @@ -651,14 +753,15 @@ void ADSBRxView::refresh_ui() { // Process the entries list. for (const auto& entry : recent) { // Found the entry being shown in details view. Update it. - if (entry.key() == detail_key) { + if (entry.key() == detail_key) { // we don't add it to the markers, since this is the currently selected and shown in the middle one. This eliminates the "shadow" marker, and saves ram details_view->update(entry); current_updated = true; - } - - // NB: current entry also gets a marker so it shows up if map is panned. - if (map_needs_update && entry.pos.pos_valid && entry.state <= ADSBAgeState::Recent) { - map_needs_update = details_view->add_map_marker(entry); + } else { + // Add others only + // NB: current entry also gets a marker so it shows up if map is panned. + if (map_needs_update && entry.pos.pos_valid && entry.state <= ADSBAgeState::Recent) { + map_needs_update = details_view->add_map_marker(entry); + } } // Any work left to do? diff --git a/firmware/application/apps/ui_adsb_rx.hpp b/firmware/application/apps/ui_adsb_rx.hpp index 15280e051..c9a6e0468 100644 --- a/firmware/application/apps/ui_adsb_rx.hpp +++ b/firmware/application/apps/ui_adsb_rx.hpp @@ -64,9 +64,9 @@ namespace ui { #define VEL_AIR_SUBSONIC 3 #define VEL_AIR_SUPERSONIC 4 -#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames -#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm -#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map +#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames +#define MARKER_UPDATE_SECONDS_OSM 8 // "other" map marker redraw interval for osm +#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map /* Thresholds (in seconds) that define the transition between ages. */ struct ADSBAgeLimit { @@ -150,6 +150,19 @@ struct AircraftRecentEntry { age = 0; } + int32_t get_ground_speed() const { + if (velo.valid == false) return 0; + if (velo.type == SPD_GND) + return velo.speed; + else if (velo.type == SPD_IAS) { + if (!pos.alt_valid) return velo.speed; // can't correct without altitude, so just return IAS + return (int32_t)(velo.speed * (1.0f + (0.02f * (pos.altitude / 1000.0f)))); + } else if (velo.type == SPD_TAS) + return velo.speed; // We don't know the wind speed + else + return 0; + } + void inc_age(int delta) { age += delta; @@ -208,50 +221,50 @@ class ADSBRxAircraftDetailsView : public View { private: Labels labels{ - {{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 2 * 16}, "Registration:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 3 * 16}, "Manufacturer:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 5 * 16}, "Model:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 7 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 8 * 16}, "Number of engines:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 9 * 16}, "Engine type:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 11 * 16}, "Owner:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 13 * 16}, "Operator:", Theme::getInstance()->fg_light->foreground}}; + {{UI_POS_X(0), UI_POS_Y(1)}, "ICAO:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Registration:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "Manufacturer:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(5)}, "Model:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(7)}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(8)}, "Number of engines:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(9)}, "Engine type:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(11)}, "Owner:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(13)}, "Operator:", Theme::getInstance()->fg_light->foreground}}; Text text_icao_address{ - {5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(5), UI_POS_Y(1), 6 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_registration{ - {13 * 8, 2 * 16, 8 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(13), UI_POS_Y(2), 8 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_manufacturer{ - {UI_POS_X(0), 4 * 16, 19 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(4), 19 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_model{ - {UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_type{ - {5 * 8, 7 * 16, 22 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(5), UI_POS_Y(7), 22 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_number_of_engines{ - {18 * 8, 8 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(18), UI_POS_Y(8), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_engine_type{ - {UI_POS_X(0), 10 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(10), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_owner{ - {UI_POS_X(0), 12 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(12), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_operator{ - {UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)}, "-"}; Button button_close{ @@ -270,11 +283,14 @@ class ADSBRxDetailsView : public View { void focus() override; void update(const AircraftRecentEntry& entry); + void get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b); /* Calls forwarded to map view if shown. */ bool map_active() const { return geomap_view_; } void clear_map_markers(); /* Adds a marker for the entry to the map. Returns true on success. */ bool add_map_marker(const AircraftRecentEntry& entry); + void add_map_trail(const AircraftRecentEntry& entry); + void refresh_trail_markers(); std::string title() const override { return "Details"; } @@ -297,57 +313,72 @@ class ADSBRxDetailsView : public View { // if removed from the recent entries list. AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key}; bool airline_checked{false}; + uint8_t map_filter{0}; // 0: all, 1: only this, set by opt_map_list. + struct PosHistory { + float lat; + float lon; + uint16_t heading; + int32_t altitude; + }; + std::vector pos_history{}; Labels labels{ - {{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground}, - {{13 * 8, 1 * 16}, "Callsign:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 2 * 16}, "Last seen:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 3 * 16}, "Airline:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 5 * 16}, "Country:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 13 * 16}, "Even position frame:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 15 * 16}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}}; + {{UI_POS_X(0), UI_POS_Y(0)}, "ICAO:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(13), UI_POS_Y(0)}, "Callsign:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(1)}, "Last seen:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Airline:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(4)}, "Country:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(7)}, "Map filter:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(13)}, "Even position frame:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(15)}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}}; Text text_icao_address{ - {5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(5), UI_POS_Y(0), 6 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_callsign{ - {22 * 8, 1 * 16, 8 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(22), UI_POS_Y(0), 8 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_last_seen{ - {11 * 8, 2 * 16, 19 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(11), UI_POS_Y(1), 19 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_airline{ - {UI_POS_X(0), 4 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(3), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_country{ - {8 * 8, 5 * 16, 22 * 8, UI_POS_HEIGHT(1)}, + {UI_POS_X(8), UI_POS_Y(4), 22 * 8, UI_POS_HEIGHT(1)}, "-"}; Text text_infos{ - {UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(5), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_info2{ - {UI_POS_X(0), 7 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_frame_pos_even{ - {UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)}, "-"}; Text text_frame_pos_odd{ - {UI_POS_X(0), 16 * 16, screen_width, UI_POS_HEIGHT(1)}, + {UI_POS_X(0), UI_POS_Y(16), screen_width, UI_POS_HEIGHT(1)}, "-"}; Button button_aircraft_details{ - {UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, + {UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "A/C details"}; + OptionsField opt_map_list{ + {UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(7)}, + 12, + {{"All", 0}, + {"Only this", 1}}}; + Button button_see_map{ - {UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, + {UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "See on map"}; MessageHandlerRegistration message_handler_gps{ diff --git a/firmware/application/apps/ui_aprs_tx.cpp b/firmware/application/apps/ui_aprs_tx.cpp index 021104437..92713a0b7 100644 --- a/firmware/application/apps/ui_aprs_tx.cpp +++ b/firmware/application/apps/ui_aprs_tx.cpp @@ -29,6 +29,7 @@ #include "baseband_api.hpp" #include "portapack_shared_memory.hpp" #include "portapack_persistent_memory.hpp" +#include "ui_geomap.hpp" #include #include @@ -49,10 +50,24 @@ APRSTXView::~APRSTXView() { void APRSTXView::start_tx() { // TODO: Clean up this API to take string_views to avoid allocations. + std::string new_payload = payload; + + std::string gps = text_gps_coord.get(); + std::string token = "?GPS?"; + size_t pos = 0; + while ((pos = new_payload.find(token, pos)) != std::string::npos) { + new_payload.replace(pos, token.size(), gps); + pos += gps.size(); + } + + text_payload.set(new_payload); + + std::string path = text_path.get(); + make_aprs_frame( sym_source.to_string().c_str(), num_ssid_source.value(), sym_dest.to_string().c_str(), num_ssid_dest.value(), - payload); + new_payload, path); // uint8_t * bb_data_ptr = shared_memory.bb_data.data; // text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15)); @@ -77,6 +92,61 @@ void APRSTXView::on_tx_progress(const uint32_t progress, const bool done) { } } +void APRSTXView::process_coordinates(float lat_, float lon_) { + std::string s; + + char ns = lat_ >= 0 ? 'N' : 'S'; + float lat = (lat_ >= 0) ? lat_ : -lat_; + uint32_t lat_d = (uint32_t)lat; + uint32_t lat_m = (uint32_t)((lat - lat_d) * 6000.0f + 0.5f); + + if (lat_m >= 6000) { + lat_m = 0; + lat_d++; + } + + s += to_string_dec_uint(lat_d, 2, '0'); + s += to_string_dec_uint(lat_m / 100, 2, '0'); + s += "."; + s += to_string_dec_uint(lat_m % 100, 2, '0'); + s += ns; + + s += "/"; + + char ew = lon_ >= 0 ? 'E' : 'W'; + float lon = (lon_ >= 0) ? lon_ : -lon_; + uint32_t lon_d = (uint32_t)lon; + uint32_t lon_m = (uint32_t)((lon - lon_d) * 6000.0f + 0.5f); + + if (lon_m >= 6000) { + lon_m = 0; + lon_d++; + } + + s += to_string_dec_uint(lon_d, 3, '0'); + s += to_string_dec_uint(lon_m / 100, 2, '0'); + s += "."; + s += to_string_dec_uint(lon_m % 100, 2, '0'); + s += ew; + text_gps_coord.set(s); +} + +void APRSTXView::on_gps(const GPSPosDataMessage* message) { + if (manual_gps_mode) { + return; // no more update once set manually + } + if (message->lat < -90.0 || message->lat > 90.0 || + message->lon < -180.0 || message->lon > 180.0) { + return; + } + if (message->lat == 0.0f && message->lon == 0.0f) { + return; + } + last_lat = message->lat; + last_lon = message->lon; + process_coordinates(message->lat, message->lon); +} + APRSTXView::APRSTXView(NavigationView& nav) { baseband::run_image(portapack::spi_flash::image_tag_afsk); @@ -87,8 +157,33 @@ APRSTXView::APRSTXView(NavigationView& nav) { &num_ssid_dest, &text_payload, &button_set, + &text_gps_coord, + &button_mangps, + &gps_is_manual, + &text_path, + &button_setpath, &tx_view}); + sym_source.set_value(symsrc); + num_ssid_source.set_value(ssidsrc); + sym_dest.set_value(symdst); + num_ssid_dest.set_value(ssiddst); + text_payload.set(payload); + text_path.set(path_cache); + + sym_source.on_change = [this](SymField&) { + symsrc = sym_source.to_string(); + }; + num_ssid_source.on_change = [this](int32_t v) { + ssidsrc = v; + }; + sym_dest.on_change = [this](SymField&) { + symdst = sym_dest.to_string(); + }; + num_ssid_dest.on_change = [this](int32_t v) { + ssiddst = v; + }; + button_set.on_select = [this, &nav](Button&) { text_prompt( nav, @@ -99,6 +194,17 @@ APRSTXView::APRSTXView(NavigationView& nav) { text_payload.set(s); }); }; + button_setpath.on_select = [this, &nav](Button&) { + text_prompt( + nav, + path_cache, + 60, + ENTER_KEYBOARD_MODE_ALPHA, + [this](std::string& s) { + text_path.set(s); + path_cache = s; + }); + }; tx_view.on_edit_frequency = [this, &nav]() { auto new_view = nav.push(transmitter_model.target_frequency()); @@ -116,6 +222,23 @@ APRSTXView::APRSTXView(NavigationView& nav) { tx_view.set_transmitting(false); transmitter_model.disable(); }; + + button_mangps.on_select = [this, &nav](Button&) { + nav.push( + 0, + GeoPos::alt_unit::METERS, + GeoPos::spd_unit::HIDDEN, + last_lat, + last_lon, + [this](int32_t, float lat, float lon, int32_t) { + last_lat = lat; + last_lon = lon; + manual_gps_mode = true; + gps_is_manual.set("MANUAL"); + process_coordinates(lat, lon); + }); + }; + // process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users } } /* namespace ui */ diff --git a/firmware/application/apps/ui_aprs_tx.hpp b/firmware/application/apps/ui_aprs_tx.hpp index 18f499441..769a0bcf8 100644 --- a/firmware/application/apps/ui_aprs_tx.hpp +++ b/firmware/application/apps/ui_aprs_tx.hpp @@ -50,59 +50,109 @@ class APRSTXView : public View { 1750000 /* bandwidth */, AFSK_TX_SAMPLERATE /* sampling rate */ }; - app_settings::SettingsManager settings_{ - "tx_aprs", app_settings::Mode::TX}; + std::string symsrc = ""; + std::string symdst = ""; std::string payload{""}; + std::string path_cache = ""; + int32_t ssidsrc = 0; + int32_t ssiddst = 0; + + bool manual_gps_mode = false; + float last_lat = 0.0f; + float last_lon = 0.0f; + + app_settings::SettingsManager settings_{ + "tx_aprs", + app_settings::Mode::TX, + {{"symsrc"sv, &symsrc}, + {"symdst"sv, &symdst}, + {"ssidsrc"sv, &ssidsrc}, + {"ssiddst"sv, &ssiddst}, + {"payload"sv, &payload}, + {"last_lat"sv, &last_lat}, + {"last_lon"sv, &last_lon}, + {"path"sv, &path_cache}}}; void start_tx(); void generate_frame(); void generate_frame_pos(); void on_tx_progress(const uint32_t progress, const bool done); + void on_gps(const GPSPosDataMessage* message); + void process_coordinates(float lat, float lon); Labels labels{ - {{UI_POS_X(0), 1 * 16}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID - {{UI_POS_X(0), 2 * 16}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 4 * 16}, "Info field:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(0)}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID + {{UI_POS_X(0), UI_POS_Y(1)}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Path: (ex.: WIDE1-1,WIDE2-1)", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(6)}, "Info field:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(10)}, "GPS:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_CENTER(22), UI_POS_Y(14)}, "Use ?GPS? in the info", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_CENTER(17), UI_POS_Y(15)}, "as a placeholder.", Theme::getInstance()->fg_light->foreground}, + }; + + Text text_path{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + "WIDE1-1", + }; + Text gps_is_manual{ + {UI_POS_X(6), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, + "", }; SymField sym_source{ - {7 * 8, 1 * 16}, + {UI_POS_X(7), UI_POS_Y(0)}, 6, SymField::Type::Alpha}; NumberField num_ssid_source{ - {19 * 8, 1 * 16}, + {UI_POS_X(19), UI_POS_Y(0)}, 2, {0, 15}, 1, ' '}; SymField sym_dest{ - {7 * 8, 2 * 16}, + {UI_POS_X(7), UI_POS_Y(1)}, 6, SymField::Type::Alpha}; NumberField num_ssid_dest{ - {19 * 8, 2 * 16}, + {UI_POS_X(19), UI_POS_Y(1)}, 2, {0, 15}, 1, ' '}; Text text_payload{ - {UI_POS_X(0), 5 * 16, screen_width, 16}, - "-"}; + {UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + ""}; Button button_set{ - {UI_POS_X(0), 6 * 16, 80, 32}, + {UI_POS_X(0), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)}, "Set"}; + Text text_gps_coord{ + {UI_POS_X(0), UI_POS_Y(11), UI_POS_WIDTH(20), UI_POS_HEIGHT(1)}, + "-"}; + + Button button_mangps{ + {UI_POS_X(0), UI_POS_Y(12), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)}, + "Manual GPS"}; + Button button_setpath{ + {UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)}, + "Set Path"}; TransmitterView tx_view{ (int16_t)UI_POS_Y_BOTTOM(4), 5000, 0 // disable setting bandwith, since APRS used fixed 10k bandwidth }; + MessageHandlerRegistration message_handler_gps{ + Message::ID::GPSPosData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_gps(message); + }}; MessageHandlerRegistration message_handler_tx_progress{ Message::ID::TXProgress, [this](const Message* const p) { diff --git a/firmware/application/apps/ui_battinfo.hpp b/firmware/application/apps/ui_battinfo.hpp index 062f163f0..5357f2578 100644 --- a/firmware/application/apps/ui_battinfo.hpp +++ b/firmware/application/apps/ui_battinfo.hpp @@ -98,7 +98,7 @@ class BattinfoView : public View { "Volt"}; Button button_exit{ - {UI_POS_X_CENTER(12), 17 * 16, UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, + {UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, "Back"}; static msg_t static_fn(void* arg); Thread* thread{nullptr}; diff --git a/firmware/application/apps/ui_debug.cpp b/firmware/application/apps/ui_debug.cpp index b12ce79f0..b3dae22a4 100644 --- a/firmware/application/apps/ui_debug.cpp +++ b/firmware/application/apps/ui_debug.cpp @@ -25,6 +25,7 @@ #include "debug.hpp" #include "ch.h" +#include "hal.h" #include "radio.hpp" #include "string_format.hpp" @@ -45,6 +46,11 @@ using namespace portapack; #include "irq_controls.hpp" +#ifdef PRALINE +#include "max2831.hpp" +using namespace max2831; +#endif + namespace ui { /* DebugMemoryView *******************************************************/ @@ -148,6 +154,12 @@ uint32_t RegistersWidget::reg_read(const uint32_t register_number) { } case CT_AUDIO: return audio::debug::reg_read(register_number); +#ifdef PRALINE + case CT_FPGA: + return radio::debug::fpga::register_read(register_number); +#endif + case CT_SGPIO: + return radio::debug::sgpio::register_read(register_number); } } return 0xFFFF; @@ -175,6 +187,14 @@ void RegistersWidget::reg_write(const uint32_t register_number, const uint32_t v case CT_AUDIO: audio::debug::reg_write(register_number, value); break; +#ifdef PRALINE + case CT_FPGA: + radio::debug::fpga::register_write(register_number, value); + break; +#endif + case CT_SGPIO: + // SGPIO registers are read-only for debug purposes + break; } } } @@ -369,6 +389,3041 @@ void DebugControlsView::focus() { switches_widget.focus(); } +#ifdef PRALINE +/* RadioDiagnosticsView **************************************************/ + +RadioDiagnosticsView::RadioDiagnosticsView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_rffc, + &text_rffc_status, + &text_lbl_max, + &text_max_status, + &text_lbl_adc, + &text_adc_status, + &text_lbl_fpga, + &text_fpga_status, + &text_lbl_sgpio, + &text_sgpio_status, + &text_lbl_clock, + &text_clock_status, + &text_regs_title, + &text_lbl_rffc_reg, + &text_rffc_reg, + &text_lbl_max_reg, + &text_max_reg, + &text_lbl_fpga_reg, + &text_fpga_reg, + &text_lbl_sgpio_reg, + &text_sgpio_reg, + &text_test_result, + &button_refresh, + &button_done, + }); + + // Set title colors + text_title.set_style(Theme::getInstance()->fg_yellow); + text_regs_title.set_style(Theme::getInstance()->fg_yellow); + +#ifdef PRALINE + text_lbl_fpga.set("FPGA (iCE40):"); +#else + text_lbl_fpga.set("CPLD:"); +#endif + + button_refresh.on_select = [this](Button&) { + update_status(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial status update + update_status(); +} + +void RadioDiagnosticsView::focus() { + button_refresh.focus(); +} + +void RadioDiagnosticsView::update_status() { + // Read RFFC5072 register 0 to check if it responds + uint32_t rffc_reg0 = radio::debug::first_if::register_read(0); + bool rffc_ok = (rffc_reg0 != 0x0000) && (rffc_reg0 != 0xFFFF); + text_rffc_status.set(rffc_ok ? "OK" : "FAIL"); + text_rffc_status.set_style(rffc_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_rffc_reg.set(to_string_hex(rffc_reg0, 4)); + + // Read MAX283x register 0 to check if it responds + uint32_t max_reg0 = radio::debug::second_if::register_read(0); + bool max_ok = (max_reg0 != 0x0000) && (max_reg0 != 0x3FFF); + text_max_status.set(max_ok ? "OK" : "FAIL"); + text_max_status.set_style(max_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_max_reg.set(to_string_hex(max_reg0, 4)); + + // MAX5864 has no readback - assume OK if other SPI works + text_adc_status.set("(no readback)"); + text_adc_status.set_style(Theme::getInstance()->fg_medium); + +#ifdef PRALINE + // Read FPGA control register + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + bool fpga_ok = (fpga_ctrl != 0xFF); // 0xFF = not responding + bool dc_block = (fpga_ctrl & 0x01) != 0; + text_fpga_status.set(fpga_ok ? (dc_block ? "OK DC_BLK" : "OK NO_DC") : "FAIL"); + text_fpga_status.set_style(fpga_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // Show all FPGA registers + uint32_t fpga_r2 = radio::debug::fpga::register_read(2); + uint32_t fpga_r3 = radio::debug::fpga::register_read(3); + text_fpga_reg.set("C:" + to_string_hex(fpga_ctrl, 2) + + " D:" + to_string_hex(fpga_r2, 2) + + " T:" + to_string_hex(fpga_r3, 2)); +#else + text_fpga_status.set("(CPLD)"); + text_fpga_status.set_style(Theme::getInstance()->fg_medium); + text_fpga_reg.set("N/A"); +#endif + + // Check SGPIO status + uint32_t sgpio_enable = radio::debug::sgpio::register_read(0); // CTRL_ENABLE + uint32_t sgpio_status = radio::debug::sgpio::register_read(4); // STATUS_1 + bool sgpio_ok = (sgpio_enable != 0); + text_sgpio_status.set(sgpio_ok ? "ENABLED" : "DISABLED"); + text_sgpio_status.set_style(sgpio_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_orange); + text_sgpio_reg.set("EN:" + to_string_hex(sgpio_enable, 4) + + " ST:" + to_string_hex(sgpio_status, 4)); + + // Clock status - check if Si5351 is configured + // We can't easily read back clock status, so just show assumed state + text_clock_status.set("(assumed OK)"); + text_clock_status.set_style(Theme::getInstance()->fg_medium); + + // Summary + bool all_ok = rffc_ok && max_ok; +#ifdef PRALINE + all_ok = all_ok && fpga_ok; +#endif + if (all_ok) { + text_test_result.set("Peripherals responding. Try RX app."); + text_test_result.set_style(Theme::getInstance()->fg_green); + } else { + text_test_result.set("Check failed peripherals above."); + text_test_result.set_style(Theme::getInstance()->fg_red); + } +} + +#ifdef PRALINE +PralineRadioDebugView::PralineRadioDebugView(NavigationView& nav) { + add_children({&text_title, &text_lbl_lock, &text_lock_status, + &text_lbl_clk5, &text_clk5_status, + &text_lbl_spi, &text_spi_status, &text_lbl_fpga_ctrl, &text_fpga_ctrl, + &text_lbl_vaa, &text_vaa_status, &text_status_msg, + &button_refresh, &button_toggle_clk5, &button_done}); + + button_refresh.on_select = [this](Button&) { this->refresh(); }; + button_toggle_clk5.on_select = [this](Button&) { this->toggle_clk5(); }; + button_done.on_select = [&nav](Button&) { nav.pop(); }; + + refresh(); +} + +void PralineRadioDebugView::focus() { + button_refresh.focus(); +} + +void PralineRadioDebugView::toggle_clk5() { + // Si5351 Register 3 is the Output Enable mask. Bit 5 = CLK5. + // 0 = Enabled, 1 = Disabled. + uint8_t reg3 = portapack::clock_manager.si5351_read_register(3); + reg3 ^= 0x20; // Toggle Bit 5 (CLK5) + portapack::clock_manager.si5351_write_register(3, reg3); + refresh(); +} + +void PralineRadioDebugView::refresh() { + // 1. Check Mixer Lock Detect (GPIO6[25] / PD_11) [cite: 16, 17] + uint32_t gpio6_state = LPC_GPIO->PIN[6]; + bool locked = (gpio6_state >> 25) & 1; + text_lock_status.set(locked ? "LOCKED (OK)" : "UNLOCKED!"); + text_lock_status.set_style(locked ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // 2. Check Si5351 CLK5 Status (Reg 3, Bit 5) - 0 = ON, 1 = OFF + uint8_t si_reg3 = portapack::clock_manager.si5351_read_register(3); + bool clk5_on = !(si_reg3 & 0x20); + text_clk5_status.set(clk5_on ? "ON (40MHz)" : "OFF"); + text_clk5_status.set_style(clk5_on ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // 3. Check SPI Bit Mode (SSP1 CR0) + // DSS (Data Size Select) is Bit 3:0. 0x8 = 9-bit (MAX2831), 0xF = 16-bit (Classic) + uint32_t cr0 = LPC_SSP1->CR0; + uint8_t dss = cr0 & 0x0F; + if (dss == 0x08) + text_spi_status.set("9-Bit (Pro)"); + else + text_spi_status.set("Other (Err)"); + text_spi_status.set_style((dss == 0x08) ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // 4. Check FPGA Register 1 (DC Block status) [cite: 12, 13] + uint8_t fpga_r1 = radio::debug::fpga::register_read(1); + text_fpga_ctrl.set(to_string_hex(fpga_r1, 2)); + + // 5. Check VAA RF Power (GPIO4[1] / P8_1) - Active LOW [cite: 16, 17] + uint32_t gpio4_state = LPC_GPIO->PIN[4]; + bool vaa_on = !((gpio4_state >> 1) & 1); + text_vaa_status.set(vaa_on ? "ON" : "OFF"); + text_vaa_status.set_style(vaa_on ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // Diagnostic Summary + if (!locked && clk5_on && vaa_on) { + text_status_msg.set("Clock/Pwr OK. PLL not locked. Check tuning registers."); + text_status_msg.set_style(Theme::getInstance()->fg_red); + } else if (!clk5_on) { + text_status_msg.set("Mixer Clock is OFF. PLL cannot lock."); + text_status_msg.set_style(Theme::getInstance()->fg_red); + } else { + text_status_msg.set("Hardware Link Active."); + text_status_msg.set_style(Theme::getInstance()->fg_green); + } +} +#endif + +#ifdef PRALINE +/* WFMAudioDebugView *************************************************/ + +WFMAudioDebugView::WFMAudioDebugView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_clk0, + &text_clk0, + &text_lbl_fpga_dec, + &text_fpga_dec, + &text_lbl_post_fpga, + &text_post_fpga, + &text_section1, + &text_lbl_reg8, + &text_reg8, + &text_lbl_lpf_bw, + &text_lpf_bw, + &text_section2, + &text_lbl_fpga_r1, + &text_fpga_r1, + &text_lbl_dc_q, + &text_dc_q, + &text_section3, + &text_lbl_expected, + &text_expected, + &text_lbl_deemph, + &text_deemph, + &text_status, + &text_status2, + &button_refresh, + &button_toggle_q, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + text_section1.set_style(Theme::getInstance()->fg_yellow); + text_section2.set_style(Theme::getInstance()->fg_yellow); + text_section3.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_toggle_q.on_select = [this](Button&) { + uint32_t current = radio::debug::fpga::register_read(1); + uint8_t new_val = current ^ 0x02; // Toggle Q_INVERT bit + radio::debug::fpga::register_write(1, new_val); + radio::invalidate_spi_config(); + refresh(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void WFMAudioDebugView::focus() { + button_refresh.focus(); +} + +void WFMAudioDebugView::refresh() { + // === Si5351 CLK0 Sample Rate === + // Read MS0 parameters to calculate frequency + uint8_t reg44 = portapack::clock_manager.si5351_read_register(44); + uint8_t reg45 = portapack::clock_manager.si5351_read_register(45); + uint8_t reg46 = portapack::clock_manager.si5351_read_register(46); + + uint8_t r_div_encoded = (reg44 >> 4) & 0x07; + uint32_t r_div = 1 << r_div_encoded; + uint32_t p1 = ((uint32_t)(reg44 & 0x03) << 16) | ((uint32_t)reg45 << 8) | reg46; + uint32_t ms_div = (p1 + 512) / 128; + + // PLL A is 800 MHz + uint32_t clk0_khz = 800000 / ms_div / r_div; + uint32_t clk0_mhz_int = clk0_khz / 1000; + uint32_t clk0_khz_frac = clk0_khz % 1000; + + text_clk0.set(to_string_dec_uint(clk0_mhz_int) + "." + + to_string_dec_uint(clk0_khz_frac / 100) + + to_string_dec_uint((clk0_khz_frac / 10) % 10) + + to_string_dec_uint(clk0_khz_frac % 10) + " MHz"); + + if (clk0_khz >= 3000 && clk0_khz <= 3200) { + text_clk0.set_style(Theme::getInstance()->fg_green); + } else { + text_clk0.set_style(Theme::getInstance()->fg_red); + } + + // === FPGA Decimation === + uint8_t fpga_decim = radio::debug::fpga::register_read(2); + uint32_t fpga_div = 1 << fpga_decim; + text_fpga_dec.set("/" + to_string_dec_uint(fpga_div) + " (n=" + to_string_dec_uint(fpga_decim) + ")"); + + // === Post-FPGA Rate === + uint32_t post_fpga_khz = clk0_khz / fpga_div; + text_post_fpga.set(to_string_dec_uint(post_fpga_khz) + " kHz"); + + // For WFM, post-FPGA should be >= 384 kHz for proper audio decimation + if (post_fpga_khz >= 384) { + text_post_fpga.set_style(Theme::getInstance()->fg_green); + } else { + text_post_fpga.set_style(Theme::getInstance()->fg_orange); + } + + // === MAX2831 LPF === + uint32_t reg8 = radio::debug::second_if::register_read(8); + text_reg8.set("0x" + to_string_hex(reg8, 4)); + + uint8_t lpf_coarse = reg8 & 0x03; + const char* lpf_names[] = {"7.5 MHz", "8.5 MHz", "15 MHz", "18 MHz"}; + text_lpf_bw.set(lpf_names[lpf_coarse]); + + // 7.5 MHz is minimum, OK for mono WFM but tight for stereo + if (lpf_coarse >= 1) { + text_lpf_bw.set_style(Theme::getInstance()->fg_green); + } else { + text_lpf_bw.set_style(Theme::getInstance()->fg_orange); + } + + // === FPGA Control Register === + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + text_fpga_r1.set("0x" + to_string_hex(fpga_ctrl, 2)); + + bool dc_block = fpga_ctrl & 0x01; + bool q_invert = fpga_ctrl & 0x02; + uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03; + + text_dc_q.set(std::string(dc_block ? "DC:ON" : "DC:OFF") + + " Q:" + std::string(q_invert ? "INV" : "NOR") + + " QS:" + to_string_dec_uint(quarter_shift)); + + if (dc_block) { + text_dc_q.set_style(Theme::getInstance()->fg_green); + } else { + text_dc_q.set_style(Theme::getInstance()->fg_orange); + } + + // === Expected Audio Rate === + // WFM typically: 3072 kHz / 64 = 48 kHz audio + // Or: 3072 kHz β†’ /8 (channel) β†’ 384 kHz β†’ /8 (audio) β†’ 48 kHz + uint32_t expected_audio = post_fpga_khz / 64; // Simplified assumption + text_expected.set(to_string_dec_uint(expected_audio) + " kHz (est)"); + + if (expected_audio >= 44 && expected_audio <= 50) { + text_expected.set_style(Theme::getInstance()->fg_green); + } else { + text_expected.set_style(Theme::getInstance()->fg_red); + } + + // === De-emphasis Status === + // We can't directly read the M4 de-emphasis config, but we can indicate what SHOULD be set + // 75Β΅s for USA, 50Β΅s for Europe + text_deemph.set("Chk M4 cfg."); + text_deemph.set_style(Theme::getInstance()->fg_orange); + + // === Status Summary === + bool sample_rate_ok = (clk0_khz >= 3000 && clk0_khz <= 3200); + bool lpf_ok = (lpf_coarse <= 0x0F); // check against the 4-bit max + bool dc_ok = dc_block; + + if (sample_rate_ok && lpf_ok && dc_ok) { + text_status.set("Hardware config looks OK."); + text_status.set_style(Theme::getInstance()->fg_green); + text_status2.set("If ringy: Chk d-emph in M4!"); + text_status2.set_style(Theme::getInstance()->fg_orange); + } else { + std::string issues = "Issues: "; + if (!sample_rate_ok) issues += "SampleRate "; + if (!lpf_ok) issues += "LPF "; + if (!dc_ok) issues += "DC_Block "; + text_status.set(issues); + text_status.set_style(Theme::getInstance()->fg_red); + text_status2.set("Fix before checking audio."); + text_status2.set_style(Theme::getInstance()->fg_red); + } +} +#endif + +/* BasebandStatusView ******************************************************/ + +BasebandStatusView::BasebandStatusView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_marker, + &text_marker, + &text_lbl_loops, + &text_loops, + &text_lbl_wait, + &text_wait, + &text_lbl_xfr, + &text_xfr, + &text_lbl_missed, + &text_missed, + &text_status_line1, + &text_status_line2, + &text_status_line3, + &button_refresh, + &button_done, + }); + + // Set title color + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + update(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + update(); +} + +void BasebandStatusView::focus() { + button_refresh.focus(); +} + +void BasebandStatusView::update() { + // Read counters from shared memory + uint8_t marker = shared_memory.m4_streaming_marker; + uint32_t loops = shared_memory.m4_baseband_loops; + uint32_t wait = shared_memory.m4_dma_wait_count; + uint32_t xfr = shared_memory.m4_dma_xfr_count; + uint16_t missed = shared_memory.m4_buffer_missed; + + // Display counter values + text_marker.set(to_string_hex(marker, 2)); + text_marker.set_style((marker == 0xAA) ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + text_loops.set(to_string_dec_uint(loops)); + text_wait.set(to_string_dec_uint(wait)); + text_xfr.set(to_string_dec_uint(xfr)); + text_missed.set(to_string_dec_uint(missed)); + + // Status interpretation + if (marker == 0x00) { + text_status_line1.set("Thread NOT started!"); + text_status_line2.set("M4 baseband crash."); + text_status_line3.set("Check thread race condition."); + text_status_line1.set_style(Theme::getInstance()->fg_red); + text_status_line2.set_style(Theme::getInstance()->fg_red); + text_status_line3.set_style(Theme::getInstance()->fg_red); + } else if (marker == 0xAA && loops == 0) { + text_status_line1.set("Thread started but"); + text_status_line2.set("not looping yet."); + text_status_line3.set("Wait a moment..."); + text_status_line1.set_style(Theme::getInstance()->fg_orange); + text_status_line2.set_style(Theme::getInstance()->fg_orange); + text_status_line3.set_style(Theme::getInstance()->fg_orange); + } else if (marker == 0xAA && xfr == 0) { + text_status_line1.set("Thread looping " + to_string_dec_uint(loops) + "x"); + text_status_line2.set("But DMA NOT firing!"); + text_status_line3.set("Check SGPIO14 enable."); + text_status_line1.set_style(Theme::getInstance()->fg_orange); + text_status_line2.set_style(Theme::getInstance()->fg_orange); + text_status_line3.set_style(Theme::getInstance()->fg_orange); + } else if (xfr > 0) { + text_status_line1.set("DMA WORKING!"); + text_status_line2.set("Xfr: " + to_string_dec_uint(xfr)); + text_status_line3.set("Data flowing to baseband."); + text_status_line1.set_style(Theme::getInstance()->fg_green); + text_status_line2.set_style(Theme::getInstance()->fg_green); + text_status_line3.set_style(Theme::getInstance()->fg_green); + } +} + +/* SGPIOLiveMonitorView ****************************************************/ + +SGPIOLiveMonitorView::SGPIOLiveMonitorView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_ctrl, + &text_ctrl, + &text_lbl_in, + &text_in, + &text_lbl_ss, + &text_ss, + &text_lbl_status, + &text_status, + &text_lbl_out, + &text_out, + &text_lbl_oen, + &text_oen, + &text_diag_line1, + &text_diag_line2, + &text_diag_line3, + &text_diag_line4, + &button_refresh, + &button_done, + }); + + // Set title color + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + update(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + update(); +} + +void SGPIOLiveMonitorView::focus() { + button_refresh.focus(); +} + +void SGPIOLiveMonitorView::update() { + // Read SGPIO registers via radio debug namespace + uint32_t ctrl = radio::debug::sgpio::register_read(0); // CTRL_ENABLE + uint32_t in_reg = radio::debug::sgpio::register_read(1); // GPIO_INREG + uint32_t status = radio::debug::sgpio::register_read(4); // STATUS_1 + + // Read registers directly from LPC_SGPIO peripheral + uint32_t reg_ss = LPC_SGPIO->REG_SS[0]; + uint32_t out_reg = LPC_SGPIO->GPIO_OUTREG; + uint32_t oen_reg = LPC_SGPIO->GPIO_OENREG; + + // Display register values + text_ctrl.set(to_string_hex(ctrl, 4)); + text_in.set(to_string_hex(in_reg, 8)); + text_ss.set(to_string_hex(reg_ss, 8)); + text_status.set(to_string_hex(status, 4)); + text_out.set(to_string_hex(out_reg, 4)); + text_oen.set(to_string_hex(oen_reg, 4)); + + // Diagnostics based on register values + bool gpio_changing = (in_reg & 0xFF) != 0; // Check data pins + bool regss_active = (reg_ss != 0); + bool disable_high = (out_reg & (1U << 10)) != 0; // Bit 10 = DISABLE signal + bool sgpio8_high = (in_reg & (1U << 8)) != 0; // Bit 8 = SGPIO8 clock + bool sgpio8_output = (oen_reg & (1U << 8)) != 0; // Bit 8 = SGPIO8 direction (should be INPUT=0) + + // Line 1: SGPIO8 direction check (CRITICAL - must be INPUT) + if (sgpio8_output) { + text_diag_line1.set("SGPIO8 OUTPUT! (bus conflict)"); + text_diag_line1.set_style(Theme::getInstance()->fg_red); + } else if (disable_high) { + text_diag_line1.set("DISABLE=HIGH! FPGA stopped!"); + text_diag_line1.set_style(Theme::getInstance()->fg_red); + } else { + text_diag_line1.set("SGPIO8=IN, DISABLE=LOW"); + text_diag_line1.set_style(Theme::getInstance()->fg_green); + } + + // Line 2: Clock signal status (snapshot - can't detect toggling) + if (disable_high) { + text_diag_line2.set("Clock N/A (FPGA disabled)"); + text_diag_line2.set_style(Theme::getInstance()->fg_medium); + } else if (sgpio8_high) { + text_diag_line2.set("SGPIO8=HIGH (snapshot)"); + text_diag_line2.set_style(Theme::getInstance()->fg_green); + } else { + text_diag_line2.set("SGPIO8=LOW (snapshot)"); + text_diag_line2.set_style(Theme::getInstance()->fg_green); + } + + // Line 3: REG_SS[0] capture status + if (!regss_active && !disable_high) { + text_diag_line3.set("REG_SS[0]=0 (NOT CAPTURING!)"); + text_diag_line3.set_style(Theme::getInstance()->fg_red); + } else if (regss_active) { + text_diag_line3.set("REG_SS[0] has data"); + text_diag_line3.set_style(Theme::getInstance()->fg_green); + } else { + text_diag_line3.set("Capture N/A (FPGA disabled)"); + text_diag_line3.set_style(Theme::getInstance()->fg_medium); + } + + // Line 4: Summary based on key indicators + if (sgpio8_output) { + text_diag_line4.set("FIX: Set SGPIO8 to INPUT!"); + text_diag_line4.set_style(Theme::getInstance()->fg_red); + } else if (disable_high) { + text_diag_line4.set("FIX: Clear DISABLE bit!"); + text_diag_line4.set_style(Theme::getInstance()->fg_red); + } else if (regss_active && gpio_changing) { + text_diag_line4.set("SGPIO capturing data"); + text_diag_line4.set_style(Theme::getInstance()->fg_green); + } else if (!regss_active && gpio_changing) { + text_diag_line4.set("Data present, check slices"); + text_diag_line4.set_style(Theme::getInstance()->fg_orange); + } else if (!regss_active) { + text_diag_line4.set("No data activity"); + text_diag_line4.set_style(Theme::getInstance()->fg_orange); + } else { + text_diag_line4.set("Check DMA config"); + text_diag_line4.set_style(Theme::getInstance()->fg_green); + } +} + +/* RadioRxTestView ********************************************************/ + +RadioRxTestView::RadioRxTestView(NavigationView& nav) + : nav_(nav) { + add_children({ + &labels, + &console, + &button_init, + &button_rx, + &button_freq, + &button_sgpio, + &button_full, + &button_step, + &button_done, + }); + + button_init.on_select = [this](Button&) { + run_init_test(); + }; + + button_rx.on_select = [this](Button&) { + run_rx_mode_test(); + }; + + button_freq.on_select = [this](Button&) { + run_freq_test(); + }; + + button_sgpio.on_select = [this](Button&) { + run_sgpio_test(); + }; + + button_full.on_select = [this](Button&) { + run_full_test(); + }; + + button_step.on_select = [this](Button&) { + run_step_test(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + log("Ready. Press buttons to test."); + log("Init->RX->Freq->SGPIO"); +} + +void RadioRxTestView::focus() { + button_full.focus(); +} + +void RadioRxTestView::log(const std::string& msg) { + console.writeln(msg); +} + +void RadioRxTestView::log_registers(const std::string& label) { + // RFFC5072 register 0 + uint32_t rffc_r0 = radio::debug::first_if::register_read(0); + + // MAX283x registers 0, 3, 4 (key freq regs) + uint32_t max_r0 = radio::debug::second_if::register_read(0); + uint32_t max_r3 = radio::debug::second_if::register_read(3); + uint32_t max_r4 = radio::debug::second_if::register_read(4); + + // SGPIO + uint32_t sgpio_en = radio::debug::sgpio::register_read(0); + uint32_t sgpio_data = radio::debug::sgpio::register_read(5); + + log(label); + log(" RFFC:" + to_string_hex(rffc_r0, 4)); + log(" MAX r0:" + to_string_hex(max_r0, 4) + + " r3:" + to_string_hex(max_r3, 4) + + " r4:" + to_string_hex(max_r4, 4)); + log(" SGPIO en:" + to_string_hex(sgpio_en, 4) + + " dat:" + to_string_hex(sgpio_data, 8)); + +#ifdef PRALINE + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + log(" FPGA ctrl:" + to_string_hex(fpga_ctrl, 2)); +#endif +} + +void RadioRxTestView::run_init_test() { + console.clear(true); + log("=== INIT TEST ==="); + + log("radio::init()..."); + radio::init(); + radio_initialized_ = true; + + log("set_baseband_rate(8M)..."); + radio::set_baseband_rate(8000000); + + // Read Si5351 status to check PLL lock + uint8_t si_status = portapack::clock_manager.si5351_read_status(); + log("Si5351 status: " + to_string_hex(si_status, 2)); + if (si_status & 0x20) { + log(" WARNING: PLL A unlocked!"); + } else { + log(" PLL A locked OK"); + } + + // Read crystal cap register + uint8_t xtal_cap = portapack::clock_manager.si5351_read_register(183); + log("Crystal cap: " + to_string_hex(xtal_cap, 2)); + + log_registers("[After init]"); + log("Init+clocks done."); +} + +void RadioRxTestView::run_rx_mode_test() { + console.clear(true); + log("=== RX MODE TEST ==="); + + if (!radio_initialized_) { + log("ERROR: Run Init first!"); + return; + } + + log_registers("[Before RX mode]"); + + log("Calling set_direction(Receive)..."); + radio::set_direction(rf::Direction::Receive); + + log_registers("[After RX mode]"); + + // Check MAX283x mode register + uint32_t max_r0 = radio::debug::second_if::register_read(0); + log("MAX r0 after RX: " + to_string_hex(max_r0, 4)); + log("RX mode set."); +} + +void RadioRxTestView::run_freq_test() { + console.clear(true); + log("=== FREQ TEST ==="); + + if (!radio_initialized_) { + log("ERROR: Run Init first!"); + return; + } + + log_registers("[Before freq set]"); + + log("Setting " + to_string_dec_uint(test_frequency_ / 1000000) + " MHz..."); + bool result = radio::set_tuning_frequency(test_frequency_); + + log_registers("[After freq set]"); + + log(result ? "Freq set OK" : "Freq set FAILED"); + + // Show expected vs actual for MAX2831 freq regs + // For 433 MHz with MAX2831: F_LO = 40M * (N + F/2^20) / 2 + // N = 43, F = ~629146 for ~433 MHz + log("(Expected: N~43 in r3, F_hi in r4)"); +} + +void RadioRxTestView::run_sgpio_test() { + console.clear(true); + log("=== SGPIO TEST (FIXED) ==="); + + // CRITICAL FIX: Set DISABLE=HIGH first (reference HackRF pattern) + LPC_SGPIO->GPIO_OENREG = (1U << 10) | (1U << 11); // SGPIO10,11 outputs + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH during config + log("Set DISABLE=HIGH"); + + // Small delay for signals to settle + for (volatile int i = 0; i < 10000; i++) { + } + + // NOW enable streaming (DISABLE=LOW) + LPC_SGPIO->GPIO_OUTREG = 0; // DISABLE=LOW, DIRECTION=LOW (RX) + log("Set DISABLE=LOW (streaming)"); + for (volatile int i = 0; i < 10000; i++) { + } + + // Read raw GPIO_INREG multiple times + uint32_t g[4]; + for (int i = 0; i < 4; i++) { + g[i] = LPC_SGPIO->GPIO_INREG; + for (volatile int j = 0; j < 10000; j++) { + } + } + + log("GPIO_IN:"); + log(" " + to_string_hex(g[0], 8) + " " + to_string_hex(g[1], 8)); + log(" " + to_string_hex(g[2], 8) + " " + to_string_hex(g[3], 8)); + + bool changing = (g[0] != g[1]) || (g[1] != g[2]) || (g[2] != g[3]); + + if (changing) { + log("PASS: Data changing!"); + } else if (g[0] == 0) { + log("FAIL: All zeros"); + } else if (g[0] == 0x00000FFF) { + log("FAIL: 0xFFF = pull-ups"); + log("FPGA not driving data"); + } else { + log("FAIL: Static " + to_string_hex(g[0], 8)); + } + + uint32_t out = LPC_SGPIO->GPIO_OUTREG; + log("HOST_DIS=" + to_string_dec_uint((out >> 10) & 1)); +} + +void RadioRxTestView::run_full_test() { + console.clear(true); + log("=== FULL RX TEST ==="); + + // Step 1: Init + log("[1/6] Init radio..."); + radio::init(); + radio_initialized_ = true; + + // Step 2: Set sample rate (configures Si5351 clocks!) + log("[2/6] Set 8M sample rate..."); + radio::set_baseband_rate(8000000); + + // Step 3: RX mode + log("[3/6] Set RX mode..."); + radio::set_direction(rf::Direction::Receive); + + // Step 4: Frequency - use 2437 MHz (WiFi ch6) which is in MAX2831 range + uint32_t wifi_freq = 2437000000; + log("[4/6] Set 2437 MHz (WiFi)..."); + bool freq_ok = radio::set_tuning_frequency(wifi_freq); + log(freq_ok ? " Freq OK" : " Freq FAIL"); + + // Step 5: Configure SGPIO outputs with correct DISABLE sequence + log("[5/6] Configure SGPIO..."); + // CRITICAL FIX: Set DISABLE=HIGH first + LPC_SGPIO->GPIO_OENREG = (1U << 10) | (1U << 11); // SGPIO10,11 as outputs + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH during config + log(" DISABLE=HIGH"); + + // Delay for settle + for (volatile int i = 0; i < 10000; i++) { + } + + // NOW enable streaming (DISABLE=LOW) + LPC_SGPIO->GPIO_OUTREG = 0; // DISABLE=LOW, DIRECTION=LOW (RX) + log(" DISABLE=LOW (streaming)"); + + // Step 6: Check raw GPIO pins + log("[6/6] Check GPIO pins..."); + + // Delay for stabilization + for (volatile int i = 0; i < 200000; i++) { + } + + // Read raw GPIO_INREG multiple times + uint32_t g1 = LPC_SGPIO->GPIO_INREG; + for (volatile int i = 0; i < 10000; i++) { + } + uint32_t g2 = LPC_SGPIO->GPIO_INREG; + for (volatile int i = 0; i < 10000; i++) { + } + uint32_t g3 = LPC_SGPIO->GPIO_INREG; + for (volatile int i = 0; i < 10000; i++) { + } + uint32_t g4 = LPC_SGPIO->GPIO_INREG; + + log("GPIO_IN readings:"); + log(" " + to_string_hex(g1, 8) + " " + to_string_hex(g2, 8)); + log(" " + to_string_hex(g3, 8) + " " + to_string_hex(g4, 8)); + + bool gpio_changing = (g1 != g2) || (g2 != g3) || (g3 != g4); + bool gpio_not_zero = (g1 != 0); + bool gpio_not_fff = (g1 != 0x00000FFF); + + // Check Si5351 output enable register (reg 3) + // Bits 0-7: CLK0-7 output enable (0=enabled, 1=disabled) + // We want CLK0 and CLK1 enabled (bits 0,1 = 0) + log("---"); + + if (gpio_changing) { + log("=== PASS: Data flowing! ==="); + } else if (gpio_not_fff && gpio_not_zero) { + log("=== PARTIAL: Static data ==="); + log("FPGA outputs but no clock?"); + } else if (!gpio_not_zero) { + log("=== FAIL: All zeros ==="); + log("FPGA not driving outputs"); + } else { + log("=== FAIL: All FFF (pull-ups) ==="); + log("FPGA outputs high-Z"); + log("Check: Si5351 CLK0/CLK1"); + } + + log_registers("[Final]"); +} + +bool RadioRxTestView::check_gpio_changing() { + uint32_t g[4]; + for (int i = 0; i < 4; i++) { + g[i] = LPC_SGPIO->GPIO_INREG; + for (volatile int j = 0; j < 10000; j++) { + } + } + return (g[0] != g[1]) || (g[1] != g[2]) || (g[2] != g[3]); +} + +void RadioRxTestView::run_step_test() { + console.clear(true); + log("=== STEP TEST (FIXED) ==="); + log("Correct DISABLE sequence"); + + // Ensure radio is initialized + if (!radio_initialized_) { + log("Init radio..."); + radio::init(); + radio_initialized_ = true; + radio::set_baseband_rate(8000000); + radio::set_direction(rf::Direction::Receive); + radio::set_tuning_frequency(2437000000); + } + + // Step 0: Baseline with DISABLE=HIGH first + log("[0] Baseline (DISABLE=HIGH)"); + LPC_SGPIO->CTRL_ENABLE = 0; // Disable all slices + LPC_SGPIO->GPIO_OENREG = 0x0C00; // Bits 10, 11 outputs + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH first! + for (volatile int i = 0; i < 100000; i++) { + } + + // Now enable streaming to check baseline + LPC_SGPIO->GPIO_OUTREG = 0x0000; // DISABLE=LOW + for (volatile int i = 0; i < 100000; i++) { + } + bool step0 = check_gpio_changing(); + log(step0 ? " PASS: Data changing" : " FAIL: Data static"); + if (!step0) { + log("ABORT: Baseline broken"); + return; + } + + // NOW disable streaming for configuration + log("[Config] Set DISABLE=HIGH"); + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH + for (volatile int i = 0; i < 100000; i++) { + } + + // Step 1: OUT_MUX_CFG data pins - test individually + log("[1] OUT_MUX_CFG[0-7] data"); + uint32_t data_out_mux = (9U << 0) | (0U << 4); // DOUT_DOUTM8A, GPIO_OE + + for (size_t i = 0; i < 8; i++) { + uint32_t before = LPC_SGPIO->GPIO_INREG; + LPC_SGPIO->OUT_MUX_CFG[i] = data_out_mux; + for (volatile int j = 0; j < 50000; j++) { + } + uint32_t after = LPC_SGPIO->GPIO_INREG; + bool ok = check_gpio_changing(); + + log(" [" + to_string_dec_uint(i) + "] " + + to_string_hex(before & 0xFF, 2) + "->" + + to_string_hex(after & 0xFF, 2) + + (ok ? " OK" : " FAIL")); + + if (!ok) { + log("CULPRIT: OUT_MUX_CFG[" + to_string_dec_uint(i) + "]"); + return; + } + } + log(" All data pins PASS"); + + // Step 2: OUT_MUX_CFG control pins - SKIP PIN 10 (HOST_DISABLE) + log("[2] OUT_MUX_CFG ctrl pins"); + log(" (skipping pin 10 - breaks)"); + + struct { + int pin; + uint32_t val; + } ctrl_pins[] = { + {8, (0U << 0) | (0U << 4)}, + {9, (0U << 0) | (0U << 4)}, + // {10, (4U << 0) | (0U << 4)}, // SKIP - causes failure + {11, (4U << 0) | (0U << 4)}, + {14, (0U << 0) | (0U << 4)}}; + + for (auto& p : ctrl_pins) { + uint32_t before = LPC_SGPIO->GPIO_INREG; + LPC_SGPIO->OUT_MUX_CFG[p.pin] = p.val; + for (volatile int i = 0; i < 50000; i++) { + } + uint32_t after = LPC_SGPIO->GPIO_INREG; + bool ok = check_gpio_changing(); + + log(" [" + to_string_dec_uint(p.pin) + "] " + + to_string_hex(before & 0xFF, 2) + "->" + + to_string_hex(after & 0xFF, 2) + + (ok ? " OK" : " FAIL")); + + if (!ok) { + log("CULPRIT: OUT_MUX_CFG[" + to_string_dec_uint(p.pin) + "]"); + return; + } + } + log(" All ctrl pins PASS"); + + // Step 3: Set GPIO_OENREG for RX + log("[3] GPIO_OENREG full RX"); + LPC_SGPIO->GPIO_OENREG = 0x0C00; // Keep same as baseline + for (volatile int i = 0; i < 100000; i++) { + } + bool step3 = check_gpio_changing(); + log(step3 ? " PASS" : " FAIL: Data stopped!"); + if (!step3) { + log("CULPRIT: GPIO_OENREG"); + return; + } + + // Step 3.5: Configure slice D as clock source (CRITICAL!) + log("[3.5] Slice D clock source"); + const uint32_t slice_d = 3; + // SGPIO_MUX_CFG: External clock from SGPIO8, qualifier from SGPIO9 + LPC_SGPIO->SGPIO_MUX_CFG[slice_d] = (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (0U << 11) | (0U << 12); + // SLICE_MUX_CFG: 1 bit per clock, CLKGEN_MODE=1 (external clock!) <- FIX + LPC_SGPIO->SLICE_MUX_CFG[slice_d] = (0U << 0) | (0U << 1) | (0U << 2) | (0U << 3) | (1U << 4) | (0U << 6) | (0U << 8); + LPC_SGPIO->PRESET[slice_d] = 0; + LPC_SGPIO->COUNT[slice_d] = 0; + LPC_SGPIO->POS[slice_d] = (0x1F << 0) | (0x1F << 8); + LPC_SGPIO->REG[slice_d] = 0x11111111; + LPC_SGPIO->REG_SS[slice_d] = 0x11111111; + // Enable slice D counter + LPC_SGPIO->CTRL_ENABLE = (1U << slice_d); + for (volatile int i = 0; i < 100000; i++) { + } + bool step3_5 = check_gpio_changing(); + log(step3_5 ? " PASS" : " FAIL: Data stopped!"); + if (!step3_5) { + log("CULPRIT: Slice D config"); + return; + } + + // Step 4: SGPIO_MUX_CFG slice A + log("[4] SGPIO_MUX_CFG[A]"); + LPC_SGPIO->SGPIO_MUX_CFG[0] = (1U << 0) | (0U << 1) | (3U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (0U << 11) | (0U << 12); // Clock from slice D (bit3-4=3), external pin SGPIO8, qualifier SGPIO9 + for (volatile int i = 0; i < 100000; i++) { + } + bool step4 = check_gpio_changing(); + log(step4 ? " PASS" : " FAIL: Data stopped!"); + if (!step4) { + log("CULPRIT: SGPIO_MUX_CFG[A]"); + return; + } + + // Step 5: SLICE_MUX_CFG slice A + log("[5] SLICE_MUX_CFG[A]"); + LPC_SGPIO->SLICE_MUX_CFG[0] = (0U << 0) | (0U << 1) | (1U << 2) | (0U << 3) | (1U << 4) | (3U << 6) | (0U << 8); // CLKGEN_MODE=1 (external clock!), PARALLEL_MODE 1 byte + for (volatile int i = 0; i < 100000; i++) { + } + bool step5 = check_gpio_changing(); + log(step5 ? " PASS" : " FAIL: Data stopped!"); + if (!step5) { + log("CULPRIT: SLICE_MUX_CFG[A]"); + return; + } + + // Step 6: Slice A registers + log("[6] Slice A registers"); + LPC_SGPIO->PRESET[0] = 0; + LPC_SGPIO->COUNT[0] = 0; + LPC_SGPIO->POS[0] = (0x1F << 0) | (0x1F << 8); // pos, pos_reset + LPC_SGPIO->REG[0] = 0; + LPC_SGPIO->REG_SS[0] = 0; + for (volatile int i = 0; i < 100000; i++) { + } + bool step6 = check_gpio_changing(); + log(step6 ? " PASS" : " FAIL: Data stopped!"); + if (!step6) { + log("CULPRIT: Slice A registers"); + return; + } + + // Step 7: Enable slice A counter (keep slice D enabled) - still with DISABLE=HIGH + log("[7] Enable slices D+A"); + LPC_SGPIO->CTRL_ENABLE = (1U << 3) | (1U << 0); // Slice D + Slice A + for (volatile int i = 0; i < 100000; i++) { + } + + // Check STATUS_1 BEFORE enabling streaming + uint32_t status_pre = LPC_SGPIO->STATUS_1; + log(" STATUS_1 (pre): " + to_string_hex(status_pre, 4)); + + // Step 8: Enable streaming (DISABLE=LOW) - THIS IS THE CRITICAL TEST + log("[8] Enable streaming (DISABLE=LOW)"); + LPC_SGPIO->GPIO_OUTREG = 0; // DISABLE=LOW + for (volatile int i = 0; i < 100000; i++) { + } + + // Check if slices become active + uint32_t status_post = LPC_SGPIO->STATUS_1; + uint32_t regss = LPC_SGPIO->REG_SS[0]; + uint32_t count_a = LPC_SGPIO->COUNT[0]; + + log(" STATUS_1 (post): " + to_string_hex(status_post, 4)); + log(" REG_SS[0]: " + to_string_hex(regss, 8)); + log(" COUNT[0]: " + to_string_hex(count_a, 8)); + + bool step8 = check_gpio_changing(); + log(step8 ? " GPIO still changing" : " GPIO stopped!"); + + if ((status_post & 1) && regss != 0) { + log("=== SUCCESS! Slice A capturing! ==="); + } else if (status_post & 1) { + log("=== PARTIAL: Slice A active but REG_SS=0 ==="); + } else { + log("=== FAIL: Slice A not active ==="); + log("Expected: STATUS_1 bit 0 = 1"); + log("Actual: STATUS_1 bit 0 = " + to_string_dec_uint(status_post & 1)); + } +} + +/* SGPIO8ClockDetectorView ***********************************************/ + +SGPIO8ClockDetectorView::SGPIO8ClockDetectorView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_samples, + &text_samples, + &text_lbl_toggles, + &text_toggles, + &text_status, + &button_sample, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_sample.on_select = [this](Button&) { + sample_sgpio8(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Auto-sample on load + sample_sgpio8(); +} + +void SGPIO8ClockDetectorView::focus() { + button_sample.focus(); +} + +void SGPIO8ClockDetectorView::sample_sgpio8() { + // Sample SGPIO8 (bit 8 of GPIO_INREG) as fast as possible + // NOTE: Software sampling cannot accurately measure clock frequency + // This only detects presence/absence of clock activity + const int num_samples = 2000; + uint8_t samples[num_samples]; + + // Sample as fast as possible + for (int i = 0; i < num_samples; i++) { + samples[i] = (LPC_SGPIO->GPIO_INREG >> 8) & 1; + } + + // Count toggles (transitions 0β†’1 or 1β†’0) + int toggles = 0; + for (int i = 1; i < num_samples; i++) { + if (samples[i] != samples[i - 1]) { + toggles++; + } + } + + // Display first 20 samples + std::string sample_str; + for (int i = 0; i < 20 && i < num_samples; i++) { + sample_str += (samples[i] ? "1" : "0"); + } + text_samples.set(sample_str); + + // Display toggle count + text_toggles.set(to_string_dec_uint(toggles) + " / " + + to_string_dec_uint(num_samples - 1) + " transitions"); + + // Status interpretation - just presence detection + if (toggles > 100) { + text_status.set("CLOCK ACTIVE"); + text_status.set_style(Theme::getInstance()->fg_green); + } else if (toggles > 0) { + text_status.set("SOME ACTIVITY (" + to_string_dec_uint(toggles) + ")"); + text_status.set_style(Theme::getInstance()->fg_orange); + } else { + text_status.set("NO CLOCK - Stuck " + + std::string(samples[0] ? "HIGH" : "LOW")); + text_status.set_style(Theme::getInstance()->fg_red); + } +} + +/* Si5351DebugView *******************************************************/ + +Si5351DebugView::Si5351DebugView(NavigationView& nav) + : nav_(nav) { + add_children({&text_title, + &text_status_label, + &text_status_value, + &text_pll_a_label, + &text_pll_a_status, + &text_pll_b_label, + &text_pll_b_status, + &text_sys_init_label, + &text_sys_init_status, + &text_xtal_cap_label, + &text_xtal_cap_value, + &text_clk0_label, + &text_clk0_status, + &text_clk0_freq_value, + &text_clk0_div_value, + &text_clk1_label, + &text_clk1_status, + &text_clk4_label, + &text_clk4_status, + &text_clk5_label, + &text_clk5_status, + &button_refresh, + &button_reset_pll, + &button_done}); + + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh_status(); + }; + + button_reset_pll.on_select = [this](Button&) { + reset_pll(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Auto-refresh on load + refresh_status(); +} + +void Si5351DebugView::focus() { + button_refresh.focus(); +} + +void Si5351DebugView::refresh_status() { + // Read device status register (reg 0) + uint8_t status = portapack::clock_manager.si5351_read_status(); + text_status_value.set("0x" + to_string_hex(status, 2)); + + // Decode status bits + bool pll_a_locked = !(status & 0x20); // Bit 5: LOL_A (Loss of Lock A) + bool pll_b_locked = !(status & 0x40); // Bit 6: LOL_B (Loss of Lock B) + bool sys_init = (status & 0x80); // Bit 7: SYS_INIT + // bool los_clkin = (status & 0x10); // Bit 4: LOS (Loss of Signal) (quoted as it's computed but unused) + + // PLL A status + if (pll_a_locked) { + text_pll_a_status.set("LOCKED"); + text_pll_a_status.set_style(Theme::getInstance()->fg_green); + } else { + text_pll_a_status.set("UNLOCKED"); + text_pll_a_status.set_style(Theme::getInstance()->fg_red); + } + + // PLL B status + if (pll_b_locked) { + text_pll_b_status.set("LOCKED"); + text_pll_b_status.set_style(Theme::getInstance()->fg_green); + } else { + text_pll_b_status.set("UNLOCKED (unused)"); + text_pll_b_status.set_style(Theme::getInstance()->fg_orange); + } + + // SYS_INIT status + if (sys_init) { + text_sys_init_status.set("IN PROGRESS"); + text_sys_init_status.set_style(Theme::getInstance()->fg_orange); + } else { + text_sys_init_status.set("COMPLETE"); + text_sys_init_status.set_style(Theme::getInstance()->fg_green); + } + + // Read crystal load capacitance (reg 183) + uint8_t xtal_cap = portapack::clock_manager.si5351_read_register(183); + text_xtal_cap_value.set("0x" + to_string_hex(xtal_cap, 2) + + " (" + to_string_dec_uint((xtal_cap >> 6) & 0x03) + ")"); + + // Read clock output enables (reg 16-23 control, reg 3 for output enable mask) + uint8_t output_enable_mask = portapack::clock_manager.si5351_read_register(3); + + // CLK0 (bit 0 of reg 3, reg 16 for control) + uint8_t clk0_ctrl = portapack::clock_manager.si5351_read_register(16); + bool clk0_enabled = !(output_enable_mask & 0x01) && !(clk0_ctrl & 0x80); + text_clk0_status.set(clk0_enabled ? "ON" : "OFF"); + text_clk0_status.set_style(clk0_enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // Read MS0 multisynth parameters (registers 42-49) to calculate actual frequency + // Si5351 MS0 Register Layout: + // Reg 42: P3[15:8] + // Reg 43: P3[7:0] + // Reg 44: bits 6:4 = R_DIV[2:0], bits 1:0 = P1[17:16] + // Reg 45: P1[15:8] + // Reg 46: P1[7:0] + // Reg 47: bits 7:4 = P3[19:16], bits 3:0 = P2[19:16] + // Reg 48: P2[15:8] + // Reg 49: P2[7:0] + uint8_t reg44 = portapack::clock_manager.si5351_read_register(44); + uint8_t reg45 = portapack::clock_manager.si5351_read_register(45); + uint8_t reg46 = portapack::clock_manager.si5351_read_register(46); + + // Decode R divider from bits 6:4 of register 44 + uint8_t r_div_encoded = (reg44 >> 4) & 0x07; + uint32_t r_div = 1 << r_div_encoded; // R = 2^r_div_encoded + + // Decode P1 (18-bit value): bits 1:0 of reg44 = P1[17:16], reg45 = P1[15:8], reg46 = P1[7:0] + uint32_t p1 = ((uint32_t)(reg44 & 0x03) << 16) | ((uint32_t)reg45 << 8) | reg46; + + // Calculate divider: a = (P1 + 512) / 128 for integer dividers (b=0) + // Expected for 8 MHz: P1=5888 (0x1700), a=50 + uint32_t ms_div = (p1 + 512) / 128; // Integer divider value + + // Calculate frequency: f_out = 800 or 800 MHz / ms_div / r_div + uint32_t vco_khz = (clk0_ctrl & 0x20) ? 800000 : 800000; // PLLB vs PLLA + uint32_t freq_khz = vco_khz / ms_div / r_div; // Result in kHz + + // Show P1 value and R45 for debugging + text_clk0_freq_value.set("F:" + to_string_dec_uint(freq_khz / 1000) + "MHz (P1:" + to_string_hex(p1, 4) + ")"); + text_clk0_div_value.set("DIV: MS=" + to_string_dec_uint(ms_div) + + " R=" + to_string_dec_uint(r_div)); + + // Color code based on expected 8 MHz + if (freq_khz >= 7900 && freq_khz <= 8100) { + text_clk0_freq_value.set_style(Theme::getInstance()->fg_green); + text_clk0_div_value.set_style(Theme::getInstance()->fg_green); + } else { + text_clk0_freq_value.set_style(Theme::getInstance()->fg_red); + text_clk0_div_value.set_style(Theme::getInstance()->fg_red); + } + + // CLK1 (bit 1 of reg 3, reg 17 for control) + uint8_t clk1_ctrl = portapack::clock_manager.si5351_read_register(17); + bool clk1_enabled = !(output_enable_mask & 0x02) && !(clk1_ctrl & 0x80); + text_clk1_status.set(clk1_enabled ? "ON" : "OFF"); + text_clk1_status.set_style(clk1_enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // CLK4 (MAX2831 reference - bit 4 of reg 3, reg 20 for control) + uint8_t clk4_ctrl = portapack::clock_manager.si5351_read_register(20); + bool clk4_enabled = !(output_enable_mask & 0x10) && !(clk4_ctrl & 0x80); + text_clk4_status.set(clk4_enabled ? "ON (40MHz)" : "OFF"); + text_clk4_status.set_style(clk4_enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // CLK5 (RFFC5072 reference - bit 5 of reg 3, reg 21 for control) + uint8_t clk5_ctrl = portapack::clock_manager.si5351_read_register(21); + bool clk5_enabled = !(output_enable_mask & 0x20) && !(clk5_ctrl & 0x80); + text_clk5_status.set(clk5_enabled ? "ON (40MHz)" : "OFF"); + text_clk5_status.set_style(clk5_enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); +} + +void Si5351DebugView::reset_pll() { + // Reset both PLLs (write to reg 177) + portapack::clock_manager.si5351_read_register(177); // Read first + portapack::clock_manager.si5351_write_register(177, 0xAC); // Reset both PLLs + + // Small delay for PLL to settle + chThdSleepMilliseconds(10); + + // Refresh status to show new lock state + refresh_status(); +} + +#ifdef PRALINE +/* SignalPathStatusView *************************************************/ + +SignalPathStatusView::SignalPathStatusView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_max_enable, + &text_max_enable, + &text_lbl_max_mode, + &text_max_mode, + &text_lbl_rf_path, + &text_rf_path, + &text_lbl_filter, + &text_filter, + &text_lbl_mixer, + &text_mixer, + &text_lbl_rf_amp, + &text_rf_amp, + &text_lbl_lna, + &text_lna, + &text_lbl_vga, + &text_vga, + &text_lbl_fpga_decim, + &text_fpga_decim, + &text_lbl_fpga_ctrl_dc_q, + &text_fpga_ctrl_dc_q, + &text_lbl_fpga_ctrl_qs, + &text_fpga_ctrl_qs, + &text_status, + &button_refresh, + &button_toggle_q, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh_status(); + }; + + button_toggle_q.on_select = [this](Button&) { + static bool q_override = false; + q_override = !q_override; + + uint8_t ctrl_reg = 0x01; // DC_BLOCK always on + if (q_override) { + ctrl_reg |= 0x02; // Force Q_INVERT ON + } + + radio::debug::fpga::register_write(1, ctrl_reg); + radio::invalidate_spi_config(); + + refresh_status(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + refresh_status(); +} + +void SignalPathStatusView::focus() { + button_refresh.focus(); +} + +void SignalPathStatusView::refresh_status() { + // Get cached state from radio driver + rf::Direction direction = radio::debug::get_cached_direction(); + bool rf_amp = radio::debug::get_cached_rf_amp(); + int_fast8_t cached_lna = radio::debug::get_cached_lna_gain(); + int_fast8_t cached_vga = radio::debug::get_cached_vga_gain(); + + // Get current band. + auto current_band = radio::debug::rf_path_info::get_current_band(); + switch (current_band) { + case rf::path::Band::Low: + text_filter.set("LOW PASS"); + text_filter.set_style(Theme::getInstance()->fg_green); + text_mixer.set("ENABLED"); + text_mixer.set_style(Theme::getInstance()->fg_green); + break; + + case rf::path::Band::Mid: + text_filter.set("BYPASS"); + text_filter.set_style(Theme::getInstance()->fg_green); + text_mixer.set("DISABLED"); + text_mixer.set_style(Theme::getInstance()->fg_orange); + break; + + case rf::path::Band::High: + text_filter.set("HIGH PASS"); + text_filter.set_style(Theme::getInstance()->fg_green); + text_mixer.set("ENABLED"); + text_mixer.set_style(Theme::getInstance()->fg_green); + break; + + default: + text_filter.set("UNKNOWN"); + text_filter.set_style(Theme::getInstance()->fg_red); + text_mixer.set("UNKNOWN"); + text_mixer.set_style(Theme::getInstance()->fg_red); + } + + // Read actual register values to verify + uint32_t max_r11 = radio::debug::second_if::register_read(11); + + // Decode actual LNA gain from register (bits 6:5) + uint8_t lna_bits = (max_r11 >> 5) & 0x03; + int actual_lna_db; + switch (lna_bits) { + case 0: + actual_lna_db = 0; + break; // -33 dB from max + case 2: + actual_lna_db = 17; + break; // -16 dB from max + case 3: + actual_lna_db = 33; + break; // Maximum + default: + actual_lna_db = -1; + break; // Invalid + } + + // Decode actual VGA gain from register (bits 4:0) + uint8_t vga_bits = (max_r11 >> 0) & 0x1F; + int actual_vga_db = vga_bits * 2; // 0-31 β†’ 0-62 dB + + // MAX2831 Enable/Mode (GPIO-controlled, show cached state) + bool rx_mode = (direction == rf::Direction::Receive); + + text_max_enable.set("ENABLED cached"); + text_max_enable.set_style(Theme::getInstance()->fg_green); + + text_max_mode.set(rx_mode ? "RX cached" : "TX cached"); + text_max_mode.set_style(rx_mode ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // RF path direction + text_rf_path.set(rx_mode ? "RECEIVE" : "TRANSMIT"); + text_rf_path.set_style(rx_mode ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // RF amp (GPIO-controlled, show cached state) + text_rf_amp.set(rf_amp ? "ON cached" : "OFF cached"); + text_rf_amp.set_style(rf_amp ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // LNA gain - show both cached and actual + if (actual_lna_db == cached_lna) { + text_lna.set(to_string_dec_uint(actual_lna_db) + " dB"); + text_lna.set_style(Theme::getInstance()->fg_green); + } else if (actual_lna_db >= 0) { + // MAX2831 has discrete steps: 0, 17, 33 dB + // Allow Β±8 dB tolerance for rounding + int diff = (actual_lna_db > cached_lna) ? (actual_lna_db - cached_lna) : (cached_lna - actual_lna_db); + + if (diff <= 8) { + // Within rounding tolerance - show as OK with note + text_lna.set(to_string_dec_uint(actual_lna_db) + " dB (req:" + + to_string_dec_uint(cached_lna) + ")"); + text_lna.set_style(Theme::getInstance()->fg_green); + } else { + // Genuine mismatch + text_lna.set(to_string_dec_uint(actual_lna_db) + " dB (!" + + to_string_dec_uint(cached_lna) + ")"); + text_lna.set_style(Theme::getInstance()->fg_red); + } + } else { + text_lna.set("INVALID"); + text_lna.set_style(Theme::getInstance()->fg_red); + } + + // VGA gain - show both cached and actual + if (actual_vga_db == cached_vga) { + text_vga.set(to_string_dec_uint(actual_vga_db) + " dB"); + text_vga.set_style(Theme::getInstance()->fg_green); + } else { + text_vga.set(to_string_dec_uint(actual_vga_db) + " dB (!" + + to_string_dec_uint(cached_vga) + ")"); + text_vga.set_style(Theme::getInstance()->fg_red); + } + + // FPGA decimation register + uint8_t fpga_decim = radio::debug::fpga::register_read(2); + text_fpga_decim.set("n=" + to_string_dec_uint(fpga_decim) + + " (/" + to_string_dec_uint(1 << fpga_decim) + ")"); + + // FPGA Register Ctrl Info + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + // text_fpga_ctrl_dc_q.set(to_string_hex(fpga_ctrl, 2)); + // text_fpga_ctrl_qs.set(to_string_hex(fpga_ctrl, 2)); + + // Decode bits + bool dc_block = fpga_ctrl & 0x01; + bool q_invert = fpga_ctrl & 0x02; + uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03; + + // Display human-readable + std::string fpga_status_dc_q = "DC:" + std::string(dc_block ? "ON" : "OFF") + + " Q:" + std::string(q_invert ? "INV" : "NOR"); + + std::string fpga_status_qs = "QS:" + to_string_dec_uint(quarter_shift); + + text_fpga_ctrl_dc_q.set(fpga_status_dc_q); + text_fpga_ctrl_qs.set(fpga_status_qs); + + // Summary status + // Summary status - update to account for rounding tolerance + int lna_diff = (actual_lna_db > cached_lna) ? (actual_lna_db - cached_lna) : (cached_lna - actual_lna_db); + bool lna_ok = (actual_lna_db == cached_lna) || (lna_diff <= 8); + bool gains_match = lna_ok && (actual_vga_db == cached_vga); + if (rx_mode && gains_match) { + text_status.set("RX mode, gains verified!"); + text_status.set_style(Theme::getInstance()->fg_green); + } else if (rx_mode && !gains_match) { + text_status.set("RX mode, gain MISMATCH!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (gains_match) { + text_status.set("TX mode, gains verified βœ“"); + text_status.set_style(Theme::getInstance()->fg_orange); + } else { + text_status.set("TX mode, gain MISMATCH!"); + text_status.set_style(Theme::getInstance()->fg_red); + } +} +#endif + +#ifdef PRALINE +/* SystemDiagnosticsView *************************************************/ +SystemDiagnosticsView::SystemDiagnosticsView(NavigationView& nav) { + add_children({ + &text_title, + &text_lbl_sample, + &text_sample_rate, + &text_lbl_band, + &text_band, + &button_sample_2m, + &button_sample_4m, + &button_sample_8m, + &button_sample_20m, + &text_lbl_bb_filter, + &text_bb_filter, + &text_lbl_reg8, + &text_reg8, + &text_lbl_gpio, + &text_lbl_lpf, + &text_gpio_lpf, + &text_lbl_mix, + &text_gpio_mix, + &text_lbl_amp, + &text_gpio_amp, + &text_lbl_fpga, + &text_lbl_fpga_ctrl, + &text_fpga_ctrl, + &text_lbl_fpga_decode, + &button_toggle_q, + &button_toggle_dc, + &button_refresh, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + text_lbl_gpio.set_style(Theme::getInstance()->fg_yellow); + text_lbl_fpga.set_style(Theme::getInstance()->fg_yellow); + + // Sample rate buttons + button_sample_2m.on_select = [this](Button&) { + set_sample_rate(2000000); + }; + + button_sample_4m.on_select = [this](Button&) { + set_sample_rate(4000000); + }; + + button_sample_8m.on_select = [this](Button&) { + set_sample_rate(8000000); + }; + + button_sample_20m.on_select = [this](Button&) { + set_sample_rate(20000000); + }; + + // Q inversion toggle + button_toggle_q.on_select = [this](Button&) { + uint32_t current = radio::debug::fpga::register_read(1); + uint8_t new_val = current ^ 0x02; // Toggle Q_INVERT bit + radio::debug::fpga::register_write(1, new_val); + radio::invalidate_spi_config(); + refresh(); + }; + + // DC block toggle + button_toggle_dc.on_select = [this](Button&) { + uint32_t current = radio::debug::fpga::register_read(1); + uint8_t new_val = current ^ 0x01; // Toggle DC_BLOCK bit + radio::debug::fpga::register_write(1, new_val); + radio::invalidate_spi_config(); + refresh(); + }; + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void SystemDiagnosticsView::focus() { + button_refresh.focus(); +} + +void SystemDiagnosticsView::set_sample_rate(uint32_t rate) { + portapack::clock_manager.set_sampling_frequency(rate); + refresh(); +} + +void SystemDiagnosticsView::read_gpio_states() { + // Read actual GPIO port states + uint32_t gpio3_state = LPC_GPIO->PIN[3]; // GPIO3 for mixer + uint32_t gpio4_state = LPC_GPIO->PIN[4]; // GPIO4 for LPF and amp + + bool mix_n_actual = (gpio3_state >> 2) & 1; // GPIO3[2] + bool lpf_actual = (gpio4_state >> 8) & 1; // GPIO4[8] + bool amp_actual = (gpio4_state >> 9) & 1; // GPIO4[9] + + // Mixer is active LOW, so invert for display + bool mixer_enabled = !mix_n_actual; + + // Display with GPIO pin numbers + text_gpio_lpf.set( + std::string(lpf_actual ? "ON" : "OFF") + + " (GPIO4[8]=" + to_string_dec_uint(lpf_actual ? 1 : 0) + ")"); + + text_gpio_mix.set( + std::string(mixer_enabled ? "ENABLED" : "BYPASSED") + + " (GPIO3[2]=" + to_string_dec_uint(mix_n_actual ? 1 : 0) + ")"); + + text_gpio_amp.set( + std::string(amp_actual ? "ON" : "OFF") + + " (GPIO4[9]=" + to_string_dec_uint(amp_actual ? 1 : 0) + ")"); + + // Color code + text_gpio_lpf.set_style(lpf_actual ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_gpio_mix.set_style(mixer_enabled ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_gpio_amp.set_style(amp_actual ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_orange); +} + +void SystemDiagnosticsView::refresh() { + // Sample rate + uint32_t sample_rate = portapack::clock_manager.get_sampling_frequency(); + if (sample_rate >= 1000000) { + text_sample_rate.set(to_string_dec_uint(sample_rate / 1000000) + " MSS"); + } else { + text_sample_rate.set(to_string_dec_uint(sample_rate / 1000) + " kSS"); + } + + // Baseband filter bandwidth + uint32_t reg8 = radio::debug::second_if::register_read(8); + text_reg8.set("0x" + to_string_hex(reg8, 4)); + + uint8_t lpf_coarse = reg8 & 0x03; // Bits 1:0 + const char* bw_names[] = {"7.5 MHz", "8.5 MHz", "15 MHz", "18 MHz"}; + text_bb_filter.set(bw_names[lpf_coarse]); + + // Color code - green if >= 8 MHz, red otherwise + if (lpf_coarse >= 1) { + text_bb_filter.set_style(Theme::getInstance()->fg_green); + } else { + text_bb_filter.set_style(Theme::getInstance()->fg_red); + } + + // Read actual GPIO pin states + read_gpio_states(); + + uint32_t gpio6_pin = LPC_GPIO->PIN[6]; + bool rffc_locked = (gpio6_pin >> 25) & 1; + + // Display it by changing one of the existing fields temporarily + // For example, modify the band display to show lock status: + + auto current_band = radio::debug::rf_path_info::get_current_band(); + switch (current_band) { + case rf::path::Band::Low: + text_band.set("LOW(0-2320MHz)|" + std::string(rffc_locked ? "LCK)" : "ULCK)")); + text_band.set_style(rffc_locked ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + break; + case rf::path::Band::Mid: + text_band.set("MID(2320-2740MHz)"); + text_band.set_style(Theme::getInstance()->fg_green); + break; + case rf::path::Band::High: + text_band.set("HIGH(2740-7250MHz)"); + text_band.set_style(Theme::getInstance()->fg_green); + break; + } + // FPGA control register + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + text_fpga_ctrl.set("0x" + to_string_hex(fpga_ctrl, 2)); + + // Decode FPGA register bits + bool dc_block = fpga_ctrl & 0x01; + bool q_invert = fpga_ctrl & 0x02; + uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03; + + text_lbl_fpga_decode.set( + "|DC:" + std::string(dc_block ? "ON" : "OFF") + + " Q:" + std::string(q_invert ? "INV" : "NOR") + + " QS:" + to_string_dec_uint(quarter_shift)); +} +#endif + +#ifdef PRALINE +/* Si5351PLLADebugView *******************************************************/ + +Si5351PLLADebugView::Si5351PLLADebugView(NavigationView& nav) + : nav_(nav) { + add_children({&text_title, &text_lbl_raw, &text_r26_27, &text_r28_30, &text_r31_33, + &text_lbl_decoded, &text_lbl_p1, &text_p1, &text_lbl_p2, &text_p2, + &text_lbl_p3, &text_p3, &text_lbl_calc, &text_lbl_mult, &text_mult, + &text_lbl_vco, &text_vco, &text_status, &button_refresh, &button_done}); + + text_title.set_style(Theme::getInstance()->fg_yellow); + text_lbl_raw.set_style(Theme::getInstance()->fg_yellow); + text_lbl_decoded.set_style(Theme::getInstance()->fg_yellow); + text_lbl_calc.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { refresh(); }; + button_done.on_select = [&nav](Button&) { nav.pop(); }; + + refresh(); +} + +void Si5351PLLADebugView::focus() { + button_refresh.focus(); +} + +void Si5351PLLADebugView::refresh() { + // Read PLL A parameters + uint8_t r26 = portapack::clock_manager.si5351_read_register(26); + uint8_t r27 = portapack::clock_manager.si5351_read_register(27); + uint8_t r28 = portapack::clock_manager.si5351_read_register(28); + uint8_t r29 = portapack::clock_manager.si5351_read_register(29); + uint8_t r30 = portapack::clock_manager.si5351_read_register(30); + uint8_t r31 = portapack::clock_manager.si5351_read_register(31); + uint8_t r32 = portapack::clock_manager.si5351_read_register(32); + uint8_t r33 = portapack::clock_manager.si5351_read_register(33); + + // Display Raw registers + text_r26_27.set("R26-27 (P3 LO): " + to_string_hex(r26, 2) + " " + to_string_hex(r27, 2)); + text_r28_30.set("R28-30 (P1): " + to_string_hex(r28, 2) + " " + to_string_hex(r29, 2) + " " + to_string_hex(r30, 2)); + text_r31_33.set("R31-33 (P3H|P2):" + to_string_hex(r31, 2) + " " + to_string_hex(r32, 2) + " " + to_string_hex(r33, 2)); + + // Decode parameters + uint32_t pll_p1 = ((uint32_t)(r28 & 0x03) << 16) | ((uint32_t)r29 << 8) | r30; + uint32_t pll_p2 = ((uint32_t)(r31 & 0x0F) << 16) | ((uint32_t)r32 << 8) | r33; + uint32_t pll_p3 = ((uint32_t)(r31 >> 4) << 16) | ((uint32_t)r26 << 8) | r27; + + text_p1.set(to_string_dec_uint(pll_p1) + " (0x" + to_string_hex(pll_p1, 5) + ")"); + text_p2.set(to_string_dec_uint(pll_p2) + " (0x" + to_string_hex(pll_p2, 5) + ")"); + text_p3.set(to_string_dec_uint(pll_p3) + " (0x" + to_string_hex(pll_p3, 5) + ")"); + + // Calculate Multiplier: M = (128 * P3 + P1 * 128 + 512 + P2) / (128 * P3) + // Simplified as: Multiplier = ((P1 + 512) / 128) + (P2 / P3) + uint32_t a = (pll_p1 + 512) / 128; + + if (pll_p3 > 0) { + text_mult.set(to_string_dec_uint(a) + " + " + to_string_dec_uint(pll_p2) + "/" + to_string_dec_uint(pll_p3)); + } else { + text_mult.set("ERR: P3=0"); + } + + // Calculate VCO Frequency (f_vco = f_xtal * Multiplier) + // The HackRF crystal (f_xtal) is 25 MHz. + if (pll_p3 > 0) { + uint64_t vco_num = (uint64_t)pll_p2 + (uint64_t)pll_p3 * (pll_p1 + 512); + uint64_t vco_den = 128ULL * pll_p3; + uint32_t vco_khz = (uint32_t)((25000ULL * vco_num) / vco_den); + + text_vco.set(to_string_dec_uint(vco_khz / 1000) + "." + to_string_dec_uint(vco_khz % 1000, 3) + " MHz"); + + // Si5351 VCO range is 600-900 MHz + if (vco_khz >= 600000 && vco_khz <= 900000) { + text_vco.set_style(Theme::getInstance()->fg_green); + text_status.set("VCO within valid range."); + } else { + text_vco.set_style(Theme::getInstance()->fg_red); + text_status.set("VCO OUT OF RANGE (600-900)!"); + } + } +} +#endif + +#ifdef PRALINE +Si5351PLLBDebugView::Si5351PLLBDebugView(NavigationView& nav) + : nav_(nav) { + add_children({&text_title, + &text_lbl_raw, &text_r34_35, &text_r36_38, &text_r39_41, + &text_lbl_decoded, &text_lbl_p1, &text_p1, &text_lbl_p2, &text_p2, &text_lbl_p3, &text_p3, + &text_lbl_calc, &text_lbl_vco, &text_vco, + &text_status, &button_refresh, &button_done}); + + text_title.set_style(Theme::getInstance()->fg_yellow); + text_lbl_raw.set_style(Theme::getInstance()->fg_yellow); + text_lbl_decoded.set_style(Theme::getInstance()->fg_yellow); + text_lbl_calc.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { refresh(); }; + button_done.on_select = [&nav](Button&) { nav.pop(); }; + refresh(); +} + +void Si5351PLLBDebugView::focus() { + button_refresh.focus(); +} + +void Si5351PLLBDebugView::refresh() { + uint8_t r34 = portapack::clock_manager.si5351_read_register(34); // P3[15:8] + uint8_t r35 = portapack::clock_manager.si5351_read_register(35); // P3[7:0] + uint8_t r36 = portapack::clock_manager.si5351_read_register(36); // P1[17:16] + uint8_t r37 = portapack::clock_manager.si5351_read_register(37); // P1[15:8] + uint8_t r38 = portapack::clock_manager.si5351_read_register(38); // P1[7:0] + uint8_t r39 = portapack::clock_manager.si5351_read_register(39); // P3[19:16] | P2[19:16] + uint8_t r40 = portapack::clock_manager.si5351_read_register(40); // P2[15:8] + uint8_t r41 = portapack::clock_manager.si5351_read_register(41); // P2[7:0] + + text_r34_35.set("R34-35: " + to_string_hex(r34, 2) + " " + to_string_hex(r35, 2)); + text_r36_38.set("R36-38: " + to_string_hex(r36, 2) + " " + to_string_hex(r37, 2) + " " + to_string_hex(r38, 2)); + text_r39_41.set("R39-41: " + to_string_hex(r39, 2) + " " + to_string_hex(r40, 2) + " " + to_string_hex(r41, 2)); + + uint32_t p1 = ((uint32_t)(r36 & 0x03) << 16) | ((uint32_t)r37 << 8) | r38; + uint32_t p2 = ((uint32_t)(r39 & 0x0F) << 16) | ((uint32_t)r40 << 8) | r41; + uint32_t p3 = ((uint32_t)(r39 >> 4) << 16) | ((uint32_t)r34 << 8) | r35; + + text_p1.set(to_string_dec_uint(p1) + " (0x" + to_string_hex(p1, 5) + ")"); + text_p2.set(to_string_dec_uint(p2) + " (0x" + to_string_hex(p2, 5) + ")"); + text_p3.set(to_string_dec_uint(p3) + " (0x" + to_string_hex(p3, 5) + ")"); + + if (p3 > 0) { + uint64_t vco_num = (uint64_t)p2 + (uint64_t)p3 * (p1 + 512); + uint64_t vco_den = 128ULL * p3; + uint32_t vco_khz = (uint32_t)((25000ULL * vco_num) / vco_den); + text_vco.set(to_string_dec_uint(vco_khz / 1000) + "." + to_string_dec_uint(vco_khz % 1000, 3) + " MHz"); + + bool ok = (vco_khz >= 600000 && vco_khz <= 900000); + text_vco.set_style(ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_status.set(ok ? "PLL B VCO OK" : "VCO OUT OF RANGE!"); + } +} +#endif + +#ifdef PRALINE +/* Si5351MultiSynthDebugView *************************************************/ + +Si5351MultiSynthDebugView::Si5351MultiSynthDebugView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_clk_ctrl, + &text_clk_ctrl, + &text_lbl_ms_int, + &text_ms_int, + &text_lbl_raw, + &text_lbl_r42_43, + &text_r42_43, + &text_lbl_r44_46, + &text_r44_46, + &text_lbl_r47_49, + &text_r47_49, + &text_lbl_decoded, + &text_lbl_p1, + &text_p1, + &text_lbl_p2, + &text_p2, + &text_lbl_p3, + &text_p3, + &text_lbl_rdiv, + &text_rdiv, + &text_lbl_calc, + &text_lbl_div, + &text_div, + &text_lbl_freq, + &text_freq, + &text_status, + &button_refresh, + &button_reset, + &button_frac, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + text_lbl_raw.set_style(Theme::getInstance()->fg_yellow); + text_lbl_decoded.set_style(Theme::getInstance()->fg_yellow); + text_lbl_calc.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_reset.on_select = [this](Button&) { + force_pll_reset(); + }; + + button_frac.on_select = [this](Button&) { + force_fractional_mode(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void Si5351MultiSynthDebugView::focus() { + button_refresh.focus(); +} + +void Si5351MultiSynthDebugView::force_pll_reset() { + // Reset PLL A (bit 5) + portapack::clock_manager.si5351_write_register(177, 0x20); + + // Wait for PLL to settle + chThdSleepMilliseconds(10); + + refresh(); +} + +void Si5351MultiSynthDebugView::force_fractional_mode() { + // Force CLK0 to fractional mode + // Read current control register + uint8_t clk0_ctrl = portapack::clock_manager.si5351_read_register(16); + + // Clear MS_INT bit (bit 6) to enable fractional mode + clk0_ctrl &= ~0x40; + + // Write back + portapack::clock_manager.si5351_write_register(16, clk0_ctrl); + + // Reset PLL to apply + portapack::clock_manager.si5351_write_register(177, 0x20); + + chThdSleepMilliseconds(10); + + refresh(); +} + +void Si5351MultiSynthDebugView::refresh() { + // === Clock Control Register 16 (CLK0) === + uint8_t clk0_ctrl = portapack::clock_manager.si5351_read_register(16); + text_clk_ctrl.set("0x" + to_string_hex(clk0_ctrl, 2) + + " (" + to_string_bin(clk0_ctrl, 8) + ")"); + + // Decode MS_INT bit (bit 6) + bool ms_int = (clk0_ctrl >> 6) & 1; + if (ms_int) { + text_ms_int.set("1:INT MODE!"); + text_ms_int.set_style(Theme::getInstance()->fg_red); + } else { + text_ms_int.set("0:Fract Mode"); + text_ms_int.set_style(Theme::getInstance()->fg_green); + } + + // === Read Raw MS0 Registers (42-49) === + uint8_t r42 = portapack::clock_manager.si5351_read_register(42); + uint8_t r43 = portapack::clock_manager.si5351_read_register(43); + uint8_t r44 = portapack::clock_manager.si5351_read_register(44); + uint8_t r45 = portapack::clock_manager.si5351_read_register(45); + uint8_t r46 = portapack::clock_manager.si5351_read_register(46); + uint8_t r47 = portapack::clock_manager.si5351_read_register(47); + uint8_t r48 = portapack::clock_manager.si5351_read_register(48); + uint8_t r49 = portapack::clock_manager.si5351_read_register(49); + + // Display raw registers + text_r42_43.set(to_string_hex(r42, 2) + " " + to_string_hex(r43, 2) + + " (P3[15:0])"); + text_r44_46.set(to_string_hex(r44, 2) + " " + to_string_hex(r45, 2) + + " " + to_string_hex(r46, 2) + " (R|P1)"); + text_r47_49.set(to_string_hex(r47, 2) + " " + to_string_hex(r48, 2) + + " " + to_string_hex(r49, 2) + " (P3|P2)"); + + // === Decode P1, P2, P3 === + // Si5351 MS Register Layout: + // Reg 42: P3[15:8] + // Reg 43: P3[7:0] + // Reg 44: bits 6:4 = R_DIV[2:0], bits 1:0 = P1[17:16] + // Reg 45: P1[15:8] + // Reg 46: P1[7:0] + // Reg 47: bits 7:4 = P3[19:16], bits 3:0 = P2[19:16] + // Reg 48: P2[15:8] + // Reg 49: P2[7:0] + + // Decode R_DIV + uint8_t r_div_encoded = (r44 >> 4) & 0x07; + uint32_t r_div = 1 << r_div_encoded; + text_rdiv.set("/" + to_string_dec_uint(r_div) + " (enc=" + to_string_dec_uint(r_div_encoded) + ")"); + + // Decode P1 (18-bit) + uint32_t p1 = ((uint32_t)(r44 & 0x03) << 16) | ((uint32_t)r45 << 8) | r46; + text_p1.set(to_string_dec_uint(p1) + " (0x" + to_string_hex(p1, 5) + ")"); + + // Decode P2 (20-bit) + uint32_t p2 = ((uint32_t)(r47 & 0x0F) << 16) | ((uint32_t)r48 << 8) | r49; + text_p2.set(to_string_dec_uint(p2) + " (0x" + to_string_hex(p2, 5) + ")"); + + // Decode P3 (20-bit) + uint32_t p3 = ((uint32_t)(r47 >> 4) << 16) | ((uint32_t)r42 << 8) | r43; + text_p3.set(to_string_dec_uint(p3) + " (0x" + to_string_hex(p3, 5) + ")"); + + // Color code P2/P3 based on whether fractional is being used + if (p2 == 0 && p3 == 1) { + text_p2.set_style(Theme::getInstance()->fg_orange); + text_p3.set_style(Theme::getInstance()->fg_orange); + } else if (p3 > 1) { + text_p2.set_style(Theme::getInstance()->fg_green); + text_p3.set_style(Theme::getInstance()->fg_green); + } else { + // Default/neutral style when P2/P3 don't match known patterns + text_p2.set_style(Theme::getInstance()->fg_light); + text_p3.set_style(Theme::getInstance()->fg_light); + } + + // === Calculate Output Frequency === + + // === Calculate Multisynth Divider === + // Correct Si5351 formula: + // MS_DIV = (P2+P3 Γ— (P1 + 512)) / (128 Γ— P3) + // MS_DIV = P2/(128*P3) + P1+512/(128*P3) + // x = (P1 + 512) / 128 + // y = P2/128 + // z = P3 + // f_out = f_vco / MS_DIV / R_DIV + // a = floor((P1 + 512) / 128) + // k = (P1 + 512) - 128*a + // b = b = (P2 + c*k) / 128 + // c = P3 + // In the case of integer division: b=0, c=1, so MS_DIV = a + + uint64_t ms_div_numerator = (uint64_t)p2 + (uint64_t)p3 * (p1 + 512); + uint64_t ms_div_denominator = 128ULL * p3; + + uint32_t x = (p1 + 512) / 128; + uint32_t y = p2 / 128; + uint32_t z = p3; + + // For display, show the full fractional value + // MS_DIV = (a+b)/c + if (p3 > 1 && p2 > 0) { + std::string div_str = "(" + to_string_dec_uint(x) + "+" + to_string_dec_uint(y) + ")/" + to_string_dec_uint(z); + text_div.set(div_str); + } else { + std::string div_str = to_string_dec_uint(x); + text_div.set(div_str); + } + + // === Calculate Output Frequency === + + // f_vco = 800,000,000 (PLL A) + // f_out in kHz = 800,000,000 / MS_DIV / r_div / 1000 + // = 800,000 Γ— ms_div_denominator / ms_div_numerator / r_div + + uint32_t freq_khz = 0; + if (ms_div_numerator > 0) { + freq_khz = (uint32_t)((800000ULL * ms_div_denominator) / ms_div_numerator / r_div); + } + + uint32_t freq_mhz = freq_khz / 1000; + uint32_t freq_frac = freq_khz % 1000; + + text_freq.set(to_string_dec_uint(freq_mhz) + "." + + to_string_dec_uint(freq_frac / 100) + + to_string_dec_uint((freq_frac / 10) % 10) + + to_string_dec_uint(freq_frac % 10) + " MHz"); + + // Color code based on expected ~24.576 MHz for WFM stereo + if (freq_khz >= 24500 && freq_khz <= 24700) { + text_freq.set_style(Theme::getInstance()->fg_green); + } else if (freq_khz >= 24000 && freq_khz <= 26000) { + text_freq.set_style(Theme::getInstance()->fg_orange); + } else { + text_freq.set_style(Theme::getInstance()->fg_red); + } + + // === Status Summary === + // Expected values for 24.576 MHz (3.072 MHz * 8 decimation): + // VCO = 800 MHz + // Target freq = 49.152 MHz (before R_DIV=/2) + // MS_DIV = 800M / 49.152M = 16.276... + // a = 16, b = 53, c = 192 + // P1 = 128*16 + floor(128*53/192) - 512 = 2048 + 35 - 512 = 1571 + // P2 = 128*53 - 192*35 = 6784 - 6720 = 64 + // P3 = 192 + + if (ms_int) { + text_status.set("ERROR: Integer mode! P2/P3 ignored!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (p2 == 0 && p3 == 1) { + text_status.set("WARN P2:0,P3:1 INT Equiv"); + text_status.set_style(Theme::getInstance()->fg_orange); + } else if (p3 == 192 && p2 == 64) { + text_status.set("GOOD Exp 3.072M values!"); + text_status.set_style(Theme::getInstance()->fg_green); + } else if (freq_khz >= 24500 && freq_khz <= 24700) { + text_status.set("OK: Freq in range"); + text_status.set_style(Theme::getInstance()->fg_green); + } else { + text_status.set("CHECK: P2=" + to_string_dec_uint(p2) + + " P3=" + to_string_dec_uint(p3)); + text_status.set_style(Theme::getInstance()->fg_orange); + } +} +#endif + +#ifdef PRALINE +PralineClockDebugView::PralineClockDebugView(NavigationView& nav) + : View(), + rows{ + {&t0_id, &t0_ma, &t0_mode, &t0_src, &t0_ph, &t0_st}, + {&t1_id, &t1_ma, &t1_mode, &t1_src, &t1_ph, &t1_st}, + {&t2_id, &t2_ma, &t2_mode, &t2_src, &t2_ph, &t2_st}, + {&t3_id, &t3_ma, &t3_mode, &t3_src, &t3_ph, &t3_st}, + {&t4_id, &t4_ma, &t4_mode, &t4_src, &t4_ph, &t4_st}, + {&t5_id, &t5_ma, &t5_mode, &t5_src, &t5_ph, &t5_st}, + {&t6_id, &t6_ma, &t6_mode, &t6_src, &t6_ph, &t6_st}, + {&t7_id, &t7_ma, &t7_mode, &t7_src, &t7_ph, &t7_st}} { + add_children({&text_title, &text_lbl_pll, &text_pll_status, + &text_lbl_afe, &text_afe_rate, &text_lbl_n, &text_n_val, + &text_header, + &t0_id, &t0_ma, &t0_mode, &t0_src, &t0_ph, &t0_st, + &t1_id, &t1_ma, &t1_mode, &t1_src, &t1_ph, &t1_st, + &t2_id, &t2_ma, &t2_mode, &t2_src, &t2_ph, &t2_st, + &t3_id, &t3_ma, &t3_mode, &t3_src, &t3_ph, &t3_st, + &t4_id, &t4_ma, &t4_mode, &t4_src, &t4_ph, &t4_st, + &t5_id, &t5_ma, &t5_mode, &t5_src, &t5_ph, &t5_st, + &t6_id, &t6_ma, &t6_mode, &t6_src, &t6_ph, &t6_st, + &t7_id, &t7_ma, &t7_mode, &t7_src, &t7_ph, &t7_st, + &button_refresh, &button_done}); + + button_refresh.on_select = [this](Button&) { this->refresh(); }; + button_done.on_select = [&nav](Button&) { nav.pop(); }; + + refresh(); +} + +void PralineClockDebugView::focus() { + button_refresh.focus(); +} + +void PralineClockDebugView::refresh() { + // 1. System Status + uint8_t status = portapack::clock_manager.si5351_read_status(); + bool pll_a = !(status & 0x20); + bool pll_b = !(status & 0x40); + text_pll_status.set(std::string(pll_a ? "A:OK " : "A:ERR ") + (pll_b ? "B:OK" : "B:ERR")); + text_pll_status.set_style((pll_a && pll_b) ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // 2. AFE & Decimation Info + uint32_t base_rate = portapack::clock_manager.get_sampling_frequency(); + uint8_t n = portapack::clock_manager.get_resampling_n(); + text_afe_rate.set(to_string_dec_uint(base_rate << n) + " Hz"); + text_n_val.set(to_string_dec_uint(n)); + + // 3. Clock Table Decoding + uint8_t output_en = portapack::clock_manager.si5351_read_register(3); + const char* ma_lookup[] = {"2m", "4m", "6m", "8m"}; + + for (size_t i = 0; i < 8; i++) { + uint8_t ctrl = portapack::clock_manager.si5351_read_register(16 + i); + + // mA (Bits 1:0) + rows[i].ma->set(ma_lookup[ctrl & 0x03]); + + // Mode (Bit 6: 1=Integer, 0=Fractional) + rows[i].mode->set((ctrl & 0x40) ? "INT" : "FRAC"); + rows[i].mode->set_style((ctrl & 0x40) ? Theme::getInstance()->fg_blue : Theme::getInstance()->fg_yellow); + + // PLL Source (Bit 5: 0=PLLA, 1=PLLB) + rows[i].src->set((ctrl & 0x20) ? "PLLB" : "PLLA"); + rows[i].src->set_style((ctrl & 0x20) ? Theme::getInstance()->fg_blue : Theme::getInstance()->fg_green); + + // Phase (Bit 4: 1=Inverted, 0=Normal) + // Use 0x10 (Bit 4) + rows[i].phase->set((ctrl & 0x10) ? "INVRT" : "NORM "); + rows[i].phase->set_style((ctrl & 0x10) ? Theme::getInstance()->fg_orange : Theme::getInstance()->fg_light); + + // Status (Powered On and Output Enabled) + bool is_on = !(ctrl & 0x80) && !(output_en & (1 << i)); + rows[i].stat->set(is_on ? "ON" : "OFF"); + rows[i].stat->set_style(is_on ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + } +} +#endif + +#ifdef PRALINE +/* GPIODebugView *************************************************/ +GPIODebugView::GPIODebugView(NavigationView& nav) { + add_children({ + &text_lbl_gpio4, + &text_lbl_mixr1, + &text_mixr1, + &text_lbl_pin4, + &text_pin4, + &text_lbl_set4, + &text_set4, + &text_lbl_lpf_bit, + &text_lpf_dir, + &text_lpf_pin, + &text_lpf_set, + &button_lpf_toggle, + &button_lpf_on, + &button_lpf_off, + &text_lbl_amp_bit, + &text_amp_dir, + &text_amp_pin, + &text_amp_set, + &button_amp_toggle, + &button_amp_on, + &button_amp_off, + &text_lbl_gpio3, + &text_lbl_mix_bit, + &text_mix_dir, + &text_mix_pin, + &text_mix_set, + &button_refresh, + &button_done, + }); + + text_lbl_gpio4.set_style(Theme::getInstance()->fg_yellow); + text_lbl_gpio3.set_style(Theme::getInstance()->fg_yellow); + + // LPF control buttons + button_lpf_toggle.on_select = [this](Button&) { + // Read current state + uint32_t current = LPC_GPIO->PIN[4]; + bool current_state = (current >> 8) & 1; + + // Toggle + if (current_state) { + LPC_GPIO->CLR[4] = (1 << 8); // Clear bit 8 + } else { + LPC_GPIO->SET[4] = (1 << 8); // Set bit 8 + } + + refresh(); + }; + + button_lpf_on.on_select = [this](Button&) { + LPC_GPIO->SET[4] = (1 << 8); // Force ON + refresh(); + }; + + button_lpf_off.on_select = [this](Button&) { + LPC_GPIO->CLR[4] = (1 << 8); // Force OFF + refresh(); + }; + + // RF Amp control buttons + button_amp_toggle.on_select = [this](Button&) { + uint32_t current = LPC_GPIO->PIN[4]; + bool current_state = (current >> 9) & 1; + + if (current_state) { + LPC_GPIO->CLR[4] = (1 << 9); + } else { + LPC_GPIO->SET[4] = (1 << 9); + } + + refresh(); + }; + + button_amp_on.on_select = [this](Button&) { + LPC_GPIO->SET[4] = (1 << 9); // Force ON + refresh(); + }; + + button_amp_off.on_select = [this](Button&) { + LPC_GPIO->CLR[4] = (1 << 9); // Force OFF + refresh(); + }; + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void GPIODebugView::focus() { + button_refresh.focus(); +} + +void GPIODebugView::refresh() { + // Read GPIO4 registers + uint32_t gpio4_dir = LPC_GPIO->DIR[4]; // Direction: 1=output, 0=input + uint32_t gpio4_pin = LPC_GPIO->PIN[4]; // Actual pin state + uint32_t gpio4_set = LPC_GPIO->SET[4]; // What we're trying to output + + // Display full registers + // text_dir4.set("0x" + to_string_hex(gpio4_dir, 8)); + text_pin4.set("0x" + to_string_hex(gpio4_pin, 8)); + text_set4.set("0x" + to_string_hex(gpio4_set, 8)); + + // Extract bit 8 (LPF) + bool lpf_dir = (gpio4_dir >> 8) & 1; + bool lpf_pin = (gpio4_pin >> 8) & 1; + bool lpf_set = (gpio4_set >> 8) & 1; + + text_lpf_dir.set("DIR: " + std::string(lpf_dir ? "OUT" : "IN")); + text_lpf_pin.set("PIN: " + std::string(lpf_pin ? "1" : "0")); + text_lpf_set.set("SET: " + std::string(lpf_set ? "1" : "0")); + + // Color code + text_lpf_dir.set_style(lpf_dir ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_lpf_pin.set_style(lpf_pin ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // Extract bit 9 (RF Amp) + bool amp_dir = (gpio4_dir >> 9) & 1; + bool amp_pin = (gpio4_pin >> 9) & 1; + bool amp_set = (gpio4_set >> 9) & 1; + + text_amp_dir.set("DIR: " + std::string(amp_dir ? "OUT" : "IN")); + text_amp_pin.set("PIN: " + std::string(amp_pin ? "1" : "0")); + text_amp_set.set("SET: " + std::string(amp_set ? "1" : "0")); + + text_amp_dir.set_style(amp_dir ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_amp_pin.set_style(amp_pin ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // Read GPIO3 (Mixer) - bit 2 + uint32_t gpio3_dir = LPC_GPIO->DIR[3]; + uint32_t gpio3_pin = LPC_GPIO->PIN[3]; + uint32_t gpio3_set = LPC_GPIO->SET[3]; + + bool mix_dir = (gpio3_dir >> 2) & 1; + bool mix_pin = (gpio3_pin >> 2) & 1; + bool mix_set = (gpio3_set >> 2) & 1; + + text_mix_dir.set("DIR: " + std::string(mix_dir ? "OUT" : "IN")); + text_mix_pin.set("PIN: " + std::string(mix_pin ? "1" : "0")); + text_mix_set.set("SET: " + std::string(mix_set ? "1" : "0")); + + text_mix_dir.set_style(mix_dir ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_mix_pin.set_style(mix_pin ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // Read GPIO5 (Mixer R1) - bit 2 + uint32_t gpio3_state = LPC_GPIO->PIN[3]; // GPIO3 for mixer + bool mix_n_actual = (gpio3_state >> 2) & 1; // GPIO3[2] + uint32_t gpio5_state = LPC_GPIO->PIN[5]; + bool mix_r10_pin = (gpio5_state >> 6) & 1; // GPIO5[6] = P2_6 + // Mixer is active LOW, so invert for display + bool mixer_enabled = !mix_n_actual; + + // Append to existing mixer display: + text_mixr1.set( + std::string(mixer_enabled ? "ENABLED" : "BYPASSED") + + " P6_3=" + to_string_dec_uint(mix_n_actual ? 1 : 0) + + " P2_6=" + to_string_dec_uint(mix_r10_pin ? 1 : 0)); + + text_mixr1.set_style(mixer_enabled ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); +} +#endif + +#ifdef PRALINE +/* RFFC5072StatusView *************************************************/ + +RFFC5072StatusView::RFFC5072StatusView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_gpio4, + &text_ctrl, + &text_lbl_enabled, + &text_enabled, + &text_lbl_freq, + &text_freq, + &text_lbl_path, + &text_path, + &text_lbl_mixer, + &text_mixer, + &text_lbl_r0, + &text_r0, + &text_lbl_r1, + &text_r1, + &text_lbl_r2, + &text_r2, + &text_lbl_n, + &text_n, + &text_lbl_lodiv, + &text_lodiv, + &text_lbl_calc, + &text_calc, + &text_status, + &text_status2, + &text_status3, + &text_regs_status, + &button_refresh, + &button_force, + &button_done, + }); + + button_refresh.on_select = [this](Button&) { + refresh_status(); + }; + + button_force.on_select = [this](Button&) { + // Force ENX to OUTPUT and drive LOW + // LPC_GPIO->DIR[2] |= (1 << 13); // Set as OUTPUT + // LPC_GPIO->CLR[2] = (1 << 13); // Drive LOW (enabled) + + // refresh_status(); + + // Disable RFFC5072 + // uint32_t r0 = radio::debug::first_if::register_read(0); + // radio::debug::first_if::register_write(0, r0 & ~0x0010); // Clear ENBL + + // Wait 1ms + // chThdSleepMilliseconds(1); + + // Re-enable - this triggers new calibration + // radio::debug::first_if::register_write(0, r0 | 0x0010); // Set ENBL + + // Wait for calibration + // chThdSleepMilliseconds(10); + + // refresh_status(); + + // Force lodiv=4 (log2=2) instead of lodiv=2 (log2=1) + // This gives VCO = LO Γ— 4 = 2595 Γ— 4 = 10380 MHz - TOO HIGH! + + // Actually, we need lodiv=1 which gives VCO = 2595 MHz - TOO LOW (below 2700) + + // Let's try a different approach: manually write registers for VCO ~ 3500 MHz + // LO = 3500/2 = 1750 MHz (not useful for FM, but tests if VCO can lock) + + // VCO = 3500 MHz, lodiv=2, presc=2, f_ref=40 + // N = (VCO Γ— presc) / f_ref = (3500 Γ— 2) / 40 = 175 + + // Write P2_FREQ1: N=175, lodiv=1 (log2), presc=1 (log2) + // uint16_t p2_freq1 = (175 << 7) | (1 << 4) | (1 << 2); + // radio::debug::first_if::register_write(15, p2_freq1); + + // Clear fractional part + // radio::debug::first_if::register_write(16, 0); + // radio::debug::first_if::register_write(17, 0); + + // Trigger recalibration by toggling ENBL + // uint32_t r0 = radio::debug::first_if::register_read(0); + // radio::debug::first_if::register_write(0, r0 & ~0x0010); + // chThdSleepMilliseconds(1); + // radio::debug::first_if::register_write(0, r0 | 0x0010); + // chThdSleepMilliseconds(20); + + // refresh_status(); + + // Test SPI SDATA direction switching + // PRALINE: SDATA = P9_2 = GPIO4[14] + + // Check current direction + uint32_t dir_before = LPC_GPIO->DIR[4]; + bool sdata_output_before = (dir_before >> 14) & 1; + + // Try a register read + uint32_t dummy = radio::debug::first_if::register_read(0); + (void)dummy; + + // Check direction after read + uint32_t dir_after = LPC_GPIO->DIR[4]; + bool sdata_output_after = (dir_after >> 14) & 1; + + // Read the actual SDATA pin state + uint32_t pin_state = LPC_GPIO->PIN[4]; + bool sdata_pin = (pin_state >> 14) & 1; + + text_status.set("SDATA: dir_b=" + to_string_dec_uint(sdata_output_before) + + " dir_a=" + to_string_dec_uint(sdata_output_after) + + " pin=" + to_string_dec_uint(sdata_pin) + " "); + + // If both are 1 (OUTPUT), the read direction switch isn't happening + // if (sdata_output_before && sdata_output_after) { + // text_status2.set("ERROR: SDATA stuck as OUTPUT! "); + // text_status2.set_style(Theme::getInstance()->fg_red); + //} else { + // text_status2.set("SDATA direction OK "); + // text_status2.set_style(Theme::getInstance()->fg_green); + //} + + // Test: Write a known pattern to register 0, then read back + // Register 0 (DEV_CTRL) default = 0xBEFA + + // Step 1: Read current value + uint32_t before = radio::debug::first_if::register_read(0); + + // Step 2: Write a different value (change ENBL bit to toggle) + uint32_t test_val = before ^ 0x0010; // Toggle ENBL bit + radio::debug::first_if::register_write(0, test_val); + + // Step 3: Read back + uint32_t after = radio::debug::first_if::register_read(0); + + // Step 4: Restore original + radio::debug::first_if::register_write(0, before); + + // Display results + text_status.set("WR TEST: " + to_string_hex(before, 4) + + "->" + to_string_hex(test_val, 4) + + " rb:" + to_string_hex(after, 4)); + + // If after == before (not test_val), reads are broken + // If after == test_val, reads work + if (after == test_val) { + text_status2.set("READ-AFTER-WRITE: PASS! "); + text_status2.set_style(Theme::getInstance()->fg_green); + } else if (after == before) { + text_status2.set("READ-AFTER-WRITE: FAIL (no change) "); + text_status2.set_style(Theme::getInstance()->fg_red); + } else { + text_status2.set("READ-AFTER-WRITE: CORRUPT " + to_string_hex(after, 4) + " "); + text_status2.set_style(Theme::getInstance()->fg_red); + } + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + refresh_status(); +} + +void RFFC5072StatusView::focus() { + button_refresh.focus(); +} + +void RFFC5072StatusView::refresh_status() { + // === DIAGNOSTIC: Capture initial GPIO state === + uint32_t gpio2_initial = LPC_GPIO->PIN[2]; + uint32_t dir2_initial = LPC_GPIO->DIR[2]; + bool enx_initial = (gpio2_initial >> 13) & 1; + + // === READ RAW GPIO STATES FOR DISPLAY === + uint32_t gpio2_dir = LPC_GPIO->DIR[2]; + uint32_t gpio2_pin = LPC_GPIO->PIN[2]; + bool enx_is_output = (gpio2_dir >> 13) & 1; + bool resetx_is_output = (gpio2_dir >> 14) & 1; + + // === LOCK DETECT === + uint32_t gpio6_pin = LPC_GPIO->PIN[6]; + bool rffc_locked = (gpio6_pin >> 25) & 1; + + // === FPGA REGISTERS (non-SPI) === + uint8_t fpga_reg1 = radio::debug::fpga::register_read(1); + uint8_t fpga_reg2 = radio::debug::fpga::register_read(2); + uint8_t fpga_reg3 = radio::debug::fpga::register_read(3); + text_regs_status.set("FPGA R1:" + to_string_hex(fpga_reg1, 2) + + " R2:" + to_string_hex(fpga_reg2, 2) + + " R3:" + to_string_hex(fpga_reg3, 2)); + + // === CONTROL PINS === + bool enx = (gpio2_pin >> 13) & 1; + bool resetx = (gpio2_pin >> 14) & 1; + text_ctrl.set("ENX: " + std::string(enx ? "DIS" : "EN") + + " O:" + std::string(enx_is_output ? "Y" : "N") + + " | RSTX: " + std::string(resetx ? "H" : "L") + + " O:" + std::string(resetx_is_output ? "Y" : "N")); + text_ctrl.set_style((enx == 0 && resetx == 1) ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // === DIAGNOSTIC: Check BEFORE first RFFC5072 SPI read === + uint32_t gpio2_before_spi = LPC_GPIO->PIN[2]; + bool enx_before_spi = (gpio2_before_spi >> 13) & 1; + + // === RFFC5072 REGISTERS (SPI reads - this is where corruption happens) === + uint32_t r0 = radio::debug::first_if::register_read(0); + + // === DIAGNOSTIC: Check AFTER first read === + uint32_t gpio2_after_r0 = LPC_GPIO->PIN[2]; + bool enx_after_r0 = (gpio2_after_r0 >> 13) & 1; + + uint32_t r15 = radio::debug::first_if::register_read(15); + + // === DIAGNOSTIC: Check AFTER second read === + uint32_t gpio2_after_r15 = LPC_GPIO->PIN[2]; + bool enx_after_r15 = (gpio2_after_r15 >> 13) & 1; + + uint32_t r16 = radio::debug::first_if::register_read(16); + + // === DIAGNOSTIC: Check AFTER third read === + uint32_t gpio2_final = LPC_GPIO->PIN[2]; + uint32_t dir2_final = LPC_GPIO->DIR[2]; + bool enx_final = (gpio2_final >> 13) & 1; + + // === Display register values === + text_r0.set(to_string_hex(r0, 4)); + text_r1.set(to_string_hex(r15, 4) + " (R15)"); + text_r2.set(to_string_hex(r16, 4) + " (R16)"); + + bool enabled = (r0 & 0x0010) != 0; + text_enabled.set(enabled ? "ENABLED" : "DISABLED"); + text_enabled.set_style(enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // === Decode frequency info (keeping existing code) === + uint16_t n_int = (r15 >> 7) & 0x1FF; + uint8_t lodiv_sel = (r15 >> 4) & 0x07; + uint8_t presc_sel = (r15 >> 2) & 0x03; + text_n.set(to_string_dec_uint(n_int)); + + uint16_t lodiv_val = 1u << lodiv_sel; + uint16_t presc_val = 1u << presc_sel; + text_lodiv.set("/" + to_string_dec_uint(lodiv_val) + + " (P:/" + to_string_dec_uint(presc_val) + ")"); + + const uint32_t f_ref_mhz = 40; + uint32_t f_vco_mhz = (f_ref_mhz * n_int) / presc_val; + uint32_t f_lo_mhz = f_vco_mhz / lodiv_val; + + bool vco_ok = (f_vco_mhz >= 2700) && (f_vco_mhz <= 5400); + bool lo_ok = (f_lo_mhz >= 85) && (f_lo_mhz <= 4200); + bool in_bypass_range = (f_lo_mhz >= 2320) && (f_lo_mhz <= 2740); + + if (in_bypass_range) { + text_calc.set(to_string_dec_uint(f_lo_mhz) + " MHz (MID)"); + text_calc.set_style(Theme::getInstance()->fg_orange); + } else { + text_calc.set(to_string_dec_uint(f_lo_mhz) + " MHz"); + text_calc.set_style(lo_ok ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + } + + text_freq.set(to_string_dec_uint(f_vco_mhz) + " MHz VCO"); + text_freq.set_style(vco_ok ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + bool path2_active = (r0 & 0x0020) != 0; + text_path.set(path2_active ? "PATH2" : "PATH1"); + text_mixer.set(path2_active ? "ACTIVE" : "INACTIVE"); + text_mixer.set_style(path2_active ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // === DIAGNOSTIC STATUS (replaces normal status) === + // Read register 31 with readsel=0 (device ID) + radio::debug::first_if::register_write(0, (r0 & 0xFFF0) | 0x0000); // readsel=0 + uint32_t device_id = radio::debug::first_if::register_read(31); + + // Read calibration status (readback register 1) + // First, set DEV_CTRL.readsel = 1, then read READBACK register + uint32_t dev_ctrl_orig = radio::debug::first_if::register_read(0); // Save original + + // Write DEV_CTRL with readsel=1 (bits 3:0) + radio::debug::first_if::register_write(0, (dev_ctrl_orig & 0xFFF0) | 0x0001); + + // Now read the READBACK register (register address for readback) + uint32_t cal_status = radio::debug::first_if::register_read(31); // READBACK is at reg 31 + + // Decode calibration status: + // Bit 15: lock (should be 1) + // Bits 14:8: ct_cal (coarse tune calibration value, 0-127) + // Bits 7:1: cp_cal (charge pump calibration value) + // Bit 0: ctfail (1 = calibration FAILED) + + bool lock_bit = (cal_status >> 15) & 1; + uint8_t ct_cal = (cal_status >> 8) & 0x7F; + uint8_t cp_cal = (cal_status >> 1) & 0x7F; + bool ct_fail = cal_status & 1; + + // Add to refresh_status(): + uint32_t r6 = radio::debug::first_if::register_read(6); + uint32_t r5 = radio::debug::first_if::register_read(5); + uint32_t r3 = radio::debug::first_if::register_read(3); // VCO_CTRL + + // Check SDATA (GPIO4[14]) direction + uint32_t gpio4_dir = LPC_GPIO->DIR[4]; + bool sdata_is_output = (gpio4_dir >> 14) & 1; + text_gpio4.set("GPIO4 DIR: " + to_string_hex(gpio4_dir, 8) + + " SDATA=" + std::string(sdata_is_output ? "OUT" : "IN")); + + // Display these values + text_status2.set("CAL ct=" + to_string_dec_uint(ct_cal) + + " cp=" + to_string_dec_uint(cp_cal) + + (ct_fail ? " FAIL!" : " OK") + + " lck_b=" + to_string_dec_uint(lock_bit)); + text_status3.set("R3:" + to_string_hex(r3, 4) + + " R5:" + to_string_hex(r5, 4) + + " R6:" + to_string_hex(r6, 4)); + + if (enx_initial != enx_final || dir2_initial != dir2_final) { + // ENX or DIR changed - report which operation caused it + std::string diag = "CHG: "; + if (enx_initial != enx_before_spi) diag += "pre "; + if (enx_before_spi != enx_after_r0) diag += "R0 "; + if (enx_after_r0 != enx_after_r15) diag += "R15 "; + if (enx_after_r15 != enx_final) diag += "R16 "; + diag += std::to_string(enx_initial) + "->" + std::to_string(enx_final); + + if (dir2_initial != dir2_final) { + diag += " DIR!"; + } + + text_status.set(diag); + text_status.set_style(Theme::getInstance()->fg_red); + + // Blink LED + /*for (int i = 0; i < 3; i++) { + hackrf::one::led_rx.on(); + chThdSleepMilliseconds(100); + hackrf::one::led_rx.off(); + chThdSleepMilliseconds(100); + }*/ + } else { + // No change - normal status + if (!rffc_locked) { + text_status.set("ID 0x" + to_string_hex(device_id, 4) + " PLL UNLOCKED!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (enx == 1) { + text_status.set("ID 0x" + to_string_hex(device_id, 4) + " DSBLD,ENX=1!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else { + text_status.set("ID 0x" + to_string_hex(device_id, 4) + " Passed!"); + text_status.set_style(Theme::getInstance()->fg_green); + } + } +} + +/* RFFCTuningDebugView *************************************************/ +RFFCTuningDebugView::RFFCTuningDebugView(NavigationView& nav) { + add_children({ + &text_title, + &text_lbl_called, + &text_called, + &text_lbl_req, + &text_req, + &text_lbl_exp_n, + &text_exp_n, + &text_lbl_act_n, + &text_act_n, + &text_lbl_exp_div, + &text_exp_div, + &text_lbl_act_div, + &text_act_div, + &text_lbl_calc, + &text_calc, + &text_lbl_calc_lo, + &text_calc_lo, + &text_lbl_calc_vco, + &text_calc_vco, + &text_lbl_vco, + &text_vco, + &text_lbl_n_q24, + &text_n_q24, + &text_status, + &button_refresh, + &button_done, + }); + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void RFFCTuningDebugView::focus() { + button_refresh.focus(); +} + +void RFFCTuningDebugView::refresh() { + // Get expected values from last tuning attempt + auto tuning = radio::debug::first_if::get_tuning_info(); + + // Show if set_frequency was ever called + if (tuning.was_called) { + text_called.set("YES"); + text_called.set_style(Theme::getInstance()->fg_green); + + text_req.set(to_string_dec_uint(tuning.requested_freq_mhz) + " MHz"); + text_exp_n.set(to_string_dec_uint(tuning.expected_n)); + + uint16_t exp_lo = 1 << tuning.expected_lodiv; + uint16_t exp_pr = 1 << tuning.expected_presc; + text_exp_div.set(to_string_dec_uint(exp_lo) + " / " + to_string_dec_uint(exp_pr)); + } else { + text_called.set("NO"); + text_called.set_style(Theme::getInstance()->fg_red); + text_req.set("---"); + text_exp_n.set("---"); + text_exp_div.set("---"); + } + + // Read actual hardware values + uint32_t r15 = radio::debug::first_if::register_read(15); + + uint16_t act_n = (r15 >> 7) & 0x1FF; + uint8_t act_lo_sel = (r15 >> 4) & 0x07; + uint8_t act_pr_sel = (r15 >> 2) & 0x03; + + uint16_t act_lo = 1 << act_lo_sel; + uint16_t act_pr = 1 << act_pr_sel; + + text_act_n.set(to_string_dec_uint(act_n)); + text_act_div.set(to_string_dec_uint(act_lo) + " / " + to_string_dec_uint(act_pr)); + + // Calculate what this produces + uint32_t calc_vco = (40 * act_n) / act_pr; + uint32_t calc_lo = calc_vco / act_lo; + text_calc.set(to_string_dec_uint(calc_lo) + " MHz"); + text_vco.set(to_string_dec_uint(tuning.calculated_vco_mhz) + " MHz"); + + text_calc_lo.set(to_string_dec_uint(tuning.calc_lo_freq_mhz) + " MHz"); + text_calc_vco.set(to_string_dec_uint(tuning.calc_vco_inside_mhz) + " MHz"); + text_n_q24.set(to_string_dec_uint(tuning.calc_n_q24 >> 24)); // Show integer part + + // Status comparison + if (!tuning.was_called) { + text_status.set("RFFC Freq set NEVER called!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (act_n == tuning.expected_n) { + text_status.set("MATCH! Hardware as expected!"); + text_status.set_style(Theme::getInstance()->fg_green); + } else { + text_status.set("MISMATCH! Exp:" + to_string_dec_uint(tuning.expected_n) + + " Act:" + to_string_dec_uint(act_n)); + text_status.set_style(Theme::getInstance()->fg_red); + } +} + +/* MAX2831DebugView *************************************************/ +MAX2831DebugView::MAX2831DebugView(NavigationView& nav) { + add_children({ + &text_title, + &text_lbl_called, + &text_called, + &text_lbl_valid, + &text_valid, + &text_lbl_req, + &text_req, + &text_lbl_calc_n, + &text_calc_n, + &text_lbl_calc_frac, + &text_calc_frac, + &text_spacer, + &text_lbl_r3, + &text_r3, + &text_lbl_r4, + &text_r4, + &text_lbl_act_n, + &text_act_n, + &text_lbl_act_frac, + &text_act_frac, + &text_lbl_calc_freq, + &text_calc_freq, + &text_status, + &button_refresh, + &button_done, + }); + + text_spacer.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void MAX2831DebugView::focus() { + button_refresh.focus(); +} + +void MAX2831DebugView::refresh() { + auto info = get_max2831_info(); + + // Show if set_frequency was called + if (info.set_frequency_called) { + text_called.set("YES"); + text_called.set_style(Theme::getInstance()->fg_green); + + if (info.frequency_valid) { + text_valid.set("YES (2.3-2.6G)"); + text_valid.set_style(Theme::getInstance()->fg_green); + } else { + text_valid.set("NO - OUT OF RANGE!"); + text_valid.set_style(Theme::getInstance()->fg_red); + } + + text_req.set(to_string_dec_uint(info.requested_freq_mhz) + " MHz"); + text_calc_n.set(to_string_dec_uint(info.calculated_n)); + text_calc_frac.set(to_string_hex(info.calculated_frac, 5)); + } else { + text_called.set("NO"); + text_called.set_style(Theme::getInstance()->fg_red); + text_valid.set("---"); + text_req.set("---"); + text_calc_n.set("---"); + text_calc_frac.set("---"); + } + + // Read actual hardware registers + uint32_t r3 = radio::debug::second_if::register_read(3); + uint32_t r4 = radio::debug::second_if::register_read(4); + + text_r3.set(to_string_hex(r3, 4)); + text_r4.set(to_string_hex(r4, 4)); + + // Decode actual values from registers + uint16_t act_n = r3 & 0xFF; + uint32_t act_frac_lo = (r3 >> 8) & 0x3F; + uint32_t act_frac_hi = r4 & 0x3FFF; + uint32_t act_frac = (act_frac_hi << 6) | act_frac_lo; + + text_act_n.set(to_string_dec_uint(act_n)); + text_act_frac.set(to_string_hex(act_frac, 5)); + + // Calculate actual frequency from registers + // F_LO = 20 MHz Γ— (N + Frac/2^20) + // For display, show integer part only + uint32_t calc_freq_mhz = 20 * act_n; + // Add fractional contribution (approximate) + uint32_t frac_contribution = (act_frac * 20) >> 20; + calc_freq_mhz += frac_contribution; + + text_calc_freq.set(to_string_dec_uint(calc_freq_mhz) + " MHz"); + + // Status + if (!info.set_frequency_called) { + text_status.set("MAX2831 set_frequency\nNEVER called!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (!info.frequency_valid) { + text_status.set("Freq " + to_string_dec_uint(info.requested_freq_mhz) + + " MHz OUT OF RANGE!\n(need 2300-2600)"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (act_n == info.calculated_n && act_frac == info.calculated_frac) { + text_status.set("MATCH!\nHardware = Expected"); + text_status.set_style(Theme::getInstance()->fg_green); + } else { + text_status.set("MISMATCH!\nN: exp=" + to_string_dec_uint(info.calculated_n) + + " act=" + to_string_dec_uint(act_n)); + text_status.set_style(Theme::getInstance()->fg_red); + } +} + +#endif + +#endif /* DebugPeripheralsMenuView **********************************************/ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) @@ -377,11 +3432,21 @@ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) } void DebugPeripheralsMenuView::on_populate() { +#ifdef PRALINE + const char* max283x = "MAX2831"; +#else const char* max283x = hackrf_r9 ? "MAX2839" : "MAX2837"; +#endif const char* si5351x = hackrf_r9 ? "Si5351A" : "Si5351C"; add_items({ {"RFFC5072", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push("RFFC5072", RegistersWidgetConfig{CT_RFFC5072, 31, 31, 16}); }}, +#ifdef PRALINE + {max283x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, max283x]() { nav_.push(max283x, RegistersWidgetConfig{CT_MAX283X, 16, 16, 14}); }}, + {"FPGA", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push("FPGA (iCE40)", RegistersWidgetConfig{CT_FPGA, 6, 6, 8}); }}, +#else {max283x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, max283x]() { nav_.push(max283x, RegistersWidgetConfig{CT_MAX283X, 32, 32, 10}); }}, +#endif + {"SGPIO", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push("SGPIO", RegistersWidgetConfig{CT_SGPIO, 6, 6, 16}); }}, {si5351x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, si5351x]() { nav_.push(si5351x, RegistersWidgetConfig{CT_SI5351, 188, 96, 8}); }}, {audio::debug::codec_name(), Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push(audio::debug::codec_name(), RegistersWidgetConfig{CT_AUDIO, audio::debug::reg_count(), audio::debug::reg_count(), audio::debug::reg_bits()}); }}, }); @@ -413,11 +3478,49 @@ DebugMenuView::DebugMenuView(NavigationView& nav) set_max_rows(2); // allow wider buttons } +#ifdef PRALINE +/* PralineDebugMenuView *************************************************/ + +PralineDebugMenuView::PralineDebugMenuView(NavigationView& nav) + : nav_(nav) { + set_max_rows(2); // Allows wider buttons for descriptive titles +} + +void PralineDebugMenuView::on_populate() { + if (portapack::persistent_memory::show_gui_return_icon()) { + add_items({{"..", ui::Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}}); + } + add_items({ + {"WFM Audio", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"MSynth", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Radio Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"PLL A", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"PLL B", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"GPIO", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"MAX2831", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Si5351", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"SGPIO Clk", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Baseband", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + }); +} +#endif + void DebugMenuView::on_populate() { if (portapack::persistent_memory::show_gui_return_icon()) { add_items({{"..", ui::Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}}); } add_items({ +#ifdef PRALINE + {"Pro Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_setup, [this]() { nav_.push(); }}, +#endif {"Buttons Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_controls, [this]() { nav_.push(); }}, {"M0 Stack Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { stack_dump(); }}, {"Memory Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { nav_.push(); }}, @@ -515,7 +3618,7 @@ void DebugPmemView::update() { DebugScreenTest::DebugScreenTest(NavigationView& nav) : nav_{nav} { set_focusable(true); - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); } bool DebugScreenTest::on_key(const KeyEvent key) { @@ -525,10 +3628,10 @@ bool DebugScreenTest::on_key(const KeyEvent key) { nav_.pop(); break; case KeyEvent::Down: - painter.fill_rectangle({0, 0, screen_width, screen_height}, std::rand()); + painter.fill_rectangle({0, 0, screen_width, screen_height}, rand()); break; case KeyEvent::Left: - pen_color = std::rand(); + pen_color = rand(); break; default: break; @@ -551,7 +3654,7 @@ bool DebugScreenTest::on_touch(const TouchEvent event) { void DebugScreenTest::paint(Painter& painter) { painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Theme::getInstance()->bg_darkest->foreground); painter.draw_string({10 * 8, screen_height / 2}, *Theme::getInstance()->bg_darkest, "Use Stylus"); - pen_color = std::rand(); + pen_color = rand(); } /* DebugLCRView *******************************************************/ diff --git a/firmware/application/apps/ui_debug.hpp b/firmware/application/apps/ui_debug.hpp index e6c0bf56d..85e93c710 100644 --- a/firmware/application/apps/ui_debug.hpp +++ b/firmware/application/apps/ui_debug.hpp @@ -93,6 +93,10 @@ typedef enum { CT_SI5351, CT_AUDIO, CT_MAX17055, +#ifdef PRALINE + CT_FPGA, +#endif + CT_SGPIO, } chip_type_t; struct RegistersWidgetConfig { @@ -373,6 +377,1025 @@ private: }; };*/ +/* Radio Signal Path Diagnostics View + * Shows status of each component in the RX/TX signal chain: + * Antenna -> RF Path -> RFFC5072 -> MAX283x -> MAX5864 -> FPGA -> SGPIO -> MCU + */ +class RadioDiagnosticsView : public View { + public: + RadioDiagnosticsView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "Radio Diag"; }; + + private: + NavigationView& nav_; + + void update_status(); + + Text text_title{{0, 0, 240, 16}, "=== Signal Path Status ==="}; + + Text text_lbl_rffc{{0, 20, 140, 16}, "RFFC5072 (1st IF):"}; + Text text_rffc_status{{144, 20, 96, 16}, "---"}; + + Text text_lbl_max{{0, 36, 140, 16}, "MAX283x (2nd IF):"}; + Text text_max_status{{144, 36, 96, 16}, "---"}; + + Text text_lbl_adc{{0, 52, 140, 16}, "MAX5864 (ADC):"}; + Text text_adc_status{{144, 52, 96, 16}, "---"}; + + Text text_lbl_fpga{{0, 68, 140, 16}, "FPGA/CPLD:"}; + Text text_fpga_status{{144, 68, 96, 16}, "---"}; + + Text text_lbl_sgpio{{0, 84, 140, 16}, "SGPIO:"}; + Text text_sgpio_status{{144, 84, 96, 16}, "---"}; + + Text text_lbl_clock{{0, 100, 140, 16}, "Si5351 Clocks:"}; + Text text_clock_status{{144, 100, 96, 16}, "---"}; + + Text text_regs_title{{0, 124, 240, 16}, "=== Key Registers ==="}; + + Text text_lbl_rffc_reg{{0, 144, 80, 16}, "RFFC R0:"}; + Text text_rffc_reg{{80, 144, 160, 16}, "---"}; + + Text text_lbl_max_reg{{0, 160, 80, 16}, "MAX R0:"}; + Text text_max_reg{{80, 160, 160, 16}, "---"}; + + Text text_lbl_fpga_reg{{0, 176, 80, 16}, "FPGA:"}; + Text text_fpga_reg{{80, 176, 160, 16}, "---"}; + + Text text_lbl_sgpio_reg{{0, 192, 80, 16}, "SGPIO:"}; + Text text_sgpio_reg{{80, 192, 160, 16}, "---"}; + + Text text_test_result{{0, 220, 240, 32}, ""}; + + Button button_refresh{ + {8, 280, 72, 24}, + "Refresh"}; + + Button button_done{ + {168, 280, 64, 24}, + "Done"}; +}; + +#ifdef PRALINE +class PralineDebugMenuView : public BtnGridView { + public: + PralineDebugMenuView(NavigationView& nav); + std::string title() const override { return "Pro Debug"; }; + + private: + NavigationView& nav_; + void on_populate() override; +}; + +/* Praline-Specific Radio Debug View + * Monitors Mixer Lock, SPI Bit Depth, and toggles Si5351 CLK5 + */ +class PralineRadioDebugView : public View { + public: + PralineRadioDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Pro Radio Debug"; }; + + private: + void refresh(); + void toggle_clk5(); + + Text text_title{{0, 0, 240, 16}, "=== Pro Radio Debug ==="}; + + // Mixer Lock (PD_11 / GPIO6[25]) + Text text_lbl_lock{{0, 24, 124, 16}, "Mix Lock (LD):"}; + Text text_lock_status{{126, 24, 114, 16}, "---"}; + + // Si5351 CLK5 (Mixer Reference) + Text text_lbl_clk5{{0, 40, 124, 16}, "Si5351 CLK5(Mix):"}; + Text text_clk5_status{{126, 40, 114, 16}, "---"}; + + // SPI Bus configuration check + Text text_lbl_spi{{0, 56, 124, 16}, "SPI Bit Mode:"}; + Text text_spi_status{{126, 56, 118, 16}, "---"}; + + // FPGA DC Block (Reg 1) + Text text_lbl_fpga_ctrl{{0, 72, 124, 16}, "FPGA Reg 1:"}; + Text text_fpga_ctrl{{126, 72, 118, 16}, "---"}; + + // VAA Power Rail (P8_1 / GPIO4[1]) + Text text_lbl_vaa{{0, 88, 124, 16}, "VAA RF Power:"}; + Text text_vaa_status{{126, 88, 118, 16}, "---"}; + + Text text_status_msg{{0, 110, 240, 48}, ""}; + + Button button_refresh{{8, 240, 72, 24}, "Refresh"}; + Button button_toggle_clk5{{88, 240, 72, 24}, "CLK5_T"}; + Button button_done{{168, 240, 64, 24}, "Done"}; +}; +#endif + +/* WFMAudioDebugView ***************************************************/ + +#ifdef PRALINE +class WFMAudioDebugView : public View { + public: + WFMAudioDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "WFM Audio Debug"; } + + private: + void refresh(); + + NavigationView& nav_; + + Text text_title{{0, 0, 240, 16}, "=== WFM Audio Debug ==="}; + + // Sample rates section + Text text_lbl_clk0{{0, 20, 140, 16}, "Si5351 CLK0 Rate:"}; + Text text_clk0{{150, 20, 90, 16}, "---"}; + + Text text_lbl_fpga_dec{{0, 36, 140, 16}, "FPGA Decimation:"}; + Text text_fpga_dec{{150, 36, 90, 16}, "---"}; + + Text text_lbl_post_fpga{{0, 52, 140, 16}, "Post-FPGA Rate:"}; + Text text_post_fpga{{150, 52, 90, 16}, "---"}; + + // MAX2831 section + Text text_section1{{0, 72, 240, 16}, "--- MAX2831 LPF ---"}; + + Text text_lbl_reg8{{0, 88, 140, 16}, "Reg8 (LPF RX):"}; + Text text_reg8{{150, 88, 90, 16}, "---"}; + + Text text_lbl_lpf_bw{{0, 104, 140, 16}, "LPF Bandwidth:"}; + Text text_lpf_bw{{150, 104, 90, 16}, "---"}; + + // FPGA section + Text text_section2{{0, 124, 240, 16}, "--- FPGA Control ---"}; + + Text text_lbl_fpga_r1{{0, 140, 140, 16}, "Reg1 (Ctrl):"}; + Text text_fpga_r1{{150, 140, 90, 16}, "---"}; + + Text text_lbl_dc_q{{0, 156, 70, 16}, "DC/Q/QS:"}; + Text text_dc_q{{72, 156, 168, 16}, "---"}; + + // Audio section + Text text_section3{{0, 176, 240, 16}, "--- Audio Path ---"}; + + Text text_lbl_expected{{0, 192, 96, 16}, "Expctd Aud:"}; + Text text_expected{{98, 192, 142, 16}, "---"}; + + Text text_lbl_deemph{{0, 208, 96, 16}, "D-emph Cfg:"}; + Text text_deemph{{98, 208, 132, 16}, "---"}; + + // Status + Text text_status{{0, 228, 240, 16}, "---"}; + Text text_status2{{0, 244, 240, 16}, "---"}; + + Button button_refresh{{10, 268, 70, 24}, "Refresh"}; + Button button_toggle_q{{90, 268, 70, 24}, "Toggle Q"}; + Button button_done{{170, 268, 60, 24}, "Done"}; +}; +#endif + +/* BasebandStatusView ***************************************************/ + +class BasebandStatusView : public View { + public: + BasebandStatusView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "Baseband Status"; }; + + private: + NavigationView& nav_; + + void update(); + + Text text_title{{0, 0, 240, 16}, "=== Baseband Counters ==="}; + + Text text_lbl_marker{{0, 20, 140, 16}, "Streaming Marker:"}; + Text text_marker{{144, 20, 96, 16}, "---"}; + + Text text_lbl_loops{{0, 36, 140, 16}, "Baseband Loops:"}; + Text text_loops{{144, 36, 96, 16}, "---"}; + + Text text_lbl_wait{{0, 52, 140, 16}, "DMA Wait Count:"}; + Text text_wait{{144, 52, 96, 16}, "---"}; + + Text text_lbl_xfr{{0, 68, 140, 16}, "DMA Xfr Count:"}; + Text text_xfr{{144, 68, 96, 16}, "---"}; + + Text text_lbl_missed{{0, 84, 140, 16}, "Buffer Missed:"}; + Text text_missed{{144, 84, 96, 16}, "---"}; + + Text text_status_line1{{0, 110, 240, 16}, ""}; + Text text_status_line2{{0, 126, 240, 16}, ""}; + Text text_status_line3{{0, 142, 240, 16}, ""}; + + Button button_refresh{ + {8, 280, 72, 24}, + "Refresh"}; + + Button button_done{ + {168, 280, 64, 24}, + "Done"}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { this->update(); }}; +}; + +/* SGPIOLiveMonitorView ***************************************************/ + +class SGPIOLiveMonitorView : public View { + public: + SGPIOLiveMonitorView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "SGPIO Live"; }; + + private: + NavigationView& nav_; + + void update(); + + Text text_title{{0, 0, 240, 16}, "=== SGPIO Registers ==="}; + + Text text_lbl_ctrl{{0, 20, 140, 16}, "CTRL_ENABLE:"}; + Text text_ctrl{{144, 20, 96, 16}, "---"}; + + Text text_lbl_in{{0, 36, 140, 16}, "GPIO_INREG:"}; + Text text_in{{144, 36, 96, 16}, "---"}; + + Text text_lbl_ss{{0, 52, 140, 16}, "REG_SS[0]:"}; + Text text_ss{{144, 52, 96, 16}, "---"}; + + Text text_lbl_status{{0, 68, 140, 16}, "STATUS_1:"}; + Text text_status{{144, 68, 96, 16}, "---"}; + + Text text_lbl_out{{0, 84, 140, 16}, "GPIO_OUTREG:"}; + Text text_out{{144, 84, 96, 16}, "---"}; + + Text text_lbl_oen{{0, 100, 140, 16}, "GPIO_OENREG:"}; + Text text_oen{{144, 100, 96, 16}, "---"}; + + Text text_diag_line1{{0, 126, 240, 16}, ""}; + Text text_diag_line2{{0, 142, 240, 16}, ""}; + Text text_diag_line3{{0, 158, 240, 16}, ""}; + Text text_diag_line4{{0, 174, 240, 16}, ""}; + + Button button_refresh{ + {8, 280, 72, 24}, + "Refresh"}; + + Button button_done{ + {168, 280, 64, 24}, + "Done"}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { this->update(); }}; +}; + +/* Radio RX Step-by-Step Test View + * Tests radio hardware directly without M0 baseband involvement. + * Logs each step to help isolate where the signal chain breaks. + */ +class RadioRxTestView : public View { + public: + RadioRxTestView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "RX Test"; }; + + private: + NavigationView& nav_; + bool radio_initialized_{false}; + uint32_t test_frequency_{433000000}; // 433 MHz default + + void log(const std::string& msg); + void log_registers(const std::string& label); + void run_init_test(); + void run_rx_mode_test(); + void run_freq_test(); + void run_sgpio_test(); + void run_full_test(); + void run_step_test(); + bool check_gpio_changing(); + + Labels labels{ + {{0, 0}, "=== Radio RX Test ===", Theme::getInstance()->fg_yellow->foreground}}; + + Console console{ + {0, 20, 240, 200}}; + + Button button_init{ + {0, 224, 56, 24}, + "Init"}; + + Button button_rx{ + {60, 224, 56, 24}, + "RX"}; + + Button button_freq{ + {120, 224, 56, 24}, + "Freq"}; + + Button button_sgpio{ + {180, 224, 56, 24}, + "SGPIO"}; + + Button button_full{ + {0, 252, 56, 24}, + "Full"}; + + Button button_step{ + {60, 252, 56, 24}, + "Step"}; + + Button button_done{ + {120, 252, 112, 24}, + "Done"}; +}; + +/* SGPIO8 Clock Detector View + * Samples SGPIO8 pin to verify external clock is present. + * Shows toggle count and estimated frequency. + */ +class SGPIO8ClockDetectorView : public View { + public: + SGPIO8ClockDetectorView(NavigationView& nav); + void focus() override; + std::string title() const override { return "SGPIO8 Clock"; }; + + private: + NavigationView& nav_; + + Text text_title{{8, 16, 224, 16}, "SGPIO8 Clock Detector"}; + Text text_lbl_samples{{8, 48, 160, 16}, "Samples (first 20):"}; + Text text_samples{{8, 64, 224, 16}, " "}; + Text text_lbl_toggles{{8, 96, 160, 16}, "Toggle count:"}; + Text text_toggles{{8, 112, 224, 16}, " "}; + Text text_status{{8, 144, 224, 32}, " "}; + + Button button_sample{{8, 200, 96, 24}, "Sample"}; + Button button_done{{128, 200, 96, 24}, "Done"}; + + void sample_sgpio8(); +}; + +/* Slice Status View + * Shows SGPIO slice status: which slices are enabled vs active, + * counter values, and data capture status. + */ + +/* Si5351 Debug View + * Dedicated diagnostic tool for Si5351 clock generator. + * Shows PLL lock status, clock configurations, and allows testing. + */ +class Si5351DebugView : public View { + public: + Si5351DebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Si5351 Clocks"; }; + + private: + NavigationView& nav_; + + Text text_title{{0, 0, 200, 16}, "Si5351 Clock Generator"}; + + Text text_status_label{{0, 16, 80, 16}, "Status Reg:"}; + Text text_status_value{{96, 16, 144, 16}, ""}; + + Text text_pll_a_label{{0, 32, 80, 16}, "PLL A:"}; + Text text_pll_a_status{{96, 32, 144, 16}, ""}; + + Text text_pll_b_label{{0, 48, 80, 16}, "PLL B:"}; + Text text_pll_b_status{{96, 48, 144, 16}, ""}; + + Text text_sys_init_label{{0, 64, 80, 16}, "SYS_INIT:"}; + Text text_sys_init_status{{96, 64, 144, 16}, ""}; + + Text text_xtal_cap_label{{0, 80, 80, 16}, "XTAL Cap:"}; + Text text_xtal_cap_value{{96, 80, 144, 16}, ""}; + + Text text_clk0_label{{0, 96, 48, 16}, "CLK0:"}; + Text text_clk0_status{{50, 96, 28, 16}, ""}; + Text text_clk0_freq_value{{80, 96, 160, 16}, ""}; + Text text_clk0_div_value{{50, 112, 190, 16}, ""}; + + Text text_clk1_label{{0, 128, 96, 16}, "CLK1 (SCT):"}; + Text text_clk1_status{{112, 128, 128, 16}, ""}; + + Text text_clk4_label{{0, 144, 96, 16}, "CLK4 (MAX):"}; + Text text_clk4_status{{112, 144, 128, 16}, ""}; + + Text text_clk5_label{{0, 160, 96, 16}, "CLK5 (RFFC):"}; + Text text_clk5_status{{112, 160, 128, 16}, ""}; + + Button button_refresh{{8, 240, 72, 24}, "Refresh"}; + Button button_reset_pll{{88, 240, 72, 24}, "Reset PLL"}; + Button button_done{{168, 240, 64, 24}, "Done"}; + + void refresh_status(); + void reset_pll(); +}; + +#ifdef PRALINE +/* SignalPathStatusView *************************************************/ +class SignalPathStatusView : public View { + public: + SignalPathStatusView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Signal Path"; }; + + private: + NavigationView& nav_; + void refresh_status(); + + Text text_title{{0, 0, 240, 16}, "=== Signal Path Status ==="}; + + Text text_lbl_max_enable{{0, 20, 114, 16}, "MAX2831:"}; + Text text_max_enable{{116, 20, 124, 16}, "---"}; + + Text text_lbl_max_mode{{0, 36, 114, 16}, "RX Mode:"}; + Text text_max_mode{{116, 36, 124, 16}, "---"}; + + Text text_lbl_rf_path{{0, 52, 114, 16}, "RF Path:"}; + Text text_rf_path{{116, 52, 124, 16}, "---"}; + + Text text_lbl_filter{{0, 68, 114, 16}, "Filter Band:"}; + Text text_filter{{116, 68, 124, 16}, "---"}; + + Text text_lbl_mixer{{0, 84, 114, 16}, "Mixer Enable:"}; + Text text_mixer{{116, 84, 124, 16}, "---"}; + + Text text_lbl_rf_amp{{0, 100, 114, 16}, "RF Amp:"}; + Text text_rf_amp{{116, 100, 124, 16}, "---"}; + + Text text_lbl_lna{{0, 116, 114, 16}, "LNA Gain:"}; + Text text_lna{{116, 116, 124, 16}, "---"}; + + Text text_lbl_vga{{0, 132, 114, 16}, "VGA Gain:"}; + Text text_vga{{116, 132, 124, 16}, "---"}; + + Text text_lbl_fpga_decim{{0, 148, 114, 16}, "FPGA Decim:"}; + Text text_fpga_decim{{116, 148, 124, 16}, "---"}; + + Text text_lbl_fpga_ctrl_dc_q{{0, 164, 114, 16}, "FPGA Ctrl:"}; + Text text_fpga_ctrl_dc_q{{116, 164, 124, 16}, "---"}; + + Text text_lbl_fpga_ctrl_qs{{0, 180, 114, 16}, ""}; + Text text_fpga_ctrl_qs{{116, 180, 124, 16}, "---"}; + + Text text_status{{0, 200, 240, 32}, ""}; + + Button button_refresh{{0, 280, 72, 24}, "Refresh"}; + Button button_toggle_q{{88, 280, 72, 24}, "Q Inv"}; + Button button_done{{176, 280, 64, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +class GPIODebugView : public View { + public: + GPIODebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "GPIO Debug"; }; + + private: + void refresh(); + + // GPIO4 (LPF and RF Amp) + Text text_lbl_gpio4{{0, 0, 240, 16}, "Pin Diag: GPIO4 (LPF|Amp|Mix)"}; + + Text text_lbl_mixr1{{0, 18, 56, 16}, "MixR1:"}; + Text text_mixr1{{58, 18, 180, 16}, "---"}; + + Text text_lbl_pin4{{0, 36, 114, 16}, "PIN[4] (read):"}; + Text text_pin4{{116, 36, 124, 16}, "---"}; + + Text text_lbl_set4{{0, 54, 114, 16}, "SET[4] (write):"}; + Text text_set4{{116, 54, 124, 16}, "---"}; + + // Bit 8 (LPF) + Text text_lbl_lpf_bit{{0, 78, 240, 16}, "Bit 8 (LPF - GPIO4[8]):"}; + Text text_lpf_dir{{0, 96, 80, 16}, "DIR: ?"}; + Text text_lpf_pin{{82, 96, 78, 16}, "PIN: ?"}; + Text text_lpf_set{{162, 96, 78, 16}, "SET: ?"}; + + Button button_lpf_toggle{{8, 114, 110, 24}, "Toggle LPF"}; + Button button_lpf_on{{122, 114, 50, 24}, "ON"}; + Button button_lpf_off{{176, 114, 50, 24}, "OFF"}; + + // Bit 9 (RF Amp) + Text text_lbl_amp_bit{{0, 146, 240, 16}, "Bit 9 (Amp - GPIO4[9]):"}; + Text text_amp_dir{{0, 164, 80, 16}, "DIR: ?"}; + Text text_amp_pin{{82, 164, 78, 16}, "PIN: ?"}; + Text text_amp_set{{162, 164, 78, 16}, "SET: ?"}; + + Button button_amp_toggle{{8, 182, 110, 24}, "Toggle Amp"}; + Button button_amp_on{{122, 182, 50, 24}, "ON"}; + Button button_amp_off{{176, 182, 50, 24}, "OFF"}; + + // GPIO3 (Mixer) + Text text_lbl_gpio3{{0, 214, 240, 16}, "--- GPIO3 (Mixer) ---"}; + Text text_lbl_mix_bit{{0, 232, 240, 16}, "Bit 2 (MIX - GPIO3[2]):"}; + Text text_mix_dir{{0, 250, 80, 16}, "DIR: ?"}; + Text text_mix_pin{{82, 250, 78, 16}, "PIN: ?"}; + Text text_mix_set{{162, 250, 78, 16}, "SET: ?"}; + + // Controls + Button button_refresh{{8, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +/* RFFC5072StatusView *************************************************/ +class RFFC5072StatusView : public View { + public: + RFFC5072StatusView(NavigationView& nav); + void focus() override; + std::string title() const override { return "RFFC5072 Status"; }; + + private: + NavigationView& nav_; + void refresh_status(); + + Text text_status{{0, 0, 240, 16}, "---"}; + + Text text_gpio4{{0, 16, 240, 16}, "---"}; + + Text text_ctrl{{0, 32, 240, 16}, "---"}; + + Text text_lbl_enabled{{0, 48, 114, 16}, "Status:"}; + Text text_enabled{{116, 48, 124, 16}, "---"}; + + Text text_lbl_freq{{0, 64, 114, 16}, "LO Freq:"}; + Text text_freq{{116, 64, 124, 16}, "---"}; + + Text text_lbl_path{{0, 80, 114, 16}, "Path:"}; + Text text_path{{116, 80, 124, 16}, "---"}; + + Text text_lbl_mixer{{0, 96, 114, 16}, "Mixer:"}; + Text text_mixer{{116, 96, 124, 16}, "---"}; + + Text text_lbl_r0{{0, 112, 114, 16}, "Reg 0:"}; + Text text_r0{{116, 112, 124, 16}, "---"}; + + Text text_lbl_r1{{0, 128, 114, 16}, "Reg 1 (N):"}; + Text text_r1{{116, 128, 124, 16}, "---"}; + + Text text_lbl_r2{{0, 144, 114, 16}, "Reg 2:"}; + Text text_r2{{116, 144, 124, 16}, "---"}; + + Text text_lbl_n{{0, 160, 114, 16}, "N divider:"}; + Text text_n{{116, 160, 124, 16}, "---"}; + + Text text_lbl_lodiv{{0, 176, 114, 16}, "LO divider:"}; + Text text_lodiv{{116, 176, 124, 16}, "---"}; + + Text text_lbl_calc{{0, 192, 114, 16}, "Calc freq:"}; + Text text_calc{{116, 192, 124, 16}, "---"}; + + Text text_regs_status{{0, 208, 240, 16}, "---"}; + + Text text_status2{{0, 224, 240, 16}, ""}; + Text text_status3{{0, 240, 240, 16}, ""}; + + Button button_refresh{{2, 280, 72, 24}, "Refresh"}; + Button button_force{{98, 280, 60, 24}, "SPI"}; + Button button_done{{182, 280, 56, 24}, "Done"}; +}; + +#ifdef PRALINE +/* RFFCTuningDebugView *************************************************/ +class RFFCTuningDebugView : public View { + public: + RFFCTuningDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "RFFC Tuning Debug"; }; + + private: + void refresh(); + + Text text_title{{0, 0, 240, 16}, "RFFC5072 Tuning Debug"}; + + Text text_lbl_called{{0, 18, 120, 16}, "Freq Set:"}; + Text text_called{{122, 18, 118, 16}, "NO"}; + + Text text_lbl_req{{0, 38, 120, 16}, "Requested:"}; + Text text_req{{122, 38, 118, 16}, "---"}; + + Text text_lbl_exp_n{{0, 56, 120, 16}, "Expected N:"}; + Text text_exp_n{{122, 56, 118, 16}, "---"}; + + Text text_lbl_act_n{{0, 74, 120, 16}, "Actual N:"}; + Text text_act_n{{122, 74, 118, 16}, "---"}; + + Text text_lbl_exp_div{{0, 92, 120, 16}, "Exp LO/Pres:"}; + Text text_exp_div{{122, 92, 118, 16}, "---"}; + + Text text_lbl_act_div{{0, 110, 120, 16}, "Act LO/Pres:"}; + Text text_act_div{{122, 110, 118, 16}, "---"}; + + Text text_lbl_calc{{0, 128, 120, 16}, "Calc LO freq:"}; + Text text_calc{{122, 128, 118, 16}, "---"}; + + Text text_lbl_calc_lo{{0, 146, 120, 16}, "Calc input:"}; + Text text_calc_lo{{122, 146, 118, 16}, "---"}; + + Text text_lbl_calc_vco{{0, 164, 120, 16}, "In Calc VCO:"}; + Text text_calc_vco{{122, 164, 118, 16}, "---"}; + + Text text_lbl_vco{{0, 182, 120, 16}, "Calc VCO:"}; + Text text_vco{{122, 182, 118, 16}, "---"}; + + Text text_lbl_n_q24{{0, 200, 120, 16}, "N (Q24):"}; + Text text_n_q24{{122, 200, 118, 16}, "---"}; + + Text text_status{{0, 224, 240, 48}, ""}; + + Button button_refresh{{8, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; + +#endif + +#ifdef PRALINE +/* MAX2831DebugView *************************************************/ + +class MAX2831DebugView : public View { + public: + MAX2831DebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "MAX2831 Debug"; }; + + private: + void refresh(); + + Text text_title{{0, 0, 240, 16}, "MAX2831 (2nd IF) Debug"}; + + Text text_lbl_called{{0, 24, 120, 16}, "Freq Set:"}; + Text text_called{{122, 24, 118, 16}, "NO"}; + + Text text_lbl_valid{{0, 42, 120, 16}, "In Range:"}; + Text text_valid{{122, 42, 118, 16}, "---"}; + + Text text_lbl_req{{0, 60, 120, 16}, "Requested:"}; + Text text_req{{122, 60, 118, 16}, "---"}; + + Text text_lbl_calc_n{{0, 78, 120, 16}, "Calc N:"}; + Text text_calc_n{{122, 78, 118, 16}, "---"}; + + Text text_lbl_calc_frac{{0, 96, 120, 16}, "Calc Frac:"}; + Text text_calc_frac{{122, 96, 118, 16}, "---"}; + + Text text_spacer{{0, 114, 240, 16}, "--- Hardware Regs ---"}; + + Text text_lbl_r3{{0, 132, 120, 16}, "Reg 3:"}; + Text text_r3{{122, 132, 118, 16}, "---"}; + + Text text_lbl_r4{{0, 150, 120, 16}, "Reg 4:"}; + Text text_r4{{122, 150, 118, 16}, "---"}; + + Text text_lbl_act_n{{0, 168, 120, 16}, "Actual N:"}; + Text text_act_n{{122, 168, 118, 16}, "---"}; + + Text text_lbl_act_frac{{0, 186, 120, 16}, "Actual Frac:"}; + Text text_act_frac{{122, 186, 118, 16}, "---"}; + + Text text_lbl_calc_freq{{0, 204, 120, 16}, "Calc Freq:"}; + Text text_calc_freq{{122, 204, 118, 16}, "---"}; + + Text text_status{{0, 228, 240, 44}, ""}; + + Button button_refresh{{8, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +class SystemDiagnosticsView : public View { + public: + SystemDiagnosticsView(NavigationView& nav); + void focus() override; + std::string title() const override { return "System Diagnostics"; }; + + private: + void refresh(); + void read_gpio_states(); + void set_sample_rate(uint32_t rate); + + Text text_title{{0, 0, 240, 16}, "System Diagnostics"}; + + // Sample Rate Section + Text text_lbl_sample{{0, 24, 42, 16}, "Rate:"}; + Text text_sample_rate{{44, 24, 50, 16}, "---"}; + Text text_lbl_fpga_decode{{96, 24, 144, 16}, "DC:? Q:? QS:?"}; + + Button button_sample_2m{{2, 44, 56, 24}, "2 MSPS"}; + Button button_sample_4m{{62, 44, 56, 24}, "4 MSPS"}; + Button button_sample_8m{{122, 44, 56, 24}, "8 MSPS"}; + Button button_sample_20m{{182, 44, 56, 24}, "20 MSPS"}; + + // Baseband Filter Section + Text text_lbl_bb_filter{{0, 76, 114, 16}, "BB Filter BW:"}; + Text text_bb_filter{{116, 76, 124, 16}, "---"}; + + Text text_lbl_reg8{{0, 94, 114, 16}, "MAX2831 R8:"}; + Text text_reg8{{116, 94, 124, 16}, "---"}; + + // GPIO States Section + Text text_lbl_gpio{{0, 118, 240, 16}, "--- GPIO Pin States ---"}; + + Text text_lbl_lpf{{0, 136, 56, 16}, "LPF:"}; + Text text_gpio_lpf{{58, 136, 181, 16}, "---"}; + + Text text_lbl_mix{{0, 154, 56, 16}, "Mixer:"}; + Text text_gpio_mix{{58, 154, 181, 16}, "---"}; + + Text text_lbl_amp{{0, 172, 56, 16}, "RF Amp:"}; + Text text_gpio_amp{{58, 172, 181, 16}, "---"}; + + // FPGA Control Section + Text text_lbl_fpga{{0, 196, 240, 16}, "--- FPGA Control ---"}; + + Text text_lbl_fpga_ctrl{{0, 214, 114, 16}, "FPGA Reg 1:"}; + Text text_fpga_ctrl{{116, 214, 124, 16}, "---"}; + + Text text_lbl_band{{0, 232, 48, 16}, "Band:"}; + Text text_band{{50, 232, 190, 16}, "---"}; + + Button button_toggle_q{{2, 252, 110, 24}, "Toggle Q Inv"}; + Button button_toggle_dc{{122, 252, 110, 24}, "Toggle DC Blk"}; + + // Control Buttons + Button button_refresh{{2, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +/* Si5351PLLADebugView - Debug fractional parameters for PLL A */ +class Si5351PLLADebugView : public View { + public: + Si5351PLLADebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Si5351 PLL A Debug"; }; + + private: + NavigationView& nav_; + void refresh(); + + Text text_title{{0, 0, 240, 16}, "===PLL A Fractional Debug==="}; + + // Raw register values + Text text_lbl_raw{{0, 24, 240, 16}, "---Raw Registers (26-33)---"}; + Text text_r26_27{{8, 40, 224, 16}, "---"}; + Text text_r28_30{{8, 56, 224, 16}, "---"}; + Text text_r31_33{{8, 72, 224, 16}, "---"}; + + // Decoded values + Text text_lbl_decoded{{0, 96, 240, 16}, "---Decoded Parameters---"}; + Text text_lbl_p1{{0, 112, 48, 16}, "P1:"}; + Text text_p1{{50, 112, 190, 16}, "---"}; + Text text_lbl_p2{{0, 128, 48, 16}, "P2:"}; + Text text_p2{{50, 128, 190, 16}, "---"}; + Text text_lbl_p3{{0, 144, 48, 16}, "P3:"}; + Text text_p3{{50, 144, 190, 16}, "---"}; + + // Calculated VCO + Text text_lbl_calc{{0, 168, 240, 16}, "---Calculated VCO---"}; + Text text_lbl_mult{{0, 184, 80, 16}, "Multiplier:"}; + Text text_mult{{82, 184, 158, 16}, "---"}; + Text text_lbl_vco{{0, 200, 80, 16}, "VCO Freq:"}; + Text text_vco{{82, 200, 158, 16}, "---"}; + + Text text_status{{0, 224, 240, 32}, ""}; + + Button button_refresh{{8, 280, 100, 24}, "Refresh"}; + Button button_done{{132, 280, 100, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +class Si5351PLLBDebugView : public View { + public: + Si5351PLLBDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Si5351 PLL B Debug"; }; + + private: + void refresh(); + NavigationView& nav_; + + Text text_title{{0, 0, 240, 16}, "=== PLL B Fractional Debug ==="}; + + Text text_lbl_raw{{0, 24, 240, 16}, "--- Raw Registers (34-41) ---"}; + Text text_r34_35{{8, 40, 224, 16}, "---"}; + Text text_r36_38{{8, 56, 224, 16}, "---"}; + Text text_r39_41{{8, 72, 224, 16}, "---"}; + + Text text_lbl_decoded{{0, 96, 240, 16}, "--- Decoded Parameters ---"}; + Text text_lbl_p1{{0, 112, 48, 16}, "P1:"}; + Text text_p1{{50, 112, 190, 16}, "---"}; + Text text_lbl_p2{{0, 128, 48, 16}, "P2:"}; + Text text_p2{{50, 128, 190, 16}, "---"}; + Text text_lbl_p3{{0, 144, 48, 16}, "P3:"}; + Text text_p3{{50, 144, 190, 16}, "---"}; + + Text text_lbl_calc{{0, 168, 240, 16}, "--- Calculated VCO ---"}; + Text text_lbl_vco{{0, 200, 80, 16}, "VCO Freq:"}; + Text text_vco{{82, 200, 158, 16}, "---"}; + + Text text_status{{0, 224, 240, 32}, ""}; + Button button_refresh{{8, 280, 100, 24}, "Refresh"}; + Button button_done{{132, 280, 100, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +/* Si5351MultiSynthDebugView - Debug fractional register values for MS0 */ +class Si5351MultiSynthDebugView : public View { + public: + Si5351MultiSynthDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Si5351 MS0 Debug"; }; + + private: + NavigationView& nav_; + void refresh(); + void force_pll_reset(); + void force_fractional_mode(); + + Text text_title{{0, 0, 240, 16}, "===MS0 Fractional Debug==="}; + + // Clock Control Register 16 (CLK0) + Text text_lbl_clk_ctrl{{0, 18, 100, 16}, "CLK0 Ctrl:"}; + Text text_clk_ctrl{{102, 18, 138, 16}, "---"}; + + Text text_lbl_ms_int{{0, 34, 100, 16}, "MS_INT bit:"}; + Text text_ms_int{{102, 34, 138, 16}, "---"}; + + // Raw register values + Text text_lbl_raw{{0, 54, 240, 16}, "---Raw Registers (42-49)---"}; + + Text text_lbl_r42_43{{0, 70, 80, 16}, "R42-43:"}; + Text text_r42_43{{82, 70, 158, 16}, "---"}; + + Text text_lbl_r44_46{{0, 86, 80, 16}, "R44-46:"}; + Text text_r44_46{{82, 86, 158, 16}, "---"}; + + Text text_lbl_r47_49{{0, 102, 80, 16}, "R47-49:"}; + Text text_r47_49{{82, 102, 158, 16}, "---"}; + + // Decoded values + Text text_lbl_decoded{{0, 122, 240, 16}, "---Decoded Values---"}; + + Text text_lbl_p1{{0, 138, 48, 16}, "P1:"}; + Text text_p1{{50, 138, 190, 16}, "---"}; + + Text text_lbl_p2{{0, 154, 48, 16}, "P2:"}; + Text text_p2{{50, 154, 190, 16}, "---"}; + + Text text_lbl_p3{{0, 170, 48, 16}, "P3:"}; + Text text_p3{{50, 170, 190, 16}, "---"}; + + Text text_lbl_rdiv{{0, 186, 48, 16}, "R_DIV:"}; + Text text_rdiv{{50, 186, 190, 16}, "---"}; + + // Calculated frequency + Text text_lbl_calc{{0, 206, 240, 16}, "---Calculated Output---"}; + + Text text_lbl_div{{0, 222, 80, 16}, "MS Div:"}; + Text text_div{{82, 222, 158, 16}, "---"}; + + Text text_lbl_freq{{0, 238, 80, 16}, "CLK0 Out:"}; + Text text_freq{{82, 238, 158, 16}, "---"}; + + // Status + Text text_status{{0, 258, 240, 16}, ""}; + + Button button_refresh{{2, 280, 56, 24}, "Refresh"}; + Button button_reset{{62, 280, 56, 24}, "PLL Rst"}; + Button button_frac{{122, 280, 56, 24}, "Frac"}; + Button button_done{{182, 280, 56, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +class PralineClockDebugView : public View { + public: + PralineClockDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Pro Clock Status"; }; + + private: + void refresh(); + + Text text_title{{0, 0, 240, 16}, "=== Pro Clock Dashboard ==="}; + + // System Status + Text text_lbl_pll{{0, 20, 80, 16}, "PLL Lock:"}; + Text text_pll_status{{88, 20, 152, 16}, "---"}; + + Text text_lbl_afe{{0, 36, 80, 16}, "AFE Rate:"}; + Text text_afe_rate{{88, 36, 152, 16}, "---"}; + + Text text_lbl_n{{0, 52, 80, 16}, "Decim (n):"}; + Text text_n_val{{88, 52, 152, 16}, "-"}; + + // Table Header (Width 240px, 30 characters) + Text text_header{{0, 72, 240, 16}, "ID mA Mode Src Phase Stat"}; + + // Helper structure to group row widgets for CLK0-CLK5 + struct ClockRow { + Text* id; + Text* ma; + Text* mode; + Text* src; // PLL A, or B + Text* phase; + Text* stat; + }; + std::vector rows; + + // We define the actual widgets for 6 clocks + // Note: Layout uses 16px vertical spacing per row + + // Full list of clock row entries (C0 through C5) + // Layout: Y starts at 88, increments by 16 per row. + + // C0: MAX5864 (AFE/Sampling) + Text t0_id{{0, 88, 24, 16}, "C0:"}; // Column 0 + Text t0_ma{{32, 88, 24, 16}, "-"}; // Column 4 (mA) + Text t0_mode{{64, 88, 48, 16}, "-"}; // Column 8 (Mode) + Text t0_src{{112, 88, 32, 16}, "-"}; // Column 14 (Src) + Text t0_ph{{160, 88, 48, 16}, "-"}; // Column 20 (Phase) + Text t0_st{{208, 88, 32, 16}, "-"}; // Column 26 (Stat) + + // C1: iCE40 FPGA Timing + Text t1_id{{0, 104, 24, 16}, "C1:"}; + Text t1_ma{{32, 104, 24, 16}, "-"}; + Text t1_mode{{64, 104, 48, 16}, "-"}; + Text t1_src{{112, 104, 32, 16}, "-"}; + Text t1_ph{{160, 104, 48, 16}, "-"}; + Text t1_st{{208, 104, 32, 16}, "-"}; + + // C2: LPC43xx MCU Input + Text t2_id{{0, 120, 24, 16}, "C2:"}; + Text t2_ma{{32, 120, 24, 16}, "-"}; + Text t2_mode{{64, 120, 48, 16}, "-"}; + Text t2_src{{112, 120, 32, 16}, "-"}; + Text t2_ph{{160, 120, 48, 16}, "-"}; + Text t2_st{{208, 120, 32, 16}, "-"}; + + // C3: SG_CLK PORT 1 (Switching Regulator/Logic Sync) + Text t3_id{{0, 136, 24, 16}, "C3:"}; + Text t3_ma{{32, 136, 24, 16}, "-"}; + Text t3_mode{{64, 136, 48, 16}, "-"}; + Text t3_src{{112, 136, 32, 16}, "-"}; + Text t3_ph{{160, 136, 48, 16}, "-"}; + Text t3_st{{208, 136, 32, 16}, "-"}; + + // C4: P_CLK (Peripheral/Expansion Clock) + Text t4_id{{0, 152, 24, 16}, "C4:"}; + Text t4_ma{{32, 152, 24, 16}, "-"}; + Text t4_mode{{64, 152, 48, 16}, "-"}; + Text t4_src{{112, 152, 32, 16}, "-"}; + Text t4_ph{{160, 152, 48, 16}, "-"}; + Text t4_st{{208, 152, 32, 16}, "-"}; + + // C5: AUX_CLK (Auxiliary Reference) + Text t5_id{{0, 168, 24, 16}, "C5:"}; + Text t5_ma{{32, 168, 24, 16}, "-"}; + Text t5_mode{{64, 168, 48, 16}, "-"}; + Text t5_src{{112, 168, 32, 16}, "-"}; + Text t5_ph{{160, 168, 48, 16}, "-"}; + Text t5_st{{208, 168, 32, 16}, "-"}; + + // C6: SG_CLK PORT 2 (Switching Regulator/Logic Sync) + Text t6_id{{0, 184, 24, 16}, "C6:"}; + Text t6_ma{{32, 184, 24, 16}, "-"}; + Text t6_mode{{64, 184, 48, 16}, "-"}; + Text t6_src{{112, 184, 32, 16}, "-"}; + Text t6_ph{{160, 184, 48, 16}, "-"}; + Text t6_st{{208, 184, 32, 16}, "-"}; + + // C7: UNUSED_CLK (Internal Expansion) + Text t7_id{{0, 200, 24, 16}, "C7:"}; + Text t7_ma{{32, 200, 24, 16}, "-"}; + Text t7_mode{{64, 200, 48, 16}, "-"}; + Text t7_src{{112, 200, 32, 16}, "-"}; + Text t7_ph{{160, 200, 48, 16}, "-"}; + Text t7_st{{208, 200, 32, 16}, "-"}; + + Button button_refresh{{8, 260, 100, 24}, "Refresh"}; + Button button_done{{132, 260, 100, 24}, "Done"}; +}; +#endif + +#endif + class DebugPeripheralsMenuView : public BtnGridView { public: DebugPeripheralsMenuView(NavigationView& nav); diff --git a/firmware/application/apps/ui_flash_utility.cpp b/firmware/application/apps/ui_flash_utility.cpp index b15bd7bdc..c9ff3f081 100644 --- a/firmware/application/apps/ui_flash_utility.cpp +++ b/firmware/application/apps/ui_flash_utility.cpp @@ -45,10 +45,14 @@ bool valid_firmware_file(std::filesystem::path::string_type path) { // test read of the whole file just to validate checksum (baseband flash code will re-read when flashing) auto result = firmware_file.open(path.c_str()); if (!result.is_valid()) { + uint64_t file_size = firmware_file.size(); + if (file_size > FLASH_ROM_SIZE) { + // Firmware file is larger than the flash size for this device + return false; + } checksum = 0; - for (uint32_t offset = 0; offset < FLASH_ROM_SIZE; offset += sizeof(read_buffer)) { + for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) { auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer)); - if ((!readResult) || (readResult.value() != sizeof(read_buffer))) { // File was smaller than 1MB: // If version is such that the file SHOULD have been 1MB, call it a checksum error (otherwise say it's OK). diff --git a/firmware/application/apps/ui_flash_utility.hpp b/firmware/application/apps/ui_flash_utility.hpp index 0d9f8d9fa..e1a0d82d3 100644 --- a/firmware/application/apps/ui_flash_utility.hpp +++ b/firmware/application/apps/ui_flash_utility.hpp @@ -32,7 +32,7 @@ #include "untar.hpp" #include -#define FLASH_ROM_SIZE 1048576 +#define FLASH_ROM_SIZE (FLASH_SIZE_MB * 1024 * 1024) #define FLASH_STARTING_ADDRESS 0x00000000 #define FLASH_EXPECTED_CHECKSUM 0x00000000 #define FLASH_CHECKSUM_ERROR 0xFFFFFFFF diff --git a/firmware/application/apps/ui_looking_glass_app.cpp b/firmware/application/apps/ui_looking_glass_app.cpp index 187c5f75c..a9d5a19fa 100644 --- a/firmware/application/apps/ui_looking_glass_app.cpp +++ b/firmware/application/apps/ui_looking_glass_app.cpp @@ -69,7 +69,7 @@ void GlassView::manage_beep_audio() { } } -void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power) { +void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power) { if (mode == LOOKING_GLASS_SINGLEPASS) { // <20MHz spectrum mode if (bin < 120) { @@ -91,7 +91,7 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint } } -rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) { +rf::Frequency GlassView::get_freq_from_bin_pos(uint16_t pos) { rf::Frequency freq_at_pos = 0; if (mode == LOOKING_GLASS_SINGLEPASS) { // starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by screen_width(240)/2 => 120 @@ -101,7 +101,14 @@ rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) { return freq_at_pos; } -void GlassView::on_marker_change() { +void GlassView::on_marker_change(int8_t delta) { + if ((marker_pixel_index + delta) < 0) + marker_pixel_index = marker_pixel_index + delta + screen_width; + else if ((marker_pixel_index + delta) > screen_width) + marker_pixel_index = marker_pixel_index + delta - screen_width; + else + marker_pixel_index = marker_pixel_index + delta; + marker = get_freq_from_bin_pos(marker_pixel_index); field_marker.set_text(to_string_short_freq(marker)); plot_marker(marker_pixel_index); // Refresh marker on screen @@ -200,7 +207,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) { baseband::spectrum_streaming_stop(); // Convert bins of this spectrum slice into a representative max_power and when enough, into pixels // we actually need screen_width (240) of those bins - for (uint8_t bin = 0; bin < bin_length; bin++) { + for (uint16_t bin = 0; bin < bin_length; bin++) { get_max_power(spectrum, bin, max_power); if (max_power > range_max_power) range_max_power = max_power; @@ -208,7 +215,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) { if (bin == 119) { uint8_t next_max_power = 0; get_max_power(spectrum, bin + 1, next_max_power); - for (uint8_t it = 0; it < ignore_dc; it++) { + for (uint16_t it = 0; it < ignore_dc; it++) { uint8_t med_max_power = (max_power + next_max_power) / 2; // due to the way process_bins works we have to keep resetting the color if (process_bins(&med_max_power) == true) return; // new line signaled, return @@ -285,7 +292,7 @@ void GlassView::on_range_changed() { max_power = 0; bins_hz_size = 0; - on_marker_change(); + on_marker_change(0); update_range_field(); // set the sample rate and bandwidth @@ -298,8 +305,8 @@ void GlassView::on_range_changed() { receiver_model.set_target_frequency(f_center); // tune rx for this slice } -void GlassView::plot_marker(uint8_t pos) { - uint8_t shift_y = 0; +void GlassView::plot_marker(uint16_t pos) { + uint16_t shift_y = 0; if (live_frequency_view > 0) // plot one line down when in live view { shift_y = 16; @@ -381,9 +388,10 @@ GlassView::GlassView( &button_beep_squelch, &field_marker, &field_trigger, + &button_marker_minus, + &button_marker_plus, &button_jump, &button_rst, - &field_rx_iq_phase_cal, &freq_stats}); load_presets(); // Load available presets from TXT files (or default). @@ -491,13 +499,7 @@ GlassView::GlassView( range_presets.set_selected_index(preset_index); field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) { - if ((marker_pixel_index + delta) < 0) - marker_pixel_index = marker_pixel_index + delta + screen_width; - else if ((marker_pixel_index + delta) > screen_width) - marker_pixel_index = marker_pixel_index + delta - screen_width; - else - marker_pixel_index = marker_pixel_index + delta; - on_marker_change(); + on_marker_change(delta); }; field_marker.on_select = [this](TextField&) { @@ -516,6 +518,14 @@ GlassView::GlassView( update_range_field(); }; + button_marker_minus.on_select = [this](Button&) { + on_marker_change(-1); + }; + + button_marker_plus.on_select = [this](Button&) { + on_marker_change(1); + }; + button_jump.on_select = [this](Button&) { // Launch Audio with peak frequency. launch_audio(max_freq_hold); @@ -525,13 +535,6 @@ GlassView::GlassView( reset_live_view(); }; - field_rx_iq_phase_cal.set_range(0, hackrf_r9 ? 63 : 31); // max2839 has 6 bits [0..63], max2837 has 5 bits [0..31] - field_rx_iq_phase_cal.set_value(get_spec_iq_phase_calibration_value()); // using accessor function of AnalogAudioView to read iq_phase_calibration_value from rx_audio.ini - field_rx_iq_phase_cal.on_change = [this](int32_t v) { - set_spec_iq_phase_calibration_value(v); // using accessor function of AnalogAudioView to write inside SPEC submenu, register value to max283x and save it to rx_audio.ini - }; - set_spec_iq_phase_calibration_value(get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio - display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates // trigger: @@ -581,15 +584,6 @@ void GlassView::on_freqchg(int64_t freq) { on_range_changed(); } -uint8_t GlassView::get_spec_iq_phase_calibration_value() { // define accessor functions inside AnalogAudioView to read & write real iq_phase_calibration_value - return iq_phase_calibration_value; -} - -void GlassView::set_spec_iq_phase_calibration_value(uint8_t cal_value) { // define accessor functions - iq_phase_calibration_value = cal_value; - radio::set_rx_max283x_iq_phase_calibration(iq_phase_calibration_value); -} - void GlassView::load_presets() { File presets_file; auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT"); diff --git a/firmware/application/apps/ui_looking_glass_app.hpp b/firmware/application/apps/ui_looking_glass_app.hpp index 5d4a99af0..fee8b2c87 100644 --- a/firmware/application/apps/ui_looking_glass_app.hpp +++ b/firmware/application/apps/ui_looking_glass_app.hpp @@ -66,9 +66,6 @@ class GlassView : public View { void on_hide() override; void focus() override; - uint8_t get_spec_iq_phase_calibration_value(); - void set_spec_iq_phase_calibration_value(uint8_t cal_value); - private: NavigationView& nav_; Gradient gradient{}; @@ -80,11 +77,10 @@ class GlassView : public View { uint8_t filter_index = 0; // OFF uint8_t trigger = 32; uint8_t mode = LOOKING_GLASS_FASTSCAN; - uint8_t live_frequency_view = 0; // Spectrum - uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4 - uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839 - int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled - bool beep_enabled = false; // activate on bip button click + uint8_t live_frequency_view = 0; // Spectrum + uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4 + int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled + bool beep_enabled = false; // activate on bip button click app_settings::SettingsManager settings_{ "rx_glass"sv, app_settings::Mode::RX, @@ -97,7 +93,6 @@ class GlassView : public View { {"scan_mode"sv, &mode}, {"freq_view"sv, &live_frequency_view}, {"freq_integrate"sv, &live_frequency_integrate}, - {"iq_phase_calibration"sv, &iq_phase_calibration_value}, // we are saving and restoring that CAL from Settings. {"beep_squelch"sv, &beep_squelch}, {"beep_enabled"sv, &beep_enabled}, }}; @@ -116,9 +111,9 @@ class GlassView : public View { void update_min(int32_t v); void update_max(int32_t v); void update_range_field(); - void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power); - rf::Frequency get_freq_from_bin_pos(uint8_t pos); - void on_marker_change(); + void get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power); + rf::Frequency get_freq_from_bin_pos(uint16_t pos); + void on_marker_change(int8_t delta); void retune(); bool process_bins(uint8_t* powerlevel); void on_channel_spectrum(const ChannelSpectrum& spectrum); @@ -128,7 +123,7 @@ class GlassView : public View { void on_range_changed(); void reset_live_view(); void add_spectrum_pixel(uint8_t power); - void plot_marker(uint8_t pos); + void plot_marker(uint16_t pos); void load_presets(); void populate_presets(); void launch_audio(rf::Frequency center_freq); @@ -138,7 +133,7 @@ class GlassView : public View { rf::Frequency f_center_ini{0}; rf::Frequency marker{0}; - uint8_t marker_pixel_index{0}; + uint16_t marker_pixel_index{0}; rf::Frequency marker_pixel_step{0}; // Size of one spectrum bin in Hz. @@ -173,8 +168,7 @@ class GlassView : public View { {{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground}, {{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground}, {{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground}, - {{0, 3 * 16}, "MARKER: MHz RXIQCAL", Theme::getInstance()->fg_light->foreground}, - //{{0, 4 * 16}, "RES: STEPS:", Theme::getInstance()->fg_light->foreground}}; + {{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground}, {{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}}; NumberField field_frequency_min{ @@ -223,16 +217,16 @@ class GlassView : public View { ""}; TextField field_marker{ - {7 * 8, 3 * 16, 9 * 8, 16}, + {12 * 8, 3 * 16, 9 * 8, 16}, ""}; - NumberField field_rx_iq_phase_cal{ - {28 * 8, 3 * 16}, - 2, - {0, 63}, // 5 or 6 bits IQ CAL phase adjustment (range updated later) - 1, - ' ', - }; + Button button_marker_minus{ + {7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8}, + "-"}; + + Button button_marker_plus{ + {9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8}, + "+"}; NumberField field_trigger{ {4 * 8, 4 * 16}, diff --git a/firmware/application/apps/ui_mictx.cpp b/firmware/application/apps/ui_mictx.cpp index 1d8e46367..01c926068 100644 --- a/firmware/application/apps/ui_mictx.cpp +++ b/firmware/application/apps/ui_mictx.cpp @@ -350,6 +350,12 @@ MicTXView::MicTXView( &tx_button, &tx_icon}); + // disable key repeat on select + initial_switch_config_ = get_switches_repeat_config(); + SwitchesState config = initial_switch_config_; + config[toUType(Switch::Sel)] = false; + set_switches_repeat_config(config); + set_rxbw_options(); set_rxbw_defaults(settings_.loaded()); @@ -640,6 +646,9 @@ MicTXView::MicTXView( } MicTXView::~MicTXView() { + // restore select key repeat mode + set_switches_repeat_config(initial_switch_config_); + audio::input::stop(); if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP) rxaudio(false); diff --git a/firmware/application/apps/ui_mictx.hpp b/firmware/application/apps/ui_mictx.hpp index cf375e4cf..770d7cf2a 100644 --- a/firmware/application/apps/ui_mictx.hpp +++ b/firmware/application/apps/ui_mictx.hpp @@ -90,6 +90,9 @@ class MicTXView : public View { sampling_rate /* sampling rate */ }; + // structure used to control key repeat + SwitchesState initial_switch_config_{}; + enum Mic_Modulation : uint32_t { MIC_MOD_NFM = 0, MIC_MOD_WFM = 1, @@ -301,7 +304,8 @@ class MicTXView : public View { { {" 8k5 ", 0}, // Initial dynamic values when we start Mic App. {" 11k ", 1}, - {" 16k ", 2}, + {" 12k5 ", 2}, + {" 16k ", 3}, }}; NumberField field_squelch{ diff --git a/firmware/application/apps/ui_recon.cpp b/firmware/application/apps/ui_recon.cpp index 255b73bb4..e0ba6a97c 100644 --- a/firmware/application/apps/ui_recon.cpp +++ b/firmware/application/apps/ui_recon.cpp @@ -1261,6 +1261,7 @@ size_t ReconView::change_mode(freqman_index_t new_mod) { else v = SPEC_MODULATION; field_mode.set_selected_index(v); + return; // Important ! Guru will occur if you don't return when modifying field from within on_change ! } last_mode = v; change_mode(v); diff --git a/firmware/application/apps/ui_settings.cpp b/firmware/application/apps/ui_settings.cpp index 7d77a0716..1e9d83375 100644 --- a/firmware/application/apps/ui_settings.cpp +++ b/firmware/application/apps/ui_settings.cpp @@ -33,6 +33,9 @@ #include "ui_touch_calibration.hpp" #include "ui_text_editor.hpp" #include "ui_external_items_menu_loader.hpp" +#include "ui_ss_viewer.hpp" +#include "ui_fileman.hpp" +#include "ui_sd_card_debug.hpp" #include "portapack_persistent_memory.hpp" #include "lpc43xx_cpp.hpp" @@ -309,13 +312,45 @@ SetFrequencyCorrectionModel SetRadioView::form_collect() { }; } +/* SetTXLimitView ************************************/ + +SetTXLimitView::SetTXLimitView(NavigationView& nav) { + add_children({ + &labels, + &tx_gain_max_db, + &tx_disable_switch, + &tx_amp_disable_switch, + &button_save, + &button_cancel, + }); + + tx_disable_switch.set_value(pmem::config_tx_disabled()); + tx_amp_disable_switch.set_value(pmem::config_tx_amp_disabled()); + tx_gain_max_db.set_value(pmem::config_tx_gain_max_db()); + + button_save.on_select = [&nav, this](Button&) { + pmem::set_config_tx_disabled(tx_disable_switch.value()); + pmem::set_config_tx_amp_disabled(tx_amp_disable_switch.value()); + pmem::set_config_tx_gain_max_db(tx_gain_max_db.value()); + send_system_refresh(); + nav.pop(); + }; + + button_cancel.on_select = [&nav, this](Button&) { + nav.pop(); + }; +} + +void SetTXLimitView::focus() { + button_save.focus(); +} + /* SetUIView *********************************************/ SetUIView::SetUIView(NavigationView& nav) { add_children({&checkbox_disable_touchscreen, &checkbox_bloff, &options_bloff, - &checkbox_showsplash, &checkbox_showclock, &options_clockformat, &checkbox_guireturnflag, @@ -342,7 +377,6 @@ SetUIView::SetUIView(NavigationView& nav) { } checkbox_disable_touchscreen.set_value(pmem::disable_touchscreen()); - checkbox_showsplash.set_value(pmem::config_splash()); checkbox_showclock.set_value(!pmem::hide_clock()); checkbox_guireturnflag.set_value(pmem::show_gui_return_icon()); @@ -381,7 +415,6 @@ SetUIView::SetUIView(NavigationView& nav) { pmem::set_clock_with_date(false); } - pmem::set_config_splash(checkbox_showsplash.value()); pmem::set_clock_hidden(!checkbox_showclock.value()); pmem::set_gui_return_icon(checkbox_guireturnflag.value()); pmem::set_disable_touchscreen(checkbox_disable_touchscreen.value()); @@ -414,7 +447,11 @@ void SetUIView::focus() { /* SetSDCardView *********************************************/ SetSDCardView::SetSDCardView(NavigationView& nav) { - add_children({&labels, + add_children({&status_labels, + &text_card_status, + &text_filesystem_type, + &button_more_info, + &labels, &checkbox_sdcard_speed, &button_test_sdcard_high_speed, &text_sdcard_test_status, @@ -423,6 +460,10 @@ SetSDCardView::SetSDCardView(NavigationView& nav) { checkbox_sdcard_speed.set_value(pmem::config_sdcard_high_speed_io()); + button_more_info.on_select = [&nav, this](Button&) { + nav.push(); + }; + button_test_sdcard_high_speed.on_select = [&nav, this](Button&) { pmem::set_config_sdcard_high_speed_io(true, false); text_sdcard_test_status.set("!! HIGH SPEED MODE ON !!"); @@ -440,7 +481,79 @@ SetSDCardView::SetSDCardView(NavigationView& nav) { } void SetSDCardView::focus() { - button_save.focus(); + button_cancel.focus(); +} + +void SetSDCardView::on_show() { + sd_card_status_signal_token = sd_card::status_signal += [this](const sd_card::Status) { + update_sd_card_status(); + }; + + update_sd_card_status(); +} + +void SetSDCardView::on_hide() { + sd_card::status_signal -= sd_card_status_signal_token; +} + +void SetSDCardView::update_sd_card_status() { + using sd_card::Status; + + const auto status = sd_card::status(); + + // Update card status text + switch (status) { + case Status::NotPresent: + text_card_status.set("Not Inserted"); + text_filesystem_type.set("---"); + break; + case Status::Present: + text_card_status.set("Inserted"); + text_filesystem_type.set("---"); + break; + case Status::Mounted: + text_card_status.set("Mounted"); + // Determine filesystem type + { + const auto fs_type = sd_card::fs.fs_type; + std::string fs_name; + switch (fs_type) { + case FS_FAT12: + fs_name = "FAT12"; + break; + case FS_FAT16: + fs_name = "FAT16"; + break; + case FS_FAT32: + fs_name = "FAT32"; + break; + case FS_EXFAT: + fs_name = "exFAT"; + break; + default: + fs_name = "Unknown"; + break; + } + text_filesystem_type.set(fs_name); + } + break; + case Status::ConnectError: + text_card_status.set("Connect Error"); + text_filesystem_type.set("---"); + break; + case Status::MountError: + text_card_status.set("Mount Error"); + text_filesystem_type.set("---"); + break; + case Status::IOError: + text_card_status.set("I/O Error"); + text_filesystem_type.set("---"); + break; + default: + text_card_status.set("Unknown"); + text_filesystem_type.set("---"); + break; + } } /* SetConverterSettingsView ******************************/ @@ -1080,6 +1193,46 @@ void SetBatteryView::focus() { button_cancel.focus(); } +/* SetSlpash *********************************************/ + +SetSplash::SetSplash(NavigationView& nav) { + add_children({&checkbox_showsplash, + &checkbox_randomsplash, + &message, + &button_picture_select, + &button_save, + &button_cancel}); + + checkbox_showsplash.set_value(pmem::config_splash()); + splash_bmp_exists = file_exists(splash_dot_bmp); + checkbox_randomsplash.set_value(!splash_bmp_exists); + message.hidden(splash_bmp_exists); + + checkbox_randomsplash.on_select = [this](Checkbox&, bool v) { + random_enabled = v; + }; + + button_picture_select.on_select = [this, &nav](Button&) { + auto ret = nav.push(); + ret->push_dir(splash_dir); + }; + + button_save.on_select = [&nav, this](Button&) { + if (random_enabled == true) delete_file(splash_dot_bmp); + pmem::set_config_splash(checkbox_showsplash.value()); + send_system_refresh(); + nav.pop(); + }; + + button_cancel.on_select = [&nav, this](Button&) { + nav.pop(); + }; +} + +void SetSplash::focus() { + button_save.focus(); +} + /* SettingsMenuView **************************************/ SettingsMenuView::SettingsMenuView(NavigationView& nav) @@ -1107,11 +1260,13 @@ void SettingsMenuView::on_populate() { {"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push(); }}, {"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [this]() { nav_.push(); }}, {"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push(); }}, + {"TX Limit", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push(); }}, {"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [this]() { nav_.push(); }}, {"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [this]() { nav_.push(); }}, {"Display", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push(); }}, {"Menu Color", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push(); }}, {"Theme", ui::Color::dark_cyan(), &bitmap_icon_setup, [this]() { nav_.push(); }}, + {"Splash settings", ui::Color::dark_cyan(), &bitmap_icon_file_image, [this]() { nav_.push(); }}, }); if (battery::BatteryManagement::isDetected()) add_item({"Battery", ui::Color::dark_cyan(), &bitmap_icon_batt_icon, [this]() { nav_.push(); }}); diff --git a/firmware/application/apps/ui_settings.hpp b/firmware/application/apps/ui_settings.hpp index a2f5f8e05..0a8898a03 100644 --- a/firmware/application/apps/ui_settings.hpp +++ b/firmware/application/apps/ui_settings.hpp @@ -34,6 +34,7 @@ #include "ui_navigation.hpp" #include "bitmap.hpp" #include "ff.h" +#include "sd_card.hpp" #include "portapack_persistent_memory.hpp" #include "irq_controls.hpp" @@ -264,6 +265,51 @@ class SetRadioView : public View { SetFrequencyCorrectionModel form_collect(); }; +class SetTXLimitView : public View { + public: + SetTXLimitView(NavigationView& nav); + + void focus() override; + + std::string title() const override { return "TX Limit"; }; + + private: + Labels labels{ + {{1 * 8, 1 * 16}, "Limits RF TX Gain", Theme::getInstance()->fg_light->foreground}, + {{1 * 8, 2 * 16}, "(This may affect", Theme::getInstance()->fg_light->foreground}, + {{1 * 8, 3 * 16}, "all applications.)", Theme::getInstance()->fg_light->foreground}, + {{2 * 8, 12 * 16}, "TX Max Gain:", Theme::getInstance()->fg_light->foreground}, + + }; + + Checkbox tx_disable_switch{ + {1 * 8, 6 * 16}, + 23, + "Disable TX"}; + + Checkbox tx_amp_disable_switch{ + {1 * 8, 8 * 16}, + 23, + "Disable TX Amp"}; + + NumberField tx_gain_max_db{ + {20 * 8, 12 * 16}, + 6, + {0, 47}, + 1, + ' ', + }; + + Button button_save{ + {UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, + "Save"}; + + Button button_cancel{ + {UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, + "Cancel", + }; +}; + using portapack::persistent_memory::backlight_timeout_t; class SetUIView : public View { @@ -298,77 +344,72 @@ class SetUIView : public View { {"1 hour", backlight_timeout_t::Timeout3600Sec}, }}; - Checkbox checkbox_showsplash{ - {3 * 8, 6 * 16}, - 20, - "Show splash"}; - Checkbox checkbox_showclock{ - {3 * 8, 8 * 16}, + {3 * 8, 6 * 16}, 20, "Show clock with:"}; OptionsField options_clockformat{ - {60, 9 * 16 + 8}, + {60, 7 * 16 + 8}, 20, {{"time only", 0}, {"time and date", 1}}}; Checkbox checkbox_guireturnflag{ - {3 * 8, 11 * 16}, + {3 * 8, 9 * 16}, 20, "Back button in menu"}; Labels labels{ - {{3 * 8, 13 * 16}, "Show/Hide Status Icons", Theme::getInstance()->fg_light->foreground}, + {{3 * 8, 11 * 16}, "Show/Hide Status Icons", Theme::getInstance()->fg_light->foreground}, }; ImageToggle toggle_camera{ - {2 * 8, 14 * 16 + 2, 16, 16}, + {2 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_camera}; ImageToggle toggle_sleep{ - {4 * 8, 14 * 16 + 2, 16, 16}, + {4 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_sleep}; ImageToggle toggle_stealth{ - {6 * 8, 14 * 16 + 2, 16, 16}, + {6 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_stealth}; ImageToggle toggle_converter{ - {8 * 8, 14 * 16 + 2, 16, 16}, + {8 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_upconvert}; ImageToggle toggle_bias_tee{ - {10 * 8, 14 * 16 + 2, 16, 16}, + {10 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_biast_off}; ImageToggle toggle_clock{ - {12 * 8, 14 * 16 + 2, 8, 16}, + {12 * 8, 12 * 16 + 2, 8, 16}, &bitmap_icon_clk_ext}; ImageToggle toggle_mute{ - {13 * 8, 14 * 16 + 2, 16, 16}, + {13 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_speaker_and_headphones_mute}; ImageToggle toggle_speaker{ - {15 * 8, 14 * 16 + 2, 16, 16}, + {15 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_speaker_mute}; ImageToggle toggle_battery_icon{ - {17 * 8, 14 * 16 + 2, 16, 16}, + {17 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_batt_icon}; ImageToggle toggle_battery_text{ - {19 * 8, 14 * 16 + 2, 16, 16}, + {19 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_batt_text}; ImageToggle toggle_fake_brightness{ - {21 * 8, 14 * 16 + 2, 16, 16}, + {21 * 8, 12 * 16 + 2, 16, 16}, &bitmap_icon_brightness}; ImageToggle toggle_sd_card{ - {23 * 8, 14 * 16 + 2, 16, 16}, + {23 * 8, 12 * 16 + 2, 16, 16}, &bitmap_sd_card_ok}; Button button_save{ @@ -384,36 +425,60 @@ class SetSDCardView : public View { public: SetSDCardView(NavigationView& nav); + void on_show() override; + void on_hide() override; + void focus() override; std::string title() const override { return "SD Card"; }; private: + SignalToken sd_card_status_signal_token{}; + // Status section (top half) + Labels status_labels{ + {{10, 24}, "Card Status:", Theme::getInstance()->fg_light->foreground}, + {{10, 48}, "Filesystem:", Theme::getInstance()->fg_light->foreground}}; + + Text text_card_status{ + {120, 24, 110, 16}, + ""}; + + Text text_filesystem_type{ + {120, 48, 110, 16}, + ""}; + + Button button_more_info{ + {UI_POS_X_CENTER(20), 90, UI_POS_WIDTH(20), UI_POS_HEIGHT(2)}, + "More Info"}; + + // Settings section (bottom half) Labels labels{ // 01234567890123456789012345678 - {{UI_POS_X_CENTER(26), 120 - 48}, " HIGH SPEED SDCARD IO ", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X_CENTER(26), 120 - 32}, " May or may not work !! ", Theme::getInstance()->fg_light->foreground}}; + {{UI_POS_X_CENTER(26), 140}, " HIGH SPEED SDCARD IO ", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_CENTER(26), 156}, " May or may not work !! ", Theme::getInstance()->fg_light->foreground}}; Checkbox checkbox_sdcard_speed{ - {UI_POS_X_CENTER(26), 120}, + {UI_POS_X_CENTER(26), 180}, 20, "enable high speed IO"}; Button button_test_sdcard_high_speed{ - {UI_POS_X_CENTER(27), 152, UI_POS_WIDTH(27), UI_POS_HEIGHT(2)}, + {UI_POS_X_CENTER(27), 210, UI_POS_WIDTH(27), UI_POS_HEIGHT(2)}, "TEST BUTTON (NO PMEM SAVE)"}; Text text_sdcard_test_status{ - {UI_POS_X_CENTER(28), 198, UI_POS_WIDTH(28), UI_POS_HEIGHT(1)}, + {UI_POS_X_CENTER(28), 256, UI_POS_WIDTH(28), UI_POS_HEIGHT(1)}, ""}; Button button_save{ - {UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, + {UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 32}, "Save"}; Button button_cancel{ - {UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, + {UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 32}, "Cancel"}; + + void update_sd_card_status(); }; class SetConverterSettingsView : public View { @@ -1024,6 +1089,43 @@ class SettingsMenuView : public BtnGridView { void on_populate() override; }; +class SetSplash : public View { + public: + SetSplash(NavigationView& nav); + + void focus() override; + + std::string title() const override { return "Splash"; }; + + private: + bool splash_bmp_exists = false; + bool random_enabled = false; + + Checkbox checkbox_showsplash{ + {3 * 8, 1 * 16}, + 20, + "Show splash"}; + + Checkbox checkbox_randomsplash{ + {3 * 8, 3 * 16}, + 20, + "Random splash on boot"}; + + Text message{{UI_POS_X(1), UI_POS_Y(6), UI_POS_WIDTH(26), UI_POS_HEIGHT(1)}, "Pick image to stop random."}; + + Button button_picture_select{ + {UI_POS_X_CENTER(8) - UI_POS_WIDTH(4), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(13), UI_POS_HEIGHT(2)}, + "Select pict."}; + + Button button_save{ + {UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, + "Save"}; + + Button button_cancel{ + {UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)}, + "Cancel"}; +}; + } /* namespace ui */ #endif /*__UI_SETTINGS_H__*/ diff --git a/firmware/application/apps/ui_ss_viewer.cpp b/firmware/application/apps/ui_ss_viewer.cpp index 67ead2955..bc3690843 100644 --- a/firmware/application/apps/ui_ss_viewer.cpp +++ b/firmware/application/apps/ui_ss_viewer.cpp @@ -27,8 +27,6 @@ namespace fs = std::filesystem; namespace ui { -const std::filesystem::path splash_dot_bmp{u"/splash.bmp"}; - ScreenshotViewer::ScreenshotViewer( NavigationView& nav, const std::filesystem::path& path) @@ -44,7 +42,6 @@ bool ScreenshotViewer::on_key(KeyEvent) { void ScreenshotViewer::paint(Painter& painter) { File file{}; - painter.fill_rectangle({0, 0, screen_width, screen_height}, Color::black()); auto show_invalid = [&]() { diff --git a/firmware/application/apps/ui_ss_viewer.hpp b/firmware/application/apps/ui_ss_viewer.hpp index 852513256..d35ce0708 100644 --- a/firmware/application/apps/ui_ss_viewer.hpp +++ b/firmware/application/apps/ui_ss_viewer.hpp @@ -28,11 +28,10 @@ #include "ui_painter.hpp" #include "ui_widget.hpp" #include "file.hpp" +#include "file_path.hpp" namespace ui { -extern const std::filesystem::path splash_dot_bmp; - class ScreenshotViewer : public View { public: ScreenshotViewer(NavigationView& nav, const std::filesystem::path& path); @@ -49,7 +48,6 @@ class SplashViewer : public View { SplashViewer(NavigationView& nav, const std::filesystem::path& path); bool on_key(KeyEvent key) override; void paint(Painter& painter) override; - void update_ss(void); private: NavigationView& nav_; diff --git a/firmware/application/apps/ui_standalone_view.cpp b/firmware/application/apps/ui_standalone_view.cpp index 33ab46486..540a18396 100644 --- a/firmware/application/apps/ui_standalone_view.cpp +++ b/firmware/application/apps/ui_standalone_view.cpp @@ -34,7 +34,7 @@ namespace ui { void create_thread(int32_t (*fn)(void*), void* arg, size_t stack_size, int priority) { - // TODO: collect memory on terminate, once this is used + // TODO: collect memory on terminate, once this is used. This is a HUGE TODO! need to call chThdWait on all of them! chThdCreateFromHeap(NULL, stack_size, priority, fn, arg); } diff --git a/firmware/application/apps/ui_subghzd.cpp b/firmware/application/apps/ui_subghzd.cpp index 6cd6318c5..027e39124 100644 --- a/firmware/application/apps/ui_subghzd.cpp +++ b/firmware/application/apps/ui_subghzd.cpp @@ -25,6 +25,10 @@ #include "baseband_api.hpp" #include "string_format.hpp" #include "file_path.hpp" +#include "ui_textentry.hpp" +#include "../keeloq_keystore.hpp" +#include "../keeloq_file.hpp" +#include "../keeloq_common.hpp" #include "portapack_persistent_memory.hpp" using namespace portapack; @@ -56,6 +60,51 @@ void SubGhzDRecentEntryDetailView::update_data() { if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt)); if (entry_.data != 0) console.writeln("Data: " + to_string_hex(entry_.data)); + + if (entry_.sensorType == FPS_RESTAURANT_PAGER) { + uint8_t pager_addr = (entry_.data >> 5) & 0x0F; + uint8_t func_code = (entry_.data >> 1) & 0x0F; + console.writeln("Station: 0x" + to_string_hex(serial) + " (" + to_string_dec_uint(serial) + ")"); + console.writeln("Pager: " + to_string_dec_uint(pager_addr)); + if (func_code == 0x0D) + console.writeln("Action: Buzz"); + else if (func_code == 0x0F) + console.writeln("Action: Sync"); + else + console.writeln("Action: 0x" + to_string_hex(func_code)); + } + + if (entry_.sensorType == FPS_KEELOQ) { + console.writeln("Fix: " + to_string_hex(fix)); + console.writeln("Encrypted: " + to_string_hex(encrypted)); + console.writeln("Manufacturer: " + mf_name); + + if (hop != SD_NO_HOP) { + console.writeln("Hop: " + to_string_hex(hop)); + + add_children({&button_save}); + + button_save.on_select = [this](const Button&) { + keeloq_file_buffer.clear(); + + text_prompt( + nav_, + keeloq_file_buffer, + 64, + ENTER_KEYBOARD_MODE_ALPHA, + [this](std::string& buffer) { + KeeloqData params{ + mf_name, + serial, + (uint16_t)cnt, + btn}; + + ensure_directory(keeloq_remotes_dir); + write_keeloq_file(keeloq_remotes_dir / buffer + ".KEELOQ", params); + }); + }; + } + } } SubGhzDRecentEntryDetailView::SubGhzDRecentEntryDetailView(NavigationView& nav, const SubGhzDRecentEntry& entry) @@ -242,6 +291,10 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) { return "GangQi"; case FPS_MARANTEC24: return "Marantec24"; + case FPS_HOLTEKHT6P20B: + return "Holtek HT6P20B"; + case FPS_RESTAURANT_PAGER: + return "Rest. Pager"; case FPS_Invalid: default: return "Unknown"; @@ -268,7 +321,14 @@ void RecentEntriesTable::draw( line.reserve(30); line = SubGhzDView::getSensorTypeName((FPROTO_SUBGHZD_SENSOR)entry.sensorType); - line = line + " " + to_string_hex(entry.data << 32); + if (entry.sensorType == FPS_RESTAURANT_PAGER) { + uint8_t pgr = (entry.data >> 5) & 0x0F; + uint8_t func = (entry.data >> 1) & 0x0F; + line += " P:" + to_string_dec_uint(pgr) + (func == 0x0D ? " Buzz" : func == 0x0F ? " Sync" + : ""); + } else { + line = line + " " + to_string_hex(entry.data << 32); + } line.resize(columns.at(0).second, ' '); std::string ageStr = to_string_dec_uint(entry.age); std::string bitsStr = to_string_dec_uint(entry.bits); @@ -301,6 +361,29 @@ void atomo_decrypt(uint8_t* buff) { } } +bool SubGhzDRecentEntryDetailView::keeloq_check_decrypt(uint32_t decrypt) { + uint16_t end_serial = serial & 0xFF; + + if ((decrypt >> 28 == btn) && (((((uint16_t)(decrypt >> 16)) & 0xFF) == end_serial) || + ((((uint16_t)(decrypt >> 16)) & 0xFF) == 0))) { + cnt = decrypt & 0xFFFF; + + return true; + } + + return false; +} + +bool SubGhzDRecentEntryDetailView::keeloq_check_decrypt_centurion(uint32_t decrypt) { + if ((decrypt >> 28 == btn) && ((((uint16_t)(decrypt >> 16)) & 0x3FF) == 0x1CE)) { + cnt = decrypt & 0xFFFF; + + return true; + } + + return false; +} + const uint32_t came_twee_magic_numbers_xor[15] = { 0x0E0E0E00, 0x1D1D1D11, @@ -589,7 +672,57 @@ void SubGhzDRecentEntryDetailView::parseProtocol() { } if (entry_.sensorType == FPS_KEELOQ) { - // too many sub protocol versions, skipping. maybe in future when we'll have much more fw space + uint64_t data_rev = FProtoGeneral::subghz_protocol_blocks_reverse_key(entry_.data, 64); + + btn = data_rev >> 60; + serial = (data_rev >> 32) & 0xFFFFFFF; + fix = data_rev >> 32; + encrypted = data_rev & 0xFFFFFFFF; + + KeeloqKeystore keystore{}; + + const auto& keys = keystore.get_keys(); + + if (keys.empty()) { + return; + } + + for (const auto& key : keys) { + switch (key.type) { + case KEELOQ_SIMPLE_LEARNING: { + uint32_t decrypted = keeloq_decrypt(encrypted, key.key); + + if (keeloq_check_decrypt(decrypted)) { + mf_name = key.mf_name; + hop = decrypted; + + return; + } + + break; + } + + case KEELOQ_NORMAL_LEARNING: { + uint64_t man = keeloq_normal_learning(fix, key.key); + uint32_t decrypted = keeloq_decrypt(encrypted, man); + + if (key.mf_name == "Centurion" && keeloq_check_decrypt_centurion(decrypted)) { + mf_name = "Centurion"; + hop = decrypted; + + return; + } else if (keeloq_check_decrypt(decrypted)) { + mf_name = key.mf_name; + hop = decrypted; + + return; + } + + break; + } + } + } + return; } @@ -756,5 +889,18 @@ void SubGhzDRecentEntryDetailView::parseProtocol() { btn = entry_.data & 0xf; return; } + + if (entry_.sensorType == FPS_HOLTEKHT6P20B) { + serial = entry_.data >> 8; + btn = (entry_.data >> 4) & 0xF; + return; + } + + if (entry_.sensorType == FPS_RESTAURANT_PAGER) { + // 25-bit EV1527-variant: [sysid:16][pager:4][func:4][stop:1] + serial = (entry_.data >> 9) & 0xFFFF; // System ID + // btn/cnt left as SD_NO values; friendly output in update_data() + return; + } } -} // namespace ui \ No newline at end of file +} // namespace ui diff --git a/firmware/application/apps/ui_subghzd.hpp b/firmware/application/apps/ui_subghzd.hpp index 0c0ed8a3f..cbf5f34e5 100644 --- a/firmware/application/apps/ui_subghzd.hpp +++ b/firmware/application/apps/ui_subghzd.hpp @@ -25,7 +25,11 @@ #define SD_NO_SERIAL 0xFFFFFFFF #define SD_NO_BTN 0xFF -#define SD_NO_CNT 0xFF +#define SD_NO_CNT 0xFFFFFFFF + +#define SD_NO_HOP 0xFFFFFFFF +#define SD_NO_FIX 0xFFFFFFFF +#define SD_NO_ENCRYPTED 0xFFFFFFFF #include "ui.hpp" #include "ui_navigation.hpp" @@ -191,6 +195,19 @@ class SubGhzDRecentEntryDetailView : public View { uint32_t cnt = SD_NO_CNT; uint32_t seed = 0; + // keeloq specific + std::string keeloq_file_buffer{}; + + uint32_t hop = SD_NO_HOP; + uint32_t fix = SD_NO_FIX; + uint32_t encrypted = SD_NO_ENCRYPTED; + + std::string mf_name = "Unknown"; + + bool keeloq_check_decrypt(uint32_t decrypt); + bool keeloq_check_decrypt_centurion(uint32_t decrypt); + // end keeloq specific + Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"}; Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"}; @@ -207,6 +224,10 @@ class SubGhzDRecentEntryDetailView : public View { {screen_width - 96 - 4, screen_height - 32 - 12, 96, 32}, "Done"}; + Button button_save{ + {4, screen_height - 32 - 12, 96, 32}, + "Save"}; + void parseProtocol(); }; diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 7152b7430..42a172e7b 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -21,6 +21,7 @@ */ #include "baseband_api.hpp" +#include #include "audio.hpp" #include "tonesets.hpp" @@ -52,12 +53,21 @@ static void send_message(const Message* const message) { creg::m0apptxevent::assert_event(); if constexpr (check_for_message_hang) { +#ifdef PRALINE + /* Timeout: ~3 seconds at typical clock speeds */ + auto count = 200'000'000u; +#else auto count = UINT32_MAX; +#endif while (shared_memory.baseband_message && --count) /* spin */; if (count == 0) +#ifdef PRALINE + chDbgPanic("BB Msg Timeout"); +#else chDbgPanic("Baseband Send Fail"); +#endif } else { while (shared_memory.baseband_message) /* spin */; @@ -325,12 +335,22 @@ void set_rds_data(const uint16_t message_length) { send_message(&message); } -void set_spectrum(const size_t sampling_rate, const size_t trigger) { +void set_spectrum( + const size_t sampling_rate, + const size_t trigger) { const WidebandSpectrumConfigMessage message{ sampling_rate, trigger}; send_message(&message); } +void set_time_sink( + const size_t sampling_rate, + const size_t trigger) { + const TimeSinkConfigMessage message{ + sampling_rate, trigger}; + send_message(&message); +} + void set_wefax_config(uint8_t lpm = 120, uint8_t ioc = 0) { const WeFaxRxConfigureMessage message{lpm, ioc}; send_message(&message); @@ -368,6 +388,54 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) { send_message(&message); } +void set_moreserx_config(uint8_t mode) { + const MorseRXConfigureMessage message{mode}; + send_message(&message); +} + +void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta) { + const MorseTXConfigureMessage message{mode, tone, fm_delta}; + send_message(&message); +} + +void set_morsetx_key(bool key_down) { + const MorseTXkeyMessage message{key_down}; + send_message(&message); +} + +void set_bitstream_config(uint32_t deviation, uint8_t mode) { + const StreamTXConfigurationMessage message{deviation, mode}; + send_message(&message); +} + +void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload, uint16_t payload_length) { + RTTYDataMessage message{baud, shift}; + if (payload && payload_length > 0) { + message.data_len = payload_length > message.max_len ? message.max_len : payload_length; + for (size_t i = 0; i < message.data_len; ++i) { + message.data[i] = payload[i]; + } + } + send_message(&message); +} + +void set_rtty_config(RTTYDataMessage& message) { + send_message(&message); +} + +void set_epirb_tx_config(EPIRBTXDataMessage& message) { + send_message(&message); +} + +void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) { + const size_t max_len = sizeof(shared_memory.bb_data.data); + if (frame_length > max_len) frame_length = max_len; + memcpy(shared_memory.bb_data.data, dibits, frame_length); + P25TxConfigureMessage msg{}; + msg.frame_length = frame_length; + send_message(&msg); +} + static bool baseband_image_running = false; void run_image(const spi_flash::image_tag_t image_tag) { @@ -429,7 +497,12 @@ void shutdown() { send_message(&message); shared_memory.application_queue.reset(); - + // Allow time for the shutdown message to be processed and for the baseband + // core to stop before starting another image. Otherwise, the M4 may still be + // running and cause a crash when the next image is started. +#ifdef PRALINE + chThdSleepMilliseconds(20); +#endif baseband_image_running = false; } diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 86d06604c..95a6b6913 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -97,14 +97,27 @@ void set_pocsag(int8_t baud_config = -1); void set_adsb(); void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed); void set_rds_data(const uint16_t message_length); -void set_spectrum(const size_t sampling_rate, const size_t trigger); +void set_spectrum( + const size_t sampling_rate, + const size_t trigger); +void set_time_sink( + const size_t sampling_rate, + const size_t trigger); void set_siggen_tone(const uint32_t tone); void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration); void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw); void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate); +void set_moreserx_config(uint8_t mode); +void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta); +void set_morsetx_key(bool key_down); void set_wefax_config(uint8_t lpm, uint8_t ioc); void set_noaaapt_config(); void set_flex_config(); +void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk +void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100 +void set_rtty_config(RTTYDataMessage& message); +void set_epirb_tx_config(EPIRBTXDataMessage& message); +void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length); void request_roger_beep(); void request_rssi_beep(); diff --git a/firmware/application/bitmap.hpp b/firmware/application/bitmap.hpp index 417cfdac9..2ae27c494 100644 --- a/firmware/application/bitmap.hpp +++ b/firmware/application/bitmap.hpp @@ -4297,7 +4297,7 @@ static constexpr Bitmap bitmap_icon_tpms{ {16, 16}, bitmap_icon_tpms_data}; -static constexpr uint8_t bitmap_icon_tranceivers_data[] = { +static constexpr uint8_t bitmap_icon_transceivers_data[] = { 0x80, 0x01, 0xC0, @@ -4331,9 +4331,9 @@ static constexpr uint8_t bitmap_icon_tranceivers_data[] = { 0xFF, 0xFF, }; -static constexpr Bitmap bitmap_icon_tranceivers{ +static constexpr Bitmap bitmap_icon_transceivers{ {16, 16}, - bitmap_icon_tranceivers_data}; + bitmap_icon_transceivers_data}; static constexpr uint8_t bitmap_icon_transmit_data[] = { 0x80, diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp index 391a687d1..063492706 100644 --- a/firmware/application/clock_manager.cpp +++ b/firmware/application/clock_manager.cpp @@ -23,6 +23,7 @@ #include "portapack_persistent_memory.hpp" #include "portapack_io.hpp" +#include "portapack.hpp" #include "hackrf_hal.hpp" using namespace hackrf::one; @@ -30,8 +31,20 @@ using namespace hackrf::one; #include "lpc43xx_cpp.hpp" using namespace lpc43xx; +#ifdef PRALINE +extern "C" { +#include "fpga_bridge.h" +} +// Need access to ssp1_arbiter from radio namespace for FPGA related radio method dependencies. +#include "radio.hpp" +#endif + constexpr uint32_t si5351_vco_f = 800000000; +#ifdef PRALINE +constexpr uint32_t si5351_vco_afe_f = 800000000; // If necessary may be changed to 768 MHz for optimal for 3.072 MHz sample frequencies commonly used by apps +#endif + constexpr si5351::Inputs si5351_inputs{ .f_xtal = si5351_xtal_f, .f_clkin = si5351_clkin_f, @@ -53,7 +66,49 @@ constexpr si5351::PLL si5351_pll_xtal_25m{ .b = 0, .c = 1, }; + +#ifdef PRALINE +// Define pll_a from 25MHz clock for stable PLL A, and AFE locked reference +// PLL A: 800 MHz VCO (32x Multiplier for jitter-free 3.072 MHz sampling) +constexpr si5351::PLL si5351_pll_a_afe_800m{ + .f_in = si5351_inputs.f_xtal, // 25,000,000 Hz + .a = 32, // Multiplier: 25 * 32 = 800 + .b = 0, + .c = 1, +}; +// PLL A: registers (Base 34) 800 MHz VCO (For jitter-free AFE sampling frequencies) +constexpr auto si5351_pll_a_800_reg = si5351_pll_a_afe_800m.reg(0); // Base 26 + +static_assert(si5351_pll_a_afe_800m.f_vco() == si5351_vco_f, "PLL A XTAL frequency wrong"); +static_assert(si5351_pll_a_afe_800m.p1() == 3584, "PLL A XTAL P1 wrong"); +static_assert(si5351_pll_a_afe_800m.p2() == 0, "PLL A XTAL P2 wrong"); +static_assert(si5351_pll_a_afe_800m.p3() == 1, "PLL A XTAL P3 wrong"); + +// Define pll_b 25MHz clock for stable PLL B +// PLL B: 800 MHz VCO (32x Multiplier for (For stable Digital/SGPIO bus) +constexpr si5351::PLL si5351_pll_b_800m{ + .f_in = si5351_inputs.f_xtal, // 25,000,000 Hz + .a = 32, // Multiplier: 25 * 32 = 800 + .b = 0, + .c = 1, +}; +// PLL B: registers (Base 34) 800 MHz VCO (For stable Digital/SGPIO bus) +constexpr auto si5351_pll_b_800_reg = si5351_pll_b_800m.reg(1); // Base 34 + +static_assert(si5351_pll_b_800m.f_vco() == si5351_vco_f, "PLL B XTAL frequency wrong"); +static_assert(si5351_pll_b_800m.p1() == 3584, "PLL B XTAL P1 wrong"); +static_assert(si5351_pll_b_800m.p2() == 0, "PLL B XTAL P2 wrong"); +static_assert(si5351_pll_b_800m.p3() == 1, "PLL B XTAL P3 wrong"); + +#else + +// PLL A registers (Base 26) constexpr auto si5351_pll_a_xtal_reg = si5351_pll_xtal_25m.reg(0); +static_assert(si5351_pll_xtal_25m.f_vco() == si5351_vco_f, "PLL XTAL frequency wrong"); +static_assert(si5351_pll_xtal_25m.p1() == 3584, "PLL XTAL P1 wrong"); +static_assert(si5351_pll_xtal_25m.p2() == 0, "PLL XTAL P2 wrong"); +static_assert(si5351_pll_xtal_25m.p3() == 1, "PLL XTAL P3 wrong"); +#endif constexpr si5351::PLL si5351_pll_clkin_10m{ .f_in = si5351_inputs.f_clkin_out(), @@ -64,11 +119,6 @@ constexpr si5351::PLL si5351_pll_clkin_10m{ constexpr auto si5351c_pll_b_clkin_reg = si5351_pll_clkin_10m.reg(1); constexpr auto si5351a_pll_a_clkin_reg = si5351_pll_clkin_10m.reg(0); -static_assert(si5351_pll_xtal_25m.f_vco() == si5351_vco_f, "PLL XTAL frequency wrong"); -static_assert(si5351_pll_xtal_25m.p1() == 3584, "PLL XTAL P1 wrong"); -static_assert(si5351_pll_xtal_25m.p2() == 0, "PLL XTAL P2 wrong"); -static_assert(si5351_pll_xtal_25m.p3() == 1, "PLL XTAL P3 wrong"); - static_assert(si5351_pll_clkin_10m.f_vco() == si5351_vco_f, "PLL CLKIN frequency wrong"); static_assert(si5351_pll_clkin_10m.p1() == 9728, "PLL CLKIN P1 wrong"); static_assert(si5351_pll_clkin_10m.p2() == 0, "PLL CLKIN P2 wrong"); @@ -82,16 +132,16 @@ constexpr si5351::MultisynthFractional si5351_ms_18m432 { .r_div = 1, }; */ -/* -constexpr si5351::MultisynthFractional si5351_ms_0_20m { - .f_src = si5351_vco_f, - .a = 20, - .b = 0, - .c = 1, - .r_div = 1, +// constexpr auto si5351_ms_0_20m_reg = si5351_ms_0_20m.reg(0); + +constexpr si5351::MultisynthFractional si5351_ms_0_4m{ + .f_src = si5351_vco_f, // 800,000,000 Hz + .a = 100, // Integer divider 100 + .b = 0, + .c = 1, + .r_div = 1 // Final R-divider: 2^1 = 2 }; -constexpr auto si5351_ms_0_20m_reg = si5351_ms_0_20m.reg(0); -*/ + constexpr si5351::MultisynthFractional si5351_ms_0_8m{ .f_src = si5351_vco_f, .a = 50, @@ -99,8 +149,91 @@ constexpr si5351::MultisynthFractional si5351_ms_0_8m{ .c = 1, .r_div = 1, }; + +#ifdef PRALINE +// (initial 4 MHz from 800 MHz VCO: 800 / 200 = 4) +constexpr si5351::MultisynthFractional si5351_ms_afe_4m{ + .f_src = si5351_vco_afe_f, // 800 MHz + .a = 200, + .b = 0, + .c = 1, + .r_div = 0}; + +// (10 MHz from 800 MHz VCO: 800 / 80 = 10) +constexpr si5351::MultisynthFractional si5351_ms_afe_10m{ + .f_src = si5351_vco_afe_f, // 800 MHz + .a = 80, + .b = 0, + .c = 1, + .r_div = 0}; + +// (20 MHz from 800 MHz VCO: 800 / 40 = 20) +constexpr si5351::MultisynthFractional si5351_ms_afe_20m{ + .f_src = si5351_vco_afe_f, + .a = 40, + .b = 0, + .c = 1, + .r_div = 0, +}; + +// (25 MHz from 800 MHz VCO: 800 / 32 = 25) +// Define xtal MultiSynth 25MHz clock for stable PLL A, and AFE locked XTAL reference +constexpr si5351::MultisynthFractional si5351_ms_afe_25m{ + .f_src = si5351_vco_afe_f, // 800,000,000 Hz + .a = 32, // 800 / 32 = 25 MHz + .b = 0, + .c = 1, + .r_div = 0 // No final bit-shifting +}; + +// (40 MHz from 800 MHz VCO: 800 / 20 = 40) +constexpr si5351::MultisynthFractional si5351_ms_afe_40m{ + .f_src = si5351_vco_afe_f, // 800 MHz + .a = 20, + .b = 0, + .c = 1, + .r_div = 0}; + +// (20 MHz from 800 MHz VCO: 800 / 40 = 20) +constexpr si5351::MultisynthFractional si5351_ms_20m{ + .f_src = si5351_vco_f, + .a = 40, + .b = 0, + .c = 1, + .r_div = 0, +}; +// constexpr auto si5351_ms_20m_reg = si5351_ms_20m.reg(0); + +// (25 MHz from 800 MHz VCO: 800 / 32 = 25) +// Define xtal MultiSynth 25MHz clock for stable PLL B, and XTAL reference +constexpr si5351::MultisynthFractional si5351_ms_25m{ + .f_src = si5351_vco_f, // 800,000,000 Hz + .a = 32, // 800 / 32 = 25 MHz + .b = 0, + .c = 1, + .r_div = 0 // No final bit-shifting +}; + +#endif + constexpr auto si5351c_ms_0_8m_reg = si5351_ms_0_8m.reg(clock_generator_output_og_codec); +#ifdef PRALINE +// Verify compile-time values for 8 MHz config +static_assert(si5351_ms_0_8m.p1() == 5888, "MS0 8MHz P1 should be 5888 (0x1700)"); +static_assert(si5351_ms_0_8m.p2() == 0, "MS0 8MHz P2 should be 0"); +static_assert(si5351_ms_0_8m.p3() == 1, "MS0 8MHz P3 should be 1"); +static_assert(si5351_ms_0_8m.f_out() == 8000000, "MS0 should output 8 MHz"); + +// Verify register array encoding +static_assert(si5351c_ms_0_8m_reg[0] == 42, "MS0 base register should be 42"); +static_assert(si5351c_ms_0_8m_reg[1] == 0x00, "MS0 reg43 P3[15:8] should be 0x00"); +static_assert(si5351c_ms_0_8m_reg[2] == 0x01, "MS0 reg44 P3[7:0] should be 0x01"); +static_assert(si5351c_ms_0_8m_reg[3] == 0x10, "MS0 reg45 R_DIV should be 0x10"); +static_assert(si5351c_ms_0_8m_reg[4] == 0x17, "MS0 reg46 P1[15:8] should be 0x17"); +static_assert(si5351c_ms_0_8m_reg[5] == 0x00, "MS0 reg47 P1[7:0] should be 0x00"); +#endif + constexpr si5351::MultisynthFractional si5351_ms_group{ .f_src = si5351_vco_f, .a = 80, /* Don't care */ @@ -186,12 +319,21 @@ static_assert(si5351a_ms_int_mcu_clkin.f_out() == mcu_clkin_r9_f, "MS int MCU CL using namespace si5351; +#ifdef PRALINE +static constexpr ClockControl::MultiSynthSource get_si5351a_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) { + return (reference_source == ClockManager::ReferenceSource::Xtal) + ? ClockControl::MultiSynthSource::PLLA + : ClockControl::MultiSynthSource::PLLB; +} +#else static constexpr ClockControl::MultiSynthSource get_si5351c_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) { return (reference_source == ClockManager::ReferenceSource::Xtal) ? ClockControl::MultiSynthSource::PLLA : ClockControl::MultiSynthSource::PLLB; } +#endif +#ifndef PRALINE constexpr ClockControls si5351c_clock_control_common{{ {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Group, ClockControl::ClockInvert::Invert, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, @@ -202,17 +344,43 @@ constexpr ClockControls si5351c_clock_control_common{{ {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, }}; +#endif constexpr ClockControls si5351a_clock_control_common{{ +#ifdef PRALINE + // CLK0: MAX5864 (ADC) - 4mA, Normal PLLA Integer + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK1: SCT_CLK (iCE40 FPGA) - 4mA, Normal PLLB Integer + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK2: LPC43xx MCU - 2mA, Normal PLLA (Must be Integer for MCU stability) + {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK3: CLKOUT SMA Port P1 - 2mA, Normal PLLB Integer Power_Off + {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK4: MAX2831 reference (40 MHz) - 4mA, Invert PLLA Integer (Required for mixer lock) + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK5: RFFC5072 reference (40 MHz) - 4mA, Invert PLLA Integer (Required for mixer lock) + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK6: Not used (disabled) 2mA, Normal PLLB Integer, Power_Off + {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK7: Not used (disabled) 2mA, Normal PLLB Integer, Power_Off + {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, +#else {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK4: HackRF r9 - not inverted {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK5: HackRF r9 - not used {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK6: Not used {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK7: Not used {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, +#endif + }}; + ClockManager::Reference ClockManager::get_reference() const { return reference; } @@ -255,9 +423,48 @@ static void portapack_tcxo_disable() { using namespace hackrf::one; void ClockManager::init_clock_generator() { +#ifdef PRALINE + // PRALINE: Configure clock input mux GPIO + // GPIO0_15 (clkin_ctrl) selects GP_CLKIN source: + // 0 = P1 connector (external) + // 1 = P22 (internal Si5351 CLK2) + constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; + gpio_clkin_ctrl.output(); + gpio_clkin_ctrl.write(1); // CLKIN_SIGNAL_P22 = 1 = internal Si5351 CLK2 + + // Also enable MCU clock gate (GPIO0_8) + gpio_r9_mcu_clk_en.output(); + gpio_r9_mcu_clk_en.write(1); +#else + // HackRF One r9: GPIO0_8 (mcu_clk_en) gates Si5351 CLK2/CLK7 to GP_CLKIN + if (hackrf_r9) { + gpio_r9_mcu_clk_en.output(); + gpio_r9_mcu_clk_en.write(1); + } +#endif + clock_generator.reset(); clock_generator.set_crystal_internal_load_capacitance(CrystalInternalLoadCapacitance::XTAL_CL_8pF); clock_generator.enable_fanout(); + +#ifdef PRALINE + /* PRALINE has Si5351A (NOT Si5351C like HackRF One OG). + * Must use Si5351A configuration: PLLA only, no CLKIN support. + * + * IMPORTANT: Follow HackRF reference sequence: + * 1. Set PLL input sources + * 2. Configure PLL and multisynths + * 3. Set clock control registers (AFTER multisynths!) + * 4. Reset PLLs + * 5. Enable outputs + */ + clock_generator.set_pll_input_sources(si5351a_pll_input_sources); + + /* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */ + reference = Reference{ReferenceSource::Xtal, 0}; // PLLA + + /* Clock control will be set AFTER multisynth configuration - see below */ +#else clock_generator.set_pll_input_sources(hackrf_r9 ? si5351a_pll_input_sources : si5351c_pll_input_sources); @@ -298,7 +505,90 @@ void ClockManager::init_clock_generator() { si5351_clock_control_common[7].ms_src(ref_pll), }}; clock_generator.set_clock_control(si5351_clock_control); +#endif +#ifdef PRALINE + + /* * Praline HackRF Pro Clock Assignments (800 MHz VCO Configuration) + * VCO Frequency: 800,000,000 Hz (Master Reference) + * * CLK0: AFE_CLK (MAX5864 Codec & FPGA ADC Interface) + * - Note: Defines hardware sample rate. Essential for WFM purity. + * * CLK1: SCT_CLK (iCE40 FPGA System/Timing Clock) + * - Note: Timing for SGPIO data bus; scales to 2x SR in wideband modes. + * * CLK2: MCU_CLKIN (LPC43xx MCU External Clock Input) + * - Note: Synchronizes MCU processing to the RF clock tree. + * * CLK3: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 1 + * - Note: Used for internal FPGA logic/gateware synchronization. + * * CLK4: P_CLK (MAX2831 Peripheral/Expansion Clock) + * - Note: Routed to expansion headers for external hardware sync. + * * CLK5: AUX_CLK (RFFC5371 Auxiliary reference for secondary logic) + * - Note: Provides additional timing flexibility for the iCE40 FPGA. + * * CLK6: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 2 + * - Note: Used for internal FPGA logic/gateware synchronization. + * * CLK7: Unused / Power-Down + * - State: Disabled (si5351a_ms6_7_off_reg) + * - Note: Kept OFF to reduce EMI/RFI near the RF front-end. + * * CLKOUT: Optional external clock output on the header. + */ + + /* Write PLL A (800 MHz based on 25 MHz xtal for RF) and + * PLL B (800 MHZ based on 25 MHz xtalfor Digital) + * Use single-byte writes to debug I2C issues + */ + { + // Write PLLA (Registers 26-33) + /* Write PLL A configuration (Base 26) */ + const auto& pll_a = si5351_pll_a_800_reg; + for (size_t i = 1; i < pll_a.size(); i++) { + clock_generator.write_register(pll_a[0] + i - 1, pll_a[i]); + } + + // Write PLLB (Registers 34-41) + /* Write PLL B configuration (Base 34) */ + const auto& pll_b = si5351_pll_b_800_reg; + for (size_t i = 1; i < pll_b.size(); i++) { + clock_generator.write_register(pll_b[0] + i - 1, pll_b[i]); + } + } + + /* Write multisynth configurations using single-byte writes */ + // These cover all active channels on the Praline board + clock_generator.write_ms_single_byte(0, si5351_ms_afe_4m); // CLK0: PLL A AFE Codec (4 MHz) + clock_generator.write_ms_single_byte(1, si5351_ms_10m); // CLK1: PLL B SGPIO/FPGA Timing (10 MHz) + clock_generator.write_ms_single_byte(2, si5351_ms_afe_40m); // CLK2: PLL A Audio and MCU Input (40 MHz) + clock_generator.write_ms_single_byte(3, si5351_ms_0_4m); // CLK3: PLL B SMA Port 1 Logic Sync (4 MHz or 10 MHz) + clock_generator.write_ms_single_byte(4, si5351_ms_afe_40m); // CLK4: PLL A MAX2831 Second IF (40 MHz) + clock_generator.write_ms_single_byte(5, si5351_ms_afe_40m); // CLK5: PLL A RFFC5071First IF (40 MHz) + clock_generator.write_ms_single_byte(6, si5351_ms_0_4m); // CLK6: PLL B SMA Port 2 Logic Sync (4 MHz or 10 MHz) + clock_generator.write_ms_single_byte(7, si5351_ms_0_4m); // CLK7: PLL B Unused (4 MHz) + + /* NOW set clock control registers (AFTER multisynths per HackRF reference) */ + const auto ref_pll_a = ClockControl::MultiSynthSource::PLLA; + const auto ref_pll_b = ClockControl::MultiSynthSource::PLLB; + const ClockControls si5351_clock_control = ClockControls{{ + si5351a_clock_control_common[0].ms_src(ref_pll_a), + si5351a_clock_control_common[1].ms_src(ref_pll_b), + si5351a_clock_control_common[2].ms_src(ref_pll_a), + si5351a_clock_control_common[3].ms_src(ref_pll_b), + si5351a_clock_control_common[4].ms_src(ref_pll_a), + si5351a_clock_control_common[5].ms_src(ref_pll_a), + si5351a_clock_control_common[6].ms_src(ref_pll_b), + si5351a_clock_control_common[7].ms_src(ref_pll_b), + }}; + // clock_generator.set_clock_control(si5351_clock_control); + // Use single-byte writes instead of multi-byte + clock_generator.set_clock_control_single_byte(si5351_clock_control); + + // Don't write CLKS 3, 6, and 7 multisynth + // Ensure CLK3 clock control has Power_Off + // Verify output is disabled + clock_generator.disable_output(3); + clock_generator.disable_clock(3); + clock_generator.disable_output(6); + clock_generator.disable_clock(6); + clock_generator.disable_output(7); + clock_generator.disable_clock(7); +#else if (hackrf_r9) { const PLLReg pll_reg = (reference.source == ReferenceSource::Xtal) ? si5351_pll_a_xtal_reg @@ -319,21 +609,37 @@ void ClockManager::init_clock_generator() { clock_generator.write(si5351c_ms_5_reg); clock_generator.write(si5351c_ms6_7_off_mcu_clkin_reg); } +#endif clock_generator.reset_plls(); // Wait for PLL(s) to lock. +#ifdef PRALINE + // PRALINE: Wait for 0x60 (0x20 | 0x40), PLLA and PLLB to lock (0x20 = LOL_A bit, 0x40 = LOL_B bit) + uint8_t device_status_mask = 0x60; + uint32_t pll_timeout = 100000; + while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) { + pll_timeout--; + } + // Store PLL lock status for debugging + static volatile uint32_t pll_lock_timeout = pll_timeout; + (void)pll_lock_timeout; + +#else + // Wait for PLL(s) to lock - with timeout to prevent hang uint8_t device_status_mask = hackrf_r9 ? 0x20 : (ref_pll == ClockControl::MultiSynthSource::PLLB) ? 0x40 : 0x20; + while ((clock_generator.device_status() & device_status_mask) != 0); clock_generator.set_clock_control( clock_generator_output_mcu_clkin, si5351_clock_control_common[clock_generator_output_mcu_clkin].ms_src(ref_pll).clk_pdn(ClockControl::ClockPowerDown::Power_On)); clock_generator.enable_output(clock_generator_output_mcu_clkin); +#endif } uint32_t ClockManager::measure_gp_clkin_frequency() { @@ -344,12 +650,16 @@ uint32_t ClockManager::measure_gp_clkin_frequency() { } bool ClockManager::loss_of_signal() { +#ifdef PRALINE + return clock_generator.clkin_loss_of_signal(); +#else if (hackrf_r9) { const auto frequency = measure_gp_clkin_frequency(); return (frequency < 9850000) || (frequency > 10150000); } else { return clock_generator.clkin_loss_of_signal(); } +#endif } ClockManager::ReferenceSource ClockManager::detect_reference_source() { @@ -372,6 +682,17 @@ ClockManager::ReferenceSource ClockManager::detect_reference_source() { } ClockManager::Reference ClockManager::choose_reference() { +#ifdef PRALINE + const auto detected_reference = detect_reference_source(); + + if ((detected_reference == ReferenceSource::External) || + (detected_reference == ReferenceSource::PortaPack)) { + const auto frequency = measure_gp_clkin_frequency(); + if ((frequency >= 9850000) && (frequency <= 10150000)) { + return {detected_reference, 10000000}; + } + } +#else if (hackrf_r9) { gpio_r9_clkin_en.write(1); volatile uint32_t delay = 240000 + 24000; @@ -390,6 +711,7 @@ ClockManager::Reference ClockManager::choose_reference() { if (hackrf_r9) { gpio_r9_clkin_en.write(0); } +#endif portapack_tcxo_disable(); return {ReferenceSource::Xtal, 25000000}; @@ -400,6 +722,15 @@ void ClockManager::shutdown() { } void ClockManager::enable_codec_clocks() { +#ifdef PRALINE + /* PRALINE: CLK0 (AFE_CLK) for codec/FPGA, CLK1 (SCT_CLK) for FPGA timing. + * Reference hackrf_core.c shows PRALINE needs both CLK0 and CLK1. */ + clock_generator.enable_clock(clock_generator_output_og_codec); /* CLK0 MAX5864*/ + clock_generator.enable_clock(clock_generator_output_og_cpld); /* CLK1 iCE40 FPGA*/ + clock_generator.enable_clock(clock_generator_output_og_sgpio); /* CLK2 LPC43xx*/ + clock_generator.enable_output_mask( + (1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio)); +#else if (hackrf_r9) { clock_generator.enable_clock(clock_generator_output_r9_sgpio); } else { @@ -417,6 +748,7 @@ void ClockManager::enable_codec_clocks() { clock_generator.enable_output_mask( (1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio)); } +#endif } void ClockManager::disable_codec_clocks() { @@ -424,6 +756,14 @@ void ClockManager::disable_codec_clocks() { * be enabled for the output to come to rest at the state specified by * CLKx_DISABLE_STATE. */ +#ifdef PRALINE + /* PRALINE: CLK0 (AFE_CLK), CLK1 (SCT_CLK), and CLK2 MCU used for codec/FPGA */ + clock_generator.disable_output_mask( + (1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio)); + clock_generator.disable_clock(clock_generator_output_og_codec); + clock_generator.disable_clock(clock_generator_output_og_cpld); + clock_generator.disable_clock(clock_generator_output_og_sgpio); +#else if (hackrf_r9) { clock_generator.disable_output_mask(1U << clock_generator_output_r9_sgpio); clock_generator.disable_clock(clock_generator_output_r9_sgpio); @@ -434,9 +774,17 @@ void ClockManager::disable_codec_clocks() { clock_generator.disable_clock(clock_generator_output_og_cpld); clock_generator.disable_clock(clock_generator_output_og_sgpio); } +#endif } void ClockManager::enable_if_clocks() { +#ifdef PRALINE + /* PRALINE uses CLK5 (first IF) and CLK4 (second IF) */ + clock_generator.enable_clock(clock_generator_output_og_first_if); + clock_generator.enable_output_mask(1U << clock_generator_output_og_first_if); + clock_generator.enable_clock(clock_generator_output_og_second_if); + clock_generator.enable_output_mask(1U << clock_generator_output_og_second_if); +#else if (hackrf_r9) { clock_generator.enable_clock(clock_generator_output_r9_if); clock_generator.enable_output_mask(1U << clock_generator_output_r9_if); @@ -446,9 +794,16 @@ void ClockManager::enable_if_clocks() { clock_generator.enable_clock(clock_generator_output_og_second_if); clock_generator.enable_output_mask(1U << clock_generator_output_og_second_if); } +#endif } void ClockManager::disable_if_clocks() { +#ifdef PRALINE + clock_generator.disable_output_mask(1U << clock_generator_output_og_first_if); + clock_generator.disable_clock(clock_generator_output_og_first_if); + clock_generator.disable_output_mask(1U << clock_generator_output_og_second_if); + clock_generator.disable_clock(clock_generator_output_og_second_if); +#else if (hackrf_r9) { clock_generator.disable_output_mask(1U << clock_generator_output_r9_if); clock_generator.disable_clock(clock_generator_output_r9_if); @@ -458,19 +813,93 @@ void ClockManager::disable_if_clocks() { clock_generator.disable_output_mask(1U << clock_generator_output_og_second_if); clock_generator.disable_clock(clock_generator_output_og_second_if); } +#endif } void ClockManager::set_sampling_frequency(const uint32_t frequency) { +#ifdef PRALINE + /* + * PRALINE sample rate strategy: + * 1. Maximize AFE rate to push Nyquist above MAX2831's 11.6 MHz LPF minimum + * 2. Use FPGA decimation to achieve desired output rate + * 3. Ensure AFE rate is achievable by Si5351 (clean division from 800 MHz VCO) + */ + + constexpr uint32_t MAX_AFE_RATE = 40000000; // Use 40 MHz per GSG reference + constexpr uint8_t MAX_N = 5; + + _base_band_frequency = frequency; + + uint8_t n = 0; + uint32_t afe_rate = frequency; + + // Find the largest n where AFE rate stays within limit + // Start at n=0 and work up + while (n < MAX_N) { + uint32_t next_rate = afe_rate << 1; + if (next_rate > MAX_AFE_RATE) break; + afe_rate = next_rate; + n++; + } + + _resampling_n = n; + + // === Stop FPGA processing and flush filters === + fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators) + + // Verify we're in RX mode before writing RX registers + if (fpga_get_mode() != FPGA_MODE_RX) { + // Either set mode or return error + fpga_set_mode(FPGA_MODE_RX); + } + + // Set FPGA RX decimation register + fpga_debug_register_write(FPGA_REG_DECIM, n); + + /* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN. + * We shift up by (3 * n) to compensate for CIC bit-growth. + * Relationship: ds = (Stages * n) - Offset + * For a 3-stage filter, every increment of n grows the signal by 3 bits. + * We subtract a baseline offset to keep the signal within 8-bit bounds. + * Add a baseline shift to ensure the signal isn't too quiet + */ + uint8_t ds = (3 * n); + ds += 2; + fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds); + + radio::invalidate_spi_config(); + + // Configure Si5351 clocks + // CLK0: AFE_CLK (with r_div=1 for Γ·2) + // CLK1: SCT_CLK (with r_div=0 for Γ·1, runs at 2Γ— AFE for FPGA timing) + // Configure Si5351 clocks using the correct AFE VCO + clock_generator.set_ms_frequency(0, afe_rate * 2, si5351_vco_afe_f, 1); + clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0); + + // === Reset PLL A for phase alignment === + clock_generator.write_register(si5351::Register::PLLReset, 0x20); + + // Brief delay for PLL lock and clock stability === + // ~1ms at 96MHz = ~96000 cycles, use 10ms for safety + volatile uint32_t delay = 240000; // ~2.5ms + while (delay--); + + // Re-enable FPGA processing with clean state === + fpga_debug_register_write(1, 0x01); + +#else /* Codec clock is at sampling frequency, CPLD and SGPIO clocks are at * twice the frequency, and derived from the MS0 synth. So it's only * necessary to change the MS0 synth frequency, and ensure the output * is divided by two. */ + if (hackrf_r9) { clock_generator.set_ms_frequency(clock_generator_output_r9_sgpio, frequency * 2, si5351_vco_f, 0); } else { clock_generator.set_ms_frequency(clock_generator_output_og_codec, frequency * 2, si5351_vco_f, 1); } +#endif } void ClockManager::set_reference_ppb(const int32_t ppb) { @@ -478,10 +907,21 @@ void ClockManager::set_reference_ppb(const int32_t ppb) { * It is assumed an external clock coming in to CLKIN/PLLB is sufficiently accurate as to not need adjustment. * TODO: Revisit the above policy. It may be good to allow adjustment of the external reference too. */ +#ifdef PRALINE + // On Praline, only apply if we aren't locked to a superior external 10MHz source + // (Assuming you have a way to detect the 10MHz presence on Praline) + if (reference.source == ReferenceSource::External) { + return; + } + constexpr uint32_t pll_multiplier = si5351_pll_a_afe_800m.a; +#else if (hackrf_r9 && reference.source != ReferenceSource::Xtal) { return; } + constexpr uint32_t pll_multiplier = si5351_pll_xtal_25m.a; +#endif + constexpr uint32_t denominator = 1000000 / pll_multiplier; const uint32_t new_a = (ppb >= 0) ? pll_multiplier : (pll_multiplier - 1); const uint32_t new_b = (ppb >= 0) ? (ppb / 1000) : (denominator + (ppb / 1000)); @@ -493,8 +933,13 @@ void ClockManager::set_reference_ppb(const int32_t ppb) { .b = new_b, .c = new_c, }; + +#ifdef PRALINE + clock_generator.write_pll_single_byte(0, pll); +#else const auto pll_a_reg = pll.reg(0); clock_generator.write(pll_a_reg); +#endif } void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel) { @@ -509,8 +954,16 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se } void ClockManager::wait_For_frequency_monitor_measurement_done() { - // FREQ_MON mechanism fails to finish if there's no clock present on selected input?! +// FREQ_MON mechanism fails to finish if there's no clock present on selected input?! +#ifdef PRALINE + // PRALINE FIX: Add timeout to prevent infinite hang + uint32_t timeout = 100000; + while (LPC_CGU->FREQ_MON.MEAS == 1 && timeout > 0) { + timeout--; + } +#else while (LPC_CGU->FREQ_MON.MEAS == 1); +#endif } uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() { @@ -521,6 +974,91 @@ uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() { } void ClockManager::start_audio_pll() { +#ifdef PRALINE + + // Control Block + cgu::pll0audio::ctrl({ + .pd = 1, // Start powered down + .bypass = 0, // Use the PLL + .directi = 0, // Enable N-divider + .directo = 0, // Enable P-divider + .clken = 0, // Disable output initially + .frm = 0, // Normal mode + .autoblock = 1, // Glitchless switching + .pllfract_req = 1, // Integer, Disabled + .sel_ext = 1, // Use GP_CLKIN (CLK2) + .mod_pd = 0, // Modulator OFF + .clk_sel = cgu::CLK_SEL::GP_CLKIN, + }); + + /* + * Audio PLL Configuration for 48 kHz audio with 256Fs MCLK + * Target output: Fout = 12.288 MHz + * + * Formulas: + * Fout = Fin Γ— MSEL / (NSEL Γ— PSEL) + * FCO = 2 Γ— Fin Γ— MSEL / NSEL (must be 275-550 MHz) + */ + + /* + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ 10 MHz INPUT (HackRF r9 compatible but interfere with fm band) β”‚ + * β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ MSEL=3072, NSEL=125, PSEL=20 β”‚ + * β”‚ Fout = 10 Γ— 3072 / (125 Γ— 20) = 30720 / 2500 = 12.288 MHz βœ“ β”‚ + * β”‚ FCO = 2 Γ— 10 Γ— 3072 / 125 = 61440 / 125 = 491.52 MHz βœ“ β”‚ + * β”‚ β”‚ + * β”‚ Encoded values: MDEC=8308, NDEC=45, PDEC=31 β”‚ + * β”‚ CLK2 harmonics: 90, 100, 110 MHz (interfere with FM band!) β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + */ + + /* + // Math: (10 MHz * 3072) / (125 * 20) * 2 = 12.288MHz + cgu::pll0audio::mdiv({.mdec = 8308UL}); // MSEL = 3072 + cgu::pll0audio::np_div({ + .pdec = 31, // PSEL = 20 for 10 MHz + .ndec = 45 // NSEL = 125 for 10 MHz + }); + */ + + /* + * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + * β”‚ 40 MHz INPUT (Recommended - avoids FM band harmonics) β”‚ + * β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + * β”‚ MSEL=768, NSEL=125, PSEL=20 β”‚ + * β”‚ Fout = 40 Γ— 768 / (125 Γ— 20) = 30720 / 2500 = 12.288 MHz βœ“ β”‚ + * β”‚ FCO = 2 Γ— 40 Γ— 768 / 125 = 61440 / 125 = 491.52 MHz βœ“ β”‚ + * β”‚ β”‚ + * β”‚ Encoded values: MDEC=30542, NDEC=45, PDEC=31 β”‚ + * β”‚ CLK2 harmonics: 80, 120, 160 MHz (none in FM 88-108 MHz band) β”‚ + * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + */ + + // 40 MHz input β†’ 12.288 MHz output (same as HackRF OG) + // Math: (40MHz * 768) / (125 * 20) * 2 = 12.288MHz + cgu::pll0audio::mdiv({.mdec = 30542UL}); // MSEL = 768 + cgu::pll0audio::np_div({.pdec = 31, + .ndec = 45}); + + cgu::pll0audio::frac({.pllfract_ctrl = 0}); + cgu::pll0audio::power_up(); + + // Lock and Routing (Keep as is) + { + uint32_t timeout = 100000; + while (!cgu::pll0audio::is_locked() && timeout > 0) { + timeout--; + } + } + + cgu::pll0audio::clock_enable(); + set_base_audio_clock_divider(1); + + LPC_CGU->BASE_AUDIO_CLK.AUTOBLOCK = 1; + LPC_CGU->BASE_AUDIO_CLK.CLK_SEL = toUType(cgu::CLK_SEL::IDIVD); + +#else cgu::pll0audio::ctrl({ .pd = 1, .bypass = 0, @@ -555,13 +1093,15 @@ void ClockManager::start_audio_pll() { }); cgu::pll0audio::power_up(); - while (!cgu::pll0audio::is_locked()); - cgu::pll0audio::clock_enable(); + while (!cgu::pll0audio::is_locked()); + + cgu::pll0audio::clock_enable(); set_base_audio_clock_divider(1); LPC_CGU->BASE_AUDIO_CLK.AUTOBLOCK = 1; LPC_CGU->BASE_AUDIO_CLK.CLK_SEL = toUType(cgu::CLK_SEL::IDIVD); +#endif } void ClockManager::set_base_audio_clock_divider(const size_t divisor) { @@ -570,12 +1110,46 @@ void ClockManager::set_base_audio_clock_divider(const size_t divisor) { } void ClockManager::stop_audio_pll() { +#ifdef PRALINE + /* PRALINE: Gracefully switch audio peripherals away from the PLL branch */ + LPC_CGU->BASE_AUDIO_CLK.PD = 1; // Power down the branch first + LPC_CGU->BASE_AUDIO_CLK.CLK_SEL = toUType(cgu::CLK_SEL::IRC); // Reset to safe IRC source +#endif + cgu::pll0audio::clock_disable(); cgu::pll0audio::power_down(); + +#ifdef PRALINE + /* PRALINE: Add a safety timeout to the unlock check to prevent potential hangs */ + uint32_t timeout = 100000; + while (cgu::pll0audio::is_locked() && timeout > 0) { + timeout--; + } +#else while (cgu::pll0audio::is_locked()); +#endif } void ClockManager::enable_clock_output(bool enable) { +#ifdef PRALINE + auto clkout_select = clock_generator_output_og_clkout; + + if (enable) { + clock_generator.enable_output(clkout_select); + if (portapack::persistent_memory::clkout_freq() < 1000) { + clock_generator.set_ms_frequency(clkout_select, portapack::persistent_memory::clkout_freq() * 128000, si5351_vco_f, 7); + } else { + clock_generator.set_ms_frequency(clkout_select, portapack::persistent_memory::clkout_freq() * 1000, si5351_vco_f, 0); + } + + auto si5351_clock_control_common = si5351a_clock_control_common; + const auto ref_pll = get_si5351a_reference_clock_generator_pll(reference.source); + clock_generator.set_clock_control(clkout_select, si5351_clock_control_common[clkout_select].ms_src(ref_pll).clk_pdn(ClockControl::ClockPowerDown::Power_On)); + } else { + clock_generator.disable_output(clkout_select); + clock_generator.set_clock_control(clkout_select, ClockControl::power_off()); + } +#else if (hackrf_r9) { gpio_r9_clkout_en.output(); gpio_r9_clkout_en.write(enable); @@ -604,4 +1178,5 @@ void ClockManager::enable_clock_output(bool enable) { clock_generator.disable_output(clkout_select); clock_generator.set_clock_control(clkout_select, ClockControl::power_off()); } +#endif } diff --git a/firmware/application/clock_manager.hpp b/firmware/application/clock_manager.hpp index 58d9944b9..6d5f7ba05 100644 --- a/firmware/application/clock_manager.hpp +++ b/firmware/application/clock_manager.hpp @@ -70,8 +70,21 @@ class ClockManager { void set_sampling_frequency(const uint32_t frequency); + uint32_t get_sampling_frequency() const { + return _base_band_frequency; + }; + void set_reference_ppb(const int32_t ppb); +#ifdef PRALINE + uint8_t get_resampling_n() const { return _resampling_n; } + + // Si5351 diagnostic methods + uint8_t si5351_read_status() { return clock_generator.device_status(); } + uint8_t si5351_read_register(uint8_t reg) { return clock_generator.read_register(reg); } + void si5351_write_register(uint8_t reg, uint8_t value) { clock_generator.write_register(reg, value); } +#endif + uint32_t get_frequency_monitor_measurement_in_hertz(); Reference get_reference() const; @@ -85,6 +98,8 @@ class ClockManager { si5351::Si5351& clock_generator; Reference reference; + uint32_t _base_band_frequency{20000000}; + void set_gp_clkin_to_clkin_direct(); void start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel); @@ -99,6 +114,10 @@ class ClockManager { ReferenceSource detect_reference_source(); Reference choose_reference(); bool loss_of_signal(); + +#ifdef PRALINE + uint8_t _resampling_n{0}; // Current decimation factor (log2) +#endif }; #endif /*__CLOCK_MANAGER_H__*/ diff --git a/firmware/application/event_m0.cpp b/firmware/application/event_m0.cpp index d32fa0d3a..58560fbfe 100644 --- a/firmware/application/event_m0.cpp +++ b/firmware/application/event_m0.cpp @@ -256,7 +256,9 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev if (!w->hidden()) { // To achieve reverse depth ordering (last object drawn is // considered "top"), descend first. - for (const auto child : w->children()) { + auto& children = w->children(); + for (auto it = children.rbegin(); it != children.rend(); ++it) { // reverse, bc the lastly added will be "top" if overlaps + const auto& child = *it; const auto touched_widget = touch_widget(child, event); if (touched_widget) { return touched_widget; diff --git a/firmware/application/external/acars_rx/acars_app.cpp b/firmware/application/external/acars_rx/acars_app.cpp index d7b5079d0..c88307198 100644 --- a/firmware/application/external/acars_rx/acars_app.cpp +++ b/firmware/application/external/acars_rx/acars_app.cpp @@ -33,6 +33,76 @@ using namespace portapack; namespace ui::external_app::acars_rx { +// ACARS frame field layout (0-based byte offsets): +// [0] SOH framing byte (skipped) +// [1..7] Aircraft registration (7 chars) +// [8] STX framing byte (skipped) +// [9..10] Label (2 chars) +// [11] Block ID (1 char) +// [12..14] Message number (3 chars) +// [15..20] Flight ID (6 chars) +// [21..N-3] Free-text payload (variable) +// [N-2..N-1] CRC-16/CCITT (2 bytes, MSB first, NOT part of payload) +// +// Minimum valid frame: 21-byte fixed header + 2 CRC bytes = 23 bytes. +// (A zero-length payload is legal per spec; we require at least 23 bytes.) +static constexpr std::string::size_type kAcarsCrcLen = 2; +static constexpr std::string::size_type kAcarsHeaderLen = 21; +static constexpr std::string::size_type kAcarsMinLen = kAcarsHeaderLen + kAcarsCrcLen; + +// CRC-16/CCITT: poly 0x1021, init 0x0000, no reflection, no final XOR. +// This is the variant used by ACARS (same as XMODEM CRC). +static uint16_t acars_crc16(const std::string& data, std::string::size_type len) { + uint16_t crc = 0x0000; + for (std::string::size_type i = 0; i < len; ++i) { + crc ^= static_cast(static_cast(data[i])) << 8; + for (int bit = 0; bit < 8; ++bit) { + if (crc & 0x8000) + crc = static_cast((crc << 1) ^ 0x1021); + else + crc = static_cast(crc << 1); + } + } + return crc; +} + +AcarsDecoded acars_decode(const std::string& raw) { + AcarsDecoded result; + if (raw.size() < kAcarsMinLen) { + result.txt = "ACARS message too short (" + std::to_string(raw.size()) + + " bytes, need " + std::to_string(kAcarsMinLen) + ")"; + return result; + } + + // Verify CRC: computed over everything except the last 2 bytes, + // then compared against those 2 bytes (MSB first). + const std::string::size_type payload_len = raw.size() - kAcarsCrcLen; + const uint16_t computed = acars_crc16(raw, payload_len); + const uint16_t received = (static_cast(static_cast(raw[raw.size() - 2])) << 8) | + static_cast(static_cast(raw[raw.size() - 1])); + result.crc_ok = (computed == received); + + result.reg = raw.substr(1, 7); + result.label = raw.substr(9, 2); + result.block_id = raw[11]; + result.msg_num = raw.substr(12, 3); + result.flight_id = raw.substr(15, 6); + // Payload sits between end of fixed header and the 2 trailing CRC bytes. + if (payload_len > kAcarsHeaderLen) + result.txt = raw.substr(kAcarsHeaderLen, payload_len - kAcarsHeaderLen); + return result; +} + +std::string acars_format(const AcarsDecoded& msg) { + return std::string("ACARS Decoded Result\nCRC: ") + (msg.crc_ok ? "OK" : "FAIL") + + "\nRegistration: " + msg.reg + + "\nLabel: " + msg.label + + "\nBlockID: " + msg.block_id + + "\nMsgNum: " + msg.msg_num + + "\nFlightID: " + msg.flight_id + + "\nMessage: " + msg.txt; +} + void ACARSLogger::log_str(std::string msg) { log_file.write_entry(msg); } @@ -77,17 +147,16 @@ void ACARSAppView::focus() { void ACARSAppView::on_packet(const ACARSPacketMessage* packet) { std::string console_info; - if (packet->state == 255) { // got a packet, parse it, and display rtc::RTC datetime; rtc_time::now(datetime); - // todo parity error recovery console_info = to_string_datetime(datetime, HMS); console_info += ": "; - console_info += packet->message; + std::string message{packet->message, packet->message + packet->msg_len}; + AcarsDecoded decoded = acars_decode(message); + console_info += acars_format(decoded); console.writeln(console_info); - // Log raw data whatever it contains if (logger && logging) logger->log_str(console_info); } else { diff --git a/firmware/application/external/acars_rx/acars_app.hpp b/firmware/application/external/acars_rx/acars_app.hpp index 8e5175ce3..b8889cc8b 100644 --- a/firmware/application/external/acars_rx/acars_app.hpp +++ b/firmware/application/external/acars_rx/acars_app.hpp @@ -33,6 +33,26 @@ namespace ui::external_app::acars_rx { +// Decoded ACARS message fields extracted from a raw frame. +// CRC-16/CCITT (poly 0x1021, init 0x0000) is verified against the two +// trailing bytes of the raw frame; crc_ok reflects that result. +struct AcarsDecoded { + bool crc_ok{false}; + std::string reg{}; + std::string label{}; + std::string flight_id{}; + std::string msg_num{}; + char block_id{'\0'}; + std::string txt{}; +}; + +// Decode a raw ACARS frame: verify CRC-16/CCITT and extract fixed-offset fields. +// Returns a partially-filled AcarsDecoded (txt error only) if the frame is too short. +AcarsDecoded acars_decode(const std::string& raw); + +// Format a decoded ACARS message for display or logging. +std::string acars_format(const AcarsDecoded& msg); + class ACARSLogger { public: Optional append(const std::filesystem::path& filename) { @@ -106,4 +126,4 @@ class ACARSAppView : public View { } // namespace ui::external_app::acars_rx -#endif /*__ACARS_APP_H__*/ \ No newline at end of file +#endif /*__ACARS_APP_H__*/ diff --git a/firmware/application/external/acars_rx/main.cpp b/firmware/application/external/acars_rx/main.cpp index d11adcb8f..0d2af54fe 100644 --- a/firmware/application/external/acars_rx/main.cpp +++ b/firmware/application/external/acars_rx/main.cpp @@ -1,83 +1,83 @@ -/* - * Copyright (C) 2023 Bernd Herzog - * - * This file is part of PortaPack. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, Inc., 51 Franklin Street, - * Boston, MA 02110-1301, USA. - */ - -#include "ui.hpp" -#include "acars_app.hpp" -#include "ui_navigation.hpp" -#include "external_app.hpp" - -namespace ui::external_app::acars_rx { -void initialize_app(ui::NavigationView& nav) { - nav.push(); -} -} // namespace ui::external_app::acars_rx - -extern "C" { - -__attribute__((section(".external_app.app_acars_rx.application_information"), used)) application_information_t _application_information_acars_rx = { - /*.memory_location = */ (uint8_t*)0x00000000, - /*.externalAppEntry = */ ui::external_app::acars_rx::initialize_app, - /*.header_version = */ CURRENT_HEADER_VERSION, - /*.app_version = */ VERSION_MD5, - - /*.app_name = */ "ACARS", - /*.bitmap_data = */ { - 0x80, - 0x01, - 0xC0, - 0x03, - 0xC0, - 0x03, - 0xC0, - 0x03, - 0xC0, - 0x03, - 0xE0, - 0x07, - 0xF8, - 0x1F, - 0xFE, - 0x7F, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xC0, - 0x03, - 0xC0, - 0x03, - 0xC0, - 0x03, - 0xE0, - 0x07, - 0xF0, - 0x0F, - 0xF8, - 0x1F, - }, - /*.icon_color = */ ui::Color::orange().v, - /*.menu_location = */ app_location_t::RX, - /*.desired_menu_position = */ -1, - - /*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'A', 'C', 'A'}, - /*.m4_app_offset = */ 0x00000000, // will be filled at compile time -}; -} +/* + * Copyright (C) 2023 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "acars_app.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::acars_rx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::acars_rx + +extern "C" { + +__attribute__((section(".external_app.app_acars_rx.application_information"), used)) application_information_t _application_information_acars_rx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::acars_rx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "ACARS", + /*.bitmap_data = */ { + 0x80, + 0x01, + 0xC0, + 0x03, + 0xC0, + 0x03, + 0xC0, + 0x03, + 0xC0, + 0x03, + 0xE0, + 0x07, + 0xF8, + 0x1F, + 0xFE, + 0x7F, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xC0, + 0x03, + 0xC0, + 0x03, + 0xC0, + 0x03, + 0xE0, + 0x07, + 0xF0, + 0x0F, + 0xF8, + 0x1F, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'A', 'C', 'A'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp b/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp index 1187b33e5..5885970ab 100644 --- a/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp +++ b/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp @@ -339,7 +339,7 @@ void AdultToysView::randomizeMac() { } void AdultToysView::randomChn() { - channel_number = 37 + std::rand() % (39 - 37 + 1); + channel_number = 37 + rand() % (39 - 37 + 1); field_frequency.set_value(get_freq_by_channel_number(channel_number)); } diff --git a/firmware/application/external/app_manager/ui_app_manager.cpp b/firmware/application/external/app_manager/ui_app_manager.cpp index f8ccf70fd..98107e5a9 100644 --- a/firmware/application/external/app_manager/ui_app_manager.cpp +++ b/firmware/application/external/app_manager/ui_app_manager.cpp @@ -62,19 +62,24 @@ AppManagerView::AppManagerView(NavigationView& nav) auto app_name = get_app_info(menu_view.highlighted_index(), true); auto app_id = get_app_info(menu_view.highlighted_index(), false); - info += "Hidden:"; + if (!app_name.empty()) { + info += "Hidden:"; - if (is_blacklisted(app_name)) { - info += "Yes "; - } else { - info += "No "; + if (is_blacklisted(app_name)) { + info += "Yes "; + } else { + info += "No "; + } } - info += "Autostart:"; - if (is_autostart_app(app_id)) { - info += "Yes"; - } else { - info += "No"; + if (!app_id.empty()) { + info += "Autostart:"; + + if (is_autostart_app(app_id)) { + info += "Yes"; + } else { + info += "No"; + } } if (info.empty()) { @@ -82,6 +87,8 @@ AppManagerView::AppManagerView(NavigationView& nav) } text_app_info.set(info); + button_hide_unhide.set_focusable(!app_name.empty()); + button_set_cancel_autostart.set_focusable(!app_id.empty()); }; button_hide_unhide.on_select = [this](Button&) { @@ -120,27 +127,23 @@ void AppManagerView::refresh_list() { }; for (auto& app : NavigationView::appList) { - if (app.id == nullptr) continue; - app_list_index++; - menu_view.add_item({app.displayName + std::string(is_autostart_app(app.id) ? padding(app.displayName) : ""), + menu_view.add_item({app.displayName + std::string((app.id != nullptr && is_autostart_app(app.id)) ? padding(app.displayName) : ""), app.iconColor, app.icon, - [this, app_id = std::string(app.id)](KeyEvent) { + [this, app_id = std::string(app.id != nullptr ? app.id : "")](KeyEvent) { button_hide_unhide.focus(); }}); } ExternalItemsMenuLoader::load_all_external_items_callback([this, &index, &padding](ui::AppInfoConsole& app) { - if (app.appCallName == nullptr) return; - app_list_index++; - menu_view.add_item({app.appFriendlyName + std::string(is_autostart_app(app.appCallName) ? padding(app.appFriendlyName) : ""), + menu_view.add_item({app.appFriendlyName + std::string((app.appCallName != nullptr && is_autostart_app(app.appCallName)) ? padding(app.appFriendlyName) : ""), ui::Color::light_grey(), &bitmap_icon_sdcard, - [this, app_id = std::string(app.appCallName)](KeyEvent) { + [this, app_id = std::string(app.appCallName != nullptr ? app.appCallName : "")](KeyEvent) { button_hide_unhide.focus(); }}); }); @@ -156,19 +159,29 @@ void AppManagerView::hide_app() { std::vector blacklist; get_blacklist(blacklist); - blacklist.push_back(get_app_info(menu_view.highlighted_index(), true)); + auto app_name = get_app_info(menu_view.highlighted_index(), true); + if (app_name.empty()) return; + + blacklist.push_back(app_name); write_blacklist(blacklist); } void AppManagerView::unhide_app() { std::vector blacklist; get_blacklist(blacklist); - blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), get_app_info(menu_view.highlighted_index(), true)), blacklist.end()); + + auto app_name = get_app_info(menu_view.highlighted_index(), true); + if (app_name.empty()) return; + + blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), app_name), blacklist.end()); write_blacklist(blacklist); } void AppManagerView::hide_unhide_app() { - if (is_blacklisted(get_app_info(menu_view.highlighted_index(), true))) + auto app_name = get_app_info(menu_view.highlighted_index(), true); + if (app_name.empty()) return; + + if (is_blacklisted(app_name)) unhide_app(); else hide_app(); @@ -218,6 +231,7 @@ void AppManagerView::set_auto_start() { if (app_index >= app_list_index) return; auto id_aka_app_call_name = get_app_info(app_index, false); + if (id_aka_app_call_name.empty()) return; autostart_app = id_aka_app_call_name; @@ -234,6 +248,7 @@ void AppManagerView::set_unset_autostart_app() { if (app_index >= app_list_index) return; auto id_aka_friendly_name = get_app_info(app_index, false); + if (id_aka_friendly_name.empty()) return; if (is_autostart_app(id_aka_friendly_name)) unset_auto_start(); @@ -254,17 +269,15 @@ std::string AppManagerView::get_app_info(uint16_t index, bool is_display_name) { std::string result; for (auto& app : NavigationView::appList) { - if (app.id == nullptr) continue; if (current_index == index) { - return is_display_name ? std::string(app.displayName) : std::string(app.id); + return is_display_name ? std::string(app.displayName != nullptr ? app.displayName : "") : std::string(app.id != nullptr ? app.id : ""); } current_index++; } ExternalItemsMenuLoader::load_all_external_items_callback([¤t_index, &index, &result, &is_display_name](ui::AppInfoConsole& app) { - if (app.appCallName == nullptr) return; if (current_index == index) { - result = is_display_name ? app.appFriendlyName : app.appCallName; + result = is_display_name ? (app.appFriendlyName != nullptr ? app.appFriendlyName : "") : (app.appCallName != nullptr ? app.appCallName : ""); } current_index++; }); diff --git a/firmware/application/external/battleship/main.cpp b/firmware/application/external/battleship/main.cpp index 3ba40f401..f7d442520 100644 --- a/firmware/application/external/battleship/main.cpp +++ b/firmware/application/external/battleship/main.cpp @@ -45,7 +45,7 @@ __attribute__((section(".external_app.app_battleship.application_information"), 0xFF, 0xFF, // ################ 0xFC, 0x3F, // ..############.. }, - /*.icon_color = */ ui::Color::blue().v, + /*.icon_color = */ ui::Color::green().v, /*.menu_location = */ app_location_t::GAMES, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/blackjack/main.cpp b/firmware/application/external/blackjack/main.cpp index cb27fc4ed..6306b15f4 100644 --- a/firmware/application/external/blackjack/main.cpp +++ b/firmware/application/external/blackjack/main.cpp @@ -43,7 +43,7 @@ __attribute__((section(".external_app.app_blackjack.application_information"), u 0x80, 0x01, // .......##....... 0x00, 0x00, // ................ }, - ui::Color::red().v, // Red color for diamonds + ui::Color::green().v, // Red color for diamonds app_location_t::GAMES, -1, diff --git a/firmware/application/external/blackjack/ui_blackjack.cpp b/firmware/application/external/blackjack/ui_blackjack.cpp index 6b42b19b4..77390061f 100644 --- a/firmware/application/external/blackjack/ui_blackjack.cpp +++ b/firmware/application/external/blackjack/ui_blackjack.cpp @@ -72,7 +72,7 @@ void BlackjackView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_deck(); } } diff --git a/firmware/application/external/blespam/ui_blespam.cpp b/firmware/application/external/blespam/ui_blespam.cpp index d90a290c4..93dde5ab4 100644 --- a/firmware/application/external/blespam/ui_blespam.cpp +++ b/firmware/application/external/blespam/ui_blespam.cpp @@ -132,7 +132,7 @@ void BLESpamView::reset() { } void BLESpamView::randomChn() { - channel_number = 37 + std::rand() % (39 - 37 + 1); + channel_number = 37 + rand() % (39 - 37 + 1); field_frequency.set_value(get_freq_by_channel_number(channel_number)); } diff --git a/firmware/application/external/breakout/ui_breakout.cpp b/firmware/application/external/breakout/ui_breakout.cpp index d894e7673..af7c07015 100644 --- a/firmware/application/external/breakout/ui_breakout.cpp +++ b/firmware/application/external/breakout/ui_breakout.cpp @@ -655,7 +655,7 @@ void BreakoutView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_game(); } } diff --git a/firmware/application/external/cvs_spam/main.cpp b/firmware/application/external/cvs_spam/main.cpp index b9ccb5f02..ae866fb1b 100644 --- a/firmware/application/external/cvs_spam/main.cpp +++ b/firmware/application/external/cvs_spam/main.cpp @@ -24,7 +24,7 @@ __attribute__((section(".external_app.app_cvs_spam.application_information"), us /*.app_name = */ "CVS Spam", /*.bitmap_data = */ {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81}, - /*.icon_color = */ ui::Color::red().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::TX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/detector_rx/main.cpp b/firmware/application/external/detector_rx/main.cpp index 2745f970e..12e6e88f8 100644 --- a/firmware/application/external/detector_rx/main.cpp +++ b/firmware/application/external/detector_rx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_detector_rx.application_information"), 0x0C, 0xF0, 0x0F}, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::RX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/dinogame/ui_dinogame.cpp b/firmware/application/external/dinogame/ui_dinogame.cpp index 20a108e57..d7614ae7d 100644 --- a/firmware/application/external/dinogame/ui_dinogame.cpp +++ b/firmware/application/external/dinogame/ui_dinogame.cpp @@ -125,7 +125,7 @@ void DinoGameView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_game(); } } @@ -789,8 +789,8 @@ void DinoGameView::draw_current_score() { void DinoGameView::draw_high_score() { auto style = *ui::Theme::getInstance()->fg_light; - painter.fill_rectangle({UI_POS_X_RIGHT(90), 28, 90, 18}, Color::black()); - painter.draw_string({UI_POS_X_RIGHT(90), 30}, style, "HI " + score_to_string(highScore)); + painter.fill_rectangle({UI_POS_X_RIGHT(10), 28, 90, 18}, Color::black()); + painter.draw_string({UI_POS_X_RIGHT(10), 30}, style, "HI " + score_to_string(highScore)); } void DinoGameView::jump() { diff --git a/firmware/application/external/doom/main.cpp b/firmware/application/external/doom/main.cpp index 417b7c625..e10812ab9 100644 --- a/firmware/application/external/doom/main.cpp +++ b/firmware/application/external/doom/main.cpp @@ -58,7 +58,7 @@ __attribute__((section(".external_app.app_doom.application_information"), used)) 0x00, 0x00, }, - ui::Color::red().v, + ui::Color::green().v, app_location_t::GAMES, -1, {0, 0, 0, 0}, diff --git a/firmware/application/external/doom/ui_doom.cpp b/firmware/application/external/doom/ui_doom.cpp index 275dc4b50..75ac2f8bb 100644 --- a/firmware/application/external/doom/ui_doom.cpp +++ b/firmware/application/external/doom/ui_doom.cpp @@ -204,11 +204,11 @@ void spawn_entity(uint8_t type, uint8_t x, uint8_t y) { void spawn_random_entity(uint8_t type) { if (num_entities >= MAX_ENTITIES) return; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); uint8_t spawn_x, spawn_y; do { - spawn_x = std::rand() % LEVEL_WIDTH; - spawn_y = std::rand() % LEVEL_HEIGHT; + spawn_x = rand() % LEVEL_WIDTH; + spawn_y = rand() % LEVEL_HEIGHT; } while (get_block_at(spawn_x, spawn_y) == 0xF || (spawn_x == (uint8_t)player.pos.x && spawn_y == (uint8_t)player.pos.y)); @@ -227,9 +227,9 @@ void remove_entity(uint8_t index) { bool spawned = false; while (!spawned && attempts < 50) { - std::srand(LPC_RTC->CTIME0 + attempts); - uint8_t spawn_x = std::rand() % LEVEL_WIDTH; - uint8_t spawn_y = std::rand() % LEVEL_HEIGHT; + srand(LPC_RTC->CTIME0 + attempts); + uint8_t spawn_x = rand() % LEVEL_WIDTH; + uint8_t spawn_y = rand() % LEVEL_HEIGHT; int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x; int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y; @@ -270,9 +270,9 @@ void initialize_level() { uint8_t attempts = 0; while (initial_enemies < 2 && num_entities < MAX_ENTITIES && attempts < 50) { - std::srand(LPC_RTC->CTIME0 + attempts); - int8_t offset_x = (std::rand() % 11) - 5; - int8_t offset_y = (std::rand() % 11) - 5; + srand(LPC_RTC->CTIME0 + attempts); + int8_t offset_x = (rand() % 11) - 5; + int8_t offset_y = (rand() % 11) - 5; if (abs(offset_x) < 3 && abs(offset_y) < 3) { attempts++; @@ -307,10 +307,10 @@ void initialize_level() { attempts = 0; while (num_entities < MIN_ENTITIES && attempts < 100) { - std::srand(LPC_RTC->CTIME0 + attempts + 100); + srand(LPC_RTC->CTIME0 + attempts + 100); - uint8_t spawn_x = std::rand() % LEVEL_WIDTH; - uint8_t spawn_y = std::rand() % LEVEL_HEIGHT; + uint8_t spawn_x = rand() % LEVEL_WIDTH; + uint8_t spawn_y = rand() % LEVEL_HEIGHT; int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x; int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y; @@ -1045,7 +1045,7 @@ void DoomView::on_show() { void DoomView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); scene = 0; up = down = left = right = fired = false; jogging = view_height = 0; @@ -1081,7 +1081,7 @@ void DoomView::paint(Painter& painter) { int hud_y = RENDER_HEIGHT + 5; painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health)); painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills)); - painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo)); + painter.draw_string({UI_POS_X_RIGHT(10), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo)); prev_velocity_moving = (player.velocity != 0); needs_redraw = false; @@ -1113,7 +1113,7 @@ void DoomView::paint(Painter& painter) { int hud_y = RENDER_HEIGHT + 5; painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health)); painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills)); - painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo)); + painter.draw_string({UI_POS_X_RIGHT(10), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo)); needs_gun_redraw = false; } diff --git a/firmware/application/external/epirb_rx/main.cpp b/firmware/application/external/epirb_rx/main.cpp index 1da7b6f94..40fc5ec28 100644 --- a/firmware/application/external/epirb_rx/main.cpp +++ b/firmware/application/external/epirb_rx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us 0x00, 0x00, }, - /*.icon_color = */ ui::Color::red().v, + /*.icon_color = */ ui::Color::orange().v, /*.menu_location = */ app_location_t::RX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/epirb_tx/beacon.hpp b/firmware/application/external/epirb_tx/beacon.hpp new file mode 100644 index 000000000..c6918b488 --- /dev/null +++ b/firmware/application/external/epirb_tx/beacon.hpp @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ +#ifndef __BEACON_H__ +#define __BEACON_H__ + +#include "portapack.hpp" +#include "ui_epirb_tx.hpp" +#include "location.hpp" + +namespace ui::external_app::epirb_tx { + +/** + * Get bits frop the provided frame + * @param data the frame data + * @param startBit the start bit number (one based to match documentation) + * @param endBit the end bit number (one based to match documentation) + * @return the selected bits (max 64) + */ +static uint64_t get_bits(uint8_t* data, int startBit, int endBit) { + uint64_t result = 0; + // 0 bases bit count + startBit--; + int numBits = endBit - startBit; + + // get a pointer to the starting byte... + const uint8_t* pData = &(data[startBit / 8]); + uint8_t b = *pData; + + // calculate the starting bit within that byte... + int bitOffset = 7 - (startBit % 8); + + // iterate for the desired number of bits... + for (int i = 0; i < numBits; ++i) { + // make room for the next bit... + result <<= 1; + // copy the bit... + result |= ((b >> bitOffset) & 0x01); + // reached the end of the current byte? + if (--bitOffset < 0) { + b = *(++pData); // go to the next byte... + bitOffset = 7; // restart at the first bit in that byte... + } + } + + // all done... + return result; +} + +/** + * Compute a BCH code + * @param frame the frame data + * @param startBit the bit to start BCH calculation + * @param endBit the bit to stop calculation + * @param poly the BCH polynome + * @param polyLength the BCH polynome length + * @return the BCH code + */ +static uint64_t compute_bch(uint8_t* frame, int startBit, int endBit, unsigned long poly, int polyLength) { // Length of data to be checked (not including the BCH code) + int dataLength = endBit - startBit + 1; + // Total lengh (including the BCH code that will be padded to zeros (BCH code length is polyLengh-1)) + int totalLength = dataLength + polyLength - 1; + // Start with the first polyLength bits + uint64_t result = get_bits(frame, startBit, startBit + polyLength - 1); + for (int i = polyLength; i <= totalLength; i++) { // Iterate on each bit after the first polyLength batch + bool firstBit = result >> (polyLength - 1); + if (firstBit) { // We have a leading 1 => xor the result with the poly + result = result ^ poly; + } + if (i < totalLength) { // Move to next bit + result = result << 1; + if (i < dataLength) { // Append next bit + result |= get_bits(frame, startBit + i, startBit + i); + } // else : 0 padding after data length + } + } + return result; +} + +// 21 bits BCH polynomial +#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL +#define BCH_21_POLY_LENGTH 22 +// 12 bits BCH polynomial +#define BCH_12_POLYNOMIAL 0b1010100111001UL +#define BCH_12_POLY_LENGTH 13 + +/** + * Combyte BCH1 code for the provided frame + */ +static uint64_t compute_bch1(uint8_t* frame) { + return compute_bch(frame, 25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH); +} + +/** + * Combyte BCH2 code for the provided frame + */ +static uint64_t compute_bch2(uint8_t* frame) { + return compute_bch(frame, 107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH); +} + +/** + * Set a bit in the provided frame + * @param buf the frame + * @param bit the position of the bit to set (0 based) + * @param v the bit value + */ +static void set_bit(uint8_t* buf, int bit, bool v) { + int byte = bit >> 3; + int off = 7 - (bit & 7); + + if (v) + buf[byte] |= (1 << off); + else + buf[byte] &= ~(1 << off); +} + +/** + * Push bits to the provided frame + * @param buf the frame + * @param pos the current frame possition (in/out, will be incremented during the opreration) + * @param v the bits to push (max 64) + * @param n the number of bits to push + */ +static void push_bits(uint8_t* buf, int& pos, uint64_t v, int n) { + for (int i = n - 1; i >= 0; i--) + set_bit(buf, pos++, (v >> i) & 1); +} + +/** + * Convert a beacon to hex string representation + * @param frame the frame to convert + * @param start true for the first half of the frame, false for the second half + * @return the hex string representation of the specieid half of the frame + */ +std::string beacon_to_hex_string(const uint8_t* frame, bool start) { + static const char hex[] = "0123456789ABCDEF"; + + std::string out; + out.resize(18); + + int offset = start ? 0 : 9; + + for (int i = 0; i < 9; i++) { + uint8_t b = frame[offset + i]; + + out[i * 2] = hex[b >> 4]; + out[i * 2 + 1] = hex[b & 0x0F]; + } + + return out; +} + +/** + * Generate a beacon in the provided buffer + * @param frame the buffer to generate the frame in + * @param params the beacon parameters + * @return the size of the generated frame + */ +size_t generate_beacon(uint8_t* frame, const BeaconParams& params) { + // Clear the content of the frame + memset(frame, 0, 18); + + int pos = 0; + uint32_t deg, min, sec; + + // bit sync + for (int i = 0; i < 15; i++) + push_bits(frame, pos, 1, 1); + + // frame sync + push_bits(frame, pos, params.is_test ? 0b011010000 : 0b000101111, 9); + + // PDF-1 (Protexted Data field 1) + int pdf1_start = pos; + + push_bits(frame, pos, 1, 1); // format flag (long) + bool is_user = (params.protocol == BeaconProtocol::USER); + bool is_standard = (params.protocol == BeaconProtocol::STANDARD); + push_bits(frame, pos, is_user, 1); // protocol flag + push_bits(frame, pos, params.country, 10); // country code + switch (params.type) { + case BeaconType::EPIRB: + if (is_user) + push_bits(frame, pos, 0b010, 3); + else if (is_standard) + push_bits(frame, pos, 0b0010, 4); + else + push_bits(frame, pos, 0b1010, 4); + break; + case BeaconType::PLB: + if (is_user) + push_bits(frame, pos, 0b011, 3); + else if (is_standard) + push_bits(frame, pos, 0b0111, 4); + else + push_bits(frame, pos, 0b1011, 4); + break; + default: + case BeaconType::ELT: + if (is_user) + push_bits(frame, pos, 0b001, 3); + else if (is_standard) + push_bits(frame, pos, 0b0011, 4); + else + push_bits(frame, pos, 0b1000, 4); + break; + } + + // Fill the rest of PDF 1 with zeros + while (pos < pdf1_start + 61) + push_bits(frame, pos, 0, 1); + + if (is_user) { + // User Location Protocol: no position in PDF1 + if (params.type == BeaconType::PLB) { + // Set bits for PLB + set_bit(frame, 40 - 1, 1); + set_bit(frame, 41 - 1, 1); + set_bit(frame, 42 - 1, 0); + } + + set_bit(frame, 85 - 1, params.has_121_5); + } else if (is_standard) { + // Standard Location Protocol + // North / south + set_bit(frame, 65 - 1, params.location.south); + // E / W + set_bit(frame, 75 - 1, params.location.west); + pos = 66 - 1; + // Latitude 1/4 degrees (9 bits) + deg = (params.location.lat_deg << 2) + (params.location.lat_min / 15); + push_bits(frame, pos, deg, 9); + pos = 76 - 1; + // Longitude 1/4 degrees (10 bits) + deg = (params.location.long_deg << 2) + (params.location.long_min / 15); + push_bits(frame, pos, deg, 10); + pos = pdf1_start + 61; + } else { + // National Location Protocol + // North / south + set_bit(frame, 59 - 1, params.location.south); + // E / W + set_bit(frame, 72 - 1, params.location.west); + pos = 60 - 1; + // Latitude degrees (7 bits) + push_bits(frame, pos, params.location.lat_deg, 7); + // Latitude min (5 bits, 2 min increment) + min = params.location.lat_min / 2; + push_bits(frame, pos, min, 5); + pos = 73 - 1; + // Longitude degrees (8 bits) + push_bits(frame, pos, params.location.long_deg, 8); + // Longiitude min (5 bits, 2 min increment) + min = params.location.long_min / 2; + push_bits(frame, pos, min, 5); + pos = pdf1_start + 61; + } + + // Set BCH1 + uint64_t bch1 = compute_bch1(frame); + push_bits(frame, pos, bch1, 21); + + // PDF-2 (Protexted Data Field 2) + int pdf2_start = pos; + if (is_user) { + // User Location Protocol + push_bits(frame, pos, params.is_internal, 1); + // Latitude N/S + push_bits(frame, pos, params.location.south, 1); + // Latitude degrees (7 bits) + push_bits(frame, pos, params.location.lat_deg, 7); + // Latitude minutes (4 bits, 4 minutes precision) + min = params.location.lat_min / 4; + push_bits(frame, pos, min, 4); + // Longitude E/W + push_bits(frame, pos, params.location.west, 1); + // Longitude degrees (8 bits) + push_bits(frame, pos, params.location.long_deg, 8); + // Longitude minutes (4 bits, 4 minutes precision) + min = params.location.long_min / 4; + push_bits(frame, pos, min, 4); + } else if (is_standard) { + // Standard Location Protocol + push_bits(frame, pos, 0b1101, 4); + push_bits(frame, pos, params.is_internal, 1); + push_bits(frame, pos, params.has_121_5, 1); + // Latiitude + // + + push_bits(frame, pos, 1, 1); + // Min + min = params.location.lat_min % 15; + push_bits(frame, pos, min, 5); + // Sec (4 bits, 4 sec precision) + sec = params.location.lat_sec / 4; + push_bits(frame, pos, sec, 4); + // Longitude + // + + push_bits(frame, pos, 1, 1); + // Min + min = params.location.long_min % 15; + push_bits(frame, pos, min, 5); + // Sec (4 bits, 4 sec precision) + sec = params.location.long_sec / 4; + push_bits(frame, pos, sec, 4); + } else { + // National Location Protocol + push_bits(frame, pos, 0b1101, 4); + push_bits(frame, pos, params.is_internal, 1); + push_bits(frame, pos, params.has_121_5, 1); + // Lat + // + + push_bits(frame, pos, 1, 1); + // Min + push_bits(frame, pos, (params.location.lat_min % 2), 2); + // Sec (4 bits, 4 sec precision) + sec = params.location.lat_sec / 4; + push_bits(frame, pos, sec, 4); + // LLon + // + + push_bits(frame, pos, 1, 1); + // Min + push_bits(frame, pos, (params.location.long_min % 2), 2); + // Sec (4 bits, 4 sec precision) + sec = params.location.long_sec / 4; + push_bits(frame, pos, sec, 4); + } + + while (pos < pdf2_start + 26) + push_bits(frame, pos, 0, 1); + + // Compute BCH 2 + uint64_t bch2 = compute_bch2(frame); + push_bits(frame, pos, bch2, 12); + return 18; +} + +} // namespace ui::external_app::epirb_tx + +#endif /*__BEACON_H__*/ \ No newline at end of file diff --git a/firmware/application/external/epirb_tx/location.hpp b/firmware/application/external/epirb_tx/location.hpp new file mode 100644 index 000000000..70d76f045 --- /dev/null +++ b/firmware/application/external/epirb_tx/location.hpp @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ +#ifndef __LOCATION_H__ +#define __LOCATION_H__ + +#include "portapack.hpp" +#include "ui_epirb_tx.hpp" +#include +#include +#include + +namespace ui::external_app::epirb_tx { + +/** + * Convert decimal coordinates to sexagesimal coordinates + * @param value the decimal value + * @param negative output N(false)/S(true) or E(false)/W(true) value + * @param deg output degrees value + * @param min output minutes value + * @param sec output seconds value + */ +static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t& min, uint8_t& sec) { + negative = value < 0; + value = std::fabs(value); + + deg = (uint16_t)value; + + double m = (value - deg) * 60.0; + min = (uint8_t)m; + + double s = (m - min) * 60.0; + sec = (uint8_t)(s + 0.5); + + if (sec == 60) { + sec = 0; + min++; + } + + if (min == 60) { + min = 0; + deg++; + } +} + +/** + * Convert sexagesimal coordinates to decimal + * @param negative N(false)/S(true) or E(false)/W(true) value + * @param deg degrees value + * @param min minutes value + * @param sec seconds value + * @return value the decimal value + */ +static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t sec) { + double v = deg + min / 60.0 + sec / 3600.0; + return negative ? -v : v; +} + +/** + * Convert maidenhead locator to decima coordinates + * @param loc the locator + * @param lat output latitude + * @param lon output longitude + */ +static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) { + static const double lon_step[] = + { + 20.0, + 2.0, + 1.0 / 12.0, + 1.0 / 120.0, + 1.0 / 2880.0}; + + static const double lat_step[] = + { + 10.0, + 1.0, + 1.0 / 24.0, + 1.0 / 240.0, + 1.0 / 5760.0}; + + lon = -180.0; + lat = -90.0; + + int pairs = loc.size() / 2; + if (pairs > 5) + pairs = 5; + + for (int i = 0; i < pairs; i++) { + char c1 = std::toupper(loc[i * 2]); + char c2 = std::toupper(loc[i * 2 + 1]); + + double lon_size = lon_step[i]; + double lat_size = lat_step[i]; + + int v1, v2; + + if (i % 2 == 0) // letters + { + v1 = c1 - 'A'; + v2 = c2 - 'A'; + } else // digits + { + v1 = c1 - '0'; + v2 = c2 - '0'; + } + + lon += v1 * lon_size; + lat += v2 * lat_size; + } + + // Cell center + lon += lon_step[pairs - 1] / 2.0; + lat += lat_step[pairs - 1] / 2.0; +} + +/** + * Convert decimal coordinates to maidenhead locator + * @param lat the latitude + * @param lon the longitude + * @param precision (optional, defaults to 8) the number of charater for the maidenhead locator + * @return the locator + */ +static std::string decimal_to_maidenhead(double lat, double lon, int precision = 8) { + lon += 180.0; + lat += 90.0; + + int A = lon / 20; + int B = lat / 10; + + lon -= A * 20; + lat -= B * 10; + + int C = lon / 2; + int D = lat / 1; + + lon -= C * 2; + lat -= D * 1; + + int E = lon / (5.0 / 60.0); + int F = lat / (2.5 / 60.0); + + lon -= E * (5.0 / 60.0); + lat -= F * (2.5 / 60.0); + + int G = lon / (5.0 / 600.0); + int H = lat / (2.5 / 600.0); + + std::string locator; + + locator += char('A' + A); + locator += char('A' + B); + + if (precision >= 4) { + locator += char('0' + C); + locator += char('0' + D); + } + + if (precision >= 6) { + locator += char('a' + E); + locator += char('a' + F); + } + + if (precision >= 8) { + locator += char('0' + G); + locator += char('0' + H); + } + + return locator; +} + +/** + * Init the provided Location values from its locator + */ +void init_from_locator(Location& loc) { + maidenhead_to_decimal(loc.locator, loc.latitude, loc.longitude); + + decimal_to_dms(loc.latitude, + loc.south, + loc.lat_deg, + loc.lat_min, + loc.lat_sec); + + decimal_to_dms(loc.longitude, + loc.west, + loc.long_deg, + loc.long_min, + loc.long_sec); +} + +/** + * Init the provided Location values from its decimal coordinates + */ +void init_from_decimal(Location& loc) { + decimal_to_dms(loc.latitude, + loc.south, + loc.lat_deg, + loc.lat_min, + loc.lat_sec); + + decimal_to_dms(loc.longitude, + loc.west, + loc.long_deg, + loc.long_min, + loc.long_sec); + + loc.locator = decimal_to_maidenhead(loc.latitude, loc.longitude); +} + +/** + * Init the provided Location values from its sexagesimal coordinates + */ +void init_from_dms(Location& loc) { + loc.latitude = dms_to_decimal( + loc.south, + loc.lat_deg, + loc.lat_min, + loc.lat_sec); + + loc.longitude = dms_to_decimal( + loc.west, + loc.long_deg, + loc.long_min, + loc.long_sec); + + loc.locator = decimal_to_maidenhead(loc.latitude, loc.longitude); +} + +/** + * Convert the provided Loaction to it's latitude string (format ) + */ +std::string to_latitude_string(const Location& location) { + char buffer[16]; + // Format : + snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N'); + return std::string(buffer); +} + +/** + * Convert the provided Loaction to it's longitude string (format ) + */ +std::string to_longitude_string(const Location& location) { + char buffer[16]; + // Format : + snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E'); + return std::string(buffer); +} + +} // namespace ui::external_app::epirb_tx + +#endif /*__LOCATION_H__*/ \ No newline at end of file diff --git a/firmware/application/external/epirb_tx/main.cpp b/firmware/application/external/epirb_tx/main.cpp new file mode 100644 index 000000000..3765b739a --- /dev/null +++ b/firmware/application/external/epirb_tx/main.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_epirb_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::epirb_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::epirb_tx +extern "C" { + +__attribute__((section(".external_app.app_epirb_tx.application_information"), used)) application_information_t _application_information_epirb_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::epirb_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "EPIRB TX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0x7C, + 0x3E, + 0xFE, + 0x7F, + 0xFF, + 0xFF, + 0xF7, + 0xEF, + 0xE3, + 0xC7, + 0xE3, + 0xC7, + 0xE3, + 0xC7, + 0xE3, + 0xC7, + 0xF7, + 0xEF, + 0xFF, + 0xFF, + 0xFE, + 0x7F, + 0x7C, + 0x3E, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_epirb_tx */ {'P', 'E', 'P', 'T'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/epirb_tx/ui_epirb_tx.cpp b/firmware/application/external/epirb_tx/ui_epirb_tx.cpp new file mode 100644 index 000000000..fdc1a3c19 --- /dev/null +++ b/firmware/application/external/epirb_tx/ui_epirb_tx.cpp @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_epirb_tx.hpp" + +#include "tonesets.hpp" +#include "portapack.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "file_reader.hpp" +#include "file_path.hpp" +#include "ui_geomap.hpp" +#include "ui_alphanum.hpp" + +#include +#include + +#include "beacon.hpp" + +using namespace portapack; + +namespace ui::external_app::epirb_tx { + +void EPIRBTXAppView::focus() { + button_tx.focus(); +} + +EPIRBTXAppView::~EPIRBTXAppView() { + // Restore original frequency before leaving + transmitter_model.set_target_frequency(original_frequency); + transmitter_model.disable(); + baseband::shutdown(); +} + +/** + * Conversion from hex char to half byte + */ +static uint8_t hexval(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return 0; +} + +/** + * Convert from hex chars to uint8_t + */ +static uint8_t hexToByte(char high, char low) { + return (hexval(high) << 4) | hexval(low); +} + +std::string EPIRBTXAppView::frame_to_hex_string(bool start) { + return beacon_to_hex_string(epirb_tx_message.data, start); +} + +void EPIRBTXAppView::generate_frame(BeaconParams params) { + epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params); +} + +void EPIRBTXAppView::on_timer() { + // Timer is called on display refresh + if (loop) { + if (loop_enabled) { + // chTimeNow() returns milliseconds on our version of ChibiOS / Hardware + auto now = chTimeNow(); + auto elapsed = ((now - last_frame_time) / 1000); + std::string timeout = std::to_string((uint32_t)(delay > elapsed ? delay - elapsed : 0)); + if (timeout != text_timeout.get()) { + // Update timeout text every seconds + text_timeout.set(timeout); + } + if (now > (last_frame_time + (delay * 1000))) { + // Send a new frame after the delay + start_tx(); + } + } else { + // Stop send loop + loop = false; + } + } +} + +void EPIRBTXAppView::update_frame(bool updateConfig) { + if (mode_file) { + // In file mode, currently selected beacon has changed => load the new one + Beacon& beacon = beacons[selected_beacon]; + // Set desciption + text_description.set(beacon.description.substr(0, max_text_width_ext)); + text_description_end.set(beacon.description.size() > max_text_width_ext ? "-" + beacon.description.substr(max_text_width_ext, max_text_width_ext + max_text_width_ext - 1) : ""); + // Udapte frame content on display + text_frame.set(beacon.frame.substr(0, 18)); + text_frame_end.set(beacon.frame.size() > 18 ? beacon.frame.substr(18, 36) : ""); + // Prepare tx configuration + epirb_tx_message.data_len = std::min((beacon.frame.size() / 2), 18); + for (uint8_t i = 0; i < epirb_tx_message.data_len; i++) { + epirb_tx_message.data[i] = hexToByte( + beacon.frame[2 * i], + beacon.frame[2 * i + 1]); + } + } else { + // In manual mode, generate frame content for current beacon params + generate_frame(beacon_params); + // Update frame content on display + text_frame.set(frame_to_hex_string(true)); + text_frame_end.set(frame_to_hex_string(false)); + } + if (updateConfig && send_on_change && loop) { + // Need to update config / send new beacon + if (am_enabled) { + // Already transmitting => update config + last_frame_time = chTimeNow(); + update_config(); + } else { + // Not yet transmitting => start tx + start_tx(); + } + } +} + +void EPIRBTXAppView::update_config() { + if (epirb_tx_message.mode_bpsk) { + // Already in BPSK mode => backup bpsk frequency + bpsk_frequency = transmitter_model.target_frequency(); + } else { + // Previously in AM mode => restore bpsk frequency + transmitter_model.set_target_frequency(bpsk_frequency); + } + // Set mode to bpsk + epirb_tx_message.mode_bpsk = true; + // Set pre/post count + epirb_tx_message.pre_count = (150 * TONES_SAMPLERATE) / 1000; // 150 ms + epirb_tx_message.post_count = (100 * TONES_SAMPLERATE) / 1000; // 100 ms + // Send config to baseband + baseband::set_epirb_tx_config(epirb_tx_message); +} + +void EPIRBTXAppView::set_tx_button_state(bool active) { + button_tx.set_text(active ? "START" : "STOP"); + button_tx.set_style(active ? &style_tx_start : &style_tx_stop); +} + +void EPIRBTXAppView::start_tx() { + last_frame_time = chTimeNow(); + update_config(); + loop = loop_enabled; + transmitter_model.enable(); + tx_view.set_transmitting(true); + set_tx_button_state(false); + transmitting = true; +} + +void EPIRBTXAppView::stop_tx() { + loop = false; + transmitter_model.disable(); + tx_view.set_transmitting(false); + set_tx_button_state(true); + transmitting = false; +} + +void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) { + (void)progress; + if (done) { + if (am_enabled) { + // BPSK frame sent, switch back to 121.5 AM signal + epirb_tx_message.mode_bpsk = false; + // Backup bpsk frequency for next run + bpsk_frequency = transmitter_model.target_frequency(); + // Restore am frequency + transmitter_model.set_target_frequency(am_frequency); + // Send config to baseband + baseband::set_epirb_tx_config(epirb_tx_message); + } else { + // End of BPSK frame + transmitter_model.disable(); + tx_view.set_transmitting(false); + if (!loop) { + // Not looping => update transmission state + set_tx_button_state(true); + transmitting = false; + } + } + } +} + +void EPIRBTXAppView::update_location(bool updateLocatorField) { + locator = beacon_params.location.locator; + if (updateLocatorField) text_field_beacon_locator.set_text(beacon_params.location.locator); + text_beacon_latitude_value.set(to_latitude_string(beacon_params.location)); + text_beacon_longitude_value.set(to_longitude_string(beacon_params.location)); +} + +void EPIRBTXAppView::update_mode() { + // Hide / show widgets for file mode or manual mode + text_beacon.hidden(!mode_file); + text_description_label.hidden(!mode_file); + options_frame.hidden(!mode_file); + text_description.hidden(!mode_file); + text_description_end.hidden(!mode_file); + text_beacon_type.hidden(mode_file); + text_beacon_country.hidden(mode_file); + checkbox_beacon_internal.hidden(mode_file); + text_beacon_locator.hidden(mode_file); + text_beacon_latitude.hidden(mode_file); + text_beacon_latitude_value.hidden(mode_file); + text_beacon_longitude.hidden(mode_file); + text_beacon_longitude_value.hidden(mode_file); + button_mangps.hidden(mode_file); + options_beacon_type.hidden(mode_file); + options_beacon_protocol.hidden(mode_file); + options_beacon_country.hidden(mode_file); + text_field_beacon_locator.hidden(mode_file); +} + +EPIRBTXAppView::EPIRBTXAppView( + NavigationView& nav) { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({&labels, + &options_mode, + &text_beacon, + &text_description_label, + &text_beacon_type, + &options_beacon_type, + &options_beacon_protocol, + &text_beacon_country, + &options_beacon_country, + &checkbox_beacon_internal, + &text_beacon_locator, + &text_beacon_latitude, + &text_beacon_latitude_value, + &text_beacon_longitude, + &text_beacon_longitude_value, + &button_mangps, + &text_field_beacon_locator, + &options_frame, + &text_description, + &text_description_end, + &text_frame, + &text_frame_end, + &text_timeout, + &checkbox_loop, + &field_delay, + &button_tx, + &checkbox_am, + &field_am_frequency, + &checkbox_send_on_change, + &tx_view}); + + text_beacon.set_style(Theme::getInstance()->fg_light); + text_description_label.set_style(Theme::getInstance()->fg_light); + text_beacon_type.set_style(Theme::getInstance()->fg_light); + text_beacon_country.set_style(Theme::getInstance()->fg_light); + text_beacon_locator.set_style(Theme::getInstance()->fg_light); + text_beacon_latitude.set_style(Theme::getInstance()->fg_light); + text_beacon_longitude.set_style(Theme::getInstance()->fg_light); + + // Restore settings + checkbox_am.set_value(am_enabled); + checkbox_loop.set_value(loop_enabled); + checkbox_send_on_change.set_value(send_on_change); + options_mode.set_by_value(!mode_file); + transmitter_model.set_target_frequency(bpsk_frequency); + field_am_frequency.set_value(am_frequency); + field_delay.set_value(delay); + options_beacon_type.set_by_value(beacon_type); + options_beacon_protocol.set_by_value(beacon_protocol); + options_beacon_country.set_by_value(beacon_country); + checkbox_beacon_internal.set_value(beacon_internal); + beacon_params.type = (BeaconType)beacon_type; + beacon_params.has_121_5 = am_enabled; + beacon_params.location.locator = locator; + beacon_params.is_internal = beacon_internal; + init_from_locator(beacon_params.location); + update_mode(); + update_location(); + + options_mode.on_change = [this](size_t index, OptionsField::value_t) { + mode_file = (index == 0); + update_mode(); + update_frame(); + set_dirty(); + }; + + options_beacon_type.on_change = [this](size_t index, OptionsField::value_t) { + beacon_params.type = (BeaconType)index; + beacon_type = index; + update_frame(); + set_dirty(); + }; + + options_beacon_protocol.on_change = [this](size_t index, OptionsField::value_t) { + beacon_params.protocol = (BeaconProtocol)index; + beacon_protocol = index; + update_frame(); + set_dirty(); + }; + + options_beacon_country.on_change = [this](size_t, OptionsField::value_t v) { + beacon_params.country = v; + beacon_country = v; + update_frame(); + set_dirty(); + }; + + checkbox_beacon_internal.on_select = [this](Checkbox&, bool v) { + beacon_internal = v; + beacon_params.is_internal = v; + update_frame(); + set_dirty(); + }; + + text_field_beacon_locator.on_select = [this, &nav](TextField&) mutable { + auto te_view = nav.push(locator, 10, ENTER_KEYBOARD_MODE_ALPHA); + te_view->on_changed = [this](std::string& value) { + beacon_params.location.locator = value; + init_from_locator(beacon_params.location); + update_location(); + update_frame(); + set_dirty(); + }; + }; + + button_mangps.on_select = [this, &nav](Button&) { + nav.push( + 0, + GeoPos::alt_unit::METERS, + GeoPos::spd_unit::HIDDEN, + beacon_params.location.latitude, + beacon_params.location.longitude, + [this](int32_t, float lat, float lon, int32_t) { + beacon_params.location.latitude = lat; + beacon_params.location.longitude = lon; + init_from_decimal(beacon_params.location); + // Update locator field + update_location(); + update_frame(); + set_dirty(); + }); + }; + + field_am_frequency.on_change = [this](rf::Frequency freq) { + am_frequency = freq; + if (transmitting && !epirb_tx_message.mode_bpsk && am_enabled) + // Update transmitter frequency + transmitter_model.set_target_frequency(am_frequency); + }; + + // Load available beacons from BEACONS.TXT files (or default). + load_beacons(); + // Setup options_frame content with loaded beacons + using option_t = std::pair; + using options_t = std::vector; + options_t entries; + for (const auto& beacon : beacons) + entries.emplace_back(beacon.title, entries.size()); + + options_frame.set_options(std::move(entries)); + if (selected_beacon >= beacons.size()) + // BEACONS.TXT file has changed since last launch, default index to 0 + selected_beacon = 0; + options_frame.set_selected_index(selected_beacon); + options_frame.on_change = [this](size_t index, OptionsField::value_t) { + selected_beacon = index; + update_frame(); + set_dirty(); + }; + + // Init frame content / baseband param with currently setup beacon + update_frame(false); + + checkbox_loop.on_select = [this](Checkbox&, bool v) { + loop_enabled = v; + }; + + field_delay.on_change = [this](int32_t v) { + delay = v; + }; + + checkbox_am.on_select = [this](Checkbox&, bool v) { + beacon_params.has_121_5 = v; + am_enabled = v; + if (!mode_file) update_frame(false); + }; + + // AM frequency field edit + field_am_frequency.on_edit = [this, &nav]() { + auto new_view = nav.push(field_am_frequency.value()); + new_view->on_changed = [this](rf::Frequency f) { + field_am_frequency.set_value(f); + }; + }; + + checkbox_send_on_change.on_select = [this](Checkbox&, bool v) { + send_on_change = v; + }; + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + transmitter_model.set_target_frequency(f); + bpsk_frequency = f; + }; + }; + + tx_view.on_start = [this]() { + start_tx(); + }; + + tx_view.on_stop = [this]() { + stop_tx(); + }; + + button_tx.on_select = [this](Button&) { + if (!transmitting) + start_tx(); + else + stop_tx(); + }; +} + +/** + * Load beacons from /EPIRB/BEACONS.TXT file on sd card + */ +void EPIRBTXAppView::load_beacons() { + File beacons_file; + auto error = beacons_file.open(epirb_dir / u"BEACONS.TXT"); + beacons.clear(); + + if (!error) { + // BEACONS.TXT file exists + auto reader = FileLineReader(beacons_file); + for (const auto& line : reader) { + // Skip comment lines + if (line.length() == 0 || line[0] == '#') + continue; + + // Split line with ';' separator + auto cols = split_string(line, ';'); + if (cols.size() != 3) + continue; + + // Read current beacon + Beacon beacon{}; + beacon.title = trim(cols[0]); + beacon.description = trim(cols[1]); + // Make sure frame is not longer tha 18 bytes / 36 hex character + beacon.frame = trim(cols[2]).substr(0, 36); + size_t size = beacon.frame.size(); + if (size <= 0) + continue; // Invalid line. + // Beacon is valid, add it to the list + beacons.emplace_back(std::move(beacon)); + } + } + if (beacons.empty()) { + // No beacons file or empty flile: just add default beacon + beacons.push_back(default_beacon); + } +} + +} // namespace ui::external_app::epirb_tx \ No newline at end of file diff --git a/firmware/application/external/epirb_tx/ui_epirb_tx.hpp b/firmware/application/external/epirb_tx/ui_epirb_tx.hpp new file mode 100644 index 000000000..132e933d7 --- /dev/null +++ b/firmware/application/external/epirb_tx/ui_epirb_tx.hpp @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __EPIRB_TX_H__ +#define __EPIRB_TX_H__ + +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" + +#include "portapack.hpp" +#include "message.hpp" +#include "tonesets.hpp" + +#define BEACON_HEXA_SIZE 36 +#define BEACON_HEXA_HALF_SIZE 18 +#define BEACON_SIZE 18 + +namespace ui::external_app::epirb_tx { + +enum class BeaconType { + EPIRB = 0, + ELT = 1, + PLB = 2 +}; + +enum class BeaconProtocol { + USER = 0, + STANDARD = 1, + NATIONAL = 2 +}; + +struct Location { + std::string locator; + bool south; + uint16_t lat_deg; + uint8_t lat_min; + uint8_t lat_sec; + float latitude; + bool west; + uint16_t long_deg; + uint8_t long_min; + uint8_t long_sec; + float longitude; +}; + +struct BeaconParams { + BeaconType type; + BeaconProtocol protocol; + uint32_t country; + bool is_test; + bool is_internal; + bool has_121_5; + Location location; +}; + +class EPIRBTXAppView : public View { + public: + EPIRBTXAppView(NavigationView& nav); + ~EPIRBTXAppView(); + + void focus() override; + + std::string title() const override { return "EPIRB TX"; }; + + private: + void start_tx(); + void stop_tx(); + void update_config(); + void on_tx_progress(const uint32_t progress, const bool done); + void on_timer(); + void load_beacons(); + void set_tx_button_state(bool active); + std::string frame_to_hex_string(bool start); + void generate_frame(BeaconParams params); + void update_frame(bool updateConfig = true); + void update_mode(); + void update_location(bool updateLocatorField = true); + + struct Beacon { + std::string title{}; + std::string description{}; + std::string frame{}; + }; + std::vector beacons{}; + Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"}; + + BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}}; + + // Currently selected beacon index (from BEACONS.TXT file / options_frame combo) + uint32_t selected_beacon{0}; + + // Frequency of the transmitter before starting the app (used to restore frequency when leaving) + rf::Frequency original_frequency{0}; + // Frequency of the AM emergency signal + rf::Frequency am_frequency{121500000}; + // Frequency of the 406 MHz BPSK signal + rf::Frequency bpsk_frequency{406025000}; + + // True when using a beacon from the BEACONS.TXT file + bool mode_file{true}; + // True when looping on sending beacons is enabled + bool loop_enabled{true}; + // True if AM emergency signal transmission is enabled + bool am_enabled{true}; + // True if we want to send a new frame each time the user changes the current beacon + bool send_on_change{false}; + // The current locator string + std::string locator{"JN03RO"}; + // The delay between each frame when on loop mode + uint32_t delay{50}; + uint8_t beacon_type{0}; + // Currently selected beacon protocol + uint8_t beacon_protocol{0}; + // Currently selected beacon country + uint32_t beacon_country{227}; + // Current beacon's internal state (true for internal location system) + bool beacon_internal{true}; + + TxRadioState radio_state_{ + 0 /* frequency */, + 1750000 /* bandwidth */, + TONES_SAMPLERATE /* sampling rate */ + }; + app_settings::SettingsManager settings_{ + "tx_epirb", + app_settings::Mode::TX, + { + {"sbeacon"sv, &selected_beacon}, + {"amfreq"sv, &am_frequency}, + {"bpskfreq"sv, &bpsk_frequency}, + {"loop"sv, &loop_enabled}, + {"delay"sv, &delay}, + {"file"sv, &mode_file}, + {"am"sv, &am_enabled}, + {"soc"sv, &send_on_change}, + {"type"sv, &beacon_type}, + {"proto"sv, &beacon_protocol}, + {"country"sv, &beacon_country}, + {"internal"sv, &beacon_internal}, + {"locator"sv, &locator}, + }}; + + // Time of the last sent frame + uint32_t last_frame_time{0}; + // True when transmission is enabled + bool transmitting{false}; + // True when currently looping on sending beacons + bool loop{false}; + + // Current EPIRBTXDataMessage for baseband + EPIRBTXDataMessage epirb_tx_message{}; + + const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH; + const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency MHz", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}}; + + // For file mode + Text text_beacon{ + {UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT}, + "Beacon:"}; + // Beacon selection from BEACONS.TXT + OptionsField options_frame{ + {UI_POS_X(7), UI_POS_Y(1)}, + 30, + {}}; + Text text_description_label{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT}, + "Description:"}; + Text text_description{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT}, + ""}; + Text text_description_end{ + {UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT}, + ""}; + + // For manual mode + Text text_beacon_type{ + {UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, + "Type:"}; + Text text_beacon_country{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, + "Country:"}; + Checkbox checkbox_beacon_internal{ + {UI_POS_X_RIGHT(12), UI_POS_Y(2)}, + 12, + "Internal", + true}; + Text text_beacon_locator{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, + "Locator:"}; + Text text_beacon_latitude{ + {UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, + "Lat.:"}; + Text text_beacon_longitude{ + {UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, + "Long.:"}; + Text text_beacon_latitude_value{ + {UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT}, + ""}; + Text text_beacon_longitude_value{ + {UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT}, + ""}; + OptionsField options_beacon_type{ + {UI_POS_X(9), UI_POS_Y(1)}, + 7, + {{"EPIRB", 0}, + {"ELT", 1}, + {"PLB", 2}}}; + OptionsField options_beacon_protocol{ + {UI_POS_X(9 + 7), UI_POS_Y(1)}, + 30, + {{"User", 0}, + {"Standard", 1}, + {"National", 2}}}; + OptionsField options_beacon_country{ + {UI_POS_X(9), UI_POS_Y(2)}, + 7, + {{"France", 227}, + {"USA", 366}, + {"Germany", 211}, + {"Russia", 273}, + {"Spain", 224}, + {"Japan", 431}, + {"UK", 232}}}; + TextField text_field_beacon_locator{ + {UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT}, + "JN03RO"}; + Button button_mangps{ + {UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)}, + "Set pos."}; + + // Mode selection (file/manual) + OptionsField options_mode{ + {UI_POS_X(7), UI_POS_Y(0)}, + 30, + {{"File (BEACONS.TXT)", 0}, + {"Manual (Editor)", 1}}}; + + // Frame content + Text text_frame{ + {UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT}, + ""}; + Text text_frame_end{ + {UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT}, + ""}; + + Text text_timeout{ + {UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT}, + ""}; + + // Transmission settings + Checkbox checkbox_loop{ + {UI_POS_X(0), UI_POS_Y(9)}, + 10, + "Resend every", + true}; + NumberField field_delay{ + {UI_POS_X(15), UI_POS_Y(9)}, + 2, + {1, 99}, + 1, + ' '}; + Checkbox checkbox_am{ + {UI_POS_X(0), UI_POS_Y(11)}, + 10, + "AM signal", + true}; + FrequencyField field_am_frequency{ + {UI_POS_X(13), UI_POS_Y(12)}}; + Checkbox checkbox_send_on_change{ + {UI_POS_X(0), UI_POS_Y(13)}, + 14, + "Send on change", + true}; + Button button_tx{ + {UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)}, + "START"}; + const Style& style_tx_start = *Theme::getInstance()->fg_green; + const Style& style_tx_stop = *Theme::getInstance()->fg_red; + + // Transmitter view + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(4), + 10000, + 12}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.progress, message.done); + }}; + + MessageHandlerRegistration message_handler_frame_sync{ + // Use frame sync for our applcation timer callback + Message::ID::DisplayFrameSync, + [this](const Message* const) { + this->on_timer(); + }}; +}; + +} // namespace ui::external_app::epirb_tx + +#endif /*__EPIRB_TX_H__*/ diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index b0b33d87d..dfa89efc4 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -83,6 +83,10 @@ set(EXTCPPSRC external/tpmsrx/main.cpp external/tpmsrx/tpms_app.cpp + #tpmstx 800 bytes - TPMS transmit with editable fields + external/tpmstx/main.cpp + external/tpmstx/tpms_tx_app.cpp + #protoview 8 byte external/protoview/main.cpp external/protoview/ui_protoview.cpp @@ -91,14 +95,22 @@ set(EXTCPPSRC external/adsbtx/main.cpp external/adsbtx/ui_adsb_tx.cpp - #morse_tx 768 bytes - external/morse_tx/main.cpp - external/morse_tx/ui_morse.cpp + #morse_tx 768 bytes -- disabled because of the new morse tx app with more functions + #external/morse_tx/main.cpp + #external/morse_tx/ui_morse.cpp #sstvtx 456 bytes external/sstvtx/main.cpp external/sstvtx/ui_sstvtx.cpp + #same_tx + external/same_tx/main.cpp + external/same_tx/ui_same_tx.cpp + + #mdc_tx + external/mdc_tx/main.cpp + external/mdc_tx/ui_mdc_tx.cpp + #sstvrx external/sstvrx/main.cpp external/sstvrx/ui_sstvrx.cpp @@ -109,8 +121,8 @@ set(EXTCPPSRC external/random_password/sha512.cpp #acars - #external/acars_rx/main.cpp - #external/acars_rx/acars_app.cpp + external/acars_rx/main.cpp + external/acars_rx/acars_app.cpp #wefax_rx 192 bytes external/wefax_rx/main.cpp @@ -222,7 +234,11 @@ set(EXTCPPSRC #gfxEQ 80 byte external/gfxeq/main.cpp - external/gfxeq/ui_gfxeq.cpp + external/gfxeq/ui_gfxeq.cpp + + #waterfall designer + external/waterfall_designer/main.cpp + external/waterfall_designer/ui_waterfall_designer.cpp #detector_rx 168 byte external/detector_rx/main.cpp @@ -248,6 +264,10 @@ set(EXTCPPSRC external/epirb_rx/main.cpp external/epirb_rx/ui_epirb_rx.cpp + #epirb_tx + external/epirb_tx/main.cpp + external/epirb_tx/ui_epirb_tx.cpp + #soundboard 272byte - 1236 bytes external/soundboard/main.cpp external/soundboard/soundboard_app.cpp @@ -276,6 +296,50 @@ set(EXTCPPSRC #subcarrx external/subcarrx/main.cpp external/subcarrx/ui_subcar.cpp + + #siggen + external/siggen/main.cpp + external/siggen/ui_siggen.cpp + + #morse_radio + external/morse_radio/main.cpp + external/morse_radio/ui_morse_radio.cpp + + #morseradiotx + external/morseradiotx/main.cpp + external/morseradiotx/ui_morse_radiotx.cpp + + external/keeloqtx/main.cpp + external/keeloqtx/ui_keeloqtx.cpp + #rtty_rx + external/rtty_rx/main.cpp + external/rtty_rx/ui_rtty_rx.cpp + external/rtty_rx/baudot.cpp + + #rtty_tx + external/rtty_tx/main.cpp + external/rtty_tx/ui_rtty_tx.cpp + external/rtty_tx/baudot.cpp + + #pocsag_tx + external/pocsag_tx/main.cpp + external/pocsag_tx/ui_pocsag_tx.cpp + + #time_sink + external/time_sink/main.cpp + external/time_sink/ui_time_sink.cpp + + #kiss_tnc + external/kiss_tnc/main.cpp + external/kiss_tnc/ui_kiss_tnc.cpp + + #fpv_detect + external/fpv_detect/main.cpp + external/fpv_detect/ui_fpv_detect.cpp + + #p25_tx + external/p25_tx/main.cpp + external/p25_tx/ui_p25_tx.cpp ) set(EXTAPPLIST @@ -298,13 +362,16 @@ set(EXTAPPLIST audio_test wardrivemap tpmsrx + tpmstx protoview adsbtx - morse_tx + #morse_tx sstvtx + same_tx + mdc_tx sstvrx random_password - # acars_rx --not working + acars_rx wefax_rx noaaapt_rx shoppingcart_lock @@ -332,12 +399,15 @@ set(EXTAPPLIST scanner level gfxeq + waterfall_designer detector_rx + fpv_detect spaceinv blackjack battleship ert epirb_rx + epirb_tx soundboard game2048 bht_tx @@ -345,4 +415,24 @@ set(EXTAPPLIST adult_toys_controller flex_rx subcarrx + siggen + morse_radio + morseradiotx + keeloqtx + rtty_rx + rtty_tx + pocsag_tx + time_sink + kiss_tnc + p25_tx ) + +# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds +if(NOT BOARD STREQUAL "PRALINE") + list(APPEND EXTCPPSRC + external/sdusb/main.cpp + external/sdusb/ui_sd_over_usb.cpp + ) + list(APPEND EXTAPPLIST sdusb) +endif() + diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 6ee5cc178..cd61633c5 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -23,73 +23,89 @@ MEMORY * Also need to consider processor memory map - reading 0xADxxxxxx generates a fault which may be better than unexpected behavior. * External app address ranges below must match those in python file "external_app_info.py". */ - ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k - ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k - ram_external_app_font_viewer(rwx) : org = 0xADB30000, len = 32k - ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k - ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k - ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k - ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k - ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k - ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k - ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k - ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k - ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k - ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k - ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k - ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k - ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k - ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k - ram_external_app_wardrivemap(rwx) : org = 0xADC20000, len = 32k - ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k - ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k - ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k - ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k - ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k - ram_external_app_random_password(rwx): org = 0xADC80000, len = 32k - ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k - ram_external_app_shoppingcart_lock(rwx): org = 0xADCA0000, len = 32k - ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k - ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k - ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k - ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k - ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k - ram_external_app_mcu_temperature(rwx): org = 0xADD00000, len = 32k - ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k - ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k - ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k - ram_external_app_app_manager(rwx) : org = 0xADD40000, len = 32k - ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k - ram_external_app_antenna_length(rwx): org = 0xADD60000, len = 32k - ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k - ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k - ram_external_app_playlist_editor(rwx): org = 0xADD90000, len = 32k - ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k - ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k - ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k - ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k - ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k - ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k - ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k - ram_external_app_level (rwx) : org = 0xADE10000, len = 32k - ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k - ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k - ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k - ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k - ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k - ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k - ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k - ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k - ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k - ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k - ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k - ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k - ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k - ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k - ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k - ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k - ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k - + ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k + ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k + ram_external_app_font_viewer (rwx) : org = 0xADB30000, len = 32k + ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k + ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k + ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k + ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k + ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k + ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k + ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k + ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k + ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k + ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k + ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k + ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k + ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k + ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k + ram_external_app_wardrivemap (rwx) : org = 0xADC20000, len = 32k + ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k + ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k + ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k + ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k + ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k + ram_external_app_random_password (rwx) : org = 0xADC80000, len = 32k + ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k + ram_external_app_shoppingcart_lock (rwx) : org = 0xADCA0000, len = 32k + ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k + ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k + ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k + ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k + ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k + ram_external_app_mcu_temperature (rwx) : org = 0xADD00000, len = 32k + ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k + ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k + ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k + ram_external_app_app_manager (rwx) : org = 0xADD40000, len = 32k + ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k + ram_external_app_antenna_length (rwx) : org = 0xADD60000, len = 32k + ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k + ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k + ram_external_app_playlist_editor (rwx) : org = 0xADD90000, len = 32k + ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k + ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k + ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k + ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k + ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k + ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k + ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k + ram_external_app_level (rwx) : org = 0xADE10000, len = 32k + ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k + ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k + ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k + ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k + ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k + ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k + ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k + ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k + ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k + ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k + ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k + ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k + ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k + ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k + ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k + ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k + ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k + ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k + ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k + ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k + ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k + ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k + ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k + ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k + ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k + ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k + ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k + ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k + ram_external_app_same_tx (rwx) : org = 0xADFE0000, len = 32k + ram_external_app_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k + ram_external_app_mdc_tx (rwx) : org = 0xAE000000, len = 32k + ram_external_app_epirb_tx (rwx) : org = 0xAE010000, len = 32k + ram_external_app_fpv_detect (rwx) : org = 0xAE020000, len = 32k + ram_external_app_p25_tx (rwx) : org = 0xAE030000, len = 32k } SECTIONS @@ -130,7 +146,7 @@ SECTIONS *(*ui*external_app*nrf_rx*); } > ram_external_app_nrf_rx - .external_app_coasterp : ALIGN(4) SUBALIGN(4) + .external_app_coasterp : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_coasterp.application_information)); *(*ui*external_app*coasterp*); @@ -178,55 +194,61 @@ SECTIONS *(*ui*external_app*tetris*); } > ram_external_app_tetris - .external_app_extsensors : ALIGN(4) SUBALIGN(4) + .external_app_extsensors : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_extsensors.application_information)); *(*ui*external_app*extsensors*); } > ram_external_app_extsensors - .external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4) + .external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_foxhunt_rx.application_information)); *(*ui*external_app*foxhunt_rx*); } > ram_external_app_foxhunt_rx - .external_app_audio_test : ALIGN(4) SUBALIGN(4) + .external_app_audio_test : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_audio_test.application_information)); *(*ui*external_app*audio_test*); } > ram_external_app_audio_test - .external_app_wardrivemap : ALIGN(4) SUBALIGN(4) + .external_app_wardrivemap : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wardrivemap.application_information)); *(*ui*external_app*wardrivemap*); } > ram_external_app_wardrivemap - .external_app_tpmsrx : ALIGN(4) SUBALIGN(4) + .external_app_tpmsrx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tpmsrx.application_information)); *(*ui*external_app*tpmsrx*); } > ram_external_app_tpmsrx - .external_app_protoview : ALIGN(4) SUBALIGN(4) + .external_app_tpmstx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_tpmstx.application_information)); + *(*ui*external_app*tpmstx*); + } > ram_external_app_tpmstx + + .external_app_protoview : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_protoview.application_information)); *(*ui*external_app*protoview*); } > ram_external_app_protoview - .external_app_adsbtx : ALIGN(4) SUBALIGN(4) + .external_app_adsbtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_adsbtx.application_information)); *(*ui*external_app*adsbtx*); } > ram_external_app_adsbtx - .external_app_morse_tx : ALIGN(4) SUBALIGN(4) + .external_app_morse_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_tx.application_information)); *(*ui*external_app*morse_tx*); } > ram_external_app_morse_tx - .external_app_sstvtx : ALIGN(4) SUBALIGN(4) + .external_app_sstvtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sstvtx.application_information)); *(*ui*external_app*sstvtx*); @@ -244,19 +266,19 @@ SECTIONS *(*ui*external_app*acars_rx*); } > ram_external_app_acars_rx - .external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4) + .external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_shoppingcart_lock.application_information)); *(*ui*external_app*shoppingcart_lock*); } > ram_external_app_shoppingcart_lock - .external_app_cvs_spam : ALIGN(4) SUBALIGN(4) + .external_app_cvs_spam : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_cvs_spam.application_information)); *(*ui*external_app*cvs_spam*); } > ram_external_app_cvs_spam - .external_app_ookbrute : ALIGN(4) SUBALIGN(4) + .external_app_ookbrute : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_ookbrute.application_information)); *(*ui*external_app*ookbrute*); @@ -268,7 +290,7 @@ SECTIONS *(*ui*external_app*ook_editor*); } > ram_external_app_ook_editor - .external_app_flippertx : ALIGN(4) SUBALIGN(4) + .external_app_flippertx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_flippertx.application_information)); *(*ui*external_app*flippertx*); @@ -352,13 +374,13 @@ SECTIONS *(*ui*external_app*stopwatch*); } > ram_external_app_stopwatch - .external_app_wefax_rx : ALIGN(4) SUBALIGN(4) + .external_app_wefax_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wefax_rx.application_information)); *(*ui*external_app*wefax_rx*); } > ram_external_app_wefax_rx - .external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4) + .external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_noaaapt_rx.application_information)); *(*ui*external_app*noaaapt_rx*); @@ -400,6 +422,12 @@ SECTIONS *(*ui*external_app*gfxeq*); } > ram_external_app_gfxeq + .external_app_waterfall_designer : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_waterfall_designer.application_information)); + *(*ui*external_app*waterfall_designer*); + } > ram_external_app_waterfall_designer + .external_app_detector_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_detector_rx.application_information)); @@ -454,14 +482,12 @@ SECTIONS *(*ui*external_app*game2048*); } > ram_external_app_game2048 - .external_app_bht_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_bht_tx.application_information)); *(*ui*external_app*bht_tx*); } > ram_external_app_bht_tx - .external_app_morse_practice : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_practice.application_information)); @@ -478,7 +504,7 @@ SECTIONS { KEEP(*(.external_app.app_flex_rx.application_information)); *(*ui*external_app*flex_rx*); - } > ram_external_app_flex_rx + } > ram_external_app_flex_rx .external_app_sstvrx : ALIGN(4) SUBALIGN(4) { @@ -492,6 +518,93 @@ SECTIONS *(*ui*external_app*subcarrx*); } > ram_external_app_subcarrx - -} + .external_app_siggen : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_siggen.application_information)); + *(*ui*external_app*siggen*); + } > ram_external_app_siggen + .external_app_sdusb : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_sdusb.application_information)); + *(*ui*external_app*sdusb*); + } > ram_external_app_sdusb + + .external_app_morse_radio : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_morse_radio.application_information)); + *(*ui*external_app*morse_radio*); + } > ram_external_app_morse_radio + + .external_app_morseradiotx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_morseradiotx.application_information)); + *(*ui*external_app*morseradiotx*); + } > ram_external_app_morseradiotx + + .external_app_keeloqtx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_keeloqtx.application_information)); + *(*ui*external_app*keeloqtx*); + } > ram_external_app_keeloqtx + + .external_app_rtty_rx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_rtty_rx.application_information)); + *(*ui*external_app*rtty_rx*); + } > ram_external_app_rtty_rx + + .external_app_rtty_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_rtty_tx.application_information)); + *(*ui*external_app*rtty_tx*); + } > ram_external_app_rtty_tx + + .external_app_pocsag_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_pocsag_tx.application_information)); + *(*ui*external_app*pocsag_tx*); + } > ram_external_app_pocsag_tx + + .external_app_time_sink : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_time_sink.application_information)); + *(*ui*external_app*time_sink*); + } > ram_external_app_time_sink + + .external_app_same_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_same_tx.application_information)); + *(*ui*external_app*same_tx*); + } > ram_external_app_same_tx + + .external_app_kiss_tnc : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_kiss_tnc.application_information)); + *(*ui*external_app*kiss_tnc*); + } > ram_external_app_kiss_tnc + + .external_app_mdc_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_mdc_tx.application_information)); + *(*ui*external_app*mdc_tx*); + } > ram_external_app_mdc_tx + + .external_app_epirb_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_epirb_tx.application_information)); + *(*ui*external_app*epirb_tx*); + } > ram_external_app_epirb_tx + + .external_app_fpv_detect : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_fpv_detect.application_information)); + *(*ui*external_app*fpv_detect*); + } > ram_external_app_fpv_detect + + .external_app_p25_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_p25_tx.application_information)); + *(*ui*external_app*p25_tx*); + } > ram_external_app_p25_tx +} diff --git a/firmware/application/external/extsensors/main.cpp b/firmware/application/external/extsensors/main.cpp index 4597dd19c..970e2b295 100644 --- a/firmware/application/external/extsensors/main.cpp +++ b/firmware/application/external/extsensors/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_extsensors.application_information"), 0xF0, 0x0F, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::DEBUG, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/flex_rx/main.cpp b/firmware/application/external/flex_rx/main.cpp index 6d6faa7dc..a475ee4b3 100644 --- a/firmware/application/external/flex_rx/main.cpp +++ b/firmware/application/external/flex_rx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_flex_rx.application_information"), use 0x00, 0x00, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::RX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/flippertx/main.cpp b/firmware/application/external/flippertx/main.cpp index 3dde72c26..dc5f8117d 100644 --- a/firmware/application/external/flippertx/main.cpp +++ b/firmware/application/external/flippertx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_flippertx.application_information"), u 0xE0, 0x07, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::TX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/flippertx/ui_flippertx.cpp b/firmware/application/external/flippertx/ui_flippertx.cpp index 03572ed00..45165c380 100644 --- a/firmware/application/external/flippertx/ui_flippertx.cpp +++ b/firmware/application/external/flippertx/ui_flippertx.cpp @@ -80,7 +80,7 @@ bool FlipperTxView::on_file_changed(std::filesystem::path new_file_path) { return false; } preset = submeta.value().preset; - if (preset != FLIPPER_PRESET_OOK) { + if (preset != FLIPPER_PRESET_OOK && preset != FLIPPER_PRESET_2FSK) { field_filename.set_text("File: err, not supp. preset"); return false; } @@ -108,8 +108,13 @@ void FlipperTxView::stop() { bool FlipperTxView::start() { if (filename.empty()) return false; baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + transmitter_model.set_sampling_rate(OOK_SAMPLERATE); transmitter_model.enable(); button_startstop.set_text(LanguageHelper::currentMessages[LANG_STOP]); + uint8_t mode = 0; + if (preset == FLIPPER_PRESET_2FSK) + mode = 1; + baseband::set_bitstream_config(FM_DEVIATION, mode); // start thread replay_thread = std::make_unique( filename, diff --git a/firmware/application/external/flippertx/ui_flippertx.hpp b/firmware/application/external/flippertx/ui_flippertx.hpp index 0f30ab342..9775c5ffa 100644 --- a/firmware/application/external/flippertx/ui_flippertx.hpp +++ b/firmware/application/external/flippertx/ui_flippertx.hpp @@ -28,6 +28,8 @@ using namespace ui; namespace ui::external_app::flippertx { #define OOK_SAMPLERATE 2280000U +#define FM_DEVIATION 60000U + class FlipperPlayThread; class FlipperTxView : public View { public: diff --git a/firmware/application/external/fmradio/ui_fmradio.cpp b/firmware/application/external/fmradio/ui_fmradio.cpp index 73927a89e..bb4fcfbb9 100644 --- a/firmware/application/external/fmradio/ui_fmradio.cpp +++ b/firmware/application/external/fmradio/ui_fmradio.cpp @@ -165,7 +165,7 @@ FmRadioView::FmRadioView(NavigationView& nav) &gr}); txt_save_help.set_focusable(false); - txt_save_help.visible(false); + txt_save_help.hidden(true); for (uint8_t i = 0; i < 12; ++i) { if (freq_fav_list[i].frequency == 0) { freq_fav_list[i].frequency = 87000000; @@ -195,7 +195,7 @@ FmRadioView::FmRadioView(NavigationView& nav) btn_fav_save.on_select = [this](Button&) { save_fav = !save_fav; txt_save_help.set_text(save_fav ? "Select slot" : ""); - txt_save_help.visible(save_fav); + txt_save_help.hidden(!save_fav); txt_save_help.set_dirty(); }; @@ -229,9 +229,9 @@ void FmRadioView::on_btn_clicked(uint8_t i) { freq_fav_list[i].modulation = field_modulation.selected_index_value(); freq_fav_list[i].bandwidth = radio_bw; update_fav_btn_texts(); - txt_save_help.visible(save_fav); + txt_save_help.hidden(true); txt_save_help.set_text(""); - txt_save_help.set_dirty(); + set_dirty(); return; } field_frequency.set_value(freq_fav_list[i].frequency); @@ -266,7 +266,7 @@ FmRadioView::~FmRadioView() { } void FmRadioView::on_audio_spectrum() { - if (gr.visible() && audio_spectrum_data) gr.update_audio_spectrum(*audio_spectrum_data); + if (gr.drawn() && audio_spectrum_data) gr.update_audio_spectrum(*audio_spectrum_data); if (audio_spectrum_data && audio_spectrum_data->db.size() <= 128) { for (size_t i = 0; i < audio_spectrum_data->db.size(); ++i) { audio_spectrum[i] = ((int16_t)audio_spectrum_data->db[i] - 127) * 256; diff --git a/firmware/application/external/fpv_detect/main.cpp b/firmware/application/external/fpv_detect/main.cpp new file mode 100644 index 000000000..d8348ec4b --- /dev/null +++ b/firmware/application/external/fpv_detect/main.cpp @@ -0,0 +1,65 @@ +// Author: berkeozkir (Berke Γ–zkΔ±r) + +#include "ui.hpp" +#include "ui_fpv_detect.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::fpv_detect { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::fpv_detect + +extern "C" { + +__attribute__((section(".external_app.app_fpv_detect.application_information"), used)) application_information_t _application_information_fpv_detect = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::fpv_detect::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "FPV DETECT", + /*.bitmap_data = */ + { + 0x00, + 0x00, + 0xC0, + 0x03, + 0x20, + 0x04, + 0x10, + 0x08, + 0x48, + 0x12, + 0xA4, + 0x25, + 0x54, + 0x2A, + 0xA8, + 0x15, + 0x54, + 0x2A, + 0xA4, + 0x25, + 0x48, + 0x12, + 0x10, + 0x08, + 0x20, + 0x04, + 0xC0, + 0x03, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_wfm_audio */ {'P', 'W', 'F', 'M'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/fpv_detect/ui_fpv_detect.cpp b/firmware/application/external/fpv_detect/ui_fpv_detect.cpp new file mode 100644 index 000000000..7fd62af94 --- /dev/null +++ b/firmware/application/external/fpv_detect/ui_fpv_detect.cpp @@ -0,0 +1,666 @@ +// Author: berkeozkir (Berke Γ–zkΔ±r) + +/* + * FPV RX β€” how frequency search and lock work + * + * 1. Power metric: We use channelized power (baseband IQ magnitudeΒ² in the + * capture bandwidth), not raw RF RSSI. Stats come from ChannelStatsCollector + * over filtered IQ, so we see power in the tuned channel, not wideband. + * + * 2. Scanning: We step through FPV bands/channels (A/B/E/F/R, 8 ch each). When + * power on the current channel exceeds the detect threshold, we enter + * "Candidate" and run verification β€” we do not lock on a single peak. + * + * 3. Verification (making sure the drone is on that freq): + * - Multiple samples: verify_hits / verify_misses over several updates. + * - Confidence score: combines average above threshold, peak margin, + * hit count, and crucially neighbor_margin = center power βˆ’ max(adjacent ch). + * - Neighbor check: we only lock if the center channel is at least + * MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB above both adjacent channels. That + * avoids locking on wide/noise that looks strong on many channels. + * During Candidate we re-sample left/right adjacent channels so the + * margin is based on current measurements, not stale scan data. + * - Lock requires: enough hits, confidence β‰₯ LOCK_CONFIDENCE_MIN, and + * center stronger than neighbors (strong lock path has same requirement). + * + * 4. Lock hold: Once locked, we only unlock when power stays below + * unlock_threshold_db() long enough to drain lock_hold_ (down from + * LOCK_HOLD_MAX), so brief fades don't drop lock. + */ + +#include "ui_fpv_detect.hpp" + +#include + +#include "baseband_api.hpp" +#include "oversample.hpp" + +using namespace portapack; + +namespace ui::external_app::fpv_detect { + +static constexpr uint8_t SCAN_DWELL_FRAMES = 1; +static constexpr uint8_t VERIFY_SAMPLE_TARGET = 5; +static constexpr uint8_t VERIFY_MIN_HITS = 4; +static constexpr uint8_t STRONG_LOCK_PEAK_MARGIN_DB = 8; +static constexpr uint8_t LOCK_HOLD_MAX = 12; +static constexpr uint8_t LOCK_CONFIDENCE_MIN = 72; +/** Center channel must be at least this many dB above adjacent channels to lock. + * Ensures we lock on a real FPV carrier (narrow peak) rather than wide/noise. */ +static constexpr int32_t MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB = 3; +static constexpr uint8_t BEEP_GUARD_FRAMES = 8; + +int32_t FpvDetectView::clamp_value(int32_t value, int32_t low, int32_t high) { + if (value < low) return low; + if (value > high) return high; + return value; +} + +int32_t FpvDetectView::map_value(int32_t value, int32_t from_low, int32_t from_high, int32_t to_low, int32_t to_high) { + const auto clamped = clamp_value(value, from_low, from_high); + return to_low + (clamped - from_low) * (to_high - to_low) / (from_high - from_low); +} + +void FpvDetectView::focus() { + field_lna.focus(); +} + +FpvDetectView::~FpvDetectView() { + shared_memory.request_m4_performance_counter = 0; + receiver_model.disable(); + audio::output::stop(); + baseband::shutdown(); +} + +uint8_t FpvDetectView::channel_index(uint8_t band, uint8_t ch) const { + return static_cast(band * FPV_CHANNELS_PER_BAND + ch); +} + +uint8_t FpvDetectView::current_channel_index() const { + return channel_index(scan_band, scan_ch); +} + +int16_t FpvDetectView::detect_threshold_db() const { + return static_cast(detect_threshold_db_); +} + +int16_t FpvDetectView::unlock_threshold_db() const { + return static_cast(detect_threshold_db() - 6); +} + +int16_t FpvDetectView::candidate_average_db() const { + if (!verify_samples_) { + return -120; + } + return static_cast(verify_sum_db_ / verify_samples_); +} + +int16_t FpvDetectView::neighbor_best_db() const { + int16_t best = -120; + + if (candidate_ch_ > 0) { + const auto& left = channel_memory_[channel_index(candidate_band_, static_cast(candidate_ch_ - 1))]; + if (left.last_db > best) { + best = left.last_db; + } + } + + if ((candidate_ch_ + 1) < FPV_CHANNELS_PER_BAND) { + const auto& right = channel_memory_[channel_index(candidate_band_, static_cast(candidate_ch_ + 1))]; + if (right.last_db > best) { + best = right.last_db; + } + } + + return best; +} + +void FpvDetectView::request_event_beep(uint32_t hz, uint32_t duration_ms) { + if ((frame_counter_ - last_beep_frame_) < BEEP_GUARD_FRAMES) { + return; + } + last_beep_frame_ = frame_counter_; + baseband::request_audio_beep(hz, 24000, duration_ms); +} + +void FpvDetectView::retune_to(uint8_t band, uint8_t ch) { + scan_band = band; + scan_ch = ch; + freq_ = fpv_frequencies[band][ch]; + receiver_model.set_target_frequency(freq_); + update_freq_display(); +} + +void FpvDetectView::step_scan() { + if (band_mode < FPV_NUM_BANDS) { + scan_band = band_mode; + scan_ch = static_cast((scan_ch + 1) % FPV_CHANNELS_PER_BAND); + } else { + scan_ch = static_cast(scan_ch + 1); + if (scan_ch >= FPV_CHANNELS_PER_BAND) { + scan_ch = 0; + scan_band = static_cast(scan_band + 1); + if (scan_band >= FPV_NUM_BANDS) { + scan_band = 0; + } + } + } + + retune_to(scan_band, scan_ch); + scan_dwell_frames_ = SCAN_DWELL_FRAMES; +} + +void FpvDetectView::reset_detector(bool retune_current) { + detect_state_ = DetectState::Scanning; + candidate_band_ = scan_band; + candidate_ch_ = scan_ch; + verify_samples_ = 0; + verify_hits_ = 0; + verify_misses_ = 0; + verify_sum_db_ = 0; + verify_peak_db_ = -120; + candidate_confidence_ = 0; + lock_hold_ = 0; + neighbor_phase_ = 0; + neighbors_fresh_ = false; + scan_dwell_frames_ = 0; + + update_state_badge(); + update_confidence_text(); + update_status_text(); + update_detail_text(); + update_visual_state(); + + if (retune_current) { + retune_to(scan_band, scan_ch); + scan_dwell_frames_ = SCAN_DWELL_FRAMES; + } +} + +bool FpvDetectView::is_possible_analog_carrier(const ChannelStatistics& statistics) const { + return statistics.max_db >= detect_threshold_db(); +} + +uint8_t FpvDetectView::compute_candidate_confidence() const { + if (!verify_samples_) { + return 0; + } + + const int32_t avg_db = candidate_average_db(); + const int32_t center_margin = avg_db - detect_threshold_db(); + const int32_t peak_margin = verify_peak_db_ - detect_threshold_db(); + const int32_t neighbor_margin = avg_db - neighbor_best_db(); + + int32_t score = 18; + score += center_margin * 7; + score += peak_margin * 4; + score += static_cast(verify_hits_) * 5; + score -= static_cast(verify_misses_) * 10; + + if (neighbor_margin >= 6) { + score += 12; + } else if (neighbor_margin >= 3) { + score += 6; + } else if (neighbor_margin <= 0) { + score -= 8; + } + + const auto& mem = channel_memory_[channel_index(candidate_band_, candidate_ch_)]; + if (mem.hits >= 2) { + score += 4; + } + + return static_cast(clamp_value(score, 0, 99)); +} + +void FpvDetectView::enter_candidate(const ChannelStatistics& statistics) { + detect_state_ = DetectState::Candidate; + candidate_band_ = scan_band; + candidate_ch_ = scan_ch; + verify_samples_ = 1; + verify_hits_ = 1; + verify_misses_ = 0; + verify_sum_db_ = statistics.max_db; + verify_peak_db_ = statistics.max_db; + candidate_confidence_ = compute_candidate_confidence(); + neighbor_phase_ = 0; + neighbors_fresh_ = false; + + retune_to(candidate_band_, candidate_ch_); + + update_state_badge(); + update_confidence_text(); + update_status_text(); + update_detail_text(); + update_visual_state(); + request_event_beep(1150, 60); +} + +void FpvDetectView::evaluate_candidate_sample(const ChannelStatistics& statistics) { + if (detect_state_ != DetectState::Candidate) { + return; + } + + ++verify_samples_; + verify_sum_db_ += statistics.max_db; + if (statistics.max_db > verify_peak_db_) { + verify_peak_db_ = statistics.max_db; + } + + if (statistics.max_db >= detect_threshold_db()) { + ++verify_hits_; + } else { + ++verify_misses_; + } + + candidate_confidence_ = compute_candidate_confidence(); + update_confidence_text(); + update_detail_text(); + + /* Refresh adjacent channel power so neighbor_best_db() uses current + * measurements instead of stale scan data. Do this before any lock decision. */ + if (verify_samples_ >= 2 && !neighbors_fresh_) { + if (candidate_ch_ > 0) { + neighbor_phase_ = 1; + retune_to(candidate_band_, static_cast(candidate_ch_ - 1)); + } else { + neighbor_phase_ = 2; + retune_to(candidate_band_, static_cast(candidate_ch_ + 1)); + } + return; + } + + /* Lock decisions use neighbor margin; only run when we have fresh neighbor data. */ + if (!neighbors_fresh_) { + return; + } + + const bool strong_lock = + verify_samples_ >= 3 && + verify_hits_ >= 3 && + verify_peak_db_ >= (detect_threshold_db() + STRONG_LOCK_PEAK_MARGIN_DB) && + candidate_confidence_ >= 80 && + (candidate_average_db() - neighbor_best_db()) >= MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB; + + if (strong_lock) { + enter_lock(); + return; + } + + if (verify_samples_ < VERIFY_SAMPLE_TARGET) { + return; + } + + const bool center_stronger_than_neighbors = + (candidate_average_db() - neighbor_best_db()) >= MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB; + if (verify_hits_ >= VERIFY_MIN_HITS && candidate_confidence_ >= LOCK_CONFIDENCE_MIN && + center_stronger_than_neighbors) { + enter_lock(); + return; + } + + detect_state_ = DetectState::Scanning; + candidate_confidence_ = static_cast(candidate_confidence_ / 2); + verify_samples_ = 0; + verify_hits_ = 0; + verify_misses_ = 0; + verify_sum_db_ = 0; + verify_peak_db_ = -120; + scan_dwell_frames_ = 0; + + update_state_badge(); + update_confidence_text(); + update_status_text(); + update_detail_text(); + update_visual_state(); +} + +void FpvDetectView::enter_lock() { + detect_state_ = DetectState::Locked; + lock_hold_ = LOCK_HOLD_MAX; + candidate_confidence_ = static_cast(clamp_value(candidate_confidence_, LOCK_CONFIDENCE_MIN, 99)); + + retune_to(candidate_band_, candidate_ch_); + + update_state_badge(); + update_confidence_text(); + update_status_text(); + update_detail_text(); + update_visual_state(); + + request_event_beep(1850, 180); +} + +void FpvDetectView::update_lock(const ChannelStatistics& statistics) { + if (detect_state_ != DetectState::Locked) { + return; + } + + if (statistics.max_db >= unlock_threshold_db()) { + if (lock_hold_ < LOCK_HOLD_MAX) { + ++lock_hold_; + } + if (candidate_confidence_ < 99) { + ++candidate_confidence_; + } + } else { + if (lock_hold_ > 0) { + --lock_hold_; + } + if (candidate_confidence_ > 0) { + --candidate_confidence_; + } + } + + update_confidence_text(); + update_detail_text(); + + if (lock_hold_ > 0) { + return; + } + + detect_state_ = DetectState::Scanning; + candidate_confidence_ = 0; + verify_samples_ = 0; + verify_hits_ = 0; + verify_misses_ = 0; + verify_sum_db_ = 0; + verify_peak_db_ = -120; + scan_dwell_frames_ = 0; + + update_state_badge(); + update_confidence_text(); + update_status_text(); + update_detail_text(); + update_visual_state(); + + request_event_beep(650, 90); +} + +void FpvDetectView::update_freq_display() { + char buf[28]; + std::snprintf(buf, sizeof(buf), "%c%d %ld MHz", + band_labels[scan_band], + static_cast(scan_ch + 1), + static_cast(freq_ / 1000000LL)); + text_freq.set(buf); +} + +void FpvDetectView::update_state_badge() { + switch (detect_state_) { + case DetectState::Scanning: + text_state.set("SCANNING"); + break; + case DetectState::Candidate: + text_state.set("VERIFY FPV"); + break; + case DetectState::Locked: + text_state.set("DRONE FOUND"); + break; + } +} + +void FpvDetectView::update_confidence_text() { + char buf[16]; + std::snprintf(buf, sizeof(buf), "Conf %u%%", static_cast(candidate_confidence_)); + text_confidence.set(buf); +} + +void FpvDetectView::update_status_text() { + char buf[40]; + + switch (detect_state_) { + case DetectState::Scanning: + if (band_mode < FPV_NUM_BANDS) { + std::snprintf(buf, sizeof(buf), "Scanning band %c for analog FPV", band_labels[band_mode]); + } else { + std::snprintf(buf, sizeof(buf), "Scanning all FPV bands"); + } + break; + + case DetectState::Candidate: + std::snprintf(buf, sizeof(buf), "VERIFYING %c%d %ld MHz", + band_labels[candidate_band_], + static_cast(candidate_ch_ + 1), + static_cast(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL)); + break; + + case DetectState::Locked: + std::snprintf(buf, sizeof(buf), "!!! DRONE FOUND !!! %c%d %ld", + band_labels[candidate_band_], + static_cast(candidate_ch_ + 1), + static_cast(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL)); + break; + } + + text_status.set(buf); +} + +void FpvDetectView::update_detail_text() { + char buf[40]; + + switch (detect_state_) { + case DetectState::Scanning: + std::snprintf(buf, sizeof(buf), "Need >= %d dB, stable dwell", static_cast(detect_threshold_db())); + break; + + case DetectState::Candidate: + std::snprintf(buf, sizeof(buf), "avg %d peak %d hits %u/%u", + static_cast(candidate_average_db()), + static_cast(verify_peak_db_), + static_cast(verify_hits_), + static_cast(verify_samples_)); + break; + + case DetectState::Locked: + std::snprintf(buf, sizeof(buf), "LOCKED %c%d conf %u%% hold %u", + band_labels[candidate_band_], + static_cast(candidate_ch_ + 1), + static_cast(candidate_confidence_), + static_cast(lock_hold_)); + break; + } + + text_detail.set(buf); +} + +void FpvDetectView::update_visual_state() { + auto* theme = Theme::getInstance(); + + /* + * Conservative styling for cross-branch compatibility. + * If your branch has explicit blue/red banner theme styles, + * swap the styles in this function only. + */ + auto normal_style = theme->bg_darkest; + auto attention_style = theme->fg_red; + + switch (detect_state_) { + case DetectState::Scanning: + text_state.set_style(normal_style); + text_status.set_style(normal_style); + break; + case DetectState::Candidate: + text_state.set_style(normal_style); + text_status.set_style(normal_style); + break; + case DetectState::Locked: + text_state.set_style(attention_style); + text_status.set_style(attention_style); + break; + } + + freq_stats_rssi.set_style(normal_style); + freq_stats_db.set_style(normal_style); + text_detail.set_style(normal_style); + text_confidence.set_style(normal_style); +} + +void FpvDetectView::update_stats_text(const ChannelStatistics& statistics) { + rssi_graph.add_values(rssi.get_min(), rssi.get_avg(), rssi.get_max(), statistics.max_db); + + auto& mem = channel_memory_[current_channel_index()]; + mem.last_db = statistics.max_db; + if (statistics.max_db > mem.peak_db) { + mem.peak_db = statistics.max_db; + } + if (statistics.max_db >= detect_threshold_db()) { + if (mem.hits < 255) { + ++mem.hits; + } + } + mem.confidence = candidate_confidence_; + + if (last_max_db_ != statistics.max_db) { + last_max_db_ = statistics.max_db; + char power_buf[20]; + std::snprintf(power_buf, sizeof(power_buf), "PWR %d dB", static_cast(statistics.max_db)); + freq_stats_db.set(power_buf); + rssi.set_db(statistics.max_db); + } + + const uint8_t graph_min = rssi_graph.get_graph_min(); + const uint8_t graph_avg = rssi_graph.get_graph_avg(); + const uint8_t graph_max = rssi_graph.get_graph_max(); + + if (last_min_rssi_ != graph_min || last_avg_rssi_ != graph_avg || last_max_rssi_ != graph_max) { + last_min_rssi_ = graph_min; + last_avg_rssi_ = graph_avg; + last_max_rssi_ = graph_max; + + char rssi_buf[22]; + std::snprintf(rssi_buf, sizeof(rssi_buf), "RSSI %u/%u/%u", + static_cast(graph_min), + static_cast(graph_avg), + static_cast(graph_max)); + freq_stats_rssi.set(rssi_buf); + } +} + +void FpvDetectView::on_timer() { + ++frame_counter_; + + if (detect_state_ != DetectState::Scanning) { + return; + } + + if (scan_dwell_frames_ > 0) { + --scan_dwell_frames_; + return; + } + + step_scan(); +} + +FpvDetectView::FpvDetectView(NavigationView& nav) + : nav_{nav} { + add_children({ + &labels, + &field_lna, + &field_vga, + &field_rf_amp, + &field_volume, + &field_band, + &text_freq, + &text_state, + &text_confidence, + &text_detect_label, + &field_detect_threshold, + &freq_stats_rssi, + &freq_stats_db, + &text_status, + &text_detail, + &rssi, + &rssi_graph, + }); + + rssi.set_vertical_rssi(true); + rssi.set_peak(true, 3000); + rssi_graph.set_nb_columns(256); + + field_detect_threshold.set_value(detect_threshold_db_); + field_detect_threshold.on_change = [this](int32_t v) { + detect_threshold_db_ = v; + update_detail_text(); + }; + + field_band.on_change = [this](size_t, int32_t value) { + band_mode = static_cast(value); + scan_band = (band_mode < FPV_NUM_BANDS) ? band_mode : 0; + scan_ch = 0; + reset_detector(true); + }; + + change_mode(); + field_band.set_selected_index(FPV_AUTO_SCAN_MODE); + reset_detector(true); +} + +void FpvDetectView::on_statistics_update(const ChannelStatistics& statistics) { + const bool is_sampling_neighbor = + (detect_state_ == DetectState::Candidate && (neighbor_phase_ == 1 || neighbor_phase_ == 2)); + if (!is_sampling_neighbor) { + update_stats_text(statistics); + } + + switch (detect_state_) { + case DetectState::Scanning: + if (is_possible_analog_carrier(statistics)) { + enter_candidate(statistics); + } + break; + + case DetectState::Candidate: + if (neighbor_phase_ == 1) { + const auto idx = channel_index(candidate_band_, static_cast(candidate_ch_ - 1)); + channel_memory_[idx].last_db = statistics.max_db; + if (candidate_ch_ + 1 < FPV_CHANNELS_PER_BAND) { + neighbor_phase_ = 2; + retune_to(candidate_band_, static_cast(candidate_ch_ + 1)); + } else { + neighbor_phase_ = 0; + neighbors_fresh_ = true; + retune_to(candidate_band_, candidate_ch_); + } + break; + } + if (neighbor_phase_ == 2) { + const auto idx = channel_index(candidate_band_, static_cast(candidate_ch_ + 1)); + channel_memory_[idx].last_db = statistics.max_db; + neighbor_phase_ = 0; + neighbors_fresh_ = true; + retune_to(candidate_band_, candidate_ch_); + break; + } + evaluate_candidate_sample(statistics); + break; + + case DetectState::Locked: + update_lock(statistics); + break; + } +} + +size_t FpvDetectView::change_mode() { + audio::output::stop(); + receiver_model.disable(); + baseband::shutdown(); + + audio_sampling_rate = audio::Rate::Hz_24000; + baseband::run_image(portapack::spi_flash::image_tag_capture); + receiver_model.set_modulation(ReceiverModel::Mode::Capture); + + baseband::set_sample_rate(FPV_RX_BW, get_oversample_rate(FPV_RX_BW)); + auto actual_sampling_rate = get_actual_sample_rate(FPV_RX_BW); + receiver_model.set_sampling_rate(actual_sampling_rate); + receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate)); + + audio::set_rate(audio_sampling_rate); + audio::output::start(); + receiver_model.set_headphone_volume(receiver_model.headphone_volume()); + receiver_model.enable(); + + return 0; +} + +} // namespace ui::external_app::fpv_detect diff --git a/firmware/application/external/fpv_detect/ui_fpv_detect.hpp b/firmware/application/external/fpv_detect/ui_fpv_detect.hpp new file mode 100644 index 000000000..a0e950273 --- /dev/null +++ b/firmware/application/external/fpv_detect/ui_fpv_detect.hpp @@ -0,0 +1,200 @@ +// Author: berkeozkir (Berke Γ–zkΔ±r) + +#ifndef _UI_FPV_DETECT +#define _UI_FPV_DETECT + +#include +#include + +#include "analog_audio_app.hpp" +#include "app_settings.hpp" +#include "audio.hpp" +#include "baseband_api.hpp" +#include "radio_state.hpp" +#include "receiver_model.hpp" +#include "string_format.hpp" +#include "ui.hpp" +#include "ui_receiver.hpp" +#include "ui_spectrum.hpp" + +namespace ui::external_app::fpv_detect { + +static constexpr uint8_t FPV_NUM_BANDS = 5; +static constexpr uint8_t FPV_CHANNELS_PER_BAND = 8; +static constexpr uint8_t FPV_TOTAL_CHANNELS = FPV_NUM_BANDS * FPV_CHANNELS_PER_BAND; +static constexpr uint8_t FPV_AUTO_SCAN_MODE = FPV_NUM_BANDS; + +static constexpr int64_t fpv_frequencies[FPV_NUM_BANDS][FPV_CHANNELS_PER_BAND] = { + /* Band A */ {5865000000LL, 5845000000LL, 5825000000LL, 5805000000LL, 5785000000LL, 5765000000LL, 5745000000LL, 5725000000LL}, + /* Band B */ {5733000000LL, 5752000000LL, 5771000000LL, 5790000000LL, 5809000000LL, 5828000000LL, 5847000000LL, 5866000000LL}, + /* Band E */ {5705000000LL, 5685000000LL, 5665000000LL, 5645000000LL, 5885000000LL, 5905000000LL, 5925000000LL, 5945000000LL}, + /* Band F */ {5740000000LL, 5760000000LL, 5780000000LL, 5800000000LL, 5820000000LL, 5840000000LL, 5860000000LL, 5880000000LL}, + /* Band R */ {5658000000LL, 5695000000LL, 5732000000LL, 5769000000LL, 5806000000LL, 5843000000LL, 5880000000LL, 5917000000LL}, +}; + +static constexpr char band_labels[] = {'A', 'B', 'E', 'F', 'R'}; + +#define FPV_RX_BW 750000 + +class FpvDetectView : public View { + public: + explicit FpvDetectView(NavigationView& nav); + ~FpvDetectView(); + + void focus() override; + std::string title() const override { return "FPV DETECT"; }; + + private: + enum class DetectState : uint8_t { + Scanning, + Candidate, + Locked, + }; + + struct ChannelMemory { + int16_t last_db = -120; + int16_t peak_db = -120; + uint8_t hits = 0; + uint8_t confidence = 0; + }; + + NavigationView& nav_; + RxRadioState radio_state_{}; + + static int32_t clamp_value(int32_t value, int32_t low, int32_t high); + static int32_t map_value(int32_t value, int32_t from_low, int32_t from_high, int32_t to_low, int32_t to_high); + + size_t change_mode(); + void on_statistics_update(const ChannelStatistics& statistics); + void on_timer(); + + void retune_to(uint8_t band, uint8_t ch); + void step_scan(); + void reset_detector(bool retune_current); + + bool is_possible_analog_carrier(const ChannelStatistics& statistics) const; + void enter_candidate(const ChannelStatistics& statistics); + void evaluate_candidate_sample(const ChannelStatistics& statistics); + void enter_lock(); + void update_lock(const ChannelStatistics& statistics); + + uint8_t current_channel_index() const; + uint8_t channel_index(uint8_t band, uint8_t ch) const; + uint8_t compute_candidate_confidence() const; + int16_t detect_threshold_db() const; + int16_t unlock_threshold_db() const; + int16_t candidate_average_db() const; + int16_t neighbor_best_db() const; + + void request_event_beep(uint32_t hz, uint32_t duration_ms); + void update_freq_display(); + void update_state_badge(); + void update_confidence_text(); + void update_status_text(); + void update_detail_text(); + void update_stats_text(const ChannelStatistics& statistics); + void update_visual_state(); + + DetectState detect_state_ = DetectState::Scanning; + + uint8_t scan_band = 0; + uint8_t scan_ch = 0; + uint8_t band_mode = FPV_AUTO_SCAN_MODE; + + uint8_t candidate_band_ = 0; + uint8_t candidate_ch_ = 0; + uint8_t verify_samples_ = 0; + uint8_t verify_hits_ = 0; + uint8_t verify_misses_ = 0; + int32_t verify_sum_db_ = 0; + int16_t verify_peak_db_ = -120; + uint8_t candidate_confidence_ = 0; + uint8_t lock_hold_ = 0; + /** During Candidate: 0=center, 1=left neighbor, 2=right neighbor. Used to + * re-sample adjacent channels so neighbor_margin uses current measurements. */ + uint8_t neighbor_phase_ = 0; + bool neighbors_fresh_ = false; + + uint8_t scan_dwell_frames_ = 0; + uint32_t frame_counter_ = 0; + uint32_t last_beep_frame_ = 0; + + int16_t last_max_db_ = -127; + uint8_t last_min_rssi_ = 0; + uint8_t last_avg_rssi_ = 0; + uint8_t last_max_rssi_ = 0; + + std::array channel_memory_{}; + + rf::Frequency freq_ = {fpv_frequencies[0][0]}; + int32_t detect_threshold_db_ = -38; + audio::Rate audio_sampling_rate = audio::Rate::Hz_48000; + + app_settings::SettingsManager settings_{ + "rx_fpv", + app_settings::Mode::RX, + { + {"detect_threshold"sv, &detect_threshold_db_}, + }}; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(0)}, "LNA: VGA: AMP: ", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_RIGHT(6), UI_POS_Y(0)}, "VOL: ", Theme::getInstance()->fg_light->foreground}, + }; + + LNAGainField field_lna{{UI_POS_X(4), UI_POS_Y(0)}}; + VGAGainField field_vga{{UI_POS_X(11), UI_POS_Y(0)}}; + RFAmpField field_rf_amp{{UI_POS_X(18), UI_POS_Y(0)}}; + AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; + + OptionsField field_band{ + {UI_POS_X(0), UI_POS_Y(1)}, + 9, + { + {"Band A ", 0}, + {"Band B ", 1}, + {"Band E ", 2}, + {"Band F ", 3}, + {"Band R ", 4}, + {"AutoScan", FPV_AUTO_SCAN_MODE}, + }}; + + Text text_freq{{UI_POS_X_RIGHT(20), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT}, ""}; + + Text text_state{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT}, "SCANNING"}; + Text text_confidence{{UI_POS_X(12), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, "Conf 0%"}; + Text text_detect_label{{UI_POS_X_RIGHT(10), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_DEFAULT_HEIGHT}, "Thr>"}; + + NumberField field_detect_threshold{ + {UI_POS_X_RIGHT(5), UI_POS_Y(2)}, + 4, + {-100, 20}, + 1, + ' ', + }; + + Text freq_stats_rssi{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, "RSSI 0/0/0"}; + Text freq_stats_db{{UI_POS_X_RIGHT(14), UI_POS_Y(3), UI_POS_WIDTH(14), UI_POS_DEFAULT_HEIGHT}, "PWR -120 dB"}; + + Text text_status{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "SCANNING FOR ANALOG FPV"}; + Text text_detail{{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "Waiting for FPV-like carrier"}; + + RSSIGraph rssi_graph{{UI_POS_X(0), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(5), UI_POS_HEIGHT_REMAINING(7)}}; + RSSI rssi{{UI_POS_X_RIGHT(5), UI_POS_Y(6), UI_POS_WIDTH(5), UI_POS_HEIGHT_REMAINING(7)}}; + + MessageHandlerRegistration message_handler_stats{ + Message::ID::ChannelStatistics, + [this](const Message* const p) { + this->on_statistics_update(static_cast(p)->statistics); + }}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + this->on_timer(); + }}; +}; + +} // namespace ui::external_app::fpv_detect + +#endif diff --git a/firmware/application/external/game2048/ui_game2048.cpp b/firmware/application/external/game2048/ui_game2048.cpp index 694d951da..4e68a5b1b 100644 --- a/firmware/application/external/game2048/ui_game2048.cpp +++ b/firmware/application/external/game2048/ui_game2048.cpp @@ -33,7 +33,7 @@ Game2048View::Game2048View(NavigationView& nav) void Game2048View::on_show() { if (!initialized) { - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); reset_game(); initialized = true; } diff --git a/firmware/application/external/keeloqtx/main.cpp b/firmware/application/external/keeloqtx/main.cpp new file mode 100644 index 000000000..a7252e095 --- /dev/null +++ b/firmware/application/external/keeloqtx/main.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_navigation.hpp" +#include "ui_keeloqtx.hpp" +#include "external_app.hpp" + +namespace ui::external_app::ui_keeloqtx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::ui_keeloqtx + +extern "C" { + +__attribute__((section(".external_app.app_keeloqtx.application_information"), used)) application_information_t _application_information_keeloqtx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::ui_keeloqtx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + /*.app_name = */ "KeeLoq TX", + /*.bitmap_data = */ { + 0x20, + 0x00, + 0x20, + 0x00, + 0x20, + 0x00, + 0x20, + 0x00, + 0xE0, + 0x07, + 0xF0, + 0x0F, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0xF0, + 0x0F, + 0xF0, + 0x0F, + 0x70, + 0x0D, + 0xB0, + 0x0E, + 0x70, + 0x0D, + 0xB0, + 0x0E, + 0xF0, + 0x0F, + 0xE0, + 0x07, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + /*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/keeloqtx/ui_keeloqtx.cpp b/firmware/application/external/keeloqtx/ui_keeloqtx.cpp new file mode 100644 index 000000000..eb753f15c --- /dev/null +++ b/firmware/application/external/keeloqtx/ui_keeloqtx.cpp @@ -0,0 +1,248 @@ +#include "ui_keeloqtx.hpp" +#include "ui_fileman.hpp" +#include "baseband_api.hpp" +#include "../../keeloq_common.hpp" +#include "../../baseband/fprotos/fprotogeneral.hpp" + +using namespace portapack; +using namespace ui; + +namespace fs = std::filesystem; + +namespace ui::external_app::ui_keeloqtx { + +void KeeloqTXView::focus() { + button_open.focus(); +} + +void KeeloqTXView::update_hop() { + hop = data.btn << 28 | (data.serial & 0x3FF) << 16 | data.counter; + + if (data.mf_name == "Aprimatic") { + uint32_t apri_serial = data.serial; + uint8_t apr1 = 0; + + for (uint16_t i = 1; i != 0b10000000000; i <<= 1) { + if (apri_serial & i) apr1++; + } + + apri_serial &= 0b00001111111111; + + if (apr1 % 2 == 0) { + apri_serial |= 0b110000000000; + } + + hop = data.btn << 28 | (apri_serial & 0xFFF) << 16 | data.counter; + } else if ( + data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449") { + hop = data.btn << 28 | (data.serial & 0xFFF) << 16 | data.counter; + } else if ( + data.mf_name == "NICE_Smilo" || data.mf_name == "NICE_MHOUSE" || data.mf_name == "JCM_Tech") { + hop = data.btn << 28 | (data.serial & 0xFF) << 16 | data.counter; + } else if (data.mf_name == "Merlin") { + hop = data.btn << 28 | (0x000) << 16 | data.counter; + } else if (data.mf_name == "Centurion") { + hop = data.btn << 28 | (0x1CE) << 16 | data.counter; + } else if (data.mf_name == "Monarch") { + hop = data.btn << 28 | (0x100) << 16 | data.counter; + } else if (data.mf_name == "Dea_Mio") { + uint8_t first_disc_num = (data.serial >> 8) & 0xF; + uint8_t result_disc = (0xC + (first_disc_num % 4)); + + uint32_t dea_serial = (data.serial & 0xFF) | (((uint32_t)result_disc) << 8); + + hop = data.btn << 28 | (dea_serial & 0xFFF) << 16 | data.counter; + } + + text_hop.set(to_string_hex(hop)); +} + +void KeeloqTXView::update_payload() { + uint64_t encrypt = 0; + + switch (current_key.type) { + case KEELOQ_SIMPLE_LEARNING: { + encrypt = keeloq_encrypt(hop, current_key.key); + + break; + } + case KEELOQ_NORMAL_LEARNING: { + uint64_t man = keeloq_normal_learning(fix, current_key.key); + + encrypt = keeloq_encrypt(hop, man); + + break; + } + } + + payload = (uint64_t)fix << 32 | encrypt; + + uint64_t preview_payload = FProtoGeneral::subghz_protocol_blocks_reverse_key(payload, 64); + + text_payload.set(to_string_hex(preview_payload)); + encode_data(); +} + +void KeeloqTXView::encode_data() { + std::string fragments{}; + + for (uint32_t i = 0; i < 64; ++i) { + fragments += keeloq_fragments[bit(payload, i)]; + } + + encoded_data = KEELOQ_HEADER + fragments + "1001"; + + if (data.mf_name == "Sommer") { + pause_duration = 28; + } else { + pause_duration = 39; + } +} + +KeeloqTXView::KeeloqTXView(NavigationView& nav) + : nav_{nav} { + baseband::run_image(portapack::spi_flash::image_tag_ook); + + add_children({&labels, + &text_mf_name, + &text_serial, + &text_fix, + &text_hop, + &field_counter, + &field_button, + &field_repeat, + &text_payload, + &button_open, + &text_status, + &progressbar, + &tx_view}); + + field_counter.on_change = [this](uint32_t value) { + data.counter = (uint16_t)value; + + update_hop(); + update_payload(); + }; + + field_button.on_change = [this](uint32_t value) { + data.btn = (uint8_t)value; + + fix &= 0xFFFFFFF; + fix |= data.btn << 28; + + text_fix.set(to_string_hex(fix)); + + update_hop(); + update_payload(); + }; + + field_repeat.on_change = [this](uint32_t value) { + repeat = value; + }; + + field_repeat.set_value(4, true); + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + transmitter_model.set_target_frequency(f); + }; + }; + + tx_view.on_start = [this]() { + start_tx(); + }; + + tx_view.on_stop = [this]() { + baseband::kill_ook(); + stop_tx(); + }; + + button_open.on_select = [this](const Button&) { + auto open_view = nav_.push(".KEELOQ"); + + ensure_directory(keeloq_remotes_dir); + open_view->push_dir(keeloq_remotes_dir); + open_view->on_changed = [this](fs::path path) { + nav_.set_on_pop([this, path]() { + if (!read_keeloq_file(path, data)) { + return; + } + + file_path = path; + + for (const auto& key : keystore.get_keys()) { + if (key.mf_name == data.mf_name) { + current_key = key; + + break; + } + } + + fix = data.btn << 28 | data.serial; + + update_hop(); + + text_mf_name.set(data.mf_name); + text_serial.set(to_string_hex(data.serial)); + text_fix.set(to_string_hex(fix)); + + field_counter.set_value(data.counter, false); + field_button.set_value(data.btn, false); + + update_payload(); + + field_counter.focus(); + }); + }; + }; +} + +void KeeloqTXView::start_tx() { + size_t bitstream_length = encoders::make_bitstream(encoded_data); + + progressbar.set_max(repeat); + tx_view.set_transmitting(true); + + transmitter_model.enable(); + + baseband::set_ook_data( + bitstream_length, + OOK_SAMPLERATE * (400.0 / 1000000.0), + repeat, + pause_duration); +} + +void KeeloqTXView::stop_tx() { + transmitter_model.disable(); + text_status.set("Ready"); + progressbar.set_value(0); + tx_view.set_transmitting(false); + + data.counter += 1; + + field_counter.set_value(data.counter, false); + + update_hop(); + update_payload(); + + write_keeloq_file(file_path, data); +} + +void KeeloqTXView::on_tx_progress(uint32_t progress, bool done) { + if (!done) { + text_status.set(to_string_dec_uint(progress + 1) + "/" + to_string_dec_uint(repeat)); + progressbar.set_value(progress + 1); + } else { + chThdSleepMilliseconds(10); + + stop_tx(); + } +} + +KeeloqTXView::~KeeloqTXView() { + transmitter_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::ui_keeloqtx diff --git a/firmware/application/external/keeloqtx/ui_keeloqtx.hpp b/firmware/application/external/keeloqtx/ui_keeloqtx.hpp new file mode 100644 index 000000000..9c081f6ce --- /dev/null +++ b/firmware/application/external/keeloqtx/ui_keeloqtx.hpp @@ -0,0 +1,129 @@ +#ifndef __KEELOQTX__ +#define __KEELOQTX__ + +#include "ui.hpp" +#include "ui_transmitter.hpp" +#include "transmitter_model.hpp" +#include "file_path.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "encoders.hpp" +#include "string_format.hpp" +#include "../../keeloq_file.hpp" +#include "../../keeloq_keystore.hpp" + +#define KEELOQ_HEADER "101010101010101010101010000000000" + +namespace ui::external_app::ui_keeloqtx { + +class KeeloqTXView : public View { + public: + KeeloqTXView(NavigationView& nav); + ~KeeloqTXView(); + + void focus() override; + + std::string title() const override { + return "KeeLoqTX"; + } + + private: + const std::string keeloq_fragments[2] = {"110", "100"}; + + NavigationView& nav_; + + TxRadioState radio_state_{ + 433920000, + 1750000, + OOK_SAMPLERATE}; + + app_settings::SettingsManager settings_{ + "tx_keeloq", app_settings::Mode::TX}; + + std::filesystem::path file_path{}; + + KeeloqKeystore keystore{}; + KeeloqKey current_key{}; + KeeloqData data{}; + + uint32_t fix = 0; + uint32_t hop = 0; + uint64_t payload = 0; + + void update_hop(); + + Labels labels{ + {{UI_POS_X(1), UI_POS_Y(0)}, "Manufacturer:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(1)}, "Serial:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(2)}, "Fix:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(3)}, "Hop:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(4)}, "Counter:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(5)}, "Button:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(6)}, "Repeat:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(2), UI_POS_Y(8)}, "Payload:", Theme::getInstance()->fg_light->foreground}, + }; + + Text text_mf_name{{UI_POS_X(15), UI_POS_Y(0), 128, 16}, ""}; + Text text_serial{{UI_POS_X(9), UI_POS_Y(1), 64, 16}, "00000000"}; + Text text_fix{{UI_POS_X(6), UI_POS_Y(2), 64, 16}, "00000000"}; + Text text_hop{{UI_POS_X(6), UI_POS_Y(3), 64, 16}, "00000000"}; + + NumberField field_counter{ + {UI_POS_X(10), UI_POS_Y(4)}, + 5, + {0, 65535}, + 1, + ' '}; + NumberField field_button{ + {UI_POS_X(9), UI_POS_Y(5)}, + 2, + {0, 15}, + 1, + ' '}; + NumberField field_repeat{ + {UI_POS_X(10), UI_POS_Y(6)}, + 3, + {0, 100}, + 1, + ' '}; + + Text text_payload{{UI_POS_X(2), UI_POS_Y(9), 128, 16}, "0000000000000000"}; + + void update_payload(); + + Button button_open{{UI_POS_X(0), UI_POS_Y(11), screen_width, 32}, "Open file"}; + + Text text_status{{UI_POS_X(2), UI_POS_Y_BOTTOM(7), 128, 16}, "Ready"}; + + ProgressBar progressbar{ + {UI_POS_X(2), UI_POS_Y_BOTTOM(7) + 20, UI_POS_WIDTH_REMAINING(4), 16}}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(4), + 50000, + 9}; + + std::string encoded_data{}; + + void encode_data(); + + uint32_t repeat = 4; + uint32_t pause_duration = 0; + + void start_tx(); + void stop_tx(); + + void update_progress(); + void on_tx_progress(uint32_t progress, bool done); + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.progress, message.done); + }}; +}; + +} // namespace ui::external_app::ui_keeloqtx + +#endif diff --git a/firmware/application/external/kiss_tnc/main.cpp b/firmware/application/external/kiss_tnc/main.cpp new file mode 100644 index 000000000..9c2392f9c --- /dev/null +++ b/firmware/application/external/kiss_tnc/main.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_kiss_tnc.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::kiss_tnc { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::kiss_tnc + +extern "C" { + +__attribute__((section(".external_app.app_kiss_tnc.application_information"), used)) application_information_t _application_information_kiss_tnc = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::kiss_tnc::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "KISS TNC", + /*.bitmap_data = */ { + // 16x16 icon - radio tower / TNC + 0x00, + 0x00, + 0x08, + 0x00, + 0x14, + 0x00, + 0x22, + 0x00, + 0x08, + 0x00, + 0x08, + 0x00, + 0x08, + 0x00, + 0x3E, + 0x00, + 0x22, + 0x00, + 0x22, + 0x00, + 0x22, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x1C, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::TRX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_aprs_rx */ {'P', 'A', 'P', 'R'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/kiss_tnc/ui_kiss_tnc.cpp b/firmware/application/external/kiss_tnc/ui_kiss_tnc.cpp new file mode 100644 index 000000000..4511b53e8 --- /dev/null +++ b/firmware/application/external/kiss_tnc/ui_kiss_tnc.cpp @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2024 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_kiss_tnc.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "receiver_model.hpp" +#include "transmitter_model.hpp" +#include "string_format.hpp" +#include "memory_map.hpp" +#include "ax25.hpp" +#include "spi_image.hpp" + +extern "C" { +#include "usb_serial_device_to_host.h" +} + +using namespace portapack; + +namespace ui::external_app::kiss_tnc { + +namespace { +KissTncView* g_kiss_tnc_instance = nullptr; +} + +void KissTncView::kiss_input_trampoline(const uint8_t* data, size_t len) { + if (g_kiss_tnc_instance) g_kiss_tnc_instance->on_kiss_bytes(data, len); +} + +KissTncView::KissTncView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + add_children({&field_frequency, + &field_rf_amp, + &field_lna, + &field_vga, + &rssi, + &channel, + &text_status, + &text_usb_status, + &text_rx_count, + &text_tx_count, + &labels, + &console}); + receiver_model.enable(); + baseband::set_aprs(1200); + g_kiss_tnc_instance = this; + usb_input_handler_.emplace(kiss_input_trampoline); + update_stats(); +} + +KissTncView::~KissTncView() { + g_kiss_tnc_instance = nullptr; + usb_input_handler_.reset(); + if (tx_active_) { + transmitter_model.disable(); + } else { + receiver_model.disable(); + } + baseband::shutdown(); +} + +void KissTncView::focus() { + field_frequency.focus(); +} + +void KissTncView::update_stats() { + text_rx_count.set(to_string_dec_uint(rx_count_)); + text_tx_count.set(to_string_dec_uint(tx_count_)); + text_usb_status.set(portapack::usb_serial.serial_connected() ? "Connected" : "Disconnected"); +} + +void KissTncView::send_kiss_frame(const uint8_t* data, size_t len) { + // Stack-allocate output buffer: max KISS overhead is 2x data + 2 FENDs + 1 cmd byte + uint8_t buf[512]; + size_t i = 0; + buf[i++] = 0xC0; // FEND + buf[i++] = 0x00; // data command + for (size_t j = 0; j < len; j++) { + if (data[j] == 0xC0) { + if (i + 3 > sizeof(buf)) break; // need 2 for escape + 1 for final FEND + buf[i++] = 0xDB; + buf[i++] = 0xDC; + } else if (data[j] == 0xDB) { + if (i + 3 > sizeof(buf)) break; // need 2 for escape + 1 for final FEND + buf[i++] = 0xDB; + buf[i++] = 0xDD; + } else { + if (i + 2 > sizeof(buf)) break; // need 1 for data + 1 for final FEND + buf[i++] = data[j]; + } + } + buf[i++] = 0xC0; // FEND + if (usb_input_handler_) + usb_input_handler_->write(buf, i); +} + +void KissTncView::on_packet(const APRSPacketMessage* message) { + aprs::APRSPacket pkt = message->packet; + + uint8_t payload_size = pkt.size(); + if (payload_size > 2) { + uint8_t raw[256]; + size_t raw_len = payload_size - 2; + for (size_t i = 0; i < raw_len; i++) + raw[i] = static_cast(pkt[i]); + send_kiss_frame(raw, raw_len); + } + + console.writeln(pkt.get_source_formatted() + ">" + pkt.get_destination_formatted()); + rx_count_++; + update_stats(); +} + +void KissTncView::on_kiss_bytes(const uint8_t* data, size_t len) { + for (size_t i = 0; i < len; i++) { + uint8_t b = data[i]; + switch (kiss_state_) { + case KissState::IDLE: + if (b == 0xC0) kiss_state_ = KissState::CMD; + break; + case KissState::CMD: + if (b == 0xC0) break; + if (b == 0x00) { + kiss_idx_ = 0; + kiss_state_ = KissState::DATA; + } else { + kiss_state_ = KissState::IDLE; + } + break; + case KissState::DATA: + if (b == 0xC0) { + if (kiss_idx_ > 0) process_kiss_frame(); + kiss_idx_ = 0; + kiss_state_ = KissState::CMD; + } else if (b == 0xDB) { + kiss_state_ = KissState::ESC; + } else if (kiss_idx_ < sizeof(kiss_buf_)) { + kiss_buf_[kiss_idx_++] = b; + } + break; + case KissState::ESC: + if (b == 0xDC) { + if (kiss_idx_ < sizeof(kiss_buf_)) kiss_buf_[kiss_idx_++] = 0xC0; + } else if (b == 0xDD) { + if (kiss_idx_ < sizeof(kiss_buf_)) kiss_buf_[kiss_idx_++] = 0xDB; + } + kiss_state_ = KissState::DATA; + break; + } + } +} + +void KissTncView::process_kiss_frame() { + if (tx_active_ || kiss_idx_ == 0) return; + // AFSK shared buffer is 512 bytes (256 uint16_t words). Each raw byte + // produces ~10 encoded bits after bit-stuffing + flags, so cap at 200 bytes + // to ensure the bitstream plus required 0-word terminator always fits. + if (kiss_idx_ > 200) return; + start_tx(); +} + +void KissTncView::start_tx() { + tx_active_ = true; + + ax25::AX25Frame frame; + frame.make_frame_from_raw(kiss_buf_, kiss_idx_); + + rx_frequency_ = receiver_model.target_frequency(); + receiver_model.disable(); + baseband::shutdown(); + + baseband::run_image(portapack::spi_flash::image_tag_afsk); + + transmitter_model.set_target_frequency(rx_frequency_); + transmitter_model.set_sampling_rate(AFSK_SAMPLE_RATE); + transmitter_model.set_baseband_bandwidth(AFSK_BASEBAND_BW); + transmitter_model.enable(); + + baseband::set_afsk_data(AFSK_SAMPLE_RATE / 1200, 1200, 2200, 1, 10000, 8); + + text_status.set("Transmitting"); + tx_count_++; + update_stats(); +} + +void KissTncView::finish_tx() { + transmitter_model.disable(); + baseband::shutdown(); + + // Must reload from SPI flash β€” run_image(afsk) overwrote m4_code.base() + baseband::run_image(portapack::spi_flash::image_tag_aprs_rx); + + receiver_model.set_target_frequency(rx_frequency_); + receiver_model.set_sampling_rate(APRS_RX_SAMPLE_RATE); + receiver_model.set_baseband_bandwidth(APRS_RX_BASEBAND_BW); + receiver_model.enable(); + baseband::set_aprs(1200); + tx_active_ = false; + text_status.set("Listening"); + update_stats(); +} + +} // namespace ui::external_app::kiss_tnc diff --git a/firmware/application/external/kiss_tnc/ui_kiss_tnc.hpp b/firmware/application/external/kiss_tnc/ui_kiss_tnc.hpp new file mode 100644 index 000000000..18168fa36 --- /dev/null +++ b/firmware/application/external/kiss_tnc/ui_kiss_tnc.hpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_freq_field.hpp" +#include "ui_rssi.hpp" +#include "radio_state.hpp" +#include "app_settings.hpp" +#include "message.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "receiver_model.hpp" +#include "aprs_packet.hpp" +#include "usb_serial_host_to_device.hpp" + +#include + +namespace ui::external_app::kiss_tnc { + +class KissTncView : public View { + public: + KissTncView(NavigationView& nav); + ~KissTncView(); + + void focus() override; + std::string title() const override { return "KISS TNC"; } + + private: + static constexpr rf::Frequency APRS_FREQ_DEFAULT = 144390000; + static constexpr uint32_t APRS_RX_SAMPLE_RATE = 3072000U; + static constexpr uint32_t APRS_RX_BASEBAND_BW = 1750000U; + static constexpr uint32_t AFSK_SAMPLE_RATE = 1536000U; + static constexpr uint32_t AFSK_BASEBAND_BW = 1750000U; + + NavigationView& nav_; + + // CRITICAL: RxRadioState must be declared before SettingsManager + RxRadioState radio_state_{ + APRS_FREQ_DEFAULT, + APRS_RX_BASEBAND_BW, + APRS_RX_SAMPLE_RATE}; + + app_settings::SettingsManager settings_{"kiss_tnc", app_settings::Mode::RX}; + + RxFrequencyField field_frequency{ + {0, 0}, + nav_}; + RFAmpField field_rf_amp{{13 * 8, 0}}; + LNAGainField field_lna{{15 * 8, 0}}; + VGAGainField field_vga{{18 * 8, 0}}; + RSSI rssi{{21 * 8, 0, 6 * 8, 4}}; + Channel channel{{21 * 8, 5, 6 * 8, 4}}; + + Text text_status{{0, 16, 240, 16}, "Listening"}; + Text text_usb_status{{4 * 8, 2 * 16, 14 * 8, 16}, "Disconnected"}; + Text text_rx_count{{3 * 8, 3 * 16, 5 * 8, 16}, "0"}; + Text text_tx_count{{11 * 8, 3 * 16, 5 * 8, 16}, "0"}; + + Labels labels{{ + {{0, 2 * 16}, "USB:", Color::light_grey()}, + {{0, 3 * 16}, "RX:", Color::light_grey()}, + {{8 * 8, 3 * 16}, "TX:", Color::light_grey()}, + }}; + + Console console{{0, 4 * 16, 240, 180}}; + + enum class KissState : uint8_t { IDLE, + CMD, + DATA, + ESC }; + KissState kiss_state_{KissState::IDLE}; + uint8_t kiss_buf_[350]{}; + size_t kiss_idx_{0}; + + bool tx_active_{false}; + rf::Frequency rx_frequency_{APRS_FREQ_DEFAULT}; + uint32_t rx_count_{0}; + uint32_t tx_count_{0}; + + void on_packet(const APRSPacketMessage* message); + void on_kiss_bytes(const uint8_t* data, size_t len); + void process_kiss_frame(); + void send_kiss_frame(const uint8_t* data, size_t len); + void start_tx(); + void finish_tx(); + void update_stats(); + + uint32_t frame_counter_{0}; + + static void kiss_input_trampoline(const uint8_t* data, size_t len); + std::optional usb_input_handler_{}; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::APRSPacket, + [this](Message* const p) { + on_packet(reinterpret_cast(p)); + }}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](Message* const p) { + auto* msg = reinterpret_cast(p); + if (msg->done) finish_tx(); + }}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + if (++frame_counter_ == 60) { + frame_counter_ = 0; + text_usb_status.set(portapack::usb_serial.serial_connected() ? "Connected" : "Disconnected"); + } + }}; +}; + +} // namespace ui::external_app::kiss_tnc diff --git a/firmware/application/external/level/ui_level.cpp b/firmware/application/external/level/ui_level.cpp index e8034f31d..d5608b6b3 100644 --- a/firmware/application/external/level/ui_level.cpp +++ b/firmware/application/external/level/ui_level.cpp @@ -137,6 +137,7 @@ LevelView::LevelView(NavigationView& nav) else v = SPEC_MODULATION; field_mode.set_selected_index(v); + return; // Important ! Guru will occur if you don't return when modifying field from within on_change ! } last_mode = v; change_mode(v); diff --git a/firmware/application/external/mdc_tx/main.cpp b/firmware/application/external/mdc_tx/main.cpp new file mode 100644 index 000000000..93956a7f8 --- /dev/null +++ b/firmware/application/external/mdc_tx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_mdc_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::mdc_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::mdc_tx + +extern "C" { + +__attribute__((section(".external_app.app_mdc_tx.application_information"), used)) +application_information_t _application_information_mdc_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::mdc_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "MDC-1200 TX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x7F, + 0xFF, + 0xFF, + 0xBB, + 0xD0, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x0B, + 0xE1, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xEB, + 0xD0, + 0xFF, + 0xFF, + 0xFE, + 0x7F, + 0x70, + 0x00, + 0x30, + 0x00, + 0x10, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + /*.m4_app_tag = */ {'P', 'A', 'F', 'T'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/mdc_tx/ui_mdc_tx.cpp b/firmware/application/external/mdc_tx/ui_mdc_tx.cpp new file mode 100644 index 000000000..44693f3ce --- /dev/null +++ b/firmware/application/external/mdc_tx/ui_mdc_tx.cpp @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_mdc_tx.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "portapack_shared_memory.hpp" +#include "transmitter_model.hpp" +#include "ui/ui_receiver.hpp" + +using namespace portapack; + +namespace ui::external_app::mdc_tx { + +static constexpr uint32_t MDC_SAMPLES_PER_BIT = AFSK_TX_SAMPLERATE / 1200U; +static constexpr uint32_t MDC_MARK_FREQ = 1800U; +static constexpr uint32_t MDC_SPACE_FREQ = 1200U; +static constexpr uint32_t MDC_FM_DEVIATION = 2500U; + +static constexpr uint8_t MDC_PREAMBLE[7] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}; +static constexpr uint8_t MDC_SYNC[5] = {0x07, 0x09, 0x2A, 0x44, 0x6F}; +static constexpr int MDC_NUM_PREAMBLES = 3; + +static constexpr uint8_t MDC_DP_OP = 0x35; +static constexpr uint8_t MDC_DP_ARG = 0x89; +static constexpr uint8_t MDC_DP_CALL_ARG = 0x0D; + +static uint8_t mdc_flip8(uint8_t in) { + uint8_t out = 0; + for (int i = 0; i < 8; i++) { + out = static_cast((out << 1) | (in & 1)); + in >>= 1; + } + return out; +} + +static uint16_t mdc_flip16(uint16_t in) { + uint16_t out = 0; + for (int i = 0; i < 16; i++) { + out = static_cast((out << 1) | (in & 1)); + in >>= 1; + } + return out; +} + +static uint16_t mdc_crc(const uint8_t* data, int length) { + uint16_t crc = 0; + for (int i = 0; i < length; i++) { + uint16_t c = mdc_flip8(data[i]); + for (int j = 0x80; j != 0; j >>= 1) { + uint16_t bit = crc & 0x8000; + crc = static_cast(crc << 1); + if (c & static_cast(j)) bit ^= 0x8000; + if (bit) crc ^= 0x1021; + } + } + crc = mdc_flip16(crc); + crc ^= 0xFFFF; + return crc; +} + +// CRC + convolutional encoding + bit interleaving for a 14-byte codeword. +// Uses lbits_ (class member) so no large allocation hits the stack. +void MdcTxView::pack_codeword(uint8_t cw[14]) { + uint16_t crc = mdc_crc(cw, 4); + cw[4] = static_cast(crc & 0xFF); + cw[5] = static_cast((crc >> 8) & 0xFF); + cw[6] = 0x00; + + // Convolutional encoding β€” csr[7] is only 7 bytes, fine on stack + uint8_t csr[7] = {}; + for (int i = 0; i < 7; i++) { + cw[i + 7] = 0; + for (int j = 0; j <= 7; j++) { + for (int k = 6; k > 0; k--) csr[k] = csr[k - 1]; + csr[0] = (cw[i] >> j) & 1; + int b = csr[0] + csr[2] + csr[5] + csr[6]; + cw[i + 7] |= static_cast((b & 1) << j); + } + } + + // Bit interleaving β€” uses lbits_ class member (112 bytes off the stack) + for (int i = 0; i < 112; i++) lbits_[i] = 0; + int l = 0, m = 0; + for (int i = 0; i < 14; i++) { + for (int j = 0; j <= 7; j++) { + lbits_[l] = (cw[i] >> j) & 1; + l += 16; + if (l > 111) { + m++; + l = m; + } + } + } + l = 0; + for (int i = 0; i < 14; i++) { + cw[i] = 0; + for (int j = 7; j >= 0; j--) { + if (lbits_[l]) cw[i] |= static_cast(1 << j); + l++; + } + } +} + +// Writes preamble + sync + one packed codeword into tx_stream_, returns length. +int MdcTxView::build_single(uint8_t op, uint8_t arg, uint8_t id_hi, uint8_t id_lo) { + int idx = 0; + for (int i = 0; i < MDC_NUM_PREAMBLES; i++) + for (int j = 0; j < 7; j++) tx_stream_[idx++] = MDC_PREAMBLE[j]; + for (int j = 0; j < 5; j++) tx_stream_[idx++] = MDC_SYNC[j]; + + uint8_t cw[14] = {}; + cw[0] = op; + cw[1] = arg; + cw[2] = id_hi; + cw[3] = id_lo; + pack_codeword(cw); + for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw[j]; + return idx; +} + +// Writes preamble + sync + two packed codewords into tx_stream_, returns length. +int MdcTxView::build_double(uint8_t op1, uint8_t arg1, uint8_t id1_hi, uint8_t id1_lo, uint8_t op2, uint8_t arg2, uint8_t id2_hi, uint8_t id2_lo) { + int idx = 0; + for (int i = 0; i < MDC_NUM_PREAMBLES; i++) + for (int j = 0; j < 7; j++) tx_stream_[idx++] = MDC_PREAMBLE[j]; + for (int j = 0; j < 5; j++) tx_stream_[idx++] = MDC_SYNC[j]; + + uint8_t cw1[14] = {}; + cw1[0] = op1; + cw1[1] = arg1; + cw1[2] = id1_hi; + cw1[3] = id1_lo; + pack_codeword(cw1); + for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw1[j]; + + uint8_t cw2[14] = {}; + cw2[0] = op2; + cw2[1] = arg2; + cw2[2] = id2_hi; + cw2[3] = id2_lo; + pack_codeword(cw2); + for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw2[j]; + return idx; +} + +MdcTxView::MdcTxView(NavigationView& nav) + : nav_{nav} { + baseband::run_image(portapack::spi_flash::image_tag_afsk); + + add_children({ + &labels_, + &field_op, + &field_id_hi, + &field_id_lo, + &field_arg, + &field_src_hi, + &field_src_lo, + &text_status, + &tx_view, + }); + + tx_view.on_start = [this]() { + if (!tx_active_) { + start_tx(); + tx_view.set_transmitting(true); + } + }; + tx_view.on_stop = [this]() { stop_tx(); }; + + transmitter_model.set_tx_gain(30); + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + transmitter_model.set_target_frequency(f); + }; + }; + + field_op.on_change = [this](size_t, int32_t op) { + switch (op) { + case 0xFE: + field_arg.set_value(0x0C); + break; // Unstun + case 0x2B: + field_arg.set_value(0x00); + break; // Stun + case 0x63: + field_arg.set_value(0x85); + break; // Radio Check + case 0x11: + field_arg.set_value(0x8A); + break; // Remote Monitor + default: + field_arg.set_value(0x00); + break; + } + }; +} + +MdcTxView::~MdcTxView() { + transmitter_model.disable(); + baseband::shutdown(); +} + +void MdcTxView::focus() { + field_op.focus(); +} + +void MdcTxView::start_tx() { + int32_t raw_op = field_op.selected_index_value(); + uint8_t op = (raw_op == 0xFE) ? 0x2B : static_cast(raw_op); + uint8_t arg = static_cast(field_arg.value()); + uint8_t id_hi = static_cast(field_id_hi.value()); + uint8_t id_lo = static_cast(field_id_lo.value()); + uint8_t src_hi = static_cast(field_src_hi.value()); + uint8_t src_lo = static_cast(field_src_lo.value()); + + int stream_len; + bool is_double = (op == 0x80 || op == 0x81 || op == 0x82); + if (is_double) { + stream_len = build_double(MDC_DP_OP, MDC_DP_ARG, id_hi, id_lo, + op, MDC_DP_CALL_ARG, src_hi, src_lo); + } else { + stream_len = build_single(op, arg, id_hi, id_lo); + } + + // NRZI encode tx_stream_ into shared memory words + auto* words = reinterpret_cast(shared_memory.bb_data.data); + uint8_t prev_bit = 0; + for (int i = 0; i < stream_len; i++) { + uint8_t raw = tx_stream_[i]; + uint8_t nrzi = 0; + for (int bit = 7; bit >= 0; bit--) { + uint8_t rb = (raw >> bit) & 1; + uint8_t nb = rb ^ prev_bit; + prev_bit = rb; + nrzi = static_cast((nrzi << 1) | nb); + } + words[i] = nrzi; + } + words[stream_len] = 0; + + tx_active_ = true; + text_status.set("TX..."); + transmitter_model.enable(); + baseband::set_afsk_data( + MDC_SAMPLES_PER_BIT, + MDC_MARK_FREQ, + MDC_SPACE_FREQ, + 1, + MDC_FM_DEVIATION, + 8); +} + +void MdcTxView::stop_tx() { + baseband::kill_afsk(); + transmitter_model.disable(); + tx_active_ = false; + text_status.set("Ready"); + tx_view.set_transmitting(false); +} + +void MdcTxView::on_tx_progress(uint32_t, bool done) { + if (done) { + stop_tx(); + text_status.set("Done!"); + } +} + +} // namespace ui::external_app::mdc_tx diff --git a/firmware/application/external/mdc_tx/ui_mdc_tx.hpp b/firmware/application/external/mdc_tx/ui_mdc_tx.hpp new file mode 100644 index 000000000..3fe80762e --- /dev/null +++ b/firmware/application/external/mdc_tx/ui_mdc_tx.hpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_MDC_TX_H__ +#define __UI_MDC_TX_H__ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "message.hpp" +#include "modems.hpp" + +namespace ui::external_app::mdc_tx { + +class MdcTxView : public View { + public: + MdcTxView(NavigationView& nav); + ~MdcTxView(); + MdcTxView(const MdcTxView&) = delete; + MdcTxView& operator=(const MdcTxView&) = delete; + + void focus() override; + std::string title() const override { return "MDC-1200 TX"; } + + private: + NavigationView& nav_; + + // TxRadioState MUST be declared before SettingsManager + TxRadioState radio_state_{ + 462562500, + 1750000, + AFSK_TX_SAMPLERATE}; + app_settings::SettingsManager settings_{ + "tx_mdc_tx", + app_settings::Mode::TX}; + + Labels labels_{{ + {{UI_POS_X(0), UI_POS_Y(0)}, "Op:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(1)}, "ID:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(13), UI_POS_Y(1)}, "Arg:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Src:", ui::Theme::getInstance()->fg_light->foreground}, + }}; + + // Only one OptionsField allowed per external app + // Stun=Op 0x2B Arg 0x00, Unstun=Op 0x2B Arg 0x0C (auto-set via on_change) + OptionsField field_op{ + {UI_POS_X(4), UI_POS_Y(0)}, + 14, + {{"PTT ID", 0x01}, + {"Emergency", 0x00}, + {"Radio Check", 0x63}, + {"Stun", 0x2B}, + {"Unstun", 0xFE}, // sentinel: op=0x2B, arg=0x0C + {"Call Alert", 0x81}, + {"Sel Call 1", 0x80}, + {"Sel Call 2", 0x82}, + {"Status Req", 0x22}, + {"Status Resp", 0x06}, + {"Rem Monitor", 0x11}, + {"Message", 0x07}}}; + + // Unit ID: two decimal bytes (0-255) combined into a 16-bit ID + NumberField field_id_hi{{UI_POS_X(4), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'}; + NumberField field_id_lo{{UI_POS_X(7), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'}; + + // Argument byte (decimal 0-255, auto-set by op selection) + NumberField field_arg{{UI_POS_X(17), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'}; + + // Source ID: two decimal bytes (0-255) combined into a 16-bit ID + NumberField field_src_hi{{UI_POS_X(4), UI_POS_Y(2)}, 3, {0, 255}, 1, '0'}; + NumberField field_src_lo{{UI_POS_X(7), UI_POS_Y(2)}, 3, {0, 255}, 1, '0'}; + + Text text_status{{0, UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_DEFAULT_HEIGHT}, "Ready"}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(4), + 12500, + 0}; + + bool tx_active_{false}; + + // Class-owned scratch buffers β€” keep large allocations off the stack + uint8_t tx_stream_[128]{}; + uint8_t lbits_[112]{}; + + void pack_codeword(uint8_t cw[14]); + int build_single(uint8_t op, uint8_t arg, uint8_t id_hi, uint8_t id_lo); + int build_double(uint8_t op1, uint8_t arg1, uint8_t id1_hi, uint8_t id1_lo, uint8_t op2, uint8_t arg2, uint8_t id2_hi, uint8_t id2_lo); + void start_tx(); + void stop_tx(); + void on_tx_progress(uint32_t progress, bool done); + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto msg = *reinterpret_cast(p); + this->on_tx_progress(msg.progress, msg.done); + }}; +}; + +} // namespace ui::external_app::mdc_tx + +#endif diff --git a/firmware/application/external/morse_practice/morsedecoder.hpp b/firmware/application/external/morse_practice/morsedecoder.hpp index 4e1058f04..bd73f936d 100644 --- a/firmware/application/external/morse_practice/morsedecoder.hpp +++ b/firmware/application/external/morse_practice/morsedecoder.hpp @@ -24,8 +24,7 @@ #include #include - -#define MORSEDEC_ERROR "" +#include "string_format.hpp" namespace ui::external_app::morse_practice { @@ -89,21 +88,28 @@ class MorseDecoder { }; MorseDecoder() {} - DecodeResult handleInput(int32_t duration_ms) { + + DecodeResult + handleInput(int32_t duration_ms) { DecodeResult result = {"", 0.0}; + + if (duration_ms < 5 && duration_ms > -5) return result; + if (duration_ms > 0) { pulse_history_.push_back(duration_ms); double dah_prob = getDahProbability(duration_ms); current_sequence_ += (dah_prob > 0.5) ? '-' : '.'; last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob); last_sequence_ = current_sequence_; + } else { uint32_t gap_duration = -duration_ms; pulse_gaps_.push_back(gap_duration); if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) { result.text = lookupMorse(current_sequence_); - result.confidence = (result.text != MORSEDEC_ERROR) ? last_confidence_ : 0.0; + + result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0; if (gap_duration >= getInterWordThreshold()) { result.text += " "; } @@ -116,9 +122,9 @@ class MorseDecoder { return result; } - inline double getInterElementThreshold() { return time_unit_ms_ * 2.0; } - inline double getInterCharThreshold() { return time_unit_ms_ * 4.0; } - inline double getInterWordThreshold() { return time_unit_ms_ * 7.0; } + inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; } + inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; } + inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; } inline double getCurrentTimeUnit() { return time_unit_ms_; } inline std::string getLastSequence() { return last_sequence_; } @@ -126,7 +132,7 @@ class MorseDecoder { private: std::string current_sequence_ = ""; std::string last_sequence_ = ""; - double time_unit_ms_ = 160.0; + double time_unit_ms_ = 119.0; double last_confidence_ = 0.0; MorseRingBuffer pulse_history_{}; MorseRingBuffer pulse_gaps_{}; @@ -136,7 +142,7 @@ class MorseDecoder { if (seq == morse_table_[i].code) return morse_table_[i].letter; } - return MORSEDEC_ERROR; // not found + return "{" + seq + "}"; // not found } double getDahProbability(uint32_t duration_ms) { @@ -298,8 +304,8 @@ class MorseDecoder { time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor); } - size_t morse_table_size_ = 41; - MorseEntry morse_table_[41] = { + size_t morse_table_size_ = 50; + MorseEntry morse_table_[50] = { {".-", "A"}, {"-...", "B"}, {"-.-.", "C"}, @@ -340,7 +346,16 @@ class MorseDecoder { {"..--..", "?"}, {"-.-.--", "!"}, {"--..--", ","}, - {"-...-", "="}}; + {"-...-", "="}, + {"-..-.", "/"}, + {".--.-.", "@"}, + {"---...", ":"}, + {"-....-", "-"}, + {".----.", "'"}, + {".-..-.", "\""}, + {"-.--.", "("}, + {"-.--.-", ")"}, + {".-.-.", "+"}}; }; } // namespace ui::external_app::morse_practice diff --git a/firmware/application/external/morse_practice/ui_morse_practice.cpp b/firmware/application/external/morse_practice/ui_morse_practice.cpp index d2b12d8ae..a92795be3 100644 --- a/firmware/application/external/morse_practice/ui_morse_practice.cpp +++ b/firmware/application/external/morse_practice/ui_morse_practice.cpp @@ -12,6 +12,13 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav) &btn_clear, &console_text, &field_volume}); + + initial_switch_config_ = get_switches_repeat_config(); + + SwitchesState config = initial_switch_config_; + config[toUType(Switch::Sel)] = false; + set_switches_repeat_config(config); + audio::set_rate(audio::Rate::Hz_24000); btn_tt.on_select = [this](Button&) { @@ -43,6 +50,7 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav) } MorsePracticeView::~MorsePracticeView() { + set_switches_repeat_config(initial_switch_config_); receiver_model.disable(); baseband::shutdown(); audio::output::stop(); @@ -98,7 +106,7 @@ void MorsePracticeView::writeCharToConsole(const std::string& ch, double confide if (ch == " ") { color_id = 0; - } else if (ch == MORSEDEC_ERROR) { + } else if (ch[0] == '{') { // no match color_id = 0; } else { if (confidence < 0.8) diff --git a/firmware/application/external/morse_practice/ui_morse_practice.hpp b/firmware/application/external/morse_practice/ui_morse_practice.hpp index 5ad19ab54..64135df03 100644 --- a/firmware/application/external/morse_practice/ui_morse_practice.hpp +++ b/firmware/application/external/morse_practice/ui_morse_practice.hpp @@ -47,7 +47,7 @@ class MorsePracticeView : public ui::View { MorsePracticeView(ui::NavigationView& nav); ~MorsePracticeView(); - std::string title() { return "Morse P"; } + std::string title() const override { return "Morse Practice"; } void focus() override; void on_show() override; @@ -68,7 +68,7 @@ class MorsePracticeView : public ui::View { ui::Text txt_last{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, ""}; ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"}; ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(10)}}; - AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_X(0)}}; + AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; uint8_t last_color_id = 255; uint8_t color_id = 255; @@ -77,6 +77,7 @@ class MorsePracticeView : public ui::View { bool button_touch = false; bool button_was_selected = false; bool decode_timeout_calc = false; + SwitchesState initial_switch_config_{}; MessageHandlerRegistration message_handler_framesync{ Message::ID::DisplayFrameSync, diff --git a/firmware/application/external/morse_radio/main.cpp b/firmware/application/external/morse_radio/main.cpp new file mode 100644 index 000000000..f4ff612b9 --- /dev/null +++ b/firmware/application/external/morse_radio/main.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2026 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_morse_radio.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::morse_radio { + +void initialize_app(NavigationView& nav) { + nav.push(); +} + +} // namespace ui::external_app::morse_radio + +extern "C" { + +__attribute__((section(".external_app.app_morse_radio.application_information"), used)) +application_information_t _application_information_morse_radio = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::morse_radio::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Morse", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x7F, + 0xFF, + 0xFF, + 0xBB, + 0xD0, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x0B, + 0xE1, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xEB, + 0xD0, + 0xFF, + 0xFF, + 0xFE, + 0x7F, + 0x70, + 0x00, + 0x30, + 0x00, + 0x10, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_morse_radio */ {'P', 'M', 'R', 'S'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/morse_radio/morsedecoder.hpp b/firmware/application/external/morse_radio/morsedecoder.hpp new file mode 100644 index 000000000..50fb01b7b --- /dev/null +++ b/firmware/application/external/morse_radio/morsedecoder.hpp @@ -0,0 +1,378 @@ +#ifndef __MORSEDECODER_HPP__ +#define __MORSEDECODER_HPP__ + +/* + * Copyright (C) 2025 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include +#include +#include "string_format.hpp" + +namespace ui::external_app::morse_radio { + +class MorseRingBuffer { + public: + MorseRingBuffer() + : head_(0), tail_(0), count_(0) {} + + void push_back(const uint32_t& value) { + data_[head_] = value; + head_ = (head_ + 1) % 40; + if (count_ < 40) { + count_++; + } else { + // overwrite oldest element + tail_ = (tail_ + 1) % 40; + } + } + + void pop_front() { + if (count_ > 0) { + tail_ = (tail_ + 1) % 40; + count_--; + } + } + + size_t size() const { return count_; } + bool empty() const { return count_ == 0; } + const uint32_t& front() const { return data_[tail_]; } + // Access by index (0 = oldest) + uint32_t operator[](size_t idx) const { + return data_[(tail_ + idx) % 40]; + } + // Convert to vector-like access for sorting etc. + void copy_to_array(uint32_t* out) const { + for (size_t i = 0; i < count_; ++i) + out[i] = (*this)[i]; + } + + void clear() { + head_ = 0; + tail_ = 0; + count_ = 0; + } + + private: + uint32_t data_[40]; + size_t head_; + size_t tail_; + size_t count_; +}; + +class MorseDecoder { + public: + struct DecodeResult { + std::string text = ""; + double confidence = 0.0; + + bool isValid() const { + return !text.empty(); + } + }; + + struct MorseEntry { + std::string code; + std::string letter; + }; + + MorseDecoder() {} + + void resetLearning() { + time_unit_ms_ = 119.0; + current_sequence_ = ""; + last_sequence_ = ""; + last_confidence_ = 0.0; + pulse_history_.clear(); + pulse_gaps_.clear(); + } + + DecodeResult + handleInput(int32_t duration_ms) { + DecodeResult result = {"", 0.0}; + + if (duration_ms < 5 && duration_ms > -5) return result; + + if (duration_ms > 0) { + pulse_history_.push_back(duration_ms); + double dah_prob = getDahProbability(duration_ms); + current_sequence_ += (dah_prob > 0.5) ? '-' : '.'; + last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob); + last_sequence_ = current_sequence_; + + } else { + uint32_t gap_duration = -duration_ms; + pulse_gaps_.push_back(gap_duration); + + if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) { + result.text = lookupMorse(current_sequence_); + + result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0; + if (gap_duration >= getInterWordThreshold()) { + result.text += " "; + } + current_sequence_ = ""; + } + } + + updateLearning(); + + return result; + } + + inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; } + inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; } + inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; } + + inline double getCurrentTimeUnit() { return time_unit_ms_; } + inline std::string getLastSequence() { return last_sequence_; } + + private: + std::string current_sequence_ = ""; + std::string last_sequence_ = ""; + double time_unit_ms_ = 119.0; + double last_confidence_ = 0.0; + MorseRingBuffer pulse_history_{}; + MorseRingBuffer pulse_gaps_{}; + + std::string lookupMorse(const std::string& seq) { + for (size_t i = 0; i < morse_table_size_; i++) { + if (seq == morse_table_[i].code) + return morse_table_[i].letter; + } + return "{" + seq + "}"; // not found + } + + double getDahProbability(uint32_t duration_ms) { + double start_interp = 1.5 * time_unit_ms_; + double end_interp = 2.5 * time_unit_ms_; + + if (duration_ms <= start_interp) return 0.0; + if (duration_ms >= end_interp) return 1.0; + return ((double)(duration_ms)-start_interp) / (end_interp - start_interp); + } + + size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) { + if (sorted_data_size < 4) return 0; + + size_t best_split_index = 0; + uint32_t max_diff = 0; + + for (size_t i = 1; i < sorted_data_size; ++i) { + uint32_t diff = sorted_data[i] - sorted_data[i - 1]; + if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) { + max_diff = diff; + best_split_index = i; + } + } + return best_split_index; + } + + bool calculatePulseUnit(double& unit, double& confidence) { + if (pulse_history_.size() < 10) return false; + uint32_t sorted_pulses[pulse_history_.size()]; + pulse_history_.copy_to_array(sorted_pulses); + sort_uint32(sorted_pulses, pulse_history_.size()); + + size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size()); + if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) { + return false; + } + double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index); + double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size()); + + double avg_dit = dit_sum / split_index; + double avg_dah = dah_sum / (pulse_history_.size() - split_index); + if (avg_dah <= avg_dit) return false; + + double ratio = avg_dah / avg_dit; + if (ratio > 1.5 && ratio < 5.0) { + unit = avg_dit; + double tmpabs = ratio - 3.0; + if (tmpabs < 0) tmpabs *= -1; + tmpabs /= 3.0; + tmpabs = 1.0 - tmpabs; + if (tmpabs < 0) tmpabs = 0; + confidence = tmpabs; // 0..1 + return true; + } + + return false; + } + + bool calculateGapUnit(double& unit, double& confidence) { + if (pulse_gaps_.size() < 10) return false; + double threshold = getInterElementThreshold(); + double valid_gaps[pulse_gaps_.size()]; + size_t valid_count = 0; + for (size_t i = 0; i < pulse_gaps_.size(); i++) { + double gap = pulse_gaps_[i]; + if (gap <= threshold) { + valid_gaps[valid_count++] = gap; + } + } + if (valid_count < 2) { + return false; + } + double sum = sum_double_range(valid_gaps, 0, valid_count); + size_t count_to_average = valid_count; + + if (count_to_average > 0) { + unit = sum / count_to_average; + confidence = 0.8; + return true; + } + + return false; + } + + double sum_uint32_range(const uint32_t* data, size_t start, size_t end) { + double sum = 0.0; + for (size_t i = start; i < end; i++) { + sum += data[i]; + } + return sum; + } + double sum_double_range(const double* data, size_t start, size_t end) { + double sum = 0.0; + for (size_t i = start; i < end; i++) { + sum += data[i]; + } + return sum; + } + + void sort_uint32(uint32_t* data, size_t size) { + if (size < 2) + return; + + for (size_t i = 1; i < size; i++) { + uint32_t key = data[i]; + size_t j = i; + + while (j > 0 && data[j - 1] > key) { + data[j] = data[j - 1]; + j--; + } + + data[j] = key; + } + } + + double clamp_double(double value, double min_val, double max_val) { + if (value < min_val) + return min_val; + else if (value > max_val) + return max_val; + else + return value; + } + + void updateLearning() { + double pulse_unit = -1.0, pulse_confidence = 0.0; + double gap_unit = -1.0, gap_confidence = 0.0; + + bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence); + bool gap_success = calculateGapUnit(gap_unit, gap_confidence); + + double new_time_unit = -1.0; + if (pulse_success && pulse_confidence > 0.5) { + new_time_unit = pulse_unit; + } else if (pulse_success && gap_success) { + gap_confidence = 0.2; + double total_confidence = pulse_confidence + gap_confidence; + new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence; + } else if (gap_success) { + new_time_unit = gap_unit; + } else { + return; + } + + double max_change = time_unit_ms_ * 0.25; + new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change); + double DEFAULT_TIME_UNIT = 160.0; + double BASE_LEARNING_RATE = 0.05; + double MAX_LEARNING_RATE = 0.25; + double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT; + if (tudeltaabs < 0) tudeltaabs *= -1; + double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT; + double tpp = deviation_from_default * 2.0; + if (tpp > 1) tpp = 1; + double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp; + + time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor); + } + + size_t morse_table_size_ = 50; + MorseEntry morse_table_[50] = { + {".-", "A"}, + {"-...", "B"}, + {"-.-.", "C"}, + {"-..", "D"}, + {".", "E"}, + {"..-.", "F"}, + {"--.", "G"}, + {"....", "H"}, + {"..", "I"}, + {".---", "J"}, + {"-.-", "K"}, + {".-..", "L"}, + {"--", "M"}, + {"-.", "N"}, + {"---", "O"}, + {".--.", "P"}, + {"--.-", "Q"}, + {".-.", "R"}, + {"...", "S"}, + {"-", "T"}, + {"..-", "U"}, + {"...-", "V"}, + {".--", "W"}, + {"-..-", "X"}, + {"-.--", "Y"}, + {"--..", "Z"}, + {".----", "1"}, + {"..---", "2"}, + {"...--", "3"}, + {"....-", "4"}, + {".....", "5"}, + {"-....", "6"}, + {"--...", "7"}, + {"---..", "8"}, + {"----.", "9"}, + {"-----", "0"}, + {".-.-.-", "."}, + {"..--..", "?"}, + {"-.-.--", "!"}, + {"--..--", ","}, + {"-...-", "="}, + {"-..-.", "/"}, + {".--.-.", "@"}, + {"---...", ":"}, + {"-....-", "-"}, + {".----.", "'"}, + {".-..-.", "\""}, + {"-.--.", "("}, + {"-.--.-", ")"}, + {".-.-.", "+"}}; +}; + +} // namespace ui::external_app::morse_radio + +#endif // __MORSEDECODER_HPP__ diff --git a/firmware/application/external/morse_radio/ui_morse_radio.cpp b/firmware/application/external/morse_radio/ui_morse_radio.cpp new file mode 100644 index 000000000..42066790a --- /dev/null +++ b/firmware/application/external/morse_radio/ui_morse_radio.cpp @@ -0,0 +1,282 @@ +#include "ui_morse_radio.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; + +namespace ui::external_app::morse_radio { + +MorseRadioView::MorseRadioView(ui::NavigationView& nav) + : nav_(nav) { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + add_children({&rssi, + &field_rf_amp, + &field_lna, + &field_vga, + &field_volume, + &field_frequency, + &txt_last, + &txt_speed, + &txt_freq, + &txt_clip, + &options_mode, + &btn_clear, + &console_text, + &labels, + &chk_log, + &field_squelch}); + field_frequency.set_step(100); + + btn_clear.on_select = [this](Button&) { + console_text.clear(true); + txt_last.set(""); + time_stamp = false; + if (logger && save_log) + logger->init_daily_log(logs_dir); + }; + + field_squelch.on_change = [this](int32_t v) { + receiver_model.set_squelch_level(v); + }; + + options_mode.on_change = [this](size_t, int32_t mode) { + audio::output::stop(); + receiver_model.disable(); + morse_decoder_.resetLearning(); + if (mode == MORSE_NFM) { + receiver_model.set_am_configuration(4); + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + field_squelch.set_style(Theme::getInstance()->option_active); + field_squelch.set_focusable(true); + field_squelch.set_value(receiver_model.squelch_level()); + audio::set_rate(audio::Rate::Hz_24000); + } else { + audio::set_rate(audio::Rate::Hz_12000); + receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); + if (mode == MORSE_AM_CW || mode == MORSE_AM_DSB) + receiver_model.set_am_configuration(7); + else if (mode == MORSE_AM_USB) + receiver_model.set_am_configuration(9); + else // LSB + receiver_model.set_am_configuration(10); + field_squelch.set_style(Theme::getInstance()->fg_dark); + field_squelch.set_focusable(false); + } + baseband::set_moreserx_config(mode); + saved_mode = mode; + + audio::output::start(); + receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack. + + receiver_model.set_sampling_rate(3072000); + receiver_model.set_baseband_bandwidth(1750000); + receiver_model.enable(); + }; + options_mode.set_selected_index(saved_mode); + + logger = std::make_unique(); + chk_log.on_select = [this](Checkbox&, bool save) { + save_log = save; + if (logger && save_log) + logger->init_daily_log(logs_dir); + }; +} + +void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) { + auto now = rtc_time::now(); + std::string pattern = "morse_"; + uint16_t y = now.year(); + pattern += (char)('0' + (y / 1000) % 10); + pattern += (char)('0' + (y / 100) % 10); + pattern += (char)('0' + (y / 10) % 10); + pattern += (char)('0' + (y % 10)); + + uint8_t m = now.month(); + pattern += (char)('0' + (m / 10) % 10); + pattern += (char)('0' + (m % 10)); + + uint8_t d = now.day(); + pattern += (char)('0' + (d / 10) % 10); + pattern += (char)('0' + (d % 10)); + + pattern += "_???.TXT"; + + auto full_pattern_path = log_dir / pattern; + auto final_path = next_filename_matching_pattern(full_pattern_path); + if (!final_path.empty()) { + this->append(final_path); + } +} + +void MorseLogger::radio_set_log(const std::string& morse_mode) { + int64_t freq = receiver_model.target_frequency(); + std::string header = "Freq:" + to_string_rounded_freq(freq, 4); + header += "MHz, "; + header += "RX MODE:" + morse_mode; + header += "\r\nMessage:"; + log_file.write_raw(header); +} + +bool MorseLogger::on_packet(const std::string& content, bool time, const std::string& morse_mode) { + if (!time) { + log_file.write_raw_no_newline("\r\n\r\n"); + auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS); + log_file.write_raw("[" + timestamp + "] "); + radio_set_log(morse_mode); + char_count = 0; + time = true; + } + + if (content.empty()) return time; + std::string s; + for (char c : content) { + s = c; + if ((char_count >= 35 && c == ' ') || (char_count >= 47)) { + log_file.write_raw_no_newline("\r\n"); + + if (c != ' ') { + log_file.write_raw_no_newline(s); + char_count = 1; + } else { + char_count = 0; + } + } else { + log_file.write_raw_no_newline(s); + char_count++; + } + } + + return time; +} + +MorseRadioView::~MorseRadioView() { + receiver_model.disable(); + baseband::shutdown(); + audio::output::stop(); +} + +void MorseRadioView::focus() { + field_frequency.focus(); + txt_clip.set_style(Theme::getInstance()->fg_red); + txt_clip.hidden(true); +} + +void MorseRadioView::check_for_timeout() { + uint64_t now = chTimeNow(); + if (last_activity_time == 0) { + last_activity_time = now; + return; + } + uint64_t quiet_duration = now - last_activity_time; + if (quiet_duration >= 60000) { + morse_decoder_.resetLearning(); + time_stamp = false; + last_activity_time = now; + } +} + +int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) { + int8_t sign = (sig_time_us > 0) ? 1 : ((sig_time_us < 0) ? -1 : 0); + int32_t result = 0; + + if (last_sign_ == 0) { + last_sign_ = sign; + accumulator_us_ = sig_time_us; + long_pause_sent_ = false; + return 0; + } + if (sign == last_sign_) { + accumulator_us_ += sig_time_us; + + if (sign < 0) { + int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000; + if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) { + result = accumulator_us_ / 1000; + long_pause_sent_ = true; + } + } + } else { + // state change + if (!long_pause_sent_) { + result = accumulator_us_ / 1000; + } else { + result = 0; + } + accumulator_us_ = sig_time_us; + last_sign_ = sign; + long_pause_sent_ = false; + } + return result; +} + +void MorseRadioView::on_data(const MorseRXDataMessage* message) { + int32_t r; + + txt_clip.hidden(!message->clipped); + + for (uint8_t i = 0; i <= message->state_cnt; ++i) { + r = ProcessSignal(message->state_durations[i]); + auto result = morse_decoder_.handleInput((r != 0) ? r : 0); + if (result.isValid()) { + last_activity_time = chTimeNow(); // start reset timer on valid input + writeCharToConsole(result.text, result.confidence); + if (logger && save_log) { + time_stamp = logger->on_packet(result.text, time_stamp, options_mode.selected_index_name()); + } + float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f; + if (dah_time > 0) { + uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f); + txt_speed.set(to_string_dec_uint(wpm)); + } + } + } +} + +void MorseRadioView::on_freq(const MorseRXfreqMessage* message) { + std::string ret = " "; + uint32_t freq = message->measured_frequency; + if (freq < 301) { + ret = "<"; + freq = 300; + } + if (freq > 2299) { + freq = 2300; + ret = ">"; + } + if (freq < 400 || freq > 1400) + txt_freq.set_style(Theme::getInstance()->fg_red); + else if (freq <= 580 || freq >= 1220) + txt_freq.set_style(Theme::getInstance()->fg_yellow); + else + txt_freq.set_style(Theme::getInstance()->fg_green); + ret += to_string_dec_uint(freq); + txt_freq.set(ret); +} + +void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) { + if (ch.empty()) { + return; + } + + txt_last.set(morse_decoder_.getLastSequence().c_str()); + + last_color_id = color_id; + std::string color = ""; + + if (ch == " ") { + color_id = 0; + } else if (ch[0] == '{') { // no match + color_id = 0; + } else { + if (confidence < 0.8) + color_id = 1; + else if (confidence < 0.9) + color_id = 2; + else + color_id = 3; + } + color = arr_color[color_id]; + last_color_id = color_id; + console_text.write(color + ch); +} + +} // namespace ui::external_app::morse_radio diff --git a/firmware/application/external/morse_radio/ui_morse_radio.hpp b/firmware/application/external/morse_radio/ui_morse_radio.hpp new file mode 100644 index 000000000..bbf7de95e --- /dev/null +++ b/firmware/application/external/morse_radio/ui_morse_radio.hpp @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2026 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __MORSE_RADIO_H__ +#define __MORSE_RADIO_H__ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "string_format.hpp" +#include "external_app.hpp" +#include "ui_freq_field.hpp" +#include "radio_state.hpp" +#include "portapack.hpp" +#include "message.hpp" +#include "audio.hpp" +#include "baseband_api.hpp" +#include "receiver_model.hpp" +#include "tone_key.hpp" +#include "log_file.hpp" +#include "file_path.hpp" +#include "file.hpp" +#include "rtc_time.hpp" +#include "morsedecoder.hpp" + +#include + +namespace ui::external_app::morse_radio { + +class MorseRadioView; + +class MorseLogger { + public: + Optional append(const std::filesystem::path& filename) { + return log_file.append(filename); + } + + void init_daily_log(const std::filesystem::path& log_dir); + bool on_packet(const std::string& content, bool time, const std::string& morse_mode); + void radio_set_log(const std::string& morse_mode); + + private: + LogFile log_file{}; + uint8_t char_count{0}; // log file line break +}; + +class MorseRadioView : public ui::View { + public: + MorseRadioView(ui::NavigationView& nav); + ~MorseRadioView(); + std::string title() const override { + return "Morse"; + } + void focus() override; + + private: + void writeCharToConsole(const std::string& ch, double confidence); + void on_data(const MorseRXDataMessage* message); + void on_freq(const MorseRXfreqMessage* message); + void on_message(const Message* const p); + void check_for_timeout(); + int32_t ProcessSignal(int32_t sig_time_us); + ui::NavigationView& nav_; + MorseDecoder morse_decoder_{}; + RxRadioState radio_state_{}; + std::unique_ptr logger{}; + + enum morse_modes : uint8_t { + MORSE_AM_CW = 0, + MORSE_NFM, + MORSE_AM_DSB, + MORSE_AM_USB, + MORSE_AM_LSB, + }; + uint8_t saved_mode = MORSE_AM_CW; + + app_settings::SettingsManager settings_{ + "rx_morse_radio", + app_settings::Mode::RX, + { + {"cwmode"sv, &saved_mode}, + }}; + + RxFrequencyField field_frequency{ + {UI_POS_X(0), UI_POS_Y(0)}, + nav_}; + RFAmpField field_rf_amp{ + {UI_POS_X(13), UI_POS_Y(0)}}; + LNAGainField field_lna{ + {UI_POS_X(15), UI_POS_Y(0)}}; + VGAGainField field_vga{ + {UI_POS_X(18), UI_POS_Y(0)}}; + RSSI rssi{ + {UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}}; + + AudioVolumeField field_volume{ + {UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; + + NumberField field_squelch{ + {UI_POS_X(10), UI_POS_Y(1)}, + 2, + {0, 99}, + 1, + ' ', + }; + ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"}; + ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""}; + ui::Text txt_freq{{UI_POS_X(21), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "??"}; + ui::Text txt_clip{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "clipping"}; + ui::Console console_text{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(7)}}; + ui::Labels labels{ + {{UI_POS_X(0), UI_POS_Y(1)}, "Squelch:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(15), UI_POS_Y(2)}, "Tone:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}}; + + ui::OptionsField options_mode{ + {UI_POS_X(8), UI_POS_Y(2) + 4}, // +4 to align with 'Log' checkbox text + 6, + { + {"AM/CW", MORSE_AM_CW}, + {"NFM", MORSE_NFM}, + {"AM/DSB", MORSE_AM_DSB}, + {"AM/USB", MORSE_AM_USB}, + {"AM/LSB", MORSE_AM_LSB}, + }}; + + Checkbox chk_log{{UI_POS_X(0), UI_POS_Y(2)}, 12, "Log", false}; + ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"}; + + std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN}; + + bool long_pause_sent_{false}; + bool save_log{false}; + bool reset_triggered_{false}; + bool time_stamp{false}; + uint8_t last_color_id{255}; + uint8_t color_id{255}; + int8_t last_sign_{0}; + uint8_t space_timer{0}; + int32_t accumulator_us_{0}; + uint64_t last_activity_time{0}; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::MorseRXData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_data(message); + }}; + + MessageHandlerRegistration message_handler_freq{ + Message::ID::MorseRXfreq, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freq(message); + }}; + + MessageHandlerRegistration message_handler_framesync{ + Message::ID::DisplayFrameSync, + [this](const Message* const p) { + (void)p; + this->check_for_timeout(); + }}; +}; + +} // namespace ui::external_app::morse_radio + +#endif // __MORSE_RADIO_H__ diff --git a/firmware/application/external/morseradiotx/main.cpp b/firmware/application/external/morseradiotx/main.cpp new file mode 100644 index 000000000..c4af55f94 --- /dev/null +++ b/firmware/application/external/morseradiotx/main.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_morse_radiotx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::morseradiotx { + +void initialize_app(NavigationView& nav) { + nav.push(); +} + +} // namespace ui::external_app::morseradiotx + +extern "C" { + +__attribute__((section(".external_app.app_morseradiotx.application_information"), used)) +application_information_t _application_information_morseradiotx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::morseradiotx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Morse TX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x7F, + 0xFF, + 0xFF, + 0xBB, + 0xD0, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x0B, + 0xE1, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xEB, + 0xD0, + 0xFF, + 0xFF, + 0xFE, + 0x7F, + 0x70, + 0x00, + 0x30, + 0x00, + 0x10, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'M', 'R', 'T'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; + +} // extern "C" diff --git a/firmware/application/external/morseradiotx/morsedecoder.hpp b/firmware/application/external/morseradiotx/morsedecoder.hpp new file mode 100644 index 000000000..47b473f4d --- /dev/null +++ b/firmware/application/external/morseradiotx/morsedecoder.hpp @@ -0,0 +1,389 @@ +#ifndef __MORSEDECODER_HPP__ +#define __MORSEDECODER_HPP__ + +/* + * Copyright (C) 2025 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include +#include +#include "string_format.hpp" + +namespace ui::external_app::morseradiotx { + +class MorseRingBuffer { + public: + MorseRingBuffer() + : head_(0), tail_(0), count_(0) {} + + void push_back(const uint32_t& value) { + data_[head_] = value; + head_ = (head_ + 1) % 40; + if (count_ < 40) { + count_++; + } else { + // overwrite oldest element + tail_ = (tail_ + 1) % 40; + } + } + + void pop_front() { + if (count_ > 0) { + tail_ = (tail_ + 1) % 40; + count_--; + } + } + + size_t size() const { return count_; } + bool empty() const { return count_ == 0; } + const uint32_t& front() const { return data_[tail_]; } + // Access by index (0 = oldest) + uint32_t operator[](size_t idx) const { + return data_[(tail_ + idx) % 40]; + } + // Convert to vector-like access for sorting etc. + void copy_to_array(uint32_t* out) const { + for (size_t i = 0; i < count_; ++i) + out[i] = (*this)[i]; + } + + void clear() { + head_ = 0; + tail_ = 0; + count_ = 0; + } + + private: + uint32_t data_[40]; + size_t head_; + size_t tail_; + size_t count_; +}; + +class MorseDecoder { + public: + struct DecodeResult { + std::string text = ""; + double confidence = 0.0; + + bool isValid() const { + return !text.empty(); + } + }; + + struct MorseEntry { + std::string code; + std::string letter; + }; + + MorseDecoder() {} + + void resetLearning() { + time_unit_ms_ = 119.0; + current_sequence_ = ""; + last_sequence_ = ""; + last_confidence_ = 0.0; + pulse_history_.clear(); + pulse_gaps_.clear(); + } + + const char* get_morse_pattern(char c) { + char upper_c = (c >= 'a' && c <= 'z') ? c - 32 : c; + + for (size_t i = 0; i < morse_table_size_; ++i) { + if (morse_table_[i].letter[0] == upper_c) { + return morse_table_[i].code.c_str(); + } + } + return nullptr; + } + + DecodeResult + handleInput(int32_t duration_ms) { + DecodeResult result = {"", 0.0}; + + if (duration_ms < 5 && duration_ms > -5) return result; + + if (duration_ms > 0) { + pulse_history_.push_back(duration_ms); + double dah_prob = getDahProbability(duration_ms); + current_sequence_ += (dah_prob > 0.5) ? '-' : '.'; + last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob); + last_sequence_ = current_sequence_; + + } else { + uint32_t gap_duration = -duration_ms; + pulse_gaps_.push_back(gap_duration); + + if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) { + result.text = lookupMorse(current_sequence_); + + result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0; + if (gap_duration >= getInterWordThreshold()) { + result.text += " "; + } + current_sequence_ = ""; + } + } + + updateLearning(); + + return result; + } + + inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; } + inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; } + inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; } + + inline double getCurrentTimeUnit() { return time_unit_ms_; } + inline std::string getLastSequence() { return last_sequence_; } + + private: + std::string current_sequence_ = ""; + std::string last_sequence_ = ""; + double time_unit_ms_ = 119.0; + double last_confidence_ = 0.0; + MorseRingBuffer pulse_history_{}; + MorseRingBuffer pulse_gaps_{}; + + std::string lookupMorse(const std::string& seq) { + for (size_t i = 0; i < morse_table_size_; i++) { + if (seq == morse_table_[i].code) + return morse_table_[i].letter; + } + return "{" + seq + "}"; // not found + } + + double getDahProbability(uint32_t duration_ms) { + double start_interp = 1.5 * time_unit_ms_; + double end_interp = 2.5 * time_unit_ms_; + + if (duration_ms <= start_interp) return 0.0; + if (duration_ms >= end_interp) return 1.0; + return ((double)(duration_ms)-start_interp) / (end_interp - start_interp); + } + + size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) { + if (sorted_data_size < 4) return 0; + + size_t best_split_index = 0; + uint32_t max_diff = 0; + + for (size_t i = 1; i < sorted_data_size; ++i) { + uint32_t diff = sorted_data[i] - sorted_data[i - 1]; + if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) { + max_diff = diff; + best_split_index = i; + } + } + return best_split_index; + } + + bool calculatePulseUnit(double& unit, double& confidence) { + if (pulse_history_.size() < 10) return false; + uint32_t sorted_pulses[pulse_history_.size()]; + pulse_history_.copy_to_array(sorted_pulses); + sort_uint32(sorted_pulses, pulse_history_.size()); + + size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size()); + if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) { + return false; + } + double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index); + double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size()); + + double avg_dit = dit_sum / split_index; + double avg_dah = dah_sum / (pulse_history_.size() - split_index); + if (avg_dah <= avg_dit) return false; + + double ratio = avg_dah / avg_dit; + if (ratio > 1.5 && ratio < 5.0) { + unit = avg_dit; + double tmpabs = ratio - 3.0; + if (tmpabs < 0) tmpabs *= -1; + tmpabs /= 3.0; + tmpabs = 1.0 - tmpabs; + if (tmpabs < 0) tmpabs = 0; + confidence = tmpabs; // 0..1 + return true; + } + + return false; + } + + bool calculateGapUnit(double& unit, double& confidence) { + if (pulse_gaps_.size() < 10) return false; + double threshold = getInterElementThreshold(); + double valid_gaps[pulse_gaps_.size()]; + size_t valid_count = 0; + for (size_t i = 0; i < pulse_gaps_.size(); i++) { + double gap = pulse_gaps_[i]; + if (gap <= threshold) { + valid_gaps[valid_count++] = gap; + } + } + if (valid_count < 2) { + return false; + } + double sum = sum_double_range(valid_gaps, 0, valid_count); + size_t count_to_average = valid_count; + + if (count_to_average > 0) { + unit = sum / count_to_average; + confidence = 0.8; + return true; + } + + return false; + } + + double sum_uint32_range(const uint32_t* data, size_t start, size_t end) { + double sum = 0.0; + for (size_t i = start; i < end; i++) { + sum += data[i]; + } + return sum; + } + double sum_double_range(const double* data, size_t start, size_t end) { + double sum = 0.0; + for (size_t i = start; i < end; i++) { + sum += data[i]; + } + return sum; + } + + void sort_uint32(uint32_t* data, size_t size) { + if (size < 2) + return; + + for (size_t i = 1; i < size; i++) { + uint32_t key = data[i]; + size_t j = i; + + while (j > 0 && data[j - 1] > key) { + data[j] = data[j - 1]; + j--; + } + + data[j] = key; + } + } + + double clamp_double(double value, double min_val, double max_val) { + if (value < min_val) + return min_val; + else if (value > max_val) + return max_val; + else + return value; + } + + void updateLearning() { + double pulse_unit = -1.0, pulse_confidence = 0.0; + double gap_unit = -1.0, gap_confidence = 0.0; + + bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence); + bool gap_success = calculateGapUnit(gap_unit, gap_confidence); + + double new_time_unit = -1.0; + if (pulse_success && pulse_confidence > 0.5) { + new_time_unit = pulse_unit; + } else if (pulse_success && gap_success) { + gap_confidence = 0.2; + double total_confidence = pulse_confidence + gap_confidence; + new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence; + } else if (gap_success) { + new_time_unit = gap_unit; + } else { + return; + } + + double max_change = time_unit_ms_ * 0.25; + new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change); + double DEFAULT_TIME_UNIT = 160.0; + double BASE_LEARNING_RATE = 0.05; + double MAX_LEARNING_RATE = 0.25; + double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT; + if (tudeltaabs < 0) tudeltaabs *= -1; + double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT; + double tpp = deviation_from_default * 2.0; + if (tpp > 1) tpp = 1; + double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp; + + time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor); + } + + size_t morse_table_size_ = 50; + MorseEntry morse_table_[50] = { + {".-", "A"}, + {"-...", "B"}, + {"-.-.", "C"}, + {"-..", "D"}, + {".", "E"}, + {"..-.", "F"}, + {"--.", "G"}, + {"....", "H"}, + {"..", "I"}, + {".---", "J"}, + {"-.-", "K"}, + {".-..", "L"}, + {"--", "M"}, + {"-.", "N"}, + {"---", "O"}, + {".--.", "P"}, + {"--.-", "Q"}, + {".-.", "R"}, + {"...", "S"}, + {"-", "T"}, + {"..-", "U"}, + {"...-", "V"}, + {".--", "W"}, + {"-..-", "X"}, + {"-.--", "Y"}, + {"--..", "Z"}, + {".----", "1"}, + {"..---", "2"}, + {"...--", "3"}, + {"....-", "4"}, + {".....", "5"}, + {"-....", "6"}, + {"--...", "7"}, + {"---..", "8"}, + {"----.", "9"}, + {"-----", "0"}, + {".-.-.-", "."}, + {"..--..", "?"}, + {"-.-.--", "!"}, + {"--..--", ","}, + {"-...-", "="}, + {"-..-.", "/"}, + {".--.-.", "@"}, + {"---...", ":"}, + {"-....-", "-"}, + {".----.", "'"}, + {".-..-.", "\""}, + {"-.--.", "("}, + {"-.--.-", ")"}, + {".-.-.", "+"}}; +}; + +} // namespace ui::external_app::morseradiotx + +#endif // __MORSEDECODER_HPP__ diff --git a/firmware/application/external/morseradiotx/ui_morse_radiotx.cpp b/firmware/application/external/morseradiotx/ui_morse_radiotx.cpp new file mode 100644 index 000000000..5ef99e5c7 --- /dev/null +++ b/firmware/application/external/morseradiotx/ui_morse_radiotx.cpp @@ -0,0 +1,437 @@ +#include "ui_morse_radiotx.hpp" + +using namespace portapack; + +namespace ui::external_app::morseradiotx { + +static msg_t tx_thread_fn(void* arg) { + auto view = reinterpret_cast(arg); + chRegSetThreadName("morse_tx_thread"); + view->transmit_morse_message(); + return 0; +} + +MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav) + : current_timings(calculate_morse_timings(20)), + nav_(nav) { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + add_children({&tx_view, + &field_volume, + &labels, + &options_mode, + &tone_, + &wpm_, + &txt_msg, + &btn_message, + &btn_calls, + &chk_trans, + &bandwidth, + &chk_callsgn, + &txt_last, + &console_text, + &btn_clear, + &btn_ptt}); + + initial_switch_config_ = get_switches_repeat_config(); + + SwitchesState config = initial_switch_config_; + config[toUType(Switch::Sel)] = false; + set_switches_repeat_config(config); + + audio::set_rate(audio::Rate::Hz_24000); + + btn_clear.on_select = [this](Button&) { + console_text.clear(true); + txt_last.set(""); + }; + + options_mode.on_change = [this](size_t, int32_t value) { + current_mode = (uint8_t)value; + baseband::set_morsetx_config(current_mode, tone, band); + ui_toggle(); + }; + + tone_.on_change = [this](int32_t v) { + tone = static_cast(v); + baseband::set_morsetx_config(current_mode, tone, band); + }; + + wpm_.on_change = [this](int16_t v) { + wpm = v; + current_timings = calculate_morse_timings(wpm); + }; + + bandwidth.on_change = [this](float v) { + band = v; + baseband::set_morsetx_config(current_mode, tone, band); + }; + + options_mode.set_selected_index(current_mode, true); + tone_.set_value(tone, true); + wpm_.set_value(wpm, true); + bandwidth.set_value(band, true); + + btn_ptt.on_select = [this](Button&) { + if (button_touch) { + button_touch = false; + return; + } + button_was_selected = true; + onPress(); + }; + + btn_ptt.on_touch_press = [this](Button&) { + button_touch = true; + button_was_selected = false; + onPress(); + }; + btn_ptt.on_touch_release = [this](Button&) { + if (chk_trans.value() && transmit) { + button_touch = true; + button_was_selected = false; + onRelease(); + } + }; + + btn_message.on_select = [this, &nav](Button&) { + if (!transmit) + text_prompt(nav, msg_buffer, 27, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) { + msg_buffer = buffer; + txt_msg.set("[" + msg_buffer + "] "); + }); + }; + + btn_calls.on_select = [this, &nav](Button&) { + if (!transmit) { + text_prompt(nav, call_sign, 10, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) { + call_sign = buffer; + if (call_sign.empty()) + btn_calls.set_text("call sign?"); + else + btn_calls.set_text(call_sign); + }); + } + }; + + audio::output::start(); + auto vol = field_volume.value(); + field_volume.set_value(0); + field_volume.set_value(vol); + + tx_view.on_start = [this]() { + if (!chk_trans.value() && msg_buffer.empty()) { + tx_view.set_transmitting(false); + return; + } + tx_view.focus(); + if (!tx_thread && !thread_running) { + thread_running = true; + tx_thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 5, tx_thread_fn, this); + tx_view.set_transmitting(true); + } + + else + tx_view.set_transmitting(false); + }; + + tx_view.on_stop = [this]() { + baseband::set_morsetx_key(false); + transmitter_model.disable(); + + if (tx_thread) { + chThdTerminate(tx_thread); + chThdWait(tx_thread); + } + transmit = false; + transmit_time = 0; + tx_thread = nullptr; + thread_running = false; + tx_view.set_transmitting(false); + ui_toggle(); + }; + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + transmitter_model.set_target_frequency(f); + }; + }; +} + +MorseRadiotxView::~MorseRadiotxView() { + set_switches_repeat_config(initial_switch_config_); + if (tx_thread) { + chThdTerminate(tx_thread); + chThdWait(tx_thread); + if (!thread_running) tx_thread = nullptr; + } + + transmitter_model.disable(); + baseband::shutdown(); + audio::output::stop(); +} + +void MorseRadiotxView::on_show() { + ptt_button_visibility(true); + start_time = 0; + end_time = 0; + transmit_time = 0; +} + +void MorseRadiotxView::focus() { + options_mode.focus(); +} + +MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_t wpm) { + MorseRadiotxView::MorseTimings t; + if (wpm < 10) wpm = 10; + if (wpm > 45) wpm = 45; + t.dot_ms = 1200 / wpm; + + t.dash_ms = 3 * t.dot_ms; + t.symbol_gap = t.dot_ms; + t.char_gap = 3 * t.dot_ms; + t.word_gap = 7 * t.dot_ms; + return t; +} + +void MorseRadiotxView::transmit_morse_message() { + std::string full_message = ""; + // cal sign + if (chk_callsgn.value() && !call_sign.empty()) { + full_message = call_sign; + if (!chk_trans.value() && !msg_buffer.empty()) { + full_message += " " + msg_buffer; + } + } else { + if (!chk_trans.value()) full_message = msg_buffer; + } + + if (full_message.empty() && !chk_trans.value()) { + ptt_button_visibility(false); + return; + } + + // enable transmit + if (!transmit) { + transmit = true; + transmitter_model.enable(); + ui_toggle(); + } + + for (size_t i = 0; i < full_message.length(); i++) { + if (chThdShouldTerminate()) break; + + char c = full_message[i]; + + std::string s_char(1, c); + // space + if (c == ' ') { + console_text.write(" "); + chThdSleepMilliseconds(current_timings.word_gap); + continue; + } + + const char* pattern = morse_decoder_.get_morse_pattern(c); + + if (pattern != nullptr) { + txt_last.set(pattern); + + // write blue char to console + console_text.write(STR_COLOR_BLUE + s_char); + + // Morze (dih/dah) send + for (int j = 0; pattern[j] != '\0'; j++) { + baseband::set_morsetx_key(true); + if (pattern[j] == '.') { + chThdSleepMilliseconds(current_timings.dot_ms); + } else if (pattern[j] == '-') { + chThdSleepMilliseconds(current_timings.dash_ms); + } + if (chThdShouldTerminate()) break; + // pause between signs + baseband::set_morsetx_key(false); + chThdSleepMilliseconds(current_timings.symbol_gap); + } + if (chThdShouldTerminate()) break; + if (current_timings.char_gap > current_timings.symbol_gap) { + chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap); + } + } + } + + if (chk_trans.value()) ptt_button_visibility(false); + baseband::set_morsetx_key(false); + transmit_time = chTimeNow(); + decode_timeout_calc = false; + thread_running = false; +} + +void MorseRadiotxView::onPress() { + start_time = chTimeNow(); + if (!transmit) { + transmit = true; + transmitter_model.enable(); + } + baseband::set_morsetx_key(true); + if (end_time != 0) { + int64_t gap_delta = (chTimeNow() - end_time); + auto result = morse_decoder_.handleInput(-gap_delta); + if (result.isValid()) { + writeCharToConsole(result.text, result.confidence); + } + } + end_time = 0; + transmit_time = 0; + decode_timeout_calc = false; +} + +void MorseRadiotxView::onRelease() { + end_time = chTimeNow(); + transmit_time = end_time; + baseband::set_morsetx_key(false); + if (start_time != 0) { + int32_t press_delta = (end_time - start_time); + auto result = morse_decoder_.handleInput(press_delta); + if (result.isValid()) { + writeCharToConsole(result.text, result.confidence); + } + } + start_time = 0; + decode_timeout_calc = true; + baseband::request_beep_stop(); +} + +void MorseRadiotxView::writeCharToConsole(const std::string& ch, double confidence) { + if (ch.empty()) { + return; + } + + txt_last.set(morse_decoder_.getLastSequence().c_str()); + + last_color_id = color_id; + std::string color = ""; + + if (ch == " ") { + color_id = 0; + } else if (ch[0] == '{') { // no match + color_id = 0; + } else { + if (confidence == 1.5) + color_id = 4; + else if (confidence < 0.8) + color_id = 1; + else if (confidence < 0.9) + color_id = 2; + else + color_id = 3; + } + color = arr_color[color_id]; + last_color_id = color_id; + console_text.write(color + ch); +} + +void MorseRadiotxView::ptt_button_visibility(bool hidden) { + bool hiddenorig = btn_ptt.hidden(); + if (hiddenorig != hidden) { + btn_ptt.hidden(hidden); + if (!hidden) btn_ptt.focus(); + if (hidden) { + // hack fix until widget hidig problem is not solved. + auto r = btn_ptt.screen_rect(); + Painter p; + p.fill_rectangle_unrolled8(r, Theme::getInstance()->fg_light->background); + } + } +} + +void MorseRadiotxView::ui_toggle() { + // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB + + if (transmit) { + options_mode.set_style(Theme::getInstance()->fg_dark); + options_mode.set_focusable(false); + tone_.set_style(Theme::getInstance()->fg_dark); + tone_.set_focusable(false); + wpm_.set_style(Theme::getInstance()->fg_dark); + wpm_.set_focusable(false); + btn_message.set_style(Theme::getInstance()->fg_dark); + btn_calls.set_style(Theme::getInstance()->fg_dark); + chk_trans.set_style(Theme::getInstance()->fg_dark); + chk_trans.set_focusable(false); + bandwidth.set_style(Theme::getInstance()->fg_dark); + bandwidth.set_focusable(false); + chk_callsgn.set_style(Theme::getInstance()->fg_dark); + chk_callsgn.set_focusable(false); + + } else { + options_mode.set_style(Theme::getInstance()->bg_darker); + options_mode.set_focusable(true); + wpm_.set_style(Theme::getInstance()->bg_darker); + wpm_.set_focusable(true); + btn_message.set_style(Theme::getInstance()->bg_darker); + btn_calls.set_style(Theme::getInstance()->bg_darker); + chk_trans.set_style(Theme::getInstance()->bg_darker); + chk_trans.set_focusable(true); + chk_callsgn.set_style(Theme::getInstance()->bg_darker); + chk_callsgn.set_focusable(true); + ptt_button_visibility(true); + tone_.set_style(Theme::getInstance()->bg_darker); + tone_.set_focusable(true); + + if (current_mode == 1) { // FM mode + bandwidth.set_style(Theme::getInstance()->bg_darker); + bandwidth.set_focusable(true); + } else { + bandwidth.set_style(Theme::getInstance()->fg_dark); + bandwidth.set_focusable(false); + } + } // transmit false +} + +inline bool MorseRadiotxView::tx_button_held() { + const auto switches_state = get_switches_state(); + return switches_state[(size_t)ui::KeyEvent::Select]; +} + +void MorseRadiotxView::on_framesync() { + if (button_was_selected && !button_touch && !tx_button_held()) { + button_was_selected = false; + onRelease(); + } + + if (end_time != 0 && decode_timeout_calc) { + int64_t gap_delta = (chTimeNow() - end_time); + + if (gap_delta >= morse_decoder_.getInterCharThreshold()) { + auto result = morse_decoder_.handleInput(-(int32_t)gap_delta); + if (result.isValid()) { + writeCharToConsole(result.text, result.confidence); + } + } + if (gap_delta >= morse_decoder_.getInterWordThreshold()) { + writeCharToConsole(" ", 1.0); + end_time = 0; + decode_timeout_calc = false; + } + } + if (transmit_time != 0 && transmit) { // Tx disable if time is up + int64_t gap_delta = (chTimeNow() - transmit_time); + if (gap_delta >= ((morse_decoder_.getInterWordThreshold() * ((chk_trans.value()) ? 10 : 1)))) { + if (tx_thread) { + chThdTerminate(tx_thread); + chThdWait(tx_thread); + } + transmit = false; + tx_view.set_transmitting(false); + transmitter_model.disable(); + thread_running = false; + tx_thread = nullptr; + transmit_time = 0; + if (!chk_trans.value()) writeCharToConsole(" ", 1.0); + ui_toggle(); + } + } +} + +} // namespace ui::external_app::morseradiotx diff --git a/firmware/application/external/morseradiotx/ui_morse_radiotx.hpp b/firmware/application/external/morseradiotx/ui_morse_radiotx.hpp new file mode 100644 index 000000000..c74bb576a --- /dev/null +++ b/firmware/application/external/morseradiotx/ui_morse_radiotx.hpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2026 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __MORSE_RADIOTX_H__ +#define __MORSE_RADIOTX_H__ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_language.hpp" +#include "ui_painter.hpp" +#include "ui_freq_field.hpp" +#include "ui_transmitter.hpp" +#include "ui_textentry.hpp" +#include "string_format.hpp" +#include "morsedecoder.hpp" +#include "irq_controls.hpp" +#include "radio_state.hpp" +#include "portapack.hpp" +#include "message.hpp" +#include "volume.hpp" +#include "audio.hpp" +#include "baseband_api.hpp" +#include "external_app.hpp" +#include "string_format.hpp" +#include +#include + +namespace ui::external_app::morseradiotx { + +class MorseRadiotxView : public ui::View { + public: + MorseRadiotxView(ui::NavigationView& nav); + ~MorseRadiotxView(); + + MorseRadiotxView(const MorseRadiotxView&) = delete; + MorseRadiotxView(MorseRadiotxView&&) = delete; + MorseRadiotxView& operator=(const MorseRadiotxView&) = delete; + MorseRadiotxView& operator=(MorseRadiotxView&&) = delete; + + std::string title() const override { return "Morse Tx"; } + void focus() override; + void on_show() override; + void transmit_morse_message(); + + private: + struct MorseTimings { + uint32_t dot_ms; + uint32_t dash_ms; + uint32_t symbol_gap; + uint32_t char_gap; + uint32_t word_gap; + }; + + MorseTimings current_timings{}; + + std::string msg_indicator{""}; + MorseTimings calculate_morse_timings(uint32_t wpm); + void onPress(); + void onRelease(); + void on_framesync(); + void writeCharToConsole(const std::string& ch, double confidence); + void ui_toggle(); + bool tx_button_held(); + void ptt_button_visibility(bool hidden); + + ui::NavigationView& nav_; + MorseDecoder morse_decoder_{}; + TxRadioState radio_state_{}; + Thread* tx_thread{nullptr}; + + std::string msg_buffer{"PORTAPACK"}; + uint8_t current_mode{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB + uint8_t wpm{20}; + uint32_t tone{700}; + float band{5.8}; + std::string call_sign{""}; + + app_settings::SettingsManager settings_{ + "tx_morseradio", + app_settings::Mode::TX, + { + {"cmode"sv, ¤t_mode}, + {"audtone"sv, &tone}, + {"wpm"sv, &wpm}, + {"message"sv, &msg_buffer}, + {"bandwith"sv, &band}, + {"call_sign"sv, &call_sign}, + }}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(4), + 10000, + 0, false}; + + AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y_BOTTOM(5)}}; + ui::OptionsField options_mode{ + {UI_POS_X(5), UI_POS_Y(0)}, + 3, + {{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}}; + NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true}; + NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true}; + FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1}; + + ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "}; + ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"}; + ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign}; + Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true}; + Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true}; + ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""}; + ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}}; + ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"}; + ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"}; + + ui::Labels labels{ + {{UI_POS_X(0), UI_POS_Y(0)}, "Mode:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground}, + }; + + uint8_t last_color_id{255}; + uint8_t color_id{255}; + std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN}; + + bool button_touch{false}; + bool button_was_selected{false}; + bool decode_timeout_calc{false}; + bool transmit{false}; + bool thread_running = false; + + uint8_t send_indicator{5}; + int64_t start_time{0}; + int64_t end_time{0}; + int64_t transmit_time{0}; + SwitchesState initial_switch_config_{}; + + MessageHandlerRegistration message_handler_framesync{ + Message::ID::DisplayFrameSync, + [this](const Message* const p) { + (void)p; + this->on_framesync(); + }}; +}; + +} // namespace ui::external_app::morseradiotx + +#endif // __MORSE_RADIOTX_H__ diff --git a/firmware/application/external/noaaapt_rx/main.cpp b/firmware/application/external/noaaapt_rx/main.cpp index 0e4c8e8fb..4e6e36e2a 100644 --- a/firmware/application/external/noaaapt_rx/main.cpp +++ b/firmware/application/external/noaaapt_rx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_noaaapt_rx.application_information"), 0x78, 0x00, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::RX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/ook_editor/main.cpp b/firmware/application/external/ook_editor/main.cpp index 39692a4ab..3b64951e6 100644 --- a/firmware/application/external/ook_editor/main.cpp +++ b/firmware/application/external/ook_editor/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_ook_editor.application_information"), 0xE0, 0x07, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::TX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/ookbrute/main.cpp b/firmware/application/external/ookbrute/main.cpp index e323fda26..6eab43f7f 100644 --- a/firmware/application/external/ookbrute/main.cpp +++ b/firmware/application/external/ookbrute/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_ookbrute.application_information"), us 0xE0, 0x07, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::TX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/p25_tx/main.cpp b/firmware/application/external/p25_tx/main.cpp new file mode 100644 index 000000000..7d17ca539 --- /dev/null +++ b/firmware/application/external/p25_tx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_p25_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::p25_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::p25_tx + +extern "C" { + +__attribute__((section(".external_app.app_p25_tx.application_information"), used)) +application_information_t _application_information_p25_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::p25_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "P25 TX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x7F, + 0x02, + 0x40, + 0xFA, + 0x5F, + 0x0A, + 0x50, + 0x0A, + 0x50, + 0xFA, + 0x5F, + 0x02, + 0x40, + 0x02, + 0x40, + 0xE2, + 0x47, + 0x22, + 0x44, + 0xE2, + 0x47, + 0x02, + 0x40, + 0x02, + 0x40, + 0xFE, + 0x7F, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + /*.m4_app_tag = */ {'P', '2', '5', 'T'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/p25_tx/ui_p25_tx.cpp b/firmware/application/external/p25_tx/ui_p25_tx.cpp new file mode 100644 index 000000000..7f78d3a20 --- /dev/null +++ b/firmware/application/external/p25_tx/ui_p25_tx.cpp @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_p25_tx.hpp" +#include "portapack.hpp" +#include "transmitter_model.hpp" +#include "baseband_api.hpp" +#include "ui_freq_field.hpp" +#include + +using namespace portapack; + +namespace ui::external_app::p25_tx { + +static constexpr uint8_t DUID_TSBK = 0x07; +static constexpr uint8_t SS_TSCC = 0x02; + +// BCH(63,16) generator matrix from TIA-102.BAAA-A (ccemu/p25/bch.go) +static const uint64_t bch_matrix[16] = { + 0x8000cd930bdd3b2aULL, + 0x4000ab5a8e33a6beULL, + 0x2000983e4cc4e874ULL, + 0x10004c1f2662743aULL, + 0x0800eb9c98ec0136ULL, + 0x0400b85d47ab3bb0ULL, + 0x02005c2ea3d59dd8ULL, + 0x01002e1751eaceecULL, + 0x0080170ba8f56776ULL, + 0x0040c616dfa78890ULL, + 0x0020630b6fd3c448ULL, + 0x00103185b7e9e224ULL, + 0x000818c2dbf4f112ULL, + 0x0004c1f2662743a2ULL, + 0x0002ad6a38ce9afbULL, + 0x00019b2617ba7657ULL, +}; + +static uint64_t bch_encode(uint16_t data) { + uint64_t cw = 0; + for (int i = 0; i < 16; i++) + if (data & (0x8000u >> i)) + cw ^= bch_matrix[i]; + return cw; +} + +// CRC-CCITT bit-by-bit, init=0, final XOR=0xFFFF (ccemu/p25/crc.go) +static uint16_t crc_ccitt_80(uint16_t high, uint64_t low) { + uint32_t crc = 0; + for (int i = 15; i >= 0; i--) { + crc <<= 1; + if (((crc >> 16) ^ ((high >> i) & 1)) & 1) crc ^= 0x1021; + } + for (int i = 63; i >= 0; i--) { + crc <<= 1; + if (((crc >> 16) ^ ((uint32_t)((low >> i) & 1))) & 1) crc ^= 0x1021; + } + return (uint16_t)((crc & 0xFFFF) ^ 0xFFFF); +} + +// 1/2-rate trellis: [state][inputDibit]={outDibit0,outDibit1} (ccemu/p25/trellis.go) +static const uint8_t trellis_table[4][4][2] = { + {{0, 2}, {3, 0}, {0, 1}, {3, 3}}, + {{3, 2}, {0, 0}, {3, 1}, {0, 3}}, + {{2, 1}, {1, 3}, {2, 2}, {1, 0}}, + {{1, 1}, {2, 3}, {1, 2}, {2, 0}}, +}; + +static void trellis_encode(const uint8_t* in48, uint8_t* out98) { + uint8_t state = 0; + for (int i = 0; i < 49; i++) { + uint8_t d = (i < 48) ? in48[i] : 0; + out98[i * 2 + 0] = trellis_table[state][d][0]; + out98[i * 2 + 1] = trellis_table[state][d][1]; + state = d; + } +} + +static void data_interleave(const uint8_t* in98, uint8_t* out98) { + int idx = 0; + for (int j = 0; j < 97; j += 8) { + out98[idx++] = in98[j]; + out98[idx++] = in98[j + 1]; + } + for (int i = 2; i < 7; i += 2) + for (int j = 0; j < 89; j += 8) { + out98[idx++] = in98[i + j]; + out98[idx++] = in98[i + j + 1]; + } +} + +static int insert_status(const uint8_t* data, int dlen, uint8_t* out, uint8_t ss) { + int num_ss = (dlen + 34) / 35, remaining = num_ss, oi = 0, i = 1; + for (int d = 0; d < dlen; d++) { + out[oi++] = data[d]; + if ((i % 35 == 0) && remaining > 0) { + out[oi++] = ss; + remaining--; + } + i++; + } + while (remaining > 0) { + out[oi++] = 0; + if (i % 35 == 0) { + out[oi++] = ss; + remaining--; + } + i++; + } + return oi; +} + +static void build_tsbk(uint8_t* out12, uint8_t opcode, uint64_t args) { + uint16_t high = (1u << 15) | ((uint16_t)opcode << 8); + uint16_t crc = crc_ccitt_80(high, args); + out12[0] = (uint8_t)(high >> 8); + out12[1] = (uint8_t)high; + for (int i = 0; i < 8; i++) out12[2 + i] = (uint8_t)(args >> (56 - 8 * i)); + out12[10] = (uint8_t)(crc >> 8); + out12[11] = (uint8_t)crc; +} + +static void tsbk_iden_up(uint8_t* out12, uint64_t freq_hz) { + uint64_t args = ((uint64_t)100 << 51) | ((uint64_t)100 << 32) | ((freq_hz / 5) & 0xFFFFFFFF); + build_tsbk(out12, 0x3D, args); +} +static void tsbk_net_status(uint8_t* out12, uint32_t wacn, uint16_t sysid) { + build_tsbk(out12, 0x3B, ((uint64_t)(wacn & 0xFFFFF) << 36) | ((uint64_t)(sysid & 0xFFF) << 24)); +} + +static void tsbk_grp_v_grant(uint8_t* out12, uint8_t chan_id, uint16_t chan_num, uint16_t tg) { + uint64_t ch = ((uint64_t)(chan_id & 0xF) << 12) | (chan_num & 0xFFF); + // opts: bit1=group + uint64_t args = ((uint64_t)0x02 << 56) | (ch << 40) | ((uint64_t)tg << 24); + build_tsbk(out12, 0x00, args); +} +static void tsbk_rfss_status(uint8_t* out12, uint16_t sysid, uint8_t rfssid, uint8_t siteid) { + build_tsbk(out12, 0x3A, ((uint64_t)(sysid & 0xFFF) << 40) | ((uint64_t)rfssid << 32) | ((uint64_t)siteid << 24)); +} + +static int build_frame(uint8_t* out, uint16_t nac, const uint8_t* tsbk12) { + uint8_t pre[160]; + int idx = 0; + static const uint64_t FSW = 0x5575F5FF77FFULL; + for (int i = 0; i < 24; i++) pre[idx++] = (uint8_t)((FSW >> (46 - i * 2)) & 3); + uint64_t nid = bch_encode((uint16_t)((nac << 4) | DUID_TSBK)); + for (int i = 0; i < 32; i++) pre[idx++] = (uint8_t)((nid >> (62 - i * 2)) & 3); + uint8_t in_d[48]; + for (int i = 0; i < 12; i++) { + in_d[i * 4 + 0] = (tsbk12[i] >> 6) & 3; + in_d[i * 4 + 1] = (tsbk12[i] >> 4) & 3; + in_d[i * 4 + 2] = (tsbk12[i] >> 2) & 3; + in_d[i * 4 + 3] = (tsbk12[i] >> 0) & 3; + } + uint8_t enc[98], ilv[98]; + trellis_encode(in_d, enc); + data_interleave(enc, ilv); + memcpy(&pre[idx], ilv, 98); + idx += 98; + return insert_status(pre, idx, out, SS_TSCC); +} + +static constexpr int TX_BUF_SIZE = 512; + +static int fill_tx_buffer(uint8_t* buf, uint16_t nac, uint32_t wacn, uint16_t sysid, uint8_t rfssid, uint8_t siteid, uint64_t freq_hz, uint16_t tg, uint16_t vch, int& tsbk_idx) { + memset(buf, 0, TX_BUF_SIZE); + int total = 0; + uint8_t tsbk[12]; + while (true) { + switch (tsbk_idx % 5) { + case 0: + tsbk_iden_up(tsbk, freq_hz); + break; + case 1: + tsbk_net_status(tsbk, wacn, sysid); + break; + case 2: + tsbk_rfss_status(tsbk, sysid, rfssid, siteid); + break; + case 3: + tsbk_grp_v_grant(tsbk, 0, vch, tg); + break; + default: + tsbk_net_status(tsbk, wacn, sysid); + break; + } + tsbk_idx++; + uint8_t probe[200]; + int flen = build_frame(probe, nac, tsbk); + if (total + flen > TX_BUF_SIZE) break; + memcpy(&buf[total], probe, flen); + total += flen; + } + return total; +} + +void P25TxView::start_tx() { + uint8_t dibits[TX_BUF_SIZE]; + int len = fill_tx_buffer(dibits, + (uint16_t)field_nac.to_integer(), (uint32_t)field_wacn.to_integer(), + (uint16_t)field_sysid.to_integer(), (uint8_t)field_rfssid.to_integer(), + (uint8_t)field_siteid.to_integer(), (uint64_t)transmitter_model.target_frequency(), + (uint16_t)field_tg.to_integer(), (uint16_t)field_vch.to_integer(), + tsbk_idx_); + transmitter_model.enable(); + tx_view.set_transmitting(true); + text_status.set("TX TSCC"); + transmitting = true; + baseband::set_p25tx_data(dibits, (uint16_t)len); +} + +void P25TxView::stop_tx() { + transmitting = false; + transmitter_model.disable(); + tx_view.set_transmitting(false); + text_status.set("Ready"); + tsbk_idx_ = 0; +} + +void P25TxView::on_tx_progress(const uint32_t, const bool done) { + if (done && transmitting) { + uint8_t dibits[TX_BUF_SIZE]; + int len = fill_tx_buffer(dibits, + (uint16_t)field_nac.to_integer(), (uint32_t)field_wacn.to_integer(), + (uint16_t)field_sysid.to_integer(), (uint8_t)field_rfssid.to_integer(), + (uint8_t)field_siteid.to_integer(), (uint64_t)transmitter_model.target_frequency(), + (uint16_t)field_tg.to_integer(), (uint16_t)field_vch.to_integer(), + tsbk_idx_); + baseband::set_p25tx_data(dibits, (uint16_t)len); + } +} + +P25TxView::P25TxView(NavigationView& nav) + : nav_{nav} { + add_children({&labels_, &field_nac, &field_sysid, &field_wacn, + &field_rfssid, &field_siteid, &field_tg, &field_vch, + &text_status, &tx_view}); + field_nac.set_value(0x293); + field_wacn.set_value(0xBEEF0); + field_sysid.set_value(0x001); + field_rfssid.set_value(1); + field_siteid.set_value(1); + field_tg.set_value(1); + field_vch.set_value(1); + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + tx_view.on_start = [this]() { start_tx(); }; + tx_view.on_stop = [this]() { stop_tx(); }; + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { transmitter_model.set_target_frequency(f); }; + }; +} + +P25TxView::~P25TxView() { + if (transmitting) + stop_tx(); + transmitter_model.disable(); + baseband::shutdown(); +} + +void P25TxView::focus() { + field_nac.focus(); +} + +} // namespace ui::external_app::p25_tx diff --git a/firmware/application/external/p25_tx/ui_p25_tx.hpp b/firmware/application/external/p25_tx/ui_p25_tx.hpp new file mode 100644 index 000000000..b54f31af1 --- /dev/null +++ b/firmware/application/external/p25_tx/ui_p25_tx.hpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_P25_TX +#define __UI_P25_TX + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_transmitter.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "message.hpp" + +namespace ui::external_app::p25_tx { + +class P25TxView : public View { + public: + explicit P25TxView(NavigationView& nav); + ~P25TxView(); + + void focus() override; + std::string title() const override { return "P25 TX"; } + + private: + void start_tx(); + void stop_tx(); + void on_tx_progress(const uint32_t progress, const bool done); + NavigationView& nav_; + + TxRadioState radio_state_{ + 155'000'000, + 12500, + 2'400'000}; + + app_settings::SettingsManager settings_{ + "tx_p25_tx", + app_settings::Mode::TX}; + + Labels labels_{{ + {{UI_POS_X(0), UI_POS_Y(0)}, "NAC:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(0)}, "SYS:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(1)}, "WACN:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "RFSS:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(2)}, "SITE:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "TG:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(3)}, "VCH:", ui::Theme::getInstance()->fg_light->foreground}, + }}; + + SymField field_nac{{UI_POS_X(4), UI_POS_Y(0)}, 3, SymField::Type::Hex}; + SymField field_sysid{{UI_POS_X(13), UI_POS_Y(0)}, 3, SymField::Type::Hex}; + SymField field_wacn{{UI_POS_X(5), UI_POS_Y(1)}, 5, SymField::Type::Hex}; + SymField field_rfssid{{UI_POS_X(5), UI_POS_Y(2)}, 2, SymField::Type::Hex}; + SymField field_siteid{{UI_POS_X(14), UI_POS_Y(2)}, 2, SymField::Type::Hex}; + SymField field_tg{{UI_POS_X(3), UI_POS_Y(3)}, 4, SymField::Type::Hex}; + SymField field_vch{{UI_POS_X(13), UI_POS_Y(3)}, 3, SymField::Type::Hex}; + + Text text_status{{0, UI_POS_Y(4), UI_POS_MAXWIDTH, UI_POS_DEFAULT_HEIGHT}, "Ready"}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(5), + 12500, + 0}; + + bool transmitting{false}; + int tsbk_idx_{0}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.progress, message.done); + }}; +}; + +} // namespace ui::external_app::p25_tx + +#endif /* __UI_P25_TX */ diff --git a/firmware/application/external/pocsag_tx/main.cpp b/firmware/application/external/pocsag_tx/main.cpp new file mode 100644 index 000000000..dc2a95fcf --- /dev/null +++ b/firmware/application/external/pocsag_tx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_pocsag_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::pocsag_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::pocsag_tx + +extern "C" { + +__attribute__((section(".external_app.app_pocsag_tx.application_information"), used)) application_information_t _application_information_pocsag_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::pocsag_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "POCSG TX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFC, + 0x3F, + 0xFE, + 0x7F, + 0x02, + 0x40, + 0xBA, + 0x45, + 0x02, + 0x40, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0x92, + 0x7C, + 0x92, + 0x7C, + 0xFC, + 0x3F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_fsktx */ {'P', 'F', 'S', 'K'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/apps/ui_pocsag_tx.cpp b/firmware/application/external/pocsag_tx/ui_pocsag_tx.cpp similarity index 96% rename from firmware/application/apps/ui_pocsag_tx.cpp rename to firmware/application/external/pocsag_tx/ui_pocsag_tx.cpp index b71aca63f..7c7a841aa 100644 --- a/firmware/application/apps/ui_pocsag_tx.cpp +++ b/firmware/application/external/pocsag_tx/ui_pocsag_tx.cpp @@ -31,7 +31,7 @@ using namespace portapack; using namespace pocsag; -namespace ui { +namespace ui::external_app::pocsag_tx { #define MAX_POCSAG_LENGTH 80 @@ -44,7 +44,7 @@ POCSAGTXView::~POCSAGTXView() { baseband::shutdown(); } -void POCSAGTXView::on_remote(const PocsagTosendMessage data) { +void POCSAGTXView::on_remete(const PocsagTosendMessage data) { // check if still sending or not size_t tmp = 0; if (data.baud == 1200) tmp = 1; @@ -159,7 +159,7 @@ void POCSAGTXView::on_set_text(NavigationView& nav) { POCSAGTXView::POCSAGTXView( NavigationView& nav) : nav_(nav) { - baseband::run_image(portapack::spi_flash::image_tag_fsktx); + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); add_children({&labels, &options_bitrate, @@ -205,4 +205,4 @@ POCSAGTXView::POCSAGTXView( }; } -} /* namespace ui */ +} /* namespace ui::external_app::pocsag_tx */ diff --git a/firmware/application/apps/ui_pocsag_tx.hpp b/firmware/application/external/pocsag_tx/ui_pocsag_tx.hpp similarity index 94% rename from firmware/application/apps/ui_pocsag_tx.hpp rename to firmware/application/external/pocsag_tx/ui_pocsag_tx.hpp index daafb574e..0005688ce 100644 --- a/firmware/application/apps/ui_pocsag_tx.hpp +++ b/firmware/application/external/pocsag_tx/ui_pocsag_tx.hpp @@ -37,7 +37,7 @@ using namespace pocsag; -namespace ui { +namespace ui::external_app::pocsag_tx { class POCSAGTXView : public View { public: @@ -76,7 +76,7 @@ class POCSAGTXView : public View { void on_set_text(NavigationView& nav); void on_tx_progress(const uint32_t progress, const bool done); - void on_remote(const PocsagTosendMessage data); + void on_remete(const PocsagTosendMessage data); bool start_tx(); Labels labels{ @@ -140,21 +140,21 @@ class POCSAGTXView : public View { 10000, 9}; + MessageHandlerRegistration message_handler_tx_remete{ + Message::ID::PocsagTosend, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_remete(message); + }}; + MessageHandlerRegistration message_handler_tx_progress{ Message::ID::TXProgress, [this](const Message* const p) { const auto message = *reinterpret_cast(p); this->on_tx_progress(message.progress, message.done); }}; - - MessageHandlerRegistration message_handler_tx_remote{ - Message::ID::PocsagTosend, - [this](const Message* const p) { - const auto message = *reinterpret_cast(p); - this->on_remote(message); - }}; }; -} /* namespace ui */ +} /* namespace ui::external_app::pocsag_tx */ #endif /*__POCSAG_TX_H__*/ diff --git a/firmware/application/external/random_password/ui_random_password.cpp b/firmware/application/external/random_password/ui_random_password.cpp index 5c6372eac..1c87d58f6 100644 --- a/firmware/application/external/random_password/ui_random_password.cpp +++ b/firmware/application/external/random_password/ui_random_password.cpp @@ -239,10 +239,10 @@ void RandomPasswordView::on_freqchg(int64_t freq) { } void RandomPasswordView::set_random_freq() { - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); // this is only for seed to visit random freq, the radio is still real random - auto random_freq = 100000000 + (std::rand() % 900000000); // 100mhz to 1ghz + auto random_freq = 100000000 + (rand() % 900000000); // 100mhz to 1ghz receiver_model.set_target_frequency(random_freq); field_frequency.set_value(random_freq); } @@ -297,12 +297,12 @@ void RandomPasswordView::new_password() { /// roll worker for (int i = 0; i < password_length * 2; i += 2) { unsigned int seed = seeds_deque[i]; - std::srand(seed); + srand(seed); uint8_t rollnum = (uint8_t)(seeds_deque[i + 1] % 128); uint8_t nu = 0; - for (uint8_t o = 0; o < rollnum; ++o) nu = std::rand(); + for (uint8_t o = 0; o < rollnum; ++o) nu = rand(); nu++; - char c = charset[std::rand() % charset.length()]; + char c = charset[rand() % charset.length()]; initial_password += c; } diff --git a/firmware/application/external/rtty_rx/baudot.cpp b/firmware/application/external/rtty_rx/baudot.cpp new file mode 100644 index 000000000..bdf3e811a --- /dev/null +++ b/firmware/application/external/rtty_rx/baudot.cpp @@ -0,0 +1,104 @@ +#include "baudot.hpp" +#include + +namespace ui::external_app::rtty_rx { + +constexpr uint8_t CODE_FIGS = 0x1B; +constexpr uint8_t CODE_LTRS = 0x1F; +constexpr uint8_t CODE_SPACE = 0x04; + +char BaudotCoder::get_char_mapping(bool is_figures, uint8_t index) const { + if (index >= 32) return 0; // Safety check + + if (is_figures) { + // ITA2 FIGURES Table + const char figures[32] = { + 0, '3', '\n', '-', ' ', '\'', '8', '7', + '\r', '$', '4', '\a', ',', '!', ':', '(', + '5', '+', ')', '2', '#', '6', '0', '1', + '9', '?', '&', 0, '.', '/', '=', 0}; + return figures[index]; + } else { + // ITA2 LETTERS Table + const char letters[32] = { + 0, 'E', '\n', 'A', ' ', 'S', 'I', 'U', + '\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K', + 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', + 'O', 'B', 'G', 0, 'M', 'X', 'V', 0}; + return letters[index]; + } +} + +// --- DECODE --- +char BaudotCoder::decode(uint8_t baudotCode) { + uint8_t code = baudotCode & 0x1F; + if (code == CODE_FIGS) { + shiftState = FIGURES; + return 0; + } + if (code == CODE_LTRS) { + shiftState = LETTERS; + return 0; + } + if (usos_enabled && shiftState == FIGURES && code == CODE_SPACE) { + shiftState = LETTERS; + return ' '; + } + return get_char_mapping((shiftState == FIGURES), code); +} + +void BaudotCoder::encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size) { + uint16_t idx = 0; + + for (char c : src) { + if (idx >= dest_max_size) break; + + char upper_c = std::toupper(c); + uint8_t code = 0; + bool found = false; + bool target_is_figures = false; + if (upper_c == ' ') { + code = CODE_SPACE; + found = true; + } else { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(false, i) == upper_c) { + code = i; + found = true; + target_is_figures = false; + break; + } + } + if (!found) { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(true, i) == upper_c) { + code = i; + found = true; + target_is_figures = true; + break; + } + } + } + } + + if (found) { + if (target_is_figures && shiftState != FIGURES) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_FIGS; + shiftState = FIGURES; + } else if (!target_is_figures && shiftState != LETTERS && code != CODE_SPACE) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_LTRS; + shiftState = LETTERS; + } + + dest[idx++] = code; + } + } + + if (dest_length) { + *dest_length = idx; + } +} + +} // namespace ui::external_app::rtty_rx \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/baudot.hpp b/firmware/application/external/rtty_rx/baudot.hpp new file mode 100644 index 000000000..9e9b5f068 --- /dev/null +++ b/firmware/application/external/rtty_rx/baudot.hpp @@ -0,0 +1,28 @@ +#ifndef BAUDOT_HPP +#define BAUDOT_HPP + +#include +#include + +namespace ui::external_app::rtty_rx { + +class BaudotCoder { + public: + enum ShiftState { LETTERS, + FIGURES }; + + BaudotCoder() + : shiftState(LETTERS) {} + char decode(uint8_t baudotCode); + void encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size); + void set_usos(bool enable) { usos_enabled = enable; } // shift == set to letters too + + private: + ShiftState shiftState; + bool usos_enabled = true; + char get_char_mapping(bool is_figures, uint8_t index) const; +}; + +} // namespace ui::external_app::rtty_rx + +#endif // BAUDOT_HPP \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/main.cpp b/firmware/application/external/rtty_rx/main.cpp new file mode 100644 index 000000000..f75046ff9 --- /dev/null +++ b/firmware/application/external/rtty_rx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_rtty_rx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::rtty_rx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::rtty_rx + +extern "C" { + +__attribute__((section(".external_app.app_rtty_rx.application_information"), used)) application_information_t _application_information_rtty_rx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::rtty_rx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "RTTY", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x1F, + 0xFE, + 0x3F, + 0x0E, + 0x78, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x78, + 0xFE, + 0x3F, + 0xFE, + 0x1F, + 0x8E, + 0x07, + 0x0E, + 0x0F, + 0x0E, + 0x1E, + 0x0E, + 0x3C, + 0x0E, + 0x78, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_rttyrx */ {'P', 'R', 'T', 'R'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/ui_rtty_rx.cpp b/firmware/application/external/rtty_rx/ui_rtty_rx.cpp new file mode 100644 index 000000000..d36050dc1 --- /dev/null +++ b/firmware/application/external/rtty_rx/ui_rtty_rx.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_rtty_rx.hpp" + +#include "audio.hpp" +#include "rtc_time.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; +using namespace ui; + +// todo add timeout, so add newline and time when new data arrives after a time. + +namespace ui::external_app::rtty_rx { + +void RttyRxView::focus() { + field_frequency.focus(); +} + +RttyRxView::RttyRxView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + add_children({ + &rssi, + &field_rf_amp, + &field_lna, + &field_vga, + &field_volume, + &field_frequency, + &labels, + &options_baud, + &console, + }); + field_frequency.set_step(100); + audio::set_rate(audio::Rate::Hz_24000); + audio::output::start(); + + receiver_model.set_hidden_offset(0); + receiver_model.set_sampling_rate(3072000); // set the needed baseband SR. + receiver_model.set_baseband_bandwidth(1750000); // set the front-end RF BW filter. + receiver_model.enable(); + console.enable_scrolling(false); + + options_baud.on_change = [this](size_t, int32_t value) { + baseband::set_rtty_config(value, 170); + baud_conf = value; + }; + options_baud.set_by_value(baud_conf); + if (baud_conf == 0) { // to trigger it when not changed + baseband::set_rtty_config(0, 170); + } +} + +void RttyRxView::got_message(std::string msg) { + console.write(msg); +} + +RttyRxView::~RttyRxView() { + receiver_model.set_hidden_offset(0); + receiver_model.disable(); + baseband::shutdown(); + audio::output::stop(); +} + +void RttyRxView::on_data(const RTTYDataMessage* message) { + std::string msg = ""; + for (size_t i = 0; i < message->data_len; i++) { + const auto c = baudot_decoder.decode(message->data[i]); + if (c != 0) + msg += c; + } + got_message(msg); +} + +} // namespace ui::external_app::rtty_rx \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/ui_rtty_rx.hpp b/firmware/application/external/rtty_rx/ui_rtty_rx.hpp new file mode 100644 index 000000000..47101dcbe --- /dev/null +++ b/firmware/application/external/rtty_rx/ui_rtty_rx.hpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_RTTY_RX_H__ +#define __UI_RTTY_RX_H__ + +#include "ui.hpp" +#include "ui_language.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "ui_fileman.hpp" +#include "file_path.hpp" +#include "baudot.hpp" + +using namespace ui; + +namespace ui::external_app::rtty_rx { + +class RttyRxView : public View { + public: + RttyRxView(NavigationView& nav); + ~RttyRxView(); + + void focus() override; + + std::string title() const override { return "RTTY"; }; + + private: + NavigationView& nav_; + RxRadioState radio_state_{}; + uint32_t baud_conf = 0; + app_settings::SettingsManager settings_{ + "rx_rtty", + app_settings::Mode::RX, + { + {"baud_conf"sv, &baud_conf}, + }}; + + RxFrequencyField field_frequency{ + {UI_POS_X(0), UI_POS_Y(0)}, + nav_}; + RFAmpField field_rf_amp{ + {UI_POS_X(13), UI_POS_Y(0)}}; + LNAGainField field_lna{ + {UI_POS_X(15), UI_POS_Y(0)}}; + VGAGainField field_vga{ + {UI_POS_X(18), UI_POS_Y(0)}}; + RSSI rssi{ + {UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}}; + AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(1)}, "Baud:", Theme::getInstance()->fg_light->foreground}}; + + OptionsField options_baud{ + {UI_POS_X(7), UI_POS_Y(1)}, + 5, + {{"Auto", 0}, + {"45", 4500}, + {"45.45", 4545}, + {"50", 5000}, + {"75", 7500}, + {"100", 10000}, + {"110", 11000}, + {"150", 15000}, + {"200", 20000}}}; + + Console console{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(3)}}; + + BaudotCoder baudot_decoder{}; + + void on_data(const RTTYDataMessage* message); + void got_message(std::string msg); + + MessageHandlerRegistration message_handler_data{ + Message::ID::RTTYData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_data(message); + }}; +}; + +} // namespace ui::external_app::rtty_rx + +#endif /*__UI_RTTY_RX_H__*/ \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/baudot.cpp b/firmware/application/external/rtty_tx/baudot.cpp new file mode 100644 index 000000000..be47ed2a3 --- /dev/null +++ b/firmware/application/external/rtty_tx/baudot.cpp @@ -0,0 +1,104 @@ +#include "baudot.hpp" +#include + +namespace ui::external_app::rtty_tx { + +constexpr uint8_t CODE_FIGS = 0x1B; +constexpr uint8_t CODE_LTRS = 0x1F; +constexpr uint8_t CODE_SPACE = 0x04; + +char BaudotCoder::get_char_mapping(bool is_figures, uint8_t index) const { + if (index >= 32) return 0; // Safety check + + if (is_figures) { + // ITA2 FIGURES Table + const char figures[32] = { + 0, '3', '\n', '-', ' ', '\'', '8', '7', + '\r', '$', '4', '\a', ',', '!', ':', '(', + '5', '+', ')', '2', '#', '6', '0', '1', + '9', '?', '&', 0, '.', '/', '=', 0}; + return figures[index]; + } else { + // ITA2 LETTERS Table + const char letters[32] = { + 0, 'E', '\n', 'A', ' ', 'S', 'I', 'U', + '\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K', + 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', + 'O', 'B', 'G', 0, 'M', 'X', 'V', 0}; + return letters[index]; + } +} + +// --- DECODE --- +char BaudotCoder::decode(uint8_t baudotCode) { + uint8_t code = baudotCode & 0x1F; + if (code == CODE_FIGS) { + shiftState = FIGURES; + return 0; + } + if (code == CODE_LTRS) { + shiftState = LETTERS; + return 0; + } + if (usos_enabled && shiftState == FIGURES && code == CODE_SPACE) { + shiftState = LETTERS; + return ' '; + } + return get_char_mapping((shiftState == FIGURES), code); +} + +void BaudotCoder::encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size) { + uint16_t idx = 0; + + for (char c : src) { + if (idx >= dest_max_size) break; + + char upper_c = std::toupper(c); + uint8_t code = 0; + bool found = false; + bool target_is_figures = false; + if (upper_c == ' ') { + code = CODE_SPACE; + found = true; + } else { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(false, i) == upper_c) { + code = i; + found = true; + target_is_figures = false; + break; + } + } + if (!found) { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(true, i) == upper_c) { + code = i; + found = true; + target_is_figures = true; + break; + } + } + } + } + + if (found) { + if (target_is_figures && shiftState != FIGURES) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_FIGS; + shiftState = FIGURES; + } else if (!target_is_figures && shiftState != LETTERS && code != CODE_SPACE) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_LTRS; + shiftState = LETTERS; + } + + dest[idx++] = code; + } + } + + if (dest_length) { + *dest_length = idx; + } +} + +} // namespace ui::external_app::rtty_tx \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/baudot.hpp b/firmware/application/external/rtty_tx/baudot.hpp new file mode 100644 index 000000000..7ccea500f --- /dev/null +++ b/firmware/application/external/rtty_tx/baudot.hpp @@ -0,0 +1,28 @@ +#ifndef BAUDOT_HPP +#define BAUDOT_HPP + +#include +#include + +namespace ui::external_app::rtty_tx { + +class BaudotCoder { + public: + enum ShiftState { LETTERS, + FIGURES }; + + BaudotCoder() + : shiftState(LETTERS) {} + char decode(uint8_t baudotCode); + void encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size); + void set_usos(bool enable) { usos_enabled = enable; } // shift == set to letters too + + private: + ShiftState shiftState; + bool usos_enabled = true; + char get_char_mapping(bool is_figures, uint8_t index) const; +}; + +} // namespace ui::external_app::rtty_tx + +#endif // BAUDOT_HPP \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/main.cpp b/firmware/application/external/rtty_tx/main.cpp new file mode 100644 index 000000000..9d80484e9 --- /dev/null +++ b/firmware/application/external/rtty_tx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_rtty_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::rtty_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::rtty_tx + +extern "C" { + +__attribute__((section(".external_app.app_rtty_tx.application_information"), used)) application_information_t _application_information_rtty_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::rtty_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "RTTY", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x1F, + 0xFE, + 0x3F, + 0x0E, + 0x78, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x78, + 0xFE, + 0x3F, + 0xFE, + 0x1F, + 0x8E, + 0x07, + 0x0E, + 0x0F, + 0x0E, + 0x1E, + 0x0E, + 0x3C, + 0x0E, + 0x78, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_rttytx */ {'P', 'R', 'T', 'T'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/ui_rtty_tx.cpp b/firmware/application/external/rtty_tx/ui_rtty_tx.cpp new file mode 100644 index 000000000..8bbd552e7 --- /dev/null +++ b/firmware/application/external/rtty_tx/ui_rtty_tx.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_rtty_tx.hpp" + +#include "audio.hpp" +#include "rtc_time.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; +using namespace ui; + +namespace ui::external_app::rtty_tx { + +void RttyTxView::focus() { + options_baud.focus(); +} + +RttyTxView::RttyTxView(NavigationView& nav) + : nav_{nav} { + add_children({&tx_view, + &labels, + &options_baud, + &options_shift, + &btn_message, + &text_message, + &check_inverted, + &options_tone, + &text_tones}); + + options_baud.on_change = [this](size_t, int32_t value) { + baud_conf = value; + }; + options_baud.set_by_value(baud_conf); + + options_shift.on_change = [this](size_t, int32_t value) { + shift = value; + refresh_tones(); + }; + options_shift.set_by_value(shift); + + btn_message.on_select = [this](Button&) { + text_prompt(nav_, message, 64, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_message) { + message = new_message; + text_message.set(message); // maybe replace to console + }); + }; + text_message.set(message); + options_tone.set_by_value(mark_tone); + options_tone.on_change = [this](size_t, int32_t value) { + mark_tone = value; + refresh_tones(); + }; + check_inverted.on_select = [this](Checkbox&, bool) { + refresh_tones(); + }; + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + transmitter_model.set_target_frequency(f); + }; + }; + + tx_view.on_start = [this]() { + if (message.empty()) { + nav_.display_modal("Error", "Message is empty. Please set a message to transmit."); + return; + } + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + transmitter_model.set_baseband_bandwidth(2048000); + baseband::set_sample_rate(2048000, OversampleRate::None); + transmitter_model.set_sampling_rate(2048000); + transmitter_model.set_baseband_bandwidth(2048000); + transmitter_model.enable(); + rtty_message.baud = baud_conf; + rtty_message.shift = shift; + rtty_message.inverted = false; // check_inverted.value(); //i already invert it with the mark and space calculation + rtty_message.mark_tone = mark; + rtty_message.space_tone = space; + rtty_message.stopbits = stop_bits; + baudot_encoder.set_usos(false); // to be compatible with all + baudot_encoder.encode(message, rtty_message.data, &rtty_message.data_len, rtty_message.max_len); + baseband::set_rtty_config(rtty_message); + tx_view.set_transmitting(true); + }; + + tx_view.on_stop = [this]() { + stop(); + }; + + refresh_tones(); +} + +void RttyTxView::refresh_tones() { + std::string tones_str = ""; + + int16_t shift = options_shift.selected_index_value(); + if (mark_tone == -32000) { + mark = -shift / 2; + space = shift / 2; + } else { + if (mark_tone < 0) { + space = -(shift + abs(mark_tone)); + } else { + space = shift + mark_tone; + } + mark = mark_tone; + } + + if (check_inverted.value()) { + int16_t tmp = mark; + mark = space; + space = tmp; + } + tones_str = "Mark:" + to_string_dec_int(mark) + " Hz, Space:" + to_string_dec_int(space) + " Hz"; + text_tones.set(tones_str); +} + +void RttyTxView::on_tx_progress(bool done) { + if (done) stop(); +} + +void RttyTxView::stop() { + tx_view.set_transmitting(false); + transmitter_model.disable(); + baseband::shutdown(); +} + +RttyTxView::~RttyTxView() { + stop(); +} + +} // namespace ui::external_app::rtty_tx \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/ui_rtty_tx.hpp b/firmware/application/external/rtty_tx/ui_rtty_tx.hpp new file mode 100644 index 000000000..3bf4ba435 --- /dev/null +++ b/firmware/application/external/rtty_tx/ui_rtty_tx.hpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_RTTY_TX_H__ +#define __UI_RTTY_TX_H__ + +#include "ui.hpp" +#include "ui_language.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "ui_fileman.hpp" +#include "file_path.hpp" +#include "baudot.hpp" + +using namespace ui; + +namespace ui::external_app::rtty_tx { + +class RttyTxView : public View { + public: + RttyTxView(NavigationView& nav); + ~RttyTxView(); + void focus() override; + std::string title() const override { return "RTTY"; }; + + private: + void on_tx_progress(bool done); + void stop(); + void refresh_tones(); + + NavigationView& nav_; + RxRadioState radio_state_{}; + uint32_t baud_conf = 4545; // default to 45.45 baud, which is common for RTTY. + uint32_t shift = 170; // 170 hz + std::string message = "PORTAPACK"; + int32_t mark_tone = 0; // for option + int16_t mark = 0, space = 170; + uint8_t stop_bits = 3; // 2=1.0, 3=1.5, 4=2.0 + app_settings::SettingsManager settings_{ + "tx_rtty", + app_settings::Mode::TX, + { + {"baud_conf"sv, &baud_conf}, + {"shift"sv, &shift}, + {"message"sv, &message}, + {"mark_tone"sv, &mark_tone}, + }}; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(0)}, "Baud:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(14), UI_POS_Y(0)}, "Shift:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(15), UI_POS_Y(1)}, "Mark t:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "Message:", Theme::getInstance()->fg_light->foreground}}; + + OptionsField options_baud{ + {UI_POS_X(7), UI_POS_Y(0)}, + 5, + {{"45", 4500}, + {"45.45", 4545}, + {"50", 5000}, + {"75", 7500}, + {"100", 10000}, + {"110", 11000}, + {"150", 15000}, + {"200", 20000}}}; + + OptionsField options_shift{ + {UI_POS_X(21), UI_POS_Y(0)}, + 5, + {{"170", 170}, + {"85", 85}, + {"200", 200}, + {"450", 450}, + {"850", 850}}}; + + Checkbox check_inverted{ + {UI_POS_X(0), UI_POS_Y(1)}, + 10, + "Inverted", + true}; + + OptionsField options_tone{ + {UI_POS_X(25), UI_POS_Y(1)}, + 5, + {{"CNT", -32000}, + {"0", 0}, + {"2125", 2125}, + {"-2125", -2125}, + {"1275", 1275}, + {"-1275", -1275}}}; + + Text text_tones{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + "-"}; + + Button btn_message{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(2)}, + "Set message"}; + + Text text_message{ + {UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + ""}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(4), + 1000, + 0}; + + RTTYDataMessage rtty_message{}; + BaudotCoder baudot_encoder{}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.done); + }}; +}; + +} // namespace ui::external_app::rtty_tx + +#endif /*__UI_RTTY_TX_H__*/ \ No newline at end of file diff --git a/firmware/application/external/same_tx/main.cpp b/firmware/application/external/same_tx/main.cpp new file mode 100644 index 000000000..858e32ed1 --- /dev/null +++ b/firmware/application/external/same_tx/main.cpp @@ -0,0 +1,79 @@ +/* + * SAME TX - Specific Area Message Encoding Transmitter + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "ui.hpp" +#include "ui_same_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::same_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::same_tx + +extern "C" { + +__attribute__((section(".external_app.app_same_tx.application_information"), used)) application_information_t _application_information_same_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::same_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "SAME TX", + /*.bitmap_data = */ { + // 16x16 icon - simple "!" alert symbol + 0x00, + 0x00, + 0x18, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x18, + 0x00, + 0x18, + 0x00, + 0x00, + 0x00, + 0x18, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x18, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_afsk */ {'P', 'A', 'F', 'T'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/same_tx/ui_same_tx.cpp b/firmware/application/external/same_tx/ui_same_tx.cpp new file mode 100644 index 000000000..849ee6c85 --- /dev/null +++ b/firmware/application/external/same_tx/ui_same_tx.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ +#include "ui_same_tx.hpp" +#include "theme.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "portapack_shared_memory.hpp" +#include "transmitter_model.hpp" + +using namespace portapack; + +namespace ui::external_app::same_tx { + +static constexpr uint32_t SAME_SAMPLES_PER_BIT = 2949; +static constexpr uint32_t SAME_MARK_FREQ = 2083; +static constexpr uint32_t SAME_SPACE_FREQ = 1563; +static constexpr uint32_t SAME_FM_DEVIATION = 5000; +static constexpr uint8_t SAME_REPEAT = 3; + +static constexpr char SAME_ORG_CODES[][4] = {"WXR", "EAS", "CIV", "PEP"}; +static constexpr char SAME_EVT_CODES[][4] = { + "RWT", + "RMT", + "NPT", + "NST", + "NMT", + "EAN", + "EAT", + "NIC", + "ADR", + "AVA", + "AVW", + "BZW", + "CFW", + "CFS", + "DSW", + "EQW", + "EVI", + "FFW", + "FFS", + "FFH", + "FRW", + "HLS", + "HUW", + "HUH", + "SVR", + "TOR", +}; + +static uint8_t bitrev8(uint8_t b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +static void fmt2d(char* buf, int v) { + buf[0] = '0' + v / 10; + buf[1] = '0' + v % 10; +} +static void fmt3d(char* buf, int v) { + buf[0] = '0' + v / 100; + buf[1] = '0' + (v / 10) % 10; + buf[2] = '0' + v % 10; +} + +void SameTxView::update_msg_preview() { + const char* org = SAME_ORG_CODES[field_org.selected_index()]; + const char* evt = SAME_EVT_CODES[field_event_idx.value()]; + int ss = field_fips_state.value(); + int ccc = field_fips_county.value(); + int dh = field_dur_h.value(); + int dm = field_dur_m.value(); + char buf[48]; + char* p = buf; + // "WXR-RWT-SS0CCC+HHmm" (preview, no ZCZC header) + for (const char* s = org; *s; s++) *p++ = *s; + *p++ = '-'; + for (const char* s = evt; *s; s++) *p++ = *s; + *p++ = '-'; + fmt2d(p, ss); + p += 2; + *p++ = '0'; + fmt3d(p, ccc); + p += 3; + *p++ = '+'; + fmt2d(p, dh); + p += 2; + fmt2d(p, dm); + p += 2; + *p = '\0'; + text_msg.set(buf); +} + +void SameTxView::start_tx() { + const char* org = SAME_ORG_CODES[field_org.selected_index()]; + const char* evt = SAME_EVT_CODES[field_event_idx.value()]; + int ss = field_fips_state.value(); + int ccc = field_fips_county.value(); + int dh = field_dur_h.value(); + int dm = field_dur_m.value(); + + // Build full SAME message: "ZCZC-ORG-EVT-SS0CCC+HHMM-0010000-STATION-" + char msg[64]; + char* p = msg; + const char* hdr = "ZCZC-"; + for (const char* s = hdr; *s; s++) *p++ = *s; + for (const char* s = org; *s; s++) *p++ = *s; + *p++ = '-'; + for (const char* s = evt; *s; s++) *p++ = *s; + *p++ = '-'; + fmt2d(p, ss); + p += 2; + *p++ = '0'; + fmt3d(p, ccc); + p += 3; + *p++ = '+'; + fmt2d(p, dh); + p += 2; + fmt2d(p, dm); + p += 2; + *p++ = '-'; + // Issue time: fixed 0010000 (placeholder), station: SAME-TX + const char* tail = "0010000-SAMETX--"; + for (const char* s = tail; *s; s++) *p++ = *s; + *p = '\0'; + + auto* words = reinterpret_cast(shared_memory.bb_data.data); + size_t idx = 0; + for (uint8_t i = 0; i < 16; i++) + words[idx++] = bitrev8(0xAB); + for (const char* c = msg; *c; c++) + words[idx++] = bitrev8(static_cast(*c)); + words[idx] = 0; + + tx_active_ = true; + text_status.set("Transmitting..."); + transmitter_model.enable(); + baseband::set_afsk_data( + SAME_SAMPLES_PER_BIT, + SAME_MARK_FREQ, + SAME_SPACE_FREQ, + SAME_REPEAT, + SAME_FM_DEVIATION, + 8); +} + +void SameTxView::stop_tx() { + baseband::kill_afsk(); + transmitter_model.disable(); + tx_active_ = false; + text_status.set("Ready"); + tx_view.set_transmitting(false); +} + +void SameTxView::on_tx_progress(const uint32_t progress, const bool done) { + if (done) { + stop_tx(); + text_status.set("Done!"); + } else { + (void)progress; + text_status.set("TX..."); + } +} + +void SameTxView::focus() { + field_org.focus(); +} + +SameTxView::SameTxView(NavigationView& nav) + : nav_{nav} { + baseband::run_image(portapack::spi_flash::image_tag_afsk); + + add_children({ + &labels, + &field_org, + &field_event_idx, + &text_evt, + &field_fips_state, + &field_fips_county, + &field_dur_h, + &field_dur_m, + &text_msg, + &text_status, + &tx_view, + }); + + // Defaults: Ohio, county 007 (Ashtabula), 30 min + field_fips_state.set_value(39); + field_fips_county.set_value(7); + field_dur_h.set_value(0); + field_dur_m.set_value(30); + + auto refresh = [this](int32_t) { + text_evt.set(SAME_EVT_CODES[field_event_idx.value()]); + update_msg_preview(); + }; + field_event_idx.on_change = refresh; + field_org.on_change = [this](size_t, int32_t) { update_msg_preview(); }; + field_fips_state.on_change = [this](int32_t) { update_msg_preview(); }; + field_fips_county.on_change = [this](int32_t) { update_msg_preview(); }; + field_dur_h.on_change = [this](int32_t) { update_msg_preview(); }; + field_dur_m.on_change = [this](int32_t) { update_msg_preview(); }; + + update_msg_preview(); + + tx_view.on_start = [this]() { + if (!tx_active_) { + start_tx(); + tx_view.set_transmitting(true); + } + }; + tx_view.on_stop = [this]() { stop_tx(); }; +} + +SameTxView::~SameTxView() { + transmitter_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::same_tx diff --git a/firmware/application/external/same_tx/ui_same_tx.hpp b/firmware/application/external/same_tx/ui_same_tx.hpp new file mode 100644 index 000000000..2331935d7 --- /dev/null +++ b/firmware/application/external/same_tx/ui_same_tx.hpp @@ -0,0 +1,84 @@ +#ifndef __UI_SAME_TX_H__ +#define __UI_SAME_TX_H__ + +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "message.hpp" +#include "modems.hpp" + +namespace ui::external_app::same_tx { + +class SameTxView : public View { + public: + SameTxView(NavigationView& nav); + ~SameTxView(); + SameTxView(const SameTxView&) = delete; + SameTxView& operator=(const SameTxView&) = delete; + void focus() override; + std::string title() const override { return "SAME TX"; } + + private: + NavigationView& nav_; + bool tx_active_{false}; + void start_tx(); + void stop_tx(); + void on_tx_progress(const uint32_t progress, const bool done); + void update_msg_preview(); + + TxRadioState radio_state_{162550000, 1750000, AFSK_TX_SAMPLERATE}; + app_settings::SettingsManager settings_{"tx_same", app_settings::Mode::TX}; + + Labels labels{{ + {{0 * 8, 0 * 16}, "Org:", ui::Theme::getInstance()->fg_light->foreground}, + {{8 * 8, 0 * 16}, "Evt:", ui::Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 1 * 16}, "FIPS:", ui::Theme::getInstance()->fg_light->foreground}, + {{16 * 8, 1 * 16}, "Dur:", ui::Theme::getInstance()->fg_light->foreground}, + }}; + OptionsField field_org{ + {4 * 8, 0 * 16}, + 3, + {{"WXR", 0}, {"EAS", 1}, {"CIV", 2}, {"PEP", 3}}}; + NumberField field_event_idx{{12 * 8, 0 * 16}, 2, {0, 25}, 1, '0', true}; + Text text_evt{{15 * 8, 0 * 16, 3 * 8, 16}, "RWT"}; + NumberField field_fips_state{{5 * 8, 1 * 16}, 2, {0, 99}, 1, '0'}; + NumberField field_fips_county{{7 * 8 + 4, 1 * 16}, 3, {0, 999}, 1, '0'}; + NumberField field_dur_h{{20 * 8, 1 * 16}, 2, {0, 99}, 1, '0'}; + NumberField field_dur_m{{22 * 8, 1 * 16}, 2, {0, 59}, 1, '0'}; + Text text_msg{{0, 2 * 16, 240, 16}, ""}; + Text text_status{{0, 3 * 16, 240, 16}, "Ready"}; + TransmitterView tx_view{(int16_t)UI_POS_Y_BOTTOM(4), 1000, 0}; + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto msg = *reinterpret_cast(p); + this->on_tx_progress(msg.progress, msg.done); + }}; +}; + +} // namespace ui::external_app::same_tx +#endif diff --git a/firmware/application/external/scanner/ui_scanner.cpp b/firmware/application/external/scanner/ui_scanner.cpp index 8d87b6c39..cc116eefe 100644 --- a/firmware/application/external/scanner/ui_scanner.cpp +++ b/firmware/application/external/scanner/ui_scanner.cpp @@ -461,6 +461,7 @@ ScannerView::ScannerView( else v = WFM_MODULATION; field_mode.set_selected_index(v); + return; // Important ! Guru will occur if you don't return when modifying field from within on_change ! } last_mode = v; receiver_model.disable(); diff --git a/firmware/application/external/sd_wipe/ui_sd_wipe.cpp b/firmware/application/external/sd_wipe/ui_sd_wipe.cpp index a482e784a..00bf9a8be 100644 --- a/firmware/application/external/sd_wipe/ui_sd_wipe.cpp +++ b/firmware/application/external/sd_wipe/ui_sd_wipe.cpp @@ -36,8 +36,11 @@ WipeSDView::WipeSDView(NavigationView& nav) } WipeSDView::~WipeSDView() { - if (thread) + if (thread) { chThdTerminate(thread); + chThdWait(thread); + thread = nullptr; + } } void WipeSDView::focus() { diff --git a/firmware/application/external/sdusb/main.cpp b/firmware/application/external/sdusb/main.cpp new file mode 100644 index 000000000..c1eb2fe55 --- /dev/null +++ b/firmware/application/external/sdusb/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_sd_over_usb.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::sdusb { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::sdusb + +extern "C" { + +__attribute__((section(".external_app.app_sdusb.application_information"), used)) application_information_t _application_information_sdusb = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::sdusb::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "SD Over USB", + /*.bitmap_data = */ { + 0xF0, + 0x0F, + 0x10, + 0x08, + 0x50, + 0x0A, + 0x10, + 0x08, + 0x10, + 0x08, + 0x10, + 0x08, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0xF0, + 0x0F, + 0x80, + 0x01, + 0x80, + 0x01, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::UTILITIES, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_usb_sd */ {'P', 'U', 'S', 'B'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/apps/ui_sd_over_usb.cpp b/firmware/application/external/sdusb/ui_sd_over_usb.cpp similarity index 92% rename from firmware/application/apps/ui_sd_over_usb.cpp rename to firmware/application/external/sdusb/ui_sd_over_usb.cpp index 937809151..57a20466d 100644 --- a/firmware/application/apps/ui_sd_over_usb.cpp +++ b/firmware/application/external/sdusb/ui_sd_over_usb.cpp @@ -23,7 +23,7 @@ #include "ui_sd_over_usb.hpp" #include "portapack_shared_memory.hpp" -namespace ui { +namespace ui::external_app::sdusb { SdOverUsbView::SdOverUsbView(NavigationView& nav) : nav_(nav) { @@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav) sdcStop(&SDCD1); portapack::shutdown(true); - m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false); + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); m0_halt(); /* will not return*/ }; @@ -56,4 +56,4 @@ void SdOverUsbView::focus() { button_run.focus(); } -} /* namespace ui */ +} // namespace ui::external_app::sdusb diff --git a/firmware/application/apps/ui_sd_over_usb.hpp b/firmware/application/external/sdusb/ui_sd_over_usb.hpp similarity index 96% rename from firmware/application/apps/ui_sd_over_usb.hpp rename to firmware/application/external/sdusb/ui_sd_over_usb.hpp index 1c39c7e89..0784136ed 100644 --- a/firmware/application/apps/ui_sd_over_usb.hpp +++ b/firmware/application/external/sdusb/ui_sd_over_usb.hpp @@ -32,7 +32,7 @@ #include -namespace ui { +namespace ui::external_app::sdusb { class SdOverUsbView : public View { public: @@ -58,6 +58,6 @@ class SdOverUsbView : public View { "Run"}; }; -} /* namespace ui */ +} // namespace ui::external_app::sdusb #endif /*__UI_SD_OVER_USB_H__*/ diff --git a/firmware/application/external/shoppingcart_lock/main.cpp b/firmware/application/external/shoppingcart_lock/main.cpp index e7f483a49..874a93d25 100644 --- a/firmware/application/external/shoppingcart_lock/main.cpp +++ b/firmware/application/external/shoppingcart_lock/main.cpp @@ -58,7 +58,7 @@ __attribute__((section(".external_app.app_shoppingcart_lock.application_informat 0x7E, 0x00, }, - /*.icon_color = */ ui::Color::red().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::UTILITIES, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/siggen/main.cpp b/firmware/application/external/siggen/main.cpp new file mode 100644 index 000000000..d4c62a968 --- /dev/null +++ b/firmware/application/external/siggen/main.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_siggen.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::siggen { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::siggen +extern "C" { + +__attribute__((section(".external_app.app_siggen.application_information"), used)) application_information_t _application_information_siggen = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::siggen::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Signal gen", + /*.bitmap_data = */ { + 0x18, + 0x00, + 0x24, + 0x00, + 0x42, + 0x00, + 0x42, + 0x00, + 0x42, + 0x00, + 0x42, + 0x00, + 0x81, + 0x00, + 0xAB, + 0x6A, + 0x80, + 0x40, + 0x00, + 0x21, + 0x00, + 0x21, + 0x00, + 0x21, + 0x00, + 0x21, + 0x00, + 0x12, + 0x00, + 0x0C, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_siggen */ {'P', 'S', 'I', 'G'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/apps/ui_siggen.cpp b/firmware/application/external/siggen/ui_siggen.cpp similarity index 97% rename from firmware/application/apps/ui_siggen.cpp rename to firmware/application/external/siggen/ui_siggen.cpp index 032dbd56c..24d3e8931 100644 --- a/firmware/application/apps/ui_siggen.cpp +++ b/firmware/application/external/siggen/ui_siggen.cpp @@ -31,7 +31,7 @@ using namespace portapack; -namespace ui { +namespace ui::external_app::siggen { void SigGenView::focus() { options_shape.focus(); @@ -75,7 +75,7 @@ void SigGenView::on_tx_progress(const uint32_t progress, const bool done) { SigGenView::SigGenView( NavigationView& nav) { - baseband::run_image(portapack::spi_flash::image_tag_siggen); + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); add_children({&labels, &options_mod, @@ -172,4 +172,4 @@ SigGenView::SigGenView( }; } -} /* namespace ui */ \ No newline at end of file +} // namespace ui::external_app::siggen \ No newline at end of file diff --git a/firmware/application/apps/ui_siggen.hpp b/firmware/application/external/siggen/ui_siggen.hpp similarity index 98% rename from firmware/application/apps/ui_siggen.hpp rename to firmware/application/external/siggen/ui_siggen.hpp index ce771fff0..bac6da79a 100644 --- a/firmware/application/apps/ui_siggen.hpp +++ b/firmware/application/external/siggen/ui_siggen.hpp @@ -33,7 +33,7 @@ #include "portapack.hpp" #include "message.hpp" -namespace ui { +namespace ui::external_app::siggen { class SigGenView : public View { public: @@ -139,6 +139,6 @@ class SigGenView : public View { }}; }; -} /* namespace ui */ +} // namespace ui::external_app::siggen #endif /*__SIGGEN_H__*/ diff --git a/firmware/application/external/snake/ui_snake.cpp b/firmware/application/external/snake/ui_snake.cpp index 2b62e9f77..25ba87b75 100644 --- a/firmware/application/external/snake/ui_snake.cpp +++ b/firmware/application/external/snake/ui_snake.cpp @@ -221,7 +221,7 @@ void SnakeView::paint(Painter& painter) { (void)painter; if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_game(); } } diff --git a/firmware/application/external/soundboard/main.cpp b/firmware/application/external/soundboard/main.cpp index f5fd39d20..6478464e3 100644 --- a/firmware/application/external/soundboard/main.cpp +++ b/firmware/application/external/soundboard/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_soundboard.application_information"), 0xF0, 0xFF, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::TX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/spainter/main.cpp b/firmware/application/external/spainter/main.cpp index b12817a64..5e038f52a 100644 --- a/firmware/application/external/spainter/main.cpp +++ b/firmware/application/external/spainter/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_spainter.application_information"), us 0xC0, 0x01, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::TX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/subcarrx/ui_subcar.cpp b/firmware/application/external/subcarrx/ui_subcar.cpp index cfd6bdef0..7b88ec740 100644 --- a/firmware/application/external/subcarrx/ui_subcar.cpp +++ b/firmware/application/external/subcarrx/ui_subcar.cpp @@ -91,6 +91,7 @@ SubCarView::SubCarView(NavigationView& nav) &button_clear_list, &check_log, &labels, + &options_mode, &recent_entries_view}); baseband::run_prepared_image(portapack::memory::map::m4_code.base()); @@ -114,11 +115,17 @@ SubCarView::SubCarView(NavigationView& nav) recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) { nav_.push(entry); }; - baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am - receiver_model.enable(); + + options_mode.on_change = [this](size_t, int32_t v) { + modulation = v; + chThdSleepMilliseconds(100); // wait for the baseband thread to process the previous config, to avoid glitchy output when switching modes + baseband::set_subghzd_config(modulation, receiver_model.sampling_rate()); + }; signal_token_tick_second = rtc_time::signal_tick_second += [this]() { on_tick_second(); }; + options_mode.set_selected_index(modulation, true); + receiver_model.enable(); } void SubCarView::on_tick_second() { @@ -176,6 +183,8 @@ const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) { return "Fiat V0"; case FPC_BMWV0: return "BMW V0"; + /* case FPC_KIAV6: + return "Kia V6";*/ case FPC_Invalid: default: @@ -266,10 +275,11 @@ void SubCarRecentEntryDetailView::parseProtocol() { if (entry_.sensorType == FPC_Invalid) return; if (entry_.sensorType == FPC_SUZUKI) { - uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12); - serial = serial_button >> 4; - uint8_t buttonid = serial_button & 0xF; - cnt = (entry_.data >> 44) & 0xFFFF; + uint32_t data_high = (uint32_t)(entry_.data >> 32); + uint32_t data_low = (uint32_t)entry_.data; + serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); + uint8_t buttonid = (data_low >> 12) & 0xF; + cnt = (data_high << 4) >> 16; btn = to_string_dec_uint(buttonid); return; } @@ -320,15 +330,78 @@ void SubCarRecentEntryDetailView::parseProtocol() { } if (entry_.sensorType == FPC_KIAV5) { - serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1); - uint8_t button = (entry_.data >> 61) & 0x07; + uint64_t yek = 0; + for (int i = 0; i < 8; i++) { + uint8_t byte = (entry_.data2 >> (i * 8)) & 0xFF; + uint8_t reversed = 0; + for (int b = 0; b < 8; b++) { + if (byte & (1 << b)) + reversed |= (1 << (7 - b)); + } + yek |= ((uint64_t)reversed << ((7 - i) * 8)); + } + serial = (uint32_t)((yek >> 32) & 0x0FFFFFFF); + uint8_t button = (uint8_t)((yek >> 60) & 0x0F); + uint32_t encrypted = (uint32_t)(yek & 0xFFFFFFFF); btn = to_string_dec_uint(button); - cnt = (uint16_t)(entry_.data & 0xFFFF); + // decode + uint8_t keystore_bytes[] = {0x53, 0x54, 0x46, 0x52, 0x4b, 0x45, 0x30, 0x30}; + uint8_t s0 = (encrypted & 0xFF); + uint8_t s1 = (encrypted >> 8) & 0xFF; + uint8_t s2 = (encrypted >> 16) & 0xFF; + uint8_t s3 = (encrypted >> 24) & 0xFF; + int round_index = 1; + for (size_t i = 0; i < 18; i++) { + uint8_t r = keystore_bytes[round_index] & 0xFF; + int steps = 8; + while (steps > 0) { + uint8_t base; + if ((s3 & 0x40) == 0) { + base = (s3 & 0x02) == 0 ? 0x74 : 0x2E; + } else { + base = (s3 & 0x02) == 0 ? 0x3A : 0x5C; + } + + if (s2 & 0x08) { + base = (((base >> 4) & 0x0F) | ((base & 0x0F) << 4)) & 0xFF; + } + if (s1 & 0x01) { + base = ((base & 0x3F) << 2) & 0xFF; + } + if (s0 & 0x01) { + base = (base << 1) & 0xFF; + } + + uint8_t temp = (s3 ^ s1) & 0xFF; + s3 = ((s3 & 0x7F) << 1) & 0xFF; + if (s2 & 0x80) { + s3 |= 0x01; + } + s2 = ((s2 & 0x7F) << 1) & 0xFF; + if (s1 & 0x80) { + s2 |= 0x01; + } + s1 = ((s1 & 0x7F) << 1) & 0xFF; + if (s0 & 0x80) { + s1 |= 0x01; + } + s0 = ((s0 & 0x7F) << 1) & 0xFF; + + uint8_t chk = (base ^ (r ^ temp)) & 0xFF; + if (chk & 0x80) { + s0 |= 0x01; + } + r = ((r & 0x7F) << 1) & 0xFF; + steps--; + } + round_index = (round_index - 1) & 0x7; + } + cnt = (s0 + (s1 << 8)) & 0xFFFF; } if (entry_.sensorType == FPC_KIAV3V4) { // not decrypted! - serial = SD_NO_SERIAL; //(uint32_t)entry_.data; + serial = (uint32_t)entry_.data; // uint8_t button = entry_.data2 & 0xFF; btn = "?"; // to_string_dec_uint(button); } @@ -344,7 +417,7 @@ void SubCarRecentEntryDetailView::parseProtocol() { if (entry_.sensorType == FPC_KIAV1) { serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF); uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF); - cnt = (uint8_t)((entry_.data >> 8) & 0xFF); + cnt = (uint8_t)((entry_.data >> 4) & 0xF) << 8 | ((entry_.data >> 8) & 0xFF); btn = to_string_dec_uint(button); } @@ -427,6 +500,13 @@ void SubCarRecentEntryDetailView::parseProtocol() { btn = to_string_dec_uint(button); } + /*if (entry_.sensorType == FPC_KIAV6) { + // not decrypted! + serial = 0; + btn = "?"; + cnt = 0; + }*/ + return; } diff --git a/firmware/application/external/subcarrx/ui_subcar.hpp b/firmware/application/external/subcarrx/ui_subcar.hpp index a309fffa1..5fd9af62e 100644 --- a/firmware/application/external/subcarrx/ui_subcar.hpp +++ b/firmware/application/external/subcarrx/ui_subcar.hpp @@ -116,25 +116,27 @@ class SubCarView : public View { 4'000'000 /* sampling rate */, ReceiverModel::Mode::AMAudio}; bool logging = false; + uint8_t modulation = 0; app_settings::SettingsManager settings_{ "rx_subcar", app_settings::Mode::RX, { {"log"sv, &logging}, + {"modulationmode"sv, &modulation}, }}; SubCarRecentEntries recent{}; RFAmpField field_rf_amp{ - {13 * 8, UI_POS_Y(0)}}; + {UI_POS_X(13), UI_POS_Y(0)}}; LNAGainField field_lna{ - {15 * 8, UI_POS_Y(0)}}; + {UI_POS_X(15), UI_POS_Y(0)}}; VGAGainField field_vga{ - {18 * 8, UI_POS_Y(0)}}; + {UI_POS_X(18), UI_POS_Y(0)}}; RSSI rssi{ - {21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}}; + {UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}}; Channel channel{ - {21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4}, + {UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}, }; RxFrequencyField field_frequency{ {UI_POS_X(0), UI_POS_Y(0)}, @@ -143,18 +145,22 @@ class SubCarView : public View { SignalToken signal_token_tick_second{}; Button button_clear_list{ - {0, 16, 7 * 8, 32}, + {UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_HEIGHT(2)}, "Clear"}; Checkbox check_log{ - {10 * 8, 18}, + {UI_POS_X(8), UI_POS_Y(1)}, 3, "Log", true}; Labels labels{ - {{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(15), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground}, }; + ui::OptionsField options_mode{ + {UI_POS_X(22), UI_POS_Y(1)}, + 3, + {{"AM", 0}, {"FM", 1}}}; static constexpr auto header_height = 3 * 16; @@ -198,16 +204,16 @@ class SubCarRecentEntryDetailView : public View { std::string btn = ""; uint32_t cnt = SD_NO_CNT; - Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"}; - Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"}; + Text text_type{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "?"}; + Text text_id{{UI_POS_X(6), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "?"}; Console console{ - {0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}}; + {UI_POS_X(0), UI_POS_Y(4), UI_POS_MAXWIDTH, screen_height - (4 * 16) - 36}}; Labels labels{ {{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Serial: ", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "Data:", Theme::getInstance()->fg_light->foreground}, }; Button button_done{ diff --git a/firmware/application/external/tetris/ui_tetris.cpp b/firmware/application/external/tetris/ui_tetris.cpp index 3d8886955..1f0119f90 100644 --- a/firmware/application/external/tetris/ui_tetris.cpp +++ b/firmware/application/external/tetris/ui_tetris.cpp @@ -605,7 +605,7 @@ void pause_game() { } int main() { - std::srand(GenerateRandomSeed()); + srand(GenerateRandomSeed()); Init(); ShowLevelMenu(); joystick.attach(&ReadJoystickForLevel, 0.3); diff --git a/firmware/application/external/time_sink/main.cpp b/firmware/application/external/time_sink/main.cpp new file mode 100644 index 000000000..f7f011635 --- /dev/null +++ b/firmware/application/external/time_sink/main.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_time_sink.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::time_sink { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::time_sink +extern "C" { + +__attribute__((section(".external_app.app_time_sink.application_information"), used)) application_information_t _application_information_time_sink = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::time_sink::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Time Sink", + /*.bitmap_data = */ { + 0x18, + 0x00, + 0x24, + 0x00, + 0x42, + 0x00, + 0x42, + 0x00, + 0x42, + 0x00, + 0x42, + 0x00, + 0x81, + 0x00, + 0xAB, + 0x6A, + 0x80, + 0x40, + 0x00, + 0x21, + 0x00, + 0x21, + 0x00, + 0x21, + 0x00, + 0x21, + 0x00, + 0x12, + 0x00, + 0x0C, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'T', 'S', 'K'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/time_sink/ui_time_sink.cpp b/firmware/application/external/time_sink/ui_time_sink.cpp new file mode 100644 index 000000000..c8be8f8cc --- /dev/null +++ b/firmware/application/external/time_sink/ui_time_sink.cpp @@ -0,0 +1,352 @@ +/* + * Copyleft zxkmm (>) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_time_sink.hpp" + +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "ui_spectrum.hpp" + +#include + +using namespace portapack; + +namespace ui::external_app::time_sink { + +TimeSinkWaveformWidget::TimeSinkWaveformWidget( + Rect parent_rect, + const int16_t* data, + size_t length, + Color color) + : Widget{parent_rect}, + data_{data}, + length_{length}, + color_{color} { + reset_cache(); +} + +void TimeSinkWaveformWidget::set_parent_rect(const Rect new_parent_rect) { + Widget::set_parent_rect(new_parent_rect); + reset_cache(); +} + +void TimeSinkWaveformWidget::on_show() { + reset_cache(); + set_dirty(); +} + +void TimeSinkWaveformWidget::reset_cache() { + history_count_ = 0; + history_head_ = 0; + needs_clear_ = true; +} + +void TimeSinkWaveformWidget::set_persistence_frames(uint8_t frames) { + const auto clamped = static_cast(std::clamp( + frames, + 1, + max_persistence_frames)); + if (clamped != persistence_frames_) { + persistence_frames_ = clamped; + reset_cache(); + set_dirty(); + } +} + +Coord TimeSinkWaveformWidget::sample_to_y(const Rect& r, int16_t sample) const { + const int32_t y_center = r.top() + (r.height() / 2); + const int32_t y_span = std::max(1, r.height() - 1); + const int32_t y = y_center - (static_cast(sample) * y_span) / 65536; + return static_cast(std::clamp(y, r.top(), r.bottom() - 1)); +} + +void TimeSinkWaveformWidget::paint(Painter& painter) { + const auto r = screen_rect(); + const auto background = Theme::getInstance()->bg_darkest->background; + + if (!r || !data_ || !length_) { + if (needs_clear_) { + painter.fill_rectangle_unrolled8(r, background); + needs_clear_ = false; + } + history_count_ = 0; + history_head_ = 0; + return; + } + + const size_t columns = std::min({ + length_, + max_columns, + static_cast(r.width()), + }); + if (!columns) { + history_count_ = 0; + history_head_ = 0; + return; + } + + if (needs_clear_) { + painter.fill_rectangle_unrolled8(r, background); + needs_clear_ = false; + history_count_ = 0; + history_head_ = 0; + } + + for (size_t x = 0; x < columns; ++x) { + const size_t src_index = (x * length_) / columns; + current_y_[x] = sample_to_y(r, data_[src_index]); + } + + // Draw first, then erase stale pixels so the trace never disappears mid-refresh. + for (size_t x = 0; x < columns; ++x) { + display.draw_pixel( + {static_cast(r.left() + x), current_y_[x]}, + color_); + } + + if (history_count_ >= persistence_frames_) { + const size_t expired_slot = history_head_; + + for (size_t x = 0; x < columns; ++x) { + const auto expired_y = history_y_[expired_slot][x]; + bool keep = (expired_y == current_y_[x]); + + if (!keep) { + for (size_t i = 1; i < history_count_; ++i) { + const size_t slot = (history_head_ + i) % max_persistence_frames; + if (history_y_[slot][x] == expired_y) { + keep = true; + break; + } + } + } + + if (!keep) { + display.draw_pixel( + {static_cast(r.left() + x), expired_y}, + background); + } + } + + history_head_ = (history_head_ + 1) % max_persistence_frames; + --history_count_; + } + + const size_t tail_slot = (history_head_ + history_count_) % max_persistence_frames; + std::copy_n(current_y_.begin(), columns, history_y_[tail_slot].begin()); + ++history_count_; +} + +TimeSinkView::TimeSinkView(NavigationView& nav) + : nav_(nav) { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({ + &labels, + &field_frequency, + &field_frequency_step, + &field_rf_amp, + &field_lna, + &field_vga, + &options_sample_rate, + &field_trigger, + &options_persistence, + &options_trigger_mode, + &field_trigger_level, + &waveform, + }); + + field_frequency_step.set_by_value(receiver_model.frequency_step()); + field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) { + receiver_model.set_frequency_step(v); + field_frequency.set_step(v); + }; + + options_sample_rate.set_by_nearest_value(sampling_rate); + sampling_rate = options_sample_rate.selected_index_value(); + options_sample_rate.on_change = [this](size_t, OptionsField::value_t v) { + sampling_rate = v; + apply_spectrum_config(); + }; + + field_trigger.set_value(trigger); + field_trigger.on_change = [this](int32_t v) { + trigger = static_cast(v); + apply_spectrum_config(); + }; + + options_persistence.set_by_nearest_value(persistence_frames); + persistence_frames = options_persistence.selected_index_value(); + waveform.set_persistence_frames(persistence_frames); + options_persistence.on_change = [this](size_t, OptionsField::value_t v) { + persistence_frames = static_cast(v); + waveform.set_persistence_frames(persistence_frames); + }; + + options_trigger_mode.set_by_nearest_value(trigger_mode); + trigger_mode = static_cast(options_trigger_mode.selected_index_value()); + options_trigger_mode.on_change = [this](size_t, OptionsField::value_t v) { + trigger_mode = static_cast(v); + trigger_lock_valid = false; + }; + + field_trigger_level.set_value(trigger_level); + field_trigger_level.on_change = [this](int32_t v) { + trigger_level = v; + trigger_lock_valid = false; + }; + + receiver_model.set_squelch_level(0); + receiver_model.enable(); + apply_spectrum_config(); +} + +TimeSinkView::~TimeSinkView() { + receiver_model.disable(); + baseband::shutdown(); +} + +void TimeSinkView::focus() { + field_frequency.focus(); +} + +void TimeSinkView::on_show() { + baseband::spectrum_streaming_start(); +} + +void TimeSinkView::on_hide() { + baseband::spectrum_streaming_stop(); +} + +void TimeSinkView::set_parent_rect(const Rect new_parent_rect) { + View::set_parent_rect(new_parent_rect); + waveform.set_parent_rect({0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height}); +} + +void TimeSinkView::apply_spectrum_config() { + receiver_model.set_sampling_rate(sampling_rate); + receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(sampling_rate)); + + baseband::set_time_sink( + sampling_rate, + trigger); +} + +size_t TimeSinkView::find_stable_trigger_index(const ChannelSpectrum& spectrum) { + if (spectrum.db.size() < 2) { + return 0; + } + + TriggerMode mode = TriggerMode::Rising; + switch (trigger_mode) { + case static_cast(TriggerMode::Off): + mode = TriggerMode::Off; + break; + case static_cast(TriggerMode::Rising): + mode = TriggerMode::Rising; + break; + case static_cast(TriggerMode::Falling): + mode = TriggerMode::Falling; + break; + default: + break; + } + + if (mode == TriggerMode::Off) { + trigger_lock_valid = false; + return 0; + } + + constexpr int32_t hysteresis = 2; + const int32_t threshold = std::clamp( + 128 + trigger_level, + hysteresis, + 255 - hysteresis); + const size_t center = spectrum.db.size() / 2; + const size_t reference_index = trigger_lock_valid ? trigger_lock_index : center; + + bool found = false; + size_t best_index = 0; + size_t best_distance = spectrum.db.size(); + const auto circular_distance = [count = spectrum.db.size()](size_t a, size_t b) -> size_t { + const size_t linear = + (a > b) ? (a - b) : (b - a); + return std::min(linear, count - linear); + }; + + for (size_t i = 1; i < spectrum.db.size(); ++i) { + const int32_t prev = spectrum.db[i - 1]; + const int32_t curr = spectrum.db[i]; + + bool crossing = false; + if (mode == TriggerMode::Rising) { + crossing = + (prev <= (threshold - hysteresis)) && + (curr >= (threshold + hysteresis)); + } else { + crossing = + (prev >= (threshold + hysteresis)) && + (curr <= (threshold - hysteresis)); + } + + if (!crossing) { + continue; + } + + const size_t distance = circular_distance(i, reference_index); + if (!found || (distance < best_distance)) { + found = true; + best_index = i; + best_distance = distance; + } + } + + if (found) { + trigger_lock_index = best_index; + trigger_lock_valid = true; + return best_index; + } + + return trigger_lock_valid ? trigger_lock_index : 0; +} + +void TimeSinkView::on_channel_spectrum(const ChannelSpectrum& spectrum) { + const size_t trigger_index = find_stable_trigger_index(spectrum); + const size_t source_count = spectrum.db.size(); + const size_t window_size = source_count; + + for (size_t x = 0; x < waveform_points; x++) { + const size_t offset = (x * window_size) / waveform_points; + const size_t src_index = + (trigger_index + offset) % source_count; + const int32_t centered = static_cast(spectrum.db[src_index]) - 128; + const int32_t scaled = centered * 256; + waveform_buffer[x] = static_cast(std::clamp(scaled, -32768, 32767)); + } + + waveform.set_dirty(); +} + +void TimeSinkView::on_freqchg(int64_t freq) { + field_frequency.set_value(freq); +} + +} // namespace ui::external_app::time_sink diff --git a/firmware/application/external/time_sink/ui_time_sink.hpp b/firmware/application/external/time_sink/ui_time_sink.hpp new file mode 100644 index 000000000..611c555d5 --- /dev/null +++ b/firmware/application/external/time_sink/ui_time_sink.hpp @@ -0,0 +1,238 @@ +/* + * Copyleft zxkmm (>) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_TIME_SINK_APP_H__ +#define __UI_TIME_SINK_APP_H__ + +#include "app_settings.hpp" +#include "baseband_api.hpp" +#include "message.hpp" +#include "radio_state.hpp" +#include "ui.hpp" +#include "ui_freq_field.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_widget.hpp" + +#include + +namespace ui::external_app::time_sink { + +constexpr size_t time_sink_waveform_points = 240; + +class TimeSinkWaveformWidget : public Widget { + public: + TimeSinkWaveformWidget(Rect parent_rect, const int16_t* data, size_t length, Color color); + TimeSinkWaveformWidget(const TimeSinkWaveformWidget&) = delete; + TimeSinkWaveformWidget(TimeSinkWaveformWidget&&) = delete; + TimeSinkWaveformWidget& operator=(const TimeSinkWaveformWidget&) = delete; + TimeSinkWaveformWidget& operator=(TimeSinkWaveformWidget&&) = delete; + + void set_parent_rect(const Rect new_parent_rect) override; + void on_show() override; + void paint(Painter& painter) override; + void set_persistence_frames(uint8_t frames); + + private: + static constexpr size_t max_columns = time_sink_waveform_points; + static constexpr size_t max_persistence_frames = 16; // this is sad that we cant have 32 histories in ext app due to memory constraints + + void reset_cache(); + Coord sample_to_y(const Rect& r, int16_t sample) const; + + const int16_t* data_; + size_t length_; + Color color_; + std::array current_y_{}; + std::array, max_persistence_frames> history_y_{}; + size_t history_count_{0}; + size_t history_head_{0}; + uint8_t persistence_frames_{1}; + bool needs_clear_{true}; +}; + +class TimeSinkView : public View { + public: + TimeSinkView(NavigationView& nav); + ~TimeSinkView(); + + TimeSinkView(const TimeSinkView&) = delete; + TimeSinkView(TimeSinkView&&) = delete; + TimeSinkView& operator=(const TimeSinkView&) = delete; + TimeSinkView& operator=(TimeSinkView&&) = delete; + + std::string title() const override { return "Time Sink"; }; + void focus() override; + void on_show() override; + void on_hide() override; + void set_parent_rect(const Rect new_parent_rect) override; + + private: + enum class TriggerMode : uint8_t { + Off = 0, + Rising = 1, + Falling = 2, + }; + + static constexpr Dim header_height = 3 * 16; + static constexpr size_t waveform_points = time_sink_waveform_points; + + NavigationView& nav_; + RxRadioState radio_state_{ + 433'920'000, + 1'750'000, + 2'000'000, + ReceiverModel::Mode::SpectrumAnalysis}; + + uint32_t sampling_rate{2'000'000}; + uint8_t trigger{0}; + uint8_t persistence_frames{1}; + uint8_t trigger_mode{static_cast(TriggerMode::Rising)}; + int32_t trigger_level{0}; + size_t trigger_lock_index{0}; + bool trigger_lock_valid{false}; + app_settings::SettingsManager settings_{ + "rx_time_sink"sv, + app_settings::Mode::RX, + { + {"sampling_rate"sv, &sampling_rate}, + {"trigger"sv, &trigger}, + {"persistence_frames"sv, &persistence_frames}, + {"trigger_mode"sv, &trigger_mode}, + {"trigger_level"sv, &trigger_level}, + }}; + + int16_t waveform_buffer[waveform_points]{0}; + ChannelSpectrumFIFO* fifo = nullptr; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(1)}, "SR:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(11), UI_POS_Y(1)}, "DEC:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "PST:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(2)}, "TRM:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(19), UI_POS_Y(2)}, "LVL:", Theme::getInstance()->fg_light->foreground}, + }; + + RxFrequencyField field_frequency{ + {UI_POS_X(0), UI_POS_Y(0)}, + nav_}; + + FrequencyStepView field_frequency_step{ + {10 * 8, UI_POS_Y(0)}}; + + RFAmpField field_rf_amp{ + {16 * 8, UI_POS_Y(0)}}; + + LNAGainField field_lna{ + {18 * 8, UI_POS_Y(0)}}; + + VGAGainField field_vga{ + {21 * 8, UI_POS_Y(0)}}; + + OptionsField options_sample_rate{ + {UI_POS_X(3), UI_POS_Y(1)}, + 6, + { + {"1.0M ", 1'000'000}, + {"2.0M ", 2'000'000}, + {"5.0M ", 5'000'000}, + {"10.0M ", 10'000'000}, + {"20.0M ", 20'000'000}, + }}; + + NumberField field_trigger{ + {UI_POS_X(17), UI_POS_Y(1)}, + 3, + {0, 128}, + 1, + ' '}; + + OptionsField options_persistence{ + {UI_POS_X(3), UI_POS_Y(2)}, + 3, + { + {"1 ", 1}, + {"2 ", 2}, + {"4 ", 4}, + {"8 ", 8}, + {"16 ", 16}, + }}; + + OptionsField options_trigger_mode{ + {UI_POS_X(13), UI_POS_Y(2)}, + 4, + { + {"Off ", static_cast(TriggerMode::Off)}, + {"Rise", static_cast(TriggerMode::Rising)}, + {"Fall", static_cast(TriggerMode::Falling)}, + }}; + + NumberField field_trigger_level{ + {UI_POS_X(23), UI_POS_Y(2)}, + 4, + {-127, 127}, + 1, + ' '}; + + TimeSinkWaveformWidget waveform{ + {0, header_height, screen_width, screen_height - header_height}, + waveform_buffer, + waveform_points, + Theme::getInstance()->fg_light->foreground}; + + MessageHandlerRegistration message_handler_spectrum_config{ + Message::ID::ChannelSpectrumConfig, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->fifo = message.fifo; + }}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + if (this->fifo) { + ChannelSpectrum spectrum{}; + bool has_spectrum = false; + while (fifo->out(spectrum)) { + has_spectrum = true; + } + if (has_spectrum) { + this->on_channel_spectrum(spectrum); + } + } + }}; + + MessageHandlerRegistration message_handler_freqchg{ + Message::ID::FreqChangeCommand, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freqchg(message->freq); + }}; + + void apply_spectrum_config(); + size_t find_stable_trigger_index(const ChannelSpectrum& spectrum); + void on_channel_spectrum(const ChannelSpectrum& spectrum); + void on_freqchg(int64_t freq); +}; + +} // namespace ui::external_app::time_sink + +#endif // __UI_TIME_SINK_APP_H__ diff --git a/firmware/application/external/tpmsrx/main.cpp b/firmware/application/external/tpmsrx/main.cpp index 552a14c06..6e429b3dc 100644 --- a/firmware/application/external/tpmsrx/main.cpp +++ b/firmware/application/external/tpmsrx/main.cpp @@ -75,7 +75,7 @@ __attribute__((section(".external_app.app_tpmsrx.application_information"), used }, /*.icon_color = */ ui::Color::green().v, /*.menu_location = */ app_location_t::RX, - /*.desired_menu_position = */ -1, + /*.desired_menu_position = */ 7, /*.m4_app_tag = portapack::spi_flash::image_tag_tpms */ {'P', 'T', 'P', 'M'}, /*.m4_app_offset = */ 0x00000000, // will be filled at compile time diff --git a/firmware/application/external/tpmsrx/tpms_app.cpp b/firmware/application/external/tpmsrx/tpms_app.cpp index ef31ca8fa..c75ba385c 100644 --- a/firmware/application/external/tpmsrx/tpms_app.cpp +++ b/firmware/application/external/tpmsrx/tpms_app.cpp @@ -42,16 +42,37 @@ std::string type(tpms::Reading::Type type) { return to_string_dec_uint(toUType(type), 2); } +std::string type_name(tpms::Reading::Type type) { + switch (type) { + case tpms::Reading::Type::None: + return "None"; + case tpms::Reading::Type::FLM_64: + return "FLM_64"; + case tpms::Reading::Type::FLM_72: + return "FLM_72"; + case tpms::Reading::Type::FLM_80: + return "FLM_80"; + case tpms::Reading::Type::Schrader: + return "Schrader"; + case tpms::Reading::Type::GMC_96: + return "GMC_96"; + default: + return "Unknown"; + } +} + std::string id(tpms::TransponderID id) { return to_string_hex(id.value(), 8); } std::string pressure(Pressure pressure) { - return to_string_dec_int(units_psi ? pressure.psi() : pressure.kilopascal(), 3); + return to_string_dec_int(pressure_unit == PRESSURE_UNIT_PSI ? pressure.psi() : pressure_unit == PRESSURE_UNIT_BAR ? pressure.bar() + : pressure.kilopascal(), + 3); } std::string temperature(Temperature temperature) { - return to_string_dec_int(units_fahr ? temperature.fahrenheit() : temperature.celsius(), 3); + return to_string_dec_int(temp_unit == TEMP_UNIT_CELSIUS ? temperature.celsius() : temperature.fahrenheit(), 3); } std::string flags(tpms::Flags flags) { @@ -99,7 +120,8 @@ void TPMSRecentEntry::update(const tpms::Reading& reading) { } } -TPMSAppView::TPMSAppView(NavigationView&) { +TPMSAppView::TPMSAppView(NavigationView& nav) + : nav_{nav} { // baseband::run_image(portapack::spi_flash::image_tag_tpms); baseband::run_prepared_image(portapack::memory::map::m4_code.base()); @@ -122,16 +144,21 @@ TPMSAppView::TPMSAppView(NavigationView&) { options_band.set_by_value(receiver_model.target_frequency()); options_pressure.on_change = [this](size_t, int32_t i) { - format::units_psi = (bool)i; + format::pressure_unit = (uint8_t)i; update_view(); }; - options_pressure.set_selected_index(format::units_psi, true); + options_pressure.set_by_value(format::pressure_unit); options_temperature.on_change = [this](size_t, int32_t i) { - format::units_fahr = (bool)i; + format::temp_unit = (uint8_t)i; update_view(); }; - options_temperature.set_selected_index(format::units_fahr, true); + options_temperature.set_by_value(format::temp_unit); + + // Add on_select handler for entries + recent_entries_view.on_select = [this](const TPMSRecentEntry& entry) { + on_show_detail(entry); + }; logger = std::make_unique(); if (logger) { @@ -176,6 +203,7 @@ void TPMSAppView::on_packet(const tpms::Packet& packet) { const auto reading = reading_opt.value(); auto& entry = ::on_packet(recent, TPMSRecentEntry::Key{reading.type(), reading.id()}); entry.update(reading); + entry.signal_type = packet.signal_type(); recent_entries_view.set_dirty(); } @@ -189,6 +217,116 @@ void TPMSAppView::on_show_list() { recent_entries_view.focus(); } +void TPMSAppView::on_show_detail(const TPMSRecentEntry& entry) { + nav_.push(entry); +} + +// Detail view implementation +TPMSRecentEntryDetailView::TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry) + : nav_{nav}, + entry_{entry} { + add_children({&labels, + &text_type, + &text_id, + &text_pressure, + &text_temperature, + &text_flags, + &text_count, + &button_done, + &button_save}); + + // Display entry data + text_type.set(format::type_name(entry.type)); + text_id.set(to_string_hex(entry.id.value(), 8)); + + if (entry.last_pressure.is_valid()) { + std::string pressure_str = format::pressure(entry.last_pressure.value()); + std::string unit_str = format::pressure_unit == PRESSURE_UNIT_PSI ? " PSI" : format::pressure_unit == PRESSURE_UNIT_BAR ? " BAR" + : " kPa"; + text_pressure.set(pressure_str + unit_str); + } else { + text_pressure.set("---"); + } + + if (entry.last_temperature.is_valid()) { + text_temperature.set(to_string_dec_int(entry.last_temperature.value().celsius(), 3) + " C"); + } else { + text_temperature.set("---"); + } + + if (entry.last_flags.is_valid()) { + text_flags.set(to_string_hex(entry.last_flags.value(), 2)); + } else { + text_flags.set("--"); + } + + text_count.set(to_string_dec_uint(entry.received_count, 4)); + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + button_save.on_select = [this](Button&) { + on_save(); + }; +} + +void TPMSRecentEntryDetailView::focus() { + button_done.focus(); +} + +void TPMSRecentEntryDetailView::set_entry(const TPMSRecentEntry& entry) { + entry_ = entry; +} + +void TPMSRecentEntryDetailView::on_save() { + auto timestamp = to_string_timestamp(rtc_time::now()); + std::string file_name = "TPMS_" + timestamp + ".TXT"; + ensure_directory(tpms_dir); + auto file_path = tpms_dir / file_name; + + if (save_file(file_path)) { + nav_.display_modal("Saved", "Packet saved to:\n" + file_name); + } else { + nav_.display_modal("Error", "Failed to save\npacket"); + } +} + +bool TPMSRecentEntryDetailView::save_file(const std::filesystem::path& path) { + File f; + auto error = f.create(path); + if (error.is_valid()) + return false; + + // Save in format compatible with TX app + std::string content = "Type=" + to_string_dec_uint(toUType(entry_.type), 1) + "\n"; + content += "ID=" + to_string_hex(entry_.id.value(), 8) + "\n"; + + if (entry_.last_pressure.is_valid()) { + content += "Pressure=" + to_string_dec_int(entry_.last_pressure.value().kilopascal(), 1) + "\n"; + } else { + content += "Pressure=240\n"; // Default value + } + + if (entry_.last_temperature.is_valid()) { + content += "Temperature=" + to_string_dec_int(entry_.last_temperature.value().celsius(), 1) + "\n"; + } else { + content += "Temperature=25\n"; // Default value + } + + if (entry_.last_flags.is_valid()) { + content += "Flags=" + to_string_hex(entry_.last_flags.value(), 2) + "\n"; + } else { + content += "Flags=00\n"; + } + + // Save signal type for proper retransmission + content += "SignalType=" + to_string_dec_uint(toUType(entry_.signal_type), 1) + "\n"; + + f.write(content.c_str(), content.length()); + return true; +} + } // namespace ui::external_app::tpmsrx namespace ui { @@ -239,4 +377,4 @@ void RecentEntriesTable::draw( painter.draw_string(target_rect.location(), style, line); } -} // namespace ui \ No newline at end of file +} // namespace ui diff --git a/firmware/application/external/tpmsrx/tpms_app.hpp b/firmware/application/external/tpmsrx/tpms_app.hpp index 432aa8123..3846b55ac 100644 --- a/firmware/application/external/tpmsrx/tpms_app.hpp +++ b/firmware/application/external/tpmsrx/tpms_app.hpp @@ -42,10 +42,9 @@ namespace ui::external_app::tpmsrx { namespace format { -static bool units_psi{false}; -static bool units_fahr{false}; - -} /* namespace format */ +static uint8_t pressure_unit{PRESSURE_UNIT_KPA}; +static uint8_t temp_unit{TEMP_UNIT_CELSIUS}; +} // namespace format struct TPMSRecentEntry { using Key = std::pair; @@ -54,6 +53,7 @@ struct TPMSRecentEntry { tpms::Reading::Type type{invalid_key.first}; tpms::TransponderID id{invalid_key.second}; + tpms::SignalType signal_type{tpms::SignalType::OOK_8k192_Schrader}; size_t received_count{0}; @@ -90,6 +90,64 @@ class TPMSLogger { using TPMSRecentEntriesView = RecentEntriesView; +class TPMSRecentEntryDetailView : public View { + public: + TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry); + + void set_entry(const TPMSRecentEntry& entry); + const TPMSRecentEntry& entry() const { return entry_; } + + void focus() override; + + private: + NavigationView& nav_; + TPMSRecentEntry entry_; + + void on_save(); + bool save_file(const std::filesystem::path& path); + + Labels labels{ + {{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 5 * 16}, "Pressure:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 7 * 16}, "Temperature:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 9 * 16}, "Flags:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 11 * 16}, "Count:", Theme::getInstance()->fg_light->foreground}, + }; + + Text text_type{ + {8 * 8, 1 * 16, 20 * 8, 16}, + ""}; + + Text text_id{ + {8 * 8, 3 * 16, 20 * 8, 16}, + ""}; + + Text text_pressure{ + {12 * 8, 5 * 16, 16 * 8, 16}, + ""}; + + Text text_temperature{ + {14 * 8, 7 * 16, 14 * 8, 16}, + ""}; + + Text text_flags{ + {8 * 8, 9 * 16, 20 * 8, 16}, + ""}; + + Text text_count{ + {8 * 8, 11 * 16, 20 * 8, 16}, + ""}; + + Button button_save{ + {0 * 8, 13 * 16, 14 * 8, 32}, + "Save"}; + + Button button_done{ + {16 * 8, 13 * 16, 14 * 8, 32}, + "Done"}; +}; + class TPMSAppView : public View { public: TPMSAppView(NavigationView& nav); @@ -106,6 +164,8 @@ class TPMSAppView : public View { std::string title() const override { return "TPMS RX"; }; private: + NavigationView& nav_; + RxRadioState radio_state_{ 314900000 /* frequency*/ , @@ -117,8 +177,8 @@ class TPMSAppView : public View { "rx_tpms", app_settings::Mode::RX, { - {"units_psi"sv, &format::units_psi}, - {"units_fahr"sv, &format::units_fahr}, + {"pressure_unit"sv, &format::pressure_unit}, + {"temp_unit"sv, &format::temp_unit}, }}; MessageHandlerRegistration message_handler_packet{ @@ -154,15 +214,16 @@ class TPMSAppView : public View { OptionsField options_pressure{ {6 * 8, UI_POS_Y(0)}, - 3, - {{"kPa", 0}, - {"PSI", 1}}}; + 4, + {{"kPa", PRESSURE_UNIT_KPA}, + {"PSI", PRESSURE_UNIT_PSI}, + {"BAR", PRESSURE_UNIT_BAR}}}; OptionsField options_temperature{ {10 * 8, UI_POS_Y(0)}, 2, - {{STR_DEGREES_C, 0}, - {STR_DEGREES_F, 1}}}; + {{STR_DEGREES_C, TEMP_UNIT_CELSIUS}, + {STR_DEGREES_F, TEMP_UNIT_FAHRENHEIT}}}; RFAmpField field_rf_amp{ {13 * 8, UI_POS_Y(0)}}; @@ -188,6 +249,7 @@ class TPMSAppView : public View { void on_packet(const tpms::Packet& packet); void on_show_list(); + void on_show_detail(const TPMSRecentEntry& entry); void update_view(); }; diff --git a/firmware/application/external/tpmstx/main.cpp b/firmware/application/external/tpmstx/main.cpp new file mode 100644 index 000000000..1f323bff2 --- /dev/null +++ b/firmware/application/external/tpmstx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "tpms_tx_app.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::tpmstx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::tpmstx + +extern "C" { + +__attribute__((section(".external_app.app_tpmstx.application_information"), used)) application_information_t _application_information_tpmstx = { + /*.memory_location = */ (uint8_t*)0x00000000, // will be filled at compile time + /*.externalAppEntry = */ ui::external_app::tpmstx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "TPMS TX", + /*.bitmap_data = */ { + 0xC0, + 0x03, + 0xF0, + 0x0F, + 0x18, + 0x18, + 0xEC, + 0x37, + 0x36, + 0x6D, + 0x3A, + 0x59, + 0x4B, + 0xD5, + 0x8B, + 0xD3, + 0xCB, + 0xD1, + 0xAB, + 0xD2, + 0x9A, + 0x5C, + 0xB6, + 0x6C, + 0xEC, + 0x37, + 0x18, + 0x18, + 0xF0, + 0x0F, + 0xC0, + 0x03, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ 6, + + /*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/tpmstx/tpms_tx_app.cpp b/firmware/application/external/tpmstx/tpms_tx_app.cpp new file mode 100644 index 000000000..5315dc73a --- /dev/null +++ b/firmware/application/external/tpmstx/tpms_tx_app.cpp @@ -0,0 +1,812 @@ +/* + * Copyright (C) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "tpms_tx_app.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "spi_image.hpp" +#include "ui_fileman.hpp" +#include "ui_freqman.hpp" +#include "manchester.hpp" +#include "rtc_time.hpp" +#include "file_path.hpp" +#include "encoders.hpp" +#include "crc.hpp" + +using namespace portapack; +using namespace tpms; + +namespace ui::external_app::tpmstx { + +void TPMSTXView::focus() { + options_packet_type.focus(); +} + +void TPMSTXView::update_signal_type_from_packet() { + // Auto-select signal type based on packet type + switch (packet_type_) { + case tpms::Reading::Type::Schrader: + signal_type_ = tpms::SignalType::OOK_8k192_Schrader; + break; + case tpms::Reading::Type::FLM_64: + case tpms::Reading::Type::FLM_72: + case tpms::Reading::Type::FLM_80: + signal_type_ = tpms::SignalType::FSK_19k2_Schrader; + break; + case tpms::Reading::Type::GMC_96: + signal_type_ = tpms::SignalType::OOK_8k4_Schrader; + break; + default: + signal_type_ = tpms::SignalType::OOK_8k192_Schrader; + break; + } + // Note: Don't call switch_baseband() here - it's called when needed +} + +void TPMSTXView::switch_baseband() { + // Switch baseband image based on signal type + // Must shutdown first to avoid BBRunning error + baseband::shutdown(); + chThdSleepMilliseconds(100); + + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + baseband::run_image(portapack::spi_flash::image_tag_fsktx); + } else { + baseband::run_image(portapack::spi_flash::image_tag_ook); + } + + chThdSleepMilliseconds(100); +} + +void TPMSTXView::update_packet_display() { + // Only update status text - field values are already set by user interaction + // or by explicit set_value calls when loading from file + std::string status = "ID:" + to_string_hex(transponder_id_, 8); + + // Note protocol-specific limitations + if (packet_type_ == tpms::Reading::Type::Schrader) { + status = "ID:" + to_string_hex(transponder_id_ & 0x00FFFFFF, 6) + " (24bit)"; + } + + status += " " + to_string_dec_uint(pressure_kpa_) + "kPa"; + + // Check for pressure overflow and warn user + if (packet_type_ == tpms::Reading::Type::GMC_96) { + if (pressure_kpa_ > 701) { + status += " !MAX"; // GMC_96 max is 701 kPa (101.7 PSI) + } + } else { + if (pressure_kpa_ > 340) { + status += " !MAX"; // Schrader/FLM max is 340 kPa (49.3 PSI) + } + } + + // Temperature note for protocols that support it + if (packet_type_ == tpms::Reading::Type::GMC_96 || + packet_type_ == tpms::Reading::Type::FLM_64 || + packet_type_ == tpms::Reading::Type::FLM_72 || + packet_type_ == tpms::Reading::Type::FLM_80) { + status += " " + to_string_dec_int(temperature_c_) + "C"; + } else { + status += " (no temp)"; + } + + text_status.set(status); +} + +void TPMSTXView::update_field_visibility() { + // Schrader: Show Func, Hide Temp (no temperature support) + // GMC_96, FLM_xx: Hide Func, Show Temp (temperature supported, no func field) + if (packet_type_ == tpms::Reading::Type::Schrader) { + // Show Func field and label + label_flags.hidden(false); + field_flags.hidden(false); + // Hide Temp field, label, and unit selector + label_temperature.hidden(true); + field_temperature.hidden(true); + options_temperature.hidden(true); + // Show 24-bit ID field, hide 32-bit ID field + field_transponder_id_24.hidden(false); + field_transponder_id_32.hidden(true); + } else { + // Hide Func field and label + label_flags.hidden(true); + field_flags.hidden(true); + // Show Temp field, label, and unit selector + label_temperature.hidden(false); + field_temperature.hidden(false); + options_temperature.hidden(false); + // Show 32-bit ID field, hide 24-bit ID field + field_transponder_id_24.hidden(true); + field_transponder_id_32.hidden(false); + } + // Force screen refresh to clear hidden widgets + set_dirty(); +} + +void TPMSTXView::on_pressure_unit_change() { + // Convert displayed pressure to new unit and update field + units::Pressure pressure(pressure_kpa_); + int display_value = 0; + + if (format::pressure_unit == PRESSURE_UNIT_PSI) { + display_value = pressure.psi(); + } else if (format::pressure_unit == PRESSURE_UNIT_BAR) { + display_value = pressure.bar(); + } else { + display_value = pressure.kilopascal(); + } + + field_pressure.set_value(display_value); +} + +void TPMSTXView::on_temperature_unit_change() { + // Convert displayed temperature to new unit and update field + units::Temperature temperature(temperature_c_); + int display_value = 0; + + if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) { + display_value = temperature.fahrenheit(); + } else { + display_value = temperature.celsius(); + } + + field_temperature.set_value(display_value); +} + +void TPMSTXView::encode_and_transmit() { + if (!is_transmitting_) return; + + // Build TPMS packet based on signal type + std::string binary_string; + uint32_t symbol_rate; + uint32_t sample_rate; + + // Set sample rate based on signal type to match transmitter config + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + sample_rate = 2280000; // 2.28 MHz for FSK (matches transmitter_model config) + } else { + sample_rate = 2000000; // 2 MHz for OOK + } + + if (signal_type_ == tpms::SignalType::OOK_8k192_Schrader) { + // Schrader OOK 8k192 format (Type = Schrader) + // Preamble: 11*2, 01*14, 11, 10 + // Data: 3 bits function code, 24 bits ID, 8 bits pressure, 2 bits checksum + // Total: 37 Manchester symbols (74 bits after encoding) + // NOTE: This protocol does NOT support temperature transmission + // NOTE: Only lower 24 bits of ID are transmitted + // NOTE: Function code (flags_) is stored as 3-bit value (0-7) + // RX will display it combined with checksum in upper nibble + + symbol_rate = 8192; + + // Preamble as expected by RX decoder + binary_string = "1111"; // 11*2 + for (int i = 0; i < 14; i++) { + binary_string += "01"; + } + binary_string += "1110"; // 11, 10 + + // Build data bits (pre-Manchester) + uint64_t data = 0; + + // 3-bit flags (0-7, stored directly) + uint8_t flags_3bit = flags_ & 0x07; + data |= ((uint64_t)flags_3bit << 34); + + // 24-bit ID (only use lower 24 bits - protocol limitation) + uint32_t id_24bit = transponder_id_ & 0x00FFFFFF; + data |= ((uint64_t)id_24bit << 10); + + // 8-bit pressure (convert kPa to raw: kPa * 3/4) + // Clamp to prevent overflow: max raw = 255, max kPa = 255 * 4/3 = 340 (49.3 PSI) + uint16_t pressure_clamped = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + uint8_t pressure_raw = (pressure_clamped * 3 / 4); + data |= ((uint64_t)pressure_raw << 2); + + // Calculate 2-bit checksum: sum all 2-bit pairs, result & 3 must equal 3 + uint32_t checksum_calc = (data >> 36) & 1; // First bit + for (size_t i = 1; i < 37; i += 2) { + checksum_calc += (data >> (37 - i - 2)) & 3; + } + uint8_t checksum_2bit = (3 - (checksum_calc & 3)) & 3; + data |= checksum_2bit; + + // Manchester encode the 37 data bits + for (int i = 36; i >= 0; i--) { + if ((data >> i) & 1) { + binary_string += "10"; // '1' = 10 in Manchester + } else { + binary_string += "01"; // '0' = 01 in Manchester + } + } + + } else if (signal_type_ == tpms::SignalType::OOK_8k4_Schrader) { + // GMC_96 OOK 8k4 format + symbol_rate = 8400; + + // Preamble: 01*40 + for (int i = 0; i < 40; i++) { + binary_string += "01"; + } + + // First nibble of System ID (0x4) - part of preamble pattern match + // RX looks for pattern ending with: 01010101...01100101 + // The "01100101" = Manchester for 0x4, and is consumed by pattern matcher + binary_string += "01"; // 0 + binary_string += "10"; // 1 + binary_string += "01"; // 0 + binary_string += "01"; // 0 (0100 = 0x4) + + // Remaining 20 bits of system ID (padding with zeros) + // These are the first 20 bits of the 76-bit payload + for (int i = 0; i < 20; i++) { + binary_string += "01"; // All zeros for padding + } + + // 32-bit ID (Manchester encoded) + for (int i = 31; i >= 0; i--) { + if ((transponder_id_ >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + // Pressure: kPa * 4/11 + // Clamp to prevent overflow: max raw = 255, max kPa = 255 * 11/4 = 701.25 (101.7 PSI) + uint16_t pressure_clamped = (pressure_kpa_ > 701) ? 701 : pressure_kpa_; + uint8_t pressure_gmc = (pressure_clamped * 4 / 11); + for (int i = 7; i >= 0; i--) { + if ((pressure_gmc >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + // Temperature: temp + 61 + uint8_t temp_gmc = (temperature_c_ + 61) & 0xFF; + for (int i = 7; i >= 0; i--) { + if ((temp_gmc >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + // Checksum: sum of all data bytes (system ID + ID + pressure + temp) + // System ID byte 0: (0x4 << 4) | 0x0 = 0x40 + // System ID byte 1: 0x00 + // System ID byte 2: 0x00 + uint8_t checksum = 0x40; // First byte of system ID + checksum += 0x00; // Second byte of system ID + checksum += 0x00; // Third byte of system ID + + // Add all 4 bytes of the ID + checksum += (transponder_id_ >> 24) & 0xFF; + checksum += (transponder_id_ >> 16) & 0xFF; + checksum += (transponder_id_ >> 8) & 0xFF; + checksum += transponder_id_ & 0xFF; + + // Add pressure and temperature + checksum += pressure_gmc; + checksum += temp_gmc; + + for (int i = 7; i >= 0; i--) { + if ((checksum >> i) & 1) { + binary_string += "10"; + } else { + binary_string += "01"; + } + } + + } else if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + // FSK 19.2k for FLM variants + // Data is Manchester encoded: each data bit becomes two FSK symbols + // Preamble: 14 x '01' + '10' = 30 bits (matches RX pattern exactly) + // Payload: 160 Manchester-encoded bits (80 data bits max) + + symbol_rate = 19200; + + // Build preamble: 14 pairs of "01" followed by "10" + for (int i = 0; i < 14; i++) { + binary_string += "01"; + } + binary_string += "10"; + + std::array data_bytes = {0}; // 160 bits = 20 bytes max + size_t num_data_bits = 0; + + if (packet_type_ == tpms::Reading::Type::FLM_64) { + // FLM_64: 64 bits (8 bytes) + // Bytes 0-3: ID (32 bits) + // Byte 4: Pressure (8 bits) + // Byte 5: Temperature (8 bits, LSB 7 bits used) + // Byte 6: Padding + // Byte 7: Sum checksum of bytes 0-6 (low 8 bits) + + num_data_bits = 64; + + data_bytes[0] = (transponder_id_ >> 24) & 0xFF; + data_bytes[1] = (transponder_id_ >> 16) & 0xFF; + data_bytes[2] = (transponder_id_ >> 8) & 0xFF; + data_bytes[3] = transponder_id_ & 0xFF; + // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) + uint16_t pressure_clamped_flm64 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + data_bytes[4] = (pressure_clamped_flm64 * 3 / 4); + data_bytes[5] = ((temperature_c_ + 56) & 0x7F); // LSB 7 bits only + data_bytes[6] = 0x00; // Padding + + // Addition checksum over bytes 0-6 + uint32_t checksum = 0; + for (int i = 0; i < 7; i++) { + checksum += data_bytes[i]; + } + data_bytes[7] = checksum & 0xFF; + + } else if (packet_type_ == tpms::Reading::Type::FLM_72) { + // FLM_72: 72 bits (9 bytes) + // Bytes 0-3: ID (32 bits) + // Byte 4: Padding + // Byte 5: Pressure (8 bits) + // Byte 6: Temperature (8 bits) + // Bytes 7-8: CRC field - RX processes ALL 9 bytes and expects CRC result = 0 + + num_data_bits = 72; + + data_bytes[0] = (transponder_id_ >> 24) & 0xFF; + data_bytes[1] = (transponder_id_ >> 16) & 0xFF; + data_bytes[2] = (transponder_id_ >> 8) & 0xFF; + data_bytes[3] = transponder_id_ & 0xFF; + data_bytes[4] = 0x00; // Padding + // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) + uint16_t pressure_clamped_flm = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + data_bytes[5] = (pressure_clamped_flm * 3 / 4); + data_bytes[6] = (temperature_c_ + 56) & 0xFF; + data_bytes[7] = 0x00; // Set to 0 initially + + // Calculate CRC over bytes 0-7, place result in byte 8 + // When RX calculates CRC over bytes 0-8, it should get residual 0 + CRC<8> crc{0x01, 0x00}; + for (int i = 0; i < 8; i++) { + crc.process_byte(data_bytes[i]); + } + data_bytes[8] = crc.checksum() & 0xFF; + + } else if (packet_type_ == tpms::Reading::Type::FLM_80) { + // FLM_80: 80 bits (10 bytes) + // Byte 0: Padding + // Bytes 1-4: ID (32 bits) + // Byte 5: Padding + // Byte 6: Pressure (8 bits) + // Byte 7: Temperature (8 bits) + // Bytes 8-9: CRC field - RX processes bytes 1-9 and expects CRC result = 0 + + num_data_bits = 80; + + data_bytes[0] = 0x00; // Padding (not included in CRC) + data_bytes[1] = (transponder_id_ >> 24) & 0xFF; + data_bytes[2] = (transponder_id_ >> 16) & 0xFF; + data_bytes[3] = (transponder_id_ >> 8) & 0xFF; + data_bytes[4] = transponder_id_ & 0xFF; + data_bytes[5] = 0x00; // Padding + // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) + uint16_t pressure_clamped_flm80 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; + data_bytes[6] = (pressure_clamped_flm80 * 3 / 4); + data_bytes[7] = (temperature_c_ + 56) & 0xFF; + data_bytes[8] = 0x00; // Padding/Reserved + data_bytes[9] = 0x00; // CRC byte + + // Calculate CRC over bytes 1-8 (all bytes except 0 and 9) + // When RX calculates CRC over bytes 1-9, it should get residual 0 + CRC<8> crc{0x01, 0x00}; + for (int i = 1; i <= 8; i++) { + crc.process_byte(data_bytes[i]); + } + data_bytes[9] = crc.checksum() & 0xFF; + } + + // Manchester-encode data bits for FSK + // RX ManchesterDecoder with sense=0: '1' = "10", '0' = "01" + size_t num_bytes = num_data_bits / 8; + for (size_t byte_idx = 0; byte_idx < num_bytes; byte_idx++) { + uint8_t byte_val = data_bytes[byte_idx]; + for (int bit_idx = 7; bit_idx >= 0; bit_idx--) { + if ((byte_val >> bit_idx) & 1) { + binary_string += "10"; // Manchester '1' + } else { + binary_string += "01"; // Manchester '0' + } + } + } + + // Pad payload to 160 bits with valid Manchester pairs ("01" = 0) + // Using proper Manchester encoding maintains clock recovery sync + while (binary_string.length() < 190) { // 30 preamble + 160 payload + binary_string += "01"; // Manchester-encoded zero + } + + } else { + text_status.set("Unknown signal type"); + stop_tx(); + return; + } + + // Convert binary string to bitstream + size_t bitstream_length = encoders::make_bitstream(binary_string); + + // Calculate samples per bit (round to nearest for best timing accuracy) + uint32_t samples_per_bit = (sample_rate + symbol_rate / 2) / symbol_rate; + + // Update status to show we're sending + text_status.set("TX: " + to_string_dec_uint(binary_string.length()) + " bits"); + + // Send via appropriate baseband (OOK or FSK) + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + // FSK mode: 19.2 kbaud, 38.4 kHz shift + baseband::set_fsk_data( + bitstream_length, + samples_per_bit, + 38400, // 38.4 kHz shift + 256); // Progress notice interval + } else { + // OOK mode + baseband::set_ook_data( + bitstream_length, + samples_per_bit, + repeat_count_, + pause_duration_); + } +} + +void TPMSTXView::handle_tx_complete() { + // For FSK (FLM packets), handle repeats at application level + // OOK repeats are handled by the baseband processor + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + fsk_repeat_counter_++; + + if (fsk_repeat_counter_ < repeat_count_) { + // More repeats needed - send the next one + // Update progress bar + progressbar.set_value(fsk_repeat_counter_); + + // Wait for FSK processor to fully complete and reset + // This allows the shared memory bitstream to be safely rewritten + chThdSleepMilliseconds(50); + + encode_and_transmit(); + } else { + // All FSK repeats complete + stop_tx(); + } + } else { + // OOK repeats are handled by baseband, just stop here + stop_tx(); + } +} + +void TPMSTXView::start_tx() { + if (is_transmitting_) + return; + + is_transmitting_ = true; + + progressbar.set_max(repeat_count_); + progressbar.set_value(0); + + button_transmit.set_text("STOP TX"); + text_status.set("Transmitting..."); + + // Initialize FSK repeat counter for application-level repeat handling + fsk_repeat_counter_ = 0; + + // Switch to appropriate baseband before transmission + switch_baseband(); + + // Configure transmitter based on modulation type + if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { + // FSK configuration + transmitter_model.set_sampling_rate(2280000); // 2.28 MHz for FSK + transmitter_model.set_baseband_bandwidth(1750000); + } else { + // OOK configuration + transmitter_model.set_sampling_rate(2000000); // 2 MHz for OOK + transmitter_model.set_baseband_bandwidth(1750000); + } + + transmitter_model.enable(); + + // Start transmission + encode_and_transmit(); +} + +void TPMSTXView::stop_tx() { + if (!is_transmitting_) + return; + + is_transmitting_ = false; + transmitter_model.disable(); + + button_transmit.set_text("START TX"); + text_status.set("Transmission complete"); + progressbar.set_value(0); +} + +TPMSTXView::TPMSTXView(NavigationView& nav) + : nav_{nav} { + // Start with OOK baseband (will switch if needed) + baseband::run_image(portapack::spi_flash::image_tag_ook); + + add_children({&labels, + &label_temperature, + &label_flags, + &options_frequency, + &options_packet_type, + &field_transponder_id_24, + &field_transponder_id_32, + &field_pressure, + &options_pressure, + &field_temperature, + &options_temperature, + &field_flags, + &field_repeat, + &button_load, + &button_save, + &tx_view, + &button_transmit, + &text_status, + &progressbar}); + + // Set label styles to match other labels (light grey) + label_temperature.set_style(Theme::getInstance()->fg_light); + label_flags.set_style(Theme::getInstance()->fg_light); + + // Initialize values + options_frequency.set_by_value(transmitter_model.target_frequency()); + options_packet_type.set_selected_index(0); + field_repeat.set_value(repeat_count_); + + // Set initial signal type based on packet type + update_signal_type_from_packet(); + + // Initialize unit selection + options_pressure.set_by_value(format::pressure_unit); + options_temperature.set_by_value(format::temp_unit); + + // Initialize field values from default member variables + // For pressure and temperature, use unit conversion to display in selected units + field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF); + field_transponder_id_32.set_value(transponder_id_); + on_pressure_unit_change(); + on_temperature_unit_change(); + field_flags.set_value(flags_); + + update_field_visibility(); + update_packet_display(); + + // Event handlers + options_frequency.on_change = [this](size_t, OptionsField::value_t v) { + transmitter_model.set_target_frequency(v); + }; + + options_packet_type.on_change = [this](size_t, int32_t value) { + packet_type_ = static_cast(value); + update_signal_type_from_packet(); + + // Sync field values when switching packet types + if (packet_type_ == tpms::Reading::Type::Schrader) { + // Switching to Schrader: copy from 32-bit field to 24-bit field (masked) + transponder_id_ = field_transponder_id_32.to_integer() & 0x00FFFFFF; + field_transponder_id_24.set_value(transponder_id_); + } else { + // Switching from Schrader: copy from 24-bit field to 32-bit field + transponder_id_ = field_transponder_id_24.to_integer(); + field_transponder_id_32.set_value(transponder_id_); + } + + update_field_visibility(); + update_packet_display(); + }; + + options_pressure.on_change = [this](size_t, int32_t i) { + format::pressure_unit = (uint8_t)i; + on_pressure_unit_change(); + }; + + options_temperature.on_change = [this](size_t, int32_t i) { + format::temp_unit = (uint8_t)i; + on_temperature_unit_change(); + }; + + field_transponder_id_24.on_change = [this](SymField&) { + transponder_id_ = field_transponder_id_24.to_integer() & 0x00FFFFFF; + update_packet_display(); + }; + + field_transponder_id_32.on_change = [this](SymField&) { + transponder_id_ = field_transponder_id_32.to_integer(); + update_packet_display(); + }; + + field_pressure.on_change = [this](int32_t value) { + // Convert from displayed unit to kPa for internal storage + if (format::pressure_unit == PRESSURE_UNIT_PSI) { + pressure_kpa_ = value * 6895 / 1000; + } else if (format::pressure_unit == PRESSURE_UNIT_BAR) { + pressure_kpa_ = value * 100; + } else { + pressure_kpa_ = value; + } + update_packet_display(); + }; + + field_temperature.on_change = [this](int32_t value) { + // Convert from displayed unit to Celsius for internal storage + if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) { + temperature_c_ = (value - 32) * 5 / 9; + } else { + temperature_c_ = value; + } + update_packet_display(); + }; + + field_flags.on_change = [this](int32_t value) { + flags_ = value & 0x07; + update_packet_display(); + }; + + field_repeat.on_change = [this](int32_t value) { + repeat_count_ = value; + }; + + button_transmit.on_select = [this](Button&) { + if (is_transmitting_) { + stop_tx(); + } else { + start_tx(); + } + }; + + button_load.on_select = [this, &nav](Button&) { + auto open_view = nav.push(".TXT"); + open_view->on_changed = [this](std::filesystem::path file_path) { + // Load TPMS packet from file + File f; + auto error = f.open(file_path); + if (!error.is_valid()) { + char buffer[512]; + auto result = f.read(buffer, sizeof(buffer) - 1); + if (result.is_ok()) { + buffer[result.value()] = 0; + + // Parse the file format (key=value format, one per line) + std::string content(buffer); + size_t pos = 0; + + auto parse_line = [&content, &pos](const std::string& key) -> std::string { + size_t key_pos = content.find(key + "=", pos); + if (key_pos != std::string::npos) { + size_t value_start = key_pos + key.length() + 1; + size_t value_end = content.find('\n', value_start); + if (value_end == std::string::npos) value_end = content.length(); + return content.substr(value_start, value_end - value_start); + } + return ""; + }; + + std::string type_str = parse_line("Type"); + std::string id_str = parse_line("ID"); + std::string pressure_str = parse_line("Pressure"); + std::string temp_str = parse_line("Temperature"); + std::string flags_str = parse_line("Flags"); + std::string signal_type_str = parse_line("SignalType"); + + if (!type_str.empty()) { + int type = std::stoi(type_str); + if (type >= static_cast(tpms::Reading::Type::FLM_64) && + type <= static_cast(tpms::Reading::Type::GMC_96)) { + packet_type_ = static_cast(type); + options_packet_type.set_by_value(type); + } + } + + if (!id_str.empty()) { + transponder_id_ = std::stoul(id_str, nullptr, 16); + field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF); + field_transponder_id_32.set_value(transponder_id_); + } + + if (!pressure_str.empty()) { + pressure_kpa_ = std::stoi(pressure_str); + on_pressure_unit_change(); + } + + if (!temp_str.empty()) { + temperature_c_ = std::stoi(temp_str); + field_temperature.set_value(temperature_c_); + } + + if (!flags_str.empty()) { + uint8_t loaded_flags = std::stoul(flags_str, nullptr, 16); + // Handle old format files: extract bits 6-4 if value > 7 + flags_ = (loaded_flags > 7) ? ((loaded_flags >> 4) & 0x07) : (loaded_flags & 0x07); + field_flags.set_value(flags_); + } + + // Load signal type if available, otherwise auto-select based on packet type + if (!signal_type_str.empty()) { + int signal_type = std::stoi(signal_type_str); + if (signal_type >= 1 && signal_type <= 3) { + signal_type_ = static_cast(signal_type); + } else { + // Fall back to auto-detect + update_signal_type_from_packet(); + } + } else { + // Fall back to auto-detect for backwards compatibility + update_signal_type_from_packet(); + } + + update_packet_display(); + update_field_visibility(); + text_status.set("Loaded: " + file_path.filename().string()); + } else { + text_status.set("Error reading file"); + } + } else { + text_status.set("Error opening file"); + } + }; + }; + + button_save.on_select = [this](Button&) { + // Save current TPMS packet to file + auto timestamp = to_string_timestamp(rtc_time::now()); + std::string file_name = "TPMS_" + timestamp + ".TXT"; + ensure_directory(tpms_dir); + auto file_path = tpms_dir / file_name; + + File f; + auto error = f.create(file_path); + if (!error.is_valid()) { + std::string content = "Type=" + to_string_dec_uint(toUType(packet_type_), 1) + "\n"; + content += "ID=" + to_string_hex(transponder_id_, 8) + "\n"; + content += "Pressure=" + to_string_dec_uint(pressure_kpa_) + "\n"; + content += "Temperature=" + to_string_dec_int(temperature_c_) + "\n"; + content += "Flags=" + to_string_hex(flags_ & 0x07, 1) + "\n"; + + f.write(content.c_str(), content.length()); + text_status.set("Saved: " + file_name); + } else { + text_status.set("Error saving file"); + } + }; +} + +TPMSTXView::~TPMSTXView() { + stop_tx(); + transmitter_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::tpmstx diff --git a/firmware/application/external/tpmstx/tpms_tx_app.hpp b/firmware/application/external/tpmstx/tpms_tx_app.hpp new file mode 100644 index 000000000..69d0d10e1 --- /dev/null +++ b/firmware/application/external/tpmstx/tpms_tx_app.hpp @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __TPMS_TX_APP_H__ +#define __TPMS_TX_APP_H__ + +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" +#include "transmitter_model.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "portapack.hpp" +#include "message.hpp" +#include "tpms_packet.hpp" +#include "file_path.hpp" +#include "string_format.hpp" +#include "units.hpp" + +namespace ui::external_app::tpmstx { + +namespace format { +static uint8_t pressure_unit{PRESSURE_UNIT_PSI}; +static uint8_t temp_unit{TEMP_UNIT_CELSIUS}; +} // namespace format + +class TPMSTXView : public View { + public: + TPMSTXView(NavigationView& nav); + ~TPMSTXView(); + + void focus() override; + + std::string title() const override { return "TPMS TX"; }; + + private: + NavigationView& nav_; + + TxRadioState radio_state_{ + 314900000, /* frequency */ + 1750000, /* bandwidth */ + 2457600 /* sampling rate */ + }; + + app_settings::SettingsManager settings_{ + "tx_tpms", + app_settings::Mode::TX, + { + {"pressure_unit"sv, &format::pressure_unit}, + {"temp_unit"sv, &format::temp_unit}, + }}; + + // TPMS packet data + tpms::Reading::Type packet_type_{tpms::Reading::Type::Schrader}; + uint32_t transponder_id_{0x12345678}; + uint16_t pressure_kpa_{240}; // Default ~35 PSI + int16_t temperature_c_{25}; // Default 25Β°C + uint8_t flags_{0x00}; // 3-bit function code (0-7), checksum is auto-calculated + tpms::SignalType signal_type_{tpms::SignalType::FSK_19k2_Schrader}; + + uint8_t repeat_count_{5}; + uint32_t pause_duration_{50}; // ms between repeats + + bool is_transmitting_{false}; + + // FSK-specific repeat tracking (OOK repeats are handled by baseband) + uint8_t fsk_repeat_counter_{0}; + uint32_t fsk_repeat_timer_id_{0}; + + void update_signal_type_from_packet(); + void switch_baseband(); + + void start_tx(); + void stop_tx(); + void send_packet(); + void encode_and_transmit(); + void update_packet_display(); + void on_pressure_unit_change(); + void on_temperature_unit_change(); + void update_field_visibility(); + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + progressbar.set_value(message.progress); + if (message.done) { + handle_tx_complete(); + } + }}; + + void handle_tx_complete(); + + Labels labels{ + {{0 * 8, 0 * 16}, "Freq:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 2 * 16}, "ID:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 3 * 16}, "Pres:", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 5 * 16}, "Rpt:", Theme::getInstance()->fg_light->foreground}, + }; + + Text label_temperature{ + {0 * 8, 4 * 16, 5 * 8, 16}, + "Temp:"}; + + Text label_flags{ + {0 * 8, 4 * 16, 5 * 8, 16}, + "Func:"}; + + OptionsField options_frequency{ + {6 * 8, 0 * 16}, + 5, + { + {"314.9", 314900000}, + {"315.0", 315000000}, + {"433.9", 433920000}, + }}; + + OptionsField options_packet_type{ + {6 * 8, 1 * 16}, + 10, + { + {"Schrader", (int32_t)tpms::Reading::Type::Schrader}, + {"FLM_64", (int32_t)tpms::Reading::Type::FLM_64}, + {"FLM_72", (int32_t)tpms::Reading::Type::FLM_72}, + {"FLM_80", (int32_t)tpms::Reading::Type::FLM_80}, + {"GMC_96", (int32_t)tpms::Reading::Type::GMC_96}, + }}; + + OptionsField options_pressure{ + {11 * 8, 3 * 16}, + 4, + {{"kPa", PRESSURE_UNIT_KPA}, + {"PSI", PRESSURE_UNIT_PSI}, + {"BAR", PRESSURE_UNIT_BAR}}}; + + OptionsField options_temperature{ + {11 * 8, 4 * 16}, + 2, + {{STR_DEGREES_C, TEMP_UNIT_CELSIUS}, + {STR_DEGREES_F, TEMP_UNIT_FAHRENHEIT}}}; + + SymField field_transponder_id_24{ + {6 * 8, 2 * 16}, + 6, + SymField::Type::Hex}; + + SymField field_transponder_id_32{ + {6 * 8, 2 * 16}, + 8, + SymField::Type::Hex}; + + NumberField field_pressure{ + {6 * 8, 3 * 16}, + 4, + {0, 9999}, + 1, + ' '}; + + NumberField field_temperature{ + {6 * 8, 4 * 16}, + 4, + {-99, 999}, + 1, + ' '}; + + NumberField field_flags{ + {6 * 8, 4 * 16}, + 1, + {0, 7}, + 1, + ' '}; + + NumberField field_repeat{ + {6 * 8, 5 * 16}, + 3, + {1, 100}, + 1, + ' '}; + + Button button_load{ + {0 * 8, 8 * 16 + 4, 7 * 8, 24}, + "Load"}; + + Button button_save{ + {8 * 8, 8 * 16 + 4, 7 * 8, 24}, + "Save"}; + + TransmitterView2 tx_view{ + {16 * 8, 0 * 16}, + true // Short UI format + }; + + Button button_transmit{ + {0 * 8, 11 * 16, 15 * 8, 32}, + "START TX"}; + + Text text_status{ + {0 * 8, 13 * 16 + 4, 30 * 8, 16}, + "Ready"}; + + ProgressBar progressbar{ + {0 * 8, 14 * 16 + 8, 30 * 8, 16}}; +}; + +} // namespace ui::external_app::tpmstx + +#endif /*__TPMS_TX_APP_H__*/ diff --git a/firmware/application/external/waterfall_designer/main.cpp b/firmware/application/external/waterfall_designer/main.cpp new file mode 100644 index 000000000..dce575fa9 --- /dev/null +++ b/firmware/application/external/waterfall_designer/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_waterfall_designer.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::waterfall_designer { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::waterfall_designer + +extern "C" { + +__attribute__((section(".external_app.app_waterfall_designer.application_information"), used)) application_information_t _application_information_waterfall_designer = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::waterfall_designer::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Wt Design", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0xFE, + 0x7F, + 0x03, + 0xC0, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0x03, + 0xC0, + 0xFF, + 0xFF, + 0xFB, + 0xD7, + 0xFE, + 0x7F, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::cyan().v, + /*.menu_location = */ app_location_t::UTILITIES, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'C', 'A', 'P'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp new file mode 100644 index 000000000..e4d3da40c --- /dev/null +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -0,0 +1,503 @@ +/* + * Copyleft Mr. Robot + * Copyright HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_waterfall_designer.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "ui_freqman.hpp" +#include "file_path.hpp" +#include "ui_fileman.hpp" +#include "file_reader.hpp" +#include "ui_textentry.hpp" + +using namespace portapack; + +namespace ui::external_app::waterfall_designer { + +WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({&labels, + &field_frequency, + &field_frequency_step, + &field_rf_amp, + &field_lna, + &field_vga, + &option_bandwidth, + &record_view, + &menu_view, + &button_new, + &button_open, + &button_save, + &button_add_level, + &button_remove_level, + &button_edit_color, + &button_apply_setting, + &waterfall}); + ensure_directory(waterfalls_dir); + ui::Rect waterfall_rect{0, header_height, screen_rect().width(), screen_rect().height() - header_height}; + waterfall.set_parent_rect(waterfall_rect); + + menu_view.set_parent_rect({0, 1 * 16, screen_width, 7 * 16}); + + field_frequency_step.set_by_value(receiver_model.frequency_step()); + field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) { + receiver_model.set_frequency_step(v); + this->field_frequency.set_step(v); + }; + + freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth); + option_bandwidth.on_change = [this](size_t, uint32_t new_capture_rate) { + /* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC + * provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */ + + /* capture_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */ + /* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */ + /* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */ + + waterfall.stop(); + + // record_view determines the correct oversampling to apply and returns the actual sample rate. + // NB: record_view is what actually updates proc_capture baseband settings. + auto actual_sample_rate = record_view.set_sampling_rate(new_capture_rate); + + // Update the radio model with the actual sampling rate. + receiver_model.set_sampling_rate(actual_sample_rate); + + // Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate. + auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate); + receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth); + + capture_rate = new_capture_rate; + + waterfall.start(); + }; + + button_new.on_select = [this]() { + on_create_new_profile(); + }; + + button_open.on_select = [this]() { + on_open_profile(); + }; + + button_save.on_select = [this]() { + on_save_profile(); + }; + + button_add_level.on_select = [this]() { + on_add_level(); + }; + + button_remove_level.on_select = [this]() { + on_remove_level(); + }; + + button_edit_color.on_select = [this]() { + on_edit_color(); + }; + + button_apply_setting.on_select = [this]() { + if_apply_setting = true; + on_apply_current_to_wtf(); + // nav_.pop(); + }; + + menu_view.on_highlight = [this]() { + highlighted_index_ = menu_view.highlighted_index(); + }; + + receiver_model.enable(); + option_bandwidth.set_by_value(capture_rate); + + record_view.on_error = [&nav](std::string message) { + nav.display_modal("Error", message); + }; + + button_save.hidden(true); + button_add_level.hidden(true); + button_remove_level.hidden(true); + button_edit_color.hidden(true); + button_apply_setting.hidden(true); + + refresh_menu_view(); +} + +WaterfallDesignerView::WaterfallDesignerView( + NavigationView& nav, + ReceiverModel::settings_t override) + : WaterfallDesignerView(nav) { + // Settings to override when launched from another app (versus from AppSettings .ini file) + field_frequency.set_value(override.frequency_app_override); +} + +WaterfallDesignerView::~WaterfallDesignerView() { + if (!if_apply_setting) restore_current_profile(); + receiver_model.disable(); + baseband::shutdown(); +} + +void WaterfallDesignerView::set_parent_rect(const Rect new_parent_rect) { + View::set_parent_rect(new_parent_rect); + + ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height}; + waterfall.set_parent_rect(waterfall_rect); +} + +void WaterfallDesignerView::focus() { + button_open.focus(); +} + +void WaterfallDesignerView::on_freqchg(int64_t freq) { + field_frequency.set_value(freq); +} + +void WaterfallDesignerView::on_open_profile() { + auto open_view = nav_.push(".txt"); + open_view->push_dir(waterfalls_dir); + open_view->on_changed = [this](std::filesystem::path new_file_path) { + on_profile_changed(new_file_path); + }; +} + +void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) { + current_profile_path = new_profile_path; + profile_levels.clear(); + + File playlist_file; + auto error = playlist_file.open(new_profile_path.string()); + + if (error) return; + + menu_view.clear(); + auto reader = FileLineReader(playlist_file); + + for (const auto& line : reader) { + profile_levels.push_back(line); + } + + for (auto& line : profile_levels) { + // remove empty lines + if (line == "\n" || line == "\r\n" || line == "\r") { + profile_levels.erase(std::remove(profile_levels.begin(), profile_levels.end(), line), profile_levels.end()); + } + + // remove line end \n etc + if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) { + line = line.substr(0, line.length() - 1); + } + } + + button_save.hidden(false); + button_add_level.hidden(false); + button_remove_level.hidden(false); + button_edit_color.hidden(false); + button_apply_setting.hidden(false); + + refresh_menu_view(); + on_apply_current_to_wtf(); +} + +void WaterfallDesignerView::refresh_menu_view() { + menu_view.clear(); + + for (const auto& line : profile_levels) { + if (line.length() == 0 || line[0] == '#') { + menu_view.add_item({line, + ui::Color::grey(), + &bitmap_icon_notepad, + [this](KeyEvent) { + button_add_level.focus(); + }}); + } else { + // index,R,G,B + size_t pos = 0; + size_t next_pos = 0; + + // pass index + next_pos = line.find(',', pos); + if (next_pos == std::string::npos) continue; + pos = next_pos + 1; + + // r + next_pos = line.find(',', pos); + if (next_pos == std::string::npos) continue; + uint8_t r = static_cast(std::stoi(line.substr(pos, next_pos - pos))); + pos = next_pos + 1; + + // g + next_pos = line.find(',', pos); + if (next_pos == std::string::npos) continue; + uint8_t g = static_cast(std::stoi(line.substr(pos, next_pos - pos))); + pos = next_pos + 1; + + // b + uint8_t b = static_cast(std::stoi(line.substr(pos))); + + ui::Color color = ui::Color(r, g, b); + menu_view.add_item({line, + color, + &bitmap_icon_cwgen, + [this](KeyEvent) { + button_remove_level.focus(); + }}); + } + } + set_dirty(); +} + +void WaterfallDesignerView::on_apply_current_to_wtf() { + std::filesystem::path system_read_path = "waterfall.txt"; + copy_file(current_profile_path, system_read_path); + + waterfall.load_gradient(); + + set_dirty(); +} + +void WaterfallDesignerView::on_save_profile() { + if (current_profile_path.empty()) { + nav_.display_modal("Err", "No profile file loaded"); + return; + } else if (profile_levels.empty()) { + nav_.display_modal("Err", "List is empty"); + return; + } + + File profile_file; + auto error = profile_file.open(current_profile_path.string(), false, false); + + if (error) { + nav_.display_modal("Err", "open err"); + return; + } + + // clear file + profile_file.seek(0); + profile_file.truncate(); + + // write new data + for (const auto& entry : profile_levels) { + profile_file.write_line(entry); + } + + nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string()); +} + +void WaterfallDesignerView::on_add_level() { + if (highlighted_index_ >= profile_levels.size()) return; + if (profile_levels[highlighted_index_].empty()) return; + if (profile_levels[highlighted_index_][0] == '#') return; + if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + size_t insert_pos = highlighted_index_; + std::string new_entry = "0,128,128,128"; + profile_levels.insert(profile_levels.begin() + insert_pos, new_entry); + refresh_menu_view(); + on_edit_color(); +} + +void WaterfallDesignerView::on_remove_level() { + if (highlighted_index_ >= profile_levels.size()) return; + if (profile_levels[highlighted_index_].empty()) return; + if (profile_levels[highlighted_index_][0] == '#') return; + if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + profile_levels.erase(profile_levels.begin() + highlighted_index_); + refresh_menu_view(); +} + +void WaterfallDesignerView::on_edit_color() { + if (highlighted_index_ >= profile_levels.size()) return; + if (profile_levels[highlighted_index_].empty()) return; + if (profile_levels[highlighted_index_][0] == '#') return; + if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + + auto color_picker_view = nav_.push(profile_levels[highlighted_index_]); + color_picker_view->on_save = [this](std::string new_color) { + profile_levels[highlighted_index_] = new_color; + refresh_menu_view(); + on_apply_current_to_wtf(); + }; +} + +void WaterfallDesignerView::backup_current_profile() { + std::filesystem::path curren_wtf_path = "waterfall.txt"; + std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; + copy_file(curren_wtf_path, backup_path); +} + +void WaterfallDesignerView::restore_current_profile() { + std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; + std::filesystem::path put_back_path = "waterfall.txt"; + copy_file(backup_path, put_back_path); + delete_file(backup_path); +} + +void WaterfallDesignerView::on_apply_setting() { +} + +void WaterfallDesignerView::on_create_new_profile() { + text_prompt( + nav_, + file_name_buffer, + 32 - 4, // fat32 + ENTER_KEYBOARD_MODE_ALPHA, + [this](std::string& buffer) { + if (buffer.empty()) return; + + if (buffer.length() < 4 || buffer.substr(buffer.length() - 4) != ".txt") { + buffer += ".txt"; + } + + File new_file; + auto error = new_file.create(waterfalls_dir / buffer); + if (error) { + nav_.display_modal("Err", "create file err"); + return; + } + + profile_levels.clear(); + current_profile_path = waterfalls_dir / buffer; + on_profile_changed(current_profile_path); + }); +} + +WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str) + : nav_{nav}, + color_str_{color_str} { + add_children({&labels, + &field_red, + &field_green, + &field_blue, + &field_step, + &field_index, + &progressbar, + &button_save}); + + progressbar.set_max(UINT8_MAX); + + size_t pos = 0; + size_t next_pos = 0; + + // index + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + index_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); + pos = next_pos + 1; + } + + // r + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + red_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); + pos = next_pos + 1; + } + + // g + next_pos = color_str.find(',', pos); + if (next_pos != std::string::npos) { + green_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); + pos = next_pos + 1; + } + + // b + blue_ = static_cast(std::stoi(color_str.substr(pos))); + + field_red.set_value(red_); + field_green.set_value(green_); + field_blue.set_value(blue_); + field_index.set_value(index_); + + // cb + field_red.on_change = [this](int32_t) { + update_color_index(); + }; + + field_green.on_change = [this](int32_t) { + update_color_index(); + }; + + field_blue.on_change = [this](int32_t) { + update_color_index(); + }; + + button_save.on_select = [this](Button&) { + if (on_save) on_save(build_color_str()); + nav_.pop(); + }; + + field_index.on_change = [this](int32_t) { + update_color_index(); + }; + + field_step.on_change = [this](int32_t) { + field_red.set_step(field_step.value()); + field_green.set_step(field_step.value()); + field_blue.set_step(field_step.value()); + field_index.set_step(field_step.value()); + }; + + update_color_index(); +} + +void WaterfallDesignerColorPickerView::focus() { + button_save.focus(); +} + +void WaterfallDesignerColorPickerView::update_color_index() { + index_ = static_cast(field_index.value()); + red_ = static_cast(field_red.value()); + green_ = static_cast(field_green.value()); + blue_ = static_cast(field_blue.value()); + + const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40}; + + Painter painter_instance_2; + painter_instance_2.fill_rectangle( + {preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()}, + ui::Color(red_, green_, blue_)); +} + +void WaterfallDesignerColorPickerView::paint(Painter& painter) { + // this is not duplicated code. + // because need to display color when enter, + // but it is too early to call update_color() in the constructor. + + const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40}; + + painter.fill_rectangle( + {preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()}, + ui::Color(red_, green_, blue_)); +} + +std::string WaterfallDesignerColorPickerView::build_color_str() { + size_t index_pos = color_str_.find(','); + if (index_pos != std::string::npos) { + return color_str_.substr(0, index_pos + 1) + + to_string_dec_uint(red_) + "," + + to_string_dec_uint(green_) + "," + + to_string_dec_uint(blue_); + } + return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_); +} + +} /* namespace ui::external_app::waterfall_designer */ diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp new file mode 100644 index 000000000..e1fadf5fe --- /dev/null +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -0,0 +1,265 @@ +/* + * Copyleft Mr. Robot + * Copyright HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __WATERFALL_DESIGNER_APP_HPP__ +#define __WATERFALL_DESIGNER_APP_HPP__ + +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "ui_record_view.hpp" +#include "ui_spectrum.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "file_path.hpp" + +namespace ui::external_app::waterfall_designer { + +enum class ColorComponent { + RED, + GREEN, + BLUE +}; + +class WaterfallDesignerColorPickerView : public View { + public: + std::function on_save{}; + + WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str); + + std::string title() const override { return "Color Picker"; }; + void focus() override; + void paint(Painter& painter) override; + + private: + NavigationView& nav_; + std::string color_str_; + uint8_t index_{0}; + uint8_t red_{0}; + uint8_t green_{0}; + uint8_t blue_{0}; + + void update_color_index(); + std::string build_color_str(); + + Labels labels{ + {{0 * 8, 0 * 16}, "Index", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 2 * 16}, "Red", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 4 * 16}, "Green", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 6 * 16}, "Blue", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 8 * 16}, "Step", Theme::getInstance()->fg_light->foreground}}; + + NumberField field_index{ + {0 * 8, 1 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_red{ + {0 * 8, 3 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_green{ + {0 * 8, 5 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_blue{ + {0 * 8, 7 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + NumberField field_step{ + {0 * 8, 9 * 16}, + 3, + {0, 255}, + 1, + ' '}; + + ProgressBar progressbar{ + {0 * 8, + screen_height - 4 * 16 - 2 * 16 - 1 * 16, + screen_width, + 2 * 16}}; + + Button button_save{ + {0, screen_height - 4 * 16, screen_width, 4 * 16}, + "Save"}; +}; + +class WaterfallDesignerView : public View { + public: + WaterfallDesignerView(NavigationView& nav); + WaterfallDesignerView(NavigationView& nav, ReceiverModel::settings_t override); + ~WaterfallDesignerView(); + + void focus() override; + void set_parent_rect(const Rect new_parent_rect) override; + + std::string title() const override { return "Wtf Design"; }; + + private: + uint32_t capture_rate{500000}; + uint32_t file_format{0}; + uint32_t highlighted_index_{0}; + bool trim{false}; + std::filesystem::path current_profile_path = ""; + NavigationView& nav_; + RxRadioState radio_state_{ReceiverModel::Mode::Capture}; + app_settings::SettingsManager settings_{ + "rx_wtf_designer", + app_settings::Mode::RX, + { + {"capture_rate"sv, &capture_rate}, + {"file_format"sv, &file_format}, + {"trim"sv, &trim}, + }}; + + Labels labels{ + {{0 * 8, 1 * 16}, "Rate:", Theme::getInstance()->fg_light->foreground}, + {{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground}, + }; + + RxFrequencyField field_frequency{ + {0 * 8, 0 * 16}, + nav_}; + + FrequencyStepView field_frequency_step{ + {10 * 8, 0 * 16}}; + + RFAmpField field_rf_amp{ + {16 * 8, 0 * 16}}; + + LNAGainField field_lna{ + {18 * 8, 0 * 16}}; + + VGAGainField field_vga{ + {21 * 8, 0 * 16}}; + + OptionsField option_bandwidth{ + {24 * 8, 0 * 16}, + 5, + {}}; + + MenuView menu_view{}; + + NewButton button_new{ + {0 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_file, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_open{ + {4 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_load, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_save{ + {8 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_save, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_add_level{ + {12 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_add, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_remove_level{ + {16 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_delete, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_edit_color{ + {20 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_notepad, + Theme::getInstance()->fg_blue->foreground}; + + NewButton button_apply_setting{ + {24 * 8, 8 * 16, 4 * 8, 32}, + {}, + &bitmap_icon_replay, + Theme::getInstance()->fg_blue->foreground}; + + void backup_current_profile(); + void restore_current_profile(); + void on_create_new_profile(); + void on_open_profile(); + void on_profile_changed(std::filesystem::path new_profile_path); + void on_save_profile(); + void on_add_level(); + void on_remove_level(); + void on_edit_color(); + + void refresh_menu_view(); + + void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct + void on_apply_setting(); // apply set + + bool if_apply_setting{false}; + /*NB: + this works as: + each time you change color, it apply as file realtime + however if you don't push the apply (play) btn, it would resotore in distructor, + if you push apply, it would apply and exit*/ + + std::vector profile_levels{}; + + static constexpr ui::Dim header_height = 10 * 16; + + RecordView record_view{// we still need it cuz it make waterfall correct + {screen_width, screen_height, 30 * 8, 1 * 16}, + u"BBD_????.*", + captures_dir, + RecordView::FileType::RawS16, + 16384, + 3}; + + spectrum::WaterfallView waterfall{}; + std::string file_name_buffer{}; // needed by text_prompy + + MessageHandlerRegistration message_handler_freqchg{ + Message::ID::FreqChangeCommand, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freqchg(message->freq); + }}; + + void on_freqchg(int64_t freq); +}; + +} /* namespace ui::external_app::waterfall_designer */ + +#endif /*__WATERFALL_DESIGNER_APP_HPP__*/ diff --git a/firmware/application/external/wefax_rx/main.cpp b/firmware/application/external/wefax_rx/main.cpp index 0ab7f5e7e..380eb6f0a 100644 --- a/firmware/application/external/wefax_rx/main.cpp +++ b/firmware/application/external/wefax_rx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_wefax_rx.application_information"), us 0x00, 0x00, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::yellow().v, /*.menu_location = */ app_location_t::RX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index df0f22849..08bf0f36f 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -161,7 +161,7 @@ Optional File::sync() { File::Result File::read_file(const std::filesystem::path& filename) { constexpr size_t buffer_size = 0x80; - char* buffer[buffer_size]; + char buffer[buffer_size]; File f; auto error = f.open(filename); @@ -205,6 +205,7 @@ static std::filesystem::path find_last_ordinal_match( const std::filesystem::path& pattern, pattern_range range) { auto last_match = std::filesystem::path(); + if (range.start == std::string::npos || range.end == std::string::npos || range.start > range.end) return last_match; auto can_increment = [range](const auto& path) { for (auto i = range.start; i <= range.end; ++i) if (!isdigit(path.native()[i])) @@ -230,7 +231,7 @@ static std::filesystem::path increment_filename_ordinal( const std::filesystem::path& path, pattern_range range) { auto name = path.filename().native(); - + if (range.start == std::string::npos || range.end == std::string::npos || range.start > range.end) return {name}; for (auto i = range.end; i >= range.start; --i) { auto& c = name[i]; diff --git a/firmware/application/file_path.cpp b/firmware/application/file_path.cpp index a08811fb6..95e504957 100644 --- a/firmware/application/file_path.cpp +++ b/firmware/application/file_path.cpp @@ -51,5 +51,10 @@ const std::filesystem::path whipcalc_dir = u"WHIPCALC"; const std::filesystem::path ook_editor_dir = u"OOKFILES"; const std::filesystem::path hopper_dir = u"HOPPER"; const std::filesystem::path subghz_dir = u"SUBGHZ"; +const std::filesystem::path tpms_dir = u"TPMS"; const std::filesystem::path waterfalls_dir = u"WATERFALLS"; const std::filesystem::path macaddress_dir = u"MACADDRESS"; +const std::filesystem::path splash_dot_bmp = u"/splash.bmp"; +const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS"; +const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES"; +const std::filesystem::path epirb_dir = u"EPIRB"; diff --git a/firmware/application/file_path.hpp b/firmware/application/file_path.hpp index 0f8548ee0..af244c6eb 100644 --- a/firmware/application/file_path.hpp +++ b/firmware/application/file_path.hpp @@ -53,7 +53,13 @@ extern const std::filesystem::path whipcalc_dir; extern const std::filesystem::path ook_editor_dir; extern const std::filesystem::path hopper_dir; extern const std::filesystem::path subghz_dir; +extern const std::filesystem::path tpms_dir; extern const std::filesystem::path waterfalls_dir; extern const std::filesystem::path macaddress_dir; +extern const std::filesystem::path keeloq_keys_dir; +extern const std::filesystem::path keeloq_remotes_dir; +extern const std::filesystem::path epirb_dir; + +extern const std::filesystem::path splash_dot_bmp; #endif /* __FILE_PATH_H__ */ diff --git a/firmware/application/file_reader.hpp b/firmware/application/file_reader.hpp index bcd9f1517..03de8caa6 100644 --- a/firmware/application/file_reader.hpp +++ b/firmware/application/file_reader.hpp @@ -117,7 +117,7 @@ class BufferLineReader { std::strncpy(&it.line_data_[offset], buf, len); offset += len; - if (len < buf_size) + if (len < buf_size || buf[len - 1] == '\n') break; } diff --git a/firmware/application/firmware_info.c b/firmware/application/firmware_info.c index a026a1871..cd5e70950 100644 --- a/firmware/application/firmware_info.c +++ b/firmware/application/firmware_info.c @@ -1,7 +1,19 @@ #include "../../hackrf/firmware/common/firmware_info.h" #include "../../hackrf/firmware/common/platform_detect.h" +// Use the same logic as official HackRF firmware +#ifdef JAWBREAKER +#define SUPPORTED_PLATFORM PLATFORM_JAWBREAKER +#elif HACKRF_ONE #define SUPPORTED_PLATFORM (PLATFORM_HACKRF1_OG | PLATFORM_HACKRF1_R9) +#elif RAD1O +#define SUPPORTED_PLATFORM PLATFORM_RAD1O +#elif PRALINE +#define SUPPORTED_PLATFORM PLATFORM_PRALINE +#else +#define SUPPORTED_PLATFORM 0 +#endif + #define DFU_MODE_VALUE 0 __attribute__((section(".firmware_info"))) const struct firmware_info_t firmware_info = { diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp index 659e759d5..615b75cf4 100644 --- a/firmware/application/freqman_db.cpp +++ b/firmware/application/freqman_db.cpp @@ -66,7 +66,8 @@ options_t freqman_bandwidths[6] = { // NFM {"8k5", 0}, {"11k", 1}, - {"16k", 2}, + {"12k5", 2}, + {"16k", 3}, }, { // WFM @@ -122,7 +123,7 @@ options_t freqman_steps = { {"1kHz ", 1000}, {"5kHz (SA AM)", 5000}, {"6.25kHz(NFM)", 6250}, - {"8.33kHz(AIR)", 8330}, + {"8.33kHz(AIR)", 8333}, {"9kHz (EU AM)", 9000}, {"10kHz(US AM)", 10000}, {"12.5kHz(NFM)", 12500}, @@ -144,7 +145,7 @@ options_t freqman_steps_short = { {"1kHz", 1000}, {"5kHz", 5000}, {"6.25kHz", 6250}, - {"8.33kHz", 8330}, + {"8.33kHz", 8333}, {"9kHz", 9000}, {"10kHz", 10000}, {"12.5kHz", 12500}, diff --git a/firmware/application/gradient.cpp b/firmware/application/gradient.cpp index 2ca27e6bb..f8564526a 100644 --- a/firmware/application/gradient.cpp +++ b/firmware/application/gradient.cpp @@ -84,8 +84,7 @@ void Gradient::step(int16_t index, int16_t r, int16_t g, int16_t b) { int16_t new_r = prev_r * y + r * x; int16_t new_g = prev_g * y + g * x; int16_t new_b = prev_b * y + b * x; - - lut[i] = ui::Color(new_r, new_g, new_b); + if (i <= 255) lut[i] = ui::Color(new_r, new_g, new_b); } prev_index = index; diff --git a/firmware/application/hackrf_core_mini.c b/firmware/application/hackrf_core_mini.c new file mode 100644 index 000000000..980a96cf9 --- /dev/null +++ b/firmware/application/hackrf_core_mini.c @@ -0,0 +1,56 @@ +#include "gpio_lpc.h" + +typedef enum { + LED1 = 0, + LED2 = 1, + LED3 = 2, + LED4 = 3, +} led_t; + +/* GPIO Output PinMux */ +static struct gpio_t gpio_led[] = { + GPIO(2, 1), + GPIO(2, 2), + GPIO(2, 8), +#ifdef RAD1O + GPIO(5, 26), +#endif +}; + +void delay(uint32_t duration) { + while (duration--) { + /* cannot be optimized out */ + __asm__ volatile("nop"); + } +} + +void delay_us_at_mhz(uint32_t us, uint32_t mhz) { + /* overflow-safe multiply */ + uint64_t cycles64 = (uint64_t)us * (uint64_t)mhz; + if (cycles64 > UINT32_MAX) { + cycles64 = UINT32_MAX; + } + delay((uint32_t)cycles64); +} + +void led_on(const led_t led) { + gpio_set(&gpio_led[led]); +} + +void led_off(const led_t led) { + gpio_clear(&gpio_led[led]); +} + +void halt_and_flash(const uint32_t duration) { + /* blink LED1, LED2, and LED3 */ + while (1) { + led_on(LED1); + led_on(LED2); + led_on(LED3); + delay(duration); + led_off(LED1); + led_off(LED2); + led_off(LED3); + delay(duration); + } +} diff --git a/firmware/application/hw/debounce.cpp b/firmware/application/hw/debounce.cpp index 17905edb1..585b66753 100644 --- a/firmware/application/hw/debounce.cpp +++ b/firmware/application/hw/debounce.cpp @@ -37,6 +37,14 @@ void Debounce::enable_repeat() { repeat_enabled_ = true; } +void Debounce::set_enable_repeat(bool enabled) { + repeat_enabled_ = enabled; +} + +bool Debounce::get_repeat_enabled() { + return repeat_enabled_; +} + bool Debounce::get_long_press_enabled() const { return long_press_enabled_; } diff --git a/firmware/application/hw/debounce.hpp b/firmware/application/hw/debounce.hpp index b40475588..d7a696125 100644 --- a/firmware/application/hw/debounce.hpp +++ b/firmware/application/hw/debounce.hpp @@ -43,6 +43,8 @@ class Debounce { bool feed(const uint8_t bit); uint8_t state(); void enable_repeat(); + void set_enable_repeat(bool enabled); + bool get_repeat_enabled(); bool get_long_press_enabled() const; void set_long_press_enabled(bool v); bool long_press_occurred(); diff --git a/firmware/application/hw/max2831.cpp b/firmware/application/hw/max2831.cpp new file mode 100644 index 000000000..4d5073d0b --- /dev/null +++ b/firmware/application/hw/max2831.cpp @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2025 Great Scott Gadgets + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +/* + * MAX2831 driver ported from GSG HackRF reference implementation (max2831.c). + * Adapted to work with Mayhem's MAX283x abstraction layer. + */ + +#ifdef PRALINE + +#include "max2831.hpp" + +#include "hackrf_hal.hpp" +#include "hackrf_gpio.hpp" +using namespace hackrf::one; + +#include "ch.h" +#include "hal.h" + +#include +#include + +namespace max2831 { + +using namespace max283x; + +static MAX2831Info max2831_info = {0, 0, 0, false, false}; + +MAX2831Info get_max2831_info() { + return { + max2831_info.requested_freq_mhz, + max2831_info.calculated_n, + max2831_info.calculated_frac, + max2831_info.set_frequency_called, + max2831_info.frequency_valid}; +} + +/* + * MAX2831 uses 9-bit SPI transfers. + * An 18-bit word is sent as two 9-bit transfers: + * Word format: [VALUE:14][REG:4] + * First transfer: bits 17:9 (high 9 bits) + * Second transfer: bits 8:0 (low 9 bits) + * + * This matches the GSG reference implementation exactly. + */ +void MAX2831::write_reg(const uint8_t reg, const uint16_t value) { + uint32_t word = (((uint32_t)value & 0x3fff) << 4) | (reg & 0xf); + uint16_t values[2] = { + static_cast(word >> 9), + static_cast(word & 0x1ff)}; + _target.transfer(values, 2); +} + +void MAX2831::set_reg_field(const uint8_t reg, const uint16_t mask, const uint16_t value) { + _regs[reg] = (_regs[reg] & ~mask) | (value & mask); + mark_dirty(reg); +} + +uint16_t MAX2831::get_reg_field(const uint8_t reg, const uint16_t mask, const uint8_t shift) { + return (_regs[reg] & mask) >> shift; +} + +void MAX2831::mark_dirty(const uint8_t reg) { + _regs_dirty |= (1 << reg); +} + +void MAX2831::mark_clean(const uint8_t reg) { + _regs_dirty &= ~(1 << reg); +} + +void MAX2831::flush_reg(const uint8_t reg) { + write_reg(reg, _regs[reg]); + mark_clean(reg); +} + +void MAX2831::flush_dirty() { + for (size_t r = 0; r < reg_count; r++) { + if ((_regs_dirty >> r) & 0x1) { + flush_reg(r); + } + } +} + +void MAX2831::init() { + set_mode(Mode::Shutdown); + + /* Configure GPIO pins for MAX2831 control */ + gpio_max283x_enable.output(); + gpio_max2831_rx_enable.output(); + gpio_max2831_rxhp.output(); + gpio_max2831_rxhp.write(0); /* RXHP low = 100 Hz HPF (default) */ + + /* Reset to default register values */ + std::memcpy(_regs.data(), default_regs.data(), sizeof(_regs)); + _regs_dirty = 0xFFFF; + + /* Write default register values to chip */ + flush_dirty(); + + /* Use SPI control instead of B1-B7 pins for gain settings. + * This matches the GSG reference: max2831_setup() */ + set_reg_field(8, REG8_RXVGA_GAIN_SPI_EN, REG8_RXVGA_GAIN_SPI_EN); + set_reg_field(9, REG9_TXVGA_GAIN_SPI_EN, REG9_TXVGA_GAIN_SPI_EN); + + /* Set initial gains - matches GSG reference */ + set_reg_field(12, REG12_TXVGA_GAIN_MASK, 0x00); /* Minimum TX gain */ + set_reg_field(7, REG7_RX_HPF_SEL_MASK, REG7_RX_HPF_30KHZ); + set_reg_field(11, REG11_LNA_GAIN_MASK, REG11_LNA_GAIN_MAX); + set_reg_field(11, REG11_RXVGA_GAIN_MASK, 0x18); // Moderate RX VGA gain + + /* FORCE MAXIMUM GAIN FOR TESTING */ + // set_reg_field(11, REG11_RXVGA_GAIN_MASK, 0x1F); // 62 dB VGA = MAX + + /* Configure baseband filter for 8 MHz TX - matches GSG reference */ + // set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_7_5M); + set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_15M); + set_reg_field(7, REG7_RX_LPF_FINE_MASK, REG7_RX_LPF_FINE_100); + set_reg_field(7, REG7_TX_LPF_FINE_MASK, REG7_TX_LPF_FINE_100); + + /* Disable clock output */ + set_reg_field(14, REG14_CLKOUT_PIN_EN, 0); + + /* Write all modified registers */ + flush_dirty(); + + set_mode(Mode::Standby); +} + +void MAX2831::set_mode(const Mode mode) { + _mode = mode; + + /* + * MAX2831 mode control via ENABLE and RXTX pins. + * From GSG hackrf max2831_target.c: + * + * Shutdown: ENABLE=0, RXTX=0 + * Standby: ENABLE=0, RXTX=1 (PLL/VCO/LO on, ready for quick TX/RX) + * RX: ENABLE=1, RXTX=0 + * TX: ENABLE=1, RXTX=1 + * + * Note: gpio_max2831_rx_enable is the RXTX mode select pin. + * RXTX=0 selects RX, RXTX=1 selects TX. + */ + + /* Handle calibration mode bits if needed */ + bool tx_cal = (mode == Mode::Tx_Calibration); + bool rx_cal = (mode == Mode::Rx_Calibration); + + uint16_t current_tx_cal = get_reg_field(6, REG6_TX_CAL_MODE_EN, REG6_TX_CAL_MODE_EN_SHIFT); + uint16_t current_rx_cal = get_reg_field(6, REG6_RX_CAL_MODE_EN, REG6_RX_CAL_MODE_EN_SHIFT); + + if (current_tx_cal != (tx_cal ? 1 : 0)) { + set_reg_field(6, REG6_TX_CAL_MODE_EN, tx_cal ? REG6_TX_CAL_MODE_EN : 0); + flush_dirty(); + } + if (current_rx_cal != (rx_cal ? 1 : 0)) { + set_reg_field(6, REG6_RX_CAL_MODE_EN, rx_cal ? REG6_RX_CAL_MODE_EN : 0); + flush_dirty(); + } + + switch (mode) { + default: + case Mode::Shutdown: + gpio_max2831_rx_enable.write(0); /* RXTX=0 */ + gpio_max283x_enable.write(0); /* ENABLE=0 */ + break; + case Mode::Standby: + gpio_max2831_rx_enable.write(1); /* RXTX=1 */ + gpio_max283x_enable.write(0); /* ENABLE=0 */ + break; + case Mode::Transmit: + case Mode::Tx_Calibration: + gpio_max2831_rx_enable.write(1); /* RXTX=1 for TX */ + gpio_max283x_enable.write(1); /* ENABLE=1 */ + break; + case Mode::Receive: + case Mode::Rx_Calibration: + gpio_max2831_rx_enable.write(0); /* RXTX=0 for RX */ + gpio_max283x_enable.write(1); /* ENABLE=1 */ + break; + } + + /* Update LPF bandwidth for current mode */ + if (_desired_lpf_bw > 0) { + set_lpf_bandwidth_internal(_desired_lpf_bw); + } +} + +void MAX2831::set_tx_vga_gain(const int_fast8_t db) { + /* TX VGA gain: 0-31 dB in ~1 dB steps + * Register value: gain * 2 | 1, max 0x3F + * This matches GSG reference: max2831_set_txvga_gain() */ + int_fast8_t db_clipped = std::max(0, std::min(31, (int)db)); + uint16_t value = std::min((db_clipped << 1) | 1, 0x3f); + set_reg_field(12, REG12_TXVGA_GAIN_MASK, value); + flush_reg(12); +} + +void MAX2831::set_lna_gain(const int_fast8_t db) { + /* + * LNA gain has 3 settings (from GSG reference): + * MAX (33 dB), -16 dB from max (17 dB), -33 dB from max (0 dB) + * Map from MAX2837 8 dB steps for compatibility + */ + uint16_t gain_val; + if (db >= 32) { + gain_val = REG11_LNA_GAIN_MAX; + } else if (db >= 16) { + gain_val = REG11_LNA_GAIN_M16; + } else { + gain_val = REG11_LNA_GAIN_M33; + } + set_reg_field(11, REG11_LNA_GAIN_MASK, gain_val); + flush_reg(11); +} + +void MAX2831::set_vga_gain(const int_fast8_t db) { + /* VGA gain: 0-62 dB in 2 dB steps + * This matches GSG reference: max2831_set_vga_gain() */ + if ((db & 0x1) || db > 62) { + return; /* Invalid: must be even and <= 62 */ + } + int_fast8_t db_clipped = std::max(0, std::min(62, (int)db)); + uint16_t value = (db_clipped >> 1) & 0x1f; + set_reg_field(11, REG11_RXVGA_GAIN_MASK, value); + flush_reg(11); +} + +/* + * LPF bandwidth tables from GSG reference max2831.c + */ +struct lpf_ft_t { + uint32_t bandwidth_hz; + uint8_t ft; +}; + +struct lpf_ft_fine_t { + uint8_t percent; + uint8_t ft_fine; +}; + +/* Measured -0.5 dB complex baseband bandwidth for each register setting */ +static constexpr lpf_ft_t rx_lpf_ft[] = { + {11600000, REG8_RX_LPF_7_5M}, + {15100000, REG8_RX_LPF_8_5M}, + {22600000, REG8_RX_LPF_15M}, + {28300000, REG8_RX_LPF_18M}, + {0, 0}, +}; + +static constexpr lpf_ft_fine_t rx_lpf_ft_fine[] = { + {90, REG7_RX_LPF_FINE_90}, + {95, REG7_RX_LPF_FINE_95}, + {100, REG7_RX_LPF_FINE_100}, + {105, REG7_RX_LPF_FINE_105}, + {110, REG7_RX_LPF_FINE_110}, + {0, 0}, +}; + +static constexpr lpf_ft_t tx_lpf_ft[] = { + {11900000, REG8_TX_LPF_8M}, + {15800000, REG8_TX_LPF_11M}, + {23600000, REG8_TX_LPF_16_5M}, + {31300000, REG8_TX_LPF_22_5M}, + {0, 0}, +}; + +static constexpr lpf_ft_fine_t tx_lpf_ft_fine[] = { + {90, REG7_TX_LPF_FINE_90}, + {95, REG7_TX_LPF_FINE_95}, + {100, REG7_TX_LPF_FINE_100}, + {105, REG7_TX_LPF_FINE_105}, + {110, REG7_TX_LPF_FINE_110}, + {115, REG7_TX_LPF_FINE_115}, + {0, 0}, +}; + +uint32_t MAX2831::set_lpf_bandwidth_internal(const uint32_t bandwidth_hz) { + const lpf_ft_t* coarse; + const lpf_ft_fine_t* fine; + + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + coarse = rx_lpf_ft; + fine = rx_lpf_ft_fine; + } else { + coarse = tx_lpf_ft; + fine = tx_lpf_ft_fine; + } + + /* Find coarse and fine settings for LPF - matches GSG reference */ + bool found = false; + const lpf_ft_fine_t* f = fine; + for (; coarse->bandwidth_hz != 0; coarse++) { + uint32_t coarse_aux = coarse->bandwidth_hz / 100; + for (f = fine; f->percent != 0; f++) { + if ((coarse_aux * f->percent) >= bandwidth_hz) { + found = true; + break; + } + } + if (found) break; + } + + /* Use the widest setting if a wider bandwidth than our maximum is requested */ + if (!found) { + coarse--; + f--; + } + + /* Program found settings */ + set_reg_field(8, REG8_LPF_COARSE_MASK, coarse->ft); + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + set_reg_field(7, REG7_RX_LPF_FINE_MASK, f->ft_fine); + } else { + /* TX fine values are already shifted in the constants (REG7_TX_LPF_FINE_*) */ + set_reg_field(7, REG7_TX_LPF_FINE_MASK, f->ft_fine); + } + flush_dirty(); + + return coarse->bandwidth_hz * f->percent / 100; +} + +void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) { + _desired_lpf_bw = bandwidth_minimum; +#ifdef PRALINE + uint32_t actual_bw = bandwidth_minimum; + + _desired_lpf_bw = actual_bw; + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + set_lpf_bandwidth_internal(actual_bw); + } +#else + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + set_lpf_bandwidth_internal(bandwidth_minimum); + } +#endif +} + +void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) { + _desired_lpf_bw = bandwidth_minimum; + if (_mode == Mode::Transmit || _mode == Mode::Tx_Calibration) { + set_lpf_bandwidth_internal(bandwidth_minimum); + } +} + +bool MAX2831::set_frequency(const rf::Frequency lo_frequency) { + /* + * MAX2831 frequency synthesis from GSG reference max2831_set_frequency(): + * F_LO = F_REF * (N + F/2^20) / R + * Where: + * F_REF = 40 MHz reference + * R = reference divider (1 or 2), we use R=2 + * N = integer divider (8 bits) + * F = fractional divider (20 bits) + * + * Using R=2: F_LO = 40M * (N + F/2^20) / 2 = 20M * (N + F/2^20) + */ + + /* MAX2831 supports 2.3-2.6 GHz */ + // if (lo_frequency < MAX2831_MIN_LO_FREQUENCY_HZ || lo_frequency > MAX2831_MAX_LO_FREQUENCY_HZ) { + // return false; + // } + + bool valid = (lo_frequency >= MAX2831_MIN_LO_FREQUENCY_HZ && lo_frequency <= MAX2831_MAX_LO_FREQUENCY_HZ); + + // TRACK REQUEST IMMEDIATELY + max2831_info.requested_freq_mhz = lo_frequency / 1000000; + max2831_info.set_frequency_called = true; + max2831_info.frequency_valid = valid; + + if (!valid) { + max2831_info.calculated_n = 0; + max2831_info.calculated_frac = 0; + return false; + } + + /* From GSG reference: ASSUME 40MHz PLL. Ratio = F*R/40,000,000. + * TODO: fixed to R=2. Check if it's worth exploring R=1. */ + uint32_t freq = lo_frequency; + freq += (20000000 >> 21); /* Round to nearest frequency */ + uint32_t div_int = freq / 20000000; + uint32_t div_rem = freq % 20000000; + uint32_t div_frac = 0; + uint32_t div_cmp = 20000000; + + for (int i = 0; i < 20; i++) { + div_frac <<= 1; + div_rem <<= 1; + if (div_rem >= div_cmp) { + div_frac |= 0x1; + div_rem -= div_cmp; + } + } + + // TRACK CALCULATED VALUES + max2831_info.calculated_n = div_int; + max2831_info.calculated_frac = div_frac; + + /* Write order matters - matches GSG reference */ + /* REG 3: SYN_INT (bits 7:0) and SYN_FRAC_LO (bits 13:8) */ + uint16_t reg3_val = (div_int & 0xFF) | ((div_frac & 0x3F) << 8); + _regs[3] = reg3_val; + mark_dirty(3); + + /* REG 4: SYN_FRAC_HI (bits 13:0) - upper 14 bits of 20-bit fractional */ + uint16_t reg4_val = (div_frac >> 6) & 0x3FFF; + _regs[4] = reg4_val; + mark_dirty(4); + + flush_dirty(); + + return true; +} + +void MAX2831::set_rx_LO_iq_phase_calibration(const size_t v) { + /* MAX2831 doesn't have the same IQ calibration as MAX2837 */ + (void)v; +} + +void MAX2831::set_tx_LO_iq_phase_calibration(const size_t v) { + /* MAX2831 doesn't have the same IQ calibration as MAX2837 */ + (void)v; +} + +void MAX2831::set_rx_buff_vcm(const size_t v) { + /* MAX2831 RX IQ common mode voltage is in register 15 + * Values: 0=1.1V, 1=1.2V, 2=1.3V, 3=1.45V */ + uint16_t vcm = std::min(v, (size_t)3) << REG15_RXIQ_VCM_SHIFT; + set_reg_field(15, REG15_RXIQ_VCM_MASK, vcm); + flush_reg(15); +} + +int8_t MAX2831::temp_sense() { + /* MAX2831 temperature sensor can be read via RSSI MUX. + * This would require: + * 1. Switch RSSI_MUX to temperature mode + * 2. Read the ADC + * 3. Switch back to RSSI mode + * For now, return a placeholder value. */ + return 25; /* Room temperature placeholder */ +} + +reg_t MAX2831::read(const address_t reg_num) { + /* MAX2831 doesn't support SPI read, return cached value */ + if (reg_num < reg_count) { + return _regs[reg_num]; + } + return 0; +} + +void MAX2831::write(const address_t reg_num, const reg_t value) { + if (reg_num < reg_count) { + _regs[reg_num] = value & 0x3FFF; /* 14-bit registers */ + write_reg(reg_num, _regs[reg_num]); + mark_clean(reg_num); + } +} + +} // namespace max2831 +#endif diff --git a/firmware/application/hw/max2831.hpp b/firmware/application/hw/max2831.hpp new file mode 100644 index 000000000..7db101825 --- /dev/null +++ b/firmware/application/hw/max2831.hpp @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2025 Great Scott Gadgets + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +/* + * MAX2831 driver ported from GSG HackRF reference implementation. + * Register definitions match max2831_regs.def from hackrf firmware. + */ + +#ifndef __MAX2831_H__ +#define __MAX2831_H__ + +#include "max283x.hpp" +#include "gpio.hpp" +#include "spi_arbiter.hpp" + +#include +#include + +namespace max2831 { + +using namespace max283x; + +// Global debug tracking for MAX2831 +struct MAX2831Info { + uint32_t requested_freq_mhz; + uint32_t calculated_n; + uint32_t calculated_frac; + bool set_frequency_called; + bool frequency_valid; +}; + +MAX2831Info get_max2831_info(); + +/* minumum and maximum lo_frequencies supported by max2831 */ +constexpr rf::Frequency MAX2831_MIN_LO_FREQUENCY_HZ = 2300000000LL; +constexpr rf::Frequency MAX2831_MAX_LO_FREQUENCY_HZ = 2600000000LL; + +/* MAX2831 has 16 registers, each containing 14 bits of data */ +constexpr size_t reg_count = 16; + +/* Default register values from GSG HackRF reference (max2831.c) */ +constexpr std::array default_regs = { + 0x1740, /* 0: enable fractional mode (Table 16 recommends 0x0740, clearing unknown bit) */ + 0x119a, /* 1 */ + 0x1003, /* 2 */ + 0x0079, /* 3: PLL divider settings for 2437 MHz */ + 0x3666, /* 4: PLL divider settings for 2437 MHz */ + 0x00a4, /* 5: divide reference frequency by 2 */ + 0x0060, /* 6: enable TX power detector */ + 0x1022, /* 7: 110% TX LPF bandwidth */ + 0x2021, /* 8: pin control of RX gain, 11 MHz LPF bandwidth */ + 0x03b5, /* 9: pin control of TX gain */ + 0x1d80, /* 10: 3.5 us PA enable delay, zero PA bias */ + 0x0074, /* 11: LNA high gain, RX VGA moderate gain (Table 27 recommends 0x007f, maximum gain) */ + 0x0140, /* 12: TX VGA minimum */ + 0x0e92, /* 13 */ + 0x0100, /* 14: reference clock output disabled */ + 0x0145, /* 15: RX IQ common mode 1.1 V */ +}; + +/* + * Register bit field definitions from max2831_regs.def + * Format: REG__ + */ + +/* REG 0: PLL Mode */ +constexpr uint16_t REG0_PLL_MODE_SHIFT = 10; +constexpr uint16_t REG0_PLL_MODE_MASK = (1 << REG0_PLL_MODE_SHIFT); +constexpr uint16_t REG0_PLL_MODE_INTEGER = 0; +constexpr uint16_t REG0_PLL_MODE_FRACTIONAL = 1; + +/* REG 3: Synthesizer Integer and Fractional Low */ +constexpr uint16_t REG3_SYN_INT_SHIFT = 0; +constexpr uint16_t REG3_SYN_INT_MASK = 0x00FF; /* D7:D0 - Integer divider (8 bits) */ +constexpr uint16_t REG3_SYN_FRAC_LO_SHIFT = 8; +constexpr uint16_t REG3_SYN_FRAC_LO_MASK = 0x3F00; /* D13:D8 - Low 6 bits of fractional divider */ + +/* REG 4: Synthesizer Fractional High */ +constexpr uint16_t REG4_SYN_FRAC_HI_MASK = 0x3FFF; /* D13:D0 - High 14 bits of fractional divider */ + +/* REG 5: Reference Divider and Lock Detect */ +constexpr uint16_t REG5_SYN_REF_DIV_SHIFT = 2; +constexpr uint16_t REG5_SYN_REF_DIV_1 = (0 << REG5_SYN_REF_DIV_SHIFT); +constexpr uint16_t REG5_SYN_REF_DIV_2 = (1 << REG5_SYN_REF_DIV_SHIFT); + +/* REG 6: Calibration Mode */ +constexpr uint16_t REG6_RX_CAL_MODE_EN_SHIFT = 0; +constexpr uint16_t REG6_RX_CAL_MODE_EN = (1 << REG6_RX_CAL_MODE_EN_SHIFT); +constexpr uint16_t REG6_TX_CAL_MODE_EN_SHIFT = 1; +constexpr uint16_t REG6_TX_CAL_MODE_EN = (1 << REG6_TX_CAL_MODE_EN_SHIFT); +constexpr uint16_t REG6_TX_POWER_DETECT_EN_SHIFT = 6; +constexpr uint16_t REG6_TX_POWER_DETECT_EN = (1 << REG6_TX_POWER_DETECT_EN_SHIFT); + +/* REG 7: LPF Fine Adjustment and RX HPF */ +constexpr uint16_t REG7_RX_LPF_FINE_SHIFT = 0; +constexpr uint16_t REG7_RX_LPF_FINE_MASK = 0x0007; /* D2:D0 */ +constexpr uint16_t REG7_RX_LPF_FINE_90 = 0; +constexpr uint16_t REG7_RX_LPF_FINE_95 = 1; +constexpr uint16_t REG7_RX_LPF_FINE_100 = 2; +constexpr uint16_t REG7_RX_LPF_FINE_105 = 3; +constexpr uint16_t REG7_RX_LPF_FINE_110 = 4; + +constexpr uint16_t REG7_TX_LPF_FINE_SHIFT = 3; +constexpr uint16_t REG7_TX_LPF_FINE_MASK = 0x0038; /* D5:D3 */ +constexpr uint16_t REG7_TX_LPF_FINE_90 = (0 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_95 = (1 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_100 = (2 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_105 = (3 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_110 = (4 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_115 = (5 << REG7_TX_LPF_FINE_SHIFT); + +constexpr uint16_t REG7_RX_HPF_SEL_SHIFT = 12; +constexpr uint16_t REG7_RX_HPF_SEL_MASK = 0x3000; /* D13:D12 */ +constexpr uint16_t REG7_RX_HPF_100HZ = (0 << REG7_RX_HPF_SEL_SHIFT); +constexpr uint16_t REG7_RX_HPF_4KHZ = (1 << REG7_RX_HPF_SEL_SHIFT); +constexpr uint16_t REG7_RX_HPF_30KHZ = (2 << REG7_RX_HPF_SEL_SHIFT); + +/* REG 8: LPF Coarse, RSSI MUX, and RX VGA SPI Enable */ +constexpr uint16_t REG8_LPF_COARSE_SHIFT = 0; +constexpr uint16_t REG8_LPF_COARSE_MASK = 0x0003; /* D1:D0 */ +/* RX and TX share the same coarse LPF setting bits */ +constexpr uint16_t REG8_RX_LPF_7_5M = 0; +constexpr uint16_t REG8_RX_LPF_8_5M = 1; +constexpr uint16_t REG8_RX_LPF_15M = 2; +constexpr uint16_t REG8_RX_LPF_18M = 3; +constexpr uint16_t REG8_TX_LPF_8M = 0; +constexpr uint16_t REG8_TX_LPF_11M = 1; +constexpr uint16_t REG8_TX_LPF_16_5M = 2; +constexpr uint16_t REG8_TX_LPF_22_5M = 3; + +constexpr uint16_t REG8_RSSI_MUX_SHIFT = 8; +constexpr uint16_t REG8_RSSI_MUX_MASK = 0x0300; /* D9:D8 */ +constexpr uint16_t REG8_RSSI_MUX_RSSI = (0 << REG8_RSSI_MUX_SHIFT); +constexpr uint16_t REG8_RSSI_MUX_TEMP = (1 << REG8_RSSI_MUX_SHIFT); +constexpr uint16_t REG8_RSSI_MUX_TX_POWER = (2 << REG8_RSSI_MUX_SHIFT); + +constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN_SHIFT = 12; +constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN = (1 << REG8_RXVGA_GAIN_SPI_EN_SHIFT); + +/* REG 9: TX VGA SPI Enable */ +constexpr uint16_t REG9_TXVGA_GAIN_SPI_EN_SHIFT = 10; +constexpr uint16_t REG9_TXVGA_GAIN_SPI_EN = (1 << REG9_TXVGA_GAIN_SPI_EN_SHIFT); + +/* REG 11: RX Gain */ +constexpr uint16_t REG11_RXVGA_GAIN_SHIFT = 0; +constexpr uint16_t REG11_RXVGA_GAIN_MASK = 0x001F; /* D4:D0 - 5 bits */ + +constexpr uint16_t REG11_LNA_GAIN_SHIFT = 5; +constexpr uint16_t REG11_LNA_GAIN_MASK = 0x0060; /* D6:D5 - 2 bits */ +constexpr uint16_t REG11_LNA_GAIN_M33 = (0 << REG11_LNA_GAIN_SHIFT); /* -33 dB from max (min) */ +constexpr uint16_t REG11_LNA_GAIN_M16 = (2 << REG11_LNA_GAIN_SHIFT); /* -16 dB from max */ +constexpr uint16_t REG11_LNA_GAIN_MAX = (3 << REG11_LNA_GAIN_SHIFT); /* Maximum LNA gain */ + +/* REG 12: TX VGA Gain */ +constexpr uint16_t REG12_TXVGA_GAIN_SHIFT = 0; +constexpr uint16_t REG12_TXVGA_GAIN_MASK = 0x003F; /* D5:D0 - 6 bits */ + +/* REG 14: Clock Output */ +constexpr uint16_t REG14_CLKOUT_PIN_EN_SHIFT = 9; +constexpr uint16_t REG14_CLKOUT_PIN_EN = (1 << REG14_CLKOUT_PIN_EN_SHIFT); + +/* REG 15: RX IQ Common Mode */ +constexpr uint16_t REG15_RXIQ_VCM_SHIFT = 10; +constexpr uint16_t REG15_RXIQ_VCM_MASK = 0x0C00; /* D11:D10 - 2 bits */ +constexpr uint16_t REG15_RXIQ_VCM_1_1 = (0 << REG15_RXIQ_VCM_SHIFT); /* 1.1V */ +constexpr uint16_t REG15_RXIQ_VCM_1_2 = (1 << REG15_RXIQ_VCM_SHIFT); /* 1.2V */ +constexpr uint16_t REG15_RXIQ_VCM_1_3 = (2 << REG15_RXIQ_VCM_SHIFT); /* 1.3V */ +constexpr uint16_t REG15_RXIQ_VCM_1_45 = (3 << REG15_RXIQ_VCM_SHIFT); /* 1.45V */ + +class MAX2831 : public MAX283x { + public: + constexpr MAX2831( + spi::arbiter::Target& target) + : _target(target) { + } + + void init() override; + void set_mode(const Mode mode) override; + + void set_tx_vga_gain(const int_fast8_t db) override; + void set_lna_gain(const int_fast8_t db) override; + void set_vga_gain(const int_fast8_t db) override; + void set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) override; + void set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) override; + + bool set_frequency(const rf::Frequency lo_frequency) override; + + void set_rx_LO_iq_phase_calibration(const size_t v) override; + void set_tx_LO_iq_phase_calibration(const size_t v) override; + + void set_rx_buff_vcm(const size_t v) override; + + int8_t temp_sense() override; + + reg_t read(const address_t reg_num) override; + void write(const address_t reg_num, const reg_t value) override; + + private: + spi::arbiter::Target& _target; + Mode _mode{Mode::Standby}; + std::array _regs{default_regs}; + uint16_t _regs_dirty{0xFFFF}; /* Track which registers need to be written */ + uint32_t _desired_lpf_bw{0}; /* Desired LPF bandwidth in Hz */ + + void write_reg(const uint8_t reg, const uint16_t value); + void set_reg_field(const uint8_t reg, const uint16_t mask, const uint16_t value); + uint16_t get_reg_field(const uint8_t reg, const uint16_t mask, const uint8_t shift); + void mark_dirty(const uint8_t reg); + void mark_clean(const uint8_t reg); + void flush_reg(const uint8_t reg); + void flush_dirty(); + uint32_t set_lpf_bandwidth_internal(const uint32_t bandwidth_hz); +}; + +} // namespace max2831 + +#endif /*__MAX2831_H__*/ diff --git a/firmware/application/hw/rffc507x.cpp b/firmware/application/hw/rffc507x.cpp index 448187054..a7793c398 100644 --- a/firmware/application/hw/rffc507x.cpp +++ b/firmware/application/hw/rffc507x.cpp @@ -31,6 +31,24 @@ using namespace hackrf::one; #include "hal.h" +#ifdef PRALINE +// Global debug tracking - visible to other files +struct rffc507x_debug_t { + uint32_t requested_freq_mhz; + uint32_t calculated_vco_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; + uint32_t calc_lo_freq_mhz; // Input to calculate() + uint32_t calc_vco_inside_mhz; // VCO calculated inside calculate() + uint8_t calc_lodiv_log2; // LO divider log2 + uint8_t calc_presc_log2; // Prescaler log2 + uint64_t calc_n_q24; // N in Q24 format before shift +}; +rffc507x_debug_t rffc507x_debug_info = {0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0}; +#endif + namespace rffc507x { /* Empirical tests indicate no minimum reset pulse width, but the speed @@ -47,7 +65,11 @@ constexpr halrtcnt_t ticks_during_reset = (base_m4_clk_f * seconds_during_reset constexpr float seconds_after_reset = 5.0e-6; constexpr halrtcnt_t ticks_after_reset = (base_m4_clk_f * seconds_after_reset + 1); +#ifdef PRALINE +constexpr rf::Frequency reference_frequency = 40000000ULL; +#else constexpr auto reference_frequency = rffc5072_reference_f; +#endif namespace vco { @@ -124,6 +146,15 @@ struct SynthConfig { const uint64_t prescaled_lo_q24 = vco_frequency << (24 - prescaler_divider_log2); const uint64_t n_divider_q24 = prescaled_lo_q24 / reference_frequency; +#ifdef PRALINE + // DEBUG: Track everything + rffc507x_debug_info.calc_lo_freq_mhz = lo_frequency / 1000000; + rffc507x_debug_info.calc_vco_inside_mhz = vco_frequency / 1000000; + rffc507x_debug_info.calc_lodiv_log2 = lo_divider_log2; + rffc507x_debug_info.calc_presc_log2 = prescaler_divider_log2; + rffc507x_debug_info.calc_n_q24 = n_divider_q24; +#endif + return { lo_divider_log2, prescaler_divider_log2, @@ -233,6 +264,21 @@ void RFFC507x::set_mixer_current(const uint8_t value) { void RFFC507x::set_frequency(const rf::Frequency lo_frequency) { const SynthConfig synth_config = SynthConfig::calculate(lo_frequency); +#ifdef PRALINE + + // Calculate VCO frequency from LO frequency and divider + const size_t lo_divider = 1U << synth_config.lo_divider_log2; // 2^lodiv_log2 + const rf::Frequency vco_freq = lo_frequency * lo_divider; + + // Track what we calculated + rffc507x_debug_info.requested_freq_mhz = lo_frequency / 1000000; + rffc507x_debug_info.calculated_vco_mhz = vco_freq / 1000000; + rffc507x_debug_info.expected_n = synth_config.n_divider_q24 >> 24; + rffc507x_debug_info.expected_lodiv = synth_config.lo_divider_log2; + rffc507x_debug_info.expected_presc = synth_config.prescaler_divider_log2; + rffc507x_debug_info.was_called = true; +#endif + /* Boost charge pump leakage if VCO frequency > 3.2GHz, indicated by * prescaler divider set to 4 (log2=2) instead of 2 (log2=1). */ diff --git a/firmware/application/hw/si5351.cpp b/firmware/application/hw/si5351.cpp index 236150aef..10c0aa8dd 100644 --- a/firmware/application/hw/si5351.cpp +++ b/firmware/application/hw/si5351.cpp @@ -57,7 +57,15 @@ void Si5351::reset() { write_register(Register::CrystalInternalLoadCapacitance, 0b11010010); write_register(Register::FanoutEnable, 0x00); +#ifndef PRALINE reset_plls(); +#endif + // NOTE: Do NOT call reset_plls() here! + // Multisynth registers are not yet configured at this point. + // Resetting PLLs with power-on default multisynth values (divider=6) + // causes Si5351 to output wrong frequencies (66.666 MHz instead of 8 MHz). + // The PLL reset should happen in init_clock_generator() AFTER multisynths + // are properly configured. This matches HackRF reference firmware. } Si5351::regvalue_t Si5351::read_register(const uint8_t reg) { @@ -93,7 +101,16 @@ void Si5351::set_ms_frequency( .r_div = r_div, }; const auto regs = ms.reg(ms_number); + +#ifdef PRALINE + /* PRALINE: Use single-byte writes - multi-byte I2C writes seem to fail */ + const uint8_t base_reg = regs[0]; + for (size_t i = 1; i < regs.size(); i++) { + write_register(base_reg + i - 1, regs[i]); + } +#else write(regs); +#endif } } /* namespace si5351 */ diff --git a/firmware/application/hw/si5351.hpp b/firmware/application/hw/si5351.hpp index 1768a8a2f..416f92852 100644 --- a/firmware/application/hw/si5351.hpp +++ b/firmware/application/hw/si5351.hpp @@ -361,9 +361,31 @@ class Si5351 { } void wait_for_device_ready() { +#ifdef PRALINE + // Add timeout to prevent infinite loop if I2C communication fails + // (e.g., on PRALINE hardware with different configuration) + uint32_t timeout = 100000; + while ((device_status() & 0x80) && (timeout > 0)) { + timeout--; + if (timeout == 0) { + chDbgAssert(false, "si5351: wait_for_device_ready timeout", nullptr); + } + } +#else while (device_status() & 0x80); +#endif } +#ifdef PRALINE + void set_clock_control_single_byte(const ClockControls& clock_control) { + _clock_control = clock_control; + // Use single-byte writes for PRALINE (multi-byte I2C fails) + for (size_t i = 0; i < 8; i++) { + write_register(Register::CLKControl_Base + i, _clock_control[i]); + } + } +#endif + bool plla_loss_of_signal() { return (device_status() >> 5) & 1; } @@ -377,10 +399,16 @@ class Si5351 { } void reset_plls() { +#ifdef PRALINE + // Reset both PLLA and PLLB. Use 0xA0 to match HackRF reference firmware. + // The low nibble bits are reserved/undefined in AN619. + write_register(Register::PLLReset, 0xa0); +#else // Datasheet recommends value 0xac, though the low nibble bits are not defined in AN619. write_register(Register::PLLReset, 0xac); - } +#endif + } regvalue_t read_register(const uint8_t reg); template @@ -397,6 +425,28 @@ class Si5351 { write(config.reg(ms_number)); } +#ifdef PRALINE + /* Write multisynth config using single-byte writes for debugging */ + + void write_ms_single_byte(const size_t ms_number, const MultisynthFractional& config) { + const auto regs = config.reg(ms_number); + // regs[0] is the base register address, regs[1-8] are the data bytes + const uint8_t base_reg = regs[0]; + for (size_t i = 1; i < regs.size(); i++) { + write_register(base_reg + i - 1, regs[i]); + } + } + + void write_pll_single_byte(const uint8_t pll_n, const PLL& pll_config) { + const auto regs = pll_config.reg(pll_n); + // regs[0] is base register address, regs[1-8] are data + const uint8_t base_reg = regs[0]; + for (size_t i = 1; i < regs.size(); i++) { + write_register(base_reg + i - 1, regs[i]); + } + } +#endif + void set_ms_frequency( const size_t ms_number, const uint32_t frequency, @@ -480,7 +530,6 @@ class Si5351 { }}); } }; - } // namespace si5351 #endif /*__SI5351_H__*/ diff --git a/firmware/application/hw/spi_arbiter.hpp b/firmware/application/hw/spi_arbiter.hpp index dd0c2a394..1e31c35dd 100644 --- a/firmware/application/hw/spi_arbiter.hpp +++ b/firmware/application/hw/spi_arbiter.hpp @@ -46,6 +46,14 @@ class Arbiter { _bus.transfer(data, count); } +#ifdef PRALINE + /* Invalidate cached config - forces reconfiguration on next transfer. + * Call this after directly manipulating SSP registers (e.g., FPGA access). */ + void invalidate() { + _config = nullptr; + } +#endif + private: SPI& _bus; const SPIConfig* _config; diff --git a/firmware/application/io_wave.hpp b/firmware/application/io_wave.hpp index 6d32454f8..2cbfa4109 100644 --- a/firmware/application/io_wave.hpp +++ b/firmware/application/io_wave.hpp @@ -81,7 +81,8 @@ struct header_t { struct tags_t { constexpr tags_t( const std::string& title_str) { - strcpy(title, title_str.c_str()); + strncpy(title, title_str.c_str(), sizeof(title) - 1); + title[sizeof(title) - 1] = '\0'; cksize = sizeof(tags_t) - 8; } diff --git a/firmware/application/irq_controls.cpp b/firmware/application/irq_controls.cpp index f9b53321a..df5bbed47 100644 --- a/firmware/application/irq_controls.cpp +++ b/firmware/application/irq_controls.cpp @@ -194,8 +194,12 @@ void timer0_callback(GPTDriver* const) { if (encoder_update(switches_raw) || encoder_read()) event_mask |= EVT_MASK_ENCODER; - /* Signal event loop */ + /* Signal event loop */ +#ifdef PRALINE + if (event_mask && thread_controls_event) { +#else if (event_mask) { +#endif chSysLockFromIsr(); chEvtSignalI(thread_controls_event, event_mask); chSysUnlockFromIsr(); @@ -227,8 +231,13 @@ void controls_init() { gptStart(&GPTD1, &timer0_config); gptStartContinuous(&GPTD1, timer0_match_count); +#ifdef PRALINE // Enable repeat for directional switches only for (auto i = Switch::Right; i <= Switch::Up; incr(i)) +#else + // Enable repeat for directional and Select switches only + for (auto i = Switch::Right; i <= Switch::Sel; incr(i)) +#endif switch_debounce[toUType(i)].enable_repeat(); } @@ -250,6 +259,22 @@ SwitchesState get_switches_state() { return result; } +/* Gets the repeat enabled state for all the switches. */ +SwitchesState get_switches_repeat_config() { + SwitchesState result; + + for (size_t i = 0; i < result.size(); i++) + result[i] = switch_debounce[i].get_repeat_enabled(); + + return result; +} + +/* Configures which switches support repeat.*/ +void set_switches_repeat_config(SwitchesState switch_config) { + for (size_t i = 0; i < switch_config.size(); i++) + switch_debounce[i].set_enable_repeat(switch_config[i]); +} + /* Gets the long press enabled state for all the switches. */ SwitchesState get_switches_long_press_config() { SwitchesState result; diff --git a/firmware/application/irq_controls.hpp b/firmware/application/irq_controls.hpp index cf0883c2b..6815b6db4 100644 --- a/firmware/application/irq_controls.hpp +++ b/firmware/application/irq_controls.hpp @@ -48,6 +48,8 @@ uint8_t swizzled_switches(); SwitchesState get_switches_state(); EncoderPosition get_encoder_position(); touch::Frame get_touch_frame(); +SwitchesState get_switches_repeat_config(); +void set_switches_repeat_config(SwitchesState switch_config); SwitchesState get_switches_long_press_config(); void set_switches_long_press_config(SwitchesState switch_config); diff --git a/firmware/application/irq_lcd_frame.cpp b/firmware/application/irq_lcd_frame.cpp index 86e11b6d8..689430558 100644 --- a/firmware/application/irq_lcd_frame.cpp +++ b/firmware/application/irq_lcd_frame.cpp @@ -52,10 +52,17 @@ extern "C" { CH_IRQ_HANDLER(PIN_INT4_IRQHandler) { CH_IRQ_PROLOGUE(); - +#ifdef PRALINE + if (thread_lcd_frame_event) { + chSysLockFromIsr(); + chEvtSignalI(thread_lcd_frame_event, EVT_MASK_LCD_FRAME_SYNC); + chSysUnlockFromIsr(); + } +#else chSysLockFromIsr(); chEvtSignalI(thread_lcd_frame_event, EVT_MASK_LCD_FRAME_SYNC); chSysUnlockFromIsr(); +#endif LPC_GPIO_INT->IST = (1U << 4); diff --git a/firmware/application/irq_rtc.cpp b/firmware/application/irq_rtc.cpp index d48ea065a..5fa44b090 100644 --- a/firmware/application/irq_rtc.cpp +++ b/firmware/application/irq_rtc.cpp @@ -40,10 +40,17 @@ extern "C" { CH_IRQ_HANDLER(RTC_IRQHandler) { CH_IRQ_PROLOGUE(); - +#ifdef PRALINE + if (thread_rtc_event) { + chSysLockFromIsr(); + chEvtSignalI(thread_rtc_event, EVT_MASK_RTC_TICK); + chSysUnlockFromIsr(); + } +#else chSysLockFromIsr(); chEvtSignalI(thread_rtc_event, EVT_MASK_RTC_TICK); chSysUnlockFromIsr(); +#endif rtc::interrupt::clear_all(); diff --git a/firmware/application/keeloq_common.cpp b/firmware/application/keeloq_common.cpp new file mode 100644 index 000000000..28c487cd7 --- /dev/null +++ b/firmware/application/keeloq_common.cpp @@ -0,0 +1,36 @@ +#include "keeloq_common.hpp" + +uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key) { + uint32_t x = data, r; + + for (r = 0; r < 528; r++) + x = (x >> 1) ^ ((bit(x, 0) ^ bit(x, 16) ^ (uint32_t)bit(key, r & 63) ^ + bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31))) + << 31); + + return x; +} + +uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key) { + uint32_t x = data, r; + + for (r = 0; r < 528; r++) + x = (x << 1) ^ bit(x, 31) ^ bit(x, 15) ^ (uint32_t)bit(key, (15 - r) & 63) ^ + bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); + + return x; +} + +uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key) { + uint32_t k1, k2; + + data &= 0x0FFFFFFF; + data |= 0x20000000; + k1 = keeloq_decrypt(data, key); + + data &= 0x0FFFFFFF; + data |= 0x60000000; + k2 = keeloq_decrypt(data, key); + + return ((uint64_t)k2 << 32) | k1; +} diff --git a/firmware/application/keeloq_common.hpp b/firmware/application/keeloq_common.hpp new file mode 100644 index 000000000..04db42ed1 --- /dev/null +++ b/firmware/application/keeloq_common.hpp @@ -0,0 +1,16 @@ +#ifndef __KEELOQ_COMMON__ +#define __KEELOQ_COMMON__ + +#include + +#define bit(x, n) (((x) >> (n)) & 1) +#define g5(x, a, b, c, d, e) \ + (bit(x, a) + bit(x, b) * 2 + bit(x, c) * 4 + bit(x, d) * 8 + bit(x, e) * 16) + +#define KEELOQ_NLF 0x3A5C742E + +uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key); +uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key); +uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key); + +#endif diff --git a/firmware/application/keeloq_file.cpp b/firmware/application/keeloq_file.cpp new file mode 100644 index 000000000..0000643f9 --- /dev/null +++ b/firmware/application/keeloq_file.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "keeloq_file.hpp" + +namespace fs = std::filesystem; + +bool read_keeloq_file(const fs::path& path, KeeloqData& data) { + File file{}; + + auto result = file.open(path); + + if (result) { + return false; + } + + FileLineReader reader{file}; + + std::string raw = *reader.begin(); + auto chunks = split_string(raw, ';'); + + if (chunks.size() != 4) { + return false; + } + + data.mf_name = std::string{chunks[0]}; + data.serial = std::strtoul(chunks[1].data(), NULL, 16); + data.counter = (uint16_t)std::atoi(chunks[2].data()); + data.btn = (uint8_t)std::atoi(chunks[3].data()); + + return true; +} + +bool write_keeloq_file(const fs::path& path, const KeeloqData& data) { + delete_file(path); + + File file{}; + + auto result = file.open(path, false, true); + + if (result) { + return false; + } + + std::string formatted = data.mf_name + ";" + + to_string_hex(data.serial) + ";" + + to_string_dec_uint(data.counter) + ";" + + to_string_dec_uint(data.btn); + + file.write_line(formatted); + file.close(); + + return true; +} diff --git a/firmware/application/keeloq_file.hpp b/firmware/application/keeloq_file.hpp new file mode 100644 index 000000000..ca0ff1b42 --- /dev/null +++ b/firmware/application/keeloq_file.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __KEELOQ_FILE__ +#define __KEELOQ_FILE__ + +#include "portapack.hpp" +#include "file_reader.hpp" +#include "string_format.hpp" +#include + +struct KeeloqData { + std::string mf_name{}; + uint32_t serial = 0; + uint16_t counter = 0; + uint8_t btn = 0; +}; + +bool read_keeloq_file(const std::filesystem::path&, KeeloqData&); +bool write_keeloq_file(const std::filesystem::path&, const KeeloqData&); + +#endif diff --git a/firmware/application/keeloq_keystore.cpp b/firmware/application/keeloq_keystore.cpp new file mode 100644 index 000000000..f57839de9 --- /dev/null +++ b/firmware/application/keeloq_keystore.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "keeloq_keystore.hpp" + +KeeloqKeystore::KeeloqKeystore() { + ensure_directory(keeloq_keys_dir); + + File keeloq_keys_file{}; + + if (keeloq_keys_file.open(keeloq_keys_dir / "MFCODES")) { + return; + } + + FileLineReader reader{keeloq_keys_file}; + + for (const std::string& line : reader) { + auto cols = split_string(line, ';'); + + if (cols.size() != 3) { + return; + } + + KeeloqKey key{ + std::string{cols[0]}, + std::strtoull(cols[1].data(), NULL, 16), + (uint8_t)std::atoi(cols[2].data())}; + + keys.push_back(key); + } +} + +const std::vector& KeeloqKeystore::get_keys() { + return keys; +} diff --git a/firmware/application/keeloq_keystore.hpp b/firmware/application/keeloq_keystore.hpp new file mode 100644 index 000000000..777e77e1a --- /dev/null +++ b/firmware/application/keeloq_keystore.hpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __KEELOQ_KEYSTORE__ +#define __KEELOQ_KEYSTORE__ + +#include "file_reader.hpp" +#include "file_path.hpp" +#include "string_format.hpp" +#include + +#define KEELOQ_SIMPLE_LEARNING 1 +#define KEELOQ_NORMAL_LEARNING 2 + +struct KeeloqKey { + std::string mf_name{}; + uint64_t key = 0; + uint8_t type = 0; +}; + +class KeeloqKeystore { + public: + KeeloqKeystore(); + + const std::vector& get_keys(); + + private: + std::vector keys{}; +}; + +#endif diff --git a/firmware/application/log_file.cpp b/firmware/application/log_file.cpp index 105fd7bad..8f915dc6e 100644 --- a/firmware/application/log_file.cpp +++ b/firmware/application/log_file.cpp @@ -38,3 +38,11 @@ Optional LogFile::write_raw(const std::string& message) { } return error; } + +Optional LogFile::write_raw_no_newline(const std::string& message) { + auto result_s = file.write(message.c_str(), message.size()); + if (!result_s.is_error()) { + file.sync(); + } + return {result_s.error()}; +} diff --git a/firmware/application/log_file.hpp b/firmware/application/log_file.hpp index 423552e38..b5e963318 100644 --- a/firmware/application/log_file.hpp +++ b/firmware/application/log_file.hpp @@ -40,6 +40,7 @@ class LogFile { Optional write_entry(const std::string& entry); Optional write_entry(const rtc::RTC& datetime, const std::string& entry); Optional write_raw(const std::string& message); + Optional write_raw_no_newline(const std::string& message); private: File file{}; diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp old mode 100755 new mode 100644 index c1ca5b5c5..ef2eb9435 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -91,7 +91,7 @@ Continuous (Fox-oring) // TODO: ADS-B draw trajectory + GPS coordinates + scale, and playback // TODO: RDS multiple groups (sequence) // TODO: Use ModalMessageView confirmation for TX ? -// TODO: Use msgpack for settings, lists... on sd card +// TODO: Use msgpack for settings, lists... on sd card. and make it safe, since now it has a lot of oob potentials // Multimon-style stuff: // TODO: DMR detector @@ -145,6 +145,21 @@ Continuous (Fox-oring) rffc507x::RFFC507x first_if; ui::SystemView* system_view_ptr; +static uint32_t random_seed_state = 123456789; +extern "C" int rand(void) { + random_seed_state ^= random_seed_state << 13; + random_seed_state ^= random_seed_state >> 17; + random_seed_state ^= random_seed_state << 5; + return (int)(random_seed_state & 0x7FFFFFFF); +} +extern "C" void srand(unsigned int seed) { + if (seed == 0) { + random_seed_state = 123456789; + } else { + random_seed_state = seed; + } +} + static void event_loop() { static ui::Context context; static ui::SystemView system_view{ @@ -166,7 +181,9 @@ static void event_loop() { } int main(void) { +#ifndef PRALINE // Do not perform quick set up of GP01_RFF507X = 1 for PRALINE first_if.init(); /* To avoid initial short Ant_DC_Bias pulse ,we need quick set up GP01_RFF507X =1 */ +#endif if (config_mode_should_enter()) { config_mode_clear(); @@ -187,7 +204,7 @@ int main(void) { lcd_frame_sync_configure(); rtc_interrupt_enable(); - Theme::SetTheme((Theme::ThemeId)portapack::persistent_memory::ui_theme_id()); // load theme + Theme::SetTheme((Theme::ThemeId)portapack::persistent_memory::ui_theme_id()); event_loop(); diff --git a/firmware/application/portapack.cpp b/firmware/application/portapack.cpp index 7ec63ac0c..67f4faf8d 100644 --- a/firmware/application/portapack.cpp +++ b/firmware/application/portapack.cpp @@ -56,6 +56,14 @@ using asahi_kasei::ak4951::AK4951; #include "i2cdevmanager.hpp" #include "battery.hpp" +extern "C" { +#include "platform_detect.h" + +#ifdef PRALINE +#include "fpga_bridge.h" +#endif +} + namespace portapack { const char* init_error = nullptr; @@ -337,14 +345,37 @@ static void set_cpu_clock_speed() { */ /* Step into the 90-110MHz M4 clock range */ +#ifdef PRALINE + /* PRALINE: Enable and use 12MHz XTAL directly (no GP_CLKIN from Si5351) */ + + /* Step 1: Enable the crystal oscillator */ + LPC_CGU->XTAL_OSC_CTRL.ENABLE = 0; // 0 = enable (active low) + LPC_CGU->XTAL_OSC_CTRL.HF = 0; // 0 = low frequency mode (1-20MHz) + + /* Step 2: Wait for oscillator to stabilize (~250us at IRC speed) */ + volatile uint32_t delay = 3000; // ~250us at 12MHz IRC + while (delay--); + + /* Step 3: Configure PLL1 from XTAL + * Fclkin = 12M, /N=1 = 12M, Fcco = 12M * 17 = 204M + * Fclk = Fcco / (2*(P=1)) = 102M + */ + cgu::pll1::ctrl({ + .pd = 1, + .bypass = 0, + .fbsel = 0, + .direct = 0, + .psel = 0, + .autoblock = 1, + .nsel = 0, // N = 1 + .msel = 16, // M = 17, so 12MHz * 17 = 204MHz + .clk_sel = cgu::CLK_SEL::XTAL, + }); +#else /* OG: - * Fclkin = 40M - * /N=2 = 20M = PFDin - * Fcco = PFDin * (M=10) = 200M + * Fclkin = 40M, /N=2 = 20M, Fcco = 20M * 10 = 200M * r9: - * Fclkin = 10M - * /N=1 = 10M = PFDin - * Fcco = PFDin * (M=20) = 200M + * Fclkin = 10M, /N=1 = 10M, Fcco = 10M * 20 = 200M * Fclk = Fcco / (2*(P=1)) = 100M */ cgu::pll1::ctrl({ @@ -358,20 +389,43 @@ static void set_cpu_clock_speed() { .msel = hackrf_r9 ? 19UL : 9UL, .clk_sel = cgu::CLK_SEL::GP_CLKIN, }); +#endif cgu::pll1::enable(); + +#ifdef PRALINE + // Wait for PLL1 to lock with timeout + { + uint32_t timeout = 100000; + while (!cgu::pll1::is_locked() && timeout > 0) { + timeout--; + } + } + + if (cgu::pll1::is_locked()) { + set_clock_config(clock_config_pll1_step); + + /* Delay >50us at 90-110MHz clock speed */ + volatile uint32_t delay = 1400; + while (delay--); + set_clock_config(clock_config_pll1); + + /* Remove /2P divider from PLL1 output to achieve full speed */ + cgu::pll1::direct(); + } +#else while (!cgu::pll1::is_locked()); set_clock_config(clock_config_pll1_step); - /* Delay >50us at 90-110MHz clock speed */ volatile uint32_t delay = 1400; while (delay--); - set_clock_config(clock_config_pll1); /* Remove /2P divider from PLL1 output to achieve full speed */ cgu::pll1::direct(); + +#endif } static void draw_splash_screen_icon(int16_t n, const ui::Bitmap& bitmap) { @@ -499,12 +553,23 @@ static void initialize_boot_splash_screen() { */ init_status_t init() { +#ifdef PRALINE + /* 1. HOLD FPGA IN RESET (Active Low) */ + // P5_2 is GPIO2[11] (FPGA CRESET) + palClearPad(GPIO2, 11); +#endif + set_idivc_base_clocks(cgu::CLK_SEL::IDIVC); i2c0.start(i2c_config_boot_clock); chThdSleepMilliseconds(100); + detect_hardware_platform(); + finalize_detect_hardware_platform(); + + chThdSleepMilliseconds(100); + configure_pins_portapack(); portapack::io.init(); @@ -534,11 +599,11 @@ init_status_t init() { cgu::pll1::disable(); set_cpu_clock_speed(); - /* sample max: 1023 sample_t AKA uint16_t * touch_sensitivity: range: 1 to 128 * threshold range: 1023/1 to 1023/128 = 1023 to 8 */ + touch_threshold = portapack::persistent_memory::touchscreen_threshold(); if (lcd_fast_setup) @@ -561,8 +626,31 @@ init_status_t init() { chThdSleepMilliseconds(10); clock_manager.set_reference_ppb(persistent_memory::correction_ppb()); + clock_manager.enable_if_clocks(); clock_manager.enable_codec_clocks(); + +#ifdef PRALINE + chThdSleepMilliseconds(20); + + // This function returns LD_SUCCESS (0) if the FPGA confirms the bitstream + // Call fpga_bridge_init and continue boot regardless of result + // (Watchdog was resetting device when we halted with while(1)) + int load_result = fpga_bridge_init(); + (void)load_result; // Ignore result for now, just let boot continue + + /* RELEASE FPGA RESET */ + // FPGA wakes up and latches the stable 40MHz CLK1 + // P5_2 is GPIO2[11] (FPGA CRESET) + palSetPad(GPIO2, 11); + + // Allow FPGA and related logic a brief stabilization period without busy-waiting + chThdSleepMilliseconds(10); + + // Keep LEDs off after FPGA load + LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8); +#endif + radio::init(); sdcStart(&SDCD1, nullptr); @@ -574,12 +662,16 @@ init_status_t init() { draw_splash_screen_icon(2, ui::bitmap_icon_sd); init_status_t return_code = init_status_t::INIT_SUCCESS; + +#ifndef PRALINE + // HackRF One uses CPLD - load it via JTAG if (!hackrf::cpld::load_sram()) { if (lcd_fast_setup) chDbgPanic("HACKRF CPLD FAILED"); return_code = init_status_t::INIT_HACKRF_CPLD_FAILED; } +#endif if (lcd_fast_setup) draw_splash_screen_icon(3, ui::bitmap_icon_hackrf); @@ -592,6 +684,7 @@ init_status_t init() { chThdSleepMilliseconds(10); audio::init(portapack_audio_codec()); + battery::BatteryManagement::set_calc_override(persistent_memory::ui_override_batt_calc()); i2cdev::I2CDevManager::init(); diff --git a/firmware/application/protocols/aprs.cpp b/firmware/application/protocols/aprs.cpp index 4317e44cb..27bdd0e01 100644 --- a/firmware/application/protocols/aprs.cpp +++ b/firmware/application/protocols/aprs.cpp @@ -31,20 +31,67 @@ using namespace ax25; namespace aprs { -void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload) { +void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload, const std::string& path) { AX25Frame frame; char address[14] = {0}; + memset(address, ' ', 14); - memcpy(&address[0], dest_address, 6); - memcpy(&address[7], src_address, 6); + size_t dest_len = strlen(dest_address); + if (dest_len > 6) dest_len = 6; + memcpy(&address[0], dest_address, dest_len); + size_t src_len = strlen(src_address); + if (src_len > 6) src_len = 6; + memcpy(&address[7], src_address, src_len); // euquiq: According to ax.25 doc section 2.2.13.x.x and 2.4.1.2 - // SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number) - // Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end) - address[6] = (dest_ssid | 48); - address[13] = (src_ssid | 48); + // SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number) + // Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end) + address[6] = (dest_ssid | 0x30); + address[13] = (src_ssid | 0x30); - frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload); + // fix path ssid bits. the result will be the same as above + std::string fixed_path = ""; + + const char* p = path.c_str(); + + while (*p) { + while (*p == ' ') p++; + if (!*p) break; + + char call[6] = {' ', ' ', ' ', ' ', ' ', ' '}; + int idx = 0; + + while (*p && *p != '-' && *p != ',') { + if (*p != ' ') { + if (idx < 6) { + char c = *p; + if (c >= 'a' && c <= 'z') c -= 32; + call[idx++] = c; + } + } + p++; + } + + int ssid = 0; + if (*p == '-') { + p++; + while (*p == ' ') p++; + while (*p >= '0' && *p <= '9') { + ssid = ssid * 10 + (*p - '0'); + p++; + } + } + + if (ssid >= 0 && ssid <= 15) { + for (int i = 0; i < 6; i++) fixed_path += call[i]; + fixed_path += (char)(ssid | 0x30); + } + + while (*p && *p != ',') p++; + if (*p == ',') p++; + } + + frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload, fixed_path); } } /* namespace aprs */ diff --git a/firmware/application/protocols/aprs.hpp b/firmware/application/protocols/aprs.hpp index 25d7a973a..dcca44a08 100644 --- a/firmware/application/protocols/aprs.hpp +++ b/firmware/application/protocols/aprs.hpp @@ -33,7 +33,8 @@ void make_aprs_frame( const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, - const std::string& payload); + const std::string& payload, + const std::string& path = ""); } /* namespace aprs */ diff --git a/firmware/application/protocols/ax25.cpp b/firmware/application/protocols/ax25.cpp index a9bbfb07c..4a47e29ec 100644 --- a/firmware/application/protocols/ax25.cpp +++ b/firmware/application/protocols/ax25.cpp @@ -26,13 +26,16 @@ namespace ax25 { -void AX25Frame::make_extended_field(char* const data, size_t length) { +void AX25Frame::make_extended_field(char* const data, size_t length, bool is_last) { size_t i = 0; for (i = 0; i < length - 1; i++) add_data(data[i] << 1); - add_data((data[i] << 1) | 1); + if (is_last) + add_data((data[i] << 1) | 1); + else + add_data((data[i] << 1)); } void AX25Frame::NRZI_add_bit(const uint32_t bit) { @@ -92,7 +95,7 @@ void AX25Frame::add_checksum() { add_byte(checksum >> 8, false, false); } -void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info) { +void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path) { size_t i; bb_data_ptr = (uint16_t*)shared_memory.bb_data.data; @@ -103,17 +106,47 @@ void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const ones_counter = 0; crc_ccitt.reset(); + add_flag(); // 0x73 + add_flag(); // 0x73 + add_flag(); // 0x73 + add_flag(); // 0x73 + + // path must not contain the - character! just the callsign and SSID next to each other + bool has_path = (path.size() > 0 && path.size() <= 63 && path.size() % 7 == 0); + make_extended_field(address, 14, !has_path); + if (has_path) { + make_extended_field(const_cast(path.data()), path.size(), true); + } + add_data(control); // 0x03 + add_data(protocol); // 0xf0 + + for (i = 0; i < info.size(); i++) + add_data(info[i]); + + add_checksum(); // fcs + + add_flag(); // 0x73 + add_flag(); // 0x73 + + flush(); +} + +void AX25Frame::make_frame_from_raw(const uint8_t* data, size_t len) { + bb_data_ptr = (uint16_t*)shared_memory.bb_data.data; + memset(bb_data_ptr, 0, sizeof(shared_memory.bb_data.data)); + bit_counter = 0; + current_bit = 0; + current_byte = 0; + ones_counter = 0; + crc_ccitt.reset(); + add_flag(); add_flag(); add_flag(); add_flag(); - make_extended_field(address, 14); - add_data(control); - add_data(protocol); - - for (i = 0; i < info.size(); i++) - add_data(info[i]); + for (size_t i = 0; i < len; i++) + add_data(data[i]); add_checksum(); diff --git a/firmware/application/protocols/ax25.hpp b/firmware/application/protocols/ax25.hpp index 4b02e5934..32bd4ab5c 100644 --- a/firmware/application/protocols/ax25.hpp +++ b/firmware/application/protocols/ax25.hpp @@ -43,11 +43,12 @@ enum protocol_id_t { class AX25Frame { public: - void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info); + void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path = ""); + void make_frame_from_raw(const uint8_t* data, size_t len); private: void NRZI_add_bit(const uint32_t bit); - void make_extended_field(char* const data, size_t length); + void make_extended_field(char* const data, size_t length, bool is_last = false); void add_byte(uint8_t byte, bool is_flag, bool is_data); void add_data(uint8_t byte); void add_checksum(); diff --git a/firmware/application/radio.cpp b/firmware/application/radio.cpp index 1e00c5cdb..dae757f51 100644 --- a/firmware/application/radio.cpp +++ b/firmware/application/radio.cpp @@ -24,10 +24,19 @@ #include "rf_path.hpp" #include "rffc507x.hpp" + +#ifdef PRALINE +#include "max2831.hpp" +extern "C" { +#include "fpga_bridge.h" +} +#else #include "max2837.hpp" #include "max2839.hpp" -#include "max5864.hpp" #include "baseband_cpld.hpp" +#endif + +#include "max5864.hpp" #include "tuning.hpp" @@ -42,6 +51,10 @@ using namespace hackrf::one; #include "portapack.hpp" #include "portapack_persistent_memory.hpp" +#include "hal.h" // For LPC_SGPIO + +#include + /* Direct access to the radio. Setting values incorrectly can damage * the device. Applications should use ReceiverModel or TransmitterModel * instead of calling these functions directly. */ @@ -56,6 +69,27 @@ static constexpr uint32_t ssp_scr( return static_cast(pclk_f / cpsr / spi_f - 1); } +#ifdef PRALINE +/* MAX2831 uses 9-bit SPI transfers */ +static constexpr SPIConfig ssp_config_max283x = { + .end_cb = NULL, + .ssport = gpio_max283x_select.port(), + .sspad = gpio_max283x_select.pad(), + .cr0 = + CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS9BIT, + .cpsr = ssp1_cpsr, +}; + +static max283x::MAX283x* transceiver = nullptr; + +void set_rx_buff_vcm(const size_t v) { + if (transceiver) { + transceiver->set_rx_buff_vcm(v); + } +} + +#else +/* MAX2837/MAX2839 use 16-bit SPI transfers */ static constexpr SPIConfig ssp_config_max283x = { .end_cb = NULL, .ssport = gpio_max283x_select.port(), @@ -64,6 +98,7 @@ static constexpr SPIConfig ssp_config_max283x = { CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS16BIT, .cpsr = ssp1_cpsr, }; +#endif static constexpr SPIConfig ssp_config_max5864 = { .end_cb = NULL, @@ -87,29 +122,87 @@ static spi::arbiter::Target ssp1_target_max5864{ static rf::path::Path rf_path; rffc507x::RFFC507x first_if; max283x::MAX283x* second_if; +#ifdef PRALINE +max2831::MAX2831 second_if_max2831{ssp1_target_max283x}; +#else max2837::MAX2837 second_if_max2837{ssp1_target_max283x}; max2839::MAX2839 second_if_max2839{ssp1_target_max283x}; -static max5864::MAX5864 baseband_codec{ssp1_target_max5864}; static baseband::CPLD baseband_cpld; +#endif +static max5864::MAX5864 baseband_codec{ssp1_target_max5864}; // load_sram() is called at boot in portapack.cpp, including verify CPLD part, so default direction is Receive static rf::Direction direction{rf::Direction::Receive}; static bool baseband_invert = false; static bool mixer_invert = false; +#ifdef PRALINE +static rf::Direction cached_direction = rf::Direction::Receive; +static bool cached_rf_amp = false; +static int_fast8_t cached_lna_gain = 0; +static int_fast8_t cached_vga_gain = 0; +#endif + void init() { +#ifdef PRALINE + /* PRALINE uses MAX2831 transceiver */ + second_if = (max283x::MAX283x*)&second_if_max2831; +#else if (hackrf_r9) { gpio_r9_not_ant_pwr.write(1); gpio_r9_not_ant_pwr.output(); } - rf_path.init(); - first_if.init(); second_if = hackrf_r9 ? (max283x::MAX283x*)&second_if_max2839 : (max283x::MAX283x*)&second_if_max2837; +#endif + + rf_path.init(); + first_if.init(); second_if->init(); baseband_codec.init(); + +#ifdef PRALINE + + /* Praline-Specific Bus and Gateware Configuration */ + + // SYNC SGPIO TO FPGA CLOCK: + // Configure all 16 SGPIO slices to use the external clock (SGPIO8) + // provided by the FPGA. This allows the MCU to stay at 40MHz + // while the data bus scales to the RF sample rate. + // Bit 2:1 of SGPIO_MUX_CFG = 01 (External clock from SGPIO8) + // SYNC SGPIO TO FPGA CLOCK WITH FALLING EDGE LATCH + for (int i = 0; i < 16; i++) { + // (1 << 1) = External clock from SGPIO8 + // (1 << 3) = Sample on the FALLING edge of the clock + LPC_SGPIO->SGPIO_MUX_CFG[i] = (1 << 1) | (1 << 3); + } + + /* Initialize FPGA registers - DC_BLOCK must be enabled for RX */ + // debug::fpga::init(); + + fpga_set_mode(FPGA_MODE_RX); + + // These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation. + fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0 + fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim + + // RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift). + fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN); + + /* RX Mode: Initialize DC Block parameters to standard Praline values. + * 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability. + */ + fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH); + fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE); + + ssp1_arbiter.invalidate(); + chThdSleepMilliseconds(10); // Let FPGA registers settle +#else + /* HackRF One uses CPLD for Q inversion control. + * PRALINE uses FPGA and the pin (P2_3) is used for LCD_TE on H4M. */ baseband_cpld.init(); +#endif } void set_direction(const rf::Direction new_direction) { @@ -120,6 +213,28 @@ void set_direction(const rf::Direction new_direction) { // Deleting that load_sram_no_verify() (or the original , load_sram() ), solves random TX swap I/Q problem in H1R1 , others OK- (and no side effects to all). // hackrf::cpld::load_sram_no_verify(); // After commit "removed the use of the hackrf cpld eeprom #1732", in a H1R1, Mic App wrong SSB TX with random USB/LSB change. +#ifdef PRALINE + cached_direction = new_direction; // Track state for debug and potentially other purposes. + + if (new_direction == rf::Direction::Transmit) { + fpga_set_mode(FPGA_MODE_TX); + // TX Mode: Clear RX gain and ensure NCO is off initially + fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00); + // Placeholder: Set TX-specific interpolation and phase + fpga_debug_register_write(FPGA_REG_TX_INTERP, 0x00); + fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00); + } else { + fpga_set_mode(FPGA_MODE_RX); + // RX Mode: Ensure NCO is disabled and reset digital gain + fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN); + /* RX Mode: Initialize DC Block parameters to standard Praline values. + * 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability. + */ + fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH); + fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE); + } +#endif + direction = new_direction; if (hackrf_r9) { @@ -143,7 +258,21 @@ void set_direction(const rf::Direction new_direction) { */ baseband_invert = false; } + +#ifdef PRALINE + + // Q inversion controlled by GPIO0[13] (SGPIO12), not FPGA register + bool q_invert = mixer_invert ^ baseband_invert; + if (q_invert) { + LPC_GPIO->SET[0] = (1 << 13); // SGPIO12 = 1 (Q inverted) + } else { + LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal) + } + + ssp1_arbiter.invalidate(); +#else baseband_cpld.set_invert(mixer_invert ^ baseband_invert); +#endif second_if->set_mode((direction == rf::Direction::Transmit) ? max283x::Mode::Transmit : max283x::Mode::Receive); rf_path.set_direction(direction); @@ -189,6 +318,10 @@ bool set_tuning_frequency(const rf::Frequency frequency) { if (tuning_config.first_lo_frequency) { first_if.set_frequency(tuning_config.first_lo_frequency); first_if.enable(); +#ifdef PRALINE + first_if.flush(); // Force register write with reference clock present + chThdSleepMilliseconds(10); // Allow PLL to settle +#endif } // Program second local oscillator frequency into MAX283x @@ -196,7 +329,21 @@ bool set_tuning_frequency(const rf::Frequency frequency) { rf_path.set_band(tuning_config.rf_path_band); mixer_invert = tuning_config.mixer_invert; + +#ifdef PRALINE + + // Q inversion controlled by GPIO0[13] (SGPIO12), not FPGA register + bool q_invert = mixer_invert ^ baseband_invert; + if (q_invert) { + LPC_GPIO->SET[0] = (1 << 13); // SGPIO12 = 1 (Q inverted) + } else { + LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal) + } + + ssp1_arbiter.invalidate(); +#else baseband_cpld.set_invert(mixer_invert ^ baseband_invert); +#endif return result_second_if; } else { @@ -205,21 +352,23 @@ bool set_tuning_frequency(const rf::Frequency frequency) { } void set_rf_amp(const bool rf_amp) { +#ifdef PRALINE + cached_rf_amp = rf_amp; // Track state for debug and potentialy other purposes. +#endif rf_path.set_rf_amp(rf_amp); - - if (direction == rf::Direction::Transmit) { - if (rf_amp) - led_tx.on(); - else - led_tx.off(); - } } void set_lna_gain(const int_fast8_t db) { +#ifdef PRALINE + cached_lna_gain = db; // Track state for debug and potentially other purposes. +#endif second_if->set_lna_gain(db); } void set_vga_gain(const int_fast8_t db) { +#ifdef PRALINE + cached_vga_gain = db; // Track state for debug and potentially other purposes. +#endif second_if->set_vga_gain(db); } @@ -242,11 +391,17 @@ void set_baseband_rate(const uint32_t rate) { void set_antenna_bias(const bool on) { /* Pull MOSFET gate low to turn on antenna bias. */ +#ifdef PRALINE + // Praline: P2_12 = GPIO1[12], ANT_BIAS_EN_N (active LOW) + LPC_GPIO->CLR[1] = on ? (1 << 12) : 0; + LPC_GPIO->SET[1] = on ? 0 : (1 << 12); +#else if (hackrf_r9) { gpio_r9_not_ant_pwr.write(on ? 0 : 1); } else { first_if.set_gpo1(on ? 0 : 1); } +#endif } void set_tx_max283x_iq_phase_calibration(const size_t v) { @@ -257,24 +412,14 @@ void set_rx_max283x_iq_phase_calibration(const size_t v) { second_if->set_rx_LO_iq_phase_calibration(v); } -/*void enable(Configuration configuration) { - configure(configuration); -} - -void configure(Configuration configuration) { - set_tuning_frequency(configuration.tuning_frequency); - set_rf_amp(configuration.rf_amp); - set_lna_gain(configuration.lna_gain); - set_vga_gain(configuration.vga_gain); - set_baseband_rate(configuration.baseband_rate); - set_baseband_filter_bandwidth(configuration.baseband_filter_bandwidth); - set_direction(configuration.direction); -}*/ - void disable() { set_antenna_bias(false); baseband_codec.set_mode(max5864::Mode::Shutdown); +#ifdef PRALINE + second_if->set_mode(max283x::Mode::Standby); +#else second_if->set_mode(max2837::Mode::Standby); +#endif first_if.disable(); set_rf_amp(false); @@ -282,8 +427,32 @@ void disable() { led_tx.off(); } +#ifdef PRALINE +void invalidate_spi_config() { + ssp1_arbiter.invalidate(); +} +#endif + namespace debug { +#ifdef PRALINE +rf::Direction get_cached_direction() { + return cached_direction; +} + +bool get_cached_rf_amp() { + return cached_rf_amp; +} + +int_fast8_t get_cached_lna_gain() { + return cached_lna_gain; +} + +int_fast8_t get_cached_vga_gain() { + return cached_vga_gain; +} +#endif + namespace first_if { uint32_t register_read(const size_t register_number) { @@ -294,6 +463,48 @@ void register_write(const size_t register_number, uint32_t value) { radio::first_if.write(register_number, value); } +#ifdef PRALINE +extern "C" { +extern struct rffc507x_debug_t { + uint32_t requested_freq_mhz; + uint32_t calculated_vco_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; + uint32_t calc_lo_freq_mhz; + uint32_t calc_vco_inside_mhz; + uint8_t calc_lodiv_log2; + uint8_t calc_presc_log2; + uint64_t calc_n_q24; +} rffc507x_debug_info; +} + +/*struct TuningInfo { + uint32_t requested_freq_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; +};*/ + +TuningInfo get_tuning_info() { + return { + rffc507x_debug_info.requested_freq_mhz, + rffc507x_debug_info.calculated_vco_mhz, + rffc507x_debug_info.expected_n, + rffc507x_debug_info.expected_lodiv, + rffc507x_debug_info.expected_presc, + rffc507x_debug_info.was_called, + rffc507x_debug_info.calc_lo_freq_mhz, + rffc507x_debug_info.calc_vco_inside_mhz, + rffc507x_debug_info.calc_lodiv_log2, + rffc507x_debug_info.calc_presc_log2, + rffc507x_debug_info.calc_n_q24, + }; +} +#endif + } /* namespace first_if */ namespace second_if { @@ -312,6 +523,87 @@ int8_t temp_sense() { } /* namespace second_if */ +namespace rf_path_info { +rf::path::Band get_current_band() { + return radio::rf_path.get_band(); +} +} /* namespace rf_path_info */ + +#ifdef PRALINE +namespace fpga { + +/* Use fpga_bridge.c functions for FPGA register access. + * These properly switch SPI mode between iCE40 (Mode 3, 8-bit) + * and MAX2831 (Mode 0, 9-bit). After each access, we must + * invalidate the SPI arbiter's cached config since fpga_bridge.c + * modifies SSP1 registers directly. */ + +uint32_t register_read(const size_t register_number) { + uint32_t result = fpga_debug_register_read(static_cast(register_number)); + ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer + return result; +} + +void register_write(const size_t register_number, uint32_t value) { + fpga_debug_register_write(static_cast(register_number), static_cast(value)); + ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer +} + +void init() { + fpga_set_mode(FPGA_MODE_RX); + + // These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation. + fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0 + fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim + + // RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift). + fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN); + + /* RX Mode: Initialize DC Block parameters to standard Praline values. + * 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability. + */ + fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH); + fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE); + + ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer +} + +} /* namespace fpga */ +#endif + +namespace sgpio { + +/* SGPIO register map for debug viewing + * We expose key registers for diagnosing data flow issues. + * Register numbers map to: + * 0: CTRL_ENABLE - Which slices are enabled + * 1: GPIO_INREG - GPIO input register (data pins state) + * 2: GPIO_OUTREG - GPIO output register (direction, disable, etc) + * 3: GPIO_OENREG - GPIO output enable register + * 4: STATUS_1 - Exchange interrupt status (slice A = bit 0) + * 5: REG_SS[0] - Shadow register slice A (current sample data) + */ +uint32_t register_read(const size_t register_number) { + switch (register_number) { + case 0: + return LPC_SGPIO->CTRL_ENABLE; + case 1: + return LPC_SGPIO->GPIO_INREG; + case 2: + return LPC_SGPIO->GPIO_OUTREG; + case 3: + return LPC_SGPIO->GPIO_OENREG; + case 4: + return LPC_SGPIO->STATUS_1; + case 5: + return LPC_SGPIO->REG_SS[0]; + default: + return 0xFFFFFFFF; + } +} + +} /* namespace sgpio */ + } /* namespace debug */ } /* namespace radio */ diff --git a/firmware/application/radio.hpp b/firmware/application/radio.hpp index 560653497..9a23f3f57 100644 --- a/firmware/application/radio.hpp +++ b/firmware/application/radio.hpp @@ -62,6 +62,11 @@ void set_rx_max283x_iq_phase_calibration(const size_t v); // void configure(Configuration configuration); void disable(); +#ifdef PRALINE +void invalidate_spi_config(); +void set_rx_buff_vcm(const size_t v); +#endif + namespace debug { namespace first_if { @@ -69,6 +74,24 @@ namespace first_if { uint32_t register_read(const size_t register_number); void register_write(const size_t register_number, uint32_t value); +#ifdef PRALINE +struct TuningInfo { + uint32_t requested_freq_mhz; + uint32_t calculated_vco_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; + uint32_t calc_lo_freq_mhz; + uint32_t calc_vco_inside_mhz; + uint8_t calc_lodiv_log2; + uint8_t calc_presc_log2; + uint64_t calc_n_q24; +}; + +TuningInfo get_tuning_info(); +#endif + } /* namespace first_if */ namespace second_if { @@ -81,6 +104,34 @@ int8_t temp_sense(); } /* namespace second_if */ +namespace rf_path_info { + +rf::path::Band get_current_band(); + +} /* namespace rf_path_info */ + +#ifdef PRALINE +namespace fpga { + +uint32_t register_read(const size_t register_number); +void register_write(const size_t register_number, uint32_t value); +void init(); + +} /* namespace fpga */ + +/* State tracking - GPIO pins are write-only so we cache last known state */ +rf::Direction get_cached_direction(); +bool get_cached_rf_amp(); +int_fast8_t get_cached_lna_gain(); +int_fast8_t get_cached_vga_gain(); +#endif + +namespace sgpio { + +uint32_t register_read(const size_t register_number); + +} /* namespace sgpio */ + } /* namespace debug */ } /* namespace radio */ diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index d08249271..716286c16 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -57,9 +57,10 @@ static constexpr std::array am_configs{{ {taps_6k0_narrow_decim_1, taps_6k0_decim_2, taps_2k6_usb_wefax_channel, AMConfigureMessage::Modulation::SSB_FM, apt_audio_12k_lpf_1500hz_config, (int)AMConfigureMessage::Zoom_waterfall::ZOOM_x_2}, // SSB USB+FM to demod. Subcarrier FM Audio Tones to get APT Weather Fax with waterfall zoom x 2 (we need taps_6k0_narrow_decim_1 to minimize aliasing) }}; -static constexpr std::array nbfm_configs{{ +static constexpr std::array nbfm_configs{{ {taps_4k25_decim_0, taps_4k25_decim_1, taps_4k25_channel, 2500}, {taps_11k0_decim_0, taps_11k0_decim_1, taps_11k0_channel, 2500}, + {taps_12k5_decim_0, taps_12k5_decim_1, taps_12k5_channel, 2500}, {taps_16k0_decim_0, taps_16k0_decim_1, taps_16k0_channel, 5000}, }}; @@ -241,6 +242,14 @@ void ReceiverModel::set_normalized_headphone_volume(uint8_t v) { void ReceiverModel::enable() { enabled_ = true; radio::set_direction(rf::Direction::Receive); + +#ifdef PRALINE + /* Anchor the Common Mode Voltage (VCM) to 1.2V. + * This stabilizes the electrical floor of the I/Q signals. + */ + radio::set_rx_buff_vcm(1); +#endif + update_tuning_frequency(); update_antenna_bias(); update_rf_amp(); @@ -252,8 +261,6 @@ void ReceiverModel::enable() { // TODO: maybe not the perfect place for this, but it's reasonable. update_headphone_volume(); - - led_rx.on(); } void ReceiverModel::disable() { @@ -262,7 +269,6 @@ void ReceiverModel::disable() { // TODO: Responsibility for enabling/disabling the radio is muddy. // Some happens in ReceiverModel, some inside radio namespace. radio::disable(); - led_rx.off(); } void ReceiverModel::initialize() { @@ -306,7 +312,9 @@ int32_t ReceiverModel::tuning_offset() { void ReceiverModel::update_tuning_frequency() { // TODO: use positive offset if freq < offset. - radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset()); + if (enabled_) { + radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset()); + } } void ReceiverModel::set_hidden_offset(rf::Frequency offset) { @@ -315,7 +323,55 @@ void ReceiverModel::set_hidden_offset(rf::Frequency offset) { } void ReceiverModel::update_baseband_bandwidth() { - radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth()); + if (enabled_) { +#ifdef PRALINE + /* + * PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c + * + * The LPF should be set to capture the desired signal bandwidth + * while the FPGA decimation filter handles anti-aliasing. + * + * For most modes: LPF = (output_sample_rate * 3) / 8 + * For quarter-shift: add offset for shifted spectrum + * + * Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates + * the hardware limit applies and FPGA filter does the real work. + */ + + uint32_t sample_rate = sampling_rate(); + uint8_t resampling_n = portapack::clock_manager.get_resampling_n(); + uint32_t afe_rate = sample_rate << resampling_n; + + // Base LPF: enough to capture desired bandwidth + uint32_t lpf_bandwidth = (sample_rate * 3) / 8; + + // Check if quarter-shift is enabled (FPGA register 1, bits 2-3) + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03; + + if (quarter_shift != 0) { + // Quarter-shift moves spectrum by AFE_rate/4, need wider LPF + uint32_t offset = afe_rate / 8; + lpf_bandwidth += offset * 2; + } + + // For best anti-alias performance, also consider AFE Nyquist + // If our calculated LPF is below MAX2831 minimum, it doesn't matter + // But if we can set LPF to just below AFE Nyquist, that's optimal + uint32_t afe_nyquist = afe_rate / 2; + + // Use the larger of: signal bandwidth requirement OR Nyquist protection + // (but MAX2831 driver will clamp to its available settings anyway) + if (lpf_bandwidth < afe_nyquist) { + // Set LPF close to Nyquist for maximum alias rejection + lpf_bandwidth = (afe_nyquist * 9) / 10; // 90% of Nyquist + } + + radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth); +#else + radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth()); +#endif + } } void ReceiverModel::update_sampling_rate() { @@ -324,23 +380,37 @@ void ReceiverModel::update_sampling_rate() { // protocols that need quick RX/TX turn-around. // Disabling baseband while changing sampling rates seems like a good idea... - radio::set_baseband_rate(sampling_rate()); + if (enabled_) { + radio::set_baseband_rate(sampling_rate()); + } update_tuning_frequency(); + +#ifdef PRALINE + // GSG reference: re-apply frequency after sample rate change + // This reconfigures LPF bandwidth based on new decimation + update_baseband_bandwidth(); +#endif } void ReceiverModel::update_lna() { - radio::set_lna_gain(lna()); + if (enabled_) + radio::set_lna_gain(lna()); } void ReceiverModel::update_vga() { - radio::set_vga_gain(vga()); + if (enabled_) + radio::set_vga_gain(vga()); } void ReceiverModel::update_rf_amp() { - radio::set_rf_amp(rf_amp()); + if (enabled_) + radio::set_rf_amp(rf_amp()); } void ReceiverModel::update_modulation() { + if (!enabled_) + return; + switch (modulation()) { default: case Mode::AMAudio: @@ -395,5 +465,6 @@ void ReceiverModel::update_antenna_bias() { } void ReceiverModel::update_headphone_volume() { - audio::headphone::set_volume(headphone_volume()); + if (enabled_) + audio::headphone::set_volume(headphone_volume()); } diff --git a/firmware/application/recent_entries.hpp b/firmware/application/recent_entries.hpp index 8c5e35d31..6880f479a 100644 --- a/firmware/application/recent_entries.hpp +++ b/firmware/application/recent_entries.hpp @@ -108,9 +108,10 @@ void resetFilteredEntries(ContainerType& entries, KeySelector keySelector) { auto it = entries.begin(); while (it != entries.end()) { if (keySelector(*it)) { - entries.erase(it); // Add a new entry to filteredEntries + it = entries.erase(it); // Add a new entry to filteredEntries + } else { + ++it; // Move to the next element, outside of the if block } - ++it; // Move to the next element, outside of the if block } } diff --git a/firmware/application/rf_path.cpp b/firmware/application/rf_path.cpp index 905a346cb..45e655b09 100644 --- a/firmware/application/rf_path.cpp +++ b/firmware/application/rf_path.cpp @@ -35,6 +35,36 @@ namespace path { namespace { +#ifdef PRALINE +/* PRALINE uses a simplified RF path with only 5 control signals. + * The RF path architecture is completely different from HackRF One. + */ +struct PralineConfig { + bool tx_en; + bool mix_bypass; // RF path mixer bypass (GPIO3[2]) + bool lpf_en; + bool rf_amp_en; + bool ant_bias_en_n; // Inverted: 0 = bias enabled + + static void gpio_init() { + gpio_tx_enable.output(); + gpio_mix_bypass.output(); + gpio_lpf_enable.output(); + gpio_rf_amp_enable.output(); + gpio_ant_bias_disable.output(); + } + + void apply() const { + gpio_tx_enable.write(tx_en); + gpio_mix_bypass.write(mix_bypass); // Control RF path mixer + gpio_lpf_enable.write(lpf_en); + gpio_rf_amp_enable.write(rf_amp_en); + gpio_ant_bias_disable.write(ant_bias_en_n); + } +}; + +#else +/* HackRF One uses 11 GPIOs for RF path control */ using GPIOs = std::array; /* TODO: ARM GCC 4.8 2014q3 doesn't like this array inside struct Config. @@ -54,6 +84,7 @@ constexpr GPIOs gpios{ gpio_not_rx_amp_pwr, }; +/* HackRF One Config struct - not used on PRALINE */ struct Config { using base_type = uint16_t; @@ -154,6 +185,7 @@ struct Config { } }; +/* HackRF One config table - not used on PRALINE */ using ConfigAmp = std::array; using ConfigDirection = std::array; using ConfigBand = std::array; @@ -193,12 +225,26 @@ constexpr Config get_config( const bool amplify) { return config_table[toUType(band)][toUType(direction)][amplify ? 1 : 0]; } +#endif /* PRALINE */ } /* namespace */ void Path::init() { +#ifdef PRALINE + PralineConfig::gpio_init(); + /* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */ + PralineConfig config = { + .tx_en = false, + .mix_bypass = false, // RF path mixer bypass (GPIO3[2]) + .lpf_en = true, // LPF on for low band + .rf_amp_en = false, // Amp off + .ant_bias_en_n = true // Bias off (inverted) + }; + config.apply(); +#else update(); Config::gpio_init(); +#endif } void Path::set_direction(const Direction new_direction) { @@ -208,6 +254,7 @@ void Path::set_direction(const Direction new_direction) { void Path::set_band(const Band new_band) { band = new_band; + _band = new_band; update(); } @@ -220,20 +267,50 @@ void Path::update() { /* 0 ^ 0 => 0 & 0 = 0 ^ 0 = 0 (no change) * 0 ^ 1 => 1 & 0 = 0 ^ 0 = 0 (ignore change to 1) * 1 ^ 0 => 1 & 1 = 1 ^ 1 = 0 (allow change to 0) - * 1 ^ 1 => 0 & 1 = 0 ^ 1 = 1 (no change) + * 1 ^ 1 => 0 & 1 = 0 ^ 1 = 1 (no change) */ + +#ifdef PRALINE + /* PRALINE RF path control: + * - tx_en: 1 for TX, 0 for RX + * - mix_bypass: 0 to enable RF path mixer bypass (GPIO3[2]) + * - lpf_en: 1 for low band (< 2.4 GHz), 0 for high band + * - rf_amp_en: 1 to enable RF amplifier + * - ant_bias_en_n: 0 to enable antenna bias (inverted) */ // const Config changed = _config ^ config_next; // const Config turned_off = _config & changed; + PralineConfig config; + /* In transition, ignore the bits that are turning on. So this transition phase * only turns off signals. It doesn't turn on signals. */ // const Config transition_config = _config ^ turned_off; // update_signals(transition_config); + config.tx_en = (direction == Direction::Transmit); + + // RF path mixer bypass: 0=enabled, 1=bypassed + config.mix_bypass = (band == Band::Mid); + /* Move to the final state by turning on required signals. */ + /* LPF for low band */ + + config.lpf_en = (band == Band::Low); + + /* RF amp when amplification requested */ + config.rf_amp_en = rf_amp; + + /* Antenna bias off by default */ + config.ant_bias_en_n = true; + + config.apply(); + +#else + /* HackRF One RF path control */ const auto config = get_config(direction, band, rf_amp); config.apply(); +#endif } } // namespace path diff --git a/firmware/application/rf_path.hpp b/firmware/application/rf_path.hpp index e886624d8..d098b2e37 100644 --- a/firmware/application/rf_path.hpp +++ b/firmware/application/rf_path.hpp @@ -39,9 +39,17 @@ enum class Direction { namespace path { +#ifdef PRALINE +/* PRALINE: MAX2831 direct path is 2320-2740 MHz */ +constexpr FrequencyRange band_low{0, 2320'000'000}; +constexpr FrequencyRange band_high{2740'000'000, 7250'000'000}; +constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum}; +#else +/* HackRF One: Original band boundaries */ constexpr FrequencyRange band_low{0, 2170'000'000}; constexpr FrequencyRange band_high{2740'000'000, 7250'000'000}; constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum}; +#endif enum class Band { /* Zero-based, used as index into frequency_bands table */ @@ -58,12 +66,16 @@ class Path { void set_band(const Band band); void set_rf_amp(const bool rf_amp); + Band get_band() const { return _band; } //_band is used solely for debugging purposes. + private: Direction direction{Direction::Receive}; Band band{Band::Mid}; bool rf_amp{false}; void update(); + + Band _band{Band::Mid}; //_band is solely used of debugging purposes }; } // namespace path diff --git a/firmware/application/sd_card.cpp b/firmware/application/sd_card.cpp index 6edfe9aa8..d7a14bc61 100644 --- a/firmware/application/sd_card.cpp +++ b/firmware/application/sd_card.cpp @@ -36,7 +36,7 @@ bool card_present = false; Status status_{Status::NotPresent}; FRESULT mount() { - return f_mount(&fs, reinterpret_cast(_T("")), 0); + return f_mount(&fs, reinterpret_cast(_T("")), 1); } } /* namespace */ diff --git a/firmware/application/shell.cpp b/firmware/application/shell.cpp index 96779307b..67ae90744 100644 --- a/firmware/application/shell.cpp +++ b/firmware/application/shell.cpp @@ -32,6 +32,10 @@ #include "chprintf.h" #include "portapack.hpp" +extern "C" { +#include "platform_detect.h" +} + /** * @brief Shell termination event source. */ @@ -65,8 +69,58 @@ static void list_commands(BaseSequentialStream* chp, const ShellCommand* scp) { } } +static const char* get_board_revision_string(board_rev_t rev) { + switch (rev) { + case BOARD_REV_HACKRF1_OLD: + return "HackRF R1-R5"; + case BOARD_REV_HACKRF1_R6: + return "HackRF R6"; + case BOARD_REV_HACKRF1_R7: + return "HackRF R7"; + case BOARD_REV_HACKRF1_R8: + return "HackRF R8"; + case BOARD_REV_HACKRF1_R9: + return "HackRF R9"; + case BOARD_REV_HACKRF1_R10: + return "HackRF R10"; + case BOARD_REV_GSG_HACKRF1_R6: + return "GSG HackRF R6"; + case BOARD_REV_GSG_HACKRF1_R7: + return "GSG HackRF R7"; + case BOARD_REV_GSG_HACKRF1_R8: + return "GSG HackRF R8"; + case BOARD_REV_GSG_HACKRF1_R9: + return "GSG HackRF R9"; + case BOARD_REV_GSG_HACKRF1_R10: + return "GSG HackRF R10"; + case BOARD_REV_PRALINE_R0_1: + case BOARD_REV_PRALINE_R0_2: + case BOARD_REV_PRALINE_R0_3: + return "HackRF Pro R0"; + case BOARD_REV_PRALINE_R1_0: + case BOARD_REV_PRALINE_R1_1: + case BOARD_REV_PRALINE_R1_2: + return "HackRF Pro R1"; + case BOARD_REV_GSG_PRALINE_R0_1: + case BOARD_REV_GSG_PRALINE_R0_2: + case BOARD_REV_GSG_PRALINE_R0_3: + return "GSG HackRF Pro R0"; + case BOARD_REV_GSG_PRALINE_R1_0: + case BOARD_REV_GSG_PRALINE_R1_1: + case BOARD_REV_GSG_PRALINE_R1_2: + return "GSG HackRF Pro R1"; + case BOARD_REV_UNRECOGNIZED: + return "Unrecognized"; + case BOARD_REV_UNDETECTED: + return "Undetected"; + default: + return "Unknown"; + } +} + static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) { (void)argv; + if (argc > 0) { usage(chp, const_cast("info")); return; @@ -92,7 +146,40 @@ static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) { #ifdef VERSION_STRING chprintf(chp, "Mayhem Version: %s\r\n", VERSION_STRING); #endif - chprintf(chp, "HackRF Board Rev: %s\r\n", hackrf_r9 ? "R9" : "R1-R8"); + + board_rev_t revision = detected_revision(); + const char* revision_string = get_board_revision_string(revision); + chprintf(chp, "HackRF Board Rev: %s\r\n", revision_string); + + // Determine device type string + const char* device_str; +#ifdef PRALINE + device_str = "HackRF Pro"; +#else + if (portapack::device_type == portapack::DEV_PORTARF) { + device_str = "PortaRF"; + } else { + device_str = "HackRF One"; + } +#endif + chprintf(chp, "Device: %s\r\n", device_str); + + // Flash info: build-time allowed MB, runtime-detected limit, current firmware size + // Use explicit uint32_t cast: FLASH_SIZE_LIMIT_MB may be a float (e.g. 3.5 for HPro) + uint8_t flash_allowrun; +#ifdef PRALINE + flash_allowrun = 4; // HackRF Pro +#else + if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) { + flash_allowrun = 1; // PortaPack classic + } else { + flash_allowrun = FLASH_SIZE_MB; // PortaRF + } +#endif + chprintf(chp, "Flash Allowed: %dMB\r\n", (uint32_t)FLASH_SIZE_MB); + chprintf(chp, "Flash Runtime: %dMB\r\n", (uint32_t)flash_allowrun); + chprintf(chp, "Flash Current: %dMB\r\n", (uint32_t)FLASH_SIZE_LIMIT_MB); + chprintf(chp, "Reference Source: %s\r\n", portapack::clock_manager.get_source().c_str()); chprintf(chp, "Reference Freq: %s\r\n", portapack::clock_manager.get_freq().c_str()); #ifdef __DATE__ diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index 764b7f7e9..faf5e62dc 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -100,7 +100,8 @@ std::string to_string_dec_uint(uint64_t n) { std::string to_string_bin( const uint32_t n, - const uint8_t l) { + uint8_t l) { + if (l >= 33) l = 32; char p[33]; for (uint8_t c = 0; c < l; c++) { if (n & (1 << (l - 1 - c))) diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index cfebd70f3..bf803ced3 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -50,7 +50,7 @@ char* to_string_dec_uint(uint64_t n, StringFormatBuffer& buffer, size_t& length) std::string to_string_dec_int(int64_t n); std::string to_string_dec_uint(uint64_t n); -std::string to_string_bin(const uint32_t n, const uint8_t l = 0); +std::string to_string_bin(const uint32_t n, uint8_t l = 0); std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' '); std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0); std::string to_string_decimal(float decimal, int8_t precision); diff --git a/firmware/application/theme.hpp b/firmware/application/theme.hpp index a03b7d887..1f7f8c96e 100644 --- a/firmware/application/theme.hpp +++ b/firmware/application/theme.hpp @@ -32,40 +32,40 @@ namespace ui { class ThemeTemplate { public: - ~ThemeTemplate(); - Style* bg_lightest; - Style* bg_lightest_small; - Style* bg_light; - Style* bg_medium; - Style* bg_dark; - Style* bg_darker; + virtual ~ThemeTemplate(); + Style* bg_lightest = nullptr; + Style* bg_lightest_small = nullptr; + Style* bg_light = nullptr; + Style* bg_medium = nullptr; + Style* bg_dark = nullptr; + Style* bg_darker = nullptr; - Style* bg_darkest; - Style* bg_darkest_small; + Style* bg_darkest = nullptr; + Style* bg_darkest_small = nullptr; - Style* bg_important_small; + Style* bg_important_small = nullptr; - Style* error_dark; - Style* warning_dark; - Style* ok_dark; + Style* error_dark = nullptr; + Style* warning_dark = nullptr; + Style* ok_dark = nullptr; - Style* fg_dark; - Style* fg_medium; - Style* fg_light; + Style* fg_dark = nullptr; + Style* fg_medium = nullptr; + Style* fg_light = nullptr; - Style* fg_red; - Style* fg_green; - Style* fg_yellow; - Style* fg_orange; - Style* fg_blue; - Style* fg_cyan; - Style* fg_darkcyan; - Style* fg_magenta; + Style* fg_red = nullptr; + Style* fg_green = nullptr; + Style* fg_yellow = nullptr; + Style* fg_orange = nullptr; + Style* fg_blue = nullptr; + Style* fg_cyan = nullptr; + Style* fg_darkcyan = nullptr; + Style* fg_magenta = nullptr; - Style* option_active; + Style* option_active = nullptr; - Color* status_active; // green, the status bar icons when active - Color* bg_table_header; + Color* status_active = nullptr; // green, the status bar icons when active + Color* bg_table_header = nullptr; }; class ThemeDefault : public ThemeTemplate { diff --git a/firmware/application/transmitter_model.cpp b/firmware/application/transmitter_model.cpp index e2cedb3cb..00d19e97d 100644 --- a/firmware/application/transmitter_model.cpp +++ b/firmware/application/transmitter_model.cpp @@ -31,6 +31,8 @@ #include "portapack_persistent_memory.hpp" #include "radio.hpp" +#include + using namespace hackrf::one; using namespace portapack; @@ -70,7 +72,7 @@ void TransmitterModel::set_channel_bandwidth(uint32_t v) { } uint8_t TransmitterModel::tx_gain() const { - return settings_.tx_gain_db; + return std::min(settings_.tx_gain_db, portapack::persistent_memory::config_tx_gain_max_db()); } void TransmitterModel::set_tx_gain(uint8_t v_db) { @@ -79,7 +81,7 @@ void TransmitterModel::set_tx_gain(uint8_t v_db) { } bool TransmitterModel::rf_amp() const { - return settings_.rf_amp; + return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled(); } void TransmitterModel::set_rf_amp(bool enabled) { @@ -92,6 +94,15 @@ void TransmitterModel::set_antenna_bias() { } void TransmitterModel::enable() { + if (portapack::persistent_memory::config_tx_disabled()) { + radio::disable(); + + TXDisabledMessage message; + EventDispatcher::send_message(message); + + return; + } + enabled_ = true; radio::set_direction(rf::Direction::Transmit); update_tuning_frequency(); @@ -101,7 +112,6 @@ void TransmitterModel::enable() { update_sampling_rate(); update_tx_gain(); - led_tx.on(); signal_token_tick_second = rtc_time::signal_tick_second += [this]() { this->on_tick_second(); }; @@ -118,7 +128,6 @@ void TransmitterModel::disable() { radio::disable(); rtc_time::signal_tick_second -= signal_token_tick_second; - led_tx.off(); } void TransmitterModel::initialize() { @@ -135,11 +144,13 @@ void TransmitterModel::configure_from_app_settings( } void TransmitterModel::update_tuning_frequency() { - radio::set_tuning_frequency(target_frequency()); + if (enabled_) + radio::set_tuning_frequency(target_frequency()); } void TransmitterModel::update_baseband_bandwidth() { - radio::set_baseband_filter_bandwidth_tx(baseband_bandwidth()); + if (enabled_) + radio::set_baseband_filter_bandwidth_tx(baseband_bandwidth()); } void TransmitterModel::update_sampling_rate() { @@ -148,17 +159,20 @@ void TransmitterModel::update_sampling_rate() { // protocols that need quick RX/TX turn-around. // Disabling baseband while changing sampling rates seems like a good idea... + if (enabled_) + radio::set_baseband_rate(sampling_rate()); - radio::set_baseband_rate(sampling_rate()); update_tuning_frequency(); } void TransmitterModel::update_tx_gain() { - radio::set_tx_gain(tx_gain()); + if (enabled_) + radio::set_tx_gain(tx_gain()); } void TransmitterModel::update_rf_amp() { - radio::set_rf_amp(rf_amp()); + if (enabled_) + radio::set_rf_amp(rf_amp()); } void TransmitterModel::update_antenna_bias() { diff --git a/firmware/application/tuning.cpp b/firmware/application/tuning.cpp index f1425a52f..e1b2d74ba 100644 --- a/firmware/application/tuning.cpp +++ b/firmware/application/tuning.cpp @@ -26,34 +26,174 @@ namespace tuning { namespace config { -// Low band <2170 Mhz: +// Forward declarations +Config low_band(const rf::Frequency target_frequency); +Config mid_band(const rf::Frequency target_frequency); +Config high_band(const rf::Frequency target_frequency); + +#ifdef PRALINE +/* + * PRALINE Tuning Configuration + * ============================ + * + * Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[] + * + * The hackrf_usb firmware uses a table-driven approach where each entry + * specifies: + * - rf_range_end_mhz: Upper frequency limit for this config + * - if_mhz: IF frequency (what MAX2831 tunes to) + * - high_lo: true = high-side injection, false = low-side + * - shift: FPGA quarter-shift mode (not implemented in Mayhem yet) + * + * Key insight: The IF frequency varies to keep the RFFC5072 VCO in a + * safe operating range (ideally 3500-5000 MHz, avoiding extremes). + * + * RFFC5072 VCO calculation: + * High-side injection: LO = IF + RF, VCO = LO Γ— lodiv + * Low-side injection: LO = IF - RF, VCO = LO Γ— lodiv + * Where lodiv = 2 for frequencies where VCO > 2700 MHz + * + * From hackrf_usb tune_config_rx (simplified): + * 0-2100 MHz: IF=2375, high_lo=true β†’ VCO = (2375+RF)Γ—2 + * 2105-2115: IF=2375, high_lo=false β†’ VCO = (2375-RF)Γ—2 + * 2115-2130: IF=2425, high_lo=false β†’ VCO = (2425-RF)Γ—2 + * ... (more entries for fine-grained control) + * 2320-2580: IF=0 (bypass mode, no mixer) + * 2580+: High-pass mode + */ + +// Simplified tune_config lookup for Mayhem +// Returns the IF frequency in Hz for a given target frequency +constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) { + const uint32_t freq_mhz = target_frequency / 1'000'000; + + // Based on hackrf_usb tune_config_rx table + if (freq_mhz < 2100) { + // Most low-band frequencies: use 2375 MHz IF + // This keeps VCO around 4750-4950 MHz for FM band + return 2375'000'000; + } else if (freq_mhz < 2320) { + // Transition zone: use varying IF to avoid VCO edges + // These frequencies are tricky - near MAX2831 minimum + // Use 2425 MHz to give some margin + return 2425'000'000; + } else { + // Bypass mode or high-band - IF not used for mixer + return 0; + } +} + +// Returns true for high-side injection, false for low-side +constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) { + const uint32_t freq_mhz = target_frequency / 1'000'000; + + // Based on hackrf_usb tune_config_rx table + if (freq_mhz < 2100) { + // Standard low-band: high-side injection + // LO = IF + RF, mixer inverts spectrum + return true; + } else if (freq_mhz < 2105) { + // Narrow transition: still high-side + return true; + } else if (freq_mhz < 2320) { + // Near MAX2831 minimum: use low-side injection + // LO = IF - RF, no spectrum inversion + return false; + } else { + // Bypass/high-band - doesn't matter, mixer bypassed + return false; + } +} +#endif // PRALINE + +// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE): constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) { +#ifdef PRALINE + // Use the tune_config lookup for PRALINE + return praline_get_if_frequency(target_frequency); +#else return 2650'000'000 - (target_frequency / 7); +#endif } Config low_band(const rf::Frequency target_frequency) { const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency); + +#ifdef PRALINE + rf::Frequency first_lo_frequency; + bool mixer_invert; + + if (praline_use_high_side_injection(target_frequency)) { + // High-side injection: LO = IF + RF + first_lo_frequency = second_lo_frequency + target_frequency; + mixer_invert = true; + } else { + // Low-side injection: LO = IF - RF + first_lo_frequency = second_lo_frequency - target_frequency; + mixer_invert = false; + } + + return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert}; +#else const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency; const bool mixer_invert = true; return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert}; +#endif } -// Mid band 2170-2740 Mhz: +// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE): Config mid_band(const rf::Frequency target_frequency) { +#ifdef PRALINE + // For Praline with MAX2831 (2.3-2.6 GHz range) + // Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum + if (target_frequency < 2300'000'000) { + // Treat as low band - need mixer + return low_band(target_frequency); + } + // Frequencies 2300-2600 MHz can go direct (no RFFC5072) + else if (target_frequency <= 2600'000'000) { + const rf::Frequency second_lo_frequency = target_frequency; + const rf::Frequency first_lo_frequency = 0; + const bool mixer_invert = false; + return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert}; + } + // Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum + else { + // Treat as high band + return high_band(target_frequency); + } +#else const rf::Frequency second_lo_frequency = target_frequency; const rf::Frequency first_lo_frequency = 0; const bool mixer_invert = false; return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert}; +#endif } -// High band >2740 Mhz: +// High band >2740 Mhz (HackRF One) or >2580 MHz (PRALINE): constexpr rf::Frequency high_band_second_lo_frequency(const rf::Frequency target_frequency) { +#ifdef PRALINE + // Praline formula tuned for MAX2831 (2.3-2.6 GHz range) + // Keep second_lo in MAX2831's range while allowing RFFC5072 to work + // + // For high-band, we use LOW-side injection: LO = RF - IF + // So IF should be chosen to keep LO (and thus VCO) in a good range + // + // Based on hackrf_usb tune_config_tx patterns: + if (target_frequency < 3600'000'000) + return 2400'000'000 + ((target_frequency - 2740'000'000) / 4); + else if (target_frequency < 5100'000'000) + return 2500'000'000 + ((target_frequency - 3600'000'000) / 6); + else + return 2550'000'000 + ((target_frequency - 5100'000'000) / 10); +#else if (target_frequency < 3600'000'000) return (2170'000'000 + (((target_frequency - 2740'000'000) * 57) / 86)); else if (target_frequency < 5100'000'000) return (2350'000'000 + ((target_frequency - 3600'000'000) / 5)); else return (2500'000'000 + ((target_frequency - 5100'000'000) / 9)); +#endif } Config high_band(const rf::Frequency target_frequency) { diff --git a/firmware/application/ui/external/ui_grapheq.cpi b/firmware/application/ui/external/ui_grapheq.cpi index 6629bc256..950f7ff3f 100644 --- a/firmware/application/ui/external/ui_grapheq.cpi +++ b/firmware/application/ui/external/ui_grapheq.cpi @@ -185,7 +185,7 @@ void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) { } void GraphEq::paint(Painter& painter) { - if (!visible()) return; + if (!drawn()) return; if (!is_calculated) { // calc positions first calculate_params(); is_calculated = true; diff --git a/firmware/application/ui/ui_bmpview.cpp b/firmware/application/ui/ui_bmpview.cpp index 9a8a23803..9556a9f15 100644 --- a/firmware/application/ui/ui_bmpview.cpp +++ b/firmware/application/ui/ui_bmpview.cpp @@ -92,7 +92,7 @@ void BMPViewer::get_line(ui::Color* line, uint32_t bx, uint32_t by, uint32_t cnt for (uint32_t x = 0; x < cnt; x++) { uint32_t targetx = (zoom < 0) ? bx + x * -1 * zoom : bx + x / zoom; // on zoom out could probably avg the pixels, or apply some smoothing, but this is way faster. if (last_targetx == targetx) { - line[x] = line[x - 1]; + if (x > 0) line[x] = line[x - 1]; continue; } last_targetx = targetx; @@ -123,7 +123,7 @@ void BMPViewer::paint(Painter& painter) { last_by = by; portapack::display.draw_pixels({rect.left(), rect.top() + y, d_width, 1}, line, d_width); } - delete line; + delete[] line; } int8_t BMPViewer::get_zoom() { diff --git a/firmware/application/ui/ui_btngrid.cpp b/firmware/application/ui/ui_btngrid.cpp index 8ab4615c2..4dba58a54 100644 --- a/firmware/application/ui/ui_btngrid.cpp +++ b/firmware/application/ui/ui_btngrid.cpp @@ -26,6 +26,8 @@ #include "ui_btngrid.hpp" #include "rtc_time.hpp" +#include "sd_card.hpp" +#include namespace ui { @@ -42,19 +44,12 @@ BtnGridView::BtnGridView( } button_pgup.set_focusable(false); button_pgup.on_select = [this](Button&) { - if (arrow_up_enabled) { - if (((int64_t)highlighted_item - displayed_max) > 0) - set_highlighted(highlighted_item - displayed_max); - else - set_highlighted(0); - } + page_up(); }; button_pgdown.set_focusable(false); button_pgdown.on_select = [this](Button&) { - if (arrow_down_enabled) { - set_highlighted(highlighted_item + displayed_max); - } + page_down(); }; button_pgup.set_style(Theme::getInstance()->bg_darkest_small); @@ -143,6 +138,7 @@ void BtnGridView::clear() { // clear vector and release memory, not using swap since it's causing capture to glitch/fault menu_items.clear(); + // TODO(u-foka): Clean up my mess, move this somewhere to clear memory when the view is not visible, but not to be confused with clearing the menu items... for (auto& item : menu_item_views) remove_child(item.get()); @@ -186,6 +182,13 @@ void BtnGridView::insert_item(const GridItem& new_item, size_t position, bool in } void BtnGridView::show_hide_arrows() { + // if there are no menu items, disable both arrows and avoid size-1 underflow + if (menu_items.empty()) { + set_arrow_up_enabled(false); + set_arrow_down_enabled(false); + return; + } + if (highlighted_item == 0) { set_arrow_up_enabled(false); } else { @@ -198,6 +201,15 @@ void BtnGridView::show_hide_arrows() { } } +void BtnGridView::reload_items() { + menu_items.clear(); + on_populate(); + set_highlighted(highlighted_item, true); + show_hide_arrows(); + + set_dirty(); // Redraw the now potentially empty space as well +} + void BtnGridView::update_items() { size_t i = 0; @@ -237,9 +249,20 @@ void BtnGridView::show_arrows_enabled(bool enabled) { } } -bool BtnGridView::set_highlighted(int32_t new_value) { +bool BtnGridView::set_highlighted(int32_t new_value, bool force_update) { int32_t item_count = (int32_t)menu_items.size(); + // nothing to highlight when the list is empty + if (item_count == 0) { + highlighted_item = 0; + offset = 0; + show_hide_arrows(); + if (force_update) { + update_items(); + } + return false; + } + if (new_value < 0) return false; @@ -247,13 +270,15 @@ bool BtnGridView::set_highlighted(int32_t new_value) { new_value = item_count - 1; } + bool needs_update = false; + if (((uint32_t)new_value > offset) && ((new_value - offset) >= displayed_max)) { // Shift BtnGridView up highlighted_item = new_value; // rounding up new offset to next multiple of rows offset = new_value - displayed_max + rows_; offset -= (offset % rows_); - update_items(); + needs_update = true; // refresh whole screen (display flickers) only if scrolling last row up and a blank button is needed at the bottom if ((new_value + rows_ >= item_count) && (item_count % rows_) != 0) set_dirty(); @@ -261,15 +286,32 @@ bool BtnGridView::set_highlighted(int32_t new_value) { // Shift BtnGridView down highlighted_item = new_value; offset = (new_value / rows_) * rows_; - update_items(); + needs_update = true; // no need to set_dirty() here since all buttons have been repainted } else { // Just update highlight highlighted_item = new_value; } - if (visible()) - item_view(highlighted_item - offset)->focus(); + // Normalize offset to show maximum items when count decreased + if (item_count > 0 && offset + displayed_max > static_cast(item_count)) { + if (static_cast(item_count) >= displayed_max) { + offset = item_count - displayed_max; + } else { + offset = 0; + } + needs_update = true; + } + + if (needs_update || force_update) { + update_items(); + } + + if (drawn()) { + size_t idx = highlighted_item - offset; + if (idx < menu_item_views.size()) + item_view(idx)->focus(); + } show_hide_arrows(); @@ -281,7 +323,11 @@ uint32_t BtnGridView::highlighted_index() { } void BtnGridView::on_focus() { - item_view(highlighted_item - offset)->focus(); + if (!menu_items.empty()) { + size_t idx = highlighted_item - offset; + if (idx < menu_item_views.size()) + item_view(idx)->focus(); + } } void BtnGridView::on_blur() { @@ -292,12 +338,18 @@ void BtnGridView::on_blur() { } void BtnGridView::on_show() { - on_populate(); View::on_show(); - set_highlighted(highlighted_item); + + sd_card_status_signal_token = sd_card::status_signal += [this](const sd_card::Status /*status*/) { + this->reload_items(); + }; + + reload_items(); } void BtnGridView::on_hide() { + sd_card::status_signal -= sd_card_status_signal_token; + View::on_hide(); clear(); set_arrow_up_enabled(false); @@ -319,8 +371,10 @@ bool BtnGridView::on_key(const KeyEvent key) { return set_highlighted(highlighted_item - 1); case KeyEvent::Select: - if (menu_items[highlighted_item].on_select) { - menu_items[highlighted_item].on_select(); + if (!menu_items.empty() && highlighted_item < menu_items.size()) { + if (menu_items[highlighted_item].on_select) { + menu_items[highlighted_item].on_select(); + } } return true; @@ -370,4 +424,80 @@ bool BtnGridView::blacklisted_app(GridItem new_item) { return std::search(blacklist_ptr.get(), blacklist_ptr.get() + blacklist_len, app_name.begin(), app_name.end()) < blacklist_ptr.get() + blacklist_len; } +void BtnGridView::page_up() { + if (arrow_up_enabled) { + size_t item_count = menu_items.size(); + if (item_count == 0) + return; + + size_t new_offset; + if (offset > displayed_max) { + new_offset = offset - displayed_max; + } else { + new_offset = 0; + } + + // If we can't move further, just move highlight to start + if (new_offset == offset) { + set_highlighted(0); + return; + } + + bool was_visible = (highlighted_item >= new_offset && highlighted_item < new_offset + displayed_max); + + offset = new_offset; + update_items(); + + if (was_visible) { + if (drawn()) { + size_t idx = highlighted_item - offset; + if (idx < menu_item_views.size()) + item_view(idx)->focus(); + } + } else { + // focus last item on the new page (clamp to last item overall) + size_t last_on_page = std::min(new_offset + displayed_max, item_count) - 1; + set_highlighted((int)last_on_page); + } + } +} + +void BtnGridView::page_down() { + if (arrow_down_enabled) { + size_t item_count = menu_items.size(); + if (item_count == 0) + return; + + size_t max_offset; + if (item_count > displayed_max) { + max_offset = item_count - displayed_max; + } else { + max_offset = 0; + } + size_t new_offset = std::min(offset + displayed_max, max_offset); + + // If we can't move further, just move highlight to last + if (new_offset == offset) { + set_highlighted((int)(item_count - 1)); + return; + } + + bool was_visible = (highlighted_item >= new_offset && highlighted_item < new_offset + displayed_max); + + offset = new_offset; + update_items(); + + if (was_visible) { + if (drawn()) { + size_t idx = highlighted_item - offset; + if (idx < menu_item_views.size()) + item_view(idx)->focus(); + } + } else { + // focus first item on the new page + set_highlighted((int)new_offset); + } + } +} + } /* namespace ui */ diff --git a/firmware/application/ui/ui_btngrid.hpp b/firmware/application/ui/ui_btngrid.hpp index a4a0be1f7..f8cb23bcf 100644 --- a/firmware/application/ui/ui_btngrid.hpp +++ b/firmware/application/ui/ui_btngrid.hpp @@ -71,7 +71,7 @@ class BtnGridView : public View { bool show_arrows{true}; // flag used to hide arrows in main menu void show_arrows_enabled(bool enabled); - bool set_highlighted(int32_t new_value); + bool set_highlighted(int32_t new_value, bool force_update = false); uint32_t highlighted_index(); void set_parent_rect(const Rect new_parent_rect) override; @@ -88,11 +88,15 @@ class BtnGridView : public View { bool on_encoder(const EncoderEvent event) override; bool blacklisted_app(GridItem new_item); + void reload_items(); void update_items(); void set_btn_height_fixed(uint8_t h) { button_h = h; } + void page_up(); + void page_down(); + protected: virtual void on_populate() = 0; @@ -116,6 +120,7 @@ class BtnGridView : public View { size_t displayed_max{0}; size_t highlighted_item{0}; size_t offset{0}; + SignalToken sd_card_status_signal_token{}; }; } /* namespace ui */ diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index fca90d717..88402a8f5 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -265,7 +265,7 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) { void GeoMap::draw_markers(Painter& painter) { for (int i = 0; i < markerListLen; ++i) { - draw_marker_item(painter, markerList[i], Color::blue(), Color::blue(), Color::magenta()); + draw_marker_item(painter, markerList[i], markerList[i].color, markerList[i].color, Color::magenta()); } } @@ -657,7 +657,13 @@ void GeoMap::move(const float lon, const float lat) { } else { if (is_changed) { set_osm_max_zoom(); - redraw_map = true; + double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom); + double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom); + // Redraw if viewport moved by at least 1 pixel (includes zoom level changes) + if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 || + abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) { + redraw_map = true; + } } } } @@ -820,7 +826,7 @@ void GeoMap::update_my_position(float lat, float lon, int32_t altitude) { my_pos.lat = lat; my_pos.lon = lon; my_altitude = altitude; - redraw_map = is_changed; + redraw_map |= is_changed; set_dirty(); } @@ -870,8 +876,10 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a geopos.set_report_change(true); geomap.set_angle(angle); - if (is_changed) geomap.move(lon_, lat_); - geomap.set_dirty(); + if (is_changed) { + geomap.move(lon_, lat_); + geomap.set_dirty(); + } } void GeoMapView::update_tag(const std::string tag) { diff --git a/firmware/application/ui/ui_geomap.hpp b/firmware/application/ui/ui_geomap.hpp index 38ebd0ca8..b8c70da0d 100644 --- a/firmware/application/ui/ui_geomap.hpp +++ b/firmware/application/ui/ui_geomap.hpp @@ -62,13 +62,14 @@ struct GeoMarker { float lon{0}; uint16_t angle{0}; std::string tag{""}; + Color color{Color::blue()}; - GeoMarker& operator=(GeoMarker& rhs) { + GeoMarker& operator=(const GeoMarker& rhs) { lat = rhs.lat; lon = rhs.lon; angle = rhs.angle; tag = rhs.tag; - + color = rhs.color; return *this; } }; diff --git a/firmware/application/ui/ui_receiver.cpp b/firmware/application/ui/ui_receiver.cpp index 429ee7e17..17d08ca4e 100644 --- a/firmware/application/ui/ui_receiver.cpp +++ b/firmware/application/ui/ui_receiver.cpp @@ -27,6 +27,10 @@ #include "ui_receiver.hpp" #include "ui_freqman.hpp" +#ifndef PRALINE +#include "audio.hpp" +#endif + using namespace portapack; namespace ui { @@ -591,8 +595,18 @@ AudioVolumeField::AudioVolumeField( /* fill char */ ' '} { set_value(receiver_model.normalized_headphone_volume()); +#ifdef PRALINE on_change = [](int32_t v) { receiver_model.set_normalized_headphone_volume(v); +#else + on_change = [](int32_t vol) { + // don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too. + // this is identical to the receiver model's method + uint8_t v = clip(vol, 0, 99); + auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max; + persistent_memory::set_headphone_volume(new_volume); + audio::headphone::set_volume(new_volume); +#endif }; } diff --git a/firmware/application/ui/ui_rssi.cpp b/firmware/application/ui/ui_rssi.cpp index de0018f62..1d3b8b359 100644 --- a/firmware/application/ui/ui_rssi.cpp +++ b/firmware/application/ui/ui_rssi.cpp @@ -46,12 +46,16 @@ RSSI::RSSI( void RSSI::paint(Painter& painter) { const auto r = screen_rect(); + /* RSSI scaling based on transceiver output voltage range. + * MAX2837 (HackRF One): 0.4V to 2.2V + * MAX2831 (HackRF Pro): 0.5V to 2.0V (similar enough to use same scaling) + */ constexpr int rssi_sample_range = 256; // constexpr float rssi_voltage_min = 0.4; constexpr float rssi_voltage_max = 2.2; - constexpr float adc_voltage_max = 3.3; // constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; constexpr int raw_min = 0; + constexpr float adc_voltage_max = 3.3; constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; constexpr int raw_delta = raw_max - raw_min; @@ -113,6 +117,7 @@ void RSSI::paint(Painter& painter) { const Rect r_db{r.left() + x_db, r.top(), 1, r.height()}; if (db_) painter.fill_rectangle(r_db, Color::green()); + } else { // vertical bottom to top level meters const range_t y_avg_range{0, r.height() - 1}; @@ -221,6 +226,7 @@ void RSSI::on_statistics_update(const RSSIStatistics& statistics) { min_ = statistics.min; avg_ = statistics.accumulator / statistics.count; max_ = statistics.max; + if (peak_enabled) { peak_duration_ = peak_duration_ + 100; if (max_ > peak_) { @@ -431,9 +437,9 @@ void RSSIGraph::add_values(int16_t rssi_min, int16_t rssi_avg, int16_t rssi_max, constexpr int rssi_sample_range = 256; // constexpr float rssi_voltage_min = 0.4; constexpr float rssi_voltage_max = 2.2; - constexpr float adc_voltage_max = 3.3; // constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; constexpr int raw_min = 0; + constexpr float adc_voltage_max = 3.3; constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; constexpr int raw_delta = raw_max - raw_min; @@ -523,6 +529,12 @@ bool RSSI::on_touch(const TouchEvent event) { } void RSSI::set_db(int16_t db) { +#ifdef PRALINE + /* Add a +30dB global boost to align 40MHz/1.2V VCM data + with the UI's existing display scale. */ + db_ = db + 30; +#else db_ = db; +#endif } } /* namespace ui */ diff --git a/firmware/application/ui/ui_rssi.hpp b/firmware/application/ui/ui_rssi.hpp index ade17a158..65fac2312 100644 --- a/firmware/application/ui/ui_rssi.hpp +++ b/firmware/application/ui/ui_rssi.hpp @@ -64,10 +64,18 @@ class RSSI : public Widget { void set_db(int16_t db); private: +#ifdef PRALINE + // Changed from int8_t to uint8_t: + uint8_t min_ = 0; + uint8_t avg_ = 0; + uint8_t max_ = 0; + uint8_t peak_ = 0; +#else int8_t min_ = 0; int8_t avg_ = 0; int8_t max_ = 0; int8_t peak_ = 0; +#endif size_t peak_duration_ = 0; int16_t db_ = 0; bool instant_exec_{false}; diff --git a/firmware/application/ui/ui_spectrum.cpp b/firmware/application/ui/ui_spectrum.cpp index f548c542b..0fcb8c504 100644 --- a/firmware/application/ui/ui_spectrum.cpp +++ b/firmware/application/ui/ui_spectrum.cpp @@ -332,6 +332,10 @@ WaterfallView::WaterfallView(const bool cursor) { } }; + load_gradient(); +} + +void WaterfallView::load_gradient() { if (!waterfall_widget.gradient.load_file(default_gradient_file)) { waterfall_widget.gradient.set_default(); } diff --git a/firmware/application/ui/ui_spectrum.hpp b/firmware/application/ui/ui_spectrum.hpp index 9b8cf3ffc..ecc8988a8 100644 --- a/firmware/application/ui/ui_spectrum.hpp +++ b/firmware/application/ui/ui_spectrum.hpp @@ -146,6 +146,7 @@ class WaterfallView : public View { void set_parent_rect(const Rect new_parent_rect) override; void show_audio_spectrum_view(const bool show); + void load_gradient(); private: void update_widgets_rect(); diff --git a/firmware/application/ui_external_items_menu_loader.cpp b/firmware/application/ui_external_items_menu_loader.cpp index c0532579b..568372d8a 100644 --- a/firmware/application/ui_external_items_menu_loader.cpp +++ b/firmware/application/ui_external_items_menu_loader.cpp @@ -132,8 +132,8 @@ namespace ui { gridItem.on_select = [&nav, appInfo, i]() { auto dev2 = (i2cdev::I2cDev_PPmod*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDECMDL_PPMOD); - dev2->lockDevice(); if (dev2) { + dev2->lockDevice(); // auto app_image = reinterpret_cast(portapack::memory::map::m4_code.end() - appInfo->binary_size); auto app_image = reinterpret_cast(portapack::memory::map::local_sram_0.base()); uint8_t errorcnt = 0; diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index a88506a1a..342ad2a2d 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -45,14 +45,10 @@ #include "ui_mictx.hpp" #include "ui_playlist.hpp" -#include "ui_pocsag_tx.hpp" #include "ui_rds.hpp" #include "ui_recon.hpp" -// #include "ui_scanner.hpp" -#include "ui_sd_over_usb.hpp" #include "ui_search.hpp" #include "ui_settings.hpp" -#include "ui_siggen.hpp" #include "ui_sonde.hpp" #include "ui_ss_viewer.hpp" // #include "ui_test.hpp" @@ -65,7 +61,6 @@ #include "ais_app.hpp" #include "analog_audio_app.hpp" -// #include "ble_comm_app.hpp" #include "ble_rx_app.hpp" #include "ble_tx_app.hpp" #include "capture_app.hpp" @@ -114,7 +109,7 @@ const NavigationView::AppList NavigationView::appList = { /* HOME ******************************************************************/ {nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory()}, {nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory()}, - {nullptr, "Tranceiver", HOME, Color::cyan(), &bitmap_icon_tranceivers, new ViewFactory()}, + {nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory()}, {"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory()}, {"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory()}, {"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory()}, @@ -137,10 +132,8 @@ const NavigationView::AppList NavigationView::appList = { {"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory()}, {"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, new ViewFactory()}, {"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory()}, - {"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory()}, {"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory()}, {"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory()}, - {"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory()}, /* TRX ********************************************************************/ {"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory()}, /* UTILITIES *************************************************************/ @@ -148,7 +141,6 @@ const NavigationView::AppList NavigationView::appList = { {"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory()}, {"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory()}, {"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory()}, - {nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory()}, {nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory()}, //{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory()}, // Dangerous apps. @@ -309,6 +301,10 @@ SystemStatusView::SystemStatusView( this->on_clk(); }; + sd_card_status_view.on_select = [this](ImageButton&) { + this->on_sd_card(); + }; + // Initialize toggle buttons toggle_speaker.set_value(pmem::config_speaker_disable()); toggle_mute.set_value(pmem::config_audio_mute()); @@ -320,6 +316,14 @@ SystemStatusView::SystemStatusView( refresh(); } +void SystemStatusView::on_tx_disabled() { + if (!nav_.is_valid()) + return; + + nav_.pop(); + nav_.display_modal("Error", "RF transmit disabled.\nApplication closed."); +} + // when battery icon / text is clicked void SystemStatusView::on_battery_details() { if (!nav_.is_valid()) return; @@ -507,6 +511,16 @@ void SystemStatusView::on_clk() { refresh(); } +void SystemStatusView::on_sd_card() { + if (!nav_.is_valid()) return; + if (sd_info_up) return; + sd_info_up = true; + nav_.push(); + nav_.set_on_pop([this]() { + sd_info_up = false; + }); +} + void SystemStatusView::on_title() { if (nav_.is_top()) nav_.push(); @@ -601,7 +615,14 @@ bool InformationView::firmware_checksum_error() { // only checking firmware checksum once per boot if (!fw_checksum_checked) { - fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_ROM_SIZE) != FLASH_EXPECTED_CHECKSUM); +#ifdef PRALINE + fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, 4 * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM); + // TODO: This is a minimal workaround to fix the FLASH ERR checksum, bc GSG's cmake doesn't define the PortaRF board, + // so if we do, we need to patch many codes. so we can't define the PortaRF board currently in our cmake. + // discuss needed to find a better way but currently we need CI works and make hackrf pro works as much as possible. +#else + fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_SIZE_LIMIT_MB * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM); +#endif } return fw_checksum_error; } @@ -764,8 +785,6 @@ void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) { true); } }; - - grid.update_items(); } // clang-format off @@ -782,7 +801,7 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie "Check SD card\n" "Update SD card content\n"); }}, - error_tile_pos); + error_tile_pos, true); } else { std::sort(externalItems.begin(), externalItems.end(), [](const auto &a, const auto &b) { @@ -797,18 +816,10 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie } } - - grid.update_items(); } } // clang-format on -bool verify_sdcard_format() { - FATFS* fs = &sd_card::fs; - return (fs->fs_type == FS_FAT32 || fs->fs_type == FS_EXFAT) || !(sd_card::status() == sd_card::Status::Mounted); - /* ^ to satisfy those users that not use an sd*/ -} - /* ReceiversMenuView *****************************************************/ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) @@ -817,7 +828,8 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) void ReceiversMenuView::on_populate() { bool return_icon = pmem::show_gui_return_icon(); if (return_icon) { - add_item({"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}); + add_item({"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}, + true); } add_apps(nav_, *this, RX); add_external_items(nav_, app_location_t::RX, *this, return_icon ? 1 : 0); @@ -837,12 +849,12 @@ void TransmittersMenuView::on_populate() { add_external_items(nav_, app_location_t::TX, *this, return_icon ? 1 : 0); } -/* TranceiversMenuView **************************************************/ +/* TransceiversMenuView **************************************************/ -TranceiversMenuView::TranceiversMenuView(NavigationView& nav) +TransceiversMenuView::TransceiversMenuView(NavigationView& nav) : nav_(nav) {} -void TranceiversMenuView::on_populate() { +void TransceiversMenuView::on_populate() { bool return_icon = pmem::show_gui_return_icon(); if (return_icon) { add_items({{"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}}); @@ -906,11 +918,6 @@ SystemMenuView::SystemMenuView(NavigationView& nav) } void SystemMenuView::on_populate() { - if (!verify_sdcard_format()) { - add_item({"SDCard Error", Theme::getInstance()->error_dark->foreground, nullptr, [this]() { - nav_.display_modal("Error", "SD Card is not exFAT/FAT32"); - }}); - } add_apps(nav_, *this, HOME); add_external_items(nav_, app_location_t::HOME, *this, 0); add_item({"HackRF", Theme::getInstance()->fg_cyan->foreground, &bitmap_icon_hackrf, [this]() { hackrf_mode(nav_); }}); @@ -963,6 +970,8 @@ SystemView::SystemView( navigation_view.push(); + add_child(¬ification_view); + if (pmem::config_splash()) { navigation_view.push(); } @@ -1048,17 +1057,56 @@ SplashScreenView::SplashScreenView(NavigationView& nav) }; } +void SplashScreenView::get_random_splash_file(std::filesystem::path& path) { + path = u""; + + srand(LPC_RTC->CTIME0); + + DIR dir; + FILINFO fno; + int32_t valid_count = 0; + + // Open directory + if (f_opendir(&dir, (const TCHAR*)splash_dir.c_str()) == FR_OK) { + while (f_readdir(&dir, &fno) == FR_OK && fno.fname[0] != 0) { + // Skip directories + if (fno.fattrib & AM_DIR) continue; + int len = 0; + while (fno.fname[len]) len++; // Get length + if (len >= 4) { + const TCHAR* ext = &fno.fname[len - 4]; + // Check extension case-insensitively + if (ext[0] == '.' && + (ext[1] == 'B' || ext[1] == 'b') && + (ext[2] == 'M' || ext[2] == 'm') && + (ext[3] == 'P' || ext[3] == 'p')) { + valid_count++; + // Reservoir Sampling: + if ((rand() % valid_count) == 0) { + path = splash_dir / fno.fname; + } + } + } + } + f_closedir(&dir); + } +} + void SplashScreenView::paint(Painter&) { - constexpr const uint8_t zoom = 3; + set_clean(); // if (!bmp_view.load_bmp(splash_dot_bmp)) { //--too slow drawing, bc of the more bmp format support, and up-> down drawing - if (!portapack::display.draw_bmp_from_sdcard_file({0, 0}, splash_dot_bmp)) - // ^ try draw bmp file from sdcard at (0,0), and the (0,0) already bypassed the status bar, so actual pos is (0, STATUS_BAR_HEIGHT) - portapack::display.draw_bitmap({screen_width / 2 - ((bitmap_titlebar_image.size.width() * zoom) / 2), - screen_height / 2}, - bitmap_titlebar_image.size, - bitmap_titlebar_image.data, - Theme::getInstance()->bg_darkest->foreground, - Theme::getInstance()->bg_darkest->background, zoom); + if (portapack::display.draw_bmp_from_sdcard_file({0, 0}, splash_dot_bmp)) return; + // ^ try draw bmp file from sdcard at (0,0), and the (0,0) already bypassed the status bar, so actual pos is (0, STATUS_BAR_HEIGHT) + + std::filesystem::path path{}; + get_random_splash_file(path); + if (portapack::display.draw_bmp_from_sdcard_file({0, 0}, path)) return; + portapack::display.draw_bitmap({screen_width / 2 - ((bitmap_titlebar_image.size.width() * 3) / 2), + screen_height / 2}, + bitmap_titlebar_image.size, + bitmap_titlebar_image.data, + Theme::getInstance()->bg_darkest->foreground, + Theme::getInstance()->bg_darkest->background, 3); // ^ draw BMP HEX arr in firmware, note that the BMP HEX arr only cover the image part (instead of fill the screen with background, this position is located it in the center) } @@ -1076,7 +1124,6 @@ bool SplashScreenView::on_touch(const TouchEvent event) { case TouchEvent::Type::Start: handle_pop(); return false; - default: break; } diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 33d0e6358..64c3e6bb4 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -42,7 +42,7 @@ #include "ui_audio.hpp" #include "ui_sd_card_status_view.hpp" #include "ui_dfu_menu.hpp" - +#include "ui_notifications.hpp" #include "bitmap.hpp" #include "ui_bmpview.hpp" #include "ff.h" @@ -62,7 +62,6 @@ namespace ui { void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc); void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile = true); -bool verify_sdcard_format(); enum modal_t { INFO = 0, @@ -202,6 +201,7 @@ class SystemStatusView : public View { static constexpr auto default_title = ""; bool batt_was_inited = false; // if the battery was off on tart, but later turned on. bool batt_info_up = false; // to prevent show multiple batt info dialog + bool sd_info_up = false; // to prevent show multiple sd info dialog NavigationView& nav_; @@ -303,6 +303,8 @@ class SystemStatusView : public View { void on_title(); void refresh(); void on_clk(); + void on_sd_card(); + void on_tx_disabled(); void rtc_battery_workaround(); void on_battery_data(const BatteryStateMessage* msg); void on_battery_details(); @@ -314,6 +316,13 @@ class SystemStatusView : public View { this->refresh(); }}; + MessageHandlerRegistration message_handler_tx_disabled{ + Message::ID::TXDisabled, + [this](const Message* const p) { + (void)p; + this->on_tx_disabled(); + }}; + MessageHandlerRegistration message_handler_battery{ Message::ID::BatteryStateData, [this](const Message* const p) { @@ -358,7 +367,7 @@ class SplashScreenView : public View { Button button_done{ {screen_width, 0, 1, 1}, ""}; - // BMPViewer bmp_view{ {0, 0, screen_width, screen_height - 16}}; + void get_random_splash_file(std::filesystem::path& path); }; class ReceiversMenuView : public BtnGridView { @@ -381,10 +390,10 @@ class TransmittersMenuView : public BtnGridView { void on_populate() override; }; -class TranceiversMenuView : public BtnGridView { +class TransceiversMenuView : public BtnGridView { public: - TranceiversMenuView(NavigationView& nav); - std::string title() const override { return "Tranceiver"; }; + TransceiversMenuView(NavigationView& nav); + std::string title() const override { return "Transceiver"; }; private: NavigationView& nav_; @@ -438,11 +447,12 @@ class SystemView : public View { private: uint8_t overlay_active{0}; + NavigationView navigation_view{}; SystemStatusView status_view{navigation_view}; InformationView info_view{navigation_view}; + NotificationView notification_view{navigation_view}; DfuMenu overlay{navigation_view}; DfuMenu2 overlay2{navigation_view}; - NavigationView navigation_view{}; Context& context_; }; diff --git a/firmware/application/ui_notifications.cpp b/firmware/application/ui_notifications.cpp new file mode 100644 index 000000000..d116f8524 --- /dev/null +++ b/firmware/application/ui_notifications.cpp @@ -0,0 +1,131 @@ +#include "ui_notifications.hpp" + +#include "ui_navigation.hpp" + +extern ui::SystemView* system_view_ptr; + +namespace ui { + +NotificationEntryView::NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler) + : entry_(entry), notifhandler_(notifhandler) { + add_children({&background, &border, &title_text, &message_text, &close_button}); + border.set_outline(true); + if (entry_.icon != NOTIF_ICON_NONE) { + add_child(&icon_image); + icon_image.set_parent_rect({UI_POS_X(0) + 2, UI_POS_Y(1), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}); + message_text.set_parent_rect({UI_POS_X(2) + 2, UI_POS_Y(1), UI_POS_WIDTH_REMAINING(6) - 2, UI_POS_HEIGHT(2)}); + if (entry_.icon == NOTIF_ICON_MESSAGE) { + icon_image.set_bitmap(&bitmap_icon_pocsag); + } + + } else { + message_text.set_parent_rect({UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)}); + } + title_text.set_style(Theme::getInstance()->bg_dark); + title_text.set(entry_.title); + message_text.set_style(Theme::getInstance()->bg_darkest); + message_text.set(entry_.message); + + close_button.on_select = [this](Button&) { + if (notifhandler_) { + notifhandler_->remove_notification(entry_.id); + } + }; + + message_text.on_select = [this](Text&) { + if (!entry_.source_app.empty() && notifhandler_) { + notifhandler_->open_notification(entry_.source_app); + // notifhandler_->remove_notification(entry_.id); + } + }; + title_text.on_select = message_text.on_select; +} + +NotificationView::NotificationView(NavigationView& nav) + : nav_(nav) { + signal_token_tick_second = rtc_time::signal_tick_second += [this]() { + this->on_tick_second(); + }; +} + +NotificationView::~NotificationView() { + rtc_time::signal_tick_second -= signal_token_tick_second; +} + +void NotificationView::on_tick_second() { + bool changed = false; + for (size_t i = 0; i < notification_views_.size();) { + if (notification_views_[i]->entry()->increase_time(1000)) { + notification_views_.erase(notification_views_.begin() + i); + changed = true; + } else { + i++; + } + } + + if (changed) { + rearrange_notifications(); + } +} + +void NotificationView::rearrange_notifications() { + size_t count = children().size(); + while (children().size() > 0) { + remove_child(children().back()); + } + + const int entry_height = UI_POS_HEIGHT(3) + 1; + int index = 0; + + for (auto& view_ptr : notification_views_) { + view_ptr->set_parent_rect({UI_POS_X(0), + (Coord)(UI_POS_Y(0) + (index * entry_height)), + UI_POS_MAXWIDTH, + entry_height}); + add_child(view_ptr.get()); + index++; + } + + set_parent_rect({UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, (Coord)(notification_views_.size() * entry_height)}); + set_dirty(); + if (count != children().size()) { + // refresh screen!!! + system_view_ptr->set_dirty(); + } +} + +void NotificationView::add_notification(NotificationEntry& entry) { + if (notification_views_.size() >= max_notifications) { + notification_views_.erase(notification_views_.begin()); + } + + entry.id = ++curr_not_id; + notification_views_.push_back(std::make_unique(entry, this)); + + rearrange_notifications(); +} + +void NotificationView::remove_notification(uint16_t id) { + bool found = false; + size_t found_index = 0; + + for (size_t i = 0; i < notification_views_.size(); i++) { + if (notification_views_[i]->id() == id) { + found = true; + found_index = i; + break; + } + } + + if (found) { + notification_views_.erase(notification_views_.begin() + found_index); + rearrange_notifications(); + } +} + +void NotificationView::open_notification(std::string app_name) { + if (app_name.empty()) return; + nav_.StartAppByName(app_name.c_str()); +} + +} // namespace ui \ No newline at end of file diff --git a/firmware/application/ui_notifications.hpp b/firmware/application/ui_notifications.hpp new file mode 100644 index 000000000..0c3d59ad1 --- /dev/null +++ b/firmware/application/ui_notifications.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include "ui.hpp" +#include "ui_widget.hpp" +#include "signal.hpp" +#include "rtc_time.hpp" +#include "event_m0.hpp" + +namespace ui { + +class NotificationView; +class NavigationView; + +enum notification_icon_t : uint8_t { + NOTIF_ICON_NONE = 0, + NOTIF_ICON_MESSAGE +}; + +class NotificationEntry { + public: + std::string source_app{}; + std::string title{}; + std::string message{}; + notification_icon_t icon = NOTIF_ICON_NONE; + uint16_t timeout = 10000; + uint16_t id = 0; + + NotificationEntry() = default; + NotificationEntry(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000, uint16_t id = 0) + : source_app(source_app), title(title), message(message), icon(icon), timeout(timeout), id(id) {} + + static NotificationEntry build(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000) { + return NotificationEntry{source_app, title, message, icon, timeout, 0}; + } + + bool increase_time(uint16_t delta) { + current_time += delta; + return (current_time >= timeout); + } + + private: + uint16_t current_time = 0; +}; + +class NotificationEntryView : public View { + public: + NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler); + + NotificationEntryView(const NotificationEntryView&) = delete; + NotificationEntryView& operator=(const NotificationEntryView&) = delete; + + ui::Rectangle background{{1, 1, UI_POS_MAXWIDTH - 2, UI_POS_HEIGHT(3) - 2}, Theme::getInstance()->bg_darkest->background}; + ui::Rectangle border{{0, 0, UI_POS_MAXWIDTH, UI_POS_HEIGHT(3) + 1}, Theme::getInstance()->bg_light->background}; + ui::Text title_text{{UI_POS_X(0) + 1, UI_POS_Y(0) + 1, UI_POS_WIDTH_REMAINING(4) - 3, UI_POS_HEIGHT(1)}, ""}; + ui::Text message_text{{UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)}, ""}; + ui::Image icon_image{}; // 16*16 bitmap + ui::Button close_button{{UI_POS_X_RIGHT(4) - 2, UI_POS_Y(0) + 1, UI_POS_WIDTH(4), UI_POS_HEIGHT(2)}, "X"}; + + uint16_t id() const { return entry_.id; } + NotificationEntry* entry() { return &entry_; } + + private: + NotificationEntry entry_; + NotificationView* notifhandler_; +}; + +class NotificationView : public View { + public: + NotificationView(NavigationView& nav); + ~NotificationView(); + + void add_notification(NotificationEntry& entry); + void remove_notification(uint16_t id); + void open_notification(std::string app_name); + + private: + void on_tick_second(); + void rearrange_notifications(); + + NavigationView& nav_; + + // Changed to unique_ptr to handle non-copyable Views safely + std::vector> notification_views_{}; + + SignalToken signal_token_tick_second{}; + + uint16_t curr_not_id = 0; + constexpr static uint8_t max_notifications = 4; + + MessageHandlerRegistration message_handler_notifs{ + Message::ID::NotificationData, [this](Message* const p) { + const auto message = static_cast(p); + NotificationEntry entry = NotificationEntry::build(message->source_app, message->title, message->message, static_cast(message->icon), message->timeout); + this->add_notification(entry); + }}; +}; + +} // namespace ui \ No newline at end of file diff --git a/firmware/application/ui_sd_card_debug.cpp b/firmware/application/ui_sd_card_debug.cpp index c39594f65..3370b7edb 100644 --- a/firmware/application/ui_sd_card_debug.cpp +++ b/firmware/application/ui_sd_card_debug.cpp @@ -91,7 +91,7 @@ class SDCardTestThread { } private: - static constexpr File::Size write_size = 16384; + static constexpr File::Size write_size = 4096; // will call it 4 times static constexpr File::Size bytes_to_write = 16 * 1024 * 1024; static constexpr File::Size bytes_to_read = bytes_to_write; @@ -160,12 +160,14 @@ class SDCardTestThread { sizeof(*buffer.get()) / sizeof(lfsr_word_t)); const halrtcnt_t write_start = halGetCounterValue(); - const auto result_write = file.write(buffer->data(), buffer->size()); - if (result_write.is_error()) { - break; + for (uint8_t i = 0; i < 4; i++) { + const auto result_write = file.write(buffer->data(), buffer->size()); + if (result_write.is_error()) { + break; + } + _stats.write_bytes += buffer->size(); } const halrtcnt_t write_end = halGetCounterValue(); - _stats.write_bytes += buffer->size(); _stats.write_count++; const halrtcnt_t write_duration = write_end - write_start; @@ -202,12 +204,14 @@ class SDCardTestThread { const halrtcnt_t test_start = halGetCounterValue(); while (!chThdShouldTerminate() && (_stats.read_bytes < bytes_to_read)) { const halrtcnt_t read_start = halGetCounterValue(); - const auto result_read = file.read(buffer->data(), buffer->size()); - if (result_read.is_error()) { - break; + for (uint8_t i = 0; i < 4; i++) { + const auto result_read = file.read(buffer->data(), buffer->size()); + if (result_read.is_error()) { + break; + } + _stats.read_bytes += buffer->size(); } const halrtcnt_t read_end = halGetCounterValue(); - _stats.read_bytes += buffer->size(); _stats.read_count++; const halrtcnt_t read_duration = read_end - read_start; diff --git a/firmware/application/ui_sd_card_status_view.cpp b/firmware/application/ui_sd_card_status_view.cpp index 7736aaff0..c9e0327f8 100644 --- a/firmware/application/ui_sd_card_status_view.cpp +++ b/firmware/application/ui_sd_card_status_view.cpp @@ -83,7 +83,7 @@ const Color color_sd_card(const sd_card::Status status) { SDCardStatusView::SDCardStatusView( const Rect parent_rect) - : Image{parent_rect, &bitmap_sd_card_unknown, detail::color_sd_card_unknown, Theme::getInstance()->bg_dark->background} { + : ImageButton{parent_rect, &bitmap_sd_card_unknown, detail::color_sd_card_unknown, Theme::getInstance()->bg_dark->background} { } void SDCardStatusView::on_show() { @@ -101,7 +101,7 @@ void SDCardStatusView::paint(Painter& painter) { set_bitmap(&detail::bitmap_sd_card(status)); set_foreground(detail::color_sd_card(status)); - Image::paint(painter); + ImageButton::paint(painter); } void SDCardStatusView::on_status(const sd_card::Status) { diff --git a/firmware/application/ui_sd_card_status_view.hpp b/firmware/application/ui_sd_card_status_view.hpp index e6f7bd03b..aff8d243e 100644 --- a/firmware/application/ui_sd_card_status_view.hpp +++ b/firmware/application/ui_sd_card_status_view.hpp @@ -26,10 +26,11 @@ #include "theme.hpp" #include "ui_widget.hpp" #include "sd_card.hpp" +#include namespace ui { -class SDCardStatusView : public Image { +class SDCardStatusView : public ImageButton { public: SDCardStatusView(const Rect parent_rect); diff --git a/firmware/application/usb_serial_cdc.c b/firmware/application/usb_serial_cdc.c index bbeb161eb..9e0468bd0 100644 --- a/firmware/application/usb_serial_cdc.c +++ b/firmware/application/usb_serial_cdc.c @@ -37,9 +37,17 @@ CH_IRQ_HANDLER(USB0_IRQHandler) { usb0_isr(); if (status & USB0_USBSTS_D_UI) { +#ifdef PRALINE + if (thread_usb_event) { + chSysLockFromIsr(); + chEvtSignalI(thread_usb_event, EVT_MASK_USB); + chSysUnlockFromIsr(); + } +#else chSysLockFromIsr(); chEvtSignalI(thread_usb_event, EVT_MASK_USB); chSysUnlockFromIsr(); +#endif } if (status & USB0_USBSTS_D_SLI) { @@ -71,9 +79,9 @@ void nvic_enable_irq(uint8_t irqn) { void usb_configuration_changed(usb_device_t* const device) { (void)device; - usb_endpoint_init(&usb_endpoint_int_in); - usb_endpoint_init(&usb_endpoint_bulk_in); - usb_endpoint_init(&usb_endpoint_bulk_out); + usb_endpoint_init(&usb_endpoint_int_in, false); + usb_endpoint_init(&usb_endpoint_bulk_in, false); + usb_endpoint_init(&usb_endpoint_bulk_out, false); } void setup_usb_serial_controller(void) { @@ -88,8 +96,8 @@ void setup_usb_serial_controller(void) { usb_queue_init(&usb_endpoint_bulk_out_queue); usb_queue_init(&usb_endpoint_bulk_in_queue); - usb_endpoint_init(&usb_endpoint_control_out); - usb_endpoint_init(&usb_endpoint_control_in); + usb_endpoint_init(&usb_endpoint_control_out, false); + usb_endpoint_init(&usb_endpoint_control_in, false); usb_run(&usb_device); } diff --git a/firmware/application/usb_serial_host_to_device.cpp b/firmware/application/usb_serial_host_to_device.cpp index 47f6067ba..cfe0c30a3 100644 --- a/firmware/application/usb_serial_host_to_device.cpp +++ b/firmware/application/usb_serial_host_to_device.cpp @@ -33,6 +33,7 @@ extern "C" { #include static Thread* thread_usb_event = NULL; +usb_serial_input_handler_t usb_serial_active_input_handler = nullptr; struct usb_bulk_buffer_t { uint8_t* data; @@ -110,20 +111,27 @@ void complete_host_to_device_transfer() { return; chSysLock(); - for (unsigned int i = 0; i < transfer_data->length; i++) { - msg_t ret; - do { - ret = chIQPutI(&SUSBD1.iqueue, transfer_data->data[i]); + if (usb_serial_active_input_handler) { + // An input handler is active: route raw bytes directly to it + chSysUnlock(); + usb_serial_active_input_handler(transfer_data->data, transfer_data->length); + } else { + // Normal operation: feed bytes into the shell iqueue + for (unsigned int i = 0; i < transfer_data->length; i++) { + msg_t ret; + do { + ret = chIQPutI(&SUSBD1.iqueue, transfer_data->data[i]); - if (ret == Q_FULL) { - chSysUnlock(); - chThdSleepMilliseconds(1); // wait for shell thread when buffer is full - chSysLock(); - } + if (ret == Q_FULL) { + chSysUnlock(); + chThdSleepMilliseconds(1); // wait for shell thread when buffer is full + chSysLock(); + } - } while (ret == Q_FULL); + } while (ret == Q_FULL); + } + chSysUnlock(); } - chSysUnlock(); usb_bulk_buffer_spare.push(transfer_data); } diff --git a/firmware/application/usb_serial_host_to_device.hpp b/firmware/application/usb_serial_host_to_device.hpp index 8a0b9d71c..5f4566174 100644 --- a/firmware/application/usb_serial_host_to_device.hpp +++ b/firmware/application/usb_serial_host_to_device.hpp @@ -24,6 +24,7 @@ #include "ch.h" #include "hal.h" +#include "usb_serial_device_to_host.h" #define USB_BULK_BUFFER_SIZE 64 @@ -33,4 +34,46 @@ void serial_bulk_transfer_complete(void* user_data, unsigned int bytes_transferr void schedule_host_to_device_transfer(); void complete_host_to_device_transfer(); +typedef void (*usb_serial_input_handler_t)(const uint8_t* data, size_t len); + +/** + * Global storing the currently active USB serial input handler. + * When non-null, all incoming USB bytes are routed to this handler + * instead of the normal shell iqueue. + * + * Managed via UsbSerialInputHandler RAII below β€” do not write directly. + */ +extern usb_serial_input_handler_t usb_serial_active_input_handler; + +/** + * RAII wrapper that registers a USB serial input handler on construction + * and automatically deregisters it on destruction. + * + * Only one handler may be active at a time. + * + * The handler is called from the USB transfer completion context (main event + * loop thread). It must return quickly; blocking will stall USB/UI servicing. + * The data pointer is only valid for the duration of the call. + * + * Use write() to send data to the host. By default bytes are dropped if the + * TX queue is full (TIME_IMMEDIATE); pass a timeout in ticks to block instead. + */ +class UsbSerialInputHandler { + public: + UsbSerialInputHandler() = delete; + explicit UsbSerialInputHandler(usb_serial_input_handler_t handler) { + usb_serial_active_input_handler = handler; + } + ~UsbSerialInputHandler() { + usb_serial_active_input_handler = nullptr; + } + UsbSerialInputHandler(const UsbSerialInputHandler&) = delete; + UsbSerialInputHandler& operator=(const UsbSerialInputHandler&) = delete; + + // Send bytes to the USB host. timeout defaults to TIME_IMMEDIATE (drop if full). + void write(const uint8_t* data, size_t len, systime_t timeout = TIME_IMMEDIATE) { + chOQWriteTimeout(&SUSBD1.oqueue, data, len, timeout); + } +}; + #endif diff --git a/firmware/application/usb_serial_shell.cpp b/firmware/application/usb_serial_shell.cpp index 8bd65afe1..9c1483f89 100644 --- a/firmware/application/usb_serial_shell.cpp +++ b/firmware/application/usb_serial_shell.cpp @@ -105,26 +105,23 @@ static void cmd_sd_over_usb(BaseSequentialStream* chp, int argc, char* argv[]) { (void)chp; (void)argc; (void)argv; - - ui::Painter painter; - painter.fill_rectangle( - {0, 0, portapack::display.width(), portapack::display.height()}, - Theme::getInstance()->fg_yellow->background); - - painter.draw_bitmap( - {portapack::display.width() / 2 - 8, portapack::display.height() / 2 - 8}, - ui::bitmap_icon_hackrf, - Theme::getInstance()->fg_yellow->foreground, - Theme::getInstance()->fg_yellow->background); - - sdcDisconnect(&SDCD1); - sdcStop(&SDCD1); - - m4_request_shutdown(); - chThdSleepMilliseconds(50); - portapack::shutdown(true); - m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false); - m0_halt(); + auto evtd = getEventDispatcherInstance(); + if (!evtd) return; + auto top_widget = evtd->getTopWidget(); + if (!top_widget) return; + auto nav = static_cast(top_widget)->get_navigation_view(); + if (!nav) return; + nav->home(false); + std::string appwithpath = "/" + apps_dir.string() + "/"; + appwithpath += "sdusb.ppma"; + bool ret = ui::ExternalItemsMenuLoader::run_external_app(*nav, path_from_string8((char*)appwithpath.c_str())); + if (ret) { + chprintf(chp, "ok\r\n"); + } else { + chprintf(chp, "Failed to start SD over USB\r\n"); + } + evtd->wait_finish_frame(); + evtd->emulateKeyboard('\n'); } bool strEndsWith(const std::u16string& str, const std::u16string& suffix) { @@ -1144,7 +1141,7 @@ static void cmd_gotenv(BaseSequentialStream* chp, int argc, char* argv[]) { uint16_t light = 0; if (argc > 1) humi = atof(argv[1]); if (argc > 2) pressure = atof(argv[2]); - if (argc > 3) light = strtol(argv[0], NULL, 10); + if (argc > 3) light = strtol(argv[3], NULL, 10); EnvironmentDataMessage msg{temp, humi, pressure}; EventDispatcher::send_message(msg); // compatibility: @@ -1383,6 +1380,79 @@ static void cmd_getres(BaseSequentialStream* chp, int argc, char* argv[]) { chprintf(chp, res.c_str()); } +static void cmd_getflash(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argc; + (void)argv; + // FLASH_SIZE_LIMIT_MB may be a float (e.g. 3.5 for HPro), always cast explicitly. + uint8_t allowrun; +#ifdef PRALINE + allowrun = 4; // HackRF Pro +#else + if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) { + allowrun = 1; // PortaPack classic + } else { + allowrun = FLASH_SIZE_MB; // PortaRF + } +#endif + std::string res = "ALLOWEDFW:" + to_string_dec_uint((uint32_t)FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint((uint32_t)allowrun) + "\r\nCURRENT:" + to_string_dec_uint((uint32_t)FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n"; + chprintf(chp, res.c_str()); +} + +static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argc; + (void)argv; + std::string res; +#ifdef PRALINE + res = "HPRO"; +#else + if (portapack::device_type == portapack::DeviceType::DEV_PORTARF) { + res = "PORTARF"; + } else { + res = "PORTAPACK"; + } +#endif + res += "\r\nok\r\n"; + chprintf(chp, res.c_str()); +} + +static void cmd_notification(BaseSequentialStream* chp, int argc, char* argv[]) { + const char* usage = "usage: notif [appname]\r\n"; + if (argc < 1) { + chprintf(chp, usage); + return; + } + int iconindex = atoi(argv[0]); + std::string appname = (argc > 1) ? argv[1] : ""; + chprintf(chp, "Send title, and a \r\n"); + std::string title{}; + uint8_t msg[1]{0}; + do { + size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1); + if (bytes_read != 1) + break; + if (msg[0] == '\r') // end of title + break; + if (msg[0] == '\n') continue; + title += (char)msg[0]; + } while (title.size() < 48); + std::string message{}; + chprintf(chp, "Send message, and a \r\n"); + do { + size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1); + if (bytes_read != 1) + break; + if (msg[0] == '\r') // end of message + break; + if (msg[0] == '\n') continue; + message += (char)msg[0]; + } while (message.size() < 298); + + NotificationDataMessage msgn{appname.c_str(), title.c_str(), message.c_str(), (uint8_t)iconindex}; + EventDispatcher::send_message(msgn); + + chprintf(chp, "ok\r\n"); +} + static const ShellCommand commands[] = { {"reboot", cmd_reboot}, {"dfu", cmd_dfu}, @@ -1419,6 +1489,9 @@ static const ShellCommand commands[] = { {"asyncmsg", cmd_asyncmsg}, {"setfreq", cmd_setfreq}, {"getres", cmd_getres}, + {"getflash", cmd_getflash}, + {"getdevtype", cmd_getdevtype}, + {"notif", cmd_notification}, {NULL, NULL}}; static const ShellConfig shell_cfg1 = { diff --git a/firmware/application/usb_serial_shell_filesystem.cpp b/firmware/application/usb_serial_shell_filesystem.cpp index 1ba036ec6..8f39f7a83 100644 --- a/firmware/application/usb_serial_shell_filesystem.cpp +++ b/firmware/application/usb_serial_shell_filesystem.cpp @@ -148,7 +148,11 @@ void cmd_sd_open(BaseSequentialStream* chp, int argc, char* argv[]) { auto path = path_from_string8((char*)full_fn_from_args(argc, argv).c_str()); shell_file = new File(); auto error = shell_file->open(path, false, true); - if (report_on_error(chp, error)) return; + if (report_on_error(chp, error)) { + delete shell_file; + shell_file = nullptr; + return; + } chprintf(chp, "ok\r\n"); } diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 5a92c17ed..38f499d4b 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -213,7 +213,11 @@ set(CPPWARN "-Wall -Wextra") # List all default C defines here, like -D_DEBUG=1 # TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI? # NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types -set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'") +# Use BOARD variable if set, otherwise default to HACKRF_ONE +if(NOT DEFINED BOARD) + set(BOARD "HACKRF_ONE") +endif() +set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -D${BOARD} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'") # List all default ASM defines here, like -D_DEBUG=1 set(DADEFS) @@ -447,14 +451,6 @@ set(MODE_CPPSRC ) DeclareTargets(PREP replay) -### Signal generator - -set(MODE_CPPSRC - proc_siggen.cpp -) -DeclareTargets(PSIG siggen) - - ### Tones @@ -471,6 +467,7 @@ set(MODE_CPPSRC ) DeclareTargets(PSPE wideband_spectrum) + ### WFM Audio set(MODE_CPPSRC @@ -505,53 +502,6 @@ set(MODE_CPPSRC ) DeclareTargets(PFUT flash_utility) -### SD over USB - -set(MODE_INCDIR - ${HACKRF_PATH}/firmware - ${HACKRF_PATH}/firmware/common - ${HACKRF_PATH}/firmware/libopencm3/include -) -set(MODE_CPPSRC - sd_over_usb/proc_sd_over_usb.cpp - - sd_over_usb/scsi.c - sd_over_usb/diskio.c - sd_over_usb/sd_over_usb.c - sd_over_usb/usb_descriptor.c - sd_over_usb/hackrf_core.c - - ${HACKRF_PATH}/firmware/common/usb.c - ${HACKRF_PATH}/firmware/common/usb_queue.c - ${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c - ${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c - ${HACKRF_PATH}/firmware/common/usb_request.c - ${HACKRF_PATH}/firmware/common/usb_standard_request.c - ${HACKRF_PATH}/firmware/common/platform_detect.c - ${HACKRF_PATH}/firmware/common/gpio_lpc.c - ${HACKRF_PATH}/firmware/common/firmware_info.c - ${HACKRF_PATH}/firmware/common/si5351c.c - ${HACKRF_PATH}/firmware/common/i2c_bus.c - ${HACKRF_PATH}/firmware/common/mixer.c - ${HACKRF_PATH}/firmware/common/clkin.c - ${HACKRF_PATH}/firmware/common/spi_bus.c - ${HACKRF_PATH}/firmware/common/sgpio.c - ${HACKRF_PATH}/firmware/common/rf_path.c - ${HACKRF_PATH}/firmware/common/i2c_lpc.c - ${HACKRF_PATH}/firmware/common/spi_ssp.c - ${HACKRF_PATH}/firmware/common/rffc5071_spi.c - ${HACKRF_PATH}/firmware/common/rffc5071.c - ${HACKRF_PATH}/firmware/common/clkin.c - ${HACKRF_PATH}/firmware/common/gpdma.c - - ${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c - ${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c - ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c - ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c - ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c -) -DeclareTargets(PUSB sd_over_usb) - ### Place external app and disabled images below so they don't get added to the firmware set(add_to_firmware FALSE) set(MODE_FLAGS "-O3") @@ -585,6 +535,21 @@ set(MODE_CPPSRC ) DeclareTargets(PEPI epirb_rx) +### EPIRB TX + +set(MODE_CPPSRC + proc_epirb_tx.cpp +) +DeclareTargets(PEPT epirb_tx) + + +### P25 TX + +set(MODE_CPPSRC + proc_p25_tx.cpp +) +DeclareTargets(P25T p25_tx) + ### NRF RX @@ -673,14 +638,21 @@ set(MODE_CPPSRC ) DeclareTargets(PADT adsbtx) -### OOKStream +### Binary Timed stream TX set(MODE_CPPSRC - proc_ook_stream_tx.cpp + proc_bint_stream_tx.cpp ) -DeclareTargets(POSK ookstream) +DeclareTargets(POSK bintstream) +### Signal generator + +set(MODE_CPPSRC + proc_siggen.cpp +) +DeclareTargets(PSIG siggen) + ### WeFax Rx @@ -711,6 +683,96 @@ set(MODE_CPPSRC ) DeclareTargets(PSCD subcar) +### Morse RX Decoder + +set(MODE_CPPSRC + proc_morse.cpp +) +DeclareTargets(PMRS morse) + + +### RTTY RX + +set(MODE_CPPSRC + proc_rtty_rx.cpp +) +DeclareTargets(PRTR rtty_rx) + + +### RTTY TX + +set(MODE_CPPSRC + proc_rtty_tx.cpp +) +DeclareTargets(PRTT rtty_tx) + +### SD over USB + +set(MODE_INCDIR + ${HACKRF_PATH}/firmware + ${HACKRF_PATH}/firmware/common + ${HACKRF_PATH}/firmware/libopencm3/include +) + +### Morse TX + +set(MODE_CPPSRC + proc_morsetx.cpp +) +DeclareTargets(PMRT morsetx) + +### Time Sink + +set(MODE_CPPSRC + proc_time_sink.cpp +) +DeclareTargets(PTSK time_sink) + +set(MODE_CPPSRC + sd_over_usb/proc_sd_over_usb.cpp + + sd_over_usb/scsi.c + sd_over_usb/diskio.c + sd_over_usb/sd_over_usb.c + sd_over_usb/usb_descriptor.c + sd_over_usb/hackrf_core.c + + ${HACKRF_PATH}/firmware/common/adc.c + ${HACKRF_PATH}/firmware/common/selftest.c + ${HACKRF_PATH}/firmware/common/usb.c + ${HACKRF_PATH}/firmware/common/usb_queue.c + ${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c + ${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c + ${HACKRF_PATH}/firmware/common/usb_request.c + ${HACKRF_PATH}/firmware/common/usb_standard_request.c + ${HACKRF_PATH}/firmware/common/platform_detect.c + ${HACKRF_PATH}/firmware/common/gpio_lpc.c + ${HACKRF_PATH}/firmware/common/firmware_info.c + ${HACKRF_PATH}/firmware/common/si5351c.c + ${HACKRF_PATH}/firmware/common/i2c_bus.c + ${HACKRF_PATH}/firmware/common/mixer.c + ${HACKRF_PATH}/firmware/common/clkin.c + ${HACKRF_PATH}/firmware/common/spi_bus.c + ${HACKRF_PATH}/firmware/common/sgpio.c + ${HACKRF_PATH}/firmware/common/rf_path.c + ${HACKRF_PATH}/firmware/common/i2c_lpc.c + ${HACKRF_PATH}/firmware/common/spi_ssp.c + ${HACKRF_PATH}/firmware/common/rffc5071_spi.c + ${HACKRF_PATH}/firmware/common/rffc5071.c + ${HACKRF_PATH}/firmware/common/clkin.c + ${HACKRF_PATH}/firmware/common/gpdma.c + + ${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c + ${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c + ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c + ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c + ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c +) +# sd_over_usb has type conflicts with PRALINE (HackRF Pro) - disable for now +if(NOT BOARD STREQUAL "PRALINE") +DeclareTargets(PUSB sd_over_usb) +endif() + ### HackRF "factory" firmware diff --git a/firmware/baseband/baseband_dma.cpp b/firmware/baseband/baseband_dma.cpp index 412942661..825e89de6 100644 --- a/firmware/baseband/baseband_dma.cpp +++ b/firmware/baseband/baseband_dma.cpp @@ -110,6 +110,9 @@ volatile uint32_t buffer_handled = 0; static void transfer_complete() { const auto next_lli_index = gpdma_channel_sgpio.next_lli() - &lli_loop[0]; buffer_transfered++; +#ifdef PRALINE + shared_memory.m4_dma_xfr_count++; // Phase 0 instrumentation +#endif thread_wait.wake_from_interrupt(next_lli_index); } @@ -161,6 +164,10 @@ void disable() { } baseband::buffer_t wait_for_buffer() { +#ifdef PRALINE + shared_memory.m4_dma_wait_count++; // Phase 0 instrumentation +#endif + const auto next_index = thread_wait.sleep(); buffer_handled++; diff --git a/firmware/baseband/baseband_thread.cpp b/firmware/baseband/baseband_thread.cpp index 4d366f67b..c5f2859f7 100644 --- a/firmware/baseband/baseband_thread.cpp +++ b/firmware/baseband/baseband_thread.cpp @@ -87,14 +87,46 @@ void BasebandThread::run() { baseband_sgpio.configure(direction()); baseband::dma::enable(direction()); baseband_sgpio.streaming_enable(); +#ifdef PRALINE + shared_memory.m4_streaming_marker = 0xAA; // Phase 0 instrumentation +#endif while (!chThdShouldTerminate()) { +#ifdef PRALINE + shared_memory.m4_baseband_loops++; // Phase 0 instrumentation +#endif + // TODO: Place correct sampling rate into buffer returned here: const auto buffer_tmp = baseband::dma::wait_for_buffer(); if (buffer_tmp) { buffer_c8_t buffer{ buffer_tmp.p, buffer_tmp.count, sampling_rate_}; +#ifdef PRALINE + /* + * Software RSSI: Copy 8 I/Q samples spread across buffer. + * + * Just pack and copy - no computation here. + * rssi_thread does __SMUAD power and rssi calculation. + * 8 samples avoids zero-crossing artifacts. + */ + if (direction_ == baseband::Direction::Receive && buffer_tmp.count >= 32) { + const size_t step = buffer_tmp.count / 8; + + for (size_t i = 0; i < 8; i++) { + const size_t idx = i * step + (step / 2); + const auto sample = buffer_tmp.p[idx]; + + // Pack into 32 bits: Q in high 16 bits, I in low 16 bits, the safe approach: + // 1. Cast to uint16_t to capture the raw 16-bit pattern (e.g., -1 becomes 0xFFFF) + // 2. OR them together. The uint16_t will be promoted to uint32_t cleanly. + // This is accomplished with the specific hardware instruction designed + // for this __PKHBT (Pack Halfword Bottom Top). + shared_memory.software_rssi_iq[i] = __PKHBT(sample.real(), sample.imag(), 16); + } + } +#endif + if (shared_memory.request_m4_performance_counter == 0x02) { uint8_t max = shared_memory.m4_performance_counter; for (size_t i = 0; i < buffer_tmp.count; i++) { diff --git a/firmware/baseband/event_m4.cpp b/firmware/baseband/event_m4.cpp index e4888dbc1..10ac57b5c 100644 --- a/firmware/baseband/event_m4.cpp +++ b/firmware/baseband/event_m4.cpp @@ -99,6 +99,9 @@ void EventDispatcher::on_message(const Message* const message) { switch (message->id) { case Message::ID::Shutdown: on_message_shutdown(*reinterpret_cast(message)); +#ifdef PRALINE + shared_memory.baseband_message = nullptr; // Must clear before M4 exits! +#endif break; default: diff --git a/firmware/baseband/event_m4.hpp b/firmware/baseband/event_m4.hpp index 9dfa57950..06b5af9d1 100644 --- a/firmware/baseband/event_m4.hpp +++ b/firmware/baseband/event_m4.hpp @@ -46,7 +46,13 @@ class EventDispatcher { } static inline void events_flag_isr(const eventmask_t events) { +#ifdef PRALINE + if (thread_event_loop) { + chEvtSignalI(thread_event_loop, events); + } +#else chEvtSignalI(thread_event_loop, events); +#endif } private: diff --git a/firmware/baseband/fprotos/c-fiat_v0.hpp b/firmware/baseband/fprotos/c-fiat_v0.hpp index 589cfff79..b68311700 100644 --- a/firmware/baseband/fprotos/c-fiat_v0.hpp +++ b/firmware/baseband/fprotos/c-fiat_v0.hpp @@ -161,8 +161,6 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase { } if (bit_count > 0x46) { - final_count = bit_count; - endbyte = (uint8_t)data_low; /* generic.data = ((uint64_t)hop << 32) | fix; @@ -192,13 +190,11 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase { ManchesterState manchester_state = ManchesterStateMid1; uint8_t decoder_state = 0; + uint8_t bit_count = 0; + uint8_t endbyte = 0; uint16_t preamble_count = 0; uint32_t data_low = 0; uint32_t data_high = 0; - uint8_t bit_count = 0; uint32_t hop = 0; uint32_t fix = 0; - uint8_t endbyte = 0; - uint8_t final_count = 0; - uint32_t te_last = 0; }; diff --git a/firmware/baseband/fprotos/c-kia_v0.hpp b/firmware/baseband/fprotos/c-kia_v0.hpp index 7b2bf1a1b..5b17a1a6d 100644 --- a/firmware/baseband/fprotos/c-kia_v0.hpp +++ b/firmware/baseband/fprotos/c-kia_v0.hpp @@ -59,20 +59,26 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase { break; case KIADecoderStepSaveDuration: if (level) { - if (duration >= - (te_long + te_delta * 2UL)) { - // Signal ended too early! - // FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration); - + if (duration >= (te_long + te_delta * 2UL)) { + // End of transmission detected parser_step = KIADecoderStepReset; + if (decode_count_bit == min_count_bit_for_found) { - // instance->generic.data = decode_data; + // data = decode_data; data_count_bit = decode_count_bit; + // just used for log yet, so skip + // if (kia_verify_crc()) { + // FURI_LOG_I(TAG, "Valid signal received with correct CRC"); + // } else { + // FURI_LOG_W(TAG, "Signal received but CRC mismatch!"); + // } + if (callback) callback(this); } else { - // FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit); + // FURI_LOG_E(TAG, "Incomplete signal: only %u bits", instance->decoder.decode_count_bit); } + decode_data = 0; decode_count_bit = 0; break; @@ -80,26 +86,19 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase { te_last = duration; parser_step = KIADecoderStepCheckDuration; } + } else { parser_step = KIADecoderStepReset; } break; case KIADecoderStepCheckDuration: if (!level) { - if ((DURATION_DIFF(te_last, te_short) < te_delta) && - (DURATION_DIFF(duration, te_short) < te_delta)) { + if ((DURATION_DIFF(te_last, te_short) < te_delta) && (DURATION_DIFF(duration, te_short) < te_delta)) { subghz_protocol_blocks_add_bit(0); - if (decode_count_bit % 10 == 0) { - // FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit); - } parser_step = KIADecoderStepSaveDuration; } else if ( - (DURATION_DIFF(te_last, te_long) < te_delta) && - (DURATION_DIFF(duration, te_long) < te_delta)) { + (DURATION_DIFF(te_last, te_long) < te_delta) && (DURATION_DIFF(duration, te_long) < te_delta)) { subghz_protocol_blocks_add_bit(1); - if (decode_count_bit % 10 == 0) { - // FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit); - } parser_step = KIADecoderStepSaveDuration; } else { // FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration); diff --git a/firmware/baseband/fprotos/c-kia_v1.hpp b/firmware/baseband/fprotos/c-kia_v1.hpp index 7a0202d18..dd28d1290 100644 --- a/firmware/baseband/fprotos/c-kia_v1.hpp +++ b/firmware/baseband/fprotos/c-kia_v1.hpp @@ -5,8 +5,7 @@ typedef enum { KiaV1DecoderStepReset = 0, KiaV1DecoderStepCheckPreamble, - KiaV1DecoderStepFoundShortLow, - KiaV1DecoderStepCollectRawBits, + KiaV1DecoderStepDecodeData, } KiaV1DecoderStep; class FProtoSubCarKiaV1 : public FProtoSubCarBase { @@ -16,177 +15,74 @@ class FProtoSubCarKiaV1 : public FProtoSubCarBase { te_short = 800; te_long = 1600; te_delta = 200; - min_count_bit_for_found = 56; - } - void kia_v1_add_raw_bit(bool bit) { - if (raw_bit_count < 192) { - uint16_t byte_idx = raw_bit_count / 8; - uint8_t bit_idx = 7 - (raw_bit_count % 8); - if (bit) { - raw_bits[byte_idx] |= (1 << bit_idx); - } else { - raw_bits[byte_idx] &= ~(1 << bit_idx); - } - raw_bit_count++; - } - } - inline bool kia_v1_get_raw_bit(uint16_t idx) { - uint16_t byte_idx = idx / 8; - uint8_t bit_idx = 7 - (idx % 8); - return (raw_bits[byte_idx] >> bit_idx) & 1; - } - bool kia_v1_manchester_decode() { - if (raw_bit_count < 113) { - // FURI_LOG_D(TAG, "Not enough raw bits: %u", raw_bit_count); - return false; - } - // Try different offsets to find best alignment (RTL-433 uses -1 bit offset) - uint16_t best_bits = 0; - uint64_t best_data = 0; - // uint16_t best_offset = 0; - - for (uint16_t offset = 0; offset < 8; offset++) { - uint64_t data = 0; - uint16_t decoded_bits = 0; - - for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 56; i += 2) { - bool bit1 = kia_v1_get_raw_bit(i); - bool bit2 = kia_v1_get_raw_bit(i + 1); - - uint8_t two_bits = (bit1 << 1) | bit2; - - // V1 uses: 10=1, 01=0 - if (two_bits == 0x02) { // 10 = decoded 1 - data = (data << 1) | 1; - decoded_bits++; - } else if (two_bits == 0x01) { // 01 = decoded 0 - data = (data << 1); - decoded_bits++; - } else { - break; - } - } - - if (decoded_bits > best_bits) { - best_bits = decoded_bits; - best_data = data; - // best_offset = offset; - } - } - - // FURI_LOG_I(TAG, "Best: offset=%u bits=%u data=%014llX", best_offset, best_bits, best_data); - - decode_data = best_data; - decode_count_bit = best_bits; - - return best_bits >= min_count_bit_for_found; + min_count_bit_for_found = 57; } void feed(bool level, uint32_t duration) { + ManchesterEvent event = ManchesterEventReset; + switch (parser_step) { case KiaV1DecoderStepReset: - // Preamble 0xCCCCCCCD produces alternating LONG pulses - if ((level) && (DURATION_DIFF(duration, te_long) < - te_delta)) { + if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) { parser_step = KiaV1DecoderStepCheckPreamble; te_last = duration; - header_count = 1; + header_count = 0; + decode_data = 0; + decode_count_bit = 0; + FProtoGeneral::manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL); } break; case KiaV1DecoderStepCheckPreamble: - if (level) { - if (DURATION_DIFF(duration, te_long) < - te_delta) { - te_last = duration; + if (!level) { + if ((DURATION_DIFF(duration, te_long) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) { header_count++; - } else if ( - DURATION_DIFF(duration, te_short) < - te_delta) { te_last = duration; } else { parser_step = KiaV1DecoderStepReset; } - } else { - // LOW pulse - if (DURATION_DIFF(duration, te_long) < - te_delta) { - header_count++; - } else if ( - DURATION_DIFF(duration, te_short) < - te_delta) { - // Short LOW - this is the start of sync (0xCD ends: ...long H, short L, short H) - if (header_count > 12) { - parser_step = KiaV1DecoderStepFoundShortLow; - } - } else { - parser_step = KiaV1DecoderStepReset; + } + if (header_count > 70) { + if ((!level) && (DURATION_DIFF(duration, te_short) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) { + decode_count_bit = 1; + subghz_protocol_blocks_add_bit(1); + header_count = 0; + parser_step = KiaV1DecoderStepDecodeData; } } break; - case KiaV1DecoderStepFoundShortLow: - // Expecting SHORT HIGH to complete sync - if (level && (DURATION_DIFF(duration, te_short) < - te_delta)) { - // FURI_LOG_I(TAG, "Sync! hdr=%u", header_count); - parser_step = KiaV1DecoderStepCollectRawBits; - raw_bit_count = 0; - memset(raw_bits, 0, sizeof(raw_bits)); - // Add the sync short HIGH as first raw bit - kia_v1_add_raw_bit(true); - } else { - parser_step = KiaV1DecoderStepReset; + case KiaV1DecoderStepDecodeData: + if ((DURATION_DIFF(duration, te_short) < te_delta)) { + event = level ? ManchesterEventShortLow : ManchesterEventShortHigh; + } else if ((DURATION_DIFF(duration, te_long) < + te_delta)) { + event = level ? ManchesterEventLongLow : ManchesterEventLongHigh; } - break; - case KiaV1DecoderStepCollectRawBits: - if (duration > 2400) { - // FURI_LOG_I(TAG, "End! raw_bits=%u", raw_bit_count); - - if (kia_v1_manchester_decode()) { - // instance->generic.data = decode_data; - data_count_bit = raw_bit_count / 8; - - // Extract fields from 56-bit data per RTL-433: - // Serial: bits 55-24 (32 bits) - // Btn: bits 23-16 (8 bits) - // Count: bits 15-8 (8 bits) - // CRC: bits 7-0 (8 bits) - // instance->generic.serial = (uint32_t)((instance->generic.data >> 24) & 0xFFFFFFFF); - // instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0xFF); - // instance->generic.cnt = (uint8_t)((instance->generic.data >> 8) & 0xFF); - - if (callback) - callback(this); + if (event != ManchesterEventReset) { + bool data; + bool data_ok = FProtoGeneral::manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data); + if (data_ok) { + decode_data = (decode_data << 1) | data; + decode_count_bit++; } + } + if (decode_count_bit == min_count_bit_for_found) { + // instance->generic.data = decode_data; + data_count_bit = decode_count_bit; + if (callback) + callback(this); + + decode_data = 0; + decode_count_bit = 0; parser_step = KiaV1DecoderStepReset; - break; } - - int num_bits = 0; - if (DURATION_DIFF(duration, te_short) < - te_delta) { - num_bits = 1; - } else if ( - DURATION_DIFF(duration, te_long) < - te_delta) { - num_bits = 2; - } else { - parser_step = KiaV1DecoderStepReset; - break; - } - - for (int i = 0; i < num_bits; i++) { - kia_v1_add_raw_bit(level); - } - break; } } - uint8_t raw_bits[24]{0}; - uint16_t raw_bit_count = 0; - uint16_t header_count = 0; + uint8_t header_count = 0; + ManchesterState manchester_saved_state = ManchesterStateStart1; }; diff --git a/firmware/baseband/fprotos/c-kia_v2.hpp b/firmware/baseband/fprotos/c-kia_v2.hpp index 2f86d5013..82682f45a 100644 --- a/firmware/baseband/fprotos/c-kia_v2.hpp +++ b/firmware/baseband/fprotos/c-kia_v2.hpp @@ -15,65 +15,7 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase { te_short = 500; te_long = 1000; te_delta = 160; - min_count_bit_for_found = 51; - } - - void kia_v2_add_raw_bit(bool bit) { - if (raw_bit_count < 160) { - uint16_t byte_idx = raw_bit_count / 8; - uint8_t bit_idx = 7 - (raw_bit_count % 8); - if (bit) { - raw_bits[byte_idx] |= (1 << bit_idx); - } else { - raw_bits[byte_idx] &= ~(1 << bit_idx); - } - raw_bit_count++; - } - } - inline bool kia_v2_get_raw_bit(uint16_t idx) { - uint16_t byte_idx = idx / 8; - uint8_t bit_idx = 7 - (idx % 8); - return (raw_bits[byte_idx] >> bit_idx) & 1; - } - bool kia_v2_manchester_decode() { - if (raw_bit_count < 100) { - return false; - } - - uint16_t best_bits = 0; - uint64_t best_data = 0; - - for (uint16_t offset = 0; offset < 8; offset++) { - uint64_t data = 0; - uint16_t decoded_bits = 0; - - for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 53; i += 2) { - bool bit1 = kia_v2_get_raw_bit(i); - bool bit2 = kia_v2_get_raw_bit(i + 1); - - uint8_t two_bits = (bit1 << 1) | bit2; - - if (two_bits == 0x02) { - data = (data << 1) | 1; - decoded_bits++; - } else if (two_bits == 0x01) { - data = (data << 1); - decoded_bits++; - } else { - break; - } - } - - if (decoded_bits > best_bits) { - best_bits = decoded_bits; - best_data = data; - } - } - - decode_data = best_data; - decode_count_bit = best_bits; - - return best_bits >= min_count_bit_for_found; + min_count_bit_for_found = 53; } void feed(bool level, uint32_t duration) { @@ -82,86 +24,85 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase { if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) { parser_step = KiaV2DecoderStepCheckPreamble; te_last = duration; - header_count = 1; + header_count = 0; + FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL); } break; - case KiaV2DecoderStepCheckPreamble: - if (level) { - if (DURATION_DIFF(duration, te_long) < - te_delta) { + if (level) // HIGH pulse + { + if (DURATION_DIFF(duration, te_long) < te_delta) { te_last = duration; header_count++; } else if ( - DURATION_DIFF(duration, te_short) < - te_delta) { - te_last = duration; - } else { - parser_step = KiaV2DecoderStepReset; - } - } else { - if (DURATION_DIFF(duration, te_long) < - te_delta) { - header_count++; - } else if ( - DURATION_DIFF(duration, te_short) < - te_delta) { - if (header_count > 10 && - DURATION_DIFF(te_last, te_short) < - te_delta) { + DURATION_DIFF(duration, te_short) < te_delta) { + if (header_count >= 100) { + header_count = 0; + decode_data = 0; + decode_count_bit = 1; parser_step = KiaV2DecoderStepCollectRawBits; - raw_bit_count = 0; - memset(raw_bits, 0, sizeof(raw_bits)); + subghz_protocol_blocks_add_bit(1); + } else { + te_last = duration; } } else { parser_step = KiaV2DecoderStepReset; } + } else { + if (DURATION_DIFF(duration, te_long) < te_delta) { + header_count++; + te_last = duration; + } else if ( + DURATION_DIFF(duration, te_short) < te_delta) { + te_last = duration; + } else { + parser_step = KiaV2DecoderStepReset; + } } break; - case KiaV2DecoderStepCollectRawBits: - if (duration > 1500) { - if (kia_v2_manchester_decode()) { - /*data = decode_data; - data_count_bit = decode_count_bit; + case KiaV2DecoderStepCollectRawBits: { + ManchesterEvent event; - serial = (uint32_t)((data >> 20) & 0xFFFFFFFF); - btn = (uint8_t)((data >> 16) & 0x0F); - - uint16_t raw_count = (uint16_t)((data >> 4) & 0xFFF); - cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF; - */ - data_count_bit = decode_count_bit; - if (callback) - callback(this); - } - - parser_step = KiaV2DecoderStepReset; - break; - } - - int num_bits = 0; - if (DURATION_DIFF(duration, te_short) < - te_delta) { - num_bits = 1; + if (DURATION_DIFF(duration, te_short) < te_delta) { + event = level ? ManchesterEventShortLow : ManchesterEventShortHigh; } else if ( - DURATION_DIFF(duration, te_long) < - te_delta) { - num_bits = 2; + DURATION_DIFF(duration, te_long) < te_delta) { + event = level ? ManchesterEventLongLow : ManchesterEventLongHigh; } else { parser_step = KiaV2DecoderStepReset; break; } - for (int i = 0; i < num_bits; i++) { - kia_v2_add_raw_bit(level); - } + bool data_bit; + if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) { + decode_data = (decode_data << 1) | data_bit; + decode_count_bit++; + if (decode_count_bit == 53) { + // instance->generic.data = decode_data; + data_count_bit = decode_count_bit; + + // instance->generic.serial = (uint32_t)((instance->generic.data >> 20) & 0xFFFFFFFF); + // instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0x0F); + + // uint16_t raw_count = (uint16_t)((instance->generic.data >> 4) & 0xFFF); + // instance->generic.cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF; + + if (callback) + callback(this); + + decode_data = 0; + decode_count_bit = 0; + header_count = 0; + parser_step = KiaV2DecoderStepReset; + } + } break; + } } } - uint8_t raw_bits[20]{0}; - uint16_t raw_bit_count = 0; uint16_t header_count = 0; + ManchesterState manchester_state = ManchesterStateMid1; }; diff --git a/firmware/baseband/fprotos/c-kia_v3v4.hpp b/firmware/baseband/fprotos/c-kia_v3v4.hpp index bede831c1..68b2ba918 100644 --- a/firmware/baseband/fprotos/c-kia_v3v4.hpp +++ b/firmware/baseband/fprotos/c-kia_v3v4.hpp @@ -15,7 +15,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { te_short = 400; te_long = 800; te_delta = 150; - min_count_bit_for_found = 64; + min_count_bit_for_found = 68; } uint8_t reverse8(uint8_t byte) { byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4; @@ -36,7 +36,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { } } bool kia_v3_v4_process_buffer() { - if (raw_bit_count < 64) { + if (raw_bit_count < 68) { return false; } @@ -56,7 +56,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4; decode_data = serial; - decode_count_bit = 64; + decode_count_bit = 68; decode_data2 = btn; data_count_bit = decode_count_bit; if (callback) @@ -93,8 +93,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { void feed(bool level, uint32_t duration) { switch (parser_step) { case KiaV3V4DecoderStepReset: - if (level && DURATION_DIFF(duration, te_short) < - te_delta) { + if (level && DURATION_DIFF(duration, te_short) < te_delta) { parser_step = KiaV3V4DecoderStepCheckPreamble; te_last = duration; header_count = 1; @@ -103,8 +102,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { case KiaV3V4DecoderStepCheckPreamble: if (level) { - if (DURATION_DIFF(duration, te_short) < - te_delta) { + if (DURATION_DIFF(duration, te_short) < te_delta) { te_last = duration; } else if (duration > 1000 && duration < 1500) { // V4 style: Sync is LONG HIGH @@ -130,11 +128,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { } else { parser_step = KiaV3V4DecoderStepReset; } - } else if ( - DURATION_DIFF(duration, te_short) < - te_delta && - DURATION_DIFF(te_last, te_short) < - te_delta) { + } else if (DURATION_DIFF(duration, te_short) < te_delta && DURATION_DIFF(te_last, te_short) < te_delta) { header_count++; } else if (duration > 1500) { parser_step = KiaV3V4DecoderStepReset; @@ -149,12 +143,10 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { kia_v3_v4_process_buffer(); parser_step = KiaV3V4DecoderStepReset; } else if ( - DURATION_DIFF(duration, te_short) < - te_delta) { + DURATION_DIFF(duration, te_short) < te_delta) { kia_v3_v4_add_raw_bit(false); } else if ( - DURATION_DIFF(duration, te_long) < - te_delta) { + DURATION_DIFF(duration, te_long) < te_delta) { kia_v3_v4_add_raw_bit(true); } else { parser_step = KiaV3V4DecoderStepReset; diff --git a/firmware/baseband/fprotos/c-kia_v5.hpp b/firmware/baseband/fprotos/c-kia_v5.hpp index d62d70dc6..c3d3234c3 100644 --- a/firmware/baseband/fprotos/c-kia_v5.hpp +++ b/firmware/baseband/fprotos/c-kia_v5.hpp @@ -1,10 +1,11 @@ #pragma once #include "subcarbase.hpp" #include + typedef enum { KiaV5DecoderStepReset = 0, KiaV5DecoderStepCheckPreamble, - KiaV5DecoderStepCollectRawBits, + KiaV5DecoderStepData, } KiaV5DecoderStep; class FProtoSubCarKiaV5 : public FProtoSubCarBase { @@ -17,74 +18,37 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase { min_count_bit_for_found = 64; } - inline bool kia_v5_get_raw_bit(uint16_t idx) { - uint16_t byte_idx = idx / 8; - uint8_t bit_idx = 7 - (idx % 8); - return (raw_bits[byte_idx] >> bit_idx) & 1; - } - - void kia_v5_add_raw_bit(bool bit) { - if (raw_bit_count < 256) { - uint16_t byte_idx = raw_bit_count / 8; - uint8_t bit_idx = 7 - (raw_bit_count % 8); - if (bit) { - raw_bits[byte_idx] |= (1 << bit_idx); - } else { - raw_bits[byte_idx] &= ~(1 << bit_idx); - } - raw_bit_count++; - } - } - - bool kia_v5_manchester_decode() { - if (raw_bit_count < 130) { - return false; - } - - decode_data = 0; - decode_count_bit = 0; - - // Start at offset 2 for proper Manchester alignment - const uint16_t start_bit = 2; - - for (uint16_t i = start_bit; - i + 1 < raw_bit_count && decode_count_bit < 64; - i += 2) { - bool bit1 = kia_v5_get_raw_bit(i); - bool bit2 = kia_v5_get_raw_bit(i + 1); - - uint8_t two_bits = (bit1 << 1) | bit2; - - if (two_bits == 0x01) { // 01 = decoded 1 - decode_data = (decode_data << 1) | 1; - decode_count_bit++; - } else if (two_bits == 0x02) { // 10 = decoded 0 - decode_data = (decode_data << 1); - decode_count_bit++; - } else { - break; - } - } - return decode_count_bit >= min_count_bit_for_found; + void kia_v5_add_bit(bool bit) { + decode_data = (decode_data << 1) | (bit ? 1 : 0); + decode_count_bit++; } void feed(bool level, uint32_t duration) { switch (parser_step) { case KiaV5DecoderStepReset: - if ((level) && (DURATION_DIFF(duration, te_short) < - te_delta)) { + if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) { parser_step = KiaV5DecoderStepCheckPreamble; te_last = duration; header_count = 1; + decode_count_bit = 0; + decode_data = 0; + FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL); } break; case KiaV5DecoderStepCheckPreamble: if (level) { - if ((DURATION_DIFF(duration, te_short) < - te_delta) || - (DURATION_DIFF(duration, te_long) < - te_delta)) { + if (DURATION_DIFF(duration, te_long) < te_delta) { + if (header_count > 40) { + parser_step = KiaV5DecoderStepData; + decode_count_bit = 0; + decode_data = 0; + decode_data2 = 0; + header_count = 0; + } else { + te_last = duration; + } + } else if (DURATION_DIFF(duration, te_short) < te_delta) { te_last = duration; } else { parser_step = KiaV5DecoderStepReset; @@ -100,13 +64,7 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase { te_delta) && (DURATION_DIFF(te_last, te_short) < te_delta)) { - if (header_count > 40) { - parser_step = KiaV5DecoderStepCollectRawBits; - raw_bit_count = 0; - memset(raw_bits, 0, sizeof(raw_bits)); - } else { - header_count++; - } + header_count++; } else if ( DURATION_DIFF(te_last, te_long) < te_delta) { @@ -114,33 +72,21 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase { } else { parser_step = KiaV5DecoderStepReset; } + te_last = duration; } break; - case KiaV5DecoderStepCollectRawBits: - if (duration > 1200) { - if (kia_v5_manchester_decode()) { - // generic.data = decode_data; - // generic.data_count_bit = decode_count_bit; - data_count_bit = decode_count_bit; - // Compute yek (bit-reverse each byte) - uint64_t yek = 0; - for (int i = 0; i < 8; i++) { - uint8_t byte = (decode_data >> (i * 8)) & 0xFF; - uint8_t reversed = 0; - for (int b = 0; b < 8; b++) { - if (byte & (1 << b)) - reversed |= (1 << (7 - b)); - } - yek |= ((uint64_t)reversed << ((7 - i) * 8)); - } - decode_data = yek; - - // Shift serial right by 1 to correct alignment - // generic.serial = (uint32_t)(((yek >> 32) & 0x0FFFFFFF) >> 1); - // generic.btn = (uint8_t)((yek >> 61) & 0x07); // Shift btn too - // generic.cnt = (uint16_t)(yek & 0xFFFF); + case KiaV5DecoderStepData: { + ManchesterEvent event; + if (DURATION_DIFF(duration, te_short) < te_delta) { + event = level ? ManchesterEventShortHigh : ManchesterEventShortLow; + } else if (DURATION_DIFF(duration, te_long) < te_delta) { + event = level ? ManchesterEventLongHigh : ManchesterEventLongLow; + } else { + if (decode_count_bit >= min_count_bit_for_found) { + // instance->generic.data = instance->decode_data2; + data_count_bit = (decode_count_bit > 67) ? 67 : decode_count_bit; if (callback) callback(this); } @@ -149,28 +95,22 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase { break; } - int num_bits = 0; - if (DURATION_DIFF(duration, te_short) < - te_delta) { - num_bits = 1; - } else if ( - DURATION_DIFF(duration, te_long) < - te_delta) { - num_bits = 2; - } else { - parser_step = KiaV5DecoderStepReset; - break; - } - - for (int i = 0; i < num_bits; i++) { - kia_v5_add_raw_bit(level); + bool data_bit; + if (decode_count_bit <= 66 && FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) { + kia_v5_add_bit(data_bit); + + if (decode_count_bit == 64) { + decode_data2 = decode_data; + decode_data = 0; + } } + te_last = duration; break; + } } } - uint8_t raw_bits[32]{}; - uint16_t raw_bit_count = 0; uint16_t header_count = 0; + ManchesterState manchester_state = ManchesterStateMid1; }; diff --git a/firmware/baseband/fprotos/c-kia_v6.hpp b/firmware/baseband/fprotos/c-kia_v6.hpp new file mode 100644 index 000000000..49e4167f6 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v6.hpp @@ -0,0 +1,204 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + KiaV6DecoderStepReset = 0, + KiaV6DecoderStepWaitFirstHigh, + KiaV6DecoderStepCountPreamble, + KiaV6DecoderStepWaitLongHigh, + KiaV6DecoderStepData, +} KiaV6DecoderStep; + +#define KIA_V6_XOR_MASK_LOW 0x84AF25FB +#define KIA_V6_XOR_MASK_HIGH 0x638766AB + +class FProtoSubCarKiaV6 : public FProtoSubCarBase { + public: + FProtoSubCarKiaV6() { + sensorType = FPC_KIAV6; + te_short = 200; + te_long = 400; + te_delta = 100; + min_count_bit_for_found = 144; + } + + void feed(bool level, uint32_t duration) { + uint32_t uVar4, uVar5; + ManchesterEvent event; + bool data_bit; + uint8_t bit_count_inc; + uint32_t step_value; + + switch (parser_step) { + case KiaV6DecoderStepReset: // case 0 + if (level == 0) { + return; + } + if (DURATION_DIFF(duration, te_short) < + te_delta) { + parser_step = KiaV6DecoderStepWaitFirstHigh; + te_last = duration; + header_count = 0; + FProtoGeneral::manchester_advance( + manchester_state, + ManchesterEventReset, + &manchester_state, + NULL); + } + return; + + case KiaV6DecoderStepWaitFirstHigh: { // case 1 + if (level != 0) { + return; + } + uint32_t diff_short = DURATION_DIFF(duration, te_short); + uint32_t diff_long = DURATION_DIFF(duration, te_long); + + uint32_t diff = (diff_long < diff_short) ? diff_long : diff_short; + + if (diff_long < te_delta && diff_long < diff_short) { + if (header_count >= 0x259) { // 601 decimal + header_count = 0; + te_last = duration; + parser_step = KiaV6DecoderStepWaitLongHigh; + return; + } + } + + if (diff >= te_delta) { + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + } + + if (DURATION_DIFF(te_last, te_short) < + te_delta) { + te_last = duration; + header_count++; + return; + } else { + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + } + } + case KiaV6DecoderStepWaitLongHigh: { // case 2 + if (level == 0) { + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + } + uint32_t diff_long_check = DURATION_DIFF(duration, te_long); + uint32_t diff_short_check = DURATION_DIFF(duration, te_short); + + if (diff_long_check >= te_delta) { + if (diff_short_check >= te_delta) { + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + } + } + + if (DURATION_DIFF(te_last, te_long) >= + te_delta) { + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + } + decode_data = 0; + decode_count_bit = 0; + + subghz_protocol_blocks_add_bit(1); + subghz_protocol_blocks_add_bit(1); + subghz_protocol_blocks_add_bit(0); + subghz_protocol_blocks_add_bit(1); + + data_part1_low = (uint32_t)(decode_data & 0xFFFFFFFF); + data_part1_high = (uint32_t)((decode_data >> 32) & 0xFFFFFFFF); + bit_count = decode_count_bit; + + parser_step = KiaV6DecoderStepData; + return; + } + case KiaV6DecoderStepData: // case 3 + if (DURATION_DIFF(duration, te_short) < + te_delta) { + event = (ManchesterEvent)((level & 0x7F) << 1); + goto manchester_process; + } else if ( + DURATION_DIFF(duration, te_long) < + te_delta) { + event = (ManchesterEvent)(level ? 6 : 4); + goto manchester_process; + } + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + + manchester_process: + if (FProtoGeneral::manchester_advance( + manchester_state, event, &manchester_state, &data_bit)) { + uVar4 = data_part1_low; + uVar5 = (uVar4 << 1) | (data_bit ? 1 : 0); + + uint32_t carry = (uVar4 >> 31) & 1; + uVar4 = (data_part1_high << 1) | carry; + + data_part1_low = uVar5; + data_part1_high = uVar4; + + decode_data = ((uint64_t)uVar4 << 32) | uVar5; + + bit_count_inc = bit_count + 1; + bit_count = bit_count_inc; + + if (bit_count_inc == 0x40) { + // stored_part1_low = ~uVar5; + // stored_part1_high = ~uVar4; + data_part1_low = 0; + data_part1_high = 0; + } else if (bit_count_inc == 0x80) { + // stored_part2_low = ~uVar5; + // stored_part2_high = ~uVar4; + data_part1_low = 0; + data_part1_high = 0; + } + } + + te_last = duration; + + if (bit_count != min_count_bit_for_found) { + return; + } + data_count_bit = min_count_bit_for_found; + // data_part3 = ~((uint16_t)data_part1_low); + + // kia_v6_decrypt(); --won't + decode_data = data_part1_low | ((uint64_t)data_part1_high << 32); + if (callback) { + callback(this); + } + + data_part1_low = 0; + data_part1_high = 0; + bit_count = 0; + step_value = KiaV6DecoderStepReset; + goto LAB_reset; + + default: + return; + } + + LAB_reset: + parser_step = step_value; + return; + } + + uint8_t bit_count = 0; + + uint16_t header_count = 0; + ManchesterState manchester_state = ManchesterStateMid1; + uint32_t data_part1_low = 0; + uint32_t data_part1_high = 0; + + // uint32_t stored_part1_low = 0; + // uint32_t stored_part1_high = 0; + // uint32_t stored_part2_low = 0; + // uint32_t stored_part2_high = 0; + // uint16_t data_part3 = 0; +}; diff --git a/firmware/baseband/fprotos/c-subaru.hpp b/firmware/baseband/fprotos/c-subaru.hpp index 6e1b3a5e0..dffc29f26 100644 --- a/firmware/baseband/fprotos/c-subaru.hpp +++ b/firmware/baseband/fprotos/c-subaru.hpp @@ -20,6 +20,7 @@ class FProtoSubCarSubaru : public FProtoSubCarBase { te_delta = 260; min_count_bit_for_found = 64; } + void subghz_protocol_decoder_subaru_reset() { parser_step = SubaruDecoderStepReset; te_last = 0; diff --git a/firmware/baseband/fprotos/c-suzuki.hpp b/firmware/baseband/fprotos/c-suzuki.hpp index 93340df01..08ab1535f 100644 --- a/firmware/baseband/fprotos/c-suzuki.hpp +++ b/firmware/baseband/fprotos/c-suzuki.hpp @@ -6,8 +6,8 @@ typedef enum { SuzukiDecoderStepReset = 0, - SuzukiDecoderStepFoundStartPulse, - SuzukiDecoderStepSaveDuration, + SuzukiDecoderStepCountPreamble = 1, + SuzukiDecoderStepDecodeData = 2, } SuzukiDecoderStep; class FProtoSubCarSuzuki : public FProtoSubCarBase { @@ -20,54 +20,45 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase { min_count_bit_for_found = 64; } void suzuki_add_bit(uint32_t bit) { - uint32_t carry = data_low >> 31; - data_low = (data_low << 1) | bit; - data_high = (data_high << 1) | carry; - data_count_bit++; + decode_data = (decode_data << 1) | bit; + decode_count_bit++; } void subghz_protocol_decoder_suzuki_reset() { parser_step = SuzukiDecoderStepReset; header_count = 0; data_count_bit = 0; - data_low = 0; - data_high = 0; + decode_data = 0; } void feed(bool level, uint32_t duration) { switch (parser_step) { case SuzukiDecoderStepReset: - // Wait for short HIGH pulse (~250Β΅s) to start preamble - if (!level) + // Wait for HIGH pulse (~250Β΅s) to start preamble + if (!level) { return; - + } if (DURATION_DIFF(duration, te_short) > te_delta) { return; } - data_low = 0; - data_high = 0; - parser_step = SuzukiDecoderStepFoundStartPulse; + decode_data = 0; + decode_count_bit = 0; + parser_step = SuzukiDecoderStepCountPreamble; header_count = 0; - data_count_bit = 0; break; - case SuzukiDecoderStepFoundStartPulse: + case SuzukiDecoderStepCountPreamble: if (level) { // HIGH pulse - if (header_count < 257) { - // Still in preamble - just count - return; - } - // After preamble, look for long HIGH to start data - if (DURATION_DIFF(duration, te_long) < te_delta) { - parser_step = SuzukiDecoderStepSaveDuration; - suzuki_add_bit(1); + if (header_count >= 300) { + if (DURATION_DIFF(duration, te_long) <= te_delta) { + parser_step = SuzukiDecoderStepDecodeData; + suzuki_add_bit(1); + } } - // Ignore short HIGHs after preamble until we see a long one } else { - // LOW pulse - count as header if short - if (DURATION_DIFF(duration, te_short) < te_delta) { + if (DURATION_DIFF(duration, te_short) <= te_delta) { te_last = duration; header_count++; } else { @@ -76,45 +67,69 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase { } break; - case SuzukiDecoderStepSaveDuration: + case SuzukiDecoderStepDecodeData: + if (level) { // HIGH pulse - determines bit value - // Long HIGH (~500Β΅s) = 1, Short HIGH (~250Β΅s) = 0 - if (DURATION_DIFF(duration, te_long) < te_delta) { - suzuki_add_bit(1); - } else if (DURATION_DIFF(duration, te_short) < te_delta) { - suzuki_add_bit(0); + if (duration < te_long) { + uint32_t diff_long = 500 - duration; + if (diff_long > 99) { + uint32_t diff_short; + if (duration < 250) { + diff_short = 250 - duration; + } else { + diff_short = duration - 250; + } + + if (diff_short <= 99) { + suzuki_add_bit(0); + } + } else { + suzuki_add_bit(1); + } } else { - parser_step = SuzukiDecoderStepReset; + uint32_t diff_long = duration - 500; + if (diff_long <= 99) { + suzuki_add_bit(1); + } } - // Stay in this state for next bit } else { // LOW pulse - check for gap (end of transmission) - if (DURATION_DIFF(duration, SUZUKI_GAP_TIME) < SUZUKI_GAP_DELTA) { - // Gap found - end of transmission - if (data_count_bit == 64) { + uint32_t diff_gap; + if (duration < SUZUKI_GAP_TIME) { + diff_gap = SUZUKI_GAP_TIME - duration; + } else { + diff_gap = duration - SUZUKI_GAP_TIME; + } + + if (diff_gap <= SUZUKI_GAP_DELTA) { + if (decode_count_bit == 64) { + // instance->generic.data = decode_data; data_count_bit = 64; - decode_data = ((uint64_t)data_high << 32) | (uint64_t)data_low; - // Check manufacturer nibble (should be 0xF) - uint8_t manufacturer = (data_high >> 28) & 0xF; - if (manufacturer == 0xF) { - // Extract fields - decode_data2 = 0; // Not used - if (callback) { - callback(this); - } + + /*uint64_t data = instance->generic.data; + uint32_t data_high = (uint32_t)(data >> 32); + uint32_t data_low = (uint32_t)data; + + instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); + + instance->generic.btn = (data_low >> 12) & 0xF; + instance->generic.cnt = (data_high << 4) >> 16; +*/ + if (callback) { + callback(this); } } + + decode_data = 0; + decode_count_bit = 0; parser_step = SuzukiDecoderStepReset; } - // Short LOW pulses are ignored - stay in this state } break; } } uint16_t header_count = 0; - uint32_t data_high = 0; - uint32_t data_low = 0; uint8_t data_count_bit = 0; }; diff --git a/firmware/baseband/fprotos/s-holtek_ht6p20b.hpp b/firmware/baseband/fprotos/s-holtek_ht6p20b.hpp new file mode 100644 index 000000000..344b81f6b --- /dev/null +++ b/firmware/baseband/fprotos/s-holtek_ht6p20b.hpp @@ -0,0 +1,84 @@ + +#ifndef __FPROTO_HOLTEKHT6P20B_H__ +#define __FPROTO_HOLTEKHT6P20B_H__ + +#include "subghzdbase.hpp" + +typedef enum : uint8_t { + Holtek_HT6P20BDecoderStepReset = 0, + Holtek_HT6P20BDecoderStepFoundStartBit, + Holtek_HT6P20BDecoderStepSaveDuration, + Holtek_HT6P20BDecoderStepCheckDuration, +} Holtek_HT6P20BDecoderStep; + +class FProtoSubGhzDHoltekHt6p20b : public FProtoSubGhzDBase { + public: + FProtoSubGhzDHoltekHt6p20b() { + sensorType = FPS_HOLTEKHT6P20B; + te_short = 450; + te_long = 900; + te_delta = 270; + min_count_bit_for_found = 28; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case Holtek_HT6P20BDecoderStepReset: + if ((!level) && (DURATION_DIFF(duration, te_short * 23) < te_delta * 23)) { + // Found Preambula + parser_step = Holtek_HT6P20BDecoderStepFoundStartBit; + } + break; + case Holtek_HT6P20BDecoderStepFoundStartBit: + if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) { + // Found StartBit + parser_step = Holtek_HT6P20BDecoderStepSaveDuration; + decode_data = 0; + decode_count_bit = 0; + } else { + parser_step = Holtek_HT6P20BDecoderStepReset; + } + break; + case Holtek_HT6P20BDecoderStepSaveDuration: + // save duration + if (!level) { + if (duration >= ((uint32_t)te_short * 10 + te_delta)) { + if (decode_count_bit == min_count_bit_for_found) { + data_count_bit = decode_count_bit; + if (callback) callback(this); + } + decode_data = 0; + decode_count_bit = 0; + parser_step = Holtek_HT6P20BDecoderStepFoundStartBit; + break; + } else { + te_last = duration; + parser_step = Holtek_HT6P20BDecoderStepCheckDuration; + } + } else { + parser_step = Holtek_HT6P20BDecoderStepReset; + } + break; + case Holtek_HT6P20BDecoderStepCheckDuration: + if (level) { + if ((DURATION_DIFF(te_last, te_long) < te_delta * 2) && + (DURATION_DIFF(duration, te_short) < te_delta)) { + subghz_protocol_blocks_add_bit(1); + parser_step = Holtek_HT6P20BDecoderStepSaveDuration; + } else if ( + (DURATION_DIFF(te_last, te_short) < te_delta) && + (DURATION_DIFF(duration, te_long) < te_delta * 2)) { + subghz_protocol_blocks_add_bit(0); + parser_step = Holtek_HT6P20BDecoderStepSaveDuration; + } else { + parser_step = Holtek_HT6P20BDecoderStepReset; + } + } else { + parser_step = Holtek_HT6P20BDecoderStepReset; + } + break; + } + } +}; + +#endif diff --git a/firmware/baseband/fprotos/s-restaurant_pager.hpp b/firmware/baseband/fprotos/s-restaurant_pager.hpp new file mode 100644 index 000000000..875f2368c --- /dev/null +++ b/firmware/baseband/fprotos/s-restaurant_pager.hpp @@ -0,0 +1,75 @@ + +#ifndef __FPROTO_RESTAURANT_PAGER_H__ +#define __FPROTO_RESTAURANT_PAGER_H__ + +#include "subghzdbase.hpp" + +typedef enum : uint8_t { + RestaurantPagerDecoderStepReset = 0, + RestaurantPagerDecoderStepSaveDuration, + RestaurantPagerDecoderStepCheckDuration, +} RestaurantPagerDecoderStep; + +class FProtoSubGhzDRestaurantPager : public FProtoSubGhzDBase { + public: + FProtoSubGhzDRestaurantPager() { + sensorType = FPS_RESTAURANT_PAGER; + te_short = 204; + te_long = 636; + te_delta = 100; + min_count_bit_for_found = 25; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case RestaurantPagerDecoderStepReset: + if ((!level) && (DURATION_DIFF(duration, te_short * 36) < te_delta * 36)) { + // Found preamble + parser_step = RestaurantPagerDecoderStepSaveDuration; + decode_data = 0; + decode_count_bit = 0; + } + break; + case RestaurantPagerDecoderStepSaveDuration: + if (level) { + te_last = duration; + parser_step = RestaurantPagerDecoderStepCheckDuration; + } + break; + case RestaurantPagerDecoderStepCheckDuration: + if (!level) { + if (duration >= ((uint32_t)te_long * 2)) { + parser_step = RestaurantPagerDecoderStepSaveDuration; + if (decode_count_bit == min_count_bit_for_found) { + // Skip all-ones preamble frames + if ((decode_data >> 1) != 0xFFFFFF) { + data_count_bit = decode_count_bit; + if (callback) callback(this); + } + } + decode_data = 0; + decode_count_bit = 0; + break; + } + + if ((DURATION_DIFF(te_last, te_short) < te_delta) && + (DURATION_DIFF(duration, te_long) < te_delta * 3)) { + subghz_protocol_blocks_add_bit(0); + parser_step = RestaurantPagerDecoderStepSaveDuration; + } else if ( + (DURATION_DIFF(te_last, te_long) < te_delta * 3) && + (DURATION_DIFF(duration, te_short) < te_delta)) { + subghz_protocol_blocks_add_bit(1); + parser_step = RestaurantPagerDecoderStepSaveDuration; + } else { + parser_step = RestaurantPagerDecoderStepReset; + } + } else { + parser_step = RestaurantPagerDecoderStepReset; + } + break; + } + } +}; + +#endif diff --git a/firmware/baseband/fprotos/s-secplus_v1.hpp b/firmware/baseband/fprotos/s-secplus_v1.hpp index 98413486a..ce34fd634 100644 --- a/firmware/baseband/fprotos/s-secplus_v1.hpp +++ b/firmware/baseband/fprotos/s-secplus_v1.hpp @@ -76,7 +76,7 @@ class FProtoSubGhzDSecPlusV1 : public FProtoSubGhzDBase { packet_accepted |= SECPLUS_V1_PACKET_2_ACCEPTED; if (packet_accepted == (SECPLUS_V1_PACKET_1_ACCEPTED | SECPLUS_V1_PACKET_2_ACCEPTED)) { - // subghz_protocol_secplus_v1_decode(); // disabled doe to lack of flash + // subghz_protocol_secplus_v1_decode(); // disabled due to lack of flash // controller // uint32_t fixed = (data >> 32) & 0xFFFFFFFF; // cnt = data & 0xFFFFFFFF; diff --git a/firmware/baseband/fprotos/subcarprotos.hpp b/firmware/baseband/fprotos/subcarprotos.hpp index d88e8d69f..96952e264 100644 --- a/firmware/baseband/fprotos/subcarprotos.hpp +++ b/firmware/baseband/fprotos/subcarprotos.hpp @@ -21,14 +21,15 @@ So include here the .hpp, and add a new element to the protos vector in the cons #include "c-ford_v0.hpp" #include "c-fiat_v0.hpp" #include "c-bmw_v0.hpp" +// #include "c-kia_v6.hpp" #ifndef __FPROTO_PROTOLISTCAR_H__ #define __FPROTO_PROTOLISTCAR_H__ class SubCarProtos : public FProtoListGeneral { public: - SubCarProtos(const SubCarProtos&) { SubCarProtos(); }; // won't use, but makes compiler happy - SubCarProtos& operator=(const SubCarProtos&) { return *this; } // won't use, but makes compiler happy + SubCarProtos(const SubCarProtos&) = delete; + SubCarProtos& operator=(const SubCarProtos&) = delete; SubCarProtos() { // add protos protos[FPC_SUZUKI] = new FProtoSubCarSuzuki(); @@ -42,6 +43,7 @@ class SubCarProtos : public FProtoListGeneral { protos[FPC_FORDV0] = new FProtoSubCarFordV0(); protos[FPC_FIATV0] = new FProtoSubCarFiatV0(); protos[FPC_BMWV0] = new FProtoSubCarBMWV0(); + // protos[FPC_KIAV6] = new FProtoSubCarKiaV6(); //-- disabled, due to whole encrypted. for (uint8_t i = 0; i < FPC_COUNT; ++i) { if (protos[i] != NULL) protos[i]->setCallback(callbackTarget); diff --git a/firmware/baseband/fprotos/subcartypes.hpp b/firmware/baseband/fprotos/subcartypes.hpp index 3b01cdbc7..052ecf2cd 100644 --- a/firmware/baseband/fprotos/subcartypes.hpp +++ b/firmware/baseband/fprotos/subcartypes.hpp @@ -8,9 +8,6 @@ These values must be present on the protocol's constructor, like FProtoWeatherAc Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name. */ -#define FPM_AM 0 -#define FPM_FM 1 - enum FPROTO_SUBCAR_SENSOR : uint8_t { FPC_Invalid = 0, FPC_SUZUKI = 1, @@ -24,6 +21,8 @@ enum FPROTO_SUBCAR_SENSOR : uint8_t { FPC_FORDV0 = 9, FPC_FIATV0 = 10, FPC_BMWV0 = 11, + // FPC_KIAV6 = 12, //disabled, due to whole encrypted. + // FPC_PSA = 13, // IS whole encrypted FPC_COUNT }; diff --git a/firmware/baseband/fprotos/subghzdprotos.hpp b/firmware/baseband/fprotos/subghzdprotos.hpp index b4d5ad429..6efea5d88 100644 --- a/firmware/baseband/fprotos/subghzdprotos.hpp +++ b/firmware/baseband/fprotos/subghzdprotos.hpp @@ -54,14 +54,16 @@ So include here the .hpp, and add a new element to the protos vector in the cons #include "s-gangqi.hpp" #include "s-marantec24.hpp" // GENIE FROM PR +#include "s-holtek_ht6p20b.hpp" +#include "s-restaurant_pager.hpp" #ifndef __FPROTO_PROTOLISTSGZ_H__ #define __FPROTO_PROTOLISTSGZ_H__ class SubGhzDProtos : public FProtoListGeneral { public: - SubGhzDProtos(const SubGhzDProtos&) { SubGhzDProtos(); }; // won't use, but makes compiler happy - SubGhzDProtos& operator=(const SubGhzDProtos&) { return *this; } // won't use, but makes compiler happy + SubGhzDProtos(const SubGhzDProtos&) = delete; + SubGhzDProtos& operator=(const SubGhzDProtos&) = delete; SubGhzDProtos() { // add protos protos[FPS_PRINCETON] = new FProtoSubGhzDPrinceton(); @@ -107,6 +109,8 @@ class SubGhzDProtos : public FProtoListGeneral { protos[FPS_LEGRAND] = new FProtoSubGhzDLegrand(); protos[FPS_GANGQI] = new FProtoSubGhzDGangqi(); protos[FPS_MARANTEC24] = new FProtoSubGhzDMarantec24(); + protos[FPS_HOLTEKHT6P20B] = new FProtoSubGhzDHoltekHt6p20b(); + protos[FPS_RESTAURANT_PAGER] = new FProtoSubGhzDRestaurantPager(); for (uint8_t i = 0; i < FPS_COUNT; ++i) { if (protos[i] != NULL) protos[i]->setCallback(callbackTarget); diff --git a/firmware/baseband/fprotos/subghztypes.hpp b/firmware/baseband/fprotos/subghztypes.hpp index 0eb1ec139..89cdcb53d 100644 --- a/firmware/baseband/fprotos/subghztypes.hpp +++ b/firmware/baseband/fprotos/subghztypes.hpp @@ -58,6 +58,8 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t { FPS_SOMIFY_TELIS, FPS_GANGQI, FPS_MARANTEC24, + FPS_HOLTEKHT6P20B, + FPS_RESTAURANT_PAGER, FPS_COUNT }; diff --git a/firmware/baseband/fprotos/weatherprotos.hpp b/firmware/baseband/fprotos/weatherprotos.hpp index 98dec833e..5f5c70ac4 100644 --- a/firmware/baseband/fprotos/weatherprotos.hpp +++ b/firmware/baseband/fprotos/weatherprotos.hpp @@ -41,8 +41,8 @@ So include here the .hpp, and add a new element to the protos vector in the cons class WeatherProtos : public FProtoListGeneral { public: - WeatherProtos(const WeatherProtos&) { WeatherProtos(); }; // won't use, but makes compiler happy - WeatherProtos& operator=(const WeatherProtos&) { return *this; } // won't use, but makes compiler happy + WeatherProtos(const WeatherProtos&) = delete; + WeatherProtos& operator=(const WeatherProtos&) = delete; WeatherProtos() { // add protos protos[FPW_NexusTH] = new FProtoWeatherNexusTH(); diff --git a/firmware/baseband/proc_am_audio.cpp b/firmware/baseband/proc_am_audio.cpp index 95cbe3b58..a4bb87487 100644 --- a/firmware/baseband/proc_am_audio.cpp +++ b/firmware/baseband/proc_am_audio.cpp @@ -29,6 +29,16 @@ #include #include "dsp_hilbert.hpp" +// Phase 2: Constructor to start threads AFTER object is fully initialized +NarrowbandAMAudio::NarrowbandAMAudio() { + // Initialize members that threads might access + channel_spectrum.set_decimation_factor(1); + + // Start threads AFTER everything initialized + baseband_thread.start(); + rssi_thread.start(); +} + void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) { if (!configured) { return; diff --git a/firmware/baseband/proc_am_audio.hpp b/firmware/baseband/proc_am_audio.hpp index 8f651bb5b..296094889 100644 --- a/firmware/baseband/proc_am_audio.hpp +++ b/firmware/baseband/proc_am_audio.hpp @@ -37,6 +37,8 @@ class NarrowbandAMAudio : public BasebandProcessor { public: + NarrowbandAMAudio(); // Phase 2: Explicit constructor for manual thread start + void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; @@ -74,8 +76,14 @@ class NarrowbandAMAudio : public BasebandProcessor { SpectrumCollector channel_spectrum{}; /* NB: Threads should be the last members in the class definition. */ +#ifdef PRALINE + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, + /*auto_start*/ false}; // Phase 2: Manual start + RSSIThread rssi_thread{/*auto_start*/ false}; // Phase 2: Manual start +#else BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; RSSIThread rssi_thread{}; +#endif void configure(const AMConfigureMessage& message); void capture_config(const CaptureConfigMessage& message); diff --git a/firmware/baseband/proc_aprsrx.cpp b/firmware/baseband/proc_aprsrx.cpp index 60b048b02..fba43ebdd 100644 --- a/firmware/baseband/proc_aprsrx.cpp +++ b/firmware/baseband/proc_aprsrx.cpp @@ -27,7 +27,8 @@ #include "event_m4.hpp" -#include "stdio.h" +APRSRxProcessor::APRSRxProcessor() { +} void APRSRxProcessor::execute(const buffer_c8_t& buffer) { // This is called at 3072000 / 2048 = 1500Hz diff --git a/firmware/baseband/proc_aprsrx.hpp b/firmware/baseband/proc_aprsrx.hpp index 6eca9be7e..58b6599cf 100644 --- a/firmware/baseband/proc_aprsrx.hpp +++ b/firmware/baseband/proc_aprsrx.hpp @@ -74,6 +74,7 @@ static uint16_t crc_ccitt_tab[256] = { class APRSRxProcessor : public BasebandProcessor { public: + APRSRxProcessor(); void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; diff --git a/firmware/baseband/proc_ook_stream_tx.cpp b/firmware/baseband/proc_bint_stream_tx.cpp similarity index 71% rename from firmware/baseband/proc_ook_stream_tx.cpp rename to firmware/baseband/proc_bint_stream_tx.cpp index a82f413c4..244451aca 100644 --- a/firmware/baseband/proc_ook_stream_tx.cpp +++ b/firmware/baseband/proc_bint_stream_tx.cpp @@ -19,7 +19,7 @@ * Boston, MA 02110-1301, USA. */ -#include "proc_ook_stream_tx.hpp" +#include "proc_bint_stream_tx.hpp" #include "sine_table_int8.hpp" #include "portapack_shared_memory.hpp" @@ -27,27 +27,35 @@ #include "utility.hpp" -OOKProcessorStreamed::OOKProcessorStreamed() { +BinaryTimedProcessorStreamed::BinaryTimedProcessorStreamed() { configured = false; baseband_thread.start(); } -inline void OOKProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) { - int8_t re, im; - - if (bit_value) { - phase = (phase + 200); // What ? +inline void BinaryTimedProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) { + int8_t re = 0, im = 0; + if (mode == 0) { + if (bit_value) { + phase = (phase + 200); + sphase = phase + (64 << 18); + re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]); + im = (sine_table_i8[(phase & 0x03FC0000) >> 18]); + } + } else if (mode == 1) { + // calculate the re, im based on the deviation uint32_t variable to get the re, im, to send out 2fsk signal. based on the bit_value + if (bit_value) { + phase += deviation_delta; + } else { + phase -= deviation_delta; + } sphase = phase + (64 << 18); re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]); im = (sine_table_i8[(phase & 0x03FC0000) >> 18]); - } else { - re = 0; - im = 0; } buffer.p[i] = {re, im}; } -void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) { +void BinaryTimedProcessorStreamed::execute(const buffer_c8_t& buffer) { if (!configured || !stream) return; for (size_t i = 0; i < buffer.count; i++) { @@ -87,7 +95,7 @@ void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) { } } -void OOKProcessorStreamed::on_message(const Message* const message) { +void BinaryTimedProcessorStreamed::on_message(const Message* const message) { switch (message->id) { case Message::ID::ReplayConfig: configured = false; @@ -100,12 +108,23 @@ void OOKProcessorStreamed::on_message(const Message* const message) { shared_memory.application_queue.push(txprogress_message); break; + case Message::ID::StreamTXConfiguration: + streamtx_config(*reinterpret_cast(message)); + break; + default: break; } } -void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) { +void BinaryTimedProcessorStreamed::streamtx_config(const StreamTXConfigurationMessage& message) { + mode = message.mode; + deviation = message.deviation; + uint64_t big_calc = (uint64_t)message.deviation << 26; + deviation_delta = (uint32_t)(big_calc / OOK_SAMPLERATE); +} + +void BinaryTimedProcessorStreamed::replay_config(const ReplayConfigMessage& message) { if (message.config) { txprogress_message.progress = -2; shared_memory.application_queue.push(txprogress_message); @@ -121,7 +140,7 @@ void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) { } int main() { - EventDispatcher event_dispatcher{std::make_unique()}; + EventDispatcher event_dispatcher{std::make_unique()}; event_dispatcher.run(); return 0; } diff --git a/firmware/baseband/proc_ook_stream_tx.hpp b/firmware/baseband/proc_bint_stream_tx.hpp similarity index 84% rename from firmware/baseband/proc_ook_stream_tx.hpp rename to firmware/baseband/proc_bint_stream_tx.hpp index 44c1629e7..09ad51092 100644 --- a/firmware/baseband/proc_ook_stream_tx.hpp +++ b/firmware/baseband/proc_bint_stream_tx.hpp @@ -34,10 +34,11 @@ #include #define OOK_SAMPLERATE 2280000U +#define FM_DEVIATION 60000U -class OOKProcessorStreamed : public BasebandProcessor { +class BinaryTimedProcessorStreamed : public BasebandProcessor { public: - OOKProcessorStreamed(); + BinaryTimedProcessorStreamed(); void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; @@ -53,6 +54,11 @@ class OOKProcessorStreamed : public BasebandProcessor { std::unique_ptr stream{}; bool configured{false}; void replay_config(const ReplayConfigMessage& message); + void streamtx_config(const StreamTXConfigurationMessage& message); + + uint8_t mode = 0; // am = 0, 2fsk = 1 + uint32_t deviation = FM_DEVIATION; // used in 2fsk + uint32_t deviation_delta = (FM_DEVIATION * 4294967296ULL) / OOK_SAMPLERATE; int32_t endsignals[3] = {0, 42069, 613379}; // 0 is skipped, count from 1, don't ask... uint8_t readerrs = 0; // to count in the array diff --git a/firmware/baseband/proc_ble_tx.cpp b/firmware/baseband/proc_ble_tx.cpp index c789bbfa1..612d0ffb7 100644 --- a/firmware/baseband/proc_ble_tx.cpp +++ b/firmware/baseband/proc_ble_tx.cpp @@ -195,7 +195,7 @@ void BTLETxProcessor::scramble(char* bit_in, int num_bit, int channel_number, ch void BTLETxProcessor::disp_bit_in_hex(char* bit, int num_bit) { int i, a; - for (i = 0; i < num_bit; i = i + 8) { + for (i = 0; i + 7 < num_bit; i = i + 8) { a = bit[i] + bit[i + 1] * 2 + bit[i + 2] * 4 + bit[i + 3] * 8 + bit[i + 4] * 16 + bit[i + 5] * 32 + bit[i + 6] * 64 + bit[i + 7] * 128; data_message.is_data = true; diff --git a/firmware/baseband/proc_epirb_tx.cpp b/firmware/baseband/proc_epirb_tx.cpp new file mode 100644 index 000000000..90a603a2b --- /dev/null +++ b/firmware/baseband/proc_epirb_tx.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "proc_epirb_tx.hpp" +#include "portapack_shared_memory.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" + +#include +#include + +/** + * Processing method for this processor + */ +void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + // Iterate on each sample of the buffer + for (size_t i = 0; i < buffer.count; i++) { + if (end_of_transmission) { + // Stop transmission + configured = false; + end_of_transmission = false; + txprogress_message.done = true; + shared_memory.application_queue.push(txprogress_message); + } + + if (mode_bpsk) { + // BPSK Manchester beacon signal + if (bpsk_pre_count < config_pre_count) { + // Pre-count state: send a negative phase carrier during pre-count + bpsk_pre_count++; + re = i_neg; + im = q_neg; + } else if (bpsk_post_count > 0) { + // Post-count: send a negative phase carrier during post-count + bpsk_post_count++; + re = i_neg; + im = q_neg; + if (bpsk_post_count >= config_post_count) { + // End transmission here + byte_index = 0; + bpsk_post_count = 0; + bpsk_pre_count = 0; + end_of_transmission = true; + } + } else { + if (sample_counter == 0 && manchester_half == false) { + if (bit_index == 0) { + // Read current byte + current_byte = frame_data[byte_index]; + // Move to next byte + byte_index++; + } + // Get current bit + current_bit = (current_byte >> (7 - bit_index)) & 0x01; + } + + // Manchester encoding + if (current_bit == 1) { + // 1 = falling signal + if (manchester_half == false) { + re = i_pos; + im = q_pos; + } else { + re = i_neg; + im = q_neg; + } + } else { + // 0 = rising signal + if (manchester_half == false) { + re = i_neg; + im = q_neg; + } else { + re = i_pos; + im = q_pos; + } + } + // Move to next sample + sample_counter++; + + if (sample_counter >= samples_per_halfbit) { + // Move to next half-bit + sample_counter = 0; + manchester_half = !manchester_half; + + // Next bit after two half bits + if (manchester_half == false) { + // Move to next bit + bit_index++; + if (bit_index >= 8) { + // End of byte + bit_index = 0; + if (byte_index >= frame_data_len) { + // End of frame => move to post-count + bpsk_post_count = 1; + } + } + } + } + } + } else { + // AM 127.5 MHz sine sweep + // ---- 3 Hz Sweep ---- + sweep_phase += sweep_inc; + uint8_t sweep_index = (sweep_phase & 0xFF000000) >> 24; + int8_t sweep = sine_table_i8[sweep_index]; // -128..127 + + // Audio frequency based on sweep + int32_t audio_freq = center_freq + sweep * freq_dev; + + // ---- Audio signal (sine wave) ---- + uint32_t audio_inc = audio_freq * freq_scale; + audio_phase += audio_inc; + + uint8_t audio_index = (audio_phase & 0xFF000000) >> 24; + int8_t audio = sine_table_i8[audio_index]; + + // ---- AM ---- + // Double Side Band modulation with modulation index of ~80% (100/128) + offset (74) + int16_t amplitude = 74 + ((100 * audio) >> 7); // 1/128 via shift + + if (amplitude > 127) amplitude = 127; + if (amplitude < -128) amplitude = -128; + + re = (int8_t)amplitude; + im = 0; + } + buffer.p[i] = {re, im}; + } +}; + +void EPIRBTXProcessor::on_message(const Message* const msg) { + // Configure the processor + switch (msg->id) { + case Message::ID::EPIRBTXData: { + const auto message = *reinterpret_cast(msg); + // Check transmission mode + mode_bpsk = message.mode_bpsk; + if (mode_bpsk) { + // BPSK mode for 406 frame + config_pre_count = message.pre_count; + config_post_count = message.post_count; + frame_data_len = message.data_len; + // Get the frame data from the message + memcpy(frame_data, message.data, std::min(frame_data_len, EPIRBTXDataMessage::max_len)); + // Init BPSK sequencer + sample_counter = 0; + bpsk_pre_count = 0; + bpsk_post_count = 0; + bit_index = 0; + byte_index = 0; + current_byte = 0; + current_bit = 0; + } else { + // AM mode for 121.5 signal => init AM sequencer + sweep_phase = 0; + audio_phase = 0; + } + // Tell the processor to start + configured = true; + } break; + + default: + break; + } +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_epirb_tx.hpp b/firmware/baseband/proc_epirb_tx.hpp new file mode 100644 index 000000000..1b624b56e --- /dev/null +++ b/firmware/baseband/proc_epirb_tx.hpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __PROC_EPIRB_TX_H__ +#define __PROC_EPIRB_TX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "portapack_shared_memory.hpp" +#include "tonesets.hpp" +#include + +/** + * Processor used by epirb_tx app to simulate a COSPAS/SARSAT emergency beacon + * The processor will alternatively: + * - Send a 406 MHz Manchester encoded BPSK signal containing the beacon information + * - Send a 127.5 MHz AM distress audio signal + */ +class EPIRBTXProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg) override; + + private: + // True when the processor has received a configuration message from the app + bool configured{false}; + // True when in BPSK transmission mode, false for AM transmission mode + bool mode_bpsk{false}; + + // True when the transmission has to be stopped (e.g. at the end of a frame) + bool end_of_transmission{}; + // I/Q values for current sample (ranging from -128 to 127) + int8_t re{0}, im{0}; + + // Configured pre-count value: used to generate a carrier for config_pre_count samples before starting a frame + uint32_t config_pre_count = 0; + // Configured post-count value: used to continue the carrier for config_post_count samples after the end of a frame + uint32_t config_post_count = 0; + + // Data of the frame to send in BPSK mode + uint8_t frame_data[18]{0}; + // Size of the frame to send in BPSK mode + uint8_t frame_data_len = 0; + + // BPSK parameters: Target phase +/-63Β° as per COSPAS/SARSAT specifications + static constexpr float phase_rad = 63.0f * M_PI / 180.0f; + + // I/Q values for BPSK (positive phase and negative phase) + int8_t i_pos = (int8_t)(cos(phase_rad) * 127); + int8_t q_pos = (int8_t)(sin(phase_rad) * 127); + int8_t i_neg = i_pos; + int8_t q_neg = -q_pos; + + // COSPAS/SARSAT signal is manchester (2 states per bit) encoded 400 bit/sec + static const uint32_t samples_per_halfbit = TONES_SAMPLERATE / 400 / 2; + // Sequencer state for BPSK + uint32_t sample_counter = 0; + uint32_t bpsk_pre_count = 0; + uint32_t bpsk_post_count = 0; + // Bit position in current byte + uint32_t bit_index = 0; + // Byte position in current frame + uint32_t byte_index = 0; + // Value of the current byte + uint8_t current_byte = 0; + // Value of the current bit + uint8_t current_bit = 0; + // Position in the current manchester bit + bool manchester_half = false; // false = first half + + // 127.5 AM signal parameters + static const uint32_t sweep_rate = 3; // 3 Hz + static const uint32_t f_min = 300; // Sweep min frequency (Hz) + static const uint32_t f_max = 1600; // Sweep max frequency (Hz) + static const uint32_t freq_span = f_max - f_min; + // Frequency + static const uint32_t freq_scale = (1ULL << 32) / TONES_SAMPLERATE; + static const int32_t center_freq = f_min + (freq_span / 2); + static const int32_t freq_dev = freq_span / 256; + // Increments + static const uint32_t sweep_inc = sweep_rate * freq_scale; + + // Phase accumulators for sweep and audio + uint32_t sweep_phase = 0; + uint32_t audio_phase = 0; + + TXProgressMessage txprogress_message{}; + + /* NB: Threads should be the last members in the class definition. */ + BasebandThread baseband_thread{TONES_SAMPLERATE, this, baseband::Direction::Transmit}; +}; + +#endif diff --git a/firmware/baseband/proc_flash_utility.cpp b/firmware/baseband/proc_flash_utility.cpp index 32cbd50ca..69096f35b 100644 --- a/firmware/baseband/proc_flash_utility.cpp +++ b/firmware/baseband/proc_flash_utility.cpp @@ -29,7 +29,7 @@ #include "portapack_shared_memory.hpp" #define PAGE_LEN 256U -#define NUM_PAGES 4096U +#define NUM_PAGES (4096U * FLASH_SIZE_MB) void initialize_flash(); void erase_flash(); diff --git a/firmware/baseband/proc_mictx.cpp b/firmware/baseband/proc_mictx.cpp index 4be4a81cb..8e17b34fa 100644 --- a/firmware/baseband/proc_mictx.cpp +++ b/firmware/baseband/proc_mictx.cpp @@ -101,6 +101,10 @@ void MicTXProcessor::on_message(const Message* const msg) { switch (msg->id) { case Message::ID::AudioTXConfig: + if (modulator) { + delete modulator; + modulator = NULL; + } if (fm_enabled) { dsp::modulate::FM* fm = new dsp::modulate::FM(); diff --git a/firmware/baseband/proc_morse.cpp b/firmware/baseband/proc_morse.cpp new file mode 100644 index 000000000..2d16dad2a --- /dev/null +++ b/firmware/baseband/proc_morse.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2026 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "proc_morse.hpp" +#include "audio_dma.hpp" +#include "portapack_shared_memory.hpp" +#include "event_m4.hpp" +#include + +void MorseProcessor::configure(uint8_t mode) { + configured = false; + + modulation = static_cast((uint8_t)mode); + + if (modulation == ModulationMode::FM) { // FM + decim_0.configure(taps_11k0_decim_0.taps); + decim_1.configure(taps_11k0_decim_1.taps); + channel_filter.configure(taps_11k0_channel.taps, 2); + demod_cw_fm.configure(24000, 5000); + } else { + decim_0.configure(taps_4k25_decim_0.taps); + decim_1.configure(taps_4k25_decim_1.taps); + if (modulation == ModulationMode::AM) { // AM + channel_filter.configure(taps_2k0_am_lpf_channel.taps, 4); + } else { // SSB, DSB + if (modulation == ModulationMode::DSB) // DSB + channel_filter.configure(taps_1k5_dsb_lpf.taps, 4); + else if (modulation == ModulationMode::USB) // USB + channel_filter.configure(taps_1k5_USB_channel.taps, 4); + else // LSB + channel_filter.configure(taps_1k5_LSB_channel.taps, 4); + } + } + + if (modulation == ModulationMode::FM) + audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f); + else + audio_output.configure(audio_12k_hpf_300hz_config); + + meas_samples_in_period = 0; + meas_last_period_len = 0; + meas_consistency_count = 0; + meas_signal_state_high = false; + meas_freq_accumulator = 0.0f; + meas_freq_count = 0; + ui_update_timer = 0; + + // Decoding reset + s_prev_i = 0; + s_prev2_i = 0; + goertzel_count = 0; + duration_samples = 0; + was_signaling = false; + + // Thresholds + noise_floor = 5000; + startup_delay = 20; + squelch_is_open = true; + squelch_hold = 0; + + dc_average = 0.0f; + + current_freq = 700.0f; + update_goertzel_coeff(current_freq); + + configured = true; +} + +inline buffer_f32_t MorseProcessor::demodulate(const buffer_c16_t& channel) { + // AM,SSB,DSB always keeps squelch "technically" open for the demodulator + if (modulation == ModulationMode::AM || modulation == ModulationMode::DSB) { + squelch_is_open = true; + return demod_AM.execute(channel, audio_buffer); + } else { + if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB) { + squelch_is_open = true; + return demod_ssb.execute(channel, audio_buffer); + } + } + return demod_cw_fm.execute(channel, audio_buffer); +} + +void MorseProcessor::update_goertzel_coeff(float freq) { + // limit to algo capacity min/max + if (freq < 300.0f) freq = 300.0f; + if (freq > 2300.0f) freq = 2300.0f; + + float sample_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f; + float omega = 2.0f * M_PI * freq / sample_rate; + float omega_sq = omega * omega; + float cos_approx = 1.0f - (omega_sq * 0.5f); + // 16384 is the scaling factor for the Goertzel integer math + coeff_int = (int32_t)(2.0f * cos_approx * 16384.0f); +} + +void MorseProcessor::measure_frequency(int32_t sample) { + // Noise gate threshold + const int32_t gate_threshold = (modulation == ModulationMode::FM) ? 4000 : 2000; + + if (sample > gate_threshold || sample < -gate_threshold) { + if (sample > 0 && !meas_signal_state_high) { + // Rising Edge (End of Period) + meas_signal_state_high = true; + + if (meas_samples_in_period > 0) { + bool period_is_stable = false; + if (meas_last_period_len > 0) { + int32_t diff = std::abs((int)meas_samples_in_period - (int)meas_last_period_len); + if (diff <= 2) { + meas_consistency_count++; + period_is_stable = true; + } else { + meas_consistency_count = 0; + } + } + meas_last_period_len = meas_samples_in_period; + + // Wait for AT LEAST 3 STABLE CYCLES + if (period_is_stable && meas_consistency_count > 5) { + float base_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f; + float inst_freq = base_rate / (float)meas_samples_in_period; + if (modulation == ModulationMode::DSB) { + inst_freq /= 2.0f; + } + // Check for overflows + if (inst_freq > 250 && inst_freq < 3000) { + meas_freq_accumulator += inst_freq; + meas_freq_count++; + } + } + } + meas_samples_in_period = 0; + + } else if (sample < 0) { + meas_signal_state_high = false; + } + } else { + // Silence detection + if (meas_samples_in_period > 200) { + meas_last_period_len = 0; + meas_consistency_count = 0; + } + } + + meas_samples_in_period++; + + ui_update_timer++; + uint32_t update_limit = (modulation == ModulationMode::FM) ? 4800 : 2400; // ~200ms + + if (ui_update_timer > update_limit) { + if (meas_freq_count > 0) { + float avg_freq = meas_freq_accumulator / (float)meas_freq_count; + current_freq = avg_freq; + // Round to 5Hz for display + uint32_t stable_disp = (uint32_t)avg_freq; + stable_disp = (stable_disp / 5) * 5; + + freq_message.measured_frequency = stable_disp; + shared_memory.application_queue.push(freq_message); + } + + meas_freq_accumulator = 0.0f; + meas_freq_count = 0; + ui_update_timer = 0; + } +} + +void MorseProcessor::process_decoding(int32_t sample) { + update_goertzel_coeff(current_freq); + + // 1. Goertzel Algorithm + int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i; + s_prev2_i = s_prev_i; + s_prev_i = (int32_t)s; + goertzel_count++; + + // 2. Evaluation every 60 samples + if (goertzel_count >= 60) { + if (startup_delay > 0) { + startup_delay--; + // Fast learning phase + int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i - + (((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14); + noise_floor = (noise_floor * 15 + pwr) / 16; + } else { + // Power calculation + int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i - + (((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14); + + // Adaptive Noise Floor + if (!was_signaling) { + noise_floor = (noise_floor * 127 + power) / 128; + } + + // Threshold Calculation + int64_t sensitivity = 4 + (user_squelch_level / 10); + int64_t base_pwr_threshold = noise_floor * sensitivity; + int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold; + + // Tone Detection + bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000); + int32_t time_base = 250; + // State Change Logic + if (is_tone != was_signaling) { + int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3); + + // Send message if significant + if (duration_us > 10000 || was_signaling) { + message.state_durations[0] = was_signaling ? duration_us : -duration_us; + message.state_cnt = 1; // 1 indicates valid state duration data + shared_memory.application_queue.push(message); + } + was_signaling = is_tone; + duration_samples = 0; + } + + // Timeout Logic + if (!was_signaling && duration_samples > 28800) { + int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3); + message.state_durations[0] = -duration_us; + message.state_cnt = 1; + shared_memory.application_queue.push(message); + duration_samples = 0; + } + } + + duration_samples += 60; + s_prev_i = 0; + s_prev2_i = 0; + goertzel_count = 0; + } +} + +void MorseProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); + const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); + const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); + + auto audio_buf = demodulate(channel_out); + + for (size_t i = 0; i < audio_buf.count; i++) { + float raw_audio = audio_buf.p[i]; + + // Squelch Logic + int32_t raw_int_abs = (int32_t)(raw_audio * 32768.0f); + if (raw_int_abs < 0) raw_int_abs = -raw_int_abs; + + int32_t audio_threshold = (user_squelch_level * user_squelch_level) * 3; + + if (raw_int_abs > audio_threshold || user_squelch_level == 0) { + squelch_is_open = true; + squelch_hold = (modulation == ModulationMode::FM) ? 2400 : 1200; + } else { + if (squelch_hold > 0) + squelch_hold--; + else if (modulation == ModulationMode::FM) + squelch_is_open = false; + } + + float decode_audio = raw_audio; + if (modulation != ModulationMode::FM) { + float gain = 16.0f; + if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB) + gain = 5.0f; + decode_audio *= gain; + + // Hard Limiting / Clipping + message.clipped = false; + if (decode_audio > 1.0f) { + decode_audio = 1.0f; + message.clipped = true; + } else if (decode_audio < -1.0f) { + decode_audio = -1.0f; + } + audio_buf.p[i] = decode_audio; + } + + // DC BLOCKING + if (modulation != ModulationMode::FM) { + dc_average = (dc_average * 0.95f) + (decode_audio * 0.05f); + measure_frequency((int32_t)((decode_audio - dc_average) * 32768.0f)); + } else { + measure_frequency((int32_t)(raw_audio * 32768.0f)); + } + + process_decoding((int32_t)(decode_audio * 32768.0f)); + + if (modulation == ModulationMode::FM && !squelch_is_open) { + audio_buf.p[i] = 0.0f; // mute nfm on squelch + } + } + + audio_output.write(audio_buf); +} + +void MorseProcessor::on_message(const Message* const p) { + switch (p->id) { + case Message::ID::MorseRXConfig: { + auto morse_rx_msg = *reinterpret_cast(p); + configure(morse_rx_msg.mode); + break; + } + + case Message::ID::NBFMConfigure: { + auto nbfm_msg = *reinterpret_cast(p); + user_squelch_level = nbfm_msg.squelch_level; + audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f); + break; + } + + default: + break; + } +} + +int main() { + audio::dma::init_audio_out(); + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_morse.hpp b/firmware/baseband/proc_morse.hpp new file mode 100644 index 000000000..d2b02b161 --- /dev/null +++ b/firmware/baseband/proc_morse.hpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2026 Pezsma + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __PROC_MORSE_H__ +#define __PROC_MORSE_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "message.hpp" +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" +#include "audio_output.hpp" +#include "dsp_fir_taps.hpp" +#include "rssi_thread.hpp" + +class MorseProcessor : public BasebandProcessor { + public: + MorseProcessor() {} + + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const p) override; + + private: + void configure(uint8_t mode); + buffer_f32_t demodulate(const buffer_c16_t& channel); + void update_goertzel_coeff(float freq); + void measure_frequency(int32_t sample); + void process_decoding(int32_t sample); + + BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive}; + RSSIThread rssi_thread{}; + + std::array dst{}; + const buffer_c16_t dst_buffer{ + dst.data(), + dst.size()}; + std::array audio{}; + const buffer_f32_t audio_buffer{ + audio.data(), + audio.size()}; + + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; + dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + dsp::decimate::FIRAndDecimateComplex channel_filter{}; + dsp::demodulate::AM demod_AM{}; + dsp::demodulate::FM demod_cw_fm{}; + dsp::demodulate::SSB demod_ssb{}; + AudioOutput audio_output{}; + + bool configured{false}; + + enum class ModulationMode : uint8_t { + AM = 0, + FM = 1, + DSB = 2, + USB = 3, + LSB = 4 + }; + + ModulationMode modulation = ModulationMode::AM; + int32_t user_squelch_level{0}; + bool squelch_is_open{true}; + int32_t squelch_hold{0}; + + // frequency measurement variables + uint32_t meas_samples_in_period{0}; + bool meas_signal_state_high{false}; + uint32_t meas_last_period_len{0}; + uint32_t meas_consistency_count{0}; + float meas_freq_accumulator{0.0f}; + uint32_t meas_freq_count{0}; + uint32_t ui_update_timer{0}; + float current_freq{700.0f}; + + // --- Decoding variables (Goertzel) --- + int32_t coeff_int{0}; + int32_t s_prev_i{0}; + int32_t s_prev2_i{0}; + uint32_t goertzel_count{0}; + uint32_t duration_samples{0}; + bool was_signaling{false}; + int64_t noise_floor{5000}; + int32_t startup_delay{20}; + + float dc_average = 0.0f; // DC level tracking + int32_t dc_average_int = 0; + + MorseRXDataMessage message{}; + MorseRXfreqMessage freq_message{}; +}; + +#endif \ No newline at end of file diff --git a/firmware/baseband/proc_morsetx.cpp b/firmware/baseband/proc_morsetx.cpp new file mode 100644 index 000000000..8532f1955 --- /dev/null +++ b/firmware/baseband/proc_morsetx.cpp @@ -0,0 +1,97 @@ +#include "proc_morsetx.hpp" +#include "portapack_shared_memory.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" + +#include + +void MorseTXProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + for (size_t i = 0; i < buffer.count; i++) { + int8_t sample_sin; + int8_t sample_cos; + + sample_sin = (sine_table_i8[(tone_phase & 0xFF000000) >> 24]); + tone_phase += tone_delta; + + im = 0; + re = 0; + + // modulation logic + if (modulation == 0) { // AM modulation + if (key_down) { + re = 64 + (sample_sin >> 2); + } + } else if (modulation == 1) { // FM modulation + if (key_down) { + delta = sample_sin * fm_delta; + } else { + delta = 0; + } + + phase += delta; + sphase = phase + (64 << 24); + + re = (sine_table_i8[(sphase & 0xFF000000) >> 24]); + im = (sine_table_i8[(phase & 0xFF000000) >> 24]); + + } else if (modulation == 2) { // DSB + if (key_down) { + re = sample_sin; + } + } else if (modulation == 3) { // USB + if (key_down) { + sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]); + re = sample_cos; + im = sample_sin; + } + } else if (modulation == 4) { // LSB + if (key_down) { + sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]); + re = sample_cos; + im = (sine_table_i8[((tone_phase + 0x80000000) & 0xFF000000) >> 24]); + } + } + buffer.p[i] = {re, im}; + } +} + +void MorseTXProcessor::on_message(const Message* const p) { + switch (p->id) { + case Message::ID::MorseTXConfigure: { + auto message = *reinterpret_cast(p); + tone_delta = message.tone * 1398; // scale + tone = message.tone; // audio tone + modulation = message.modulation; + if (message.fm_delta == 0 && modulation == 1) { + fm_delta = 90000; + } else { + uint64_t scale = 0xFFFFFFFFULL; // 32-bit max + fm_delta = (uint32_t)((uint64_t)message.fm_delta * (scale / 1536000)); + } + break; + } + + case Message::ID::MorseTXkey: { + auto key = *reinterpret_cast(p); + key_down = key.key_down; + configured = true; + if (key_down) + audio::dma::beep_start(tone, 24000, 0); + else + audio::dma::beep_stop(); + break; + } + + default: + break; + } +} + +int main() { + audio::dma::init_audio_out(); + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_morsetx.hpp b/firmware/baseband/proc_morsetx.hpp new file mode 100644 index 000000000..2213b7652 --- /dev/null +++ b/firmware/baseband/proc_morsetx.hpp @@ -0,0 +1,33 @@ +#ifndef __PROC_MORSETX_H__ +#define __PROC_MORSETX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "portapack_shared_memory.hpp" +#include "audio_output.hpp" +#include "audio_dma.hpp" + +#define AUDIO_OUTPUT_BUFFER_SIZE 32 +#define AUDIO_SAMPLING_RATE 24000 + +class MorseTXProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg) override; + + private: + bool configured{false}; + bool key_down{false}; // currently the virtual key is pressed or not. + + int8_t sample{0}, re{0}, im{0}; + uint8_t modulation{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB + uint32_t tone_delta{0}; // shifting value by tone + uint32_t fm_delta{}; + uint32_t tone_phase{0}; + uint32_t tone{0}; // audio tone frequeny + int32_t phase{0}, sphase{0}, delta{0}; // sample generation. + + BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit}; +}; + +#endif \ No newline at end of file diff --git a/firmware/baseband/proc_nfm_audio.cpp b/firmware/baseband/proc_nfm_audio.cpp index 47e1f7d41..72a156599 100644 --- a/firmware/baseband/proc_nfm_audio.cpp +++ b/firmware/baseband/proc_nfm_audio.cpp @@ -31,6 +31,18 @@ #include #include +#ifdef PRALINE +// Phase 2: Constructor to start threads AFTER object is fully initialized +NarrowbandFMAudio::NarrowbandFMAudio() { + // Initialize members that threads might access + channel_spectrum.set_decimation_factor(1); + + // Start threads AFTER everything initialized + baseband_thread.start(); + rssi_thread.start(); +} +#endif + void NarrowbandFMAudio::execute(const buffer_c8_t& buffer) { // bool new_state; diff --git a/firmware/baseband/proc_nfm_audio.hpp b/firmware/baseband/proc_nfm_audio.hpp index 8ac185ce4..a66975503 100644 --- a/firmware/baseband/proc_nfm_audio.hpp +++ b/firmware/baseband/proc_nfm_audio.hpp @@ -41,6 +41,10 @@ class NarrowbandFMAudio : public BasebandProcessor { public: +#ifdef PRALINE + NarrowbandFMAudio(); // Phase 2: Explicit constructor for manual thread start +#endif + void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; @@ -97,8 +101,14 @@ class NarrowbandFMAudio : public BasebandProcessor { CodedSquelchMessage ctcss_message{0}; /* NB: Threads should be the last members in the class definition. */ +#ifdef PRALINE + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, + /*auto_start*/ false}; // Phase 2: Manual start + RSSIThread rssi_thread{/*auto_start*/ false}; // Phase 2: Manual start +#else BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; RSSIThread rssi_thread{}; +#endif void pitch_rssi_config(const PitchRSSIConfigureMessage& message); void configure(const NBFMConfigureMessage& message); diff --git a/firmware/baseband/proc_p25_tx.cpp b/firmware/baseband/proc_p25_tx.cpp new file mode 100644 index 000000000..0901b217b --- /dev/null +++ b/firmware/baseband/proc_p25_tx.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 PortaPack-MAYHEM Contributors + * + * This file is part of PortaPack. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "proc_p25_tx.hpp" +#include "portapack_shared_memory.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" + +#include + +// Gray-coded dibit β†’ FM deviation level: +// dibit 0 β†’ +1 unit (+600 Hz) +// dibit 1 β†’ +3 units (+1800 Hz) +// dibit 2 β†’ -1 unit (-600 Hz) +// dibit 3 β†’ -3 units (-1800 Hz) +static constexpr int8_t dibit_to_level[4] = {1, 3, -1, -3}; + +void P25TxProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) { + // Zero output - never emit garbage while idle + for (size_t i = 0; i < buffer.count; i++) + buffer.p[i] = {0, 0}; + return; + } + + for (size_t i = 0; i < buffer.count; i++) { + if (sample_count == 0) { + if (dibit_index >= frame_length) { + // Frame complete - notify A7 and stop + txprogress_message.done = true; + shared_memory.application_queue.push(txprogress_message); + configured = false; + for (size_t j = i; j < buffer.count; j++) + buffer.p[j] = {0, 0}; + return; + } + } + + // Current dibit β†’ frequency level + uint8_t dibit = shared_memory.bb_data.data[dibit_index] & 0x03; + int8_t level = dibit_to_level[dibit]; + + // Accumulate FM phase at Β±level Γ— base_phase_step per sample + phase += (uint32_t)((int32_t)level * (int32_t)base_phase_step); + + int8_t re = sine_table_i8[(phase + 0x40000000) >> 24]; // cos + int8_t im = sine_table_i8[phase >> 24]; // sin + buffer.p[i] = {re, im}; + + sample_count++; + if (sample_count >= P25_SAMPLES_PER_DIBIT) { + sample_count = 0; + dibit_index++; + } + } +} + +void P25TxProcessor::on_message(const Message* const msg) { + if (msg->id == Message::ID::P25TxConfigure) { + const auto& m = *reinterpret_cast(msg); + if (m.frame_length == 0) { + configured = false; + return; + } + frame_length = m.frame_length; + dibit_index = 0; + sample_count = 0; + txprogress_message.done = false; + configured = true; + } +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_p25_tx.hpp b/firmware/baseband/proc_p25_tx.hpp new file mode 100644 index 000000000..8b777dc9e --- /dev/null +++ b/firmware/baseband/proc_p25_tx.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 PortaPack-MAYHEM Contributors + * + * This file is part of PortaPack. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __PROC_P25_TX_H__ +#define __PROC_P25_TX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "message.hpp" + +// P25 C4FM: 4800 baud, 2.4 Msps = 500 samples per dibit +#define P25_SAMPLERATE 2400000 +#define P25_SAMPLES_PER_DIBIT 500 + +class P25TxProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg) override; + + private: + bool configured{false}; + uint16_t frame_length{0}; // total dibits in frame + uint16_t dibit_index{0}; // current dibit being transmitted + uint32_t sample_count{0}; // samples emitted for current dibit + + // FM phase accumulator (32-bit wrapping) + uint32_t phase{0}; + + // Phase step per sample per deviation unit (600 Hz at 2.4 Msps) + // = round(600 * 2^32 / 2400000) = 1073742 + static constexpr uint32_t base_phase_step{1073742}; + + TXProgressMessage txprogress_message{}; + + /* NB: Threads should be the last members in the class definition. */ + BasebandThread baseband_thread{P25_SAMPLERATE, this, baseband::Direction::Transmit}; +}; + +#endif /* __PROC_P25_TX_H__ */ diff --git a/firmware/baseband/proc_rtty_rx.cpp b/firmware/baseband/proc_rtty_rx.cpp new file mode 100644 index 000000000..baa7ee0c4 --- /dev/null +++ b/firmware/baseband/proc_rtty_rx.cpp @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "proc_rtty_rx.hpp" +#include "portapack_shared_memory.hpp" +#include "audio_dma.hpp" +#include "event_m4.hpp" + +// RTTY Timing Limits (at 24kHz) +static constexpr uint32_t MIN_VALID_PULSE = 200; +static constexpr uint32_t MAX_VALID_PULSE = 800; + +void RTTYRxProcessor::configure() { + configured = false; + + baseband_thread.set_sampling_rate(baseband_fs); + + // 1. 3.072M -> 384k + decim_0.configure(taps_4k25_decim_0.taps); + + // 2. 384k -> 48k + decim_1.configure(taps_4k25_decim_1.taps); + + // 3. 48k -> 24k + channel_filter.configure(taps_11k0_channel.taps, 2); + + // FM Demodulator + demod.configure(24000, 9000); + + // Audio Output + audio_output.configure(iir_config_passthrough, iir_config_passthrough, 1.0f); + + // Reset State + val_max = -200000; + val_min = 200000; + + uart_state = WAIT_START; + + inverted_polarity = false; + // Default to standard 45.45 baud + estimated_bit_width = 528; + samples_per_bit = 528; + pulse_measure_counter = 0; + + configured = true; +} + +// Variables for Fast-Lock Auto Baud +uint32_t candidate_width = 0; +uint8_t candidate_hits = 0; +uint32_t squelch_closed_timer = 0; +bool is_squelched = true; + +void RTTYRxProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); + buffer_c16_t decim_1_target{dst_buffer_data.data() + 256, 256}; + const auto decim_1_out = decim_1.execute(decim_0_out, decim_1_target); + const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); + + auto audio = demod.execute(channel_out, audio_buffer); + + feed_channel_stats(channel_out); + + for (size_t i = 0; i < audio.count; i++) { + int16_t sample = audio.p[i]; + + // DECODER INPUT + int32_t fm_val = (int32_t)sample * 32; + + // 1. ENVELOPE TRACKING (Floating) + if (fm_val > val_max) val_max = fm_val; + if (fm_val < val_min) val_min = fm_val; + + // 2. DECAY + // Shrink the envelope spread (Max - Min) slowly. + if (++decay_timer == 0) { + int32_t spread = val_max - val_min; + if (spread > 200) { + int32_t decay = (spread >> 7) + 1; + if (val_max > val_min + decay) val_max -= decay; + if (val_min < val_max - decay) val_min += decay; + } else { + // val_max += 100; + // val_min -= 100; + // temp off. but kept it for memory + } + } + + // 3. OFFSET CALCULATION + int32_t midpoint = (val_max + val_min) / 2; + + // 4. SIGNAL CENTERING + int32_t centered_val = fm_val - midpoint; + + // LPF for Slicer + fm_val_smoothed += (centered_val - fm_val_smoothed) >> LPF_ALPHA_SHIFT; + + // 5. SQUELCH + int32_t spread = val_max - val_min; + + if (is_squelched) { + if (spread > 600) is_squelched = false; + } else { + if (spread < 300) is_squelched = true; + } + + process_demodulated_sample(fm_val_smoothed); + + // 6. AUDIO PATH + if (is_squelched) { + audio.p[i] = 0; + } else { + int32_t audio_boost = sample * 48; + if (audio_boost > 32767) + audio_boost = 32767; + else if (audio_boost < -32768) + audio_boost = -32768; + audio.p[i] = (int16_t)audio_boost; + } + } + + audio_output.write(audio); + + // UI Update + if (tx_message.data_len > 0) { + if (baud_rate == 0 && samples_per_bit > 0) { + uint32_t b = (final_fs * 100) / samples_per_bit; + if (b > 4300 && b < 4700) + b = 4500; + else if (b > 4800 && b < 5200) + b = 5000; + else if (b > 7200 && b < 7800) + b = 7500; + tx_message.baud = (uint16_t)b; + } else { + tx_message.baud = baud_rate; + } + tx_message.shift = shift_hz; + + if (shared_memory.application_queue.push(tx_message)) { + tx_message.data_len = 0; + } + } +} + +void RTTYRxProcessor::process_demodulated_sample(int32_t sample) { + // 1. Squelch Check + if (is_squelched) { + squelch_closed_timer++; + if (squelch_closed_timer > 12000) { + uart_state = WAIT_START; + current_slicer_bit = 1; + pulse_measure_counter = 0; + inverted_polarity = false; + if (baud_rate == 0) { + estimated_bit_width = 528; + samples_per_bit = 528; + } + } + return; + } + + squelch_closed_timer = 0; + + // 2. Schmitt Trigger + int32_t hysteresis = (val_max - val_min) / 8; + + uint8_t raw_bit = current_slicer_bit; + if (inverted_polarity) raw_bit = !raw_bit; + + if (sample > hysteresis) + raw_bit = 1; + else if (sample < -hysteresis) + raw_bit = 0; + + // Polarity Check + if (raw_bit == 0) { + if (++polarity_timer > 7200) { + inverted_polarity = !inverted_polarity; + polarity_timer = 0; + val_max = -200000; + val_min = 200000; + uart_state = WAIT_START; + } + } else { + polarity_timer = 0; + } + + current_slicer_bit = inverted_polarity ? !raw_bit : raw_bit; + + // 3. Auto Baud + if (baud_rate == 0) { + pulse_measure_counter++; + if (current_slicer_bit != last_bit_state) { + update_baud_estimation(pulse_measure_counter); + pulse_measure_counter = 0; + last_bit_state = current_slicer_bit; + } + } + + // 4. UART State Machine + switch (uart_state) { + case WAIT_START: + if (current_slicer_bit == 0) { + phase_counter = samples_per_bit / 2; + uart_state = CHECK_START; + } + break; + + case CHECK_START: + if (--phase_counter == 0) { + if (current_slicer_bit == 0) { + phase_counter = samples_per_bit; + bit_counter = 0; + shift_reg = 0; + uart_state = READ_BITS; + } else { + uart_state = WAIT_START; + } + } + break; + + case READ_BITS: + if (--phase_counter == 0) { + if (current_slicer_bit) shift_reg |= (1 << bit_counter); + + phase_counter = samples_per_bit; + bit_counter++; + + if (bit_counter >= 5) { + uart_state = WAIT_STOP; + } + } + break; + + case WAIT_STOP: + if (--phase_counter == 0) { + // Accept data even if stop bit is noisy (0) + // This improves reception during fades + // if (current_slicer_bit == 1) { + append_data(shift_reg & 0x1F); + //} + uart_state = WAIT_START; + } + break; + } +} + +void RTTYRxProcessor::update_baud_estimation(uint32_t pulse_width) { + if (pulse_width < MIN_VALID_PULSE || pulse_width > MAX_VALID_PULSE) return; + + int32_t diff = (int32_t)pulse_width - (int32_t)estimated_bit_width; + if (diff < 0) diff = -diff; + + if (diff < (int32_t)(estimated_bit_width / 6)) { + estimated_bit_width = (estimated_bit_width * 7 + pulse_width) / 8; + samples_per_bit = estimated_bit_width; + candidate_hits = 0; + } else { + int32_t cand_diff = (int32_t)pulse_width - (int32_t)candidate_width; + if (cand_diff < 0) cand_diff = -cand_diff; + + if (cand_diff < (int32_t)(candidate_width / 8)) { + candidate_hits++; + if (candidate_hits >= 3) { + estimated_bit_width = (candidate_width + pulse_width) / 2; + samples_per_bit = estimated_bit_width; + candidate_hits = 0; + uart_state = WAIT_START; + } + } else { + candidate_width = pulse_width; + candidate_hits = 1; + } + } +} + +void RTTYRxProcessor::append_data(uint8_t raw_baudot_code) { + if (tx_message.data_len < tx_message.max_len) { + tx_message.data[tx_message.data_len] = raw_baudot_code; + tx_message.data_len++; + } +} + +void RTTYRxProcessor::on_message(const Message* const message) { + if (message->id == Message::ID::RTTYData) { + const auto& rtty_msg = static_cast(*message); + + if (rtty_msg.baud != baud_rate) { + baud_rate = rtty_msg.baud; + if (baud_rate > 0) { + const float real_baud = (float)baud_rate / 100.0f; + samples_per_bit = (uint32_t)((float)final_fs / real_baud); + estimated_bit_width = samples_per_bit; + } else { + estimated_bit_width = 528; + samples_per_bit = 528; + inverted_polarity = false; + } + uart_state = WAIT_START; + } + shift_hz = rtty_msg.shift; + if (!configured) { + configure(); + } + } +} + +int main() { + audio::dma::init_audio_out(); + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_rtty_rx.hpp b/firmware/baseband/proc_rtty_rx.hpp new file mode 100644 index 000000000..f7a66ed53 --- /dev/null +++ b/firmware/baseband/proc_rtty_rx.hpp @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __PROC_RTTY_RX_H__ +#define __PROC_RTTY_RX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" +#include "message.hpp" +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" +#include "audio_output.hpp" +#include "dsp_fir_taps.hpp" + +class RTTYRxProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const message) override; + + private: + static constexpr size_t baseband_fs = 3072000; + + // RTTY Config + uint16_t baud_rate = 0; // 0 = Auto-detect + uint16_t shift_hz = 170; + bool configured = false; + + // DSP Rates + // Stage 0: 3.072M / 8 = 384k + // Stage 1: 384k / 8 = 48k + // Stage 2: 48k / 2 = 24k (Audio/Demod) + static constexpr uint32_t decim_0_out_fs = baseband_fs / 8; + static constexpr uint32_t decim_1_out_fs = decim_0_out_fs / 8; + static constexpr uint32_t final_fs = decim_1_out_fs / 2; // 24000 Hz + + // Tuning Constants + static constexpr int32_t LPF_ALPHA_SHIFT = 2; + static constexpr int32_t MIN_SHIFT_SPREAD = 400; + + // Decimation Chain + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; + dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + dsp::decimate::FIRAndDecimateComplex channel_filter{}; + + // Demodulator & Audio + dsp::demodulate::FM demod{}; + AudioOutput audio_output{}; + + // Buffers + std::array dst_buffer_data{}; + const buffer_c16_t dst_buffer{dst_buffer_data.data(), dst_buffer_data.size()}; + + std::array audio_data{}; + const buffer_s16_t audio_buffer{audio_data.data(), audio_data.size()}; + + // Output Message + RTTYDataMessage tx_message{}; + + // Demodulator State + int32_t fm_val_smoothed = 0; + + // Tracker State + int32_t val_max = 0; + int32_t val_min = 0; + uint8_t decay_timer = 0; + + // Auto-Baud State + uint32_t pulse_measure_counter = 0; + uint8_t last_bit_state = 0; + uint32_t estimated_bit_width = 528; // ~45 baud @ 24k + + // UART State + enum UartState { + WAIT_START, + CHECK_START, + READ_BITS, + WAIT_STOP + }; + + UartState uart_state = WAIT_START; + uint32_t samples_per_bit = 528; + uint32_t phase_counter = 0; + uint8_t bit_counter = 0; + uint8_t shift_reg = 0; + uint8_t current_slicer_bit = 1; + + // Polarity + bool inverted_polarity = true; + uint32_t polarity_timer = 0; + + void configure(); + void process_demodulated_sample(int32_t sample); + void update_baud_estimation(uint32_t pulse_width); + void append_data(uint8_t raw_baudot_code); + + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; + RSSIThread rssi_thread{}; +}; + +#endif /*__PROC_RTTY_RX_H__*/ \ No newline at end of file diff --git a/firmware/baseband/proc_rtty_tx.cpp b/firmware/baseband/proc_rtty_tx.cpp new file mode 100644 index 000000000..0b650714d --- /dev/null +++ b/firmware/baseband/proc_rtty_tx.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + */ + +#include "proc_rtty_tx.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" +#include + +static constexpr uint32_t LEAD_IN_SAMPLES = 204800; + +static inline uint32_t hz_to_delta(int32_t hz, uint32_t fs) { + int64_t delta = ((int64_t)hz * (int64_t)UINT32_MAX) / (int64_t)fs; + return (uint32_t)delta; +} + +void RTTYTXProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) { + for (size_t i = 0; i < buffer.count; i++) { + buffer.p[i] = {0, 0}; + } + return; + } + + for (size_t i = 0; i < buffer.count; i++) { + bool advance = false; + + if (state == State::LeadIn) { + lead_counter++; + if (lead_counter >= LEAD_IN_SAMPLES) { + advance = true; + } + } else if (state == State::LeadOut) { + lead_counter++; + if (lead_counter >= 460000) { + txprogress_message.done = true; + shared_memory.application_queue.push(txprogress_message); + configured = false; + state = State::Idle; + } + } else if (state != State::Idle) { + uint32_t previous_phase = baud_phase; + baud_phase += baud_phase_increment; + + if (baud_phase < previous_phase) { + advance = true; + } + } + + if (advance) { + advance_state(); + } + + // Tone selection + uint32_t target_delta; + bool is_mark = true; + + switch (state) { + case State::StartBit: + is_mark = false; + break; + case State::DataBits: + is_mark = (current_char >> bit_pos) & 1; + break; + default: + is_mark = true; + break; + } + + target_delta = is_mark ? delta_mark : delta_space; + + // Slew limiter + int32_t diff = (int32_t)target_delta - (int32_t)current_delta; + int32_t abs_diff = diff < 0 ? -diff : diff; + + if (abs_diff <= (int32_t)slew_rate) { + current_delta = target_delta; + } else { + current_delta += (diff > 0) ? slew_rate : -slew_rate; + } + + phase += current_delta; + int8_t re = sine_table_i8[((phase + 0x40000000) & 0xFF000000) >> 24]; + int8_t im = sine_table_i8[(phase & 0xFF000000) >> 24]; + + buffer.p[i] = {re, im}; + } +} + +void RTTYTXProcessor::advance_state() { + switch (state) { + case State::Idle: + state = State::LeadIn; + lead_counter = 0; + break; + + case State::LeadIn: + if (buffer_pop(current_char)) { + state = State::StartBit; + baud_phase = 0; + baud_phase_increment = base_baud_phase_increment; + } else { + state = State::LeadOut; + lead_counter = 0; + } + break; + + case State::StartBit: + state = State::DataBits; + bit_pos = 0; + break; + + case State::DataBits: + bit_pos++; + if (bit_pos >= 5) { + state = State::StopBit; + } + break; + + case State::StopBit: + if (buffer_pop(current_char)) { + state = State::StartBit; + } else { + state = State::LeadOut; + lead_counter = 0; + } + break; + + case State::LeadOut: + break; + } +} + +void RTTYTXProcessor::configure( + uint16_t baud, + uint16_t shift, + int16_t mark_tone_, + int16_t space_tone_, + uint8_t stop_bits_, + bool inverted_) { + if (baud == 0) return; + + // baud phase increment + uint32_t new_base_baud_inc = (uint32_t)((uint64_t)baud * UINT32_MAX / (baseband_fs * 100ULL)); + + // Stop bits + configured_stop_bits = stop_bits_; + if (configured_stop_bits < 2) configured_stop_bits = 2; + + int32_t freq_mark = mark_tone_; + int32_t freq_space = space_tone_; + + if (inverted_) { + std::swap(freq_mark, freq_space); + } + + uint32_t new_delta_mark = hz_to_delta(freq_mark, baseband_fs); + uint32_t new_delta_space = hz_to_delta(freq_space, baseband_fs); + + // Slew rate + uint32_t samples_per_bit = (uint32_t)((uint64_t)UINT32_MAX / new_base_baud_inc); + uint32_t transition_samples = samples_per_bit / 10; + if (transition_samples == 0) transition_samples = 1; + + uint32_t shift_delta = hz_to_delta(shift, baseband_fs); + uint32_t new_slew_rate = shift_delta / transition_samples; + if (new_slew_rate == 0) new_slew_rate = 1; + + base_baud_phase_increment = new_base_baud_inc; + delta_mark = new_delta_mark; + delta_space = new_delta_space; + slew_rate = new_slew_rate; + + if (!configured) { + current_delta = delta_mark; + lead_counter = 0; + phase = 0; + baud_phase = 0; + baud_phase_increment = base_baud_phase_increment; + configured = true; + } +} + +void RTTYTXProcessor::on_message(const Message* const msg) { + if (msg->id == Message::ID::RTTYData) { + const auto& rtty_msg = *reinterpret_cast(msg); + configure(rtty_msg.baud, + rtty_msg.shift, + rtty_msg.mark_tone, + rtty_msg.space_tone, + rtty_msg.stopbits, + rtty_msg.inverted); + + for (int i = 0; i < 15; i++) { + buffer_push(0x1F); // LTRS + } + + buffer_push(0x08); // CR + buffer_push(0x02); // LF + + for (uint16_t i = 0; i < rtty_msg.data_len && i < rtty_msg.max_len; i++) { + buffer_push(rtty_msg.data[i]); + } + buffer_push(0x08); // CR + buffer_push(0x02); // LF + + if (state == State::Idle) { + state = State::LeadIn; + lead_counter = 0; + } + } +} + +// Ring Buffer Logic +bool RTTYTXProcessor::buffer_push(uint8_t byte) { + size_t next_head = (head + 1) % data_buffer.size(); + if (next_head == tail) return false; + data_buffer[head] = byte; + head = next_head; + return true; +} + +bool RTTYTXProcessor::buffer_pop(uint8_t& byte) { + if (head == tail) return false; + byte = data_buffer[tail]; + tail = (tail + 1) % data_buffer.size(); + return true; +} + +bool RTTYTXProcessor::buffer_empty() const { + return head == tail; +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_rtty_tx.hpp b/firmware/baseband/proc_rtty_tx.hpp new file mode 100644 index 000000000..2db628993 --- /dev/null +++ b/firmware/baseband/proc_rtty_tx.hpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __PROC_RTTY_TX_H__ +#define __PROC_RTTY_TX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "portapack_shared_memory.hpp" +#include + +class RTTYTXProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg) override; + + private: + static constexpr uint32_t baseband_fs = 2048000; + + bool configured{false}; + + // RTTY Configuration + uint32_t samples_per_bit{0}; + + // Stop bits configuration: 2=1.0, 3=1.5, 4=2.0 + uint8_t configured_stop_bits{2}; + + // FSK State + uint32_t delta_mark{0}; // Phase step for Mark + uint32_t delta_space{0}; // Phase step for Space + uint32_t current_delta{0}; // Smoothed delta + uint32_t slew_rate{0}; // Max change per sample + uint32_t phase{0}; // Phase accumulator + + // Precision Timing + uint32_t baud_phase{0}; + uint32_t baud_phase_increment{0}; + uint32_t base_baud_phase_increment{0}; // Store the standard 1.0 bit rate + + // State Machine + enum class State { + Idle, + LeadIn, + StartBit, + DataBits, + StopBit, + LeadOut + }; + State state{State::Idle}; + + uint32_t lead_counter{0}; + uint8_t current_char{0}; + uint8_t bit_pos{0}; + + // Ring Buffer + std::array data_buffer{}; + volatile size_t head{0}; + volatile size_t tail{0}; + + void configure(uint16_t baud, uint16_t shift, int16_t mark_tone_, int16_t space_tone_, uint8_t stop_bits_, bool inverted_); + void advance_state(); + + bool buffer_push(uint8_t byte); + bool buffer_pop(uint8_t& byte); + bool buffer_empty() const; + + TXProgressMessage txprogress_message{}; + + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit}; +}; + +#endif /* __PROC_RTTY_TX_H__ */ \ No newline at end of file diff --git a/firmware/baseband/proc_subcar.cpp b/firmware/baseband/proc_subcar.cpp index 846266d99..a604d774f 100644 --- a/firmware/baseband/proc_subcar.cpp +++ b/firmware/baseband/proc_subcar.cpp @@ -25,14 +25,6 @@ #include "portapack_shared_memory.hpp" #include "event_m4.hpp" -static inline int get_quadrant(int16_t i, int16_t q) { - if (i >= 0) { - return (q >= 0) ? 0 : 3; - } else { - return (q >= 0) ? 1 : 2; - } -} - void SubCarProcessor::execute(const buffer_c8_t& buffer) { if (!configured) return; @@ -46,10 +38,6 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) { const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples) feed_channel_stats(decim_1_out); - // for fm - const int32_t DC_ALPHA = 5; // Auto-centering speed - int32_t buffer_rotation_sum = 0; - for (size_t i = 0; i < decim_1_out.count; i++) { // am threshold = (low_estimate + high_estimate) / 2; @@ -57,126 +45,103 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) { int16_t re = decim_1_out.p[i].real(); int16_t im = decim_1_out.p[i].imag(); uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im); - mag = (mag >> 10); - int32_t const ook_low_delta = mag - low_estimate; - bool meashl = currentHiLow; - if (sig_state == STATE_IDLE) { - if (mag > (threshold + hysteresis)) { // just become high - meashl = true; - sig_state = STATE_PULSE; - numg = 0; - } else { - meashl = false; // still low - low_estimate += ook_low_delta / OOK_EST_LOW_RATIO; - low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling - // Calculate default OOK high level estimate - high_estimate = 1.35 * low_estimate; // Default is a ratio of low level - high_estimate = std::max(high_estimate, min_high_level); - high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL); - } - - } else if (sig_state == STATE_PULSE) { - ++numg; - if (numg > 100) numg = 100; - if (mag < (threshold - hysteresis)) { - // check if really a bad value - if (numg < 3) { - // susp - sig_state = STATE_GAP; - } else { + if (modulation == 0) { + int32_t const ook_low_delta = mag - low_estimate; + bool meashl = currentHiLow; + if (sig_state == STATE_IDLE) { + if (mag > (threshold + hysteresis)) { // just become high + meashl = true; + sig_state = STATE_PULSE; numg = 0; - sig_state = STATE_GAP_START; + } else { + meashl = false; // still low + low_estimate += ook_low_delta / OOK_EST_LOW_RATIO; + low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling + // Calculate default OOK high level estimate + high_estimate = 1.35 * low_estimate; // Default is a ratio of low level + high_estimate = std::max(high_estimate, min_high_level); + high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL); + } + + } else if (sig_state == STATE_PULSE) { + ++numg; + if (numg > 100) numg = 100; + if (mag < (threshold - hysteresis)) { + // check if really a bad value + if (numg < 3) { + // susp + sig_state = STATE_GAP; + } else { + numg = 0; + sig_state = STATE_GAP_START; + } + meashl = false; // low + } else { + high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO; + high_estimate = std::max(high_estimate, min_high_level); + high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL); + meashl = true; // still high + } + } else if (sig_state == STATE_GAP_START) { + ++numg; + if (mag > (threshold + hysteresis)) { // New pulse? + sig_state = STATE_PULSE; + meashl = true; + } else if (numg >= 3) { + sig_state = STATE_GAP; + meashl = false; // gap + } + } else if (sig_state == STATE_GAP) { + ++numg; + if (mag > (threshold + hysteresis)) { // New pulse? + numg = 0; + sig_state = STATE_PULSE; + meashl = true; + } else { + meashl = false; + } + } + + if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal + { + currentDuration += nsPerDecSamp; + } else { // called on change, so send the last duration and dir. + if (currentDuration >= 30'000'000) sig_state = STATE_IDLE; + if (protoList) protoList->feed(currentHiLow, currentDuration / 1000); + currentDuration = nsPerDecSamp; + currentHiLow = meashl; + } + } + if (modulation == 1) { + int32_t discrim = ((int32_t)im * fm_state.last_re) - ((int32_t)re * fm_state.last_im); + fm_state.last_re = re; + fm_state.last_im = im; + fm_state.smoothed_discrim += (discrim - fm_state.smoothed_discrim) >> 4; + + // --- FM Part (Simple 2-FSK) --- + if (mag > (threshold / 2)) { + const int32_t fm_hysteresis = 2000; + bool new_level = fm_state.current_logic_level; + if (fm_state.smoothed_discrim > fm_hysteresis) { + new_level = true; + } else if (fm_state.smoothed_discrim < -fm_hysteresis) { + new_level = false; + } + if (new_level == fm_state.current_logic_level) { + fm_state.buffer_count++; + } else { + int32_t duration_us = (fm_state.buffer_count * nsPerDecSamp) / 1000; + if (duration_us > 15) { + if (protoList) protoList->feed(fm_state.current_logic_level, duration_us); + } + fm_state.current_logic_level = new_level; + fm_state.buffer_count = 1; } - meashl = false; // low } else { - high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO; - high_estimate = std::max(high_estimate, min_high_level); - high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL); - meashl = true; // still high - } - } else if (sig_state == STATE_GAP_START) { - ++numg; - if (mag > (threshold + hysteresis)) { // New pulse? - sig_state = STATE_PULSE; - meashl = true; - } else if (numg >= 3) { - sig_state = STATE_GAP; - meashl = false; // gap - } - } else if (sig_state == STATE_GAP) { - ++numg; - if (mag > (threshold + hysteresis)) { // New pulse? - numg = 0; - sig_state = STATE_PULSE; - meashl = true; - } else { - meashl = false; + fm_state.buffer_count = 0; } } - - if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal - { - currentDuration += nsPerDecSamp; - } else { // called on change, so send the last duration and dir. - if (currentDuration >= 30'000'000) sig_state = STATE_IDLE; - if (protoList) protoList->feed(currentHiLow, currentDuration / 1000); - currentDuration = nsPerDecSamp; - currentHiLow = meashl; - } - - // fm part: -- NOT WORKING!!!! TODO FIX. AI code ;) - int current_quad = get_quadrant(re, im); - // Calculate Step (Current - Previous) - int diff = current_quad - fm_state.prev_quad; - // Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3) - // 3 -> 0 should be +1 (CCW) - // 0 -> 3 should be -1 (CW) - if (diff == -3) - diff = 1; - else if (diff == 3) - diff = -1; - // Update History - fm_state.prev_quad = current_quad; - // Accumulate Rotation - buffer_rotation_sum += diff; - } - - // fm finish: - // 3. AUTO-CENTERING (DC BLOCKER) - // Even with quadrant counting, "drift" (hand effect) makes the wheel spin - // faster or slower. We need to subtract the average speed. - // Update our "Average Speed" estimate - // Note: buffer_rotation_sum is roughly proportional to frequency. - fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA; - // Remove the drift - int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset; - // 4. LOW PASS FILTER - const int32_t LPF_ALPHA = 4; - fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA; - // 5. DECISION LOGIC - // Threshold is small now because we are counting quadrant steps. - // Max steps per buffer (256 samples) is 256. - // Typical FSK deviation might give you +/- 10 to 50 steps per buffer. - const int32_t THRESHOLD = 3; - bool new_level = fm_state.current_logic_level; - if (fm_state.smoothed_error > THRESHOLD) { - new_level = true; - } else if (fm_state.smoothed_error < -THRESHOLD) { - new_level = false; - } - // 6. TIMING OUTPUT - if (new_level == fm_state.current_logic_level) { - fm_state.buffer_count++; - } else { - // Output pulse duration - int32_t duration_us = fm_state.buffer_count * 512; - - if (duration_us > 250) { - if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us); - } - fm_state.current_logic_level = new_level; - fm_state.buffer_count = 1; } } @@ -189,6 +154,14 @@ void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) { // constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused // constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused + if (modulation != message.modulation) { + // reload protos to reset them all + if (protoList) { + delete protoList; + } + protoList = new SubCarProtos(); + } + modulation = message.modulation; baseband_fs = message.sampling_rate; baseband_thread.set_sampling_rate(baseband_fs); nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8 diff --git a/firmware/baseband/proc_subcar.hpp b/firmware/baseband/proc_subcar.hpp index 9d16e79ba..6c923ce11 100644 --- a/firmware/baseband/proc_subcar.hpp +++ b/firmware/baseband/proc_subcar.hpp @@ -56,8 +56,8 @@ class SubCarProcessor : public BasebandProcessor { uint32_t low_estimate = 100; uint32_t high_estimate = 12000; uint32_t min_high_level = 10; - uint8_t numg = 0; // count of matched signals to filter spikes - size_t baseband_fs = 0; // will be set later by configure message + uint8_t numg = 0; // count of matched signals to filter spikes + size_t baseband_fs = 4'000'000; // will be set later by configure message uint32_t nsPerDecSamp = 0; /* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */ @@ -74,20 +74,20 @@ class SubCarProcessor : public BasebandProcessor { uint32_t threshold = 0x0630; bool currentHiLow = false; bool configured{false}; - uint8_t mode = 0; // 0 = am, 1 = fm // fm part: struct DemodFMState { - int prev_quad = 0; // Stores 0, 1, 2, or 3 - int32_t dc_offset = 0; - int32_t smoothed_error = 0; bool current_logic_level = false; uint32_t buffer_count = 0; + int16_t last_re = 0; // Store previous Real sample + int16_t last_im = 0; + int32_t smoothed_discrim = 0; }; DemodFMState fm_state{}; - FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse - FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual) + uint8_t modulation = 0; // 0 am, 1 fm + + FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse void configure(const SubGhzFPRxConfigureMessage& message); /* NB: Threads should be the last members in the class definition. */ diff --git a/firmware/baseband/proc_time_sink.cpp b/firmware/baseband/proc_time_sink.cpp new file mode 100644 index 000000000..782fbaf98 --- /dev/null +++ b/firmware/baseband/proc_time_sink.cpp @@ -0,0 +1,128 @@ +/* + * Copyleft zxkmm (>) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "proc_time_sink.hpp" + +#include "event_m4.hpp" +#include "portapack_shared_memory.hpp" + +#include +#include +#include +#include + +void TimeSinkProcessor::execute(const buffer_c8_t& buffer) { + // 2048 complex8_t samples per buffer. + // 102.4us per buffer. 20480 instruction cycles per buffer. + + if (!configured) { + return; + } + + execute_time_domain(buffer); +} + +void TimeSinkProcessor::execute_time_domain(const buffer_c8_t& buffer) { + if (!time_streaming || buffer.count == 0) { + return; + } + + if (phase < trigger) { + phase++; + return; + } + + phase = 0; + + if (time_domain_request_update) { + return; + } + + const size_t stride = std::max(1, buffer.count / time_domain_spectrum.db.size()); + time_domain_spectrum.sampling_rate = buffer.sampling_rate / stride; + time_domain_spectrum.channel_filter_low_frequency = 0; + time_domain_spectrum.channel_filter_high_frequency = 0; + time_domain_spectrum.channel_filter_transition = 0; + + for (size_t i = 0; i < time_domain_spectrum.db.size(); i++) { + const size_t sample_index = std::min(i * stride, buffer.count - 1); + const int32_t normalized = std::clamp( + static_cast(buffer.p[sample_index].real()) + 128, + 0, + 255); + time_domain_spectrum.db[i] = static_cast(normalized); + } + + time_domain_request_update = true; + EventDispatcher::events_flag(EVT_MASK_SPECTRUM); +} + +void TimeSinkProcessor::set_time_streaming_state(const SpectrumStreamingConfigMessage& message) { + if (message.mode == SpectrumStreamingConfigMessage::Mode::Running) { + time_streaming = true; + ChannelSpectrumConfigMessage fifo_message{&fifo}; + shared_memory.application_queue.push(fifo_message); + } else { + time_streaming = false; + time_domain_request_update = false; + fifo.reset_in(); + } +} + +void TimeSinkProcessor::update_time_domain() { + if (time_streaming && time_domain_request_update) { + fifo.in(time_domain_spectrum); + } + + time_domain_request_update = false; +} + +void TimeSinkProcessor::on_message(const Message* const msg) { + switch (msg->id) { + case Message::ID::UpdateSpectrum: + update_time_domain(); + break; + + case Message::ID::SpectrumStreamingConfig: + set_time_streaming_state(*reinterpret_cast(msg)); + break; + + case Message::ID::TimeSinkConfig: { + const auto& message = *reinterpret_cast(msg); + baseband_fs = message.sampling_rate; + trigger = message.trigger; + baseband_thread.set_sampling_rate(baseband_fs); + phase = 0; + time_domain_request_update = false; + configured = true; + break; + } + + default: + break; + } +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_time_sink.hpp b/firmware/baseband/proc_time_sink.hpp new file mode 100644 index 000000000..56676f15d --- /dev/null +++ b/firmware/baseband/proc_time_sink.hpp @@ -0,0 +1,56 @@ +/* + * Copyleft zxkmm (>) 2026 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __PROC_TIME_SINK_H__ +#define __PROC_TIME_SINK_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" + +#include "message.hpp" + +#include + +class TimeSinkProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const message) override; + + private: + bool configured = false; + size_t baseband_fs = 20000000; + + void execute_time_domain(const buffer_c8_t& buffer); + void set_time_streaming_state(const SpectrumStreamingConfigMessage& message); + void update_time_domain(); + ChannelSpectrum time_domain_spectrum{}; + ChannelSpectrum fifo_data[1 << ChannelSpectrumConfigMessage::fifo_k]{}; + ChannelSpectrumFIFO fifo{fifo_data, ChannelSpectrumConfigMessage::fifo_k}; + + size_t phase = 0, trigger = 127; + bool time_streaming = false; + volatile bool time_domain_request_update = false; + + /* NB: Threads should be the last members in the class definition. */ + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; +}; + +#endif /*__PROC_TIME_SINK_H__*/ diff --git a/firmware/baseband/proc_wfm_audio.cpp b/firmware/baseband/proc_wfm_audio.cpp index de265aaf5..4f43186d2 100644 --- a/firmware/baseband/proc_wfm_audio.cpp +++ b/firmware/baseband/proc_wfm_audio.cpp @@ -113,13 +113,13 @@ void WidebandFMAudio::execute(const buffer_c8_t& buffer) { fft_c_preswapped(audio_spectrum, fft_step, fft_step + 1); fft_step++; } else { + constexpr float mag_scale = 5.0f; const size_t spectrum_end = spectrum.db.size(); for (size_t i = 0; i < spectrum_end; i++) { // const auto corrected_sample = spectrum_window_hamming_3(audio_spectrum, i); const auto corrected_sample = audio_spectrum[i]; const auto mag2 = magnitude_squared(corrected_sample * (1.0f / 32768.0f)); const float db = mag2_to_dbv_norm(mag2); - constexpr float mag_scale = 5.0f; const unsigned int v = (db * mag_scale) + 255.0f; spectrum.db[i] = std::max(0U, std::min(255U, v)); } diff --git a/firmware/baseband/rssi_thread.cpp b/firmware/baseband/rssi_thread.cpp index 812f8c1a7..9f02d8b67 100644 --- a/firmware/baseband/rssi_thread.cpp +++ b/firmware/baseband/rssi_thread.cpp @@ -28,6 +28,110 @@ #include "message.hpp" #include "portapack_shared_memory.hpp" +#ifdef PRALINE +/* + * ============================================================================= + * PRALINE Software RSSI + * ============================================================================= + * + * Architecture: + * - baseband_thread: Copies 8 packed I/Q samples (no computation) + * - rssi_thread: __SMUAD power calc, avg power, LUT, trackers + * + * All DSP happens here to keep baseband_thread minimal. + */ + +/* + * Power-to-RSSI Lookup Table (32 entries) + * + * Maps IΒ²+QΒ² power to RSSI (0-255) using logarithmic scaling. + * For 8-bit I/Q: max |I|=|Q|=127, so max IΒ²+QΒ² = 32258 + * + * For example, the formula: rssi = 32 * log2((index * 8) + 1), clamped to 255 + * where using >> 8 scaling provide 4x more sensitive than >> 10: + * + * The trade-off: more sensitivity means the meter saturates (hits max) at lower signal levels. + * We'll want the bar to be mid-range at typical signal levels, not pegged at max. + * - Index 0: power 0-255 (I/Q magnitude ~11) + * - Index 31: power 7936+ (I/Q magnitude ~63+) + */ +static constexpr uint8_t power_to_rssi_lut[32] = { + 0, 101, 130, 148, 161, 171, 179, 186, + 192, 197, 202, 206, 210, 213, 217, 220, + 222, 225, 227, 230, 232, 234, 236, 238, + 240, 241, 243, 244, 246, 247, 249, 255}; + +/* + * Convert power to RSSI using 32-entry LUT. + * If more sensitivity thank >> 10 is needed: + * use power >= 8192 & >> 8, yields 4x more sensitivity than >> 10. + * use power >= 4096 & >> 7, yields 8x more sensitivity than >> 10 + * use power >= 2048 & >> 6, yields 16x more sensitivity than >> 10 + * use power >= 1024 & >> 5, yields 32x more sensitivity than >> 10 + */ +static inline uint8_t power_to_rssi(uint32_t power) { + // uint8_t index = (power >= 32768) ? 31 : static_cast(power >> 10); + uint8_t index = (power >= 2048) ? 31 : static_cast(power >> 6); + return power_to_rssi_lut[index]; +} + +/* + * IIR Smoothing Filter (Exponential Moving Average) + * Formula: smooth = (current + 7*smooth) / 8 (Ξ± = 1/8) + */ +class IIRFilter { + public: + uint8_t update(uint8_t current) { + uint16_t current_q8 = static_cast(current) << 8; + smooth_q8_ = (current_q8 + 7 * smooth_q8_) >> 3; + return static_cast(smooth_q8_ >> 8); + } + + private: + uint16_t smooth_q8_ = 0; +}; + +/* + * Running min tracker with decay. + * Instantly captures new minimums, slowly decays upward. + */ +class MinTracker { + public: + uint8_t update(uint8_t current) { + if (current < min_) { + min_ = current; + } else { + // Slow decay upward (Ξ± = 1/16) + min_ = min_ + ((current - min_) >> 4); + } + return min_; + } + + private: + uint8_t min_ = 255; +}; + +/* + * Running max tracker with decay. + * Instantly captures new maximums, slowly decays downward. + */ +class MaxTracker { + public: + uint8_t update(uint8_t current) { + if (current > max_) { + max_ = current; + } else { + // Slow decay downward (Ξ± = 1/16) + max_ = max_ - ((max_ - current) >> 4); + } + return max_; + } + + private: + uint8_t max_ = 0; +}; +#endif // PRALINE + WORKING_AREA(rssi_thread_wa, 128); Thread* RSSIThread::thread = nullptr; @@ -54,6 +158,85 @@ void RSSIThread::start() { } void RSSIThread::run() { +#ifdef PRALINE + + /* + * PRALINE (HackRF Pro): Software RSSI from I/Q samples + * + * Hardware ADC-based RSSI doesn't work on HackRF Pro. + * + * baseband_thread copies 8 packed I/Q samples. + * We use __SMUAD to compute power, then apply LUT and + * maintain running stats. + */ + + IIRFilter avg_filter; + MinTracker min_tracker; + MaxTracker max_tracker; + + RSSIStatistics stats{}; + uint32_t accumulator = 0; + uint32_t sample_count = 0; + + constexpr uint32_t samples_per_report = 50; // ~10Hz reporting at 2ms poll + + while (!chThdShouldTerminate()) { + chThdSleepMilliseconds(2); // Poll at ~500Hz + + /* + * SIMD-accelerated I/Q power calculation for Cortex-M4. + * + * Uses __SMUAD (Signed Multiply Accumulate Dual) instruction to compute + * sum of products of packed halfwords: (a0*a0) + (a1*a1) + * + * Processes complex samples per iteration, computing IΒ²+QΒ² with SIMD. + * SIMD-accelerated I/Q power calculation for Cortex-M4. + * ~4x faster than scalar loop. + * Sum power from all 8 samples (more stable than peak). + * + * __SMUAD(val, val) computes: + * (low16 * low16) + (high16 * high16) = IΒ² + QΒ² + */ + + uint32_t total_power = 0; + + for (size_t i = 0; i < 8; i++) { + const uint32_t packed = shared_memory.software_rssi_iq[i]; + total_power += __SMUAD(packed, packed); + } + + // Average the 8 samples for min, and avg power + const uint32_t avg_power = total_power >> 3; + + // Convert to RSSI scale (0-255) via LUT + const uint8_t avg_rssi = power_to_rssi(avg_power); + + // Update running trackers + const uint8_t smooth_min = min_tracker.update(avg_rssi); + const uint8_t smooth_avg = avg_filter.update(avg_rssi); + const uint8_t smooth_max = max_tracker.update(avg_rssi); + + // Accumulate for periodic report + accumulator += smooth_avg; + sample_count++; + + // Report periodically + if (sample_count >= samples_per_report) { + stats.min = smooth_min; + stats.max = smooth_max; + stats.accumulator = accumulator; + stats.count = sample_count; + + const RSSIStatisticsMessage message{stats}; + shared_memory.application_queue.push(message); + + // Reset accumulator for next period + accumulator = 0; + sample_count = 0; + } + } +#else + /* HackRF One: Use hardware ADC-based RSSI */ rf::rssi::init(); rf::rssi::dma::allocate(4, 400); @@ -77,4 +260,5 @@ void RSSIThread::run() { rf::rssi::stop(); rf::rssi::dma::free(); +#endif } diff --git a/firmware/baseband/sd_over_usb/hackrf_core.c b/firmware/baseband/sd_over_usb/hackrf_core.c index 5e66612d9..b3587dff3 100644 --- a/firmware/baseband/sd_over_usb/hackrf_core.c +++ b/firmware/baseband/sd_over_usb/hackrf_core.c @@ -241,7 +241,7 @@ w25q80bv_driver_t spi_flash = { sgpio_config_t sgpio_config = { .gpio_q_invert = &gpio_q_invert, - .gpio_hw_sync_enable = &gpio_hw_sync_enable, + .gpio_trigger_enable = &gpio_hw_sync_enable, .slice_mode_multislice = true, }; @@ -950,7 +950,7 @@ void pin_setup(void) { #ifdef HACKRF_ONE if (detected_platform() == BOARD_ID_HACKRF1_R9) { rf_path.gpio_rx = &gpio_h1r9_rx; - sgpio_config.gpio_hw_sync_enable = &gpio_h1r9_hw_sync_enable; + sgpio_config.gpio_trigger_enable = &gpio_h1r9_hw_sync_enable; } #endif rf_path_pin_setup(&rf_path); @@ -1041,8 +1041,8 @@ void set_leds(const uint8_t state) { } } -void hw_sync_enable(const hw_sync_mode_t hw_sync_mode) { - gpio_write(sgpio_config.gpio_hw_sync_enable, hw_sync_mode == 1); +void trigger_enable(bool hw_sync_mode) { + gpio_write(sgpio_config.gpio_trigger_enable, hw_sync_mode == 1); } void halt_and_flash(const uint32_t duration) { diff --git a/firmware/baseband/sd_over_usb/scsi.c b/firmware/baseband/sd_over_usb/scsi.c index 7867a261d..3c40d2c82 100644 --- a/firmware/baseband/sd_over_usb/scsi.c +++ b/firmware/baseband/sd_over_usb/scsi.c @@ -25,6 +25,7 @@ #include #include #include +#include "delay.h" volatile bool usb_bulk_block_done = false; @@ -296,4 +297,4 @@ void scsi_command(msd_cbw_t* msd_cbw_data) { } usb_send_csw(msd_cbw_data, status); -} \ No newline at end of file +} diff --git a/firmware/baseband/sd_over_usb/sd_over_usb.c b/firmware/baseband/sd_over_usb/sd_over_usb.c index 9d516203a..ac0e24ba3 100644 --- a/firmware/baseband/sd_over_usb/sd_over_usb.c +++ b/firmware/baseband/sd_over_usb/sd_over_usb.c @@ -65,8 +65,8 @@ const usb_request_handlers_t usb_request_handlers = { void usb_configuration_changed(usb_device_t* const device) { (void)device; - usb_endpoint_init(&usb_endpoint_bulk_in); - usb_endpoint_init(&usb_endpoint_bulk_out); + usb_endpoint_init(&usb_endpoint_bulk_in, false); + usb_endpoint_init(&usb_endpoint_bulk_out, false); } void start_usb(void) { @@ -84,8 +84,8 @@ void start_usb(void) { usb_queue_init(&usb_endpoint_bulk_out_queue); usb_queue_init(&usb_endpoint_bulk_in_queue); - usb_endpoint_init(&usb_endpoint_control_out); - usb_endpoint_init(&usb_endpoint_control_in); + usb_endpoint_init(&usb_endpoint_control_out, false); + usb_endpoint_init(&usb_endpoint_control_in, false); nvic_set_priority(NVIC_USB0_IRQ, 255); @@ -126,4 +126,4 @@ void usb_transfer(void) { scsi_command(msd_cbw_data); } } -} \ No newline at end of file +} diff --git a/firmware/baseband/w25q80bv.cpp b/firmware/baseband/w25q80bv.cpp index f83bfff1e..c3c056d11 100644 --- a/firmware/baseband/w25q80bv.cpp +++ b/firmware/baseband/w25q80bv.cpp @@ -77,7 +77,8 @@ void wait_for_device() { do { device_id = get_device_id(); } while (device_id != W25Q80BV_DEVICE_ID_RES && - device_id != W25Q16DV_DEVICE_ID_RES); + device_id != W25Q16DV_DEVICE_ID_RES && + device_id != W25Q32JV_DEVICE_ID_RES); } void wait_not_busy() { diff --git a/firmware/baseband/w25q80bv.hpp b/firmware/baseband/w25q80bv.hpp index b1705e2bf..9beae42d5 100644 --- a/firmware/baseband/w25q80bv.hpp +++ b/firmware/baseband/w25q80bv.hpp @@ -83,6 +83,7 @@ #define W25Q80BV_DEVICE_ID_RES 0x13 /* Expected device_id for W25Q80BV */ #define W25Q16DV_DEVICE_ID_RES 0x14 /* Expected device_id for W25Q16DV */ +#define W25Q32JV_DEVICE_ID_RES 0x15 /* Expected device_id for W25Q32JV (Praline) */ #define SSP_CR1(port) MMIO32(port + 0x004) #define PERIPH_BASE_APB0 0x40080000 diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake index 62e8ec328..8c32a69e7 100644 --- a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake @@ -3,7 +3,20 @@ set(BOARDSRC ${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION/board.cpp ) +# Add FPGA bridge for PRALINE (HackRF Pro) +if(BOARD STREQUAL "PRALINE") + list(APPEND BOARDSRC + ${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION/fpga_bridge.c + ${CHIBIOS_PORTAPACK}/../../hackrf/firmware/common/lz4_blk.c + ) +endif() + # Required include directories set(BOARDINC ${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION ) + +# Add hackrf common include for PRALINE (for lz4_blk.h) +if(BOARD STREQUAL "PRALINE") + list(APPEND BOARDINC ${CHIBIOS_PORTAPACK}/../../hackrf/firmware/common) +endif() diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp index 3a9aa6223..e80772532 100755 --- a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp @@ -15,11 +15,13 @@ limitations under the License. */ + #include "ch.h" #include "hal.h" #include +// Declare wrapper function. board.cpp to avoid conflicting gpio_t definitions. bool hackrf_r9; #if HAL_USE_PAL || defined(__DOXYGEN__) @@ -52,7 +54,11 @@ const PALConfig pal_default_config = { .P = { { // GPIO0 .data +#ifdef PRALINE + = (0 << 15) // P1_20: CLKIN_CTRL - start low +#else = (1 << 15) // P1_20: CS_XCVR +#endif | (1 << 14) // P2_10: AMP_BYPASS | (0 << 13) // P1_18: SGPIO12, HOST_Q_INVERT | (0 << 12) // P1_17: SGPIO11, HOST_DIRECTION @@ -70,8 +76,12 @@ const PALConfig pal_default_config = { | (1 << 0) // P0_0: SGPIO0, HOST_DATA0 , .dir +#ifdef PRALINE + = (1 << 15) // P1_20: CLKIN_CTRL - output +#else = (1 << 15) // P1_20: CS_XCVR - | (1 << 14) // P2_10: AMP_BYPASS +#endif + | (1 << 14) // P2_10: AMP_BYPASS | (1 << 13) // P1_18: SGPIO12, HOST_Q_INVERT | (0 << 12) // P1_17: SGPIO11, HOST_DIRECTION | (0 << 11) // P1_4: SSP1_MOSI @@ -128,7 +138,11 @@ const PALConfig pal_default_config = { .data = (0 << 15) // P5_6: TX_AMP | (1 << 14) // P5_5: MIXER_RESETX, 10K PU +#ifdef PRALINE + | (0 << 13) // P5_4: MIXER_ENX, 10K PU +#else | (1 << 13) // P5_4: MIXER_ENX, 10K PU +#endif | (1 << 12) // P5_3: RX_MIX_BP | (0 << 11) // P5_2: TX_MIX_BP | (0 << 10) // P5_1: LP @@ -145,11 +159,19 @@ const PALConfig pal_default_config = { , .dir = (1 << 15) // P5_6: TX_AMP - | (1 << 14) // P5_5: MIXER_RESETX, 10K PU - | (1 << 13) // P5_4: MIXER_ENX, 10K PU +#ifdef PRALINE + | (0 << 14) // P5_5: MIXER_RESETX, 10K PU + | (1 << 13) // P5_4: MIXER_ENX - OUTPUT (MCU controlled) + | (0 << 12) // P5_3: RX_MIX_BP - unused on PRALINE + | (0 << 11) // P5_2: FPGA_CRESET - INPUT initially + | (0 << 10) // P5_1: FPGA_SPI_CS - INPUT initially +#else + | (1 << 14) // P5_5: MIXER_RESETX, 10K PU + | (1 << 13) // P5_4: MIXER_ENX, 10K PU | (1 << 12) // P5_3: RX_MIX_BP | (1 << 11) // P5_2: TX_MIX_BP | (1 << 10) // P5_1: LP +#endif | (0 << 9) // P5_0: Varies by revision, float until detection | (1 << 8) // P6_12: LED3 (TX) | (1 << 7) // P5_7: CS_AD @@ -262,9 +284,9 @@ const PALConfig pal_default_config = { { 4, 7, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* GP_CLKIN/P72/MCU_CLK: SI5351C.CLK7(O) */ /* HackRF: LEDs. Configured early so we can use them to indicate boot status. */ - { 4, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* LED1: LED1.A(I) */ - { 4, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* LED2: LED2.A(I) */ - { 6, 12, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* LED3: LED3.A(I) */ + { 4, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* LED1: LED1.A(I) */ + { 4, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* LED2: LED2.A(I) */ + { 6, 12, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* LED3: LED3.A(I) */ /* Power control */ { 6, 11, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* VREGMODE/P69: TPS62410.MODE/DATA(I) */ @@ -297,7 +319,13 @@ const PALConfig pal_default_config = { { 1, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* !MIX_BYPASS/P35: U1.VCTL1(I), U11.VCTL2(I), U9.V2(I) */ { 1, 19, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* SSP1_SCK/P39: MAX2837.SCLK(I), MAX5864.SCLK(I) */ { 1, 20, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CS_XCVR/P53: MAX2837.CS(I) */ + +#ifdef PRALINE + { 2, 6, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=1, .ezi=0, .zif=1 } }, /* TRIGGER_OUT / MIX_EN_N_R1_0: GPIO5[6] - PRALINE */ +#else + /* HackRF One RFFC5072 SPI pins */ { 2, 6, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIXER_SCLK/P31: 33pF, RFFC5072.SCLK(I) */ +#endif { 2, 10, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* AMP_BYPASS/P50: U14.V2(I), U12.V2(I) */ { 2, 11, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* RX_AMP/P49: U12.V1(I), U14.V3(I) */ { 2, 12, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* !RX_AMP_PWR/P52: 10K PU, Q1.G(I), power to U13 (RX amp) */ @@ -310,8 +338,16 @@ const PALConfig pal_default_config = { { 5, 4, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIXER_ENX/P32: 10K PU, 33pF, RFFC5072.ENX(I) */ { 5, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIXER_RESETX/P33: 10K PU, 33pF, RFFC5072.RESETX(I) */ { 5, 6, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* TX_AMP/P48: U12.V3(I), U14.V1(I) */ +#ifdef PRALINE + /* PRALINE RFFC5072 SPI pins - different from HackRF One */ + { 5, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* P5_7: GPIO2[7], RFFC5072 CS */ + { 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=1, .ezi=0, .zif=0 } }, /* P9_5: GPIO5[18], RFFC5072 SCLK */ + { 9, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=1, .ezi=1, .zif=0 } }, /* P9_2: GPIO4[14], RFFC5072 SDATA (bidirectional) */ +#else +/* HackRF One RFFC5072 SPI pins - NOT used on PRALINE */ { 5, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CS_AD/P54: MAX5864.CS(I) */ { 6, 4, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* MIXER_SDATA/P27: 33pF, RFFC5072.SDATA(IO) */ +#endif { 6, 8, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIX_BYPASS/P34: U1.VCTL2(I), U11.VCTL1(I) */ { 6, 9, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* !TX_AMP_PWR/P51: 10K PU, Q2.G(I), power to U25 (TX amp) */ @@ -332,11 +368,14 @@ const PALConfig pal_default_config = { { 4, 9, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* SGPIO14/BANK2F3M4: CPLD.81/CPLD_P81 */ { 4, 10, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* SGPIO15/BANK2F3M6: CPLD.78/CPLD_P78 */ - /* HackRF: CPLD */ - { 6, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TCK: CPLD.TCK(I), PortaPack CPLD.TCK(I) */ - { 6, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDI: CPLD.TDI(I), PortaPack I2S0_RX_SDA(O), PortaPack CPLD.TDI(I) */ - { 6, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TMS: CPLD.TMS(I) */ - { 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDO: CPLD.TDO(O) */ + /* PortaPack CPLD JTAG pins - same for all builds including PRALINE + * (PRALINE FPGA uses P5_1/P5_2/P4_10, not these pins) */ + { 6, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TCK: PortaPack CPLD.TCK(I) */ + { 6, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDI: PortaPack CPLD.TDI(I), I2S0_RX_SDA(O) */ + { 6, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TMS: HackRF CPLD.TMS(I) */ +#ifndef PRALINE + { 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDO: HackRF CPLD.TDO(O) */ +#endif /* PortaPack CPLD */ { 1, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_POW: PortaPack CPLD.TDO(O) */ @@ -503,6 +542,50 @@ static const std::array pins_setup_r9 { { #endif +#ifdef PRALINE + +static const std::array pins_setup_portapack { { + { 2, 0, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_TXD: PortaPack P2_0/IO_STBX */ + { 2, 1, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_RXD: PortaPack P2_1/ADDR */ + { 2, 3, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* I2C1_SDA: PortaPack P2_3/LCD_TE */ + { 2, 4, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* I2C1_SCL: PortaPack P2_4/LCD_RDX */ + { 2, 8, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* P2_8: 10K PD, BOOT2, DFU switch, PortaPack P2_8/ */ + { 2, 9, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* P2_9: 10K PD, BOOT3, PortaPack P2_9/LCD_WRX */ + { 2, 13, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* P2_13: PortaPack P2_13/DIR */ + { 7, 0, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_8: PortaPack GPIO3_8(IO) */ + { 7, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_9: PortaPack GPIO3_9(IO) */ + { 7, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_10: PortaPack GPIO3_10(IO) */ + { 7, 3, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_11: PortaPack GPIO3_11(IO) */ + { 7, 4, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_12: PortaPack GPIO3_12(IO) */ + { 7, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_13: PortaPack GPIO3_13(IO) */ + { 7, 6, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_14: PortaPack GPIO3_14(IO) */ + { 7, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_15: PortaPack GPIO3_15(IO) */ + + /* PortaPack: Audio */ + { 3, 0, scu_config_normal_drive_t { .mode=2, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* I2S0_TX_SCK: PortaPack I2S0_TX_SCK(I) */ + { 3, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* I2S0_RX_WS: PortaPack I2S0_TX_WS(I). Input enabled to fold back into RX. */ + { 3, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* I2S0_RX_SDA: PortaPack I2S0_TX_SDA(I) */ + { 24, 2, scu_config_normal_drive_t { .mode=6, .epd=1, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* I2S0_TX_CLK: PortaPack I2S0_TX_MCLK */ + + /* PortaPack: SD card socket */ + { 24, 0, scu_config_normal_drive_t { .mode=4, .epd=1, .epun=1, .ehs=0, .ezi=1, .zif=1 } }, /* SD_CLK: PortaPack SD.CLK, enable input buffer for timing feedback? */ + { 1, 6, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_CMD: PortaPack SD.CMD(IO) */ + { 1, 9, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT0: PortaPack SD.DAT0(IO) */ + { 1, 10, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT1: PortaPack SD.DAT1(IO) */ + { 1, 11, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT2: PortaPack SD.DAT2(IO) */ + { 1, 12, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT3: PortaPack SD.DAT3(IO) */ + { 1, 13, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_CD: PortaPack SD.CD(O) */ + + // for pro touch + // those pins should choose reserved function mode + // and the behavior same as hackrf one(YN YP XN XP) + { 11, 6, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* PB_6: ADC0_0 yp*/ + { 4, 5, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* P4_5: ADC0_2 yn*/ + { 4, 4, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* P4_4: ADC0_5 xp */ + { 15, 4, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* PF_4: ADC0_6 xn*/ +} }; + +#else static const std::array pins_setup_portapack { { { 2, 0, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_TXD: PortaPack P2_0/IO_STBX */ { 2, 1, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_RXD: PortaPack P2_1/ADDR */ @@ -535,6 +618,7 @@ static const std::array pins_setup_portapack { { { 1, 12, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT3: PortaPack SD.DAT3(IO) */ { 1, 13, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_CD: PortaPack SD.CD(O) */ } }; +#endif static const std::array pins_setup_spifi { { { 3, 3, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=1, .ehs=1, .ezi=1, .zif=1 } }, /* SPIFI_SCK: W25Q80BV.CLK(I), enable input buffer for timing feedback */ @@ -619,6 +703,10 @@ static const motocon_pwm_resources_t motocon_pwm_resources = { static const scu_setup_t pin_setup_vaa_enablex_pwm = { 5, 0, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; static const scu_setup_t pin_setup_vaa_enablex_gpio_og = { 5, 0, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; static const scu_setup_t pin_setup_vaa_enablex_gpio_r9 = { 6, 10, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; +#ifdef PRALINE +/* PRALINE uses P8_1 (GPIO4[1]) for VAA_DISABLE (high = VAA off, low = VAA on) */ +static const scu_setup_t pin_setup_vaa_disable_praline = { 8, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; +#endif /* VAA powers: * MAX5864 analog section. @@ -824,7 +912,168 @@ extern "C" void __late_init(void) { * @todo Add your board-specific code, if any. */ extern "C" void boardInit(void) { +#ifdef PRALINE + /* HackRF Pro Specific: Initialize and Load FPGA */ + hackrf_r9 = false; + /* Enable 3.3V aux power - P6_7 = GPIO5[15], active LOW (clear to enable) */ + LPC_SCU->SFSP[6][7] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->DIR[5] |= (1 << 15); + LPC_GPIO->CLR[5] = (1 << 15); /* Clear = enable 3.3V aux */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Enable 1.2V for FPGA - P8_7 = GPIO4[7], active high */ + LPC_SCU->SFSP[8][7] = 0x10; + LPC_GPIO->DIR[4] |= (1 << 7); + LPC_GPIO->SET[4] = (1 << 7); + { volatile uint32_t delay = 200000; while(delay--); } + + /* Enable VAA for RF - P8_1 = GPIO4[1], active low */ + LPC_SCU->SFSP[8][1] = 0x10; + LPC_GPIO->DIR[4] |= (1 << 1); + LPC_GPIO->CLR[4] = (1 << 1); + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure RFFC5072 pins for PRALINE */ + /* Set GPIO directions for RFFC5072 SPI pins */ + /* P5_7 = GPIO2[7] RFFC5072 CS */ + LPC_GPIO->SET[2] = (1 << 7); /* CS high (deselected) */ + LPC_GPIO->DIR[2] |= (1 << 7); /* CS as output */ + + /* P9_5 = GPIO5[18] RFFC5072 SCLK */ + LPC_GPIO->CLR[5] = (1 << 18); /* CLK low */ + LPC_GPIO->DIR[5] |= (1 << 18); /* CLK as output */ + + /* P9_2 = GPIO4[14] RFFC5072 DATA (bidirectional) */ + LPC_GPIO->DIR[4] |= (1 << 14); /* DATA as output initially */ + + /* P2_6 = GPIO5[6] TRIGGER_OUT / MIX_EN_N_R1_0 (PRALINE R1.0 mixer bypass) */ + /* SCU configured in PAL array above with mode=4 */ + LPC_GPIO->CLR[5] = (1 << 6); /* Default low (mixer enabled) */ + LPC_GPIO->DIR[5] |= (1 << 6); /* Output */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure Port D pins for PRALINE (use SFSPD registers) */ + /* PD_14 = GPIO6[28] MAX2831 chip select */ + LPC_SCU->SFSPD[14] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->SET[6] = (1 << 28); /* CS high (inactive) */ + LPC_GPIO->DIR[6] |= (1 << 28); /* Output */ + + /* PD_15 = GPIO6[29] MAX2831 RXHP control */ + LPC_SCU->SFSPD[15] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[6] = (1 << 29); /* RXHP low = 100 Hz HPF */ + LPC_GPIO->DIR[6] |= (1 << 29); /* Output */ + + /* PD_16 = GPIO6[30] MAX5864 chip select */ + LPC_SCU->SFSPD[16] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->SET[6] |= (1 << 30); /* CS high (inactive) */ + LPC_GPIO->DIR[6] |= (1 << 30); /* Output */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure Port E pins for MAX2831 control (use SFSPE registers) */ + /* PE_1 = GPIO7[1] MAX2831 ENABLE */ + LPC_SCU->SFSPE[1] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[7] = (1 << 1); /* Start disabled */ + LPC_GPIO->DIR[7] |= (1 << 1); /* Output */ + /* PE_2 = GPIO7[2] MAX2831 RXTX mode select */ + LPC_SCU->SFSPE[2] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[7] = (1 << 2); /* Start in shutdown mode */ + LPC_GPIO->DIR[7] |= (1 << 2); /* Output */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure Port 6 pins for RF path control */ + /* P6_3 = GPIO3[2] Mixer enable (inverted: 0 = mixer ON) */ + LPC_SCU->SFSP[6][3] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */ + LPC_GPIO->CLR[3] = (1 << 2); /* Mixer enabled by default */ + LPC_GPIO->DIR[3] |= (1 << 2); /* Output */ + /* P6_5 = GPIO3[4] TX enable */ + LPC_SCU->SFSP[6][5] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */ + LPC_GPIO->CLR[3] = (1 << 4); /* TX off by default (RX mode) */ + LPC_GPIO->DIR[3] |= (1 << 4); /* Output */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure Port A pins for RF path control */ + /* PA_1 = GPIO4[8] LPF enable */ + LPC_SCU->SFSP[0xA][1] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */ + LPC_GPIO->SET[4] = (1 << 8); /* LPF enabled by default (low band) */ + LPC_GPIO->DIR[4] |= (1 << 8); /* Output */ + /* PA_2 = GPIO4[9] RF amp enable */ + LPC_SCU->SFSP[0xA][2] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */ + LPC_GPIO->CLR[4] = (1 << 9); /* RF amp off by default */ + LPC_GPIO->DIR[4] |= (1 << 9); /* Output */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure RFFC5072 control pins for PRALINE */ + /* P5_4 = GPIO2[13] RFFC5072 ENX - SPI chip select (managed by SPI driver) */ + LPC_SCU->SFSP[5][4] = 0x10; /* FUNCTION0 (GPIO), pull-up, slow mode (matches HackRF USB) */ + LPC_GPIO->DIR[2] |= (1 << 13); /* ENX: OUTPUT */ + LPC_GPIO->SET[2] = (1 << 13); /* ENX = 1 (deselected initially) */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* P5_5 = GPIO2[14] RFFC5072 RESETX (active high: 1=running) */ + LPC_SCU->SFSP[5][5] = 0x10; /* FUNCTION0 (GPIO), pull-up, slow mode (matches HackRF USB) */ + LPC_GPIO->DIR[2] |= (1 << 14); /* RESETX: OUTPUT */ + LPC_GPIO->SET[2] = (1 << 14); /* RESETX = 1 (RUNNING) */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Ensure RESETX is stable */ + for (volatile int i = 0; i < 10; i++) { + LPC_GPIO->W2[14] = 1; + } + + /* PD_11 = GPIO6[25] RFFC5072 Lock Detect (input) */ + LPC_SCU->SFSPD[11] = 0x10; /* FUNCTION0 (GPIO), pull-up (matches HackRF USB) */ + LPC_GPIO->DIR[6] &= ~(1 << 25); /* LD: INPUT */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* Configure PRALINE-specific SGPIO pins for FPGA sample interface. + * These override the HackRF One pin config from pins_setup. + * PRALINE uses different pins than HackRF One for SGPIO4/8/9/10. + * SCU_GPIO_FAST = 0xF0 (EPUN + EHS + EZI + ZIF) + */ + /* CRITICAL: Disable HackRF One SGPIO8 pin (P9_6) - PRALINE uses P8_0 instead */ + LPC_SCU->SFSP[9][6] = 0x00; /* P9_6 = GPIO mode 0, disable SGPIO function */ + + /* SGPIO4 = P9_4 function 6 (HOST_DATA4) */ + LPC_SCU->SFSP[9][4] = 0xF6; /* SCU_GPIO_FAST | func 6 */ + /* SGPIO8 = P8_0 function 4 (SGPIO_CLK - clock from FPGA) */ + LPC_SCU->SFSP[8][0] = 0xF4; /* SCU_GPIO_FAST | func 4 */ + /* SGPIO9 = P9_3 function 6 (HOST_CAPTURE) */ + LPC_SCU->SFSP[9][3] = 0xF6; /* SCU_GPIO_FAST | func 6 */ + /* SGPIO10 = P8_2 function 4 (HOST_DISABLE - output to FPGA) */ + LPC_SCU->SFSP[8][2] = 0xF4; /* SCU_GPIO_FAST | func 4 */ + /* SGPIO11 = P1_17 function 6 (HOST_DIRECTION - output to FPGA, tells FPGA TX vs RX) */ + LPC_SCU->SFSP[1][17] = 0xF6; /* SCU_GPIO_FAST | func 6 */ + { volatile uint32_t delay = 200000; while(delay--); } + + /* SGPIO data pins (SGPIO0-7) - all 8 bits required for sample data */ + LPC_SCU->SFSP[0][0] = 0xF3; /* SGPIO0: P0_0 function 3, HOST_DATA0 */ + LPC_SCU->SFSP[0][1] = 0xF3; /* SGPIO1: P0_1 function 3, HOST_DATA1 */ + LPC_SCU->SFSP[1][15] = 0xF2; /* SGPIO2: P1_15 function 2, HOST_DATA2 */ + LPC_SCU->SFSP[1][16] = 0xF2; /* SGPIO3: P1_16 function 2, HOST_DATA3 */ + /* SGPIO4 already configured above at line 942 */ + LPC_SCU->SFSP[6][6] = 0xF2; /* SGPIO5: P6_6 function 2, HOST_DATA5 */ + LPC_SCU->SFSP[2][2] = 0xF0; /* SGPIO6: P2_2 function 0, HOST_DATA6 */ + LPC_SCU->SFSP[1][0] = 0xF6; /* SGPIO7: P1_0 function 6, HOST_DATA7 */ + { volatile uint32_t delay = 200000; while(delay--); } + + // Trigger FPGA bitstream loading via fpga bridge in portapack.cpp + // Use LEDs to check if boardInit initialization is successful. + + // Setup LED pin directions + // LED1 (USB) = GPIO2[1], LED2 (RX) = GPIO2[2], LED3 (TX) = GPIO2[8] + LPC_GPIO->DIR[2] |= (1 << 1) | (1 << 2) | (1 << 8); + + // Turn off all LEDs to start + // PRALINE LEDs are active-low: SET (HIGH) = OFF, CLR (LOW) = ON + LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8); + { volatile uint32_t delay = 200000; while(delay--); } + +#else + /* Detect HackRF variant */ + /* 1. Perform Standard Initialization first */ + /* This configures VAA power, LED pins, and detects board revision */ + /* Let detect_hackrf_r9() run - don't force for PRALINE */ hackrf_r9 = detect_hackrf_r9(); /* Configure variant-dependent pins. */ if (hackrf_r9) { @@ -834,12 +1083,19 @@ extern "C" void boardInit(void) { setup_gpios(gpio_setup_og); setup_pins(pins_setup_og); } + + /* 2. Turn on VAA (Critical for Radio/Transceiver) */ vaa_power_on(); + + /* 3. Handle VAA Enable Pin Latching */ if (hackrf_r9) { LPC_GPIO->W2[9] = 1; } else { LPC_GPIO->W3[6] = 1; } + +#endif + } extern "C" void _default_exit(void) { @@ -848,10 +1104,10 @@ extern "C" void _default_exit(void) { } else { LPC_GPIO->W3[6] = 0; } + vaa_power_off(); chSysDisable(); - systick_stop(); /* Don't reset these peripherals, as they're operating during shutdown: diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c new file mode 100644 index 000000000..d11c2ee40 --- /dev/null +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c @@ -0,0 +1,622 @@ +// This bridge allows calls for functions in their native C context +// HackRF headers here - C for use within the C++ board.cpp context + +// Check if PRALINE was passed from CMake +#ifdef PRALINE + #include "fpga_bridge.h" + + // Necessary headers + #include "lz4_blk.h" + + // LIBOPENCM3 Headers (only CGU for clock setup) + #include + + #include + #include + #include + #include + + // SPIFI memory-mapped base address + // Flash is mapped starting at 0x14000000 + // FPGA bitstream at flash address 0x380000 = memory address 0x14380000 + // PRALINE: Moved to 1.5MB offset to allow larger base firmware + #define SPIFI_DATA_BASE 0x14000000 + #define FPGA_BITSTREAM_FLASH_ADDR 0x380000 // Was 0x100000 (1MB), then 0x180000 (1.5MB), now 0x380000 (3.5MB) + #define FPGA_BITSTREAM_MEM_ADDR (SPIFI_DATA_BASE + FPGA_BITSTREAM_FLASH_ADDR) + + // MMIO32 direct register access + #define MMIO32_LOCAL(addr) (*(volatile uint32_t*)(addr)) + + // SSP1 base address + #define SSP1_BASE_LOCAL 0x400C5000 + + // SSP register offsets + #define SSP_CR0_OFF 0x000 + #define SSP_CR1_OFF 0x004 + #define SSP_DR_OFF 0x008 + #define SSP_SR_OFF 0x00C + #define SSP_CPSR_OFF 0x010 + + // SSP register access + #define SSP1_CR0_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_CR0_OFF) + #define SSP1_CR1_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_CR1_OFF) + #define SSP1_DR_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_DR_OFF) + #define SSP1_SR_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_SR_OFF) + #define SSP1_CPSR_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_CPSR_OFF) + + // SSP status bits + #define SSP_SR_TNF_LOCAL (1 << 1) // TX FIFO not full + #define SSP_SR_RNE_LOCAL (1 << 2) // RX FIFO not empty + #define SSP_SR_BSY_LOCAL (1 << 4) // Busy + + // SSP CR0 bits + #define SSP_CR0_DSS_8BIT (0x7) // 8-bit data + #define SSP_CR0_FRF_SPI (0x0) // SPI frame format + #define SSP_CR0_CPOL (1 << 6) // Clock polarity + #define SSP_CR0_CPHA (1 << 7) // Clock phase + + // SSP CR1 bits + #define SSP_CR1_SSE (1 << 1) // SSP enable + + // SCU pin configuration registers + #define PERIPH_BASE_APB0_LOCAL 0x40080000 + #define SCU_BASE_LOCAL (PERIPH_BASE_APB0_LOCAL + 0x06000) + #define PIN_GROUP1_LOCAL (SCU_BASE_LOCAL + 0x080) + #define PIN_GROUP4_LOCAL (SCU_BASE_LOCAL + 0x200) + #define PIN_GROUP5_LOCAL (SCU_BASE_LOCAL + 0x280) + #define PIN3_LOCAL 0x00C + #define PIN4_LOCAL 0x010 + #define PIN1_LOCAL 0x004 + #define PIN2_LOCAL 0x008 + #define PIN10_LOCAL 0x028 + #define PIN19_LOCAL 0x04C + + // SCU configuration flags + #define SCU_CONF_EPUN_DIS_PULLUP_LOCAL (1 << 4) + #define SCU_CONF_EHS_FAST_LOCAL (1 << 5) + #define SCU_CONF_EZI_EN_IN_BUFFER_LOCAL (1 << 6) + #define SCU_CONF_ZIF_DIS_IN_GLITCH_FILT_LOCAL (1 << 7) + #define SCU_GPIO_FAST_LOCAL (SCU_CONF_EPUN_DIS_PULLUP_LOCAL | \ + SCU_CONF_EHS_FAST_LOCAL | \ + SCU_CONF_EZI_EN_IN_BUFFER_LOCAL | \ + SCU_CONF_ZIF_DIS_IN_GLITCH_FILT_LOCAL) + #define SCU_SSP_IO_LOCAL SCU_GPIO_FAST_LOCAL + + // Function select values + #define SCU_CONF_FUNCTION0_LOCAL (0x0) + #define SCU_CONF_FUNCTION1_LOCAL (0x1) + #define SCU_CONF_FUNCTION4_LOCAL (0x4) + #define SCU_CONF_FUNCTION5_LOCAL (0x5) + #define SCU_GPIO_NOPULL_LOCAL (SCU_CONF_EZI_EN_IN_BUFFER_LOCAL | SCU_CONF_ZIF_DIS_IN_GLITCH_FILT_LOCAL) + #define SCU_GPIO_PUP_LOCAL (SCU_CONF_EZI_EN_IN_BUFFER_LOCAL) + + // SSP1 pins (for FPGA programming) + #define SCU_SSP1_CIPO_LOCAL (PIN_GROUP1_LOCAL + PIN3_LOCAL) // P1_3 + #define SCU_SSP1_COPI_LOCAL (PIN_GROUP1_LOCAL + PIN4_LOCAL) // P1_4 + #define SCU_SSP1_SCK_LOCAL (PIN_GROUP1_LOCAL + PIN19_LOCAL) // P1_19 + + // FPGA control pins + #define SCU_FPGA_CRESET_LOCAL (PIN_GROUP5_LOCAL + PIN2_LOCAL) // P5_2 GPIO2[11] + #define SCU_FPGA_CDONE_LOCAL (PIN_GROUP4_LOCAL + PIN10_LOCAL) // P4_10 GPIO5[14] + #define SCU_FPGA_SPI_CS_LOCAL (PIN_GROUP5_LOCAL + PIN1_LOCAL) // P5_1 GPIO2[10] + + // GPIO register addresses for direct MMIO access + #define GPIO_LPC_BASE_LOCAL 0x400F4000 + #define GPIO_DIR_BASE (GPIO_LPC_BASE_LOCAL + 0x2000) // Direction registers + #define GPIO_SET_BASE (GPIO_LPC_BASE_LOCAL + 0x2200) // Set registers + #define GPIO_CLR_BASE (GPIO_LPC_BASE_LOCAL + 0x2280) // Clear registers + #define GPIO_PIN_BASE (GPIO_LPC_BASE_LOCAL + 0x2100) // Pin read registers + + // GPIO port access macros + #define GPIO_DIR(port) MMIO32_LOCAL(GPIO_DIR_BASE + (port)*4) + #define GPIO_SET(port) MMIO32_LOCAL(GPIO_SET_BASE + (port)*4) + #define GPIO_CLR(port) MMIO32_LOCAL(GPIO_CLR_BASE + (port)*4) + #define GPIO_PIN(port) MMIO32_LOCAL(GPIO_PIN_BASE + (port)*4) + + // FPGA control GPIO pins + // GPIO2[11] = CRESET, GPIO5[14] = CDONE, GPIO2[10] = SPI_CS + #define FPGA_CRESET_PORT 2 + #define FPGA_CRESET_PIN 11 + #define FPGA_CDONE_PORT 5 + #define FPGA_CDONE_PIN 14 + #define FPGA_SPI_CS_PORT 2 + #define FPGA_SPI_CS_PIN 10 + + // ============================================================================ + // Canonical Default Values - SINGLE SOURCE OF TRUTH + // ============================================================================ + /* Define the canonical RX defaults in ONE place */ + #define FPGA_RX_DEFAULT_DC_WIDTH 0x04 /* Typical for 40MHz stability */ + #define FPGA_RX_DEFAULT_ADAPT_RATE 0x08 /* Typical for 40MHz stability */ + #define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00 /* No shift initially */ + + /* Define TX defaults */ + #define FPGA_TX_DEFAULT_NCO_CTRL 0x00 /* NCO disabled */ + #define FPGA_TX_DEFAULT_INTERP 0x00 /* No interpolation */ + #define FPGA_TX_DEFAULT_PHASE_STEP 0x00 /* Zero phase step */ + + + // ============================================================================ + // Static Variables - Declare BEFORE use + // ============================================================================ + static fpga_mode_t current_mode = FPGA_MODE_OFF; + // Cached register values for debug reads (since reads may require mode switch) + static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00}; + + // Context structure for SPIFI-based reading + struct spifi_fpga_read_ctx { + const uint8_t* mem_ptr; // Current read position in SPIFI memory + size_t next_block_sz; + uint8_t init_flag; + uint8_t buffer[4096 + 2]; // Compressed block + next size + }; + + // ============================================================================ + // Low-Level Helper Functions + // ============================================================================ + + // Simple delay loop + static void delay_cycles(volatile uint32_t count) { + while (count--) { + __asm__ volatile ("nop"); + } + } + + // Microsecond delay (approximate, assuming ~200MHz clock) + static void delay_us(uint32_t us) { + // ~50 cycles per microsecond at 200MHz + delay_cycles(us * 50); + } + + // SSP1 transfer one byte + static uint8_t ssp1_transfer_byte(uint8_t data) { + // Wait for TX FIFO not full + while ((SSP1_SR_LOCAL & SSP_SR_TNF_LOCAL) == 0) {} + SSP1_DR_LOCAL = data; + // Wait for not busy + while (SSP1_SR_LOCAL & SSP_SR_BSY_LOCAL) {} + // Wait for RX FIFO not empty + while ((SSP1_SR_LOCAL & SSP_SR_RNE_LOCAL) == 0) {} + return SSP1_DR_LOCAL; + } + + // Configure SSP1 for iCE40 programming (SPI mode 3: CPOL=1, CPHA=1) + static void ssp1_init_ice40(void) { + // Disable SSP1 first + SSP1_CR1_LOCAL = 0; + + // Configure: 8-bit, SPI mode 3 (CPOL=1, CPHA=1), master mode + // SCR=21 for ~4MHz at 200MHz clock + SSP1_CR0_LOCAL = SSP_CR0_DSS_8BIT | SSP_CR0_FRF_SPI | SSP_CR0_CPOL | SSP_CR0_CPHA | (21 << 8); + + // Clock prescaler = 2 (divide by 2) + SSP1_CPSR_LOCAL = 2; + + // Enable SSP1 + SSP1_CR1_LOCAL = SSP_CR1_SSE; + } + + // Configure SSP1 pins via SCU + static void configure_ssp1_pins(void) { + // P1_3 = SSP1_MISO (function 5) + MMIO32_LOCAL(SCU_SSP1_CIPO_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL; + // P1_4 = SSP1_MOSI (function 5) + MMIO32_LOCAL(SCU_SSP1_COPI_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL; + // P1_19 = SSP1_SCK (function 1) + MMIO32_LOCAL(SCU_SSP1_SCK_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION1_LOCAL; + } + + // Configure FPGA control pins via SCU and GPIO + static void configure_fpga_control_pins(void) { + // P5_2 = GPIO2[11] = CRESET (function 0, output) + MMIO32_LOCAL(SCU_FPGA_CRESET_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL; + // P4_10 = GPIO5[14] = CDONE (function 4, input with pullup) + MMIO32_LOCAL(SCU_FPGA_CDONE_LOCAL) = SCU_GPIO_PUP_LOCAL | SCU_CONF_FUNCTION4_LOCAL; + // P5_1 = GPIO2[10] = SPI_CS (function 0, output) + MMIO32_LOCAL(SCU_FPGA_SPI_CS_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL; + + // Set CRESET and SPI_CS as outputs (GPIO2[11] and GPIO2[10]) + GPIO_DIR(FPGA_CRESET_PORT) |= (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN); + // Clear both initially + GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN); + + // CDONE is input (GPIO5[14]) + GPIO_DIR(FPGA_CDONE_PORT) &= ~(1 << FPGA_CDONE_PIN); + } + + // GPIO control helpers + static void fpga_creset_low(void) { + GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN); + } + static void fpga_creset_high(void) { + GPIO_SET(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN); + } + static void fpga_cs_low(void) { + GPIO_CLR(FPGA_SPI_CS_PORT) = (1 << FPGA_SPI_CS_PIN); + } + static void fpga_cs_high(void) { + GPIO_SET(FPGA_SPI_CS_PORT) = (1 << FPGA_SPI_CS_PIN); + } + static bool fpga_cdone_read(void) { + return (GPIO_PIN(FPGA_CDONE_PORT) & (1 << FPGA_CDONE_PIN)) != 0; + } + + // ============================================================================ + // FPGA Register Access via SPI (iCE40) + // ============================================================================ + // These functions allow reading/writing FPGA internal registers via SPI. + // The FPGA bitstream implements a simple SPI register interface. + // + // FPGA Register Map: + // Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7) + // Reg 2 (RX_DECIM): Decimation ratio [2:0] + // Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control + // Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0] + // Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0] + // + // SPI Protocol: + // Read: Send [reg & 0x7F, 0x00, 0x00] -> value in byte 3 + // Write: Send [(reg | 0x80), value, 0x00] + + + // ============================================================================ + // SPI Mode Switching + // ============================================================================ + + // Configure SSP1 for iCE40 FPGA register access (Mode 3, 8-bit) + static void ssp1_set_mode_ice40(void) { + SSP1_CR1_LOCAL = 0; // Disable SSP1 + SSP1_CR0_LOCAL = SSP_CR0_DSS_8BIT | SSP_CR0_FRF_SPI | SSP_CR0_CPOL | SSP_CR0_CPHA | (21 << 8); + SSP1_CPSR_LOCAL = 2; + SSP1_CR1_LOCAL = SSP_CR1_SSE; // Enable SSP1 + } + + // Configure SSP1 back to MAX2831 mode (Mode 0, 9-bit) + static void ssp1_set_mode_max2831(void) { + SSP1_CR1_LOCAL = 0; // Disable SSP1 + SSP1_CR0_LOCAL = (0x08) | // 9-bit data (DSS = 0x08) + (0x00) | // SPI frame format + (0 << 6) | // CPOL = 0 (Mode 0) + (0 << 7) | // CPHA = 0 (Mode 0) + (21 << 8); // SCR = 21 + SSP1_CPSR_LOCAL = 2; + SSP1_CR1_LOCAL = SSP_CR1_SSE; // Enable SSP1 + } + + + // ============================================================================ + // Low-Level SPI Register Access (internal, no mode switch) + // ============================================================================ + + // Read an FPGA register via SPI + static uint8_t fpga_spi_read(uint8_t reg) { + uint8_t value; + fpga_cs_low(); + ssp1_transfer_byte(reg & 0x7F); // Clear MSB for read + ssp1_transfer_byte(0x00); // Dummy byte + value = ssp1_transfer_byte(0x00); // Read value + fpga_cs_high(); + return value; + } + + // Write an FPGA register via SPI + static void fpga_spi_write(uint8_t reg, uint8_t value) { + fpga_cs_low(); + ssp1_transfer_byte((reg & 0x7F) | 0x80); // Set MSB for write + ssp1_transfer_byte(value); + ssp1_transfer_byte(0x00); // Dummy byte + fpga_cs_high(); + } + + // ============================================================================ + // Public Debug Functions (switch SPI mode, access register, switch back) + // ============================================================================ + + // Public function to read FPGA register (callable from C++ application code) + // Switches SPI mode, reads register, switches back + uint8_t fpga_debug_register_read(uint8_t reg) { + if (reg == 0 || reg > 5) return 0xFF; + + uint8_t value; + ssp1_set_mode_ice40(); + value = fpga_spi_read(reg); + ssp1_set_mode_max2831(); + + fpga_reg_cache[reg] = value; + return value; + } + + // Public function to write FPGA register (callable from C++ application code) + void fpga_debug_register_write(uint8_t reg, uint8_t value) { + if (reg == 0 || reg > 5) return; + + ssp1_set_mode_ice40(); + fpga_spi_write(reg, value); + ssp1_set_mode_max2831(); + + fpga_reg_cache[reg] = value; + } + + // ============================================================================ + // Public Register Access (wraps debug functions) + // ============================================================================ + + uint8_t fpga_register_read(uint8_t reg) { + if (reg == 0 || reg > 5) return 0xFF; + + ssp1_set_mode_ice40(); + uint8_t val = fpga_spi_read(reg); + ssp1_set_mode_max2831(); + + fpga_reg_cache[reg] = val; + return val; + } + + void fpga_register_write(uint8_t reg, uint8_t value) { + if (reg == 0 || reg > 5) return; + + ssp1_set_mode_ice40(); + fpga_spi_write(reg, value); + ssp1_set_mode_max2831(); + + fpga_reg_cache[reg] = value; + } + + // ============================================================================ + // Mode Management + // ============================================================================ + + fpga_mode_t fpga_get_mode(void) { + return current_mode; + } + + /* fpga_set_mode with consistent values */ + void fpga_set_mode(fpga_mode_t mode) { + current_mode = mode; + } + + // ============================================================================ + // RX Mode Functions (with mode assertion) + // ============================================================================ + + /* RX decimation */ + void fpga_rx_set_decimation(uint8_t ratio) { + if (current_mode != FPGA_MODE_RX) return; + fpga_register_write(FPGA_REG_DECIM, ratio & 0x07); + } + + /* RX DC block enable (bit 0 of register 1) */ + void fpga_rx_enable_dc_block(bool enable) { + if (current_mode != FPGA_MODE_RX) return; + uint8_t ctrl = fpga_register_read(FPGA_REG_CTRL); + if (enable) + ctrl |= FPGA_CTRL_DC_BLOCK_EN; + else + ctrl &= ~FPGA_CTRL_DC_BLOCK_EN; + fpga_register_write(FPGA_REG_CTRL, ctrl); + } + + /* RX Functions with mode assertion */ + void fpga_rx_set_digital_gain(uint8_t shift) { + if (current_mode != FPGA_MODE_RX) { + /* Log error or assert - wrong mode! */ + return; + } + fpga_register_write(FPGA_REG_SHARED_3, shift & FPGA_RX_GAIN_SHIFT_MASK); + } + + void fpga_rx_set_dc_block_width(uint8_t width) { + if (current_mode != FPGA_MODE_RX) return; + fpga_register_write(FPGA_REG_SHARED_4, width & FPGA_RX_DC_WIDTH_MASK); + } + + void fpga_rx_set_dc_adapt_rate(uint8_t rate) { + if (current_mode != FPGA_MODE_RX) return; + fpga_register_write(FPGA_REG_SHARED_5, rate); + } + + // ============================================================================ + // TX Mode Functions (with mode assertion) + // ============================================================================ + + /* TX Functions with mode assertion */ + void fpga_tx_set_nco_enable(bool enable) { + if (current_mode != FPGA_MODE_TX) return; + uint8_t val = fpga_register_read(FPGA_REG_SHARED_3); + if (enable) + val |= FPGA_TX_NCO_EN; + else + val &= ~FPGA_TX_NCO_EN; + fpga_register_write(FPGA_REG_SHARED_3, val); + } + + void fpga_tx_set_interpolation(uint8_t ratio) { + if (current_mode != FPGA_MODE_TX) return; + fpga_register_write(FPGA_REG_SHARED_4, ratio & FPGA_TX_INTERP_MASK); + } + + void fpga_tx_set_phase_step(uint8_t step) { + if (current_mode != FPGA_MODE_TX) return; + fpga_register_write(FPGA_REG_SHARED_5, step); + } + + // ============================================================================ + // FPGA Register Initialization (called after bitstream load) + // ============================================================================ + + // Initialize FPGA registers after bitstream load + // This is equivalent to fpga_init() in the reference HackRF firmware + /* fpga_register_init with consistent values */ + static void fpga_register_init(void) { + + /* Boot into RX mode */ + current_mode = FPGA_MODE_RX; + + fpga_spi_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); + fpga_spi_write(FPGA_REG_DECIM, 0x00); + fpga_spi_write(FPGA_REG_SHARED_3, FPGA_RX_DEFAULT_DIGITAL_GAIN); + fpga_spi_write(FPGA_REG_SHARED_4, FPGA_RX_DEFAULT_DC_WIDTH); + fpga_spi_write(FPGA_REG_SHARED_5, FPGA_RX_DEFAULT_ADAPT_RATE); + + /* Update cache */ + fpga_reg_cache[1] = FPGA_CTRL_DC_BLOCK_EN; + fpga_reg_cache[2] = 0x00; + fpga_reg_cache[3] = FPGA_RX_DEFAULT_DIGITAL_GAIN; + fpga_reg_cache[4] = FPGA_RX_DEFAULT_DC_WIDTH; + fpga_reg_cache[5] = FPGA_RX_DEFAULT_ADAPT_RATE; + } + + // ============================================================================ + // LZ4 Decompression for FPGA Bitstream + // ============================================================================ + + // SPIFI-based read callback for LZ4 decompression + // Reads from SPIFI memory-mapped address instead of using SPI flash driver + static size_t spifi_fpga_read_block_cb(void* _ctx, uint8_t* out_buffer) { + struct spifi_fpga_read_ctx* ctx = (struct spifi_fpga_read_ctx*)_ctx; + size_t block_sz = ctx->next_block_sz; + + // First iteration: read first block size from SPIFI memory + if (ctx->init_flag == 0) { + block_sz = ctx->mem_ptr[0] | (ctx->mem_ptr[1] << 8); + ctx->mem_ptr += 2; + ctx->init_flag = 1; + } + + // Finish at end marker (block_sz == 0) + if (block_sz == 0) return 0; + + // Read compressed block from SPIFI memory + memcpy(ctx->buffer, ctx->mem_ptr, block_sz + 2); + ctx->mem_ptr += block_sz + 2; + + // Extract next block size + ctx->next_block_sz = ctx->buffer[block_sz] | (ctx->buffer[block_sz + 1] << 8); + + // Decompress block using LZ4 + return lz4_blk_decompress(ctx->buffer, out_buffer, block_sz); + } + + // Program iCE40 FPGA using SPIFI memory-mapped data + // Based on ice40_spi_syscfg_program() from ice40_spi.c + static bool program_fpga_from_spifi(const uint8_t* bitstream_start) { + // Drive CRESET_B = 0, SPI_SS = 0 + fpga_creset_low(); + fpga_cs_low(); + + // Wait minimum 200ns + delay_us(1); + + // Release CRESET_B (drive high) + fpga_creset_high(); + + // Wait minimum 1200us (we wait 1800us to be safe) + delay_us(1800); + + // Set SPI_SS = 1, send 8 dummy clocks + fpga_cs_high(); + ssp1_transfer_byte(0); + + // Send configuration image + // Use static buffers to avoid stack overflow (~8KB would be needed) + static uint8_t out_buffer[4096]; + static struct spifi_fpga_read_ctx ctx; + ctx.mem_ptr = bitstream_start; + ctx.next_block_sz = 0; + ctx.init_flag = 0; + + fpga_cs_low(); + // Full LZ4 decompress and send all bytes + for (;;) { + size_t read_sz = spifi_fpga_read_block_cb(&ctx, out_buffer); + if (read_sz == 0) break; + for (size_t j = 0; j < read_sz; j++) { + ssp1_transfer_byte(out_buffer[j]); + } + } + + // Wait for 100 clock cycles for CDONE to go high + fpga_cs_high(); + for (size_t j = 0; j < 13; j++) { + ssp1_transfer_byte(0); + } + + // Check CDONE status + bool success = fpga_cdone_read(); + + // NOTE: FPGA register initialization is done later in radio::init() + // The FPGA needs time to stabilize after configuration before accepting register writes + + // CRITICAL: Reconfigure SSP1 for MAX2831 (PRALINE RF chip) after FPGA programming + // iCE40 uses Mode 3 (CPOL=1, CPHA=1), 8-bit + // MAX2831 (PRALINE) uses Mode 0 (CPOL=0, CPHA=0), 9-bit (vs 16-bit for MAX283x on HackRF One) + // Without this, RF communication will fail! + /*SSP1_CR1_LOCAL = 0; // Disable SSP1 + SSP1_CR0_LOCAL = (0x08) | // 9-bit data (DSS = 0x08) for MAX2831/PRALINE + (0x00) | // SPI frame format + (0 << 6) | // CPOL = 0 (Mode 0) + (0 << 7) | // CPHA = 0 (Mode 0) + (21 << 8); // SCR = 21 (same as ssp_config_max283x for PRALINE) + SSP1_CPSR_LOCAL = 2; // Clock prescaler + SSP1_CR1_LOCAL = SSP_CR1_SSE; // Re-enable SSP1*/ + + return success; + } + + // ============================================================================ + // Main Initialization Entry Point + // ============================================================================ + + int fpga_bridge_init(void) { + // Enable SSP1 clock for FPGA programming + // Use PLL1 (204MHz) to match original HackRF - IRC (12MHz) is 17x too slow + CGU_BASE_SSP1_CLK = CGU_BASE_SSP1_CLK_AUTOBLOCK(1) | + CGU_BASE_SSP1_CLK_CLK_SEL(CGU_SRC_PLL1); + + // Configure SSP1 pins + configure_ssp1_pins(); + + // Configure FPGA control pins + configure_fpga_control_pins(); + + // Initialize SSP1 for iCE40 programming + ssp1_init_ice40(); + + // Read FPGA bitstream header from SPIFI memory + const uint8_t* fpga_header = (const uint8_t*)FPGA_BITSTREAM_MEM_ADDR; + uint32_t num_bitstreams = fpga_header[0] | (fpga_header[1] << 8) | + (fpga_header[2] << 16) | (fpga_header[3] << 24); + + // Check if header looks valid + if (num_bitstreams == 0 || num_bitstreams > 16 || num_bitstreams == 0xFFFFFFFF) { + // No valid FPGA bitstream - skip programming but continue boot + return 1; + } + + // Get offset of first bitstream (index 0 = standard_fpga) + uint32_t bitstream_offset = fpga_header[4] | (fpga_header[5] << 8) | + (fpga_header[6] << 16) | (fpga_header[7] << 24); + + // Calculate start address of first bitstream in SPIFI memory + const uint8_t* bitstream_start = (const uint8_t*)(FPGA_BITSTREAM_MEM_ADDR + bitstream_offset); + + // Full FPGA programming + bool success = program_fpga_from_spifi(bitstream_start); + + // Initialize FPGA registers immediately after programming + if (success) { + // Give FPGA 100us to stabilize after configuration + delay_us(100); + + // Initialize FPGA registers (DC_BLOCK, etc.) + fpga_register_init(); + + // Now switch to MAX2831 mode + ssp1_set_mode_max2831(); + } + + return success ? 0 : 2; + } +#else +#warning "Building for HackRF_One with CPLD." +#endif diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h new file mode 100644 index 000000000..e7fb0bf6c --- /dev/null +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h @@ -0,0 +1,167 @@ +/* + * FPGA Bridge Header - PRALINE iCE40 FPGA interface + * + * Provides functions for initializing and accessing the FPGA on HackRF Pro (PRALINE). + */ + +#ifndef __FPGA_BRIDGE_H__ +#define __FPGA_BRIDGE_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifdef PRALINE + +/* FPGA Register Map Address 0x03 (Dual Purpose) */ +#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */ +#define FPGA_REG_TX_CONTROL 0x03 /* NCO_EN and TX flags (TX Mode) */ + +/* FPGA Register Map Address 0x04 (Shared) */ +#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */ +#define FPGA_REG_TX_INTERP 0x04 /* Interpolation ratio (TX Mode) */ + +/* FPGA Register Map Address 0x05 (Shared) */ +#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */ +#define FPGA_REG_TX_PHASE_STEP 0x05 /* NCO frequency step (TX Mode) */ + +/* + * FPGA Operating Mode + */ +typedef enum { + FPGA_MODE_OFF = 0, + FPGA_MODE_RX, + FPGA_MODE_TX +} fpga_mode_t; + +/* + * FPGA Register Addresses + * Note: Registers 3-5 are dual-purpose (meaning depends on RX/TX mode) + */ +#define FPGA_REG_CTRL 0x01 /* Control register */ +#define FPGA_REG_DECIM 0x02 /* Decimation (RX) / unused (TX) */ +#define FPGA_REG_SHARED_3 0x03 /* Dual-purpose register */ +#define FPGA_REG_SHARED_4 0x04 /* Dual-purpose register */ +#define FPGA_REG_SHARED_5 0x05 /* Dual-purpose register */ + +/* + * Register 1 (CTRL) Bit Definitions + */ +#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */ +#define FPGA_CTRL_QUARTER_SHIFT_EN (1 << 1) /* Quarter-rate shift enable */ +#define FPGA_CTRL_QUARTER_SHIFT_UP (1 << 2) /* Shift direction: 1=up, 0=down */ +#define FPGA_CTRL_TX_MODE (1 << 5) /* TX mode indicator (if applicable) */ +#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */ +#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */ + +/* + * Register 3 Dual-Purpose Definitions + */ +/* RX Mode: Digital gain/shift */ +#define FPGA_REG3_RX_DIGITAL_GAIN 0x03 +#define FPGA_RX_GAIN_SHIFT_MASK 0x0F /* Bits [3:0] - shift amount */ + +/* TX Mode: NCO control */ +#define FPGA_REG3_TX_NCO_CTRL 0x03 +#define FPGA_TX_NCO_EN (1 << 0) /* NCO enable */ +#define FPGA_TX_NCO_INVERT (1 << 1) /* Invert spectrum */ + +/* + * Register 4 Dual-Purpose Definitions + */ +/* RX Mode: DC block notch width */ +#define FPGA_REG4_RX_DC_WIDTH 0x04 +#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */ + +/* TX Mode: Interpolation ratio */ +#define FPGA_REG4_TX_INTERP 0x04 +#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */ + +/* + * Register 5 Dual-Purpose Definitions + */ +/* RX Mode: DC block adaptation rate */ +#define FPGA_REG5_RX_DC_RATE 0x05 +#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */ + +/* TX Mode: NCO phase step (frequency) */ +#define FPGA_REG5_TX_PHASE_STEP 0x05 +#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */ + +/* Export default values so other methods can use them */ +#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00 +#define FPGA_RX_DEFAULT_DC_WIDTH 0x04 +#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08 + +/* + * Core Functions + */ + +/* Initialize the FPGA - loads bitstream from SPIFI flash + * Returns: 0 on success, non-zero on failure */ +int fpga_bridge_init(void); + +/* Set operating mode - MUST be called before using mode-specific functions + * This ensures registers 3-5 are interpreted correctly */ +void fpga_set_mode(fpga_mode_t mode); + +/* Get current operating mode */ +fpga_mode_t fpga_get_mode(void); + +/* + * Low-Level Register Access (use with caution) + */ +uint8_t fpga_register_read(uint8_t reg); +void fpga_register_write(uint8_t reg, uint8_t value); + +/* + * RX Mode Functions (only valid when mode == FPGA_MODE_RX) + */ +void fpga_rx_set_decimation(uint8_t ratio); +void fpga_rx_set_digital_gain(uint8_t shift); +void fpga_rx_set_dc_block_width(uint8_t width); +void fpga_rx_set_dc_adapt_rate(uint8_t rate); +void fpga_rx_enable_dc_block(bool enable); + +/* + * TX Mode Functions (only valid when mode == FPGA_MODE_TX) + */ +void fpga_tx_set_interpolation(uint8_t ratio); +void fpga_tx_set_nco_enable(bool enable); +void fpga_tx_set_phase_step(uint8_t step); + +/* + * Debug Functions + */ + +/* + * Read an FPGA register via SPI + * reg: Register number (1-5) + * Returns: Register value, or 0xFF if invalid register + * + * FPGA Register Map: + * Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7) + * Reg 2 (RX_DECIM): Decimation ratio [2:0] + * Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control + * Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0] + * Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0] + */ +uint8_t fpga_debug_register_read(uint8_t reg); + +/* + * Write an FPGA register via SPI + * reg: Register number (1-5) + * value: Value to write + */ +void fpga_debug_register_write(uint8_t reg, uint8_t value); + +#endif /* PRALINE */ + +#ifdef __cplusplus +} +#endif + +#endif /* __FPGA_BRIDGE_H__ */ diff --git a/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M0/ld/LPC43xx_M0.ld b/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M0/ld/LPC43xx_M0.ld index a6a9050e7..03c185ef2 100755 --- a/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M0/ld/LPC43xx_M0.ld +++ b/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M0/ld/LPC43xx_M0.ld @@ -23,8 +23,16 @@ __process_stack_size__ = 0x1000; /* main() stack */ MEMORY { - flash (rx) : org = 0x00000000, len = 1024k /* SPIFI flash @ 0x140????? */ - ram (rwx) : org = 0x20000000, len = 64k /* AHB SRAM @ 0x20000000 */ + flash (rx) : org = 0x00000000, len = LD_FLASH_SIZE /* SPIFI flash @ 0x140????? */ + + /* HackRF One (Standard): 64KB AHB SRAM @ 0x20000000 + * HackRF Pro (Praline): Hardware supports 128KB high-throughput buffers. + * The actual size is controlled by LD_RAM_SIZE (set in rules.cmake). + * AHB SRAM @ 0x20000000 (stacks, data, bss) */ + ram (rwx) : org = 0x20000000, len = LD_RAM_SIZE + + /* Local SRAM slice for extended heap (0 length on HackRF One) */ + ram_local (rwx) : org = LD_M0_LOCAL_HEAP_ORIGIN, len = LD_M0_LOCAL_HEAP_SIZE } __ram_start__ = ORIGIN(ram); @@ -151,5 +159,10 @@ SECTIONS PROVIDE(end = .); _end = .; -__heap_base__ = _end; -__heap_end__ = __ram_end__; +/* + * Heap configuration: + * - If ram_local has size > 0: use Local SRAM for heap (larger, Praline) + * - Otherwise: use remaining AHB SRAM after bss (HackRF One) + */ +__heap_base__ = (LD_M0_LOCAL_HEAP_SIZE > 0) ? ORIGIN(ram_local) : _end; +__heap_end__ = (LD_M0_LOCAL_HEAP_SIZE > 0) ? (ORIGIN(ram_local) + LENGTH(ram_local)) : __ram_end__; diff --git a/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4.ld b/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4.ld index 145be5c67..c4bafd761 100755 --- a/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4.ld +++ b/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4.ld @@ -23,12 +23,23 @@ __process_stack_size__ = 0x1000; /* main() stack */ MEMORY { - flash (rx) : org = 0x00000000, len = 32752 /* Local SRAM @ 0x10080000 */ - ram (rwx) : org = 0x10000000, len = 96k /* Local SRAM @ 0x10000000 */ - ram_usb (rwx) : org = 0x20008000, len = 32K + /* flash (rx) is actually Local SRAM Bank 2 (shadowed to 0x0 code/rodata) */ + flash (rx) : org = 0x00000000, len = LD_M4_FLASH_LEN /* Local SRAM @ 0x10080000 */ + + /* ram (rwx) is + * Local SRAM Bank 1 @ 0x10000000 (data/bss/stacks) + * Note: On Praline, M0 local heap uses 0x10018000-0x1001FFFF */ + ram (rwx) : org = 0x10000000, len = LD_M4_RAM_LEN /* Local SRAM @ 0x10000000 */ + + /* ram_usb is AHB SRAM Bank 1 @ 0x20008000 (USB buffer) + * HackRF One: 16KB?? originally set to 32KB, so left alone + * Praline: same configuration as HackRF One, while M0 uses all AHB SRAM) */ + ram_usb (rwx) : org = 0x20008000, len = LD_USB_RAM_LEN } +/* Only meaningful if LD_USB_RAM_LEN > 0 */ usb_bulk_buffer = ORIGIN(ram_usb); + __ram_start__ = ORIGIN(ram); __ram_size__ = LENGTH(ram); __ram_end__ = __ram_start__ + __ram_size__; diff --git a/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4_ram.ld b/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4_ram.ld index 7ced05a94..e9756405e 100755 --- a/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4_ram.ld +++ b/firmware/chibios-portapack/os/ports/GCC/ARMCMx/LPC43xx_M4/ld/LPC43xx_M4_ram.ld @@ -23,8 +23,15 @@ __process_stack_size__ = 0x1000; /* main() stack */ MEMORY { - flash : org = 0x00000000, len = 96k /* Local SRAM @ 0x10000000 */ - ram : org = 0x10080000, len = 32k /* Local SRAM @ 0x10080000 */ + /* Local SRAM Bank 1 shadowed to 0x0 (code) + * HackRF One: 96KB + * Praline: 96KB (M0 local heap uses remaining 32KB) */ + flash : org = 0x00000000, len = LD_M4_RAM_LEN /* Local SRAM @ 0x10000000 */ + + /* Local SRAM Bank 2 @ 0x10080000 (data/bss/stacks) + * HackRF One: ~32KB + * Praline: 72KB */ + ram : org = 0x10080000, len = LD_M4_FLASH_LEN /* Local SRAM @ 0x10080000 */ } __ram_start__ = ORIGIN(ram); diff --git a/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake b/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake index 2e20d8536..b4b8c0e71 100644 --- a/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake +++ b/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake @@ -104,4 +104,47 @@ endif() set(CMAKE_C_FLAGS "${CFLAGS} ${TOPT}") set(CMAKE_CXX_FLAGS "${CPPFLAGS} ${TOPT}") set(CMAKE_AS_FLAGS "${ASFLAGS} ${TOPT}") -set(CMAKE_EXE_LINKER_FLAGS "${LDFLAGS}") + +if(BOARD STREQUAL "PRALINE") + # Praline (LPC4330) - Expanded Memory + # AHB SRAM: 32KB + 32KB = 64KB + # Left M0_RAM_SIZE as 64k and USB_RAM_SIZE as 32k for + # backwards consistenct with hackrf one setup. + # Recommend revisit by devs. + # Local SRAM: 128KB (Bank 1) + 72KB (Bank 2) + # Bank 1 can also be allocated as 96KB for M4, and 32KB for M0 + set(M0_RAM_SIZE "64k") # AHB SRAM Bank 0 for M0 (stacks, data, bss) + set(M0_LOCAL_HEAP_SIZE "0") # No Local SRAM, could be 32k heap for additional 16-bit IQ + set(M0_LOCAL_HEAP_ORIGIN "0x10020000") # 0x10018000 if allocating 32k heap for M0 after M4's 96KB + set(M4_RAM_SIZE "128k") # Local SRAM Bank 1 Fully allocated to M4 (HackRF One is 96k) + set(M4_FLASH_SIZE "72k") # Local SRAM Bank 2 + set(USB_RAM_SIZE "32k") # AHB SRAM shared with M0 + set(FLASH_SIZE "4M") +else() + # HackRF One (LPC4320) - Standard Memory + # AHB SRAM: 32KB + 16KB = 48KB (split between M0 and USB) + # Left M0_RAM_SIZE as 64k and USB_RAM_SIZE as 32k to keep + # original hackrf one settings intact. + # Recommend revisit by devs. + # Local SRAM: 96KB (Bank 1) + 40KB (Bank 2) + set(M0_RAM_SIZE "64k") # Standard AHB SRAM for M0 + set(M0_LOCAL_HEAP_SIZE "0k") # No local heap available + set(M0_LOCAL_HEAP_ORIGIN "0") # Not used + set(M4_RAM_SIZE "96k") # Standard Local SRAM Bank 1 + set(M4_FLASH_SIZE "32752") # Standard Local SRAM Bank 2 (~32KB) + set(USB_RAM_SIZE "32k") # Standard 32k/32k split for AHB Bank 1 + set(FLASH_SIZE "1M") # Standard SPI Flash limit +endif() + +# Apply all symbols to the linker flags in one command +# Build linker flags +set(CMAKE_EXE_LINKER_FLAGS "${LDFLAGS} \ + -Wl,--defsym=LD_RAM_SIZE=${M0_RAM_SIZE} \ + -Wl,--defsym=LD_M0_LOCAL_HEAP_SIZE=${M0_LOCAL_HEAP_SIZE} \ + -Wl,--defsym=LD_M0_LOCAL_HEAP_ORIGIN=${M0_LOCAL_HEAP_ORIGIN} \ + -Wl,--defsym=LD_M4_RAM_LEN=${M4_RAM_SIZE} \ + -Wl,--defsym=LD_M4_FLASH_LEN=${M4_FLASH_SIZE} \ + -Wl,--defsym=LD_USB_RAM_LEN=${USB_RAM_SIZE} \ + -Wl,--defsym=LD_FLASH_SIZE=${FLASH_SIZE}" +) + diff --git a/firmware/common/aprs_packet.hpp b/firmware/common/aprs_packet.hpp index 52894b19c..a0d986471 100644 --- a/firmware/common/aprs_packet.hpp +++ b/firmware/common/aprs_packet.hpp @@ -103,8 +103,10 @@ class APRSPacket { std::string get_digipeaters_formatted() { uint8_t position = DIGIPEATER_START; bool has_more = parse_address(SOURCE_START, REPEATER); - - std::string repeaters = ""; + if (!has_more) { + return ""; + } + std::string repeaters = ","; while (has_more) { has_more = parse_address(position, REPEATER); repeaters += std::string(address_buffer); @@ -112,7 +114,7 @@ class APRSPacket { position += ADDRESS_SIZE; if (has_more) { - repeaters += ">"; + repeaters += ","; } } @@ -146,8 +148,8 @@ class APRSPacket { } std::string get_stream_text() { - std::string stream = get_source_formatted() + ">" + get_destination_formatted() + ";" + get_digipeaters_formatted() + ";" + get_information_text_formatted(); - + // get_digipeaters_formatted will add the "," to the beginning if there are digipeaters + std::string stream = get_source_formatted() + ">" + get_destination_formatted() + get_digipeaters_formatted() + ":" + get_information_text_formatted(); return stream; } diff --git a/firmware/common/baseband_sgpio.cpp b/firmware/common/baseband_sgpio.cpp index 611d8aaa9..3b90b918e 100644 --- a/firmware/common/baseband_sgpio.cpp +++ b/firmware/common/baseband_sgpio.cpp @@ -189,6 +189,7 @@ constexpr uint32_t gpio_outreg(const Direction direction) { constexpr uint32_t gpio_oenreg(const Direction direction) { return (0U << PIN_P78) | (0U << PIN_P81) | (0U << PIN_SYNC_EN) | (0U << PIN_INVERT) | (1U << PIN_DIRECTION) | (1U << PIN_DISABLE) | (0U << PIN_CAPTURE) | (0U << PIN_CLKIN) | ((direction == Direction::Transmit) ? 0xffU : 0x00U); + // ^^^^^^^^^^^^^ REVERTED: SGPIO14 must be DISABLED - enabling it blocks data capture! } constexpr uint32_t out_mux_cfg(const P_OUT_CFG out, const P_OE_CFG oe) { @@ -198,13 +199,23 @@ constexpr uint32_t out_mux_cfg(const P_OUT_CFG out, const P_OE_CFG oe) { constexpr uint32_t data_sgpio_mux_cfg( const CONCAT_ENABLE concat_enable, const CONCAT_ORDER concat_order) { +#ifdef PRALINE + return (1U << 0) | (0U << 1) | (3U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12); + // Bits 3-4: CLK_SOURCE_SLICE_MODE = 3 (slice D as clock source for data slices) +#else return (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12); +#endif } constexpr uint32_t data_slice_mux_cfg( const PARALLEL_MODE parallel_mode, const CLK_CAPTURE_MODE clk_capture_mode) { +#ifdef PRALINE + return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (1U << 4) | (toUType(parallel_mode) << 6) | (0U << 8); + // Bit 4 CLKGEN_MODE: 0=internal counter, 1=external clock (REQUIRED for PRALINE!) +#else return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (0U << 4) | (toUType(parallel_mode) << 6) | (0U << 8); +#endif } constexpr uint32_t pos( @@ -264,28 +275,54 @@ void SGPIO::init() { void SGPIO::configure(const Direction direction) { disable_all_slice_counters(); +#ifndef PRALINE // Set data pins as input, temporarily. LPC_SGPIO->GPIO_OENREG = gpio_oenreg(Direction::Receive); // Now that data pins are inputs, safe to change CPLD direction. +#endif + + // HackRF reference: Set GPIO_OUTREG first (DISABLE=HIGH during config) LPC_SGPIO->GPIO_OUTREG = gpio_outreg(direction); +#ifdef PRALINE + // HackRF reference: Set GPIO_OENREG ONCE before OUT_MUX_CFG (not twice!) + LPC_SGPIO->GPIO_OENREG = gpio_oenreg(direction); + + // Now configure OUT_MUX_CFG (HackRF order: control pins first, then data pins) +#endif + LPC_SGPIO->OUT_MUX_CFG[8] = out_mux_cfg(P_OUT_CFG::DOUT_DOUTM1, P_OE_CFG::GPIO_OE); LPC_SGPIO->OUT_MUX_CFG[9] = out_mux_cfg(P_OUT_CFG::DOUT_DOUTM1, P_OE_CFG::GPIO_OE); + +#ifndef PRALINE + // OUT_MUX_CFG[10] NOT configured for PRALINE - breaks PRALINE (HOST_DISABLE signal) LPC_SGPIO->OUT_MUX_CFG[10] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#endif + LPC_SGPIO->OUT_MUX_CFG[11] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#ifndef PRALINE + // SGPIO 12 and 13 are NOT configured - not used per HackRF reference for PRALINE LPC_SGPIO->OUT_MUX_CFG[12] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); LPC_SGPIO->OUT_MUX_CFG[13] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#endif + LPC_SGPIO->OUT_MUX_CFG[14] = out_mux_cfg(P_OUT_CFG::DOUT_DOUTM1, P_OE_CFG::GPIO_OE); + +#ifndef PRALINE + // SGPIO 15 is NOT configured - not used per HackRF reference for PRALINE LPC_SGPIO->OUT_MUX_CFG[15] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#endif const auto data_out_mux_cfg = out_mux_cfg(data_p_out_cfg(slice_mode_multislice), P_OE_CFG::GPIO_OE); for (size_t i = 0; i < 8; i++) { LPC_SGPIO->OUT_MUX_CFG[i] = data_out_mux_cfg; } +#ifndef PRALINE // Now that output enable sources are set, enable data bus in correct direction. LPC_SGPIO->GPIO_OENREG = gpio_oenreg(direction); +#endif const auto slice_gpdma = Slice::H; @@ -293,7 +330,23 @@ void SGPIO::configure(const Direction direction) { const auto clk_capture_mode = data_clk_capture_mode(direction); const auto single_slice = !slice_mode_multislice; +#ifdef PRALINE + // Configure slice D as clock generator (REQUIRED for PRALINE!) + // Reference: HackRF sgpio.c line 193 + const auto slice_d = toUType(Slice::D); + LPC_SGPIO->SGPIO_MUX_CFG[slice_d] = (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (0U << 11) | (0U << 12); + LPC_SGPIO->SLICE_MUX_CFG[slice_d] = (0U << 0) | (0U << 1) | (0U << 2) | (0U << 3) | (1U << 4) | (0U << 6) | (0U << 8); // CLKGEN_MODE=1 + LPC_SGPIO->PRESET[slice_d] = 0; + LPC_SGPIO->COUNT[slice_d] = 0; + LPC_SGPIO->POS[slice_d] = pos(0x1f, 0x1f); + LPC_SGPIO->REG[slice_d] = 0x11111111; + LPC_SGPIO->REG_SS[slice_d] = 0x11111111; + + uint32_t slice_enable_mask = (1U << slice_d); // Start with slice D enabled +#else uint32_t slice_enable_mask = 0; +#endif + for (size_t i = 0; i < slice_count; i++) { const auto slice = slice_order[i]; const auto slice_index = toUType(slice); diff --git a/firmware/common/cpld_update.cpp b/firmware/common/cpld_update.cpp index b0d359aa0..8865bb08a 100644 --- a/firmware/common/cpld_update.cpp +++ b/firmware/common/cpld_update.cpp @@ -213,6 +213,24 @@ CpldUpdateStatus update_autodetect(const Config config_rev_20150901, const Confi namespace hackrf { namespace cpld { +#ifdef PRALINE + +/* PRALINE has no HackRF CPLD - stub these functions out */ +bool load_sram() { + return true; +} +void load_sram_no_verify() { + return; +} +bool verify_eeprom() { + return true; +} +void init_from_eeprom() { + return; +} + +#else + static jtag::GPIOTarget jtag_target_hackrf() { return { hackrf::one::gpio_cpld_tck, @@ -262,5 +280,7 @@ void init_from_eeprom() { hackrf_cpld.init_from_eeprom(); } +#endif // PRALINE + } /* namespace cpld */ } /* namespace hackrf */ diff --git a/firmware/common/dsp_fir_taps.hpp b/firmware/common/dsp_fir_taps.hpp index c1f5e9787..37ea6307b 100644 --- a/firmware/common/dsp_fir_taps.hpp +++ b/firmware/common/dsp_fir_taps.hpp @@ -278,6 +278,37 @@ constexpr fir_taps_real<32> taps_11k0_channel{ }}, }; +// NBFM 12K5F3E emission type ///////////////////////////////////////////// + +// IFIR image-reject filter: fs=3072000, pass=6250, stop=342250, decim=8, fout=384000 +constexpr fir_taps_real<24> taps_12k5_decim_0{ + .low_frequency_normalized = -6250.0f / 3072000.0f, + .high_frequency_normalized = 6250.0f / 3072000.0f, + .transition_normalized = 336000.0f / 3072000.0f, + .taps = {{42, 108, 226, 412, 672, 1004, 1391, 1807, 2211, 2565, 2828, 2969, + 2969, 2828, 2565, 2211, 1807, 1391, 1004, 672, 412, 226, 108, 42}}, +}; + +// IFIR prototype filter: fs=384000, pass=6250, stop=42500, decim=8, fout=48000 +constexpr fir_taps_real<32> taps_12k5_decim_1{ + .low_frequency_normalized = -6250.0f / 384000.0f, + .high_frequency_normalized = 6250.0f / 384000.0f, + .transition_normalized = 36250.0f / 384000.0f, + .taps = {{-38, -85, -162, -245, -315, -338, -275, -85, 255, 752, 1378, 2088, + 2807, 3452, 3939, 4202, 4202, 3939, 3452, 2807, 2088, 1378, 752, 255, + -85, -275, -338, -315, -245, -162, -85, -38}}, +}; + +// Channel filter: fs=48000, pass=6250, stop=9250, decim=1, fout=48000 +constexpr fir_taps_real<32> taps_12k5_channel{ + .low_frequency_normalized = -6250.0f / 48000.0f, + .high_frequency_normalized = 6250.0f / 48000.0f, + .transition_normalized = 3000.0f / 48000.0f, + .taps = {{-75, -362, -712, -908, -592, 268, 1324, 1720, 684, -1522, -3541, -3385, + 342, 7150, 14642, 19541, 19541, 14642, 7150, 342, -3385, -3541, -1522, 684, + 1720, 1324, 268, -592, -908, -712, -362, -75}}, +}; + // NBFM 8K50F3E emission type ///////////////////////////////////////////// // IFIR image-reject filter: fs=3072000, pass=4250, stop=340250, decim=8, fout=384000 @@ -1681,4 +1712,362 @@ static constexpr fir_taps_real<24> taps_BTLE_Dual_PHY = { 3}}, }; +static constexpr fir_taps_real<63> taps_2k0_am_lpf_channel = { + .low_frequency_normalized = -2000.0f / 48000.0f, + .high_frequency_normalized = 2000.0f / 48000.0f, + .transition_normalized = 1500.0f / 48000.0f, + .taps = {{ + 26, + 29, + 31, + 33, + 32, + 28, + 18, + 0, + -26, + -61, + -104, + -152, + -202, + -246, + -279, + -292, + -277, + -227, + -136, + 0, + 182, + 409, + 673, + 968, + 1280, + 1595, + 1899, + 2174, + 2407, + 2583, + 2693, + 2731, + 2693, + 2583, + 2407, + 2174, + 1899, + 1595, + 1280, + 968, + 673, + 409, + 182, + 0, + -136, + -227, + -277, + -292, + -279, + -246, + -202, + -152, + -104, + -61, + -26, + 0, + 18, + 28, + 32, + 33, + 31, + 29, + 26, + }}, +}; + +static constexpr fir_taps_real<64> taps_1K5_FM_channel = { + .low_frequency_normalized = 300.0f / 48000.0f, + .high_frequency_normalized = 1800.0f / 48000.0f, + .transition_normalized = 1000.0f / 48000.0f, + .taps = {{ + 24, + 22, + 20, + 15, + 9, + -2, + -18, + -39, + -66, + -98, + -132, + -167, + -198, + -220, + -228, + -217, + -181, + -117, + -19, + 112, + 277, + 474, + 697, + 941, + 1197, + 1454, + 1702, + 1930, + 2128, + 2285, + 2394, + 2451, + 2451, + 2394, + 2285, + 2128, + 1930, + 1702, + 1454, + 1197, + 941, + 697, + 474, + 277, + 112, + -19, + -117, + -181, + -217, + -228, + -220, + -198, + -167, + -132, + -98, + -66, + -39, + -18, + -2, + 9, + 15, + 20, + 22, + 24, + }}, +}; + +static constexpr fir_taps_real<63> taps_1k5_dsb_lpf = { + .low_frequency_normalized = -1500.0f / 48000.0f, + .high_frequency_normalized = 1500.0f / 48000.0f, + .transition_normalized = 1000.0f / 48000.0f, + .taps = {{ + 0, + 0, + -1, + -5, + -10, + -19, + -31, + -46, + -64, + -82, + -99, + -111, + -113, + -100, + -64, + 0, + 99, + 239, + 424, + 655, + 932, + 1251, + 1605, + 1983, + 2372, + 2757, + 3120, + 3447, + 3719, + 3925, + 4053, + 4096, + 4053, + 3925, + 3719, + 3447, + 3120, + 2757, + 2372, + 1983, + 1605, + 1251, + 932, + 655, + 424, + 239, + 99, + 0, + -64, + -100, + -113, + -111, + -99, + -82, + -64, + -46, + -31, + -19, + -10, + -5, + -1, + 0, + 0, + }}, +}; + +// GAIN: 4.0x +static constexpr fir_taps_complex<63> taps_1k5_USB_channel = { + .low_frequency_normalized = 300.0f / 48000.0f, + .high_frequency_normalized = 1800.0f / 48000.0f, + .transition_normalized = 300.0f / 48000.0f, + .taps = {{ + {0, 0}, + {0, 0}, + {2, 0}, + {4, 2}, + {10, 6}, + {17, 14}, + {27, 29}, + {37, 53}, + {45, 89}, + {49, 140}, + {41, 207}, + {17, 290}, + {-31, 389}, + {-109, 499}, + {-227, 614}, + {-387, 725}, + {-595, 819}, + {-850, 884}, + {-1147, 904}, + {-1477, 866}, + {-1827, 757}, + {-2179, 569}, + {-2512, 297}, + {-2804, -55}, + {-3031, -480}, + {-3173, -962}, + {-3213, -1481}, + {-3142, -2011}, + {-2955, -2524}, + {-2658, -2991}, + {-2262, -3386}, + {-1788, -3685}, + {-1258, -3873}, + {-703, -3939}, + {-153, -3884}, + {366, -3713}, + {826, -3440}, + {1208, -3087}, + {1499, -2677}, + {1693, -2236}, + {1789, -1789}, + {1796, -1359}, + {1726, -966}, + {1594, -624}, + {1420, -341}, + {1220, -120}, + {1012, 40}, + {809, 144}, + {622, 202}, + {460, 223}, + {324, 217}, + {217, 193}, + {137, 160}, + {80, 125}, + {42, 91}, + {19, 62}, + {6, 39}, + {0, 22}, + {-1, 11}, + {-1, 5}, + {-1, 1}, + {0, 0}, + {0, 0}, + }}, +}; + +// GAIN: 4.0x +static constexpr fir_taps_complex<63> taps_1k5_LSB_channel = { + .low_frequency_normalized = 300.0f / 48000.0f, + .high_frequency_normalized = 1800.0f / 48000.0f, + .transition_normalized = 300.0f / 48000.0f, + .taps = {{ + {0, 0}, + {0, 0}, + {2, 0}, + {4, -2}, + {10, -6}, + {17, -14}, + {27, -29}, + {37, -53}, + {45, -89}, + {49, -140}, + {41, -207}, + {17, -290}, + {-31, -389}, + {-109, -499}, + {-227, -614}, + {-387, -725}, + {-595, -819}, + {-850, -884}, + {-1147, -904}, + {-1477, -866}, + {-1827, -757}, + {-2179, -569}, + {-2512, -297}, + {-2804, 55}, + {-3031, 480}, + {-3173, 962}, + {-3213, 1481}, + {-3142, 2011}, + {-2955, 2524}, + {-2658, 2991}, + {-2262, 3386}, + {-1788, 3685}, + {-1258, 3873}, + {-703, 3939}, + {-153, 3884}, + {366, 3713}, + {826, 3440}, + {1208, 3087}, + {1499, 2677}, + {1693, 2236}, + {1789, 1789}, + {1796, 1359}, + {1726, 966}, + {1594, 624}, + {1420, 341}, + {1220, 120}, + {1012, -40}, + {809, -144}, + {622, -202}, + {460, -223}, + {324, -217}, + {217, -193}, + {137, -160}, + {80, -125}, + {42, -91}, + {19, -62}, + {6, -39}, + {0, -22}, + {-1, -11}, + {-1, -5}, + {-1, -1}, + {0, 0}, + {0, 0}, + }}, +}; + #endif /*__DSP_FIR_TAPS_H__*/ diff --git a/firmware/common/hackrf_gpio.hpp b/firmware/common/hackrf_gpio.hpp index acbb820c3..459bea7c5 100644 --- a/firmware/common/hackrf_gpio.hpp +++ b/firmware/common/hackrf_gpio.hpp @@ -41,12 +41,23 @@ constexpr GPIO gpio_led_tx = gpio[GPIO2_8]; constexpr GPIO gpio_og_1v8_enable = gpio[GPIO3_6]; constexpr GPIO gpio_r9_1v8_enable = gpio[GPIO2_9]; constexpr GPIO gpio_vregmode = gpio[GPIO3_7]; +#ifdef PRALINE +// PRALINE uses different power control pins +constexpr GPIO gpio_og_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1) +constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1) +#else constexpr GPIO gpio_og_vaa_disable = gpio[GPIO2_9]; constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO3_6]; +#endif constexpr GPIO gpio_rx_mix_bp = gpio[GPIO2_12]; constexpr GPIO gpio_tx_mix_bp = gpio[GPIO2_11]; +#ifdef PRALINE +constexpr GPIO gpio_mix_bypass = gpio[GPIO3_2]; // P6_3: PRALINE RF path mixer bypass inverted +constexpr GPIO gpio_mix_en_n_r1_0 = gpio[GPIO5_6]; // P2_6: R1.0 board mixer bypass +#else constexpr GPIO gpio_mix_bypass = gpio[GPIO5_16]; +#endif constexpr GPIO gpio_not_mix_bypass = gpio[GPIO1_0]; constexpr GPIO gpio_og_rx = gpio[GPIO5_5]; @@ -62,24 +73,100 @@ constexpr GPIO gpio_amp_bypass = gpio[GPIO0_14]; constexpr GPIO gpio_not_rx_amp_pwr = gpio[GPIO1_12]; constexpr GPIO gpio_not_tx_amp_pwr = gpio[GPIO3_5]; -constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14]; +#ifdef PRALINE +// PRALINE: GPIO2[13] is SPI CS only, FPGA controls ENX/RESETX +constexpr GPIO gpio_rffc5072_select = gpio[GPIO2_13]; // P5_4: SPI CS (ENX) +constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14]; // P5_5: LPC43xx controls directly +#else constexpr GPIO gpio_rffc5072_select = gpio[GPIO2_13]; +constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14]; +#endif + +#ifdef PRALINE +constexpr GPIO gpio_rffc5072_clock = gpio[GPIO5_18]; +constexpr GPIO gpio_rffc5072_data = gpio[GPIO4_14]; +#else constexpr GPIO gpio_rffc5072_clock = gpio[GPIO5_6]; constexpr GPIO gpio_rffc5072_data = gpio[GPIO3_3]; +#endif +#ifdef PRALINE +constexpr GPIO gpio_max283x_select = gpio[GPIO6_28]; +#else constexpr GPIO gpio_max283x_select = gpio[GPIO0_15]; +#endif + +#ifdef PRALINE +// PRALINE uses MAX2831 transceiver with different control pins +constexpr GPIO gpio_max283x_enable = gpio[GPIO7_1]; // MAX2831 ENABLE (PE_1) +constexpr GPIO gpio_max2831_enable = gpio[GPIO7_1]; // Alias +constexpr GPIO gpio_max2831_rx_enable = gpio[GPIO7_2]; // MAX2831 RX_ENABLE (PE_2) +constexpr GPIO gpio_max2831_rxhp = gpio[GPIO6_29]; // MAX2831 RXHP (PD_15) +constexpr GPIO gpio_max2831_ld = gpio[GPIO4_11]; // MAX2831 Lock Detect (P9_6) +// Legacy aliases for code compatibility +constexpr GPIO gpio_max2837_rxenable = gpio[GPIO7_2]; +constexpr GPIO gpio_max2837_txenable = gpio[GPIO7_2]; // MAX2831 uses single RX/TX control +constexpr GPIO gpio_max2839_rxtx = gpio[GPIO7_2]; +#else constexpr GPIO gpio_max283x_enable = gpio[GPIO2_6]; constexpr GPIO gpio_max2837_rxenable = gpio[GPIO2_5]; constexpr GPIO gpio_max2837_txenable = gpio[GPIO2_4]; constexpr GPIO gpio_max2839_rxtx = gpio[GPIO2_5]; +#endif +#ifdef PRALINE +constexpr GPIO gpio_max5864_select = gpio[GPIO6_30]; // PD_16: PRALINE MAX5864 CS +constexpr GPIO gpio_fpga_select = gpio[GPIO2_10]; // FPGA SPI CS (P5_1) +#else constexpr GPIO gpio_max5864_select = gpio[GPIO2_7]; +#endif constexpr GPIO gpio_q_invert = gpio[GPIO0_13]; +#ifdef PRALINE +// PRALINE power control +constexpr GPIO gpio_vaa_disable = gpio[GPIO4_1]; // VAA disable (P8_1) +constexpr GPIO gpio_1v2_enable = gpio[GPIO4_7]; // 1V2 enable (P8_7) +constexpr GPIO gpio_3v3aux_disable = gpio[GPIO5_15]; // 3V3 aux disable (P6_7) + +// PRALINE RF path control +constexpr GPIO gpio_tx_enable = gpio[GPIO3_4]; // TX enable (P6_5) +// constexpr GPIO gpio_mix_enable_n = gpio[GPIO3_2]; // Mixer enable inverted (P6_3) +constexpr GPIO gpio_lpf_enable = gpio[GPIO4_8]; // LPF enable (PA_1) +constexpr GPIO gpio_rf_amp_enable = gpio[GPIO4_9]; // RF amp enable (PA_2) +constexpr GPIO gpio_ant_bias_disable = gpio[GPIO1_12]; // Antenna bias disable (P2_12) + +// PRALINE mixer lock detect (gpio_max2831_ld defined above at line 93) +constexpr GPIO gpio_rffc5072_ld = gpio[GPIO6_25]; // Mixer lock detect (PD_11) + +// PRALINE LED4 +constexpr GPIO gpio_led4 = gpio[GPIO4_6]; // LED4 (P8_6) + +// PRALINE dual port control +constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14]; // P1 control 0 (P2_10) +constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16]; // P1 control 1 (P6_8) +constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5]; // P1 control 2 (P6_9) +constexpr GPIO gpio_p2_ctrl0 = gpio[GPIO7_3]; // P2 control 0 (PE_3) +constexpr GPIO gpio_p2_ctrl1 = gpio[GPIO7_4]; // P2 control 1 (PE_4) +constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; // CLKIN control (P1_20) + +// PRALINE trigger/sync I/O +constexpr GPIO gpio_trigger_in = gpio[GPIO6_26]; // Trigger input (PD_12) +constexpr GPIO gpio_trigger_out = gpio[GPIO5_6]; // Trigger output (P2_6) +constexpr GPIO gpio_pps_out = gpio[GPIO5_5]; // PPS output (P2_5) +#endif + +#ifdef PRALINE +/* PRALINE has no HackRF CPLD. These pins are used for RFFC5072 and TX_EN instead. + * Dummy assignments here allow cpld_update.cpp to compile; the functions + * that use them are never called on PRALINE. */ +constexpr GPIO gpio_cpld_tdo = gpio[GPIO3_0]; // dummy: reuse TCK pin +constexpr GPIO gpio_cpld_tms = gpio[GPIO3_1]; // dummy: reuse TDI pin +#else constexpr GPIO gpio_cpld_tdo = gpio[GPIO5_18]; -constexpr GPIO gpio_cpld_tck = gpio[GPIO3_0]; constexpr GPIO gpio_cpld_tms = gpio[GPIO3_4]; +#endif +constexpr GPIO gpio_cpld_tck = gpio[GPIO3_0]; constexpr GPIO gpio_cpld_tdi = gpio[GPIO3_1]; constexpr GPIO gpio_r9_clkin_en = gpio[GPIO5_15]; diff --git a/firmware/common/hackrf_hal.hpp b/firmware/common/hackrf_hal.hpp index 2798a4ad1..efd905c14 100644 --- a/firmware/common/hackrf_hal.hpp +++ b/firmware/common/hackrf_hal.hpp @@ -62,8 +62,16 @@ constexpr size_t clock_generator_output_og_codec = 0; constexpr size_t clock_generator_output_og_cpld = 1; constexpr size_t clock_generator_output_og_sgpio = 2; constexpr size_t clock_generator_output_og_clkout = 3; +#ifdef PRALINE +/* PRALINE has swapped CLK4/CLK5 vs HackRF One OG: + * CLK4 = XCVR_CLK (MAX2831) = second IF + * CLK5 = MIX_CLK (RFFC5072) = first IF */ +constexpr size_t clock_generator_output_og_first_if = 5; // RFFC5072 on CLK5 +constexpr size_t clock_generator_output_og_second_if = 4; // MAX2831 on CLK4 +#else constexpr size_t clock_generator_output_og_first_if = 4; constexpr size_t clock_generator_output_og_second_if = 5; +#endif constexpr size_t clock_generator_output_og_mcu_clkin = 7; constexpr size_t clock_generator_output_r9_if = 0; diff --git a/firmware/common/i2cdevmanager.cpp b/firmware/common/i2cdevmanager.cpp index c1bef6a8e..f7de439cc 100644 --- a/firmware/common/i2cdevmanager.cpp +++ b/firmware/common/i2cdevmanager.cpp @@ -205,7 +205,7 @@ uint16_t I2cDev::read16_1(uint8_t reg) { } uint32_t I2cDev::read24_1(uint8_t reg) { uint8_t buffer[3]; - i2c_read(®, 1, buffer, 2); + i2c_read(®, 1, buffer, 3); return uint32_t(buffer[0]) << 16 | uint32_t(buffer[1]) << 8 | uint32_t(buffer[2]); } diff --git a/firmware/common/i2cdevmanager.hpp b/firmware/common/i2cdevmanager.hpp index 617ee8706..3690da5d0 100644 --- a/firmware/common/i2cdevmanager.hpp +++ b/firmware/common/i2cdevmanager.hpp @@ -110,4 +110,4 @@ class I2CDevManager { }; }; // namespace i2cdev -#endif \ No newline at end of file +#endif diff --git a/firmware/common/led.hpp b/firmware/common/led.hpp index ed62dfa27..9c93400a6 100644 --- a/firmware/common/led.hpp +++ b/firmware/common/led.hpp @@ -30,17 +30,30 @@ struct LED { } void setup() const { +#ifdef PRALINE + /* PRALINE LEDs are active-low (GPIO LOW = LED ON) */ + _gpio.set(); /* Start with LED OFF (HIGH) */ +#else _gpio.clear(); +#endif _gpio.output(); _gpio.configure(); } void on() const { +#ifdef PRALINE + _gpio.clear(); /* LOW = ON for PRALINE */ +#else _gpio.set(); +#endif } void off() const { +#ifdef PRALINE + _gpio.set(); /* HIGH = OFF for PRALINE */ +#else _gpio.clear(); +#endif } void toggle() const { @@ -48,7 +61,11 @@ struct LED { } void write(const bool value) const { +#ifdef PRALINE + _gpio.write(!value); /* Invert for PRALINE */ +#else _gpio.write(value); +#endif } private: diff --git a/firmware/common/memory_map.hpp b/firmware/common/memory_map.hpp index afce53694..fd5e10e62 100644 --- a/firmware/common/memory_map.hpp +++ b/firmware/common/memory_map.hpp @@ -70,7 +70,7 @@ constexpr region_t ahb_ram_2{0x2000c000, 16_KiB}; constexpr region_t backup_ram{LPC_BACKUP_REG_BASE, 256}; -constexpr region_t spifi_uncached{LPC_SPIFI_DATA_BASE, 1_MiB}; +constexpr region_t spifi_uncached{LPC_SPIFI_DATA_BASE, FLASH_SIZE_MB * 1024 * 1024}; constexpr region_t spifi_cached{LPC_SPIFI_DATA_CACHED_BASE, spifi_uncached.size()}; ///////////////////////////////// diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index f9b774908..475fcf585 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -146,6 +146,18 @@ class Message { SSTVRXPhaseSlant = 88, SSTVRXCalibration = 89, SubCarData = 90, + MorseRXData = 91, + MorseRXfreq = 92, + MorseRXConfig = 93, + TXDisabled = 94, + MorseTXConfigure = 95, + MorseTXkey = 96, + StreamTXConfiguration = 97, + RTTYData = 98, + NotificationData = 99, + TimeSinkConfig = 100, + EPIRBTXData = 101, + P25TxConfigure = 102, MAX }; @@ -300,6 +312,20 @@ class WidebandSpectrumConfigMessage : public Message { size_t trigger{0}; }; +class TimeSinkConfigMessage : public Message { + public: + constexpr TimeSinkConfigMessage( + size_t sampling_rate, + size_t trigger) + : Message{ID::TimeSinkConfig}, + sampling_rate{sampling_rate}, + trigger{trigger} { + } + + size_t sampling_rate{0}; + size_t trigger{0}; +}; + struct AudioSpectrum { std::array db{{0}}; // uint32_t sampling_rate { 0 }; @@ -1086,6 +1112,27 @@ class SigGenToneMessage : public Message { const uint32_t tone_delta; }; +class EPIRBTXDataMessage : public Message { + public: + static constexpr uint8_t max_len = 18; + constexpr EPIRBTXDataMessage() + : Message{ID::EPIRBTXData} { + } + bool mode_bpsk = true; + uint8_t data[max_len]{0}; + uint8_t data_len = 0; + uint32_t pre_count = 0; + uint32_t post_count = 0; +}; + +class P25TxConfigureMessage : public Message { + public: + constexpr P25TxConfigureMessage() + : Message{ID::P25TxConfigure} { + } + uint16_t frame_length{0}; +}; + class AFSKTxConfigureMessage : public Message { public: constexpr AFSKTxConfigureMessage( @@ -1699,4 +1746,119 @@ class SubCarDataMessage : public Message { uint64_t data2 = 0; }; +class MorseRXDataMessage : public Message { + public: + constexpr MorseRXDataMessage() + : Message{ID::MorseRXData} {} + int32_t state_durations[5] = {0}; // positive: high, negative: low + uint8_t state_cnt = 0; + bool clipped = false; + const uint8_t maxptr = 4; +}; + +class MorseRXfreqMessage : public Message { + public: + constexpr MorseRXfreqMessage() + : Message{ID::MorseRXfreq} {} + uint32_t measured_frequency = 0; +}; + +class MorseRXConfigureMessage : public Message { + public: + constexpr MorseRXConfigureMessage(uint8_t mode) + : Message{ID::MorseRXConfig}, + mode{mode} {} + uint8_t mode = 0; +}; + +class TXDisabledMessage : public Message { + public: + constexpr TXDisabledMessage() + : Message{ID::TXDisabled} { + } +}; + +class MorseTXConfigureMessage : public Message { + public: + constexpr MorseTXConfigureMessage(uint8_t modulation, uint32_t tone, float fm_delta) + : Message{ID::MorseTXConfigure}, + modulation{modulation}, + tone{tone}, + fm_delta{fm_delta} {} + + uint8_t modulation = 0; + uint32_t tone = 0; + float fm_delta = 0; +}; + +class MorseTXkeyMessage : public Message { + public: + constexpr MorseTXkeyMessage(bool key_down) + : Message{ID::MorseTXkey}, + key_down{key_down} {} + bool key_down = false; +}; + +class StreamTXConfigurationMessage : public Message { + public: + constexpr StreamTXConfigurationMessage(uint32_t deviation, uint8_t mode) + : Message{ID::StreamTXConfiguration}, + deviation{deviation}, + mode{mode} {} + + uint32_t deviation = 60000; // used in 2fsk + uint8_t mode = 0; // am = 0, 2fsk = 1 +}; + +class RTTYDataMessage : public Message { + public: + constexpr RTTYDataMessage(uint16_t baud = 4545, uint16_t shift = 170, int16_t mark_tone = -85, int16_t space_tone = 85, uint8_t stopbits = 3, bool inverted = false) + : Message{ID::RTTYData}, + baud(baud), + shift(shift), + mark_tone(mark_tone), + space_tone(space_tone), + inverted(inverted), + stopbits(stopbits) { + } + uint8_t data[490]{0}; // 5bit data, stored on 8 bits. + uint16_t data_len = 0; // count of data sent + uint16_t baud = 4545; // /100 baud 45.45 = 4545 + uint16_t shift = 170; // hz + int16_t mark_tone = 0; // for tx., hz + int16_t space_tone = 170; // hz + bool inverted = false; // for tx, if true, mark and space tones are swapped. + uint8_t stopbits = 3; // doubled value is stored here, so stop 2 = 1 stop bit, 3 = 1.5, 4 = 2. + static constexpr uint16_t max_len = 490; +}; + +class NotificationDataMessage : public Message { + public: + constexpr NotificationDataMessage(const char* source_app, const char* title, const char* message, uint8_t icon = 0, uint16_t timeout = 10000) + : Message{ID::NotificationData}, + icon(icon), + timeout(timeout) { + if (source_app) { + size_t len = std::min(strlen(source_app), (size_t)19); + memcpy(this->source_app, source_app, len); + this->source_app[len] = '\0'; + } + if (title) { + size_t len = std::min(strlen(title), (size_t)49); + memcpy(this->title, title, len); + this->title[len] = '\0'; + } + if (message) { + size_t len = std::min(strlen(message), (size_t)299); + memcpy(this->message, message, len); + this->message[len] = '\0'; + } + } + char source_app[20]{0}; // source application name, null-terminated, max 19 chars + null + char title[50]{0}; // title, null-terminated, max 49 chars + null + char message[300]{0}; // message, null-terminated, max 299 chars + null + uint8_t icon = 0; + uint16_t timeout = 10000; +}; + #endif /*__MESSAGE_H__*/ diff --git a/firmware/common/morse.cpp b/firmware/common/morse.cpp index b91a3a183..ff1f809b3 100644 --- a/firmware/common/morse.cpp +++ b/firmware/common/morse.cpp @@ -41,7 +41,7 @@ size_t morse_encode(std::string& message, const uint32_t time_unit_ms, const uin i = 0; for (char& ch : message) { - if (i > 256) return 0; // Message too long + if (i >= 256) return 0; // Message too long if ((ch >= 'a') && (ch <= 'z')) // Make uppercase ch -= 32; diff --git a/firmware/common/pins.hpp b/firmware/common/pins.hpp index ca45d1bea..15fb1b285 100644 --- a/firmware/common/pins.hpp +++ b/firmware/common/pins.hpp @@ -109,6 +109,29 @@ enum Pins { P7_7, P9_5, P9_6, +#ifdef PRALINE + P9_2, + // Port A pins (PRALINE RF path control) + PA_1, + PA_2, + // Port 8 pins (PRALINE power control and LED4) + P8_1, + P8_6, + P8_7, + // Port E pins (PRALINE transceiver control) + PE_1, + PE_2, + PE_3, + PE_4, + // Port D pins (PRALINE - GPIO-only high-drive pins) + // These use SCU_SFSPD registers, not normal SFSP + // SCU configuration must be done separately in board.cpp + PD_11, + PD_12, + PD_14, + PD_15, + PD_16, +#endif PF_4, CLK0, CLK2, @@ -197,6 +220,28 @@ constexpr Pin pins[]{ [P7_7] = {7, 7}, [P9_5] = {9, 5}, [P9_6] = {9, 6}, +#ifdef PRALINE + [P9_2] = {9, 2}, + // Port A pins (function 0 = GPIO) + [PA_1] = {0xA, 1}, + [PA_2] = {0xA, 2}, + // Port 8 pins + [P8_1] = {8, 1}, + [P8_6] = {8, 6}, + [P8_7] = {8, 7}, + // Port E pins (function 4 = GPIO) + [PE_1] = {0xE, 1}, + [PE_2] = {0xE, 2}, + [PE_3] = {0xE, 3}, + [PE_4] = {0xE, 4}, + // Port D pins - GPIO-only, no pin mux needed + // Using 0xD as marker for Port D (SCU_SFSPD registers) + [PD_11] = {0xD, 11}, + [PD_12] = {0xD, 12}, + [PD_14] = {0xD, 14}, + [PD_15] = {0xD, 15}, + [PD_16] = {0xD, 16}, +#endif [PF_4] = {15, 4}, [CLK0] = {24, 0}, [CLK2] = {24, 2}, @@ -209,7 +254,8 @@ enum GPIOs { GPIO0_3, GPIO0_4, GPIO0_5, - /*GPIO0_6,*/ GPIO0_7, + /*GPIO0_6,*/ + GPIO0_7, GPIO0_8, GPIO0_9, GPIO0_10, @@ -275,12 +321,35 @@ enum GPIOs { GPIO5_7, GPIO5_8, GPIO5_9, - /*GPIO5_10, GPIO5_11,*/ GPIO5_12, + /*GPIO5_10, GPIO5_11,*/ + GPIO5_12, GPIO5_13, GPIO5_14, GPIO5_15, GPIO5_16, GPIO5_18, + // PRALINE-specific GPIOs (always defined to avoid compile-time array gaps) + // RFFC5072 mixer data + GPIO4_14, + // Power control and LED4 + GPIO4_1, + GPIO4_6, + GPIO4_7, + // RF path control + GPIO4_8, + GPIO4_9, + // Transceiver (MAX2831) control + GPIO7_1, + GPIO7_2, + // Dual port control + GPIO7_3, + GPIO7_4, + // GPIO6 (Port D pins) + GPIO6_25, + GPIO6_26, + GPIO6_28, + GPIO6_29, + GPIO6_30, }; constexpr GPIO gpio[] = { @@ -372,6 +441,41 @@ constexpr GPIO gpio[] = { [GPIO5_15] = {pins[P6_7], 5, 15, 4}, [GPIO5_16] = {pins[P6_8], 5, 16, 4}, [GPIO5_18] = {pins[P9_5], 5, 18, 4}, +// PRALINE-specific GPIOs (use placeholder pins on non-PRALINE builds) +#ifdef PRALINE + [GPIO4_14] = {pins[P9_2], 4, 14, 0}, // RFFC5072 mixer data + [GPIO4_1] = {pins[P8_1], 4, 1, 0}, // VAA disable + [GPIO4_6] = {pins[P8_6], 4, 6, 0}, // LED4 + [GPIO4_7] = {pins[P8_7], 4, 7, 0}, // 1V2 enable + [GPIO4_8] = {pins[PA_1], 4, 8, 0}, // LPF enable + [GPIO4_9] = {pins[PA_2], 4, 9, 0}, // RF amp enable + [GPIO7_1] = {pins[PE_1], 7, 1, 4}, // MAX2831 enable + [GPIO7_2] = {pins[PE_2], 7, 2, 4}, // MAX2831 RX enable + [GPIO7_3] = {pins[PE_3], 7, 3, 4}, // P2 port control 0 + [GPIO7_4] = {pins[PE_4], 7, 4, 4}, // P2 port control 1 + [GPIO6_25] = {pins[PD_11], 6, 25, 4}, // Mixer lock detect + [GPIO6_26] = {pins[PD_12], 6, 26, 4}, // Trigger input + [GPIO6_28] = {pins[PD_14], 6, 28, 4}, // MAX283x chip select + [GPIO6_29] = {pins[PD_15], 6, 29, 4}, // MAX2831 RXHP + [GPIO6_30] = {pins[PD_16], 6, 30, 4}, // MAX5864 chip select +#else + // Placeholder entries for non-PRALINE builds (use P0_0 as dummy) + [GPIO4_14] = {pins[P0_0], 4, 14, 0}, + [GPIO4_1] = {pins[P0_0], 4, 1, 0}, + [GPIO4_6] = {pins[P0_0], 4, 6, 0}, + [GPIO4_7] = {pins[P0_0], 4, 7, 0}, + [GPIO4_8] = {pins[P0_0], 4, 8, 0}, + [GPIO4_9] = {pins[P0_0], 4, 9, 0}, + [GPIO7_1] = {pins[P0_0], 7, 1, 0}, + [GPIO7_2] = {pins[P0_0], 7, 2, 0}, + [GPIO7_3] = {pins[P0_0], 7, 3, 0}, + [GPIO7_4] = {pins[P0_0], 7, 4, 0}, + [GPIO6_25] = {pins[P0_0], 6, 25, 0}, + [GPIO6_26] = {pins[P0_0], 6, 26, 0}, + [GPIO6_28] = {pins[P0_0], 6, 28, 0}, + [GPIO6_29] = {pins[P0_0], 6, 29, 0}, + [GPIO6_30] = {pins[P0_0], 6, 30, 0}, +#endif }; } // namespace lpc43xx diff --git a/firmware/common/portapack_adc.hpp b/firmware/common/portapack_adc.hpp index 4f0892419..e2630dd34 100644 --- a/firmware/common/portapack_adc.hpp +++ b/firmware/common/portapack_adc.hpp @@ -35,6 +35,12 @@ constexpr size_t adc0_touch_xn_input = 6; /* ADC1 */ +/* + * RSSI input channel is the same on both platforms: + * - HackRF One (MAX2837): RSSI routed to ADC1 Channel 1 (PC_0) + * - HackRF Pro/PRALINE (MAX2831): RSSI also routed to ADC1 Channel 1 (PC_0) + * Confirmed by GSG hackrf firmware: adc_read(1) used for RSSI + */ constexpr size_t adc1_rssi_input = 1; } /* namespace portapack */ diff --git a/firmware/common/portapack_cpld_data.hpp b/firmware/common/portapack_cpld_data.hpp index 04165344a..52278ec52 100644 --- a/firmware/common/portapack_cpld_data.hpp +++ b/firmware/common/portapack_cpld_data.hpp @@ -51,6 +51,17 @@ const Config config{block_0, block_1}; } /* namespace rev_20170522 */ +#ifdef PRALINE +namespace rev_h4m { + +extern const std::array block_0; +extern const std::array block_1; + +const Config config{block_0, block_1}; + +} /* namespace rev_h4m */ +#endif + } /* namespace cpld */ } /* namespace portapack */ diff --git a/firmware/common/portapack_persistent_memory.cpp b/firmware/common/portapack_persistent_memory.cpp index 26365d09b..103ee6d7a 100644 --- a/firmware/common/portapack_persistent_memory.cpp +++ b/firmware/common/portapack_persistent_memory.cpp @@ -152,12 +152,12 @@ struct misc_config_t { bool config_sdcard_high_speed_io : 1; bool config_disable_config_mode : 1; bool beep_on_packets : 1; - bool UNUSED_6 : 1; - bool UNUSED_7 : 1; + bool tx_disabled : 1; + bool tx_amp_disabled : 1; + uint8_t tx_gain_max_db; uint8_t PLACEHOLDER_1; uint8_t PLACEHOLDER_2; - uint8_t PLACEHOLDER_3; }; static_assert(sizeof(misc_config_t) == sizeof(uint32_t)); @@ -441,6 +441,10 @@ void defaults() { set_recon_repeat_delay(1); set_config_sdcard_high_speed_io(false, true); + + set_config_tx_disabled(false); + set_config_tx_amp_disabled(false); + set_config_tx_gain_max_db(47); } void init() { @@ -649,6 +653,18 @@ bool config_sdcard_high_speed_io() { return data->misc_config.config_sdcard_high_speed_io; } +bool config_tx_disabled() { + return data->misc_config.tx_disabled; +} + +bool config_tx_amp_disabled() { + return data->misc_config.tx_amp_disabled; +} + +uint8_t config_tx_gain_max_db() { + return data->misc_config.tx_gain_max_db; +} + bool stealth_mode() { return data->ui_config.stealth_mode; } @@ -743,6 +759,18 @@ void set_config_sdcard_high_speed_io(bool v, bool save) { data->misc_config.config_sdcard_high_speed_io = v; } +void set_config_tx_disabled(bool v) { + data->misc_config.tx_disabled = v; +} + +void set_config_tx_amp_disabled(bool v) { + data->misc_config.tx_amp_disabled = v; +} + +void set_config_tx_gain_max_db(uint8_t v) { + data->misc_config.tx_gain_max_db = v; +} + void set_stealth_mode(bool v) { data->ui_config.stealth_mode = v; } diff --git a/firmware/common/portapack_persistent_memory.hpp b/firmware/common/portapack_persistent_memory.hpp index 35810c384..cdac91a15 100644 --- a/firmware/common/portapack_persistent_memory.hpp +++ b/firmware/common/portapack_persistent_memory.hpp @@ -203,6 +203,9 @@ bool config_disable_external_tcxo(); bool config_sdcard_high_speed_io(); bool config_disable_config_mode(); bool beep_on_packets(); +bool config_tx_disabled(); +bool config_tx_amp_disabled(); +uint8_t config_tx_gain_max_db(); bool config_splash(); bool config_converter(); @@ -226,6 +229,9 @@ void set_config_disable_external_tcxo(bool v); void set_config_sdcard_high_speed_io(bool v, bool save); void set_config_disable_config_mode(bool v); void set_beep_on_packets(bool v); +void set_config_tx_disabled(bool v); +void set_config_tx_amp_disabled(bool v); +void set_config_tx_gain_max_db(uint8_t v); void set_config_splash(bool v); bool config_converter(); diff --git a/firmware/common/portapack_shared_memory.hpp b/firmware/common/portapack_shared_memory.hpp index e6a2cc906..464ceb0de 100644 --- a/firmware/common/portapack_shared_memory.hpp +++ b/firmware/common/portapack_shared_memory.hpp @@ -55,6 +55,19 @@ struct ToneData { /* NOTE: These structures must be located in the same location in both M4 and M0 binaries */ struct SharedMemory { +#ifdef PRALINE + /* + * Software RSSI: 8 packed I/Q samples from baseband_thread. + * + * baseband_thread copies 8 samples spread across buffer (no computation). + * Each sample is packed: Q in high 16 bits, I in low 16 bits. + * rssi_thread uses __SMUAD on each to compute IΒ²+QΒ², finds peak. + * + * 8 samples avoids zero-crossing artifacts from single-sample approach. + */ + volatile uint32_t software_rssi_iq[8]{0}; // 8 packed I/Q samples +#endif + static constexpr size_t application_queue_k = 11; static constexpr size_t app_local_queue_k = 11; @@ -85,6 +98,14 @@ struct SharedMemory { uint16_t volatile m4_stack_usage{0}; uint32_t volatile m4_heap_usage{0}; uint16_t volatile m4_buffer_missed{0}; + +#ifdef PRALINE + // Phase 0 instrumentation counters for PRALINE radio debugging + uint32_t volatile m4_dma_xfr_count{0}; // DMA transfer_complete() calls + uint32_t volatile m4_dma_wait_count{0}; // wait_for_buffer() calls + uint32_t volatile m4_baseband_loops{0}; // Main loop iterations + uint8_t volatile m4_streaming_marker{0}; // Proves streaming_enable() called +#endif }; extern SharedMemory& shared_memory; diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 6dee37495..6c9fde35e 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -88,6 +88,8 @@ constexpr image_tag_t image_tag_am_tv{'P', 'A', 'M', 'T'}; constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'}; constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'}; constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'}; +constexpr image_tag_t image_tag_epirb_tx{'P', 'E', 'P', 'T'}; +constexpr image_tag_t image_tag_p25_tx{'P', '2', '5', 'T'}; constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'}; constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'}; constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'}; @@ -96,6 +98,7 @@ constexpr image_tag_t image_tag_sonde{'P', 'S', 'O', 'N'}; constexpr image_tag_t image_tag_tpms{'P', 'T', 'P', 'M'}; constexpr image_tag_t image_tag_wfm_audio{'P', 'W', 'F', 'M'}; constexpr image_tag_t image_tag_wideband_spectrum{'P', 'S', 'P', 'E'}; +constexpr image_tag_t image_tag_time_sink{'P', 'T', 'S', 'K'}; constexpr image_tag_t image_tag_test{'P', 'T', 'S', 'T'}; constexpr image_tag_t image_tag_adsb_tx{'P', 'A', 'D', 'T'}; @@ -124,6 +127,10 @@ constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'}; constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'}; constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'}; constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'}; +constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'}; +constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'}; +constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'}; +constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'}; constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'}; diff --git a/firmware/common/ui_focus.cpp b/firmware/common/ui_focus.cpp index dc92cec17..ca682d2e3 100644 --- a/firmware/common/ui_focus.cpp +++ b/firmware/common/ui_focus.cpp @@ -171,7 +171,7 @@ void FocusManager::update(Widget* const top_widget, const KeyEvent event) { const auto focus_screen_rect = focus_widget()->screen_rect(); const auto test_fn = [&focus_screen_rect, event](ui::Widget* const w) -> test_result_t { - // if( w->visible() && w->focusable() ) { + // if( w->drawn() && w->focusable() ) { if (w->focusable()) { const auto distance = rect_distances(event, focus_screen_rect, w->screen_rect()); if (distance >= 0) { diff --git a/firmware/common/ui_painter.cpp b/firmware/common/ui_painter.cpp index 899acb001..9b8d7a916 100644 --- a/firmware/common/ui_painter.cpp +++ b/firmware/common/ui_painter.cpp @@ -121,11 +121,11 @@ void Painter::paint_widget_tree(Widget* w) { void Painter::paint_widget(Widget* w) { if (w->hidden()) { - // Mark widget (and all children) as invisible. - w->visible(false); + // Mark widget (and all children) as not drawn. + w->drawn(false); } else { - // Mark this widget as visible and recurse. - w->visible(true); + // Mark this widget as drawn and recurse. + w->drawn(true); if (w->dirty()) { w->paint(*this); diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 2f884f0cb..0e8619b16 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -91,9 +91,9 @@ void Widget::set_parent(Widget* const widget) { } if (parent_ && !widget) { - // We have a parent, but are losing it. Update visible status. + // We have a parent, but are losing it. Update drawn status. dirty_overlapping_children_in_rect(screen_rect()); - visible(false); + drawn(false); } if (widget == nullptr) @@ -209,9 +209,9 @@ const Style& Widget::style() const { return style_ ? *style_ : parent()->style(); } -void Widget::visible(bool v) { - if (v != flags.visible) { - flags.visible = v; +void Widget::drawn(bool v) { + if (v != flags.drawn) { + flags.drawn = v; /* TODO: This on_show/on_hide implementation seems inelegant. * But I need *some* way to take/configure resources when @@ -224,9 +224,9 @@ void Widget::visible(bool v) { } else { on_hide(); - // Set all children invisible too. + // Set all children not drawn too. for (const auto child : children()) { - child->visible(false); + child->drawn(false); } } } @@ -272,9 +272,8 @@ void View::add_child(Widget* const widget) { } void View::add_children(const std::initializer_list children) { - children_.insert(std::end(children_), children); for (auto child : children) { - child->set_parent(this); + add_child(child); } } @@ -384,6 +383,10 @@ void Text::set(std::string_view value) { set_dirty(); } +std::string Text::get() { + return text; +} + void Text::getAccessibilityText(std::string& result) { result = text; } @@ -393,18 +396,35 @@ void Text::getWidgetName(std::string& result) { void Text::paint(Painter& painter) { const auto rect = screen_rect(); auto s = has_focus() ? style().invert() : style(); - auto max_len = (unsigned)rect.width() / s.font.char_width(); - auto text_view = std::string_view{text}; - painter.fill_rectangle(rect, s.background); + const int char_width = s.font.char_width(); + const int line_height = s.font.line_height(); + if (char_width == 0 || line_height == 0) return; + const size_t chars_per_line = rect.width() / char_width; + if (chars_per_line == 0) return; + size_t lines_capacity = rect.height() / line_height; + if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically + auto text_view = std::string_view{text}; + size_t current_offset = 0; + for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) { + if (current_offset >= text_view.length()) break; + size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset); + painter.draw_string( + rect.location() + Point(0, line_idx * line_height), + s, + text_view.substr(current_offset, chunk_len)); + current_offset += chunk_len; + } +} - if (text_view.length() > max_len) - text_view = text_view.substr(0, max_len); - - painter.draw_string( - rect.location(), - s, - text_view); +bool Text::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) { + if (on_select) { + on_select(*this); + return true; + } + } + return false; } /* Labels ****************************************************************/ @@ -601,8 +621,8 @@ ProgressBar::ProgressBar( void ProgressBar::set_max(const uint32_t max) { if (max == _max) return; - if (_value > _max) - _value = _max; + if (_value > max) + _value = max; _max = max; set_dirty(); @@ -630,9 +650,11 @@ void ProgressBar::paint(Painter& painter) { const auto sr = screen_rect(); const auto s = style(); - - v_scaled = (sr.size().width() * (uint64_t)_value) / _max; - + if (_max == 0) { + v_scaled = 0; + } else { + v_scaled = (sr.size().width() * (uint64_t)_value) / _max; + } painter.fill_rectangle({sr.location(), {v_scaled, sr.size().height()}}, style().foreground); painter.fill_rectangle({{sr.location().x() + v_scaled, sr.location().y()}, {sr.size().width() - v_scaled, sr.size().height()}}, s.background); @@ -672,7 +694,7 @@ void Console::clear(bool clear_buffer = false) { if (clear_buffer) buffer.clear(); - if (!hidden() && visible()) { + if (!hidden() && drawn()) { display.fill_rectangle( screen_rect(), Theme::getInstance()->bg_darkest->background); @@ -684,7 +706,7 @@ void Console::clear(bool clear_buffer = false) { void Console::write(std::string message) { bool escape = false; - if (!hidden() && visible()) { + if (!hidden() && drawn()) { const Style& s = style(); const Font& font = s.font; auto rect = screen_rect(); @@ -775,7 +797,7 @@ void Console::on_hide() { } void Console::crlf() { - if (hidden() || !visible()) return; + if (hidden() || !drawn()) return; const auto& s = style(); auto sr = screen_rect(); @@ -1246,7 +1268,7 @@ bool ButtonWithEncoder::on_encoder(const EncoderEvent delta) { if (delta != 0) { encoder_delta += delta; delta_change = true; - on_change(); + if (on_change) on_change(); } else delta_change = 0; return true; @@ -1358,14 +1380,18 @@ void NewButton::paint(Painter& painter) { } if (!text_.empty()) { - const auto label_r = style.font.size_of(text_); + auto label_r = style.font.size_of(text_); + std::string text_to_draw = text_; + if (label_r.width() > r.width() - 2) { + // Truncate text to fit + size_t max_chars = (r.width() - 2) / style.font.char_width(); + text_to_draw = text_.substr(0, max_chars); + label_r = style.font.size_of(text_to_draw); + } if (bitmap_) { y += spacing; } - painter.draw_string( - {r.left() + (r.width() - label_r.width()) / 2, y}, - style, - text_); + painter.draw_string({r.left() + (r.width() - label_r.width()) / 2, y}, style, text_to_draw); } } else { // no valign if (bitmap_) { @@ -1379,11 +1405,16 @@ void NewButton::paint(Painter& painter) { } if (!text_.empty()) { - const auto label_r = style.font.size_of(text_); - painter.draw_string( - {r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2}, - style, - text_); + auto label_r = style.font.size_of(text_); + std::string text_to_draw = text_; + if (label_r.width() > r.width() - 2) { + // Truncate text to fit + size_t max_chars = (r.width() - 2) / style.font.char_width(); + text_to_draw = text_.substr(0, max_chars); + label_r = style.font.size_of(text_to_draw); + } + painter.draw_string({r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2}, style, + text_to_draw); } } } @@ -1732,6 +1763,14 @@ bool ImageOptionsField::on_keyboard(const KeyboardEvent key) { return false; } +bool ImageOptionsField::on_key(const KeyEvent event) { + if (event == KeyEvent::Select) { + on_encoder(1); + return true; + } + return false; +} + bool ImageOptionsField::on_touch(const TouchEvent event) { if (event.type == TouchEvent::Type::Start) { focus(); @@ -1866,6 +1905,7 @@ void OptionsField::on_focus() { } bool OptionsField::on_encoder(const EncoderEvent delta) { + if (options_.empty()) return false; int32_t new_value = selected_index() + delta; if (new_value < 0) new_value = options_.size() - 1; @@ -1881,6 +1921,14 @@ bool OptionsField::on_keyboard(const KeyboardEvent key) { return false; } +bool OptionsField::on_key(const KeyEvent event) { + if (event == KeyEvent::Select) { + on_encoder(1); + return true; + } + return false; +} + bool OptionsField::on_touch(const TouchEvent event) { if (event.type == TouchEvent::Type::Start) { focus(); @@ -2269,20 +2317,44 @@ void NumberField::getWidgetName(std::string& result) { } void NumberField::set_value(int32_t new_value, bool trigger_change) { + const int32_t lo = range.first; + const int32_t hi = range.second; + + // Helper: floor-div for negatives + auto floordiv = [](int64_t a, int64_t b) -> int64_t { // b > 0 + int64_t q = a / b; + int64_t r = a % b; + return (r < 0) ? (q - 1) : q; + }; + // Helper: positive modulo + auto posmod = [](int64_t a, int64_t m) -> int64_t { // m > 0 + int64_t r = a % m; + return (r < 0) ? (r + m) : r; + }; + if (can_loop) { - if (new_value >= range.first) - new_value = new_value % (range.second + 1); - else - new_value = range.second + new_value + 1; + // number of discrete positions + const int64_t count = (int64_t)(hi - lo) / step + 1; + + // map value -> step index (floor so negatives behave) + int64_t idx = floordiv((int64_t)new_value - lo, step); + + // wrap index + idx = posmod(idx, count); + + // rebuild value + new_value = (int32_t)(lo + idx * step); + } else { + new_value = clip(new_value, lo, hi); + // optionally snap to step here too + int64_t idx = floordiv((int64_t)new_value - lo, step); + new_value = (int32_t)(lo + idx * step); } - new_value = clip(new_value, range.first, range.second); - + // set final value if needed if (new_value != value()) { value_ = new_value; - if (on_change && trigger_change) { - on_change(value_); - } + if (on_change && trigger_change) on_change(value_); set_dirty(); } } @@ -2313,6 +2385,8 @@ bool NumberField::on_key(const KeyEvent key) { if (on_select) { on_select(*this); return true; + } else { + return on_encoder(1); } } @@ -2355,6 +2429,137 @@ bool NumberField::on_touch(const TouchEvent event) { return true; } +/* FloatField ***********************************************************/ + +FloatField::FloatField( + Point parent_pos, + int length, + range_t range, + float step, + char fill_char, + bool can_loop, + uint8_t precision_) + : Widget{{parent_pos, {8 * length, 16}}}, + range{range}, + step{step}, + length_{length}, + fill_char{fill_char}, + can_loop{can_loop}, + precision{precision_} { + set_focusable(true); +} + +float FloatField::value() const { + return value_; +} + +void FloatField::getAccessibilityText(std::string& result) { + result = to_string_decimal(value_, precision); +} +void FloatField::getWidgetName(std::string& result) { + result = "FloatField"; +} + +void FloatField::set_value(float new_value, bool trigger_change) { + const float lo = range.first; + const float hi = range.second; + + if (can_loop) { + if (new_value > hi) + new_value = lo; + else if (new_value < lo) + new_value = hi; + } + new_value = clip(new_value, lo, hi); + + // set final value if needed + if (new_value != value_) { + value_ = new_value; + if (on_change && trigger_change) on_change(value_); + set_dirty(); + } +} + +void FloatField::set_range(const float min, const float max) { + range.first = min; + range.second = max; + set_value(value_, false); +} + +void FloatField::set_step(const float new_step) { + step = new_step; +} + +void FloatField::paint(Painter& painter) { + auto text = to_string_decimal(value_, precision); + const auto paint_style = has_focus() ? style().invert() : style(); + // clip to widget size + const auto r = screen_rect(); + auto label_r = style().font.size_of(text); + size_t max_chars = (r.width()) / style().font.char_width(); + if (label_r.width() > r.width()) { + text = text.substr(0, max_chars); + } + size_t padneeded = max_chars - text.length(); + if (padneeded > 0 && fill_char) { + std::string filler(padneeded, fill_char); + text = filler + text; + } + painter.fill_rectangle(r, style().background); + painter.draw_string( + screen_pos(), + paint_style, + text); +} + +bool FloatField::on_key(const KeyEvent key) { + if (key == KeyEvent::Select) { + if (on_select) { + on_select(*this); + return true; + } else { + return on_encoder(1); + } + } + return false; +} + +bool FloatField::on_encoder(const EncoderEvent delta) { + float old_value = value_; + set_value(value() + (delta * step)); + + if (on_wrap) { + if ((delta > 0) && (value_ < old_value)) + on_wrap(1); + else if ((delta < 0) && (value_ > old_value)) + on_wrap(-1); + } + return true; +} + +bool FloatField::on_keyboard(const KeyboardEvent key) { + if (key == 10) { + if (on_select) { + on_select(*this); + return true; + } + } + if (key == '+' || key == ' ') { + return on_encoder(1); + } + if (key == '-' || key == 8) { + return on_encoder(-1); + } + return false; +} + +bool FloatField::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) { + focus(); + } + return true; +} + /* SymField **************************************************************/ SymField::SymField( @@ -3027,7 +3232,7 @@ void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) { } void GraphEq::paint(Painter& painter) { - if (!visible()) return; + if (!drawn()) return; if (!is_calculated) { // calc positions first calculate_params(); is_calculated = true; diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 452d1b2cc..06a386746 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -126,8 +126,8 @@ class Widget { bool dirty() const; void set_clean(); - void visible(bool v); - bool visible() { return flags.visible; }; + void drawn(bool v); + bool drawn() { return flags.drawn; }; bool highlighted() const; void set_highlighted(const bool value); @@ -146,7 +146,7 @@ class Widget { bool hidden : 1; // Hide widget and children. bool focusable : 1; // Widget can receive focus. bool highlighted : 1; // Show in a highlighted style. - bool visible : 1; // Object was visible during last paint. + bool drawn : 1; // Object was drawn during last paint. }; flags_t flags{ @@ -154,7 +154,7 @@ class Widget { .hidden = false, .focusable = false, .highlighted = false, - .visible = false, + .drawn = false, }; static const std::vector no_children; @@ -209,6 +209,7 @@ class Rectangle : public Widget { class Text : public Widget { public: + std::function on_select{}; Text() : text{""} { } @@ -217,11 +218,14 @@ class Text : public Widget { Text(Rect parent_rect); void set(std::string_view value); + std::string get(); void paint(Painter& painter) override; void getAccessibilityText(std::string& result) override; void getWidgetName(std::string& result) override; + bool on_touch(const TouchEvent event) override; + protected: // NB: Don't truncate this string. The UI will only render // as many characters as will fit in its rectange. @@ -666,6 +670,8 @@ class ImageOptionsField : public Widget { bool on_encoder(const EncoderEvent delta) override; bool on_touch(const TouchEvent event) override; bool on_keyboard(const KeyboardEvent event) override; + bool on_key(const KeyEvent event) override; + void getAccessibilityText(std::string& result) override; void getWidgetName(std::string& result) override; @@ -674,6 +680,7 @@ class ImageOptionsField : public Widget { size_t selected_index_{0}; Color foreground_; Color background_; + bool is_activated = false; }; class OptionsField : public Widget { @@ -706,6 +713,7 @@ class OptionsField : public Widget { bool on_encoder(const EncoderEvent delta) override; bool on_touch(const TouchEvent event) override; bool on_keyboard(const KeyboardEvent event) override; + bool on_key(const KeyEvent event) override; void getAccessibilityText(std::string& result) override; void getWidgetName(std::string& result) override; @@ -715,6 +723,7 @@ class OptionsField : public Widget { options_t options_; size_t selected_index_{0}; bool centered_{false}; // e.g.: length as screen_width/8, x position as 0, it will be centered in x axis + bool is_activated = false; }; // A TextEdit is bound to a string reference and allows the string @@ -851,11 +860,11 @@ class NumberField : public Widget { NumberField(Point parent_pos, int length, range_t range, int32_t step, char fill_char, bool can_loop); NumberField(Point parent_pos, int length, range_t range, int32_t step, char fill_char) - : NumberField{parent_pos, length, range, step, fill_char, false} { + : NumberField{parent_pos, length, range, step, fill_char, true} { } NumberField() - : NumberField{{0, 0}, 1, {0, 1}, 1, ' ', false} { + : NumberField{{0, 0}, 1, {0, 1}, 1, ' ', true} { } NumberField(const NumberField&) = delete; @@ -885,6 +894,53 @@ class NumberField : public Widget { bool can_loop{}; }; +class FloatField : public Widget { + public: + std::function on_select{}; + std::function on_change{}; + std::function on_wrap{}; + + using range_t = std::pair; + + FloatField(Point parent_pos, int length, range_t range, float step, char fill_char, bool can_loop, uint8_t precision_ = 1); + + FloatField(Point parent_pos, int length, range_t range, float step, char fill_char) + : FloatField{parent_pos, length, range, step, fill_char, true, 1} { + } + + FloatField() + : FloatField{{0, 0}, 1, {0, 1}, 1, ' ', true, 1} { + } + + FloatField(const FloatField&) = delete; + FloatField(FloatField&&) = delete; + + float value() const; + void set_value(float new_value, bool trigger_change = true); + void set_range(const float min, const float max); + void set_step(const float new_step); + void set_precision(uint8_t precision); + + void paint(Painter& painter) override; + + bool on_key(const KeyEvent key) override; + bool on_encoder(const EncoderEvent delta) override; + bool on_touch(const TouchEvent event) override; + bool on_keyboard(const KeyboardEvent event) override; + + void getAccessibilityText(std::string& result) override; + void getWidgetName(std::string& result) override; + + private: + range_t range; + float step; + const int length_; + const char fill_char; + float value_{0}; + bool can_loop{}; + uint8_t precision = 1; +}; + /* A widget that allows for character-by-character editing of its value. */ class SymField : public Widget { public: diff --git a/firmware/common/units.hpp b/firmware/common/units.hpp index f15e985fd..78070e631 100644 --- a/firmware/common/units.hpp +++ b/firmware/common/units.hpp @@ -22,6 +22,12 @@ #ifndef __UNITS_H__ #define __UNITS_H__ +#define TEMP_UNIT_CELSIUS 0 +#define TEMP_UNIT_FAHRENHEIT 1 +#define PRESSURE_UNIT_KPA 2 +#define PRESSURE_UNIT_BAR 3 +#define PRESSURE_UNIT_PSI 4 + #include #define STR_DEGREES_C "\xB0\x43" // ΒΊC - 0xB0 is degree Β° symbol in our 8x16 font, 0x43 is "C" @@ -48,6 +54,10 @@ class Pressure { return kpa_ * 1000 / 6895; } + int bar() const { + return kpa_ / 100; + } + private: int16_t kpa_; }; diff --git a/firmware/common/utility.cpp b/firmware/common/utility.cpp index c4af7f400..d73584ef3 100644 --- a/firmware/common/utility.cpp +++ b/firmware/common/utility.cpp @@ -22,6 +22,7 @@ #include "utility.hpp" #include +#include #if 0 uint32_t gcd(const uint32_t u, const uint32_t v) { @@ -257,8 +258,23 @@ std::string join(char c, std::initializer_list strings) { } uint32_t simple_checksum(uint32_t buffer_address, uint32_t length) { + if (buffer_address == 0 || length == 0) { + return 0; + } uint32_t checksum = 0; - for (uint32_t i = 0; i < length; i += 4) - checksum += *(uint32_t*)(buffer_address + i); + const uint8_t* ptr = (const uint8_t*)(uintptr_t)buffer_address; + uint32_t i = 0; + for (; i + 3 < length; i += 4) { + uint32_t chunk; + // memcpy prevents unaligned access hard-faults on embedded processors + memcpy(&chunk, ptr + i, 4); + checksum += chunk; + } + // Leftover bytes (won't happen hopefully) + if (i < length) { + uint32_t remainder = 0; + memcpy(&remainder, ptr + i, length - i); + checksum += remainder; + } return checksum; } \ No newline at end of file diff --git a/firmware/flashsize.h.in b/firmware/flashsize.h.in new file mode 100644 index 000000000..a35ea111c --- /dev/null +++ b/firmware/flashsize.h.in @@ -0,0 +1,9 @@ +#pragma once +// DO NOT EDIT: IT IS AUTO GENERATED BY CMAKE!!!! + +// clang-format off +//Allowed fw size in MB +#define FLASH_SIZE_MB @FLASH_MB_SIZE@ +//Current compiled fw size in MB +#define FLASH_SIZE_LIMIT_MB @FLASH_MB_LIMIT_SIZE@ +// clang-format on \ No newline at end of file diff --git a/firmware/standalone/common/ui/file_path.cpp b/firmware/standalone/common/ui/file_path.cpp index a08811fb6..33c4e0057 100644 --- a/firmware/standalone/common/ui/file_path.cpp +++ b/firmware/standalone/common/ui/file_path.cpp @@ -53,3 +53,5 @@ const std::filesystem::path hopper_dir = u"HOPPER"; const std::filesystem::path subghz_dir = u"SUBGHZ"; const std::filesystem::path waterfalls_dir = u"WATERFALLS"; const std::filesystem::path macaddress_dir = u"MACADDRESS"; + +const std::filesystem::path splash_dot_bmp = u"/splash.bmp"; \ No newline at end of file diff --git a/firmware/standalone/common/ui/file_path.hpp b/firmware/standalone/common/ui/file_path.hpp index 0f8548ee0..9acac1f2c 100644 --- a/firmware/standalone/common/ui/file_path.hpp +++ b/firmware/standalone/common/ui/file_path.hpp @@ -56,4 +56,6 @@ extern const std::filesystem::path subghz_dir; extern const std::filesystem::path waterfalls_dir; extern const std::filesystem::path macaddress_dir; +extern const std::filesystem::path splash_dot_bmp; + #endif /* __FILE_PATH_H__ */ diff --git a/firmware/standalone/common/ui/ui_focus.cpp b/firmware/standalone/common/ui/ui_focus.cpp index c8015a700..f8676ff45 100644 --- a/firmware/standalone/common/ui/ui_focus.cpp +++ b/firmware/standalone/common/ui/ui_focus.cpp @@ -160,7 +160,7 @@ void FocusManager::update( const auto focus_screen_rect = focus_widget()->screen_rect(); const auto test_fn = [&focus_screen_rect, event](ui::Widget* const w) -> test_result_t { - // if( w->visible() && w->focusable() ) { + // if( w->drawn() && w->focusable() ) { if (w->focusable()) { const auto distance = rect_distances(event, focus_screen_rect, w->screen_rect()); if (distance >= 0) { diff --git a/firmware/standalone/common/ui/ui_geomap.cpp b/firmware/standalone/common/ui/ui_geomap.cpp index 3f1fd9abd..76c1a8cdc 100644 --- a/firmware/standalone/common/ui/ui_geomap.cpp +++ b/firmware/standalone/common/ui/ui_geomap.cpp @@ -650,7 +650,13 @@ void GeoMap::move(const float lon, const float lat) { } else { if (is_changed) { set_osm_max_zoom(); - redraw_map = true; + double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom); + double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom); + // Redraw if viewport moved by at least 1 pixel (includes zoom level changes) + if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 || + abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) { + redraw_map = true; + } } } } diff --git a/firmware/standalone/common/ui/ui_painter.cpp b/firmware/standalone/common/ui/ui_painter.cpp index 892815c5e..29a16c183 100644 --- a/firmware/standalone/common/ui/ui_painter.cpp +++ b/firmware/standalone/common/ui/ui_painter.cpp @@ -118,11 +118,11 @@ void Painter::paint_widget_tree(Widget* w) { void Painter::paint_widget(Widget* w) { if (w->hidden()) { - // Mark widget (and all children) as invisible. - w->visible(false); + // Mark widget (and all children) as not drawn. + w->drawn(false); } else { - // Mark this widget as visible and recurse. - w->visible(true); + // Mark this widget as drawn and recurse. + w->drawn(true); if (w->dirty()) { w->paint(*this); diff --git a/firmware/standalone/common/ui/ui_widget.cpp b/firmware/standalone/common/ui/ui_widget.cpp index f509f9d9a..c29560479 100644 --- a/firmware/standalone/common/ui/ui_widget.cpp +++ b/firmware/standalone/common/ui/ui_widget.cpp @@ -91,9 +91,9 @@ void Widget::set_parent(Widget* const widget) { } if (parent_ && !widget) { - // We have a parent, but are losing it. Update visible status. + // We have a parent, but are losing it. Update drawn status. dirty_overlapping_children_in_rect(screen_rect()); - visible(false); + drawn(false); } parent_ = widget; @@ -212,9 +212,9 @@ const Style& Widget::style() const { } } -void Widget::visible(bool v) { - if (v != flags.visible) { - flags.visible = v; +void Widget::drawn(bool v) { + if (v != flags.drawn) { + flags.drawn = v; /* TODO: This on_show/on_hide implementation seems inelegant. * But I need *some* way to take/configure resources when @@ -227,9 +227,9 @@ void Widget::visible(bool v) { } else { on_hide(); - // Set all children invisible too. + // Mark all children as not drawn too. for (const auto child : children()) { - child->visible(false); + child->drawn(false); } } } @@ -386,7 +386,9 @@ void Text::set(std::string_view value) { text = std::string{value}; set_dirty(); } - +std::string Text::get() { + return text; +} void Text::getAccessibilityText(std::string& result) { result = text; } @@ -396,18 +398,35 @@ void Text::getWidgetName(std::string& result) { void Text::paint(Painter& painter) { const auto rect = screen_rect(); auto s = has_focus() ? style().invert() : style(); - auto max_len = (unsigned)rect.width() / s.font.char_width(); - auto text_view = std::string_view{text}; - painter.fill_rectangle(rect, s.background); + const int char_width = s.font.char_width(); + const int line_height = s.font.line_height(); + if (char_width == 0 || line_height == 0) return; + const size_t chars_per_line = rect.width() / char_width; + if (chars_per_line == 0) return; + size_t lines_capacity = rect.height() / line_height; + if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically + auto text_view = std::string_view{text}; + size_t current_offset = 0; + for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) { + if (current_offset >= text_view.length()) break; + size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset); + painter.draw_string( + rect.location() + Point(0, line_idx * line_height), + s, + text_view.substr(current_offset, chunk_len)); + current_offset += chunk_len; + } +} - if (text_view.length() > max_len) - text_view = text_view.substr(0, max_len); - - painter.draw_string( - rect.location(), - s, - text_view); +bool Text::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) { + if (on_select) { + on_select(*this); + return true; + } + } + return false; } /* Labels ****************************************************************/ @@ -682,7 +701,7 @@ void Console::clear(bool clear_buffer = false) { if (clear_buffer) buffer.clear(); - if (!hidden() && visible()) { + if (!hidden() && drawn()) { _api->fill_rectangle(screen_rect().left(), screen_rect().top(), screen_rect().width(), screen_rect().height(), Theme::getInstance()->bg_darkest->background.v); } @@ -692,7 +711,7 @@ void Console::clear(bool clear_buffer = false) { void Console::write(std::string message) { bool escape = false; - if (!hidden() && visible()) { + if (!hidden() && drawn()) { const Style& s = style(); const Font& font = s.font; auto rect = screen_rect(); @@ -784,7 +803,7 @@ void Console::on_hide() { } void Console::crlf() { - if (hidden() || !visible()) + if (hidden() || !drawn()) return; const auto& s = style(); @@ -2338,6 +2357,137 @@ bool NumberField::on_touch(const TouchEvent event) { return true; } +/* FloatField ***********************************************************/ + +FloatField::FloatField( + Point parent_pos, + int length, + range_t range, + float step, + char fill_char, + bool can_loop, + uint8_t precision_) + : Widget{{parent_pos, {8 * length, 16}}}, + range{range}, + step{step}, + length_{length}, + fill_char{fill_char}, + can_loop{can_loop}, + precision{precision_} { + set_focusable(true); +} + +float FloatField::value() const { + return value_; +} + +void FloatField::getAccessibilityText(std::string& result) { + result = to_string_decimal(value_, precision); +} +void FloatField::getWidgetName(std::string& result) { + result = "FloatField"; +} + +void FloatField::set_value(float new_value, bool trigger_change) { + const float lo = range.first; + const float hi = range.second; + + if (can_loop) { + if (new_value > hi) + new_value = lo; + else if (new_value < lo) + new_value = hi; + } + new_value = clip(new_value, lo, hi); + + // set final value if needed + if (new_value != value_) { + value_ = new_value; + if (on_change && trigger_change) on_change(value_); + set_dirty(); + } +} + +void FloatField::set_range(const float min, const float max) { + range.first = min; + range.second = max; + set_value(value_, false); +} + +void FloatField::set_step(const float new_step) { + step = new_step; +} + +void FloatField::paint(Painter& painter) { + auto text = to_string_decimal(value_, precision); + const auto paint_style = has_focus() ? style().invert() : style(); + // clip to widget size + const auto r = screen_rect(); + auto label_r = style().font.size_of(text); + size_t max_chars = (r.width()) / style().font.char_width(); + if (label_r.width() > r.width()) { + text = text.substr(0, max_chars); + } + size_t padneeded = max_chars - text.length(); + if (padneeded > 0 && fill_char) { + std::string filler(fill_char, padneeded); + text = filler + text; + } + painter.fill_rectangle(r, style().background); + painter.draw_string( + screen_pos(), + paint_style, + text); +} + +bool FloatField::on_key(const KeyEvent key) { + if (key == KeyEvent::Select) { + if (on_select) { + on_select(*this); + return true; + } else { + return on_encoder(1); + } + } + return false; +} + +bool FloatField::on_encoder(const EncoderEvent delta) { + float old_value = value(); + set_value(old_value + ((float)delta * step)); + + if (on_wrap) { + if ((delta > 0) && (value_ < old_value)) + on_wrap(1); + else if ((delta < 0) && (value_ > old_value)) + on_wrap(-1); + } + return true; +} + +bool FloatField::on_keyboard(const KeyboardEvent key) { + if (key == 10) { + if (on_select) { + on_select(*this); + return true; + } + } + if (key == '+' || key == ' ') { + return on_encoder(1); + } + if (key == '-' || key == 8) { + return on_encoder(-1); + } + return false; +} + +bool FloatField::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) { + focus(); + } + return true; +} + /* SymField **************************************************************/ SymField::SymField( diff --git a/firmware/standalone/common/ui/ui_widget.hpp b/firmware/standalone/common/ui/ui_widget.hpp index d7d965e1b..036e04193 100644 --- a/firmware/standalone/common/ui/ui_widget.hpp +++ b/firmware/standalone/common/ui/ui_widget.hpp @@ -127,8 +127,8 @@ class Widget { bool dirty() const; void set_clean(); - void visible(bool v); - bool visible() { return flags.visible; }; + void drawn(bool v); + bool drawn() const { return flags.drawn; }; bool highlighted() const; void set_highlighted(const bool value); @@ -147,7 +147,7 @@ class Widget { bool hidden : 1; // Hide widget and children. bool focusable : 1; // Widget can receive focus. bool highlighted : 1; // Show in a highlighted style. - bool visible : 1; // Object was visible during last paint. + bool drawn : 1; // Object was drawn during last paint. }; flags_t flags{ @@ -155,7 +155,7 @@ class Widget { .hidden = false, .focusable = false, .highlighted = false, - .visible = false, + .drawn = false, }; static const std::vector no_children; @@ -210,6 +210,7 @@ class Rectangle : public Widget { class Text : public Widget { public: + std::function on_select{}; Text() : text{""} { } @@ -218,11 +219,14 @@ class Text : public Widget { Text(Rect parent_rect); void set(std::string_view value); + std::string get(); void paint(Painter& painter) override; void getAccessibilityText(std::string& result) override; void getWidgetName(std::string& result) override; + bool on_touch(const TouchEvent event) override; + protected: // NB: Don't truncate this string. The UI will only render // as many characters as will fit in its rectange. @@ -885,6 +889,53 @@ class NumberField : public Widget { bool can_loop{}; }; +class FloatField : public Widget { + public: + std::function on_select{}; + std::function on_change{}; + std::function on_wrap{}; + + using range_t = std::pair; + + FloatField(Point parent_pos, int length, range_t range, float step, char fill_char, bool can_loop, uint8_t precision_ = 1); + + FloatField(Point parent_pos, int length, range_t range, float step, char fill_char) + : FloatField{parent_pos, length, range, step, fill_char, true, 1} { + } + + FloatField() + : FloatField{{0, 0}, 1, {0, 1}, 1, ' ', true, 1} { + } + + FloatField(const FloatField&) = delete; + FloatField(FloatField&&) = delete; + + float value() const; + void set_value(float new_value, bool trigger_change = true); + void set_range(const float min, const float max); + void set_step(const float new_step); + void set_precision(uint8_t precision); + + void paint(Painter& painter) override; + + bool on_key(const KeyEvent key) override; + bool on_encoder(const EncoderEvent delta) override; + bool on_touch(const TouchEvent event) override; + bool on_keyboard(const KeyboardEvent event) override; + + void getAccessibilityText(std::string& result) override; + void getWidgetName(std::string& result) override; + + private: + range_t range; + float step; + const int length_; + const char fill_char; + float value_{0}; + bool can_loop{}; + uint8_t precision = 1; +}; + /* A widget that allows for character-by-character editing of its value. */ class SymField : public Widget { public: diff --git a/firmware/standalone/common/ui/utility.hpp b/firmware/standalone/common/ui/utility.hpp index 9eab69e6d..6b88875f7 100644 --- a/firmware/standalone/common/ui/utility.hpp +++ b/firmware/standalone/common/ui/utility.hpp @@ -126,9 +126,11 @@ struct is_flags_type { template constexpr bool is_flags_type_v = is_flags_type::value; -#define ENABLE_FLAGS_OPERATORS(type) \ - template <> \ - struct is_flags_type { static constexpr bool value = true; }; +#define ENABLE_FLAGS_OPERATORS(type) \ + template <> \ + struct is_flags_type { \ + static constexpr bool value = true; \ + }; template constexpr std::enable_if_t, TEnum> operator|(TEnum a, TEnum b) { diff --git a/firmware/standalone/digitalrain/digitalrain.hpp b/firmware/standalone/digitalrain/digitalrain.hpp index 18100971e..9a22e5621 100644 --- a/firmware/standalone/digitalrain/digitalrain.hpp +++ b/firmware/standalone/digitalrain/digitalrain.hpp @@ -125,7 +125,7 @@ class DigitalRain { public: DigitalRain() { - std::srand(0); + srand(0); WIDTH = screen_width; HEIGHT = screen_height + 5; COLS = WIDTH / CHAR_WIDTH; diff --git a/firmware/test/include/doctest.h b/firmware/test/include/doctest.h index 6a134e16f..d02516978 100644 --- a/firmware/test/include/doctest.h +++ b/firmware/test/include/doctest.h @@ -6996,7 +6996,7 @@ int Context::run() { } else if (p->order_by.compare("name", true) == 0) { std::sort(testArray.begin(), testArray.end(), nameOrderComparator); } else if (p->order_by.compare("rand", true) == 0) { - std::srand(p->rand_seed); + srand(p->rand_seed); // random_shuffle implementation const auto first = &testArray[0]; diff --git a/firmware/tools/append_fpga_bitstream.py b/firmware/tools/append_fpga_bitstream.py new file mode 100644 index 000000000..df1d47bb0 --- /dev/null +++ b/firmware/tools/append_fpga_bitstream.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# Append FPGA bitstream to PRALINE firmware +# +# This script takes a PortaPack firmware.bin and appends the PRALINE FPGA +# bitstream at offset 0x100000 (1MB), then recalculates the checksum. +# +# Usage: append_fpga_bitstream.py +# + +import sys +import os + +def read_image(path): + with open(path, 'rb') as f: + return bytearray(f.read()) + +def write_image(data, path): + with open(path, 'wb') as f: + f.write(data) + +def main(): + if len(sys.argv) != 5: + print("Usage: append_fpga_bitstream.py ") + print(" firmware.bin: Input PortaPack firmware (1MB with checksum)") + print(" fpga.bin: PRALINE FPGA bitstream (LZ4 compressed)") + print(" output.bin: Output combined firmware") + print(" total_size: Total flash size in bytes (e.g., 2097152 for 2MB)") + sys.exit(1) + + firmware_path = sys.argv[1] + fpga_path = sys.argv[2] + output_path = sys.argv[3] + total_size = int(sys.argv[4], 0) + + # FPGA bitstream offset in flash + # PRALINE: Moved to 3.5MB to allow larger base firmware + FPGA_OFFSET = 0x380000 # 3.5MB (was 0x180000 = 1.5MB) + + # Read input files + firmware = read_image(firmware_path) + fpga = read_image(fpga_path) + + print(f"Firmware size: {len(firmware)} bytes") + print(f"FPGA bitstream size: {len(fpga)} bytes") + print(f"Target total size: {total_size} bytes") + + # The firmware has a 4-byte checksum at the end + # Remove it - we'll recalculate after adding FPGA bitstream + firmware_content = firmware[:-4] + + # Check that firmware content fits before FPGA offset + if len(firmware_content) > FPGA_OFFSET: + print(f"ERROR: Firmware content ({len(firmware_content)} bytes) exceeds FPGA offset ({FPGA_OFFSET})") + sys.exit(1) + + # Create output image + output = bytearray() + + # Add firmware content (padded to FPGA_OFFSET with 0xFF) + output.extend(firmware_content) + pad_size = FPGA_OFFSET - len(firmware_content) + output.extend(bytes([0xFF] * pad_size)) + + print(f"Padded firmware to {len(output)} bytes (FPGA offset)") + + # Add FPGA bitstream + output.extend(fpga) + print(f"Added FPGA bitstream, total now {len(output)} bytes") + + # Check total size + if len(output) > total_size - 4: + print(f"ERROR: Combined image ({len(output)} bytes) exceeds max size ({total_size - 4} bytes)") + sys.exit(1) + + # Pad to total_size - 4 (leaving room for checksum) + pad_size = total_size - 4 - len(output) + output.extend(bytes([0xFF] * pad_size)) + print(f"Padded to {len(output)} bytes (before checksum)") + + # Calculate checksum (same algorithm as make_spi_image.py) + checksum = 0 + for i in range(0, len(output), 4): + snippet = output[i:i + 4] + if len(snippet) == 4: + val = int.from_bytes(snippet, byteorder='little') + checksum += val + + final_checksum = 0 + checksum = (final_checksum - checksum) & 0xFFFFFFFF + output.extend(checksum.to_bytes(4, 'little')) + + print(f"Final image size: {len(output)} bytes") + print(f"Checksum: 0x{checksum:08X}") + + # Write output + write_image(output, output_path) + print(f"Written to {output_path}") + + # Report space usage + fpga_end = FPGA_OFFSET + len(fpga) + space_after_fpga = total_size - 4 - fpga_end + print(f"\nSpace usage:") + print(f" Firmware: 0x000000 - 0x{len(firmware_content):06X} ({len(firmware_content)} bytes)") + print(f" FPGA: 0x{FPGA_OFFSET:06X} - 0x{fpga_end:06X} ({len(fpga)} bytes)") + print(f" Free: 0x{fpga_end:06X} - 0x{total_size-4:06X} ({space_after_fpga} bytes)") + +if __name__ == "__main__": + main() diff --git a/firmware/tools/docker-entrypoint.sh b/firmware/tools/docker-entrypoint.sh index 8c955dd99..4891b18a8 100755 --- a/firmware/tools/docker-entrypoint.sh +++ b/firmware/tools/docker-entrypoint.sh @@ -21,35 +21,70 @@ # Boston, MA 02110-1301, USA. # -set -e # exit immediatelly on any failure +set -e # exit immediately on any failure + +BUILD_DIR="build" +CMAKE_ARGS=() +BUILD_ARGS=() + +parse_build_args() { + # 1. PARSE BUILD ARGUMENTS + # We separate arguments into two lists, and possibly get an override for the build dir: + # - BUILD_DIR: Defaults to build, can be overriden (e.g., -B build-4mb, --workdir build-praline) + # - CMAKE_ARGS: Anything starting with -D (e.g., -DFLASH_MB_SIZE=4) + # - BUILD_ARGS: Everything else (e.g., make, ninja, -j20) + + while [[ $# -gt 0 ]]; do + if [[ "$1" == -B ]] || [[ "$1" == "--workdir" ]]; then + shift + BUILD_DIR=$1 + shift + elif [[ "$1" == -D* ]]; then + CMAKE_ARGS+=("$1") + shift + else + BUILD_ARGS+=("$1") + shift + fi + done +} build_make() { + parse_build_args "$@" + echo "Building in workdir: \"$BUILD_DIR\" using CMake options: \"${CMAKE_ARGS[@]}\" and make options: \"${BUILD_ARGS[@]}\"" cd .. - mkdir -p build - cd build - cmake .. - make "$@" - exit 0 # Report success :) + mkdir -p "$BUILD_DIR" + cd "$BUILD_DIR" + cmake "${CMAKE_ARGS[@]}" .. + make "${BUILD_ARGS[@]}" + exit 0 } build_ninja() { + parse_build_args "$@" + echo "Building in workdir: \"$BUILD_DIR\" using CMake options: \"${CMAKE_ARGS[@]}\" and ninja options: \"${BUILD_ARGS[@]}\"" cd .. - mkdir -p build - cd build - cmake -G Ninja .. - ninja "$@" - exit 0 # Report success :) + mkdir -p "$BUILD_DIR" + cd "$BUILD_DIR" + cmake -G Ninja "${CMAKE_ARGS[@]}" .. + ninja "${BUILD_ARGS[@]}" + exit 0 } -if [ "$1" = 'make' ]; then # build the default (or specified) target with make - shift # remove the first item from $@ as we consumed it, we can then pass the rest on to make +if [ "$1" = 'make' ]; then + # User explicitly typed 'make' + shift build_make "$@" -elif [[ $1 == -j* ]]; then # allow passing -j without typing make_default - # dont shift here as we wanna pass the -j + +elif [[ $1 == -* ]]; then + # User passed a switch not a command as first argument. Assuming make. (e.g., -j20) build_make "$@" -elif [ "$1" = 'ninja' ]; then # build the default (or specified) target with ninja - shift # remove the first item from $@ as we consumed it, we can then pass the rest on to make + +elif [ "$1" = 'ninja' ]; then + # User explicitly typed 'ninja' + shift build_ninja "$@" fi -exec "$@" +# Fallback for other commands (e.g. /bin/bash) +exec "$@" \ No newline at end of file diff --git a/firmware/tools/external_app_info.py b/firmware/tools/external_app_info.py index 51fe7823e..41f7e9943 100644 --- a/firmware/tools/external_app_info.py +++ b/firmware/tools/external_app_info.py @@ -24,4 +24,4 @@ # external app address ranges below must match those in linker file "external.ld" maximum_application_size = 32*1024 external_apps_address_start = 0xADB00000 -external_apps_address_end = 0xADF00000 \ No newline at end of file +external_apps_address_end = 0xAE020000 diff --git a/firmware/tools/make_spi_image.py b/firmware/tools/make_spi_image.py index 0e4b28e50..47924b255 100755 --- a/firmware/tools/make_spi_image.py +++ b/firmware/tools/make_spi_image.py @@ -36,8 +36,9 @@ from pathlib import Path usage_message = """ PortaPack SPI flash image generator -Usage: +Usage: Where paths refer to the .bin files for each component project. + spi_size is the total size of the target flash (e.g. 1048576). """ @@ -166,13 +167,14 @@ def get_gcc_version_from_elf_files_in_giving_path_or_filename_s_path(path): #^^^^^^^^gcc version check from elf file^^^^^^^^ -if len(sys.argv) != 4: +if len(sys.argv) != 5: print(usage_message) sys.exit(-1) application_image = read_image(sys.argv[1]) baseband_image = read_image(sys.argv[2]) output_path = sys.argv[3] +spi_size = int(sys.argv[4], 0) print("\ncheck gcc versions from all elf target\n") application_gcc_versions = get_gcc_version_from_elf_files_in_giving_path_or_filename_s_path(sys.argv[1]) @@ -207,8 +209,6 @@ except Exception as e: #^^^^^^^^external app linker script address check worker^^^^^^^^ -spi_size = 1048576 - images = ( { 'name': 'application', diff --git a/flashing/README.txt b/flashing/README.txt index 214c6bdc4..488f2eb43 100644 --- a/flashing/README.txt +++ b/flashing/README.txt @@ -1,3 +1,3 @@ -Plug HackRF+Portapack, set it in HackRF mode, launch flash_portapack_mayhem.bat +Run mayhem_flasher.bat and follow the prompts to select your device and action (flash Mayhem, DFU unbrick, or restore factory firmware). -Read https://github.com/portapack-mayhem/mayhem-firmware/wiki/Update-firmware for more detailed information \ No newline at end of file +Read https://github.com/portapack-mayhem/mayhem-firmware/wiki/Update-firmware for more detailed information. \ No newline at end of file diff --git a/flashing/dfu_hackrf_one.bat b/flashing/dfu_hackrf_one.bat deleted file mode 100644 index e11b61fc8..000000000 --- a/flashing/dfu_hackrf_one.bat +++ /dev/null @@ -1,19 +0,0 @@ -@echo off - -echo *** Run HackRF firmware in RAM via LPC DFU *** -echo. -echo This is used to "unbrick" your HackRF, if you are no longer able to use -echo HackRF tools to flash or operate your HackRF. -echo. -echo Connect your HackRF One to a USB port on your computer. -echo. -echo Hold down both the DFU and RESET buttons on the HackRF. -echo Then release the RESET button (closest to the edge). -echo Then release the DFU button. -echo. -pause - -echo. -"utils/dfu-util-static.exe" --device 1fc9:000c --download "utils/hackrf_one_usb.dfu" --reset -echo. -pause diff --git a/flashing/flash_portapack_mayhem.bat b/flashing/flash_portapack_mayhem.bat deleted file mode 100644 index 8eb7b707f..000000000 --- a/flashing/flash_portapack_mayhem.bat +++ /dev/null @@ -1,32 +0,0 @@ -@echo off - -echo *** Re-flash the HackRF with PortaPack firmware *** -echo. -echo Connect your HackRF One to a USB port on your computer. -echo. -echo If using a PortaPack, put the PortaPack in HackRF mode by selecting -echo the "HackRF" option from the main menu. -echo. -pause - -echo. - -REM Check if the firmware file exists -if not exist portapack-mayhem-firmware.bin ( - echo The firmware file "portapack-mayhem-firmware.bin" does not exist. - echo Please ensure that you have downloaded the latest release from: - echo https://github.com/portapack-mayhem/mayhem-firmware/releases/ - echo. - pause - exit /b -) - -"utils/hackrf_update.exe" portapack-mayhem-firmware.bin -echo. -echo If your device never boot after flashing, please refer to won't boot article" -echo. -echo "click-to-open url: https://github.com/portapack-mayhem/mayhem-firmware/wiki/Won%%27t-boot" -echo "or -echo "copy-and-paste url: https://github.com/portapack-mayhem/mayhem-firmware/wiki/Won't-boot" -echo. -pause diff --git a/flashing/mayhem_flasher.bat b/flashing/mayhem_flasher.bat new file mode 100644 index 000000000..0bb9e3906 --- /dev/null +++ b/flashing/mayhem_flasher.bat @@ -0,0 +1,310 @@ +@echo off +setlocal EnableDelayedExpansion + +echo ========================================================= +echo PortaPack Mayhem - Device Flasher +echo ========================================================= +echo. +echo Connect your device to a USB port on your computer. +echo. + +REM ── Step 1: Select device ───────────────────────────────── +echo What is your device? +echo. +echo 1. HackRF One / PortaPack +echo 2. PortaRF +echo 3. HackRF Pro / PortaPack +echo. +set /p DEVICE_CHOICE="Enter your choice (1, 2 or 3): " + +if "%DEVICE_CHOICE%"=="1" set DEVICE_NAME=HackRF One / PortaPack +if "%DEVICE_CHOICE%"=="2" set DEVICE_NAME=PortaRF +if "%DEVICE_CHOICE%"=="3" set DEVICE_NAME=HackRF Pro / PortaPack + +if not defined DEVICE_NAME ( + echo. + echo Invalid choice. Please run the script again and enter 1, 2, or 3. + echo. + pause + exit /b +) + +echo. +echo Device: %DEVICE_NAME% +echo. + +REM ── Step 2: Select action ────────────────────────────────── +echo What would you like to do? +echo. +echo 1. Flash Mayhem firmware +echo 2. Flash DFU then Mayhem (DFU unbrick followed by Mayhem flash in one go) +echo 3. Flash via DFU (unbrick - run HackRF firmware from RAM) +echo 4. Flash factory HackRF firmware (HackRF only - removes PortaPack support) +echo. +set /p ACTION_CHOICE="Enter your choice (1, 2, 3 or 4): " + +if "%ACTION_CHOICE%"=="1" goto :flash_mayhem +if "%ACTION_CHOICE%"=="2" goto :flash_dfu_then_mayhem +if "%ACTION_CHOICE%"=="3" goto :flash_dfu +if "%ACTION_CHOICE%"=="4" goto :flash_factory + +echo. +echo Invalid choice. Please run the script again and enter 1, 2, 3, or 4. +echo. +pause +exit /b + + +REM ── Action 1: Flash Mayhem ───────────────────────────────── +:flash_mayhem +echo. +call :set_firmware +if "%DEVICE_CHOICE%"=="1" ( + echo If your device has a PortaPack attached, switch it to HackRF mode first by + echo selecting the "HackRF" option from the main menu. + echo. +) +echo Firmware: %FIRMWARE% +echo. +call :check_file_exists "%FIRMWARE%" "firmware file" || (pause & exit /b) +call :do_flash +if "!FLASH_OK!"=="1" call :print_success +pause +exit /b + + +REM ── Action 2: DFU unbrick ────────────────────────────────── +:flash_dfu +echo. +echo *** Load HackRF firmware into RAM via LPC DFU *** +echo. +echo Use this to unbrick your device if you can no longer use HackRF tools +echo to flash or communicate with it. +echo. +echo Before pressing any key, put your device into DFU mode: +echo 1. Hold down both the DFU and RESET buttons. +echo 2. Release the RESET button first (the one closest to the edge). +echo 3. Then release the DFU button. +echo. +call :set_dfu_file +call :check_file_exists "%DFU_FILE%" "DFU file" || (pause & exit /b) +echo. +call :do_dfu +echo. +pause +exit /b + + +REM ── Action 3: DFU then flash Mayhem ───────────────────────── +:flash_dfu_then_mayhem +echo. +echo *** DFU unbrick followed by Mayhem firmware flash *** +echo. +echo This will first load HackRF firmware into RAM via DFU, then flash Mayhem. +echo. +echo Before pressing any key, put your device into DFU mode: +echo 1. Hold down both the DFU and RESET buttons. +echo 2. Release the RESET button first (the one closest to the edge). +echo 3. Then release the DFU button. +echo. +call :set_dfu_file +call :check_file_exists "%DFU_FILE%" "DFU file" || (pause & exit /b) +call :set_firmware +call :check_file_exists "%FIRMWARE%" "firmware file" || (pause & exit /b) + +echo Step 1 of 2: Loading HackRF firmware via DFU... +echo. +call :do_dfu + +echo. +echo DFU complete. Waiting 5 seconds for device to reconnect... +timeout /t 5 /nobreak >nul + +echo. +echo Step 2 of 2: Flashing Mayhem firmware (%FIRMWARE%)... +echo. +call :do_flash +if "!FLASH_OK!"=="1" call :print_success +pause +exit /b + + +REM ── Serial fallback: boot device into HackRF mode via serial ── +:serial_fallback +set SERIAL_OK=0 +set COM_PORT= +echo. + +REM Enumerate available COM ports into a temp file +powershell -NoProfile -Command "[System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object | ForEach-Object { $_ }" > "%TEMP%\mayhem_comports.txt" 2>nul + +REM Count and index ports +set PORT_COUNT=0 +for /f "usebackq tokens=*" %%P in ("%TEMP%\mayhem_comports.txt") do ( + set /a PORT_COUNT+=1 + set PORT_!PORT_COUNT!=%%P +) + +if !PORT_COUNT! equ 0 ( + goto :eof +) + +if !PORT_COUNT! equ 1 ( + set COM_PORT=!PORT_1! +) else ( + echo Multiple COM ports detected. Select the one your device is on: + echo. + for /l %%I in (1,1,!PORT_COUNT!) do ( + echo %%I. !PORT_%%I! + ) + echo. + set /p PORT_CHOICE="Enter your choice (1-!PORT_COUNT!): " + set COM_PORT= + for /l %%I in (1,1,!PORT_COUNT!) do ( + if "!PORT_CHOICE!"=="%%I" set COM_PORT=!PORT_%%I! + ) + if not defined COM_PORT ( + echo Invalid selection. + goto :eof + ) +) + +echo Connecting to !COM_PORT!... +powershell -NoProfile -Command "$p = New-Object System.IO.Ports.SerialPort('!COM_PORT!', 115200, [System.IO.Ports.Parity]::None, 8, [System.IO.Ports.StopBits]::One); try { $p.Open(); $p.Write('hackrf' + [char]13 + [char]10); Start-Sleep -Milliseconds 500 } catch { Write-Host ('Serial error: ' + $_.Exception.Message); exit 1 }; try { $p.Close() } catch {}; exit 0" +if %ERRORLEVEL% equ 0 ( + set SERIAL_OK=1 + echo Switching to HackRF mode, waiting for device... + timeout /t 7 /nobreak >nul +) else ( + echo Could not open !COM_PORT!. Make sure the port is not in use. +) +goto :eof + + +REM ── Action 4: Flash factory HackRF firmware ──────────────── +:flash_factory +echo. +echo *** Restore factory HackRF firmware *** +echo. +echo This will remove Mayhem and restore the original HackRF firmware. +echo PortaPack functionality will no longer be available after this. +echo. + +if "%DEVICE_CHOICE%"=="3" ( + set FACTORY_BIN=firmware\hackrf_hpro_usb.bin +) else ( + set FACTORY_BIN=firmware\hackrf_usb.bin +) + +call :check_file_exists "%FACTORY_BIN%" "factory firmware" || (pause & exit /b) +echo. +"utils/hackrf_spiflash.exe" -R -w "%FACTORY_BIN%" +echo. +pause +exit /b + + +REM ── Helper: Set FIRMWARE based on DEVICE_CHOICE ───────────── +:set_firmware +if "%DEVICE_CHOICE%"=="1" set FIRMWARE=firmware\firmware_hackrf.bin +if "%DEVICE_CHOICE%"=="2" set FIRMWARE=firmware\firmware_portarf.bin +if "%DEVICE_CHOICE%"=="3" set FIRMWARE=firmware\firmware_hpro.bin +goto :eof + + +REM ── Helper: Set DFU_FILE based on DEVICE_CHOICE ───────────── +:set_dfu_file +if "%DEVICE_CHOICE%"=="3" ( + set DFU_FILE=firmware\hackrf_hpro_usb.dfu +) else ( + set DFU_FILE=firmware\hackrf_usb.dfu +) +goto :eof + + +REM ── Helper: Check a required file exists ───────────────────── +REM %1 = quoted file path %2 = human-readable label +:check_file_exists +if not exist %1 ( + echo ERROR: The %~2 file %1 was not found. + echo Please ensure you have downloaded the latest release from: + echo https://release.hackrf.app/ + echo. + exit /b 1 +) +exit /b 0 + + +REM ── Helper: Flash FIRMWARE with serial fallback ─────────────── +:do_flash +set FLASH_OK=0 +"utils/hackrf_spiflash.exe" -R -w "%FIRMWARE%" +if %ERRORLEVEL% equ 0 ( + set FLASH_OK=1 + goto :eof +) +call :serial_fallback +if "!SERIAL_OK!"=="1" ( + echo. + echo Device should now be in HackRF mode. Retrying flash... + echo. + "utils/hackrf_spiflash.exe" -R -w "%FIRMWARE%" + if !ERRORLEVEL! equ 0 ( + set FLASH_OK=1 + ) else ( + echo. + call :print_error + ) +) else ( + echo. + call :print_error +) +goto :eof + + +REM ── Helper: Run DFU flash ──────────────────────────────────── +:do_dfu +"utils/dfu-util-static.exe" --device 1fc9:000c --download "%DFU_FILE%" +goto :eof + + +REM ── Helper: Print error banner ────────────────────────────── +:print_error +echo. +echo $$$$$$\ +echo $$ __$$\ +echo $$ / $$ ^| $$$$$$\ $$$$$$\ $$$$$$$\ +echo $$ ^| $$ ^|$$ __$$\ $$ __$$\ $$ _____^| +echo $$ ^| $$ ^|$$ / $$ ^|$$ / $$ ^|\$$$$$$\ +echo $$ ^| $$ ^|$$ ^| $$ ^|$$ ^| $$ ^| \____$$\ +echo $$$$$$ ^|\$$$$$$ ^|$$$$$$$ ^|$$$$$$$ ^| +echo \______/ \______/ $$ ____/ \_______/ +echo $$ ^| +echo $$ ^| +echo \__^| +echo. +echo ERROR: Device not found. Make sure your device is connected, +echo powered on, and in HackRF mode, then try again. +echo. +goto :eof + + +REM ── Helper: Print success banner ───────────────────────────── +:print_success +echo. +echo ______ __ __ ______ ______ ________ ______ ______ +echo / \ ^| \ ^| \ / \ / \ ^| \ / \ / \ +echo ^| $$$$$$\^| $$ ^| $$^| $$$$$$\^| $$$$$$\^| $$$$$$$$^| $$$$$$\^| $$$$$$\ +echo ^| $$___\$$^| $$ ^| $$^| $$ \$$^| $$ \$$^| $$__ ^| $$___\$$^| $$___\$$ +echo \$$ \ ^| $$ ^| $$^| $$ ^| $$ ^| $$ \ \$$ \ \$$ \ +echo _\$$$$$$\^| $$ ^| $$^| $$ __ ^| $$ __ ^| $$$$$ _\$$$$$$\ _\$$$$$$\ +echo ^| \__^| $$^| $$__/ $$^| $$__/ \^| $$__/ \^| $$_____ ^| \__^| $$^| \__^| $$ +echo \$$ $$ \$$ $$ \$$ $$ \$$ $$^| $$ \ \$$ $$ \$$ $$ +echo \$$$$$$ \$$$$$$ \$$$$$$ \$$$$$$ \$$$$$$$$ \$$$$$$ \$$$$$$ +echo. +echo Firmware flashed successfully. Your device is rebooting... +echo. +echo If your device does not boot after flashing, see the troubleshooting wiki: +echo https://github.com/portapack-mayhem/mayhem-firmware/wiki/Wont-boot +echo. +goto :eof diff --git a/flashing/utils/flash_hackrf_one.bat b/flashing/utils/flash_hackrf_one.bat deleted file mode 100644 index 26e270683..000000000 --- a/flashing/utils/flash_hackrf_one.bat +++ /dev/null @@ -1,15 +0,0 @@ -@echo off - -echo *** Re-flash the HackRF with HackRF firmware *** -echo. -echo Connect your HackRF One to a USB port on your computer. -echo. -echo If using a PortaPack, put the PortaPack in HackRF mode by selecting -echo the "HackRF" option from the main menu. -echo. -pause - -echo. -"utils/hackrf_update.exe" "utils/hackrf_one_usb.bin" -echo. -pause diff --git a/flashing/utils/hackrf_biast.exe b/flashing/utils/hackrf_biast.exe new file mode 100644 index 000000000..b90929eef Binary files /dev/null and b/flashing/utils/hackrf_biast.exe differ diff --git a/flashing/utils/hackrf_clock.exe b/flashing/utils/hackrf_clock.exe new file mode 100644 index 000000000..8dc9dfa9a Binary files /dev/null and b/flashing/utils/hackrf_clock.exe differ diff --git a/flashing/utils/hackrf_cpldjtag.exe b/flashing/utils/hackrf_cpldjtag.exe new file mode 100644 index 000000000..33cdebf86 Binary files /dev/null and b/flashing/utils/hackrf_cpldjtag.exe differ diff --git a/flashing/utils/hackrf_debug.exe b/flashing/utils/hackrf_debug.exe new file mode 100644 index 000000000..46cdacc1b Binary files /dev/null and b/flashing/utils/hackrf_debug.exe differ diff --git a/flashing/utils/hackrf_info.exe b/flashing/utils/hackrf_info.exe new file mode 100644 index 000000000..611662bdc Binary files /dev/null and b/flashing/utils/hackrf_info.exe differ diff --git a/flashing/utils/hackrf_operacake.exe b/flashing/utils/hackrf_operacake.exe new file mode 100644 index 000000000..8612fd1e9 Binary files /dev/null and b/flashing/utils/hackrf_operacake.exe differ diff --git a/flashing/utils/hackrf_spiflash.exe b/flashing/utils/hackrf_spiflash.exe new file mode 100644 index 000000000..c1741f498 Binary files /dev/null and b/flashing/utils/hackrf_spiflash.exe differ diff --git a/flashing/utils/hackrf_sweep.exe b/flashing/utils/hackrf_sweep.exe new file mode 100644 index 000000000..9324ad99e Binary files /dev/null and b/flashing/utils/hackrf_sweep.exe differ diff --git a/flashing/utils/hackrf_transfer.exe b/flashing/utils/hackrf_transfer.exe new file mode 100644 index 000000000..cb087af19 Binary files /dev/null and b/flashing/utils/hackrf_transfer.exe differ diff --git a/flashing/utils/hackrf_update.exe b/flashing/utils/hackrf_update.exe deleted file mode 100644 index 529e45f84..000000000 Binary files a/flashing/utils/hackrf_update.exe and /dev/null differ diff --git a/flashing/utils/msvcp120.dll b/flashing/utils/msvcp120.dll deleted file mode 100644 index 4ea1efa73..000000000 Binary files a/flashing/utils/msvcp120.dll and /dev/null differ diff --git a/flashing/utils/msvcr120.dll b/flashing/utils/msvcr120.dll deleted file mode 100644 index d711c9223..000000000 Binary files a/flashing/utils/msvcr120.dll and /dev/null differ diff --git a/format-code.ps1 b/format-code.ps1 new file mode 100644 index 000000000..0b4fa7d6e --- /dev/null +++ b/format-code.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = "Stop" + +# Run from repo root (script is in repo root) +$CF = Join-Path $PSScriptRoot "tools\clang-format.ps1" + +if (-not (Test-Path $CF)) { + throw "ERROR: $CF not found. Did you commit tools\clang-format.ps1 and the pinned binaries?" +} + +# Print version for traceability +& $CF --version | Write-Host + +# Collect files (PowerShell-native; avoids xargs/find differences on Windows) +$roots = @( + "firmware/common", + "firmware/baseband", + "firmware/application", + "firmware/test/application", + "firmware/test/baseband" +) + +$files = foreach ($r in $roots) { + if (Test-Path $r) { + Get-ChildItem -Path $r -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.Extension -in @(".h", ".hpp", ".c", ".cpp") } | + ForEach-Object { $_.FullName } + } +} + +if (-not $files -or $files.Count -eq 0) { + Write-Host "No matching source files found." + exit 0 +} + +# Format in place +& $CF -style=file -i -- $files +exit $LASTEXITCODE diff --git a/format-code.sh b/format-code.sh index b0e9bb507..c065ab21f 100755 --- a/format-code.sh +++ b/format-code.sh @@ -1,2 +1,24 @@ #!/bin/sh -find firmware/common firmware/baseband firmware/application firmware/test/application firmware/test/baseband -iname '*.h' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.c' | xargs clang-format-18 -style=file -i +set -eu + +# Run from repo root (script is in repo root) +CF="./tools/clang-format.sh" + +# Sanity check +if [ ! -x "$CF" ]; then + echo "ERROR: $CF not found or not executable." >&2 + echo "Did you commit the pinned clang-format binaries and wrappers under tools/ ?" >&2 + exit 2 +fi + +# Print version for traceability +"$CF" --version + +find firmware/common \ + firmware/baseband \ + firmware/application \ + firmware/test/application \ + firmware/test/baseband \ + \( -iname '*.h' -o -iname '*.hpp' -o -iname '*.c' -o -iname '*.cpp' \) \ + -print0 | \ + xargs -0 -r "$CF" -style=file -i diff --git a/hackrf b/hackrf index c0b15549c..38e082b93 160000 --- a/hackrf +++ b/hackrf @@ -1 +1 @@ -Subproject commit c0b15549cbcf18fbef5221e471335f72198f9c71 +Subproject commit 38e082b9399fae30241a6ca5a1e1be0e157115de diff --git a/pull_request_template.md b/pull_request_template.md deleted file mode 100644 index 70ac08e14..000000000 --- a/pull_request_template.md +++ /dev/null @@ -1,5 +0,0 @@ -## Brief description what you did - -## Checklist -- [ ] Kept changes minimal and limited to necessary files -- [ ] Verified functionality remains intact and code compiles diff --git a/sdcard/EPIRB/BEACONS.TXT b/sdcard/EPIRB/BEACONS.TXT new file mode 100644 index 000000000..e5ba8406d --- /dev/null +++ b/sdcard/EPIRB/BEACONS.TXT @@ -0,0 +1,25 @@ +#Title; Description; Frame (Hex format : 18 Bytes / 36 Hex char) +Selftest; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041775302D +Standard Test; ADRASEC02 30/11/2014 N49d16m32s/E3d16m32s; FFFED08e3e0425a8318074fe44b735cd7b46 +User protocol KO; Radio call sign (wrong BCH1); FFFED03EF613523F81FE0 +User protocol OK; Radio call sign (valid BCH1); FFFED056E6804002202009655250 +RLS Location; RLS Location; FFFED08E0D0990014710021963C85C7009F5 +PLB Location; PLB Location: National Location; FFFED08E3B15F1DFC0FF07FD1F769F3C0672 +RLS Location 2; RLS Location Protocol; FFFED08F4DCBC01ACD004BB10D4E79C4DD86 +Std Loc. ELT 24; ELT 24-bit Address Protocol; FFFED096E3AAAAAA7FDFF8211F3583E0FAA8 +Standard Location; Standard Location Protocol EPIRB (Serial); FFFED096E61B0CAE7FDFFF0E58B583E0FAA8 +PLB Location; PLB Location: National Location; FFFED09F7B00F9E8EC737E5312378A1802B0 +EPIRB; Standard Location Protocol EPIRB-MMSI; FFFED0A0D205F260850F3DC9E0B70B6E4FD7 +PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437 +SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA +ELT 24; Serial user Location (ELT with Aircraft 24-bit Address); FFFED0CE46E76EF8C00C2BAA31CFE0FF0F61 +MMSO; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC +Float Free EPIRB; Float Free EPIRB with Serial Identification Number; FFFED0DF76A9A9C800174DE27BE1F0277E45 +Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146 +Radio Call Sign; Radio Call Sign - EPIRB; FFFED0E0DDAE599508268E4C05E054651307 +ELT Aviation; ELT Aviation User; FFFED0E29325ACB12E938B671E0FE0FF0F61 +ELT Aircraft; ELT with Aircraft Operator Designator & Serial Number; FFFED0EDF67B7182038C2F0E10CFE0FF0F61 +PLB Serial; Standard Location Protocol - PLB (Serial); FFFED0A157B081437FDFF8B4833783E0F66C +RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8 +Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500 +ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072 \ No newline at end of file diff --git a/sdcard/KEELOQKEYS/MFCODES b/sdcard/KEELOQKEYS/MFCODES new file mode 100644 index 000000000..d3a5b26bd --- /dev/null +++ b/sdcard/KEELOQKEYS/MFCODES @@ -0,0 +1 @@ +Stilmatic;0123456789ABCDEF;2 diff --git a/sdcard/KEELOQKEYS/README.md b/sdcard/KEELOQKEYS/README.md new file mode 100644 index 000000000..dd530d519 --- /dev/null +++ b/sdcard/KEELOQKEYS/README.md @@ -0,0 +1,14 @@ +# Keystore File Specification +## Introduction +This directory contains the KeeLoq keystore file needed by SubGhzD to decrypt KeeLoq signals and used by the KeeLoq TX app to emulate KeeLoq transmitters. +## Where to get more KeeLoq keys +To gather more KeeLoq keys you can either extract them directly from the receiver's firmware or you can grab them from Flipper Zero firmwares, such as Unleashed and Momentum. + +To do the latter a Flipper Zero device is required, since the `keeloq_mfcodes` file on the flipper is encrypted using the factory AES-128 keys stored in the its security enclave chip. In order to decrypt them and have them exported for use in Mayhem, you can use [Rocket God's SubGHz toolkit app](https://github.com/RocketGod-git/RocketGods-SubGHz-Toolkit). + +## File structure +Each line in the MFCODES file contains three fields, each separated by a semicolon: Manufacturer, key and key type. For example: +``` +Stilmatic;0123456789ABCDEF;2 +``` +The type field can be either 1 or 2 currently (KeeLoq simple learning and normal learning, respectively) since those are the currently supported learning modes. diff --git a/tools/clang-format-bin/18.1.8/linux-arm64/clang-format b/tools/clang-format-bin/18.1.8/linux-arm64/clang-format new file mode 100755 index 000000000..bb75ea2a3 Binary files /dev/null and b/tools/clang-format-bin/18.1.8/linux-arm64/clang-format differ diff --git a/tools/clang-format-bin/18.1.8/linux-arm64/lib/libtinfo.so.5 b/tools/clang-format-bin/18.1.8/linux-arm64/lib/libtinfo.so.5 new file mode 100644 index 000000000..8fbf4440b Binary files /dev/null and b/tools/clang-format-bin/18.1.8/linux-arm64/lib/libtinfo.so.5 differ diff --git a/tools/clang-format-bin/18.1.8/linux-x86_64/clang-format b/tools/clang-format-bin/18.1.8/linux-x86_64/clang-format new file mode 100755 index 000000000..36e8a0db9 Binary files /dev/null and b/tools/clang-format-bin/18.1.8/linux-x86_64/clang-format differ diff --git a/tools/clang-format-bin/18.1.8/linux-x86_64/lib/libtinfo.so.5 b/tools/clang-format-bin/18.1.8/linux-x86_64/lib/libtinfo.so.5 new file mode 100644 index 000000000..6caaddc9f Binary files /dev/null and b/tools/clang-format-bin/18.1.8/linux-x86_64/lib/libtinfo.so.5 differ diff --git a/tools/clang-format-bin/18.1.8/macos-arm64/clang-format b/tools/clang-format-bin/18.1.8/macos-arm64/clang-format new file mode 100755 index 000000000..e12a38676 Binary files /dev/null and b/tools/clang-format-bin/18.1.8/macos-arm64/clang-format differ diff --git a/tools/clang-format-bin/18.1.8/windows-x86_64/clang-format.exe b/tools/clang-format-bin/18.1.8/windows-x86_64/clang-format.exe new file mode 100755 index 000000000..84beede49 Binary files /dev/null and b/tools/clang-format-bin/18.1.8/windows-x86_64/clang-format.exe differ diff --git a/tools/clang-format.ps1 b/tools/clang-format.ps1 new file mode 100644 index 000000000..e510348bb --- /dev/null +++ b/tools/clang-format.ps1 @@ -0,0 +1,12 @@ +$ErrorActionPreference = "Stop" + +$Ver = "18.1.8" +$Root = Resolve-Path (Join-Path $PSScriptRoot "..") + +$Bin = Join-Path $Root "tools\clang-format-bin\$Ver\windows-x86_64\clang-format.exe" +if (-not (Test-Path $Bin)) { + throw "Missing clang-format binary: $Bin" +} + +& $Bin @args +exit $LASTEXITCODE diff --git a/tools/clang-format.sh b/tools/clang-format.sh new file mode 100755 index 000000000..6b0d48625 --- /dev/null +++ b/tools/clang-format.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +VER="18.1.8" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Linux) OS_ID="linux" ;; + Darwin) OS_ID="macos" ;; + *) echo "Unsupported OS: $OS" >&2; exit 2 ;; +esac + +case "$ARCH" in + x86_64|amd64) ARCH_ID="x86_64" ;; + arm64|aarch64) ARCH_ID="arm64" ;; + *) echo "Unsupported arch: $ARCH" >&2; exit 2 ;; +esac + +BIN="$ROOT/tools/clang-format-bin/$VER/$OS_ID-$ARCH_ID/clang-format" +if [[ ! -x "$BIN" ]]; then + echo "Missing clang-format binary: $BIN" >&2 + exit 3 +fi + +# Fix Ubuntu-built clang-format runtime deps on Debian/Arch by vendoring needed libs +if [[ "$OS_ID" == "linux" ]]; then + LIBDIR="$ROOT/tools/clang-format-bin/$VER/$OS_ID-$ARCH_ID/lib" + if [[ -d "$LIBDIR" ]]; then + export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + fi +fi + +exec "$BIN" "$@" diff --git a/tools/fetch-clang-format.sh b/tools/fetch-clang-format.sh new file mode 100755 index 000000000..3f875273f --- /dev/null +++ b/tools/fetch-clang-format.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +VER="18.1.8" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DEST="$ROOT/tools/clang-format-bin/$VER" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +base="https://github.com/llvm/llvm-project/releases/download/llvmorg-${VER}" + +# Archives you said you want to support +LINUX_X86="clang+llvm-${VER}-x86_64-linux-gnu-ubuntu-18.04.tar.xz" +LINUX_A64="clang+llvm-${VER}-aarch64-linux-gnu.tar.xz" +MAC_A64="clang+llvm-${VER}-arm64-apple-macos11.tar.xz" +WIN_X86="clang+llvm-${VER}-x86_64-pc-windows-msvc.tar.xz" + +mkdir -p \ + "$DEST/linux-x86_64" \ + "$DEST/linux-arm64" \ + "$DEST/macos-arm64" \ + "$DEST/windows-x86_64" + +fetch_and_extract() { + local archive="$1" + local outpath="$2" + local inner="$3" + + echo "Downloading $archive" + curl -fsSL "$base/$archive" -o "$TMP/$archive" + + echo "Extracting $inner" + tar -xf "$TMP/$archive" -C "$TMP" + + # The tarball extracts into a single top-level directory; find it + local topdir + topdir="$(find "$TMP" -maxdepth 1 -type d -name "clang+llvm-${VER}-*" | head -n 1)" + if [[ -z "${topdir:-}" ]]; then + echo "Could not locate extracted directory for $archive" >&2 + exit 4 + fi + + if [[ ! -f "$topdir/$inner" ]]; then + echo "Missing expected file inside archive: $inner" >&2 + exit 5 + fi + + cp "$topdir/$inner" "$outpath" + rm -rf "$topdir" +} + +# Linux x86_64 -> clang-format +fetch_and_extract "$LINUX_X86" "$DEST/linux-x86_64/clang-format" "bin/clang-format" +chmod +x "$DEST/linux-x86_64/clang-format" + +# Linux arm64 -> clang-format +fetch_and_extract "$LINUX_A64" "$DEST/linux-arm64/clang-format" "bin/clang-format" +chmod +x "$DEST/linux-arm64/clang-format" + +# macOS arm64 -> clang-format +fetch_and_extract "$MAC_A64" "$DEST/macos-arm64/clang-format" "bin/clang-format" +chmod +x "$DEST/macos-arm64/clang-format" + +# Windows x86_64 -> clang-format.exe +fetch_and_extract "$WIN_X86" "$DEST/windows-x86_64/clang-format.exe" "bin/clang-format.exe" + +echo +echo "Done. Installed pinned clang-format binaries under:" +echo " $DEST" +echo +echo "Verify:" +"$ROOT/tools/clang-format" --version || true + diff --git a/tools/get_libtinfo_linux_x86_64_arm64.sh b/tools/get_libtinfo_linux_x86_64_arm64.sh new file mode 100755 index 000000000..3dd1c1333 --- /dev/null +++ b/tools/get_libtinfo_linux_x86_64_arm64.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +VER="18.1.8" +X86_DEST="clang-format-bin/${VER}/linux-x86_64/lib" +ARM_DEST="clang-format-bin/${VER}/linux-arm64/lib" + +mkdir -p "$X86_DEST" "$ARM_DEST" + +docker run --rm \ + --platform linux/amd64 \ + -v "$PWD":/repo \ + ubuntu:18.04 \ + bash -lc " + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + + apt-get update + apt-get install -y --no-install-recommends ca-certificates dpkg-dev curl + + mkdir -p /tmp/dl /tmp/t + cd /tmp/dl + + echo '[*] amd64: download + extract libtinfo5' + apt-get download libtinfo5 + rm -rf /tmp/t && mkdir -p /tmp/t + dpkg-deb -x /tmp/dl/libtinfo5_*_amd64.deb /tmp/t + cp -av /tmp/t/lib/x86_64-linux-gnu/libtinfo.so.5* /repo/${X86_DEST}/ + + echo '[*] arm64: download libtinfo5 .deb directly + extract' + # This is the Ubuntu 18.04 (bionic-updates) arm64 libtinfo5 package matching the amd64 one above. + ARM_DEB='libtinfo5_6.1-1ubuntu1.18.04.1_arm64.deb' + ARM_URL='http://ports.ubuntu.com/ubuntu-ports/pool/main/n/ncurses/'\"\$ARM_DEB\" + + curl -fsSL \"\$ARM_URL\" -o \"/tmp/dl/\$ARM_DEB\" + + rm -rf /tmp/t && mkdir -p /tmp/t + dpkg-deb -x \"/tmp/dl/\$ARM_DEB\" /tmp/t + cp -av /tmp/t/lib/aarch64-linux-gnu/libtinfo.so.5* /repo/${ARM_DEST}/ + + echo + echo '[*] Done. Repo now contains:' + ls -la /repo/${X86_DEST} || true + ls -la /repo/${ARM_DEST} || true + "