mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-14 18:43:01 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef2ae9874b | |||
| 5deeaed5f0 | |||
| d5f94398ce | |||
| fba15c48e5 | |||
| 888634ce35 | |||
| 03131c8a0c | |||
| 605c9efa0c | |||
| aee54e5b2d | |||
| e33e697cca | |||
| d7d27d99e7 | |||
| 78d3480b68 | |||
| 70b42f4a56 | |||
| 5c719645d6 | |||
| b0eb776bdd | |||
| ce87161f9b | |||
| 8883f2decc | |||
| e06d23ea3e | |||
| 0da90c7c5e | |||
| e7572e5d41 | |||
| 50f1e51d7e | |||
| 55a15958ec | |||
| 395e20d17b | |||
| 9bf1f2175e | |||
| ec57f99a2c | |||
| 5c2d1d3dfb | |||
| 52dd2ea2bb | |||
| b273165a0c | |||
| 5aecd4097e | |||
| 2c2959df30 | |||
| 9a3c2cc6ac | |||
| c91a6db165 | |||
| bf5ea0a004 | |||
| bf166bb2fc | |||
| 52239d320a | |||
| 561143cfb0 | |||
| 678ee2963e | |||
| bae3854d27 | |||
| 0645f1897a | |||
| da0a5c3e73 | |||
| 22932e636d | |||
| 2b7302e659 | |||
| 60ce89021a | |||
| 25556428a7 | |||
| 584587d814 | |||
| a0179117b1 | |||
| ad577a1c66 | |||
| b9958a689b | |||
| 3d8bc308ee | |||
| 9ca36394d0 | |||
| f489d43259 | |||
| 6b663894b0 | |||
| b8ca356ce3 | |||
| 3a54b112ec | |||
| b013e68b17 | |||
| b2bf0f2459 |
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
👋 Thank you for your contribution!
|
||||
|
||||
⚠️ To help avoid extensive change requests or potential rejection, please review our simple Contributing Guidelines before you start coding: https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines.
|
||||
|
||||
Thank you again for contributing!
|
||||
-->
|
||||
|
||||
## Brief description what you did
|
||||
<!-- Describe your changes in detail here. What problem does this PR solve? What features does it add or fix? -->
|
||||
|
||||
|
||||
## Checklist
|
||||
- [ ] Kept changes minimal and limited to necessary files
|
||||
- [ ] Verified functionality remains intact and code compiles
|
||||
@@ -72,7 +72,7 @@ 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 *
|
||||
@@ -103,6 +103,8 @@ jobs:
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -129,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.
|
||||
@@ -145,114 +175,4 @@ jobs:
|
||||
${{ steps.changelog.outputs.content }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
# Default device assets
|
||||
- name: Upload Default 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: artifacts/oci-tar-default/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: OCI_hackrf_mayhem_nightly_${{ steps.version_date.outputs.date }}.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload Default 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: artifacts/firmware-default/firmware.zip
|
||||
asset_name: FIRMWARE_hackrf_mayhem_nightly_${{ steps.version_date.outputs.date }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload Default 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: artifacts/sdcard-default/sdcard.zip
|
||||
asset_name: COPY_TO_SDCARD_hackrf_mayhem_nightly_${{ steps.version_date.outputs.date }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload Default 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: artifacts/sdcard-no-map-default/sdcard-no-map.zip
|
||||
asset_name: COPY_TO_SDCARD_hackrf_mayhem_nightly_${{ steps.version_date.outputs.date }}-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
# PortaRF device assets
|
||||
- name: Upload PortaRF 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: artifacts/oci-tar-portarf/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: OCI_portarf_mayhem_nightly_${{ steps.version_date.outputs.date }}.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload PortaRF 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: artifacts/firmware-portarf/firmware.zip
|
||||
asset_name: FIRMWARE_portarf_mayhem_nightly_${{ steps.version_date.outputs.date }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload PortaRF 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: artifacts/sdcard-portarf/sdcard.zip
|
||||
asset_name: COPY_TO_SDCARD_portarf_mayhem_nightly_${{ steps.version_date.outputs.date }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload PortaRF 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: artifacts/sdcard-no-map-portarf/sdcard-no-map.zip
|
||||
asset_name: COPY_TO_SDCARD_portarf_mayhem_nightly_${{ steps.version_date.outputs.date }}-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
# HPro device assets
|
||||
- name: Upload HPro 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: artifacts/oci-tar-hpro/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: OCI_hpro_mayhem_nightly_${{ steps.version_date.outputs.date }}.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload HPro 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: artifacts/firmware-hpro/firmware.zip
|
||||
asset_name: FIRMWARE_hpro_mayhem_nightly_${{ steps.version_date.outputs.date }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload HPro 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: artifacts/sdcard-hpro/sdcard.zip
|
||||
asset_name: COPY_TO_SDCARD_hpro_mayhem_nightly_${{ steps.version_date.outputs.date }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload HPro 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: artifacts/sdcard-no-map-hpro/sdcard-no-map.zip
|
||||
asset_name: COPY_TO_SDCARD_hpro_mayhem_nightly_${{ steps.version_date.outputs.date }}-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
files: release_assets/*
|
||||
|
||||
@@ -55,7 +55,7 @@ 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 *
|
||||
@@ -86,6 +86,8 @@ jobs:
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
@@ -115,14 +117,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.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.
|
||||
@@ -141,114 +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
|
||||
# Default device assets
|
||||
- name: Upload Default 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: artifacts/oci-tar-default/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: OCI_hackrf_mayhem_${{ steps.version.outputs.version }}.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload Default 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: artifacts/firmware-default/firmware.zip
|
||||
asset_name: FIRMWARE_hackrf_mayhem_${{ steps.version.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload Default 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: artifacts/sdcard-default/sdcard.zip
|
||||
asset_name: COPY_TO_SDCARD_hackrf_mayhem_${{ steps.version.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload Default 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: artifacts/sdcard-no-map-default/sdcard-no-map.zip
|
||||
asset_name: COPY_TO_SDCARD_hackrf_mayhem_${{ steps.version.outputs.version }}-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
# PortaRF device assets
|
||||
- name: Upload PortaRF 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: artifacts/oci-tar-portarf/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: OCI_portarf_mayhem_${{ steps.version.outputs.version }}.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload PortaRF 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: artifacts/firmware-portarf/firmware.zip
|
||||
asset_name: FIRMWARE_portarf_mayhem_${{ steps.version.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload PortaRF 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: artifacts/sdcard-portarf/sdcard.zip
|
||||
asset_name: COPY_TO_SDCARD_portarf_mayhem_${{ steps.version.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload PortaRF 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: artifacts/sdcard-no-map-portarf/sdcard-no-map.zip
|
||||
asset_name: COPY_TO_SDCARD_portarf_mayhem_${{ steps.version.outputs.version }}-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
# HPro device assets
|
||||
- name: Upload HPro 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: artifacts/oci-tar-hpro/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: OCI_hpro_mayhem_${{ steps.version.outputs.version }}.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload HPro 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: artifacts/firmware-hpro/firmware.zip
|
||||
asset_name: FIRMWARE_hpro_mayhem_${{ steps.version.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload HPro 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: artifacts/sdcard-hpro/sdcard.zip
|
||||
asset_name: COPY_TO_SDCARD_hpro_mayhem_${{ steps.version.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload HPro 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: artifacts/sdcard-no-map-hpro/sdcard-no-map.zip
|
||||
asset_name: COPY_TO_SDCARD_hpro_mayhem_${{ steps.version.outputs.version }}-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
files: release_assets/*
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.3.1
|
||||
v2.3.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.3.2
|
||||
v2.4.0
|
||||
|
||||
@@ -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.
|
||||
|
||||
[<img src="https://github.com/user-attachments/assets/dea337ab-fb64-4a2a-b419-69afd272e815" height="310">](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#new-h4m-mayhem-edition)
|
||||
[<img src="https://camo.githubusercontent.com/5c1f1da0688240ac7b2ccca0c8dbfd1d73f2540741ad8b1828ba4d5ea68af248/68747470733a2f2f6769746875622d70726f64756374696f6e2d757365722d61737365742d3632313064662e73332e616d617a6f6e6177732e636f6d2f343339333937392f3239353533323731382d38653562363631632d663934362d346365652d386232642d3061363135663737313566342e706e67" height="310">](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h2m-mayhem-edition)
|
||||
[<img src="https://camo.githubusercontent.com/c1f7dd1e7672324f60a513f0de23de76da6a669e63896a9de535d8c8093fc3c7/68747470733a2f2f7261772e6769746875622e636f6d2f7368617265627261696e65642f706f7274617061636b2d6861636b72662f6d61737465722f646f632f696d616765732f68617264776172652f706f7274617061636b5f68315f6f7065726174696e672e6a7067" height="310">]([https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h2m-mayhem-edition](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions#h1r1r2))
|
||||
[<img src="https://github-production-user-asset-6210df.s3.amazonaws.com/4393979/295532718-8e5b661c-f946-4cee-8b2d-0a615f7715f4.png" height="310">](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:
|
||||
|
||||
[<img alt="The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?" src="https://img.youtube.com/vi/Ew2qDgm2hf0/maxresdefault.jpg" width="701">](https://share.hackrf.app/6HKX9A)
|
||||
[<img alt="PortaPack H4M Crash Course" src="https://img.youtube.com/vi/CjI-nUzdpas/maxresdefault.jpg" width="701">](https://share.hackrf.app/SPFOZO)
|
||||
|
||||
[<img alt="The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?" src="https://img.youtube.com/vi/Ew2qDgm2hf0/maxresdefault.jpg" width="172">](https://share.hackrf.app/6HKX9A)
|
||||
[<img alt="It’s TOO Easy to Accidentally Do Illegal Stuff with This" src="https://img.youtube.com/vi/OPckpjBSAOw/maxresdefault.jpg" width="172">](https://share.hackrf.app/X4D5TF)
|
||||
[<img alt="HackRF Portapack H4M - Getting Started Guide" src="https://img.youtube.com/vi/wzP0zWi85SI/maxresdefault.jpg" width="172">](https://share.hackrf.app/F9MPOO)
|
||||
[<img alt="The new HackRF Portapack H4M" src="https://img.youtube.com/vi/onQRdCITmuk/maxresdefault.jpg" width="172">](https://share.hackrf.app/0JUHZ6)
|
||||
|
||||
[<img alt="It’s TOO Easy to Accidentally Do Illegal Stuff with This" src="https://img.youtube.com/vi/OPckpjBSAOw/maxresdefault.jpg" width="172">](https://share.hackrf.app/X4D5TF) [<img alt="HackRF Portapack H4M - Getting Started Guide" src="https://img.youtube.com/vi/wzP0zWi85SI/maxresdefault.jpg" width="172">](https://share.hackrf.app/F9MPOO) [<img alt="The new HackRF Portapack H4M" src="https://img.youtube.com/vi/onQRdCITmuk/maxresdefault.jpg" width="172">](https://share.hackrf.app/0JUHZ6) [<img alt="HackRF 101 : Everything You Need to Know to Get Started!" src="https://img.youtube.com/vi/xGR_PMD9LeU/maxresdefault.jpg" width="172">](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?
|
||||
|
||||
<!---not direct to h4m but to opensourcesdrlab https://share.hackrf.app/TUOLYI--->
|
||||
:heavy_check_mark:  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:  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 [](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).
|
||||
|
||||
|
||||
+17
-1
@@ -1 +1,17 @@
|
||||
Please check [Intended-Use-and-Legality](https://github.com/portapack-mayhem/mayhem-firmware/wiki/)
|
||||
# Security Policy
|
||||
|
||||
## Scope and Threat Model
|
||||
This repository hosts the codebase for an embedded system designed for security research. Because the system operates primarily offline, traditional network-based vulnerabilities are generally not applicable. However, we still welcome and review security reports concerning the code within this repository.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
If you discover a vulnerability within this tool itself, please submit a report. Please be aware that the maintainers do not promise assistance with obtaining CVE IDs or managing formal security advisories.
|
||||
|
||||
## Out of Scope
|
||||
* **Vulnerabilities found *using* this tool:** Please do not submit reports here for vulnerabilities you have discovered in other systems or targets using this tool. This channel is strictly for reporting vulnerabilities found *in* the tool itself.
|
||||
* **Other Repositories:** For security issues concerning related tools, such as MayhemHub, please submit your reports directly to their respective repositories.
|
||||
|
||||
## Research Attribution
|
||||
If you utilize this tool to successfully discover vulnerabilities in external systems, we would greatly appreciate it if you credit or mention our project in your published research or write-ups.
|
||||
|
||||
## Legal and Security Disclaimer
|
||||
For comprehensive guidelines regarding the legal and secure usage of this tool, as well as our full liability disclaimer, please refer to: [Intended-Use-and-Legality](https://github.com/portapack-mayhem/mayhem-firmware/wiki/). Users are expected to comply with all applicable laws and regulations when using this software and/or hardware.
|
||||
|
||||
@@ -203,22 +203,44 @@ SPECOptionsView::SPECOptionsView(
|
||||
|
||||
#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,
|
||||
&label_dec, &options_dec});
|
||||
&label_qi, &options_qi, &label_qs, &options_qs});
|
||||
|
||||
options_sr.set_value(receiver_model.sampling_rate() / 1000);
|
||||
options_dc.set_by_value(1);
|
||||
options_qi.set_by_value(0);
|
||||
options_qs.set_by_value(0);
|
||||
options_dec.set_by_value(0);
|
||||
// READ the current state from hardware
|
||||
fpga_reg_1 = radio::debug::fpga::register_read(1);
|
||||
|
||||
options_sr.on_change = [this](int32_t v) { receiver_model.set_sampling_rate(static_cast<uint32_t>(v) * 1000); };
|
||||
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(); };
|
||||
options_dec.on_change = [this](size_t, OptionsField::value_t v) { radio::debug::fpga::register_write(2, v); };
|
||||
// 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<uint32_t>(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() {
|
||||
@@ -249,7 +271,6 @@ AnalogAudioView::AnalogAudioView(
|
||||
&button_pro
|
||||
#else
|
||||
&waterfall
|
||||
|
||||
#endif
|
||||
});
|
||||
|
||||
@@ -398,10 +419,10 @@ void AnalogAudioView::on_baseband_bandwidth_changed(uint32_t bandwidth_hz) {
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_modulation_changed(ReceiverModel::Mode modulation) {
|
||||
baseband::spectrum_streaming_stop();
|
||||
waterfall.stop();
|
||||
update_modulation(modulation);
|
||||
on_show_options_modulation();
|
||||
baseband::spectrum_streaming_start();
|
||||
waterfall.start();
|
||||
}
|
||||
|
||||
void AnalogAudioView::remove_options_widget() {
|
||||
@@ -549,13 +570,8 @@ void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) {
|
||||
const auto is_wideband_spectrum_mode = (modulation == ReceiverModel::Mode::SpectrumAnalysis);
|
||||
receiver_model.set_modulation(modulation);
|
||||
|
||||
#ifdef PRALINE
|
||||
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 4000000);
|
||||
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 2000000);
|
||||
#else
|
||||
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000);
|
||||
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000);
|
||||
#endif
|
||||
|
||||
receiver_model.set_hidden_offset(modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); // wefax needs to be shifted, see wefax rx app.
|
||||
|
||||
|
||||
@@ -49,25 +49,22 @@ class PralineOptionsView : public View {
|
||||
// Parameters: Position, Length (in chars), Range, Step, and default value
|
||||
NumberField options_sr{
|
||||
{UI_POS_X(3), UI_POS_Y(0)},
|
||||
5, // Number of digits to show
|
||||
{0, 40000}, // Range: 0 to 40,000 kHz
|
||||
1, // Step: 1 kHz
|
||||
' ' // Fix: Single quotes for char
|
||||
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(8), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "DC"};
|
||||
OptionsField options_dc{{UI_POS_X(11), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}};
|
||||
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(14), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "QI"};
|
||||
OptionsField options_qi{{UI_POS_X(17), 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(20), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "QS"};
|
||||
OptionsField options_qs{{UI_POS_X(23), 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}}};
|
||||
|
||||
Text label_dec{{UI_POS_X(26), UI_POS_Y(0), UI_POS_WIDTH(1), UI_POS_HEIGHT(1)}, "D"};
|
||||
OptionsField options_dec{{UI_POS_X(28), UI_POS_Y(0)}, 2, {{" 1", 0}, {" 2", 1}, {" 4", 2}, {" 8", 3}, {"16", 4}}};
|
||||
|
||||
uint8_t fpga_reg_1{0x01}; // Tracks DC, QI, QS bits
|
||||
uint8_t fpga_reg_1{0x00}; // Tracks DC, QI, QS bits
|
||||
void update_fpga_ctrl();
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<File::Error> 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 */
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include "portapack.hpp"
|
||||
#include "battery.hpp"
|
||||
#include <cstring>
|
||||
#include "ui_settings.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -138,10 +140,8 @@ void BattinfoView::update_result() {
|
||||
}
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_PERCENT) == battery::BatteryManagement::BATT_VALID_PERCENT) {
|
||||
text_method.set("IC");
|
||||
button_mode.set_text("Volt");
|
||||
} else {
|
||||
text_method.set("Voltage");
|
||||
button_mode.set_text("IC");
|
||||
}
|
||||
if (uichg) set_dirty();
|
||||
// to update status bar too, send message in behalf of batt manager
|
||||
@@ -158,8 +158,9 @@ BattinfoView::BattinfoView(NavigationView& nav)
|
||||
&text_current,
|
||||
&text_charge,
|
||||
&text_method,
|
||||
&button_mode,
|
||||
&button_settings,
|
||||
&button_exit,
|
||||
&text_capacity,
|
||||
// &text_cycles,
|
||||
// &text_warn,
|
||||
&text_ttef});
|
||||
@@ -167,17 +168,11 @@ BattinfoView::BattinfoView(NavigationView& nav)
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
button_mode.on_select = [this, &nav](Button&) {
|
||||
if (button_mode.text() == "IC") {
|
||||
battery::BatteryManagement::set_calc_override(false);
|
||||
persistent_memory::set_ui_override_batt_calc(false);
|
||||
button_mode.set_text("Volt");
|
||||
} else {
|
||||
battery::BatteryManagement::set_calc_override(true);
|
||||
persistent_memory::set_ui_override_batt_calc(true);
|
||||
button_mode.set_text("IC");
|
||||
}
|
||||
button_settings.on_select = [this, &nav](Button&) {
|
||||
nav.replace<SetBatteryView>();
|
||||
};
|
||||
text_capacity.set(to_string_dec_uint(persistent_memory::battery_cap_mah()) + " mAh");
|
||||
if (!persistent_memory::battery_cap_valid()) text_capacity.set_style(Theme::getInstance()->fg_red);
|
||||
update_result();
|
||||
if (thread == nullptr) thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 10, BattinfoView::static_fn, this);
|
||||
}
|
||||
|
||||
@@ -54,48 +54,53 @@ class BattinfoView : public View {
|
||||
int32_t current = 0;
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 1 * 16}, "Percent:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 2 * 16}, "Voltage:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 3 * 16}, "Method:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(1)}, "Percent:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(2)}, "Voltage:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(3)}, "Method:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(4)}, "Capacity:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Labels labels_opt{
|
||||
{{2 * 8, 4 * 16}, "Current:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 5 * 16}, "Charge:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 6 * 16}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
|
||||
// {{2 * 8, 7 * 16}, "Cycles:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 10 * 16}, "Change method:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(5)}, "Current:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(6)}, "Charge:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(7)}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
|
||||
// {{UI_POS_X(2), UI_POS_Y(8)}, "Cycles:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(10)}, "Change settings:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text text_percent{
|
||||
{13 * 8, 1 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_voltage{
|
||||
{13 * 8, 2 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_method{
|
||||
{13 * 8, 3 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_capacity{
|
||||
{UI_POS_X(13), UI_POS_Y(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_current{
|
||||
{13 * 8, 4 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_charge{
|
||||
{13 * 8, 5 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_ttef{
|
||||
{13 * 8, 6 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
/* Text text_cycles{
|
||||
{13 * 8, 7 * 16, 10 * 16, 16},
|
||||
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_warn{
|
||||
{1 * 8, 8 * 16, screen_width, 2 * 16},
|
||||
{UI_POS_X(1), UI_POS_Y(9), screen_width, UI_POS_HEIGHT(2)},
|
||||
""}; */
|
||||
|
||||
Button button_mode{
|
||||
{2 * 8, 11 * 16 + 5, 5 * 16, 32},
|
||||
"Volt"};
|
||||
Button button_settings{
|
||||
{UI_POS_X(2), UI_POS_Y(11) + 5, UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
|
||||
"Settings"};
|
||||
|
||||
Button button_exit{
|
||||
{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
|
||||
@@ -745,18 +745,18 @@ void WFMAudioDebugView::refresh() {
|
||||
// === 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("Check M4 config");
|
||||
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 >= 0); // 7.5 MHz minimum is technically OK
|
||||
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: Check de-emphasis in M4!");
|
||||
text_status2.set("If ringy: Chk d-emph in M4!");
|
||||
text_status2.set_style(Theme::getInstance()->fg_orange);
|
||||
} else {
|
||||
std::string issues = "Issues: ";
|
||||
@@ -765,7 +765,7 @@ void WFMAudioDebugView::refresh() {
|
||||
if (!dc_ok) issues += "DC_Block ";
|
||||
text_status.set(issues);
|
||||
text_status.set_style(Theme::getInstance()->fg_red);
|
||||
text_status2.set("Fix above before checking audio.");
|
||||
text_status2.set("Fix before checking audio.");
|
||||
text_status2.set_style(Theme::getInstance()->fg_red);
|
||||
}
|
||||
}
|
||||
@@ -1712,8 +1712,9 @@ void Si5351DebugView::refresh_status() {
|
||||
// Expected for 8 MHz: P1=5888 (0x1700), a=50
|
||||
uint32_t ms_div = (p1 + 512) / 128; // Integer divider value
|
||||
|
||||
// Calculate frequency: f_out = 800 MHz / ms_div / r_div
|
||||
uint32_t freq_khz = 800000 / ms_div / r_div; // Result in kHz
|
||||
// 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) + ")");
|
||||
@@ -2182,6 +2183,487 @@ void SystemDiagnosticsView::refresh() {
|
||||
}
|
||||
#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) {
|
||||
@@ -2996,26 +3478,48 @@ 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<WFMAudioDebugView>(); }},
|
||||
{"Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<ui::PralineClockDebugView>(); }},
|
||||
{"MSynth", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351MultiSynthDebugView>(); }},
|
||||
{"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioDiagnosticsView>(); }},
|
||||
{"Radio Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<PralineRadioDebugView>(); }},
|
||||
{"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SignalPathStatusView>(); }},
|
||||
{"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SystemDiagnosticsView>(); }},
|
||||
{"PLL A", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351PLLADebugView>(); }},
|
||||
{"PLL B", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351PLLBDebugView>(); }},
|
||||
{"GPIO", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<GPIODebugView>(); }},
|
||||
{"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFC5072StatusView>(); }},
|
||||
{"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFCTuningDebugView>(); }},
|
||||
{"MAX2831", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<MAX2831DebugView>(); }},
|
||||
{"Si5351", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351DebugView>(); }},
|
||||
{"SGPIO Clk", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIO8ClockDetectorView>(); }},
|
||||
{"Baseband", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<BasebandStatusView>(); }},
|
||||
{"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIOLiveMonitorView>(); }},
|
||||
{"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioRxTestView>(); }},
|
||||
});
|
||||
}
|
||||
#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
|
||||
{"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SystemDiagnosticsView>(); }},
|
||||
{"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioDiagnosticsView>(); }},
|
||||
{"WFM Audio", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<WFMAudioDebugView>(); }},
|
||||
{"ProRadio Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<PralineRadioDebugView>(); }},
|
||||
{"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SignalPathStatusView>(); }},
|
||||
{"GPIO Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<GPIODebugView>(); }},
|
||||
{"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFC5072StatusView>(); }},
|
||||
{"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFCTuningDebugView>(); }},
|
||||
{"MAX2831 Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<MAX2831DebugView>(); }},
|
||||
{"Si5351 Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351DebugView>(); }},
|
||||
{"SGPIO8 Clock", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIO8ClockDetectorView>(); }},
|
||||
{"Baseband Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<BasebandStatusView>(); }},
|
||||
{"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIOLiveMonitorView>(); }},
|
||||
{"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioRxTestView>(); }},
|
||||
{"Pro Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_setup, [this]() { nav_.push<PralineDebugMenuView>(); }},
|
||||
#endif
|
||||
{"Buttons Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_controls, [this]() { nav_.push<DebugControlsView>(); }},
|
||||
{"M0 Stack Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { stack_dump(); }},
|
||||
|
||||
@@ -439,6 +439,16 @@ class RadioDiagnosticsView : public View {
|
||||
};
|
||||
|
||||
#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
|
||||
*/
|
||||
@@ -523,17 +533,17 @@ class WFMAudioDebugView : public View {
|
||||
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, 140, 16}, "DC/Q/QS:"};
|
||||
Text text_dc_q{{150, 156, 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, 140, 16}, "Expected Audio:"};
|
||||
Text text_expected{{150, 192, 90, 16}, "---"};
|
||||
Text text_lbl_expected{{0, 192, 96, 16}, "Expctd Aud:"};
|
||||
Text text_expected{{98, 192, 142, 16}, "---"};
|
||||
|
||||
Text text_lbl_deemph{{0, 208, 140, 16}, "De-emph Config:"};
|
||||
Text text_deemph{{150, 208, 90, 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}, "---"};
|
||||
@@ -1125,6 +1135,265 @@ class SystemDiagnosticsView : public View {
|
||||
};
|
||||
#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<ClockRow> 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 {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#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"
|
||||
@@ -446,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,
|
||||
@@ -455,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<SDCardDebugView>();
|
||||
};
|
||||
|
||||
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 !!");
|
||||
@@ -472,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 ******************************/
|
||||
@@ -1079,6 +1160,8 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&button_save,
|
||||
&button_cancel,
|
||||
&field_battcap,
|
||||
&button_help_cap,
|
||||
&checkbox_overridebatt,
|
||||
&checkbox_battery_charge_hint});
|
||||
|
||||
@@ -1088,21 +1171,44 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
|
||||
pmem::set_ui_override_batt_calc(checkbox_overridebatt.value());
|
||||
pmem::set_ui_battery_charge_hint(checkbox_battery_charge_hint.value());
|
||||
battery::BatteryManagement::set_calc_override(checkbox_overridebatt.value());
|
||||
if (((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) {
|
||||
pmem::set_battery_cap_mah(field_battcap.value());
|
||||
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
|
||||
if (dev && !dev->reInit()) {
|
||||
nav.display_modal("Error", "Battery gauge re-init failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
send_system_refresh();
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_reset.on_select = [&nav, this](Button&) {
|
||||
auto dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
|
||||
if (dev->reset_learned())
|
||||
if (dev && dev->reset_learned())
|
||||
nav.display_modal("Reset", "Battery parameters reset");
|
||||
else
|
||||
nav.display_modal("Error", "Error parameter reset");
|
||||
};
|
||||
|
||||
button_help_cap.on_select = [&nav, this](Button&) {
|
||||
nav.display_modal("Battery Capacity",
|
||||
"Only change default, if you\n"
|
||||
" changed the battery!\n"
|
||||
"Defaults:\n"
|
||||
"H4 + Hackrf One: 2500\n"
|
||||
"H4 + Hackrf Pro: 2000\n"
|
||||
"H4Pro + Hackrf Pro: custom\n"
|
||||
"PortaRf: 3000\n"
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
checkbox_overridebatt.set_value(pmem::ui_override_batt_calc());
|
||||
checkbox_battery_charge_hint.set_value(pmem::ui_battery_charge_hint());
|
||||
|
||||
field_battcap.set_value(pmem::battery_cap_mah());
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -424,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 {
|
||||
@@ -1020,21 +1045,36 @@ class SetBatteryView : public View {
|
||||
private:
|
||||
int32_t selected = 0;
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Override batt calculation", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 2 * 16}, "method to voltage based", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_CENTER(26), UI_POS_Y(0)}, "Override batt calculation", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_CENTER(24), UI_POS_Y(1)}, "method to voltage based", Theme::getInstance()->fg_light->foreground},
|
||||
/**/
|
||||
{{1 * 8, 6 * 16}, "Display a hint to remind you", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 7 * 16}, "when you charge", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X_CENTER(29), UI_POS_Y(4)}, "Display a hint to remind you", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_CENTER(16), UI_POS_Y(5)}, "when you charge", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_CENTER(17), UI_POS_Y(8)}, "Battery capacity", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(7), UI_POS_Y(9)}, "mAh", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Labels labels2{{{1 * 8, 11 * 16}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
|
||||
Labels labels2{{{UI_POS_X(1), UI_POS_Y(11)}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_battcap{
|
||||
{UI_POS_X(1), UI_POS_Y(9)},
|
||||
5,
|
||||
{BATT_18650_MIN_MAH, BATT_18650_MAX_MAH},
|
||||
100,
|
||||
' ',
|
||||
};
|
||||
|
||||
Button button_help_cap{
|
||||
{UI_POS_X(12), UI_POS_Y(9), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)},
|
||||
"Help",
|
||||
};
|
||||
|
||||
Checkbox checkbox_overridebatt{
|
||||
{2 * 8, 4 * 16},
|
||||
{UI_POS_X(2), UI_POS_Y(2)},
|
||||
23,
|
||||
"Override"};
|
||||
|
||||
Checkbox checkbox_battery_charge_hint{
|
||||
{2 * 8, 9 * 16},
|
||||
{UI_POS_X(2), UI_POS_Y(6)},
|
||||
23,
|
||||
"Charge hint"};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include <cstring>
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "tonesets.hpp"
|
||||
@@ -334,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);
|
||||
@@ -382,6 +393,11 @@ void set_moreserx_config(uint8_t mode) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_tonedetect_config(uint8_t squelch, uint32_t ctcss_freq_x10) {
|
||||
const ToneDetectConfigureMessage message{squelch, ctcss_freq_x10};
|
||||
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);
|
||||
@@ -412,6 +428,19 @@ 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) {
|
||||
|
||||
@@ -97,12 +97,18 @@ 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_tonedetect_config(uint8_t squelch, uint32_t ctcss_freq_x10 = 0);
|
||||
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);
|
||||
@@ -111,6 +117,8 @@ 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();
|
||||
|
||||
@@ -35,12 +35,16 @@ using namespace lpc43xx;
|
||||
extern "C" {
|
||||
#include "fpga_bridge.h"
|
||||
}
|
||||
// Need access to ssp1_arbiter from radio namespace
|
||||
// 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,
|
||||
@@ -62,8 +66,44 @@ constexpr si5351::PLL si5351_pll_xtal_25m{
|
||||
.b = 0,
|
||||
.c = 1,
|
||||
};
|
||||
constexpr auto si5351_pll_a_xtal_reg = si5351_pll_xtal_25m.reg(0);
|
||||
|
||||
#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");
|
||||
@@ -79,13 +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);
|
||||
|
||||
#ifndef PRALINE
|
||||
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
|
||||
|
||||
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");
|
||||
@@ -99,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,
|
||||
@@ -116,6 +149,73 @@ 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
|
||||
@@ -219,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},
|
||||
@@ -235,24 +344,26 @@ 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)
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK1: SCT_CLK - iCE40 FPGA timing clock
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK2: LPC43xx MCU
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK3: CLKOUT (optional) SMA Port P1
|
||||
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK4: PRALINE MAX2831 reference (40 MHz) - INVERTED per hackrf_usb, 4mA, Integer mode
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK5: PRALINE RFFC5072 reference (40 MHz) - INVERTED, 6mA, Integer mode
|
||||
// This matches HackRF One OG configuration for RFFC5072
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK6: SMA Port P2
|
||||
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// 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},
|
||||
@@ -264,10 +375,9 @@ constexpr ClockControls si5351a_clock_control_common{{
|
||||
{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},
|
||||
#endif
|
||||
|
||||
// CLK7: Not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
#endif
|
||||
|
||||
}};
|
||||
|
||||
@@ -351,7 +461,7 @@ void ClockManager::init_clock_generator() {
|
||||
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};
|
||||
reference = Reference{ReferenceSource::Xtal, 0}; // PLLA
|
||||
|
||||
/* Clock control will be set AFTER multisynth configuration - see below */
|
||||
#else
|
||||
@@ -398,58 +508,86 @@ void ClockManager::init_clock_generator() {
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE uses Si5351A with:
|
||||
* CLK0 = AFE_CLK (codec/FPGA sample clock)
|
||||
* CLK1 = SCT_CLK (FPGA timing clock at 2x sample rate)
|
||||
* CLK4 = second IF (MAX2831)
|
||||
* CLK5 = first IF (RFFC5072)
|
||||
* Uses PLLA on XTAL only (no CLKIN support).
|
||||
|
||||
/* * 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.
|
||||
*/
|
||||
|
||||
/* Step 1: Write PLL A configuration (800 MHz VCO from 25 MHz XTAL) */
|
||||
/* Use single-byte writes to debug I2C issues */
|
||||
/* 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
|
||||
*/
|
||||
{
|
||||
const auto& pll_regs = si5351_pll_a_xtal_reg;
|
||||
const uint8_t base_reg = pll_regs[0];
|
||||
for (size_t i = 1; i < pll_regs.size(); i++) {
|
||||
clock_generator.write_register(base_reg + i - 1, pll_regs[i]);
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 2: Write multisynth configurations using single-byte writes */
|
||||
clock_generator.write_ms_single_byte(0, si5351_ms_0_8m); // MS0 = divider 50, r_div=1 for 8 MHz
|
||||
clock_generator.write_ms_single_byte(1, si5351_ms_16m); // MS1 = divider 50, r_div=0 for 16 MHz
|
||||
/* 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)
|
||||
|
||||
/* CLK4 and CLK5 - use single-byte writes too */
|
||||
{
|
||||
const auto& ms4_regs = si5351c_ms_4_reg;
|
||||
const uint8_t base_reg = ms4_regs[0];
|
||||
for (size_t i = 1; i < ms4_regs.size(); i++) {
|
||||
clock_generator.write_register(base_reg + i - 1, ms4_regs[i]);
|
||||
}
|
||||
}
|
||||
{
|
||||
const auto& ms5_regs = si5351c_ms_5_reg;
|
||||
const uint8_t base_reg = ms5_regs[0];
|
||||
for (size_t i = 1; i < ms5_regs.size(); i++) {
|
||||
clock_generator.write_register(base_reg + i - 1, ms5_regs[i]);
|
||||
}
|
||||
}
|
||||
clock_generator.write(si5351a_ms6_7_off_reg); // MS6/7 off - short write is OK
|
||||
|
||||
/* Step 3: NOW set clock control registers (AFTER multisynths per HackRF reference) */
|
||||
const auto ref_pll = ClockControl::MultiSynthSource::PLLA;
|
||||
/* 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),
|
||||
si5351a_clock_control_common[1].ms_src(ref_pll),
|
||||
si5351a_clock_control_common[2].ms_src(ref_pll),
|
||||
si5351a_clock_control_common[3].ms_src(ref_pll),
|
||||
si5351a_clock_control_common[4].ms_src(ref_pll),
|
||||
si5351a_clock_control_common[5].ms_src(ref_pll),
|
||||
si5351a_clock_control_common[6].ms_src(ref_pll),
|
||||
si5351a_clock_control_common[7].ms_src(ref_pll),
|
||||
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);
|
||||
// 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)
|
||||
@@ -477,8 +615,8 @@ void ClockManager::init_clock_generator() {
|
||||
|
||||
// Wait for PLL(s) to lock.
|
||||
#ifdef PRALINE
|
||||
// PRALINE: Wait for PLLA to lock (0x20 = LOL_A bit)
|
||||
uint8_t device_status_mask = 0x20;
|
||||
// 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--;
|
||||
@@ -487,23 +625,6 @@ void ClockManager::init_clock_generator() {
|
||||
static volatile uint32_t pll_lock_timeout = pll_timeout;
|
||||
(void)pll_lock_timeout;
|
||||
|
||||
// CRITICAL: Add delay to ensure Si5351 writes complete before I2C bus stops
|
||||
chThdSleepMilliseconds(100);
|
||||
|
||||
// ===== ADD THIS SAFETY BLOCK =====
|
||||
// Pre-configure CLK4/CLK5 with correct settings including inversion.
|
||||
// This ensures the registers have correct values even if enable_if_clocks()
|
||||
// is not called or fails. The clocks will still be powered off until
|
||||
// enable_if_clocks() enables the outputs.
|
||||
|
||||
// CLK5: OFF (for now), Integer, PLLA, INVERTED, MS_Self, 6mA = 0xDE
|
||||
// (Same as 0x5E but with bit 7 set for power off)
|
||||
clock_generator.write_register(21, 0xDE);
|
||||
|
||||
// CLK4: OFF (for now), Integer, PLLA, INVERTED, MS_Self, 4mA = 0xDD
|
||||
clock_generator.write_register(20, 0xDD);
|
||||
// ===== END SAFETY BLOCK =====
|
||||
|
||||
#else
|
||||
// Wait for PLL(s) to lock - with timeout to prevent hang
|
||||
uint8_t device_status_mask = hackrf_r9
|
||||
@@ -529,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() {
|
||||
@@ -557,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;
|
||||
@@ -575,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};
|
||||
@@ -642,27 +779,6 @@ void ClockManager::disable_codec_clocks() {
|
||||
|
||||
void ClockManager::enable_if_clocks() {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: CLK4=MAX2831, CLK5=RFFC5072
|
||||
*
|
||||
* Force-write complete configuration to guarantee correct setup.
|
||||
* Added force-write to verify correct registers are applying necessary register clock settings.
|
||||
*/
|
||||
|
||||
// Configure and enable CLK4 (MAX2831)
|
||||
// Register 20: ON, Integer, PLLA, INVERTED, MS_Self, 4mA = 0x5D
|
||||
clock_generator.write_register(20, 0x5D);
|
||||
|
||||
// Configure and enable CLK5 (RFFC5072) - CRITICAL!
|
||||
// Register 21: ON, Integer, PLLA, INVERTED, MS_Self, 6mA = 0x5E
|
||||
clock_generator.write_register(21, 0x5E);
|
||||
|
||||
// Enable outputs (register 3, bits 4 and 5 = 0)
|
||||
uint8_t reg3 = clock_generator.read_register(3);
|
||||
reg3 &= ~0x30;
|
||||
clock_generator.write_register(3, reg3);
|
||||
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
/* 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);
|
||||
@@ -702,23 +818,75 @@ void ClockManager::disable_if_clocks() {
|
||||
|
||||
void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: CLK0=AFE_CLK runs at sample rate (VCO/divider/2)
|
||||
* CLK1=SCT_CLK runs at 2x sample rate (VCO/divider/1)
|
||||
* Reference: hackrf_core.c sample_rate_frac_set() lines 580-582
|
||||
/*
|
||||
* 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)
|
||||
*/
|
||||
|
||||
/* PRALINE: Match HackRF USB sample_rate_frac_set()
|
||||
* Reference: hackrf_usb radio.c lines 29-91, hackrf_core.c lines 501-685 */
|
||||
constexpr uint32_t MAX_AFE_RATE = 40000000; // Use 40 MHz per GSG reference
|
||||
constexpr uint8_t MAX_N = 5;
|
||||
|
||||
_base_band_frequency = frequency;
|
||||
|
||||
// Set FPGA decimation to 0 (no decimation) for direct passthrough
|
||||
fpga_debug_register_write(2, 0x00);
|
||||
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();
|
||||
|
||||
// The following was originally from @kitty. Adopting for testing radio.
|
||||
clock_generator.set_ms_frequency(0, frequency * 2, si5351_vco_f, 1); // CLK0: r_div=1 (÷2)
|
||||
clock_generator.set_ms_frequency(1, frequency * 2, si5351_vco_f, 0); // CLK1: r_div=0 (÷1)
|
||||
// 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
|
||||
@@ -739,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));
|
||||
@@ -754,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) {
|
||||
@@ -770,15 +954,15 @@ 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?!
|
||||
#ifndef PRALINE
|
||||
while (LPC_CGU->FREQ_MON.MEAS == 1);
|
||||
#else
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -791,33 +975,89 @@ uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() {
|
||||
|
||||
void ClockManager::start_audio_pll() {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: Use 12MHz XTAL for audio PLL
|
||||
* For 12MHz XTAL input, 48kHz audio rate, 256Fs MCLK:
|
||||
* Fout=12.288MHz, Fcco=491.52MHz
|
||||
* 12MHz * 1024 / 25 = 491.52MHz
|
||||
* MSEL=1024, NSEL=25, PSEL=20
|
||||
*/
|
||||
|
||||
// Control Block
|
||||
cgu::pll0audio::ctrl({
|
||||
.pd = 1,
|
||||
.bypass = 0,
|
||||
.directi = 0,
|
||||
.directo = 0,
|
||||
.clken = 0,
|
||||
.frm = 0,
|
||||
.autoblock = 1,
|
||||
.pllfract_req = 0,
|
||||
.sel_ext = 1,
|
||||
.mod_pd = 1,
|
||||
.clk_sel = cgu::CLK_SEL::XTAL,
|
||||
.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,
|
||||
});
|
||||
|
||||
cgu::pll0audio::mdiv({
|
||||
.mdec = 22625UL, // MDEC for MSEL=1024
|
||||
});
|
||||
/*
|
||||
* 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
|
||||
.ndec = 69, // NDEC for NSEL=25
|
||||
.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,
|
||||
@@ -847,30 +1087,21 @@ void ClockManager::start_audio_pll() {
|
||||
.pdec = 31,
|
||||
.ndec = 45,
|
||||
});
|
||||
#endif
|
||||
|
||||
cgu::pll0audio::frac({
|
||||
.pllfract_ctrl = 0,
|
||||
});
|
||||
|
||||
cgu::pll0audio::power_up();
|
||||
#ifndef PRALINE
|
||||
while (!cgu::pll0audio::is_locked());
|
||||
#else
|
||||
// PRALINE FIX: Add timeout to prevent infinite hang if GP_CLKIN not present
|
||||
{
|
||||
uint32_t timeout = 100000;
|
||||
while (!cgu::pll0audio::is_locked() && timeout > 0) {
|
||||
timeout--;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
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) {
|
||||
@@ -879,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);
|
||||
@@ -913,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
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ class ClockManager {
|
||||
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); }
|
||||
@@ -112,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__*/
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+177
-46
@@ -26,6 +26,7 @@
|
||||
#include "ui_freqman.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
@@ -35,36 +36,112 @@ using portapack::memory::map::backup_ram;
|
||||
|
||||
namespace ui::external_app::detector_rx {
|
||||
|
||||
// Function to map the value from one range to another
|
||||
int32_t DetectorRxView::map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh) {
|
||||
return toLow + (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow);
|
||||
}
|
||||
|
||||
void DetectorRxView::focus() {
|
||||
field_lna.focus();
|
||||
button_index.focus();
|
||||
}
|
||||
|
||||
DetectorRxView::~DetectorRxView() {
|
||||
// reset performance counters request to default
|
||||
shared_memory.request_m4_performance_counter = 0;
|
||||
receiver_model.disable();
|
||||
audio::output::stop();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void DetectorRxView::on_timer() {
|
||||
freq_index++;
|
||||
if (freq_mode == 0 && freq_index >= tetra_uplink_monitoring_frequencies_hz.size()) freq_index = 0; // TETRA UP
|
||||
if (freq_mode == 1 && freq_index >= lora_monitoring_frequencies_hz.size()) freq_index = 0; // Lora
|
||||
if (freq_mode == 2 && freq_index >= remotes_monitoring_frequencies_hz.size()) freq_index = 0; // Remotes
|
||||
std::string DetectorRxView::format_freq_mhz(int64_t freq_hz) {
|
||||
int64_t mhz = freq_hz / 1000000;
|
||||
int64_t khz_frac = (freq_hz % 1000000) / 1000;
|
||||
return "< " + to_string_dec_uint(mhz) + "." + to_string_dec_uint(khz_frac, 3, '0') + " MHz >";
|
||||
}
|
||||
|
||||
if (freq_mode == 2)
|
||||
freq_ = remotes_monitoring_frequencies_hz[freq_index];
|
||||
else if (freq_mode == 1)
|
||||
freq_ = lora_monitoring_frequencies_hz[freq_index];
|
||||
else
|
||||
freq_ = tetra_uplink_monitoring_frequencies_hz[freq_index];
|
||||
receiver_model.set_target_frequency(freq_);
|
||||
void DetectorRxView::load_freqman() {
|
||||
freqman_load_options options{};
|
||||
options.load_freqs = true;
|
||||
options.load_ranges = true;
|
||||
options.load_hamradios = false;
|
||||
options.load_repeaters = false;
|
||||
|
||||
if (!load_freqman_file(freq_file_stem, frequency_list, options) || frequency_list.empty()) {
|
||||
button_file.set_text("No file!");
|
||||
button_index.set_text("");
|
||||
text_entry_desc.set("");
|
||||
button_freq.set_text("");
|
||||
frequency_list.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
current_index = 0;
|
||||
init_current_entry();
|
||||
button_file.set_text(freq_file_stem);
|
||||
}
|
||||
|
||||
void DetectorRxView::init_current_entry() {
|
||||
if (frequency_list.empty()) return;
|
||||
|
||||
auto& entry = *frequency_list[current_index];
|
||||
if (entry.type == freqman_type::Range) {
|
||||
minfreq = entry.frequency_a;
|
||||
maxfreq = entry.frequency_b;
|
||||
current_freq = minfreq;
|
||||
current_step = is_valid(entry.step) ? freqman_entry_get_step_value(entry.step) : DETECTOR_BW;
|
||||
} else {
|
||||
current_freq = entry.frequency_a;
|
||||
minfreq = current_freq;
|
||||
maxfreq = current_freq;
|
||||
current_step = DETECTOR_BW;
|
||||
}
|
||||
|
||||
update_entry_display();
|
||||
update_freq_display();
|
||||
receiver_model.set_target_frequency(current_freq);
|
||||
}
|
||||
|
||||
void DetectorRxView::update_entry_display() {
|
||||
if (frequency_list.empty()) return;
|
||||
|
||||
auto& entry = *frequency_list[current_index];
|
||||
button_index.set_text(
|
||||
to_string_dec_uint(current_index + 1) + "/" +
|
||||
to_string_dec_uint(frequency_list.size()));
|
||||
text_entry_desc.set(entry.description);
|
||||
}
|
||||
|
||||
void DetectorRxView::update_freq_display() {
|
||||
if (auto_scan && (minfreq != maxfreq)) {
|
||||
if (last_update_was_auto_range_type != 1) {
|
||||
button_freq.set_text("RANGE SCAN...");
|
||||
last_update_was_auto_range_type = 1;
|
||||
}
|
||||
} else {
|
||||
button_freq.set_text(format_freq_mhz(current_freq));
|
||||
last_update_was_auto_range_type = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void DetectorRxView::on_timer() {
|
||||
if (frequency_list.empty() || !auto_scan) return;
|
||||
|
||||
auto& entry = *frequency_list[current_index];
|
||||
if (entry.type == freqman_type::Range) {
|
||||
current_freq += current_step;
|
||||
if (current_freq > maxfreq) {
|
||||
if (auto_advance) {
|
||||
current_index = (current_index + 1) % frequency_list.size();
|
||||
init_current_entry();
|
||||
return;
|
||||
}
|
||||
current_freq = minfreq;
|
||||
}
|
||||
receiver_model.set_target_frequency(current_freq);
|
||||
update_freq_display();
|
||||
} else if (auto_advance) {
|
||||
// Single frequency: advance to next entry
|
||||
current_index = (current_index + 1) % frequency_list.size();
|
||||
init_current_entry();
|
||||
}
|
||||
}
|
||||
|
||||
DetectorRxView::DetectorRxView(NavigationView& nav)
|
||||
@@ -75,21 +152,22 @@ DetectorRxView::DetectorRxView(NavigationView& nav)
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&field_volume,
|
||||
&text_frequency,
|
||||
&freq_stats_rssi,
|
||||
&freq_stats_db,
|
||||
&button_file,
|
||||
&button_index,
|
||||
&text_entry_desc,
|
||||
&text_beep_squelch,
|
||||
&field_beep_squelch,
|
||||
&freq_stats_db,
|
||||
&freq_stats_rssi,
|
||||
&button_freq,
|
||||
&button_auto_scan,
|
||||
&button_auto_advance,
|
||||
&rssi,
|
||||
&rssi_graph,
|
||||
&field_mode,
|
||||
});
|
||||
|
||||
// activate vertical bar mode
|
||||
rssi.set_vertical_rssi(true);
|
||||
|
||||
freq_ = receiver_model.target_frequency();
|
||||
|
||||
field_beep_squelch.set_value(beep_squelch);
|
||||
field_beep_squelch.on_change = [this](int32_t v) {
|
||||
beep_squelch = v;
|
||||
@@ -97,29 +175,85 @@ DetectorRxView::DetectorRxView(NavigationView& nav)
|
||||
|
||||
rssi_graph.set_nb_columns(256);
|
||||
|
||||
// FILE button opens file picker
|
||||
button_file.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
if (new_file_path.native().find((u"/" / freqman_dir).native()) != 0) {
|
||||
button_file.set_text("Invalid file");
|
||||
return;
|
||||
}
|
||||
freq_file_stem = new_file_path.stem().string();
|
||||
load_freqman();
|
||||
};
|
||||
};
|
||||
|
||||
// Index encoder: rotate to change entry, click does nothing
|
||||
button_index.on_change = [this]() {
|
||||
if (frequency_list.empty()) return;
|
||||
int32_t delta = button_index.get_encoder_delta();
|
||||
button_index.set_encoder_delta(0);
|
||||
if (delta == 0) return;
|
||||
|
||||
// Use signed arithmetic for index wraparound to avoid unsigned promotion issues.
|
||||
const int32_t list_size = static_cast<int32_t>(frequency_list.size());
|
||||
int32_t idx = static_cast<int32_t>(current_index);
|
||||
if (delta > 0) {
|
||||
idx = (idx + 1) % list_size;
|
||||
} else {
|
||||
idx = (idx - 1 + list_size) % list_size;
|
||||
}
|
||||
current_index = static_cast<size_t>(idx);
|
||||
init_current_entry();
|
||||
};
|
||||
|
||||
// Frequency encoder: rotate to step within range
|
||||
button_freq.on_change = [this]() {
|
||||
if (frequency_list.empty()) return;
|
||||
auto& entry = *frequency_list[current_index];
|
||||
if (entry.type != freqman_type::Range) {
|
||||
button_freq.set_encoder_delta(0);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t delta = button_freq.get_encoder_delta();
|
||||
button_freq.set_encoder_delta(0);
|
||||
if (delta == 0) return;
|
||||
|
||||
if (delta > 0) {
|
||||
current_freq += current_step;
|
||||
if (current_freq > maxfreq) current_freq = minfreq;
|
||||
} else {
|
||||
current_freq -= current_step;
|
||||
if (current_freq < minfreq) current_freq = maxfreq;
|
||||
}
|
||||
receiver_model.set_target_frequency(current_freq);
|
||||
update_freq_display();
|
||||
};
|
||||
|
||||
// Auto-scan toggle
|
||||
button_auto_scan.on_select = [this](Button&) {
|
||||
auto_scan = !auto_scan;
|
||||
button_auto_scan.set_text(auto_scan ? "AUTOSCAN" : "NO SCAN");
|
||||
update_freq_display();
|
||||
};
|
||||
button_auto_scan.set_text(auto_scan ? "AUTOSCAN" : "NO SCAN");
|
||||
|
||||
// Auto-advance toggle
|
||||
button_auto_advance.on_select = [this](Button&) {
|
||||
auto_advance = !auto_advance;
|
||||
button_auto_advance.set_text(auto_advance ? "AUTOADV" : "NO ADV");
|
||||
};
|
||||
button_auto_advance.set_text(auto_advance ? "AUTOADV" : "NO ADV");
|
||||
|
||||
change_mode();
|
||||
rssi.set_peak(true, 3000);
|
||||
|
||||
// FILL STEP OPTIONS
|
||||
freq_stats_rssi.set_style(Theme::getInstance()->bg_darkest);
|
||||
freq_stats_db.set_style(Theme::getInstance()->bg_darkest);
|
||||
|
||||
field_mode.on_change = [this](size_t, int32_t value) {
|
||||
freq_mode = value;
|
||||
freq_index = 0;
|
||||
switch (value) {
|
||||
case 1: // Lora
|
||||
text_frequency.set(" 433, 868, 915 Mhz");
|
||||
break;
|
||||
case 2: // Remotes
|
||||
text_frequency.set(" 433, 315 Mhz");
|
||||
break;
|
||||
default:
|
||||
case 0: // TETRA UP
|
||||
text_frequency.set(" 380-390 Mhz");
|
||||
break;
|
||||
}
|
||||
};
|
||||
load_freqman();
|
||||
}
|
||||
|
||||
void DetectorRxView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
@@ -130,25 +264,23 @@ void DetectorRxView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
|
||||
rssi_graph.add_values(rssi.get_min(), rssi.get_avg(), rssi.get_max(), statistics.max_db);
|
||||
|
||||
// refresh db
|
||||
if (last_max_db != statistics.max_db) {
|
||||
last_max_db = statistics.max_db;
|
||||
freq_stats_db.set("Power: " + to_string_dec_int(statistics.max_db) + " db");
|
||||
rssi.set_db(statistics.max_db);
|
||||
}
|
||||
// refresh rssi
|
||||
|
||||
if (last_min_rssi != rssi_graph.get_graph_min() || last_avg_rssi != rssi_graph.get_graph_avg() || last_max_rssi != rssi_graph.get_graph_max()) {
|
||||
last_min_rssi = rssi_graph.get_graph_min();
|
||||
last_avg_rssi = rssi_graph.get_graph_avg();
|
||||
last_max_rssi = rssi_graph.get_graph_max();
|
||||
freq_stats_rssi.set("RSSI: " + to_string_dec_uint(last_min_rssi) + "/" + to_string_dec_uint(last_avg_rssi) + "/" + to_string_dec_uint(last_max_rssi));
|
||||
freq_stats_rssi.set("RSSI:" + to_string_dec_uint(last_min_rssi) + "/" + to_string_dec_uint(last_avg_rssi) + "/" + to_string_dec_uint(last_max_rssi));
|
||||
}
|
||||
|
||||
if (statistics.max_db > beep_squelch) {
|
||||
baseband::request_audio_beep(map(statistics.max_db, -100, 20, 400, 2600), 24000, 150);
|
||||
}
|
||||
|
||||
} /* on_statistic_updates */
|
||||
}
|
||||
|
||||
size_t DetectorRxView::change_mode() {
|
||||
audio::output::stop();
|
||||
@@ -160,14 +292,13 @@ size_t DetectorRxView::change_mode() {
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
|
||||
baseband::set_sample_rate(DETECTOR_BW, get_oversample_rate(DETECTOR_BW));
|
||||
// The radio needs to know the effective sampling rate.
|
||||
auto actual_sampling_rate = get_actual_sample_rate(DETECTOR_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()); // WM8731 hack.
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume());
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
|
||||
+53
-72
@@ -29,6 +29,7 @@
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "radio_state.hpp"
|
||||
@@ -60,20 +61,35 @@ class DetectorRxView : public View {
|
||||
int32_t map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh);
|
||||
size_t change_mode();
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void set_display_freq(int64_t freq);
|
||||
void on_timer();
|
||||
void load_freqman();
|
||||
void init_current_entry();
|
||||
void update_entry_display();
|
||||
void update_freq_display();
|
||||
std::string format_freq_mhz(int64_t freq_hz);
|
||||
|
||||
freqman_db frequency_list{};
|
||||
size_t current_index{0};
|
||||
int64_t current_freq{0};
|
||||
int64_t minfreq{0};
|
||||
int64_t maxfreq{0};
|
||||
int32_t current_step{DETECTOR_BW};
|
||||
std::string freq_file_stem{"DETECTOR"};
|
||||
bool auto_scan{true};
|
||||
bool auto_advance{false};
|
||||
uint8_t last_update_was_auto_range_type = -1;
|
||||
|
||||
uint8_t freq_index = 0;
|
||||
rf::Frequency freq_ = {433920000};
|
||||
int32_t beep_squelch = 0;
|
||||
audio::Rate audio_sampling_rate = audio::Rate::Hz_48000;
|
||||
uint8_t freq_mode = 0;
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_detector",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"beep_squelch"sv, &beep_squelch},
|
||||
{"freq_file"sv, &freq_file_stem},
|
||||
{"auto_scan"sv, &auto_scan},
|
||||
{"auto_advance"sv, &auto_advance},
|
||||
}};
|
||||
|
||||
Labels labels{
|
||||
@@ -93,41 +109,55 @@ class DetectorRxView : public View {
|
||||
AudioVolumeField field_volume{
|
||||
{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
OptionsField field_mode{
|
||||
{UI_POS_X(0), UI_POS_Y(1)},
|
||||
9,
|
||||
{
|
||||
{"TETRA UP", 0},
|
||||
{"Lora", 1},
|
||||
{"Remotes", 2},
|
||||
}};
|
||||
// Row 1: filename + auto advance
|
||||
Button button_file{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_frequency{
|
||||
{UI_POS_X_RIGHT(20), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT},
|
||||
Button button_auto_advance{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(1), UI_POS_WIDTH(9), UI_POS_DEFAULT_HEIGHT},
|
||||
"NO ADV"};
|
||||
|
||||
// Row 2: index encoder + description + auto scan
|
||||
ButtonWithEncoder button_index{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(4), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_entry_desc{
|
||||
{UI_POS_X(4), UI_POS_Y(2), UI_POS_WIDTH(17), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Button button_auto_scan{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(2), UI_POS_WIDTH(9), UI_POS_DEFAULT_HEIGHT},
|
||||
"AUTO SCAN"};
|
||||
|
||||
// Row 3: frequency encoder + bip level
|
||||
ButtonWithEncoder button_freq{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_beep_squelch{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(2), UI_POS_WIDTH(4), UI_POS_DEFAULT_HEIGHT},
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(4), UI_POS_DEFAULT_HEIGHT},
|
||||
"Bip>"};
|
||||
|
||||
NumberField field_beep_squelch{
|
||||
{UI_POS_X_RIGHT(5), UI_POS_Y(2)},
|
||||
{UI_POS_X_RIGHT(5), UI_POS_Y(3)},
|
||||
4,
|
||||
{-100, 20},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
// RSSI: XX/XX/XXX
|
||||
Text freq_stats_rssi{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT},
|
||||
};
|
||||
|
||||
// Power: -XXX db
|
||||
// Row 4: Power + RSSI on same line
|
||||
Text freq_stats_db{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT},
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT},
|
||||
};
|
||||
|
||||
Text freq_stats_rssi{
|
||||
{UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT},
|
||||
};
|
||||
|
||||
// Row 5+: RSSI graph + vertical bar
|
||||
RSSIGraph rssi_graph{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(5), UI_POS_HEIGHT_REMAINING(6)},
|
||||
};
|
||||
@@ -147,55 +177,6 @@ class DetectorRxView : public View {
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
|
||||
const std::vector<uint32_t> remotes_monitoring_frequencies_hz = {
|
||||
// Around 315 MHz (common for older remotes, key fobs in some regions)
|
||||
// Window centered on 315 MHz, covers 314.625 - 315.375 MHz
|
||||
315000000,
|
||||
|
||||
// Around 433.92 MHz (very common for remotes, sensors, key fobs globally)
|
||||
// Window centered on 433.92 MHz, covers 433.545 - 434.295 MHz
|
||||
433920000,
|
||||
};
|
||||
const std::vector<uint32_t> lora_monitoring_frequencies_hz = {
|
||||
// EU433 Band (Europe, typically 433.05 MHz to 434.79 MHz)
|
||||
// Scanning the approximate range 433.0 MHz to 434.8 MHz with 750kHz steps
|
||||
433375000, // Covers 433.000 - 433.750 MHz
|
||||
434125000, // Covers 433.750 - 434.500 MHz (includes 433.92 MHz)
|
||||
434875000, // Covers 434.500 - 435.250 MHz (covers up to 434.79 MHz)
|
||||
|
||||
// EU868 Band (Europe, typically 863 MHz to 870 MHz, specific channels around 868 MHz)
|
||||
// Targeting common LoRaWAN channel groups (approx 867.0 - 868.6 MHz) with 750kHz steps
|
||||
867375000, // Covers 867.000 - 867.750 MHz
|
||||
868125000, // Covers 867.750 - 868.500 MHz
|
||||
868875000, // Covers 868.500 - 869.250 MHz (covers up to 868.6 MHz)
|
||||
|
||||
// US915 Band (North America, typically 902 MHz to 928 MHz, specific channels around 915 MHz)
|
||||
// Providing a few sample windows around the 915 MHz area with 750kHz steps.
|
||||
// This band is wide; a full scan would require many more frequencies.
|
||||
914250000, // Covers 913.875 - 914.625 MHz
|
||||
915000000, // Covers 914.625 - 915.375 MHz (Centered on 915 MHz)
|
||||
915750000, // Covers 915.375 - 916.125 MHz
|
||||
};
|
||||
const std::vector<uint32_t> tetra_uplink_monitoring_frequencies_hz = {
|
||||
// Band starts at 380,000,000 Hz, ends at 390,000,000 Hz.
|
||||
// First center: 380,000,000 + 375,000 = 380,375,000 Hz
|
||||
// Last center: 380,375,000 + 13 * 750,000 = 390,125,000 Hz (14 frequencies total for this band)
|
||||
380375000, // Covers 380.000 - 380.750 MHz
|
||||
381125000, // Covers 380.750 - 381.500 MHz
|
||||
381875000, // Covers 381.500 - 382.250 MHz
|
||||
382625000, // Covers 382.250 - 383.000 MHz
|
||||
383375000, // Covers 383.000 - 383.750 MHz
|
||||
384125000, // Covers 383.750 - 384.500 MHz
|
||||
384875000, // Covers 384.500 - 385.250 MHz
|
||||
385625000, // Covers 385.250 - 386.000 MHz
|
||||
386375000, // Covers 386.000 - 386.750 MHz
|
||||
387125000, // Covers 386.750 - 387.500 MHz
|
||||
387875000, // Covers 387.500 - 388.250 MHz
|
||||
388625000, // Covers 388.250 - 389.000 MHz
|
||||
389375000, // Covers 389.000 - 389.750 MHz
|
||||
390125000, // Covers 389.750 - 390.500 MHz
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::detector_rx
|
||||
|
||||
+1
-1
@@ -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},
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+356
@@ -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__*/
|
||||
+268
@@ -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 <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
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 <xxx°yy'zz"N>)
|
||||
*/
|
||||
std::string to_latitude_string(const Location& location) {
|
||||
char buffer[16];
|
||||
// Format : <xxx°yy'zz"N>
|
||||
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 <xxx°yy'zz"W>)
|
||||
*/
|
||||
std::string to_longitude_string(const Location& location) {
|
||||
char buffer[16];
|
||||
// Format : <xxx°yy'zz"W>
|
||||
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__*/
|
||||
+82
@@ -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<EPIRBTXAppView>();
|
||||
}
|
||||
} // 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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* 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 <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
#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_bpsk_frequency() {
|
||||
bool was_transmitting = false;
|
||||
if (transmitting && (am_enabled || transmitting_bpsk)) {
|
||||
// We need to stop transmission before changing frequency
|
||||
transmitter_model.disable();
|
||||
was_transmitting = true;
|
||||
}
|
||||
transmitter_model.set_target_frequency(bpsk_frequency);
|
||||
// Update displayed frequency
|
||||
tx_view.on_show();
|
||||
if (was_transmitting) {
|
||||
// Start over
|
||||
start_tx();
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::update_am_transmission() {
|
||||
if (am_enabled && transmitting && !transmitting_bpsk) {
|
||||
// Start am transmission
|
||||
// Restore am frequency
|
||||
epirb_tx_message.mode_bpsk = false;
|
||||
transmitter_model.set_target_frequency(am_frequency);
|
||||
// Send config to baseband
|
||||
baseband::set_epirb_tx_config(epirb_tx_message);
|
||||
// Start transmitting
|
||||
transmitter_model.enable();
|
||||
} else if (transmitting && !transmitting_bpsk) {
|
||||
// Stop am transmission
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(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<size_t>((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) {
|
||||
// Previously in AM mode => restore bpsk frequency
|
||||
transmitter_model.set_target_frequency(bpsk_frequency);
|
||||
// Update displayed frequency
|
||||
tx_view.on_show();
|
||||
}
|
||||
// Set mode to bpsk
|
||||
epirb_tx_message.mode_bpsk = true;
|
||||
transmitting_bpsk = true;
|
||||
// Set pre/post count
|
||||
epirb_tx_message.pre_count = (160 * TONES_SAMPLERATE) / 1000; // 160 ms carrier (COSPAS spec.)
|
||||
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) {
|
||||
transmitting_bpsk = false;
|
||||
if (am_enabled) {
|
||||
// BPSK frame sent, switch back to 121.5 AM signal
|
||||
epirb_tx_message.mode_bpsk = false;
|
||||
// Start am transmission
|
||||
update_am_transmission();
|
||||
} 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,
|
||||
&options_am_channel,
|
||||
&options_bpsk_channel,
|
||||
&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);
|
||||
options_am_channel.set_by_value(am_channel);
|
||||
options_am_channel.set_style((am_channel == (uint8_t)AmChannel::REAL) ? Theme::getInstance()->fg_red : Theme::getInstance()->bg_darkest);
|
||||
manual_am_frequency = am_frequency;
|
||||
options_bpsk_channel.set_by_value(bpsk_channel);
|
||||
manual_bpsk_frequency = bpsk_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, OptionsField::value_t value) {
|
||||
mode_file = (((BeaconMode)value) == BeaconMode::FILE);
|
||||
update_mode();
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_beacon_type.on_change = [this](size_t, OptionsField::value_t value) {
|
||||
beacon_params.type = (BeaconType)value;
|
||||
beacon_type = value;
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_beacon_protocol.on_change = [this](size_t, OptionsField::value_t value) {
|
||||
beacon_params.protocol = (BeaconProtocol)value;
|
||||
beacon_protocol = value;
|
||||
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();
|
||||
};
|
||||
|
||||
options_am_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
bool is_real = false;
|
||||
switch ((AmChannel)v) {
|
||||
case AmChannel::REAL:
|
||||
is_real = true;
|
||||
am_frequency = AM_REAL_FREQUENCY;
|
||||
break;
|
||||
case AmChannel::MANUAL:
|
||||
am_frequency = manual_am_frequency;
|
||||
break;
|
||||
default:
|
||||
v = (uint8_t)AmChannel::TEST;
|
||||
// fallthrough
|
||||
case AmChannel::TEST:
|
||||
am_frequency = AM_TEST_FREQUENCY;
|
||||
break;
|
||||
}
|
||||
// Actual frequency change will be done by field_am_frequency.on_change()
|
||||
field_am_frequency.set_value(am_frequency);
|
||||
am_channel = v;
|
||||
options_am_channel.set_style(is_real ? Theme::getInstance()->fg_red : Theme::getInstance()->bg_darkest);
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_bpsk_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
switch ((BpskChannel)v) {
|
||||
case BpskChannel::MANUAL:
|
||||
bpsk_frequency = manual_bpsk_frequency;
|
||||
break;
|
||||
case BpskChannel::B:
|
||||
bpsk_frequency = BPSK_FREQUENCY_B;
|
||||
break;
|
||||
case BpskChannel::C:
|
||||
bpsk_frequency = BPSK_FREQUENCY_C;
|
||||
break;
|
||||
case BpskChannel::F:
|
||||
bpsk_frequency = BPSK_FREQUENCY_F;
|
||||
break;
|
||||
case BpskChannel::G:
|
||||
bpsk_frequency = BPSK_FREQUENCY_G;
|
||||
break;
|
||||
case BpskChannel::J:
|
||||
bpsk_frequency = BPSK_FREQUENCY_J;
|
||||
break;
|
||||
case BpskChannel::K:
|
||||
bpsk_frequency = BPSK_FREQUENCY_K;
|
||||
break;
|
||||
case BpskChannel::N:
|
||||
bpsk_frequency = BPSK_FREQUENCY_N;
|
||||
break;
|
||||
case BpskChannel::O:
|
||||
bpsk_frequency = BPSK_FREQUENCY_O;
|
||||
break;
|
||||
default:
|
||||
v = (uint8_t)BpskChannel::HAM;
|
||||
// fallthrough
|
||||
case BpskChannel::HAM:
|
||||
bpsk_frequency = BPSK_FREQUENCY_HAM;
|
||||
break;
|
||||
}
|
||||
bpsk_channel = v;
|
||||
update_bpsk_frequency();
|
||||
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<AlphanumView>(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<GeoMapView>(
|
||||
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 && !transmitting_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<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
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;
|
||||
update_am_transmission();
|
||||
// We update the additional location device data in the frame based on this
|
||||
if (!mode_file) update_frame(false);
|
||||
};
|
||||
|
||||
// AM frequency field edit
|
||||
field_am_frequency.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(field_am_frequency.value());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
switch (f) {
|
||||
case AM_REAL_FREQUENCY:
|
||||
am_channel = (uint8_t)AmChannel::REAL;
|
||||
break;
|
||||
case AM_TEST_FREQUENCY:
|
||||
am_channel = (uint8_t)AmChannel::TEST;
|
||||
break;
|
||||
default:
|
||||
manual_am_frequency = f;
|
||||
am_channel = (uint8_t)AmChannel::MANUAL;
|
||||
break;
|
||||
}
|
||||
// Actual frequency change will be done by options_am_channel.on_change()
|
||||
options_am_channel.set_by_value(am_channel);
|
||||
set_dirty();
|
||||
};
|
||||
};
|
||||
|
||||
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<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
bpsk_frequency = f;
|
||||
switch (f) {
|
||||
case BPSK_FREQUENCY_HAM:
|
||||
bpsk_channel = (uint8_t)BpskChannel::HAM;
|
||||
break;
|
||||
case BPSK_FREQUENCY_B:
|
||||
bpsk_channel = (uint8_t)BpskChannel::B;
|
||||
break;
|
||||
case BPSK_FREQUENCY_C:
|
||||
bpsk_channel = (uint8_t)BpskChannel::C;
|
||||
break;
|
||||
case BPSK_FREQUENCY_F:
|
||||
bpsk_channel = (uint8_t)BpskChannel::F;
|
||||
break;
|
||||
case BPSK_FREQUENCY_G:
|
||||
bpsk_channel = (uint8_t)BpskChannel::G;
|
||||
break;
|
||||
case BPSK_FREQUENCY_J:
|
||||
bpsk_channel = (uint8_t)BpskChannel::J;
|
||||
break;
|
||||
case BPSK_FREQUENCY_K:
|
||||
bpsk_channel = (uint8_t)BpskChannel::K;
|
||||
break;
|
||||
case BPSK_FREQUENCY_N:
|
||||
bpsk_channel = (uint8_t)BpskChannel::N;
|
||||
break;
|
||||
case BPSK_FREQUENCY_O:
|
||||
bpsk_channel = (uint8_t)BpskChannel::O;
|
||||
break;
|
||||
default:
|
||||
bpsk_channel = (uint8_t)BpskChannel::MANUAL;
|
||||
manual_bpsk_frequency = bpsk_frequency;
|
||||
break;
|
||||
}
|
||||
// Actual frequency change will be done by options_bpsk_channel.on_change()
|
||||
options_bpsk_channel.set_by_value(bpsk_channel);
|
||||
set_dirty();
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
#define AM_TEST_FREQUENCY 121375000
|
||||
#define AM_REAL_FREQUENCY 121500000
|
||||
|
||||
#define BPSK_FREQUENCY_HAM 433025000
|
||||
#define BPSK_FREQUENCY_B 406025000
|
||||
#define BPSK_FREQUENCY_C 406028000
|
||||
#define BPSK_FREQUENCY_F 406037000
|
||||
#define BPSK_FREQUENCY_G 406040000
|
||||
#define BPSK_FREQUENCY_J 406049000
|
||||
#define BPSK_FREQUENCY_K 406052000
|
||||
#define BPSK_FREQUENCY_N 406061000
|
||||
#define BPSK_FREQUENCY_O 406064000
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
enum class BeaconMode {
|
||||
FILE = 0,
|
||||
MODE_MANUAL = 1
|
||||
};
|
||||
|
||||
enum class BeaconType {
|
||||
EPIRB = 0,
|
||||
ELT = 1,
|
||||
PLB = 2
|
||||
};
|
||||
|
||||
enum class BeaconProtocol {
|
||||
USER = 0,
|
||||
STANDARD = 1,
|
||||
NATIONAL = 2
|
||||
};
|
||||
|
||||
enum class AmChannel {
|
||||
TEST = 0,
|
||||
REAL = 1,
|
||||
MANUAL = 2
|
||||
};
|
||||
|
||||
enum class BpskChannel {
|
||||
HAM = 0,
|
||||
B = 1,
|
||||
C = 2,
|
||||
F = 3,
|
||||
G = 4,
|
||||
J = 5,
|
||||
K = 6,
|
||||
N = 7,
|
||||
O = 8,
|
||||
MANUAL = 10
|
||||
};
|
||||
|
||||
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_bpsk_frequency();
|
||||
void update_am_transmission();
|
||||
void update_mode();
|
||||
void update_location(bool updateLocatorField = true);
|
||||
|
||||
struct Beacon {
|
||||
std::string title{};
|
||||
std::string description{};
|
||||
std::string frame{};
|
||||
};
|
||||
std::vector<Beacon> 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{AM_TEST_FREQUENCY};
|
||||
// Frequency of the 406 MHz BPSK signal
|
||||
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
// Selected am channel
|
||||
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
||||
// Selected bpsk channel
|
||||
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
||||
// Manual AM frequency value
|
||||
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
||||
// Manual BPSK frequency value
|
||||
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||
|
||||
// True when using a beacon from the BEACONS.TXT file
|
||||
bool mode_file{false};
|
||||
// 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{true};
|
||||
// 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{(uint8_t)BeaconType::EPIRB};
|
||||
// Currently selected beacon protocol
|
||||
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
||||
// 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},
|
||||
{"amchan"sv, &am_channel},
|
||||
{"bpskchan"sv, &bpsk_channel},
|
||||
{"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 transmitting a BPSK frame
|
||||
bool transmitting_bpsk{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(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", 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", (uint8_t)BeaconType::EPIRB},
|
||||
{"ELT", (uint8_t)BeaconType::ELT},
|
||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||
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)", (uint8_t)BeaconMode::FILE},
|
||||
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
||||
|
||||
// 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;
|
||||
OptionsField options_am_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(14)},
|
||||
20,
|
||||
{{"121.375 MHz (Test)", 0},
|
||||
{"121.500 MHz /!\\Real", 1},
|
||||
{"Manual", 2}}};
|
||||
OptionsField options_bpsk_channel{
|
||||
{UI_POS_X(11), UI_POS_Y(15)},
|
||||
20,
|
||||
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
||||
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
||||
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
||||
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
||||
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
||||
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||
|
||||
// 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<const TXProgressMessage*>(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__*/
|
||||
+48
-3
@@ -103,6 +103,14 @@ set(EXTCPPSRC
|
||||
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
|
||||
@@ -256,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
|
||||
@@ -297,8 +309,8 @@ set(EXTCPPSRC
|
||||
external/morseradiotx/main.cpp
|
||||
external/morseradiotx/ui_morse_radiotx.cpp
|
||||
|
||||
external/keeloqtx/main.cpp
|
||||
external/keeloqtx/ui_keeloqtx.cpp
|
||||
external/keeloqtx/main.cpp
|
||||
external/keeloqtx/ui_keeloqtx.cpp
|
||||
#rtty_rx
|
||||
external/rtty_rx/main.cpp
|
||||
external/rtty_rx/ui_rtty_rx.cpp
|
||||
@@ -312,6 +324,30 @@ set(EXTCPPSRC
|
||||
#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
|
||||
|
||||
#two_tone_pager
|
||||
external/two_tone_pager/main.cpp
|
||||
external/two_tone_pager/ui_two_tone_pager.cpp
|
||||
|
||||
#two_tone_rx
|
||||
external/two_tone_rx/main.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -339,6 +375,8 @@ set(EXTAPPLIST
|
||||
adsbtx
|
||||
#morse_tx
|
||||
sstvtx
|
||||
same_tx
|
||||
mdc_tx
|
||||
sstvrx
|
||||
random_password
|
||||
acars_rx
|
||||
@@ -371,11 +409,13 @@ set(EXTAPPLIST
|
||||
gfxeq
|
||||
waterfall_designer
|
||||
detector_rx
|
||||
fpv_detect
|
||||
spaceinv
|
||||
blackjack
|
||||
battleship
|
||||
ert
|
||||
epirb_rx
|
||||
epirb_tx
|
||||
soundboard
|
||||
game2048
|
||||
bht_tx
|
||||
@@ -386,10 +426,15 @@ set(EXTAPPLIST
|
||||
siggen
|
||||
morse_radio
|
||||
morseradiotx
|
||||
keeloqtx
|
||||
keeloqtx
|
||||
rtty_rx
|
||||
rtty_tx
|
||||
pocsag_tx
|
||||
time_sink
|
||||
kiss_tnc
|
||||
p25_tx
|
||||
two_tone_pager
|
||||
two_tone_rx
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
|
||||
+160
-99
@@ -23,82 +23,91 @@ 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_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_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
|
||||
ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k
|
||||
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -139,7 +148,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*);
|
||||
@@ -187,61 +196,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_tpmstx : 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)
|
||||
.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*);
|
||||
@@ -259,19 +268,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*);
|
||||
@@ -283,7 +292,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*);
|
||||
@@ -367,13 +376,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*);
|
||||
@@ -475,14 +484,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));
|
||||
@@ -499,7 +506,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)
|
||||
{
|
||||
@@ -519,12 +526,12 @@ SECTIONS
|
||||
*(*ui*external_app*siggen*);
|
||||
} > ram_external_app_siggen
|
||||
|
||||
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
|
||||
.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));
|
||||
@@ -542,6 +549,7 @@ SECTIONS
|
||||
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));
|
||||
@@ -559,5 +567,58 @@ SECTIONS
|
||||
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
|
||||
|
||||
.external_app_two_tone_pager : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_two_tone_pager.application_information));
|
||||
*(*ui*external_app*two_tone_pager*);
|
||||
} > ram_external_app_two_tone_pager
|
||||
|
||||
.external_app_two_tone_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_two_tone_rx.application_information));
|
||||
*(*ui*external_app*two_tone_rx*);
|
||||
} > ram_external_app_two_tone_rx
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
@@ -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<FpvDetectView>();
|
||||
}
|
||||
} // 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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
// Author: berkeozkir (Berke Özkır)
|
||||
|
||||
/*
|
||||
* FPV RX — how frequency search and lock work
|
||||
*
|
||||
* 1. Power metric: We use channelized power that portapack RSSI algo included.
|
||||
*
|
||||
* 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 - with some edge detecting algo.
|
||||
*
|
||||
* 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 <cstdio>
|
||||
|
||||
#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<uint8_t>(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<int16_t>(detect_threshold_db_);
|
||||
}
|
||||
|
||||
int16_t FpvDetectView::unlock_threshold_db() const {
|
||||
return static_cast<int16_t>(detect_threshold_db() - 6);
|
||||
}
|
||||
|
||||
int16_t FpvDetectView::candidate_average_db() const {
|
||||
if (!verify_samples_) {
|
||||
return -120;
|
||||
}
|
||||
return static_cast<int16_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>((scan_ch + 1) % FPV_CHANNELS_PER_BAND);
|
||||
} else {
|
||||
scan_ch = static_cast<uint8_t>(scan_ch + 1);
|
||||
if (scan_ch >= FPV_CHANNELS_PER_BAND) {
|
||||
scan_ch = 0;
|
||||
scan_band = static_cast<uint8_t>(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_freq_spike(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<int32_t>(verify_hits_) * 5;
|
||||
score -= static_cast<int32_t>(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<uint8_t>(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<uint8_t>(candidate_ch_ - 1));
|
||||
} else {
|
||||
neighbor_phase_ = 2;
|
||||
retune_to(candidate_band_, static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<int>(scan_ch + 1),
|
||||
static_cast<long>(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("CHECKING");
|
||||
break;
|
||||
case DetectState::Locked:
|
||||
text_state.set("FREQ FOUND");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FpvDetectView::update_confidence_text() {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "Posi %u%%", static_cast<unsigned>(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", band_labels[band_mode]);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "Scanning all from list");
|
||||
}
|
||||
break;
|
||||
|
||||
case DetectState::Candidate:
|
||||
std::snprintf(buf, sizeof(buf), "CHKING %c%d %ld MHz",
|
||||
band_labels[candidate_band_],
|
||||
static_cast<int>(candidate_ch_ + 1),
|
||||
static_cast<long>(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL));
|
||||
break;
|
||||
|
||||
case DetectState::Locked:
|
||||
std::snprintf(buf, sizeof(buf), "FREQ FOUND %c%d %ld",
|
||||
band_labels[candidate_band_],
|
||||
static_cast<int>(candidate_ch_ + 1),
|
||||
static_cast<long>(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<int>(detect_threshold_db()));
|
||||
break;
|
||||
|
||||
case DetectState::Candidate:
|
||||
std::snprintf(buf, sizeof(buf), "avg %d peak %d hits %u/%u",
|
||||
static_cast<int>(candidate_average_db()),
|
||||
static_cast<int>(verify_peak_db_),
|
||||
static_cast<unsigned>(verify_hits_),
|
||||
static_cast<unsigned>(verify_samples_));
|
||||
break;
|
||||
|
||||
case DetectState::Locked:
|
||||
std::snprintf(buf, sizeof(buf), "LOCKED %c%d posi %u%% hold %u",
|
||||
band_labels[candidate_band_],
|
||||
static_cast<int>(candidate_ch_ + 1),
|
||||
static_cast<unsigned>(candidate_confidence_),
|
||||
static_cast<unsigned>(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<int>(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<unsigned>(graph_min),
|
||||
static_cast<unsigned>(graph_avg),
|
||||
static_cast<unsigned>(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<uint8_t>(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_freq_spike(statistics)) {
|
||||
enter_candidate(statistics);
|
||||
}
|
||||
break;
|
||||
|
||||
case DetectState::Candidate:
|
||||
if (neighbor_phase_ == 1) {
|
||||
const auto idx = channel_index(candidate_band_, static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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
|
||||
@@ -0,0 +1,200 @@
|
||||
// Author: berkeozkir (Berke Özkır)
|
||||
|
||||
#ifndef _UI_FPV_DETECT
|
||||
#define _UI_FPV_DETECT
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#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_freq_spike(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<ChannelMemory, FPV_TOTAL_CHANNELS> 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}, "Posi 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 FREQS"};
|
||||
Text text_detail{{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "Waiting for FPV FREQ"};
|
||||
|
||||
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<const ChannelStatisticsMessage*>(p)->statistics);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::fpv_detect
|
||||
|
||||
#endif
|
||||
+84
@@ -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<KissTncView>();
|
||||
}
|
||||
} // 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,
|
||||
};
|
||||
}
|
||||
@@ -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<uint8_t>(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
|
||||
@@ -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 <optional>
|
||||
|
||||
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<UsbSerialInputHandler> usb_input_handler_{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::APRSPacket,
|
||||
[this](Message* const p) {
|
||||
on_packet(reinterpret_cast<const APRSPacketMessage*>(p));
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](Message* const p) {
|
||||
auto* msg = reinterpret_cast<const TXProgressMessage*>(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
|
||||
+83
@@ -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<MdcTxView>();
|
||||
}
|
||||
} // 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,
|
||||
};
|
||||
}
|
||||
+289
@@ -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<uint8_t>((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<uint16_t>((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<uint16_t>(crc << 1);
|
||||
if (c & static_cast<uint16_t>(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<uint8_t>(crc & 0xFF);
|
||||
cw[5] = static_cast<uint8_t>((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<uint8_t>((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<uint8_t>(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<FrequencyKeypadView>(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<uint8_t>(raw_op);
|
||||
uint8_t arg = static_cast<uint8_t>(field_arg.value());
|
||||
uint8_t id_hi = static_cast<uint8_t>(field_id_hi.value());
|
||||
uint8_t id_lo = static_cast<uint8_t>(field_id_lo.value());
|
||||
uint8_t src_hi = static_cast<uint8_t>(field_src_hi.value());
|
||||
uint8_t src_lo = static_cast<uint8_t>(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<uint16_t*>(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<uint8_t>((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
|
||||
+124
@@ -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<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(msg.progress, msg.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::mdc_tx
|
||||
|
||||
#endif
|
||||
+75
-42
@@ -27,6 +27,9 @@ MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
|
||||
&chk_trans,
|
||||
&bandwidth,
|
||||
&chk_callsgn,
|
||||
&chk_loop,
|
||||
&wait_time,
|
||||
&progressbar,
|
||||
&txt_last,
|
||||
&console_text,
|
||||
&btn_clear,
|
||||
@@ -69,6 +72,7 @@ MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
|
||||
options_mode.set_selected_index(current_mode, true);
|
||||
tone_.set_value(tone, true);
|
||||
wpm_.set_value(wpm, true);
|
||||
wait_time.set_value(1, true);
|
||||
bandwidth.set_value(band, true);
|
||||
|
||||
btn_ptt.on_select = [this](Button&) {
|
||||
@@ -156,6 +160,14 @@ MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
chk_trans.on_select = [this](Checkbox&, bool v) {
|
||||
if (v) chk_loop.set_value(false);
|
||||
};
|
||||
|
||||
chk_loop.on_select = [this](Checkbox&, bool v) {
|
||||
if (v) chk_trans.set_value(false);
|
||||
};
|
||||
}
|
||||
|
||||
MorseRadiotxView::~MorseRadiotxView() {
|
||||
@@ -197,19 +209,15 @@ MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_
|
||||
|
||||
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()) {
|
||||
if (chk_trans.value()) {
|
||||
ptt_button_visibility(false);
|
||||
return;
|
||||
} else {
|
||||
full_message = msg_buffer;
|
||||
if (chk_callsgn.value() && !call_sign.empty()) { // call sign
|
||||
full_message += " " + call_sign;
|
||||
}
|
||||
}
|
||||
|
||||
// enable transmit
|
||||
@@ -219,46 +227,63 @@ void MorseRadiotxView::transmit_morse_message() {
|
||||
ui_toggle();
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < full_message.length(); i++) {
|
||||
if (chThdShouldTerminate()) break;
|
||||
uint8_t loop_seconds = static_cast<uint8_t>(wait_time.value());
|
||||
progressbar.set_max(loop_seconds * 2);
|
||||
|
||||
char c = full_message[i];
|
||||
do {
|
||||
for (size_t i = 0; i < full_message.length(); i++) {
|
||||
if (chThdShouldTerminate()) break;
|
||||
|
||||
std::string s_char(1, c);
|
||||
// space
|
||||
if (c == ' ') {
|
||||
console_text.write(" ");
|
||||
chThdSleepMilliseconds(current_timings.word_gap);
|
||||
continue;
|
||||
}
|
||||
char c = full_message[i];
|
||||
|
||||
const char* pattern = morse_decoder_.get_morse_pattern(c);
|
||||
std::string s_char(1, c);
|
||||
// space
|
||||
if (c == ' ') {
|
||||
console_text.write(" ");
|
||||
chThdSleepMilliseconds(current_timings.word_gap);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pattern != nullptr) {
|
||||
txt_last.set(pattern);
|
||||
const char* pattern = morse_decoder_.get_morse_pattern(c);
|
||||
|
||||
// write blue char to console
|
||||
console_text.write(STR_COLOR_BLUE + s_char);
|
||||
if (pattern != nullptr) {
|
||||
txt_last.set(pattern);
|
||||
|
||||
// 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);
|
||||
// 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;
|
||||
// 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 (current_timings.char_gap > current_timings.symbol_gap) {
|
||||
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chThdShouldTerminate()) break;
|
||||
if (chk_loop.value()) {
|
||||
console_text.write(" ");
|
||||
for (uint8_t s = 0; s < (loop_seconds * 2); s++) {
|
||||
if (chThdShouldTerminate()) break;
|
||||
progressbar.set_value(s + 1);
|
||||
if (!chk_loop.value()) break;
|
||||
chThdSleepMilliseconds(500);
|
||||
}
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
if (!chk_loop.value()) break;
|
||||
} while (chk_loop.value() && !chThdShouldTerminate());
|
||||
|
||||
if (chk_trans.value()) ptt_button_visibility(false);
|
||||
baseband::set_morsetx_key(false);
|
||||
@@ -356,13 +381,17 @@ void MorseRadiotxView::ui_toggle() {
|
||||
wpm_.set_style(Theme::getInstance()->fg_dark);
|
||||
wpm_.set_focusable(false);
|
||||
btn_message.set_style(Theme::getInstance()->fg_dark);
|
||||
btn_message.set_focusable(false);
|
||||
btn_calls.set_style(Theme::getInstance()->fg_dark);
|
||||
btn_calls.set_focusable(false);
|
||||
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);
|
||||
wait_time.set_style(Theme::getInstance()->fg_dark);
|
||||
wait_time.set_focusable(false);
|
||||
bandwidth.set_style(Theme::getInstance()->fg_dark);
|
||||
bandwidth.set_focusable(false);
|
||||
|
||||
} else {
|
||||
options_mode.set_style(Theme::getInstance()->bg_darker);
|
||||
@@ -370,11 +399,15 @@ void MorseRadiotxView::ui_toggle() {
|
||||
wpm_.set_style(Theme::getInstance()->bg_darker);
|
||||
wpm_.set_focusable(true);
|
||||
btn_message.set_style(Theme::getInstance()->bg_darker);
|
||||
btn_message.set_focusable(true);
|
||||
btn_calls.set_style(Theme::getInstance()->bg_darker);
|
||||
btn_calls.set_focusable(true);
|
||||
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);
|
||||
wait_time.set_style(Theme::getInstance()->bg_darker);
|
||||
wait_time.set_focusable(true);
|
||||
ptt_button_visibility(true);
|
||||
tone_.set_style(Theme::getInstance()->bg_darker);
|
||||
tone_.set_focusable(true);
|
||||
|
||||
@@ -118,15 +118,20 @@ class MorseRadiotxView : public ui::View {
|
||||
{{"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};
|
||||
NumberField wait_time{{UI_POS_X(10), UI_POS_Y(4)}, 3, {1, 99}, 1, ' ', true}; // wait between tx-es in loop mode (sec)
|
||||
FloatField bandwidth{{UI_POS_X(6), UI_POS_Y(5)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{UI_POS_X(0), UI_POS_Y(6), screen_width, 16}};
|
||||
|
||||
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};
|
||||
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(3), 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)}};
|
||||
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(3)}, 9, "Call sign", true};
|
||||
Checkbox chk_loop{{UI_POS_X(14), UI_POS_Y(4)}, 4, "loop", true};
|
||||
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(9), 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"};
|
||||
|
||||
@@ -135,10 +140,11 @@ class MorseRadiotxView : public ui::View {
|
||||
{{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(0), UI_POS_Y(5)}, "BandW:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(10), UI_POS_Y(5)}, "kHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "Wait time: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(7)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(8)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+83
@@ -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<P25TxView>();
|
||||
}
|
||||
} // 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,
|
||||
};
|
||||
}
|
||||
+282
@@ -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 <cstring>
|
||||
|
||||
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<FrequencyKeypadView>(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
|
||||
@@ -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<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::p25_tx
|
||||
|
||||
#endif /* __UI_P25_TX */
|
||||
@@ -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;
|
||||
|
||||
@@ -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,19 +140,19 @@ class POCSAGTXView : public View {
|
||||
10000,
|
||||
9};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_remete{
|
||||
Message::ID::PocsagTosend,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const PocsagTosendMessage*>(p);
|
||||
this->on_remete(message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(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<const PocsagTosendMessage*>(p);
|
||||
this->on_remote(message);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui::external_app::pocsag_tx */
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_rtty_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,
|
||||
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_rtty_tx.application_information"), use
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
|
||||
+79
@@ -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<SameTxView>();
|
||||
}
|
||||
} // 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,
|
||||
};
|
||||
}
|
||||
@@ -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<uint16_t*>(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<uint8_t>(*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
|
||||
@@ -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<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(msg.progress, msg.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::same_tx
|
||||
#endif
|
||||
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
@@ -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<TimeSinkView>();
|
||||
}
|
||||
} // 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
|
||||
};
|
||||
}
|
||||
@@ -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 <algorithm>
|
||||
|
||||
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<uint8_t>(std::clamp<size_t>(
|
||||
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<int32_t>(1, r.height() - 1);
|
||||
const int32_t y = y_center - (static_cast<int32_t>(sample) * y_span) / 65536;
|
||||
return static_cast<Coord>(std::clamp<int32_t>(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<size_t>({
|
||||
length_,
|
||||
max_columns,
|
||||
static_cast<size_t>(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<Coord>(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<Coord>(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<uint8_t>(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<uint8_t>(v);
|
||||
waveform.set_persistence_frames(persistence_frames);
|
||||
};
|
||||
|
||||
options_trigger_mode.set_by_nearest_value(trigger_mode);
|
||||
trigger_mode = static_cast<uint8_t>(options_trigger_mode.selected_index_value());
|
||||
options_trigger_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
trigger_mode = static_cast<uint8_t>(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<uint8_t>(TriggerMode::Off):
|
||||
mode = TriggerMode::Off;
|
||||
break;
|
||||
case static_cast<uint8_t>(TriggerMode::Rising):
|
||||
mode = TriggerMode::Rising;
|
||||
break;
|
||||
case static_cast<uint8_t>(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<int32_t>(
|
||||
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<int32_t>(spectrum.db[src_index]) - 128;
|
||||
const int32_t scaled = centered * 256;
|
||||
waveform_buffer[x] = static_cast<int16_t>(std::clamp<int32_t>(scaled, -32768, 32767));
|
||||
}
|
||||
|
||||
waveform.set_dirty();
|
||||
}
|
||||
|
||||
void TimeSinkView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::time_sink
|
||||
@@ -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 <array>
|
||||
|
||||
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<Coord, max_columns> current_y_{};
|
||||
std::array<std::array<Coord, max_columns>, 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<uint8_t>(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<int32_t>(TriggerMode::Off)},
|
||||
{"Rise", static_cast<int32_t>(TriggerMode::Rising)},
|
||||
{"Fall", static_cast<int32_t>(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<const ChannelSpectrumConfigMessage*>(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<const FreqChangeCommandMessage*>(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__
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2024 PortaPack Mayhem
|
||||
*
|
||||
* 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_two_tone_pager.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::two_tone_pager {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<TwoTonePagerView>();
|
||||
}
|
||||
} // namespace ui::external_app::two_tone_pager
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_two_tone_pager.application_information"), used)) application_information_t _application_information_two_tone_pager = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::two_tone_pager::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "2-Tone TX",
|
||||
/*.bitmap_data = */ {
|
||||
// 16×16 pager icon — two-tone radio / pager device
|
||||
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::orange().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
// Uses the proc_tones baseband processor (same as Morse TX)
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_tones */ {'P', 'T', 'O', 'N'},
|
||||
/*.m4_app_offset = */ 0x00000000, // filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,616 @@
|
||||
/*
|
||||
* Copyright (C) 2024 PortaPack Mayhem
|
||||
*
|
||||
* 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_two_tone_pager.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "theme.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::two_tone_pager {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tone tables — store only freq×10 values; names are generated at runtime
|
||||
// to keep static data small and avoid const-char-pointer patching issues.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const uint32_t MOTO_FREQS[45] = {
|
||||
2885,
|
||||
3047,
|
||||
3217,
|
||||
3396,
|
||||
3586,
|
||||
3786,
|
||||
3998,
|
||||
4221,
|
||||
4457,
|
||||
4705,
|
||||
4968,
|
||||
5246,
|
||||
5539,
|
||||
5848,
|
||||
6174,
|
||||
6519,
|
||||
6883,
|
||||
7268,
|
||||
7674,
|
||||
8102,
|
||||
8555,
|
||||
9032,
|
||||
9537,
|
||||
10073,
|
||||
10642,
|
||||
11225,
|
||||
11247,
|
||||
11534,
|
||||
11852,
|
||||
11885,
|
||||
12178,
|
||||
12514,
|
||||
12555,
|
||||
12858,
|
||||
13258,
|
||||
13576,
|
||||
13950,
|
||||
13996,
|
||||
14768,
|
||||
15579,
|
||||
16430,
|
||||
17325,
|
||||
18262,
|
||||
19245,
|
||||
20275,
|
||||
};
|
||||
|
||||
// Index 0 = None (0), indices 1-50 = standard CTCSS tones
|
||||
static const uint32_t CTCSS_FREQS[51] = {
|
||||
0,
|
||||
670,
|
||||
719,
|
||||
744,
|
||||
770,
|
||||
797,
|
||||
825,
|
||||
854,
|
||||
885,
|
||||
915,
|
||||
948,
|
||||
974,
|
||||
1000,
|
||||
1035,
|
||||
1072,
|
||||
1109,
|
||||
1148,
|
||||
1188,
|
||||
1230,
|
||||
1273,
|
||||
1318,
|
||||
1365,
|
||||
1413,
|
||||
1462,
|
||||
1500,
|
||||
1514,
|
||||
1567,
|
||||
1598,
|
||||
1622,
|
||||
1655,
|
||||
1679,
|
||||
1713,
|
||||
1738,
|
||||
1773,
|
||||
1799,
|
||||
1835,
|
||||
1862,
|
||||
1899,
|
||||
1928,
|
||||
1966,
|
||||
1995,
|
||||
2035,
|
||||
2065,
|
||||
2107,
|
||||
2181,
|
||||
2257,
|
||||
2291,
|
||||
2336,
|
||||
2418,
|
||||
2503,
|
||||
2541,
|
||||
};
|
||||
|
||||
// Generate a display name from a freq×10 value ("288.5Hz", "None" for 0)
|
||||
static std::string freq_name(uint32_t freq_x10) {
|
||||
if (freq_x10 == 0) return "None";
|
||||
return to_string_dec_uint(freq_x10 / 10) + "." +
|
||||
to_string_dec_uint(freq_x10 % 10) + "Hz";
|
||||
}
|
||||
|
||||
// Common timing profiles used in the field
|
||||
struct TimingPreset {
|
||||
uint32_t dur_a; // ms
|
||||
uint32_t dur_b; // ms
|
||||
uint32_t gap; // ms
|
||||
};
|
||||
|
||||
static const TimingPreset TIMING_PRESETS[4] = {
|
||||
{1000, 3000, 0}, // Moto Std
|
||||
{700, 1000, 0}, // Short Alert
|
||||
{2000, 1000, 0}, // Fire Std
|
||||
{3000, 3000, 0}, // Long Alert
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Parse one unsigned decimal integer from *p, advancing p past the digits and
|
||||
// an optional trailing comma.
|
||||
static uint32_t parse_uint_field(const char*& p) {
|
||||
uint32_t v = 0;
|
||||
while (*p >= '0' && *p <= '9')
|
||||
v = v * 10 + (*p++ - '0');
|
||||
if (*p == ',')
|
||||
++p;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Phase-delta for the 32-bit sine-table accumulator used by proc_tones.
|
||||
// delta = freq_hz * 2^32 / sample_rate
|
||||
// Using freq_x10 to avoid floating-point: delta = (freq_x10 * 2^32) / (sample_rate * 10)
|
||||
uint32_t TwoTonePagerView::tone_delta(uint32_t freq_x10) const {
|
||||
if (freq_x10 == 0) return 0;
|
||||
return static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(freq_x10) << 32) /
|
||||
(static_cast<uint64_t>(SAMPLE_RATE) * 10ULL));
|
||||
}
|
||||
|
||||
uint32_t TwoTonePagerView::ms_to_samples(uint32_t ms) const {
|
||||
return static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(ms) * SAMPLE_RATE) / 1000ULL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preset encode / decode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::string TwoTonePagerView::encode_preset() const {
|
||||
return to_string_dec_uint(ctcss_idx) + "," +
|
||||
to_string_dec_uint(tone_a_idx) + "," +
|
||||
to_string_dec_uint(tone_b_idx) + "," +
|
||||
to_string_dec_uint(dur_a) + "," +
|
||||
to_string_dec_uint(dur_b) + "," +
|
||||
to_string_dec_uint(gap_ms) + "," +
|
||||
to_string_dec_uint(custom_freq_a_hz) + "," +
|
||||
to_string_dec_uint(custom_freq_b_hz);
|
||||
}
|
||||
|
||||
void TwoTonePagerView::decode_preset(const std::string& s) {
|
||||
const char* p = s.c_str();
|
||||
uint32_t ci = parse_uint_field(p);
|
||||
uint32_t ai = parse_uint_field(p);
|
||||
uint32_t bi = parse_uint_field(p);
|
||||
uint32_t da = parse_uint_field(p);
|
||||
uint32_t db = parse_uint_field(p);
|
||||
uint32_t gp = parse_uint_field(p);
|
||||
|
||||
uint32_t cfa = (*p != '\0') ? parse_uint_field(p) : 405;
|
||||
uint32_t cfb = (*p != '\0') ? parse_uint_field(p) : 814;
|
||||
|
||||
ctcss_idx = std::min(ci, static_cast<uint32_t>(CTCSS_COUNT - 1));
|
||||
tone_a_idx = std::min(ai, CUSTOM_TONE_IDX);
|
||||
tone_b_idx = std::min(bi, CUSTOM_TONE_IDX);
|
||||
dur_a = std::max(uint32_t{100}, std::min(da, uint32_t{9900}));
|
||||
dur_b = std::max(uint32_t{100}, std::min(db, uint32_t{9900}));
|
||||
gap_ms = std::min(gp, uint32_t{9900});
|
||||
custom_freq_a_hz = std::max(uint32_t{100}, std::min(cfa, uint32_t{9999}));
|
||||
custom_freq_b_hz = std::max(uint32_t{100}, std::min(cfb, uint32_t{9999}));
|
||||
}
|
||||
|
||||
std::string& TwoTonePagerView::slot_ref(uint32_t slot) {
|
||||
switch (slot) {
|
||||
case 2:
|
||||
return preset_2;
|
||||
case 3:
|
||||
return preset_3;
|
||||
case 4:
|
||||
return preset_4;
|
||||
case 5:
|
||||
return preset_5;
|
||||
default:
|
||||
return preset_1;
|
||||
}
|
||||
}
|
||||
|
||||
std::string& TwoTonePagerView::slot_name_ref(uint32_t slot) {
|
||||
switch (slot) {
|
||||
case 2:
|
||||
return preset_name_2;
|
||||
case 3:
|
||||
return preset_name_3;
|
||||
case 4:
|
||||
return preset_name_4;
|
||||
case 5:
|
||||
return preset_name_5;
|
||||
default:
|
||||
return preset_name_1;
|
||||
}
|
||||
}
|
||||
|
||||
void TwoTonePagerView::update_slot_name_display() {
|
||||
const auto& name = slot_name_ref(preset_slot);
|
||||
std::string display = name.empty() ? "(none)" : name;
|
||||
if (display.size() > 8) display = display.substr(0, 8);
|
||||
text_slot_name.set(display);
|
||||
}
|
||||
|
||||
void TwoTonePagerView::save_preset(uint32_t slot) {
|
||||
slot_ref(slot) = encode_preset();
|
||||
text_status.set("Saved to slot " + to_string_dec_uint(slot));
|
||||
}
|
||||
|
||||
void TwoTonePagerView::load_preset(uint32_t slot) {
|
||||
decode_preset(slot_ref(slot));
|
||||
|
||||
// Update all UI fields without firing their on_change callbacks
|
||||
options_ctcss.set_selected_index(ctcss_idx, false);
|
||||
options_tone_a.set_selected_index(tone_a_idx, false);
|
||||
options_tone_b.set_selected_index(tone_b_idx, false);
|
||||
field_dur_a.set_value(static_cast<int32_t>(dur_a), false);
|
||||
field_dur_b.set_value(static_cast<int32_t>(dur_b), false);
|
||||
field_gap.set_value(static_cast<int32_t>(gap_ms), false);
|
||||
symfield_custom_a.set_value(custom_freq_a_hz);
|
||||
symfield_custom_b.set_value(custom_freq_b_hz);
|
||||
options_timing.set_selected_index(detect_timing_preset(), false);
|
||||
|
||||
update_tx_time();
|
||||
update_slot_name_display();
|
||||
text_status.set("Loaded slot " + to_string_dec_uint(slot));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Timing presets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
size_t TwoTonePagerView::detect_timing_preset() const {
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
if (TIMING_PRESETS[i].dur_a == dur_a &&
|
||||
TIMING_PRESETS[i].dur_b == dur_b &&
|
||||
TIMING_PRESETS[i].gap == gap_ms)
|
||||
return i;
|
||||
}
|
||||
return 4; // "Custom"
|
||||
}
|
||||
|
||||
void TwoTonePagerView::apply_timing_preset(size_t idx) {
|
||||
if (idx >= 4) return; // "Custom" — do not override current values
|
||||
dur_a = TIMING_PRESETS[idx].dur_a;
|
||||
dur_b = TIMING_PRESETS[idx].dur_b;
|
||||
gap_ms = TIMING_PRESETS[idx].gap;
|
||||
field_dur_a.set_value(static_cast<int32_t>(dur_a), false);
|
||||
field_dur_b.set_value(static_cast<int32_t>(dur_b), false);
|
||||
field_gap.set_value(static_cast<int32_t>(gap_ms), false);
|
||||
update_tx_time();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void TwoTonePagerView::update_tx_time() {
|
||||
uint32_t total_ms = dur_a + gap_ms + dur_b;
|
||||
std::string name_a = (tone_a_idx == CUSTOM_TONE_IDX)
|
||||
? to_string_dec_uint(custom_freq_a_hz) + "Hz"
|
||||
: freq_name(MOTO_FREQS[tone_a_idx]);
|
||||
std::string name_b = (tone_b_idx == CUSTOM_TONE_IDX)
|
||||
? to_string_dec_uint(custom_freq_b_hz) + "Hz"
|
||||
: freq_name(MOTO_FREQS[tone_b_idx]);
|
||||
text_time.set("TX time: " +
|
||||
to_string_dec_uint(total_ms / 1000) + "." +
|
||||
to_string_dec_uint((total_ms / 100) % 10) + "s" +
|
||||
" A:" + name_a + " B:" + name_b);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transmission
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool TwoTonePagerView::start_tx() {
|
||||
auto& td = shared_memory.bb_data.tones_data;
|
||||
|
||||
const uint32_t freq_a_x10 = (tone_a_idx == CUSTOM_TONE_IDX)
|
||||
? custom_freq_a_hz * 10
|
||||
: MOTO_FREQS[tone_a_idx];
|
||||
const uint32_t freq_b_x10 = (tone_b_idx == CUSTOM_TONE_IDX)
|
||||
? custom_freq_b_hz * 10
|
||||
: MOTO_FREQS[tone_b_idx];
|
||||
const uint32_t delta_a = tone_delta(freq_a_x10);
|
||||
const uint32_t delta_b = tone_delta(freq_b_x10);
|
||||
const uint32_t delta_c = tone_delta(CTCSS_FREQS[ctcss_idx]);
|
||||
const uint32_t samp_a = ms_to_samples(dur_a);
|
||||
const uint32_t samp_b = ms_to_samples(dur_b);
|
||||
const uint32_t samp_g = ms_to_samples(gap_ms);
|
||||
const bool with_ctcss = (ctcss_idx > 0);
|
||||
|
||||
// Clear tone defs we'll use
|
||||
memset(&td, 0, sizeof(td));
|
||||
|
||||
uint8_t tone_count;
|
||||
|
||||
if (!with_ctcss) {
|
||||
// Single-tone sequential mode
|
||||
// tone_defs[0] = Tone A, tone_defs[1] = Tone B
|
||||
td.tone_defs[0].delta = delta_a;
|
||||
td.tone_defs[0].duration = samp_a;
|
||||
td.tone_defs[1].delta = delta_b;
|
||||
td.tone_defs[1].duration = samp_b;
|
||||
|
||||
if (gap_ms > 0) {
|
||||
td.silence = samp_g;
|
||||
td.message[0] = 0; // Tone A
|
||||
td.message[1] = 255; // Silence (any index ≥ 32 triggers silence)
|
||||
td.message[2] = 1; // Tone B
|
||||
tone_count = 3;
|
||||
} else {
|
||||
td.message[0] = 0; // Tone A
|
||||
td.message[1] = 1; // Tone B
|
||||
tone_count = 2;
|
||||
}
|
||||
} else {
|
||||
// Dual-tone mode: CTCSS mixed simultaneously with pager tones.
|
||||
//
|
||||
// proc_tones dual-tone indexing for digit n:
|
||||
// main delta = tone_deltas[n * 2] = tone_defs[n*2].delta
|
||||
// sub delta = tone_deltas[n*2 + 1] = tone_defs[n*2+1].delta
|
||||
// duration = tone_durations[n] = tone_defs[n].duration
|
||||
//
|
||||
// digit 0 → Tone A (main) + CTCSS (sub), duration from tone_defs[0]
|
||||
// digit 1 → Tone B (main) + CTCSS (sub), duration from tone_defs[1]
|
||||
td.tone_defs[0].delta = delta_a; // digit 0 main
|
||||
td.tone_defs[0].duration = samp_a;
|
||||
td.tone_defs[1].delta = delta_c; // digit 0 sub (CTCSS); also digit 1 duration source
|
||||
td.tone_defs[1].duration = samp_b;
|
||||
td.tone_defs[2].delta = delta_b; // digit 1 main (Tone B)
|
||||
td.tone_defs[2].duration = 0; // duration for digit 1 comes from tone_defs[1]
|
||||
td.tone_defs[3].delta = delta_c; // digit 1 sub (CTCSS)
|
||||
td.tone_defs[3].duration = 0;
|
||||
|
||||
if (gap_ms > 0) {
|
||||
td.silence = samp_g;
|
||||
td.message[0] = 0;
|
||||
td.message[1] = 255;
|
||||
td.message[2] = 1;
|
||||
tone_count = 3;
|
||||
} else {
|
||||
td.message[0] = 0;
|
||||
td.message[1] = 1;
|
||||
tone_count = 2;
|
||||
}
|
||||
}
|
||||
|
||||
progressbar.set_max(tone_count);
|
||||
progressbar.set_value(0);
|
||||
|
||||
transmitter_model.set_baseband_bandwidth(1'750'000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_tones_config(
|
||||
transmitter_model.channel_bandwidth(),
|
||||
0, // no pre-silence
|
||||
tone_count,
|
||||
with_ctcss, // dual_tone flag
|
||||
false); // no audio monitor output
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TwoTonePagerView::stop_tx() {
|
||||
transmitter_model.disable();
|
||||
baseband::kill_tone();
|
||||
tx_view.set_transmitting(false);
|
||||
text_status.set("Stopped.");
|
||||
}
|
||||
|
||||
void TwoTonePagerView::on_tx_progress(uint32_t progress, bool done) {
|
||||
if (done) {
|
||||
transmitter_model.disable();
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(false);
|
||||
text_status.set("Done.");
|
||||
} else {
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// View lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void TwoTonePagerView::focus() {
|
||||
options_tone_a.focus();
|
||||
}
|
||||
|
||||
TwoTonePagerView::~TwoTonePagerView() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
TwoTonePagerView::TwoTonePagerView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
// Clamp restored settings to valid ranges before touching UI
|
||||
ctcss_idx = std::min(ctcss_idx, static_cast<uint32_t>(CTCSS_COUNT - 1));
|
||||
tone_a_idx = std::min(tone_a_idx, CUSTOM_TONE_IDX);
|
||||
tone_b_idx = std::min(tone_b_idx, CUSTOM_TONE_IDX);
|
||||
dur_a = std::max(uint32_t{100}, std::min(dur_a, uint32_t{9900}));
|
||||
dur_b = std::max(uint32_t{100}, std::min(dur_b, uint32_t{9900}));
|
||||
gap_ms = std::min(gap_ms, uint32_t{9900});
|
||||
preset_slot = std::max(uint32_t{1}, std::min(preset_slot, uint32_t{5}));
|
||||
custom_freq_a_hz = std::max(uint32_t{100}, std::min(custom_freq_a_hz, uint32_t{9999}));
|
||||
custom_freq_b_hz = std::max(uint32_t{100}, std::min(custom_freq_b_hz, uint32_t{9999}));
|
||||
|
||||
// Build CTCSS options list
|
||||
{
|
||||
std::vector<std::pair<std::string, int32_t>> opts;
|
||||
opts.reserve(CTCSS_COUNT);
|
||||
for (size_t i = 0; i < CTCSS_COUNT; i++)
|
||||
opts.push_back({freq_name(CTCSS_FREQS[i]), static_cast<int32_t>(i)});
|
||||
options_ctcss.set_options(std::move(opts));
|
||||
}
|
||||
|
||||
// Build Motorola tone options (same pool for both A and B); "Custom" appended
|
||||
{
|
||||
std::vector<std::pair<std::string, int32_t>> opts;
|
||||
opts.reserve(MOTO_TONE_COUNT + 1);
|
||||
for (size_t i = 0; i < MOTO_TONE_COUNT; i++)
|
||||
opts.push_back({freq_name(MOTO_FREQS[i]), static_cast<int32_t>(i)});
|
||||
opts.push_back({"Custom", static_cast<int32_t>(CUSTOM_TONE_IDX)});
|
||||
options_tone_a.set_options(opts);
|
||||
options_tone_b.set_options(std::move(opts));
|
||||
}
|
||||
|
||||
add_children({&labels,
|
||||
&options_ctcss,
|
||||
&options_tone_a,
|
||||
&options_tone_b,
|
||||
&symfield_custom_a,
|
||||
&symfield_custom_b,
|
||||
&field_dur_a,
|
||||
&field_dur_b,
|
||||
&field_gap,
|
||||
&options_timing,
|
||||
&field_slot,
|
||||
&button_save,
|
||||
&button_load,
|
||||
&text_slot_name,
|
||||
&text_time,
|
||||
&text_status,
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// Restore saved indices into the options / number fields (no callbacks)
|
||||
options_ctcss.set_selected_index(ctcss_idx, false);
|
||||
options_tone_a.set_selected_index(tone_a_idx, false);
|
||||
options_tone_b.set_selected_index(tone_b_idx, false);
|
||||
symfield_custom_a.set_value(custom_freq_a_hz);
|
||||
symfield_custom_b.set_value(custom_freq_b_hz);
|
||||
field_dur_a.set_value(static_cast<int32_t>(dur_a), false);
|
||||
field_dur_b.set_value(static_cast<int32_t>(dur_b), false);
|
||||
field_gap.set_value(static_cast<int32_t>(gap_ms), false);
|
||||
options_timing.set_selected_index(detect_timing_preset(), false);
|
||||
field_slot.set_value(static_cast<int32_t>(preset_slot), false);
|
||||
|
||||
update_tx_time();
|
||||
update_slot_name_display();
|
||||
|
||||
// --- Callbacks ---
|
||||
|
||||
options_ctcss.on_change = [this](size_t i, int32_t) {
|
||||
ctcss_idx = static_cast<uint32_t>(i);
|
||||
};
|
||||
|
||||
options_tone_a.on_change = [this](size_t i, int32_t) {
|
||||
tone_a_idx = static_cast<uint32_t>(i);
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
options_tone_b.on_change = [this](size_t i, int32_t) {
|
||||
tone_b_idx = static_cast<uint32_t>(i);
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
symfield_custom_a.on_change = [this](SymField&) {
|
||||
uint32_t v = static_cast<uint32_t>(symfield_custom_a.to_integer());
|
||||
custom_freq_a_hz = std::max(uint32_t{100}, std::min(v, uint32_t{9999}));
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
symfield_custom_b.on_change = [this](SymField&) {
|
||||
uint32_t v = static_cast<uint32_t>(symfield_custom_b.to_integer());
|
||||
custom_freq_b_hz = std::max(uint32_t{100}, std::min(v, uint32_t{9999}));
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
field_dur_a.on_change = [this](int32_t v) {
|
||||
dur_a = static_cast<uint32_t>(v);
|
||||
options_timing.set_selected_index(detect_timing_preset(), false);
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
field_dur_b.on_change = [this](int32_t v) {
|
||||
dur_b = static_cast<uint32_t>(v);
|
||||
options_timing.set_selected_index(detect_timing_preset(), false);
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
field_gap.on_change = [this](int32_t v) {
|
||||
gap_ms = static_cast<uint32_t>(v);
|
||||
options_timing.set_selected_index(detect_timing_preset(), false);
|
||||
update_tx_time();
|
||||
};
|
||||
|
||||
options_timing.on_change = [this](size_t i, int32_t) {
|
||||
apply_timing_preset(i);
|
||||
};
|
||||
|
||||
field_slot.on_change = [this](int32_t v) {
|
||||
preset_slot = static_cast<uint32_t>(v);
|
||||
update_slot_name_display();
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
text_prompt(nav_, slot_name_ref(preset_slot), 12, ENTER_KEYBOARD_MODE_ALPHA,
|
||||
[this](std::string&) {
|
||||
save_preset(preset_slot);
|
||||
update_slot_name_display();
|
||||
});
|
||||
};
|
||||
|
||||
button_load.on_select = [this](Button&) {
|
||||
load_preset(preset_slot);
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (start_tx()) {
|
||||
tx_view.set_transmitting(true);
|
||||
text_status.set("Transmitting...");
|
||||
}
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop_tx();
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::two_tone_pager
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2024 PortaPack Mayhem
|
||||
*
|
||||
* 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_TWO_TONE_PAGER_H__
|
||||
#define __UI_TWO_TONE_PAGER_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::two_tone_pager {
|
||||
|
||||
class TwoTonePagerView : public View {
|
||||
public:
|
||||
TwoTonePagerView(NavigationView& nav);
|
||||
~TwoTonePagerView();
|
||||
|
||||
TwoTonePagerView(const TwoTonePagerView&) = delete;
|
||||
TwoTonePagerView(TwoTonePagerView&&) = delete;
|
||||
TwoTonePagerView& operator=(const TwoTonePagerView&) = delete;
|
||||
TwoTonePagerView& operator=(TwoTonePagerView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
std::string title() const override { return "2-Tone TX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
static constexpr uint32_t SAMPLE_RATE = 1536000;
|
||||
static constexpr size_t MOTO_TONE_COUNT = 45;
|
||||
static constexpr size_t CTCSS_COUNT = 51;
|
||||
static constexpr uint32_t CUSTOM_TONE_IDX = MOTO_TONE_COUNT; // sentinel: one past the table, use custom Hz field
|
||||
|
||||
TxRadioState radio_state_{
|
||||
154280000ULL, // 154.280 MHz — common VHF paging frequency
|
||||
1750000,
|
||||
SAMPLE_RATE};
|
||||
|
||||
// Persisted settings
|
||||
uint32_t ctcss_idx{0};
|
||||
uint32_t tone_a_idx{8}; // 405.3 Hz
|
||||
uint32_t tone_b_idx{24}; // 813.9 Hz
|
||||
uint32_t dur_a{1000};
|
||||
uint32_t dur_b{3000};
|
||||
uint32_t gap_ms{0};
|
||||
uint32_t preset_slot{1};
|
||||
uint32_t custom_freq_a_hz{405}; // used when tone_a_idx == CUSTOM_TONE_IDX
|
||||
uint32_t custom_freq_b_hz{814};
|
||||
|
||||
// Five preset slots — encoded tone/timing data and a user-chosen display name
|
||||
std::string preset_1{"0,8,24,1000,3000,0"};
|
||||
std::string preset_2{"0,8,24,1000,3000,0"};
|
||||
std::string preset_3{"0,8,24,1000,3000,0"};
|
||||
std::string preset_4{"0,8,24,1000,3000,0"};
|
||||
std::string preset_5{"0,8,24,1000,3000,0"};
|
||||
std::string preset_name_1{""};
|
||||
std::string preset_name_2{""};
|
||||
std::string preset_name_3{""};
|
||||
std::string preset_name_4{""};
|
||||
std::string preset_name_5{""};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_twotone",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"ctcss"sv, &ctcss_idx},
|
||||
{"tone_a"sv, &tone_a_idx},
|
||||
{"tone_b"sv, &tone_b_idx},
|
||||
{"dur_a"sv, &dur_a},
|
||||
{"dur_b"sv, &dur_b},
|
||||
{"gap"sv, &gap_ms},
|
||||
{"slot"sv, &preset_slot},
|
||||
{"custom_a"sv, &custom_freq_a_hz},
|
||||
{"custom_b"sv, &custom_freq_b_hz},
|
||||
{"preset1"sv, &preset_1},
|
||||
{"preset2"sv, &preset_2},
|
||||
{"preset3"sv, &preset_3},
|
||||
{"preset4"sv, &preset_4},
|
||||
{"preset5"sv, &preset_5},
|
||||
{"pname1"sv, &preset_name_1},
|
||||
{"pname2"sv, &preset_name_2},
|
||||
{"pname3"sv, &preset_name_3},
|
||||
{"pname4"sv, &preset_name_4},
|
||||
{"pname5"sv, &preset_name_5},
|
||||
}};
|
||||
|
||||
bool start_tx();
|
||||
void stop_tx();
|
||||
void on_tx_progress(uint32_t progress, bool done);
|
||||
void apply_timing_preset(size_t idx);
|
||||
void save_preset(uint32_t slot);
|
||||
void load_preset(uint32_t slot);
|
||||
std::string encode_preset() const;
|
||||
void decode_preset(const std::string& s);
|
||||
std::string& slot_ref(uint32_t slot);
|
||||
std::string& slot_name_ref(uint32_t slot);
|
||||
uint32_t tone_delta(uint32_t freq_x10) const;
|
||||
uint32_t ms_to_samples(uint32_t ms) const;
|
||||
size_t detect_timing_preset() const;
|
||||
void update_tx_time();
|
||||
void update_slot_name_display();
|
||||
|
||||
// --- Widgets ---
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "CTCSS:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 2 * 16}, "A:", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 2 * 16}, "B:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "ADur:", Theme::getInstance()->fg_light->foreground},
|
||||
{{9 * 8, 3 * 16}, "ms", Theme::getInstance()->fg_light->foreground},
|
||||
{{12 * 8, 3 * 16}, "BDur:", Theme::getInstance()->fg_light->foreground},
|
||||
{{21 * 8, 3 * 16}, "ms", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 4 * 16}, "Gap: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{9 * 8, 4 * 16}, "ms", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 5 * 16}, "Timing:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 6 * 16}, "Slot: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 9 * 16}, "AHz:", Theme::getInstance()->fg_light->foreground},
|
||||
{{8 * 8, 9 * 16}, "Hz", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 9 * 16}, "BHz:", Theme::getInstance()->fg_light->foreground},
|
||||
{{21 * 8, 9 * 16}, "Hz", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
OptionsField options_ctcss{
|
||||
{7 * 8, 1 * 16},
|
||||
9,
|
||||
{}};
|
||||
|
||||
OptionsField options_tone_a{
|
||||
{3 * 8, 2 * 16},
|
||||
9,
|
||||
{}};
|
||||
|
||||
OptionsField options_tone_b{
|
||||
{16 * 8, 2 * 16},
|
||||
9,
|
||||
{}};
|
||||
|
||||
NumberField field_dur_a{
|
||||
{5 * 8, 3 * 16},
|
||||
4,
|
||||
{100, 9900},
|
||||
100,
|
||||
' '};
|
||||
|
||||
NumberField field_dur_b{
|
||||
{17 * 8, 3 * 16},
|
||||
4,
|
||||
{100, 9900},
|
||||
100,
|
||||
' '};
|
||||
|
||||
// Digit-by-digit entry for custom tone frequency (integer Hz, 4 slots, 0100–9999)
|
||||
SymField symfield_custom_a{
|
||||
{4 * 8, 9 * 16},
|
||||
4,
|
||||
SymField::Type::Dec};
|
||||
|
||||
SymField symfield_custom_b{
|
||||
{17 * 8, 9 * 16},
|
||||
4,
|
||||
SymField::Type::Dec};
|
||||
|
||||
NumberField field_gap{
|
||||
{5 * 8, 4 * 16},
|
||||
4,
|
||||
{0, 9900},
|
||||
50,
|
||||
' '};
|
||||
|
||||
OptionsField options_timing{
|
||||
{8 * 8, 5 * 16},
|
||||
12,
|
||||
{{"Moto Std", 0},
|
||||
{"Short Alert", 1},
|
||||
{"Fire Std", 2},
|
||||
{"Long Alert", 3},
|
||||
{"Custom", 4}}};
|
||||
|
||||
NumberField field_slot{
|
||||
{7 * 8, 6 * 16},
|
||||
1,
|
||||
{1, 5},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Button button_save{
|
||||
{9 * 8, 6 * 16, 6 * 8, 20},
|
||||
"Save"};
|
||||
|
||||
Button button_load{
|
||||
{16 * 8, 6 * 16, 6 * 8, 20},
|
||||
"Load"};
|
||||
|
||||
// Shows the name of the currently selected preset slot (up to 8 visible chars)
|
||||
Text text_slot_name{
|
||||
{22 * 8, 6 * 16, 8 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_time{
|
||||
{0, 7 * 16, 30 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_status{
|
||||
{0, 8 * 16, 30 * 8, 16},
|
||||
""};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{2 * 8, 14 * 16, UI_POS_WIDTH_REMAINING(4), 16}};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
9};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto msg = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(msg.progress, msg.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::two_tone_pager
|
||||
|
||||
#endif /* __UI_TWO_TONE_PAGER_H__ */
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2024 PortaPack Mayhem
|
||||
*
|
||||
* 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_two_tone_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::two_tone_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<TwoToneRxView>();
|
||||
}
|
||||
} // namespace ui::external_app::two_tone_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_two_tone_rx.application_information"), used)) application_information_t _application_information_two_tone_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::two_tone_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "2-Tone RX",
|
||||
/*.bitmap_data = */ {
|
||||
// 16×16 icon — pager device with receive arrow
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xBA,
|
||||
0x5D,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x12,
|
||||
0x48,
|
||||
0x12,
|
||||
0x48,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
// Uses the proc_tonedetect baseband processor
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_tonedetect */ {'P', 'T', 'N', 'E'},
|
||||
/*.m4_app_offset = */ 0x00000000, // filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,599 @@
|
||||
/*
|
||||
* Copyright (C) 2024 PortaPack Mayhem
|
||||
*
|
||||
* 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_two_tone_rx.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::two_tone_rx {
|
||||
|
||||
static constexpr uint32_t DETECT_WINDOW_MS = 40;
|
||||
static constexpr Coord screen_width_px = 240;
|
||||
static constexpr Coord screen_height_px = 320;
|
||||
static constexpr Coord tone_log_top = 4 * 16;
|
||||
static constexpr Coord waterfall_top = 13 * 16;
|
||||
static constexpr Coord waterfall_height = 6 * 16;
|
||||
static constexpr Rect tone_log_rect{0, tone_log_top, 30 * 8, waterfall_top - tone_log_top};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tone tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const uint32_t MOTO_FREQS[45] = {
|
||||
2885,
|
||||
3047,
|
||||
3217,
|
||||
3396,
|
||||
3586,
|
||||
3786,
|
||||
3998,
|
||||
4221,
|
||||
4457,
|
||||
4705,
|
||||
4968,
|
||||
5246,
|
||||
5539,
|
||||
5848,
|
||||
6174,
|
||||
6519,
|
||||
6883,
|
||||
7268,
|
||||
7674,
|
||||
8102,
|
||||
8555,
|
||||
9032,
|
||||
9537,
|
||||
10073,
|
||||
10642,
|
||||
11225,
|
||||
11247,
|
||||
11534,
|
||||
11852,
|
||||
11885,
|
||||
12178,
|
||||
12514,
|
||||
12555,
|
||||
12858,
|
||||
13258,
|
||||
13576,
|
||||
13950,
|
||||
13996,
|
||||
14768,
|
||||
15579,
|
||||
16430,
|
||||
17325,
|
||||
18262,
|
||||
19245,
|
||||
20275,
|
||||
};
|
||||
|
||||
// Index 0 = None (0), indices 1–50 = standard CTCSS tones (freq × 10)
|
||||
static const uint32_t CTCSS_FREQS[51] = {
|
||||
0,
|
||||
670,
|
||||
719,
|
||||
744,
|
||||
770,
|
||||
797,
|
||||
825,
|
||||
854,
|
||||
885,
|
||||
915,
|
||||
948,
|
||||
974,
|
||||
1000,
|
||||
1035,
|
||||
1072,
|
||||
1109,
|
||||
1148,
|
||||
1188,
|
||||
1230,
|
||||
1273,
|
||||
1318,
|
||||
1365,
|
||||
1413,
|
||||
1462,
|
||||
1500,
|
||||
1514,
|
||||
1567,
|
||||
1598,
|
||||
1622,
|
||||
1655,
|
||||
1679,
|
||||
1713,
|
||||
1738,
|
||||
1773,
|
||||
1799,
|
||||
1835,
|
||||
1862,
|
||||
1899,
|
||||
1928,
|
||||
1966,
|
||||
1995,
|
||||
2035,
|
||||
2065,
|
||||
2107,
|
||||
2181,
|
||||
2257,
|
||||
2291,
|
||||
2336,
|
||||
2418,
|
||||
2503,
|
||||
2541,
|
||||
};
|
||||
|
||||
static std::string ctcss_name(uint32_t freq_x10) {
|
||||
if (freq_x10 == 0) return "None";
|
||||
return to_string_dec_uint(freq_x10 / 10) + "." +
|
||||
to_string_dec_uint(freq_x10 % 10) + "Hz";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MOTO table helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static constexpr uint32_t MOTO_NONE = 255;
|
||||
static constexpr uint32_t MOTO_TRANSITION_SNAP_HZ = 30; // stricter live transition tolerance
|
||||
static constexpr uint32_t MOTO_FINAL_MATCH_HZ = 60; // looser phase-end/logging tolerance
|
||||
static constexpr uint32_t MOTO_TRANSITION_DELTA_HZ = 25; // raw shift needed to confirm close-in A→B handoff
|
||||
|
||||
static uint32_t abs_diff_u32(uint32_t a, uint32_t b) {
|
||||
return (a > b) ? (a - b) : (b - a);
|
||||
}
|
||||
|
||||
// Return the nearest MOTO table index within match_hz, or MOTO_NONE.
|
||||
static uint32_t moto_index(uint32_t freq_hz, uint32_t match_hz = MOTO_TRANSITION_SNAP_HZ) {
|
||||
if (freq_hz == 0) return MOTO_NONE;
|
||||
uint32_t best_idx = MOTO_NONE;
|
||||
uint32_t best_diff = match_hz + 1;
|
||||
for (size_t i = 0; i < 45; i++) {
|
||||
const uint32_t hz = MOTO_FREQS[i] / 10;
|
||||
const uint32_t diff = (freq_hz > hz) ? (freq_hz - hz) : (hz - freq_hz);
|
||||
if (diff < best_diff) {
|
||||
best_diff = diff;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
return best_idx;
|
||||
}
|
||||
|
||||
// Format a detected tone for display, snapping to nearest MOTO table entry.
|
||||
static std::string format_tone(uint32_t freq_hz, uint32_t duration_ms) {
|
||||
const uint32_t idx = moto_index(freq_hz, MOTO_FINAL_MATCH_HZ);
|
||||
std::string freq_str;
|
||||
if (idx != MOTO_NONE) {
|
||||
const uint32_t matched_x10 = MOTO_FREQS[idx];
|
||||
freq_str = to_string_dec_uint(matched_x10 / 10) + "." +
|
||||
to_string_dec_uint(matched_x10 % 10) + "Hz";
|
||||
} else {
|
||||
freq_str = to_string_dec_uint(freq_hz) + "Hz";
|
||||
}
|
||||
return freq_str + " " + to_string_dec_uint(duration_ms) + "ms";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Waterfall rect
|
||||
// ---------------------------------------------------------------------------
|
||||
static constexpr ui::Rect waterfall_rect{0, waterfall_top, screen_width_px, waterfall_height};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TwoToneRxView
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void TwoToneRxView::focus() {
|
||||
button_startstop.focus();
|
||||
}
|
||||
|
||||
void TwoToneRxView::on_hide() {
|
||||
if (running_) stop_rx();
|
||||
}
|
||||
|
||||
TwoToneRxView::TwoToneRxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
prior_antenna_bias_ = get_antenna_bias();
|
||||
debug_file_.append(u"DEBUG/TWOTONERX.TXT");
|
||||
debug_file_.write_entry("==== two_tone_rx session start ====");
|
||||
|
||||
add_children({
|
||||
&field_frequency,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&field_volume,
|
||||
&labels,
|
||||
&options_ctcss,
|
||||
&field_squelch,
|
||||
&button_startstop,
|
||||
&button_clear,
|
||||
&text_status,
|
||||
&check_bias_t,
|
||||
&tone_log_view,
|
||||
});
|
||||
|
||||
// Populate CTCSS options
|
||||
using opt_t = std::pair<std::string, int32_t>;
|
||||
options_ctcss.set_options([&]() {
|
||||
std::vector<opt_t> opts;
|
||||
opts.reserve(51);
|
||||
for (size_t i = 0; i < 51; i++)
|
||||
opts.push_back({ctcss_name(CTCSS_FREQS[i]), (int32_t)i});
|
||||
return opts;
|
||||
}());
|
||||
options_ctcss.set_by_value((int32_t)ctcss_idx);
|
||||
options_ctcss.on_change = [this](size_t, int32_t v) {
|
||||
ctcss_idx = (uint32_t)v;
|
||||
if (running_) baseband::set_tonedetect_config((uint8_t)squelch_val, CTCSS_FREQS[ctcss_idx]);
|
||||
};
|
||||
|
||||
field_squelch.set_value((int32_t)squelch_val);
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
squelch_val = (uint32_t)v;
|
||||
if (running_) baseband::set_tonedetect_config((uint8_t)squelch_val, CTCSS_FREQS[ctcss_idx]);
|
||||
};
|
||||
|
||||
button_startstop.on_select = [this](Button&) {
|
||||
if (running_)
|
||||
stop_rx();
|
||||
else
|
||||
start_rx();
|
||||
};
|
||||
|
||||
button_clear.on_select = [this](Button&) {
|
||||
tone_log_entries_.clear();
|
||||
tone_log_view.set_dirty();
|
||||
text_status.set("");
|
||||
reset_detect_state();
|
||||
};
|
||||
|
||||
check_bias_t.set_value(bias_t_enabled);
|
||||
check_bias_t.on_select = [this](Checkbox&, bool v) {
|
||||
bias_t_enabled = v;
|
||||
apply_bias_t(running_);
|
||||
};
|
||||
|
||||
field_frequency.set_step(12500);
|
||||
tone_log_view.set_parent_rect(tone_log_rect);
|
||||
}
|
||||
|
||||
TwoToneRxView::~TwoToneRxView() {
|
||||
if (running_) stop_rx();
|
||||
}
|
||||
|
||||
void TwoToneRxView::reset_detect_state() {
|
||||
detect_state_ = DetectState::IDLE;
|
||||
phase_window_count_ = 0;
|
||||
phase_freq_accum_ = 0;
|
||||
phase_valid_windows_ = 0;
|
||||
phase_last_freq_ = 0;
|
||||
phase_last_is_first_ = true;
|
||||
t1_avg_freq_ = 0;
|
||||
t1_window_count_ = 0;
|
||||
t1_transition_candidate_windows_ = 0;
|
||||
t2_zero_window_count_ = 0;
|
||||
debug_trace_active_ = false;
|
||||
debug_trace_id_ = 0;
|
||||
}
|
||||
|
||||
void TwoToneRxView::start_rx() {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_hidden_offset(0);
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
apply_bias_t(true);
|
||||
|
||||
baseband::set_tonedetect_config((uint8_t)squelch_val, CTCSS_FREQS[ctcss_idx]);
|
||||
|
||||
add_child(&waterfall);
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
|
||||
running_ = true;
|
||||
button_startstop.set_text("Stop");
|
||||
text_status.set("");
|
||||
}
|
||||
|
||||
void TwoToneRxView::stop_rx() {
|
||||
remove_child(&waterfall);
|
||||
|
||||
apply_bias_t(false);
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
|
||||
running_ = false;
|
||||
button_startstop.set_text("Start");
|
||||
text_status.set("");
|
||||
reset_detect_state();
|
||||
}
|
||||
|
||||
void TwoToneRxView::apply_bias_t(bool active) {
|
||||
if (active && bias_t_enabled) {
|
||||
set_antenna_bias(true);
|
||||
receiver_model.set_antenna_bias();
|
||||
applied_bias_t_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!applied_bias_t_) return;
|
||||
|
||||
set_antenna_bias(prior_antenna_bias_);
|
||||
receiver_model.set_antenna_bias();
|
||||
applied_bias_t_ = false;
|
||||
}
|
||||
|
||||
void TwoToneRxView::debug_log(const std::string& line) {
|
||||
const std::string trace_prefix = debug_trace_active_
|
||||
? ("#" + to_string_dec_uint(debug_trace_id_) + " ")
|
||||
: "";
|
||||
const std::string entry = "[D] " + trace_prefix + line;
|
||||
debug_file_.write_entry(entry);
|
||||
}
|
||||
|
||||
void TwoToneRxView::debug_trace_begin(const std::string& line) {
|
||||
if (!debug_trace_active_) {
|
||||
debug_trace_id_ = ++debug_trace_counter_;
|
||||
debug_trace_active_ = true;
|
||||
}
|
||||
debug_log(line);
|
||||
}
|
||||
|
||||
void TwoToneRxView::finalize_detected_pair(uint32_t t2_avg, uint32_t t2_dur, const char* reason) {
|
||||
const uint32_t t1_dur = t1_window_count_ * DETECT_WINDOW_MS;
|
||||
const std::string suffix = std::string(" via ") + reason;
|
||||
|
||||
if (t1_avg_freq_ > 0 && t2_avg > 0 &&
|
||||
t1_dur >= 500 && t2_dur >= 500 &&
|
||||
moto_index(t1_avg_freq_, MOTO_FINAL_MATCH_HZ) != MOTO_NONE &&
|
||||
moto_index(t2_avg, MOTO_FINAL_MATCH_HZ) != MOTO_NONE) {
|
||||
debug_trace_begin("END ok T1=" + format_tone(t1_avg_freq_, t1_dur) +
|
||||
" T2=" + format_tone(t2_avg, t2_dur) + suffix);
|
||||
log_tone_pair(t1_avg_freq_, t1_dur, t2_avg, t2_dur);
|
||||
} else {
|
||||
debug_trace_begin("END drop T1=" + format_tone(t1_avg_freq_, t1_dur) +
|
||||
" T2=" + format_tone(t2_avg, t2_dur) + suffix);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// on_tone_data — window-based two-tone detection state machine
|
||||
//
|
||||
// Every 40 ms the baseband sends tone_end=false with a raw frequency estimate
|
||||
// for that window (freq_hz=0 if no stable QCII candidate).
|
||||
// When the CTCSS gate closes it sends tone_end=true (freq_hz=0).
|
||||
//
|
||||
// First and last windows of each phase are discarded:
|
||||
// - "First" is tracked with phase_last_is_first_.
|
||||
// - "Last" is phase_last_freq_, never added to the accumulator while
|
||||
// pending; discarded when the phase ends.
|
||||
//
|
||||
// T1→T2 transition: consecutive windows that snap to different MOTO table
|
||||
// entries. The transition window becomes T2's first window.
|
||||
//
|
||||
// Final matching is done from the phase-average raw estimates collected here,
|
||||
// not from the per-window nearest-table snap used for transition detection.
|
||||
// ---------------------------------------------------------------------------
|
||||
void TwoToneRxView::on_tone_data(const ToneDetectDataMessage* msg) {
|
||||
if (!running_) return;
|
||||
|
||||
if (msg->tone_end) {
|
||||
// Gate closed — finalize whatever phase we're in
|
||||
if (detect_state_ == DetectState::T2_COLLECTING) {
|
||||
// phase_last_freq_ is the last T2 window → discard it
|
||||
uint32_t t2_avg = (phase_valid_windows_ > 0)
|
||||
? (phase_freq_accum_ / phase_valid_windows_)
|
||||
: phase_last_freq_;
|
||||
uint32_t t2_dur = phase_window_count_ * DETECT_WINDOW_MS;
|
||||
finalize_detected_pair(t2_avg, t2_dur, "tone_end");
|
||||
} else if (debug_trace_active_) {
|
||||
debug_log("END before T2");
|
||||
}
|
||||
reset_detect_state();
|
||||
text_status.set("");
|
||||
return;
|
||||
}
|
||||
|
||||
// tone_end=false: this is a 40 ms measurement window
|
||||
const uint32_t freq = msg->freq_hz;
|
||||
if (freq == 0) {
|
||||
if (detect_state_ == DetectState::T2_COLLECTING) {
|
||||
const uint32_t t2_avg = (phase_valid_windows_ > 0)
|
||||
? (phase_freq_accum_ / phase_valid_windows_)
|
||||
: phase_last_freq_;
|
||||
const uint32_t t2_dur = phase_window_count_ * DETECT_WINDOW_MS;
|
||||
|
||||
if (t2_avg > 0 && t2_dur >= 2000) {
|
||||
finalize_detected_pair(t2_avg, t2_dur, "t2_break");
|
||||
reset_detect_state();
|
||||
text_status.set("");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// A single weak/noisy dropout is tolerated during T2 so standard 3 s
|
||||
// B tones are not lost to one marginal 40 ms estimate window.
|
||||
if (detect_state_ == DetectState::T2_COLLECTING && t2_zero_window_count_ == 0) {
|
||||
t2_zero_window_count_++;
|
||||
if (debug_trace_active_) {
|
||||
debug_log("T2 zero dropout");
|
||||
}
|
||||
text_status.set("T1:" + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS) +
|
||||
" T2 ?");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, treat the missing estimate as a break in the sequence.
|
||||
if (debug_trace_active_) {
|
||||
debug_log("RESET zero");
|
||||
}
|
||||
reset_detect_state();
|
||||
text_status.set("");
|
||||
return;
|
||||
}
|
||||
|
||||
t2_zero_window_count_ = 0;
|
||||
|
||||
switch (detect_state_) {
|
||||
case DetectState::IDLE:
|
||||
detect_state_ = DetectState::T1_COLLECTING;
|
||||
phase_window_count_ = 1;
|
||||
phase_freq_accum_ = 0;
|
||||
phase_valid_windows_ = 0;
|
||||
phase_last_freq_ = freq;
|
||||
phase_last_is_first_ = true;
|
||||
text_status.set("T1 ...");
|
||||
break;
|
||||
|
||||
case DetectState::T1_COLLECTING: {
|
||||
// Check for T1→T2 MOTO index transition.
|
||||
// Skip if phase_last_freq_ is the first (noisy) window — it's
|
||||
// marked for discard and must not trigger a false transition.
|
||||
if (freq > 0 && phase_last_freq_ > 0 && !phase_last_is_first_) {
|
||||
const uint32_t t1_ref_freq = (phase_valid_windows_ > 0)
|
||||
? (phase_freq_accum_ / phase_valid_windows_)
|
||||
: phase_last_freq_;
|
||||
const uint32_t idx_ref = moto_index(t1_ref_freq, MOTO_FINAL_MATCH_HZ);
|
||||
const uint32_t idx_last = moto_index(phase_last_freq_);
|
||||
const uint32_t idx_cur = moto_index(freq);
|
||||
const uint32_t idx_cur_final = moto_index(freq, MOTO_FINAL_MATCH_HZ);
|
||||
|
||||
bool transition_detected = false;
|
||||
if (idx_last != MOTO_NONE && idx_cur != MOTO_NONE && idx_last != idx_cur) {
|
||||
if (!debug_trace_active_) {
|
||||
debug_trace_begin("T1 start " + format_tone(t1_ref_freq, phase_window_count_ * DETECT_WINDOW_MS));
|
||||
}
|
||||
debug_log("T1->T2 snap " + format_tone(phase_last_freq_, DETECT_WINDOW_MS) +
|
||||
" -> " + format_tone(freq, DETECT_WINDOW_MS));
|
||||
transition_detected = true;
|
||||
} else if (idx_ref != MOTO_NONE &&
|
||||
idx_cur_final != MOTO_NONE &&
|
||||
idx_cur_final != idx_ref &&
|
||||
abs_diff_u32(freq, t1_ref_freq) >= MOTO_TRANSITION_DELTA_HZ) {
|
||||
t1_transition_candidate_windows_++;
|
||||
if (debug_trace_active_) {
|
||||
debug_log("T1 cand " + to_string_dec_uint(t1_transition_candidate_windows_) +
|
||||
" ref=" + to_string_dec_uint(t1_ref_freq) +
|
||||
" cur=" + to_string_dec_uint(freq));
|
||||
}
|
||||
transition_detected = (t1_transition_candidate_windows_ >= 2);
|
||||
} else {
|
||||
if (debug_trace_active_ && t1_transition_candidate_windows_ > 0) {
|
||||
debug_log("T1 cand reset");
|
||||
}
|
||||
t1_transition_candidate_windows_ = 0;
|
||||
}
|
||||
|
||||
if (transition_detected) {
|
||||
// Transition detected.
|
||||
// phase_last_freq_ is the last T1 window → discard.
|
||||
t1_avg_freq_ = (phase_valid_windows_ > 0)
|
||||
? (phase_freq_accum_ / phase_valid_windows_)
|
||||
: phase_last_freq_;
|
||||
t1_window_count_ = phase_window_count_;
|
||||
|
||||
// Start T2 with current window as first (also discarded)
|
||||
detect_state_ = DetectState::T2_COLLECTING;
|
||||
phase_window_count_ = 1;
|
||||
phase_freq_accum_ = 0;
|
||||
phase_valid_windows_ = 0;
|
||||
phase_last_freq_ = freq;
|
||||
phase_last_is_first_ = true;
|
||||
t1_transition_candidate_windows_ = 0;
|
||||
t2_zero_window_count_ = 0;
|
||||
|
||||
if (!debug_trace_active_) {
|
||||
debug_trace_begin("T1 start " + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS));
|
||||
}
|
||||
debug_log("T2 start " + format_tone(freq, DETECT_WINDOW_MS));
|
||||
text_status.set("T1:" + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS) + " T2...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No transition: promote pending window into accumulator (unless first)
|
||||
if (!phase_last_is_first_ && phase_last_freq_ > 0) {
|
||||
phase_freq_accum_ += phase_last_freq_;
|
||||
phase_valid_windows_++;
|
||||
}
|
||||
phase_last_freq_ = freq;
|
||||
phase_last_is_first_ = false;
|
||||
phase_window_count_++;
|
||||
|
||||
if (!debug_trace_active_ &&
|
||||
phase_window_count_ >= 3 &&
|
||||
moto_index(phase_last_freq_, MOTO_FINAL_MATCH_HZ) != MOTO_NONE) {
|
||||
const uint32_t t1_ref_freq = (phase_valid_windows_ > 0)
|
||||
? (phase_freq_accum_ / phase_valid_windows_)
|
||||
: phase_last_freq_;
|
||||
debug_trace_begin("T1 start " + format_tone(t1_ref_freq, phase_window_count_ * DETECT_WINDOW_MS));
|
||||
}
|
||||
|
||||
text_status.set("T1 " + to_string_dec_uint(phase_window_count_ * DETECT_WINDOW_MS) + "ms...");
|
||||
break;
|
||||
}
|
||||
|
||||
case DetectState::T2_COLLECTING:
|
||||
// Promote pending window into accumulator (unless first)
|
||||
if (!phase_last_is_first_ && phase_last_freq_ > 0) {
|
||||
phase_freq_accum_ += phase_last_freq_;
|
||||
phase_valid_windows_++;
|
||||
}
|
||||
phase_last_freq_ = freq;
|
||||
phase_last_is_first_ = false;
|
||||
phase_window_count_++;
|
||||
|
||||
text_status.set("T1:" + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS) +
|
||||
" T2 " + to_string_dec_uint(phase_window_count_ * DETECT_WINDOW_MS) + "ms");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TwoToneRxView::log_tone_pair(uint32_t f1, uint32_t d1_ms, uint32_t f2, uint32_t d2_ms) {
|
||||
// Keep the oldest entry at the top so newer pairs appear below it.
|
||||
tone_log_entries_.push_back({++next_log_serial_, format_tone(f1, d1_ms) + " " + format_tone(f2, d2_ms)});
|
||||
tone_log_view.set_dirty();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::two_tone_rx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<ui::external_app::two_tone_rx::TwoToneLogEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
ui::RecentEntriesColumns&) {
|
||||
std::string line = entry.line;
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (C) 2024 PortaPack Mayhem
|
||||
*
|
||||
* 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_TWO_TONE_RX_H__
|
||||
#define __UI_TWO_TONE_RX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "message.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
namespace ui::external_app::two_tone_rx {
|
||||
|
||||
struct TwoToneLogEntry {
|
||||
using Key = uint32_t;
|
||||
|
||||
static constexpr Key invalid_key = 0;
|
||||
|
||||
Key serial{};
|
||||
std::string line{};
|
||||
|
||||
Key key() const { return serial; }
|
||||
};
|
||||
|
||||
using TwoToneLogEntries = RecentEntries<TwoToneLogEntry>;
|
||||
|
||||
class TwoToneRxView : public View {
|
||||
public:
|
||||
TwoToneRxView(NavigationView& nav);
|
||||
~TwoToneRxView();
|
||||
|
||||
TwoToneRxView(const TwoToneRxView&) = delete;
|
||||
TwoToneRxView(TwoToneRxView&&) = delete;
|
||||
TwoToneRxView& operator=(const TwoToneRxView&) = delete;
|
||||
TwoToneRxView& operator=(TwoToneRxView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
void on_hide() override;
|
||||
std::string title() const override { return "2-Tone RX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
bool running_{false};
|
||||
|
||||
TwoToneLogEntries tone_log_entries_{};
|
||||
uint32_t next_log_serial_{0};
|
||||
|
||||
uint32_t squelch_val{50};
|
||||
uint32_t ctcss_idx{0};
|
||||
bool bias_t_enabled{false};
|
||||
bool prior_antenna_bias_{false};
|
||||
bool applied_bias_t_{false};
|
||||
LogFile debug_file_{};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_twotone",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"tone_sq"sv, &squelch_val},
|
||||
{"ctcss"sv, &ctcss_idx},
|
||||
{"bias_t"sv, &bias_t_enabled},
|
||||
}};
|
||||
|
||||
void start_rx();
|
||||
void stop_rx();
|
||||
void apply_bias_t(bool active);
|
||||
void debug_log(const std::string& line);
|
||||
void debug_trace_begin(const std::string& line);
|
||||
void on_tone_data(const ToneDetectDataMessage* msg);
|
||||
void log_tone_pair(uint32_t f1, uint32_t d1_ms, uint32_t f2, uint32_t d2_ms);
|
||||
void finalize_detected_pair(uint32_t t2_avg, uint32_t t2_dur, const char* reason);
|
||||
void reset_detect_state();
|
||||
|
||||
// ── Two-tone detection state machine ─────────────────────────────────────
|
||||
//
|
||||
// Each 40 ms measurement window from the baseband arrives as tone_end=false.
|
||||
// tone_end=true signals that the CTCSS gate closed (end of transmission).
|
||||
//
|
||||
// Collection rule: discard first and last window of each phase.
|
||||
// - "First" is always discarded (may contain carrier ramp-up).
|
||||
// - "Last" is never added to the accumulator while pending; it is
|
||||
// discarded when the next window confirms it is not the last, OR when
|
||||
// the phase ends (tone_end=true or T1→T2 transition).
|
||||
//
|
||||
// Transition detection: two consecutive windows that snap to different MOTO
|
||||
// table entries trigger T1→T2. The transition window becomes T2's first
|
||||
// window (also discarded as T2's first).
|
||||
|
||||
enum class DetectState : uint8_t { IDLE,
|
||||
T1_COLLECTING,
|
||||
T2_COLLECTING };
|
||||
DetectState detect_state_{DetectState::IDLE};
|
||||
|
||||
// Per-phase window collection
|
||||
uint32_t phase_window_count_{0}; // total windows seen in this phase (including first)
|
||||
uint32_t phase_freq_accum_{0}; // sum of non-first, non-last window frequencies
|
||||
uint32_t phase_valid_windows_{0}; // count of windows in phase_freq_accum_
|
||||
uint32_t phase_last_freq_{0}; // pending window (not yet added; discarded if last)
|
||||
bool phase_last_is_first_{true}; // true when phase_last_freq_ is window 1 (discard)
|
||||
|
||||
// T1 result (stored at T1→T2 transition for use when pair is logged)
|
||||
uint32_t t1_avg_freq_{0};
|
||||
uint32_t t1_window_count_{0};
|
||||
uint8_t t1_transition_candidate_windows_{0};
|
||||
uint8_t t2_zero_window_count_{0};
|
||||
bool debug_trace_active_{false};
|
||||
uint32_t debug_trace_id_{0};
|
||||
uint32_t debug_trace_counter_{0};
|
||||
|
||||
// ── Row 0: frequency + RF controls ───────────────────────────────────────
|
||||
|
||||
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)}};
|
||||
|
||||
// ── Row 1: CTCSS / Squelch / Start+Stop / Clear ──────────────────────────
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "CTCSS:", Theme::getInstance()->fg_light->foreground},
|
||||
{{15 * 8, 1 * 16}, "Sq:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
OptionsField options_ctcss{
|
||||
{7 * 8, 1 * 16},
|
||||
8,
|
||||
{}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{19 * 8, 1 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Button button_startstop{
|
||||
{21 * 8, 1 * 16, 5 * 8, 18},
|
||||
"Start"};
|
||||
|
||||
Button button_clear{
|
||||
{26 * 8, 1 * 16, 4 * 8, 18},
|
||||
"Clr"};
|
||||
|
||||
// ── Row 2: live status (active tone in progress) + Bias-T toggle ─────────
|
||||
|
||||
Text text_status{
|
||||
{0, 2 * 16 + 4, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Checkbox check_bias_t{
|
||||
{20 * 8, 2 * 16 + 2},
|
||||
9,
|
||||
"Bias-T",
|
||||
true};
|
||||
|
||||
ui::RecentEntriesColumns tone_log_columns{{
|
||||
{"Tones", 0},
|
||||
}};
|
||||
ui::RecentEntriesTable<TwoToneLogEntries> tone_log_view{tone_log_entries_, tone_log_columns};
|
||||
|
||||
// ── Waterfall (rest of screen) ────────────────────────────────────────────
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
// ── Message handler ───────────────────────────────────────────────────────
|
||||
|
||||
MessageHandlerRegistration message_handler_tone{
|
||||
Message::ID::ToneDetectData,
|
||||
[this](const Message* const p) {
|
||||
const auto* msg = reinterpret_cast<const ToneDetectDataMessage*>(p);
|
||||
this->on_tone_data(msg);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::two_tone_rx
|
||||
|
||||
#endif /* __UI_TWO_TONE_RX_H__ */
|
||||
+1
-1
@@ -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,
|
||||
|
||||
|
||||
@@ -57,3 +57,4 @@ 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";
|
||||
|
||||
@@ -58,6 +58,7 @@ 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;
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ options_t freqman_steps = {
|
||||
{"100kHz (FM2)", 100000},
|
||||
{"250kHz (N2)", 250000},
|
||||
{"500kHz (WFM)", 500000},
|
||||
{"750kHz ", 750000},
|
||||
{"1MHz ", 1000000},
|
||||
};
|
||||
|
||||
@@ -156,6 +157,7 @@ options_t freqman_steps_short = {
|
||||
{"100kHz", 100000},
|
||||
{"250kHz", 250000},
|
||||
{"500kHz", 500000},
|
||||
{"750kHz", 750000},
|
||||
{"1MHz", 1000000},
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ enum class freqman_type : uint8_t {
|
||||
_100kHz,
|
||||
_250kHz,
|
||||
_500kHz,
|
||||
_750kHz,
|
||||
_1MHz,
|
||||
Unknown,
|
||||
* };
|
||||
@@ -130,6 +131,7 @@ enum class freqman_type : uint8_t {
|
||||
freqman_step_info{ freqman_step::_100kHz, "100kHz", "100kHz (FM2)", 100'000 },
|
||||
freqman_step_info{ freqman_step::_250kHz, "250kHz", "250kHz (N2)", 250'000 },
|
||||
freqman_step_info{ freqman_step::_500kHz, "500kHz", "500kHz (WFM)", 500'000 },
|
||||
freqman_step_info{ freqman_step::_750kHz, "750kHz", "750kHz ", 750'000 },
|
||||
freqman_step_info{ freqman_step::_1MHz, "1MHz", "1MHz ", 1'000'000 },
|
||||
freqman_step_info{ freqman_step::Unknown, "Unknown", "Unknown ", 0 },
|
||||
* };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "gpio_lpc.h"
|
||||
#include "gpio.h"
|
||||
|
||||
typedef enum {
|
||||
LED1 = 0,
|
||||
@@ -8,7 +9,7 @@ typedef enum {
|
||||
} led_t;
|
||||
|
||||
/* GPIO Output PinMux */
|
||||
static struct gpio_t gpio_led[] = {
|
||||
static struct gpio gpio_led[] = {
|
||||
GPIO(2, 1),
|
||||
GPIO(2, 2),
|
||||
GPIO(2, 8),
|
||||
|
||||
@@ -182,20 +182,24 @@ void MAX2831::set_mode(const Mode mode) {
|
||||
case Mode::Shutdown:
|
||||
gpio_max2831_rx_enable.write(0); /* RXTX=0 */
|
||||
gpio_max283x_enable.write(0); /* ENABLE=0 */
|
||||
set_rssi_mux(0);
|
||||
break;
|
||||
case Mode::Standby:
|
||||
gpio_max2831_rx_enable.write(1); /* RXTX=1 */
|
||||
gpio_max283x_enable.write(0); /* ENABLE=0 */
|
||||
set_rssi_mux(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 */
|
||||
set_rssi_mux(2); // transmit power
|
||||
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 */
|
||||
set_rssi_mux(1); // RSSI
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -475,5 +479,38 @@ void MAX2831::write(const address_t reg_num, const reg_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
void MAX2831::set_rssi_mux(const uint8_t mode) {
|
||||
/* RSSI MUX allows switching the RSSI output between different internal signals.
|
||||
* 0 = disable mux
|
||||
* 1 = RSSI
|
||||
* 2 = TX_POWER
|
||||
* 3 = TEMP
|
||||
*/
|
||||
uint16_t mux_val = 0;
|
||||
|
||||
// Select the appropriate constant based on the input mode.
|
||||
if (mode == 0) {
|
||||
mux_val = 0;
|
||||
} else {
|
||||
// Select the appropriate constant based on the input mode.
|
||||
switch (mode) {
|
||||
case 3:
|
||||
mux_val = REG8_RSSI_MUX_TEMP;
|
||||
break;
|
||||
case 2:
|
||||
mux_val = REG8_RSSI_MUX_TX_POWER;
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
mux_val = REG8_RSSI_MUX_RSSI;
|
||||
break;
|
||||
}
|
||||
mux_val |= REG8_RSSI_EN;
|
||||
}
|
||||
|
||||
set_reg_field(8, REG8_RSSI_MUX_MASK | REG8_RSSI_EN, mux_val);
|
||||
flush_reg(8);
|
||||
}
|
||||
|
||||
} // namespace max2831
|
||||
#endif
|
||||
|
||||
@@ -151,6 +151,7 @@ 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_RSSI_EN = (1 << 10);
|
||||
|
||||
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);
|
||||
@@ -213,6 +214,8 @@ class MAX2831 : public MAX283x {
|
||||
reg_t read(const address_t reg_num) override;
|
||||
void write(const address_t reg_num, const reg_t value) override;
|
||||
|
||||
void set_rssi_mux(const uint8_t mode);
|
||||
|
||||
private:
|
||||
spi::arbiter::Target& _target;
|
||||
Mode _mode{Mode::Standby};
|
||||
|
||||
@@ -361,18 +361,31 @@ class Si5351 {
|
||||
}
|
||||
|
||||
void wait_for_device_ready() {
|
||||
#ifndef PRALINE
|
||||
while (device_status() & 0x80);
|
||||
#else
|
||||
#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;
|
||||
}
|
||||
@@ -386,13 +399,14 @@ class Si5351 {
|
||||
}
|
||||
|
||||
void reset_plls() {
|
||||
#ifndef PRALINE
|
||||
// Datasheet recommends value 0xac, though the low nibble bits are not defined in AN619.
|
||||
write_register(Register::PLLReset, 0xac);
|
||||
#else
|
||||
#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);
|
||||
@@ -413,6 +427,7 @@ class Si5351 {
|
||||
|
||||
#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
|
||||
@@ -421,6 +436,15 @@ class Si5351 {
|
||||
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(
|
||||
|
||||
@@ -392,18 +392,8 @@ static void set_cpu_clock_speed() {
|
||||
#endif
|
||||
|
||||
cgu::pll1::enable();
|
||||
#ifndef PRALINE
|
||||
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();
|
||||
#else
|
||||
#ifdef PRALINE
|
||||
// Wait for PLL1 to lock with timeout
|
||||
{
|
||||
uint32_t timeout = 100000;
|
||||
@@ -423,6 +413,18 @@ static void set_cpu_clock_speed() {
|
||||
/* 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
|
||||
}
|
||||
|
||||
@@ -589,25 +591,6 @@ init_status_t init() {
|
||||
|
||||
clock_manager.init_clock_generator();
|
||||
|
||||
#ifdef PRALINE
|
||||
// Force CLK4/CLK5 configuration BEFORE I2C bus stops
|
||||
// This ensures the inversion bits are written while I2C is still active
|
||||
|
||||
// CLK4 (MAX2831): ON, Integer, PLLA, INVERTED, MS_Self, 4mA = 0x5D
|
||||
clock_manager.si5351_write_register(20, 0x5D);
|
||||
|
||||
// CLK5 (RFFC5072): ON, Integer, PLLA, INVERTED, MS_Self, 6mA = 0x5E
|
||||
clock_manager.si5351_write_register(21, 0x5E);
|
||||
|
||||
// Enable CLK4 and CLK5 outputs NOW (before I2C stops)
|
||||
uint8_t reg3 = clock_manager.si5351_read_register(3);
|
||||
reg3 &= ~0x30; // Clear bits 4 and 5 to enable
|
||||
clock_manager.si5351_write_register(3, reg3);
|
||||
|
||||
// Wait for clocks to stabilize
|
||||
chThdSleepMilliseconds(10);
|
||||
#endif
|
||||
|
||||
i2c0.stop();
|
||||
|
||||
chThdSleepMilliseconds(10);
|
||||
@@ -653,7 +636,7 @@ init_status_t init() {
|
||||
// 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();
|
||||
int load_result = fpga_bridge_init(&shared_memory.bb_data.data[0]);
|
||||
(void)load_result; // Ignore result for now, just let boot continue
|
||||
|
||||
/* RELEASE FPGA RESET */
|
||||
@@ -679,6 +662,7 @@ 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()) {
|
||||
|
||||
@@ -131,4 +131,29 @@ void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const
|
||||
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();
|
||||
|
||||
for (size_t i = 0; i < len; i++)
|
||||
add_data(data[i]);
|
||||
|
||||
add_checksum();
|
||||
|
||||
add_flag();
|
||||
add_flag();
|
||||
|
||||
flush();
|
||||
}
|
||||
|
||||
} /* namespace ax25 */
|
||||
|
||||
@@ -44,6 +44,7 @@ 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, 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);
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
#include "rf_path.hpp"
|
||||
|
||||
#include "rffc507x.hpp"
|
||||
#include "max2837.hpp"
|
||||
#include "max2839.hpp"
|
||||
|
||||
#ifdef PRALINE
|
||||
#include "max2831.hpp"
|
||||
@@ -33,6 +31,8 @@ extern "C" {
|
||||
#include "fpga_bridge.h"
|
||||
}
|
||||
#else
|
||||
#include "max2837.hpp"
|
||||
#include "max2839.hpp"
|
||||
#include "baseband_cpld.hpp"
|
||||
#endif
|
||||
|
||||
@@ -79,6 +79,15 @@ static constexpr SPIConfig ssp_config_max283x = {
|
||||
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 = {
|
||||
@@ -113,11 +122,11 @@ static spi::arbiter::Target ssp1_target_max5864{
|
||||
static rf::path::Path rf_path;
|
||||
rffc507x::RFFC507x first_if;
|
||||
max283x::MAX283x* second_if;
|
||||
max2837::MAX2837 second_if_max2837{ssp1_target_max283x};
|
||||
max2839::MAX2839 second_if_max2839{ssp1_target_max283x};
|
||||
#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 baseband::CPLD baseband_cpld;
|
||||
#endif
|
||||
static max5864::MAX5864 baseband_codec{ssp1_target_max5864};
|
||||
@@ -171,12 +180,21 @@ void init() {
|
||||
|
||||
/* 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(1, 0x01); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
|
||||
fpga_debug_register_write(2, 0x00); // RX_DECIM=No Decim
|
||||
fpga_debug_register_write(3, 0x00); // TX_CTRL=0
|
||||
fpga_debug_register_write(4, 0x00); // TX_INTRP=0
|
||||
fpga_debug_register_write(5, 0x00); // TX_PSTEP=0
|
||||
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
|
||||
@@ -197,6 +215,24 @@ void set_direction(const rf::Direction new_direction) {
|
||||
|
||||
#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;
|
||||
@@ -379,7 +415,11 @@ void set_rx_max283x_iq_phase_calibration(const size_t v) {
|
||||
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);
|
||||
|
||||
@@ -510,14 +550,22 @@ void register_write(const size_t register_number, uint32_t value) {
|
||||
}
|
||||
|
||||
void init() {
|
||||
// Initialize FPGA registers after bitstream load
|
||||
// DC_BLOCK (bit 0) must be enabled for RX to work
|
||||
fpga_debug_register_write(1, 0x01); // CTRL: DC_BLOCK=1
|
||||
fpga_debug_register_write(2, 0x00); // RX_DECIM: no decimation
|
||||
fpga_debug_register_write(3, 0x00); // TX_CTRL: NCO disabled
|
||||
fpga_debug_register_write(4, 0x00); // TX_INTRP: no interpolation
|
||||
fpga_debug_register_write(5, 0x00); // TX_PSTEP: zero phase step
|
||||
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
|
||||
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 */
|
||||
|
||||
@@ -64,6 +64,7 @@ void disable();
|
||||
|
||||
#ifdef PRALINE
|
||||
void invalidate_spi_config();
|
||||
void set_rx_buff_vcm(const size_t v);
|
||||
#endif
|
||||
|
||||
namespace debug {
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
#include "dsp_iir_config.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#ifdef PRALINE
|
||||
extern "C" {
|
||||
#include "fpga_bridge.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
using namespace hackrf::one;
|
||||
using namespace portapack;
|
||||
|
||||
@@ -248,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();
|
||||
@@ -312,17 +314,6 @@ void ReceiverModel::update_tuning_frequency() {
|
||||
// TODO: use positive offset if freq < offset.
|
||||
if (enabled_) {
|
||||
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
|
||||
|
||||
#ifdef PRALINE
|
||||
/* Praline: Must re-apply baseband filter after frequency change
|
||||
* Reference: hackrf_usb radio.c radio_set_frequency()
|
||||
*
|
||||
* Different frequency ranges may use different quarter-shift modes,
|
||||
* which affects the required LPF bandwidth. For now we just
|
||||
* recalculate the filter to be safe.
|
||||
*/
|
||||
update_baseband_bandwidth();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,20 +325,47 @@ void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
|
||||
void ReceiverModel::update_baseband_bandwidth() {
|
||||
if (enabled_) {
|
||||
#ifdef PRALINE
|
||||
/* Praline: LPF bandwidth calculation
|
||||
* Reference: hackrf_usb radio.c radio_set_filter()
|
||||
/*
|
||||
* PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c
|
||||
*
|
||||
* LPF = (sample_rate * 3) / 8
|
||||
* Plus additional offset if quarter-shift is enabled (not implemented yet)
|
||||
* 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 lpf_bandwidth = (sampling_rate() * 3) / 8;
|
||||
|
||||
// For now, quarter-shift is disabled, so no offset added
|
||||
// When quarter-shift is implemented:
|
||||
// if (quarter_shift_enabled) {
|
||||
// uint32_t offset = (sampling_rate() << decimation_n) / 8;
|
||||
// lpf_bandwidth += offset * 2;
|
||||
// }
|
||||
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
|
||||
@@ -366,6 +384,12 @@ void ReceiverModel::update_sampling_rate() {
|
||||
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() {
|
||||
|
||||
@@ -48,11 +48,7 @@ class ReceiverModel {
|
||||
|
||||
struct settings_t {
|
||||
uint32_t baseband_bandwidth = max283x::filter::bandwidth_minimum;
|
||||
#ifdef PRALINE
|
||||
uint32_t sampling_rate = 4'000'000;
|
||||
#else
|
||||
uint32_t sampling_rate = 3'072'000;
|
||||
#endif
|
||||
rf::Frequency frequency_step = 25'000;
|
||||
rf::Frequency frequency_app_override = 0;
|
||||
uint8_t lna_gain_db = 32;
|
||||
|
||||
@@ -83,9 +83,7 @@ constexpr GPIOs gpios{
|
||||
gpio_rx_amp,
|
||||
gpio_not_rx_amp_pwr,
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef PRALINE
|
||||
/* HackRF One Config struct - not used on PRALINE */
|
||||
struct Config {
|
||||
using base_type = uint16_t;
|
||||
@@ -186,9 +184,7 @@ struct Config {
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif /* !PRALINE */
|
||||
|
||||
#ifndef PRALINE
|
||||
/* HackRF One config table - not used on PRALINE */
|
||||
using ConfigAmp = std::array<Config, 2>;
|
||||
using ConfigDirection = std::array<ConfigAmp, 2>;
|
||||
@@ -229,7 +225,7 @@ constexpr Config get_config(
|
||||
const bool amplify) {
|
||||
return config_table[toUType(band)][toUType(direction)][amplify ? 1 : 0];
|
||||
}
|
||||
#endif /* !PRALINE */
|
||||
#endif /* PRALINE */
|
||||
|
||||
} /* namespace */
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ bool card_present = false;
|
||||
Status status_{Status::NotPresent};
|
||||
|
||||
FRESULT mount() {
|
||||
return f_mount(&fs, reinterpret_cast<const TCHAR*>(_T("")), 0);
|
||||
return f_mount(&fs, reinterpret_cast<const TCHAR*>(_T("")), 1);
|
||||
}
|
||||
|
||||
} /* namespace */
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "ui_btngrid.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "sd_card.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
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<size_t>(item_count)) {
|
||||
if (static_cast<size_t>(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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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], markerList[i].color, markerList[i].color, Color::black());
|
||||
draw_marker_item(painter, markerList[i], markerList[i].color, markerList[i].color, Color::magenta());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +47,16 @@ void RSSI::paint(Painter& painter) {
|
||||
const auto r = screen_rect();
|
||||
|
||||
constexpr int rssi_sample_range = 256;
|
||||
// constexpr float rssi_voltage_min = 0.4;
|
||||
constexpr float rssi_voltage_min = 0.4;
|
||||
#ifdef PRALINE
|
||||
constexpr float rssi_voltage_max = 2.4;
|
||||
#else
|
||||
constexpr float rssi_voltage_max = 2.2;
|
||||
#endif
|
||||
|
||||
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 int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
|
||||
constexpr int raw_min = (rssi_sample_range * rssi_voltage_min) / adc_voltage_max;
|
||||
constexpr int raw_max = (int)(((rssi_sample_range * rssi_voltage_max) / adc_voltage_max) + 0.5f);
|
||||
constexpr int raw_delta = raw_max - raw_min;
|
||||
|
||||
if (!vertical_rssi_enabled) {
|
||||
@@ -428,25 +432,38 @@ void RSSIGraph::paint(Painter& painter) {
|
||||
void RSSIGraph::add_values(int16_t rssi_min, int16_t rssi_avg, int16_t rssi_max, int16_t db) {
|
||||
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.4V to 2.4V
|
||||
*/
|
||||
|
||||
constexpr int rssi_sample_range = 256;
|
||||
// constexpr float rssi_voltage_min = 0.4;
|
||||
constexpr float rssi_voltage_min = 0.4;
|
||||
#ifdef PRALINE
|
||||
constexpr float rssi_voltage_max = 2.4;
|
||||
#else
|
||||
constexpr float rssi_voltage_max = 2.2;
|
||||
#endif
|
||||
|
||||
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 int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
|
||||
constexpr int raw_min = (rssi_sample_range * rssi_voltage_min) / adc_voltage_max;
|
||||
constexpr int raw_max = (int)(((rssi_sample_range * rssi_voltage_max) / adc_voltage_max) + 0.5f);
|
||||
constexpr int raw_delta = raw_max - raw_min;
|
||||
|
||||
// vertical bottom to top level meters
|
||||
// y_avg
|
||||
const range_t<int> y_avg_range{0, r.height() - 1};
|
||||
const int16_t y_avg = y_avg_range.clip((rssi_avg - raw_min) * r.height() / raw_delta);
|
||||
// y_min
|
||||
const range_t<int> y_min_range{0, y_avg};
|
||||
const int16_t y_min = y_min_range.clip((rssi_min - raw_min) * r.height() / raw_delta);
|
||||
const range_t<int> y_max_range{y_avg + 1, r.height() - 1};
|
||||
// y_max
|
||||
const range_t<int> y_max_range{y_avg, r.height() - 1};
|
||||
const int16_t y_max = y_max_range.clip((rssi_max - raw_min) * r.height() / raw_delta);
|
||||
// range
|
||||
const range_t<int> db_range{-80, 10};
|
||||
int16_t db_ = db_range.clip(db);
|
||||
db_ = db_ - 10;
|
||||
db_ -= 10;
|
||||
db_ = db_ * r.height() / 90;
|
||||
db_ = r.height() + db_;
|
||||
|
||||
|
||||
@@ -64,10 +64,11 @@ class RSSI : public Widget {
|
||||
void set_db(int16_t db);
|
||||
|
||||
private:
|
||||
int8_t min_ = 0;
|
||||
int8_t avg_ = 0;
|
||||
int8_t max_ = 0;
|
||||
int8_t peak_ = 0;
|
||||
uint8_t min_ = 0;
|
||||
uint8_t avg_ = 0;
|
||||
uint8_t max_ = 0;
|
||||
uint8_t peak_ = 0;
|
||||
|
||||
size_t peak_duration_ = 0;
|
||||
int16_t db_ = 0;
|
||||
bool instant_exec_{false};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user