mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-20 06:29:02 +00:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1a2318fe4 | |||
| 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 | |||
| 56adca3bf6 | |||
| cd7c412aa4 | |||
| 16c4c28584 | |||
| 10d17fa7ce | |||
| e63f54c0a2 | |||
| b09efb4d3c | |||
| bfbbacd43c | |||
| 065d01b591 | |||
| b197a4b1de | |||
| 85eaa9d800 | |||
| 1ad092f341 | |||
| 0fe51fa7e3 | |||
| 4cc2ba690b | |||
| abee022000 | |||
| 430c8e5e79 | |||
| 7ea3eec016 | |||
| eb524f7110 | |||
| 3413d71bb3 | |||
| c64dddf5cc | |||
| 3d56bccd49 | |||
| 705e4f125f | |||
| 363b27efe8 | |||
| f8ed7489c9 | |||
| 0685fb7a9f | |||
| 6498f5a1b2 | |||
| 77f747aab2 | |||
| 819b4d0fed | |||
| 15df51f976 | |||
| e9e78609b7 | |||
| 785bd06938 | |||
| 300a4b03c6 | |||
| eaa07f8369 | |||
| c31a978e6f | |||
| 93775129cb | |||
| fe9448f149 |
@@ -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
|
||||
|
||||
@@ -83,5 +83,3 @@ venv/
|
||||
# generated bitmap arr file
|
||||
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
||||
/firmware/tools/bitmap.hpp
|
||||
# flashsize is generated by cmake
|
||||
/firmware/flashsize.h
|
||||
|
||||
+1
-4
@@ -43,7 +43,7 @@ if(NOT DEFINED FLASH_MB_LIMIT_SIZE)
|
||||
set(FLASH_MB_LIMIT_SIZE 3.5) #PRALINE 4 MB = 3.5 firmware + 0.5 MB fpga bitstream.
|
||||
else()
|
||||
set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB")
|
||||
|
||||
@@ -58,9 +58,6 @@ else()
|
||||
math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024")
|
||||
endif()
|
||||
|
||||
#generate h files
|
||||
configure_file(flashsize.h.in ${CMAKE_CURRENT_SOURCE_DIR}/firmware/flashsize.h)
|
||||
|
||||
option(USE_CCACHE "Enable ccache, please keep an eye on this because sometimes it's not quite stable and generated unusable binary" OFF)
|
||||
|
||||
project(portapack-h1)
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
+5
-2
@@ -15,8 +15,11 @@
|
||||
# - Get a shell inside the build image to
|
||||
# inspect problems: ./dockerize.sh shell
|
||||
# - Give additional parameters to the container:
|
||||
# ./dockerize.sh -j10
|
||||
# ./dockerize.sh ninja -j10
|
||||
# Additional parameters to make: ./dockerize.sh -j10
|
||||
# Use ninja instead of make: ./dockerize.sh ninja -j10
|
||||
# Addotopnal CMake parameters: ./dockerize.sh -DBOARD=PRALINE
|
||||
# Change the default build directory: ./dockerize.sh -B build-alt
|
||||
# (Alternatively --workdir works as well)
|
||||
# - Use a different dockerfile:
|
||||
# ./dockerize.sh build dockerfile-other
|
||||
# - Use a different cpu architecture:
|
||||
|
||||
+33
-26
@@ -48,43 +48,50 @@ if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_EQUAL ${EXPECTED_GCC_VERSION})
|
||||
set(GCC_VERSION_MISMATCH 1)
|
||||
endif()
|
||||
|
||||
#generate h files
|
||||
configure_file(flashsize.h.in flashsize.h)
|
||||
add_compile_options(-include ${CMAKE_CURRENT_BINARY_DIR}/flashsize.h)
|
||||
|
||||
add_subdirectory(application)
|
||||
add_subdirectory(baseband)
|
||||
add_subdirectory(standalone)
|
||||
add_subdirectory(test)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
VERBATIM
|
||||
)
|
||||
if(BOARD STREQUAL "PRALINE")
|
||||
math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024")
|
||||
set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin)
|
||||
set(APPEND_FPGA_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/append_fpga_bitstream.py)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
# PRALINE: Append FPGA bitstream to firmware at offset 0x380000
|
||||
# The base firmware is 3.5MB, FPGA bitstream gets appended at the end of it, final size is 4MB
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Appending PRALINE FPGA bitstream to firmware..."
|
||||
COMMAND python3 ${APPEND_FPGA_SCRIPT} ${FIRMWARE_FILENAME} ${PRALINE_FPGA_BIN} ${FIRMWARE_FILENAME}.tmp ${PRALINE_FINAL_SIZE}
|
||||
COMMAND ${CMAKE_COMMAND} -E rename ${FIRMWARE_FILENAME}.tmp ${FIRMWARE_FILENAME}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
${PRALINE_FPGA_BIN} ${APPEND_FPGA_SCRIPT}
|
||||
VERBATIM
|
||||
)
|
||||
else() # Sadly this part had to be duplicated (or at least I couldn't find a better way to do it)
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
firmware
|
||||
DEPENDS ${FIRMWARE_FILENAME} ${HACKRF_FIRMWARE_DFU_FILENAME} ${HACKRF_FIRMWARE_FILENAME}
|
||||
)
|
||||
|
||||
# PRALINE: Append FPGA bitstream to firmware at offset 0x180000
|
||||
# The base firmware is 1MB, FPGA goes at 1MB offset, final size is 2MB
|
||||
if(BOARD STREQUAL "PRALINE")
|
||||
math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024")
|
||||
set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin)
|
||||
set(APPEND_FPGA_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/append_fpga_bitstream.py)
|
||||
add_custom_command(
|
||||
TARGET firmware POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Appending PRALINE FPGA bitstream to firmware..."
|
||||
COMMAND python3 ${APPEND_FPGA_SCRIPT} ${FIRMWARE_FILENAME} ${PRALINE_FPGA_BIN} ${FIRMWARE_FILENAME}.tmp ${PRALINE_FINAL_SIZE}
|
||||
COMMAND ${CMAKE_COMMAND} -E rename ${FIRMWARE_FILENAME}.tmp ${FIRMWARE_FILENAME}
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "PRALINE firmware with FPGA bitstream created:"
|
||||
COMMAND ls -la ${FIRMWARE_FILENAME}
|
||||
COMMENT "Appending PRALINE FPGA bitstream"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
if(${GCC_VERSION_MISMATCH})
|
||||
set(COMPILER_MISMATCH_MESSAGE "WARNING: Compiler version mismatch, please use the official compiler version ${EXPECTED_GCC_VERSION} when sharing builds! Current compiler version: ${CMAKE_CXX_COMPILER_VERSION}")
|
||||
message(${COMPILER_MISMATCH_MESSAGE})
|
||||
|
||||
@@ -258,6 +258,7 @@ set(CPPSRC
|
||||
ook_file.cpp
|
||||
ui_baseband_stats_view.cpp
|
||||
ui_navigation.cpp
|
||||
ui_notifications.cpp
|
||||
ui_record_view.cpp
|
||||
ui_sd_card_status_view.cpp
|
||||
ui/ui_alphanum.cpp
|
||||
@@ -304,7 +305,6 @@ set(CPPSRC
|
||||
apps/ui_mictx.cpp
|
||||
apps/ui_modemsetup.cpp
|
||||
apps/ui_playlist.cpp
|
||||
apps/ui_pocsag_tx.cpp
|
||||
apps/ui_rds.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_recon.cpp
|
||||
|
||||
@@ -199,6 +199,55 @@ SPECOptionsView::SPECOptionsView(
|
||||
view->set_spec_iq_phase_calibration_value(view->get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio
|
||||
}
|
||||
|
||||
/* PralineOptionsView *******************************************************/
|
||||
|
||||
#ifdef PRALINE
|
||||
PralineOptionsView::PralineOptionsView(Rect parent_rect, const Style* style) {
|
||||
set_style(style);
|
||||
set_parent_rect(parent_rect);
|
||||
add_children({&label_sr, &options_sr, &label_dc, &options_dc,
|
||||
&label_qi, &options_qi, &label_qs, &options_qs});
|
||||
|
||||
// READ the current state from hardware
|
||||
fpga_reg_1 = radio::debug::fpga::register_read(1);
|
||||
|
||||
// INITIALIZE UI WIDGETS based on read bits
|
||||
options_dc.set_by_value((fpga_reg_1 & 0x01) ? 1 : 0); // Bit 0
|
||||
options_qi.set_by_value((fpga_reg_1 & 0x02) ? 1 : 0); // Bit 1
|
||||
options_qs.set_by_value((fpga_reg_1 & 0x04) ? 1 : 0); // Bit 2
|
||||
options_sr.set_value(receiver_model.sampling_rate());
|
||||
|
||||
options_sr.on_change = [this](int32_t v) { receiver_model.set_sampling_rate(static_cast<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() {
|
||||
radio::debug::fpga::register_write(1, fpga_reg_1);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* AnalogAudioView *******************************************************/
|
||||
|
||||
AnalogAudioView::AnalogAudioView(
|
||||
@@ -217,7 +266,13 @@ AnalogAudioView::AnalogAudioView(
|
||||
&field_volume,
|
||||
&text_ctcss,
|
||||
&record_view,
|
||||
&waterfall});
|
||||
#ifdef PRALINE
|
||||
&waterfall,
|
||||
&button_pro
|
||||
#else
|
||||
&waterfall
|
||||
#endif
|
||||
});
|
||||
|
||||
// Filename Datetime and Frequency
|
||||
record_view.set_filename_date_frequency(true);
|
||||
@@ -256,6 +311,10 @@ AnalogAudioView::AnalogAudioView(
|
||||
field_frequency.set_value(receiver_model.target_frequency() + offset);
|
||||
};
|
||||
|
||||
#ifdef PRALINE
|
||||
button_pro.on_select = [this](Button&) { this->on_show_options_praline(); };
|
||||
#endif
|
||||
|
||||
audio::output::start();
|
||||
|
||||
// This call starts the correct baseband image to run
|
||||
@@ -553,4 +612,11 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) {
|
||||
void AnalogAudioView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
void AnalogAudioView::on_show_options_praline() {
|
||||
set_options_widget(std::make_unique<PralineOptionsView>(options_view_rect, Theme::getInstance()->option_active));
|
||||
}
|
||||
#endif
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -37,6 +37,38 @@ namespace ui {
|
||||
|
||||
class AnalogAudioView;
|
||||
|
||||
#ifdef PRALINE
|
||||
class PralineOptionsView : public View {
|
||||
public:
|
||||
PralineOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
// Layout: SR (Sample Rate), DC (DC Block), QI (Q-Invert), QS (Quarter Shift), D (Decim)
|
||||
Text label_sr{{UI_POS_X(0), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "SR"};
|
||||
|
||||
// Parameters: Position, Length (in chars), Range, Step, and default value
|
||||
NumberField options_sr{
|
||||
{UI_POS_X(3), UI_POS_Y(0)},
|
||||
8, // Number of digits to show
|
||||
{0, 40000000}, // Range: 0 to 40,000 kHz
|
||||
100, // Step: 100 Hz
|
||||
' ' // Fix: Single quotes for char
|
||||
};
|
||||
|
||||
Text label_dc{{UI_POS_X(14), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "DC"};
|
||||
OptionsField options_dc{{UI_POS_X(16), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}};
|
||||
|
||||
Text label_qi{{UI_POS_X(20), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "QI"};
|
||||
OptionsField options_qi{{UI_POS_X(22), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}};
|
||||
|
||||
Text label_qs{{UI_POS_X(26), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, "QS"};
|
||||
OptionsField options_qs{{UI_POS_X(28), UI_POS_Y(0)}, 2, {{"Of", 0}, {"On", 1}}};
|
||||
|
||||
uint8_t fpga_reg_1{0x00}; // Tracks DC, QI, QS bits
|
||||
void update_fpga_ctrl();
|
||||
};
|
||||
#endif
|
||||
|
||||
class AMOptionsView : public View {
|
||||
public:
|
||||
AMOptionsView(AnalogAudioView* view, Rect parent_rect, const Style* style);
|
||||
@@ -222,6 +254,11 @@ class AnalogAudioView : public View {
|
||||
uint8_t get_previous_zoom_option();
|
||||
void set_previous_zoom_option(uint8_t zoom);
|
||||
|
||||
#ifdef PRALINE
|
||||
Button button_pro{{UI_POS_X(12), UI_POS_Y(2), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "PRO"};
|
||||
void on_show_options_praline();
|
||||
#endif
|
||||
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -199,6 +199,7 @@ void BLETxView::send_packet() {
|
||||
generateRandomMacAddress(randomMac);
|
||||
|
||||
char advertisementData[63] = {0};
|
||||
packets[current_packet].advertisementData[62] = '\0';
|
||||
strcpy(advertisementData, packets[current_packet].advertisementData);
|
||||
|
||||
// TODO: Make this a checkbox.
|
||||
@@ -225,7 +226,7 @@ void BLETxView::send_packet() {
|
||||
uint8_t min = 0x00;
|
||||
uint8_t max = 0x0F;
|
||||
|
||||
hexDigit = min + std::rand() % (max - min + 1);
|
||||
hexDigit = min + rand() % (max - min + 1);
|
||||
} break;
|
||||
default:
|
||||
hexDigit = 0;
|
||||
|
||||
@@ -271,10 +271,17 @@ ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
&text_frame_pos_even,
|
||||
&text_frame_pos_odd,
|
||||
&button_aircraft_details,
|
||||
&button_see_map});
|
||||
&button_see_map,
|
||||
&opt_map_list});
|
||||
|
||||
text_icao_address.set(entry_.icao_str);
|
||||
|
||||
opt_map_list.on_change = [this](size_t, uint32_t mf) {
|
||||
map_filter = mf;
|
||||
clear_map_markers(); // reset them
|
||||
pos_history.clear(); // erase the trail
|
||||
};
|
||||
|
||||
button_aircraft_details.on_select = [this, &nav](Button&) {
|
||||
aircraft_details_view_ = nav.push<ADSBRxAircraftDetailsView>(entry_);
|
||||
nav.set_on_pop([this]() {
|
||||
@@ -288,7 +295,7 @@ ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
get_map_tag(entry_),
|
||||
entry_.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
GeoPos::spd_unit::HIDDEN,
|
||||
GeoPos::spd_unit::KNOTS,
|
||||
entry_.pos.latitude,
|
||||
entry_.pos.longitude,
|
||||
entry_.velo.heading);
|
||||
@@ -312,17 +319,107 @@ void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
// AC Details view is showing, nothing to update.
|
||||
} else if (geomap_view_) {
|
||||
// Map is showing, update the current item.
|
||||
if (map_filter == 1) {
|
||||
// add to map trail history
|
||||
add_map_trail(entry);
|
||||
}
|
||||
geomap_view_->update_tag(get_map_tag(entry_));
|
||||
geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.velo.speed);
|
||||
geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.get_ground_speed());
|
||||
} else {
|
||||
// Details is showing, update details.
|
||||
refresh_ui();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b) {
|
||||
static const struct {
|
||||
int32_t alt;
|
||||
uint8_t r, g, b;
|
||||
} stops[] = {
|
||||
{0, 220, 220, 220}, // 0 ft: White/Grey (Ground)
|
||||
{2000, 255, 255, 0}, // 2k ft: Yellow
|
||||
{10000, 0, 255, 0}, // 10k ft: Green
|
||||
{20000, 0, 255, 255}, // 20k ft: Cyan
|
||||
{30000, 60, 60, 255}, // 30k ft: Blue (Lightened for visibility on Black)
|
||||
{40000, 255, 0, 255} // 40k+ ft: Magenta
|
||||
};
|
||||
if (alt_ft <= stops[0].alt) {
|
||||
*r = stops[0].r;
|
||||
*g = stops[0].g;
|
||||
*b = stops[0].b;
|
||||
return;
|
||||
}
|
||||
const int count = sizeof(stops) / sizeof(stops[0]);
|
||||
if (alt_ft >= stops[count - 1].alt) {
|
||||
*r = stops[count - 1].r;
|
||||
*g = stops[count - 1].g;
|
||||
*b = stops[count - 1].b;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
if (alt_ft < stops[i + 1].alt) {
|
||||
float t = (float)(alt_ft - stops[i].alt) / (stops[i + 1].alt - stops[i].alt);
|
||||
|
||||
*r = (uint8_t)(stops[i].r + (stops[i + 1].r - stops[i].r) * t);
|
||||
*g = (uint8_t)(stops[i].g + (stops[i + 1].g - stops[i].g) * t);
|
||||
*b = (uint8_t)(stops[i].b + (stops[i + 1].b - stops[i].b) * t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: in case no interval matched, default to ground color.
|
||||
*r = stops[0].r;
|
||||
*g = stops[0].g;
|
||||
*b = stops[0].b;
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::clear_map_markers() {
|
||||
if (geomap_view_)
|
||||
geomap_view_->clear_markers();
|
||||
if (map_filter == 1)
|
||||
refresh_trail_markers(); // re add trail
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::refresh_trail_markers() {
|
||||
if (!geomap_view_)
|
||||
return;
|
||||
|
||||
geomap_view_->clear_markers();
|
||||
for (const auto& pos : pos_history) {
|
||||
GeoMarker marker{};
|
||||
marker.lon = pos.lon;
|
||||
marker.lat = pos.lat;
|
||||
marker.angle = pos.heading;
|
||||
marker.tag = ""; // No tag for trail points
|
||||
uint8_t r, g, b;
|
||||
get_altitude_color(pos.altitude, &r, &g, &b);
|
||||
marker.color = Color(r, g, b);
|
||||
geomap_view_->store_marker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::add_map_trail(const AircraftRecentEntry& entry) {
|
||||
if (!geomap_view_)
|
||||
return;
|
||||
if (entry.pos.pos_valid == false)
|
||||
return;
|
||||
|
||||
if (!pos_history.empty()) {
|
||||
const auto& last_pos = pos_history.back();
|
||||
const float THRESH_DEG = 0.009f;
|
||||
const float THRESH_SQ = THRESH_DEG * THRESH_DEG; // 0.00002025f
|
||||
|
||||
float d_lat = entry.pos.latitude - last_pos.lat;
|
||||
float d_lon = entry.pos.longitude - last_pos.lon;
|
||||
if ((d_lat * d_lat) + (d_lon * d_lon) < THRESH_SQ) {
|
||||
return; // Moved less than ~1000m, skip (rough estimate, varies with latitude but good enough for our purposes). This prevents adding too many points when the plane is circling or taxiing.
|
||||
}
|
||||
}
|
||||
|
||||
// Only keep the last 30 positions in the trail.
|
||||
if (pos_history.size() >= 30)
|
||||
pos_history.erase(pos_history.begin());
|
||||
pos_history.push_back({entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude});
|
||||
refresh_trail_markers();
|
||||
}
|
||||
|
||||
bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
@@ -330,11 +427,16 @@ bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
if (!geomap_view_)
|
||||
return false;
|
||||
|
||||
if (map_filter == 1) return false; // this is the 'only me' option, so skip all others
|
||||
|
||||
GeoMarker marker{};
|
||||
marker.lon = entry.pos.longitude;
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = get_map_tag(entry);
|
||||
uint8_t r, g, b;
|
||||
get_altitude_color(entry.pos.altitude, &r, &g, &b);
|
||||
marker.color = Color(r, g, b);
|
||||
|
||||
auto markerStored = geomap_view_->store_marker(marker);
|
||||
return markerStored == MARKER_STORED;
|
||||
@@ -651,14 +753,15 @@ void ADSBRxView::refresh_ui() {
|
||||
// Process the entries list.
|
||||
for (const auto& entry : recent) {
|
||||
// Found the entry being shown in details view. Update it.
|
||||
if (entry.key() == detail_key) {
|
||||
if (entry.key() == detail_key) { // we don't add it to the markers, since this is the currently selected and shown in the middle one. This eliminates the "shadow" marker, and saves ram
|
||||
details_view->update(entry);
|
||||
current_updated = true;
|
||||
}
|
||||
|
||||
// NB: current entry also gets a marker so it shows up if map is panned.
|
||||
if (map_needs_update && entry.pos.pos_valid && entry.state <= ADSBAgeState::Recent) {
|
||||
map_needs_update = details_view->add_map_marker(entry);
|
||||
} else {
|
||||
// Add others only
|
||||
// NB: current entry also gets a marker so it shows up if map is panned.
|
||||
if (map_needs_update && entry.pos.pos_valid && entry.state <= ADSBAgeState::Recent) {
|
||||
map_needs_update = details_view->add_map_marker(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Any work left to do?
|
||||
|
||||
@@ -64,9 +64,9 @@ namespace ui {
|
||||
#define VEL_AIR_SUBSONIC 3
|
||||
#define VEL_AIR_SUPERSONIC 4
|
||||
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm
|
||||
#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS_OSM 8 // "other" map marker redraw interval for osm
|
||||
#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map
|
||||
|
||||
/* Thresholds (in seconds) that define the transition between ages. */
|
||||
struct ADSBAgeLimit {
|
||||
@@ -150,6 +150,19 @@ struct AircraftRecentEntry {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
int32_t get_ground_speed() const {
|
||||
if (velo.valid == false) return 0;
|
||||
if (velo.type == SPD_GND)
|
||||
return velo.speed;
|
||||
else if (velo.type == SPD_IAS) {
|
||||
if (!pos.alt_valid) return velo.speed; // can't correct without altitude, so just return IAS
|
||||
return (int32_t)(velo.speed * (1.0f + (0.02f * (pos.altitude / 1000.0f))));
|
||||
} else if (velo.type == SPD_TAS)
|
||||
return velo.speed; // We don't know the wind speed
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
void inc_age(int delta) {
|
||||
age += delta;
|
||||
|
||||
@@ -208,50 +221,50 @@ class ADSBRxAircraftDetailsView : public View {
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Registration:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Manufacturer:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 5 * 16}, "Model:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 7 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 8 * 16}, "Number of engines:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 9 * 16}, "Engine type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 11 * 16}, "Owner:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 13 * 16}, "Operator:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Registration:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(3)}, "Manufacturer:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(5)}, "Model:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(7)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(8)}, "Number of engines:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(9)}, "Engine type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(11)}, "Owner:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(13)}, "Operator:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_icao_address{
|
||||
{5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(5), UI_POS_Y(1), 6 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_registration{
|
||||
{13 * 8, 2 * 16, 8 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(2), 8 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_manufacturer{
|
||||
{UI_POS_X(0), 4 * 16, 19 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(4), 19 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_model{
|
||||
{UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_type{
|
||||
{5 * 8, 7 * 16, 22 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(5), UI_POS_Y(7), 22 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_number_of_engines{
|
||||
{18 * 8, 8 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(18), UI_POS_Y(8), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_engine_type{
|
||||
{UI_POS_X(0), 10 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(10), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_owner{
|
||||
{UI_POS_X(0), 12 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(12), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_operator{
|
||||
{UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Button button_close{
|
||||
@@ -270,11 +283,14 @@ class ADSBRxDetailsView : public View {
|
||||
void focus() override;
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
void get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b);
|
||||
/* Calls forwarded to map view if shown. */
|
||||
bool map_active() const { return geomap_view_; }
|
||||
void clear_map_markers();
|
||||
/* Adds a marker for the entry to the map. Returns true on success. */
|
||||
bool add_map_marker(const AircraftRecentEntry& entry);
|
||||
void add_map_trail(const AircraftRecentEntry& entry);
|
||||
void refresh_trail_markers();
|
||||
|
||||
std::string title() const override { return "Details"; }
|
||||
|
||||
@@ -297,57 +313,72 @@ class ADSBRxDetailsView : public View {
|
||||
// if removed from the recent entries list.
|
||||
AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key};
|
||||
bool airline_checked{false};
|
||||
uint8_t map_filter{0}; // 0: all, 1: only this, set by opt_map_list.
|
||||
struct PosHistory {
|
||||
float lat;
|
||||
float lon;
|
||||
uint16_t heading;
|
||||
int32_t altitude;
|
||||
};
|
||||
std::vector<PosHistory> pos_history{};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 1 * 16}, "Callsign:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Last seen:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Airline:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 5 * 16}, "Country:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 13 * 16}, "Even position frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 15 * 16}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(13), UI_POS_Y(0)}, "Callsign:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Last seen:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Airline:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "Country:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(7)}, "Map filter:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(13)}, "Even position frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_icao_address{
|
||||
{5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(5), UI_POS_Y(0), 6 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_callsign{
|
||||
{22 * 8, 1 * 16, 8 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(22), UI_POS_Y(0), 8 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_last_seen{
|
||||
{11 * 8, 2 * 16, 19 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(11), UI_POS_Y(1), 19 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_airline{
|
||||
{UI_POS_X(0), 4 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(3), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_country{
|
||||
{8 * 8, 5 * 16, 22 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(8), UI_POS_Y(4), 22 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_infos{
|
||||
{UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(5), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_info2{
|
||||
{UI_POS_X(0), 7 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_frame_pos_even{
|
||||
{UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_frame_pos_odd{
|
||||
{UI_POS_X(0), 16 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(16), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Button button_aircraft_details{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
{UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"A/C details"};
|
||||
|
||||
OptionsField opt_map_list{
|
||||
{UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(7)},
|
||||
12,
|
||||
{{"All", 0},
|
||||
{"Only this", 1}}};
|
||||
|
||||
Button button_see_map{
|
||||
{UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
{UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"See on map"};
|
||||
|
||||
MessageHandlerRegistration message_handler_gps{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -438,6 +438,123 @@ class RadioDiagnosticsView : public View {
|
||||
"Done"};
|
||||
};
|
||||
|
||||
#ifdef PRALINE
|
||||
class PralineDebugMenuView : public BtnGridView {
|
||||
public:
|
||||
PralineDebugMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Pro Debug"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
/* Praline-Specific Radio Debug View
|
||||
* Monitors Mixer Lock, SPI Bit Depth, and toggles Si5351 CLK5
|
||||
*/
|
||||
class PralineRadioDebugView : public View {
|
||||
public:
|
||||
PralineRadioDebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Pro Radio Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void toggle_clk5();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== Pro Radio Debug ==="};
|
||||
|
||||
// Mixer Lock (PD_11 / GPIO6[25])
|
||||
Text text_lbl_lock{{0, 24, 124, 16}, "Mix Lock (LD):"};
|
||||
Text text_lock_status{{126, 24, 114, 16}, "---"};
|
||||
|
||||
// Si5351 CLK5 (Mixer Reference)
|
||||
Text text_lbl_clk5{{0, 40, 124, 16}, "Si5351 CLK5(Mix):"};
|
||||
Text text_clk5_status{{126, 40, 114, 16}, "---"};
|
||||
|
||||
// SPI Bus configuration check
|
||||
Text text_lbl_spi{{0, 56, 124, 16}, "SPI Bit Mode:"};
|
||||
Text text_spi_status{{126, 56, 118, 16}, "---"};
|
||||
|
||||
// FPGA DC Block (Reg 1)
|
||||
Text text_lbl_fpga_ctrl{{0, 72, 124, 16}, "FPGA Reg 1:"};
|
||||
Text text_fpga_ctrl{{126, 72, 118, 16}, "---"};
|
||||
|
||||
// VAA Power Rail (P8_1 / GPIO4[1])
|
||||
Text text_lbl_vaa{{0, 88, 124, 16}, "VAA RF Power:"};
|
||||
Text text_vaa_status{{126, 88, 118, 16}, "---"};
|
||||
|
||||
Text text_status_msg{{0, 110, 240, 48}, ""};
|
||||
|
||||
Button button_refresh{{8, 240, 72, 24}, "Refresh"};
|
||||
Button button_toggle_clk5{{88, 240, 72, 24}, "CLK5_T"};
|
||||
Button button_done{{168, 240, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
/* WFMAudioDebugView ***************************************************/
|
||||
|
||||
#ifdef PRALINE
|
||||
class WFMAudioDebugView : public View {
|
||||
public:
|
||||
WFMAudioDebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "WFM Audio Debug"; }
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== WFM Audio Debug ==="};
|
||||
|
||||
// Sample rates section
|
||||
Text text_lbl_clk0{{0, 20, 140, 16}, "Si5351 CLK0 Rate:"};
|
||||
Text text_clk0{{150, 20, 90, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_dec{{0, 36, 140, 16}, "FPGA Decimation:"};
|
||||
Text text_fpga_dec{{150, 36, 90, 16}, "---"};
|
||||
|
||||
Text text_lbl_post_fpga{{0, 52, 140, 16}, "Post-FPGA Rate:"};
|
||||
Text text_post_fpga{{150, 52, 90, 16}, "---"};
|
||||
|
||||
// MAX2831 section
|
||||
Text text_section1{{0, 72, 240, 16}, "--- MAX2831 LPF ---"};
|
||||
|
||||
Text text_lbl_reg8{{0, 88, 140, 16}, "Reg8 (LPF RX):"};
|
||||
Text text_reg8{{150, 88, 90, 16}, "---"};
|
||||
|
||||
Text text_lbl_lpf_bw{{0, 104, 140, 16}, "LPF Bandwidth:"};
|
||||
Text text_lpf_bw{{150, 104, 90, 16}, "---"};
|
||||
|
||||
// FPGA section
|
||||
Text text_section2{{0, 124, 240, 16}, "--- FPGA Control ---"};
|
||||
|
||||
Text text_lbl_fpga_r1{{0, 140, 140, 16}, "Reg1 (Ctrl):"};
|
||||
Text text_fpga_r1{{150, 140, 90, 16}, "---"};
|
||||
|
||||
Text text_lbl_dc_q{{0, 156, 70, 16}, "DC/Q/QS:"};
|
||||
Text text_dc_q{{72, 156, 168, 16}, "---"};
|
||||
|
||||
// Audio section
|
||||
Text text_section3{{0, 176, 240, 16}, "--- Audio Path ---"};
|
||||
|
||||
Text text_lbl_expected{{0, 192, 96, 16}, "Expctd Aud:"};
|
||||
Text text_expected{{98, 192, 142, 16}, "---"};
|
||||
|
||||
Text text_lbl_deemph{{0, 208, 96, 16}, "D-emph Cfg:"};
|
||||
Text text_deemph{{98, 208, 132, 16}, "---"};
|
||||
|
||||
// Status
|
||||
Text text_status{{0, 228, 240, 16}, "---"};
|
||||
Text text_status2{{0, 244, 240, 16}, "---"};
|
||||
|
||||
Button button_refresh{{10, 268, 70, 24}, "Refresh"};
|
||||
Button button_toggle_q{{90, 268, 70, 24}, "Toggle Q"};
|
||||
Button button_done{{170, 268, 60, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
/* BasebandStatusView ***************************************************/
|
||||
|
||||
class BasebandStatusView : public View {
|
||||
@@ -643,34 +760,36 @@ class Si5351DebugView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_title{{8, 16, 200, 16}, "Si5351 Clock Generator"};
|
||||
Text text_title{{0, 0, 200, 16}, "Si5351 Clock Generator"};
|
||||
|
||||
Text text_status_label{{8, 40, 80, 16}, "Status Reg:"};
|
||||
Text text_status_value{{96, 40, 144, 16}, ""};
|
||||
Text text_status_label{{0, 16, 80, 16}, "Status Reg:"};
|
||||
Text text_status_value{{96, 16, 144, 16}, ""};
|
||||
|
||||
Text text_pll_a_label{{8, 60, 80, 16}, "PLL A:"};
|
||||
Text text_pll_a_status{{96, 60, 144, 16}, ""};
|
||||
Text text_pll_a_label{{0, 32, 80, 16}, "PLL A:"};
|
||||
Text text_pll_a_status{{96, 32, 144, 16}, ""};
|
||||
|
||||
Text text_pll_b_label{{8, 80, 80, 16}, "PLL B:"};
|
||||
Text text_pll_b_status{{96, 80, 144, 16}, ""};
|
||||
Text text_pll_b_label{{0, 48, 80, 16}, "PLL B:"};
|
||||
Text text_pll_b_status{{96, 48, 144, 16}, ""};
|
||||
|
||||
Text text_sys_init_label{{8, 100, 80, 16}, "SYS_INIT:"};
|
||||
Text text_sys_init_status{{96, 100, 144, 16}, ""};
|
||||
Text text_sys_init_label{{0, 64, 80, 16}, "SYS_INIT:"};
|
||||
Text text_sys_init_status{{96, 64, 144, 16}, ""};
|
||||
|
||||
Text text_xtal_cap_label{{8, 120, 80, 16}, "XTAL Cap:"};
|
||||
Text text_xtal_cap_value{{96, 120, 144, 16}, ""};
|
||||
Text text_xtal_cap_label{{0, 80, 80, 16}, "XTAL Cap:"};
|
||||
Text text_xtal_cap_value{{96, 80, 144, 16}, ""};
|
||||
|
||||
Text text_clk0_label{{8, 150, 72, 16}, "CLK0:"};
|
||||
Text text_clk0_status{{88, 150, 152, 16}, ""};
|
||||
Text text_clk0_label{{0, 96, 48, 16}, "CLK0:"};
|
||||
Text text_clk0_status{{50, 96, 28, 16}, ""};
|
||||
Text text_clk0_freq_value{{80, 96, 160, 16}, ""};
|
||||
Text text_clk0_div_value{{50, 112, 190, 16}, ""};
|
||||
|
||||
Text text_clk0_freq_label{{8, 170, 72, 16}, " Freq:"};
|
||||
Text text_clk0_freq_value{{88, 170, 152, 16}, ""};
|
||||
Text text_clk1_label{{0, 128, 96, 16}, "CLK1 (SCT):"};
|
||||
Text text_clk1_status{{112, 128, 128, 16}, ""};
|
||||
|
||||
Text text_clk0_div_label{{8, 190, 72, 16}, " Div:"};
|
||||
Text text_clk0_div_value{{88, 190, 152, 16}, ""};
|
||||
Text text_clk4_label{{0, 144, 96, 16}, "CLK4 (MAX):"};
|
||||
Text text_clk4_status{{112, 144, 128, 16}, ""};
|
||||
|
||||
Text text_clk1_label{{8, 210, 96, 16}, "CLK1 (SCT):"};
|
||||
Text text_clk1_status{{112, 210, 128, 16}, ""};
|
||||
Text text_clk5_label{{0, 160, 96, 16}, "CLK5 (RFFC):"};
|
||||
Text text_clk5_status{{112, 160, 128, 16}, ""};
|
||||
|
||||
Button button_refresh{{8, 240, 72, 24}, "Refresh"};
|
||||
Button button_reset_pll{{88, 240, 72, 24}, "Reset PLL"};
|
||||
@@ -694,7 +813,7 @@ class SignalPathStatusView : public View {
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== Signal Path Status ==="};
|
||||
|
||||
Text text_lbl_max_enable{{0, 20, 1114, 16}, "MAX2831:"};
|
||||
Text text_lbl_max_enable{{0, 20, 114, 16}, "MAX2831:"};
|
||||
Text text_max_enable{{116, 20, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_max_mode{{0, 36, 114, 16}, "RX Mode:"};
|
||||
@@ -703,20 +822,88 @@ class SignalPathStatusView : public View {
|
||||
Text text_lbl_rf_path{{0, 52, 114, 16}, "RF Path:"};
|
||||
Text text_rf_path{{116, 52, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_rf_amp{{0, 68, 114, 16}, "RF Amp:"};
|
||||
Text text_rf_amp{{116, 68, 124, 16}, "---"};
|
||||
Text text_lbl_filter{{0, 68, 114, 16}, "Filter Band:"};
|
||||
Text text_filter{{116, 68, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_lna{{0, 84, 114, 16}, "LNA Gain:"};
|
||||
Text text_lna{{116, 84, 124, 16}, "---"};
|
||||
Text text_lbl_mixer{{0, 84, 114, 16}, "Mixer Enable:"};
|
||||
Text text_mixer{{116, 84, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_vga{{0, 100, 114, 16}, "VGA Gain:"};
|
||||
Text text_vga{{116, 100, 124, 16}, "---"};
|
||||
Text text_lbl_rf_amp{{0, 100, 114, 16}, "RF Amp:"};
|
||||
Text text_rf_amp{{116, 100, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_decim{{0, 116, 114, 16}, "FPGA Decim:"};
|
||||
Text text_fpga_decim{{116, 116, 124, 16}, "---"};
|
||||
Text text_lbl_lna{{0, 116, 114, 16}, "LNA Gain:"};
|
||||
Text text_lna{{116, 116, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 140, 240, 32}, ""};
|
||||
Text text_lbl_vga{{0, 132, 114, 16}, "VGA Gain:"};
|
||||
Text text_vga{{116, 132, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_decim{{0, 148, 114, 16}, "FPGA Decim:"};
|
||||
Text text_fpga_decim{{116, 148, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl_dc_q{{0, 164, 114, 16}, "FPGA Ctrl:"};
|
||||
Text text_fpga_ctrl_dc_q{{116, 164, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl_qs{{0, 180, 114, 16}, ""};
|
||||
Text text_fpga_ctrl_qs{{116, 180, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 200, 240, 32}, ""};
|
||||
|
||||
Button button_refresh{{0, 280, 72, 24}, "Refresh"};
|
||||
Button button_toggle_q{{88, 280, 72, 24}, "Q Inv"};
|
||||
Button button_done{{176, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class GPIODebugView : public View {
|
||||
public:
|
||||
GPIODebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "GPIO Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
// GPIO4 (LPF and RF Amp)
|
||||
Text text_lbl_gpio4{{0, 0, 240, 16}, "Pin Diag: GPIO4 (LPF|Amp|Mix)"};
|
||||
|
||||
Text text_lbl_mixr1{{0, 18, 56, 16}, "MixR1:"};
|
||||
Text text_mixr1{{58, 18, 180, 16}, "---"};
|
||||
|
||||
Text text_lbl_pin4{{0, 36, 114, 16}, "PIN[4] (read):"};
|
||||
Text text_pin4{{116, 36, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_set4{{0, 54, 114, 16}, "SET[4] (write):"};
|
||||
Text text_set4{{116, 54, 124, 16}, "---"};
|
||||
|
||||
// Bit 8 (LPF)
|
||||
Text text_lbl_lpf_bit{{0, 78, 240, 16}, "Bit 8 (LPF - GPIO4[8]):"};
|
||||
Text text_lpf_dir{{0, 96, 80, 16}, "DIR: ?"};
|
||||
Text text_lpf_pin{{82, 96, 78, 16}, "PIN: ?"};
|
||||
Text text_lpf_set{{162, 96, 78, 16}, "SET: ?"};
|
||||
|
||||
Button button_lpf_toggle{{8, 114, 110, 24}, "Toggle LPF"};
|
||||
Button button_lpf_on{{122, 114, 50, 24}, "ON"};
|
||||
Button button_lpf_off{{176, 114, 50, 24}, "OFF"};
|
||||
|
||||
// Bit 9 (RF Amp)
|
||||
Text text_lbl_amp_bit{{0, 146, 240, 16}, "Bit 9 (Amp - GPIO4[9]):"};
|
||||
Text text_amp_dir{{0, 164, 80, 16}, "DIR: ?"};
|
||||
Text text_amp_pin{{82, 164, 78, 16}, "PIN: ?"};
|
||||
Text text_amp_set{{162, 164, 78, 16}, "SET: ?"};
|
||||
|
||||
Button button_amp_toggle{{8, 182, 110, 24}, "Toggle Amp"};
|
||||
Button button_amp_on{{122, 182, 50, 24}, "ON"};
|
||||
Button button_amp_off{{176, 182, 50, 24}, "OFF"};
|
||||
|
||||
// GPIO3 (Mixer)
|
||||
Text text_lbl_gpio3{{0, 214, 240, 16}, "--- GPIO3 (Mixer) ---"};
|
||||
Text text_lbl_mix_bit{{0, 232, 240, 16}, "Bit 2 (MIX - GPIO3[2]):"};
|
||||
Text text_mix_dir{{0, 250, 80, 16}, "DIR: ?"};
|
||||
Text text_mix_pin{{82, 250, 78, 16}, "PIN: ?"};
|
||||
Text text_mix_set{{162, 250, 78, 16}, "SET: ?"};
|
||||
|
||||
// Controls
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
@@ -734,44 +921,50 @@ class RFFC5072StatusView : public View {
|
||||
NavigationView& nav_;
|
||||
void refresh_status();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== RFFC5072 (1st IF) ==="};
|
||||
Text text_status{{0, 0, 240, 16}, "---"};
|
||||
|
||||
Text text_lbl_enabled{{0, 20, 114, 16}, "Status:"};
|
||||
Text text_enabled{{116, 20, 124, 16}, "---"};
|
||||
Text text_gpio4{{0, 16, 240, 16}, "---"};
|
||||
|
||||
Text text_lbl_freq{{0, 36, 114, 16}, "LO Freq:"};
|
||||
Text text_freq{{116, 36, 124, 16}, "---"};
|
||||
Text text_ctrl{{0, 32, 240, 16}, "---"};
|
||||
|
||||
Text text_lbl_path{{0, 52, 114, 16}, "Path:"};
|
||||
Text text_path{{116, 52, 124, 16}, "---"};
|
||||
Text text_lbl_enabled{{0, 48, 114, 16}, "Status:"};
|
||||
Text text_enabled{{116, 48, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_mixer{{0, 68, 114, 16}, "Mixer:"};
|
||||
Text text_mixer{{116, 68, 124, 16}, "---"};
|
||||
Text text_lbl_freq{{0, 64, 114, 16}, "LO Freq:"};
|
||||
Text text_freq{{116, 64, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r0{{0, 92, 114, 16}, "Reg 0:"};
|
||||
Text text_r0{{116, 92, 124, 16}, "---"};
|
||||
Text text_lbl_path{{0, 80, 114, 16}, "Path:"};
|
||||
Text text_path{{116, 80, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r1{{0, 108, 114, 16}, "Reg 1 (N):"};
|
||||
Text text_r1{{116, 108, 124, 16}, "---"};
|
||||
Text text_lbl_mixer{{0, 96, 114, 16}, "Mixer:"};
|
||||
Text text_mixer{{116, 96, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r2{{0, 124, 114, 16}, "Reg 2:"};
|
||||
Text text_r2{{116, 124, 124, 16}, "---"};
|
||||
Text text_lbl_r0{{0, 112, 114, 16}, "Reg 0:"};
|
||||
Text text_r0{{116, 112, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_decode{{0, 148, 240, 16}, "--- Decoded Values ---"};
|
||||
Text text_lbl_r1{{0, 128, 114, 16}, "Reg 1 (N):"};
|
||||
Text text_r1{{116, 128, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_n{{0, 168, 114, 16}, "N divider:"};
|
||||
Text text_n{{116, 168, 124, 16}, "---"};
|
||||
Text text_lbl_r2{{0, 144, 114, 16}, "Reg 2:"};
|
||||
Text text_r2{{116, 144, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_lodiv{{0, 184, 114, 16}, "LO divider:"};
|
||||
Text text_lodiv{{116, 184, 124, 16}, "---"};
|
||||
Text text_lbl_n{{0, 160, 114, 16}, "N divider:"};
|
||||
Text text_n{{116, 160, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc{{0, 200, 114, 16}, "Calc freq:"};
|
||||
Text text_calc{{116, 200, 124, 16}, "---"};
|
||||
Text text_lbl_lodiv{{0, 176, 114, 16}, "LO divider:"};
|
||||
Text text_lodiv{{116, 176, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 224, 240, 32}, ""};
|
||||
Text text_lbl_calc{{0, 192, 114, 16}, "Calc freq:"};
|
||||
Text text_calc{{116, 192, 124, 16}, "---"};
|
||||
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
Text text_regs_status{{0, 208, 240, 16}, "---"};
|
||||
|
||||
Text text_status2{{0, 224, 240, 16}, ""};
|
||||
Text text_status3{{0, 240, 240, 16}, ""};
|
||||
|
||||
Button button_refresh{{2, 280, 72, 24}, "Refresh"};
|
||||
Button button_force{{98, 280, 60, 24}, "SPI"};
|
||||
Button button_done{{182, 280, 56, 24}, "Done"};
|
||||
};
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -828,6 +1021,379 @@ class RFFCTuningDebugView : public View {
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* MAX2831DebugView *************************************************/
|
||||
|
||||
class MAX2831DebugView : public View {
|
||||
public:
|
||||
MAX2831DebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "MAX2831 Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "MAX2831 (2nd IF) Debug"};
|
||||
|
||||
Text text_lbl_called{{0, 24, 120, 16}, "Freq Set:"};
|
||||
Text text_called{{122, 24, 118, 16}, "NO"};
|
||||
|
||||
Text text_lbl_valid{{0, 42, 120, 16}, "In Range:"};
|
||||
Text text_valid{{122, 42, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_req{{0, 60, 120, 16}, "Requested:"};
|
||||
Text text_req{{122, 60, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_n{{0, 78, 120, 16}, "Calc N:"};
|
||||
Text text_calc_n{{122, 78, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_frac{{0, 96, 120, 16}, "Calc Frac:"};
|
||||
Text text_calc_frac{{122, 96, 118, 16}, "---"};
|
||||
|
||||
Text text_spacer{{0, 114, 240, 16}, "--- Hardware Regs ---"};
|
||||
|
||||
Text text_lbl_r3{{0, 132, 120, 16}, "Reg 3:"};
|
||||
Text text_r3{{122, 132, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_r4{{0, 150, 120, 16}, "Reg 4:"};
|
||||
Text text_r4{{122, 150, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_act_n{{0, 168, 120, 16}, "Actual N:"};
|
||||
Text text_act_n{{122, 168, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_act_frac{{0, 186, 120, 16}, "Actual Frac:"};
|
||||
Text text_act_frac{{122, 186, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_freq{{0, 204, 120, 16}, "Calc Freq:"};
|
||||
Text text_calc_freq{{122, 204, 118, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 228, 240, 44}, ""};
|
||||
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class SystemDiagnosticsView : public View {
|
||||
public:
|
||||
SystemDiagnosticsView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "System Diagnostics"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void read_gpio_states();
|
||||
void set_sample_rate(uint32_t rate);
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "System Diagnostics"};
|
||||
|
||||
// Sample Rate Section
|
||||
Text text_lbl_sample{{0, 24, 42, 16}, "Rate:"};
|
||||
Text text_sample_rate{{44, 24, 50, 16}, "---"};
|
||||
Text text_lbl_fpga_decode{{96, 24, 144, 16}, "DC:? Q:? QS:?"};
|
||||
|
||||
Button button_sample_2m{{2, 44, 56, 24}, "2 MSPS"};
|
||||
Button button_sample_4m{{62, 44, 56, 24}, "4 MSPS"};
|
||||
Button button_sample_8m{{122, 44, 56, 24}, "8 MSPS"};
|
||||
Button button_sample_20m{{182, 44, 56, 24}, "20 MSPS"};
|
||||
|
||||
// Baseband Filter Section
|
||||
Text text_lbl_bb_filter{{0, 76, 114, 16}, "BB Filter BW:"};
|
||||
Text text_bb_filter{{116, 76, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_reg8{{0, 94, 114, 16}, "MAX2831 R8:"};
|
||||
Text text_reg8{{116, 94, 124, 16}, "---"};
|
||||
|
||||
// GPIO States Section
|
||||
Text text_lbl_gpio{{0, 118, 240, 16}, "--- GPIO Pin States ---"};
|
||||
|
||||
Text text_lbl_lpf{{0, 136, 56, 16}, "LPF:"};
|
||||
Text text_gpio_lpf{{58, 136, 181, 16}, "---"};
|
||||
|
||||
Text text_lbl_mix{{0, 154, 56, 16}, "Mixer:"};
|
||||
Text text_gpio_mix{{58, 154, 181, 16}, "---"};
|
||||
|
||||
Text text_lbl_amp{{0, 172, 56, 16}, "RF Amp:"};
|
||||
Text text_gpio_amp{{58, 172, 181, 16}, "---"};
|
||||
|
||||
// FPGA Control Section
|
||||
Text text_lbl_fpga{{0, 196, 240, 16}, "--- FPGA Control ---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl{{0, 214, 114, 16}, "FPGA Reg 1:"};
|
||||
Text text_fpga_ctrl{{116, 214, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_band{{0, 232, 48, 16}, "Band:"};
|
||||
Text text_band{{50, 232, 190, 16}, "---"};
|
||||
|
||||
Button button_toggle_q{{2, 252, 110, 24}, "Toggle Q Inv"};
|
||||
Button button_toggle_dc{{122, 252, 110, 24}, "Toggle DC Blk"};
|
||||
|
||||
// Control Buttons
|
||||
Button button_refresh{{2, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* Si5351PLLADebugView - Debug fractional parameters for PLL A */
|
||||
class Si5351PLLADebugView : public View {
|
||||
public:
|
||||
Si5351PLLADebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Si5351 PLL A Debug"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
void refresh();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "===PLL A Fractional Debug==="};
|
||||
|
||||
// Raw register values
|
||||
Text text_lbl_raw{{0, 24, 240, 16}, "---Raw Registers (26-33)---"};
|
||||
Text text_r26_27{{8, 40, 224, 16}, "---"};
|
||||
Text text_r28_30{{8, 56, 224, 16}, "---"};
|
||||
Text text_r31_33{{8, 72, 224, 16}, "---"};
|
||||
|
||||
// Decoded values
|
||||
Text text_lbl_decoded{{0, 96, 240, 16}, "---Decoded Parameters---"};
|
||||
Text text_lbl_p1{{0, 112, 48, 16}, "P1:"};
|
||||
Text text_p1{{50, 112, 190, 16}, "---"};
|
||||
Text text_lbl_p2{{0, 128, 48, 16}, "P2:"};
|
||||
Text text_p2{{50, 128, 190, 16}, "---"};
|
||||
Text text_lbl_p3{{0, 144, 48, 16}, "P3:"};
|
||||
Text text_p3{{50, 144, 190, 16}, "---"};
|
||||
|
||||
// Calculated VCO
|
||||
Text text_lbl_calc{{0, 168, 240, 16}, "---Calculated VCO---"};
|
||||
Text text_lbl_mult{{0, 184, 80, 16}, "Multiplier:"};
|
||||
Text text_mult{{82, 184, 158, 16}, "---"};
|
||||
Text text_lbl_vco{{0, 200, 80, 16}, "VCO Freq:"};
|
||||
Text text_vco{{82, 200, 158, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 224, 240, 32}, ""};
|
||||
|
||||
Button button_refresh{{8, 280, 100, 24}, "Refresh"};
|
||||
Button button_done{{132, 280, 100, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class Si5351PLLBDebugView : public View {
|
||||
public:
|
||||
Si5351PLLBDebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Si5351 PLL B Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== PLL B Fractional Debug ==="};
|
||||
|
||||
Text text_lbl_raw{{0, 24, 240, 16}, "--- Raw Registers (34-41) ---"};
|
||||
Text text_r34_35{{8, 40, 224, 16}, "---"};
|
||||
Text text_r36_38{{8, 56, 224, 16}, "---"};
|
||||
Text text_r39_41{{8, 72, 224, 16}, "---"};
|
||||
|
||||
Text text_lbl_decoded{{0, 96, 240, 16}, "--- Decoded Parameters ---"};
|
||||
Text text_lbl_p1{{0, 112, 48, 16}, "P1:"};
|
||||
Text text_p1{{50, 112, 190, 16}, "---"};
|
||||
Text text_lbl_p2{{0, 128, 48, 16}, "P2:"};
|
||||
Text text_p2{{50, 128, 190, 16}, "---"};
|
||||
Text text_lbl_p3{{0, 144, 48, 16}, "P3:"};
|
||||
Text text_p3{{50, 144, 190, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc{{0, 168, 240, 16}, "--- Calculated VCO ---"};
|
||||
Text text_lbl_vco{{0, 200, 80, 16}, "VCO Freq:"};
|
||||
Text text_vco{{82, 200, 158, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 224, 240, 32}, ""};
|
||||
Button button_refresh{{8, 280, 100, 24}, "Refresh"};
|
||||
Button button_done{{132, 280, 100, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* Si5351MultiSynthDebugView - Debug fractional register values for MS0 */
|
||||
class Si5351MultiSynthDebugView : public View {
|
||||
public:
|
||||
Si5351MultiSynthDebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Si5351 MS0 Debug"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
void refresh();
|
||||
void force_pll_reset();
|
||||
void force_fractional_mode();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "===MS0 Fractional Debug==="};
|
||||
|
||||
// Clock Control Register 16 (CLK0)
|
||||
Text text_lbl_clk_ctrl{{0, 18, 100, 16}, "CLK0 Ctrl:"};
|
||||
Text text_clk_ctrl{{102, 18, 138, 16}, "---"};
|
||||
|
||||
Text text_lbl_ms_int{{0, 34, 100, 16}, "MS_INT bit:"};
|
||||
Text text_ms_int{{102, 34, 138, 16}, "---"};
|
||||
|
||||
// Raw register values
|
||||
Text text_lbl_raw{{0, 54, 240, 16}, "---Raw Registers (42-49)---"};
|
||||
|
||||
Text text_lbl_r42_43{{0, 70, 80, 16}, "R42-43:"};
|
||||
Text text_r42_43{{82, 70, 158, 16}, "---"};
|
||||
|
||||
Text text_lbl_r44_46{{0, 86, 80, 16}, "R44-46:"};
|
||||
Text text_r44_46{{82, 86, 158, 16}, "---"};
|
||||
|
||||
Text text_lbl_r47_49{{0, 102, 80, 16}, "R47-49:"};
|
||||
Text text_r47_49{{82, 102, 158, 16}, "---"};
|
||||
|
||||
// Decoded values
|
||||
Text text_lbl_decoded{{0, 122, 240, 16}, "---Decoded Values---"};
|
||||
|
||||
Text text_lbl_p1{{0, 138, 48, 16}, "P1:"};
|
||||
Text text_p1{{50, 138, 190, 16}, "---"};
|
||||
|
||||
Text text_lbl_p2{{0, 154, 48, 16}, "P2:"};
|
||||
Text text_p2{{50, 154, 190, 16}, "---"};
|
||||
|
||||
Text text_lbl_p3{{0, 170, 48, 16}, "P3:"};
|
||||
Text text_p3{{50, 170, 190, 16}, "---"};
|
||||
|
||||
Text text_lbl_rdiv{{0, 186, 48, 16}, "R_DIV:"};
|
||||
Text text_rdiv{{50, 186, 190, 16}, "---"};
|
||||
|
||||
// Calculated frequency
|
||||
Text text_lbl_calc{{0, 206, 240, 16}, "---Calculated Output---"};
|
||||
|
||||
Text text_lbl_div{{0, 222, 80, 16}, "MS Div:"};
|
||||
Text text_div{{82, 222, 158, 16}, "---"};
|
||||
|
||||
Text text_lbl_freq{{0, 238, 80, 16}, "CLK0 Out:"};
|
||||
Text text_freq{{82, 238, 158, 16}, "---"};
|
||||
|
||||
// Status
|
||||
Text text_status{{0, 258, 240, 16}, ""};
|
||||
|
||||
Button button_refresh{{2, 280, 56, 24}, "Refresh"};
|
||||
Button button_reset{{62, 280, 56, 24}, "PLL Rst"};
|
||||
Button button_frac{{122, 280, 56, 24}, "Frac"};
|
||||
Button button_done{{182, 280, 56, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class PralineClockDebugView : public View {
|
||||
public:
|
||||
PralineClockDebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Pro Clock Status"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== Pro Clock Dashboard ==="};
|
||||
|
||||
// System Status
|
||||
Text text_lbl_pll{{0, 20, 80, 16}, "PLL Lock:"};
|
||||
Text text_pll_status{{88, 20, 152, 16}, "---"};
|
||||
|
||||
Text text_lbl_afe{{0, 36, 80, 16}, "AFE Rate:"};
|
||||
Text text_afe_rate{{88, 36, 152, 16}, "---"};
|
||||
|
||||
Text text_lbl_n{{0, 52, 80, 16}, "Decim (n):"};
|
||||
Text text_n_val{{88, 52, 152, 16}, "-"};
|
||||
|
||||
// Table Header (Width 240px, 30 characters)
|
||||
Text text_header{{0, 72, 240, 16}, "ID mA Mode Src Phase Stat"};
|
||||
|
||||
// Helper structure to group row widgets for CLK0-CLK5
|
||||
struct ClockRow {
|
||||
Text* id;
|
||||
Text* ma;
|
||||
Text* mode;
|
||||
Text* src; // PLL A, or B
|
||||
Text* phase;
|
||||
Text* stat;
|
||||
};
|
||||
std::vector<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 {
|
||||
|
||||
@@ -46,11 +46,10 @@ bool valid_firmware_file(std::filesystem::path::string_type path) {
|
||||
auto result = firmware_file.open(path.c_str());
|
||||
if (!result.is_valid()) {
|
||||
uint64_t file_size = firmware_file.size();
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK && file_size > 1 * 1024 * 1024) {
|
||||
// Portapack firmware files must not be larger than 1MB
|
||||
if (file_size > FLASH_ROM_SIZE) {
|
||||
// Firmware file is larger than the flash size for this device
|
||||
return false;
|
||||
}
|
||||
// May need to add a check to portarf and portarf pro too.
|
||||
checksum = 0;
|
||||
for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) {
|
||||
auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer));
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include "untar.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../flashsize.h"
|
||||
|
||||
#define FLASH_ROM_SIZE (FLASH_SIZE_MB * 1024 * 1024)
|
||||
#define FLASH_STARTING_ADDRESS 0x00000000
|
||||
#define FLASH_EXPECTED_CHECKSUM 0x00000000
|
||||
|
||||
@@ -304,7 +304,8 @@ class MicTXView : public View {
|
||||
{
|
||||
{" 8k5 ", 0}, // Initial dynamic values when we start Mic App.
|
||||
{" 11k ", 1},
|
||||
{" 16k ", 2},
|
||||
{" 12k5 ", 2},
|
||||
{" 16k ", 3},
|
||||
}};
|
||||
|
||||
NumberField field_squelch{
|
||||
|
||||
@@ -1261,6 +1261,7 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
|
||||
else
|
||||
v = SPEC_MODULATION;
|
||||
field_mode.set_selected_index(v);
|
||||
return; // Important ! Guru will occur if you don't return when modifying field from within on_change !
|
||||
}
|
||||
last_mode = v;
|
||||
change_mode(v);
|
||||
|
||||
@@ -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 ******************************/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
namespace ui {
|
||||
|
||||
void create_thread(int32_t (*fn)(void*), void* arg, size_t stack_size, int priority) {
|
||||
// TODO: collect memory on terminate, once this is used
|
||||
// TODO: collect memory on terminate, once this is used. This is a HUGE TODO! need to call chThdWait on all of them!
|
||||
chThdCreateFromHeap(NULL, stack_size, priority, fn, arg);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,19 @@ void SubGhzDRecentEntryDetailView::update_data() {
|
||||
|
||||
if (entry_.data != 0) console.writeln("Data: " + to_string_hex(entry_.data));
|
||||
|
||||
if (entry_.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
uint8_t pager_addr = (entry_.data >> 5) & 0x0F;
|
||||
uint8_t func_code = (entry_.data >> 1) & 0x0F;
|
||||
console.writeln("Station: 0x" + to_string_hex(serial) + " (" + to_string_dec_uint(serial) + ")");
|
||||
console.writeln("Pager: " + to_string_dec_uint(pager_addr));
|
||||
if (func_code == 0x0D)
|
||||
console.writeln("Action: Buzz");
|
||||
else if (func_code == 0x0F)
|
||||
console.writeln("Action: Sync");
|
||||
else
|
||||
console.writeln("Action: 0x" + to_string_hex(func_code));
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_KEELOQ) {
|
||||
console.writeln("Fix: " + to_string_hex(fix));
|
||||
console.writeln("Encrypted: " + to_string_hex(encrypted));
|
||||
@@ -280,6 +293,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
|
||||
return "Marantec24";
|
||||
case FPS_HOLTEKHT6P20B:
|
||||
return "Holtek HT6P20B";
|
||||
case FPS_RESTAURANT_PAGER:
|
||||
return "Rest. Pager";
|
||||
case FPS_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
@@ -306,7 +321,14 @@ void RecentEntriesTable<ui::SubGhzDRecentEntries>::draw(
|
||||
line.reserve(30);
|
||||
|
||||
line = SubGhzDView::getSensorTypeName((FPROTO_SUBGHZD_SENSOR)entry.sensorType);
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
if (entry.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
uint8_t pgr = (entry.data >> 5) & 0x0F;
|
||||
uint8_t func = (entry.data >> 1) & 0x0F;
|
||||
line += " P:" + to_string_dec_uint(pgr) + (func == 0x0D ? " Buzz" : func == 0x0F ? " Sync"
|
||||
: "");
|
||||
} else {
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
}
|
||||
line.resize(columns.at(0).second, ' ');
|
||||
std::string ageStr = to_string_dec_uint(entry.age);
|
||||
std::string bitsStr = to_string_dec_uint(entry.bits);
|
||||
@@ -873,5 +895,12 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
|
||||
btn = (entry_.data >> 4) & 0xF;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
// 25-bit EV1527-variant: [sysid:16][pager:4][func:4][stop:1]
|
||||
serial = (entry_.data >> 9) & 0xFFFF; // System ID
|
||||
// btn/cnt left as SD_NO values; friendly output in update_data()
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace ui
|
||||
|
||||
@@ -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);
|
||||
@@ -412,6 +423,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) {
|
||||
@@ -473,7 +497,12 @@ void shutdown() {
|
||||
send_message(&message);
|
||||
|
||||
shared_memory.application_queue.reset();
|
||||
|
||||
// Allow time for the shutdown message to be processed and for the baseband
|
||||
// core to stop before starting another image. Otherwise, the M4 may still be
|
||||
// running and cause a crash when the next image is started.
|
||||
#ifdef PRALINE
|
||||
chThdSleepMilliseconds(20);
|
||||
#endif
|
||||
baseband_image_running = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,12 @@ 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);
|
||||
@@ -111,6 +116,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,21 +344,43 @@ constexpr ClockControls si5351c_clock_control_common{{
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
}};
|
||||
#endif
|
||||
|
||||
constexpr ClockControls si5351a_clock_control_common{{
|
||||
#ifndef PRALINE
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
#ifdef PRALINE
|
||||
// CLK0: MAX5864 (ADC) - 4mA, Normal PLLA Integer
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
|
||||
// CLK1: SCT_CLK (iCE40 FPGA) - 4mA, Normal PLLB Integer
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
|
||||
// CLK2: LPC43xx MCU - 2mA, Normal PLLA (Must be Integer for MCU stability)
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
|
||||
// CLK3: CLKOUT SMA Port P1 - 2mA, Normal PLLB Integer Power_Off
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK4: MAX2831 reference (40 MHz) - 4mA, Invert PLLA Integer (Required for mixer lock)
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
|
||||
// CLK5: RFFC5072 reference (40 MHz) - 4mA, Invert PLLA Integer (Required for mixer lock)
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
|
||||
// CLK6: Not used (disabled) 2mA, Normal PLLB Integer, Power_Off
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK7: Not used (disabled) 2mA, Normal PLLB Integer, Power_Off
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
#else
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, // CLK0: MUST be Fractional for 8 MHz!
|
||||
#endif
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK4: HackRF r9 - not inverted
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK5: HackRF r9 - not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK6: Not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK7: Not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
#endif
|
||||
|
||||
}};
|
||||
|
||||
ClockManager::Reference ClockManager::get_reference() const {
|
||||
return reference;
|
||||
}
|
||||
@@ -330,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
|
||||
@@ -377,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 = first IF (RFFC5072)
|
||||
* CLK5 = second IF (MAX2831)
|
||||
* 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)
|
||||
@@ -456,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--;
|
||||
@@ -466,8 +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);
|
||||
#else
|
||||
// Wait for PLL(s) to lock - with timeout to prevent hang
|
||||
uint8_t device_status_mask = hackrf_r9
|
||||
@@ -475,14 +632,8 @@ void ClockManager::init_clock_generator() {
|
||||
: (ref_pll == ClockControl::MultiSynthSource::PLLB)
|
||||
? 0x40
|
||||
: 0x20;
|
||||
#ifndef PRALINE
|
||||
|
||||
while ((clock_generator.device_status() & device_status_mask) != 0);
|
||||
#else
|
||||
uint32_t pll_timeout = 100000;
|
||||
while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) {
|
||||
pll_timeout--;
|
||||
}
|
||||
#endif
|
||||
|
||||
clock_generator.set_clock_control(
|
||||
clock_generator_output_mcu_clkin,
|
||||
@@ -499,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() {
|
||||
@@ -527,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;
|
||||
@@ -545,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};
|
||||
@@ -558,11 +725,11 @@ void ClockManager::enable_codec_clocks() {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: CLK0 (AFE_CLK) for codec/FPGA, CLK1 (SCT_CLK) for FPGA timing.
|
||||
* Reference hackrf_core.c shows PRALINE needs both CLK0 and CLK1. */
|
||||
clock_generator.enable_clock(clock_generator_output_og_codec); /* CLK0 */
|
||||
clock_generator.enable_clock(clock_generator_output_og_cpld); /* CLK1 */
|
||||
clock_generator.enable_clock(clock_generator_output_og_codec); /* CLK0 MAX5864*/
|
||||
clock_generator.enable_clock(clock_generator_output_og_cpld); /* CLK1 iCE40 FPGA*/
|
||||
clock_generator.enable_clock(clock_generator_output_og_sgpio); /* CLK2 LPC43xx*/
|
||||
clock_generator.enable_output_mask(
|
||||
(1U << clock_generator_output_og_codec) |
|
||||
(1U << clock_generator_output_og_cpld));
|
||||
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio));
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
clock_generator.enable_clock(clock_generator_output_r9_sgpio);
|
||||
@@ -590,12 +757,12 @@ void ClockManager::disable_codec_clocks() {
|
||||
* CLKx_DISABLE_STATE.
|
||||
*/
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: CLK0 (AFE_CLK) and CLK1 (SCT_CLK) used for codec/FPGA */
|
||||
/* PRALINE: CLK0 (AFE_CLK), CLK1 (SCT_CLK), and CLK2 MCU used for codec/FPGA */
|
||||
clock_generator.disable_output_mask(
|
||||
(1U << clock_generator_output_og_codec) |
|
||||
(1U << clock_generator_output_og_cpld));
|
||||
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio));
|
||||
clock_generator.disable_clock(clock_generator_output_og_codec);
|
||||
clock_generator.disable_clock(clock_generator_output_og_cpld);
|
||||
clock_generator.disable_clock(clock_generator_output_og_sgpio);
|
||||
#else
|
||||
if (hackrf_r9) {
|
||||
clock_generator.disable_output_mask(1U << clock_generator_output_r9_sgpio);
|
||||
@@ -612,7 +779,7 @@ void ClockManager::disable_codec_clocks() {
|
||||
|
||||
void ClockManager::enable_if_clocks() {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE uses CLK4 (first IF) and CLK5 (second IF) like original HackRF One */
|
||||
/* PRALINE uses CLK5 (first IF) and CLK4 (second IF) */
|
||||
clock_generator.enable_clock(clock_generator_output_og_first_if);
|
||||
clock_generator.enable_output_mask(1U << clock_generator_output_og_first_if);
|
||||
clock_generator.enable_clock(clock_generator_output_og_second_if);
|
||||
@@ -651,21 +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;
|
||||
|
||||
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);
|
||||
|
||||
// Set FPGA decimation to 0 (no decimation) for direct passthrough
|
||||
fpga_debug_register_write(2, 0);
|
||||
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
|
||||
@@ -686,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));
|
||||
@@ -701,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) {
|
||||
@@ -717,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
|
||||
}
|
||||
|
||||
@@ -738,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,
|
||||
@@ -794,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) {
|
||||
@@ -826,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);
|
||||
@@ -860,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
|
||||
}
|
||||
|
||||
@@ -70,9 +70,15 @@ class ClockManager {
|
||||
|
||||
void set_sampling_frequency(const uint32_t frequency);
|
||||
|
||||
uint32_t get_sampling_frequency() const {
|
||||
return _base_band_frequency;
|
||||
};
|
||||
|
||||
void set_reference_ppb(const int32_t ppb);
|
||||
|
||||
#ifdef PRALINE
|
||||
uint8_t get_resampling_n() const { return _resampling_n; }
|
||||
|
||||
// Si5351 diagnostic methods
|
||||
uint8_t si5351_read_status() { return clock_generator.device_status(); }
|
||||
uint8_t si5351_read_register(uint8_t reg) { return clock_generator.read_register(reg); }
|
||||
@@ -92,6 +98,8 @@ class ClockManager {
|
||||
si5351::Si5351& clock_generator;
|
||||
Reference reference;
|
||||
|
||||
uint32_t _base_band_frequency{20000000};
|
||||
|
||||
void set_gp_clkin_to_clkin_direct();
|
||||
|
||||
void start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel);
|
||||
@@ -106,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__*/
|
||||
|
||||
@@ -256,7 +256,9 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev
|
||||
if (!w->hidden()) {
|
||||
// To achieve reverse depth ordering (last object drawn is
|
||||
// considered "top"), descend first.
|
||||
for (const auto child : w->children()) {
|
||||
auto& children = w->children();
|
||||
for (auto it = children.rbegin(); it != children.rend(); ++it) { // reverse, bc the lastly added will be "top" if overlaps
|
||||
const auto& child = *it;
|
||||
const auto touched_widget = touch_widget(child, event);
|
||||
if (touched_widget) {
|
||||
return touched_widget;
|
||||
|
||||
+73
-4
@@ -33,6 +33,76 @@ using namespace portapack;
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
|
||||
// ACARS frame field layout (0-based byte offsets):
|
||||
// [0] SOH framing byte (skipped)
|
||||
// [1..7] Aircraft registration (7 chars)
|
||||
// [8] STX framing byte (skipped)
|
||||
// [9..10] Label (2 chars)
|
||||
// [11] Block ID (1 char)
|
||||
// [12..14] Message number (3 chars)
|
||||
// [15..20] Flight ID (6 chars)
|
||||
// [21..N-3] Free-text payload (variable)
|
||||
// [N-2..N-1] CRC-16/CCITT (2 bytes, MSB first, NOT part of payload)
|
||||
//
|
||||
// Minimum valid frame: 21-byte fixed header + 2 CRC bytes = 23 bytes.
|
||||
// (A zero-length payload is legal per spec; we require at least 23 bytes.)
|
||||
static constexpr std::string::size_type kAcarsCrcLen = 2;
|
||||
static constexpr std::string::size_type kAcarsHeaderLen = 21;
|
||||
static constexpr std::string::size_type kAcarsMinLen = kAcarsHeaderLen + kAcarsCrcLen;
|
||||
|
||||
// CRC-16/CCITT: poly 0x1021, init 0x0000, no reflection, no final XOR.
|
||||
// This is the variant used by ACARS (same as XMODEM CRC).
|
||||
static uint16_t acars_crc16(const std::string& data, std::string::size_type len) {
|
||||
uint16_t crc = 0x0000;
|
||||
for (std::string::size_type i = 0; i < len; ++i) {
|
||||
crc ^= static_cast<uint16_t>(static_cast<uint8_t>(data[i])) << 8;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if (crc & 0x8000)
|
||||
crc = static_cast<uint16_t>((crc << 1) ^ 0x1021);
|
||||
else
|
||||
crc = static_cast<uint16_t>(crc << 1);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
AcarsDecoded acars_decode(const std::string& raw) {
|
||||
AcarsDecoded result;
|
||||
if (raw.size() < kAcarsMinLen) {
|
||||
result.txt = "ACARS message too short (" + std::to_string(raw.size()) +
|
||||
" bytes, need " + std::to_string(kAcarsMinLen) + ")";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify CRC: computed over everything except the last 2 bytes,
|
||||
// then compared against those 2 bytes (MSB first).
|
||||
const std::string::size_type payload_len = raw.size() - kAcarsCrcLen;
|
||||
const uint16_t computed = acars_crc16(raw, payload_len);
|
||||
const uint16_t received = (static_cast<uint16_t>(static_cast<uint8_t>(raw[raw.size() - 2])) << 8) |
|
||||
static_cast<uint16_t>(static_cast<uint8_t>(raw[raw.size() - 1]));
|
||||
result.crc_ok = (computed == received);
|
||||
|
||||
result.reg = raw.substr(1, 7);
|
||||
result.label = raw.substr(9, 2);
|
||||
result.block_id = raw[11];
|
||||
result.msg_num = raw.substr(12, 3);
|
||||
result.flight_id = raw.substr(15, 6);
|
||||
// Payload sits between end of fixed header and the 2 trailing CRC bytes.
|
||||
if (payload_len > kAcarsHeaderLen)
|
||||
result.txt = raw.substr(kAcarsHeaderLen, payload_len - kAcarsHeaderLen);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string acars_format(const AcarsDecoded& msg) {
|
||||
return std::string("ACARS Decoded Result\nCRC: ") + (msg.crc_ok ? "OK" : "FAIL") +
|
||||
"\nRegistration: " + msg.reg +
|
||||
"\nLabel: " + msg.label +
|
||||
"\nBlockID: " + msg.block_id +
|
||||
"\nMsgNum: " + msg.msg_num +
|
||||
"\nFlightID: " + msg.flight_id +
|
||||
"\nMessage: " + msg.txt;
|
||||
}
|
||||
|
||||
void ACARSLogger::log_str(std::string msg) {
|
||||
log_file.write_entry(msg);
|
||||
}
|
||||
@@ -77,17 +147,16 @@ void ACARSAppView::focus() {
|
||||
|
||||
void ACARSAppView::on_packet(const ACARSPacketMessage* packet) {
|
||||
std::string console_info;
|
||||
|
||||
if (packet->state == 255) {
|
||||
// got a packet, parse it, and display
|
||||
rtc::RTC datetime;
|
||||
rtc_time::now(datetime);
|
||||
// todo parity error recovery
|
||||
console_info = to_string_datetime(datetime, HMS);
|
||||
console_info += ": ";
|
||||
console_info += packet->message;
|
||||
std::string message{packet->message, packet->message + packet->msg_len};
|
||||
AcarsDecoded decoded = acars_decode(message);
|
||||
console_info += acars_format(decoded);
|
||||
console.writeln(console_info);
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_str(console_info);
|
||||
} else {
|
||||
|
||||
+21
-1
@@ -33,6 +33,26 @@
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
|
||||
// Decoded ACARS message fields extracted from a raw frame.
|
||||
// CRC-16/CCITT (poly 0x1021, init 0x0000) is verified against the two
|
||||
// trailing bytes of the raw frame; crc_ok reflects that result.
|
||||
struct AcarsDecoded {
|
||||
bool crc_ok{false};
|
||||
std::string reg{};
|
||||
std::string label{};
|
||||
std::string flight_id{};
|
||||
std::string msg_num{};
|
||||
char block_id{'\0'};
|
||||
std::string txt{};
|
||||
};
|
||||
|
||||
// Decode a raw ACARS frame: verify CRC-16/CCITT and extract fixed-offset fields.
|
||||
// Returns a partially-filled AcarsDecoded (txt error only) if the frame is too short.
|
||||
AcarsDecoded acars_decode(const std::string& raw);
|
||||
|
||||
// Format a decoded ACARS message for display or logging.
|
||||
std::string acars_format(const AcarsDecoded& msg);
|
||||
|
||||
class ACARSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
@@ -106,4 +126,4 @@ class ACARSAppView : public View {
|
||||
|
||||
} // namespace ui::external_app::acars_rx
|
||||
|
||||
#endif /*__ACARS_APP_H__*/
|
||||
#endif /*__ACARS_APP_H__*/
|
||||
|
||||
+83
-83
@@ -1,83 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "acars_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ACARSAppView>();
|
||||
}
|
||||
} // namespace ui::external_app::acars_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_acars_rx.application_information"), used)) application_information_t _application_information_acars_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::acars_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "ACARS",
|
||||
/*.bitmap_data = */ {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'A', 'C', 'A'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "acars_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ACARSAppView>();
|
||||
}
|
||||
} // namespace ui::external_app::acars_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_acars_rx.application_information"), used)) application_information_t _application_information_acars_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::acars_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "ACARS",
|
||||
/*.bitmap_data = */ {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'A', 'C', 'A'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -339,7 +339,7 @@ void AdultToysView::randomizeMac() {
|
||||
}
|
||||
|
||||
void AdultToysView::randomChn() {
|
||||
channel_number = 37 + std::rand() % (39 - 37 + 1);
|
||||
channel_number = 37 + rand() % (39 - 37 + 1);
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
|
||||
|
||||
+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,
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ void BlackjackView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_deck();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ void BLESpamView::reset() {
|
||||
}
|
||||
|
||||
void BLESpamView::randomChn() {
|
||||
channel_number = 37 + std::rand() % (39 - 37 + 1);
|
||||
channel_number = 37 + rand() % (39 - 37 + 1);
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -655,7 +655,7 @@ void BreakoutView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
|
||||
+3
-3
@@ -125,7 +125,7 @@ void DinoGameView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
@@ -789,8 +789,8 @@ void DinoGameView::draw_current_score() {
|
||||
|
||||
void DinoGameView::draw_high_score() {
|
||||
auto style = *ui::Theme::getInstance()->fg_light;
|
||||
painter.fill_rectangle({UI_POS_X_RIGHT(90), 28, 90, 18}, Color::black());
|
||||
painter.draw_string({UI_POS_X_RIGHT(90), 30}, style, "HI " + score_to_string(highScore));
|
||||
painter.fill_rectangle({UI_POS_X_RIGHT(10), 28, 90, 18}, Color::black());
|
||||
painter.draw_string({UI_POS_X_RIGHT(10), 30}, style, "HI " + score_to_string(highScore));
|
||||
}
|
||||
|
||||
void DinoGameView::jump() {
|
||||
|
||||
+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},
|
||||
|
||||
+15
-15
@@ -204,11 +204,11 @@ void spawn_entity(uint8_t type, uint8_t x, uint8_t y) {
|
||||
|
||||
void spawn_random_entity(uint8_t type) {
|
||||
if (num_entities >= MAX_ENTITIES) return;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
uint8_t spawn_x, spawn_y;
|
||||
do {
|
||||
spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
spawn_x = rand() % LEVEL_WIDTH;
|
||||
spawn_y = rand() % LEVEL_HEIGHT;
|
||||
} while (get_block_at(spawn_x, spawn_y) == 0xF ||
|
||||
(spawn_x == (uint8_t)player.pos.x && spawn_y == (uint8_t)player.pos.y));
|
||||
|
||||
@@ -227,9 +227,9 @@ void remove_entity(uint8_t index) {
|
||||
bool spawned = false;
|
||||
|
||||
while (!spawned && attempts < 50) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts);
|
||||
uint8_t spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
srand(LPC_RTC->CTIME0 + attempts);
|
||||
uint8_t spawn_x = rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = rand() % LEVEL_HEIGHT;
|
||||
|
||||
int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x;
|
||||
int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y;
|
||||
@@ -270,9 +270,9 @@ void initialize_level() {
|
||||
uint8_t attempts = 0;
|
||||
|
||||
while (initial_enemies < 2 && num_entities < MAX_ENTITIES && attempts < 50) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts);
|
||||
int8_t offset_x = (std::rand() % 11) - 5;
|
||||
int8_t offset_y = (std::rand() % 11) - 5;
|
||||
srand(LPC_RTC->CTIME0 + attempts);
|
||||
int8_t offset_x = (rand() % 11) - 5;
|
||||
int8_t offset_y = (rand() % 11) - 5;
|
||||
|
||||
if (abs(offset_x) < 3 && abs(offset_y) < 3) {
|
||||
attempts++;
|
||||
@@ -307,10 +307,10 @@ void initialize_level() {
|
||||
|
||||
attempts = 0;
|
||||
while (num_entities < MIN_ENTITIES && attempts < 100) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts + 100);
|
||||
srand(LPC_RTC->CTIME0 + attempts + 100);
|
||||
|
||||
uint8_t spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
uint8_t spawn_x = rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = rand() % LEVEL_HEIGHT;
|
||||
|
||||
int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x;
|
||||
int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y;
|
||||
@@ -1045,7 +1045,7 @@ void DoomView::on_show() {
|
||||
void DoomView::paint(Painter& painter) {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
scene = 0;
|
||||
up = down = left = right = fired = false;
|
||||
jogging = view_height = 0;
|
||||
@@ -1081,7 +1081,7 @@ void DoomView::paint(Painter& painter) {
|
||||
int hud_y = RENDER_HEIGHT + 5;
|
||||
painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health));
|
||||
painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills));
|
||||
painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
painter.draw_string({UI_POS_X_RIGHT(10), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
|
||||
prev_velocity_moving = (player.velocity != 0);
|
||||
needs_redraw = false;
|
||||
@@ -1113,7 +1113,7 @@ void DoomView::paint(Painter& painter) {
|
||||
int hud_y = RENDER_HEIGHT + 5;
|
||||
painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health));
|
||||
painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills));
|
||||
painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
painter.draw_string({UI_POS_X_RIGHT(10), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
|
||||
needs_gun_redraw = false;
|
||||
}
|
||||
|
||||
+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,479 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_epirb_tx.hpp"
|
||||
|
||||
#include "tonesets.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "ui_alphanum.hpp"
|
||||
|
||||
#include <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_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) {
|
||||
// Already in BPSK mode => backup bpsk frequency
|
||||
bpsk_frequency = transmitter_model.target_frequency();
|
||||
} else {
|
||||
// Previously in AM mode => restore bpsk frequency
|
||||
transmitter_model.set_target_frequency(bpsk_frequency);
|
||||
}
|
||||
// Set mode to bpsk
|
||||
epirb_tx_message.mode_bpsk = true;
|
||||
// Set pre/post count
|
||||
epirb_tx_message.pre_count = (150 * TONES_SAMPLERATE) / 1000; // 150 ms
|
||||
epirb_tx_message.post_count = (100 * TONES_SAMPLERATE) / 1000; // 100 ms
|
||||
// Send config to baseband
|
||||
baseband::set_epirb_tx_config(epirb_tx_message);
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::set_tx_button_state(bool active) {
|
||||
button_tx.set_text(active ? "START" : "STOP");
|
||||
button_tx.set_style(active ? &style_tx_start : &style_tx_stop);
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::start_tx() {
|
||||
last_frame_time = chTimeNow();
|
||||
update_config();
|
||||
loop = loop_enabled;
|
||||
transmitter_model.enable();
|
||||
tx_view.set_transmitting(true);
|
||||
set_tx_button_state(false);
|
||||
transmitting = true;
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::stop_tx() {
|
||||
loop = false;
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
set_tx_button_state(true);
|
||||
transmitting = false;
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
(void)progress;
|
||||
if (done) {
|
||||
if (am_enabled) {
|
||||
// BPSK frame sent, switch back to 121.5 AM signal
|
||||
epirb_tx_message.mode_bpsk = false;
|
||||
// Backup bpsk frequency for next run
|
||||
bpsk_frequency = transmitter_model.target_frequency();
|
||||
// Restore am frequency
|
||||
transmitter_model.set_target_frequency(am_frequency);
|
||||
// Send config to baseband
|
||||
baseband::set_epirb_tx_config(epirb_tx_message);
|
||||
} else {
|
||||
// End of BPSK frame
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
if (!loop) {
|
||||
// Not looping => update transmission state
|
||||
set_tx_button_state(true);
|
||||
transmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::update_location(bool updateLocatorField) {
|
||||
locator = beacon_params.location.locator;
|
||||
if (updateLocatorField) text_field_beacon_locator.set_text(beacon_params.location.locator);
|
||||
text_beacon_latitude_value.set(to_latitude_string(beacon_params.location));
|
||||
text_beacon_longitude_value.set(to_longitude_string(beacon_params.location));
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::update_mode() {
|
||||
// Hide / show widgets for file mode or manual mode
|
||||
text_beacon.hidden(!mode_file);
|
||||
text_description_label.hidden(!mode_file);
|
||||
options_frame.hidden(!mode_file);
|
||||
text_description.hidden(!mode_file);
|
||||
text_description_end.hidden(!mode_file);
|
||||
text_beacon_type.hidden(mode_file);
|
||||
text_beacon_country.hidden(mode_file);
|
||||
checkbox_beacon_internal.hidden(mode_file);
|
||||
text_beacon_locator.hidden(mode_file);
|
||||
text_beacon_latitude.hidden(mode_file);
|
||||
text_beacon_latitude_value.hidden(mode_file);
|
||||
text_beacon_longitude.hidden(mode_file);
|
||||
text_beacon_longitude_value.hidden(mode_file);
|
||||
button_mangps.hidden(mode_file);
|
||||
options_beacon_type.hidden(mode_file);
|
||||
options_beacon_protocol.hidden(mode_file);
|
||||
options_beacon_country.hidden(mode_file);
|
||||
text_field_beacon_locator.hidden(mode_file);
|
||||
}
|
||||
|
||||
EPIRBTXAppView::EPIRBTXAppView(
|
||||
NavigationView& nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&options_mode,
|
||||
&text_beacon,
|
||||
&text_description_label,
|
||||
&text_beacon_type,
|
||||
&options_beacon_type,
|
||||
&options_beacon_protocol,
|
||||
&text_beacon_country,
|
||||
&options_beacon_country,
|
||||
&checkbox_beacon_internal,
|
||||
&text_beacon_locator,
|
||||
&text_beacon_latitude,
|
||||
&text_beacon_latitude_value,
|
||||
&text_beacon_longitude,
|
||||
&text_beacon_longitude_value,
|
||||
&button_mangps,
|
||||
&text_field_beacon_locator,
|
||||
&options_frame,
|
||||
&text_description,
|
||||
&text_description_end,
|
||||
&text_frame,
|
||||
&text_frame_end,
|
||||
&text_timeout,
|
||||
&checkbox_loop,
|
||||
&field_delay,
|
||||
&button_tx,
|
||||
&checkbox_am,
|
||||
&field_am_frequency,
|
||||
&checkbox_send_on_change,
|
||||
&tx_view});
|
||||
|
||||
text_beacon.set_style(Theme::getInstance()->fg_light);
|
||||
text_description_label.set_style(Theme::getInstance()->fg_light);
|
||||
text_beacon_type.set_style(Theme::getInstance()->fg_light);
|
||||
text_beacon_country.set_style(Theme::getInstance()->fg_light);
|
||||
text_beacon_locator.set_style(Theme::getInstance()->fg_light);
|
||||
text_beacon_latitude.set_style(Theme::getInstance()->fg_light);
|
||||
text_beacon_longitude.set_style(Theme::getInstance()->fg_light);
|
||||
|
||||
// Restore settings
|
||||
checkbox_am.set_value(am_enabled);
|
||||
checkbox_loop.set_value(loop_enabled);
|
||||
checkbox_send_on_change.set_value(send_on_change);
|
||||
options_mode.set_by_value(!mode_file);
|
||||
transmitter_model.set_target_frequency(bpsk_frequency);
|
||||
field_am_frequency.set_value(am_frequency);
|
||||
field_delay.set_value(delay);
|
||||
options_beacon_type.set_by_value(beacon_type);
|
||||
options_beacon_protocol.set_by_value(beacon_protocol);
|
||||
options_beacon_country.set_by_value(beacon_country);
|
||||
checkbox_beacon_internal.set_value(beacon_internal);
|
||||
beacon_params.type = (BeaconType)beacon_type;
|
||||
beacon_params.has_121_5 = am_enabled;
|
||||
beacon_params.location.locator = locator;
|
||||
beacon_params.is_internal = beacon_internal;
|
||||
init_from_locator(beacon_params.location);
|
||||
update_mode();
|
||||
update_location();
|
||||
|
||||
options_mode.on_change = [this](size_t index, OptionsField::value_t) {
|
||||
mode_file = (index == 0);
|
||||
update_mode();
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_beacon_type.on_change = [this](size_t index, OptionsField::value_t) {
|
||||
beacon_params.type = (BeaconType)index;
|
||||
beacon_type = index;
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_beacon_protocol.on_change = [this](size_t index, OptionsField::value_t) {
|
||||
beacon_params.protocol = (BeaconProtocol)index;
|
||||
beacon_protocol = index;
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_beacon_country.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
beacon_params.country = v;
|
||||
beacon_country = v;
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
checkbox_beacon_internal.on_select = [this](Checkbox&, bool v) {
|
||||
beacon_internal = v;
|
||||
beacon_params.is_internal = v;
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
text_field_beacon_locator.on_select = [this, &nav](TextField&) mutable {
|
||||
auto te_view = nav.push<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 && !epirb_tx_message.mode_bpsk && am_enabled)
|
||||
// Update transmitter frequency
|
||||
transmitter_model.set_target_frequency(am_frequency);
|
||||
};
|
||||
|
||||
// Load available beacons from BEACONS.TXT files (or default).
|
||||
load_beacons();
|
||||
// Setup options_frame content with loaded beacons
|
||||
using option_t = std::pair<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;
|
||||
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) {
|
||||
field_am_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
checkbox_send_on_change.on_select = [this](Checkbox&, bool v) {
|
||||
send_on_change = v;
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
bpsk_frequency = f;
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx();
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop_tx();
|
||||
};
|
||||
|
||||
button_tx.on_select = [this](Button&) {
|
||||
if (!transmitting)
|
||||
start_tx();
|
||||
else
|
||||
stop_tx();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load beacons from /EPIRB/BEACONS.TXT file on sd card
|
||||
*/
|
||||
void EPIRBTXAppView::load_beacons() {
|
||||
File beacons_file;
|
||||
auto error = beacons_file.open(epirb_dir / u"BEACONS.TXT");
|
||||
beacons.clear();
|
||||
|
||||
if (!error) {
|
||||
// BEACONS.TXT file exists
|
||||
auto reader = FileLineReader(beacons_file);
|
||||
for (const auto& line : reader) {
|
||||
// Skip comment lines
|
||||
if (line.length() == 0 || line[0] == '#')
|
||||
continue;
|
||||
|
||||
// Split line with ';' separator
|
||||
auto cols = split_string(line, ';');
|
||||
if (cols.size() != 3)
|
||||
continue;
|
||||
|
||||
// Read current beacon
|
||||
Beacon beacon{};
|
||||
beacon.title = trim(cols[0]);
|
||||
beacon.description = trim(cols[1]);
|
||||
// Make sure frame is not longer tha 18 bytes / 36 hex character
|
||||
beacon.frame = trim(cols[2]).substr(0, 36);
|
||||
size_t size = beacon.frame.size();
|
||||
if (size <= 0)
|
||||
continue; // Invalid line.
|
||||
// Beacon is valid, add it to the list
|
||||
beacons.emplace_back(std::move(beacon));
|
||||
}
|
||||
}
|
||||
if (beacons.empty()) {
|
||||
// No beacons file or empty flile: just add default beacon
|
||||
beacons.push_back(default_beacon);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __EPIRB_TX_H__
|
||||
#define __EPIRB_TX_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "tonesets.hpp"
|
||||
|
||||
#define BEACON_HEXA_SIZE 36
|
||||
#define BEACON_HEXA_HALF_SIZE 18
|
||||
#define BEACON_SIZE 18
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
enum class BeaconType {
|
||||
EPIRB = 0,
|
||||
ELT = 1,
|
||||
PLB = 2
|
||||
};
|
||||
|
||||
enum class BeaconProtocol {
|
||||
USER = 0,
|
||||
STANDARD = 1,
|
||||
NATIONAL = 2
|
||||
};
|
||||
|
||||
struct Location {
|
||||
std::string locator;
|
||||
bool south;
|
||||
uint16_t lat_deg;
|
||||
uint8_t lat_min;
|
||||
uint8_t lat_sec;
|
||||
float latitude;
|
||||
bool west;
|
||||
uint16_t long_deg;
|
||||
uint8_t long_min;
|
||||
uint8_t long_sec;
|
||||
float longitude;
|
||||
};
|
||||
|
||||
struct BeaconParams {
|
||||
BeaconType type;
|
||||
BeaconProtocol protocol;
|
||||
uint32_t country;
|
||||
bool is_test;
|
||||
bool is_internal;
|
||||
bool has_121_5;
|
||||
Location location;
|
||||
};
|
||||
|
||||
class EPIRBTXAppView : public View {
|
||||
public:
|
||||
EPIRBTXAppView(NavigationView& nav);
|
||||
~EPIRBTXAppView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "EPIRB TX"; };
|
||||
|
||||
private:
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void update_config();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_timer();
|
||||
void load_beacons();
|
||||
void set_tx_button_state(bool active);
|
||||
std::string frame_to_hex_string(bool start);
|
||||
void generate_frame(BeaconParams params);
|
||||
void update_frame(bool updateConfig = true);
|
||||
void update_mode();
|
||||
void update_location(bool updateLocatorField = true);
|
||||
|
||||
struct Beacon {
|
||||
std::string title{};
|
||||
std::string description{};
|
||||
std::string frame{};
|
||||
};
|
||||
std::vector<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{121500000};
|
||||
// Frequency of the 406 MHz BPSK signal
|
||||
rf::Frequency bpsk_frequency{406025000};
|
||||
|
||||
// True when using a beacon from the BEACONS.TXT file
|
||||
bool mode_file{true};
|
||||
// True when looping on sending beacons is enabled
|
||||
bool loop_enabled{true};
|
||||
// True if AM emergency signal transmission is enabled
|
||||
bool am_enabled{true};
|
||||
// True if we want to send a new frame each time the user changes the current beacon
|
||||
bool send_on_change{false};
|
||||
// The current locator string
|
||||
std::string locator{"JN03RO"};
|
||||
// The delay between each frame when on loop mode
|
||||
uint32_t delay{50};
|
||||
uint8_t beacon_type{0};
|
||||
// Currently selected beacon protocol
|
||||
uint8_t beacon_protocol{0};
|
||||
// Currently selected beacon country
|
||||
uint32_t beacon_country{227};
|
||||
// Current beacon's internal state (true for internal location system)
|
||||
bool beacon_internal{true};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
TONES_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_epirb",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"sbeacon"sv, &selected_beacon},
|
||||
{"amfreq"sv, &am_frequency},
|
||||
{"bpskfreq"sv, &bpsk_frequency},
|
||||
{"loop"sv, &loop_enabled},
|
||||
{"delay"sv, &delay},
|
||||
{"file"sv, &mode_file},
|
||||
{"am"sv, &am_enabled},
|
||||
{"soc"sv, &send_on_change},
|
||||
{"type"sv, &beacon_type},
|
||||
{"proto"sv, &beacon_protocol},
|
||||
{"country"sv, &beacon_country},
|
||||
{"internal"sv, &beacon_internal},
|
||||
{"locator"sv, &locator},
|
||||
}};
|
||||
|
||||
// Time of the last sent frame
|
||||
uint32_t last_frame_time{0};
|
||||
// True when transmission is enabled
|
||||
bool transmitting{false};
|
||||
// True when currently looping on sending beacons
|
||||
bool loop{false};
|
||||
|
||||
// Current EPIRBTXDataMessage for baseband
|
||||
EPIRBTXDataMessage epirb_tx_message{};
|
||||
|
||||
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
||||
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
// For file mode
|
||||
Text text_beacon{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
||||
"Beacon:"};
|
||||
// Beacon selection from BEACONS.TXT
|
||||
OptionsField options_frame{
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
30,
|
||||
{}};
|
||||
Text text_description_label{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
"Description:"};
|
||||
Text text_description{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_description_end{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// For manual mode
|
||||
Text text_beacon_type{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Type:"};
|
||||
Text text_beacon_country{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Country:"};
|
||||
Checkbox checkbox_beacon_internal{
|
||||
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
||||
12,
|
||||
"Internal",
|
||||
true};
|
||||
Text text_beacon_locator{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Locator:"};
|
||||
Text text_beacon_latitude{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Lat.:"};
|
||||
Text text_beacon_longitude{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||
"Long.:"};
|
||||
Text text_beacon_latitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_beacon_longitude_value{
|
||||
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
OptionsField options_beacon_type{
|
||||
{UI_POS_X(9), UI_POS_Y(1)},
|
||||
7,
|
||||
{{"EPIRB", 0},
|
||||
{"ELT", 1},
|
||||
{"PLB", 2}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
{{"User", 0},
|
||||
{"Standard", 1},
|
||||
{"National", 2}}};
|
||||
OptionsField options_beacon_country{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
7,
|
||||
{{"France", 227},
|
||||
{"USA", 366},
|
||||
{"Germany", 211},
|
||||
{"Russia", 273},
|
||||
{"Spain", 224},
|
||||
{"Japan", 431},
|
||||
{"UK", 232}}};
|
||||
TextField text_field_beacon_locator{
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
||||
"JN03RO"};
|
||||
Button button_mangps{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"Set pos."};
|
||||
|
||||
// Mode selection (file/manual)
|
||||
OptionsField options_mode{
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
30,
|
||||
{{"File (BEACONS.TXT)", 0},
|
||||
{"Manual (Editor)", 1}}};
|
||||
|
||||
// Frame content
|
||||
Text text_frame{
|
||||
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_frame_end{
|
||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_timeout{
|
||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
// Transmission settings
|
||||
Checkbox checkbox_loop{
|
||||
{UI_POS_X(0), UI_POS_Y(9)},
|
||||
10,
|
||||
"Resend every",
|
||||
true};
|
||||
NumberField field_delay{
|
||||
{UI_POS_X(15), UI_POS_Y(9)},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
Checkbox checkbox_am{
|
||||
{UI_POS_X(0), UI_POS_Y(11)},
|
||||
10,
|
||||
"AM signal",
|
||||
true};
|
||||
FrequencyField field_am_frequency{
|
||||
{UI_POS_X(13), UI_POS_Y(12)}};
|
||||
Checkbox checkbox_send_on_change{
|
||||
{UI_POS_X(0), UI_POS_Y(13)},
|
||||
14,
|
||||
"Send on change",
|
||||
true};
|
||||
Button button_tx{
|
||||
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||
"START"};
|
||||
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
||||
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
||||
|
||||
// Transmitter view
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<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__*/
|
||||
+51
-6
@@ -83,6 +83,10 @@ set(EXTCPPSRC
|
||||
external/tpmsrx/main.cpp
|
||||
external/tpmsrx/tpms_app.cpp
|
||||
|
||||
#tpmstx 800 bytes - TPMS transmit with editable fields
|
||||
external/tpmstx/main.cpp
|
||||
external/tpmstx/tpms_tx_app.cpp
|
||||
|
||||
#protoview 8 byte
|
||||
external/protoview/main.cpp
|
||||
external/protoview/ui_protoview.cpp
|
||||
@@ -99,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
|
||||
@@ -109,8 +121,8 @@ set(EXTCPPSRC
|
||||
external/random_password/sha512.cpp
|
||||
|
||||
#acars
|
||||
#external/acars_rx/main.cpp
|
||||
#external/acars_rx/acars_app.cpp
|
||||
external/acars_rx/main.cpp
|
||||
external/acars_rx/acars_app.cpp
|
||||
|
||||
#wefax_rx 192 bytes
|
||||
external/wefax_rx/main.cpp
|
||||
@@ -252,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
|
||||
@@ -293,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
|
||||
@@ -304,6 +320,26 @@ set(EXTCPPSRC
|
||||
external/rtty_tx/main.cpp
|
||||
external/rtty_tx/ui_rtty_tx.cpp
|
||||
external/rtty_tx/baudot.cpp
|
||||
|
||||
#pocsag_tx
|
||||
external/pocsag_tx/main.cpp
|
||||
external/pocsag_tx/ui_pocsag_tx.cpp
|
||||
|
||||
#time_sink
|
||||
external/time_sink/main.cpp
|
||||
external/time_sink/ui_time_sink.cpp
|
||||
|
||||
#kiss_tnc
|
||||
external/kiss_tnc/main.cpp
|
||||
external/kiss_tnc/ui_kiss_tnc.cpp
|
||||
|
||||
#fpv_detect
|
||||
external/fpv_detect/main.cpp
|
||||
external/fpv_detect/ui_fpv_detect.cpp
|
||||
|
||||
#p25_tx
|
||||
external/p25_tx/main.cpp
|
||||
external/p25_tx/ui_p25_tx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -326,13 +362,16 @@ set(EXTAPPLIST
|
||||
audio_test
|
||||
wardrivemap
|
||||
tpmsrx
|
||||
tpmstx
|
||||
protoview
|
||||
adsbtx
|
||||
#morse_tx
|
||||
sstvtx
|
||||
same_tx
|
||||
mdc_tx
|
||||
sstvrx
|
||||
random_password
|
||||
# acars_rx --not working
|
||||
acars_rx
|
||||
wefax_rx
|
||||
noaaapt_rx
|
||||
shoppingcart_lock
|
||||
@@ -362,11 +401,13 @@ set(EXTAPPLIST
|
||||
gfxeq
|
||||
waterfall_designer
|
||||
detector_rx
|
||||
fpv_detect
|
||||
spaceinv
|
||||
blackjack
|
||||
battleship
|
||||
ert
|
||||
epirb_rx
|
||||
epirb_tx
|
||||
soundboard
|
||||
game2048
|
||||
bht_tx
|
||||
@@ -377,9 +418,13 @@ set(EXTAPPLIST
|
||||
siggen
|
||||
morse_radio
|
||||
morseradiotx
|
||||
keeloqtx
|
||||
keeloqtx
|
||||
rtty_rx
|
||||
rtty_tx
|
||||
pocsag_tx
|
||||
time_sink
|
||||
kiss_tnc
|
||||
p25_tx
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
|
||||
+157
-96
@@ -23,80 +23,89 @@ MEMORY
|
||||
* Also need to consider processor memory map - reading 0xADxxxxxx generates a fault which may be better than unexpected behavior.
|
||||
* External app address ranges below must match those in python file "external_app_info.py".
|
||||
*/
|
||||
ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k
|
||||
ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k
|
||||
ram_external_app_font_viewer(rwx) : org = 0xADB30000, len = 32k
|
||||
ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k
|
||||
ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k
|
||||
ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k
|
||||
ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k
|
||||
ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k
|
||||
ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k
|
||||
ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k
|
||||
ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k
|
||||
ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k
|
||||
ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k
|
||||
ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k
|
||||
ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k
|
||||
ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k
|
||||
ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k
|
||||
ram_external_app_wardrivemap(rwx) : org = 0xADC20000, len = 32k
|
||||
ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k
|
||||
ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k
|
||||
ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k
|
||||
ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k
|
||||
ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k
|
||||
ram_external_app_random_password(rwx): org = 0xADC80000, len = 32k
|
||||
ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k
|
||||
ram_external_app_shoppingcart_lock(rwx): org = 0xADCA0000, len = 32k
|
||||
ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k
|
||||
ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k
|
||||
ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k
|
||||
ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k
|
||||
ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k
|
||||
ram_external_app_mcu_temperature(rwx): org = 0xADD00000, len = 32k
|
||||
ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k
|
||||
ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k
|
||||
ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k
|
||||
ram_external_app_app_manager(rwx) : org = 0xADD40000, len = 32k
|
||||
ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k
|
||||
ram_external_app_antenna_length(rwx): org = 0xADD60000, len = 32k
|
||||
ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k
|
||||
ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k
|
||||
ram_external_app_playlist_editor(rwx): org = 0xADD90000, len = 32k
|
||||
ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k
|
||||
ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k
|
||||
ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k
|
||||
ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k
|
||||
ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k
|
||||
ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k
|
||||
ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k
|
||||
ram_external_app_level (rwx) : org = 0xADE10000, len = 32k
|
||||
ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k
|
||||
ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k
|
||||
ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k
|
||||
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
|
||||
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
|
||||
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
|
||||
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
|
||||
ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k
|
||||
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
|
||||
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
|
||||
ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k
|
||||
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
|
||||
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
|
||||
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
|
||||
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
|
||||
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
|
||||
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
|
||||
ram_external_app_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_afsk_rx (rwx) : org = 0xADB10000, len = 32k
|
||||
ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k
|
||||
ram_external_app_font_viewer (rwx) : org = 0xADB30000, len = 32k
|
||||
ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k
|
||||
ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k
|
||||
ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k
|
||||
ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k
|
||||
ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k
|
||||
ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k
|
||||
ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k
|
||||
ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k
|
||||
ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k
|
||||
ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k
|
||||
ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k
|
||||
ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k
|
||||
ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k
|
||||
ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k
|
||||
ram_external_app_wardrivemap (rwx) : org = 0xADC20000, len = 32k
|
||||
ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k
|
||||
ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k
|
||||
ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k
|
||||
ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k
|
||||
ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k
|
||||
ram_external_app_random_password (rwx) : org = 0xADC80000, len = 32k
|
||||
ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k
|
||||
ram_external_app_shoppingcart_lock (rwx) : org = 0xADCA0000, len = 32k
|
||||
ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k
|
||||
ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k
|
||||
ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k
|
||||
ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k
|
||||
ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k
|
||||
ram_external_app_mcu_temperature (rwx) : org = 0xADD00000, len = 32k
|
||||
ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k
|
||||
ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k
|
||||
ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k
|
||||
ram_external_app_app_manager (rwx) : org = 0xADD40000, len = 32k
|
||||
ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k
|
||||
ram_external_app_antenna_length (rwx) : org = 0xADD60000, len = 32k
|
||||
ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k
|
||||
ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k
|
||||
ram_external_app_playlist_editor (rwx) : org = 0xADD90000, len = 32k
|
||||
ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k
|
||||
ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k
|
||||
ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k
|
||||
ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k
|
||||
ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k
|
||||
ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k
|
||||
ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k
|
||||
ram_external_app_level (rwx) : org = 0xADE10000, len = 32k
|
||||
ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k
|
||||
ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k
|
||||
ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k
|
||||
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
|
||||
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
|
||||
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
|
||||
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
|
||||
ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k
|
||||
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
|
||||
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
|
||||
ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k
|
||||
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
|
||||
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
|
||||
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
|
||||
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
|
||||
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
|
||||
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
|
||||
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
|
||||
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
|
||||
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
|
||||
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
|
||||
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
|
||||
ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k
|
||||
ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k
|
||||
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
|
||||
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
|
||||
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
|
||||
ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k
|
||||
ram_external_app_same_tx (rwx) : org = 0xADFE0000, len = 32k
|
||||
ram_external_app_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k
|
||||
ram_external_app_mdc_tx (rwx) : org = 0xAE000000, len = 32k
|
||||
ram_external_app_epirb_tx (rwx) : org = 0xAE010000, len = 32k
|
||||
ram_external_app_fpv_detect (rwx) : org = 0xAE020000, len = 32k
|
||||
ram_external_app_p25_tx (rwx) : org = 0xAE030000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -137,7 +146,7 @@ SECTIONS
|
||||
*(*ui*external_app*nrf_rx*);
|
||||
} > ram_external_app_nrf_rx
|
||||
|
||||
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_coasterp.application_information));
|
||||
*(*ui*external_app*coasterp*);
|
||||
@@ -185,55 +194,61 @@ SECTIONS
|
||||
*(*ui*external_app*tetris*);
|
||||
} > ram_external_app_tetris
|
||||
|
||||
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_extsensors.application_information));
|
||||
*(*ui*external_app*extsensors*);
|
||||
} > ram_external_app_extsensors
|
||||
|
||||
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_foxhunt_rx.application_information));
|
||||
*(*ui*external_app*foxhunt_rx*);
|
||||
} > ram_external_app_foxhunt_rx
|
||||
|
||||
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_audio_test.application_information));
|
||||
*(*ui*external_app*audio_test*);
|
||||
} > ram_external_app_audio_test
|
||||
|
||||
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_wardrivemap.application_information));
|
||||
*(*ui*external_app*wardrivemap*);
|
||||
} > ram_external_app_wardrivemap
|
||||
|
||||
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tpmsrx.application_information));
|
||||
*(*ui*external_app*tpmsrx*);
|
||||
} > ram_external_app_tpmsrx
|
||||
|
||||
.external_app_protoview : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tpmstx.application_information));
|
||||
*(*ui*external_app*tpmstx*);
|
||||
} > ram_external_app_tpmstx
|
||||
|
||||
.external_app_protoview : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_protoview.application_information));
|
||||
*(*ui*external_app*protoview*);
|
||||
} > ram_external_app_protoview
|
||||
|
||||
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_adsbtx.application_information));
|
||||
*(*ui*external_app*adsbtx*);
|
||||
} > ram_external_app_adsbtx
|
||||
|
||||
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_tx.application_information));
|
||||
*(*ui*external_app*morse_tx*);
|
||||
} > ram_external_app_morse_tx
|
||||
|
||||
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sstvtx.application_information));
|
||||
*(*ui*external_app*sstvtx*);
|
||||
@@ -251,19 +266,19 @@ SECTIONS
|
||||
*(*ui*external_app*acars_rx*);
|
||||
} > ram_external_app_acars_rx
|
||||
|
||||
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_shoppingcart_lock.application_information));
|
||||
*(*ui*external_app*shoppingcart_lock*);
|
||||
} > ram_external_app_shoppingcart_lock
|
||||
|
||||
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_cvs_spam.application_information));
|
||||
*(*ui*external_app*cvs_spam*);
|
||||
} > ram_external_app_cvs_spam
|
||||
|
||||
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_ookbrute.application_information));
|
||||
*(*ui*external_app*ookbrute*);
|
||||
@@ -275,7 +290,7 @@ SECTIONS
|
||||
*(*ui*external_app*ook_editor*);
|
||||
} > ram_external_app_ook_editor
|
||||
|
||||
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_flippertx.application_information));
|
||||
*(*ui*external_app*flippertx*);
|
||||
@@ -359,13 +374,13 @@ SECTIONS
|
||||
*(*ui*external_app*stopwatch*);
|
||||
} > ram_external_app_stopwatch
|
||||
|
||||
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_wefax_rx.application_information));
|
||||
*(*ui*external_app*wefax_rx*);
|
||||
} > ram_external_app_wefax_rx
|
||||
|
||||
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
|
||||
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_noaaapt_rx.application_information));
|
||||
*(*ui*external_app*noaaapt_rx*);
|
||||
@@ -467,14 +482,12 @@ SECTIONS
|
||||
*(*ui*external_app*game2048*);
|
||||
} > ram_external_app_game2048
|
||||
|
||||
|
||||
.external_app_bht_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_bht_tx.application_information));
|
||||
*(*ui*external_app*bht_tx*);
|
||||
} > ram_external_app_bht_tx
|
||||
|
||||
|
||||
.external_app_morse_practice : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_practice.application_information));
|
||||
@@ -491,7 +504,7 @@ SECTIONS
|
||||
{
|
||||
KEEP(*(.external_app.app_flex_rx.application_information));
|
||||
*(*ui*external_app*flex_rx*);
|
||||
} > ram_external_app_flex_rx
|
||||
} > ram_external_app_flex_rx
|
||||
|
||||
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
@@ -511,12 +524,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));
|
||||
@@ -534,6 +547,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));
|
||||
@@ -545,5 +559,52 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_rtty_tx.application_information));
|
||||
*(*ui*external_app*rtty_tx*);
|
||||
} > ram_external_app_rtty_tx
|
||||
}
|
||||
|
||||
.external_app_pocsag_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_pocsag_tx.application_information));
|
||||
*(*ui*external_app*pocsag_tx*);
|
||||
} > ram_external_app_pocsag_tx
|
||||
|
||||
.external_app_time_sink : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_time_sink.application_information));
|
||||
*(*ui*external_app*time_sink*);
|
||||
} > ram_external_app_time_sink
|
||||
|
||||
.external_app_same_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_same_tx.application_information));
|
||||
*(*ui*external_app*same_tx*);
|
||||
} > ram_external_app_same_tx
|
||||
|
||||
.external_app_kiss_tnc : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_kiss_tnc.application_information));
|
||||
*(*ui*external_app*kiss_tnc*);
|
||||
} > ram_external_app_kiss_tnc
|
||||
|
||||
.external_app_mdc_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_mdc_tx.application_information));
|
||||
*(*ui*external_app*mdc_tx*);
|
||||
} > ram_external_app_mdc_tx
|
||||
|
||||
.external_app_epirb_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_epirb_tx.application_information));
|
||||
*(*ui*external_app*epirb_tx*);
|
||||
} > ram_external_app_epirb_tx
|
||||
|
||||
.external_app_fpv_detect : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_fpv_detect.application_information));
|
||||
*(*ui*external_app*fpv_detect*);
|
||||
} > ram_external_app_fpv_detect
|
||||
|
||||
.external_app_p25_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_p25_tx.application_information));
|
||||
*(*ui*external_app*p25_tx*);
|
||||
} > ram_external_app_p25_tx
|
||||
}
|
||||
|
||||
+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,666 @@
|
||||
// Author: berkeozkir (Berke Özkır)
|
||||
|
||||
/*
|
||||
* FPV RX — how frequency search and lock work
|
||||
*
|
||||
* 1. Power metric: We use channelized power (baseband IQ magnitude² in the
|
||||
* capture bandwidth), not raw RF RSSI. Stats come from ChannelStatsCollector
|
||||
* over filtered IQ, so we see power in the tuned channel, not wideband.
|
||||
*
|
||||
* 2. Scanning: We step through FPV bands/channels (A/B/E/F/R, 8 ch each). When
|
||||
* power on the current channel exceeds the detect threshold, we enter
|
||||
* "Candidate" and run verification — we do not lock on a single peak.
|
||||
*
|
||||
* 3. Verification (making sure the drone is on that freq):
|
||||
* - Multiple samples: verify_hits / verify_misses over several updates.
|
||||
* - Confidence score: combines average above threshold, peak margin,
|
||||
* hit count, and crucially neighbor_margin = center power − max(adjacent ch).
|
||||
* - Neighbor check: we only lock if the center channel is at least
|
||||
* MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB above both adjacent channels. That
|
||||
* avoids locking on wide/noise that looks strong on many channels.
|
||||
* During Candidate we re-sample left/right adjacent channels so the
|
||||
* margin is based on current measurements, not stale scan data.
|
||||
* - Lock requires: enough hits, confidence ≥ LOCK_CONFIDENCE_MIN, and
|
||||
* center stronger than neighbors (strong lock path has same requirement).
|
||||
*
|
||||
* 4. Lock hold: Once locked, we only unlock when power stays below
|
||||
* unlock_threshold_db() long enough to drain lock_hold_ (down from
|
||||
* LOCK_HOLD_MAX), so brief fades don't drop lock.
|
||||
*/
|
||||
|
||||
#include "ui_fpv_detect.hpp"
|
||||
|
||||
#include <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_analog_carrier(const ChannelStatistics& statistics) const {
|
||||
return statistics.max_db >= detect_threshold_db();
|
||||
}
|
||||
|
||||
uint8_t FpvDetectView::compute_candidate_confidence() const {
|
||||
if (!verify_samples_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int32_t avg_db = candidate_average_db();
|
||||
const int32_t center_margin = avg_db - detect_threshold_db();
|
||||
const int32_t peak_margin = verify_peak_db_ - detect_threshold_db();
|
||||
const int32_t neighbor_margin = avg_db - neighbor_best_db();
|
||||
|
||||
int32_t score = 18;
|
||||
score += center_margin * 7;
|
||||
score += peak_margin * 4;
|
||||
score += static_cast<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("VERIFY FPV");
|
||||
break;
|
||||
case DetectState::Locked:
|
||||
text_state.set("DRONE FOUND");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FpvDetectView::update_confidence_text() {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "Conf %u%%", static_cast<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 for analog FPV", band_labels[band_mode]);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "Scanning all FPV bands");
|
||||
}
|
||||
break;
|
||||
|
||||
case DetectState::Candidate:
|
||||
std::snprintf(buf, sizeof(buf), "VERIFYING %c%d %ld MHz",
|
||||
band_labels[candidate_band_],
|
||||
static_cast<int>(candidate_ch_ + 1),
|
||||
static_cast<long>(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL));
|
||||
break;
|
||||
|
||||
case DetectState::Locked:
|
||||
std::snprintf(buf, sizeof(buf), "!!! DRONE FOUND !!! %c%d %ld",
|
||||
band_labels[candidate_band_],
|
||||
static_cast<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 conf %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_analog_carrier(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_analog_carrier(const ChannelStatistics& statistics) const;
|
||||
void enter_candidate(const ChannelStatistics& statistics);
|
||||
void evaluate_candidate_sample(const ChannelStatistics& statistics);
|
||||
void enter_lock();
|
||||
void update_lock(const ChannelStatistics& statistics);
|
||||
|
||||
uint8_t current_channel_index() const;
|
||||
uint8_t channel_index(uint8_t band, uint8_t ch) const;
|
||||
uint8_t compute_candidate_confidence() const;
|
||||
int16_t detect_threshold_db() const;
|
||||
int16_t unlock_threshold_db() const;
|
||||
int16_t candidate_average_db() const;
|
||||
int16_t neighbor_best_db() const;
|
||||
|
||||
void request_event_beep(uint32_t hz, uint32_t duration_ms);
|
||||
void update_freq_display();
|
||||
void update_state_badge();
|
||||
void update_confidence_text();
|
||||
void update_status_text();
|
||||
void update_detail_text();
|
||||
void update_stats_text(const ChannelStatistics& statistics);
|
||||
void update_visual_state();
|
||||
|
||||
DetectState detect_state_ = DetectState::Scanning;
|
||||
|
||||
uint8_t scan_band = 0;
|
||||
uint8_t scan_ch = 0;
|
||||
uint8_t band_mode = FPV_AUTO_SCAN_MODE;
|
||||
|
||||
uint8_t candidate_band_ = 0;
|
||||
uint8_t candidate_ch_ = 0;
|
||||
uint8_t verify_samples_ = 0;
|
||||
uint8_t verify_hits_ = 0;
|
||||
uint8_t verify_misses_ = 0;
|
||||
int32_t verify_sum_db_ = 0;
|
||||
int16_t verify_peak_db_ = -120;
|
||||
uint8_t candidate_confidence_ = 0;
|
||||
uint8_t lock_hold_ = 0;
|
||||
/** During Candidate: 0=center, 1=left neighbor, 2=right neighbor. Used to
|
||||
* re-sample adjacent channels so neighbor_margin uses current measurements. */
|
||||
uint8_t neighbor_phase_ = 0;
|
||||
bool neighbors_fresh_ = false;
|
||||
|
||||
uint8_t scan_dwell_frames_ = 0;
|
||||
uint32_t frame_counter_ = 0;
|
||||
uint32_t last_beep_frame_ = 0;
|
||||
|
||||
int16_t last_max_db_ = -127;
|
||||
uint8_t last_min_rssi_ = 0;
|
||||
uint8_t last_avg_rssi_ = 0;
|
||||
uint8_t last_max_rssi_ = 0;
|
||||
|
||||
std::array<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}, "Conf 0%"};
|
||||
Text text_detect_label{{UI_POS_X_RIGHT(10), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_DEFAULT_HEIGHT}, "Thr>"};
|
||||
|
||||
NumberField field_detect_threshold{
|
||||
{UI_POS_X_RIGHT(5), UI_POS_Y(2)},
|
||||
4,
|
||||
{-100, 20},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
Text freq_stats_rssi{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, "RSSI 0/0/0"};
|
||||
Text freq_stats_db{{UI_POS_X_RIGHT(14), UI_POS_Y(3), UI_POS_WIDTH(14), UI_POS_DEFAULT_HEIGHT}, "PWR -120 dB"};
|
||||
|
||||
Text text_status{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "SCANNING FOR ANALOG FPV"};
|
||||
Text text_detail{{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "Waiting for FPV-like carrier"};
|
||||
|
||||
RSSIGraph rssi_graph{{UI_POS_X(0), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(5), UI_POS_HEIGHT_REMAINING(7)}};
|
||||
RSSI rssi{{UI_POS_X_RIGHT(5), UI_POS_Y(6), UI_POS_WIDTH(5), UI_POS_HEIGHT_REMAINING(7)}};
|
||||
|
||||
MessageHandlerRegistration message_handler_stats{
|
||||
Message::ID::ChannelStatistics,
|
||||
[this](const Message* const p) {
|
||||
this->on_statistics_update(static_cast<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
|
||||
+1
-1
@@ -33,7 +33,7 @@ Game2048View::Game2048View(NavigationView& nav)
|
||||
|
||||
void Game2048View::on_show() {
|
||||
if (!initialized) {
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
reset_game();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
+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
|
||||
@@ -137,6 +137,7 @@ LevelView::LevelView(NavigationView& nav)
|
||||
else
|
||||
v = SPEC_MODULATION;
|
||||
field_mode.set_selected_index(v);
|
||||
return; // Important ! Guru will occur if you don't return when modifying field from within on_change !
|
||||
}
|
||||
last_mode = v;
|
||||
change_mode(v);
|
||||
|
||||
+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
|
||||
+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 */
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_pocsag_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::pocsag_tx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<POCSAGTXView>();
|
||||
}
|
||||
} // namespace ui::external_app::pocsag_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_pocsag_tx.application_information"), used)) application_information_t _application_information_pocsag_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::pocsag_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "POCSG TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xBA,
|
||||
0x45,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x92,
|
||||
0x7C,
|
||||
0x92,
|
||||
0x7C,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_fsktx */ {'P', 'F', 'S', 'K'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
+4
-4
@@ -31,7 +31,7 @@
|
||||
using namespace portapack;
|
||||
using namespace pocsag;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::pocsag_tx {
|
||||
|
||||
#define MAX_POCSAG_LENGTH 80
|
||||
|
||||
@@ -44,7 +44,7 @@ POCSAGTXView::~POCSAGTXView() {
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void POCSAGTXView::on_remote(const PocsagTosendMessage data) {
|
||||
void POCSAGTXView::on_remete(const PocsagTosendMessage data) {
|
||||
// check if still sending or not
|
||||
size_t tmp = 0;
|
||||
if (data.baud == 1200) tmp = 1;
|
||||
@@ -159,7 +159,7 @@ void POCSAGTXView::on_set_text(NavigationView& nav) {
|
||||
POCSAGTXView::POCSAGTXView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&options_bitrate,
|
||||
@@ -205,4 +205,4 @@ POCSAGTXView::POCSAGTXView(
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} /* namespace ui::external_app::pocsag_tx */
|
||||
+10
-10
@@ -37,7 +37,7 @@
|
||||
|
||||
using namespace pocsag;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::pocsag_tx {
|
||||
|
||||
class POCSAGTXView : public View {
|
||||
public:
|
||||
@@ -76,7 +76,7 @@ class POCSAGTXView : public View {
|
||||
|
||||
void on_set_text(NavigationView& nav);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_remote(const PocsagTosendMessage data);
|
||||
void on_remete(const PocsagTosendMessage data);
|
||||
bool start_tx();
|
||||
|
||||
Labels labels{
|
||||
@@ -140,21 +140,21 @@ class POCSAGTXView : public View {
|
||||
10000,
|
||||
9};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_remete{
|
||||
Message::ID::PocsagTosend,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<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 */
|
||||
} /* namespace ui::external_app::pocsag_tx */
|
||||
|
||||
#endif /*__POCSAG_TX_H__*/
|
||||
@@ -239,10 +239,10 @@ void RandomPasswordView::on_freqchg(int64_t freq) {
|
||||
}
|
||||
|
||||
void RandomPasswordView::set_random_freq() {
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
// this is only for seed to visit random freq, the radio is still real random
|
||||
|
||||
auto random_freq = 100000000 + (std::rand() % 900000000); // 100mhz to 1ghz
|
||||
auto random_freq = 100000000 + (rand() % 900000000); // 100mhz to 1ghz
|
||||
receiver_model.set_target_frequency(random_freq);
|
||||
field_frequency.set_value(random_freq);
|
||||
}
|
||||
@@ -297,12 +297,12 @@ void RandomPasswordView::new_password() {
|
||||
/// roll worker
|
||||
for (int i = 0; i < password_length * 2; i += 2) {
|
||||
unsigned int seed = seeds_deque[i];
|
||||
std::srand(seed);
|
||||
srand(seed);
|
||||
uint8_t rollnum = (uint8_t)(seeds_deque[i + 1] % 128);
|
||||
uint8_t nu = 0;
|
||||
for (uint8_t o = 0; o < rollnum; ++o) nu = std::rand();
|
||||
for (uint8_t o = 0; o < rollnum; ++o) nu = rand();
|
||||
nu++;
|
||||
char c = charset[std::rand() % charset.length()];
|
||||
char c = charset[rand() % charset.length()];
|
||||
initial_password += c;
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
@@ -461,6 +461,7 @@ ScannerView::ScannerView(
|
||||
else
|
||||
v = WFM_MODULATION;
|
||||
field_mode.set_selected_index(v);
|
||||
return; // Important ! Guru will occur if you don't return when modifying field from within on_change !
|
||||
}
|
||||
last_mode = v;
|
||||
receiver_model.disable();
|
||||
|
||||
+4
-1
@@ -36,8 +36,11 @@ WipeSDView::WipeSDView(NavigationView& nav)
|
||||
}
|
||||
|
||||
WipeSDView::~WipeSDView() {
|
||||
if (thread)
|
||||
if (thread) {
|
||||
chThdTerminate(thread);
|
||||
chThdWait(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void WipeSDView::focus() {
|
||||
|
||||
@@ -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
@@ -221,7 +221,7 @@ void SnakeView::paint(Painter& painter) {
|
||||
(void)painter;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
|
||||
+1
-1
@@ -605,7 +605,7 @@ void pause_game() {
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::srand(GenerateRandomSeed());
|
||||
srand(GenerateRandomSeed());
|
||||
Init();
|
||||
ShowLevelMenu();
|
||||
joystick.attach(&ReadJoystickForLevel, 0.3);
|
||||
|
||||
@@ -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__
|
||||
+1
-1
@@ -75,7 +75,7 @@ __attribute__((section(".external_app.app_tpmsrx.application_information"), used
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
/*.desired_menu_position = */ 7,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_tpms */ {'P', 'T', 'P', 'M'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
|
||||
+137
-1
@@ -42,6 +42,25 @@ std::string type(tpms::Reading::Type type) {
|
||||
return to_string_dec_uint(toUType(type), 2);
|
||||
}
|
||||
|
||||
std::string type_name(tpms::Reading::Type type) {
|
||||
switch (type) {
|
||||
case tpms::Reading::Type::None:
|
||||
return "None";
|
||||
case tpms::Reading::Type::FLM_64:
|
||||
return "FLM_64";
|
||||
case tpms::Reading::Type::FLM_72:
|
||||
return "FLM_72";
|
||||
case tpms::Reading::Type::FLM_80:
|
||||
return "FLM_80";
|
||||
case tpms::Reading::Type::Schrader:
|
||||
return "Schrader";
|
||||
case tpms::Reading::Type::GMC_96:
|
||||
return "GMC_96";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string id(tpms::TransponderID id) {
|
||||
return to_string_hex(id.value(), 8);
|
||||
}
|
||||
@@ -101,7 +120,8 @@ void TPMSRecentEntry::update(const tpms::Reading& reading) {
|
||||
}
|
||||
}
|
||||
|
||||
TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
TPMSAppView::TPMSAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
// baseband::run_image(portapack::spi_flash::image_tag_tpms);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
@@ -135,6 +155,11 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
};
|
||||
options_temperature.set_by_value(format::temp_unit);
|
||||
|
||||
// Add on_select handler for entries
|
||||
recent_entries_view.on_select = [this](const TPMSRecentEntry& entry) {
|
||||
on_show_detail(entry);
|
||||
};
|
||||
|
||||
logger = std::make_unique<TPMSLogger>();
|
||||
if (logger) {
|
||||
logger->append(logs_dir / u"TPMS.TXT");
|
||||
@@ -178,6 +203,7 @@ void TPMSAppView::on_packet(const tpms::Packet& packet) {
|
||||
const auto reading = reading_opt.value();
|
||||
auto& entry = ::on_packet(recent, TPMSRecentEntry::Key{reading.type(), reading.id()});
|
||||
entry.update(reading);
|
||||
entry.signal_type = packet.signal_type();
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
@@ -191,6 +217,116 @@ void TPMSAppView::on_show_list() {
|
||||
recent_entries_view.focus();
|
||||
}
|
||||
|
||||
void TPMSAppView::on_show_detail(const TPMSRecentEntry& entry) {
|
||||
nav_.push<TPMSRecentEntryDetailView>(entry);
|
||||
}
|
||||
|
||||
// Detail view implementation
|
||||
TPMSRecentEntryDetailView::TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&labels,
|
||||
&text_type,
|
||||
&text_id,
|
||||
&text_pressure,
|
||||
&text_temperature,
|
||||
&text_flags,
|
||||
&text_count,
|
||||
&button_done,
|
||||
&button_save});
|
||||
|
||||
// Display entry data
|
||||
text_type.set(format::type_name(entry.type));
|
||||
text_id.set(to_string_hex(entry.id.value(), 8));
|
||||
|
||||
if (entry.last_pressure.is_valid()) {
|
||||
std::string pressure_str = format::pressure(entry.last_pressure.value());
|
||||
std::string unit_str = format::pressure_unit == PRESSURE_UNIT_PSI ? " PSI" : format::pressure_unit == PRESSURE_UNIT_BAR ? " BAR"
|
||||
: " kPa";
|
||||
text_pressure.set(pressure_str + unit_str);
|
||||
} else {
|
||||
text_pressure.set("---");
|
||||
}
|
||||
|
||||
if (entry.last_temperature.is_valid()) {
|
||||
text_temperature.set(to_string_dec_int(entry.last_temperature.value().celsius(), 3) + " C");
|
||||
} else {
|
||||
text_temperature.set("---");
|
||||
}
|
||||
|
||||
if (entry.last_flags.is_valid()) {
|
||||
text_flags.set(to_string_hex(entry.last_flags.value(), 2));
|
||||
} else {
|
||||
text_flags.set("--");
|
||||
}
|
||||
|
||||
text_count.set(to_string_dec_uint(entry.received_count, 4));
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
on_save();
|
||||
};
|
||||
}
|
||||
|
||||
void TPMSRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void TPMSRecentEntryDetailView::set_entry(const TPMSRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
}
|
||||
|
||||
void TPMSRecentEntryDetailView::on_save() {
|
||||
auto timestamp = to_string_timestamp(rtc_time::now());
|
||||
std::string file_name = "TPMS_" + timestamp + ".TXT";
|
||||
ensure_directory(tpms_dir);
|
||||
auto file_path = tpms_dir / file_name;
|
||||
|
||||
if (save_file(file_path)) {
|
||||
nav_.display_modal("Saved", "Packet saved to:\n" + file_name);
|
||||
} else {
|
||||
nav_.display_modal("Error", "Failed to save\npacket");
|
||||
}
|
||||
}
|
||||
|
||||
bool TPMSRecentEntryDetailView::save_file(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error.is_valid())
|
||||
return false;
|
||||
|
||||
// Save in format compatible with TX app
|
||||
std::string content = "Type=" + to_string_dec_uint(toUType(entry_.type), 1) + "\n";
|
||||
content += "ID=" + to_string_hex(entry_.id.value(), 8) + "\n";
|
||||
|
||||
if (entry_.last_pressure.is_valid()) {
|
||||
content += "Pressure=" + to_string_dec_int(entry_.last_pressure.value().kilopascal(), 1) + "\n";
|
||||
} else {
|
||||
content += "Pressure=240\n"; // Default value
|
||||
}
|
||||
|
||||
if (entry_.last_temperature.is_valid()) {
|
||||
content += "Temperature=" + to_string_dec_int(entry_.last_temperature.value().celsius(), 1) + "\n";
|
||||
} else {
|
||||
content += "Temperature=25\n"; // Default value
|
||||
}
|
||||
|
||||
if (entry_.last_flags.is_valid()) {
|
||||
content += "Flags=" + to_string_hex(entry_.last_flags.value(), 2) + "\n";
|
||||
} else {
|
||||
content += "Flags=00\n";
|
||||
}
|
||||
|
||||
// Save signal type for proper retransmission
|
||||
content += "SignalType=" + to_string_dec_uint(toUType(entry_.signal_type), 1) + "\n";
|
||||
|
||||
f.write(content.c_str(), content.length());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tpmsrx
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -53,6 +53,7 @@ struct TPMSRecentEntry {
|
||||
|
||||
tpms::Reading::Type type{invalid_key.first};
|
||||
tpms::TransponderID id{invalid_key.second};
|
||||
tpms::SignalType signal_type{tpms::SignalType::OOK_8k192_Schrader};
|
||||
|
||||
size_t received_count{0};
|
||||
|
||||
@@ -89,6 +90,64 @@ class TPMSLogger {
|
||||
|
||||
using TPMSRecentEntriesView = RecentEntriesView<TPMSRecentEntries>;
|
||||
|
||||
class TPMSRecentEntryDetailView : public View {
|
||||
public:
|
||||
TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry);
|
||||
|
||||
void set_entry(const TPMSRecentEntry& entry);
|
||||
const TPMSRecentEntry& entry() const { return entry_; }
|
||||
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TPMSRecentEntry entry_;
|
||||
|
||||
void on_save();
|
||||
bool save_file(const std::filesystem::path& path);
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 5 * 16}, "Pressure:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 7 * 16}, "Temperature:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 9 * 16}, "Flags:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 11 * 16}, "Count:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text text_type{
|
||||
{8 * 8, 1 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_id{
|
||||
{8 * 8, 3 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_pressure{
|
||||
{12 * 8, 5 * 16, 16 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_temperature{
|
||||
{14 * 8, 7 * 16, 14 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_flags{
|
||||
{8 * 8, 9 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_count{
|
||||
{8 * 8, 11 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Button button_save{
|
||||
{0 * 8, 13 * 16, 14 * 8, 32},
|
||||
"Save"};
|
||||
|
||||
Button button_done{
|
||||
{16 * 8, 13 * 16, 14 * 8, 32},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
class TPMSAppView : public View {
|
||||
public:
|
||||
TPMSAppView(NavigationView& nav);
|
||||
@@ -105,6 +164,8 @@ class TPMSAppView : public View {
|
||||
std::string title() const override { return "TPMS RX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_{
|
||||
314900000 /* frequency*/
|
||||
,
|
||||
@@ -188,6 +249,7 @@ class TPMSAppView : public View {
|
||||
|
||||
void on_packet(const tpms::Packet& packet);
|
||||
void on_show_list();
|
||||
void on_show_detail(const TPMSRecentEntry& entry);
|
||||
void update_view();
|
||||
};
|
||||
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2026
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "tpms_tx_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::tpmstx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<TPMSTXView>();
|
||||
}
|
||||
} // namespace ui::external_app::tpmstx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_tpmstx.application_information"), used)) application_information_t _application_information_tpmstx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000, // will be filled at compile time
|
||||
/*.externalAppEntry = */ ui::external_app::tpmstx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "TPMS TX",
|
||||
/*.bitmap_data = */ {
|
||||
0xC0,
|
||||
0x03,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x18,
|
||||
0x18,
|
||||
0xEC,
|
||||
0x37,
|
||||
0x36,
|
||||
0x6D,
|
||||
0x3A,
|
||||
0x59,
|
||||
0x4B,
|
||||
0xD5,
|
||||
0x8B,
|
||||
0xD3,
|
||||
0xCB,
|
||||
0xD1,
|
||||
0xAB,
|
||||
0xD2,
|
||||
0x9A,
|
||||
0x5C,
|
||||
0xB6,
|
||||
0x6C,
|
||||
0xEC,
|
||||
0x37,
|
||||
0x18,
|
||||
0x18,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xC0,
|
||||
0x03,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ 6,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
/*
|
||||
* Copyright (C) 2026
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "tpms_tx_app.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "spi_image.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "manchester.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "encoders.hpp"
|
||||
#include "crc.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace tpms;
|
||||
|
||||
namespace ui::external_app::tpmstx {
|
||||
|
||||
void TPMSTXView::focus() {
|
||||
options_packet_type.focus();
|
||||
}
|
||||
|
||||
void TPMSTXView::update_signal_type_from_packet() {
|
||||
// Auto-select signal type based on packet type
|
||||
switch (packet_type_) {
|
||||
case tpms::Reading::Type::Schrader:
|
||||
signal_type_ = tpms::SignalType::OOK_8k192_Schrader;
|
||||
break;
|
||||
case tpms::Reading::Type::FLM_64:
|
||||
case tpms::Reading::Type::FLM_72:
|
||||
case tpms::Reading::Type::FLM_80:
|
||||
signal_type_ = tpms::SignalType::FSK_19k2_Schrader;
|
||||
break;
|
||||
case tpms::Reading::Type::GMC_96:
|
||||
signal_type_ = tpms::SignalType::OOK_8k4_Schrader;
|
||||
break;
|
||||
default:
|
||||
signal_type_ = tpms::SignalType::OOK_8k192_Schrader;
|
||||
break;
|
||||
}
|
||||
// Note: Don't call switch_baseband() here - it's called when needed
|
||||
}
|
||||
|
||||
void TPMSTXView::switch_baseband() {
|
||||
// Switch baseband image based on signal type
|
||||
// Must shutdown first to avoid BBRunning error
|
||||
baseband::shutdown();
|
||||
chThdSleepMilliseconds(100);
|
||||
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
} else {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
}
|
||||
|
||||
void TPMSTXView::update_packet_display() {
|
||||
// Only update status text - field values are already set by user interaction
|
||||
// or by explicit set_value calls when loading from file
|
||||
std::string status = "ID:" + to_string_hex(transponder_id_, 8);
|
||||
|
||||
// Note protocol-specific limitations
|
||||
if (packet_type_ == tpms::Reading::Type::Schrader) {
|
||||
status = "ID:" + to_string_hex(transponder_id_ & 0x00FFFFFF, 6) + " (24bit)";
|
||||
}
|
||||
|
||||
status += " " + to_string_dec_uint(pressure_kpa_) + "kPa";
|
||||
|
||||
// Check for pressure overflow and warn user
|
||||
if (packet_type_ == tpms::Reading::Type::GMC_96) {
|
||||
if (pressure_kpa_ > 701) {
|
||||
status += " !MAX"; // GMC_96 max is 701 kPa (101.7 PSI)
|
||||
}
|
||||
} else {
|
||||
if (pressure_kpa_ > 340) {
|
||||
status += " !MAX"; // Schrader/FLM max is 340 kPa (49.3 PSI)
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature note for protocols that support it
|
||||
if (packet_type_ == tpms::Reading::Type::GMC_96 ||
|
||||
packet_type_ == tpms::Reading::Type::FLM_64 ||
|
||||
packet_type_ == tpms::Reading::Type::FLM_72 ||
|
||||
packet_type_ == tpms::Reading::Type::FLM_80) {
|
||||
status += " " + to_string_dec_int(temperature_c_) + "C";
|
||||
} else {
|
||||
status += " (no temp)";
|
||||
}
|
||||
|
||||
text_status.set(status);
|
||||
}
|
||||
|
||||
void TPMSTXView::update_field_visibility() {
|
||||
// Schrader: Show Func, Hide Temp (no temperature support)
|
||||
// GMC_96, FLM_xx: Hide Func, Show Temp (temperature supported, no func field)
|
||||
if (packet_type_ == tpms::Reading::Type::Schrader) {
|
||||
// Show Func field and label
|
||||
label_flags.hidden(false);
|
||||
field_flags.hidden(false);
|
||||
// Hide Temp field, label, and unit selector
|
||||
label_temperature.hidden(true);
|
||||
field_temperature.hidden(true);
|
||||
options_temperature.hidden(true);
|
||||
// Show 24-bit ID field, hide 32-bit ID field
|
||||
field_transponder_id_24.hidden(false);
|
||||
field_transponder_id_32.hidden(true);
|
||||
} else {
|
||||
// Hide Func field and label
|
||||
label_flags.hidden(true);
|
||||
field_flags.hidden(true);
|
||||
// Show Temp field, label, and unit selector
|
||||
label_temperature.hidden(false);
|
||||
field_temperature.hidden(false);
|
||||
options_temperature.hidden(false);
|
||||
// Show 32-bit ID field, hide 24-bit ID field
|
||||
field_transponder_id_24.hidden(true);
|
||||
field_transponder_id_32.hidden(false);
|
||||
}
|
||||
// Force screen refresh to clear hidden widgets
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TPMSTXView::on_pressure_unit_change() {
|
||||
// Convert displayed pressure to new unit and update field
|
||||
units::Pressure pressure(pressure_kpa_);
|
||||
int display_value = 0;
|
||||
|
||||
if (format::pressure_unit == PRESSURE_UNIT_PSI) {
|
||||
display_value = pressure.psi();
|
||||
} else if (format::pressure_unit == PRESSURE_UNIT_BAR) {
|
||||
display_value = pressure.bar();
|
||||
} else {
|
||||
display_value = pressure.kilopascal();
|
||||
}
|
||||
|
||||
field_pressure.set_value(display_value);
|
||||
}
|
||||
|
||||
void TPMSTXView::on_temperature_unit_change() {
|
||||
// Convert displayed temperature to new unit and update field
|
||||
units::Temperature temperature(temperature_c_);
|
||||
int display_value = 0;
|
||||
|
||||
if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) {
|
||||
display_value = temperature.fahrenheit();
|
||||
} else {
|
||||
display_value = temperature.celsius();
|
||||
}
|
||||
|
||||
field_temperature.set_value(display_value);
|
||||
}
|
||||
|
||||
void TPMSTXView::encode_and_transmit() {
|
||||
if (!is_transmitting_) return;
|
||||
|
||||
// Build TPMS packet based on signal type
|
||||
std::string binary_string;
|
||||
uint32_t symbol_rate;
|
||||
uint32_t sample_rate;
|
||||
|
||||
// Set sample rate based on signal type to match transmitter config
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
sample_rate = 2280000; // 2.28 MHz for FSK (matches transmitter_model config)
|
||||
} else {
|
||||
sample_rate = 2000000; // 2 MHz for OOK
|
||||
}
|
||||
|
||||
if (signal_type_ == tpms::SignalType::OOK_8k192_Schrader) {
|
||||
// Schrader OOK 8k192 format (Type = Schrader)
|
||||
// Preamble: 11*2, 01*14, 11, 10
|
||||
// Data: 3 bits function code, 24 bits ID, 8 bits pressure, 2 bits checksum
|
||||
// Total: 37 Manchester symbols (74 bits after encoding)
|
||||
// NOTE: This protocol does NOT support temperature transmission
|
||||
// NOTE: Only lower 24 bits of ID are transmitted
|
||||
// NOTE: Function code (flags_) is stored as 3-bit value (0-7)
|
||||
// RX will display it combined with checksum in upper nibble
|
||||
|
||||
symbol_rate = 8192;
|
||||
|
||||
// Preamble as expected by RX decoder
|
||||
binary_string = "1111"; // 11*2
|
||||
for (int i = 0; i < 14; i++) {
|
||||
binary_string += "01";
|
||||
}
|
||||
binary_string += "1110"; // 11, 10
|
||||
|
||||
// Build data bits (pre-Manchester)
|
||||
uint64_t data = 0;
|
||||
|
||||
// 3-bit flags (0-7, stored directly)
|
||||
uint8_t flags_3bit = flags_ & 0x07;
|
||||
data |= ((uint64_t)flags_3bit << 34);
|
||||
|
||||
// 24-bit ID (only use lower 24 bits - protocol limitation)
|
||||
uint32_t id_24bit = transponder_id_ & 0x00FFFFFF;
|
||||
data |= ((uint64_t)id_24bit << 10);
|
||||
|
||||
// 8-bit pressure (convert kPa to raw: kPa * 3/4)
|
||||
// Clamp to prevent overflow: max raw = 255, max kPa = 255 * 4/3 = 340 (49.3 PSI)
|
||||
uint16_t pressure_clamped = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
uint8_t pressure_raw = (pressure_clamped * 3 / 4);
|
||||
data |= ((uint64_t)pressure_raw << 2);
|
||||
|
||||
// Calculate 2-bit checksum: sum all 2-bit pairs, result & 3 must equal 3
|
||||
uint32_t checksum_calc = (data >> 36) & 1; // First bit
|
||||
for (size_t i = 1; i < 37; i += 2) {
|
||||
checksum_calc += (data >> (37 - i - 2)) & 3;
|
||||
}
|
||||
uint8_t checksum_2bit = (3 - (checksum_calc & 3)) & 3;
|
||||
data |= checksum_2bit;
|
||||
|
||||
// Manchester encode the 37 data bits
|
||||
for (int i = 36; i >= 0; i--) {
|
||||
if ((data >> i) & 1) {
|
||||
binary_string += "10"; // '1' = 10 in Manchester
|
||||
} else {
|
||||
binary_string += "01"; // '0' = 01 in Manchester
|
||||
}
|
||||
}
|
||||
|
||||
} else if (signal_type_ == tpms::SignalType::OOK_8k4_Schrader) {
|
||||
// GMC_96 OOK 8k4 format
|
||||
symbol_rate = 8400;
|
||||
|
||||
// Preamble: 01*40
|
||||
for (int i = 0; i < 40; i++) {
|
||||
binary_string += "01";
|
||||
}
|
||||
|
||||
// First nibble of System ID (0x4) - part of preamble pattern match
|
||||
// RX looks for pattern ending with: 01010101...01100101
|
||||
// The "01100101" = Manchester for 0x4, and is consumed by pattern matcher
|
||||
binary_string += "01"; // 0
|
||||
binary_string += "10"; // 1
|
||||
binary_string += "01"; // 0
|
||||
binary_string += "01"; // 0 (0100 = 0x4)
|
||||
|
||||
// Remaining 20 bits of system ID (padding with zeros)
|
||||
// These are the first 20 bits of the 76-bit payload
|
||||
for (int i = 0; i < 20; i++) {
|
||||
binary_string += "01"; // All zeros for padding
|
||||
}
|
||||
|
||||
// 32-bit ID (Manchester encoded)
|
||||
for (int i = 31; i >= 0; i--) {
|
||||
if ((transponder_id_ >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
// Pressure: kPa * 4/11
|
||||
// Clamp to prevent overflow: max raw = 255, max kPa = 255 * 11/4 = 701.25 (101.7 PSI)
|
||||
uint16_t pressure_clamped = (pressure_kpa_ > 701) ? 701 : pressure_kpa_;
|
||||
uint8_t pressure_gmc = (pressure_clamped * 4 / 11);
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if ((pressure_gmc >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature: temp + 61
|
||||
uint8_t temp_gmc = (temperature_c_ + 61) & 0xFF;
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if ((temp_gmc >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
// Checksum: sum of all data bytes (system ID + ID + pressure + temp)
|
||||
// System ID byte 0: (0x4 << 4) | 0x0 = 0x40
|
||||
// System ID byte 1: 0x00
|
||||
// System ID byte 2: 0x00
|
||||
uint8_t checksum = 0x40; // First byte of system ID
|
||||
checksum += 0x00; // Second byte of system ID
|
||||
checksum += 0x00; // Third byte of system ID
|
||||
|
||||
// Add all 4 bytes of the ID
|
||||
checksum += (transponder_id_ >> 24) & 0xFF;
|
||||
checksum += (transponder_id_ >> 16) & 0xFF;
|
||||
checksum += (transponder_id_ >> 8) & 0xFF;
|
||||
checksum += transponder_id_ & 0xFF;
|
||||
|
||||
// Add pressure and temperature
|
||||
checksum += pressure_gmc;
|
||||
checksum += temp_gmc;
|
||||
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if ((checksum >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
} else if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
// FSK 19.2k for FLM variants
|
||||
// Data is Manchester encoded: each data bit becomes two FSK symbols
|
||||
// Preamble: 14 x '01' + '10' = 30 bits (matches RX pattern exactly)
|
||||
// Payload: 160 Manchester-encoded bits (80 data bits max)
|
||||
|
||||
symbol_rate = 19200;
|
||||
|
||||
// Build preamble: 14 pairs of "01" followed by "10"
|
||||
for (int i = 0; i < 14; i++) {
|
||||
binary_string += "01";
|
||||
}
|
||||
binary_string += "10";
|
||||
|
||||
std::array<uint8_t, 20> data_bytes = {0}; // 160 bits = 20 bytes max
|
||||
size_t num_data_bits = 0;
|
||||
|
||||
if (packet_type_ == tpms::Reading::Type::FLM_64) {
|
||||
// FLM_64: 64 bits (8 bytes)
|
||||
// Bytes 0-3: ID (32 bits)
|
||||
// Byte 4: Pressure (8 bits)
|
||||
// Byte 5: Temperature (8 bits, LSB 7 bits used)
|
||||
// Byte 6: Padding
|
||||
// Byte 7: Sum checksum of bytes 0-6 (low 8 bits)
|
||||
|
||||
num_data_bits = 64;
|
||||
|
||||
data_bytes[0] = (transponder_id_ >> 24) & 0xFF;
|
||||
data_bytes[1] = (transponder_id_ >> 16) & 0xFF;
|
||||
data_bytes[2] = (transponder_id_ >> 8) & 0xFF;
|
||||
data_bytes[3] = transponder_id_ & 0xFF;
|
||||
// Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI)
|
||||
uint16_t pressure_clamped_flm64 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
data_bytes[4] = (pressure_clamped_flm64 * 3 / 4);
|
||||
data_bytes[5] = ((temperature_c_ + 56) & 0x7F); // LSB 7 bits only
|
||||
data_bytes[6] = 0x00; // Padding
|
||||
|
||||
// Addition checksum over bytes 0-6
|
||||
uint32_t checksum = 0;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
checksum += data_bytes[i];
|
||||
}
|
||||
data_bytes[7] = checksum & 0xFF;
|
||||
|
||||
} else if (packet_type_ == tpms::Reading::Type::FLM_72) {
|
||||
// FLM_72: 72 bits (9 bytes)
|
||||
// Bytes 0-3: ID (32 bits)
|
||||
// Byte 4: Padding
|
||||
// Byte 5: Pressure (8 bits)
|
||||
// Byte 6: Temperature (8 bits)
|
||||
// Bytes 7-8: CRC field - RX processes ALL 9 bytes and expects CRC result = 0
|
||||
|
||||
num_data_bits = 72;
|
||||
|
||||
data_bytes[0] = (transponder_id_ >> 24) & 0xFF;
|
||||
data_bytes[1] = (transponder_id_ >> 16) & 0xFF;
|
||||
data_bytes[2] = (transponder_id_ >> 8) & 0xFF;
|
||||
data_bytes[3] = transponder_id_ & 0xFF;
|
||||
data_bytes[4] = 0x00; // Padding
|
||||
// Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI)
|
||||
uint16_t pressure_clamped_flm = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
data_bytes[5] = (pressure_clamped_flm * 3 / 4);
|
||||
data_bytes[6] = (temperature_c_ + 56) & 0xFF;
|
||||
data_bytes[7] = 0x00; // Set to 0 initially
|
||||
|
||||
// Calculate CRC over bytes 0-7, place result in byte 8
|
||||
// When RX calculates CRC over bytes 0-8, it should get residual 0
|
||||
CRC<8> crc{0x01, 0x00};
|
||||
for (int i = 0; i < 8; i++) {
|
||||
crc.process_byte(data_bytes[i]);
|
||||
}
|
||||
data_bytes[8] = crc.checksum() & 0xFF;
|
||||
|
||||
} else if (packet_type_ == tpms::Reading::Type::FLM_80) {
|
||||
// FLM_80: 80 bits (10 bytes)
|
||||
// Byte 0: Padding
|
||||
// Bytes 1-4: ID (32 bits)
|
||||
// Byte 5: Padding
|
||||
// Byte 6: Pressure (8 bits)
|
||||
// Byte 7: Temperature (8 bits)
|
||||
// Bytes 8-9: CRC field - RX processes bytes 1-9 and expects CRC result = 0
|
||||
|
||||
num_data_bits = 80;
|
||||
|
||||
data_bytes[0] = 0x00; // Padding (not included in CRC)
|
||||
data_bytes[1] = (transponder_id_ >> 24) & 0xFF;
|
||||
data_bytes[2] = (transponder_id_ >> 16) & 0xFF;
|
||||
data_bytes[3] = (transponder_id_ >> 8) & 0xFF;
|
||||
data_bytes[4] = transponder_id_ & 0xFF;
|
||||
data_bytes[5] = 0x00; // Padding
|
||||
// Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI)
|
||||
uint16_t pressure_clamped_flm80 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
data_bytes[6] = (pressure_clamped_flm80 * 3 / 4);
|
||||
data_bytes[7] = (temperature_c_ + 56) & 0xFF;
|
||||
data_bytes[8] = 0x00; // Padding/Reserved
|
||||
data_bytes[9] = 0x00; // CRC byte
|
||||
|
||||
// Calculate CRC over bytes 1-8 (all bytes except 0 and 9)
|
||||
// When RX calculates CRC over bytes 1-9, it should get residual 0
|
||||
CRC<8> crc{0x01, 0x00};
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
crc.process_byte(data_bytes[i]);
|
||||
}
|
||||
data_bytes[9] = crc.checksum() & 0xFF;
|
||||
}
|
||||
|
||||
// Manchester-encode data bits for FSK
|
||||
// RX ManchesterDecoder with sense=0: '1' = "10", '0' = "01"
|
||||
size_t num_bytes = num_data_bits / 8;
|
||||
for (size_t byte_idx = 0; byte_idx < num_bytes; byte_idx++) {
|
||||
uint8_t byte_val = data_bytes[byte_idx];
|
||||
for (int bit_idx = 7; bit_idx >= 0; bit_idx--) {
|
||||
if ((byte_val >> bit_idx) & 1) {
|
||||
binary_string += "10"; // Manchester '1'
|
||||
} else {
|
||||
binary_string += "01"; // Manchester '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pad payload to 160 bits with valid Manchester pairs ("01" = 0)
|
||||
// Using proper Manchester encoding maintains clock recovery sync
|
||||
while (binary_string.length() < 190) { // 30 preamble + 160 payload
|
||||
binary_string += "01"; // Manchester-encoded zero
|
||||
}
|
||||
|
||||
} else {
|
||||
text_status.set("Unknown signal type");
|
||||
stop_tx();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert binary string to bitstream
|
||||
size_t bitstream_length = encoders::make_bitstream(binary_string);
|
||||
|
||||
// Calculate samples per bit (round to nearest for best timing accuracy)
|
||||
uint32_t samples_per_bit = (sample_rate + symbol_rate / 2) / symbol_rate;
|
||||
|
||||
// Update status to show we're sending
|
||||
text_status.set("TX: " + to_string_dec_uint(binary_string.length()) + " bits");
|
||||
|
||||
// Send via appropriate baseband (OOK or FSK)
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
// FSK mode: 19.2 kbaud, 38.4 kHz shift
|
||||
baseband::set_fsk_data(
|
||||
bitstream_length,
|
||||
samples_per_bit,
|
||||
38400, // 38.4 kHz shift
|
||||
256); // Progress notice interval
|
||||
} else {
|
||||
// OOK mode
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
samples_per_bit,
|
||||
repeat_count_,
|
||||
pause_duration_);
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSTXView::handle_tx_complete() {
|
||||
// For FSK (FLM packets), handle repeats at application level
|
||||
// OOK repeats are handled by the baseband processor
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
fsk_repeat_counter_++;
|
||||
|
||||
if (fsk_repeat_counter_ < repeat_count_) {
|
||||
// More repeats needed - send the next one
|
||||
// Update progress bar
|
||||
progressbar.set_value(fsk_repeat_counter_);
|
||||
|
||||
// Wait for FSK processor to fully complete and reset
|
||||
// This allows the shared memory bitstream to be safely rewritten
|
||||
chThdSleepMilliseconds(50);
|
||||
|
||||
encode_and_transmit();
|
||||
} else {
|
||||
// All FSK repeats complete
|
||||
stop_tx();
|
||||
}
|
||||
} else {
|
||||
// OOK repeats are handled by baseband, just stop here
|
||||
stop_tx();
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSTXView::start_tx() {
|
||||
if (is_transmitting_)
|
||||
return;
|
||||
|
||||
is_transmitting_ = true;
|
||||
|
||||
progressbar.set_max(repeat_count_);
|
||||
progressbar.set_value(0);
|
||||
|
||||
button_transmit.set_text("STOP TX");
|
||||
text_status.set("Transmitting...");
|
||||
|
||||
// Initialize FSK repeat counter for application-level repeat handling
|
||||
fsk_repeat_counter_ = 0;
|
||||
|
||||
// Switch to appropriate baseband before transmission
|
||||
switch_baseband();
|
||||
|
||||
// Configure transmitter based on modulation type
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
// FSK configuration
|
||||
transmitter_model.set_sampling_rate(2280000); // 2.28 MHz for FSK
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
} else {
|
||||
// OOK configuration
|
||||
transmitter_model.set_sampling_rate(2000000); // 2 MHz for OOK
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
}
|
||||
|
||||
transmitter_model.enable();
|
||||
|
||||
// Start transmission
|
||||
encode_and_transmit();
|
||||
}
|
||||
|
||||
void TPMSTXView::stop_tx() {
|
||||
if (!is_transmitting_)
|
||||
return;
|
||||
|
||||
is_transmitting_ = false;
|
||||
transmitter_model.disable();
|
||||
|
||||
button_transmit.set_text("START TX");
|
||||
text_status.set("Transmission complete");
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
|
||||
TPMSTXView::TPMSTXView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
// Start with OOK baseband (will switch if needed)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
|
||||
add_children({&labels,
|
||||
&label_temperature,
|
||||
&label_flags,
|
||||
&options_frequency,
|
||||
&options_packet_type,
|
||||
&field_transponder_id_24,
|
||||
&field_transponder_id_32,
|
||||
&field_pressure,
|
||||
&options_pressure,
|
||||
&field_temperature,
|
||||
&options_temperature,
|
||||
&field_flags,
|
||||
&field_repeat,
|
||||
&button_load,
|
||||
&button_save,
|
||||
&tx_view,
|
||||
&button_transmit,
|
||||
&text_status,
|
||||
&progressbar});
|
||||
|
||||
// Set label styles to match other labels (light grey)
|
||||
label_temperature.set_style(Theme::getInstance()->fg_light);
|
||||
label_flags.set_style(Theme::getInstance()->fg_light);
|
||||
|
||||
// Initialize values
|
||||
options_frequency.set_by_value(transmitter_model.target_frequency());
|
||||
options_packet_type.set_selected_index(0);
|
||||
field_repeat.set_value(repeat_count_);
|
||||
|
||||
// Set initial signal type based on packet type
|
||||
update_signal_type_from_packet();
|
||||
|
||||
// Initialize unit selection
|
||||
options_pressure.set_by_value(format::pressure_unit);
|
||||
options_temperature.set_by_value(format::temp_unit);
|
||||
|
||||
// Initialize field values from default member variables
|
||||
// For pressure and temperature, use unit conversion to display in selected units
|
||||
field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF);
|
||||
field_transponder_id_32.set_value(transponder_id_);
|
||||
on_pressure_unit_change();
|
||||
on_temperature_unit_change();
|
||||
field_flags.set_value(flags_);
|
||||
|
||||
update_field_visibility();
|
||||
update_packet_display();
|
||||
|
||||
// Event handlers
|
||||
options_frequency.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
transmitter_model.set_target_frequency(v);
|
||||
};
|
||||
|
||||
options_packet_type.on_change = [this](size_t, int32_t value) {
|
||||
packet_type_ = static_cast<tpms::Reading::Type>(value);
|
||||
update_signal_type_from_packet();
|
||||
|
||||
// Sync field values when switching packet types
|
||||
if (packet_type_ == tpms::Reading::Type::Schrader) {
|
||||
// Switching to Schrader: copy from 32-bit field to 24-bit field (masked)
|
||||
transponder_id_ = field_transponder_id_32.to_integer() & 0x00FFFFFF;
|
||||
field_transponder_id_24.set_value(transponder_id_);
|
||||
} else {
|
||||
// Switching from Schrader: copy from 24-bit field to 32-bit field
|
||||
transponder_id_ = field_transponder_id_24.to_integer();
|
||||
field_transponder_id_32.set_value(transponder_id_);
|
||||
}
|
||||
|
||||
update_field_visibility();
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
options_pressure.on_change = [this](size_t, int32_t i) {
|
||||
format::pressure_unit = (uint8_t)i;
|
||||
on_pressure_unit_change();
|
||||
};
|
||||
|
||||
options_temperature.on_change = [this](size_t, int32_t i) {
|
||||
format::temp_unit = (uint8_t)i;
|
||||
on_temperature_unit_change();
|
||||
};
|
||||
|
||||
field_transponder_id_24.on_change = [this](SymField&) {
|
||||
transponder_id_ = field_transponder_id_24.to_integer() & 0x00FFFFFF;
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_transponder_id_32.on_change = [this](SymField&) {
|
||||
transponder_id_ = field_transponder_id_32.to_integer();
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_pressure.on_change = [this](int32_t value) {
|
||||
// Convert from displayed unit to kPa for internal storage
|
||||
if (format::pressure_unit == PRESSURE_UNIT_PSI) {
|
||||
pressure_kpa_ = value * 6895 / 1000;
|
||||
} else if (format::pressure_unit == PRESSURE_UNIT_BAR) {
|
||||
pressure_kpa_ = value * 100;
|
||||
} else {
|
||||
pressure_kpa_ = value;
|
||||
}
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_temperature.on_change = [this](int32_t value) {
|
||||
// Convert from displayed unit to Celsius for internal storage
|
||||
if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) {
|
||||
temperature_c_ = (value - 32) * 5 / 9;
|
||||
} else {
|
||||
temperature_c_ = value;
|
||||
}
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_flags.on_change = [this](int32_t value) {
|
||||
flags_ = value & 0x07;
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_repeat.on_change = [this](int32_t value) {
|
||||
repeat_count_ = value;
|
||||
};
|
||||
|
||||
button_transmit.on_select = [this](Button&) {
|
||||
if (is_transmitting_) {
|
||||
stop_tx();
|
||||
} else {
|
||||
start_tx();
|
||||
}
|
||||
};
|
||||
|
||||
button_load.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path file_path) {
|
||||
// Load TPMS packet from file
|
||||
File f;
|
||||
auto error = f.open(file_path);
|
||||
if (!error.is_valid()) {
|
||||
char buffer[512];
|
||||
auto result = f.read(buffer, sizeof(buffer) - 1);
|
||||
if (result.is_ok()) {
|
||||
buffer[result.value()] = 0;
|
||||
|
||||
// Parse the file format (key=value format, one per line)
|
||||
std::string content(buffer);
|
||||
size_t pos = 0;
|
||||
|
||||
auto parse_line = [&content, &pos](const std::string& key) -> std::string {
|
||||
size_t key_pos = content.find(key + "=", pos);
|
||||
if (key_pos != std::string::npos) {
|
||||
size_t value_start = key_pos + key.length() + 1;
|
||||
size_t value_end = content.find('\n', value_start);
|
||||
if (value_end == std::string::npos) value_end = content.length();
|
||||
return content.substr(value_start, value_end - value_start);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
std::string type_str = parse_line("Type");
|
||||
std::string id_str = parse_line("ID");
|
||||
std::string pressure_str = parse_line("Pressure");
|
||||
std::string temp_str = parse_line("Temperature");
|
||||
std::string flags_str = parse_line("Flags");
|
||||
std::string signal_type_str = parse_line("SignalType");
|
||||
|
||||
if (!type_str.empty()) {
|
||||
int type = std::stoi(type_str);
|
||||
if (type >= static_cast<int>(tpms::Reading::Type::FLM_64) &&
|
||||
type <= static_cast<int>(tpms::Reading::Type::GMC_96)) {
|
||||
packet_type_ = static_cast<tpms::Reading::Type>(type);
|
||||
options_packet_type.set_by_value(type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!id_str.empty()) {
|
||||
transponder_id_ = std::stoul(id_str, nullptr, 16);
|
||||
field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF);
|
||||
field_transponder_id_32.set_value(transponder_id_);
|
||||
}
|
||||
|
||||
if (!pressure_str.empty()) {
|
||||
pressure_kpa_ = std::stoi(pressure_str);
|
||||
on_pressure_unit_change();
|
||||
}
|
||||
|
||||
if (!temp_str.empty()) {
|
||||
temperature_c_ = std::stoi(temp_str);
|
||||
field_temperature.set_value(temperature_c_);
|
||||
}
|
||||
|
||||
if (!flags_str.empty()) {
|
||||
uint8_t loaded_flags = std::stoul(flags_str, nullptr, 16);
|
||||
// Handle old format files: extract bits 6-4 if value > 7
|
||||
flags_ = (loaded_flags > 7) ? ((loaded_flags >> 4) & 0x07) : (loaded_flags & 0x07);
|
||||
field_flags.set_value(flags_);
|
||||
}
|
||||
|
||||
// Load signal type if available, otherwise auto-select based on packet type
|
||||
if (!signal_type_str.empty()) {
|
||||
int signal_type = std::stoi(signal_type_str);
|
||||
if (signal_type >= 1 && signal_type <= 3) {
|
||||
signal_type_ = static_cast<tpms::SignalType>(signal_type);
|
||||
} else {
|
||||
// Fall back to auto-detect
|
||||
update_signal_type_from_packet();
|
||||
}
|
||||
} else {
|
||||
// Fall back to auto-detect for backwards compatibility
|
||||
update_signal_type_from_packet();
|
||||
}
|
||||
|
||||
update_packet_display();
|
||||
update_field_visibility();
|
||||
text_status.set("Loaded: " + file_path.filename().string());
|
||||
} else {
|
||||
text_status.set("Error reading file");
|
||||
}
|
||||
} else {
|
||||
text_status.set("Error opening file");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
// Save current TPMS packet to file
|
||||
auto timestamp = to_string_timestamp(rtc_time::now());
|
||||
std::string file_name = "TPMS_" + timestamp + ".TXT";
|
||||
ensure_directory(tpms_dir);
|
||||
auto file_path = tpms_dir / file_name;
|
||||
|
||||
File f;
|
||||
auto error = f.create(file_path);
|
||||
if (!error.is_valid()) {
|
||||
std::string content = "Type=" + to_string_dec_uint(toUType(packet_type_), 1) + "\n";
|
||||
content += "ID=" + to_string_hex(transponder_id_, 8) + "\n";
|
||||
content += "Pressure=" + to_string_dec_uint(pressure_kpa_) + "\n";
|
||||
content += "Temperature=" + to_string_dec_int(temperature_c_) + "\n";
|
||||
content += "Flags=" + to_string_hex(flags_ & 0x07, 1) + "\n";
|
||||
|
||||
f.write(content.c_str(), content.length());
|
||||
text_status.set("Saved: " + file_name);
|
||||
} else {
|
||||
text_status.set("Error saving file");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TPMSTXView::~TPMSTXView() {
|
||||
stop_tx();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tpmstx
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user